diff --git a/pyproject.toml b/pyproject.toml index 04e371f..c09681c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,5 +22,14 @@ dependencies = [ [project.scripts] anvil = "anvil.cli:app" +[dependency-groups] +dev = [ + "pytest>=8.0", +] + [tool.uv] package = true + +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["tests"] diff --git a/src/anvil/wizard/models.py b/src/anvil/wizard/models.py index 476b178..a6c516b 100644 --- a/src/anvil/wizard/models.py +++ b/src/anvil/wizard/models.py @@ -15,11 +15,15 @@ class TestSpec: def to_fail_to_pass_str(self) -> str: """Format fail_to_pass as a string list for instance_info.txt.""" - return "[" + ", ".join(f"'{t}'" for t in self.fail_to_pass) + "]" + # Use repr so test IDs containing quotes (e.g. parametrized pytest ids + # like "test_x[can't]") stay valid Python literals that round-trip + # through the instance_info.txt parser. Manual quoting produced invalid + # syntax and silently dropped the whole list on parse. + return repr(list(self.fail_to_pass)) def to_pass_to_pass_str(self) -> str: """Format pass_to_pass as a string list for instance_info.txt.""" - return "[" + ", ".join(f"'{t}'" for t in self.pass_to_pass) + "]" + return repr(list(self.pass_to_pass)) @dataclass diff --git a/tests/test_wizard_models.py b/tests/test_wizard_models.py new file mode 100644 index 0000000..82a77a1 --- /dev/null +++ b/tests/test_wizard_models.py @@ -0,0 +1,34 @@ +"""Tests for anvil.wizard.models.""" + +from __future__ import annotations + +import ast + +from anvil.wizard import models + + +def test_test_spec_str_roundtrip_common_case(): + spec = models.TestSpec( + fail_to_pass=["tests/test_a.py::test_one"], + pass_to_pass=["tests/test_a.py::test_two", "tests/test_b.py::test_three"], + ) + assert ast.literal_eval(spec.to_fail_to_pass_str()) == spec.fail_to_pass + assert ast.literal_eval(spec.to_pass_to_pass_str()) == spec.pass_to_pass + + +def test_test_spec_str_roundtrip_with_quotes_in_test_id(): + # Parametrized pytest ids can contain apostrophes, e.g. test_x[can't]. + # These used to produce invalid Python and get silently dropped when the + # instance_info.txt parser evaluated the string. + fail = ["tests/test_x.py::test_quote[can't]"] + pass_ = ['tests/test_y.py::test_dquote[say "hi"]'] + spec = models.TestSpec(fail_to_pass=fail, pass_to_pass=pass_) + + assert ast.literal_eval(spec.to_fail_to_pass_str()) == fail + assert ast.literal_eval(spec.to_pass_to_pass_str()) == pass_ + + +def test_test_spec_str_empty(): + spec = models.TestSpec() + assert ast.literal_eval(spec.to_fail_to_pass_str()) == [] + assert ast.literal_eval(spec.to_pass_to_pass_str()) == []