I'm writing a test for this <uv-sync> plugin, whic...
# plugins
c
I'm writing a test for this uv-sync plugin, which uses
await run_interactive_process(InteractiveProcess(...)
. From the comments across the channels, it's looking like I need to use mocking with
rule_runner.run_interactive_process
like we see in this example to test the plugin. Can someone confirm if this is the correct approach? The issue I'm facing right now is attached. TLDR
native_engine.IntrinsicError: Error executing interactive process: No such file or directory (os error 2)
. From the log error, I know the issue is related to the files being written in one directory, but the pants goal being run in a different directory.
1
From my research (i.e., looking through the source code) this is the correct approach.
h
That is a reasonable way to test rules independently of other rules (since you mock those other rules out), yes
You can also run on a set of rules, without mocking, via RuleRunner
There are examples all over the codebase (in particular there is a useful PythonRuleRunner subclass)
c
Thanks for the response @happy-kitchen-89482. I'd rather use a RuleRunner instead of mocking the tests.
The plugin I wrote writes to console, so I have to use mocking for that aspect. Here's the test code I have at the moment
Copy code
from pants.backend.python.target_types import PythonSourceTarget
from pants.testutil.rule_runner import RuleRunner, mock_console
from pants_uv_lifecycle_plugin import run_uv_sync
from pants_uv_lifecycle_plugin.run_uv_sync import UvSyncGoal, run_uv_sync_on_pyproject_directories
import pytest
from textwrap import dedent
from pathlib import Path 

@pytest.fixture
def uv_sync_rule_runner() -> RuleRunner:
    return RuleRunner(target_types=[PythonSourceTarget], rules=[*run_uv_sync.rules()])


def test_run_uv_sync(uv_sync_rule_runner: RuleRunner) -> None:
    uv_sync_rule_runner.write_files(
         {
            "test-project/src/test_project/__init__.py": "",
            "test-project/src/BUILD": "python_sources()",
            "test-project/BUILD": "python_requirements(name=\"reqs\",source=\"pyproject.toml\")",
            "test-project/pyproject.toml": dedent(
            """\
            [project]
            name = "test-project"
            version = "0.1.0"
            description = "A simple uv project to test the pants-uv-lifecycle-plugin"
            readme = "README.md"
            authors = [{ name = "aiSSEMBLE Baseline Community", email = "<mailto:aissemble@bah.com|aissemble@bah.com>"}]
            requires-python = ">=3.9"
            dependencies = ["numpy>=2.2.1"]

            [build-system]
            requires = ["hatchling"]
            build-backend = "hatchling.build"
            """),
            "test-project/README.md": "",
         }
     )
    assert Path(uv_sync_rule_runner.build_root, "test-project/src/test_project/__init__.py").read_text() == ""
    assert Path(uv_sync_rule_runner.build_root, "test-project/src/BUILD").read_text() == "python_sources()"
    assert Path(uv_sync_rule_runner.build_root, "test-project/BUILD").read_text() == "python_requirements(name=\"reqs\",source=\"pyproject.toml\")"
    assert Path(uv_sync_rule_runner.build_root, "test-project/README.md").read_text() == ""
    assert Path(uv_sync_rule_runner.build_root, "test-project/pyproject.toml").read_text() == dedent(
            """\
            [project]
            name = "test-project"
            version = "0.1.0"
            description = "A simple uv project to test the pants-uv-lifecycle-plugin"
            readme = "README.md"
            authors = [{ name = "aiSSEMBLE Baseline Community", email = "<mailto:aissemble@bah.com|aissemble@bah.com>"}]
            requires-python = ">=3.9"
            dependencies = ["numpy>=2.2.1"]

            [build-system]
            requires = ["hatchling"]
            build-backend = "hatchling.build"
            """)
    with mock_console(uv_sync_rule_runner.options_bootstrapper) as (console, _):
        result = uv_sync_rule_runner.run_goal_rule(UvSyncGoal) 
    assert result.exit_code == 0, f"The result.exit_code is {result.exit_code}, but 0 was expected."
The error I'm getting is below. I suspect that the
No such file or directory
is due to the fact that the RuleRunner doesn't have access to the
PATH
environment variable. Does anything jump out at you as far as what I could be doing wrong?
Copy code
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/Users/margaretblack/.cache/pants/named_caches/pex_root/venvs/a16dc0cecf9dbe98a962c4fa657b20606c9475de/08044421b45091240ec153bb89d50a6331335fa0/lib/python3.9/site-packages/pants/testutil/rule_runner.py:440: in run_goal_rule
    exit_code = self.scheduler.run_goal_rule(
/Users/margaretblack/.cache/pants/named_caches/pex_root/venvs/a16dc0cecf9dbe98a962c4fa657b20606c9475de/08044421b45091240ec153bb89d50a6331335fa0/lib/python3.9/site-packages/pants/engine/internals/scheduler.py:564: in run_goal_rule
    (return_value,) = self.product_request(
/Users/margaretblack/.cache/pants/named_caches/pex_root/venvs/a16dc0cecf9dbe98a962c4fa657b20606c9475de/08044421b45091240ec153bb89d50a6331335fa0/lib/python3.9/site-packages/pants/engine/internals/scheduler.py:593: in product_request
    return self.execute(request)
/Users/margaretblack/.cache/pants/named_caches/pex_root/venvs/a16dc0cecf9dbe98a962c4fa657b20606c9475de/08044421b45091240ec153bb89d50a6331335fa0/lib/python3.9/site-packages/pants/engine/internals/scheduler.py:534: in execute
    self._raise_on_error([t for _, t in throws])
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pants.engine.internals.scheduler.SchedulerSession object at 0x106fd81c0>
throws = [Throw(exc=IntrinsicError('Error executing interactive process: No such file or directory (os error 2)'), python_trace...ack=[('pants_uv_lifecycle_plugin.run_uv_sync.run_uv_sync_on_pyproject_directories', 'run `uv sync`'), ('root', None)])]

    def _raise_on_error(self, throws: list[Throw]) -> NoReturn:
        exception_noun = pluralize(len(throws), "Exception")
        others_msg = f"\n(and {len(throws) - 1} more)\n" if len(throws) > 1 else ""
>       raise ExecutionError(
            f"{exception_noun} encountered:\n\n"
            f"{throws[0].render(self._scheduler.include_trace_on_error)}\n"
            f"{others_msg}",
            wrapped_exceptions=tuple(t.exc for t in throws),
        )
E       pants.engine.internals.scheduler.ExecutionError: 1 Exception encountered:
E       
E       Engine traceback:
E         in root
E           ..
E         in pants_uv_lifecycle_plugin.run_uv_sync.run_uv_sync_on_pyproject_directories
E           run `uv sync`
E       
E       Traceback (most recent call last):
E         File "src/pants_uv_lifecycle_plugin/run_uv_sync.py", line 31, in run_uv_sync_on_pyproject_directories
E           interactive_process_result = await run_interactive_process(
E         File "/Users/margaretblack/.cache/pants/named_caches/pex_root/venvs/a16dc0cecf9dbe98a962c4fa657b20606c9475de/08044421b45091240ec153bb89d50a6331335fa0/lib/python3.9/site-packages/pants/engine/intrinsics.py", line 157, in run_interactive_process
E           ret: InteractiveProcessResult = await _interactive_process(process, **implicitly())
E         File "/Users/margaretblack/.cache/pants/named_caches/pex_root/venvs/a16dc0cecf9dbe98a962c4fa657b20606c9475de/08044421b45091240ec153bb89d50a6331335fa0/lib/python3.9/site-packages/pants/engine/rules.py", line 74, in wrapper
E           return await call
E         File "/Users/margaretblack/.cache/pants/named_caches/pex_root/venvs/a16dc0cecf9dbe98a962c4fa657b20606c9475de/08044421b45091240ec153bb89d50a6331335fa0/lib/python3.9/site-packages/pants/engine/internals/selectors.py", line 85, in __await__
E           result = yield self
E         File "/Users/margaretblack/.cache/pants/named_caches/pex_root/venvs/a16dc0cecf9dbe98a962c4fa657b20606c9475de/08044421b45091240ec153bb89d50a6331335fa0/lib/python3.9/site-packages/pants/engine/intrinsics.py", line 147, in _interactive_process
E           return await native_engine.interactive_process(process, process_execution_environment)
E       native_engine.IntrinsicError: Error executing interactive process: No such file or directory (os error 2)

/Users/margaretblack/.cache/pants/named_caches/pex_root/venvs/a16dc0cecf9dbe98a962c4fa657b20606c9475de/08044421b45091240ec153bb89d50a6331335fa0/lib/python3.9/site-packages/pants/engine/internals/scheduler.py:518: ExecutionError
- generated xml file: tests.test_run_uv_sync.py.pants-uv-lifecycle-plugin-tests.xml -
=========================== short test summary info ============================
FAILED tests/test_run_uv_sync.py::test_run_uv_sync - pants.engine.internals.s...
============================== 1 failed in 0.91s ===============================
One of my co-workers figured out the correct way to write the integration test. I'll post a link with what reference(s) she used when she gets in today.
🎉 1
My coworker's reference: https://www.pantsbuild.org/dev/docs/writing-plugins/the-rules-api/testing-plugins#approach-4-run_pants-integration-tests-for-pants. I was so fixed on using approach #3 that I didn't think to look at approach #4. 🤦‍♀️... A good reminder to step back and bounce ideas off of others when possible.
h
Glad that worked out!
run_pants()
is the most expensive way (your test starts up an entire
pants
process) but it is often very effective
c
Yeah, I've found that it does take longer, but for now, this will work. Eventually, I'm going to switch to using
behave
instead.