app/appengine: automate the hiding of old release branches

Also, because this is the first CL in this package to add tests, add a
helper to return the right template filename depending on the
environment (go test vs prod) so tests don't panic in init.

Fixes golang/go#34097
Updates golang/go#34116

Change-Id: I4b3e83c2417611cfbdc32e27941dbb90687eb509
Reviewed-on: https://go-review.googlesource.com/c/build/+/194643
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/app/appengine/dash.go b/app/appengine/dash.go
index d4dd42d..1250188 100644
--- a/app/appengine/dash.go
+++ b/app/appengine/dash.go
@@ -8,6 +8,8 @@
 	"context"
 	"encoding/gob"
 	"net/http"
+	"sort"
+	"strings"
 
 	"google.golang.org/appengine"
 )
@@ -196,19 +198,19 @@
 	},
 }
 
-// hiddenBranches specifies branches that
-// should not be displayed on the build dashboard.
-// This also prevents the builder infrastructure
-// from testing sub-repos against these branches.
-var hiddenBranches = map[string]bool{
-	"release-branch.go1.4":           true,
-	"release-branch.go1.5":           true,
-	"release-branch.go1.6":           true,
-	"release-branch.go1.7":           true,
-	"release-branch.go1.8":           true,
-	"release-branch.go1.9":           true,
-	"release-branch.go1.10":          true,
-	"release-branch.go1.10-security": true,
-	"release-branch.go1.11-security": true,
-	"release-branch.go1.11":          true,
+// supportedReleaseBranches returns a slice containing the most recent two non-security release branches
+// contained in branches.
+func supportedReleaseBranches(branches []string) (supported []string) {
+	for _, b := range branches {
+		if !strings.HasPrefix(b, "release-branch.go1.") ||
+			len(b) != len("release-branch.go1.nn") { // assumes nn in range [10, 99]
+			continue
+		}
+		supported = append(supported, b)
+	}
+	sort.Strings(supported)
+	if len(supported) > 2 {
+		supported = supported[len(supported)-2:]
+	}
+	return supported
 }
diff --git a/app/appengine/dash_test.go b/app/appengine/dash_test.go
new file mode 100644
index 0000000..18a8515
--- /dev/null
+++ b/app/appengine/dash_test.go
@@ -0,0 +1,39 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestSupportedReleaseBranches(t *testing.T) {
+	tests := []struct {
+		in, want []string
+	}{
+		{
+			in:   []string{"master", "release-branch.go1.12", "release-branch.go1.5", "release-branch.go1.14"},
+			want: []string{"release-branch.go1.12", "release-branch.go1.14"},
+		},
+		{
+			in:   []string{"master", "release-branch.go1.12", "release-branch.go1.5", "release-branch.go1.14", "release-branch.go1.15-security", "release-branch.go1.15"},
+			want: []string{"release-branch.go1.14", "release-branch.go1.15"},
+		},
+		{
+			in:   []string{"master", "release-branch.go1.12", "release-branch.go1.5"},
+			want: []string{"release-branch.go1.12"},
+		},
+		{
+			in:   []string{"master", "release-branch.go1.12-security"},
+			want: nil,
+		},
+	}
+	for _, tt := range tests {
+		got := supportedReleaseBranches(tt.in)
+		if !reflect.DeepEqual(got, tt.want) {
+			t.Errorf("supportedReleaseBranches(%q) = %q; want %q", tt.in, got, tt.want)
+		}
+	}
+}
diff --git a/app/appengine/notify.go b/app/appengine/notify.go
index 6c0b9fb..47b5adc 100644
--- a/app/appengine/notify.go
+++ b/app/appengine/notify.go
@@ -123,7 +123,7 @@
 var (
 	notifyLater = delay.Func("notify", notify)
 	notifyTmpl  = template.Must(template.New("notify.txt").
-			Funcs(template.FuncMap(tmplFuncs)).ParseFiles("app/appengine/notify.txt"))
+			Funcs(template.FuncMap(tmplFuncs)).ParseFiles(templateFile("notify.txt")))
 )
 
 // notify tries to update the CL for the given Commit with a failure message.
@@ -272,7 +272,7 @@
 	sendPerfMailTmpl  = template.Must(
 		template.New("perf_notify.txt").
 			Funcs(template.FuncMap(tmplFuncs)).
-			ParseFiles("app/appengine/perf_notify.txt"),
+			ParseFiles(templateFile("perf_notify.txt")),
 	)
 )
 
diff --git a/app/appengine/perf_changes.go b/app/appengine/perf_changes.go
index ec276ee..2602af5 100644
--- a/app/appengine/perf_changes.go
+++ b/app/appengine/perf_changes.go
@@ -215,7 +215,7 @@
 }
 
 var perfChangesTemplate = template.Must(
-	template.New("perf_changes.html").Funcs(tmplFuncs).ParseFiles("app/appengine/perf_changes.html"),
+	template.New("perf_changes.html").Funcs(tmplFuncs).ParseFiles(templateFile("perf_changes.html")),
 )
 
 type perfChangesData struct {
diff --git a/app/appengine/perf_detail.go b/app/appengine/perf_detail.go
index 2ff7a96..0a91d1c 100644
--- a/app/appengine/perf_detail.go
+++ b/app/appengine/perf_detail.go
@@ -209,5 +209,5 @@
 func (l uiPerfDetailMetrics) Less(i, j int) bool { return l[i].Name < l[j].Name }
 
 var uiPerfDetailTemplate = template.Must(
-	template.New("perf_detail.html").Funcs(tmplFuncs).ParseFiles("app/appengine/perf_detail.html"),
+	template.New("perf_detail.html").Funcs(tmplFuncs).ParseFiles(templateFile("perf_detail.html")),
 )
diff --git a/app/appengine/perf_graph.go b/app/appengine/perf_graph.go
index 319a22a..92f580c 100644
--- a/app/appengine/perf_graph.go
+++ b/app/appengine/perf_graph.go
@@ -238,7 +238,7 @@
 }
 
 var perfGraphTemplate = template.Must(
-	template.New("perf_graph.html").ParseFiles("app/appengine/perf_graph.html"),
+	template.New("perf_graph.html").ParseFiles(templateFile("perf_graph.html")),
 )
 
 type perfGraphData struct {
diff --git a/app/appengine/perf_learn.go b/app/appengine/perf_learn.go
index 156f014..eef4da6 100644
--- a/app/appengine/perf_learn.go
+++ b/app/appengine/perf_learn.go
@@ -157,7 +157,7 @@
 }
 
 var perfLearnTemplate = template.Must(
-	template.New("perf_learn.html").Funcs(tmplFuncs).ParseFiles("app/appengine/perf_learn.html"),
+	template.New("perf_learn.html").Funcs(tmplFuncs).ParseFiles(templateFile("perf_learn.html")),
 )
 
 type perfLearnData struct {
diff --git a/app/appengine/ui.go b/app/appengine/ui.go
index d273393..8f86be6 100644
--- a/app/appengine/ui.go
+++ b/app/appengine/ui.go
@@ -14,6 +14,8 @@
 	"fmt"
 	"html/template"
 	"net/http"
+	"os"
+	"path/filepath"
 	"sort"
 	"strconv"
 	"strings"
@@ -110,13 +112,7 @@
 				return
 			}
 			tagState = []*TagState{s}
-			for _, b := range branches {
-				if !strings.HasPrefix(b, "release-branch.") {
-					continue
-				}
-				if hiddenBranches[b] {
-					continue
-				}
+			for _, b := range supportedReleaseBranches(branches) {
 				s, err := GetTagState(c, "release", b)
 				if err == datastore.ErrNoSuchEntity {
 					continue
@@ -582,7 +578,7 @@
 }
 
 var uiTemplate = template.Must(
-	template.New("ui.html").Funcs(tmplFuncs).ParseFiles("app/appengine/ui.html"),
+	template.New("ui.html").Funcs(tmplFuncs).ParseFiles(templateFile("ui.html")),
 )
 
 var tmplFuncs = template.FuncMap{
@@ -706,3 +702,15 @@
 	}
 	return strings.Join(lines[len(lines)-n:], "\n")
 }
+
+// templateFile returns the path to the provided HTML template file,
+// conditionally prepending a relative path depending on the
+// environment.
+func templateFile(base string) string {
+	// In tests the current directory is ".", but in prod it's up
+	// two levels. So just look to see if it's in . first.
+	if _, err := os.Stat(base); err == nil {
+		return base
+	}
+	return filepath.Join("app/appengine", base)
+}