Skip to content

Commit

Permalink
Added support for Pytest config files (#11624)
Browse files Browse the repository at this point in the history
Closes #11526 : this adds support for a pytest.ini config file and an associated test. Pytest does not offer support for directly specifying a path for a config file, though this can be done indirectly using `--rootdir=path`, which may bring in other issues; however, this can be implemented if need be. 

[ci skip-rust]
[ci skip-build-wheels]
  • Loading branch information
wilsonliam committed Mar 8, 2021
1 parent 16040c9 commit e7e594c
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 3 deletions.
29 changes: 27 additions & 2 deletions src/python/pants/backend/python/goals/pytest_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,15 @@
)
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.addresses import UnparsedAddressInputs
from pants.engine.fs import AddPrefix, Digest, DigestSubset, MergeDigests, PathGlobs, Snapshot
from pants.engine.fs import (
AddPrefix,
Digest,
DigestSubset,
GlobMatchErrorBehavior,
MergeDigests,
PathGlobs,
Snapshot,
)
from pants.engine.process import (
FallibleProcessResult,
InteractiveProcess,
Expand Down Expand Up @@ -133,6 +141,15 @@ async def setup_pytest_for_target(
),
)

config_digest_request = Get(
Digest,
PathGlobs(
globs=[pytest.config] if pytest.config else [],
glob_match_error_behavior=GlobMatchErrorBehavior.error,
description_of_origin="the option `--pytest-config`",
),
)

prepared_sources_request = Get(
PythonSourceFiles, PythonSourceFilesRequest(all_targets, include_files=True)
)
Expand Down Expand Up @@ -161,11 +178,18 @@ async def setup_pytest_for_target(
SourceFiles, SourceFilesRequest([request.field_set.sources])
)

pytest_pex, requirements_pex, prepared_sources, field_set_source_files = await MultiGet(
(
pytest_pex,
requirements_pex,
prepared_sources,
field_set_source_files,
config_digest,
) = await MultiGet(
pytest_pex_request,
requirements_pex_request,
prepared_sources_request,
field_set_source_files_request,
config_digest_request,
)

pytest_runner_pex = await Get(
Expand All @@ -187,6 +211,7 @@ async def setup_pytest_for_target(
(
coverage_config.digest,
prepared_sources.source_files.snapshot.digest,
config_digest,
*(binary.digest for binary in assets),
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def rule_runner() -> RuleRunner:
SOURCE_ROOT = "tests/python"
PACKAGE = os.path.join(SOURCE_ROOT, "pants_test")
GOOD_SOURCE = FileContent(f"{PACKAGE}/test_good.py", b"def test():\n pass\n")
GOOD_WITH_PRINT = FileContent(f"{PACKAGE}/test_good.py", b"def test():\n print('All good!')")
BAD_SOURCE = FileContent(f"{PACKAGE}/test_bad.py", b"def test():\n assert False\n")
PY3_ONLY_SOURCE = FileContent(f"{PACKAGE}/test_py3.py", b"def test() -> None:\n pass\n")
LIBRARY_SOURCE = FileContent(f"{PACKAGE}/library.py", b"def add_two(x):\n return x + 2\n")
Expand Down Expand Up @@ -143,6 +144,7 @@ def run_pytest(
execution_slot_var: Optional[str] = None,
extra_env_vars: Optional[str] = None,
env: Optional[Mapping[str, str]] = None,
config: Optional[str] = None,
force: bool = False,
) -> TestResult:
args = [
Expand All @@ -162,6 +164,9 @@ def run_pytest(
args.append("--test-use-coverage")
if execution_slot_var:
args.append(f"--pytest-execution-slot-var={execution_slot_var}")
if config:
rule_runner.create_file(relpath="pytest.ini", contents=config)
args.append("--pytest-config=pytest.ini")
if force:
args.append("--test-force")
rule_runner.set_options(args, env=env)
Expand Down Expand Up @@ -253,6 +258,13 @@ def test():
assert f"{PACKAGE}/test_relative_import.py ." in result.stdout


def test_respects_config(rule_runner: RuleRunner) -> None:
target = create_test_target(rule_runner, [GOOD_WITH_PRINT])
result = run_pytest(rule_runner, target, config="[pytest]\naddopts = -s\n")
assert result.exit_code == 0
assert "All good!" in result.stdout and "Captured" not in result.stdout


def test_transitive_dep(rule_runner: RuleRunner) -> None:
create_python_library(rule_runner, [LIBRARY_SOURCE])
transitive_dep_fc = FileContent(
Expand Down
22 changes: 21 additions & 1 deletion src/python/pants/backend/python/subsystems/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from typing import Optional, Tuple, cast

from pants.option.custom_types import shell_str
from pants.option.custom_types import file_option, shell_str
from pants.option.subsystem import Subsystem


Expand Down Expand Up @@ -89,6 +89,22 @@ def register_options(cls, register):
"to tests under this environment variable name."
),
)
register(
"--config",
type=file_option,
default=None,
advanced=True,
help=(
"Path to pytest.ini or alternative Pytest config file.\n\n"
"Pytest will attempt to auto-discover the config file,"
"meaning that it should typically be an ancestor of your"
"tests, such as in the build root.\n\nPants will not automatically"
" set --rootdir for you to force Pytest to pick up your config "
"file, but you can manually set --rootdir in [pytest].args.\n\n"
"Refer to https://docs.pytest.org/en/stable/customize.html#"
"initialization-determining-rootdir-and-configfile."
),
)

def get_requirement_strings(self) -> Tuple[str, ...]:
"""Returns a tuple of requirements-style strings for Pytest and Pytest plugins."""
Expand All @@ -105,3 +121,7 @@ def timeout_default(self) -> Optional[int]:
@property
def timeout_maximum(self) -> Optional[int]:
return cast(Optional[int], self.options.timeout_maximum)

@property
def config(self) -> Optional[str]:
return cast(Optional[str], self.options.config)

0 comments on commit e7e594c

Please sign in to comment.