From d68826dc0d9746507a6fd213f0a2fd83062bf423 Mon Sep 17 00:00:00 2001 From: Arunanshu Biswas Date: Sun, 25 Aug 2024 00:57:00 +0530 Subject: [PATCH] test(pynanoid): simplify test suite --- pyproject.toml | 7 +- src/pynanoid/__init__.py | 2 +- src/pynanoid/nanoid.py | 16 ++--- tests/generate_test.py | 52 -------------- tests/method_test.py | 23 ------- tests/nanoid_test.py | 68 ------------------ tests/non_secure_test.py | 53 -------------- tests/test_pynanoid.py | 144 +++++++++++++++++++++++++++++++++++++++ tests/url_test.py | 10 --- 9 files changed, 154 insertions(+), 221 deletions(-) delete mode 100644 tests/generate_test.py delete mode 100644 tests/method_test.py delete mode 100644 tests/nanoid_test.py delete mode 100644 tests/non_secure_test.py create mode 100644 tests/test_pynanoid.py delete mode 100644 tests/url_test.py diff --git a/pyproject.toml b/pyproject.toml index 7ad737e..f5aae1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,12 +26,7 @@ python-source = "src" strip = true [tool.pdm.dev-dependencies] -dev = [ - "ruff>=0.6.1", - "mypy>=1.11.1", - "pip>=24.2", - "platformdirs>=4.2.2", -] +dev = ["ruff>=0.6.1", "mypy>=1.11.1", "pip>=24.2"] test = [ "pytest>=8.3.2", "pytest-cov>=5.0.0", diff --git a/src/pynanoid/__init__.py b/src/pynanoid/__init__.py index a98e0ab..d245350 100644 --- a/src/pynanoid/__init__.py +++ b/src/pynanoid/__init__.py @@ -4,7 +4,7 @@ try: # prioritize using the compiled versions if available from ._pynanoid import generate, non_secure_generate -except ImportError: +except ImportError: # pragma: no cover from .nanoid import generate, non_secure_generate diff --git a/src/pynanoid/nanoid.py b/src/pynanoid/nanoid.py index ebf8e3d..d27a20f 100644 --- a/src/pynanoid/nanoid.py +++ b/src/pynanoid/nanoid.py @@ -40,10 +40,10 @@ def non_secure_generate(alphabet: str = ALPHABET, size: int = SIZE) -> str: Returns: A NanoID of `size` length. """ - if alphabet == "": # pragma: no cover - raise ValueError("Alphabet cannot be empty") - if size < 1: # pragma: no cover - raise ValueError("Size cannot be less than 1") + if alphabet == "": + raise ValueError("alphabet cannot be empty") + if size < 1: + raise ValueError("size cannot be less than 1") alphabet_len = len(alphabet) @@ -71,10 +71,10 @@ def generate_custom( Returns: A NanoID of `size` length. """ - if alphabet == "": # pragma: no cover - raise ValueError("Alphabet cannot be empty") - if size < 1: # pragma: no cover - raise ValueError("Size cannot be less than 1") + if alphabet == "": + raise ValueError("alphabet cannot be empty") + if size < 1: + raise ValueError("size cannot be less than 1") alphabet_len = len(alphabet) diff --git a/tests/generate_test.py b/tests/generate_test.py deleted file mode 100644 index cf27e70..0000000 --- a/tests/generate_test.py +++ /dev/null @@ -1,52 +0,0 @@ -from sys import maxsize - -from hypothesis import given -from hypothesis import strategies as st - -from pynanoid import generate - - -def test_has_flat_distribution(): - count = 100 * 1000 - length = 5 - alphabet = "abcdefghijklmnopqrstuvwxyz" - - chars = {} - for _ in range(count): - id_ = generate(alphabet, length) - for j in range(len(id_)): - char = id_[j] - if not chars.get(char): - chars[char] = 0 - chars[char] += 1 - - assert len(chars.keys()) == len(alphabet) - - max_ = 0 - min_ = maxsize - for k in chars: - distribution = (chars[k] * len(alphabet)) / float(count * length) - min_ = min(distribution, min_) - max_ = max(distribution, max_) - assert max_ - min_ <= 0.05 - - -def test_has_no_collisions(): - count = 100 * 1000 - used = {} - for _ in range(count): - id_ = generate() - assert id_ not in used - used[id_] = True - - -def test_has_options(): - count = 100 * 1000 - for _ in range(count): - assert generate("a", 5) == "aaaaa" - assert len(generate(alphabet="12345a", size=3)) == 3 - - -@given(st.text(min_size=1), st.integers(min_value=1, max_value=5000)) -def test_same_size(alphabet: str, size: int): - assert len(generate(alphabet, size)) == size diff --git a/tests/method_test.py b/tests/method_test.py deleted file mode 100644 index 0b2be2d..0000000 --- a/tests/method_test.py +++ /dev/null @@ -1,23 +0,0 @@ -from hypothesis import given -from hypothesis import strategies as st - -from pynanoid import generate -from pynanoid.constants import SIZE -from pynanoid.nanoid import generate_custom - - -def test_generates_random_string(): - sequence = [2, 255, 3, 7, 7, 7, 7, 7, 0, 1] - - def rand(size: int = SIZE) -> bytes: - random_bytes = [] - for i in range(0, size, len(sequence)): - random_bytes += sequence[0 : size - i] - return bytes(random_bytes) - - assert generate_custom(rand, "abcde", 4) == "cdac" - - -@given(st.text(min_size=1), st.integers(min_value=1, max_value=5000)) -def test_same_size(alphabet: str, size: int): - assert len(generate(alphabet, size)) == size diff --git a/tests/nanoid_test.py b/tests/nanoid_test.py deleted file mode 100644 index 7e11e17..0000000 --- a/tests/nanoid_test.py +++ /dev/null @@ -1,68 +0,0 @@ -from sys import maxsize - -from pynanoid import generate, non_secure_generate -from pynanoid.constants import ALPHABET - - -def test_flat_distribution(): - count = 100 * 1000 - length = 5 - alphabet = "abcdefghijklmnopqrstuvwxyz" - - chars = {} - for _ in range(count): - id_ = generate(alphabet, length) - for j in range(len(id_)): - char = id_[j] - if not chars.get(char): - chars[char] = 0 - chars[char] += 1 - - assert len(chars.keys()) == len(alphabet) - - max_ = 0 - min_ = maxsize - for k in chars: - distribution = (chars[k] * len(alphabet)) / float(count * length) - max_ = max(distribution, max_) - min_ = min(distribution, min_) - assert max_ - min_ <= 0.05 - - -def test_generates_url_friendly_id(): - for _ in range(10): - id_ = generate() - assert len(id_) == 21 - for j in range(len(id_)): - assert id_[j] in ALPHABET - - -def test_has_no_collisions(): - count = 100 * 1000 - used = {} - for _ in range(count): - id_ = generate() - assert id_ not in used - used[id_] = True - - -def test_has_options(): - assert generate("a", 5) == "aaaaa" - - -def test_non_secure_ids(): - for _ in range(10000): - nanoid = non_secure_generate() - assert len(nanoid) == 21 - - -def test_non_secure_short_ids(): - for _ in range(10000): - nanoid = non_secure_generate("12345a", 3) - assert len(nanoid) == 3 - - -def test_short_secure_ids(): - for _ in range(10000): - nanoid = generate("12345a", 3) - assert len(nanoid) == 3 diff --git a/tests/non_secure_test.py b/tests/non_secure_test.py deleted file mode 100644 index d8cdc45..0000000 --- a/tests/non_secure_test.py +++ /dev/null @@ -1,53 +0,0 @@ -from sys import maxsize - -from hypothesis import given -from hypothesis import strategies as st - -from pynanoid import non_secure_generate -from pynanoid.constants import ALPHABET - - -@given(st.integers(min_value=1, max_value=5000)) -def test_changes_id_length(size: int): - assert len(non_secure_generate(size=size)) == size - - -def test_generates_url_friendly_id(): - for _ in range(10): - id_ = non_secure_generate() - assert len(id_) == 21 - for j in range(len(id_)): - assert ALPHABET.find(id_[j]) != -1 - - -def test_has_flat_distribution(): - count = 100 * 1000 - length = len(non_secure_generate()) - - chars = {} - for _ in range(count): - id_ = non_secure_generate() - for j in range(len(id_)): - char = id_[j] - if not chars.get(char): - chars[char] = 0 - chars[char] += 1 - - assert len(chars.keys()) == len(ALPHABET) - - max_ = 0 - min_ = maxsize - for k in chars: - distribution = (chars[k] * len(ALPHABET)) / float(count * length) - max_ = max(distribution, max_) - min_ = min(distribution, min_) - assert max_ - min_ <= 0.05 - - -def test_has_no_collisions(): - count = 100 * 1000 - used = {} - for _ in range(count): - id_ = non_secure_generate() - assert id_ not in used - used[id_] = True diff --git a/tests/test_pynanoid.py b/tests/test_pynanoid.py new file mode 100644 index 0000000..767af85 --- /dev/null +++ b/tests/test_pynanoid.py @@ -0,0 +1,144 @@ +import re + +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from pynanoid import generate, non_secure_generate +from pynanoid.nanoid import ( + ALPHABET, + generate_custom, +) +from pynanoid.nanoid import ( + generate as py_generate, +) +from pynanoid.nanoid import ( + non_secure_generate as py_non_secure_generate, +) + + +@given( + alphabet=st.text(min_size=1), + size=st.integers(min_value=1, max_value=5000), +) +@pytest.mark.parametrize( + "generate_fn", + [generate, py_generate], + ids=["rust", "python"], +) +def test_correct_length(alphabet: str, size: int, generate_fn): # noqa: ANN001 + assert len(generate_fn(alphabet, size)) == size + + +@given( + alphabet=st.text(min_size=1), + size=st.integers(min_value=1, max_value=5000), +) +@pytest.mark.parametrize( + "generate_fn", + [non_secure_generate, py_non_secure_generate], + ids=["rust", "python"], +) +def test_correct_length_non_secure(alphabet: str, size: int, generate_fn): # noqa: ANN001 + assert len(generate_fn(alphabet, size)) == size + + +@pytest.mark.parametrize( + "generate_fn", + [generate, py_generate], + ids=["rust", "python"], +) +def test_has_no_collisions(generate_fn): # noqa: ANN001 + count = 100_000 + used = set() + for _ in range(count): + id_ = generate_fn() + assert id_ not in used + used.add(id_) + + +@given(size=st.integers(min_value=1, max_value=5000)) +@pytest.mark.parametrize( + "generate_fn", + [generate, py_generate], + ids=["rust", "python"], +) +def test_generates_url_friendly_id(generate_fn, size: int): # noqa: ANN001 + regex = re.compile(r"^[0-9A-Za-z_-]+$") + id_ = generate_fn(size=size) + assert len(id_) == size + assert regex.match(id_) + + +@given(size=st.integers(min_value=1, max_value=5000)) +@pytest.mark.parametrize( + "generate_fn", + [non_secure_generate, py_non_secure_generate], + ids=["rust", "python"], +) +def test_generates_url_friendly_id_non_secure(generate_fn, size: int): # noqa: ANN001 + regex = re.compile(r"^[0-9A-Za-z_-]+$") + id_ = generate_fn(size=size) + assert len(id_) == size + assert regex.match(id_) + + +@given(size=st.integers(min_value=1, max_value=5000)) +@pytest.mark.parametrize( + "generate_fn", + [generate, py_generate], + ids=["rust", "python"], +) +def test_error_on_empty_alphabet(generate_fn, size: int): # noqa: ANN001 + with pytest.raises(ValueError, match="alphabet cannot be empty"): + generate_fn(alphabet="", size=size) + + +@given(size=st.integers(min_value=1, max_value=5000)) +@pytest.mark.parametrize( + "generate_fn", + [non_secure_generate, py_non_secure_generate], + ids=["rust", "python"], +) +def test_error_on_empty_alphabet_non_secure(generate_fn, size: int): # noqa: ANN001 + with pytest.raises(ValueError, match="alphabet cannot be empty"): + generate_fn(alphabet="", size=size) + + +@given(alphabet=st.text(min_size=1)) +@pytest.mark.parametrize( + "generate_fn", + [generate, py_generate], + ids=["rust", "python"], +) +def test_error_on_zero_size(generate_fn, alphabet: str): # noqa: ANN001 + with pytest.raises(ValueError, match="size cannot be (less than 1|zero)"): + generate_fn(alphabet, size=0) + + +@given(alphabet=st.text(min_size=1)) +@pytest.mark.parametrize( + "generate_fn", + [non_secure_generate, py_non_secure_generate], + ids=["rust", "python"], +) +def test_error_on_zero_size_non_secure(generate_fn, alphabet: str): # noqa: ANN001 + with pytest.raises(ValueError, match="size cannot be (less than 1|zero)"): + generate_fn(alphabet, size=0) + + +def test_generate_custom_randgen(): + sequence = [2, 255, 3, 7, 7, 7, 7, 7, 0, 1] + + def rand(size: int) -> bytes: + random_bytes = [] + for i in range(0, size, len(sequence)): + random_bytes += sequence[0 : size - i] + return bytes(random_bytes) + + assert generate_custom(rand, "abcde", 4) == "cdac" + + +def test_default_alphabet_ascii(): + regex = re.compile(r"^[0-9A-Za-z_-]+$") + assert regex.match(ALPHABET) diff --git a/tests/url_test.py b/tests/url_test.py deleted file mode 100644 index 5d78ddf..0000000 --- a/tests/url_test.py +++ /dev/null @@ -1,10 +0,0 @@ -from pynanoid.constants import ALPHABET - - -def test_has_no_duplicates(): - for i in range(len(ALPHABET)): - assert ALPHABET.rindex(ALPHABET[i]) == i - - -def test_is_string(): - assert isinstance(ALPHABET, str)