Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions tests/test_charset_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Invariant tests for the encoding tables in ``barcode.charsets``.

The tables are data, but they encode the symbology specifications:
EAN's A/B/C pattern relationships, ITF's two-of-five property, the
Code 39 reference map, and so on. Pinning the invariants catches
accidental edits to the tables that no behavioural test would notice.
"""

from __future__ import annotations

from barcode.charsets import codabar
from barcode.charsets import code39
from barcode.charsets import code128
from barcode.charsets import ean
from barcode.charsets import itf
from barcode.charsets import upc


def test_ean_left_pattern_table() -> None:
assert len(ean.LEFT_PATTERN) == 10
for pattern in ean.LEFT_PATTERN:
assert len(pattern) == 6
assert set(pattern) <= {"A", "B"}


def test_ean_code_tables_shape() -> None:
assert set(ean.CODES) == {"A", "B", "C"}
for table in ean.CODES.values():
assert len(table) == 10
for pattern in table:
assert len(pattern) == 7
assert set(pattern) <= {"0", "1"}


def test_ean_c_patterns_are_the_complement_of_a() -> None:
for i in range(10):
complement = "".join("1" if b == "0" else "0" for b in ean.CODES["A"][i])
assert ean.CODES["C"][i] == complement


def test_ean_b_patterns_are_the_reversed_c_patterns() -> None:
for i in range(10):
assert ean.CODES["B"][i] == ean.CODES["C"][i][::-1]


def test_upc_tables_reuse_the_ean_tables() -> None:
assert upc.CODES["L"] == ean.CODES["A"]
assert upc.CODES["R"] == ean.CODES["C"]
assert upc.EDGE == ean.EDGE == "101"
assert upc.MIDDLE == ean.MIDDLE == "01010"


def test_itf_table_has_the_two_of_five_property() -> None:
assert itf.START == "NnNn"
assert itf.STOP == "WnN"
assert len(itf.CODES) == 10
for pattern in itf.CODES:
assert len(pattern) == 5
assert set(pattern) <= {"N", "W"}
# Interleaved 2 of 5: exactly two of the five elements are wide.
assert pattern.count("W") == 2


def test_code39_tables_shape() -> None:
assert len(code39.REF) == 43
assert len(code39.CODES) == 43
assert len(code39.MAP) == 43
for pattern in code39.CODES:
assert len(pattern) == 15
assert set(pattern) <= {"0", "1"}
assert code39.EDGE == "100010111011101"
assert code39.MIDDLE == "0"


def test_code39_map_assigns_sequential_values() -> None:
for index, char in enumerate(code39.REF):
value, pattern = code39.MAP[char]
assert value == index
assert pattern == code39.CODES[index]


def test_code39_map_spot_values() -> None:
assert code39.MAP["0"] == (0, "101000111011101")
assert code39.MAP["A"] == (10, "111010100010111")
assert code39.MAP["%"] == (42, "101000100010001")


def test_code128_pattern_table_shape() -> None:
assert len(code128.CODES) == 106
assert len(set(code128.CODES)) == 106
for pattern in code128.CODES:
assert len(pattern) == 11
assert set(pattern) <= {"0", "1"}
assert code128.STOP == "11000111010"


def test_codabar_tables_shape() -> None:
assert len(codabar.CODES) == 16
for pattern in codabar.CODES.values():
assert len(pattern) == 7
assert set(pattern) <= {"N", "n", "W", "w"}
assert codabar.STARTSTOP == {
"A": "NnWwNwN",
"B": "NwNwNnW",
"C": "NnNwNwW",
"D": "NnNwWwN",
}
75 changes: 75 additions & 0 deletions tests/test_codabar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Tests for Codabar / NW-7 (``barcode.codabar``).

Codabar previously had no dedicated tests. Validation happens lazily
in ``build()`` rather than in the constructor, which these tests pin.
"""

from __future__ import annotations

import pytest

from barcode.codabar import CODABAR
from barcode.errors import BarcodeError
from barcode.errors import IllegalCharacterError


def test_codabar_str_and_fullcode_return_the_code() -> None:
code = CODABAR("A123B")
assert str(code) == "A123B"
assert code.get_fullcode() == "A123B"


def test_codabar_build_pattern_with_default_widths() -> None:
code = CODABAR("A123B").build()
assert code == [
"1100111110000011000001100110011001111100000110011001100000"
"11001111100111110000011001100110011000001100000110011111"
]
assert len(code[0]) == 114


def test_codabar_build_pattern_with_custom_widths() -> None:
assert CODABAR("A123B", narrow=1, wide=3).build() == [
"101110001000101010111000101010001011101110001010101000100010111"
]


def test_codabar_all_special_characters_build() -> None:
# Digits plus the six specials $ : / . + - are all legal in the body.
assert len(CODABAR("A$:/.+-B").build()[0]) == 192


def test_codabar_constructor_does_not_validate() -> None:
# Construction stores the code verbatim; only build() complains.
code = CODABAR("X123A")
assert code.code == "X123A"


def test_codabar_bad_start_character_raises_on_build() -> None:
with pytest.raises(BarcodeError, match="Codabar should start with either A,B,C or D"):
CODABAR("X123A").build()


def test_codabar_lowercase_start_character_raises_on_build() -> None:
with pytest.raises(BarcodeError, match="Codabar should start with either"):
CODABAR("a123b").build()


def test_codabar_bad_end_character_raises_on_build() -> None:
with pytest.raises(BarcodeError, match="Codabar should end with either A,B,C or D"):
CODABAR("A123X").build()


def test_codabar_illegal_body_character_raises_on_build() -> None:
with pytest.raises(
IllegalCharacterError, match="Codabar can only contain numerics"
):
CODABAR("A1@3B").build()


def test_codabar_empty_code_raises_index_error_on_build() -> None:
# There is no explicit emptiness check; indexing the first
# character of an empty code raises IndexError. Pins the current
# behaviour.
with pytest.raises(IndexError):
CODABAR("").build()
105 changes: 105 additions & 0 deletions tests/test_code128_charsets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Tests for the Code 128 charset tables and encoder sequences.

``test_code128.py`` covers build stability and ``test_gs1_128.py``
covers GS1 sequences; these tests pin the charset dictionaries
themselves and plain Code 128 symbol sequences (start-code folding,
charset C digit pairing, odd trailing digits, control characters).
"""

from __future__ import annotations

import pytest

from barcode.charsets import code128
from barcode.codex import Code128
from barcode.errors import IllegalCharacterError


@pytest.mark.parametrize(
("char", "value"),
[
(" ", 0),
("0", 16),
("A", 33),
("\x00", 64),
("SHIFT", 98),
("TO_C", 99),
("TO_B", 100),
],
)
def test_code128_charset_a_values(char: str, value: int) -> None:
assert code128.A[char] == value


@pytest.mark.parametrize(
("char", "value"),
[
("0", 16),
("`", 64),
("a", 65),
("TO_C", 99),
("TO_A", 101),
],
)
def test_code128_charset_b_values(char: str, value: int) -> None:
assert code128.B[char] == value


def test_code128_charset_sizes() -> None:
assert len(code128.A) == 103
assert len(code128.B) == 103


def test_code128_charset_c_switch_codes() -> None:
assert code128.C == {"TO_B": 100, "TO_A": 101, "\xf1": 102}


def test_code128_start_and_switch_tables() -> None:
assert code128.START_CODES == {"A": 103, "B": 104, "C": 105}
assert code128.TO == {101: 103, 100: 104, 99: 105}


def test_code128_fnc1_character() -> None:
# The FNC1 character U+00F1 is "ñ"; it is part of the legal input
# set and maps to value 102 in charset C (and, coincidentally,
# also in charset B).
assert "\xf1" in code128.ALL
assert code128.B["\xf1"] == 102


@pytest.mark.parametrize(
("given", "encoded"),
[
("1234", [105, 12, 34]), # pure charset C digit pairs
("12", [105, 12]),
("1", [104, 17]), # single digit: folded to START_B
("12345", [105, 12, 34, 100, 21]), # trailing digit via charset B
("AB", [104, 33, 34]), # folded to START_B
("ab", [104, 65, 66]), # lowercase requires charset B
("A\x01B", [104, 33, 101, 65, 34]), # control char switches to A
],
)
def test_code128_encoded_sequences(given: str, encoded: list[int]) -> None:
assert Code128(given).encoded == encoded


def test_code128_build_length_and_pattern() -> None:
code = Code128("1234").build()
assert len(code[0]) == 57
assert Code128("12A").build() == [
"11010011100101100111001011110111010100011000100100011001100011101011"
]


def test_code128_illegal_character_raises() -> None:
with pytest.raises(
IllegalCharacterError, match="not valid for Code 128: €"
):
Code128("A€B")


def test_code128_empty_input_raises_index_error() -> None:
# _try_to_optimize indexes the second symbol unconditionally, so
# an empty code raises IndexError. Pins the current behaviour.
with pytest.raises(IndexError):
Code128("").encoded
Loading