Simplified MockStorage and consolidated repetitive tests into parametrize blocks.
This commit is contained in:
+138
-260
@@ -19,10 +19,9 @@ templates and user scenarios.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
import sqlite3
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import ClassVar
|
|
||||||
|
|
||||||
import aiosqlite
|
|
||||||
import pytest
|
import pytest
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
@@ -33,52 +32,27 @@ from owlbot.builtin_modules.custom_commands.placeholders import process_placehol
|
|||||||
class MockStorage:
|
class MockStorage:
|
||||||
"""In-memory SQLite storage that executes real SQL queries."""
|
"""In-memory SQLite storage that executes real SQL queries."""
|
||||||
|
|
||||||
_instances: ClassVar[list[MockStorage]] = []
|
|
||||||
|
|
||||||
def __init__(self, counter_values=None):
|
def __init__(self, counter_values=None):
|
||||||
self._seed_data = counter_values or {}
|
self._conn = sqlite3.connect(":memory:")
|
||||||
self._conn = None
|
self._conn.row_factory = sqlite3.Row
|
||||||
MockStorage._instances.append(self)
|
self._conn.execute(
|
||||||
|
|
||||||
async def _ensure_conn(self):
|
|
||||||
if self._conn is not None:
|
|
||||||
return
|
|
||||||
self._conn = await aiosqlite.connect(":memory:")
|
|
||||||
self._conn.row_factory = aiosqlite.Row
|
|
||||||
await self._conn.execute(
|
|
||||||
"CREATE TABLE counters ("
|
"CREATE TABLE counters ("
|
||||||
"name TEXT PRIMARY KEY NOT NULL, "
|
"name TEXT PRIMARY KEY NOT NULL, "
|
||||||
"value INTEGER DEFAULT 0)"
|
"value INTEGER DEFAULT 0)"
|
||||||
)
|
)
|
||||||
for name, value in self._seed_data.items():
|
for name, value in (counter_values or {}).items():
|
||||||
await self._conn.execute(
|
self._conn.execute(
|
||||||
"INSERT INTO counters (name, value) VALUES (?, ?)",
|
"INSERT INTO counters (name, value) VALUES (?, ?)",
|
||||||
(name, value),
|
(name, value),
|
||||||
)
|
)
|
||||||
await self._conn.commit()
|
self._conn.commit()
|
||||||
|
|
||||||
async def fetch_one(self, query, params=None):
|
async def fetch_one(self, query, params=None):
|
||||||
await self._ensure_conn()
|
cursor = self._conn.execute(query, params or ())
|
||||||
cursor = await self._conn.execute(query, params or ())
|
row = cursor.fetchone()
|
||||||
row = await cursor.fetchone()
|
self._conn.commit()
|
||||||
await self._conn.commit()
|
|
||||||
return row
|
return row
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
if self._conn is not None:
|
|
||||||
await self._conn.close()
|
|
||||||
self._conn = None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
async def _close_mock_storages():
|
|
||||||
"""Close all MockStorage connections after each test."""
|
|
||||||
MockStorage._instances.clear()
|
|
||||||
yield
|
|
||||||
for storage in MockStorage._instances:
|
|
||||||
await storage.close()
|
|
||||||
MockStorage._instances.clear()
|
|
||||||
|
|
||||||
|
|
||||||
async def process(
|
async def process(
|
||||||
template, args=None, user="Alice", use_count=1, storage=None, max_depth=4
|
template, args=None, user="Alice", use_count=1, storage=None, max_depth=4
|
||||||
@@ -198,20 +172,18 @@ class TestArgPlaceholders:
|
|||||||
result = await process("[$(1)] [$(2)] [$(3)]")
|
result = await process("[$(1)] [$(2)] [$(3)]")
|
||||||
assert result == "[] [] []"
|
assert result == "[] [] []"
|
||||||
|
|
||||||
async def test_zero_is_not_a_valid_arg_index(self):
|
@pytest.mark.parametrize(
|
||||||
"""$(0) is not a valid positional argument and passes through unchanged."""
|
"template,args,expected",
|
||||||
result = await process("$(0)", args=["first"])
|
[
|
||||||
assert result == "$(0)"
|
("$(0)", ["first"], "$(0)"),
|
||||||
|
("$(10)", ["a"] * 10, "$(10)"),
|
||||||
async def test_out_of_range_numeric_placeholder(self):
|
("$(99)", [], "$(99)"),
|
||||||
"""$(10) is not registered (only 1-9), so it passes through."""
|
],
|
||||||
result = await process("$(10)", args=["a"] * 10)
|
)
|
||||||
assert result == "$(10)"
|
async def test_invalid_arg_index_passthrough(self, template, args, expected):
|
||||||
|
"""Out-of-range or unregistered numeric indices pass through unchanged."""
|
||||||
async def test_high_numeric_placeholder_passthrough(self):
|
result = await process(template, args=args)
|
||||||
"""$(99) passes through as an unknown placeholder."""
|
assert result == expected
|
||||||
result = await process("$(99)")
|
|
||||||
assert result == "$(99)"
|
|
||||||
|
|
||||||
async def test_empty_string_argument(self):
|
async def test_empty_string_argument(self):
|
||||||
"""$(1) with an empty string argument returns an empty string."""
|
"""$(1) with an empty string argument returns an empty string."""
|
||||||
@@ -417,17 +389,10 @@ class TestCounterNameValidation:
|
|||||||
)
|
)
|
||||||
assert row["value"] == 1
|
assert row["value"] == 1
|
||||||
|
|
||||||
async def test_count_hyphen_in_name_rejected(self):
|
@pytest.mark.parametrize("name", ["boss-kills", "boss.kills", "+5", "-3"])
|
||||||
"""Hyphens are not allowed in counter names."""
|
async def test_count_invalid_name_rejected(self, name):
|
||||||
result = await process("$(count boss-kills)")
|
"""Names with hyphens, dots, or leading +/- are rejected."""
|
||||||
assert result == (
|
result = await process(f"$(count {name})")
|
||||||
"Invalid $(count): counter name may only contain "
|
|
||||||
"letters, numbers, and underscores"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_count_dot_in_name_rejected(self):
|
|
||||||
"""Dots are not allowed in counter names."""
|
|
||||||
result = await process("$(count boss.kills)")
|
|
||||||
assert result == (
|
assert result == (
|
||||||
"Invalid $(count): counter name may only contain "
|
"Invalid $(count): counter name may only contain "
|
||||||
"letters, numbers, and underscores"
|
"letters, numbers, and underscores"
|
||||||
@@ -454,22 +419,6 @@ class TestCounterNameValidation:
|
|||||||
"letters, numbers, and underscores"
|
"letters, numbers, and underscores"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def test_count_modifier_syntax_as_name_rejected(self):
|
|
||||||
"""$(count +5) treats '+5' as the counter name, which fails validation."""
|
|
||||||
result = await process("$(count +5)")
|
|
||||||
assert result == (
|
|
||||||
"Invalid $(count): counter name may only contain "
|
|
||||||
"letters, numbers, and underscores"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_count_negative_modifier_syntax_as_name_rejected(self):
|
|
||||||
"""$(count -3) treats '-3' as the counter name, which fails validation."""
|
|
||||||
result = await process("$(count -3)")
|
|
||||||
assert result == (
|
|
||||||
"Invalid $(count): counter name may only contain "
|
|
||||||
"letters, numbers, and underscores"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_count_numeric_only_name_accepted(self):
|
async def test_count_numeric_only_name_accepted(self):
|
||||||
"""A counter name consisting of only digits is valid."""
|
"""A counter name consisting of only digits is valid."""
|
||||||
storage = MockStorage()
|
storage = MockStorage()
|
||||||
@@ -575,9 +524,17 @@ class TestCountdownCountup:
|
|||||||
result = await process("$(countdown Jan 1 2000 12:00:00 AM UTC)")
|
result = await process("$(countdown Jan 1 2000 12:00:00 AM UTC)")
|
||||||
assert result == "0 seconds"
|
assert result == "0 seconds"
|
||||||
|
|
||||||
async def test_countdown_invalid_date(self):
|
@pytest.mark.parametrize(
|
||||||
|
"bad_date",
|
||||||
|
[
|
||||||
|
"not a real date",
|
||||||
|
"monday",
|
||||||
|
"Dec 25 2099 12:00:00 AM XYZ",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_countdown_invalid_date(self, bad_date):
|
||||||
"""$(countdown) with an unparseable date reports an error."""
|
"""$(countdown) with an unparseable date reports an error."""
|
||||||
result = await process("$(countdown not a real date)")
|
result = await process(f"$(countdown {bad_date})")
|
||||||
assert result == (
|
assert result == (
|
||||||
"Invalid $(countdown): unrecognized date format, "
|
"Invalid $(countdown): unrecognized date format, "
|
||||||
"expected $(countdown Dec 25 2025 12:00:00 AM EST)"
|
"expected $(countdown Dec 25 2025 12:00:00 AM EST)"
|
||||||
@@ -591,22 +548,6 @@ class TestCountdownCountup:
|
|||||||
"expected $(countup Dec 25 2025 12:00:00 AM EST)"
|
"expected $(countup Dec 25 2025 12:00:00 AM EST)"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def test_countdown_single_word_date(self):
|
|
||||||
"""$(countdown) with a single-word date reports an error."""
|
|
||||||
result = await process("$(countdown monday)")
|
|
||||||
assert result == (
|
|
||||||
"Invalid $(countdown): unrecognized date format, "
|
|
||||||
"expected $(countdown Dec 25 2025 12:00:00 AM EST)"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_unrecognized_timezone(self):
|
|
||||||
"""$(countdown) with an unknown timezone abbreviation reports an error."""
|
|
||||||
result = await process("$(countdown Dec 25 2099 12:00:00 AM XYZ)")
|
|
||||||
assert result == (
|
|
||||||
"Invalid $(countdown): unrecognized date format, "
|
|
||||||
"expected $(countdown Dec 25 2025 12:00:00 AM EST)"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_countup_future_date_returns_zero(self):
|
async def test_countup_future_date_returns_zero(self):
|
||||||
"""$(countup) with a future date returns '0 seconds'."""
|
"""$(countup) with a future date returns '0 seconds'."""
|
||||||
result = await process("$(countup Dec 25 2099 12:00:00 AM UTC)")
|
result = await process("$(countup Dec 25 2099 12:00:00 AM UTC)")
|
||||||
@@ -627,35 +568,28 @@ class TestCountdownCountup:
|
|||||||
result = await process("$(countdown Jan 1 2000 12:00:00 AM utc)")
|
result = await process("$(countdown Jan 1 2000 12:00:00 AM utc)")
|
||||||
assert result == "0 seconds"
|
assert result == "0 seconds"
|
||||||
|
|
||||||
async def test_countdown_exactly_one_day(self):
|
@pytest.mark.parametrize(
|
||||||
"""Exactly 86400 seconds formats as '1 day' in singular form."""
|
"seconds,expected",
|
||||||
assert _format_duration(86400) == "1 day"
|
[
|
||||||
|
(0, "0 seconds"),
|
||||||
async def test_duration_singular_forms(self):
|
(1, "1 second"),
|
||||||
"""Each time unit uses singular form when the value is exactly 1."""
|
(2, "2 seconds"),
|
||||||
assert _format_duration(1) == "1 second"
|
(60, "1 minute"),
|
||||||
assert _format_duration(60) == "1 minute"
|
(120, "2 minutes"),
|
||||||
assert _format_duration(3600) == "1 hour"
|
(3600, "1 hour"),
|
||||||
assert _format_duration(86400) == "1 day"
|
(7200, "2 hours"),
|
||||||
assert _format_duration(90061) == "1 day 1 hour 1 minute 1 second"
|
(86400, "1 day"),
|
||||||
|
(172800, "2 days"),
|
||||||
async def test_duration_plural_forms(self):
|
(3601, "1 hour 1 second"),
|
||||||
"""Each unit uses plural form when the value is greater than 1."""
|
(86401, "1 day 1 second"),
|
||||||
assert _format_duration(172800) == "2 days"
|
(86460, "1 day 1 minute"),
|
||||||
assert _format_duration(7200) == "2 hours"
|
(90061, "1 day 1 hour 1 minute 1 second"),
|
||||||
assert _format_duration(120) == "2 minutes"
|
(180183, "2 days 2 hours 3 minutes 3 seconds"),
|
||||||
assert _format_duration(2) == "2 seconds"
|
],
|
||||||
assert _format_duration(180183) == "2 days 2 hours 3 minutes 3 seconds"
|
)
|
||||||
|
async def test_format_duration(self, seconds, expected):
|
||||||
async def test_duration_skipped_intermediate_units(self):
|
"""_format_duration produces the correct human-readable string."""
|
||||||
"""Units with zero value are omitted from the output."""
|
assert _format_duration(seconds) == expected
|
||||||
assert _format_duration(86401) == "1 day 1 second"
|
|
||||||
assert _format_duration(3601) == "1 hour 1 second"
|
|
||||||
assert _format_duration(86460) == "1 day 1 minute"
|
|
||||||
|
|
||||||
async def test_duration_zero(self):
|
|
||||||
"""Zero seconds produces '0 seconds'."""
|
|
||||||
assert _format_duration(0) == "0 seconds"
|
|
||||||
|
|
||||||
|
|
||||||
class TestNesting:
|
class TestNesting:
|
||||||
@@ -845,25 +779,19 @@ class TestEscaping:
|
|||||||
class TestCaseInsensitivity:
|
class TestCaseInsensitivity:
|
||||||
"""Placeholder names are lowercased before handler lookup."""
|
"""Placeholder names are lowercased before handler lookup."""
|
||||||
|
|
||||||
async def test_user_uppercase(self):
|
@pytest.mark.parametrize(
|
||||||
"""$(USER) resolves the same as $(user)."""
|
"template,kwargs,expected",
|
||||||
result = await process("$(USER)")
|
[
|
||||||
assert result == "Alice"
|
("$(USER)", {}, "Alice"),
|
||||||
|
("$(User)", {}, "Alice"),
|
||||||
async def test_user_mixed_case(self):
|
("$(Rand 1 1)", {}, "1"),
|
||||||
"""$(User) resolves the same as $(user)."""
|
("$(COUNT)", {"use_count": 5}, "5"),
|
||||||
result = await process("$(User)")
|
("$(COUNTDOWN Jan 1 2000 12:00:00 AM UTC)", {}, "0 seconds"),
|
||||||
assert result == "Alice"
|
],
|
||||||
|
)
|
||||||
async def test_rand_mixed_case(self):
|
async def test_placeholder_name_case_ignored(self, template, kwargs, expected):
|
||||||
"""$(Rand) resolves the same as $(rand)."""
|
"""Placeholder names are case-insensitive."""
|
||||||
result = await process("$(Rand 1 1)")
|
assert await process(template, **kwargs) == expected
|
||||||
assert result == "1"
|
|
||||||
|
|
||||||
async def test_count_uppercase(self):
|
|
||||||
"""$(COUNT) resolves the same as $(count)."""
|
|
||||||
result = await process("$(COUNT)", use_count=5)
|
|
||||||
assert result == "5"
|
|
||||||
|
|
||||||
async def test_getcount_mixed_case(self):
|
async def test_getcount_mixed_case(self):
|
||||||
"""$(GetCount) resolves the same as $(getcount)."""
|
"""$(GetCount) resolves the same as $(getcount)."""
|
||||||
@@ -871,11 +799,6 @@ class TestCaseInsensitivity:
|
|||||||
result = await process("$(GetCount x)", storage=storage)
|
result = await process("$(GetCount x)", storage=storage)
|
||||||
assert result == "3"
|
assert result == "3"
|
||||||
|
|
||||||
async def test_countdown_uppercase(self):
|
|
||||||
"""$(COUNTDOWN) resolves the same as $(countdown)."""
|
|
||||||
result = await process("$(COUNTDOWN Jan 1 2000 12:00:00 AM UTC)")
|
|
||||||
assert result == "0 seconds"
|
|
||||||
|
|
||||||
async def test_countup_case_insensitive(self):
|
async def test_countup_case_insensitive(self):
|
||||||
"""$(COUNTUP) resolves the same as $(countup)."""
|
"""$(COUNTUP) resolves the same as $(countup)."""
|
||||||
with freeze_time("2026-06-15 12:00:00", tz_offset=0):
|
with freeze_time("2026-06-15 12:00:00", tz_offset=0):
|
||||||
@@ -1041,36 +964,21 @@ class TestParserBoundaries:
|
|||||||
class TestUnknownPlaceholders:
|
class TestUnknownPlaceholders:
|
||||||
"""Unknown placeholder names pass through unchanged."""
|
"""Unknown placeholder names pass through unchanged."""
|
||||||
|
|
||||||
async def test_unknown_placeholder_passthrough(self):
|
@pytest.mark.parametrize(
|
||||||
"""An unrecognized placeholder with arguments passes through unchanged."""
|
"template,expected",
|
||||||
result = await process("$(madeup stuff)")
|
[
|
||||||
assert result == "$(madeup stuff)"
|
("$(madeup stuff)", "$(madeup stuff)"),
|
||||||
|
("$(banana)", "$(banana)"),
|
||||||
async def test_unknown_no_args_passthrough(self):
|
("$(madeup lots of space)", "$(madeup lots of space)"),
|
||||||
"""An unrecognized placeholder with no arguments passes through unchanged."""
|
("$(madeup)", "$(madeup)"),
|
||||||
result = await process("$(banana)")
|
("$(café)", "$(café)"),
|
||||||
assert result == "$(banana)"
|
("$(café latte)", "$(café latte)"),
|
||||||
|
],
|
||||||
async def test_multiple_spaces_normalized(self):
|
)
|
||||||
"""Extra whitespace inside an unknown placeholder is collapsed."""
|
async def test_unknown_placeholder_passthrough(self, template, expected):
|
||||||
result = await process("$(madeup lots of space)")
|
"""Unrecognized placeholders pass through unchanged."""
|
||||||
assert result == "$(madeup lots of space)"
|
result = await process(template)
|
||||||
|
assert result == expected
|
||||||
async def test_unknown_no_args_preserved(self):
|
|
||||||
"""An unknown placeholder with no arguments stays as-is."""
|
|
||||||
result = await process("$(madeup)")
|
|
||||||
assert result == "$(madeup)"
|
|
||||||
|
|
||||||
async def test_unicode_name_passes_through(self):
|
|
||||||
"""$(café) is parsed as a valid name but unknown,
|
|
||||||
and passes through unchanged."""
|
|
||||||
result = await process("$(café)")
|
|
||||||
assert result == "$(café)"
|
|
||||||
|
|
||||||
async def test_unicode_name_with_args(self):
|
|
||||||
"""$(café latte) is unknown and passes through with arguments reconstructed."""
|
|
||||||
result = await process("$(café latte)")
|
|
||||||
assert result == "$(café latte)"
|
|
||||||
|
|
||||||
|
|
||||||
class TestEvaluationSafety:
|
class TestEvaluationSafety:
|
||||||
@@ -1106,89 +1014,59 @@ class TestEvaluationSafety:
|
|||||||
class TestRuntimeErrors:
|
class TestRuntimeErrors:
|
||||||
"""A PlaceholderError cancels the entire render and returns the message."""
|
"""A PlaceholderError cancels the entire render and returns the message."""
|
||||||
|
|
||||||
async def test_rand_one_arg(self):
|
@pytest.mark.parametrize(
|
||||||
"""$(rand) with only one argument reports too few."""
|
"template,expected_error",
|
||||||
result = await process("$(rand 1)")
|
[
|
||||||
assert (
|
(
|
||||||
result == "Invalid $(rand): too few arguments, expected $(rand start stop)"
|
"$(rand 1)",
|
||||||
)
|
"Invalid $(rand): too few arguments, expected $(rand start stop)",
|
||||||
|
),
|
||||||
async def test_rand_no_args(self):
|
(
|
||||||
"""$(rand) with no arguments reports too few."""
|
"$(rand)",
|
||||||
result = await process("$(rand)")
|
"Invalid $(rand): too few arguments, expected $(rand start stop)",
|
||||||
assert (
|
),
|
||||||
result == "Invalid $(rand): too few arguments, expected $(rand start stop)"
|
(
|
||||||
)
|
"$(rand 1 2 3)",
|
||||||
|
"Invalid $(rand): too many arguments, expected $(rand start stop)",
|
||||||
async def test_rand_three_args(self):
|
),
|
||||||
"""$(rand) with three arguments reports too many."""
|
(
|
||||||
result = await process("$(rand 1 2 3)")
|
"$(rand a b)",
|
||||||
assert (
|
"Invalid $(rand): arguments must be integers, e.g., $(rand 1 100)",
|
||||||
result == "Invalid $(rand): too many arguments, expected $(rand start stop)"
|
),
|
||||||
)
|
(
|
||||||
|
"$(rand 1.5 2.5)",
|
||||||
async def test_rand_non_integer(self):
|
"Invalid $(rand): arguments must be integers, e.g., $(rand 1 100)",
|
||||||
"""$(rand) with non-integer arguments reports a type error."""
|
),
|
||||||
result = await process("$(rand a b)")
|
("$(user extra)", "Invalid $(user): does not accept arguments"),
|
||||||
assert (
|
("$(getcount)", "Invalid $(getcount): a counter name is required"),
|
||||||
result == "Invalid $(rand): arguments must be integers, e.g., $(rand 1 100)"
|
(
|
||||||
)
|
"$(getcount deaths extra)",
|
||||||
|
"Invalid $(getcount): too many arguments, expected $(getcount name)",
|
||||||
async def test_rand_float_args(self):
|
),
|
||||||
"""$(rand) with float arguments reports a type error."""
|
(
|
||||||
result = await process("$(rand 1.5 2.5)")
|
"$(count deaths abc)",
|
||||||
assert (
|
"Invalid $(count): modifier must be an integer (e.g., +5, -1, 0)",
|
||||||
result == "Invalid $(rand): arguments must be integers, e.g., $(rand 1 100)"
|
),
|
||||||
)
|
(
|
||||||
|
"$(count deaths +1 extra)",
|
||||||
async def test_user_with_args(self):
|
"Invalid $(count): too many arguments,"
|
||||||
"""$(user) with unexpected arguments reports an error."""
|
" expected $(count name [modifier])",
|
||||||
result = await process("$(user extra)")
|
),
|
||||||
assert result == "Invalid $(user): does not accept arguments"
|
(
|
||||||
|
"$(countdown)",
|
||||||
async def test_getcount_missing_name(self):
|
"Invalid $(countdown): missing date,"
|
||||||
"""$(getcount) with no counter name reports the omission."""
|
" expected $(countdown Dec 25 2025 12:00:00 AM EST)",
|
||||||
result = await process("$(getcount)")
|
),
|
||||||
assert result == "Invalid $(getcount): a counter name is required"
|
(
|
||||||
|
"$(countup)",
|
||||||
async def test_getcount_extra_args(self):
|
"Invalid $(countup): missing date,"
|
||||||
"""$(getcount) with extra arguments reports too many."""
|
" expected $(countup Dec 25 2025 12:00:00 AM EST)",
|
||||||
result = await process("$(getcount deaths extra)")
|
),
|
||||||
assert (
|
],
|
||||||
result
|
|
||||||
== "Invalid $(getcount): too many arguments, expected $(getcount name)"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_count_bad_modifier(self):
|
|
||||||
"""$(count) with a non-integer modifier reports a type error."""
|
|
||||||
result = await process("$(count deaths abc)")
|
|
||||||
assert (
|
|
||||||
result == "Invalid $(count): modifier must be an integer (e.g., +5, -1, 0)"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_count_too_many_args(self):
|
|
||||||
"""$(count) with three arguments reports too many."""
|
|
||||||
result = await process("$(count deaths +1 extra)")
|
|
||||||
assert (
|
|
||||||
result
|
|
||||||
== "Invalid $(count): too many arguments, expected $(count name [modifier])"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_countdown_no_date(self):
|
|
||||||
"""$(countdown) with no date argument reports the omission."""
|
|
||||||
result = await process("$(countdown)")
|
|
||||||
assert (
|
|
||||||
result == "Invalid $(countdown): missing date, expected"
|
|
||||||
" $(countdown Dec 25 2025 12:00:00 AM EST)"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_countup_no_date(self):
|
|
||||||
"""$(countup) with no date argument reports the omission."""
|
|
||||||
result = await process("$(countup)")
|
|
||||||
assert (
|
|
||||||
result == "Invalid $(countup): missing date, expected"
|
|
||||||
" $(countup Dec 25 2025 12:00:00 AM EST)"
|
|
||||||
)
|
)
|
||||||
|
async def test_error_message(self, template, expected_error):
|
||||||
|
"""Placeholder errors produce the correct diagnostic message."""
|
||||||
|
assert await process(template) == expected_error
|
||||||
|
|
||||||
async def test_error_cancels_entire_render(self):
|
async def test_error_cancels_entire_render(self):
|
||||||
"""An error in any placeholder cancels the whole template."""
|
"""An error in any placeholder cancels the whole template."""
|
||||||
|
|||||||
Reference in New Issue
Block a user