Add presubmit for tag headers

Adds a simple script and presubmit check for ensuring that the
WebGPU CTS expectation file tag headers remain in sync.

Also drive-by updates the webgpu-cts directory presubmit file to
version 2 of the presubmit API.

Bug: 345280734
Change-Id: I5d0bde1453b6e557fa297d46a88fc1d6364693d1
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/204434
Reviewed-by: Austin Eng <enga@chromium.org>
Commit-Queue: Brian Sheedy <bsheedy@google.com>
diff --git a/webgpu-cts/PRESUBMIT.py b/webgpu-cts/PRESUBMIT.py
index 03f9907..4fb56c3 100644
--- a/webgpu-cts/PRESUBMIT.py
+++ b/webgpu-cts/PRESUBMIT.py
@@ -27,8 +27,10 @@
 
 import sys
 
+PRESUBMIT_VERSION = '2.0.0'
 
-def _DoCommonChecks(input_api, output_api):
+
+def CheckCtsValidate(input_api, output_api):
     sys.path += [input_api.change.RepositoryRoot()]
 
     from go_presubmit_support import go_path
@@ -62,5 +64,14 @@
     return results
 
 
-CheckChangeOnUpload = _DoCommonChecks
-CheckChangeOnCommit = _DoCommonChecks
+def CheckHeaderSync(input_api, output_api):
+    results = []
+    sync_script = input_api.os_path.join(input_api.PresubmitLocalPath(),
+                                         'scripts', 'check_headers_in_sync.py')
+    try:
+        input_api.subprocess.check_call_out([sync_script],
+                                            stdout=input_api.subprocess.PIPE,
+                                            stderr=input_api.subprocess.STDOUT)
+    except input_api.subprocess.CalledProcessError as e:
+        results.append(output_api.PresubmitError(str(e)))
+    return results
diff --git a/webgpu-cts/scripts/check_headers_in_sync.py b/webgpu-cts/scripts/check_headers_in_sync.py
new file mode 100755
index 0000000..2e58678
--- /dev/null
+++ b/webgpu-cts/scripts/check_headers_in_sync.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+#
+# Copyright 2024 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.
+"""Asserts that all expectation file headers are in sync."""
+
+import os
+
+TAG_HEADER_START = '# BEGIN TAG HEADER'
+TAG_HEADER_END = '# END TAG HEADER'
+
+EXPECTATION_FILES = [
+    os.path.realpath(
+        os.path.join(os.path.dirname(__file__), '..', 'expectations.txt')),
+    os.path.realpath(
+        os.path.join(os.path.dirname(__file__), '..',
+                     'compat-expectations.txt')),
+]
+
+tag_headers = {}
+for ef in EXPECTATION_FILES:
+    with open(ef, encoding='utf-8') as infile:
+        content = infile.read()
+
+    header_lines = []
+    in_header = False
+    for line in content.splitlines():
+        line = line.strip()
+        if line.startswith(TAG_HEADER_START):
+            in_header = True
+            continue
+        if not in_header:
+            continue
+        if line.startswith(TAG_HEADER_END):
+            break
+        header_lines.append(line)
+    tag_headers[ef] = '\n'.join(header_lines)
+
+for left_ef, left_header in tag_headers.items():
+    for right_ef, right_header in tag_headers.items():
+        if left_ef == right_ef:
+            continue
+        if left_header != right_header:
+            raise RuntimeError(
+                f'The tag headers in {left_ef} and {right_ef} are out of sync')