Skip to content

Commit 6cf31c6

Browse files
committed
Improve docs and warnings for assert mode checking
1 parent 5310da6 commit 6cf31c6

File tree

6 files changed

+52
-22
lines changed

6 files changed

+52
-22
lines changed

crosshair/examples/check_examples_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def extract_linenums(text: str) -> List[int]:
2424

2525
def find_examples() -> Iterable[Path]:
2626
examples_dir = pathlib.Path(os.path.realpath(__file__)).parent
27-
for path in sorted(examples_dir.glob("**/*.py")):
27+
for path in sorted(examples_dir.glob("*/**/*.py")):
2828
if path.stem != "__init__":
2929
yield path
3030

crosshair/main.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -860,16 +860,24 @@ def check(
860860
if isinstance(entities, int):
861861
return entities
862862
full_options = DEFAULT_OPTIONS.overlay(report_verbose=False).overlay(options)
863-
for entity in entities:
864-
debug("Check ", getattr(entity, "__name__", str(entity)))
865-
for message in run_checkables(analyze_any(entity, options)):
866-
line = describe_message(message, full_options)
867-
if line is None:
868-
continue
869-
stdout.write(line + "\n")
870-
debug("Traceback for output message:\n", message.traceback)
871-
if message.state > MessageType.PRE_UNSAT:
872-
any_problems = True
863+
checkables = [c for e in entities for c in analyze_any(e, options)]
864+
if not checkables:
865+
extra_help = ""
866+
if full_options.analysis_kind == [AnalysisKind.asserts]:
867+
extra_help = "\nHINT: Ensure that your functions to analyze lead with assert statements."
868+
print(
869+
"WARNING: Targets found, but contain no checkable functions." + extra_help,
870+
file=stderr,
871+
)
872+
873+
for message in run_checkables(checkables):
874+
line = describe_message(message, full_options)
875+
if line is None:
876+
continue
877+
stdout.write(line + "\n")
878+
debug("Traceback for output message:\n", message.traceback)
879+
if message.state > MessageType.PRE_UNSAT:
880+
any_problems = True
873881
return 1 if any_problems else 0
874882

875883

crosshair/main_test.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -251,19 +251,28 @@ def test_no_args_prints_usage(root):
251251
assert re.search(r"^usage", out)
252252

253253

254-
def DISABLE_TODO_test_assert_mode_e2e(root):
254+
def test_assert_mode_e2e(root, capsys: pytest.CaptureFixture[str]):
255255
simplefs(root, ASSERT_BASED_FOO)
256-
try:
257-
sys.stdout = io.StringIO()
258-
exitcode = unwalled_main(["check", root / "foo.py", "--analysis_kind=asserts"])
259-
finally:
260-
out = sys.stdout.getvalue()
261-
sys.stdout = sys.__stdout__
262-
assert exitcode == 1
256+
exitcode = unwalled_main(["check", str(root / "foo.py"), "--analysis_kind=asserts"])
257+
(out, err) = capsys.readouterr()
258+
assert err == ""
263259
assert re.search(
264-
r"foo.py\:8\: error\: AssertionError\: when calling foofn\(x \= 100\)", out
260+
r"foo.py\:8\: error\: AssertionError\: when calling foofn\(100\)", out
265261
)
266262
assert len([ls for ls in out.split("\n") if ls]) == 1
263+
assert exitcode == 1
264+
265+
266+
def test_assert_without_checkable(root, capsys: pytest.CaptureFixture[str]):
267+
simplefs(root, SIMPLE_FOO)
268+
exitcode = unwalled_main(["check", str(root / "foo.py"), "--analysis_kind=asserts"])
269+
(out, err) = capsys.readouterr()
270+
assert (
271+
err
272+
== "WARNING: Targets found, but contain no checkable functions.\nHINT: Ensure that your functions to analyze lead with assert statements.\n"
273+
)
274+
assert out == ""
275+
assert exitcode == 0
267276

268277

269278
def test_directives(root):

doc/source/changelog.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ Next Version
99
* Nothing yet!
1010

1111

12+
Version 0.0.94
13+
--------------
14+
15+
* Add missing support for the `type` statement introduced in Python 3.12.
16+
(fixes `#362 <https://github.com/pschanely/CrossHair/issues/362>`__)
17+
* Improve documentation and warning messages for assert-mode checking.
18+
(inspired by
19+
[this thread](https://github.com/pschanely/CrossHair/issues/361#issuecomment-3019563904))
20+
21+
1222
Version 0.0.93
1323
--------------
1424

doc/source/contracts.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Contract Syntaxes
1313
CrossHair can check many different kinds of contracts; choose one that fits you best:
1414

1515
+----------------------------------------------+--------------------------------------------------------------------------+
16-
| :ref:`asserts <analysis_kind_asserts>` | Use regular Python assert statements. That's it. |
16+
| :ref:`asserts <analysis_kind_asserts>` | Use regular Python assert statements. |
1717
| | |
1818
+----------------------------------------------+--------------------------------------------------------------------------+
1919
| :ref:`PEP 316 <analysis_kind_pep316>` | Docstring-based contracts. |

doc/source/kinds_of_contracts.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Assert-based Contracts
3131

3232
This is the lowest-investment way to use contracts with CrossHair. You just use
3333
regular `assert statements`_ in your code. There's **no library to import** and
34-
**no syntax to learn**: just use assert statements.
34+
**no syntax to learn**.
3535

3636
.. _assert statements: https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement
3737

@@ -61,6 +61,9 @@ statements. (it will ignore any function that does not!)
6161
The leading assert statement(s) are considered to be preconditions: CrossHair
6262
will try to find inputs that make these true.
6363

64+
If your function does not have any preconditions, but you still want CrossHair
65+
to analyze it, you should add an ``assert True`` statement at the beginning.
66+
6467
After the precondition asserts, we expect the remaining asserts to pass for all
6568
inputs.
6669

0 commit comments

Comments
 (0)