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", ])
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()), )
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)
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)
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()))
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"])
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 )
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, ...')
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)
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))
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())
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)
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(), )