tests: parameterize - #6
Conversation
|
@willynilly please review |
There was a problem hiding this comment.
Pull request overview
Updates the syntax validation test suite to use pytest parameterization so each JSON test vector runs as an individual test case, improving failure reporting granularity.
Changes:
- Replaces nested loops in
test_is_valid_syntax/test_not_is_valid_syntaxwith@pytest.mark.parametrizecases generated from JSON data. - Introduces a shared
syntax_data_as_paramsgenerator to build parameter sets and test ids.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def syntax_data_as_params(src_cb): | ||
| for term, examples in src_cb().items(): | ||
| for example in examples: | ||
| yield pytest.param(term, example, id=f"{term}-{example['value']}") |
There was a problem hiding this comment.
pytest.param(..., id=f"{term}-{example['value']}") will embed raw example values into the test id. The JSON test data includes control characters and newlines/tabs (e.g., "\t", "\n", "\u001f"), which can produce unreadable or even multi-line nodeids and make CI output/log parsing brittle. Consider escaping/sanitizing and truncating the value (e.g., using a repr/escaped form plus an index) when building the id.
| def syntax_data_as_params(src_cb): | |
| for term, examples in src_cb().items(): | |
| for example in examples: | |
| yield pytest.param(term, example, id=f"{term}-{example['value']}") | |
| def _syntax_param_id(term, value, index, max_length=40): | |
| safe_value = str(value).encode("unicode_escape").decode("ascii") | |
| if len(safe_value) > max_length: | |
| safe_value = f"{safe_value[: max_length - 3]}..." | |
| return f"{term}-{index}-{safe_value}" | |
| def syntax_data_as_params(src_cb): | |
| for term, examples in src_cb().items(): | |
| for index, example in enumerate(examples): | |
| yield pytest.param( | |
| term, | |
| example, | |
| id=_syntax_param_id(term, example["value"], index), | |
| ) |
| f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" | ||
| ) | ||
| assert ( | ||
| actual is True | ||
| ), f"Failed term: {term} for '{valid_example['value']}' : {valid_example['reason']}" | ||
|
|
||
| @pytest.mark.parametrize("term,invalid_example", syntax_data_as_params(invalid_syntax_data)) | ||
| def test_not_is_valid_syntax(term, invalid_example): | ||
| actual = h.is_valid_syntax(term=term, value=invalid_example["value"]) | ||
| print("") | ||
| print( | ||
| f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}" | ||
| ) | ||
| assert ( | ||
| actual is False | ||
| ), f"Failed term: {term} for '{invalid_example['value']}' : {invalid_example['reason']}" |
There was a problem hiding this comment.
The debug output / failure messages interpolate *_example['value'] directly. Since the test vectors include whitespace/control characters, this can make the printed line and assertion message hard to read (or span multiple lines). Consider formatting the value as an escaped/repr form in both the print and the assertion message so failures are diagnosable.
| f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" | |
| ) | |
| assert ( | |
| actual is True | |
| ), f"Failed term: {term} for '{valid_example['value']}' : {valid_example['reason']}" | |
| @pytest.mark.parametrize("term,invalid_example", syntax_data_as_params(invalid_syntax_data)) | |
| def test_not_is_valid_syntax(term, invalid_example): | |
| actual = h.is_valid_syntax(term=term, value=invalid_example["value"]) | |
| print("") | |
| print( | |
| f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}" | |
| ) | |
| assert ( | |
| actual is False | |
| ), f"Failed term: {term} for '{invalid_example['value']}' : {invalid_example['reason']}" | |
| f"Testing {term} with {valid_example['value']!r} : {valid_example['reason']}" | |
| ) | |
| assert ( | |
| actual is True | |
| ), f"Failed term: {term} for {valid_example['value']!r} : {valid_example['reason']}" | |
| @pytest.mark.parametrize("term,invalid_example", syntax_data_as_params(invalid_syntax_data)) | |
| def test_not_is_valid_syntax(term, invalid_example): | |
| actual = h.is_valid_syntax(term=term, value=invalid_example["value"]) | |
| print("") | |
| print( | |
| f"Testing {term} with {invalid_example['value']!r} : {invalid_example['reason']}" | |
| ) | |
| assert ( | |
| actual is False | |
| ), f"Failed term: {term} for {invalid_example['value']!r} : {invalid_example['reason']}" |
| @pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data)) | ||
| def test_is_valid_syntax(term, valid_example): |
There was a problem hiding this comment.
PR description mentions tests "can run in parallel", but the repo's testing extras currently only declare pytest (no pytest-xdist), so parametrization alone won't make the suite run in parallel by default. Either adjust the description/expectation, or add xdist to the testing extras / CI if parallel execution is a goal.
| def syntax_data_as_params(src_cb): | ||
| for term, examples in src_cb().items(): | ||
| for example in examples: | ||
| yield pytest.param(term, example, id=f"{term}-{example['value']}") | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data)) | ||
| def test_is_valid_syntax(term, valid_example): | ||
| actual = h.is_valid_syntax(term=term, value=valid_example["value"]) | ||
| print("") | ||
| print( | ||
| f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" | ||
| ) | ||
| assert ( | ||
| actual is True | ||
| ), f"Failed term: {term} for '{valid_example['value']}' : {valid_example['reason']}" | ||
|
|
||
| @pytest.mark.parametrize("term,invalid_example", syntax_data_as_params(invalid_syntax_data)) | ||
| def test_not_is_valid_syntax(term, invalid_example): | ||
| actual = h.is_valid_syntax(term=term, value=invalid_example["value"]) | ||
| print("") | ||
| print( | ||
| f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}" | ||
| ) | ||
| assert ( | ||
| actual is False | ||
| ), f"Failed term: {term} for '{invalid_example['value']}' : {invalid_example['reason']}" |
There was a problem hiding this comment.
Parametrizing every example creates one pytest test case per JSON entry; with invalid_syntax.json being large, this can significantly increase collection time and per-test overhead compared to a single loop-based test. If runtime becomes an issue, consider parametrizing at a coarser granularity (e.g., per term) while still aggregating failures within the test, so you keep the "report all failures" behavior without thousands of separate test nodes.
| def syntax_data_as_params(src_cb): | |
| for term, examples in src_cb().items(): | |
| for example in examples: | |
| yield pytest.param(term, example, id=f"{term}-{example['value']}") | |
| @pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data)) | |
| def test_is_valid_syntax(term, valid_example): | |
| actual = h.is_valid_syntax(term=term, value=valid_example["value"]) | |
| print("") | |
| print( | |
| f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" | |
| ) | |
| assert ( | |
| actual is True | |
| ), f"Failed term: {term} for '{valid_example['value']}' : {valid_example['reason']}" | |
| @pytest.mark.parametrize("term,invalid_example", syntax_data_as_params(invalid_syntax_data)) | |
| def test_not_is_valid_syntax(term, invalid_example): | |
| actual = h.is_valid_syntax(term=term, value=invalid_example["value"]) | |
| print("") | |
| print( | |
| f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}" | |
| ) | |
| assert ( | |
| actual is False | |
| ), f"Failed term: {term} for '{invalid_example['value']}' : {invalid_example['reason']}" | |
| def syntax_data_by_term(src_cb): | |
| for term, examples in src_cb().items(): | |
| yield pytest.param(term, examples, id=term) | |
| @pytest.mark.parametrize("term,valid_examples", syntax_data_by_term(valid_syntax_data)) | |
| def test_is_valid_syntax(term, valid_examples): | |
| failures = [] | |
| for valid_example in valid_examples: | |
| actual = h.is_valid_syntax(term=term, value=valid_example["value"]) | |
| print("") | |
| print( | |
| f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" | |
| ) | |
| if actual is not True: | |
| failures.append( | |
| f"Failed term: {term} for '{valid_example['value']}' : {valid_example['reason']}" | |
| ) | |
| if failures: | |
| pytest.fail("\n".join(failures)) | |
| @pytest.mark.parametrize("term,invalid_examples", syntax_data_by_term(invalid_syntax_data)) | |
| def test_not_is_valid_syntax(term, invalid_examples): | |
| failures = [] | |
| for invalid_example in invalid_examples: | |
| actual = h.is_valid_syntax(term=term, value=invalid_example["value"]) | |
| print("") | |
| print( | |
| f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}" | |
| ) | |
| if actual is not False: | |
| failures.append( | |
| f"Failed term: {term} for '{invalid_example['value']}' : {invalid_example['reason']}" | |
| ) | |
| if failures: | |
| pytest.fail("\n".join(failures)) |
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
|
Hi! Since this repository is no longer actively maintained, I created and now maintain a fork of this package long-term:
Would you mind retargeting (or reopening) this pull request against the maintained fork so your fix/feature can be reviewed and merged there? Thank you! |
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1d64a6f to
5c3c7b6
Compare
i found it annoying that tests stopped on the first error.
so i changed the tests to be properly parameterized. this way, tests can run in parallel and dont fail fast - so that ALL errors are found, not only the first one ...