tools/cts: Generate test list as part of roll
Also parallelize some of the more lengthy file generation steps
Bug: dawn:1479
Change-Id: I7674fca4958e4d9948e287008916c4b0d33e1ca1
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/97022
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/tools/src/cmd/cts/roll/roll.go b/tools/src/cmd/cts/roll/roll.go
index 00c0326..69bebd1 100644
--- a/tools/src/cmd/cts/roll/roll.go
+++ b/tools/src/cmd/cts/roll/roll.go
@@ -15,6 +15,7 @@
package roll
import (
+ "bytes"
"context"
"flag"
"fmt"
@@ -25,6 +26,7 @@
"sort"
"strconv"
"strings"
+ "sync"
"text/tabwriter"
"time"
@@ -33,6 +35,7 @@
"dawn.googlesource.com/dawn/tools/src/container"
"dawn.googlesource.com/dawn/tools/src/cts/expectations"
"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"
@@ -50,6 +53,7 @@
const (
depsRelPath = "DEPS"
tsSourcesRelPath = "third_party/gn/webgpu-cts/ts_sources.txt"
+ testListRelPath = "third_party/gn/webgpu-cts/test_list.txt"
resourceFilesRelPath = "third_party/gn/webgpu-cts/resource_files.txt"
webTestsPath = "webgpu-cts/webtests"
refMain = "refs/heads/main"
@@ -58,7 +62,8 @@
type rollerFlags struct {
gitPath string
- tscPath string
+ npmPath string
+ nodePath string
auth authcli.Flags
cacheDir string
force bool // Create a new roll, even if CTS is up to date
@@ -80,10 +85,12 @@
func (c *cmd) RegisterFlags(ctx context.Context, cfg common.Config) ([]string, error) {
gitPath, _ := exec.LookPath("git")
- tscPath, _ := exec.LookPath("tsc")
+ npmPath, _ := exec.LookPath("npm")
+ nodePath, _ := exec.LookPath("node")
c.flags.auth.Register(flag.CommandLine, common.DefaultAuthOptions())
flag.StringVar(&c.flags.gitPath, "git", gitPath, "path to git")
- flag.StringVar(&c.flags.tscPath, "tsc", tscPath, "path to tsc")
+ flag.StringVar(&c.flags.npmPath, "npm", npmPath, "path to npm")
+ flag.StringVar(&c.flags.nodePath, "node", nodePath, "path to node")
flag.StringVar(&c.flags.cacheDir, "cache", common.DefaultCacheDir, "path to the results cache")
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")
@@ -104,7 +111,8 @@
name, path, hint string
}{
{name: "git", path: c.flags.gitPath},
- {name: "tsc", path: c.flags.tscPath, hint: "Try using '-tsc third_party/webgpu-cts/node_modules/.bin/tsc' after an 'npm ci'."},
+ {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)
@@ -233,24 +241,9 @@
ex = rebuilt
}
- // Map of relative file path to content of generated files
- generatedFiles := map[string]string{}
-
- // Regenerate the typescript dependency list
- tsSources, err := r.genTSDepList(ctx)
+ generatedFiles, err := r.generateFiles(ctx)
if err != nil {
- return fmt.Errorf("failed to generate ts_sources.txt: %v", err)
- }
-
- // Regenerate the resource files list
- resources, err := r.genResourceFilesList(ctx)
- if err != nil {
- return fmt.Errorf("failed to generate resource_files.txt: %v", err)
- }
-
- // Regenerate web tests HTML files
- if err := r.genWebTestSources(ctx, generatedFiles); err != nil {
- return fmt.Errorf("failed to generate web tests: %v", err)
+ return err
}
deletedFiles := []string{}
@@ -299,20 +292,16 @@
return err
}
changeID = change.ID
- log.Printf("created gerrit change %v...", change.Number)
+ log.Printf("created gerrit change %v (%v)...", change.Number, change.URL)
} else {
changeID = existingRolls[0].ID
- log.Printf("reusing existing gerrit change %v...", existingRolls[0].Number)
+ log.Printf("reusing existing gerrit change %v (%v)...", existingRolls[0].Number, existingRolls[0].URL)
}
- // Update the DEPS, and ts-sources file.
- // Update the expectations with the re-formatted content, and updated
- // timestamp.
+ // Update the DEPS, expectations, and other generated files.
updateExpectationUpdateTimestamp(&ex)
generatedFiles[depsRelPath] = updatedDEPS
generatedFiles[common.RelativeExpectationsPath] = ex.String()
- generatedFiles[tsSourcesRelPath] = tsSources
- generatedFiles[resourceFilesRelPath] = resources
msg := r.rollCommitMessage(oldCTSHash, newCTSHash, ctsLog, changeID)
ps, err := r.gerrit.EditFiles(changeID, msg, generatedFiles, deletedFiles)
@@ -601,6 +590,80 @@
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
+ {
+ log.Printf("fetching npm modules with 'npm ci'...")
+ cmd := exec.CommandContext(ctx, r.flags.npmPath, "ci")
+ cmd.Dir = r.ctsDir
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return nil, fmt.Errorf("failed to run 'npm ci': %w\n%v", err, string(out))
+ }
+ }
+
+ 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){
+ tsSourcesRelPath: r.genTSDepList,
+ testListRelPath: r.genTestList,
+ 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 the latest CTS hash.
func (r *roller) updateDEPS(ctx context.Context, dawnRef string) (newDEPS, newCTSHash, oldCTSHash string, err error) {
@@ -622,12 +685,21 @@
// 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) {
- cmd := exec.CommandContext(ctx, r.flags.tscPath, "--project",
+ 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) + "/"
@@ -645,6 +717,39 @@
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) {
+ // Run 'src/common/runtime/cmdline.ts' to obtain the full test list
+ cmd := exec.CommandContext(ctx, r.flags.nodePath,
+ "-e", "require('./src/common/tools/setup-ts-in-node.js');require('./src/common/runtime/cmdline.ts');",
+ "--", // Start of arguments
+ // src/common/runtime/helper/sys.ts expects 'node file.js <args>'
+ // and slices away the first two arguments. When running with '-e', args
+ // start at 1, so just inject a placeholder argument.
+ "placeholder-arg",
+ "--list",
+ "webgpu:*",
+ )
+ cmd.Dir = r.ctsDir
+
+ stderr := bytes.Buffer{}
+ cmd.Stderr = &stderr
+
+ out, err := cmd.Output()
+ if err != nil {
+ return "", fmt.Errorf("failed to generate test list: %w\n%v", err, stderr.String())
+ }
+
+ tests := []string{}
+ for _, test := range strings.Split(string(out), "\n") {
+ if test != "" {
+ tests = append(tests, test)
+ }
+ }
+
+ return strings.Join(tests, "\n"), nil
+}
+
// 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) {
@@ -663,10 +768,11 @@
return strings.Join(files, "\n") + "\n", nil
}
-// genWebTestSources populates a map of generated webtest file names to contents, for the CTS checkout at r.ctsDir
-func (r *roller) genWebTestSources(ctx context.Context, generatedFiles map[string]string) error {
+// 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")
- return filepath.Walk(htmlSearchDir,
+ err := filepath.Walk(htmlSearchDir,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
@@ -696,4 +802,8 @@
generatedFiles[filepath.Join(webTestsPath, relPath)] = contents
return nil
})
+ if err != nil {
+ return nil, err
+ }
+ return generatedFiles, nil
}