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
6 changes: 5 additions & 1 deletion cli/nao_core/commands/test/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ def discover_tests(project_path: Path) -> list[TestCase]:
UI.warn(f"Tests folder not found: {tests_dir}")
return []

test_files = list(tests_dir.glob("*.yml")) + list(tests_dir.glob("*.yaml"))
test_files = [
p
for p in (*tests_dir.rglob("*.yml"), *tests_dir.rglob("*.yaml"))
if "outputs" not in p.relative_to(tests_dir).parts
]

if not test_files:
UI.warn(f"No test files found in {tests_dir}")
Expand Down
44 changes: 32 additions & 12 deletions cli/nao_core/commands/test/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,17 @@ def save_results(results: list[TestRunResult], output_dir: Path) -> Path:
return output_file


def filter_test_cases(test_cases: list[TestCase], selected_tests: str | None) -> list[TestCase]:
"""Filter test cases to the selected tests, if provided."""
def filter_test_cases(
test_cases: list[TestCase],
selected_tests: str | None,
tests_dir: Path | None = None,
) -> list[TestCase]:
"""Filter test cases to the selected tests, if provided.

Each comma-separated selection matches either a single test (by ``name`` or
file stem) or, when ``tests_dir`` is given, every test under a subfolder of
tests/ (e.g. ``contracts`` selects all tests in tests/contracts/).
"""
if not selected_tests:
return test_cases

Expand All @@ -302,18 +311,28 @@ def filter_test_cases(test_cases: list[TestCase], selected_tests: str | None) ->
selected: list[TestCase] = []
seen: set[Path] = set()
for selection in selections:
matches = [tc for tc in test_cases if tc.name == selection or tc.file_path.stem == selection]
folder_matches: list[TestCase] = []
if tests_dir is not None:
for tc in test_cases:
try:
parts = tc.file_path.relative_to(tests_dir).parent.parts
except ValueError:
continue
if selection in parts:
folder_matches.append(tc)
name_matches = [tc for tc in test_cases if tc.name == selection or tc.file_path.stem == selection]
matches = folder_matches or name_matches
if not matches:
available = ", ".join(tc.name for tc in test_cases)
raise ValueError(f"Test not found: {selection}. Available tests: {available}")
if len(matches) > 1:
if not folder_matches and len(matches) > 1:
names = ", ".join(f"{tc.name} ({tc.file_path.name})" for tc in matches)
raise ValueError(f"Multiple tests match '{selection}': {names}")
match = matches[0]
if match.file_path in seen:
continue
seen.add(match.file_path)
selected.append(match)
for match in matches:
if match.file_path in seen:
continue
seen.add(match.file_path)
selected.append(match)

return selected

Expand All @@ -337,7 +356,7 @@ def test(
str | None,
Parameter(
name=["-s", "--select"],
help="Run only the selected tests by name or yaml filename stem. Comma-separated (e.g. '12,13,14').",
help="Run only selected tests by name, yaml stem, or subfolder. Comma-separated (e.g. 'contracts' or '12,13,14').",
),
] = None,
username: Annotated[
Expand Down Expand Up @@ -384,8 +403,9 @@ def test(

project_path = Path.cwd()
model_costs = config.llm.meta.costs if config.llm and config.llm.meta else None
tests_dir = project_path / TESTS_FOLDER
UI.print(f"[dim]Project: {config.project_name}[/dim]")
UI.print(f"[dim]Tests folder: {project_path / TESTS_FOLDER}[/dim]")
UI.print(f"[dim]Tests folder: {tests_dir}[/dim]")
UI.print(f"[dim]Models: {', '.join(str(m) for m in model_configs)}[/dim]\n")

test_cases = discover_tests(project_path)
Expand All @@ -395,7 +415,7 @@ def test(
return

try:
test_cases = filter_test_cases(test_cases, select)
test_cases = filter_test_cases(test_cases, select, tests_dir)
except ValueError as e:
UI.error(str(e))
return
Expand Down
24 changes: 24 additions & 0 deletions cli/tests/nao_core/commands/test_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from nao_core.commands.test.case import discover_tests


def test_discover_tests_is_recursive(tmp_path):
(tmp_path / "tests" / "revenue").mkdir(parents=True)
(tmp_path / "tests" / "revenue" / "mrr.yml").write_text("prompt: how much mrr\n")
(tmp_path / "tests" / "ops" / "sla").mkdir(parents=True)
(tmp_path / "tests" / "ops" / "sla" / "uptime.yaml").write_text("prompt: uptime\n")

cases = discover_tests(tmp_path)

names = {c.name for c in cases}
assert names == {"mrr", "uptime"}


def test_discover_tests_ignores_outputs_dir(tmp_path):
(tmp_path / "tests").mkdir()
(tmp_path / "tests" / "real.yml").write_text("prompt: real\n")
(tmp_path / "tests" / "outputs").mkdir()
(tmp_path / "tests" / "outputs" / "results.yml").write_text("prompt: not a test\n")

cases = discover_tests(tmp_path)

assert {c.name for c in cases} == {"real"}
34 changes: 34 additions & 0 deletions cli/tests/nao_core/commands/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,37 @@ def test_run_test_records_reference_sql_on_client_error(monkeypatch):
assert result.error == "backend unreachable"
assert result.details is not None
assert result.details.reference_sql == "select 1"


def test_filter_test_cases_by_folder(tmp_path):
tests_dir = tmp_path / "tests"
tc_orders = NaoTestCase(name="orders", prompt="p1", file_path=tests_dir / "revenue" / "orders.yml", sql="select 1")
tc_mrr = NaoTestCase(name="mrr", prompt="p2", file_path=tests_dir / "revenue" / "mrr.yml", sql="select 1")
tc_users = NaoTestCase(name="users", prompt="p3", file_path=tests_dir / "ops" / "users.yml", sql="select 1")

filtered = filter_test_cases([tc_orders, tc_mrr, tc_users], "revenue", tests_dir)

assert {tc.name for tc in filtered} == {"orders", "mrr"}


def test_filter_test_cases_folder_and_name_combined(tmp_path):
tests_dir = tmp_path / "tests"
tc_orders = NaoTestCase(name="orders", prompt="p1", file_path=tests_dir / "revenue" / "orders.yml", sql="select 1")
tc_users = NaoTestCase(name="users", prompt="p2", file_path=tests_dir / "ops" / "users.yml", sql="select 1")

filtered = filter_test_cases([tc_orders, tc_users], "revenue,users", tests_dir)

assert {tc.name for tc in filtered} == {"orders", "users"}


def test_filter_test_cases_by_name_without_tests_dir_unchanged():
# Backward-compat: two-arg call still filters by name/stem.
test_cases = [
NaoTestCase(name="orders", prompt="p1", file_path=Path("tests/orders.yml"), sql="select 1"),
NaoTestCase(name="users", prompt="p2", file_path=Path("tests/users.yml"), sql="select 1"),
]

filtered = filter_test_cases(test_cases, "users")

assert len(filtered) == 1
assert filtered[0].name == "users"
Loading