New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RuleRunner
as a Pytest-style replacement to TestBase
#10699
Conversation
[ci skip-rust] [ci skip-build-wheels]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I marked which files have meaningful changes. All other files are routine rewrites to Pytest style.
@@ -5,83 +5,66 @@ | |||
import zipfile |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file has meaningful changes because we now use parametrization. In fact, this resolves a TODO Benjy added a few months ago to enable Pytest style tests!
@@ -40,7 +40,7 @@ | |||
from pants.option.options_bootstrapper import OptionsBootstrapper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file has meaningful changes. We used to have one TestBase
for multiple distinct tests - we used a single config even though they were very different tests.
Now, we use a mix of inlined RuleRunner
s + 2 Pytest fixtures so that each test has the tightest possible graph.
@@ -3,80 +3,81 @@ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file has meaningful changes because we now use parametrization.
@@ -194,122 +194,118 @@ def find_root(path): | |||
assert "project2/src/python" == find_root("project2/src/python/baz/qux.py") | |||
|
|||
|
|||
class AllRootsTest(TestBase): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There wasn't any need to use TestBase
in the first place for these 2 tests.
env={}, args=["--pants-config-files=[]"], allow_pantsrc=False | ||
) | ||
global_options = options_bootstrapper.bootstrap_options.for_global_scope() | ||
local_store_dir = global_options.local_store_dir |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to still emulate isolated_local_store
, which is used in fs_test.py
to avoid using the global lmdb_store
. Saving for a followup.
def target_types(self) -> FrozenOrderedSet[Type[Target]]: | ||
return self.build_config.target_types | ||
|
||
def request_product(self, product_type: Type[_P], subjects: Iterable[Any]) -> _P: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and below is basically copy pasta from TestBase
.
execution_options=ExecutionOptions.from_bootstrap_options(global_options), | ||
).new_session(build_id="buildid_for_test", should_report_workunits=True) | ||
self.scheduler = graph_session.scheduler_session | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we have no way of registering these clean up methods:
pants/src/python/pants/testutil/test_base.py
Lines 243 to 258 in 2fc8a42
def setUp(self): | |
""" | |
:API: public | |
""" | |
super().setUp() | |
# Avoid resetting the Runtracker here, as that is specific to fork'd process cleanup. | |
clean_global_runtime_state(reset_subsystem=True) | |
self.addCleanup(self._reset_engine) | |
safe_mkdir(self.build_root, clean=True) | |
safe_mkdir(self.pants_workdir) | |
self.addCleanup(safe_rmtree, self.build_root) | |
BuildRoot().path = self.build_root | |
self.addCleanup(BuildRoot().reset) |
I don't think we need them, though. The tmpdir for build_root
should be automatically cleaned. BuildRoot().path
will get reset the next time that a new RuleRunner
is created, and it can't impact outside of the test file thanks to v2's isolation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds right to me.
# Rust tests and lints will be skipped. Delete if not intended. [ci skip-rust] # Building wheels and fs_util will be skipped. Delete if not intended. [ci skip-build-wheels]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love it!
("a/b/c.py", ("a/b/c.py",)), | ||
], | ||
) | ||
def test_valid_matches(rule_runner: RuleRunner, glob: str, paths: Tuple[str, ...]) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
execution_options=ExecutionOptions.from_bootstrap_options(global_options), | ||
).new_session(build_id="buildid_for_test", should_report_workunits=True) | ||
self.scheduler = graph_session.scheduler_session | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds right to me.
Problem
We want to be able to use Pytest-style tests, rather than unittest style tests. Pytest allows for nice features like parameterization and fixtures, including dozens of pre-built fixtures like
caplog
(capture logging).For our
TestBase
-style tests, we need a wrapper around aSchedulerSession
in order to make synchronous calls to the engine. We also need to set up a temporary build root.For isolation between tests, we must be careful to invalidate every test, such as using a new build root every individual test.
Solution
Add
RuleRunner
, which is a dataclass around all the config necessary to create aBuildConfiguration
andSchedulerSession
. This dataclass exposes the methodsrequest_product()
andrun_goal_rule()
, along with utilities likeadd_to_build_file()
.Each individual test should create a new instance of a
RuleRunner
. This is important for isolation between tests.Conventionally, this will be done with a Pytest fixture, which allows us to set up common config (e.g. rules and target types) for multiple tests but to still get a distinct
RuleRunner
instance for each test.Users can also create the
RuleRunner
inline.If there are multiple different configurations in a test file, the user may set up multiple different Pytest fixtures.
Result
We can always use Pytest style tests now in Pants. We deprecate
TestBase
to be removed in 2.1.0.dev0.Performance benchmark
We used to only create one
SchedulerSession
for the entire test class and to invalidate on every individual test. Now, we create a newSchedulerSession
every single test. This ends up having a negligible performance impact.Before:
After:
Risk: breaking test isolation
It is possible for a user to use a single
RuleRunner
for multiple tests, e.g. one instance for the entire test class. This will reduce isolation between tests.We mitigate this risk by only ever documenting how you should properly use
RuleRunner
, along with adding a warning to https://www.pantsbuild.org/v2.0/docs/rules-api-testing about this risk.Unsolved issue: rule registration
It is still clunky to register the rules you need for your test to pass. This should still be solved, but it's left for a followup.
[ci skip-rust]
[ci skip-build-wheels]