[testing] Don't use directory placeholder for untarring test results.

Bug: 43500
Change-Id: I5fd3beef085adbaf9358ab79553d9f8b8ce6d667
diff --git a/recipe_modules/fuchsia/test_api.py b/recipe_modules/fuchsia/test_api.py
index e9e699c..767de91 100644
--- a/recipe_modules/fuchsia/test_api.py
+++ b/recipe_modules/fuchsia/test_api.py
@@ -19,6 +19,7 @@
            bb_build=None,
            status='success',
            steps=(),
+           output_dir_contents=None,
            tests_json=None):  # pragma: no cover
     """Returns a test case appropriate for yielding from GenTests().
 
@@ -99,7 +100,9 @@
             ]))
         extra_steps.append(
             self.m.testing.test_step_data(
-                tests_json=tests_json, qemu=not on_device))
+                tests_json=tests_json,
+                qemu=not on_device,
+                output_dir_contents=output_dir_contents))
 
     # Assemble the return value.
     ret = self.m.status_check.test(name, status=status)
diff --git a/recipe_modules/minfs/api.py b/recipe_modules/minfs/api.py
index 9b38786..5b44d6e 100644
--- a/recipe_modules/minfs/api.py
+++ b/recipe_modules/minfs/api.py
@@ -86,9 +86,7 @@
         # Paths inside of the MinFS image are prefixed with '::', so '::'
         # refers to the root of the MinFS image.
         '::',
-        self.m.raw_io.output_dir(leak_to=out_dir),
+        out_dir,
         image_path,
         name=step_name,
-        step_test_data=lambda: self.m.raw_io.test_api.output_dir(
-            {'hello.out': 'I am output.'}),
     )
diff --git a/recipe_modules/testing/api.py b/recipe_modules/testing/api.py
index 792e72b..e2eeb80 100644
--- a/recipe_modules/testing/api.py
+++ b/recipe_modules/testing/api.py
@@ -35,8 +35,6 @@
     output_dir (Path): A directory containing the outputs of the swarming
       task that ran these tests. Anything that's in this directory will be
       uploaded to GCS when upload_results() is called.
-    outputs (dict[str]str): A mapping from of relative paths to files
-      containing stdout+stderr data to strings containing those contents.
     swarming_task_id (str): The ID of the task that ran these tests.
     symbolizer_json_output (Path or None): The path to the json trigger
       information produced by the symbolizer.
@@ -59,7 +57,6 @@
   from_fuchsia = attr.ib(type=bool)
   results_dir = attr.ib(type=Path)
   output_dir = attr.ib(type=Path)
-  outputs = attr.ib(type=dict)
   swarming_task_id = attr.ib(type=str)
   symbolizer_json_output = attr.ib(type=Path)
   _env_name = attr.ib(type=str)
@@ -70,6 +67,10 @@
   # TODO(fxb/10410): Get rid of overwrite_summary after fuchsia_perf is dead.
   _overwrite_summary = attr.ib(True, type=bool)
 
+  # A mapping of relative paths to files in the results_dir containing
+  # stdout+stderr data to strings containing those contents.
+  _outputs = attr.ib(factory=dict, init=False)
+
   # Set lazily by the `summary` property, not a parameter to __init__.
   _summary = attr.ib(None, init=False)
 
@@ -99,43 +100,41 @@
 
   @property
   def passed_test_outputs(self):
-    """All entries in |self.outputs| for tests that passed."""
+    """All entries in |self._outputs| for tests that passed."""
     return self._filter_outputs_by_test_result(self._TEST_RESULT_PASS)
 
   @property
   def failed_test_outputs(self):
-    """All entries in |self.outputs| for tests that failed."""
+    """All entries in |self._outputs| for tests that failed."""
     return self._filter_outputs_by_test_result(self._TEST_RESULT_FAIL)
 
   def _filter_outputs_by_test_result(self, result):
-    """Returns all entries in |self.outputs| whose result is |result|.
+    """Returns all entries in |self._outputs| whose result is |result|.
 
     Args:
       result (String): one of the _TEST_RESULT_* constants from this class.
 
     Returns:
-      A dict whose keys are paths to the files containing each test's
-      stderr+stdout data and whose values are strings containing those
-      contents.
+      A dict whose keys are the test names and whose values are the paths to
+      the files containing each test's stderr+stdout data.
     """
     matches = collections.OrderedDict()
-    # TODO(kjharland): Sort test names first.
     for test in self.summary.get('tests', ()):
       if test['result'] == result:
         # The 'output_file' field is a path to the file containing the
-        # stderr+stdout data for the test, and we inline the contents of that
-        # file as the value in the returned dict.
-        matches[test['name']] = self.outputs[test['output_file']]
+        # stderr+stdout data for the test.
+        matches[test['name']] = test['output_file']
 
     return matches
 
   def _parse_summary(self):
-    raw_summary = self.outputs.get(TEST_SUMMARY_JSON, '')
-    if not raw_summary:
+    summary_path = self.results_dir.join(TEST_SUMMARY_JSON)
+    if not self._api.path.exists(summary_path):
       return {}
 
     try:
-      summary = self._api.json.loads(raw_summary)
+      summary = self._api.file.read_json('read %s' % TEST_SUMMARY_JSON,
+                                         summary_path)
     except ValueError as e:  # pragma: no cover
       # TODO(olivernewman): JSONDecodeError in python >=3.5
       raise self._api.step.StepFailure('Invalid %s: %s' %
@@ -168,9 +167,19 @@
     return summary
 
   def present_tests(self, show_failures_in_red, show_passed):
-    for test, stdio in self.failed_test_outputs.iteritems():
-      step = self._api.step('failed: %s' % test, None)
-      step.presentation.logs['stdio'] = stdio.split('\n')
+    for test, stdio_file in self.failed_test_outputs.iteritems():
+      name = 'failed: %s' % test
+      if stdio_file not in self._outputs:
+        self._outputs[stdio_file] = self._api.file.read_text(
+            name, self.results_dir.join(stdio_file), test_data='output')
+        step = self._api.step.active_result
+        # file.read_text creates a step that displays a log of the file contents
+        # with the name as the basename of the file read. We want to remove this
+        # log and rename it to `stdio`.
+        step.presentation.logs.pop(self._api.path.basename(stdio_file))
+      else:
+        step = self._api.step(name, None)
+      step.presentation.logs['stdio'] = self._outputs[stdio_file].splitlines()
       if show_failures_in_red:
         step.presentation.status = self._api.step.FAILURE
 
@@ -187,6 +196,18 @@
         passed_tests_step.presentation.step_text = ''.join(
             '\n' + test_name for test_name in passed_tests)
 
+  def get_output(self, output_path):
+    """Returns the contents of the file at output_path.
+
+    The output_path should be a relative path to the results_dir.
+    """
+    if output_path not in self._outputs:
+      self._outputs[output_path] = self._api.file.read_text(
+          'read %s' % output_path,
+          self.results_dir.join(output_path),
+          test_data='output')
+    return self._outputs[output_path]
+
   def upload_results(self, gcs_bucket, upload_to_catapult):
     """Upload select test results (e.g., coverage data) to a given GCS bucket."""
     assert gcs_bucket
@@ -376,10 +397,10 @@
 
       results_dir = self._api.testing.results_dir_on_host.join(result.id)
       # pylint: disable=protected-access
-      test_results_map = self._api.testing._extract_test_results_archive(
+      self._api.testing._extract_test_results_archive(
           step_name='extract',
           archive_path=attempt.test_results_archive,
-          leak_to=results_dir,
+          directory=results_dir,
           is_minfs=self._uses_legacy_qemu,
       )
       # pylint: enable=protected-access
@@ -387,7 +408,6 @@
       attempt.test_results = FuchsiaTestResults(
           from_fuchsia=self._targets_fuchsia,
           results_dir=results_dir,
-          outputs=test_results_map,
           swarming_task_id=attempt.task_id,
           symbolizer_json_output=attempt.symbolizer_json_output,
           env_name=result.name,
@@ -507,7 +527,7 @@
         # Log the contents of each output file mentioned in the summary.
         # Note this assumes the outputs are all valid UTF-8 (See fxb/9500).
         for name, path in test_results.summary.get('outputs', {}).iteritems():
-          presentation.logs[name] = test_results.outputs[path].split('\n')
+          presentation.logs[name] = test_results.get_output(path).split('\n')
 
         test_results.present_tests(
             show_failures_in_red=show_failures_in_red, show_passed=show_passed)
@@ -615,9 +635,10 @@
         test result outputs; if not provided, that of the active result will be
         used.
     """
+    presentation = presentation or self.m.step.active_result.presentation
+
     if not test_results.summary:
       return
-    presentation = presentation or self.m.step.active_result.presentation
 
     # Log the summary file's contents.
     presentation.logs[TEST_SUMMARY_JSON] = test_results.summary_lines
@@ -626,7 +647,7 @@
     # Note this assumes the outputs are all valid UTF-8 (See fxb/9500).
     for output_name, output_path in test_results.summary.get('outputs',
                                                              {}).iteritems():
-      output_str = test_results.outputs[output_path]
+      output_str = test_results.get_output(output_path)
       presentation.logs[output_name] = output_str.split('\n')
 
     test_results.present_tests(show_failures_in_red=True, show_passed=True)
@@ -690,7 +711,7 @@
                                     step_name,
                                     archive_path,
                                     is_minfs=False,
-                                    leak_to=None):
+                                    directory=None):
     """Extracts test results from an archive.
 
     Args:
@@ -699,25 +720,30 @@
       is_minfs (bool): Whether the archive in question is a minfs image
         containing QEMU test results. If false, then the archive is assumed to
         be a tar file.
-      leak_to (Path): Optionally leak the contents of the archive to a
-        directory.
-
-    Returns:
-      A dict mapping a filepath relative to the root of the archive to the
-      contents of that file in the archive.
+      directory (Path): The directory to extract the archive contents into.
     """
+    self.m.file.ensure_directory('create test results dir', directory)
     if is_minfs:
-      return self.m.minfs.copy_image(
+      self.m.minfs.copy_image(
           step_name=step_name,
           image_path=archive_path,
-          out_dir=leak_to,
-      ).raw_io.output_dir
+          out_dir=directory,
+      )
+    else:
+      self.m.tar.extract(
+          step_name=step_name,
+          path=archive_path,
+          directory=directory,
+      )
 
-    return self.m.tar.extract(
-        step_name=step_name,
-        path=archive_path,
-        directory=self.m.raw_io.output_dir(leak_to=leak_to),
-    ).raw_io.output_dir
+    # This is only needed for the recipe tests. file.listdir() doesn't mock the
+    # existence of the paths it returns, so we must add it separately.
+    # We add summary_path because we check for its existence in
+    # FuchsiaTestResults._parse_summary().
+    summary_path = directory.join(TEST_SUMMARY_JSON)
+    outputs = self.m.file.listdir('get extracted files', directory)
+    if summary_path in outputs:
+      self.m.path.mock_add_paths(summary_path)
 
   def deprecated_test(self, *args, **kwargs):
     """Tests a Fuchsia build on the specified device with retries.
@@ -881,13 +907,13 @@
         # _extract_test_results_archive needs minfs_path to be set.
         # This is kinda ugly. It'd be better to pass this in as an argument.
         self.m.minfs.minfs_path = orchestration_inputs.minfs
-        test_results_map = self._extract_test_results_archive(
+        self._extract_test_results_archive(
             step_name='extract results',
             is_minfs=self.m.emu.is_emulator_type(device_type),
             archive_path=archive_path,
             # Write test results to a subdirectory of |results_dir_on_host|
             # so as not to collide with host test results.
-            leak_to=test_results_dir,
+            directory=test_results_dir,
         )
 
         # Remove the archive file so it doesn't get uploaded to GCS.
@@ -903,7 +929,6 @@
         test_results = self.FuchsiaTestResults(
             from_fuchsia=True,
             results_dir=test_results_dir,
-            outputs=test_results_map,
             swarming_task_id=task_result.id,
             symbolizer_json_output=symbolizer_json_output,
             env_name=task_result.name,
diff --git a/recipe_modules/testing/examples/full.expected/asan_tests.json b/recipe_modules/testing/examples/full.expected/asan_tests.json
index 1a5bb7c..32d6a1c 100644
--- a/recipe_modules/testing/examples/full.expected/asan_tests.json
+++ b/recipe_modules/testing/examples/full.expected/asan_tests.json
@@ -1355,7 +1355,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@"
     ]
@@ -1504,6 +1504,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_2/1/output.fs",
       "cp",
@@ -1523,6 +1541,28 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/1/output.fs"
     ],
@@ -1559,6 +1599,44 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "run tests.attempt 0.all test results.all passed tests",
     "~followup_annotations": [
diff --git a/recipe_modules/testing/examples/full.expected/async.json b/recipe_modules/testing/examples/full.expected/async.json
index 17eb96f..d9b75b6 100644
--- a/recipe_modules/testing/examples/full.expected/async.json
+++ b/recipe_modules/testing/examples/full.expected/async.json
@@ -1464,6 +1464,21 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "create test results dir"
+  },
+  {
+    "cmd": [
       "[START_DIR]/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_2/1/output.fs",
       "cp",
@@ -1480,6 +1495,27 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/1/output.fs"
     ],
@@ -1528,11 +1564,49 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@"
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "all test results.all passed tests",
     "~followup_annotations": [
diff --git a/recipe_modules/testing/examples/full.expected/fail_then_timeout.json b/recipe_modules/testing/examples/full.expected/fail_then_timeout.json
index 7f9adfb..fb959cb 100644
--- a/recipe_modules/testing/examples/full.expected/fail_then_timeout.json
+++ b/recipe_modules/testing/examples/full.expected/fail_then_timeout.json
@@ -1616,6 +1616,24 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/610"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0000.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.process results.fuchsia-0000.ensure bsdtar",
     "~followup_annotations": [
@@ -1673,6 +1691,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/610"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0000.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0000.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"FAIL\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"FAIL\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/610/out.tar"
     ],
@@ -1724,7 +1783,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -1735,20 +1794,59 @@
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.failed tasks.fuchsia-0000 (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/hello.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.0.failed tasks.fuchsia-0000 (attempt 0).failed: /path/to/hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/goodbye.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.0.failed tasks.fuchsia-0000 (attempt 0).failed: /path/to/goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
@@ -2090,7 +2188,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -2106,7 +2204,7 @@
     "name": "failures.fuchsia-0000.attempt 0 (fail).failed: /path/to/hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
@@ -2116,7 +2214,7 @@
     "name": "failures.fuchsia-0000.attempt 0 (fail).failed: /path/to/goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
diff --git a/recipe_modules/testing/examples/full.expected/isolated_test_device_no_pave.json b/recipe_modules/testing/examples/full.expected/isolated_test_device_no_pave.json
index f104a0f..039b1c3 100644
--- a/recipe_modules/testing/examples/full.expected/isolated_test_device_no_pave.json
+++ b/recipe_modules/testing/examples/full.expected/isolated_test_device_no_pave.json
@@ -1384,7 +1384,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@"
     ]
@@ -1551,6 +1551,24 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "run tests.attempt 0.ensure bsdtar",
     "~followup_annotations": [
@@ -1608,6 +1626,28 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/1/out.tar"
     ],
@@ -1644,6 +1684,44 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "run tests.attempt 0.all test results.all passed tests",
     "~followup_annotations": [
diff --git a/recipe_modules/testing/examples/full.expected/isolated_tests_no_json.json b/recipe_modules/testing/examples/full.expected/isolated_tests_no_json.json
index 566a7df..ff9d764 100644
--- a/recipe_modules/testing/examples/full.expected/isolated_tests_no_json.json
+++ b/recipe_modules/testing/examples/full.expected/isolated_tests_no_json.json
@@ -1481,6 +1481,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_2/1/out.tar",
       "cp",
@@ -1500,6 +1518,23 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/1/out.tar"
     ],
diff --git a/recipe_modules/testing/examples/full.expected/isolated_tests_test_failure.json b/recipe_modules/testing/examples/full.expected/isolated_tests_test_failure.json
index c080ced..e2f451c 100644
--- a/recipe_modules/testing/examples/full.expected/isolated_tests_test_failure.json
+++ b/recipe_modules/testing/examples/full.expected/isolated_tests_test_failure.json
@@ -1358,7 +1358,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@",
       "@@@STEP_FAILURE@@@"
@@ -1507,6 +1507,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_2/1/out.tar",
       "cp",
@@ -1526,6 +1544,28 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/1/out.tar"
     ],
@@ -1563,21 +1603,79 @@
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"FAIL\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"FAIL\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/hello.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "run tests.attempt 0.all test results.failed: fuchsia-pkg://fuchsia.com/hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "run tests.attempt 0.all test results.failed: fuchsia-pkg://fuchsia.com/goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
@@ -1619,7 +1717,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@",
       "@@@STEP_FAILURE@@@"
@@ -1733,6 +1831,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/2"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 1.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_2/2/out.tar",
       "cp",
@@ -1752,6 +1868,28 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/2"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 1.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/2/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/2/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/2/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/2/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/2/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/2/out.tar"
     ],
@@ -1789,21 +1927,79 @@
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/2/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 1.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"FAIL\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"FAIL\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/2/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 1.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/2/hello.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "run tests.attempt 1.all test results.failed: fuchsia-pkg://fuchsia.com/hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/2/goodbye.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "run tests.attempt 1.all test results.failed: fuchsia-pkg://fuchsia.com/goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
diff --git a/recipe_modules/testing/examples/full.expected/sharded_failure_string.json b/recipe_modules/testing/examples/full.expected/sharded_failure_string.json
index c74fdc6..c9c1f31 100644
--- a/recipe_modules/testing/examples/full.expected/sharded_failure_string.json
+++ b/recipe_modules/testing/examples/full.expected/sharded_failure_string.json
@@ -1607,6 +1607,24 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/1"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Vim2.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.process results.Vim2.ensure bsdtar",
     "~followup_annotations": [
@@ -1664,6 +1682,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/1"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Vim2.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Vim2.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"gn_label\": \"//path/to/hello:hello(//toolchain)\", \"name\": \"hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"gn_label\": \"//path/to/goodbye:goodbye(//toolchain)\", \"name\": \"goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/1/out.tar"
     ],
@@ -1717,7 +1776,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -1728,6 +1787,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.failed tasks.Vim2 (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.failed tasks.Vim2 (attempt 0).all passed tests",
     "~followup_annotations": [
@@ -1948,6 +2026,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/2"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.Vim2.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/cipd/bsdtar/bsdtar",
       "--extract",
       "--verbose",
@@ -1969,6 +2065,23 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/2"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.Vim2.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/2/out.tar"
     ],
@@ -2118,7 +2231,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
diff --git a/recipe_modules/testing/examples/full.expected/sharded_kernel_panic.json b/recipe_modules/testing/examples/full.expected/sharded_kernel_panic.json
index 1553f28..f36e1b3 100644
--- a/recipe_modules/testing/examples/full.expected/sharded_kernel_panic.json
+++ b/recipe_modules/testing/examples/full.expected/sharded_kernel_panic.json
@@ -1601,6 +1601,24 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/1"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Vim2.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.process results.Vim2.ensure bsdtar",
     "~followup_annotations": [
@@ -1658,6 +1676,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/1"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Vim2.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Vim2.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"gn_label\": \"//path/to/hello:hello(//toolchain)\", \"name\": \"hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"gn_label\": \"//path/to/goodbye:goodbye(//toolchain)\", \"name\": \"goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/1/out.tar"
     ],
@@ -1711,7 +1770,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -1722,6 +1781,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.failed tasks.Vim2 (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.failed tasks.Vim2 (attempt 0).all passed tests",
     "~followup_annotations": [
@@ -1936,6 +2014,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/2"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.Vim2.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/cipd/bsdtar/bsdtar",
       "--extract",
       "--verbose",
@@ -1957,6 +2053,23 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/2"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.Vim2.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/2/out.tar"
     ],
@@ -2106,7 +2219,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
diff --git a/recipe_modules/testing/examples/full.expected/test_in_shards_mixed_failure.json b/recipe_modules/testing/examples/full.expected/test_in_shards_mixed_failure.json
index ee0f0b4..e4746ee 100644
--- a/recipe_modules/testing/examples/full.expected/test_in_shards_mixed_failure.json
+++ b/recipe_modules/testing/examples/full.expected/test_in_shards_mixed_failure.json
@@ -2303,6 +2303,24 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/610"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0000.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.process results.fuchsia-0000.ensure bsdtar",
     "~followup_annotations": [
@@ -2360,6 +2378,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/610"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0000.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0000.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_5/610/out.tar"
     ],
@@ -2431,6 +2490,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/710"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0001.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/cipd/bsdtar/bsdtar",
       "--extract",
       "--verbose",
@@ -2452,6 +2529,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/710"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0001.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/710/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/710/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/710/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/710/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/710/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/710/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0001.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"gn_label\": \"//path/to/hello:hello(//toolchain)\", \"name\": \"hello\", \"output_file\": \"hello.out\", \"result\": \"FAIL\"}, {\"gn_label\": \"//path/to/goodbye:goodbye(//toolchain)\", \"name\": \"goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"FAIL\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_5/710/out.tar"
     ],
@@ -2583,6 +2701,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/910"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0003.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/cipd/bsdtar/bsdtar",
       "--extract",
       "--verbose",
@@ -2604,6 +2740,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/910"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0003.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/910/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/910/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/910/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/910/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/910/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/910/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.fuchsia-0003.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"gn_label\": \"//path/to/hello:hello(//toolchain)\", \"name\": \"hello\", \"output_file\": \"hello.out\", \"result\": \"FAIL\"}, {\"gn_label\": \"//path/to/goodbye:goodbye(//toolchain)\", \"name\": \"goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"FAIL\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_5/910/out.tar"
     ],
@@ -2654,7 +2831,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -2665,6 +2842,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.passed tasks.fuchsia-0000 (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.passed tasks.fuchsia-0000 (attempt 0).all passed tests",
     "~followup_annotations": [
@@ -2747,7 +2943,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -2758,20 +2954,59 @@
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/710/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.failed tasks.fuchsia-0001 (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/710/hello.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.0.failed tasks.fuchsia-0001 (attempt 0).failed: hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/710/goodbye.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.0.failed tasks.fuchsia-0001 (attempt 0).failed: goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
@@ -2907,7 +3142,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -2918,20 +3153,59 @@
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/910/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.failed tasks.fuchsia-0003 (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/910/hello.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.0.failed tasks.fuchsia-0003 (attempt 0).failed: hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/910/goodbye.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.0.failed tasks.fuchsia-0003 (attempt 0).failed: goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
@@ -3275,6 +3549,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/711"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.fuchsia-0001.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/cipd/bsdtar/bsdtar",
       "--extract",
       "--verbose",
@@ -3296,6 +3588,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/711"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.fuchsia-0001.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/711/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/711/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/711/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/711/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/711/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/711/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.fuchsia-0001.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"FAIL\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"FAIL\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_5/711/out.tar"
     ],
@@ -3427,6 +3760,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/911"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.fuchsia-0003.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/cipd/bsdtar/bsdtar",
       "--extract",
       "--verbose",
@@ -3448,6 +3799,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/911"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.fuchsia-0003.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/911/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/911/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/911/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/911/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/911/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/911/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.process results.fuchsia-0003.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"gn_label\": \"//path/to/hello:hello(//toolchain)\", \"name\": \"hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"gn_label\": \"//path/to/goodbye:goodbye(//toolchain)\", \"name\": \"goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_5/911/out.tar"
     ],
@@ -3500,7 +3892,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -3511,6 +3903,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/911/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.passed tasks.fuchsia-0003 (attempt 1).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.1.passed tasks.fuchsia-0003 (attempt 1).all passed tests",
     "~followup_annotations": [
@@ -3591,7 +4002,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -3602,20 +4013,59 @@
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/711/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.1.failed tasks.fuchsia-0001 (attempt 1).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/711/hello.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.1.failed tasks.fuchsia-0001 (attempt 1).failed: /path/to/hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/711/goodbye.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.1.failed tasks.fuchsia-0001 (attempt 1).failed: /path/to/goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
@@ -3762,7 +4212,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -3856,7 +4306,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -3871,7 +4321,7 @@
     "name": "passes.fuchsia-0003.attempt 0 (fail).failed: hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
@@ -3880,7 +4330,7 @@
     "name": "passes.fuchsia-0003.attempt 0 (fail).failed: goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
@@ -3959,7 +4409,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -4060,7 +4510,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -4076,7 +4526,7 @@
     "name": "flakes.fuchsia-0003.attempt 0 (fail).failed: hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
@@ -4086,7 +4536,7 @@
     "name": "flakes.fuchsia-0003.attempt 0 (fail).failed: goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
@@ -4166,7 +4616,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -4269,7 +4719,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -4285,7 +4735,7 @@
     "name": "failures.fuchsia-0001.attempt 0 (fail).failed: hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
@@ -4295,7 +4745,7 @@
     "name": "failures.fuchsia-0001.attempt 0 (fail).failed: goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
@@ -4374,7 +4824,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -4390,7 +4840,7 @@
     "name": "failures.fuchsia-0001.attempt 1 (fail).failed: /path/to/hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
@@ -4400,7 +4850,7 @@
     "name": "failures.fuchsia-0001.attempt 1 (fail).failed: /path/to/goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
diff --git a/recipe_modules/testing/examples/full.expected/test_in_shards_single_attempt.json b/recipe_modules/testing/examples/full.expected/test_in_shards_single_attempt.json
index beac557..5015068 100644
--- a/recipe_modules/testing/examples/full.expected/test_in_shards_single_attempt.json
+++ b/recipe_modules/testing/examples/full.expected/test_in_shards_single_attempt.json
@@ -1616,6 +1616,24 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/610"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.multiplied:fuchsia-0000.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.process results.multiplied:fuchsia-0000.ensure bsdtar",
     "~followup_annotations": [
@@ -1673,6 +1691,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/610"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.multiplied:fuchsia-0000.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.multiplied:fuchsia-0000.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"FAIL\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"FAIL\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/610/out.tar"
     ],
@@ -1724,7 +1783,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -1735,20 +1794,59 @@
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.failed tasks.multiplied:fuchsia-0000 (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/hello.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.0.failed tasks.multiplied:fuchsia-0000 (attempt 0).failed: /path/to/hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/goodbye.out",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "launch/collect.0.failed tasks.multiplied:fuchsia-0000 (attempt 0).failed: /path/to/goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@4@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@"
     ]
   },
@@ -1856,7 +1954,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -1872,7 +1970,7 @@
     "name": "failures.multiplied:fuchsia-0000.attempt 0 (fail).failed: /path/to/hello",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@hello@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
@@ -1882,7 +1980,7 @@
     "name": "failures.multiplied:fuchsia-0000.attempt 0 (fail).failed: /path/to/goodbye",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@goodbye@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
diff --git a/recipe_modules/testing/examples/full.expected/test_with_shards_arm64_serial_failure.json b/recipe_modules/testing/examples/full.expected/test_with_shards_arm64_serial_failure.json
index 0bf0b4f..20921c7 100644
--- a/recipe_modules/testing/examples/full.expected/test_with_shards_arm64_serial_failure.json
+++ b/recipe_modules/testing/examples/full.expected/test_with_shards_arm64_serial_failure.json
@@ -1601,6 +1601,24 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/0"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Vim2.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.process results.Vim2.ensure bsdtar",
     "~followup_annotations": [
@@ -1658,6 +1676,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/0"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Vim2.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/0/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Vim2.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"gn_label\": \"//path/to/hello:hello(//toolchain)\", \"name\": \"hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"gn_label\": \"//path/to/goodbye:goodbye(//toolchain)\", \"name\": \"goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/0/out.tar"
     ],
@@ -1710,7 +1769,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -1721,6 +1780,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/0/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.passed tasks.Vim2 (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.passed tasks.Vim2 (attempt 0).all passed tests",
     "~followup_annotations": [
@@ -1809,7 +1887,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
diff --git a/recipe_modules/testing/examples/full.expected/upload_test_coverage.json b/recipe_modules/testing/examples/full.expected/upload_test_coverage.json
index 1a5bb7c..32d6a1c 100644
--- a/recipe_modules/testing/examples/full.expected/upload_test_coverage.json
+++ b/recipe_modules/testing/examples/full.expected/upload_test_coverage.json
@@ -1355,7 +1355,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@"
     ]
@@ -1504,6 +1504,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_2/1/output.fs",
       "cp",
@@ -1523,6 +1541,28 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/1/output.fs"
     ],
@@ -1559,6 +1599,44 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "run tests.attempt 0.all test results.all passed tests",
     "~followup_annotations": [
diff --git a/recipe_modules/testing/examples/full.expected/upload_to_catapult.json b/recipe_modules/testing/examples/full.expected/upload_to_catapult.json
index 8a8c9ad..ac5ab48 100644
--- a/recipe_modules/testing/examples/full.expected/upload_to_catapult.json
+++ b/recipe_modules/testing/examples/full.expected/upload_to_catapult.json
@@ -1591,6 +1591,24 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/0"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Linux.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.process results.Linux.ensure bsdtar",
     "~followup_annotations": [
@@ -1648,6 +1666,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/0"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Linux.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/0/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/0/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.Linux.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"gn_label\": \"//path/to/hello:hello(//toolchain)\", \"name\": \"hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"gn_label\": \"//path/to/goodbye:goodbye(//toolchain)\", \"name\": \"goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_2/0/out.tar"
     ],
@@ -1699,7 +1758,7 @@
       "@@@STEP_LOG_END@summary.json@@@",
       "@@@STEP_LOG_LINE@symbolized log@hello world!@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LOG_LINE@syslog.txt@extra log contents@@@",
       "@@@STEP_LOG_END@syslog.txt@@@",
@@ -1710,6 +1769,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/0/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.passed tasks.Linux (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.passed tasks.Linux (attempt 0).all passed tests",
     "~followup_annotations": [
diff --git a/recipe_modules/testing/test_api.py b/recipe_modules/testing/test_api.py
index f6d0edf..f9e8283 100644
--- a/recipe_modules/testing/test_api.py
+++ b/recipe_modules/testing/test_api.py
@@ -167,6 +167,7 @@
       enable_retries=True,
       tests_json=None,
       qemu=True,
+      output_dir_contents=None,
   ):
     """Returns mock step data for test results.
 
@@ -182,17 +183,45 @@
       qemu (bool): Whether the tests are being run on QEMU.
 
     Returns:
-      RecipeTestApi.step_data for the extract_results step.
+      RecipeTestApi.step_data for the extract_results step and related steps.
     """
-    result = 'FAIL' if failure else 'PASS'
-
     if shard_name:
-      step_name = 'launch/collect.%d.process results.%s.extract' % (iteration,
-                                                                    shard_name)
+      base_name = 'launch/collect.%d.process results.%s' % (iteration,
+                                                            shard_name)
+      step_name = base_name + '.extract'
+      summary_name = base_name + '.read summary.json'
+      extract_name = base_name + '.get extracted files'
     else:
       step_name = 'extract results'
+      summary_name = 'all test results.read summary.json'
+      extract_name = 'get extracted files'
       if enable_retries:
         step_name = 'run tests.attempt %d.%s' % (iteration, step_name)
+        summary_name = 'run tests.attempt %d.%s' % (iteration, summary_name)
+        extract_name = 'run tests.attempt %d.%s' % (iteration, extract_name)
+    if output_dir_contents is None:
+      output_dir_contents = self._output_dir_contents(
+          failure=failure, tests_json=tests_json, qemu=qemu)
+    steps = self.step_data(extract_name,
+                           self.m.file.listdir(output_dir_contents.keys()))
+    if 'summary.json' in output_dir_contents:
+      steps += self.step_data(
+          summary_name,
+          self.m.file.read_text(output_dir_contents['summary.json']))
+    return steps
+
+  def _output_dir_contents(
+      self,
+      failure=False,
+      tests_json=None,
+      qemu=True,
+  ):
+    """Returns mock data for the test results directory.
+
+    Returns:
+      A map of files in the test results dir to their contents.
+    """
+    result = 'FAIL' if failure else 'PASS'
 
     output_dir_contents = {
         'goodbye.txt': 'goodbye',
@@ -224,8 +253,7 @@
             'goodbye-txt': 'goodbye.txt'
         }
     })
-    return self.step_data(step_name,
-                          self.m.raw_io.output_dir(output_dir_contents))
+    return output_dir_contents
 
   def tests_json_data(self, enable_retries=True, iteration=0, tests=None):
     """Mock the contents of the tests.json file.
diff --git a/recipes/fuchsia/fuchsia.expected/child_build_provided__test_not_in_shards.json b/recipes/fuchsia/fuchsia.expected/child_build_provided__test_not_in_shards.json
index bd4ba56..d6f7cdb 100644
--- a/recipes/fuchsia/fuchsia.expected/child_build_provided__test_not_in_shards.json
+++ b/recipes/fuchsia/fuchsia.expected/child_build_provided__test_not_in_shards.json
@@ -409,7 +409,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@"
     ]
@@ -557,6 +557,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/610"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[CLEANUP]/test-orchestration-inputs_tmp_2/minfs",
       "[CLEANUP]/swarming_tmp_3/610/out.tar",
       "cp",
@@ -576,6 +594,28 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/610"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/610/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/610/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/610/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/610/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/610/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_3/610/out.tar"
     ],
@@ -612,6 +652,44 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/610/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/610/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "run tests.attempt 0.all test results.all passed tests",
     "~followup_annotations": [
diff --git a/recipes/fuchsia/fuchsia.expected/successful_build_and_test.json b/recipes/fuchsia/fuchsia.expected/successful_build_and_test.json
index 98b9ff8..649e0d9 100644
--- a/recipes/fuchsia/fuchsia.expected/successful_build_and_test.json
+++ b/recipes/fuchsia/fuchsia.expected/successful_build_and_test.json
@@ -688,6 +688,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/610"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.QEMU.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[CLEANUP]/test-orchestration-inputs_tmp_2/minfs",
       "[CLEANUP]/swarming_tmp_3/610/out.tar",
       "cp",
@@ -707,6 +725,47 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/610"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.QEMU.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/goodbye.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/hello.out@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/610/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.process results.QEMU.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"/path/to/hello\", \"output_file\": \"hello.out\", \"result\": \"PASS\"}, {\"name\": \"/path/to/goodbye\", \"output_file\": \"goodbye.out\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_3/610/out.tar"
     ],
@@ -759,13 +818,32 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@swarming task@https://example.swarmingserver.appspot.com/task?id=610@@@",
       "@@@STEP_LINK@task outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@"
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/610/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "launch/collect.0.passed tasks.QEMU (attempt 0).read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@4@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "launch/collect.0.passed tasks.QEMU (attempt 0).all passed tests",
     "~followup_annotations": [
@@ -816,7 +894,7 @@
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_LINE@symbolized log@blah@@@",
       "@@@STEP_LOG_END@symbolized log@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@swarming task@https://example.swarmingserver.appspot.com/task?id=610@@@",
       "@@@STEP_LINK@task outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@"
diff --git a/recipes/fuchsia_perf.expected/device_tests.json b/recipes/fuchsia_perf.expected/device_tests.json
index be844c9..d8d5873 100644
--- a/recipes/fuchsia_perf.expected/device_tests.json
+++ b/recipes/fuchsia_perf.expected/device_tests.json
@@ -1989,7 +1989,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@"
     ]
@@ -2136,6 +2136,24 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "run tests.attempt 0.ensure bsdtar",
     "~followup_annotations": [
@@ -2193,6 +2211,26 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_3/1/out.tar"
     ],
@@ -2229,6 +2267,44 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"gn_label\": \"//path/to/benchmark.catapult_json:benchmark.catapult_json(//toolchain)\", \"name\": \"benchmark.catapult_json\", \"output_file\": \"benchmark.catapult_json\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "run tests.attempt 0.all test results.all passed tests",
     "~followup_annotations": [
@@ -2245,13 +2321,31 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "benchmark.catapult_json",
+      "[CLEANUP]/test_results/target/1/benchmark.catapult_json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "read benchmark.catapult_json",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@benchmark.catapult_json@output@@@",
+      "@@@STEP_LOG_END@benchmark.catapult_json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "output",
       "[CLEANUP]/swarming_tmp_3/benchmark.catapult_json"
     ],
     "infra_step": true,
     "name": "save catapult output for benchmark.catapult_json",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@benchmark.catapult_json@benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@benchmark.catapult_json@output@@@",
       "@@@STEP_LOG_END@benchmark.catapult_json@@@"
     ]
   },
diff --git a/recipes/fuchsia_perf.expected/failed_run.json b/recipes/fuchsia_perf.expected/failed_run.json
index 7f27657..f9d65dc 100644
--- a/recipes/fuchsia_perf.expected/failed_run.json
+++ b/recipes/fuchsia_perf.expected/failed_run.json
@@ -1971,7 +1971,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@",
       "@@@STEP_FAILURE@@@"
@@ -2120,6 +2120,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/fuchsia/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_3/1/output.fs",
       "cp",
@@ -2139,6 +2157,26 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_3/1/output.fs"
     ],
@@ -2176,11 +2214,59 @@
     ]
   },
   {
-    "cmd": [],
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"benchmark.catapult_json\", \"output_file\": \"benchmark.catapult_json\", \"result\": \"FAIL\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/benchmark.catapult_json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
     "name": "run tests.attempt 0.all test results.failed: benchmark.catapult_json",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@3@@@",
-      "@@@STEP_LOG_LINE@stdio@benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@stdio@output@@@",
       "@@@STEP_LOG_END@stdio@@@",
       "@@@STEP_FAILURE@@@"
     ]
diff --git a/recipes/fuchsia_perf.expected/missing_test_results.json b/recipes/fuchsia_perf.expected/missing_test_results.json
index 2d6c653..c9674a9 100644
--- a/recipes/fuchsia_perf.expected/missing_test_results.json
+++ b/recipes/fuchsia_perf.expected/missing_test_results.json
@@ -2101,6 +2101,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/fuchsia/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_3/1/output.fs",
       "cp",
@@ -2120,6 +2138,23 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_3/1/output.fs"
     ],
diff --git a/recipes/fuchsia_perf.expected/successful_run.json b/recipes/fuchsia_perf.expected/successful_run.json
index e45c098..a7b4a65 100644
--- a/recipes/fuchsia_perf.expected/successful_run.json
+++ b/recipes/fuchsia_perf.expected/successful_run.json
@@ -1968,7 +1968,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@"
     ]
@@ -2116,6 +2116,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/fuchsia/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_3/1/output.fs",
       "cp",
@@ -2135,6 +2153,26 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_3/1/output.fs"
     ],
@@ -2171,6 +2209,44 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"benchmark.catapult_json\", \"output_file\": \"benchmark.catapult_json\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "run tests.attempt 0.all test results.all passed tests",
     "~followup_annotations": [
@@ -2187,13 +2263,31 @@
       "--json-output",
       "/path/to/tmp/json",
       "copy",
-      "benchmark.catapult_json",
+      "[CLEANUP]/test_results/target/1/benchmark.catapult_json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "read benchmark.catapult_json",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@benchmark.catapult_json@output@@@",
+      "@@@STEP_LOG_END@benchmark.catapult_json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "output",
       "[CLEANUP]/swarming_tmp_3/benchmark.catapult_json"
     ],
     "infra_step": true,
     "name": "save catapult output for benchmark.catapult_json",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@benchmark.catapult_json@benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@benchmark.catapult_json@output@@@",
       "@@@STEP_LOG_END@benchmark.catapult_json@@@"
     ]
   },
diff --git a/recipes/fuchsia_perf.expected/with_patch.json b/recipes/fuchsia_perf.expected/with_patch.json
index f669855..9a6d15d 100644
--- a/recipes/fuchsia_perf.expected/with_patch.json
+++ b/recipes/fuchsia_perf.expected/with_patch.json
@@ -2057,7 +2057,7 @@
       "@@@STEP_LOG_LINE@summary.json@  ]@@@",
       "@@@STEP_LOG_LINE@summary.json@}@@@",
       "@@@STEP_LOG_END@summary.json@@@",
-      "@@@STEP_LOG_LINE@goodbye-txt@goodbye@@@",
+      "@@@STEP_LOG_LINE@goodbye-txt@output@@@",
       "@@@STEP_LOG_END@goodbye-txt@@@",
       "@@@STEP_LINK@test outputs@https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=abc123@@@"
     ]
@@ -2205,6 +2205,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/fuchsia/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_3/1/output.fs",
       "cp",
@@ -2224,6 +2242,26 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/benchmark.catapult_json@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/goodbye.txt@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_3/1/output.fs"
     ],
@@ -2260,6 +2298,44 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"outputs\": {\"goodbye-txt\": \"goodbye.txt\"}, \"tests\": [{\"name\": \"benchmark.catapult_json\", \"output_file\": \"benchmark.catapult_json\", \"result\": \"PASS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/goodbye.txt",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "run tests.attempt 0.all test results.read goodbye.txt",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@goodbye.txt@output@@@",
+      "@@@STEP_LOG_END@goodbye.txt@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "run tests.attempt 0.all test results.all passed tests",
     "~followup_annotations": [
diff --git a/recipes/fuchsia_perf.py b/recipes/fuchsia_perf.py
index 458d0ee..f6f181e 100644
--- a/recipes/fuchsia_perf.py
+++ b/recipes/fuchsia_perf.py
@@ -226,14 +226,14 @@
 
   # Upload results for all of the benchmarks that ran successfully.
   if not api.buildbucket_util.is_tryjob:
-    for test_name, file_data in test_results.passed_test_outputs.iteritems():
+    for test_name, output_file in test_results.passed_test_outputs.iteritems():
       if api.catapult.is_catapult_file(test_name):
         # Save Catapult files to the test results output dir so they get
         # uploaded by upload_results().
         api.file.write_text(
             'save catapult output for %s' % test_name,
             test_results.output_dir.join(test_name),
-            file_data,
+            test_results.get_output(output_file),
         )
     test_results.upload_results(
         gcs_bucket, upload_to_catapult=upload_to_dashboard)
@@ -362,7 +362,6 @@
       tests_json=tests_json,
       steps=[
           buildbucket_get_response,
-          api.step_data('run tests.attempt 0.extract results',
-                        api.raw_io.output_dir({})),
       ],
+      output_dir_contents={},
   )
diff --git a/recipes/fuchsia_perfcompare.expected/successful_run.json b/recipes/fuchsia_perfcompare.expected/successful_run.json
index dc79048..9be35fe 100644
--- a/recipes/fuchsia_perfcompare.expected/successful_run.json
+++ b/recipes/fuchsia_perfcompare.expected/successful_run.json
@@ -3967,6 +3967,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"before\" revision.boot 1 of 2.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/fuchsia/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_3/1/output.fs",
       "cp",
@@ -3986,6 +4004,24 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"before\" revision.boot 1 of 2.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_3/1/output.fs"
     ],
@@ -4031,6 +4067,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"before\" revision.boot 1 of 2.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"tests\": [{\"name\": \"perfcompare_benchmark.catapult_json\", \"result\": \"SUCCESS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "collect results for \"before\" revision.boot 1 of 2.all test results.all passed tests",
     "~followup_annotations": [
@@ -4159,6 +4214,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"before\" revision.boot 2 of 2.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/fuchsia/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_4/1/output.fs",
       "cp",
@@ -4178,6 +4251,24 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"before\" revision.boot 2 of 2.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_4/1/output.fs"
     ],
@@ -4223,6 +4314,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"before\" revision.boot 2 of 2.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"tests\": [{\"name\": \"perfcompare_benchmark.catapult_json\", \"result\": \"SUCCESS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "collect results for \"before\" revision.boot 2 of 2.all test results.all passed tests",
     "~followup_annotations": [
@@ -4366,6 +4476,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"after\" revision.boot 1 of 2.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/fuchsia/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_6/1/output.fs",
       "cp",
@@ -4385,6 +4513,24 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"after\" revision.boot 1 of 2.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_6/1/output.fs"
     ],
@@ -4430,6 +4576,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"after\" revision.boot 1 of 2.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"tests\": [{\"name\": \"perfcompare_benchmark.catapult_json\", \"result\": \"SUCCESS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "collect results for \"after\" revision.boot 1 of 2.all test results.all passed tests",
     "~followup_annotations": [
@@ -4558,6 +4723,24 @@
   },
   {
     "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "ensure-directory",
+      "--mode",
+      "0777",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"after\" revision.boot 2 of 2.create test results dir",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@"
+    ]
+  },
+  {
+    "cmd": [
       "[START_DIR]/fuchsia/out/default.zircon/host-linux-x64/minfs",
       "[CLEANUP]/swarming_tmp_7/1/output.fs",
       "cp",
@@ -4577,6 +4760,24 @@
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
       "--json-output",
       "/path/to/tmp/json",
+      "listdir",
+      "[CLEANUP]/test_results/target/1"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"after\" revision.boot 2 of 2.get extracted files",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@listdir@[CLEANUP]/test_results/target/1/summary.json@@@",
+      "@@@STEP_LOG_END@listdir@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
       "remove",
       "[CLEANUP]/swarming_tmp_7/1/output.fs"
     ],
@@ -4622,6 +4823,25 @@
     ]
   },
   {
+    "cmd": [
+      "vpython",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[CLEANUP]/test_results/target/1/summary.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "collect results for \"after\" revision.boot 2 of 2.all test results.read summary.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@3@@@",
+      "@@@STEP_LOG_LINE@summary.json@{\"tests\": [{\"name\": \"perfcompare_benchmark.catapult_json\", \"result\": \"SUCCESS\"}]}@@@",
+      "@@@STEP_LOG_END@summary.json@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "collect results for \"after\" revision.boot 2 of 2.all test results.all passed tests",
     "~followup_annotations": [
diff --git a/recipes/fuchsia_perfcompare.py b/recipes/fuchsia_perfcompare.py
index 17a64a6..a31be54 100644
--- a/recipes/fuchsia_perfcompare.py
+++ b/recipes/fuchsia_perfcompare.py
@@ -310,13 +310,16 @@
                 'result': 'SUCCESS',
             }]
         }
-        steps.append(
+        steps.extend([
             api.step_data(
-                base_name + '.extract results',
-                api.raw_io.output_dir({
-                    'summary.json': api.json.dumps(summary_data),
-                }),
-            ))
+                base_name + '.get extracted files',
+                api.file.listdir(['summary.json']),
+            ),
+            api.step_data(
+                base_name + '.all test results.read summary.json',
+                api.file.read_text(api.json.dumps(summary_data),),
+            )
+        ])
     del task_result_step.step_data['collect']
     return steps