crosshair-tool 0.0.83__cp38-cp38-macosx_10_9_universal2.whl → 0.0.85__cp38-cp38-macosx_10_9_universal2.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crosshair-tool might be problematic. Click here for more details.
- _crosshair_tracers.cpython-38-darwin.so +0 -0
- crosshair/__init__.py +1 -1
- crosshair/_mark_stacks.h +0 -25
- crosshair/_tracers.h +2 -0
- crosshair/_tracers_test.py +8 -2
- crosshair/auditwall.py +0 -1
- crosshair/auditwall_test.py +5 -0
- crosshair/condition_parser.py +5 -5
- crosshair/condition_parser_test.py +50 -63
- crosshair/copyext.py +23 -7
- crosshair/copyext_test.py +11 -1
- crosshair/core.py +23 -17
- crosshair/core_test.py +625 -584
- crosshair/diff_behavior_test.py +14 -21
- crosshair/dynamic_typing.py +90 -1
- crosshair/dynamic_typing_test.py +73 -1
- crosshair/enforce_test.py +15 -22
- crosshair/fnutil_test.py +4 -8
- crosshair/libimpl/arraylib.py +2 -5
- crosshair/libimpl/binasciilib.py +2 -3
- crosshair/libimpl/builtinslib.py +28 -21
- crosshair/libimpl/builtinslib_test.py +1 -8
- crosshair/libimpl/collectionslib.py +18 -3
- crosshair/libimpl/collectionslib_test.py +89 -15
- crosshair/libimpl/encodings/_encutil.py +8 -3
- crosshair/libimpl/mathlib_test.py +0 -7
- crosshair/libimpl/relib_ch_test.py +2 -2
- crosshair/libimpl/timelib.py +34 -15
- crosshair/libimpl/timelib_test.py +12 -2
- crosshair/lsp_server.py +1 -1
- crosshair/main.py +3 -1
- crosshair/objectproxy_test.py +7 -11
- crosshair/opcode_intercept.py +24 -8
- crosshair/opcode_intercept_test.py +13 -2
- crosshair/py.typed +0 -0
- crosshair/tracers.py +27 -9
- crosshair/type_repo.py +2 -2
- crosshair/unicode_categories.py +1 -0
- crosshair/util.py +45 -16
- crosshair/watcher.py +2 -2
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/METADATA +2 -2
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/RECORD +46 -45
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/LICENSE +0 -0
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/WHEEL +0 -0
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/entry_points.txt +0 -0
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/top_level.txt +0 -0
crosshair/core_test.py
CHANGED
|
@@ -4,7 +4,6 @@ import inspect
|
|
|
4
4
|
import re
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
|
-
import unittest
|
|
8
7
|
from typing import *
|
|
9
8
|
|
|
10
9
|
import pytest # type: ignore
|
|
@@ -297,13 +296,12 @@ class RegularInt:
|
|
|
297
296
|
return num
|
|
298
297
|
|
|
299
298
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
self.assertEqual(params["num"].annotation, int)
|
|
299
|
+
def test_get_constructor_signature_with_new():
|
|
300
|
+
assert RegularInt(7) == 7
|
|
301
|
+
params = get_constructor_signature(RegularInt).parameters
|
|
302
|
+
assert len(params) == 1
|
|
303
|
+
assert params["num"].name == "num"
|
|
304
|
+
assert params["num"].annotation == int
|
|
307
305
|
|
|
308
306
|
|
|
309
307
|
def test_proxy_alone() -> None:
|
|
@@ -383,393 +381,428 @@ def test_exc_handling_doesnt_catch_crosshair_timeout():
|
|
|
383
381
|
)
|
|
384
382
|
|
|
385
383
|
|
|
386
|
-
|
|
387
|
-
def
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return foo.x
|
|
384
|
+
def test_obj_member_fail() -> None:
|
|
385
|
+
def f(foo: Pokeable) -> int:
|
|
386
|
+
"""
|
|
387
|
+
pre: 0 <= foo.x <= 4
|
|
388
|
+
post[foo]: _ < 5
|
|
389
|
+
"""
|
|
390
|
+
foo.poke()
|
|
391
|
+
foo.poke()
|
|
392
|
+
return foo.x
|
|
396
393
|
|
|
397
|
-
|
|
394
|
+
check_states(f, POST_FAIL)
|
|
398
395
|
|
|
399
|
-
def test_obj_member_nochange_ok(self) -> None:
|
|
400
|
-
def f(foo: Pokeable) -> int:
|
|
401
|
-
"""post: _ == foo.x"""
|
|
402
|
-
return foo.x
|
|
403
396
|
|
|
404
|
-
|
|
397
|
+
def test_obj_member_nochange_ok() -> None:
|
|
398
|
+
def f(foo: Pokeable) -> int:
|
|
399
|
+
"""post: _ == foo.x"""
|
|
400
|
+
return foo.x
|
|
405
401
|
|
|
406
|
-
|
|
407
|
-
def f(foo: Pokeable) -> int:
|
|
408
|
-
"""
|
|
409
|
-
pre: foo.x >= 0
|
|
410
|
-
post[foo]: foo.x >= 2
|
|
411
|
-
"""
|
|
412
|
-
foo.poke()
|
|
413
|
-
foo.poke()
|
|
414
|
-
return foo.x
|
|
402
|
+
check_states(f, CONFIRMED)
|
|
415
403
|
|
|
416
|
-
check_states(f, CONFIRMED)
|
|
417
404
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
405
|
+
def test_obj_member_change_ok() -> None:
|
|
406
|
+
def f(foo: Pokeable) -> int:
|
|
407
|
+
"""
|
|
408
|
+
pre: foo.x >= 0
|
|
409
|
+
post[foo]: foo.x >= 2
|
|
410
|
+
"""
|
|
411
|
+
foo.poke()
|
|
412
|
+
foo.poke()
|
|
413
|
+
return foo.x
|
|
426
414
|
|
|
427
|
-
|
|
415
|
+
check_states(f, CONFIRMED)
|
|
428
416
|
|
|
429
|
-
def test_example_second_largest(self) -> None:
|
|
430
|
-
def second_largest(items: List[int]) -> int:
|
|
431
|
-
"""
|
|
432
|
-
pre: len(items) == 3 # (length is to cap runtime)
|
|
433
|
-
post: _ == sorted(items)[-2]
|
|
434
|
-
"""
|
|
435
|
-
next_largest, largest = items[:2]
|
|
436
|
-
if largest < next_largest:
|
|
437
|
-
next_largest, largest = largest, next_largest
|
|
438
|
-
|
|
439
|
-
for item in items[2:]:
|
|
440
|
-
if item > largest:
|
|
441
|
-
largest, next_largest = (item, largest)
|
|
442
|
-
elif item > next_largest:
|
|
443
|
-
next_largest = item
|
|
444
|
-
return next_largest
|
|
445
|
-
|
|
446
|
-
check_states(second_largest, CONFIRMED)
|
|
447
|
-
|
|
448
|
-
def test_pokeable_class(self) -> None:
|
|
449
|
-
messages = analyze_class(Pokeable)
|
|
450
|
-
line = Pokeable.wild_pokeby.__code__.co_firstlineno
|
|
451
|
-
self.assertEqual(
|
|
452
|
-
*check_messages(messages, state=MessageType.POST_FAIL, line=line, column=0)
|
|
453
|
-
)
|
|
454
417
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
messages = analyze_any(
|
|
464
|
-
walk_qualname(Person, "a_regular_method"),
|
|
465
|
-
AnalysisOptionSet(),
|
|
466
|
-
)
|
|
467
|
-
self.assertEqual(*check_messages(messages, state=MessageType.CONFIRMED))
|
|
418
|
+
def test_obj_member_change_detect() -> None:
|
|
419
|
+
def f(foo: Pokeable) -> int:
|
|
420
|
+
"""
|
|
421
|
+
pre: foo.x > 0
|
|
422
|
+
post[]: True
|
|
423
|
+
"""
|
|
424
|
+
foo.poke()
|
|
425
|
+
return foo.x
|
|
468
426
|
|
|
469
|
-
|
|
470
|
-
messages = analyze_any(
|
|
471
|
-
walk_qualname(Person, "a_class_method"),
|
|
472
|
-
AnalysisOptionSet(),
|
|
473
|
-
)
|
|
474
|
-
self.assertEqual(*check_messages(messages, state=MessageType.CONFIRMED))
|
|
427
|
+
check_states(f, POST_ERR)
|
|
475
428
|
|
|
476
|
-
def test_static_method(self) -> None:
|
|
477
|
-
messages = analyze_any(
|
|
478
|
-
walk_qualname(Person, "a_static_method"),
|
|
479
|
-
AnalysisOptionSet(),
|
|
480
|
-
)
|
|
481
|
-
self.assertEqual(*check_messages(messages, state=MessageType.CONFIRMED))
|
|
482
429
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
430
|
+
def test_example_second_largest() -> None:
|
|
431
|
+
def second_largest(items: List[int]) -> int:
|
|
432
|
+
"""
|
|
433
|
+
pre: len(items) == 3 # (length is to cap runtime)
|
|
434
|
+
post: _ == sorted(items)[-2]
|
|
435
|
+
"""
|
|
436
|
+
next_largest, largest = items[:2]
|
|
437
|
+
if largest < next_largest:
|
|
438
|
+
next_largest, largest = largest, next_largest
|
|
489
439
|
|
|
490
|
-
|
|
440
|
+
for item in items[2:]:
|
|
441
|
+
if item > largest:
|
|
442
|
+
largest, next_largest = (item, largest)
|
|
443
|
+
elif item > next_largest:
|
|
444
|
+
next_largest = item
|
|
445
|
+
return next_largest
|
|
491
446
|
|
|
492
|
-
|
|
493
|
-
def f(p: PersonWithoutAttributes) -> PersonWithoutAttributes:
|
|
494
|
-
"""
|
|
495
|
-
post: _.age != 222
|
|
496
|
-
"""
|
|
497
|
-
return PersonTuple(p.name, p.age + 1) # type: ignore
|
|
447
|
+
check_states(second_largest, CONFIRMED)
|
|
498
448
|
|
|
499
|
-
check_states(f, POST_FAIL)
|
|
500
449
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
oldbirth = p.birth
|
|
509
|
-
p.age = p.age + 1
|
|
510
|
-
assert oldbirth == p.birth + 1
|
|
450
|
+
def test_pokeable_class() -> None:
|
|
451
|
+
messages = analyze_class(Pokeable)
|
|
452
|
+
line = Pokeable.wild_pokeby.__code__.co_firstlineno
|
|
453
|
+
actual, expected = check_messages(
|
|
454
|
+
messages, state=MessageType.POST_FAIL, line=line, column=0
|
|
455
|
+
)
|
|
456
|
+
assert actual == expected
|
|
511
457
|
|
|
512
|
-
check_states(f, CONFIRMED)
|
|
513
458
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
"""post: _ == self.time"""
|
|
519
|
-
return 120
|
|
459
|
+
def test_person_class() -> None:
|
|
460
|
+
messages = analyze_class(Person)
|
|
461
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
462
|
+
assert actual == expected
|
|
520
463
|
|
|
521
|
-
messages = analyze_class(Clock)
|
|
522
|
-
self.assertEqual(*check_messages(messages, state=MessageType.CONFIRMED))
|
|
523
464
|
|
|
524
|
-
|
|
525
|
-
|
|
465
|
+
def test_methods_directly() -> None:
|
|
466
|
+
# Running analysis on individual methods directly works a little
|
|
467
|
+
# differently, especially for staticmethod/classmethod. Confirm these
|
|
468
|
+
# don't explode:
|
|
469
|
+
messages = analyze_any(
|
|
470
|
+
walk_qualname(Person, "a_regular_method"),
|
|
471
|
+
AnalysisOptionSet(),
|
|
472
|
+
)
|
|
473
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
474
|
+
assert actual == expected
|
|
526
475
|
|
|
527
|
-
@dataclasses.dataclass
|
|
528
|
-
class MaybePair(Generic[T]):
|
|
529
|
-
"""
|
|
530
|
-
inv: (self.left is None) == (self.right is None)
|
|
531
|
-
"""
|
|
532
476
|
|
|
533
|
-
|
|
534
|
-
|
|
477
|
+
def test_class_method() -> None:
|
|
478
|
+
messages = analyze_any(
|
|
479
|
+
walk_qualname(Person, "a_class_method"),
|
|
480
|
+
AnalysisOptionSet(),
|
|
481
|
+
)
|
|
482
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
483
|
+
assert actual == expected
|
|
535
484
|
|
|
536
|
-
def setpair(self, left: Optional[T], right: Optional[T]):
|
|
537
|
-
"""post[self]: True"""
|
|
538
|
-
if (left is None) ^ (right is None):
|
|
539
|
-
raise ValueError(
|
|
540
|
-
"Populate both values or neither value in the pair"
|
|
541
|
-
)
|
|
542
|
-
self.left, self.right = left, right
|
|
543
485
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
)
|
|
547
|
-
|
|
486
|
+
def test_static_method() -> None:
|
|
487
|
+
messages = analyze_any(
|
|
488
|
+
walk_qualname(Person, "a_static_method"),
|
|
489
|
+
AnalysisOptionSet(),
|
|
490
|
+
)
|
|
491
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
492
|
+
assert actual == expected
|
|
548
493
|
|
|
549
|
-
def test_bad_invariant(self):
|
|
550
|
-
class WithBadInvariant:
|
|
551
|
-
"""
|
|
552
|
-
inv: self.item == 7
|
|
553
|
-
"""
|
|
554
494
|
|
|
555
|
-
|
|
556
|
-
|
|
495
|
+
def test_extend_namedtuple() -> None:
|
|
496
|
+
def f(p: PersonTuple) -> PersonTuple:
|
|
497
|
+
"""
|
|
498
|
+
post: _.age != 222
|
|
499
|
+
"""
|
|
500
|
+
return PersonTuple(p.name, p.age + 1)
|
|
557
501
|
|
|
558
|
-
|
|
559
|
-
*check_messages(
|
|
560
|
-
analyze_class(WithBadInvariant), state=MessageType.PRE_UNSAT
|
|
561
|
-
)
|
|
562
|
-
)
|
|
502
|
+
check_states(f, POST_FAIL)
|
|
563
503
|
|
|
564
|
-
|
|
504
|
+
|
|
505
|
+
def test_without_typed_attributes() -> None:
|
|
506
|
+
def f(p: PersonWithoutAttributes) -> PersonWithoutAttributes:
|
|
565
507
|
"""
|
|
566
|
-
|
|
567
|
-
that invariants for these methods can resolve names in the
|
|
568
|
-
correct namespace.
|
|
508
|
+
post: _.age != 222
|
|
569
509
|
"""
|
|
570
|
-
|
|
571
|
-
*check_messages(
|
|
572
|
-
analyze_class(ReferenceHoldingClass), state=MessageType.CONFIRMED
|
|
573
|
-
)
|
|
574
|
-
)
|
|
510
|
+
return PersonTuple(p.name, p.age + 1) # type: ignore
|
|
575
511
|
|
|
576
|
-
|
|
577
|
-
self.assertEqual(
|
|
578
|
-
*check_messages(analyze_class(SmokeDetector), state=MessageType.CONFIRMED)
|
|
579
|
-
)
|
|
512
|
+
check_states(f, POST_FAIL)
|
|
580
513
|
|
|
581
|
-
def test_super(self):
|
|
582
|
-
class FooDetector(SmokeDetector):
|
|
583
|
-
def signaling_alarm(self, air_samples: List[int]):
|
|
584
|
-
return super().signaling_alarm(air_samples)
|
|
585
514
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
515
|
+
def test_property() -> None:
|
|
516
|
+
def f(p: Person) -> None:
|
|
517
|
+
"""
|
|
518
|
+
pre: 0 <= p.age < 100
|
|
519
|
+
post[p]: p.birth + p.age == NOW
|
|
520
|
+
"""
|
|
521
|
+
assert p.age == NOW - p.birth
|
|
522
|
+
oldbirth = p.birth
|
|
523
|
+
p.age = p.age + 1
|
|
524
|
+
assert oldbirth == p.birth + 1
|
|
589
525
|
|
|
590
|
-
|
|
591
|
-
class CarbonMonoxideDetector(SmokeDetector):
|
|
592
|
-
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
593
|
-
"""
|
|
594
|
-
post: implies(AirSample.CO2 in air_samples, _ == True)
|
|
595
|
-
"""
|
|
596
|
-
return AirSample.CO2 in air_samples # fails: does not detect smoke
|
|
597
|
-
|
|
598
|
-
self.assertEqual(
|
|
599
|
-
*check_messages(
|
|
600
|
-
analyze_class(CarbonMonoxideDetector), state=MessageType.POST_FAIL
|
|
601
|
-
)
|
|
602
|
-
)
|
|
526
|
+
check_states(f, CONFIRMED)
|
|
603
527
|
|
|
604
|
-
def test_inherited_preconditions_overridable(self):
|
|
605
|
-
@dataclasses.dataclass
|
|
606
|
-
class SmokeDetectorWithBattery(SmokeDetector):
|
|
607
|
-
_battery_power: int
|
|
608
|
-
|
|
609
|
-
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
610
|
-
"""
|
|
611
|
-
pre: self._battery_power > 0 or self._is_plugged_in
|
|
612
|
-
post: self._battery_power > 0
|
|
613
|
-
"""
|
|
614
|
-
return AirSample.SMOKE in air_samples
|
|
615
|
-
|
|
616
|
-
self.assertEqual(
|
|
617
|
-
*check_messages(
|
|
618
|
-
analyze_class(SmokeDetectorWithBattery), state=MessageType.POST_FAIL
|
|
619
|
-
)
|
|
620
|
-
)
|
|
621
528
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return foo.size()
|
|
629
|
-
|
|
630
|
-
# Type repo doesn't load crosshair classes by default; load manually:
|
|
631
|
-
type_repo._add_class(Cat)
|
|
632
|
-
type_repo._add_class(BiggerCat)
|
|
633
|
-
check_states(f, POST_FAIL)
|
|
634
|
-
|
|
635
|
-
def test_does_not_report_with_actual_repr(self):
|
|
636
|
-
def f(foo: BiggerCat) -> int:
|
|
637
|
-
"""post: False"""
|
|
638
|
-
return foo.size()
|
|
639
|
-
|
|
640
|
-
(actual, expected) = check_messages(
|
|
641
|
-
analyze_function(f),
|
|
642
|
-
state=MessageType.POST_FAIL,
|
|
643
|
-
message="false when calling f(BiggerCat()) " "(which returns 2)",
|
|
644
|
-
)
|
|
645
|
-
assert expected == actual
|
|
529
|
+
def test_readonly_property_contract() -> None:
|
|
530
|
+
class Clock:
|
|
531
|
+
@property
|
|
532
|
+
def time(self) -> int:
|
|
533
|
+
"""post: _ == self.time"""
|
|
534
|
+
return 120
|
|
646
535
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
536
|
+
messages = analyze_class(Clock)
|
|
537
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
538
|
+
assert actual == expected
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_typevar_basic() -> None:
|
|
542
|
+
T = TypeVar("T")
|
|
543
|
+
|
|
544
|
+
@dataclasses.dataclass
|
|
545
|
+
class MaybePair(Generic[T]):
|
|
546
|
+
"""
|
|
547
|
+
inv: (self.left is None) == (self.right is None)
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
left: Optional[T]
|
|
551
|
+
right: Optional[T]
|
|
552
|
+
|
|
553
|
+
def setpair(self, left: Optional[T], right: Optional[T]):
|
|
554
|
+
"""post[self]: True"""
|
|
555
|
+
if (left is None) ^ (right is None):
|
|
556
|
+
raise ValueError("Populate both values or neither value in the pair")
|
|
557
|
+
self.left, self.right = left, right
|
|
653
558
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
return other_size - self.size()
|
|
559
|
+
messages = analyze_function(
|
|
560
|
+
FunctionInfo(MaybePair, "setpair", MaybePair.__dict__["setpair"])
|
|
561
|
+
)
|
|
562
|
+
actual, expected = check_messages(messages, state=MessageType.EXEC_ERR)
|
|
563
|
+
assert actual == expected
|
|
660
564
|
|
|
661
|
-
class Child(Parent):
|
|
662
|
-
def size(self) -> int:
|
|
663
|
-
return 2
|
|
664
565
|
|
|
665
|
-
|
|
666
|
-
|
|
566
|
+
def test_bad_invariant():
|
|
567
|
+
class WithBadInvariant:
|
|
568
|
+
"""
|
|
569
|
+
inv: self.item == 7
|
|
570
|
+
"""
|
|
667
571
|
|
|
668
|
-
|
|
669
|
-
|
|
572
|
+
def do_a_thing(self) -> None:
|
|
573
|
+
pass
|
|
670
574
|
|
|
671
|
-
|
|
672
|
-
|
|
575
|
+
actual, expected = check_messages(
|
|
576
|
+
analyze_class(WithBadInvariant), state=MessageType.PRE_UNSAT
|
|
577
|
+
)
|
|
578
|
+
assert actual == expected
|
|
673
579
|
|
|
674
|
-
def __repr__(self):
|
|
675
|
-
return f"FinalCat with {self.legs} legs"
|
|
676
580
|
|
|
677
|
-
|
|
581
|
+
def test_expr_name_resolution():
|
|
582
|
+
"""
|
|
583
|
+
dataclass() generates several methods. It can be tricky to ensure
|
|
584
|
+
that invariants for these methods can resolve names in the
|
|
585
|
+
correct namespace.
|
|
586
|
+
"""
|
|
587
|
+
actual, expected = check_messages(
|
|
588
|
+
analyze_class(ReferenceHoldingClass), state=MessageType.CONFIRMED
|
|
589
|
+
)
|
|
590
|
+
assert actual == expected
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def test_inheritance_base_class_ok():
|
|
594
|
+
actual, expected = check_messages(
|
|
595
|
+
analyze_class(SmokeDetector), state=MessageType.CONFIRMED
|
|
596
|
+
)
|
|
597
|
+
assert actual == expected
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def test_super():
|
|
601
|
+
class FooDetector(SmokeDetector):
|
|
602
|
+
def signaling_alarm(self, air_samples: List[int]):
|
|
603
|
+
return super().signaling_alarm(air_samples)
|
|
604
|
+
|
|
605
|
+
actual, expected = check_messages(
|
|
606
|
+
analyze_class(FooDetector), state=MessageType.CONFIRMED
|
|
607
|
+
)
|
|
608
|
+
assert actual == expected
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def test_use_inherited_postconditions():
|
|
612
|
+
class CarbonMonoxideDetector(SmokeDetector):
|
|
613
|
+
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
678
614
|
"""
|
|
679
|
-
|
|
680
|
-
post: __return__ >= 4
|
|
615
|
+
post: implies(AirSample.CO2 in air_samples, _ == True)
|
|
681
616
|
"""
|
|
682
|
-
return
|
|
617
|
+
return AirSample.CO2 in air_samples # fails: does not detect smoke
|
|
683
618
|
|
|
684
|
-
|
|
619
|
+
actual, expected = check_messages(
|
|
620
|
+
analyze_class(CarbonMonoxideDetector), state=MessageType.POST_FAIL
|
|
621
|
+
)
|
|
622
|
+
assert actual == expected
|
|
685
623
|
|
|
686
|
-
# TODO: precondition strengthening check
|
|
687
|
-
def TODO_test_cannot_strengthen_inherited_preconditions(self):
|
|
688
|
-
class PowerHungrySmokeDetector(SmokeDetector):
|
|
689
|
-
_battery_power: int
|
|
690
|
-
|
|
691
|
-
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
692
|
-
"""
|
|
693
|
-
pre: self._is_plugged_in
|
|
694
|
-
pre: self._battery_power > 0
|
|
695
|
-
"""
|
|
696
|
-
return AirSample.SMOKE in air_samples
|
|
697
|
-
|
|
698
|
-
self.assertEqual(
|
|
699
|
-
*check_messages(
|
|
700
|
-
analyze_class(PowerHungrySmokeDetector), state=MessageType.PRE_INVALID
|
|
701
|
-
)
|
|
702
|
-
)
|
|
703
624
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
x = proxy_for_type(Number, "x", allow_subtypes=False)
|
|
709
|
-
assert isinstance(x, SymbolicInt)
|
|
625
|
+
def test_inherited_preconditions_overridable():
|
|
626
|
+
@dataclasses.dataclass
|
|
627
|
+
class SmokeDetectorWithBattery(SmokeDetector):
|
|
628
|
+
_battery_power: int
|
|
710
629
|
|
|
711
|
-
|
|
712
|
-
|
|
630
|
+
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
631
|
+
"""
|
|
632
|
+
pre: self._battery_power > 0 or self._is_plugged_in
|
|
633
|
+
post: self._battery_power > 0
|
|
634
|
+
"""
|
|
635
|
+
return AirSample.SMOKE in air_samples
|
|
636
|
+
|
|
637
|
+
actual, expected = check_messages(
|
|
638
|
+
analyze_class(SmokeDetectorWithBattery), state=MessageType.POST_FAIL
|
|
639
|
+
)
|
|
640
|
+
assert actual == expected
|
|
713
641
|
|
|
714
|
-
def f(s: Sequence[T]) -> Dict[T, T]:
|
|
715
|
-
"""post: len(_) == len(s)"""
|
|
716
|
-
return dict(zip(s, s))
|
|
717
642
|
|
|
718
|
-
|
|
719
|
-
|
|
643
|
+
def test_use_subclasses_of_arguments():
|
|
644
|
+
# Even though the argument below is typed as the base class, the fact
|
|
645
|
+
# that a faulty implementation exists is enough to produce a
|
|
646
|
+
# counterexample:
|
|
647
|
+
def f(foo: Cat) -> int:
|
|
648
|
+
"""post: _ == 1"""
|
|
649
|
+
return foo.size()
|
|
650
|
+
|
|
651
|
+
# Type repo doesn't load crosshair classes by default; load manually:
|
|
652
|
+
type_repo._add_class(Cat)
|
|
653
|
+
type_repo._add_class(BiggerCat)
|
|
654
|
+
check_states(f, POST_FAIL)
|
|
720
655
|
|
|
721
|
-
def test_typevar_bounds_fail(self) -> None:
|
|
722
|
-
T = TypeVar("T")
|
|
723
656
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
657
|
+
def test_does_not_report_with_actual_repr():
|
|
658
|
+
def f(foo: BiggerCat) -> int:
|
|
659
|
+
"""post: False"""
|
|
660
|
+
return foo.size()
|
|
727
661
|
|
|
728
|
-
|
|
662
|
+
(actual, expected) = check_messages(
|
|
663
|
+
analyze_function(f),
|
|
664
|
+
state=MessageType.POST_FAIL,
|
|
665
|
+
message="false when calling f(BiggerCat()) " "(which returns 2)",
|
|
666
|
+
)
|
|
667
|
+
assert expected == actual
|
|
729
668
|
|
|
730
|
-
def test_typevar_bounds_ok(self) -> None:
|
|
731
|
-
B = TypeVar("B", bound=int)
|
|
732
669
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
670
|
+
def test_check_parent_conditions():
|
|
671
|
+
# Ensure that conditions of parent classes are checked in children
|
|
672
|
+
# even when not overridden.
|
|
673
|
+
class Parent:
|
|
674
|
+
def size(self) -> int:
|
|
675
|
+
return 1
|
|
736
676
|
|
|
737
|
-
|
|
677
|
+
def amount_smaller(self, other_size: int) -> int:
|
|
678
|
+
"""
|
|
679
|
+
pre: other_size >= 1
|
|
680
|
+
post: _ >= 0
|
|
681
|
+
"""
|
|
682
|
+
return other_size - self.size()
|
|
738
683
|
|
|
739
|
-
|
|
740
|
-
def
|
|
741
|
-
|
|
742
|
-
return x is None
|
|
684
|
+
class Child(Parent):
|
|
685
|
+
def size(self) -> int:
|
|
686
|
+
return 2
|
|
743
687
|
|
|
744
|
-
|
|
688
|
+
messages = analyze_class(Child)
|
|
689
|
+
actual, expected = check_messages(messages, state=MessageType.POST_FAIL)
|
|
690
|
+
assert actual == expected
|
|
745
691
|
|
|
746
|
-
|
|
747
|
-
|
|
692
|
+
|
|
693
|
+
def test_final_with_concrete_proxy():
|
|
694
|
+
from typing import Final
|
|
695
|
+
|
|
696
|
+
class FinalCat:
|
|
697
|
+
legs: Final[int] = 4
|
|
698
|
+
|
|
699
|
+
def __repr__(self):
|
|
700
|
+
return f"FinalCat with {self.legs} legs"
|
|
701
|
+
|
|
702
|
+
def f(cat: FinalCat, strides: int) -> int:
|
|
703
|
+
"""
|
|
704
|
+
pre: strides > 0
|
|
705
|
+
post: __return__ >= 4
|
|
706
|
+
"""
|
|
707
|
+
return strides * cat.legs
|
|
708
|
+
|
|
709
|
+
check_states(f, CONFIRMED)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
# TODO: precondition strengthening check
|
|
713
|
+
def TODO_test_cannot_strengthen_inherited_preconditions():
|
|
714
|
+
class PowerHungrySmokeDetector(SmokeDetector):
|
|
715
|
+
_battery_power: int
|
|
716
|
+
|
|
717
|
+
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
748
718
|
"""
|
|
749
|
-
|
|
719
|
+
pre: self._is_plugged_in
|
|
720
|
+
pre: self._battery_power > 0
|
|
750
721
|
"""
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
722
|
+
return AirSample.SMOKE in air_samples
|
|
723
|
+
|
|
724
|
+
actual, expected = check_messages(
|
|
725
|
+
analyze_class(PowerHungrySmokeDetector), state=MessageType.PRE_INVALID
|
|
726
|
+
)
|
|
727
|
+
assert actual == expected
|
|
728
|
+
|
|
754
729
|
|
|
755
|
-
|
|
756
|
-
|
|
730
|
+
def test_newtype() -> None:
|
|
731
|
+
T = TypeVar("T")
|
|
732
|
+
Number = NewType("Number", int)
|
|
733
|
+
with standalone_statespace:
|
|
734
|
+
x = proxy_for_type(Number, "x", allow_subtypes=False)
|
|
735
|
+
assert isinstance(x, SymbolicInt)
|
|
757
736
|
|
|
758
|
-
def test_enforced_fn_preconditions(self) -> None:
|
|
759
|
-
def f(x: int) -> bool:
|
|
760
|
-
"""post: _ == True"""
|
|
761
|
-
return bool(fibb(x)) or True
|
|
762
737
|
|
|
763
|
-
|
|
738
|
+
def test_container_typevar() -> None:
|
|
739
|
+
T = TypeVar("T")
|
|
764
740
|
|
|
765
|
-
def
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if isinstance(thing, SmokeDetector):
|
|
769
|
-
return thing._is_plugged_in
|
|
770
|
-
return False
|
|
741
|
+
def f(s: Sequence[T]) -> Dict[T, T]:
|
|
742
|
+
"""post: len(_) == len(s)"""
|
|
743
|
+
return dict(zip(s, s))
|
|
771
744
|
|
|
772
|
-
|
|
745
|
+
# (sequence could contain duplicate items)
|
|
746
|
+
check_states(f, POST_FAIL)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def test_typevar_bounds_fail() -> None:
|
|
750
|
+
T = TypeVar("T")
|
|
751
|
+
|
|
752
|
+
def f(x: T) -> int:
|
|
753
|
+
"""post:True"""
|
|
754
|
+
return x + 1 # type: ignore
|
|
755
|
+
|
|
756
|
+
check_states(f, EXEC_ERR)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def test_typevar_bounds_ok() -> None:
|
|
760
|
+
B = TypeVar("B", bound=int)
|
|
761
|
+
|
|
762
|
+
def f(x: B) -> int:
|
|
763
|
+
"""post:True"""
|
|
764
|
+
return x + 1
|
|
765
|
+
|
|
766
|
+
check_states(f, CONFIRMED)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def test_any() -> None:
|
|
770
|
+
def f(x: Any) -> bool:
|
|
771
|
+
"""post: True"""
|
|
772
|
+
return x is None
|
|
773
|
+
|
|
774
|
+
check_states(f, CONFIRMED)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def test_meeting_class_preconditions() -> None:
|
|
778
|
+
def f() -> int:
|
|
779
|
+
"""
|
|
780
|
+
post: _ == -1
|
|
781
|
+
"""
|
|
782
|
+
pokeable = Pokeable(0)
|
|
783
|
+
pokeable.safe_pokeby(-1)
|
|
784
|
+
return pokeable.x
|
|
785
|
+
|
|
786
|
+
analyze_function(f)
|
|
787
|
+
# TODO: this doesn't test anything?
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def test_enforced_fn_preconditions() -> None:
|
|
791
|
+
def f(x: int) -> bool:
|
|
792
|
+
"""post: _ == True"""
|
|
793
|
+
return bool(fibb(x)) or True
|
|
794
|
+
|
|
795
|
+
check_states(f, EXEC_ERR)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def test_generic_object() -> None:
|
|
799
|
+
def f(thing: object):
|
|
800
|
+
"""post: True"""
|
|
801
|
+
if isinstance(thing, SmokeDetector):
|
|
802
|
+
return thing._is_plugged_in
|
|
803
|
+
return False
|
|
804
|
+
|
|
805
|
+
check_states(f, CANNOT_CONFIRM)
|
|
773
806
|
|
|
774
807
|
|
|
775
808
|
def get_natural_number() -> int:
|
|
@@ -806,214 +839,234 @@ def test_access_class_method_on_symbolic_type():
|
|
|
806
839
|
person.a_class_method(42) # Just check that this don't explode
|
|
807
840
|
|
|
808
841
|
|
|
809
|
-
|
|
810
|
-
def
|
|
811
|
-
|
|
812
|
-
"""pre: x && x"""
|
|
842
|
+
def test_syntax_error() -> None:
|
|
843
|
+
def f(x: int):
|
|
844
|
+
"""pre: x && x"""
|
|
813
845
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
)
|
|
846
|
+
actual, expected = check_messages(analyze_function(f), state=MessageType.SYNTAX_ERR)
|
|
847
|
+
assert actual == expected
|
|
817
848
|
|
|
818
|
-
def test_raises_ok(self) -> None:
|
|
819
|
-
def f() -> bool:
|
|
820
|
-
"""
|
|
821
|
-
raises: IndexError, NameError
|
|
822
|
-
post: __return__
|
|
823
|
-
"""
|
|
824
|
-
raise IndexError()
|
|
825
|
-
return True
|
|
826
849
|
|
|
827
|
-
|
|
850
|
+
def test_raises_ok() -> None:
|
|
851
|
+
def f() -> bool:
|
|
852
|
+
"""
|
|
853
|
+
raises: IndexError, NameError
|
|
854
|
+
post: __return__
|
|
855
|
+
"""
|
|
856
|
+
raise IndexError()
|
|
857
|
+
return True
|
|
828
858
|
|
|
829
|
-
|
|
830
|
-
def f(n: Optional[Pokeable]) -> bool:
|
|
831
|
-
"""post: _"""
|
|
832
|
-
return isinstance(n, Pokeable)
|
|
859
|
+
check_states(f, CONFIRMED)
|
|
833
860
|
|
|
834
|
-
check_states(f, POST_FAIL)
|
|
835
861
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
post: True
|
|
841
|
-
"""
|
|
842
|
-
foo[0].append(42)
|
|
862
|
+
def test_optional_can_be_none_fail() -> None:
|
|
863
|
+
def f(n: Optional[Pokeable]) -> bool:
|
|
864
|
+
"""post: _"""
|
|
865
|
+
return isinstance(n, Pokeable)
|
|
843
866
|
|
|
844
|
-
|
|
867
|
+
check_states(f, POST_FAIL)
|
|
845
868
|
|
|
846
|
-
def test_nonuniform_list_types_1(self) -> None:
|
|
847
|
-
def f(a: List[object], b: List[int]) -> List[object]:
|
|
848
|
-
"""
|
|
849
|
-
pre: len(b) == 5 # constraint for performance
|
|
850
|
-
post: b[0] not in _
|
|
851
|
-
"""
|
|
852
|
-
ret = a + b[1:] # type: ignore
|
|
853
|
-
return ret
|
|
854
869
|
|
|
855
|
-
|
|
870
|
+
def test_implicit_heapref_conversions() -> None:
|
|
871
|
+
def f(foo: List[List]) -> None:
|
|
872
|
+
"""
|
|
873
|
+
pre: len(foo) > 0
|
|
874
|
+
post: True
|
|
875
|
+
"""
|
|
876
|
+
foo[0].append(42)
|
|
856
877
|
|
|
857
|
-
|
|
858
|
-
def f(a: List[object], b: List[int]) -> List[object]:
|
|
859
|
-
"""
|
|
860
|
-
pre: len(b) == 5 # constraint for performance
|
|
861
|
-
post: b[-1] not in _
|
|
862
|
-
"""
|
|
863
|
-
return a + b[:-1] # type: ignore
|
|
878
|
+
check_states(f, CONFIRMED)
|
|
864
879
|
|
|
865
|
-
check_states(f, POST_FAIL)
|
|
866
880
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
881
|
+
def test_nonuniform_list_types_1() -> None:
|
|
882
|
+
def f(a: List[object], b: List[int]) -> List[object]:
|
|
883
|
+
"""
|
|
884
|
+
pre: len(b) == 5 # constraint for performance
|
|
885
|
+
post: b[0] not in _
|
|
886
|
+
"""
|
|
887
|
+
ret = a + b[1:] # type: ignore
|
|
888
|
+
return ret
|
|
871
889
|
|
|
872
|
-
|
|
890
|
+
check_states(f, POST_FAIL)
|
|
873
891
|
|
|
874
|
-
def test_varargs_ok(self) -> None:
|
|
875
|
-
def f(x: int, *a: str, **kw: bool) -> int:
|
|
876
|
-
"""post: _ >= x"""
|
|
877
|
-
return x + len(a) + (42 if kw else 0)
|
|
878
892
|
|
|
879
|
-
|
|
893
|
+
def test_nonuniform_list_types_2() -> None:
|
|
894
|
+
def f(a: List[object], b: List[int]) -> List[object]:
|
|
895
|
+
"""
|
|
896
|
+
pre: len(b) == 5 # constraint for performance
|
|
897
|
+
post: b[-1] not in _
|
|
898
|
+
"""
|
|
899
|
+
return a + b[:-1] # type: ignore
|
|
880
900
|
|
|
881
|
-
|
|
882
|
-
check_states(fibb, POST_FAIL)
|
|
901
|
+
check_states(f, POST_FAIL)
|
|
883
902
|
|
|
884
|
-
def test_recursive_fn_ok(self) -> None:
|
|
885
|
-
check_states(recursive_example, CONFIRMED)
|
|
886
903
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
904
|
+
def test_varargs_fail() -> None:
|
|
905
|
+
def f(x: int, *a: str, **kw: bool) -> int:
|
|
906
|
+
"""post: _ > x"""
|
|
907
|
+
return x + len(a) + (42 if kw else 0)
|
|
891
908
|
|
|
892
|
-
|
|
909
|
+
check_states(f, POST_FAIL)
|
|
893
910
|
|
|
894
|
-
def test_reentrant_precondition(self) -> None:
|
|
895
|
-
# Really, we're just ensuring that we don't stack-overflow here.
|
|
896
|
-
check_states(reentrant_precondition, CONFIRMED)
|
|
897
|
-
|
|
898
|
-
def test_recursive_postcondition_enforcement_suspension(self) -> None:
|
|
899
|
-
messages = analyze_class(Measurer)
|
|
900
|
-
self.assertEqual(*check_messages(messages, state=MessageType.POST_FAIL))
|
|
901
|
-
|
|
902
|
-
def test_short_circuiting(self) -> None:
|
|
903
|
-
# Some operations are hard to deal with symbolically, like hashes.
|
|
904
|
-
# CrossHair will sometimes "short-circuit" functions, in hopes that the
|
|
905
|
-
# function body isn't required to prove the postcondition.
|
|
906
|
-
# This is an example of such a case.
|
|
907
|
-
def f(x: str) -> int:
|
|
908
|
-
"""post: _ == 0"""
|
|
909
|
-
a = hash(x)
|
|
910
|
-
b = 7
|
|
911
|
-
# This is zero no matter what the hashes are:
|
|
912
|
-
return (a + b) - (b + a)
|
|
913
911
|
|
|
914
|
-
|
|
912
|
+
def test_varargs_ok() -> None:
|
|
913
|
+
def f(x: int, *a: str, **kw: bool) -> int:
|
|
914
|
+
"""post: _ >= x"""
|
|
915
|
+
return x + len(a) + (42 if kw else 0)
|
|
915
916
|
|
|
916
|
-
|
|
917
|
-
messages = analyze_class(OverloadedContainer)
|
|
918
|
-
line = ShippingContainer.total_weight.__code__.co_firstlineno + 1
|
|
919
|
-
self.assertEqual(
|
|
920
|
-
*check_messages(
|
|
921
|
-
messages,
|
|
922
|
-
state=MessageType.POST_FAIL,
|
|
923
|
-
message="false when calling total_weight(OverloadedContainer()) (which returns 13)",
|
|
924
|
-
line=line,
|
|
925
|
-
)
|
|
926
|
-
)
|
|
917
|
+
check_states(f, CANNOT_CONFIRM)
|
|
927
918
|
|
|
928
|
-
def test_error_message_has_unmodified_args(self) -> None:
|
|
929
|
-
def f(foo: List[Pokeable]) -> None:
|
|
930
|
-
"""
|
|
931
|
-
pre: len(foo) == 1
|
|
932
|
-
pre: foo[0].x == 10
|
|
933
|
-
post[foo]: foo[0].x == 12
|
|
934
|
-
"""
|
|
935
|
-
foo[0].poke()
|
|
936
919
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
analyze_function(f),
|
|
940
|
-
state=MessageType.POST_FAIL,
|
|
941
|
-
message="false when calling f([Pokeable(x=10)])",
|
|
942
|
-
)
|
|
943
|
-
)
|
|
920
|
+
def test_recursive_fn_fail() -> None:
|
|
921
|
+
check_states(fibb, POST_FAIL)
|
|
944
922
|
|
|
945
|
-
# TODO: List[List] involves no HeapRefs
|
|
946
|
-
def TODO_test_potential_circular_references(self) -> None:
|
|
947
|
-
# TODO?: potential aliasing of input argument data?
|
|
948
|
-
def f(foo: List[List], thing: object) -> None:
|
|
949
|
-
"""
|
|
950
|
-
pre: len(foo) == 2
|
|
951
|
-
pre: len(foo[0]) == 1
|
|
952
|
-
pre: len(foo[1]) == 1
|
|
953
|
-
post: len(foo[1]) == 1
|
|
954
|
-
"""
|
|
955
|
-
foo[0].append(object()) # TODO: using 42 yields a z3 sort error
|
|
956
923
|
|
|
957
|
-
|
|
924
|
+
def test_recursive_fn_ok() -> None:
|
|
925
|
+
check_states(recursive_example, CONFIRMED)
|
|
958
926
|
|
|
959
|
-
def test_nonatomic_comparison(self) -> None:
|
|
960
|
-
def f(x: int, ls: List[str]) -> bool:
|
|
961
|
-
"""post: not _"""
|
|
962
|
-
return ls == x
|
|
963
927
|
|
|
964
|
-
|
|
928
|
+
def test_recursive_postcondition_ok() -> None:
|
|
929
|
+
def f(x: int) -> int:
|
|
930
|
+
"""post: _ == f(-x)"""
|
|
931
|
+
return x * x
|
|
965
932
|
|
|
966
|
-
|
|
967
|
-
def f(x: Dict[FrozenSet[float], int]) -> bool:
|
|
968
|
-
"""post: not _"""
|
|
969
|
-
return x == {frozenset({10.0}): 1}
|
|
933
|
+
check_states(f, CONFIRMED)
|
|
970
934
|
|
|
971
|
-
check_states(f, POST_FAIL)
|
|
972
935
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
"""inv: self.count == __old__.self.count"""
|
|
936
|
+
def test_reentrant_precondition() -> None:
|
|
937
|
+
# Really, we're just ensuring that we don't stack-overflow here.
|
|
938
|
+
check_states(reentrant_precondition, CONFIRMED)
|
|
977
939
|
|
|
978
|
-
count: int
|
|
979
940
|
|
|
980
|
-
|
|
981
|
-
|
|
941
|
+
def test_recursive_postcondition_enforcement_suspension() -> None:
|
|
942
|
+
messages = analyze_class(Measurer)
|
|
943
|
+
actual, expected = check_messages(messages, state=MessageType.POST_FAIL)
|
|
944
|
+
assert actual == expected
|
|
982
945
|
|
|
983
|
-
messages = analyze_class(FrozenApples)
|
|
984
|
-
self.assertEqual(*check_messages(messages, state=MessageType.POST_FAIL))
|
|
985
946
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
947
|
+
def test_short_circuiting() -> None:
|
|
948
|
+
# Some operations are hard to deal with symbolically, like hashes.
|
|
949
|
+
# CrossHair will sometimes "short-circuit" functions, in hopes that the
|
|
950
|
+
# function body isn't required to prove the postcondition.
|
|
951
|
+
# This is an example of such a case.
|
|
952
|
+
def f(x: str) -> int:
|
|
953
|
+
"""post: _ == 0"""
|
|
954
|
+
a = hash(x)
|
|
955
|
+
b = 7
|
|
956
|
+
# This is zero no matter what the hashes are:
|
|
957
|
+
return (a + b) - (b + a)
|
|
990
958
|
|
|
991
|
-
|
|
959
|
+
check_states(f, CONFIRMED)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def test_error_message_in_unrelated_method() -> None:
|
|
963
|
+
messages = analyze_class(OverloadedContainer)
|
|
964
|
+
line = ShippingContainer.total_weight.__code__.co_firstlineno + 1
|
|
965
|
+
actual, expected = check_messages(
|
|
966
|
+
messages,
|
|
967
|
+
state=MessageType.POST_FAIL,
|
|
968
|
+
message="false when calling total_weight(OverloadedContainer()) (which returns 13)",
|
|
969
|
+
line=line,
|
|
970
|
+
)
|
|
971
|
+
assert actual == expected
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def test_error_message_has_unmodified_args() -> None:
|
|
975
|
+
def f(foo: List[Pokeable]) -> None:
|
|
976
|
+
"""
|
|
977
|
+
pre: len(foo) == 1
|
|
978
|
+
pre: foo[0].x == 10
|
|
979
|
+
post[foo]: foo[0].x == 12
|
|
980
|
+
"""
|
|
981
|
+
foo[0].poke()
|
|
992
982
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
for k, v in original_container.items():
|
|
1000
|
-
self.assertIs(ShippingContainer.__dict__[k], v)
|
|
1001
|
-
for k, v in original_overloaded.items():
|
|
1002
|
-
self.assertIs(OverloadedContainer.__dict__[k], v)
|
|
983
|
+
actual, expected = check_messages(
|
|
984
|
+
analyze_function(f),
|
|
985
|
+
state=MessageType.POST_FAIL,
|
|
986
|
+
message="false when calling f([Pokeable(x=10)])",
|
|
987
|
+
)
|
|
988
|
+
assert actual == expected
|
|
1003
989
|
|
|
1004
|
-
def test_fallback_when_smt_values_out_themselves(self) -> None:
|
|
1005
|
-
def f(items: List[str]) -> str:
|
|
1006
|
-
"""post: True"""
|
|
1007
|
-
return ",".join(items)
|
|
1008
990
|
|
|
1009
|
-
|
|
991
|
+
# TODO: List[List] involves no HeapRefs
|
|
992
|
+
def TODO_test_potential_circular_references() -> None:
|
|
993
|
+
# TODO?: potential aliasing of input argument data?
|
|
994
|
+
def f(foo: List[List], thing: object) -> None:
|
|
995
|
+
"""
|
|
996
|
+
pre: len(foo) == 2
|
|
997
|
+
pre: len(foo[0]) == 1
|
|
998
|
+
pre: len(foo[1]) == 1
|
|
999
|
+
post: len(foo[1]) == 1
|
|
1000
|
+
"""
|
|
1001
|
+
foo[0].append(object()) # TODO: using 42 yields a z3 sort error
|
|
1010
1002
|
|
|
1011
|
-
|
|
1012
|
-
def f(s: str) -> bool:
|
|
1013
|
-
"""post: True"""
|
|
1014
|
-
return bool(re.match(r"(\d+)", s))
|
|
1003
|
+
check_states(f, CONFIRMED)
|
|
1015
1004
|
|
|
1016
|
-
|
|
1005
|
+
|
|
1006
|
+
def test_nonatomic_comparison() -> None:
|
|
1007
|
+
def f(x: int, ls: List[str]) -> bool:
|
|
1008
|
+
"""post: not _"""
|
|
1009
|
+
return ls == x
|
|
1010
|
+
|
|
1011
|
+
check_states(f, CONFIRMED)
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def test_difficult_equality() -> None:
|
|
1015
|
+
def f(x: Dict[FrozenSet[float], int]) -> bool:
|
|
1016
|
+
"""post: not _"""
|
|
1017
|
+
return x == {frozenset({10.0}): 1}
|
|
1018
|
+
|
|
1019
|
+
check_states(f, POST_FAIL)
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def test_old_works_in_invariants() -> None:
|
|
1023
|
+
@dataclasses.dataclass
|
|
1024
|
+
class FrozenApples:
|
|
1025
|
+
"""inv: self.count == __old__.self.count"""
|
|
1026
|
+
|
|
1027
|
+
count: int
|
|
1028
|
+
|
|
1029
|
+
def add_one(self):
|
|
1030
|
+
self.count += 1
|
|
1031
|
+
|
|
1032
|
+
messages = analyze_class(FrozenApples)
|
|
1033
|
+
actual, expected = check_messages(messages, state=MessageType.POST_FAIL)
|
|
1034
|
+
assert actual == expected
|
|
1035
|
+
|
|
1036
|
+
# Also confirm we can create one as an argument:
|
|
1037
|
+
def f(a: FrozenApples) -> int:
|
|
1038
|
+
"""post: True"""
|
|
1039
|
+
return 0
|
|
1040
|
+
|
|
1041
|
+
check_states(f, CONFIRMED)
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def test_class_patching_is_undone() -> None:
|
|
1045
|
+
# CrossHair does a lot of monkey matching of classes
|
|
1046
|
+
# with contracts. Ensure that gets undone.
|
|
1047
|
+
original_container = ShippingContainer.__dict__.copy()
|
|
1048
|
+
original_overloaded = OverloadedContainer.__dict__.copy()
|
|
1049
|
+
run_checkables(analyze_class(OverloadedContainer))
|
|
1050
|
+
for k, v in original_container.items():
|
|
1051
|
+
assert ShippingContainer.__dict__[k] is v
|
|
1052
|
+
for k, v in original_overloaded.items():
|
|
1053
|
+
assert OverloadedContainer.__dict__[k] is v
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def test_fallback_when_smt_values_out_themselves() -> None:
|
|
1057
|
+
def f(items: List[str]) -> str:
|
|
1058
|
+
"""post: True"""
|
|
1059
|
+
return ",".join(items)
|
|
1060
|
+
|
|
1061
|
+
check_states(f, CANNOT_CONFIRM)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def test_unrelated_regex() -> None:
|
|
1065
|
+
def f(s: str) -> bool:
|
|
1066
|
+
"""post: True"""
|
|
1067
|
+
return bool(re.match(r"(\d+)", s))
|
|
1068
|
+
|
|
1069
|
+
check_states(f, CANNOT_CONFIRM)
|
|
1017
1070
|
|
|
1018
1071
|
|
|
1019
1072
|
if sys.version_info >= (3, 9):
|
|
@@ -1060,101 +1113,91 @@ def test_nondeterministic_detected_in_detached_path() -> None:
|
|
|
1060
1113
|
|
|
1061
1114
|
if icontract:
|
|
1062
1115
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
return x - y
|
|
1068
|
-
|
|
1069
|
-
check_states(some_func, POST_FAIL)
|
|
1116
|
+
def test_icontract_basic():
|
|
1117
|
+
@icontract.ensure(lambda result, x: result > x)
|
|
1118
|
+
def some_func(x: int, y: int = 5) -> int:
|
|
1119
|
+
return x - y
|
|
1070
1120
|
|
|
1071
|
-
|
|
1072
|
-
messages = analyze_function(
|
|
1073
|
-
icontract_appender,
|
|
1074
|
-
DEFAULT_OPTIONS,
|
|
1075
|
-
)
|
|
1076
|
-
line = icontract_appender.__wrapped__.__code__.co_firstlineno + 1
|
|
1077
|
-
self.assertEqual(
|
|
1078
|
-
*check_messages(
|
|
1079
|
-
messages, state=MessageType.POST_FAIL, line=line, column=0
|
|
1080
|
-
)
|
|
1081
|
-
)
|
|
1121
|
+
check_states(some_func, POST_FAIL)
|
|
1082
1122
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
)
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
IcontractB
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
IcontractB.break_my_invariant.__wrapped__.__code__.co_firstlineno
|
|
1109
|
-
)
|
|
1110
|
-
self.assertEqual(
|
|
1111
|
-
messages,
|
|
1112
|
-
{
|
|
1113
|
-
(
|
|
1114
|
-
MessageType.POST_FAIL,
|
|
1115
|
-
line_gt0,
|
|
1116
|
-
'"@icontract.invariant(lambda self: self.x > 0)" yields false '
|
|
1117
|
-
"when calling break_parent_invariant(IcontractB())",
|
|
1118
|
-
),
|
|
1119
|
-
(
|
|
1120
|
-
MessageType.POST_FAIL,
|
|
1121
|
-
line_lt100,
|
|
1122
|
-
'"@icontract.invariant(lambda self: self.x < 100)" yields false '
|
|
1123
|
-
"when calling break_my_invariant(IcontractB())",
|
|
1124
|
-
),
|
|
1125
|
-
},
|
|
1123
|
+
def test_icontract_snapshots():
|
|
1124
|
+
messages = analyze_function(
|
|
1125
|
+
icontract_appender,
|
|
1126
|
+
DEFAULT_OPTIONS,
|
|
1127
|
+
)
|
|
1128
|
+
line = icontract_appender.__wrapped__.__code__.co_firstlineno + 1
|
|
1129
|
+
actual, expected = check_messages(
|
|
1130
|
+
messages, state=MessageType.POST_FAIL, line=line, column=0
|
|
1131
|
+
)
|
|
1132
|
+
assert actual == expected
|
|
1133
|
+
|
|
1134
|
+
def test_icontract_weaken():
|
|
1135
|
+
@icontract.require(lambda x: x in (2, 3))
|
|
1136
|
+
@icontract.ensure(lambda: True)
|
|
1137
|
+
def trynum(x: int):
|
|
1138
|
+
IcontractB().weakenedfunc(x)
|
|
1139
|
+
|
|
1140
|
+
check_states(trynum, CONFIRMED)
|
|
1141
|
+
|
|
1142
|
+
def test_icontract_class():
|
|
1143
|
+
messages = run_checkables(
|
|
1144
|
+
analyze_class(
|
|
1145
|
+
IcontractB,
|
|
1146
|
+
# TODO: why is this required?
|
|
1147
|
+
DEFAULT_OPTIONS.overlay(analysis_kind=[AnalysisKind.icontract]),
|
|
1126
1148
|
)
|
|
1149
|
+
)
|
|
1150
|
+
messages = {
|
|
1151
|
+
(m.state, m.line, m.message)
|
|
1152
|
+
for m in messages
|
|
1153
|
+
if m.state != MessageType.CONFIRMED
|
|
1154
|
+
}
|
|
1155
|
+
line_gt0 = IcontractB.break_parent_invariant.__wrapped__.__code__.co_firstlineno
|
|
1156
|
+
line_lt100 = IcontractB.break_my_invariant.__wrapped__.__code__.co_firstlineno
|
|
1157
|
+
assert messages == {
|
|
1158
|
+
(
|
|
1159
|
+
MessageType.POST_FAIL,
|
|
1160
|
+
line_gt0,
|
|
1161
|
+
'"@icontract.invariant(lambda self: self.x > 0)" yields false '
|
|
1162
|
+
"when calling break_parent_invariant(IcontractB())",
|
|
1163
|
+
),
|
|
1164
|
+
(
|
|
1165
|
+
MessageType.POST_FAIL,
|
|
1166
|
+
line_lt100,
|
|
1167
|
+
'"@icontract.invariant(lambda self: self.x < 100)" yields false '
|
|
1168
|
+
"when calling break_my_invariant(IcontractB())",
|
|
1169
|
+
),
|
|
1170
|
+
}
|
|
1127
1171
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
@icontract.ensure(lambda: True)
|
|
1134
|
-
@icontract.require(lambda name: len(name) > 0)
|
|
1135
|
-
def outerfn(name: str):
|
|
1136
|
-
innerfn("00" + name)
|
|
1137
|
-
|
|
1138
|
-
self.assertEqual(
|
|
1139
|
-
*check_exec_err(
|
|
1140
|
-
outerfn,
|
|
1141
|
-
message_prefix="PreconditionFailed",
|
|
1142
|
-
)
|
|
1143
|
-
)
|
|
1172
|
+
def test_icontract_nesting():
|
|
1173
|
+
@icontract.require(lambda name: name.startswith("a"))
|
|
1174
|
+
def innerfn(name: str):
|
|
1175
|
+
pass
|
|
1144
1176
|
|
|
1177
|
+
@icontract.ensure(lambda: True)
|
|
1178
|
+
@icontract.require(lambda name: len(name) > 0)
|
|
1179
|
+
def outerfn(name: str):
|
|
1180
|
+
innerfn("00" + name)
|
|
1145
1181
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
remove_smallest_with_asserts,
|
|
1150
|
-
DEFAULT_OPTIONS.overlay(
|
|
1151
|
-
analysis_kind=[AnalysisKind.asserts],
|
|
1152
|
-
),
|
|
1153
|
-
)
|
|
1154
|
-
line = remove_smallest_with_asserts.__code__.co_firstlineno + 4
|
|
1155
|
-
self.assertEqual(
|
|
1156
|
-
*check_messages(messages, state=MessageType.EXEC_ERR, line=line, column=0)
|
|
1182
|
+
actual, expected = check_exec_err(
|
|
1183
|
+
outerfn,
|
|
1184
|
+
message_prefix="PreconditionFailed",
|
|
1157
1185
|
)
|
|
1186
|
+
assert actual == expected
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def test_asserts():
|
|
1190
|
+
messages = analyze_function(
|
|
1191
|
+
remove_smallest_with_asserts,
|
|
1192
|
+
DEFAULT_OPTIONS.overlay(
|
|
1193
|
+
analysis_kind=[AnalysisKind.asserts],
|
|
1194
|
+
),
|
|
1195
|
+
)
|
|
1196
|
+
line = remove_smallest_with_asserts.__code__.co_firstlineno + 4
|
|
1197
|
+
expected, actual = check_messages(
|
|
1198
|
+
messages, state=MessageType.EXEC_ERR, line=line, column=0
|
|
1199
|
+
)
|
|
1200
|
+
assert expected == actual
|
|
1158
1201
|
|
|
1159
1202
|
|
|
1160
1203
|
def test_unpickable_args() -> None:
|
|
@@ -1245,5 +1288,3 @@ if __name__ == "__main__":
|
|
|
1245
1288
|
import cProfile
|
|
1246
1289
|
|
|
1247
1290
|
cProfile.run("profile()", "out.pprof")
|
|
1248
|
-
else:
|
|
1249
|
-
unittest.main()
|