| // 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 task |
| |
| import ( |
| "bytes" |
| "context" |
| "flag" |
| "fmt" |
| "image" |
| "image/png" |
| "io" |
| "net/http" |
| "net/http/httptest" |
| "os" |
| "path/filepath" |
| "testing" |
| |
| "golang.org/x/build/internal/workflow" |
| ) |
| |
| func TestTweetRelease(t *testing.T) { |
| if testing.Short() { |
| // This test is useful when modifying the tweet text and image templates, |
| // but don't run it in -short mode since tweetImage involves making some |
| // HTTP GET requests to the internet. |
| t.Skip("skipping test that hits go.dev/dl/?mode=json read-only API in -short mode") |
| } |
| |
| tests := [...]struct { |
| name string |
| taskFn func(*workflow.TaskContext, ReleaseTweet, ExternalConfig) (string, error) |
| in ReleaseTweet |
| wantLog string |
| }{ |
| { |
| name: "minor", |
| taskFn: TweetMinorRelease, |
| in: ReleaseTweet{ |
| Version: "go1.17.1", |
| SecondaryVersion: "go1.16.8", |
| Security: "Includes security fixes for A and B.", |
| Announcement: "https://groups.google.com/g/golang-announce/c/dx9d7IOseHw/m/KNH37k37AAAJ", |
| RandomSeed: 234, |
| }, |
| wantLog: `tweet text: |
| 🎊 Go 1.17.1 and 1.16.8 are released! |
| |
| 🔐 Security: Includes security fixes for A and B. |
| |
| 📢 Announcement: https://groups.google.com/g/golang-announce/c/dx9d7IOseHw/m/KNH37k37AAAJ |
| |
| ⬇️ Download: https://go.dev/dl/#go1.17.1 |
| |
| #golang |
| tweet image: |
| $ go install golang.org/dl/go1.17.1@latest |
| $ go1.17.1 download |
| Downloaded 0.0% ( 0 / 102606384 bytes) ... |
| Downloaded 50.0% ( 51303192 / 102606384 bytes) ... |
| Downloaded 100.0% (102606384 / 102606384 bytes) |
| Unpacking go1.17.1.linux-arm64.tar.gz ... |
| Success. You may now run 'go1.17.1' |
| $ go1.17.1 version |
| go version go1.17.1 linux/arm64` + "\n", |
| }, |
| { |
| name: "beta", |
| taskFn: TweetBetaRelease, |
| in: ReleaseTweet{ |
| Version: "go1.17beta1", |
| Announcement: "https://groups.google.com/g/golang-announce/c/i4EliPDV9Ok/m/MxA-nj53AAAJ", |
| RandomSeed: 678, |
| }, |
| wantLog: `tweet text: |
| ⚡️ Go 1.17 Beta 1 is released! |
| |
| ⚙️ Try it! File bugs! https://go.dev/issue/new |
| |
| 🗣 Announcement: https://groups.google.com/g/golang-announce/c/i4EliPDV9Ok/m/MxA-nj53AAAJ |
| |
| 📦 Download: https://go.dev/dl/#go1.17beta1 |
| |
| #golang |
| tweet image: |
| $ go install golang.org/dl/go1.17beta1@latest |
| $ go1.17beta1 download |
| Downloaded 0.0% ( 0 / 135610703 bytes) ... |
| Downloaded 50.0% ( 67805351 / 135610703 bytes) ... |
| Downloaded 100.0% (135610703 / 135610703 bytes) |
| Unpacking go1.17beta1.darwin-amd64.tar.gz ... |
| Success. You may now run 'go1.17beta1' |
| $ go1.17beta1 version |
| go version go1.17beta1 darwin/amd64` + "\n", |
| }, |
| { |
| name: "rc", |
| taskFn: TweetRCRelease, |
| in: ReleaseTweet{ |
| Version: "go1.17rc2", |
| Announcement: "https://groups.google.com/g/golang-announce/c/yk30ovJGXWY/m/p9uUnKbbBQAJ", |
| RandomSeed: 456, |
| }, |
| wantLog: `tweet text: |
| 🎉 Go 1.17 Release Candidate 2 is released! |
| |
| 🏖 Run it in dev! Run it in prod! File bugs! https://go.dev/issue/new |
| |
| 🔈 Announcement: https://groups.google.com/g/golang-announce/c/yk30ovJGXWY/m/p9uUnKbbBQAJ |
| |
| 📦 Download: https://go.dev/dl/#go1.17rc2 |
| |
| #golang |
| tweet image: |
| $ go install golang.org/dl/go1.17rc2@latest |
| $ go1.17rc2 download |
| Downloaded 0.0% ( 0 / 116660997 bytes) ... |
| Downloaded 50.0% ( 58330498 / 116660997 bytes) ... |
| Downloaded 100.0% (116660997 / 116660997 bytes) |
| Unpacking go1.17rc2.windows-arm64.zip ... |
| Success. You may now run 'go1.17rc2' |
| $ go1.17rc2 version |
| go version go1.17rc2 windows/arm64` + "\n", |
| }, |
| { |
| name: "major", |
| taskFn: TweetMajorRelease, |
| in: ReleaseTweet{ |
| Version: "go1.17", |
| Security: "Includes a super duper security fix (CVE-123).", |
| RandomSeed: 123, |
| }, |
| wantLog: `tweet text: |
| 🥳 Go 1.17 is released! |
| |
| 🔐 Security: Includes a super duper security fix (CVE-123). |
| |
| 📝 Release notes: https://go.dev/doc/go1.17 |
| |
| 📦 Download: https://go.dev/dl/#go1.17 |
| |
| #golang |
| tweet image: |
| $ go install golang.org/dl/go1.17@latest |
| $ go1.17 download |
| Downloaded 0.0% ( 0 / 133579378 bytes) ... |
| Downloaded 50.0% ( 66789689 / 133579378 bytes) ... |
| Downloaded 100.0% (133579378 / 133579378 bytes) |
| Unpacking go1.17.freebsd-amd64.tar.gz ... |
| Success. You may now run 'go1.17' |
| $ go1.17 version |
| go version go1.17 freebsd/amd64` + "\n", |
| }, |
| } |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| // Call the tweet task function in dry-run mode so it |
| // doesn't actually try to tweet, but capture its log. |
| var buf bytes.Buffer |
| ctx := &workflow.TaskContext{Context: context.Background(), Logger: fmtWriter{&buf}} |
| tweetURL, err := tc.taskFn(ctx, tc.in, ExternalConfig{DryRun: true}) |
| if err != nil { |
| t.Fatal("got a non-nil error:", err) |
| } |
| if got, want := tweetURL, "(dry-run)"; got != want { |
| t.Errorf("unexpected tweetURL: got = %q, want %q", got, want) |
| } |
| if got, want := buf.String(), tc.wantLog; got != want { |
| t.Errorf("unexpected log:\n got: %q\nwant: %q", got, want) |
| } |
| }) |
| } |
| } |
| |
| type fmtWriter struct{ w io.Writer } |
| |
| func (f fmtWriter) Printf(format string, v ...interface{}) { |
| fmt.Fprintf(f.w, format, v...) |
| } |
| |
| var updateFlag = flag.Bool("update", false, "Update golden files.") |
| |
| func TestDrawTerminal(t *testing.T) { |
| got, err := drawTerminal(`$ go install golang.org/dl/go1.18beta1@latest |
| $ go1.18beta1 download |
| Downloaded 0.0% ( 0 / 111109966 bytes) ... |
| Downloaded 50.0% ( 55554983 / 111109966 bytes) ... |
| Downloaded 100.0% (111109966 / 111109966 bytes) |
| Unpacking go1.18beta1.linux-s390x.tar.gz ... |
| Success. You may now run 'go1.18beta1' |
| $ go1.18beta1 version |
| go version go1.18beta1 linux/s390x`) |
| if err != nil { |
| t.Fatalf("drawTerminal: got error=%v, want nil", err) |
| } |
| if *updateFlag { |
| encodePNG(t, filepath.Join("testdata", "terminal.png"), got) |
| return |
| } |
| want := decodePNG(t, filepath.Join("testdata", "terminal.png")) |
| if !got.Bounds().Eq(want.Bounds()) { |
| t.Fatalf("drawTerminal: got image bounds=%v, want %v", got.Bounds(), want.Bounds()) |
| } |
| diff := func(a, b uint32) uint64 { |
| if a < b { |
| return uint64(b - a) |
| } |
| return uint64(a - b) |
| } |
| var total uint64 |
| for y := 0; y < want.Bounds().Dy(); y++ { |
| for x := 0; x < want.Bounds().Dx(); x++ { |
| r0, g0, b0, a0 := got.At(x, y).RGBA() |
| r1, g1, b1, a1 := want.At(x, y).RGBA() |
| const D = 0xffff * 20 / 100 // Diff threshold of 20% for RGB color components. |
| if diff(r0, r1) > D || diff(g0, g1) > D || diff(b0, b1) > D || a0 != a1 { |
| t.Errorf("at (%d, %d):\n got RGBA %v\nwant RGBA %v", x, y, got.At(x, y), want.At(x, y)) |
| } |
| total += diff(r0, r1) + diff(g0, g1) + diff(b0, b1) |
| } |
| } |
| if testing.Verbose() { |
| t.Logf("average pixel color diff: %v%%", 100*float64(total)/float64(0xffff*want.Bounds().Dx()*want.Bounds().Dy())) |
| } |
| } |
| |
| func encodePNG(t *testing.T, name string, m image.Image) { |
| t.Helper() |
| var buf bytes.Buffer |
| err := (&png.Encoder{CompressionLevel: png.BestCompression}).Encode(&buf, m) |
| if err != nil { |
| t.Fatal(err) |
| } |
| err = os.WriteFile(name, buf.Bytes(), 0644) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func decodePNG(t *testing.T, name string) image.Image { |
| t.Helper() |
| f, err := os.Open(name) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer f.Close() |
| m, err := png.Decode(f) |
| if err != nil { |
| t.Fatal(err) |
| } |
| return m |
| } |
| |
| func TestPostTweet(t *testing.T) { |
| mux := http.NewServeMux() |
| mux.HandleFunc("upload.twitter.com/1.1/media/upload.json", func(w http.ResponseWriter, req *http.Request) { |
| if got, want := req.Method, http.MethodPost; got != want { |
| t.Errorf("media/upload: got method %s, want %s", got, want) |
| return |
| } |
| if got, want := req.FormValue("media_category"), "tweet_image"; got != want { |
| t.Errorf("media/upload: got media_category=%q, want %q", got, want) |
| } |
| f, hdr, err := req.FormFile("media") |
| if err != nil { |
| t.Errorf("media/upload: error getting image file: %v", err) |
| return |
| } |
| if got, want := hdr.Filename, "image.png"; got != want { |
| t.Errorf("media/upload: got file name=%q, want %q", got, want) |
| } |
| if got, want := mustRead(f), "image-png-bytes"; got != want { |
| t.Errorf("media/upload: got file content=%q, want %q", got, want) |
| return |
| } |
| mustWrite(w, `{"media_id_string": "media-123"}`) |
| }) |
| mux.HandleFunc("api.twitter.com/1.1/statuses/update.json", func(w http.ResponseWriter, req *http.Request) { |
| if got, want := req.Method, http.MethodPost; got != want { |
| t.Errorf("statuses/update: got method %s, want %s", got, want) |
| return |
| } |
| if got, want := req.FormValue("status"), "tweet-text"; got != want { |
| t.Errorf("statuses/update: got status=%q, want %q", got, want) |
| } |
| if got, want := req.FormValue("media_ids"), "media-123"; got != want { |
| t.Errorf("statuses/update: got media_ids=%q, want %q", got, want) |
| } |
| mustWrite(w, `{"id_str": "tweet-123", "user": {"screen_name": "golang"}}`) |
| }) |
| httpClient := &http.Client{Transport: localRoundTripper{mux}} |
| |
| tweetURL, err := postTweet(httpClient, "tweet-text", []byte("image-png-bytes")) |
| if err != nil { |
| t.Fatal("postTweet:", err) |
| } |
| if got, want := tweetURL, "https://twitter.com/golang/status/tweet-123"; got != want { |
| t.Errorf("got tweetURL=%q, want %q", got, want) |
| } |
| } |
| |
| // localRoundTripper is an http.RoundTripper that executes HTTP transactions |
| // by using handler directly, instead of going over an HTTP connection. |
| type localRoundTripper struct { |
| handler http.Handler |
| } |
| |
| func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { |
| w := httptest.NewRecorder() |
| l.handler.ServeHTTP(w, req) |
| return w.Result(), nil |
| } |
| |
| func mustRead(r io.Reader) string { |
| b, err := io.ReadAll(r) |
| if err != nil { |
| panic(err) |
| } |
| return string(b) |
| } |
| |
| func mustWrite(w io.Writer, s string) { |
| _, err := io.WriteString(w, s) |
| if err != nil { |
| panic(err) |
| } |
| } |