| // Copyright 2020 Google Inc. All Rights Reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package main | |
| import ( | |
| "bytes" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "io/ioutil" | |
| "os" | |
| "os/exec" | |
| "testing" | |
| ) | |
| const ( | |
| gerritURL = "https://cos-review.googlesource.com" | |
| fallbackURLs = "https://chromium-review.googlesource.com" | |
| gitilesURL = "cos.googlesource.com" | |
| manifestRepo = "cos/manifest-snapshots" | |
| ) | |
| var ( | |
| repoLogFields = []string{"Commits", "InstanceURL", "Repo", "SourceSHA", "TargetSHA", "HasMoreCommits"} | |
| commitFields = []string{"SHA", "AuthorName", "CommitterName", "Subject", "Bugs", "ReleaseNote", "CommitTime"} | |
| ) | |
| type Commit struct { | |
| SHA string | |
| AuthorName string | |
| CommitterName string | |
| Subject string | |
| Bugs []string | |
| CommitTime string | |
| ReleaseNote string | |
| } | |
| func setup() error { | |
| // check if binary exists in the desired location.If not, compile from source. | |
| _, err := os.Stat("changelogctl") | |
| if errors.Is(err, os.ErrNotExist) { | |
| cmd := exec.Command("go", "build", "-o", "changelogctl", "main.go") | |
| return cmd.Run() | |
| } | |
| return err | |
| } | |
| func cleanOutputFiles(source, target string) { | |
| additions := filename(source, target) | |
| removals := filename(target, source) | |
| cmd := exec.Command("rm", additions, removals) | |
| cmd.Run() | |
| } | |
| func filename(source, target string) string { | |
| return fmt.Sprintf("%s -> %s.json", source, target) | |
| } | |
| func fileExists(source, target string) bool { | |
| fname := filename(source, target) | |
| info, err := os.Stat(fname) | |
| if os.IsNotExist(err) || info.IsDir() { | |
| return false | |
| } | |
| return true | |
| } | |
| func fileContents(source, target string) []byte { | |
| fname := filename(source, target) | |
| contents, _ := ioutil.ReadFile(fname) | |
| return contents | |
| } | |
| func validateEmptyChangelog(source, target string) bool { | |
| contents := fileContents(source, target) | |
| return string(contents) == "{}" | |
| } | |
| // validateCommit verifies if a given interface matches the commit format | |
| func validateCommit(input interface{}) bool { | |
| commit, ok := input.(map[string]interface{}) | |
| if !ok { | |
| return false | |
| } | |
| for _, field := range commitFields { | |
| if _, ok := commit[field]; !ok { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| func validateRepoLog(input interface{}) bool { | |
| repoLog, ok := input.(map[string]interface{}) | |
| if !ok { | |
| return false | |
| } | |
| for _, field := range repoLogFields { | |
| if _, ok := repoLog[field]; !ok { | |
| return false | |
| } | |
| } | |
| commits, ok := repoLog["Commits"] | |
| if !ok { | |
| return false | |
| } | |
| if _, ok := commits.([]interface{}); !ok { | |
| return false | |
| } | |
| for _, commit := range commits.([]interface{}) { | |
| if !validateCommit(commit) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| func validateChangelogSchema(source, target string) bool { | |
| if validateEmptyChangelog(source, target) { | |
| return false | |
| } | |
| contents := fileContents(source, target) | |
| var data interface{} | |
| err := json.Unmarshal(contents, &data) | |
| if err != nil { | |
| return false | |
| } | |
| if _, ok := data.(map[string]interface{}); !ok { | |
| return false | |
| } | |
| for _, val := range data.(map[string]interface{}) { | |
| if !validateRepoLog(val) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| func TestChangelog(t *testing.T) { | |
| err := setup() | |
| if err != nil { | |
| t.Fatalf("Error compiling main.go:\n%v", err) | |
| } | |
| tests := map[string]struct { | |
| Source string | |
| Target string | |
| Args []string | |
| ShouldErr bool | |
| EmptyAdds bool | |
| EmptyRms bool | |
| }{ | |
| "basic run": { | |
| Source: "15050.0.0", | |
| Target: "15056.0.0", | |
| ShouldErr: false, | |
| EmptyAdds: false, | |
| EmptyRms: true, | |
| }, | |
| "with instance and repo": { | |
| Source: "15048.0.0", | |
| Target: "15049.0.0", | |
| Args: []string{"-gob", gitilesURL, "-r", manifestRepo}, | |
| ShouldErr: false, | |
| EmptyAdds: false, | |
| EmptyRms: true, | |
| }, | |
| "image name": { | |
| Source: "cos-rc-85-13310-1034-0", | |
| Target: "cos-rc-85-13310-1030-0", | |
| ShouldErr: false, | |
| EmptyAdds: true, | |
| EmptyRms: false, | |
| }, | |
| "invalid source": { | |
| Source: "999999.0.0", | |
| Target: "15056.0.0", | |
| ShouldErr: true, | |
| }, | |
| "invalid target": { | |
| Source: "15056.0.0", | |
| Target: "99999.0.0", | |
| ShouldErr: true, | |
| }, | |
| "invalid instance": { | |
| Source: "999999.0.0", | |
| Target: "15056.0.0", | |
| Args: []string{"-gob", "cos.gg.com", "-repo", manifestRepo}, | |
| ShouldErr: true, | |
| }, | |
| "invalid repo": { | |
| Source: "999999.0.0", | |
| Target: "15056.0.0", | |
| Args: []string{"-gob", gitilesURL, "-repo", "not/arepo"}, | |
| ShouldErr: true, | |
| }, | |
| "same source and target": { | |
| Source: "15049.0.0", | |
| Target: "15049.0.0", | |
| Args: []string{"-gob", gitilesURL, "-r", manifestRepo}, | |
| ShouldErr: false, | |
| EmptyAdds: true, | |
| EmptyRms: true, | |
| }, | |
| } | |
| for name, test := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| args := append([]string{"-mode", "changelog"}, test.Args...) | |
| args = append(args, []string{test.Source, test.Target}...) | |
| cmd := exec.Command("./changelogctl", args...) | |
| err := cmd.Run() | |
| if test.ShouldErr { | |
| switch { | |
| case err == nil: | |
| t.Fatalf("expected error, got nil") | |
| case fileExists(test.Source, test.Target): | |
| t.Fatalf("expected no files to be created, got additions file") | |
| case fileExists(test.Target, test.Source): | |
| t.Fatalf("expected no files to be created, got removals file") | |
| } | |
| } else { | |
| switch { | |
| case err != nil: | |
| t.Fatalf("expected no error, got %v", err) | |
| case !fileExists(test.Source, test.Target): | |
| t.Fatalf("expected additions file to be created, got no file") | |
| case !fileExists(test.Target, test.Source): | |
| t.Fatalf("expected removals file to be created, got no file") | |
| case test.EmptyAdds && !validateEmptyChangelog(test.Source, test.Target): | |
| t.Fatalf("expected empty additions file, got non-empty file") | |
| case !test.EmptyAdds && !validateChangelogSchema(test.Source, test.Target): | |
| t.Fatalf("expected valid, nonempty additions file, got invalid/empty file") | |
| case test.EmptyRms && !validateEmptyChangelog(test.Target, test.Source): | |
| t.Fatalf("expected empty removals file, got non-empty file") | |
| case !test.EmptyRms && !validateChangelogSchema(test.Target, test.Source): | |
| t.Fatalf("expected valid, nonempty removals file, got invalid/empty file") | |
| } | |
| cleanOutputFiles(test.Source, test.Target) | |
| } | |
| }) | |
| } | |
| } | |
| func TestFindBuild(t *testing.T) { | |
| err := setup() | |
| if err != nil { | |
| t.Fatalf("Error compiling main.go:\n%v", err) | |
| } | |
| tests := map[string]struct { | |
| CL string | |
| Args []string | |
| Output string | |
| ShouldErr bool | |
| }{ | |
| "test basic": { | |
| CL: "3781", | |
| Output: "Build: 12371.1072.0\n", | |
| }, | |
| "test commit SHA": { | |
| CL: "80809c436f1cae4cde117fce34b82f38bdc2fd36", | |
| Output: "Build: 12871.1183.0\n", | |
| }, | |
| "test string flags": { | |
| CL: "3781", | |
| Args: []string{"-gerrit", gerritURL, "-gob", gitilesURL, "-repo", manifestRepo}, | |
| Output: "Build: 12371.1072.0\n", | |
| }, | |
| "invalid gob": { | |
| CL: "2288114", | |
| Args: []string{"-gerrit", gerritURL, "-fallback", fallbackURLs, "-gob", "zop.googlesource.com", "-repo", manifestRepo}, | |
| ShouldErr: true, | |
| }, | |
| "invalid gerrit": { | |
| CL: "3781", | |
| Args: []string{"-gerrit", "https://zop-review.googlesource.com", "-fallback", fallbackURLs, "-gob", gitilesURL, "-repo", manifestRepo}, | |
| ShouldErr: true, | |
| }, | |
| "invalid fallback": { | |
| CL: "2288114", | |
| Args: []string{"-gerrit", gerritURL, "-fallback", "https://zop-review.googlesource.com", "-gob", gitilesURL, "-repo", manifestRepo}, | |
| ShouldErr: true, | |
| }, | |
| "non-existant cl": { | |
| CL: "9999999999999999999999", | |
| ShouldErr: true, | |
| }, | |
| "unsubmitted cl": { | |
| CL: "1540", | |
| ShouldErr: true, | |
| }, | |
| } | |
| for name, test := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| var out bytes.Buffer | |
| args := append([]string{"-mode", "findbuild"}, test.Args...) | |
| args = append(args, test.CL) | |
| cmd := exec.Command("./changelogctl", args...) | |
| cmd.Stdout = &out | |
| err := cmd.Run() | |
| if test.ShouldErr && err == nil { | |
| t.Fatalf("expected error, got nil") | |
| } else if !test.ShouldErr { | |
| switch { | |
| case err != nil: | |
| t.Fatalf("expected no error, got %v", err) | |
| case out.String() != test.Output: | |
| t.Fatalf("expected output %s, got %s", test.Output, out.String()) | |
| } | |
| } | |
| }) | |
| } | |
| } |