Update CTS roller to generate reftest/idl test sources

These sources need to be generated in Dawn's source tree
so that Chromium's //third_party/webgpu-cts DEP can be
removed.

Bug: chromium:1333969
Change-Id: I03c8cba691bcbfac00839f0cdd40fab6198ec83f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/91060
Commit-Queue: Austin Eng <enga@chromium.org>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/tools/src/cmd/cts/roll/roll.go b/tools/src/cmd/cts/roll/roll.go
index 489ef1d..00c0326 100644
--- a/tools/src/cmd/cts/roll/roll.go
+++ b/tools/src/cmd/cts/roll/roll.go
@@ -39,6 +39,8 @@
 	"dawn.googlesource.com/dawn/tools/src/resultsdb"
 	"go.chromium.org/luci/auth"
 	"go.chromium.org/luci/auth/client/authcli"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
 )
 
 func init() {
@@ -49,6 +51,7 @@
 	depsRelPath          = "DEPS"
 	tsSourcesRelPath     = "third_party/gn/webgpu-cts/ts_sources.txt"
 	resourceFilesRelPath = "third_party/gn/webgpu-cts/resource_files.txt"
+	webTestsPath         = "webgpu-cts/webtests"
 	refMain              = "refs/heads/main"
 	noExpectations       = `# Clear all expectations to obtain full list of results`
 )
@@ -230,6 +233,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)
 	if err != nil {
@@ -242,6 +248,31 @@
 		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)
+	}
+
+	deletedFiles := []string{}
+	if currentWebTestFiles, err := r.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 {
@@ -276,15 +307,15 @@
 
 	// Update the DEPS, and ts-sources file.
 	// Update the expectations with the re-formatted content, and updated
-	//timestamp.
+	// timestamp.
 	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, map[string]string{
-		depsRelPath:                     updatedDEPS,
-		common.RelativeExpectationsPath: ex.String(),
-		tsSourcesRelPath:                tsSources,
-		resourceFilesRelPath:            resources,
-	})
+	ps, err := r.gerrit.EditFiles(changeID, msg, generatedFiles, deletedFiles)
 	if err != nil {
 		return fmt.Errorf("failed to update change '%v': %v", changeID, err)
 	}
@@ -349,7 +380,7 @@
 		updateExpectationUpdateTimestamp(&newExpectations)
 		ps, err = r.gerrit.EditFiles(changeID, msg, map[string]string{
 			common.RelativeExpectationsPath: newExpectations.String(),
-		})
+		}, nil)
 		if err != nil {
 			return fmt.Errorf("failed to update change '%v': %v", changeID, err)
 		}
@@ -416,7 +447,11 @@
 		msg.WriteString(" commits)")
 	}
 	msg.WriteString("\n\n")
-	msg.WriteString("Update expectations and ts_sources")
+	msg.WriteString("Update:\n")
+	msg.WriteString(" - expectations.txt\n")
+	msg.WriteString(" - ts_sources.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])
@@ -627,3 +662,38 @@
 	}
 	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 {
+	htmlSearchDir := filepath.Join(r.ctsDir, "src", "webgpu")
+	return 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
+		})
+}
diff --git a/tools/src/gerrit/gerrit.go b/tools/src/gerrit/gerrit.go
index 763574f..23cb0d3 100644
--- a/tools/src/gerrit/gerrit.go
+++ b/tools/src/gerrit/gerrit.go
@@ -168,10 +168,10 @@
 	return change, nil
 }
 
-// EditFiles replaces the content of the files in the given change.
+// EditFiles replaces the content of the files in the given change. It deletes deletedFiles.
 // If newCommitMsg is not an empty string, then the commit message is replaced
 // with the string value.
-func (g *Gerrit) EditFiles(changeID, newCommitMsg string, files map[string]string) (Patchset, error) {
+func (g *Gerrit) EditFiles(changeID, newCommitMsg string, files map[string]string, deletedFiles []string) (Patchset, error) {
 	if newCommitMsg != "" {
 		resp, err := g.client.Changes.ChangeCommitMessageInChangeEdit(changeID, &gerrit.ChangeEditMessageInput{
 			Message: newCommitMsg,
@@ -186,6 +186,12 @@
 			return Patchset{}, g.maybeWrapError(err)
 		}
 	}
+	for _, path := range deletedFiles {
+		resp, err := g.client.Changes.DeleteFileInChangeEdit(changeID, path)
+		if err != nil && resp.StatusCode != 409 { // 409 no changes were made
+			return Patchset{}, g.maybeWrapError(err)
+		}
+	}
 
 	resp, err := g.client.Changes.PublishChangeEdit(changeID, "NONE")
 	if err != nil && resp.StatusCode != 409 { // 409 no changes were made
diff --git a/tools/src/gitiles/gitiles.go b/tools/src/gitiles/gitiles.go
index 1044f3b..199b3b3 100644
--- a/tools/src/gitiles/gitiles.go
+++ b/tools/src/gitiles/gitiles.go
@@ -70,3 +70,21 @@
 	}
 	return res.GetContents(), nil
 }
+
+// ListFiles lists the file paths in a project-relative path at the given reference.
+func (g *Gitiles) ListFiles(ctx context.Context, ref, path string) ([]string, error) {
+	res, err := g.client.ListFiles(ctx, &gpb.ListFilesRequest{
+		Project:    g.project,
+		Committish: ref,
+		Path:       path,
+	})
+	if err != nil {
+		return []string{}, err
+	}
+	files := res.GetFiles()
+	paths := make([]string, len(files))
+	for i, f := range files {
+		paths[i] = f.GetPath()
+	}
+	return paths, nil
+}