code_coverage: fuzzilli: run all JS testcases separately

This is necessary because `d8` runs the test cases in the same JS
namespace. This means that if identifiers are shared in between those,
we'll fail with errors such as TypeError.

Change-Id: Ide287027c278f9b4d986fa885d47969c8fe41df2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6449806
Commit-Queue: Paul Semel <[email protected]>
Reviewed-by: Ali Hijazi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1446424}
NOKEYCHECK=True
GitOrigin-RevId: b9e0508865db219977ed75521e7d8abe54ec014e
diff --git a/run_all_fuzzers.py b/run_all_fuzzers.py
index 21ae013..65b5dcb 100644
--- a/run_all_fuzzers.py
+++ b/run_all_fuzzers.py
@@ -149,9 +149,20 @@
 
 
 @dataclasses.dataclass
-class FuzzilliRunner(CmdRunner):
+class FuzzilliRunner(EngineRunner):
   """Runs a given target with Fuzzilli.
   """
+  d8_path: str
+  target_arguments: Sequence[str]
+  source_dir: str
+
+  def __post_init__(self):
+    self.d8_path = os.path.abspath(self.d8_path)
+    self.source_dir = os.path.abspath(self.source_dir)
+
+  @property
+  def cmd(self):
+    return [self.d8_path] + self.target_arguments
 
   def run_full_corpus(self, env: Mapping[str, str], timeout: float,
                       annotation: str, corpus_dir: Optional[str]) -> bool:
@@ -161,11 +172,18 @@
 
   def run_testcases(self, env: Mapping[str, str], timeout: float,
                     annotation: str, testcases: Sequence[str]) -> bool:
-    # That's a tricky part here. Basically, running cases by cases takes a
-    # gigantic amount of time, and given that we split initial targets chunks
-    # into even smaller chunks, we just hope that we execute as much test cases
-    # as possible in those runs and consider it a success.
-    super().run_testcases(env, timeout, annotation, testcases)
+    run_dir = tempfile.TemporaryDirectory()
+    os.symlink(os.path.join(self.source_dir, 'v8/test/'),
+               os.path.join(run_dir.name, 'test'))
+    # We need to run the test cases separately, because otherwise the JS files
+    # will be ran in the same JS namespace.
+    for testcase in testcases:
+      testcase = os.path.abspath(testcase)
+      _run_and_log(cmd=self.cmd + [testcase],
+                   env=env,
+                   timeout=timeout,
+                   annotation=annotation,
+                   cwd=run_dir.name)
     return True
 
 
@@ -206,8 +224,11 @@
   return False
 
 
-def _run_and_log(cmd: Sequence[str], env: Mapping[str, str], timeout: float,
-                 annotation: str) -> bool:
+def _run_and_log(cmd: Sequence[str],
+                 env: Mapping[str, str],
+                 timeout: float,
+                 annotation: str,
+                 cwd: str = None) -> bool:
   """Runs a given command and logs output in case of failure.
 
   Args:
@@ -225,7 +246,8 @@
                    env=env,
                    timeout=timeout,
                    capture_output=True,
-                   check=True)
+                   check=True,
+                   cwd=cwd)
     return True
   except Exception as e:
     if type(e) == subprocess.TimeoutExpired:
@@ -678,6 +700,7 @@
 def _get_fuzzilli_target_details(args):
   all_target_details = []
   fuzzer_target_binpath = os.path.join(args.fuzzer_binaries_dir, 'd8')
+  source_dir = os.path.abspath(os.path.join(args.fuzzer_binaries_dir, '../../'))
   if not os.path.isfile(fuzzer_target_binpath):
     logging.warning(f'Could not find binary file: {fuzzer_target_binpath}')
     return all_target_details
@@ -692,8 +715,6 @@
                                     'settings.json')
     with open(path_to_settings, 'r') as fp:
       settings = json.load(fp)
-    cmd = [fuzzer_target_binpath]
-    cmd.extend(settings['processArguments'])
     path_to_js_dir = os.path.join(target_corpora_dir, 'fuzzdir', 'corpus')
     jsfiles = [
         os.path.join(path_to_js_dir, file)
@@ -711,7 +732,9 @@
           'env':
           dict(),
           'cmd_runner':
-          FuzzilliRunner(cmd=cmd),
+          FuzzilliRunner(d8_path=fuzzer_target_binpath,
+                         target_arguments=settings['processArguments'],
+                         source_dir=source_dir),
           'corpus':
           None,
           'files':