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
92 lines
3.2 KiB
Python
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
|