Revert "[named-fiddles] Remove named-fiddles."

Then remove the web UI to make the revert easier.

This reverts commit 6e3ae4324790799d05acf4113e1d2eb0b818463b.

Reason for revert: Still needed to copy example fiddles into prod.

Original change's description:
> [named-fiddles] Remove named-fiddles.
>
> All named fiddles are be checked into https://github.com/google/skia/tree/master/docs/examples.
>
> Change-Id: I988042b15869329616f34ee6d017f746da2082f6
> Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/291836
> Commit-Queue: Joe Gregorio <jcgregorio@google.com>
> Reviewed-by: Ravi Mistry <rmistry@google.com>

TBR=rmistry@google.com,jcgregorio@google.com

Change-Id: I92c4b2a8631a1b53226e185a9f7e9dcecdd8a99d
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/302616
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
diff --git a/named-fiddles/Makefile b/named-fiddles/Makefile
new file mode 100644
index 0000000..5053dcb
--- /dev/null
+++ b/named-fiddles/Makefile
@@ -0,0 +1,12 @@
+build:
+	go install ./go/named-fiddles
+
+release: build
+	CGO_ENABLED=0 GOOS=linux go install -a ./go/named-fiddles
+	./build_release
+
+push: release
+	pushk named-fiddles
+
+testci:
+	go test ./go/...
\ No newline at end of file
diff --git a/named-fiddles/README.md b/named-fiddles/README.md
new file mode 100644
index 0000000..2ebb8aa
--- /dev/null
+++ b/named-fiddles/README.md
@@ -0,0 +1,3 @@
+# named-fiddles
+
+A background app that copies examples out of the Skia repo into fiddle.
diff --git a/named-fiddles/go/named-fiddles/main.go b/named-fiddles/go/named-fiddles/main.go
new file mode 100644
index 0000000..39f6286
--- /dev/null
+++ b/named-fiddles/go/named-fiddles/main.go
@@ -0,0 +1,183 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"go.skia.org/infra/fiddlek/go/client"
+	"go.skia.org/infra/fiddlek/go/store"
+	"go.skia.org/infra/fiddlek/go/types"
+	"go.skia.org/infra/go/auth"
+	"go.skia.org/infra/go/common"
+	"go.skia.org/infra/go/git/gitinfo"
+	"go.skia.org/infra/go/gitauth"
+	"go.skia.org/infra/go/metrics2"
+	"go.skia.org/infra/go/sklog"
+	"go.skia.org/infra/named-fiddles/go/parse"
+)
+
+// flags
+var (
+	local    = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
+	period   = flag.Duration("period", time.Hour, "How often to check if the named fiddles are valid.")
+	promPort = flag.String("prom_port", ":20000", "Metrics service address (e.g., ':10110')")
+	repoURL  = flag.String("repo_url", "https://skia.googlesource.com/skia", "Repo url")
+	repoDir  = flag.String("repo_dir", "/tmp/skia_named_fiddles", "Directory the repo is checked out into.")
+)
+
+// Server is the state of the server.
+type Server struct {
+	store *store.Store
+	repo  *gitinfo.GitInfo
+
+	livenessExamples    metrics2.Liveness    // liveness of the naming the Skia examples.
+	errorsInExamplesRun metrics2.Counter     // errorsInExamplesRun is the number of errors in a single examples run.
+	numInvalidExamples  metrics2.Int64Metric // numInvalidExamples is the number of examples that are currently invalid.
+}
+
+// New creates a new Server.
+func New() (*Server, error) {
+	st, err := store.New(*local)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to create client for GCS: %s", err)
+	}
+
+	if !*local {
+		ts, err := auth.NewDefaultTokenSource(false, auth.SCOPE_USERINFO_EMAIL, auth.SCOPE_GERRIT)
+		if err != nil {
+			sklog.Fatalf("Failed authentication: %s", err)
+		}
+		// Use the gitcookie created by the gitauth package.
+		if _, err := gitauth.New(ts, "/tmp/gitcookies", true, ""); err != nil {
+			sklog.Fatalf("Failed to create git cookie updater: %s", err)
+		}
+		sklog.Infof("Git authentication set up successfully.")
+	}
+
+	repo, err := gitinfo.CloneOrUpdate(context.Background(), *repoURL, *repoDir, false)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to create git repo: %s", err)
+	}
+
+	srv := &Server{
+		store: st,
+		repo:  repo,
+
+		livenessExamples:    metrics2.NewLiveness("named_fiddles_examples"),
+		errorsInExamplesRun: metrics2.GetCounter("named_fiddles_errors_in_examples_run", nil),
+		numInvalidExamples:  metrics2.GetInt64Metric("named_fiddles_examples_total_invalid"),
+	}
+	go srv.nameExamples()
+	return srv, nil
+}
+
+// errorsInResults returns an empty string if there are no errors, either
+// compile or runtime, found in the results. If there are errors then a string
+// describing the error is returned.
+func errorsInResults(runResults *types.RunResults, success bool) string {
+	status := ""
+	if runResults == nil {
+		status = "Failed to run."
+	} else if len(runResults.CompileErrors) > 0 || runResults.RunTimeError != "" {
+		// update validity
+		status = fmt.Sprintf("%v %s", runResults.CompileErrors, runResults.RunTimeError)
+		if len(status) > 100 {
+			status = status[:100]
+		}
+	}
+	return status
+}
+
+// exampleStep is a single run through naming all the examples.
+func (srv *Server) exampleStep() {
+	srv.errorsInExamplesRun.Reset()
+	sklog.Info("Starting exampleStep")
+	if err := srv.repo.Update(context.Background(), true, false); err != nil {
+		sklog.Errorf("Failed to sync git repo.")
+		return
+	}
+
+	var numInvalid int64
+	// Get a list of all examples.
+	dir := filepath.Join(*repoDir, "docs", "examples")
+	err := filepath.Walk(dir+"/", func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return fmt.Errorf("Failed to open %q: %s", path, err)
+		}
+		if info.IsDir() {
+			return nil
+		}
+		name := filepath.Base(info.Name())
+		if !strings.HasSuffix(name, ".cpp") {
+			return nil
+		}
+		name = name[0 : len(name)-4]
+		b, err := ioutil.ReadFile(filepath.Join(dir, info.Name()))
+		fc, err := parse.ParseCpp(string(b))
+		if err == parse.ErrorInactiveExample {
+			sklog.Infof("Inactive sample: %q", info.Name())
+			return nil
+		} else if err != nil {
+			sklog.Infof("Invalid sample: %q", info.Name())
+			numInvalid += 1
+			return nil
+		}
+		// Now run it.
+		sklog.Infof("About to run: %s", name)
+		b, err = json.Marshal(fc)
+		if err != nil {
+			sklog.Errorf("Failed to encode example to JSON: %s", err)
+			return nil
+		}
+
+		runResults, success := client.Do(b, false, "https://fiddle.skia.org", func(*types.RunResults) bool {
+			return true
+		})
+		if !success {
+			sklog.Errorf("Failed to run")
+			srv.errorsInExamplesRun.Inc(1)
+			return nil
+		}
+		status := errorsInResults(runResults, success)
+		if err := srv.store.WriteName(name, runResults.FiddleHash, "Skia example", status); err != nil {
+			sklog.Errorf("Failed to write status for %s: %s", name, err)
+			srv.errorsInExamplesRun.Inc(1)
+		}
+		return nil
+	})
+	if err != nil {
+		sklog.Errorf("Error walking the path %q: %v\n", dir, err)
+		return
+	}
+
+	srv.numInvalidExamples.Update(numInvalid)
+	srv.livenessExamples.Reset()
+}
+
+// nameExamples runs each Skia example and gives it a name.
+func (srv *Server) nameExamples() {
+	srv.exampleStep()
+	for range time.Tick(time.Minute) {
+		srv.exampleStep()
+	}
+}
+
+func main() {
+	common.InitWithMust(
+		"named-fiddles",
+		common.PrometheusOpt(promPort),
+	)
+
+	_, err := New()
+	if err != nil {
+		sklog.Fatalf("Failed to create Server: %s", err)
+	}
+	select {}
+}
diff --git a/named-fiddles/go/parse/parse.go b/named-fiddles/go/parse/parse.go
new file mode 100644
index 0000000..fdfe932
--- /dev/null
+++ b/named-fiddles/go/parse/parse.go
@@ -0,0 +1,92 @@
+package parse
+
+import (
+	"errors"
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"go.skia.org/infra/fiddlek/go/types"
+)
+
+var (
+	ErrorInactiveExample = errors.New("Inactive example (ifdef'd out)")
+
+	// re is used to parse the REG_FIDDLE macro found in the sample code.
+	re = regexp.MustCompile(`REG_FIDDLE\((?P<name>\w+),\s+(?P<width>\w+),\s+(?P<height>\w+),\s+(?P<textonly>\w+),\s+(?P<source>\w+)\)`)
+)
+
+const (
+	// The indices into the capture groups in the 're' regexp.
+	NAME     = 1
+	WIDTH    = 2
+	HEIGHT   = 3
+	TEXTONLY = 4
+	SOURCE   = 5
+)
+
+// parseCpp parses a Skia example and returns a FiddleContext that's ready to run.
+//
+// Returns ErrorInactiveExample is the example is ifdef'd out. Other errors
+// indicate a failure to parse the code or options.
+func ParseCpp(body string) (*types.FiddleContext, error) {
+	if body[:3] == "#if" {
+		return nil, ErrorInactiveExample
+	}
+
+	// Parse up the REG_FIDDLE macro values.
+	match := re.FindStringSubmatch(body)
+	if len(match) != 6 {
+		return nil, fmt.Errorf("Failed to find REG_FIDDLE macro.")
+	}
+	width, err := strconv.Atoi(match[WIDTH])
+	if err != nil {
+		return nil, fmt.Errorf("Failed to parse width: %s", err)
+	}
+	height, err := strconv.Atoi(match[HEIGHT])
+	if err != nil {
+		return nil, fmt.Errorf("Failed to parse height: %s", err)
+	}
+	source, err := strconv.Atoi(match[SOURCE])
+	if err != nil {
+		return nil, fmt.Errorf("Failed to parse source: %s", err)
+	}
+	textonly := match[TEXTONLY] == "true"
+
+	// Extract the code.
+	lines := strings.Split(body, "\n")
+
+	code := []string{}
+	foundREG := false
+	foundEnd := false
+	for _, line := range lines {
+		if !foundREG {
+			if strings.HasPrefix(line, "REG_FIDDLE(") {
+				foundREG = true
+			}
+			continue
+		}
+		if strings.Contains(line, "END FIDDLE") {
+			foundEnd = true
+			break
+		}
+		code = append(code, line)
+	}
+
+	if !foundEnd {
+		return nil, fmt.Errorf("Failed to find END FIDDLE.")
+	}
+
+	ret := &types.FiddleContext{
+		Name: match[NAME],
+		Code: strings.Join(code, "\n"),
+		Options: types.Options{
+			Width:    width,
+			Height:   height,
+			Source:   source,
+			TextOnly: textonly,
+		},
+	}
+	return ret, nil
+}
diff --git a/named-fiddles/go/parse/parse_test.go b/named-fiddles/go/parse/parse_test.go
new file mode 100644
index 0000000..f972d41
--- /dev/null
+++ b/named-fiddles/go/parse/parse_test.go
@@ -0,0 +1,135 @@
+package parse
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"go.skia.org/infra/go/testutils/unittest"
+)
+
+const good_sample = `// Copyright 2019 Google LLC.
+// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+#include "fiddle/examples.h"
+// HASH=bc9c7ea424d10bbcd1e5a88770d4794e
+REG_FIDDLE(Alpha_Constants_a, 256, 128, false, 1) {
+void draw(SkCanvas* canvas) {
+    std::vector<int32_t> srcPixels;
+    srcPixels.resize(source.height() * source.rowBytes());
+    SkPixmap pixmap(SkImageInfo::MakeN32Premul(source.width(), source.height()),
+                    &srcPixels.front(), source.rowBytes());
+    source.readPixels(pixmap, 0, 0);
+    for (int y = 0; y < 16; ++y) {
+        for (int x = 0; x < 16; ++x) {
+            int32_t* blockStart = &srcPixels.front() + y * source.width() * 16 + x * 16;
+            size_t transparentCount = 0;
+            for (int fillY = 0; fillY < source.height() / 16; ++fillY) {
+                for (int fillX = 0; fillX < source.width() / 16; ++fillX) {
+                    const SkColor color = SkUnPreMultiply::PMColorToColor(blockStart[fillX]);
+                    transparentCount += SkColorGetA(color) == SK_AlphaTRANSPARENT;
+                }
+                blockStart += source.width();
+            }
+            if (transparentCount > 200) {
+                blockStart = &srcPixels.front() + y * source.width() * 16 + x * 16;
+                for (int fillY = 0; fillY < source.height() / 16; ++fillY) {
+                    for (int fillX = 0; fillX < source.width() / 16; ++fillX) {
+                        blockStart[fillX] = SK_ColorRED;
+                    }
+                    blockStart += source.width();
+                }
+            }
+        }
+    }
+    SkBitmap bitmap;
+    bitmap.installPixels(pixmap);
+    canvas->drawBitmap(bitmap, 0, 0);
+}
+}  // END FIDDLE`
+
+const short_sample = `// Copyright 2019 Google LLC.
+REG_FIDDLE(Alpha_Constants_a, 256, 128, false, 1) {
+void draw(SkCanvas* canvas) {
+}
+}  // END FIDDLE`
+
+const missing_end_sample = `// Copyright 2019 Google LLC.
+REG_FIDDLE(Alpha_Constants_a, 256, 128, false, 1) {
+void draw(SkCanvas* canvas) {
+}
+}`
+
+const inactive_sample = `#if 0  // Disabled until updated to use current API.
+// Copyright 2019 Google LLC.
+// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+#include "fiddle/examples.h"
+// HASH=f0e584aec20eaee7a5bfed62aa885eee
+REG_FIDDLE(TextBlobBuilder_allocRun, 256, 60, false, 0) {
+void draw(SkCanvas* canvas) {
+    SkTextBlobBuilder builder;
+    SkFont font;
+    SkPaint paint;
+    const SkTextBlobBuilder::RunBuffer& run = builder.allocRun(font, 5, 20, 20);
+    paint.textToGlyphs("hello", 5, run.glyphs);
+    canvas->drawRect({20, 20, 30, 30}, paint);
+    canvas->drawTextBlob(builder.make(), 20, 20, paint);
+}
+}  // END FIDDLE
+#endif  // Disabled until updated to use current API.`
+
+const missing_reg_sample = `
+// Copyright 2019 Google LLC.
+// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+void draw(SkCanvas* canvas) {
+    SkTextBlobBuilder builder;
+    SkFont font;
+    SkPaint paint;
+    const SkTextBlobBuilder::RunBuffer& run = builder.allocRun(font, 5, 20, 20);
+    paint.textToGlyphs("hello", 5, run.glyphs);
+    canvas->drawRect({20, 20, 30, 30}, paint);
+    canvas->drawTextBlob(builder.make(), 20, 20, paint);
+}
+}  // END FIDDLE`
+
+const bad_macro = `REG_FIDDLE(TextBlobBuilder_allocRun, foo, 60, false, 0) {`
+
+const textonly_sample = `// Copyright 2019 Google LLC.
+REG_FIDDLE(Alpha_Constants_a, 256, 128, true, 0) {
+void draw(SkCanvas* canvas) {
+}
+}  // END FIDDLE`
+
+func TestParse(t *testing.T) {
+	unittest.SmallTest(t)
+
+	fc, err := ParseCpp(good_sample)
+	assert.NoError(t, err)
+
+	fc, err = ParseCpp(short_sample)
+	assert.NoError(t, err)
+	assert.Equal(t, "void draw(SkCanvas* canvas) {\n}", fc.Code)
+	assert.Equal(t, 256, fc.Options.Width)
+	assert.Equal(t, 128, fc.Options.Height)
+	assert.Equal(t, 1, fc.Options.Source)
+	assert.False(t, fc.Options.TextOnly)
+
+	fc, err = ParseCpp(missing_end_sample)
+	assert.Error(t, err)
+
+	fc, err = ParseCpp(inactive_sample)
+	assert.Equal(t, err, ErrorInactiveExample)
+	assert.Nil(t, fc)
+
+	fc, err = ParseCpp(missing_reg_sample)
+	assert.Error(t, err)
+
+	fc, err = ParseCpp(bad_macro)
+	assert.Error(t, err)
+
+	fc, err = ParseCpp(textonly_sample)
+	assert.NoError(t, err)
+	assert.Equal(t, "void draw(SkCanvas* canvas) {\n}", fc.Code)
+	assert.Equal(t, 256, fc.Options.Width)
+	assert.Equal(t, 128, fc.Options.Height)
+	assert.Equal(t, 0, fc.Options.Source)
+	assert.True(t, fc.Options.TextOnly)
+}
diff --git a/named-fiddles/images/named-fiddles/Dockerfile b/named-fiddles/images/named-fiddles/Dockerfile
new file mode 100644
index 0000000..d58390a
--- /dev/null
+++ b/named-fiddles/images/named-fiddles/Dockerfile
@@ -0,0 +1,13 @@
+FROM gcr.io/skia-public/basealpine:3.8
+
+USER root
+
+RUN apk update && \
+    apk add --no-cache git ca-certificates tzdata
+
+COPY . /
+
+USER skia
+
+ENTRYPOINT ["/usr/local/bin/named-fiddles"]
+CMD ["--logtostderr", "--prom_port=:20000", "--resources_dir=/usr/local/share/named-fiddles/"]
diff --git a/named-fiddles/images/named-fiddles/build_release b/named-fiddles/images/named-fiddles/build_release
new file mode 100755
index 0000000..a954bd0
--- /dev/null
+++ b/named-fiddles/images/named-fiddles/build_release
@@ -0,0 +1,15 @@
+#!/bin/bash
+APPNAME=named-fiddles
+
+set -x -e
+
+# Copy files into the right locations in ${ROOT}.
+copy_release_files()
+{
+INSTALL="install -D --verbose --backup=none"
+INSTALL_DIR="install -d --verbose --backup=none"
+${INSTALL} --mode=644 -T ${APPNAME}/Dockerfile    ${ROOT}/Dockerfile
+${INSTALL} --mode=755 -T ${GOPATH}/bin/${APPNAME} ${ROOT}/usr/local/bin/${APPNAME}
+}
+
+source ../bash/docker_build.sh
diff --git a/promk/prometheus/alerts_public.yml b/promk/prometheus/alerts_public.yml
index 7e8e91a..9f85f15 100644
--- a/promk/prometheus/alerts_public.yml
+++ b/promk/prometheus/alerts_public.yml
@@ -78,6 +78,16 @@
     annotations:
       description: 'Fiddle is experiencing heavy load and has insufficient idle fiddler pods. https://skia.googlesource.com/buildbot/%2B/master/fiddlek/PROD.md#fiddler_pods'
 
+  - alert: NamedFiddlesFailing
+    expr: named_fiddles_total_invalid > 0
+    for: 15m
+    labels:
+      category: infra
+      severity: warning
+      owner: jcgregorio@google.com
+    annotations:
+      description: 'Some named fiddles are failing. Visit https://named-fiddles.skia.org to see which ones.'
+
   - alert: FiddlerPodCommunicationErrors
     expr: rate(run_exhaustion[20m]) * 20 * 60 > 5
     for: 5m
diff --git a/run_unittests.go b/run_unittests.go
index c886445..ba17066 100644
--- a/run_unittests.go
+++ b/run_unittests.go
@@ -430,6 +430,7 @@
 	tests = append(tests, cmdTest([]string{"go", "run", "infra/bots/gen_tasks.go", "--test"}, ".", "gen_tasks.go --test", unittest.SMALL_TEST))
 	tests = append(tests, cmdTest([]string{"python", "go/testutils/unittest/uncategorized_tests.py"}, ".", "uncategorized tests", unittest.SMALL_TEST))
 	if runtime.GOOS == "linux" {
+		tests = append(tests, cmdTest([]string{"make", "testci"}, "named-fiddles", "named-fiddles elements", unittest.MEDIUM_TEST))
 		tests = append(tests, cmdTest([]string{"make", "test-frontend-ci"}, ".", "front-end tests", unittest.MEDIUM_TEST))
 		tests = append(tests, cmdTest([]string{"make", "validate"}, "proberk", "validate probers", unittest.SMALL_TEST))
 		tests = append(tests, cmdTest([]string{"make"}, "licenses", "check go package licenses", unittest.MEDIUM_TEST))