Files
chemenu/tools/chemenu/tests/test_titles.py
T
torben 04aebdeccf
CI / verify (push) Successful in 2m9s
Release / release (push) Successful in 38s
feat: path budget - a file's path stays at 160 characters or fewer; new, rename, move and raw accept refuse more, lint reports Long Paths (#163)
Files changed:
- CHANGES.md
- README.md
- VERSION
- instructions/page-lifecycle.md
- kb/CONTRACT.md
- tools/CONTRACT.md
- tools/chemenu/commands/_util.py
- tools/chemenu/commands/lint.py
- tools/chemenu/commands/new_page.py
- tools/chemenu/commands/page_ops.py
- tools/chemenu/commands/raw_cmd.py
- tools/chemenu/lint_core.py
- tools/chemenu/tests/test_lint.py
- tools/chemenu/tests/test_new_page.py
- tools/chemenu/tests/test_page_ops.py
- tools/chemenu/tests/test_raw_cmd.py
- tools/chemenu/tests/test_titles.py
- tools/chemenu/titles.py
2026-09-30 23:08:36 +02:00

92 lines
3.2 KiB
Python

import unicodedata
import pytest
from chemenu.titles import collision_key, title_problems
@pytest.mark.parametrize("title", [
"aurora", "gateway.example.net", "Source - CON", "Source - Aurora Notes",
"COM10", "COMM", "Ünïcode Straße", "with space inside", ".hidden", "README",
"Log", "Contract", "a-b_c (d)",
])
def test_a_valid_title_has_no_problems(title):
assert title_problems(title) == []
@pytest.mark.parametrize("char", list('<>:"/\\|?*'))
def test_each_forbidden_character_is_named(char):
problems = title_problems(f"A{char}B")
assert len(problems) == 1
assert f"'{char}'" in problems[0]
def test_an_empty_title_is_refused():
assert title_problems("") == ["the title is empty"]
def test_a_control_character_is_named_by_code_point():
assert "U+0009" in title_problems("A\tB")[0]
assert "U+007F" in title_problems("A\x7fB")[0]
@pytest.mark.parametrize("title", ["A.", "A ", "A...", "A. "])
def test_a_trailing_dot_or_space_is_refused(title):
assert any("ends with a dot or a space" in p for p in title_problems(title))
@pytest.mark.parametrize("title", [
"CON", "con", "Prn", "AUX", "nul", "COM0", "COM1", "com9", "LPT0", "lpt9",
"COM¹", "COM²", "COM³", "LPT¹", "LPT²", "LPT³",
"CON.txt", "nul.tar.gz", "COM1.example", "CON .x",
])
def test_windows_device_names_are_refused_before_the_first_dot(title):
problems = title_problems(title)
assert problems and "reserved device name" in problems[0]
@pytest.mark.parametrize("title", ["INDEX", "Index", "index", "collection", "Collection.md", "COLLECTION"])
def test_stack_names_are_refused(title):
problems = title_problems(title)
assert problems and "reserved for a file the stack" in problems[0]
def test_the_rule_reads_the_whole_title_so_a_prefix_lifts_a_reserved_name():
assert title_problems("CON") != []
assert title_problems("Source - CON") == []
assert title_problems("Source - A: B") != []
def test_every_problem_is_reported_at_once():
assert len(title_problems("CON.")) == 2
def test_collision_key_folds_case_and_normalization():
nfc = unicodedata.normalize("NFC", "Café")
nfd = unicodedata.normalize("NFD", "Café")
assert nfc != nfd
assert collision_key(nfc) == collision_key(nfd)
assert collision_key("Foo") == collision_key("foo") == collision_key("FOO")
assert collision_key("Straße") == collision_key("STRASSE")
assert collision_key("Foo") != collision_key("Foo ")
def test_path_budget_counts_utf16_code_units():
from chemenu.titles import PATH_BUDGET, path_budget_problem, path_length
assert PATH_BUDGET == 160
assert path_length("abc") == 3
assert path_length("😀") == 2
assert path_length("é") == 1
assert path_budget_problem("a" * 160) is None
over = path_budget_problem("a" * 161)
assert over is not None and "161" in over and "160" in over and "1 shorter" in over
def test_path_budget_counts_an_emoji_twice_at_the_boundary():
from chemenu.titles import path_budget_problem
# 159 characters, 160 code units: fits. One more ASCII character tips it.
assert path_budget_problem("a" * 158 + "😀") is None
assert path_budget_problem("a" * 159 + "😀") is not None