PRESUBMIT: Check that generated files aren't stale

Fails presubmit if you need to run `./tools/run gen`

Change-Id: I05311cd668c5a1f4f484b25cc1367f680a9d24eb
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/110140
Commit-Queue: Ben Clayton <bclayton@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Ben Clayton <bclayton@google.com>
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 8e03359..7c93279 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -127,8 +127,29 @@
     return file.LocalPath() not in filter_list
 
 
+def _CheckNoStaleGen(input_api, output_api):
+    results = []
+    try:
+        go = input_api.os_path.join(input_api.change.RepositoryRoot(), "tools",
+                                    "golang", "bin", "go")
+        if input_api.is_windows:
+            go += '.exe'
+        input_api.subprocess.check_call_out(
+            [go, "run", "tools/src/cmd/gen/main.go", "--check-stale"],
+            stdout=input_api.subprocess.PIPE,
+            stderr=input_api.subprocess.PIPE,
+            cwd=input_api.change.RepositoryRoot())
+    except input_api.subprocess.CalledProcessError as e:
+        if input_api.is_committing:
+            results.append(output_api.PresubmitError('%s' % (e, )))
+        else:
+            results.append(output_api.PresubmitPromptWarning('%s' % (e, )))
+    return results
+
+
 def _DoCommonChecks(input_api, output_api):
     results = []
+    results.extend(_CheckNoStaleGen(input_api, output_api))
     results.extend(
         input_api.canned_checks.CheckChangedLUCIConfigs(input_api, output_api))
 
diff --git a/tools/src/cmd/gen/main.go b/tools/src/cmd/gen/main.go
index f1ba9c3..653adbb 100644
--- a/tools/src/cmd/gen/main.go
+++ b/tools/src/cmd/gen/main.go
@@ -75,10 +75,13 @@
 func run() error {
 	outputDir := ""
 	verbose := false
+	checkStale := false
 	flag.StringVar(&outputDir, "o", "", "custom output directory (optional)")
 	flag.BoolVar(&verbose, "verbose", false, "print verbose output")
+	flag.BoolVar(&checkStale, "check-stale", false, "don't emit anything, just check that files are up to date")
 	flag.Parse()
 
+	staleFiles := []string{}
 	projectRoot := fileutils.DawnRoot()
 
 	// Find clang-format
@@ -168,8 +171,22 @@
 			sb := strings.Builder{}
 			sb.WriteString(fmt.Sprintf(header, copyrightYear, filepath.ToSlash(relTmplPath)))
 			sb.WriteString(body)
-			content := sb.String()
-			return writeFileIfChanged(outPath, content, string(existing))
+			oldContent, newContent := string(existing), sb.String()
+
+			if oldContent != newContent {
+				if checkStale {
+					staleFiles = append(staleFiles, outPath)
+				} else {
+					if err := os.MkdirAll(filepath.Dir(outPath), 0777); err != nil {
+						return fmt.Errorf("failed to create directory for '%v': %w", outPath, err)
+					}
+					if err := ioutil.WriteFile(outPath, []byte(newContent), 0666); err != nil {
+						return fmt.Errorf("failed to write file '%v': %w", outPath, err)
+					}
+				}
+			}
+
+			return nil
 		}
 
 		// Write the content generated using the template and semantic info
@@ -196,6 +213,19 @@
 		}
 	}
 
+	if len(staleFiles) > 0 {
+		fmt.Println(len(staleFiles), "files need regenerating:")
+		for _, path := range staleFiles {
+			if rel, err := filepath.Rel(projectRoot, path); err == nil {
+				fmt.Println(" •", rel)
+			} else {
+				fmt.Println(" •", path)
+			}
+		}
+		fmt.Println("Regenerate these files with: ./tools/run gen")
+		os.Exit(1)
+	}
+
 	return nil
 }
 
@@ -268,20 +298,6 @@
 	return g.cached.permuter.Permute(overload)
 }
 
-// writes content to path if the file has changed
-func writeFileIfChanged(path, newContent, oldContent string) error {
-	if oldContent == newContent {
-		return nil // Not changed
-	}
-	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
-		return fmt.Errorf("failed to create directory for '%v': %w", path, err)
-	}
-	if err := ioutil.WriteFile(path, []byte(newContent), 0666); err != nil {
-		return fmt.Errorf("failed to write file '%v': %w", path, err)
-	}
-	return nil
-}
-
 var copyrightRegex = regexp.MustCompile(`// Copyright (\d+) The`)
 
 const header = `// Copyright %v The Tint Authors.