|
| 1 | +"""A bare ``argv[0]`` that names a declared tool, and the file it then runs. |
| 2 | +
|
| 3 | +``${detector:<name>}`` is one way a recipe names a tool; a bare program name is |
| 4 | +the other, and the authoring guide promises both |
| 5 | +(``docs/authoring-plugins.spec.md``). The bare spelling used to be looked up |
| 6 | +along the default path alone, whatever the tool's detector declared — so a |
| 7 | +binstub installed only under the project's ``bin`` was cleared by the setup |
| 8 | +(``missing_tools`` asks the detector's own search paths) and then not found by |
| 9 | +the run, the very disagreement ``project_paths.tool_executable`` exists to |
| 10 | +prevent. A bare name this run's plugins declared is resolved to the same file |
| 11 | +the placeholder spelling stands for; a name nobody declared keeps the lookup |
| 12 | +the spawn always made. |
| 13 | +""" |
| 14 | + |
| 15 | +from __future__ import annotations |
| 16 | + |
| 17 | +from pathlib import Path |
| 18 | + |
| 19 | +import pytest |
| 20 | +from bare_machine import project_with_no_tools |
| 21 | +from detector_declarations import declaring |
| 22 | +from executable_stub import write_stub |
| 23 | +from plugin_fixture import one_sensor |
| 24 | + |
| 25 | +from habit_hooks.project_paths import tool_executable |
| 26 | +from habit_hooks.scope import Scope |
| 27 | +<
10BC8
span class=pl-k>from habit_hooks.sensors.execution import Execution |
| 28 | +from habit_hooks.sensors.model import Part |
| 29 | + |
| 30 | +BUNDLER = ( |
| 31 | + '{ name = "rubocop", kind = "command", install = "gem install rubocop", ' |
| 32 | + 'search_paths = ["bin"] }' |
| 33 | +) |
| 34 | + |
| 35 | + |
| 36 | +def _argv(project: Path, part: Part) -> list[str]: |
| 37 | + """The argv spawning ``part`` in ``project`` would carry.""" |
| 38 | + return Execution(project_dir=project, scope=Scope(files=[]))._expand(part) |
| 39 | + |
| 40 | + |
| 41 | +def _run(project: Path, part: Part) -> object: |
| 42 | + """What running ``part`` over one file comes to.""" |
| 43 | + scope = Scope(files=["a.rb"]) |
| 44 | + return Execution(project_dir=project, scope=scope).run_sensors([part]) |
| 45 | + |
| 46 | + |
| 47 | +def test_a_bare_program_is_looked_for_along_the_search_path_its_detector_names( |
| 48 | + tmp_path: Path, monkeypatch: pytest.MonkeyPatch |
| 49 | +) -> None: |
| 50 | + """The bare spelling of a name is resolved exactly as ``${detector:<name>}`` |
| 51 | + is: the same detector, the same search paths, the same file.""" |
| 52 | + project = project_with_no_tools(tmp_path, monkeypatch) |
| 53 | + write_stub(project / "bin", "rubocop") |
| 54 | + part = one_sensor(project, 'argv = ["rubocop", "--json"]', declaring(BUNDLER)) |
| 55 | + |
| 56 | + argv = _argv(project, part) |
| 57 | + |
| 58 | + assert Path(argv[0]).parent == project / "bin" |
| 59 | + assert Path(argv[0]).stem == "rubocop" |
| 60 | + assert argv[1:] == ["--json"] |
| 61 | + |
| 62 | + |
| 63 | +def test_a_bare_program_only_the_detector_could_find_is_the_file_that_runs( |
| 64 | + tmp_path: Path, monkeypatch: pytest.MonkeyPatch |
| 65 | +) -> None: |
| 66 | + """The whole gap: the default lookup cannot find the program, and the run |
| 67 | + still executes the detector's file — a clean run, where the bare name alone |
| 68 | + was the missing command the notice would have named.""" |
| 69 | + project = project_with_no_tools(tmp_path, monkeypatch) |
| 70 | + write_stub(project / "bin", "rubocop") |
| 71 | + part = one_sensor(project, 'argv = ["rubocop", "--json"]', declaring(BUNDLER)) |
| 72 | + |
| 73 | + assert tool_executable("rubocop", project) is None |
| 74 | + |
| 75 | + run = _run(project, part) |
| 76 | + |
| 77 | + assert run.notices == [] |
| 78 | + assert run.findings == [] |
| 79 | + |
| 80 | + |
| 81 | +def test_a_bare_program_declared_and_absent_fails_by_name( |
| 82 | + tmp_path: Path, monkeypatch: pytest.MonkeyPatch |
| 83 | +) -> None: |
| 84 | + """A declared tool the project has not got is the everyday missing tool, |
| 85 | + whichever spelling named it — answered before the spawn, naming what to |
| 86 | + install.""" |
| 87 | + project = project_with_no_tools(tmp_path, monkeypatch) |
| 88 | + part = one_sensor(project, 'argv = ["rubocop", "--json"]', declaring(BUNDLER)) |
| 89 | + |
| 90 | + run = _run(project, part) |
| 91 | + |
| 92 | + assert run.notices == [ |
| 93 | + "habit-sensors: sensor 's' needs the 'rubocop' command, which is not " |
| 94 | + "installed — install it, or disable the sensor with [sensors.s] " |
| 95 | + "disabled = true" |
| 96 | + ] |
| 97 | + |
| 98 | + |
| 99 | +def test_a_bare_program_nobody_declared_keeps_the_name_it_spelled( |
| 100 | + tmp_path: Path, monkeypatch: pytest.MonkeyPatch |
| 101 | +) -> None: |
| 102 | + """Only a declared name is resolved through its detector: ``node`` here is |
| 103 | + nobody's, and the recipe keeps the name it spelled for the spawn to look |
| 104 | + up as it always did.""" |
| 105 | + project = project_with_no_tools(tmp_path, monkeypatch) |
| 106 | + part = one_sensor(project, 'argv = ["node", "h.cjs"]', declaring(BUNDLER)) |
| 107 | + |
| 108 | + assert _argv(project, part)[0] == "node" |
0 commit comments