| // Copyright 2022 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| package roll |
| |
| import ( |
| "context" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "sort" |
| "strconv" |
| "strings" |
| "sync" |
| "text/tabwriter" |
| "time" |
| |
| commonAuth "dawn.googlesource.com/dawn/tools/src/auth" |
| "dawn.googlesource.com/dawn/tools/src/buildbucket" |
| "dawn.googlesource.com/dawn/tools/src/cmd/cts/common" |
| "dawn.googlesource.com/dawn/tools/src/container" |
| "dawn.googlesource.com/dawn/tools/src/cts/expectations" |
| "dawn.googlesource.com/dawn/tools/src/cts/query" |
| "dawn.googlesource.com/dawn/tools/src/cts/result" |
| "dawn.googlesource.com/dawn/tools/src/fileutils" |
| "dawn.googlesource.com/dawn/tools/src/gerrit" |
| "dawn.googlesource.com/dawn/tools/src/git" |
| "dawn.googlesource.com/dawn/tools/src/gitiles" |
| "dawn.googlesource.com/dawn/tools/src/glob" |
| "dawn.googlesource.com/dawn/tools/src/resultsdb" |
| "go.chromium.org/luci/auth" |
| "go.chromium.org/luci/auth/client/authcli" |
| "google.golang.org/api/sheets/v4" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/status" |
| ) |
| |
| func init() { |
| common.Register(&cmd{}) |
| } |
| |
| const ( |
| depsRelPath = "DEPS" |
| gitLinkPath = "third_party/webgpu-cts" |
| webTestsPath = "webgpu-cts/webtests" |
| refMain = "refs/heads/main" |
| noExpectations = `# Clear all expectations to obtain full list of results` |
| ) |
| |
| type rollerFlags struct { |
| gitPath string |
| npmPath string |
| nodePath string |
| auth authcli.Flags |
| cacheDir string |
| ctsGitURL string |
| ctsRevision string |
| force bool // Create a new roll, even if CTS is up to date |
| rebuild bool // Rebuild the expectations file from scratch |
| preserve bool // If false, abandon past roll changes |
| sendToGardener bool // If true, automatically send to the gardener for review |
| verbose bool |
| parentSwarmingRunID string |
| maxAttempts int |
| } |
| |
| type cmd struct { |
| flags rollerFlags |
| } |
| |
| func (cmd) Name() string { |
| return "roll" |
| } |
| |
| func (cmd) Desc() string { |
| return "roll CTS and re-generate expectations" |
| } |
| |
| func (c *cmd) RegisterFlags(ctx context.Context, cfg common.Config) ([]string, error) { |
| gitPath, _ := exec.LookPath("git") |
| npmPath, _ := exec.LookPath("npm") |
| c.flags.auth.Register(flag.CommandLine, commonAuth.DefaultAuthOptions(sheets.SpreadsheetsScope)) |
| flag.StringVar(&c.flags.gitPath, "git", gitPath, "path to git") |
| flag.StringVar(&c.flags.npmPath, "npm", npmPath, "path to npm") |
| flag.StringVar(&c.flags.nodePath, "node", fileutils.NodePath(), "path to node") |
| flag.StringVar(&c.flags.cacheDir, "cache", common.DefaultCacheDir, "path to the results cache") |
| flag.StringVar(&c.flags.ctsGitURL, "repo", cfg.Git.CTS.HttpsURL(), "the CTS source repo") |
| flag.StringVar(&c.flags.ctsRevision, "revision", refMain, "revision of the CTS to roll") |
| flag.BoolVar(&c.flags.force, "force", false, "create a new roll, even if CTS is up to date") |
| flag.BoolVar(&c.flags.rebuild, "rebuild", false, "rebuild the expectation file from scratch") |
| flag.BoolVar(&c.flags.preserve, "preserve", false, "do not abandon existing rolls") |
| flag.BoolVar(&c.flags.sendToGardener, "send-to-gardener", false, "send the CL to the WebGPU gardener for review") |
| flag.BoolVar(&c.flags.verbose, "verbose", false, "emit additional logging") |
| flag.StringVar(&c.flags.parentSwarmingRunID, "parent-swarming-run-id", "", "parent swarming run id. All triggered tasks will be children of this task and will be canceled if the parent is canceled.") |
| flag.IntVar(&c.flags.maxAttempts, "max-attempts", 3, "number of update attempts before giving up") |
| return nil, nil |
| } |
| |
| func (c *cmd) Run(ctx context.Context, cfg common.Config) error { |
| // Validate command line arguments |
| auth, err := c.flags.auth.Options() |
| if err != nil { |
| return fmt.Errorf("failed to obtain authentication options: %w", err) |
| } |
| |
| // Check tools can be found |
| for _, tool := range []struct { |
| name, path, hint string |
| }{ |
| {name: "git", path: c.flags.gitPath}, |
| {name: "npm", path: c.flags.npmPath}, |
| {name: "node", path: c.flags.nodePath}, |
| } { |
| if _, err := os.Stat(tool.path); err != nil { |
| return fmt.Errorf("failed to find path to %v: %v. %v", tool.name, err, tool.hint) |
| } |
| } |
| |
| // Create a temporary directory for local checkouts |
| tmpDir, err := os.MkdirTemp("", "dawn-cts-roll") |
| if err != nil { |
| return err |
| } |
| defer os.RemoveAll(tmpDir) |
| ctsDir := filepath.Join(tmpDir, "cts") |
| |
| // Create the various service clients |
| git, err := git.New(c.flags.gitPath) |
| if err != nil { |
| return fmt.Errorf("failed to obtain authentication options: %w", err) |
| } |
| gerrit, err := gerrit.New(ctx, auth, cfg.Gerrit.Host) |
| if err != nil { |
| return err |
| } |
| dawn, err := gitiles.New(ctx, cfg.Git.Dawn.Host, cfg.Git.Dawn.Project) |
| if err != nil { |
| return err |
| } |
| bb, err := buildbucket.New(ctx, auth) |
| if err != nil { |
| return err |
| } |
| rdb, err := resultsdb.New(ctx, auth) |
| if err != nil { |
| return err |
| } |
| |
| // Construct the roller, and roll |
| r := roller{ |
| cfg: cfg, |
| flags: c.flags, |
| auth: auth, |
| bb: bb, |
| parentSwarmingRunID: c.flags.parentSwarmingRunID, |
| rdb: rdb, |
| git: git, |
| gerrit: gerrit, |
| gitiles: gitilesRepos{dawn: dawn}, |
| ctsDir: ctsDir, |
| } |
| return r.roll(ctx) |
| } |
| |
| type gitilesRepos struct { |
| dawn *gitiles.Gitiles |
| } |
| |
| type roller struct { |
| cfg common.Config |
| flags rollerFlags |
| auth auth.Options |
| bb *buildbucket.Buildbucket |
| parentSwarmingRunID string |
| rdb *resultsdb.ResultsDB |
| git *git.Git |
| gerrit *gerrit.Gerrit |
| gitiles gitilesRepos |
| ctsDir string |
| } |
| |
| func (r *roller) roll(ctx context.Context) error { |
| // Fetch the latest Dawn main revision |
| dawnHash, err := r.gitiles.dawn.Hash(ctx, refMain) |
| if err != nil { |
| return err |
| } |
| |
| // Checkout the CTS at the latest revision |
| ctsRepo, err := r.checkout("cts", r.ctsDir, r.flags.ctsGitURL, r.flags.ctsRevision) |
| if err != nil { |
| return err |
| } |
| |
| // Obtain the target CTS revision hash |
| ctsRevisionLog, err := ctsRepo.Log(&git.LogOptions{From: r.flags.ctsRevision + "^", To: r.flags.ctsRevision}) |
| if err != nil { |
| return err |
| } |
| newCTSHash := ctsRevisionLog[0].Hash.String() |
| |
| // Update the DEPS file |
| updatedDEPS, oldCTSHash, err := r.updateDEPS(ctx, dawnHash, newCTSHash) |
| if err != nil { |
| return err |
| } |
| if newCTSHash == oldCTSHash && !r.flags.force { |
| // Already up to date |
| fmt.Println("CTS is already up to date") |
| return nil |
| } |
| |
| log.Printf("starting CTS roll from %v to %v...", oldCTSHash[:8], newCTSHash[:8]) |
| |
| // Fetch the log of changes between last roll and now |
| ctsLog, err := ctsRepo.Log(&git.LogOptions{From: oldCTSHash, To: newCTSHash}) |
| if err != nil { |
| return err |
| } |
| ctsLog = ctsLog[:len(ctsLog)-1] // Don't include the oldest change in the log |
| |
| type ExpectationsFileInfo struct { |
| path string |
| expectations expectations.Content |
| newExpectations expectations.Content |
| executionMode result.ExecutionMode |
| results result.List |
| } |
| |
| var exInfos = []*ExpectationsFileInfo{ |
| { |
| path: common.RelativeExpectationsPath, |
| executionMode: "core", |
| results: result.List{}, |
| }, |
| { |
| path: common.RelativeCompatExpectationsPath, |
| executionMode: "compat", |
| results: result.List{}, |
| }, |
| } |
| |
| // Download and parse the expectations files |
| for _, exInfo := range exInfos { |
| expectationsFile, err := r.gitiles.dawn.DownloadFile(ctx, refMain, exInfo.path) |
| if err != nil { |
| return err |
| } |
| ex, err := expectations.Parse(exInfo.path, expectationsFile) |
| if err != nil { |
| return fmt.Errorf("failed to load expectations: %v", err) |
| } |
| |
| // If the user requested a full rebuild of the expectations, strip out |
| // everything but comment chunks. |
| if r.flags.rebuild { |
| rebuilt := ex.Clone() |
| rebuilt.Chunks = rebuilt.Chunks[:0] |
| for _, c := range ex.Chunks { |
| if c.IsCommentOnly() { |
| rebuilt.Chunks = append(rebuilt.Chunks, c) |
| } |
| } |
| ex = rebuilt |
| } |
| |
| exInfo.expectations = ex |
| } |
| |
| generatedFiles, err := r.generateFiles(ctx) |
| if err != nil { |
| return err |
| } |
| |
| // Pull out the test list from the generated files |
| testlist := func() []query.Query { |
| lines := strings.Split(generatedFiles[common.TestListRelPath], "\n") |
| list := make([]query.Query, len(lines)) |
| for i, line := range lines { |
| list[i] = query.Parse(line) |
| } |
| return list |
| }() |
| |
| deletedFiles := []string{} |
| if currentWebTestFiles, err := r.gitiles.dawn.ListFiles(ctx, dawnHash, webTestsPath); err != nil { |
| // If there's an error, allow NotFound. It means the directory did not exist, so no files |
| // need to be deleted. |
| if e, ok := status.FromError(err); !ok || e.Code() != codes.NotFound { |
| return fmt.Errorf("listing current web tests failed: %v", err) |
| } |
| |
| for _, f := range currentWebTestFiles { |
| // If the file is not generated in this revision, and it is an .html file, |
| // mark it for deletion. |
| if !strings.HasSuffix(f, ".html") { |
| continue |
| } |
| if _, exists := generatedFiles[f]; !exists { |
| deletedFiles = append(deletedFiles, f) |
| } |
| } |
| } |
| |
| // Look for an existing gerrit change to update |
| existingRolls, err := r.findExistingRolls() |
| if err != nil { |
| return err |
| } |
| |
| // Abandon existing rolls, if -preserve is false |
| if !r.flags.preserve && len(existingRolls) > 0 { |
| log.Printf("abandoning %v existing roll...", len(existingRolls)) |
| for _, change := range existingRolls { |
| if err := r.gerrit.Abandon(change.ChangeID); err != nil { |
| return err |
| } |
| } |
| existingRolls = nil |
| } |
| |
| // Create a new gerrit change, if needed |
| changeID := "" |
| if r.flags.preserve || len(existingRolls) == 0 { |
| msg := r.rollCommitMessage(oldCTSHash, newCTSHash, ctsLog, "") |
| change, err := r.gerrit.CreateChange(r.cfg.Gerrit.Project, "main", msg, true) |
| if err != nil { |
| return err |
| } |
| changeID = change.ID |
| log.Printf("created gerrit change %v (%v)...", change.Number, change.URL) |
| } else { |
| changeID = existingRolls[0].ID |
| log.Printf("reusing existing gerrit change %v (%v)...", existingRolls[0].Number, existingRolls[0].URL) |
| } |
| |
| // Update the DEPS, expectations, and other generated files. |
| for _, exInfo := range exInfos { |
| updateExpectationUpdateTimestamp(&exInfo.expectations) |
| generatedFiles[exInfo.path] = exInfo.expectations.String() |
| } |
| generatedFiles[depsRelPath] = updatedDEPS |
| generatedFiles[gitLinkPath] = newCTSHash |
| |
| msg := r.rollCommitMessage(oldCTSHash, newCTSHash, ctsLog, changeID) |
| ps, err := r.gerrit.EditFiles(changeID, msg, generatedFiles, deletedFiles) |
| if err != nil { |
| return fmt.Errorf("failed to update change '%v': %v", changeID, err) |
| } |
| |
| var psResultsByExecutionMode result.ResultsByExecutionMode |
| |
| defer func() { |
| // Export the results to the Google Sheets whether the roll succeeded or failed. |
| if psResultsByExecutionMode != nil { |
| log.Println("exporting results...") |
| if err := common.Export(ctx, r.auth, r.cfg.Sheets.ID, r.ctsDir, r.flags.nodePath, r.flags.npmPath, psResultsByExecutionMode); err != nil { |
| log.Println("failed to update results spreadsheet: ", err) |
| } |
| } |
| }() |
| |
| // Begin main roll loop |
| for attempt := 0; ; attempt++ { |
| // Kick builds |
| log.Printf("building (pass %v)...\n", attempt+1) |
| builds, err := common.GetOrStartBuildsAndWait(ctx, r.cfg, ps, r.bb, r.parentSwarmingRunID, false) |
| if err != nil { |
| return err |
| } |
| |
| // Look to see if any of the builds failed |
| failingBuilds := []string{} |
| for id, build := range builds { |
| if build.Status != buildbucket.StatusSuccess { |
| failingBuilds = append(failingBuilds, id) |
| } |
| } |
| if len(failingBuilds) > 0 { |
| sort.Strings(failingBuilds) |
| log.Println("builds failed: ", failingBuilds) |
| } |
| |
| // Gather the build results |
| log.Println("gathering results...") |
| psResultsByExecutionMode, err = common.CacheResults(ctx, r.cfg, ps, r.flags.cacheDir, r.rdb, builds) |
| if err != nil { |
| return err |
| } |
| |
| // If all the builds attempted, and we updated the expectations at least once, then we're done! |
| if attempt > 0 && len(failingBuilds) == 0 { |
| break |
| } |
| |
| // Rebuild the expectations with the accumulated results |
| log.Println("building new expectations...") |
| for _, exInfo := range exInfos { |
| // Merge the new results into the accumulated results |
| log.Printf("merging results for %s ...\n", exInfo.executionMode) |
| exInfo.results = result.Merge(exInfo.results, psResultsByExecutionMode[exInfo.executionMode]) |
| |
| exInfo.newExpectations = exInfo.expectations.Clone() |
| diags, err := exInfo.newExpectations.Update(exInfo.results, testlist, r.flags.verbose) |
| if err != nil { |
| return err |
| } |
| |
| // Post statistics and expectation diagnostics |
| log.Printf("posting stats & diagnostics for %s...\n", exInfo.executionMode) |
| if err := r.postComments(ps, exInfo.path, diags, exInfo.results); err != nil { |
| return err |
| } |
| } |
| |
| // Otherwise, push the updated expectations, and try again |
| log.Println("updating expectations...") |
| |
| editedFiles := map[string]string{} |
| for _, exInfo := range exInfos { |
| updateExpectationUpdateTimestamp(&exInfo.newExpectations) |
| editedFiles[exInfo.path] = exInfo.newExpectations.String() |
| } |
| ps, err = r.gerrit.EditFiles(changeID, msg, editedFiles, nil) |
| if err != nil { |
| return fmt.Errorf("failed to update change '%v': %v", changeID, err) |
| } |
| |
| if attempt >= r.flags.maxAttempts { |
| err := fmt.Errorf("CTS failed after %v retries.\nGiving up", attempt) |
| r.gerrit.Comment(ps, err.Error(), nil) |
| return err |
| } |
| } |
| |
| reviewer := "" |
| if r.flags.sendToGardener { |
| resp, err := http.Get("https://chrome-ops-rotation-proxy.appspot.com/current/grotation:webgpu-gardener") |
| if err != nil { |
| return err |
| } |
| defer resp.Body.Close() |
| |
| jsonResponse, err := ioutil.ReadAll(resp.Body) |
| if err != nil { |
| return err |
| } |
| |
| type StructuredJSONResponse struct { |
| Emails []string |
| } |
| var jsonRes StructuredJSONResponse |
| if err := json.Unmarshal(jsonResponse, &jsonRes); err != nil { |
| return err |
| } |
| if len(jsonRes.Emails) < 1 { |
| return fmt.Errorf("expected at least one email in JSON response %s", jsonRes) |
| } |
| reviewer = jsonRes.Emails[0] |
| } |
| |
| if err := r.gerrit.SetReadyForReview(changeID, "CTS roll succeeded", reviewer); err != nil { |
| return fmt.Errorf("failed to mark change as ready for review: %v", err) |
| } |
| |
| return nil |
| } |
| |
| // Updates the '# Last rolled:' string in the expectations file. |
| func updateExpectationUpdateTimestamp(content *expectations.Content) { |
| prefix := "# Last rolled: " |
| comment := prefix + time.Now().UTC().Format("2006-01-02 03:04:05PM") |
| for _, chunk := range content.Chunks { |
| for l, line := range chunk.Comments { |
| if strings.HasPrefix(line, prefix) { |
| chunk.Comments[l] = comment |
| return |
| } |
| } |
| } |
| newChunks := []expectations.Chunk{} |
| if len(content.Chunks) > 0 { |
| newChunks = append(newChunks, |
| content.Chunks[0], |
| expectations.Chunk{}, |
| ) |
| } |
| newChunks = append(newChunks, |
| expectations.Chunk{Comments: []string{comment}}, |
| ) |
| if len(content.Chunks) > 0 { |
| newChunks = append(newChunks, content.Chunks[1:]...) |
| } |
| |
| content.Chunks = newChunks |
| } |
| |
| // rollCommitMessage returns the commit message for the roll |
| func (r *roller) rollCommitMessage( |
| oldCTSHash, newCTSHash string, |
| ctsLog []git.CommitInfo, |
| changeID string) string { |
| |
| isExternalRepo := r.flags.ctsGitURL != r.cfg.Git.CTS.HttpsURL() |
| |
| msg := &strings.Builder{} |
| if isExternalRepo { |
| // note: intentionally split to fool the pre-submit checks! |
| msg.WriteString("[DO NOT") |
| msg.WriteString(" SUBMIT] ") |
| } |
| msg.WriteString(common.RollSubjectPrefix) |
| msg.WriteString(oldCTSHash[:9]) |
| msg.WriteString("..") |
| msg.WriteString(newCTSHash[:9]) |
| msg.WriteString(" (") |
| msg.WriteString(strconv.Itoa(len(ctsLog))) |
| if len(ctsLog) == 1 { |
| msg.WriteString(" commit)") |
| } else { |
| msg.WriteString(" commits)") |
| } |
| msg.WriteString("\n\n") |
| if isExternalRepo { |
| msg.WriteString("Rolled from external repo: ") |
| msg.WriteString(r.flags.ctsGitURL) |
| msg.WriteString("\n\n") |
| } |
| msg.WriteString("Regenerated:\n") |
| msg.WriteString(" - expectations.txt\n") |
| msg.WriteString(" - compat-expectations.txt\n") |
| msg.WriteString(" - ts_sources.txt\n") |
| msg.WriteString(" - test_list.txt\n") |
| msg.WriteString(" - resource_files.txt\n") |
| msg.WriteString(" - webtest .html files\n") |
| msg.WriteString("\n\n") |
| msg.WriteString("https://chromium.googlesource.com/external/github.com/gpuweb/cts/+log/") |
| msg.WriteString(oldCTSHash[:12]) |
| msg.WriteString("..") |
| msg.WriteString(newCTSHash[:12]) |
| msg.WriteString("\n") |
| for _, change := range ctsLog { |
| msg.WriteString(" - ") |
| msg.WriteString(change.Hash.String()[:6]) |
| msg.WriteString(" ") |
| msg.WriteString(change.Subject) |
| msg.WriteString("\n") |
| } |
| msg.WriteString("\n") |
| msg.WriteString("Created with './tools/run cts roll'") |
| msg.WriteString("\n") |
| msg.WriteString("\n") |
| if len(r.cfg.Builders) > 0 { |
| msg.WriteString("Cq-Include-Trybots: ") |
| buildersByBucket := container.NewMap[string, []string]() |
| for _, build := range r.cfg.Builders { |
| key := fmt.Sprintf("luci.%v.%v", build.Project, build.Bucket) |
| buildersByBucket[key] = append(buildersByBucket[key], build.Builder) |
| } |
| first := true |
| for _, bucket := range buildersByBucket.Keys() { |
| // Cq-Include-Trybots: luci.chromium.try:win-dawn-rel;luci.dawn.try:mac-dbg,mac-rel |
| if !first { |
| msg.WriteString(";") |
| } |
| first = false |
| msg.WriteString(bucket) |
| msg.WriteString(":") |
| builders := buildersByBucket[bucket] |
| sort.Strings(builders) |
| msg.WriteString(strings.Join(builders, ",")) |
| } |
| msg.WriteString("\n") |
| } |
| msg.WriteString("Include-Ci-Only-Tests: true\n") |
| if isExternalRepo { |
| msg.WriteString("Commit: false\n") |
| } |
| if changeID != "" { |
| msg.WriteString("Change-Id: ") |
| msg.WriteString(changeID) |
| msg.WriteString("\n") |
| } |
| |
| return msg.String() |
| } |
| |
| func (r *roller) postComments(ps gerrit.Patchset, path string, diags []expectations.Diagnostic, results result.List) error { |
| fc := make([]gerrit.FileComment, len(diags)) |
| for i, d := range diags { |
| var prefix string |
| switch d.Severity { |
| case expectations.Error: |
| prefix = "🟥" |
| case expectations.Warning: |
| prefix = "🟨" |
| case expectations.Note: |
| prefix = "🟦" |
| } |
| fc[i] = gerrit.FileComment{ |
| Path: path, |
| Side: gerrit.Left, |
| Line: d.Line, |
| Message: fmt.Sprintf("%v %v: %v", prefix, d.Severity, d.Message), |
| } |
| } |
| |
| sb := &strings.Builder{} |
| |
| { |
| sb.WriteString("Tests by status:\n") |
| counts := map[result.Status]int{} |
| for _, r := range results { |
| counts[r.Status] = counts[r.Status] + 1 |
| } |
| type StatusCount struct { |
| status result.Status |
| count int |
| } |
| statusCounts := []StatusCount{} |
| for s, n := range counts { |
| if n > 0 { |
| statusCounts = append(statusCounts, StatusCount{s, n}) |
| } |
| } |
| sort.Slice(statusCounts, func(i, j int) bool { return statusCounts[i].status < statusCounts[j].status }) |
| sb.WriteString("```\n") |
| tw := tabwriter.NewWriter(sb, 0, 1, 0, ' ', 0) |
| for _, sc := range statusCounts { |
| fmt.Fprintf(tw, "%v:\t %v\n", sc.status, sc.count) |
| } |
| tw.Flush() |
| sb.WriteString("```\n") |
| } |
| { |
| sb.WriteString("Top 25 slowest tests:\n") |
| sort.Slice(results, func(i, j int) bool { |
| return results[i].Duration > results[j].Duration |
| }) |
| const N = 25 |
| topN := results |
| if len(topN) > N { |
| topN = topN[:N] |
| } |
| sb.WriteString("```\n") |
| for i, r := range topN { |
| fmt.Fprintf(sb, "%3.1d: %v\n", i+1, r) |
| } |
| sb.WriteString("```\n") |
| } |
| |
| if err := r.gerrit.Comment(ps, sb.String(), fc); err != nil { |
| return fmt.Errorf("failed to post stats on change: %v", err) |
| } |
| return nil |
| } |
| |
| // findExistingRolls looks for all existing open CTS rolls by this user |
| func (r *roller) findExistingRolls() ([]gerrit.ChangeInfo, error) { |
| // Look for an existing gerrit change to update |
| changes, _, err := r.gerrit.QueryChanges("owner:me", |
| "is:open", |
| fmt.Sprintf(`repo:"%v"`, r.cfg.Git.Dawn.Project), |
| fmt.Sprintf(`message:"%v"`, common.RollSubjectPrefix)) |
| if err != nil { |
| return nil, fmt.Errorf("failed to find existing roll gerrit changes: %v", err) |
| } |
| return changes, nil |
| } |
| |
| // checkout performs a git checkout of the repo at host to dir at the given hash |
| func (r *roller) checkout(project, dir, host, hash string) (*git.Repository, error) { |
| log.Printf("cloning %v to '%v'...", project, dir) |
| repo, err := r.git.Clone(dir, host, &git.CloneOptions{Timeout: time.Minute * 10}) |
| if err != nil { |
| return nil, fmt.Errorf("failed to clone %v: %v", project, err) |
| } |
| log.Printf("checking out %v @ '%v'...", project, hash) |
| if _, err := repo.Fetch(hash, nil); err != nil { |
| return nil, fmt.Errorf("failed to fetch project %v @ %v: %v", |
| project, hash, err) |
| } |
| if err := repo.Checkout(hash, nil); err != nil { |
| return nil, fmt.Errorf("failed to checkout project %v @ %v: %v", |
| project, hash, err) |
| } |
| return repo, nil |
| } |
| |
| // Call 'npm ci' in the CTS directory, and generates a map of project-relative |
| // file path to file content for the CTS roll's change. This includes: |
| // * type-script source files |
| // * CTS test list |
| // * resource file list |
| // * webtest file sources |
| func (r *roller) generateFiles(ctx context.Context) (map[string]string, error) { |
| // Run 'npm ci' to fetch modules and tsc |
| if err := common.InstallCTSDeps(ctx, r.ctsDir, r.flags.npmPath); err != nil { |
| return nil, err |
| } |
| |
| log.Printf("generating files for changelist...") |
| |
| // Run the below concurrently |
| mutex := sync.Mutex{} |
| files := map[string]string{} // guarded by mutex |
| wg := sync.WaitGroup{} |
| |
| errs := make(chan error, 8) |
| |
| // Generate web tests HTML files |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| if out, err := r.genWebTestSources(ctx); err == nil { |
| mutex.Lock() |
| defer mutex.Unlock() |
| for file, content := range out { |
| files[file] = content |
| } |
| } else { |
| errs <- fmt.Errorf("failed to generate web tests: %v", err) |
| } |
| }() |
| |
| // Generate typescript sources list, test list, resources file list. |
| for relPath, generator := range map[string]func(context.Context) (string, error){ |
| common.TsSourcesRelPath: r.genTSDepList, |
| common.TestListRelPath: r.genTestList, |
| common.ResourceFilesRelPath: r.genResourceFilesList, |
| } { |
| relPath, generator := relPath, generator // Capture values, not iterators |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| if out, err := generator(ctx); err == nil { |
| mutex.Lock() |
| defer mutex.Unlock() |
| files[relPath] = out |
| } else { |
| errs <- fmt.Errorf("failed to generate %v: %v", relPath, err) |
| } |
| }() |
| } |
| |
| // Wait for all the above to complete |
| wg.Wait() |
| close(errs) |
| |
| // Check for errors |
| for err := range errs { |
| return nil, err |
| } |
| |
| return files, nil |
| } |
| |
| // updateDEPS fetches and updates the Dawn DEPS file at 'dawnRef' so that all CTS hashes are changed to newCTSHash |
| func (r *roller) updateDEPS(ctx context.Context, dawnRef, newCTSHash string) (newDEPS, oldCTSHash string, err error) { |
| deps, err := r.gitiles.dawn.DownloadFile(ctx, dawnRef, depsRelPath) |
| if err != nil { |
| return "", "", err |
| } |
| newDEPS, oldCTSHash, err = common.UpdateCTSHashInDeps(deps, r.flags.ctsGitURL, newCTSHash) |
| if err != nil { |
| return "", "", err |
| } |
| |
| return newDEPS, oldCTSHash, nil |
| } |
| |
| // genTSDepList returns a list of source files, for the CTS checkout at r.ctsDir |
| // This list can be used to populate the ts_sources.txt file. |
| // Requires tsc to be found at './node_modules/.bin/tsc' in the CTS directory |
| // (e.g. must be called post 'npm ci') |
| func (r *roller) genTSDepList(ctx context.Context) (string, error) { |
| tscPath := filepath.Join(r.ctsDir, "node_modules/.bin/tsc") |
| if !fileutils.IsExe(tscPath) { |
| return "", fmt.Errorf("tsc not found at '%v'", tscPath) |
| } |
| |
| cmd := exec.CommandContext(ctx, tscPath, "--project", |
| filepath.Join(r.ctsDir, "tsconfig.json"), |
| "--listFiles", |
| "--declaration", "false", |
| "--sourceMap", "false") |
| |
| // Note: we're ignoring the error for this as tsc typically returns status 2. |
| out, _ := cmd.Output() |
| |
| prefix := filepath.ToSlash(r.ctsDir) + "/" |
| |
| deps := []string{} |
| for _, line := range strings.Split(string(out), "\n") { |
| if strings.HasPrefix(line, prefix) { |
| line = line[len(prefix):] |
| if strings.HasPrefix(line, "src/") { |
| deps = append(deps, line) |
| } |
| } |
| } |
| |
| return strings.Join(deps, "\n") + "\n", nil |
| } |
| |
| // genTestList returns the newline delimited list of test names, for the CTS checkout at r.ctsDir |
| func (r *roller) genTestList(ctx context.Context) (string, error) { |
| return common.GenTestList(ctx, r.ctsDir, r.flags.nodePath) |
| } |
| |
| // genResourceFilesList returns a list of resource files, for the CTS checkout at r.ctsDir |
| // This list can be used to populate the resource_files.txt file. |
| func (r *roller) genResourceFilesList(ctx context.Context) (string, error) { |
| dir := filepath.Join(r.ctsDir, "src", "resources") |
| files, err := glob.Glob(filepath.Join(dir, "**")) |
| if err != nil { |
| return "", err |
| } |
| for i, file := range files { |
| file, err := filepath.Rel(dir, file) |
| if err != nil { |
| return "", err |
| } |
| files[i] = file |
| } |
| return strings.Join(files, "\n") + "\n", nil |
| } |
| |
| // genWebTestSources returns a map of generated webtest file names to contents, for the CTS checkout at r.ctsDir |
| func (r *roller) genWebTestSources(ctx context.Context) (map[string]string, error) { |
| generatedFiles := map[string]string{} |
| htmlSearchDir := filepath.Join(r.ctsDir, "src", "webgpu") |
| err := filepath.Walk(htmlSearchDir, |
| func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| if !strings.HasSuffix(info.Name(), ".html") || info.IsDir() { |
| return nil |
| } |
| relPath, err := filepath.Rel(htmlSearchDir, path) |
| if err != nil { |
| return err |
| } |
| |
| data, err := os.ReadFile(path) |
| if err != nil { |
| return err |
| } |
| contents := string(data) |
| |
| // Find the index after the starting html tag. |
| i := strings.Index(contents, "<html") |
| i = i + strings.Index(contents[i:], ">") |
| i = i + 1 |
| |
| // Insert a base tag so the fetched resources will come from the generated CTS JavaScript sources. |
| contents = contents[:i] + "\n" + ` <base ref="/gen/third_party/dawn/webgpu-cts/src/webgpu" />` + contents[i:] |
| |
| generatedFiles[filepath.Join(webTestsPath, relPath)] = contents |
| return nil |
| }) |
| if err != nil { |
| return nil, err |
| } |
| return generatedFiles, nil |
| } |