Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/internals-checks-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
source: internals/checks/src
config: internals/checks/testing-conventions.toml
base: ${{ github.event.pull_request.base.sha || 'origin/main' }}
gates: '["colocated-test", "unit-lint", "integration-lint", "unit-coverage", "mutation", "e2e-verify"]'
gates: '["colocated-test", "one-function-per-file", "unit-lint", "integration-lint", "unit-coverage", "mutation", "e2e-verify"]'

internals-checks-integration:
# The conventions job runs this package's colocated unit tier and checks its
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/internals-distcheck-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
languages: '["python"]'
source: internals/distcheck/src
base: ${{ github.event.pull_request.base.sha || 'origin/main' }}
gates: '["colocated-test", "unit-lint", "integration-lint", "unit-coverage", "mutation"]'
gates: '["colocated-test", "one-function-per-file", "unit-lint", "integration-lint", "unit-coverage", "mutation"]'
2 changes: 1 addition & 1 deletion .github/workflows/plugin-dirsql-embedding-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
source: plugins/dirsql-plugin-embeddings/src
config: plugins/dirsql-plugin-embeddings/testing-conventions.toml
base: ${{ github.event.pull_request.base.sha || 'origin/main' }}
gates: '["colocated-test", "unit-lint", "integration-lint", "unit-coverage", "mutation", "e2e-verify"]'
gates: '["colocated-test", "one-function-per-file", "unit-lint", "integration-lint", "unit-coverage", "mutation", "e2e-verify"]'

embeddings-integration:
# The conventions job runs the plugin's colocated unit tier and checks its
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "uv run python -m pytest tests/e2e -x -q",
"ran_at": 1788368546,
"exit_code": 0,
"commit": "f032f0fa05bf1ba089f1a71197a4bca124c2ad36",
"branch": "claude/1018-preflight-one-function-gate"
}
34 changes: 23 additions & 11 deletions internals/checks/src/checks/preflight/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@

Three pairs cannot be run the naive way, and each is encoded rather than skipped:

* python `mutation` / `unit-coverage` -- `uvx` supplies its own environment, so
the suite's `python3 -m pytest` resolves to an interpreter with no pytest
(#706 saw it as a cosmic-ray baseline failure naming no cause). Both run via
the package's own venv instead.
* python `mutation` / `unit-coverage` -- these execute the suite, whose
`python3 -m pytest` must resolve to the package's own venv rather than an
ambient interpreter with no pytest (#706 saw it as a cosmic-ray baseline
failure naming no cause). Both run under `uv run` from the package root.
* rust `colocated-test` -- the co-change variant rides only the
python/typescript language set (rust units are inline, so no sibling can go
stale), and the CLI rejects `--base` for it.
Expand All @@ -36,13 +36,25 @@

MANIFESTS = ("pyproject.toml", "package.json", "Cargo.toml")

# The CLI, resolved the way CI resolves it. Two silent drifts made the local run
# enforce a different ruleset, and naming the tag closes both: the PyPI wheel
# `uvx` fetches lags npm by several releases, and a BARE `npx testing-conventions`
# resolves engine-aware, so a shim on node older than `engines.node` picks the
# last release below that floor -- and exits 0 with no banner to say so.
CLI = ["npx", "-y", "testing-conventions@latest"]

# How to launch a suite-executing gate, per language. Both run from the package
# root: python so `python3 -m pytest` resolves to that package's venv, typescript
# because only the npm CLI appends `--ts-mutation-adapter`, which the rule needs.
# Rust is absent -- it has no suite environment to enter, so it stays on `uvx`.
# root: python so the suite's `python3 -m pytest` resolves to that package's
# venv, typescript because only the npm CLI appends `--ts-mutation-adapter`,
# which the rule needs. Rust is absent -- it has no suite environment to enter.
#
# The python arm needs BOTH distributions. `--with` is not how the CLI is
# resolved; it puts the wheel's `testing_conventions` package on the interpreter
# the gate hands to `python3 -m`, which is the only form the mutation adapter
# ships in.
LAUNCHERS = {
"python": ["uv", "run", "--with", "testing-conventions", "testing-conventions"],
"typescript": ["npx", "-y", "testing-conventions"],
"python": ["uv", "run", "--with", "testing-conventions", *CLI],
"typescript": CLI,
}


Expand Down Expand Up @@ -95,7 +107,7 @@ def invocation(
home = package_root(root.source, exists)
return Invocation(
[
*["uvx", "testing-conventions", *gate.command, *options],
*[*CLI, *gate.command, *options],
*["--scope", root.source, *e2e_flags(e2e), home],
],
".",
Expand All @@ -107,7 +119,7 @@ def invocation(
options[-1] = os.path.relpath(root.config, cwd)
source = os.path.relpath(root.source, cwd)
return Invocation([*LAUNCHERS[language], *gate.command, *options, source], cwd)
return Invocation(["uvx", "testing-conventions", *gate.command, *options, root.source], ".")
return Invocation([*CLI, *gate.command, *options, root.source], ".")


def default_runner(argv: Sequence[str], cwd: str) -> int:
Expand Down
26 changes: 14 additions & 12 deletions internals/checks/src/checks/preflight/gate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,21 @@ def it_returns_nothing_for_an_absent_table():


def describe_invocation():
def it_runs_an_ordinary_gate_through_uvx_from_the_repo_root():
def it_runs_an_ordinary_gate_through_the_npm_cli_from_the_repo_root():
assert call(PY, "python", "unit-lint") == Invocation(
[
*["uvx", "testing-conventions", "unit", "lint"],
*["npx", "-y", "testing-conventions@latest", "unit", "lint"],
*["--language", "python"],
*["--config", "testing-conventions.toml", "packages/python/dirsql"],
],
".",
)

def it_takes_the_ordinary_path_for_a_gate_name_sorting_below_e2e_verify():
# Both paths start `uvx testing-conventions`, so `--scope` is what tells
# Both paths start with the same CLI prefix, so `--scope` is what tells
# them apart -- an `<=` here would route colocated-test into e2e's branch.
assert call(PY, "python", "colocated-test").argv == [
*["uvx", "testing-conventions", "unit", "colocated-test"],
*["npx", "-y", "testing-conventions@latest", "unit", "colocated-test"],
*["--language", "python", "--base", "origin/main"],
*["--config", "testing-conventions.toml", "packages/python/dirsql"],
]
Expand All @@ -117,7 +117,7 @@ def it_omits_config_for_a_root_that_declares_none():
def it_targets_the_package_root_for_e2e_verify_scoped_to_the_source():
assert call(PY, "python", "e2e-verify", {"extra_scope": ["packages/rust/src"]}) == Invocation(
[
*["uvx", "testing-conventions", "e2e", "verify"],
*["npx", "-y", "testing-conventions@latest", "e2e", "verify"],
*["--base", "origin/main"],
*["--scope", "packages/python/dirsql"],
*["--extra-scope", "packages/rust/src", "packages/python"],
Expand All @@ -131,8 +131,9 @@ def it_omits_language_for_e2e_verify_which_does_not_accept_it():
def it_runs_python_mutation_through_the_packages_own_venv():
mutation = call(PY, "python", "mutation")
assert mutation.cwd == "packages/python"
assert mutation.argv[:5] == [
*["uv", "run", "--with", "testing-conventions", "testing-conventions"]
assert mutation.argv[:7] == [
*["uv", "run", "--with", "testing-conventions"],
*["npx", "-y", "testing-conventions@latest"],
]

def it_rewrites_the_config_and_source_paths_relative_to_that_cwd():
Expand All @@ -154,10 +155,10 @@ def it_runs_python_unit_coverage_through_that_venv_too():
def it_runs_a_typescript_suite_gate_through_npx_from_the_package_root():
root = Root(job="ts", source="packages/ts/src", languages=["typescript"], gates=["mutation"])
mutation = call(root, "typescript", "mutation")
assert mutation.argv[:3] == ["npx", "-y", "testing-conventions"]
assert mutation.argv[:3] == ["npx", "-y", "testing-conventions@latest"]
assert (mutation.cwd, mutation.argv[-1]) == (".", "packages/ts/src")

def it_keeps_a_rust_mutation_gate_on_uvx():
def it_keeps_a_rust_mutation_gate_on_the_default_cli():
assert call(RUST, "rust", "mutation").cwd == "."


Expand Down Expand Up @@ -228,7 +229,7 @@ def it_runs_the_drift_guards_first_then_every_pair():
assert drive(runner=lambda argv, cwd: calls.append((argv, cwd)) or 0) == 0
assert [argv[:2] for argv, _cwd in calls] == [
*[["uv", "sync"], ["uv", "run"]],
*[["uvx", "testing-conventions"], ["uv", "run"]],
*[["npx", "-y"], ["uv", "run"]],
]
assert [cwd for _argv, cwd in calls] == [".", ".", ".", "packages/python"]

Expand Down Expand Up @@ -283,7 +284,7 @@ def it_echoes_the_argv_it_is_about_to_run():
lines = []
drive(only=["unit-lint"], echo=lines.append)
assert lines[0] == (
"==> python-sdk [python] unit-lint: uvx testing-conventions unit lint "
"==> python-sdk [python] unit-lint: npx -y testing-conventions@latest unit lint "
"--language python packages/python/dirsql"
)

Expand All @@ -292,7 +293,8 @@ def it_runs_only_the_gates_named_by_the_filter():
drive(only=["mutation"], runner=lambda _argv, _cwd: 0, echo=lines.append)
assert [line for line in lines if line.startswith("==>")] == [
"==> python-sdk [python] mutation: uv run --with testing-conventions "
"testing-conventions unit mutation --language python --base origin/main dirsql"
"npx -y testing-conventions@latest unit mutation --language python "
"--base origin/main dirsql"
]

def it_prints_every_pair_without_running_any_when_dry_run():
Expand Down
4 changes: 3 additions & 1 deletion internals/checks/src/checks/preflight/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# omits `gates:` gets all of them.
ROOT_GATES = [
"colocated-test",
"one-function-per-file",
"unit-lint",
"integration-lint",
"unit-coverage",
Expand Down Expand Up @@ -58,7 +59,7 @@ class Gate:
config: bool = True
base: bool = False
# Executes the package's test suite, so it needs that package's own venv on
# `python3`, not the throwaway one `uvx` supplies.
# `python3`, not the throwaway one a standalone launcher supplies.
runs_suite: bool = False
# Takes the root of a BUILT artifact, not a source dir. CI builds it from
# the manifest first; preflight has no equivalent and reports these skipped.
Expand All @@ -67,6 +68,7 @@ class Gate:

GATES = {
"colocated-test": Gate(("unit", "colocated-test"), base=True),
"one-function-per-file": Gate(("unit", "one-function-per-file")),
"unit-lint": Gate(("unit", "lint")),
"integration-lint": Gate(("integration", "lint")),
"unit-coverage": Gate(("unit", "coverage"), base=True, runs_suite=True),
Expand Down
2 changes: 2 additions & 0 deletions internals/checks/src/checks/preflight/matrix_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def it_records_the_options_and_needs_of_every_gate():
}
assert shape == {
"colocated-test": (True, True, True, False, False),
"one-function-per-file": (True, True, False, False, False),
"unit-lint": (True, True, False, False, False),
"integration-lint": (True, True, False, False, False),
"unit-coverage": (True, True, True, True, False),
Expand All @@ -101,6 +102,7 @@ def it_records_the_options_and_needs_of_every_gate():
def it_maps_each_gate_to_its_cli_subcommand():
assert {name: g.command for name, g in GATES.items()} == {
"colocated-test": ("unit", "colocated-test"),
"one-function-per-file": ("unit", "one-function-per-file"),
"unit-lint": ("unit", "lint"),
"integration-lint": ("integration", "lint"),
"unit-coverage": ("unit", "coverage"),
Expand Down
7 changes: 7 additions & 0 deletions internals/checks/tests/e2e/preflight_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ def it_prints_the_matrix_from_the_repo_root_with_no_arguments():
assert "==> rust [rust] unit-lint" in proc.stdout
assert "==> internals-checks [python] unit-lint" in proc.stdout

def it_prints_the_one_function_per_file_gate_for_a_lane_naming_no_gates():
proc = preflight()

assert proc.returncode == 0, f"stdout={proc.stdout!r} stderr={proc.stderr!r}"
assert "==> python-sdk [python] one-function-per-file" in proc.stdout
assert "==> typescript-sdk [typescript] one-function-per-file" in proc.stdout

def it_exits_with_an_actionable_message_when_a_named_workflow_is_missing():
proc = preflight("--conventions", ".github/workflows/conventions.yml")

Expand Down
18 changes: 18 additions & 0 deletions internals/checks/tests/integration/preflight_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ def it_spans_every_workflow_holding_a_caller_not_just_one(repo_root):

assert {file for job, file in jobs.items() if f"==> {job} [" in output} == set(jobs.values())

def it_derives_the_one_function_per_file_gate_for_a_lane_naming_no_gates(repo_root):
# `@v0` runs `unit one-function-per-file` in the static job, so a lane
# that names no `gates:` is held to it in CI. A matrix omitting it is a
# locally-green run that says nothing about the gate.
output = dry_run().output

assert "==> python-sdk [python] one-function-per-file" in output
assert "==> typescript-sdk [typescript] one-function-per-file" in output

def it_derives_the_one_function_per_file_gate_for_every_lane_naming_it(repo_root):
# An explicit `gates` array is an allowlist: a lane that omits the gate
# does not run it, however the reusable workflow defaults.
output = dry_run().output

assert "==> internals-checks [python] one-function-per-file" in output
assert "==> internals-distcheck [python] one-function-per-file" in output
assert "==> plugins-embeddings [python] one-function-per-file" in output

def it_names_a_missing_workflow_and_the_fix_instead_of_raising(repo_root):
result = dry_run("--conventions", ".github/workflows/conventions.yml")

Expand Down