| // Copyright 2020 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 ( |
| "context" |
| "fmt" |
| "reflect" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-github/v48/github" |
| "golang.org/x/build/gerrit" |
| "golang.org/x/build/internal/workflow" |
| ) |
| |
| func TestInterpretNextRelease(t *testing.T) { |
| tests := []struct { |
| name string |
| tags []string |
| bump string |
| want releaseVersion |
| }{ |
| { |
| name: "next minor version of v0.0.0 is v0.1.0", |
| tags: []string{"gopls/v0.0.0"}, |
| bump: "next minor", |
| want: releaseVersion{Major: 0, Minor: 1, Patch: 0}, |
| }, |
| { |
| name: "pre-release versions should be ignored", |
| tags: []string{"gopls/v0.0.0", "gopls/v0.1.0-pre.1", "gopls/v0.1.0-pre.2"}, |
| bump: "next minor", |
| want: releaseVersion{Major: 0, Minor: 1, Patch: 0}, |
| }, |
| { |
| name: "next patch version of v0.2.2 is v0.2.3", |
| tags: []string{"gopls/0.1.1", "gopls/0.2.0", "gopls/0.2.1", "gopls/v0.2.2"}, |
| bump: "next patch", |
| want: releaseVersion{Major: 0, Minor: 2, Patch: 3}, |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| tools := NewFakeRepo(t, "tools") |
| commit := tools.Commit(map[string]string{ |
| "go.mod": "module golang.org/x/tools\n", |
| "go.sum": "\n", |
| }) |
| |
| for _, tag := range tc.tags { |
| tools.Tag(tag, commit) |
| } |
| |
| gerrit := NewFakeGerrit(t, tools) |
| |
| tasks := &ReleaseGoplsTasks{ |
| Gerrit: gerrit, |
| } |
| |
| got, err := tasks.interpretNextRelease(&workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}, tc.bump) |
| if err != nil { |
| t.Fatalf("interpretNextRelease(%q) should not return error, but return %v", tc.bump, err) |
| } |
| if tc.want != got { |
| t.Errorf("interpretNextRelease(%q) = %v, want %v", tc.bump, tc.want, got) |
| } |
| }) |
| } |
| } |
| |
| func TestPossibleGoplsVersions(t *testing.T) { |
| tests := []struct { |
| name string |
| tags []string |
| want []string |
| }{ |
| { |
| name: "any one version tag should have three possible next versions", |
| tags: []string{"gopls/v1.2.3"}, |
| want: []string{"v1.2.4", "v1.3.0", "v2.0.0"}, |
| }, |
| { |
| name: "v1.2.3 and v1.1.0 share the same next major version", |
| tags: []string{"gopls/v1.2.3", "gopls/v1.1.0"}, |
| want: []string{"v1.1.1", "v1.2.0", "v1.2.4", "v1.3.0", "v2.0.0"}, |
| }, |
| { |
| name: "two versions without any duplicate next version should have 6 possible versions", |
| tags: []string{"gopls/v1.1.3", "gopls/v2.1.2"}, |
| want: []string{"v1.1.4", "v1.2.0", "v2.0.0", "v2.1.3", "v2.2.0", "v3.0.0"}, |
| }, |
| { |
| name: "v1.1.0 and v1.1.0 share the same next major and next minor", |
| tags: []string{"gopls/v1.1.2", "gopls/v1.1.0"}, |
| want: []string{"v1.1.1", "v1.1.3", "v1.2.0", "v2.0.0"}, |
| }, |
| { |
| name: "v1.1.0 next patch v1.1.1 already exist", |
| tags: []string{"gopls/v1.1.1", "gopls/v1.1.0"}, |
| want: []string{"v1.1.2", "v1.2.0", "v2.0.0"}, |
| }, |
| { |
| name: "pre-release version tag should not have any effect on the next version", |
| tags: []string{"gopls/v0.16.1-pre.1", "gopls/v0.16.1-pre.2", "gopls/v0.16.0"}, |
| want: []string{"v0.16.1", "v0.17.0", "v1.0.0"}, |
| }, |
| { |
| name: "other unrelated tag should not have any effect on the next version", |
| tags: []string{"v0.9.2", "v0.9.3", "v0.23.0", "gopls/v0.16.0"}, |
| want: []string{"v0.16.1", "v0.17.0", "v1.0.0"}, |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| tools := NewFakeRepo(t, "tools") |
| commit := tools.Commit(map[string]string{ |
| "go.mod": "module golang.org/x/tools\n", |
| "go.sum": "\n", |
| }) |
| |
| for _, tag := range tc.tags { |
| tools.Tag(tag, commit) |
| } |
| |
| gerrit := NewFakeGerrit(t, tools) |
| |
| tasks := &ReleaseGoplsTasks{ |
| Gerrit: gerrit, |
| } |
| |
| got, err := tasks.possibleGoplsVersions(&workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}) |
| if err != nil { |
| t.Fatalf("possibleGoplsVersions() should not return error, but return %v", err) |
| } |
| if diff := cmp.Diff(tc.want, got); diff != "" { |
| t.Errorf("possibleGoplsVersions() mismatch (-want +got):\n%s", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestCreateBranchIfMinor(t *testing.T) { |
| ctx := context.Background() |
| tests := []struct { |
| name string |
| version string |
| existingBranch string |
| wantErr bool |
| wantBranch string |
| }{ |
| { |
| name: "should create a release branch for a minor release", |
| version: "v1.2.0", |
| wantErr: false, |
| wantBranch: "gopls-release-branch.1.2", |
| }, |
| { |
| name: "should return nil if the release branch already exist for a minor release", |
| version: "v1.2.0", |
| existingBranch: "gopls-release-branch.1.2", |
| wantErr: false, |
| }, |
| { |
| name: "should not create a release branch for a patch release", |
| version: "v1.2.4", |
| existingBranch: "gopls-release-branch.1.2", |
| wantErr: false, |
| wantBranch: "", |
| }, |
| { |
| name: "should throw error for patch release if release branch is missing", |
| version: "v1.3.1", |
| wantErr: true, |
| wantBranch: "", |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| tools := NewFakeRepo(t, "tools") |
| _ = tools.Commit(map[string]string{ |
| "go.mod": "module golang.org/x/tools\n", |
| "go.sum": "\n", |
| }) |
| _ = tools.Commit(map[string]string{ |
| "README.md": "THIS IS READ ME.", |
| }) |
| |
| gerritClient := NewFakeGerrit(t, tools) |
| |
| masterHead, err := gerritClient.ReadBranchHead(ctx, "tools", "master") |
| if err != nil { |
| t.Fatalf("ReadBranchHead should be able to get revision of master branch's head: %v", err) |
| } |
| |
| if tc.existingBranch != "" { |
| if _, err := gerritClient.CreateBranch(ctx, "tools", tc.existingBranch, gerrit.BranchInput{Revision: masterHead}); err != nil { |
| t.Fatalf("failed to create the branch %q: %v", tc.existingBranch, err) |
| } |
| } |
| |
| tasks := &ReleaseGoplsTasks{ |
| Gerrit: gerritClient, |
| } |
| |
| release, _, _ := parseVersion(tc.version) |
| err = tasks.createBranchIfMinor(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, release) |
| |
| if tc.wantErr && err == nil { |
| t.Errorf("createBranchIfMinor() should return error but return nil") |
| } else if !tc.wantErr && err != nil { |
| t.Errorf("createBranchIfMinor() should return nil but return err: %v", err) |
| } |
| |
| // Created branch should have same revision as master branch's head. |
| if tc.wantBranch != "" { |
| gotRevision, err := gerritClient.ReadBranchHead(ctx, "tools", tc.wantBranch) |
| if err != nil { |
| t.Errorf("ReadBranchHead should be able to get revision of %s branch's head: %v", tc.wantBranch, err) |
| } |
| if masterHead != gotRevision { |
| t.Errorf("createBranchIfMinor() = %q, want %q", gotRevision, masterHead) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestUpdateCodeReviewConfig(t *testing.T) { |
| ctx := context.Background() |
| testcases := []struct { |
| name string |
| version string |
| config string |
| wantCommit bool |
| wantConfig string |
| }{ |
| { |
| name: "should update the codereview.cfg with version 1.2 for input minor release 1.2.0", |
| version: "v1.2.0", |
| config: "foo", |
| wantCommit: true, |
| wantConfig: `issuerepo: golang/go |
| branch: gopls-release-branch.1.2 |
| parent-branch: master |
| `, |
| }, |
| { |
| name: "should update the codereview.cfg with version 1.2 for input patch release 1.2.3", |
| version: "v1.2.3", |
| config: "foo", |
| wantCommit: true, |
| wantConfig: `issuerepo: golang/go |
| branch: gopls-release-branch.1.2 |
| parent-branch: master |
| `, |
| }, |
| { |
| name: "no need to update the config for a minor release 1.3.0", |
| version: "v1.3.0", |
| config: `issuerepo: golang/go |
| branch: gopls-release-branch.1.3 |
| parent-branch: master |
| `, |
| wantCommit: false, |
| wantConfig: `issuerepo: golang/go |
| branch: gopls-release-branch.1.3 |
| parent-branch: master |
| `, |
| }, |
| { |
| name: "no need to update the config for a patch release 1.3.3", |
| version: "v1.3.3", |
| config: `issuerepo: golang/go |
| branch: gopls-release-branch.1.3 |
| parent-branch: master |
| `, |
| wantCommit: false, |
| wantConfig: `issuerepo: golang/go |
| branch: gopls-release-branch.1.3 |
| parent-branch: master |
| `, |
| }, |
| } |
| for _, tc := range testcases { |
| t.Run(tc.name, func(t *testing.T) { |
| tools := NewFakeRepo(t, "tools") |
| _ = tools.Commit(map[string]string{ |
| "go.mod": "module golang.org/x/tools\n", |
| "go.sum": "\n", |
| }) |
| _ = tools.Commit(map[string]string{ |
| "codereview.cfg": tc.config, |
| }) |
| |
| gerritClient := NewFakeGerrit(t, tools) |
| |
| headMaster, err := gerritClient.ReadBranchHead(ctx, "tools", "master") |
| if err != nil { |
| t.Fatalf("ReadBranchHead should be able to get revision of master branch's head: %v", err) |
| } |
| |
| configMaster, err := gerritClient.ReadFile(ctx, "tools", headMaster, "codereview.cfg") |
| if err != nil { |
| t.Fatalf("ReadFile should be able to read the codereview.cfg file from master branch head: %v", err) |
| } |
| |
| release, _, _ := parseVersion(tc.version) |
| releaseBranch := goplsReleaseBranchName(release) |
| if _, err := gerritClient.CreateBranch(ctx, "tools", releaseBranch, gerrit.BranchInput{Revision: headMaster}); err != nil { |
| t.Fatalf("failed to create the branch %q: %v", releaseBranch, err) |
| } |
| |
| headRelease, err := gerritClient.ReadBranchHead(ctx, "tools", releaseBranch) |
| if err != nil { |
| t.Fatalf("ReadBranchHead should be able to get revision of release branch's head: %v", err) |
| } |
| |
| tasks := &ReleaseGoplsTasks{ |
| Gerrit: gerritClient, |
| CloudBuild: NewFakeCloudBuild(t, gerritClient, "", nil), |
| } |
| |
| _, err = tasks.updateCodeReviewConfig(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, release, nil, 0) |
| if err != nil { |
| t.Fatalf("updateCodeReviewConfig() returns error: %v", err) |
| } |
| |
| // master branch's head commit should not change. |
| headMasterAfter, err := gerritClient.ReadBranchHead(ctx, "tools", "master") |
| if err != nil { |
| t.Fatalf("ReadBranchHead() should be able to get revision of master branch's head: %v", err) |
| } |
| if headMasterAfter != headMaster { |
| t.Errorf("updateCodeReviewConfig() should not change master branch's head, got = %s want = %s", headMasterAfter, headMaster) |
| } |
| |
| // master branch's head codereview.cfg content should not change. |
| configMasterAfter, err := gerritClient.ReadFile(ctx, "tools", headMasterAfter, "codereview.cfg") |
| if err != nil { |
| t.Fatalf("ReadFile() should be able to read the codereview.cfg file from master branch head: %v", err) |
| } |
| if diff := cmp.Diff(configMaster, configMasterAfter); diff != "" { |
| t.Errorf("updateCodeReviewConfig() should not change codereview.cfg content in master branch (-want +got) \n %s", diff) |
| } |
| |
| // verify the release branch commit have the expected behavior. |
| headReleaseAfter, err := gerritClient.ReadBranchHead(ctx, "tools", releaseBranch) |
| if err != nil { |
| t.Fatalf("ReadBranchHead() should be able to get revision of master branch's head: %v", err) |
| } |
| if tc.wantCommit && headReleaseAfter == headRelease { |
| t.Errorf("updateCodeReviewConfig() should have one commit to release branch, head of branch got = %s want = %s", headRelease, headReleaseAfter) |
| } else if !tc.wantCommit && headReleaseAfter != headRelease { |
| t.Errorf("updateCodeReviewConfig() should have not change release branch's head, got = %s want = %s", headRelease, headReleaseAfter) |
| } |
| |
| // verify the release branch configreview.cfg have the expected content. |
| configReleaseAfter, err := gerritClient.ReadFile(ctx, "tools", headReleaseAfter, "codereview.cfg") |
| if err != nil { |
| t.Fatalf("ReadFile() should be able to read the codereview.cfg file from release branch head: %v", err) |
| } |
| if diff := cmp.Diff(tc.wantConfig, string(configReleaseAfter)); diff != "" { |
| t.Errorf("codereview.cfg mismatch (-want +got) \n %s", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestNextPrerelease(t *testing.T) { |
| ctx := context.Background() |
| testcases := []struct { |
| name string |
| tags []string |
| version string |
| want string |
| }{ |
| { |
| name: "next pre-release is 2", |
| tags: []string{"gopls/v0.16.0-pre.0", "gopls/v0.16.0-pre.1"}, |
| version: "v0.16.0", |
| want: "pre.2", |
| }, |
| { |
| name: "next pre-release is 2 regardless of other minor or patch version", |
| tags: []string{"gopls/v0.16.0-pre.0", "gopls/v0.16.0-pre.1", "gopls/v0.16.1-pre.1", "gopls/v0.2.0-pre.3"}, |
| version: "v0.16.0", |
| want: "pre.2", |
| }, |
| { |
| name: "next pre-release is 2 regardless of non-int prerelease version", |
| tags: []string{"gopls/v0.16.0-pre.0", "gopls/v0.16.0-pre.1", "gopls/v0.16.0-pre.foo", "gopls/v0.16.0-pre.bar"}, |
| version: "v0.16.0", |
| want: "pre.2", |
| }, |
| } |
| |
| for _, tc := range testcases { |
| t.Run(tc.name, func(t *testing.T) { |
| tools := NewFakeRepo(t, "tools") |
| commit := tools.Commit(map[string]string{ |
| "go.mod": "module golang.org/x/tools\n", |
| "go.sum": "\n", |
| }) |
| |
| for _, tag := range tc.tags { |
| tools.Tag(tag, commit) |
| } |
| |
| gerrit := NewFakeGerrit(t, tools) |
| |
| tasks := &ReleaseGoplsTasks{ |
| Gerrit: gerrit, |
| } |
| |
| release, _, ok := parseVersion(tc.version) |
| if !ok { |
| t.Fatalf("parseVersion(%q) failed", tc.version) |
| } |
| got, err := tasks.nextPrereleaseVersion(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, release) |
| if err != nil || tc.want != got { |
| t.Errorf("nextPrereleaseVersion(%q) = (%v, %v) but want (%v, nil)", tc.version, got, err, tc.want) |
| } |
| }) |
| } |
| } |
| |
| func TestFindOrCreateReleaseIssue(t *testing.T) { |
| ctx := context.Background() |
| testcases := []struct { |
| name string |
| version string |
| create bool |
| fakeGithub FakeGitHub |
| wantErr bool |
| wantIssue int64 |
| }{ |
| { |
| name: "milestone does not exist", |
| version: "v0.16.2", |
| create: true, |
| wantErr: true, |
| wantIssue: 0, |
| }, |
| { |
| name: "irrelevant milestone exist", |
| version: "v0.16.2", |
| create: true, |
| fakeGithub: FakeGitHub{ |
| Milestones: map[int]string{1: "gopls/v0.16.1"}, |
| }, |
| wantErr: true, |
| wantIssue: 0, |
| }, |
| { |
| name: "milestone exist, issue is missing, create true, workflow should create this issue", |
| version: "v0.16.2", |
| create: true, |
| fakeGithub: FakeGitHub{ |
| Milestones: map[int]string{1: "gopls/v0.16.2"}, |
| }, |
| wantErr: false, |
| wantIssue: 1, |
| }, |
| { |
| name: "milestone exist, issue is missing, create false, workflow error out", |
| version: "v0.16.2", |
| create: false, |
| fakeGithub: FakeGitHub{ |
| Milestones: map[int]string{1: "gopls/v0.16.2"}, |
| }, |
| wantErr: true, |
| wantIssue: 0, |
| }, |
| { |
| name: "milestone exist, issue exist, create true, workflow should reuse the issue", |
| version: "v0.16.2", |
| create: true, |
| fakeGithub: FakeGitHub{ |
| Milestones: map[int]string{1: "gopls/v0.16.2"}, |
| Issues: map[int]*github.Issue{2: {Number: github.Int(2), Title: github.String("x/tools/gopls: release version v0.16.2"), Milestone: &github.Milestone{ID: github.Int64(1)}}}, |
| }, |
| wantErr: false, |
| wantIssue: 2, |
| }, |
| { |
| name: "milestone exist, issue exist, create false, workflow should reuse the issue", |
| version: "v0.16.2", |
| create: false, |
| fakeGithub: FakeGitHub{ |
| Milestones: map[int]string{1: "gopls/v0.16.2"}, |
| Issues: map[int]*github.Issue{2: {Number: github.Int(2), Title: github.String("x/tools/gopls: release version v0.16.2"), Milestone: &github.Milestone{ID: github.Int64(1)}}}, |
| }, |
| wantErr: false, |
| wantIssue: 2, |
| }, |
| } |
| |
| for _, tc := range testcases { |
| t.Run(tc.name, func(t *testing.T) { |
| tasks := &ReleaseGoplsTasks{ |
| Github: &tc.fakeGithub, |
| } |
| |
| release, _, ok := parseVersion(tc.version) |
| if !ok { |
| t.Fatalf("parseVersion(%q) failed", tc.version) |
| } |
| gotIssue, err := tasks.findOrCreateGitHubIssue(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, release, []string{"gobot"}, tc.create) |
| |
| if tc.wantErr && err == nil { |
| t.Errorf("createReleaseIssue(%s) should return error but return nil", tc.version) |
| } else if !tc.wantErr && err != nil { |
| t.Errorf("createReleaseIssue(%s) should return nil but return err: %v", tc.version, err) |
| } |
| |
| if tc.wantIssue != gotIssue { |
| t.Errorf("createReleaseIssue(%s) = %v, want %v", tc.version, gotIssue, tc.wantIssue) |
| } |
| }) |
| } |
| } |
| |
| func TestGoplsPrereleaseFlow(t *testing.T) { |
| if testing.Short() { |
| t.Skip("skipping an end-to-end workflow test in short mode") |
| } |
| mustHaveShell(t) |
| |
| testcases := []struct { |
| name string |
| // The fields below are the prepared states before running the gopls |
| // pre-release flow. |
| // commitTags specifies a sequence of (possibly) tagged commits. |
| // For each entry, a new commit is created, and if the entry is |
| // non empty that commit is tagged with the entry value. |
| commitTags []string |
| // If set, create the release branch before starting the workflow. |
| createBranch bool |
| config string |
| release releaseVersion |
| // fields below are the desired states. |
| wantVersion string |
| wantConfig string |
| wantCommits int |
| }{ |
| { |
| name: "update all three file through two commits", |
| commitTags: []string{"gopls/v0.0.0"}, |
| createBranch: true, |
| config: " ", |
| release: releaseVersion{Major: 0, Minor: 1, Patch: 0}, |
| wantVersion: "v0.1.0-pre.1", |
| wantConfig: `issuerepo: golang/go |
| branch: gopls-release-branch.0.1 |
| parent-branch: master |
| `, |
| wantCommits: 2, |
| }, |
| { |
| name: "codereview.cfg already have expected content, update go.mod and go.sum with one commit", |
| commitTags: []string{"gopls/v0.0.0"}, |
| createBranch: true, |
| config: `issuerepo: golang/go |
| branch: gopls-release-branch.0.1 |
| parent-branch: master |
| `, |
| release: releaseVersion{Major: 0, Minor: 1, Patch: 0}, |
| wantVersion: "v0.1.0-pre.1", |
| wantConfig: `issuerepo: golang/go |
| branch: gopls-release-branch.0.1 |
| parent-branch: master |
| `, |
| wantCommits: 1, |
| }, |
| { |
| name: "create the branch for minor version", |
| commitTags: []string{"gopls/v0.11.0"}, |
| createBranch: false, |
| config: ` `, |
| release: releaseVersion{Major: 0, Minor: 12, Patch: 0}, |
| wantVersion: "v0.12.0-pre.1", |
| wantConfig: `issuerepo: golang/go |
| branch: gopls-release-branch.0.12 |
| parent-branch: master |
| `, |
| wantCommits: 2, |
| }, |
| { |
| name: "workflow should increment the pre-release number to 4", |
| commitTags: []string{"gopls/v0.8.2", "gopls/v0.8.3-pre.1", "gopls/v0.8.3-pre.2", "gopls/v0.8.3-pre.3"}, |
| createBranch: true, |
| config: " ", |
| release: releaseVersion{Major: 0, Minor: 8, Patch: 3}, |
| wantVersion: "v0.8.3-pre.4", |
| wantConfig: `issuerepo: golang/go |
| branch: gopls-release-branch.0.8 |
| parent-branch: master |
| `, |
| wantCommits: 2, |
| }, |
| } |
| |
| for _, tc := range testcases { |
| runTestWithInput := func(input map[string]any) { |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| |
| tools := NewFakeRepo(t, "tools") |
| beforeHead := tools.Commit(map[string]string{ |
| "gopls/go.mod": "module golang.org/x/tools\n", |
| "gopls/go.sum": "\n", |
| "codereview.cfg": tc.config, |
| }) |
| // Create the release branch and make a few commits to the master branch. |
| // Var beforeHead is used to track the commit of release branch's head |
| // before trigger the gopls pre-release run. If we do not need to create a |
| // release branch, beforeHead will point to the initial commit in the |
| // master branch. |
| if len(tc.commitTags) != 0 { |
| for i, tag := range tc.commitTags { |
| commit := tools.CommitOnBranch("master", map[string]string{ |
| "README.md": fmt.Sprintf("THIS IS READ ME FOR %v.", i), |
| }) |
| beforeHead = commit |
| if tag != "" { |
| tools.Tag(tag, commit) |
| } |
| } |
| } |
| |
| if tc.createBranch { |
| tools.Branch(goplsReleaseBranchName(tc.release), beforeHead) |
| } |
| |
| gerrit := NewFakeGerrit(t, tools) |
| |
| // fakeGo handles multiple arguments in gopls pre-release flow. |
| // - go get will write fake go.sum and go.mod to simulate pining the |
| // x/tools dependency. |
| // - go install will write a fake script in bin/gopls and grant execute |
| // permission to it to simulate gopls installation. |
| // - go env will return the current dir so gopls will point to the fake |
| // script that is written by go install. |
| // - go mod will exit without any error. |
| var fakeGo = fmt.Sprintf(`#!/bin/bash -exu |
| |
| case "$1" in |
| "get") |
| echo -n "test go sum" > go.sum |
| echo -n "test go mod" > go.mod |
| ;; |
| "install") |
| mkdir bin |
| # write following content to bin/gopls |
| # make sure the gopls version and gopls references have return code 0. |
| cat <<EOF > bin/gopls |
| #!/bin/bash -exu |
| |
| case "\$1" in |
| "version") |
| echo %q |
| ;; |
| "references") |
| exit 0 |
| ;; |
| *) |
| echo unexpected command "\$@" |
| exit 1 |
| ;; |
| esac |
| EOF |
| |
| # Make the bin/gopls script executable |
| chmod +x bin/gopls |
| ;; |
| "env") |
| echo "." |
| ;; |
| "mod") |
| exit 0 |
| ;; |
| *) |
| echo unexpected command $@ |
| exit 1 |
| ;; |
| esac`, tc.wantVersion) |
| |
| var gotSubject string // subject of the announcement email that was sent |
| |
| tasks := &ReleaseGoplsTasks{ |
| Gerrit: gerrit, |
| CloudBuild: NewFakeCloudBuild(t, gerrit, "", nil, FakeBinary{Name: "go", Implementation: fakeGo}), |
| Github: &FakeGitHub{ |
| Milestones: map[int]string{ |
| 1: fmt.Sprintf("gopls/%s", tc.release.String()), |
| }, |
| }, |
| SendMail: func(h MailHeader, c MailContent) error { |
| gotSubject = c.Subject |
| return nil |
| }, |
| ApproveAction: func(tc *workflow.TaskContext) error { return nil }, |
| } |
| |
| wd := tasks.NewPrereleaseDefinition() |
| w, err := workflow.Start(wd, input) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| outputs, err := w.Run(ctx, &verboseListener{t: t}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Verify that workflow will create the release branch for minor releases. |
| // The release branch is created before the flow run for patch releases. |
| afterHead, err := gerrit.ReadBranchHead(ctx, "tools", goplsReleaseBranchName(tc.release)) |
| if err != nil { |
| t.Error(err) |
| } |
| |
| // Verify that workflow return the expected pre-release version. |
| if got := outputs["version"]; got != tc.wantVersion { |
| t.Errorf("Output: got \"version\" %q, want %q", got, tc.wantVersion) |
| } |
| |
| // Verify the content of following files are expected. |
| contentChecks := []struct { |
| repo string |
| branch string |
| path string |
| want string |
| }{ |
| { |
| repo: "tools", |
| branch: goplsReleaseBranchName(tc.release), |
| path: "codereview.cfg", |
| want: tc.wantConfig, |
| }, |
| { |
| repo: "tools", |
| branch: goplsReleaseBranchName(tc.release), |
| path: "gopls/go.sum", |
| want: "test go sum", |
| }, |
| { |
| repo: "tools", |
| branch: goplsReleaseBranchName(tc.release), |
| path: "gopls/go.mod", |
| want: "test go mod", |
| }, |
| } |
| for _, check := range contentChecks { |
| commit, err := gerrit.ReadBranchHead(ctx, check.repo, check.branch) |
| if err != nil { |
| t.Fatal(err) |
| } |
| got, err := gerrit.ReadFile(ctx, check.repo, commit, check.path) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if string(got) != check.want { |
| t.Errorf("Content of %q = %q, want %q", check.path, got, check.want) |
| } |
| } |
| |
| // Verify the commits merged to release branch after the flow execution. |
| beforeIndex, afterIndex := 0, 0 |
| for i, commit := range tools.History() { |
| if commit == afterHead { |
| afterIndex = i |
| } |
| if commit == beforeHead { |
| beforeIndex = i |
| } |
| } |
| |
| if committed := beforeIndex - afterIndex; committed != tc.wantCommits { |
| t.Errorf("%v commits merged to release branch after the pre-release flow executed, but want %v commits", committed, tc.wantCommits) |
| } |
| |
| // Verify the pre-release tag is created and it's pointing to the head of |
| // the release branch. |
| info, err := gerrit.GetTag(ctx, "tools", fmt.Sprintf("gopls/%s", tc.wantVersion)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if info.Revision != afterHead { |
| t.Errorf("the pre-release tag points to commit %s, should point to the head commit of release branch %s", info.Revision, afterHead) |
| } |
| if wantSubject := "Gopls " + tc.wantVersion + " is released"; gotSubject != wantSubject { |
| // The full email content is checked by TestAnnouncementMail. |
| t.Errorf("NewPrereleaseDefinition().Run(): got email subject %q, want %q", gotSubject, wantSubject) |
| } |
| } |
| |
| t.Run("manual input version: "+tc.name, func(t *testing.T) { |
| runTestWithInput(map[string]any{ |
| releaseCoordinatorsParam.Name: []string{"gobot"}, |
| "explicit version (optional)": tc.release.String(), |
| "next version": "use explicit version", |
| }) |
| }) |
| versionBump := "next patch" |
| if tc.release.Patch == 0 { |
| versionBump = "next minor" |
| } |
| t.Run("interpret version "+versionBump+" : "+tc.name, func(t *testing.T) { |
| runTestWithInput(map[string]any{ |
| releaseCoordinatorsParam.Name: []string{"gobot"}, |
| "explicit version (optional)": "", |
| "next version": versionBump, |
| }) |
| }) |
| } |
| } |
| |
| func TestTagRelease(t *testing.T) { |
| ctx := context.Background() |
| testcases := []struct { |
| name string |
| tags []string |
| release releaseVersion |
| prerelease string |
| wantErr bool |
| }{ |
| { |
| name: "should add the release tag v0.1.0 to the commit with tag v0.1.0-pre.2", |
| tags: []string{ |
| "gopls/v0.1.0-pre.1", |
| "gopls/v0.1.0-pre.2", |
| }, |
| release: releaseVersion{Major: 0, Minor: 1, Patch: 0}, |
| prerelease: "pre.2", |
| wantErr: false, |
| }, |
| { |
| name: "should add the release tag v0.12.0 to the commit with tag v0.12.0-pre.1", |
| tags: []string{ |
| "gopls/v0.12.0-pre.1", |
| "gopls/v0.12.0-pre.2", |
| }, |
| release: releaseVersion{Major: 0, Minor: 12, Patch: 0}, |
| prerelease: "pre.1", |
| wantErr: false, |
| }, |
| { |
| name: "should error if the pre-release tag does not exist", |
| tags: []string{ |
| "gopls/v0.12.0-pre.1", |
| "gopls/v0.12.0-pre.2", |
| }, |
| release: releaseVersion{Major: 0, Minor: 12, Patch: 0}, |
| prerelease: "pre.3", |
| wantErr: true, |
| }, |
| } |
| |
| for _, tc := range testcases { |
| t.Run(tc.name, func(t *testing.T) { |
| tools := NewFakeRepo(t, "tools") |
| _ = tools.Commit(map[string]string{ |
| "go.mod": "module golang.org/x/tools\n", |
| "go.sum": "\n", |
| }) |
| |
| for i, tag := range tc.tags { |
| commit := tools.Commit(map[string]string{ |
| "README.md": fmt.Sprintf("THIS IS READ ME FOR %v.", i), |
| }) |
| tools.Tag(tag, commit) |
| } |
| |
| tasks := &ReleaseGoplsTasks{ |
| Gerrit: NewFakeGerrit(t, tools), |
| } |
| |
| err := tasks.tagRelease(&workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}, tc.release, tc.prerelease) |
| |
| if tc.wantErr && err == nil { |
| t.Errorf("tagRelease(%q) should return error but return nil", tc.release) |
| } else if !tc.wantErr && err != nil { |
| t.Errorf("tagRelease(%q) should return nil but return err: %v", tc.release, err) |
| } |
| |
| if !tc.wantErr { |
| releaseTag := fmt.Sprintf("gopls/%s", tc.release) |
| release, err := tasks.Gerrit.GetTag(ctx, "tools", releaseTag) |
| if err != nil { |
| t.Errorf("release tag %q should be added after tagRelease(%q): %v", releaseTag, tc.release, err) |
| } |
| |
| prereleaseTag := fmt.Sprintf("gopls/%s", tc.release) |
| prerelease, err := tasks.Gerrit.GetTag(ctx, "tools", prereleaseTag) |
| if err != nil { |
| t.Fatalf("failed to get tag %q: %v", prereleaseTag, err) |
| } |
| |
| // verify the release tag and the input pre-release tag point to the same |
| // commit. |
| if release.Revision != prerelease.Revision { |
| t.Errorf("tagRelease(%s) add the release tag to commit %s, but should add to commit %s", tc.release, prerelease.Revision, release.Revision) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestExecuteAndMonitorChange(t *testing.T) { |
| mustHaveShell(t) |
| |
| testcases := []struct { |
| name string |
| branch string |
| script string |
| watch []string |
| want map[string]string |
| }{ |
| { |
| name: "write all three files with different content", |
| branch: "master", |
| script: `echo -n "foo" > file_a |
| echo -n "foo" > file_b |
| echo -n "foo" > file_c |
| `, |
| watch: []string{"file_a", "file_b", "file_c"}, |
| want: map[string]string{"file_a": "foo", "file_b": "foo", "file_c": "foo"}, |
| }, |
| { |
| name: "ignore file_c changes", |
| branch: "master", |
| script: `echo -n "foo" > file_a |
| echo -n "foo" > file_b |
| echo -n "foo" > file_c |
| `, |
| watch: []string{"file_a", "file_b"}, |
| want: map[string]string{"file_a": "foo", "file_b": "foo"}, |
| }, |
| { |
| name: "write two files with different content", |
| branch: "master", |
| script: `echo -n "foo" > file_a |
| echo -n "foo" > file_b |
| `, |
| watch: []string{"file_a", "file_b", "file_c"}, |
| want: map[string]string{"file_a": "foo", "file_b": "foo"}, |
| }, |
| { |
| name: "write one file with different content in foo branch", |
| branch: "foo", |
| script: `echo -n "foo" > file_a`, |
| watch: []string{"file_a", "file_b", "file_c"}, |
| want: map[string]string{"file_a": "foo"}, |
| }, |
| { |
| name: "create a file in foo branch", |
| branch: "foo", |
| script: `echo -n "foo" > file_d`, |
| watch: []string{"file_a", "file_b", "file_c", "file_d"}, |
| want: map[string]string{"file_d": "foo"}, |
| }, |
| } |
| |
| for _, tc := range testcases { |
| t.Run(tc.name, func(t *testing.T) { |
| tools := NewFakeRepo(t, "tools") |
| initial := tools.Commit(map[string]string{ |
| "gopls/go.mod": "module golang.org/x/tools\n", |
| "gopls/go.sum": "\n", |
| "file_a": "file_a", |
| "file_b": "file_b", |
| "file_c": "file_c", |
| }) |
| if tc.branch != "master" { |
| tools.Branch(tc.branch, initial) |
| } |
| |
| cloudBuild := NewFakeCloudBuild(t, NewFakeGerrit(t, tools), "", nil) |
| ctx := &workflow.TaskContext{ |
| Context: context.Background(), |
| Logger: &testLogger{t, ""}, |
| } |
| got, err := executeAndMonitorChange(ctx, cloudBuild, "tools", tc.branch, tc.script, tc.watch) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if !reflect.DeepEqual(got, tc.want) { |
| t.Errorf("executeAndMonitorChange() = %v want = %v", got, tc.want) |
| } |
| }) |
| } |
| } |
| |
| func TestGoplsReleaseFlow(t *testing.T) { |
| if testing.Short() { |
| t.Skip("skipping an end-to-end workflow test in short mode") |
| } |
| mustHaveShell(t) |
| |
| testcases := []struct { |
| name string |
| // The fields below are the prepared states before running the gopls |
| // release flow. |
| // commitTags specifies a sequence of (possibly) tagged commits. |
| // For each entry, a new commit is created, and if the entry is |
| // non empty that commit is tagged with the entry value. |
| commitTags []string |
| // goplsGoMod specifies the content of gopls/go.mod in x/tools' master |
| // branch before the flow execution. |
| goplsGoMod string |
| release releaseVersion |
| |
| // The fields below are the desired states. |
| // wantPrereleaseTag is the expected prerelease tag in x/tools repo |
| // associated with the final release tag. |
| wantPrereleaseTag string |
| // wantCommit controls whether a commit should be made to a particular |
| // branch in a specified repository. |
| wantCommit map[string]map[string]bool |
| // wantGoplsGoMod specifies the desired content of gopls/go.mod in x/tools' |
| // master branch after the flow execution. |
| wantGoplsGoMod string |
| }{ |
| { |
| name: "release patch v0.16.3-pre.3, update vscode-go release and master branch", |
| commitTags: []string{"gopls/v0.16.2", "gopls/v0.16.3-pre.1", "gopls/v0.16.3-pre.2", "gopls/v0.16.3-pre.3"}, |
| goplsGoMod: "foo", |
| release: releaseVersion{Major: 0, Minor: 16, Patch: 3}, |
| wantPrereleaseTag: "gopls/v0.16.3-pre.3", |
| wantCommit: map[string]map[string]bool{ |
| "tools": { |
| "master": false, "gopls-release-branch.0.16": false, |
| }, |
| "vscode-go": { |
| "master": true, "release-v0.44": true, |
| }, |
| }, |
| wantGoplsGoMod: "foo", |
| }, |
| { |
| name: "release minor v0.17.0-pre.2, update vscode-go release and master branch, update tools gopls go.mod", |
| commitTags: []string{"gopls/v0.16.0", "gopls/v0.17.0-pre.1", "gopls/v0.17.0-pre.2"}, |
| goplsGoMod: "foo", |
| release: releaseVersion{Major: 0, Minor: 17, Patch: 0}, |
| wantPrereleaseTag: "gopls/v0.17.0-pre.2", |
| wantCommit: map[string]map[string]bool{ |
| "tools": { |
| "master": true, "gopls-release-branch.0.17": false, |
| }, |
| "vscode-go": { |
| "master": true, "release-v0.44": true, |
| }, |
| }, |
| wantGoplsGoMod: "bar", |
| }, |
| { |
| name: "release minor v0.17.0-pre.2, update vscode-go release and master branch, skip update tools gopls go.mod", |
| commitTags: []string{"gopls/v0.16.0", "gopls/v0.17.0-pre.1", "gopls/v0.17.0-pre.2"}, |
| goplsGoMod: "bar", |
| release: releaseVersion{Major: 0, Minor: 17, Patch: 0}, |
| wantPrereleaseTag: "gopls/v0.17.0-pre.2", |
| wantCommit: map[string]map[string]bool{ |
| "tools": { |
| "master": false, "gopls-release-branch.0.17": false, |
| }, |
| "vscode-go": { |
| "master": true, "release-v0.44": true, |
| }, |
| }, |
| wantGoplsGoMod: "bar", |
| }, |
| } |
| |
| for _, tc := range testcases { |
| runTestWithInput := func(input map[string]any) { |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| releaseBranch := goplsReleaseBranchName(tc.release) |
| |
| vscodego := NewFakeRepo(t, "vscode-go") |
| initial := vscodego.Commit(map[string]string{ |
| "extension/src/goToolsInformation.ts": "foo", // arbitrary initial contents, to be mutated by the fakeGo script below |
| }) |
| vscodego.Branch("release-v0.44", initial) |
| |
| tools := NewFakeRepo(t, "tools") |
| initial = tools.Commit(map[string]string{ |
| "gopls/go.mod": tc.goplsGoMod, |
| "gopls/go.sum": "\n", |
| }) |
| tools.Branch(releaseBranch, initial) |
| for i, tag := range tc.commitTags { |
| commit := tools.CommitOnBranch(releaseBranch, map[string]string{ |
| "README.md": fmt.Sprintf("THIS IS READ ME FOR %v.", i), |
| }) |
| if tag != "" { |
| tools.Tag(tag, commit) |
| } |
| } |
| |
| gerrit := NewFakeGerrit(t, tools, vscodego) |
| |
| // Var before records the initial state of the branch head prior to |
| // executing the release flow. |
| before := map[string]map[string]string{} |
| for repo := range tc.wantCommit { |
| if _, ok := before[repo]; !ok { |
| before[repo] = map[string]string{} |
| } |
| for branch := range tc.wantCommit[repo] { |
| commit, err := gerrit.ReadBranchHead(ctx, repo, branch) |
| if err != nil { |
| t.Fatal(err) |
| } |
| before[repo][branch] = commit |
| } |
| } |
| |
| // fakeGo handles multiple arguments in gopls release flow: |
| // - go get will write "bar" content to gopls/go.mod in x/tools master |
| // branch to simulate the dependency upgrade. |
| // - go mod will exit without error. |
| // - go run will write "bar" content to file in vscode-go project |
| // containing gopls versions. |
| var fakeGo = fmt.Sprintf(`#!/bin/bash -exu |
| |
| case "$1" in |
| "get") |
| echo -n "bar" > go.mod |
| exit 0 |
| ;; |
| "mod") |
| exit 0 |
| ;; |
| "run") |
| # Only update the goToolsInformation.ts if runs generate.go. |
| for param in "$@:2"; do |
| if [[ $param == *"generate.go"* ]]; then |
| echo -n "bar" > extension/src/goToolsInformation.ts |
| fi |
| done |
| exit 0 |
| ;; |
| *) |
| echo unexpected command $@ |
| exit 1 |
| ;; |
| esac |
| `) |
| |
| var gotSubject string // subject of the announcement email that was sent |
| |
| tasks := &ReleaseGoplsTasks{ |
| Gerrit: gerrit, |
| CloudBuild: NewFakeCloudBuild(t, gerrit, "", nil, FakeBinary{Name: "go", Implementation: fakeGo}), |
| Github: &FakeGitHub{ |
| Milestones: map[int]string{ |
| 1: fmt.Sprintf("gopls/%s", tc.release), |
| }, |
| Issues: map[int]*github.Issue{ |
| 1: { |
| Number: github.Int(1), |
| Title: github.String(fmt.Sprintf("x/tools/gopls: release version %s", tc.release)), |
| Milestone: &github.Milestone{ID: github.Int64(1)}, |
| }, |
| }, |
| }, |
| SendMail: func(h MailHeader, c MailContent) error { |
| gotSubject = c.Subject |
| return nil |
| }, |
| ApproveAction: func(tc *workflow.TaskContext) error { return nil }, |
| } |
| |
| wd := tasks.NewReleaseDefinition() |
| w, err := workflow.Start(wd, input) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = w.Run(ctx, &verboseListener{t: t}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Verify that the expected commits were made to each repository's branches. |
| // Ensure no unexpected commits were merged. |
| for repo := range tc.wantCommit { |
| for branch, wantCommit := range tc.wantCommit[repo] { |
| afterHead, err := gerrit.ReadBranchHead(ctx, repo, branch) |
| if err != nil { |
| t.Fatal(err) |
| } |
| beforeHead := before[repo][branch] |
| |
| // The branch head commit should not change after the process runs. |
| if !wantCommit { |
| if afterHead != beforeHead { |
| t.Errorf("repo %s branch %s should not have any commit, before head %s, after head %s", repo, branch, beforeHead, afterHead) |
| } |
| continue |
| } |
| |
| // The branch head should advance to the next commit. |
| var gitHistory []string |
| switch repo { |
| case "tools": |
| gitHistory = tools.History() |
| case "vscode-go": |
| gitHistory = vscodego.History() |
| default: |
| t.Fatal(fmt.Errorf("unexpected repo name %q", repo)) |
| } |
| var beforeIndex, afterIndex int |
| for i, commit := range gitHistory { |
| if commit == beforeHead { |
| beforeIndex = i |
| } |
| if commit == afterHead { |
| afterIndex = i |
| } |
| } |
| |
| if beforeIndex-afterIndex != 1 { |
| t.Errorf("the repo %s branch %s should have exactly one commit merged, before head %s, after head %s, history %v", repo, branch, beforeHead, afterHead, gitHistory) |
| } |
| } |
| } |
| |
| // Verify the release tag exists and matches the expected prerelease tag. |
| wantCommit, err := gerrit.GetTag(ctx, "tools", tc.wantPrereleaseTag) |
| if err != nil { |
| t.Fatalf("can not get the commit for tag %s", tc.wantPrereleaseTag) |
| } |
| gotCommit, err := gerrit.GetTag(ctx, "tools", fmt.Sprintf("gopls/%s", tc.release)) |
| if err != nil { |
| t.Errorf("can not get the commit for tag %s", fmt.Sprintf("gopls/%s", tc.release)) |
| } |
| if wantCommit.Revision != gotCommit.Revision { |
| t.Errorf("the flow create release tag upon commit %s, but should tag on commit %s which have tag %s", gotCommit.Revision, wantCommit.Revision, tc.wantPrereleaseTag) |
| } |
| |
| // Verify the content of following files are expected. |
| contentChecks := []struct { |
| repo string |
| branch string |
| path string |
| want string |
| }{ |
| { |
| repo: "tools", |
| branch: "master", |
| path: "gopls/go.mod", |
| want: tc.wantGoplsGoMod, |
| }, |
| { |
| repo: "vscode-go", |
| branch: "master", |
| path: "extension/src/goToolsInformation.ts", |
| want: "bar", |
| }, |
| { |
| repo: "vscode-go", |
| branch: "release-v0.44", |
| path: "extension/src/goToolsInformation.ts", |
| want: "bar", |
| }, |
| } |
| for _, check := range contentChecks { |
| commit, err := gerrit.ReadBranchHead(ctx, check.repo, check.branch) |
| if err != nil { |
| t.Fatal(err) |
| } |
| got, err := gerrit.ReadFile(ctx, check.repo, commit, check.path) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if string(got) != check.want { |
| t.Errorf("Content of %q = %q, want %q", check.path, got, check.want) |
| } |
| } |
| |
| if wantSubject := fmt.Sprintf("Gopls %s is released", tc.release); gotSubject != wantSubject { |
| // The full email content is checked by TestAnnouncementMail. |
| t.Errorf("NewReleaseDefinition().Run(): got email subject %q, want %q", gotSubject, wantSubject) |
| } |
| } |
| t.Run("manual input version: "+tc.name, func(t *testing.T) { |
| runTestWithInput(map[string]any{ |
| releaseCoordinatorsParam.Name: []string{"gobot"}, |
| "explicit version (optional)": tc.release.String(), |
| "next version": "use explicit version", |
| }) |
| }) |
| versionBump := "next patch" |
| if tc.release.Patch == 0 { |
| versionBump = "next minor" |
| } |
| t.Run("interpret version "+versionBump+": "+tc.name, func(t *testing.T) { |
| runTestWithInput(map[string]any{ |
| releaseCoordinatorsParam.Name: []string{"gobot"}, |
| "explicit version (optional)": "", |
| "next version": versionBump, |
| }) |
| }) |
| } |
| } |