David R. MacIver / drmaciver.com
hypothesis.works
from dateutil.parser import parse
from hypothesis import given, settings
from hypothesis.extra.datetime import datetimes
@given(datetimes())
def test_can_parse_iso_format(dt):
formatted = dt.isoformat()
assert formatted == parse(formatted).isoformat()
E assert '0005-01-01T00:00:05' == '0001-05-01T00:00:05'
E - 0005-01-01T00:00:05
E ? ^ ^
E + 0001-05-01T00:00:05
E ? ^ ^
Falsifying example: test_can_parse_iso_format(
dt=datetime.datetime(5, 1, 1, 0, 0, 5)
)
fluffy content begins now
(the important bits)
content warning: cynicism
writing software is easy!
print("Hello World")
What even is correct software?
writing correct software is...
2006: impossible!
2007: easy!
2010: hard...
2015: impossible!
but verification?!?
guilt-free development
The difficult... We'll do right now.
The impossible... Seems like way too much work TBH.
bugs don't matter
useful software matters
...and bugs aren't useful
correct software
useful software
mostly working software
warning: made up graphs
Thanks to JR Heard for this example
http://blog.jrheard.com/hypothesis-and-pexpect
def is_good_password(s):
return True # Eh, probably good enough
def test_short_is_not_allowed():
assert not is_good_password("a")
def test_requires_character_of_each_type():
assert not is_good_password("aaaaaaa")
def test_short_is_not_allowed():
> assert not is_good_password("a")
E AssertionError: assert not True
E + where True = is_good_password('a')
def test_requires_character_of_each_type():
> assert not is_good_password("aaaaaaa")
E AssertionError: assert not True
E + where True = is_good_password('aaaaaaa')
def is_good_password(s):
return False # Passwords are a bad security model anyway
def test_allows_password_satisfying_the_rules():
assert is_good_password("aaaaaA1!")
def test_allows_password_satisfying_the_rules():
> assert is_good_password("aaaaaA1!")
E AssertionError: assert False
E + where False = is_good_password('aaaaaA1!')
lower_letters = "abcdefghijklmnopqrstuvwyz"
upper_letters = lower_letters.upper()
letters = lower_letters + upper_letters
digits = "0123456789"
symbols = '!@#$%^&*()-_=+.,'
def is_good_password(pw):
chars = set(pw)
return not (
chars.isdisjoint(letters) or
chars.isdisjoint(digits) or
chars.isdisjoint(symbols)
)
have we written enough tests?
of course not
oh no, we have to write more?
let's try something different
from hypothesis import given, strategies as st
SYMBOLS = '!@#$%^&*()-_=+.,'
VALID_PASSWORD_CHARS = string.ascii_letters + string.digits + SYMBOLS
@given(st.text(alphabet=VALID_PASSWORD_CHARS, max_size=7))
def test_short_passwords_are_all_invalid(s):
assert not is_good_password(s)
@given(st.text(alphabet=VALID_PASSWORD_CHARS, max_size=7))
def test_short_passwords_are_all_invalid(s):
> assert not is_good_password(s)
E AssertionError: assert not True
E + where True = is_good_password('a0!')
Falsifying example: test_short_passwords_are_all_invalid(s='a0!')
lower_letters = "abcdefghijklmnopqrstuvwyz"
upper_letters = lower_letters.upper()
letters = lower_letters + upper_letters
digits = "0123456789"
symbols = '!@#$%^&*()-_=+.,'
def is_good_password(pw):
if len(pw) < 8:
return False
chars = set(pw)
return not (
chars.isdisjoint(letters) or
chars.isdisjoint(digits) or
chars.isdisjoint(symbols)
)
so it works now, right?
def is_good_password(pw):
return pw == "aaaaaA1!" # Objectively the best password
REQUIRED = [string.ascii_lowercase, string.ascii_uppercase,
string.digits, SYMBOLS]
@st.composite
def valid_password(draw):
bits = ''.join(
draw(st.text(alphabet=required, min_size=1))
for required in REQUIRED)
bits += draw(st.text(
alphabet=VALID_PASSWORD_CHARS,
min_size=max(0, 8 - len(bits))))
return ''.join(draw(st.permutations(bits)))
@given(valid_password())
def test_all_valid_passwords_are_good(s):
assert is_good_password(s)
@given(valid_password())
def test_all_valid_passwords_are_good(s):
> assert is_good_password(s)
E AssertionError: assert False
E + where False = is_good_password('x0!!!!!0')
Falsifying example: test_all_valid_passwords_are_good(s='x0!!!!!0')
NB: cheating
lower_letters = "abcdefghijklmnopqrstuvwyz"
upper_letters = lower_letters.upper()
letters = lower_letters + upper_letters
digits = "0123456789"
symbols = '!@#$%^&*()-_=+.,'
def is_good_password(pw):
if len(pw) < 8:
return False
chars = set(pw)
return not (
chars.isdisjoint(letters) or
chars.isdisjoint(digits) or
chars.isdisjoint(symbols)
)
lower_letters = "abcdefghijklmnopqrstuvwxyz"
upper_letters = lower_letters.upper()
letters = lower_letters + upper_letters
digits = "0123456789"
symbols = '!@#$%^&*()-_=+.,'
def is_good_password(pw):
if len(pw) < 8:
return False
chars = set(pw)
return not (
chars.isdisjoint(letters) or
chars.isdisjoint(digits) or
chars.isdisjoint(symbols)
)
one last thing
@given(st.data())
def test_all_required_parts_are_needed(data):
parts = list(REQUIRED)
parts.remove(
data.draw(st.sampled_from(parts), label="removed"))
password = data.draw(st.text(
alphabet=''.join(parts), min_size=8), label="password")
assert not is_good_password(password)
> assert not is_good_password(password)
E AssertionError: assert not True
E + where True = is_good_password('AAAAAA0!')
Falsifying example: test_all_required_parts_are_needed(data=data(...))
Draw 1 (removed): 'abcdefghijklmnopqrstuvwxyz'
Draw 2 (password): 'AAAAAA0!'
lower_letters = "abcdefghijklmnopqrstuvwxyz"
upper_letters = lower_letters.upper()
letters = lower_letters + upper_letters
digits = "0123456789"
symbols = '!@#$%^&*()-_=+.,'
def is_good_password(pw):
if len(pw) < 8:
return False
chars = set(pw)
return not (
chars.isdisjoint(letters) or
chars.isdisjoint(digits) or
chars.isdisjoint(symbols)
)
lower_letters = "abcdefghijklmnopqrstuvwxyz"
upper_letters = lower_letters.upper()
digits = "0123456789"
symbols = '!@#$%^&*()-_=+.,'
def is_good_password(pw):
if len(pw) < 8:
return False
chars = set(pw)
return not (
chars.isdisjoint(lower_letters) or
chars.isdisjoint(upper_letters) or
chars.isdisjoint(digits) or
chars.isdisjoint(symbols)
)
have we written enough tests now?
eh, 's probably good enough
this finds real bugs!
...but probably not all of them.
David R. MacIver / drmaciver.com