Esempio n. 1
0
 def test_adds_missing_inits_and_strips_source_roots(self) -> None:
     target_with_init = self.make_hydrated_target(source_paths=[
         "src/python/project/lib.py", "src/python/project/__init__.py"
     ], )
     target_without_init = self.make_hydrated_target(source_paths=[
         "tests/python/test_project/f1.py",
         "tests/python/test_project/f2.py"
     ], )
     files_target = self.make_hydrated_target(
         source_paths=["src/python/project/resources/loose_file.txt"],
         type_alias=Files.alias(),
     )
     result = self.request_single_product(
         ChrootedPythonSources,
         Params(
             HydratedTargets(
                 [target_with_init, target_without_init, files_target]),
             create_options_bootstrapper(),
         ),
     )
     assert sorted(result.snapshot.files) == sorted([
         "project/lib.py",
         "project/__init__.py",
         "test_project/f1.py",
         "test_project/f2.py",
         "test_project/__init__.py",
         "src/python/project/resources/loose_file.txt",
     ])
Esempio n. 2
0
    def register_options(cls, register):
        super().register_options(register)

        # TODO(John Sirois): Implement sanity checks on options wrt caching:
        # https://github.com/pantsbuild/pants/issues/5073

        register(
            "--fast",
            type=bool,
            default=False,
            fingerprint=True,
            removal_version="1.28.0.dev0",
            removal_hint=(
                "This option is going away for better isolation of tests, which provides "
                "better caching. This also prepares for upgrading to the V2 test implementation,"
                "which provides even better caching and parallelism.\n\nWe recommend running a "
                "full CI suite with `no-fast` (the default now) to see if any tests fail. If any "
                "fail, this likely signals shared state between your test targets."
            ),
            help="Run all tests in a single invocation. If turned off, each test target "
            "will run in its own invocation, which will be slower, but isolates "
            "tests from process-wide state created by tests in other targets.",
        )
        register(
            "--chroot",
            advanced=True,
            fingerprint=True,
            type=bool,
            default=True,
            help="Run tests in a chroot. Any loose files tests depend on via `{}` dependencies "
            "will be copied to the chroot.".format(Files.alias()),
        )
Esempio n. 3
0
async def strip_source_root(
        hydrated_target: HydratedTarget,
        source_root_config: SourceRootConfig) -> SourceRootStrippedSources:
    """Relativize targets to their source root, e.g.
  `src/python/pants/util/strutil.py` -> `pants/util/strutil.py ."""

    target_adaptor = hydrated_target.adaptor
    source_roots = source_root_config.get_source_roots()

    # TODO: make TargetAdaptor return a 'sources' field with an empty snapshot instead of raising to
    # simplify the hasattr() checks here!
    if not hasattr(target_adaptor, 'sources'):
        return SourceRootStrippedSources(snapshot=EMPTY_SNAPSHOT)

    digest = target_adaptor.sources.snapshot.directory_digest
    source_root = source_roots.find_by_path(target_adaptor.address.spec_path)
    if source_root is None:
        # If we found no source root, use the target's dir.
        # Note that when --source-unmatched is 'create' (the default) we'll never return None,
        # but will return the target's dir. This check allows this code to work even if
        # --source-unmatched is 'fail'.
        source_root_path = target_adaptor.address.spec_path
    else:
        source_root_path = source_root.path

    # Loose `Files`, as opposed to `Resources` or `Target`s, have no (implied) package
    # structure and so we do not remove their source root like we normally do, so that filesystem
    # APIs may still access the files. See pex_build_util.py's `_create_source_dumper`.
    if target_adaptor.type_alias == Files.alias():
        source_root_path = ''

    resulting_digest = await Get[Digest](DirectoryWithPrefixToStrip(
        directory_digest=digest, prefix=source_root_path))
    resulting_snapshot = await Get[Snapshot](Digest, resulting_digest)
    return SourceRootStrippedSources(snapshot=resulting_snapshot)
Esempio n. 4
0
async def strip_source_root(
        hydrated_target: HydratedTarget,
        source_root_config: SourceRootConfig) -> SourceRootStrippedSources:
    """Relativize targets to their source root, e.g.
  `src/python/pants/util/strutil.py` -> `pants/util/strutil.py ."""

    target_adaptor = hydrated_target.adaptor
    source_roots = source_root_config.get_source_roots()

    # TODO: make TargetAdaptor return a 'sources' field with an empty snapshot instead of raising to
    # simplify the hasattr() checks here!
    if not hasattr(target_adaptor, 'sources'):
        return SourceRootStrippedSources(snapshot=EMPTY_SNAPSHOT)

    digest = target_adaptor.sources.snapshot.directory_digest
    source_root = source_roots.find_by_path(target_adaptor.address.spec_path)

    # Loose `Files`, as opposed to `Resources` or `Target`s, have no (implied) package
    # structure and so we do not remove their source root like we normally do, so that filesystem
    # APIs may still access the files. See pex_build_util.py's `_create_source_dumper`.
    if target_adaptor.type_alias == Files.alias():
        source_root = None

    resulting_digest = await Get(
        Digest,
        DirectoryWithPrefixToStrip(
            directory_digest=digest,
            prefix=source_root.path if source_root else ""))
    resulting_snapshot = await Get(Snapshot, Digest, resulting_digest)
    return SourceRootStrippedSources(snapshot=resulting_snapshot)
Esempio n. 5
0
  def register_options(cls, register):
    super(JUnitRun, cls).register_options(register)

    register('--batch-size', advanced=True, type=int, default=sys.maxint, fingerprint=True,
             help='Run at most this many tests in a single test process.')
    register('--test', type=list, fingerprint=True,
             help='Force running of just these tests.  Tests can be specified using any of: '
                  '[classname], [classname]#[methodname], [filename] or [filename]#[methodname]')
    register('--per-test-timer', type=bool, help='Show progress and timer for each test.')
    register('--default-concurrency', advanced=True, fingerprint=True,
             choices=JUnitTests.VALID_CONCURRENCY_OPTS, default=JUnitTests.CONCURRENCY_SERIAL,
             help='Set the default concurrency mode for running tests not annotated with'
                  ' @TestParallel or @TestSerial.')
    register('--parallel-threads', advanced=True, type=int, default=0, fingerprint=True,
             help='Number of threads to run tests in parallel. 0 for autoset.')
    register('--test-shard', advanced=True, fingerprint=True,
             help='Subset of tests to run, in the form M/N, 0 <= M < N. '
                  'For example, 1/3 means run tests number 2, 5, 8, 11, ...')
    register('--output-mode', choices=['ALL', 'FAILURE_ONLY', 'NONE'], default='NONE',
             help='Specify what part of output should be passed to stdout. '
                  'In case of FAILURE_ONLY and parallel tests execution '
                  'output can be partial or even wrong. '
                  'All tests output also redirected to files in .pants.d/test/junit.')
    register('--cwd', advanced=True, fingerprint=True,
             help='Set the working directory. If no argument is passed, use the build root. '
                  'If cwd is set on a target, it will supersede this option. It is an error to '
                  'use this option in combination with `--chroot`')
    register('--chroot', advanced=True, fingerprint=True, type=bool, default=False,
             help='Run tests in a chroot. Any loose files tests depend on via `{}` dependencies '
                  'will be copied to the chroot. If cwd is set on a target, it will supersede this'
                  'option. It is an error to use this option in combination with `--cwd`'
                  .format(Files.alias()))
    register('--strict-jvm-version', type=bool, advanced=True, fingerprint=True,
             help='If true, will strictly require running junits with the same version of java as '
                  'the platform -target level. Otherwise, the platform -target level will be '
                  'treated as the minimum jvm to run.')
    register('--failure-summary', type=bool, default=True,
             help='If true, includes a summary of which test-cases failed at the end of a failed '
                  'junit run.')
    register('--allow-empty-sources', type=bool, advanced=True, fingerprint=True,
             help='Allows a junit_tests() target to be defined with no sources.  Otherwise,'
                  'such a target will raise an error during the test run.')
    register('--use-experimental-runner', type=bool, advanced=True, fingerprint=True,
             help='Use experimental junit-runner logic for more options for parallelism.')
    register('--html-report', type=bool, fingerprint=True,
             help='If true, generate an html summary report of tests that were run.')
    register('--open', type=bool, fingerprint=True,
             help='Attempt to open the html summary report in a browser (implies --html-report)')

    # TODO(John Sirois): Remove direct register when coverage steps are moved to their own tasks.
    Cobertura.register_options(register, cls.register_jvm_tool)
  def register_options(cls, register):
    super(PartitionedTestRunnerTaskMixin, cls).register_options(register)

    # TODO(John Sirois): Implement sanity checks on options wrt caching:
    # https://github.com/pantsbuild/pants/issues/5073

    register('--fast', type=bool, default=True, fingerprint=True,
             help='Run all tests in a single pytest invocation. If turned off, each test target '
                  'will run in its own pytest invocation, which will be slower, but isolates '
                  'tests from process-wide state created by tests in other targets.')
    register('--chroot', advanced=True, fingerprint=True, type=bool, default=False,
             help='Run tests in a chroot. Any loose files tests depend on via `{}` dependencies '
                  'will be copied to the chroot.'
             .format(Files.alias()))
Esempio n. 7
0
  def register_options(cls, register):
    super(PartitionedTestRunnerTaskMixin, cls).register_options(register)

    # TODO(John Sirois): Implement sanity checks on options wrt caching:
    # https://github.com/pantsbuild/pants/issues/5073

    register('--fast', type=bool, default=True, fingerprint=True,
             help='Run all tests in a single pytest invocation. If turned off, each test target '
                  'will run in its own pytest invocation, which will be slower, but isolates '
                  'tests from process-wide state created by tests in other targets.')
    register('--chroot', advanced=True, fingerprint=True, type=bool, default=False,
             help='Run tests in a chroot. Any loose files tests depend on via `{}` dependencies '
                  'will be copied to the chroot.'
             .format(Files.alias()))
Esempio n. 8
0
    def test_legacy_strip_target(self) -> None:
        def get_stripped_files_for_target(
            *,
            source_paths: Optional[List[str]],
            type_alias: Optional[str] = None,
            specified_sources: Optional[List[str]] = None,
        ) -> List[str]:
            address = (Address(spec_path=PurePath(
                source_paths[0]).parent.as_posix(),
                               target_name="target") if source_paths else
                       Address.parse("src/python/project:target"))
            sources = Mock()
            sources.snapshot = self.make_snapshot_of_empty_files(source_paths
                                                                 or [])
            specified_sources_snapshot = (
                None if not specified_sources else
                self.make_snapshot_of_empty_files(specified_sources))
            return self.get_stripped_files(
                LegacyStripTargetRequest(
                    TargetAdaptor(address=address,
                                  type_alias=type_alias,
                                  sources=sources),
                    specified_files_snapshot=specified_sources_snapshot,
                ))

        # normal target
        assert get_stripped_files_for_target(source_paths=[
            "src/python/project/f1.py", "src/python/project/f2.py"
        ]) == sorted(["project/f1.py", "project/f2.py"])

        # empty target
        assert get_stripped_files_for_target(source_paths=None) == []

        # files targets are not stripped
        assert get_stripped_files_for_target(
            source_paths=["src/python/project/f1.py"],
            type_alias=Files.alias(),
        ) == ["src/python/project/f1.py"]

        # When given `specified_files_snapshot`, only strip what is specified, even if that snapshot
        # has files not belonging to the target! (Validation of ownership would be too costly.)
        assert get_stripped_files_for_target(
            source_paths=["src/python/project/f1.py"],
            specified_sources=[
                "src/python/project/f1.py",
                "src/python/project/different_owner.py"
            ],
        ) == sorted(["project/f1.py", "project/different_owner.py"])
Esempio n. 9
0
    def register_options(cls, register):
        super().register_options(register)

        # TODO(John Sirois): Implement sanity checks on options wrt caching:
        # https://github.com/pantsbuild/pants/issues/5073

        register(
            "--chroot",
            advanced=True,
            fingerprint=True,
            type=bool,
            default=True,
            help=
            "Run tests in a chroot. Any loose files tests depend on via `{}` dependencies "
            "will be copied to the chroot.".format(Files.alias()),
        )
    def test_strip_source_roots(self) -> None:
        target1 = self.mock_target(SOURCES1)
        target2 = self.mock_target(SOURCES2)
        target3 = self.mock_target(SOURCES3)

        # We must be careful to not strip source roots for `files` targets.
        files_target = self.mock_target(SOURCES1, type_alias=Files.alias())
        files_expected = SOURCES1.source_file_absolute_paths

        def assert_source_roots_stripped(
            target: TargetAdaptorWithOrigin, sources: TargetSources
        ) -> None:
            expected = sources.source_files
            assert self.get_all_source_files([target], strip_source_roots=True) == expected
            assert self.get_specified_source_files([target], strip_source_roots=True) == expected

        assert_source_roots_stripped(target1, SOURCES1)
        assert_source_roots_stripped(target2, SOURCES2)
        assert_source_roots_stripped(target3, SOURCES3)

        assert self.get_all_source_files([files_target], strip_source_roots=True) == files_expected
        assert (
            self.get_specified_source_files([files_target], strip_source_roots=True)
            == files_expected
        )

        combined_targets = [target1, target2, target3, files_target]
        combined_expected = sorted(
            [
                *SOURCES1.source_files,
                *SOURCES2.source_files,
                *SOURCES3.source_files,
                *files_expected,
            ],
        )
        assert (
            self.get_all_source_files(combined_targets, strip_source_roots=True)
            == combined_expected
        )
        assert (
            self.get_specified_source_files(combined_targets, strip_source_roots=True)
            == combined_expected
        )
Esempio n. 11
0
  def register_options(cls, register):
    super(PytestRun, cls).register_options(register)
    register('--fast', type=bool, default=True, fingerprint=True,
             help='Run all tests in a single pytest invocation. If turned off, each test target '
                  'will run in its own pytest invocation, which will be slower, but isolates '
                  'tests from process-wide state created by tests in other targets.')

    register('--chroot', advanced=True, fingerprint=True, type=bool, default=False,
             help='Run tests in a chroot. Any loose files tests depend on via `{}` dependencies '
                  'will be copied to the chroot.'
             .format(Files.alias()))

    # NB: We always produce junit xml privately, and if this option is specified, we then copy
    # it to the user-specified directory, post any interaction with the cache to retrieve the
    # privately generated and cached xml files. As such, this option is not part of the
    # fingerprint.
    register('--junit-xml-dir', metavar='<DIR>',
             help='Specifying a directory causes junit xml results files to be emitted under '
                  'that dir for each test run.')

    register('--profile', metavar='<FILE>', fingerprint=True,
             help="Specifying a file path causes tests to be profiled with the profiling data "
                  "emitted to that file (prefix). Note that tests may run in a different cwd, so "
                  "it's best to use an absolute path to make it easy to find the subprocess "
                  "profiles later.")

    register('--options', type=list, fingerprint=True, help='Pass these options to pytest.')

    register('--coverage', fingerprint=True,
             help='Emit coverage information for specified packages or directories (absolute or '
                  'relative to the build root).  The special value "auto" indicates that Pants '
                  'should attempt to deduce which packages to emit coverage for.')
    # For a given --coverage specification (which is fingerprinted), we will always copy the
    # associated generated and cached --coverage files to this directory post any interaction with
    # the cache to retrieve the coverage files. As such, this option is not part of the fingerprint.
    register('--coverage-output-dir', metavar='<DIR>', default=None,
             help='Directory to emit coverage reports to. '
             'If not specified, a default within dist is used.')

    register('--test-shard', fingerprint=True,
             help='Subset of tests to run, in the form M/N, 0 <= M < N. For example, 1/3 means '
                  'run tests number 2, 5, 8, 11, ...')
Esempio n. 12
0
async def legacy_strip_source_roots_from_target(
    request: LegacyStripTargetRequest, ) -> LegacySourceRootStrippedSources:
    """Remove source roots from a target, e.g. `src/python/pants/util/strutil.py` ->
    `pants/util/strutil.py`."""
    target_adaptor = request.adaptor

    if not target_adaptor.has_sources():
        return LegacySourceRootStrippedSources(snapshot=EMPTY_SNAPSHOT)

    sources_snapshot = request.specified_files_snapshot or target_adaptor.sources.snapshot

    # Loose `Files`, as opposed to `Resources` or `Target`s, have no (implied) package
    # structure and so we do not remove their source root like we normally do, so that filesystem
    # APIs may still access the files. See pex_build_util.py's `_create_source_dumper`.
    if target_adaptor.type_alias == Files.alias():
        return LegacySourceRootStrippedSources(sources_snapshot)

    result = await Get[SourceRootStrippedSources](StripSnapshotRequest(
        sources_snapshot,
        representative_path=representative_path_from_address(
            target_adaptor.address),
    ))
    return LegacySourceRootStrippedSources(result.snapshot)
Esempio n. 13
0
async def strip_source_roots_from_target(
    request: StripTargetRequest, ) -> SourceRootStrippedSources:
    """Remove source roots from a target, e.g. `src/python/pants/util/strutil.py` ->
    `pants/util/strutil.py`."""
    target_adaptor = request.adaptor

    if not target_adaptor.has_sources():
        return SourceRootStrippedSources(snapshot=EMPTY_SNAPSHOT)

    sources_snapshot = request.specified_files_snapshot or target_adaptor.sources.snapshot

    # Loose `Files`, as opposed to `Resources` or `Target`s, have no (implied) package
    # structure and so we do not remove their source root like we normally do, so that filesystem
    # APIs may still access the files. See pex_build_util.py's `_create_source_dumper`.
    if target_adaptor.type_alias == Files.alias():
        return SourceRootStrippedSources(sources_snapshot)

    # NB: We generate a synthetic representative_path for the target as a performance hack so that
    # we don't need to call SourceRoots.find_by_path() on every single file belonging to the
    # target's `sources`.
    representative_path = PurePath(target_adaptor.address.spec_path,
                                   "BUILD").as_posix()
    return await Get[SourceRootStrippedSources](StripSnapshotRequest(
        sources_snapshot, representative_path=representative_path))
Esempio n. 14
0
 def test_dont_strip_source_for_files(self):
     self.assert_stripped_source_file(
         original_path='src/python/pants/util/strutil.py',
         expected_path='src/python/pants/util/strutil.py',
         target_type_alias=Files.alias())
Esempio n. 15
0
    def register_options(cls, register):
        super(JUnitRun, cls).register_options(register)

        register(
            '--fast',
            type=bool,
            default=True,
            fingerprint=True,
            help=
            'Run all tests in a single junit invocation. If turned off, each test target '
            'will run in its own junit invocation, which will be slower, but isolates '
            'tests from process-wide state created by tests in other targets.')
        register('--batch-size',
                 advanced=True,
                 type=int,
                 default=cls._BATCH_ALL,
                 fingerprint=True,
                 help='Run at most this many tests in a single test process.')
        register(
            '--test',
            type=list,
            fingerprint=True,
            help=
            'Force running of just these tests.  Tests can be specified using any of: '
            '[classname], [classname]#[methodname], [filename] or [filename]#[methodname]'
        )
        register('--per-test-timer',
                 type=bool,
                 help='Show progress and timer for each test.')
        register(
            '--default-concurrency',
            advanced=True,
            fingerprint=True,
            choices=JUnitTests.VALID_CONCURRENCY_OPTS,
            default=JUnitTests.CONCURRENCY_SERIAL,
            help=
            'Set the default concurrency mode for running tests not annotated with'
            ' @TestParallel or @TestSerial.')
        register(
            '--parallel-threads',
            advanced=True,
            type=int,
            default=0,
            fingerprint=True,
            help='Number of threads to run tests in parallel. 0 for autoset.')
        register('--test-shard',
                 advanced=True,
                 fingerprint=True,
                 help='Subset of tests to run, in the form M/N, 0 <= M < N. '
                 'For example, 1/3 means run tests number 2, 5, 8, 11, ...')
        register(
            '--output-mode',
            choices=['ALL', 'FAILURE_ONLY', 'NONE'],
            default='NONE',
            help='Specify what part of output should be passed to stdout. '
            'In case of FAILURE_ONLY and parallel tests execution '
            'output can be partial or even wrong. '
            'All tests output also redirected to files in .pants.d/test/junit.'
        )
        register(
            '--cwd',
            advanced=True,
            fingerprint=True,
            help=
            'Set the working directory. If no argument is passed, use the build root. '
            'If cwd is set on a target, it will supersede this option. It is an error to '
            'use this option in combination with `--chroot`')
        register(
            '--chroot',
            advanced=True,
            fingerprint=True,
            type=bool,
            default=False,
            help=
            'Run tests in a chroot. Any loose files tests depend on via `{}` dependencies '
            'will be copied to the chroot. If cwd is set on a target, it will supersede this'
            'option. It is an error to use this option in combination with `--cwd`'
            .format(Files.alias()))
        register(
            '--strict-jvm-version',
            type=bool,
            advanced=True,
            fingerprint=True,
            help=
            'If true, will strictly require running junits with the same version of java as '
            'the platform -target level. Otherwise, the platform -target level will be '
            'treated as the minimum jvm to run.')
        register(
            '--failure-summary',
            type=bool,
            default=True,
            help=
            'If true, includes a summary of which test-cases failed at the end of a failed '
            'junit run.')
        register(
            '--allow-empty-sources',
            type=bool,
            advanced=True,
            fingerprint=True,
            help=
            'Allows a junit_tests() target to be defined with no sources.  Otherwise,'
            'such a target will raise an error during the test run.')
        register(
            '--use-experimental-runner',
            type=bool,
            advanced=True,
            fingerprint=True,
            help=
            'Use experimental junit-runner logic for more options for parallelism.'
        )
        register(
            '--html-report',
            type=bool,
            fingerprint=True,
            help=
            'If true, generate an html summary report of tests that were run.')
        register(
            '--open',
            type=bool,
            help=
            'Attempt to open the html summary report in a browser (implies --html-report)'
        )
        register(
            '--legacy-report-layout',
            type=bool,
            default=True,
            advanced=True,
            help='Links JUnit and coverage reports to the legacy location.')

        # TODO(jtrobec): Remove direct register when coverage steps are moved to their own subsystem.
        CodeCoverage.register_junit_options(register, cls.register_jvm_tool)
Esempio n. 16
0
def run_python_test(test_target, pytest, python_setup, source_root_config, subprocess_encoding_environment):
  """Runs pytest for one target."""

  # TODO(7726): replace this with a proper API to get the `closure` for a
  # TransitiveHydratedTarget.
  transitive_hydrated_targets = yield Get(
    TransitiveHydratedTargets, BuildFileAddresses((test_target.address,))
  )
  all_targets = [t.adaptor for t in transitive_hydrated_targets.closure]

  interpreter_constraints = {
    constraint
    for target_adaptor in all_targets
    for constraint in python_setup.compatibility_or_constraints(
      getattr(target_adaptor, 'compatibility', None)
    )
  }

  # Produce a pex containing pytest and all transitive 3rdparty requirements.
  output_pytest_requirements_pex_filename = 'pytest-with-requirements.pex'
  all_target_requirements = []
  for maybe_python_req_lib in all_targets:
    # This is a python_requirement()-like target.
    if hasattr(maybe_python_req_lib, 'requirement'):
      all_target_requirements.append(str(maybe_python_req_lib.requirement))
    # This is a python_requirement_library()-like target.
    if hasattr(maybe_python_req_lib, 'requirements'):
      for py_req in maybe_python_req_lib.requirements:
        all_target_requirements.append(str(py_req.requirement))
  all_requirements = all_target_requirements + list(pytest.get_requirement_strings())
  resolved_requirements_pex = yield Get(
    RequirementsPex, RequirementsPexRequest(
      output_filename=output_pytest_requirements_pex_filename,
      requirements=tuple(sorted(all_requirements)),
      interpreter_constraints=tuple(sorted(interpreter_constraints)),
      entry_point="pytest:main",
    )
  )

  # Gather sources and adjust for source roots.
  # TODO: make TargetAdaptor return a 'sources' field with an empty snapshot instead of raising to
  # simplify the hasattr() checks here!
  source_roots = source_root_config.get_source_roots()
  sources_digest_to_source_roots: Dict[Digest, Optional[SourceRoot]] = {}
  for maybe_source_target in all_targets:
    if not hasattr(maybe_source_target, 'sources'):
      continue
    digest = maybe_source_target.sources.snapshot.directory_digest
    source_root = source_roots.find_by_path(maybe_source_target.address.spec_path)
    if maybe_source_target.type_alias == Files.alias():
      # Loose `Files`, as opposed to `Resources` or `PythonTarget`s, have no (implied) package
      # structure and so we do not remove their source root like we normally do, so that Python
      # filesystem APIs may still access the files. See pex_build_util.py's `_create_source_dumper`.
      source_root = None
    sources_digest_to_source_roots[digest] = source_root.path if source_root else ""

  stripped_sources_digests = yield [
    Get(Digest, DirectoryWithPrefixToStrip(directory_digest=digest, prefix=source_root))
    for digest, source_root in sources_digest_to_source_roots.items()
  ]

  sources_digest = yield Get(
    Digest, DirectoriesToMerge(directories=tuple(stripped_sources_digests)),
  )

  inits_digest = yield Get(InjectedInitDigest, Digest, sources_digest)

  all_input_digests = [
    sources_digest,
    inits_digest.directory_digest,
    resolved_requirements_pex.directory_digest,
  ]
  merged_input_files = yield Get(
    Digest,
    DirectoriesToMerge,
    DirectoriesToMerge(directories=tuple(all_input_digests)),
  )

  interpreter_search_paths = create_path_env_var(python_setup.interpreter_search_paths)
  pex_exe_env = {
    'PATH': interpreter_search_paths,
    **subprocess_encoding_environment.invocation_environment_dict
  }

  # NB: we use the hardcoded and generic bin name `python`, rather than something dynamic like
  # `sys.executable`, to ensure that the interpreter may be discovered both locally and in remote
  # execution (so long as `env` is populated with a `PATH` env var and `python` is discoverable
  # somewhere on that PATH). This is only used to run the downloaded PEX tool; it is not
  # necessarily the interpreter that PEX will use to execute the generated .pex file.
  request = ExecuteProcessRequest(
    argv=("python", f'./{output_pytest_requirements_pex_filename}'),
    env=pex_exe_env,
    input_files=merged_input_files,
    description=f'Run Pytest for {test_target.address.reference()}',
  )

  result = yield Get(FallibleExecuteProcessResult, ExecuteProcessRequest, request)
  status = Status.SUCCESS if result.exit_code == 0 else Status.FAILURE

  yield TestResult(
    status=status,
    stdout=result.stdout.decode(),
    stderr=result.stderr.decode(),
  )