Generate a .bat file for running tests in a rust_unit_test_group

When targetting Windows, generate a .bat file instead of a bash script.

Bug: 1271215
Change-Id: I682614081af8a30de10ae95f496373a665c078c8
Cq-Include-Trybots: luci.chromium.try:android-rust-arm32-rel,android-rust-arm64-dbg,android-rust-arm64-rel,linux-rust-x64-dbg,linux-rust-x64-rel
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4436654
Reviewed-by: Robert Sesek <rsesek@chromium.org>
Reviewed-by: Ɓukasz Anforowicz <lukasza@chromium.org>
Commit-Queue: danakj <danakj@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1132123}
diff --git a/build/rust/rust_unit_tests_group.gni b/build/rust/rust_unit_tests_group.gni
index a210d9e..c2cdfe4 100644
--- a/build/rust/rust_unit_tests_group.gni
+++ b/build/rust/rust_unit_tests_group.gni
@@ -20,11 +20,12 @@
 #
 # Example usage:
 #
-#   # This will generate a script at out/Default/bin/run_foo_tests that wraps
-#   # out/Default/foo_crate1_unittests,
-#   # out/Default/foo_mixed_source_set2_rs_unittests,
-#   # and out/Default/foo_mixed_source_set3_rs_unittests executables containing
-#   # native Rust unit tests.
+#   # This will generate a script at out/Default/bin/run_foo_tests (or
+#   # run_foo_tests.bat on Windows) that wraps the executables containing
+#   # native Rust unit tests:
+#   # * out/Default/foo_crate1_unittests
+#   # * out/Default/foo_mixed_source_set2_rs_unittests
+#   # * out/Default/foo_mixed_source_set3_rs_unittests
 #   rust_unit_tests_group("foo_tests") {
 #     deps = [
 #       "foo_crate1",
@@ -32,7 +33,6 @@
 #       "foo_mixed_source_set3",
 #     ]
 #   }
-#   # TODO(https://crbug.com/1271215): Mention .bat generation once implemented.
 
 template("rust_unit_tests_group") {
   assert(defined(invoker.deps), "deps must be listed")
@@ -40,7 +40,11 @@
   # As noted in the top-level comment of //testing/buildbot/gn_isolate_map.pyl
   # the script *must* be in output_dir/bin/run_$target (or
   # output_dir\bin\run_$target.bat on Windows).
-  _script_filepath = "$root_out_dir/bin/run_${target_name}"
+  bat = ""
+  if (is_win) {
+    bat = ".bat"
+  }
+  _script_filepath = "$root_out_dir/bin/run_${target_name}${bat}"
 
   # Gathering metadata provided by the rust_unit_test gni template from all of
   # our dependencies.
@@ -56,15 +60,11 @@
 
   # Generating a script that can run all of the wrapped Rust unit test
   # executables.
-  #
-  # TODO(https://crbug.com/1271215): Also generate: bin/run_${target_name}.bat
-  # when *targeting* Windows: if (is_win) { ... }.  (The "targeting" part means
-  # that we can't just detect whether the build is *hosted* on Windows.)
   action(target_name) {
     forward_variables_from(invoker, "*", [])
 
     testonly = true
-    script = "//testing/scripts/rust/generate_bash_script.py"
+    script = "//testing/scripts/rust/generate_script.py"
     inputs = [ _metadata_filepath ]
     outputs = [ _script_filepath ]
 
@@ -86,5 +86,8 @@
       "--script-path",
       rebase_path(_script_filepath, root_build_dir),
     ]
+    if (is_win) {
+      args += [ "--make-bat" ]
+    }
   }
 }
diff --git a/testing/scripts/rust/exe_util.py b/testing/scripts/rust/exe_util.py
index ed6fe18..32e284d54c 100644
--- a/testing/scripts/rust/exe_util.py
+++ b/testing/scripts/rust/exe_util.py
@@ -5,7 +5,7 @@
 """
 
 import os
-import pty
+import subprocess
 import re
 
 # Regex for matching 7-bit and 8-bit C1 ANSI sequences.
@@ -37,15 +37,7 @@
     Returns:
         The full executable output as an UTF-8 string.
     """
-    output_bytes = bytearray()
-
-    def read(fd):
-        data = os.read(fd, 1024)
-        output_bytes.extend(data)
-        return data
-
-    pty.spawn(args, read)
-
+    output_bytes = subprocess.check_output(args)
     # Strip ANSI / terminal escapes.
     output_bytes = _ANSI_ESCAPE_8BIT_REGEX.sub(b'', output_bytes)
 
diff --git a/testing/scripts/rust/generate_bash_script_unittests.py b/testing/scripts/rust/generate_bash_script_unittests.py
deleted file mode 100755
index cd0c6f4..0000000
--- a/testing/scripts/rust/generate_bash_script_unittests.py
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env vpython3
-
-# Copyright 2021 The Chromium Authors
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import os
-from pyfakefs import fake_filesystem_unittest
-import tempfile
-import unittest
-
-from generate_bash_script import _parse_args
-from generate_bash_script import _generate_script
-
-
-class Tests(fake_filesystem_unittest.TestCase):
-    def test_parse_args(self):
-        raw_args = [
-            '--script-path=./bin/run_foobar', '--exe-dir=.',
-            '--rust-test-executables=metadata.json'
-        ]
-        parsed_args = _parse_args(raw_args)
-        self.assertEqual('./bin/run_foobar', parsed_args.script_path)
-        self.assertEqual('.', parsed_args.exe_dir)
-        self.assertEqual('metadata.json', parsed_args.rust_test_executables)
-
-    def test_generate_script(self):
-        lib_dir = os.path.dirname(__file__)
-        out_dir = os.path.join(lib_dir, '../../../out/rust')
-        args = type('', (), {})()
-        args.script_path = os.path.join(out_dir, 'bin/run_foo_bar')
-        args.exe_dir = out_dir
-
-        # pylint: disable=unexpected-keyword-arg
-        with tempfile.NamedTemporaryFile(delete=False,
-                                         mode='w',
-                                         encoding='utf-8') as f:
-            filepath = f.name
-            f.write("foo\n")
-            f.write("bar\n")
-        try:
-            args.rust_test_executables = filepath
-            actual = _generate_script(args,
-                                      should_validate_if_exes_exist=False)
-        finally:
-            os.remove(filepath)
-
-        expected = '''
-#!/bin/bash
-SCRIPT_DIR=`dirname $0`
-EXE_DIR="$SCRIPT_DIR/.."
-LIB_DIR="$SCRIPT_DIR/../../../testing/scripts/rust"
-env vpython3 "$LIB_DIR/rust_main_program.py" \\
-    "--rust-test-executable=$EXE_DIR/bar" \\
-    "--rust-test-executable=$EXE_DIR/foo" \\
-    "$@"
-'''.strip()
-
-        self.assertEqual(expected, actual)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/testing/scripts/rust/generate_bash_script.py b/testing/scripts/rust/generate_script.py
similarity index 75%
rename from testing/scripts/rust/generate_bash_script.py
rename to testing/scripts/rust/generate_script.py
index e80ef3b..eb57652 100755
--- a/testing/scripts/rust/generate_bash_script.py
+++ b/testing/scripts/rust/generate_script.py
@@ -19,26 +19,25 @@
                   'library for running Rust unit tests with support for ' \
                   'Chromium test filters, sharding, and test output.'
     parser = argparse.ArgumentParser(description=description)
-
     parser.add_argument('--script-path',
                         dest='script_path',
                         help='Where to write the bash script.',
                         metavar='FILEPATH',
                         required=True)
-
     parser.add_argument('--exe-dir',
                         dest='exe_dir',
                         help='Directory where the wrapped executables are',
                         metavar='PATH',
                         required=True)
-
     parser.add_argument('--rust-test-executables',
                         dest='rust_test_executables',
                         help='File listing one or more executables to wrap. ' \
                              '(basenames - no .exe extension or directory)',
                         metavar='FILEPATH',
                         required=True)
-
+    parser.add_argument('--make-bat',
+                        action='store_true',
+                        help='Generate a .bat file instead of a bash script')
     return parser.parse_args(args=args)
 
 
@@ -72,29 +71,36 @@
 
 
 def _generate_script(args, should_validate_if_exes_exist=True):
-    res = '#!/bin/bash\n'
+    THIS_DIR = os.path.abspath(os.path.dirname(__file__))
+    GEN_SCRIPT_DIR = os.path.dirname(args.script_path)
 
-    script_dir = os.path.dirname(args.script_path)
-    res += 'SCRIPT_DIR=`dirname $0`\n'
-
-    exe_dir = os.path.relpath(args.exe_dir, start=script_dir)
+    # Path from the .bat or bash script to the test exes.
+    exe_dir = os.path.relpath(args.exe_dir, start=GEN_SCRIPT_DIR)
     exe_dir = os.path.normpath(exe_dir)
-    res += 'EXE_DIR="$SCRIPT_DIR/{}"\n'.format(exe_dir)
 
-    generator_script_dir = os.path.dirname(__file__)
-    lib_dir = os.path.relpath(generator_script_dir, script_dir)
-    lib_dir = os.path.normpath(lib_dir)
-    res += 'LIB_DIR="$SCRIPT_DIR/{}"\n'.format(lib_dir)
+    # Path from the .bat or bash script to the python main.
+    main_dir = os.path.relpath(THIS_DIR, start=GEN_SCRIPT_DIR)
+    main_dir = os.path.normpath(main_dir)
 
     exes = _find_test_executables(args)
     if should_validate_if_exes_exist:
         _validate_if_test_executables_exist(exes)
 
-    res += 'env vpython3 "$LIB_DIR/rust_main_program.py" \\\n'
-    for exe in exes:
-        res += '    "--rust-test-executable=$EXE_DIR/{}" \\\n'.format(exe)
-    res += '    "$@"'
-
+    if args.make_bat:
+        res = '@echo off\n'
+        res += f'vpython3 "%~dp0\\{main_dir}\\rust_main_program.py" ^\n'
+        for exe in exes:
+            res += f'    "--rust-test-executable=%~dp0\\{exe_dir}\\{exe}" ^\n'
+        res += '    %*'
+    else:
+        res = '#!/bin/bash\n'
+        res += (f'env vpython3 '
+                f'"$(dirname $0)/{main_dir}/rust_main_program.py" \\\n')
+        for exe in exes:
+            res += (
+                f'    '
+                f'"--rust-test-executable=$(dirname $0)/{exe_dir}/{exe}" \\\n')
+        res += '    "$@"'
     return res
 
 
diff --git a/testing/scripts/rust/generate_script_unittests.py b/testing/scripts/rust/generate_script_unittests.py
new file mode 100755
index 0000000..ba91e804
--- /dev/null
+++ b/testing/scripts/rust/generate_script_unittests.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env vpython3
+
+# Copyright 2021 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+from pyfakefs import fake_filesystem_unittest
+import tempfile
+import unittest
+
+from generate_script import _parse_args
+from generate_script import _generate_script
+
+
+class Tests(fake_filesystem_unittest.TestCase):
+    def test_parse_args(self):
+        raw_args = [
+            '--script-path=./bin/run_foobar', '--exe-dir=.',
+            '--rust-test-executables=metadata.json'
+        ]
+        parsed_args = _parse_args(raw_args)
+        self.assertEqual('./bin/run_foobar', parsed_args.script_path)
+        self.assertEqual('.', parsed_args.exe_dir)
+        self.assertEqual('metadata.json', parsed_args.rust_test_executables)
+
+    def test_generate_script(self):
+        lib_dir = os.path.dirname(__file__)
+        out_dir = os.path.join(lib_dir, '../../../out/rust')
+        args = type('', (), {})()
+        args.make_bat = False
+        args.script_path = os.path.join(out_dir, 'bin/run_foo_bar')
+        args.exe_dir = out_dir
+
+        # pylint: disable=unexpected-keyword-arg
+        with tempfile.NamedTemporaryFile(delete=False,
+                                         mode='w',
+                                         encoding='utf-8') as f:
+            filepath = f.name
+            f.write("foo\n")
+            f.write("bar\n")
+        try:
+            args.rust_test_executables = filepath
+            actual = _generate_script(args,
+                                      should_validate_if_exes_exist=False)
+        finally:
+            os.remove(filepath)
+
+        expected = '''
+#!/bin/bash
+env vpython3 "$(dirname $0)/../../../testing/scripts/rust/rust_main_program.py" \\
+    "--rust-test-executable=$(dirname $0)/../bar" \\
+    "--rust-test-executable=$(dirname $0)/../foo" \\
+    "$@"
+'''.strip()
+
+        self.assertEqual(expected, actual)
+
+    def test_generate_bat(self):
+        lib_dir = os.path.dirname(__file__)
+        out_dir = os.path.join(lib_dir, '../../../out/rust')
+        args = type('', (), {})()
+        args.make_bat = True
+        args.script_path = os.path.join(out_dir, 'bin/run_foo_bar')
+        args.exe_dir = out_dir
+
+        # pylint: disable=unexpected-keyword-arg
+        with tempfile.NamedTemporaryFile(delete=False,
+                                         mode='w',
+                                         encoding='utf-8') as f:
+            filepath = f.name
+            f.write("foo\n")
+            f.write("bar\n")
+        try:
+            args.rust_test_executables = filepath
+            actual = _generate_script(args,
+                                      should_validate_if_exes_exist=False)
+        finally:
+            os.remove(filepath)
+
+        expected = '''
+@echo off
+vpython3 "%~dp0\\../../../testing/scripts/rust\\rust_main_program.py" ^
+    "--rust-test-executable=%~dp0\\..\\bar" ^
+    "--rust-test-executable=%~dp0\\..\\foo" ^
+    %*
+'''.strip()
+
+        self.assertEqual(expected, actual)
+
+
+if __name__ == '__main__':
+    unittest.main()