diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..73637522f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,40 @@ +[run] +branch = True + +[report] +skip_empty = True +skip_covered = True + +# Omit; need a different approach to test modules with hardware dependencies +omit = + */__init__.py + */tests/* + */pyzbar/* + */gui/* + +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + def __str__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + # Don't complain about abstract methods, they aren't run: + @(abc\.)?abstractmethod + + +[html] +directory = coverage_html_report +skip_empty = True +skip_covered = False diff --git a/.gitignore b/.gitignore index 392faf91b..dcf343d8c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ src/seedsigner.egg-info/ src/seedsigner/models/settings_definition.json .idea *.mo +.coverage diff --git a/tests/README.md b/tests/README.md index aa79d2a72..ae8679c64 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,3 +24,19 @@ Run a specific test: ``` pytest tests/test_this_file.py::test_this_specific_test ``` + +### Test Coverage +Run tests and generate test coverage +``` +coverage run -m pytest +``` + +Show the resulting test coverage details: +``` +coverage report +``` + +Generate the html overview: +``` +coverage html +``` diff --git a/tests/requirements.txt b/tests/requirements.txt index 7f265749c..3bfdf0738 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1,3 @@ +coverage==7.2.1 +mock==4.0.3 pytest==6.2.4 -mock==4.0.3 \ No newline at end of file diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index a0c04ed87..76298d34f 100644 --- a/tests/test_mnemonic_generation.py +++ b/tests/test_mnemonic_generation.py @@ -1,3 +1,4 @@ +import pytest import random from embit import bip39 @@ -42,6 +43,35 @@ def test_calculate_checksum(): assert bip39.mnemonic_is_valid(" ".join(mnemonic)) +def test_calculate_checksum_invalid_mnemonics(): + """ + Should raise an Exception on a mnemonic that is invalid due to length or using invalid words. + """ + with pytest.raises(Exception) as e: + # Mnemonic is too short: 10 words instead of 11 + partial_mnemonic = "abandon " * 9 + "about" + mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert "12- or 24-word" in str(e) + + with pytest.raises(Exception) as e: + # Valid mnemonic but unsupported length + mnemonic = "devote myth base logic dust horse nut collect buddy element eyebrow visit empty dress jungle" + mnemonic_generation.calculate_checksum(mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert "12- or 24-word" in str(e) + + with pytest.raises(Exception) as e: + # Mnemonic is too short: 22 words instead of 23 + partial_mnemonic = "abandon " * 21 + "about" + mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert "12- or 24-word" in str(e) + + with pytest.raises(ValueError) as e: + # Invalid BIP-39 word + partial_mnemonic = "foobar " * 11 + "about" + mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert "not in the dictionary" in str(e) + + def test_calculate_checksum_with_default_final_word(): """ 11-word and 23-word mnemonics use word `0000` as a temp final word to complete @@ -62,6 +92,22 @@ def test_calculate_checksum_with_default_final_word(): assert mnemonic1 == mnemonic2 +def test_generate_mnemonic_from_bytes(): + """ + Should generate a valid BIP-39 mnemonic from entropy bytes + """ + # From iancoleman.io + entropy = "3350f6ac9eeb07d2c6209932808aa7f6" + expected_mnemonic = "crew marble private differ race truly blush basket crater affair prepare unique".split() + mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(bytes.fromhex(entropy)) + assert mnemonic == expected_mnemonic + + entropy = "5bf41629fce815c3570955e8f45422abd7e2234141bd4d7ec63b741043b98cad" + expected_mnemonic = "fossil pass media what life ticket found click trophy pencil anger fish lawsuit balance agree dash estate wage mom trial aerobic system crawl review".split() + mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(bytes.fromhex(entropy)) + assert mnemonic == expected_mnemonic + + def test_verify_against_coldcard_sample(): """ https://coldcard.com/docs/verifying-dice-roll-math """