all: promote new app deployments only after they become ready

There are differences in the App Engine environment that
cannot be adequately simulated elsewhere. Although we do
the best we can with testing locally, there will always be
differences.

The old makefiles deployed the site, then ran a regression
test against it, and then promoted the tested version.

This change does the same. The testing has moved into the
web server proper so that it can test the handler directly and
thereby check things like the responses on different domains.

The go-app-deploy.sh now always deploys --no-promote,
only promoting after a self-test passes on the deployed site.

Unlike the old check which only applied to golang.org,
the new pre-promotion testing happens for all the sites.

Also factor out GoogleCN into internal/web, because we needed
to modify it (to avoid internal/webtest's requests being diagnosed
as coming from China) and there were too many copies.

Change-Id: I0cde0e2167df2332939908e716ddb6bf429f2565
Reviewed-on: https://go-review.googlesource.com/c/website/+/329250
Trust: Russ Cox <rsc@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
This commit is contained in:
Russ Cox 2021-06-18 23:19:15 -04:00
Родитель d38c22d04f
Коммит 5f5f230b60
22 изменённых файлов: 259 добавлений и 232 удалений

Просмотреть файл

@ -20,6 +20,7 @@ import (
_ "golang.org/x/tools/playground"
"golang.org/x/website"
"golang.org/x/website/internal/backport/httpfs"
"golang.org/x/website/internal/webtest"
)
var (
@ -66,6 +67,10 @@ func main() {
if err != nil {
log.Fatal(err)
}
h = webtest.HandlerWithCheck(h, "/_readycheck",
filepath.Join(blogRoot, "testdata/*.txt"))
http.Handle("/", h)
ln, err := net.Listen("tcp", *httpAddr)

Просмотреть файл

@ -6,7 +6,6 @@ runtime: go115
main: ./cmd/golangorg
env_variables:
GOLANGORG_CHECK_COUNTRY: true
GOLANGORG_REQUIRE_DL_SECRET_KEY: true
GOLANGORG_ENFORCE_HOSTS: true
GOLANGORG_REDIS_ADDR: 10.0.0.4:6379 # instance "gophercache"

Просмотреть файл

@ -36,6 +36,7 @@ import (
"golang.org/x/website/internal/redirect"
"golang.org/x/website/internal/short"
"golang.org/x/website/internal/web"
"golang.org/x/website/internal/webtest"
// Registers "/compile" handler that redirects to play.golang.org/compile.
// If we are in prod we will register "golang.org/compile" separately,
@ -93,6 +94,9 @@ func main() {
handler := NewHandler(*contentDir, *goroot)
handler = webtest.HandlerWithCheck(handler, "/_readycheck",
filepath.Join(*contentDir, "../cmd/golangorg/testdata/*.txt"))
if *verbose {
log.Printf("golang.org server:")
log.Printf("\tversion = %s", runtime.Version())
@ -137,7 +141,6 @@ func NewHandler(contentDir, goroot string) http.Handler {
if err != nil {
log.Fatalf("NewSite: %v", err)
}
site.GoogleCN = googleCN
mux := http.NewServeMux()
mux.Handle("/", site)
@ -195,26 +198,6 @@ func appEngineSetup(site *web.Site, mux *http.ServeMux) {
log.Println("AppEngine initialization complete")
}
// googleCN reports whether request r is considered
// to be served from golang.google.cn.
// TODO: This is duplicated within internal/proxy. Move to a common location.
func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
if strings.HasSuffix(r.Host, ".cn") {
return true
}
if !env.CheckCountry() {
return false
}
switch r.Header.Get("X-Appengine-Country") {
case "", "ZZ", "CN":
return true
}
return false
}
func blogHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://blog.golang.org"+strings.TrimPrefix(r.URL.Path, "/blog"), http.StatusFound)
}

Просмотреть файл

@ -5,80 +5,13 @@
package main
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"golang.org/x/website/internal/webtest"
)
func serverAddress(t *testing.T) string {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
ln, err = net.Listen("tcp6", "[::1]:0")
}
if err != nil {
t.Fatal(err)
}
defer ln.Close()
return ln.Addr().String()
}
func waitForServerReady(t *testing.T, addr string) {
waitForServer(t,
fmt.Sprintf("http://%v/", addr),
"The Go Programming Language",
15*time.Second)
}
const pollInterval = 200 * time.Millisecond
func waitForServer(t *testing.T, url, match string, timeout time.Duration) {
// "health check" duplicated from x/tools/cmd/tipgodoc/tip.go
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
time.Sleep(pollInterval)
res, err := http.Get(url)
if err != nil {
continue
}
rbody, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err == nil && res.StatusCode == http.StatusOK {
if bytes.Contains(rbody, []byte(match)) {
return
}
}
}
t.Fatalf("Server failed to respond in %v", timeout)
}
func killAndWait(cmd *exec.Cmd) {
cmd.Process.Kill()
cmd.Wait()
}
func init() {
// TestWeb reinvokes the test binary (us) with -be-main
// to simulate running the actual golangorg binary.
if len(os.Args) >= 2 && os.Args[1] == "-be-main" {
os.Args = os.Args[1:]
os.Args[0] = "(golangorg)"
main()
os.Exit(0)
}
}
func TestWeb(t *testing.T) {
h := NewHandler("../../_content", runtime.GOROOT())
files, err := filepath.Glob("testdata/*.txt")
@ -91,16 +24,3 @@ func TestWeb(t *testing.T) {
}
}
}
// Regression tests to run against a production instance of golangorg.
var host = flag.String("regtest.host", "", "host to run regression test against")
func TestLiveServer(t *testing.T) {
*host = strings.TrimSuffix(*host, "/")
if *host == "" {
t.Skip("regtest.host flag missing.")
}
webtest.TestServer(t, "testdata/*.txt", *host)
}

14
cmd/golangorg/testdata/live.txt поставляемый
Просмотреть файл

@ -1,36 +1,36 @@
# Tests that can only run against the live server,
# because they depend on production resources.
GET /dl/
GET https://golang.org/dl/
body contains href="/dl/go1.11.windows-amd64.msi"
GET /dl/?mode=json
GET https://golang.org/dl/?mode=json
body contains .windows-amd64.msi
body !contains UA-
GET /s/go2design
GET https://golang.org/s/go2design
code == 302
body ~ proposal.*Found
body !contains UA-
POST /compile
POST https://golang.org/compile
postquery
body=package main; func main() { print(6*7); }
body == {"compile_errors":"","output":"42"}
POST /compile
POST https://golang.org/compile
postquery
body=//empty
body contains expected 'package', found 'EOF'
body !contains UA-
POST /compile
POST https://golang.org/compile
postquery
version=2
body=package main; import ("fmt"; "time"); func main() {fmt.Print("A"); time.Sleep(time.Second); fmt.Print("B")}
body == {"Errors":"","Events":[{"Message":"A","Kind":"stdout","Delay":0},{"Message":"B","Kind":"stdout","Delay":1000000000}]}
POST /share
POST https://golang.org/share
postbody
package main
body !contains UA-

2
cmd/golangorg/testdata/release.txt поставляемый
Просмотреть файл

@ -1,4 +1,4 @@
GET /doc/devel/release
GET https://golang.org/doc/devel/release
header content-type == text/html; charset=utf-8
trimbody contains
<h2 id="go1.14">go1.14 (released 2020-02-25)</h2>

86
cmd/golangorg/testdata/web.txt поставляемый
Просмотреть файл

@ -1,4 +1,4 @@
GET /
GET https://golang.org/
body contains Go is an open source programming language
body contains Binary distributions available for
@ -11,143 +11,143 @@ body contains Binary distributions available for
body contains href="/golang.org/doc
body !contains href="/doc
GET /change/75944e2e3a63
GET https://golang.org/change/75944e2e3a63
code == 302
redirect contains bdb10cf
body contains bdb10cf
body !contains UA-
GET /cmd/compile/internal/amd64/
GET https://golang.org/cmd/compile/internal/amd64/
body contains href="/src/cmd/compile/internal/amd64/ssa.go"
GET /conduct
GET https://golang.org/conduct
body contains Project Stewards
GET /doc/
GET https://golang.org/doc/
body contains an introduction to using modules in a simple project
GET /doc/asm
GET https://golang.org/doc/asm
body ~ Quick Guide.*Assembler
GET /doc/debugging_with_gdb.html
GET https://golang.org/doc/debugging_with_gdb.html
redirect == /doc/gdb
GET /doc/devel/release
GET https://golang.org/doc/devel/release
body ~ go1\.14\.2\s+\(released 2020-04-08\)\s+includes\s+fixes to cgo, the go command, the runtime,
GET /doc/devel/release.html
GET https://golang.org/doc/devel/release.html
redirect == /doc/devel/release
GET /doc/faq
GET https://golang.org/doc/faq
body contains What is the purpose of the project
GET /doc/gdb
GET https://golang.org/doc/gdb
body contains Debugging Go Code
GET /doc/go1.16.html
GET https://golang.org/doc/go1.16.html
redirect == /doc/go1.16
GET /doc/go1.16
GET https://golang.org/doc/go1.16
body contains Go 1.16
GET /doc/go_spec
GET https://golang.org/doc/go_spec
redirect == /ref/spec
GET /doc/go_spec.html
GET https://golang.org/doc/go_spec.html
redirect == /ref/spec
GET /doc/go_spec.md
GET https://golang.org/doc/go_spec.md
redirect == /ref/spec
GET /doc/go_mem.html
GET https://golang.org/doc/go_mem.html
redirect == /ref/mem
GET /doc/go_mem.md
GET https://golang.org/doc/go_mem.md
redirect == /ref/mem
GET /doc/help.html
GET https://golang.org/doc/help.html
redirect == /help
GET /help/
GET https://golang.org/help/
redirect == /help
GET /help
GET https://golang.org/help
body contains Get help
GET /pkg/fmt/
GET https://golang.org/pkg/fmt/
body contains Package fmt implements formatted I/O
GET /src/fmt/
GET https://golang.org/src/fmt/
body contains scan_test.go
GET /src/fmt/print.go
GET https://golang.org/src/fmt/print.go
body contains // Println formats using
GET /pkg
GET https://golang.org/pkg
redirect == /pkg/
GET /pkg/
GET https://golang.org/pkg/
body contains Standard library
body contains Package fmt implements formatted I/O
body !contains internal/syscall
body !contains cmd/gc
GET /pkg/?m=all
GET https://golang.org/pkg/?m=all
body contains Standard library
body contains Package fmt implements formatted I/O
body contains internal/syscall/?m=all
body !contains cmd/gc
GET /pkg/bufio/
GET https://golang.org/pkg/bufio/
body contains href="/pkg/io/#Writer
GET /pkg/database/sql/
GET https://golang.org/pkg/database/sql/
body contains The number of connections currently in use; added in Go 1.11
body contains The number of idle connections; added in Go 1.11
GET /cmd/compile/internal/amd64/
GET https://golang.org/cmd/compile/internal/amd64/
body contains href="/src/cmd/compile/internal/amd64/ssa.go"
GET /pkg/math/bits/
GET https://golang.org/pkg/math/bits/
body contains Added in Go 1.9
GET /pkg/net/
GET https://golang.org/pkg/net/
body contains // IPv6 scoped addressing zone; added in Go 1.1
GET /pkg/net/http/
GET https://golang.org/pkg/net/http/
body contains title="Added in Go 1.11"
GET /pkg/net/http/httptrace/
GET https://golang.org/pkg/net/http/httptrace/
body ~ Got1xxResponse.*// Go 1\.11
body ~ GotFirstResponseByte func\(\)\s*$
GET /pkg/os/
GET https://golang.org/pkg/os/
body contains func Open
GET /pkg/strings/
GET https://golang.org/pkg/strings/
body contains href="/src/strings/strings.go"
GET /project
GET https://golang.org/project
body contains <li><a href="/doc/go1.14">Go 1.14</a> <small>(February 2020)</small></li>
body contains <li><a href="/doc/go1.1">Go 1.1</a> <small>(May 2013)</small></li>
GET /project/
GET https://golang.org/project/
redirect == /project
GET /project/notexist
GET https://golang.org/project/notexist
code == 404
GET /ref/mem
GET https://golang.org/ref/mem
body contains Memory Model
GET /ref/spec
GET https://golang.org/ref/spec
body contains Go Programming Language Specification
GET /robots.txt
GET https://golang.org/robots.txt
body contains Disallow: /search
body !contains UA-
GET /x/net
GET https://golang.org/x/net
code == 200
body contains <meta name="go-import" content="golang.org/x/net git https://go.googlesource.com/net">
body !contains UA-

12
cmd/golangorg/testdata/x.txt поставляемый
Просмотреть файл

@ -1,25 +1,25 @@
GET /x/net
GET https://golang.org/x/net
code == 200
body contains <meta name="go-import" content="golang.org/x/net git https://go.googlesource.com/net">
body contains http-equiv="refresh" content="0; url=https://pkg.go.dev/golang.org/x/net">
GET /x/net/suffix
GET https://golang.org/x/net/suffix
code == 200
body contains <meta name="go-import" content="golang.org/x/net git https://go.googlesource.com/net">
body contains http-equiv="refresh" content="0; url=https://pkg.go.dev/golang.org/x/net/suffix">
GET /x/pkgsite
GET https://golang.org/x/pkgsite
code == 200
body contains <meta name="go-import" content="golang.org/x/pkgsite git https://go.googlesource.com/pkgsite">
body contains <a href="https://pkg.go.dev/golang.org/x/pkgsite">Redirecting to documentation...</a>
body contains http-equiv="refresh" content="0; url=https://pkg.go.dev/golang.org/x/pkgsite">
GET /x/notexist
GET https://golang.org/x/notexist
code == 404
GET /x/
GET https://golang.org/x/
code == 307
header location == https://pkg.go.dev/search?q=golang.org/x
GET /x/In%20Valid,X
GET https://golang.org/x/In%20Valid,X
code == 404

Просмотреть файл

@ -6,7 +6,7 @@
# This script is meant to be run from Cloud Build as a substitute
# for "gcloud app deploy", as in:
#
# go-app-deploy.sh app.yaml
# go-app-deploy.sh [--project=name] app.yaml
#
# It should not be run by hand and is therefore not marked executable.
#
@ -25,18 +25,68 @@
set -e
project=golang-org
case "$1" in
--project=*)
project=$(echo $1 | sed 's/--project=//')
shift
esac
yaml=app.yaml
case "$1" in
*.yaml)
yaml=$1
shift
esac
if [ $# != 0 ]; then
echo 'usage: go-app-deploy.sh [--project=name] path/to/app.yaml' >&2
exit 2
fi
promote=$(
git cat-file -p 'HEAD' |
awk '
BEGIN { flag = "--no-promote" }
/^Reviewed-on:/ { flag = "--no-promote" }
/^Website-Publish:/ { flag = "--promote" }
BEGIN { flag = "false" }
/^Reviewed-on:/ { flag = "false" }
/^Website-Publish:/ { flag = "true" }
END {print flag}
'
)
version=$(
git log -n1 --date='format:%Y-%m-%d-%H%M%S' --pretty='format:%cd-%h'
)
version=$(git log -n1 --date='format:%Y-%m-%d-%H%M%S' --pretty='format:%cd-%h')
service=$(awk '$1=="service:" {print $2}' $yaml)
servicedot="-$service-dot"
if [ "$service" = default ]; then
servicedot=""
fi
host="$version-dot$servicedot-$project.appspot.com"
echo "### deploying to https://$host"
gcloud -q --project=$project app deploy -v $version --no-promote $yaml
curl --version
for i in 1 2 3 4 5; do
if curl -s --fail --show-error "https://$host/_readycheck"; then
echo '### site is up!'
if $promote; then
serving=$(gcloud app services describe --project=$project $service | grep ': 1.0')
if [ "$serving" '>' "$version" ]; then
echo "### serving version $serving is newer than our $version; not promoting"
exit 1
fi
echo '### promoting'
gcloud -q --project=$project app services set-traffic $service --splits=$version=1
fi
exit 0
fi
echo '### not healthy'
curl "https://$host/_readycheck" # show response body
done
echo "### failed to become healthy; giving up"
exit 1
gcloud app deploy $promote -v $version "$@"

Просмотреть файл

@ -10,9 +10,11 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"golang.org/x/website/go.dev/cmd/internal/site"
"golang.org/x/website/internal/webtest"
)
var discoveryHosts = map[string]string{
@ -27,13 +29,14 @@ func main() {
// Running in repo root.
dir = "go.dev"
}
godev, err := site.Load(dir)
h, err := NewHandler(dir)
if err != nil {
log.Fatal(err)
}
http.Handle("/", addCSP(http.FileServer(godev)))
http.Handle("/explore/", http.StripPrefix("/explore/", redirectHosts(discoveryHosts)))
http.Handle("learn.go.dev/", http.HandlerFunc(redirectLearn))
h = webtest.HandlerWithCheck(h, "/_readycheck",
filepath.Join(dir, "cmd/frontend/testdata/*.txt"))
addr := ":" + listenPort()
if addr == ":0" {
@ -45,7 +48,19 @@ func main() {
}
defer l.Close()
log.Printf("Listening on http://%v/\n", l.Addr().String())
log.Print(http.Serve(l, nil))
log.Print(http.Serve(l, h))
}
func NewHandler(dir string) (http.Handler, error) {
godev, err := site.Load(dir)
if err != nil {
return nil, err
}
mux := http.NewServeMux()
mux.Handle("/", addCSP(http.FileServer(godev)))
mux.Handle("/explore/", http.StripPrefix("/explore/", redirectHosts(discoveryHosts)))
mux.Handle("learn.go.dev/", http.HandlerFunc(redirectLearn))
return mux, nil
}
func redirectLearn(w http.ResponseWriter, r *http.Request) {

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright 2021 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 (
"testing"
"golang.org/x/website/internal/webtest"
)
func TestWeb(t *testing.T) {
h, err := NewHandler("../..")
if err != nil {
t.Fatal(err)
}
webtest.TestHandler(t, "testdata/*.txt", h)
}

5
go.dev/cmd/frontend/testdata/godev.txt поставляемый Normal file
Просмотреть файл

@ -0,0 +1,5 @@
GET https://go.dev/
body contains <h2 class="WhoUses-headerH2">Companies using Go</h2>
GET https://go.dev/solutions/google/
body ~ it\s+has\s+powered\s+many\s+projects\s+at\s+Google.

Просмотреть файл

@ -115,26 +115,6 @@ func serveJSON(w http.ResponseWriter, r *http.Request, d listTemplateData) {
}
}
// googleCN reports whether request r is considered
// to be served from golang.google.cn.
// TODO: This is duplicated within internal/proxy. Move to a common location.
func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
if strings.HasSuffix(r.Host, ".cn") {
return true
}
if !env.CheckCountry() {
return false
}
switch r.Header.Get("X-Appengine-Country") {
case "", "ZZ", "CN":
return true
}
return false
}
func (h server) uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)

6
internal/env/env.go поставляемый
Просмотреть файл

@ -13,7 +13,6 @@ import (
)
var (
checkCountry = boolEnv("GOLANGORG_CHECK_COUNTRY")
enforceHosts = boolEnv("GOLANGORG_ENFORCE_HOSTS")
requireDLSecretKey = boolEnv("GOLANGORG_REQUIRE_DL_SECRET_KEY")
)
@ -25,11 +24,6 @@ func RequireDLSecretKey() bool {
return requireDLSecretKey
}
// CheckCountry reports whether country restrictions should be enforced.
func CheckCountry() bool {
return checkCountry
}
// EnforceHosts reports whether host filtering should be enforced.
func EnforceHosts() bool {
return enforceHosts

Просмотреть файл

@ -15,10 +15,9 @@ import (
"io/ioutil"
"log"
"net/http"
"strings"
"time"
"golang.org/x/website/internal/env"
"golang.org/x/website/internal/web"
)
const playgroundURL = "https://play.golang.org"
@ -124,7 +123,7 @@ func flatten(seq []Event) string {
}
func share(w http.ResponseWriter, r *http.Request) {
if googleCN(r) {
if web.GoogleCN(r) {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
@ -151,22 +150,3 @@ func share(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
// googleCN reports whether request r is considered
// to be served from golang.google.cn.
func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
if strings.HasSuffix(r.Host, ".cn") {
return true
}
if !env.CheckCountry() {
return false
}
switch r.Header.Get("X-Appengine-Country") {
case "", "ZZ", "CN":
return true
}
return false
}

28
internal/web/googlecn.go Normal file
Просмотреть файл

@ -0,0 +1,28 @@
// Copyright 2021 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 web
import (
"net/http"
"strings"
)
// GoogleCN reports whether request r is considered to be arriving from China.
// Typically that means the request is for host golang.google.cn,
// but we also report true for requests that set googlecn=1 as a query parameter
// and requests that App Engine geolocates in China or in “unknown country.”
func GoogleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
if strings.HasSuffix(r.Host, ".cn") {
return true
}
switch r.Header.Get("X-Appengine-Country") {
case "ZZ", "CN":
return true
}
return false
}

Просмотреть файл

@ -36,10 +36,6 @@ type Site struct {
Templates *template.Template
// GoogleCN reports whether this request should be marked GoogleCN.
// If the function is nil, no requests are marked GoogleCN.
GoogleCN func(*http.Request) bool
// GoogleAnalytics optionally adds Google Analytics via the provided
// tracking ID to each page.
GoogleAnalytics string
@ -122,7 +118,7 @@ type Page struct {
Data interface{} // data to be rendered into page frame
// Filled in automatically by ServePage
GoogleCN bool // page is being served from golang.google.cn
GoogleCN bool // served on golang.google.cn
GoogleAnalytics string // Google Analytics tag
Version string // current Go version
@ -135,7 +131,7 @@ func (s *Site) fullPage(r *http.Request, page Page) Page {
page.TabTitle = page.Title
}
page.Version = runtime.Version()
page.GoogleCN = s.googleCN(r)
page.GoogleCN = GoogleCN(r)
page.GoogleAnalytics = s.GoogleAnalytics
page.site = s
return page
@ -413,7 +409,3 @@ func (s *Site) serveRawText(w http.ResponseWriter, text []byte) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write(text)
}
func (s *Site) googleCN(r *http.Request) bool {
return s.GoogleCN != nil && s.GoogleCN(r)
}

Просмотреть файл

@ -164,6 +164,26 @@ import (
"unicode/utf8"
)
// HandlerWithCheck returns an http.Handler that responds to each request
// by running the test script files mached by glob against the handler h.
// If the tests pass, the returned http.Handler responds with status code 200.
// If they fail, it prints the details and responds with status code 503
// (service unavailable).
func HandlerWithCheck(h http.Handler, path, glob string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == path {
err := CheckHandler(glob, h)
if err != nil {
http.Error(w, "webtest.CheckHandler failed:\n"+err.Error()+"\n", http.StatusInternalServerError)
} else {
fmt.Fprintf(w, "ok\n")
}
return
}
h.ServeHTTP(w, r)
})
}
// CheckHandler runs the test script files matched by glob
// against the handler h. If any errors are encountered,
// CheckHandler returns an error listing the problems.

Просмотреть файл

@ -12,8 +12,10 @@ import (
"log"
"net/http"
"os"
"path/filepath"
_ "golang.org/x/tools/playground"
"golang.org/x/website/internal/webtest"
)
func gaeMain() {
@ -36,7 +38,11 @@ func gaeMain() {
if port == "" {
port = "8080"
}
log.Fatal(http.ListenAndServe(":"+port, nil))
h := webtest.HandlerWithCheck(http.DefaultServeMux, "/_readycheck",
filepath.Join(root, "testdata/*.txt"))
log.Fatal(http.ListenAndServe(":"+port, h))
}
// gaePrepContent returns a Reader that produces the content from the given

Просмотреть файл

@ -23,6 +23,7 @@ import (
"time"
"golang.org/x/tools/playground/socket"
"golang.org/x/website/internal/webtest"
)
const (
@ -126,6 +127,9 @@ func main() {
registerStatic(root)
h := webtest.HandlerWithCheck(http.DefaultServeMux, "/_readycheck",
filepath.Join(root, "testdata/*.txt"))
go func() {
url := "http://" + httpAddr
if waitServer(url) && *openBrowser && startBrowser(url) {
@ -134,7 +138,7 @@ func main() {
log.Printf("Please open your web browser and visit %s", url)
}
}()
log.Fatal(http.ListenAndServe(httpAddr, nil))
log.Fatal(http.ListenAndServe(httpAddr, h))
}
// registerStatic registers handlers to serve static content

24
tour/server_test.go Normal file
Просмотреть файл

@ -0,0 +1,24 @@
// Copyright 2021 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 (
"log"
"net/http"
"testing"
"golang.org/x/website/internal/webtest"
)
func TestWeb(t *testing.T) {
if err := initTour(".", "SocketTransport"); err != nil {
log.Fatal(err)
}
http.HandleFunc("/", rootHandler)
http.HandleFunc("/lesson/", lessonHandler)
registerStatic(".")
webtest.TestHandler(t, "testdata/*.txt", http.DefaultServeMux)
}

3
tour/testdata/tour.txt поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
GET https://tour.golang.org/
body contains >A Tour of Go<