MIL-009: verification fix #54
@@ -228,3 +228,26 @@ VERIFY_SUCCESS: Final[str] = "✅ SUCCESS: All bookings verified!"
|
|||||||
VERIFY_MISMATCH: Final[str] = "❌ MISMATCH: Missing {count} bookings"
|
VERIFY_MISMATCH: Final[str] = "❌ MISMATCH: Missing {count} bookings"
|
||||||
VERIFY_LINE_MISSING: Final[str] = " ✗ Missing: {entry}"
|
VERIFY_LINE_MISSING: Final[str] = " ✗ Missing: {entry}"
|
||||||
VERIFY_LINE_UNEXPECTED: Final[str] = " ? Unexpected: {entry}"
|
VERIFY_LINE_UNEXPECTED: Final[str] = " ? Unexpected: {entry}"
|
||||||
|
VERIFY_LINE_ALSO_BOOKED: Final[str] = " i Also booked (not checked): {entry}"
|
||||||
|
|
||||||
|
## Month abbreviations of the dates on the pages, January first.
|
||||||
|
MONTH_ABBREVIATIONS: Final[tuple[str, ...]] = (
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
)
|
||||||
|
|
||||||
|
## Days of a year, with the 29th of February, used to compare dates that have no year.
|
||||||
|
DAYS_IN_DATE_CYCLE: Final[int] = 366
|
||||||
|
|
||||||
|
## A leap year, so that the 29th of February is a valid date.
|
||||||
|
DATE_CYCLE_YEAR: Final[int] = 2000
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
from snack_and_lift import constants
|
from snack_and_lift import constants
|
||||||
from snack_and_lift.domain.booking_entry import BookingEntry
|
from snack_and_lift.domain.booking_entry import BookingEntry
|
||||||
|
from snack_and_lift.domain.errors import PageFormatError
|
||||||
from snack_and_lift.domain.models import ClassResult, Outcome
|
from snack_and_lift.domain.models import ClassResult, Outcome
|
||||||
|
|
||||||
_WAITLISTED_OUTCOMES = (Outcome.NEW_WAITLIST, Outcome.ALREADY_WAITLISTED)
|
_WAITLISTED_OUTCOMES = (Outcome.NEW_WAITLIST, Outcome.ALREADY_WAITLISTED)
|
||||||
@@ -20,8 +22,11 @@ _Key = tuple[str, str, bool]
|
|||||||
class VerificationResult:
|
class VerificationResult:
|
||||||
"""@brief What the My Bookings page shows compared with what the bot did.
|
"""@brief What the My Bookings page shows compared with what the bot did.
|
||||||
|
|
||||||
`verified`, `missing` and `unexpected` hold display names, with the waitlist suffix for a
|
`verified`, `missing`, `unexpected` and `also_booked` hold display names, with the waitlist
|
||||||
waitlist entry; `missing` and `unexpected` also say the day (`Spin Class on Tue, Aug 12`).
|
suffix for a waitlist entry; all but `verified` also say the day (`Spin Class on Tue, Aug 12`).
|
||||||
|
Only entries from the earliest to the latest class the bot handled are checked: `found`
|
||||||
|
counts them, and `also_booked` lists the entries outside that window, which never cause
|
||||||
|
a mismatch.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
expected: int
|
expected: int
|
||||||
@@ -29,6 +34,7 @@ class VerificationResult:
|
|||||||
verified: list[str]
|
verified: list[str]
|
||||||
missing: list[str]
|
missing: list[str]
|
||||||
unexpected: list[str]
|
unexpected: list[str]
|
||||||
|
also_booked: list[str]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_success(self) -> bool:
|
def is_success(self) -> bool:
|
||||||
@@ -57,6 +63,46 @@ def _describe(key: _Key) -> str:
|
|||||||
return f"{_display_name(name, waitlisted)} on {day}"
|
return f"{_display_name(name, waitlisted)} on {day}"
|
||||||
|
|
||||||
|
|
||||||
|
def _day_number(date_text: str) -> int:
|
||||||
|
"""@brief The position of a date (`Tue, Aug 12`) in a year; the page shows no year.
|
||||||
|
|
||||||
|
@exception PageFormatError The text has no valid month and day.
|
||||||
|
"""
|
||||||
|
month_text, _, day_text = date_text.partition(", ")[2].partition(" ")
|
||||||
|
try:
|
||||||
|
month = constants.MONTH_ABBREVIATIONS.index(month_text) + 1
|
||||||
|
day = date(constants.DATE_CYCLE_YEAR, month, int(day_text))
|
||||||
|
except ValueError as error:
|
||||||
|
raise PageFormatError(f"Unexpected date: {date_text!r}") from error
|
||||||
|
return day.timetuple().tm_yday
|
||||||
|
|
||||||
|
|
||||||
|
def _offset(day: int, start: int) -> int:
|
||||||
|
"""@brief The days from `start` to `day`, counting over the turn of the year."""
|
||||||
|
return (day - start) % constants.DAYS_IN_DATE_CYCLE
|
||||||
|
|
||||||
|
|
||||||
|
def _window(keys: Sequence[_Key]) -> tuple[int, int] | None:
|
||||||
|
"""@brief The first day and the length in days of the shortest span holding all the keys.
|
||||||
|
|
||||||
|
@return None when there is no key.
|
||||||
|
"""
|
||||||
|
days = [_day_number(key[1]) for key in keys]
|
||||||
|
spans = [(max(_offset(day, start) for day in days), start) for start in days]
|
||||||
|
if not spans:
|
||||||
|
return None
|
||||||
|
span, start = min(spans)
|
||||||
|
return start, span
|
||||||
|
|
||||||
|
|
||||||
|
def _is_in_window(key: _Key, window: tuple[int, int] | None) -> bool:
|
||||||
|
"""@brief Tell whether the date of the key lies in the window, ends included."""
|
||||||
|
if window is None:
|
||||||
|
return False
|
||||||
|
start, span = window
|
||||||
|
return _offset(_day_number(key[1]), start) <= span
|
||||||
|
|
||||||
|
|
||||||
def build_verification(
|
def build_verification(
|
||||||
results: Sequence[ClassResult], entries: Sequence[BookingEntry]
|
results: Sequence[ClassResult], entries: Sequence[BookingEntry]
|
||||||
) -> VerificationResult:
|
) -> VerificationResult:
|
||||||
@@ -64,11 +110,16 @@ def build_verification(
|
|||||||
|
|
||||||
@param results What happened to each class; waitlisted when the outcome is a waitlist one.
|
@param results What happened to each class; waitlisted when the outcome is a waitlist one.
|
||||||
@param entries The Tuesday and Thursday 6pm entries found on the page.
|
@param entries The Tuesday and Thursday 6pm entries found on the page.
|
||||||
@return The comparison; the counts are the numbers of results and of entries.
|
@return The comparison; only the entries from the earliest to the latest class handled
|
||||||
|
are checked, the others are `also_booked`, and no class handled means no check.
|
||||||
@exception PageFormatError An entry or a card has no valid date.
|
@exception PageFormatError An entry or a card has no valid date.
|
||||||
"""
|
"""
|
||||||
expected = _expected_keys(results)
|
expected = _expected_keys(results)
|
||||||
remaining = Counter(_entry_keys(entries))
|
window = _window(expected)
|
||||||
|
keys = _entry_keys(entries)
|
||||||
|
checked = [key for key in keys if _is_in_window(key, window)]
|
||||||
|
also_booked = [_describe(key) for key in keys if not _is_in_window(key, window)]
|
||||||
|
remaining = Counter(checked)
|
||||||
verified: list[str] = []
|
verified: list[str] = []
|
||||||
missing: list[str] = []
|
missing: list[str] = []
|
||||||
for key in expected:
|
for key in expected:
|
||||||
@@ -78,7 +129,9 @@ def build_verification(
|
|||||||
else:
|
else:
|
||||||
missing.append(_describe(key))
|
missing.append(_describe(key))
|
||||||
unexpected = [_describe(key) for key in remaining.elements()]
|
unexpected = [_describe(key) for key in remaining.elements()]
|
||||||
return VerificationResult(len(expected), len(entries), verified, missing, unexpected)
|
return VerificationResult(
|
||||||
|
len(expected), len(checked), verified, missing, unexpected, also_booked
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _count_text(template: str, count: int) -> str:
|
def _count_text(template: str, count: int) -> str:
|
||||||
@@ -99,10 +152,11 @@ def format_verification(result: VerificationResult) -> str:
|
|||||||
lines.append(constants.VERIFY_RESULT_TITLE)
|
lines.append(constants.VERIFY_RESULT_TITLE)
|
||||||
lines.append(_count_text(constants.VERIFY_EXPECTED, result.expected))
|
lines.append(_count_text(constants.VERIFY_EXPECTED, result.expected))
|
||||||
lines.append(_count_text(constants.VERIFY_FOUND, result.found))
|
lines.append(_count_text(constants.VERIFY_FOUND, result.found))
|
||||||
|
lines.extend(constants.VERIFY_LINE_ALSO_BOOKED.format(entry=t) for t in result.also_booked)
|
||||||
if result.is_success:
|
if result.is_success:
|
||||||
lines.append(constants.VERIFY_SUCCESS)
|
lines.append(constants.VERIFY_SUCCESS)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
lines.append(constants.VERIFY_MISMATCH.format(count=result.expected - result.found))
|
lines.append(constants.VERIFY_MISMATCH.format(count=len(result.missing)))
|
||||||
lines.extend(constants.VERIFY_LINE_MISSING.format(entry=text) for text in result.missing)
|
lines.extend(constants.VERIFY_LINE_MISSING.format(entry=text) for text in result.missing)
|
||||||
lines.extend(constants.VERIFY_LINE_UNEXPECTED.format(entry=text) for text in result.unexpected)
|
lines.extend(constants.VERIFY_LINE_UNEXPECTED.format(entry=text) for text in result.unexpected)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Imitates the site: a loading text, then the page for the state in the query
|
// Imitates the site: a loading text, then the page for the state in the query
|
||||||
// (?state=empty or ?state=error; anything else shows two bookings and a waitlist entry).
|
// (?state=empty or ?state=error; anything else shows two bookings and a waitlist entry;
|
||||||
|
// ?extra=future adds three more Tuesday and Thursday 6pm bookings after the others).
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const state = params.get("state");
|
const state = params.get("state");
|
||||||
// ?fail=N shows the error for the first N loads of the page in this tab (the network
|
// ?fail=N shows the error for the first N loads of the page in this tab (the network
|
||||||
@@ -20,6 +21,14 @@
|
|||||||
sessionStorage.setItem("loads", String(loads + 1));
|
sessionStorage.setItem("loads", String(loads + 1));
|
||||||
// The nav link keeps the state, so opening the page from here shows the same state again.
|
// The nav link keeps the state, so opening the page from here shows the same state again.
|
||||||
document.getElementById("my-bookings-link").href = "my_bookings.html" + window.location.search;
|
document.getElementById("my-bookings-link").href = "my_bookings.html" + window.location.search;
|
||||||
|
function futureBookings() {
|
||||||
|
const whens = ["Thu, Aug 21, 6:00 PM", "Tue, Aug 26, 6:00 PM", "Thu, Aug 28, 6:00 PM"];
|
||||||
|
return whens.map(function (when, i) {
|
||||||
|
return '<div id="booking-card-' + (13 + i) + '" data-booking-status="confirmed">' +
|
||||||
|
'<h3 id="booking-class-name-' + (13 + i) + '">Spin Class</h3>' +
|
||||||
|
'<p><strong>When:</strong> ' + when + '</p></div>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
const page = document.getElementById("my-bookings-page");
|
const page = document.getElementById("my-bookings-page");
|
||||||
const content = document.getElementById("content");
|
const content = document.getElementById("content");
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
@@ -30,7 +39,8 @@
|
|||||||
} else if (state === "empty") {
|
} else if (state === "empty") {
|
||||||
content.innerHTML = '<div id="empty-bookings-state">You have no bookings yet</div>';
|
content.innerHTML = '<div id="empty-bookings-state">You have no bookings yet</div>';
|
||||||
} else {
|
} else {
|
||||||
page.dataset.bookingsCount = "2";
|
const extra = params.get("extra") === "future" ? futureBookings() : "";
|
||||||
|
page.dataset.bookingsCount = params.get("extra") === "future" ? "5" : "2";
|
||||||
page.dataset.waitlistCount = "1";
|
page.dataset.waitlistCount = "1";
|
||||||
content.innerHTML =
|
content.innerHTML =
|
||||||
'<div id="confirmed-bookings-section">' +
|
'<div id="confirmed-bookings-section">' +
|
||||||
@@ -40,6 +50,7 @@
|
|||||||
'<div id="booking-card-12" data-booking-status="confirmed">' +
|
'<div id="booking-card-12" data-booking-status="confirmed">' +
|
||||||
'<h3 id="booking-class-name-12">Yoga Class</h3>' +
|
'<h3 id="booking-class-name-12">Yoga Class</h3>' +
|
||||||
'<p><strong>When:</strong> Wed, Aug 13, 7:00 AM</p></div>' +
|
'<p><strong>When:</strong> Wed, Aug 13, 7:00 AM</p></div>' +
|
||||||
|
extra +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div id="waitlist-section">' +
|
'<div id="waitlist-section">' +
|
||||||
'<div id="waitlist-card-21" data-booking-status="waitlisted">' +
|
'<div id="waitlist-card-21" data-booking-status="waitlisted">' +
|
||||||
|
|||||||
@@ -175,6 +175,20 @@ def test_list_my_bookings_reads_confirmed_and_waitlist_entries(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_my_bookings_returns_bookings_listed_after_the_others(
|
||||||
|
open_my_bookings_page: Callable[[str], SeleniumGym],
|
||||||
|
) -> None:
|
||||||
|
entries = get_my_bookings(open_my_bookings_page("?extra=future"))
|
||||||
|
|
||||||
|
assert [entry.when_text for entry in entries][-4:] == [
|
||||||
|
"Thu, Aug 21, 6:00 PM",
|
||||||
|
"Tue, Aug 26, 6:00 PM",
|
||||||
|
"Thu, Aug 28, 6:00 PM",
|
||||||
|
"Thu, Aug 14, 6:00 PM",
|
||||||
|
]
|
||||||
|
assert len(entries) == 6
|
||||||
|
|
||||||
|
|
||||||
def test_list_my_bookings_returns_nothing_for_the_empty_state(
|
def test_list_my_bookings_returns_nothing_for_the_empty_state(
|
||||||
open_my_bookings_page: Callable[[str], SeleniumGym],
|
open_my_bookings_page: Callable[[str], SeleniumGym],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ def test_verify_bookings_ignores_entries_that_are_not_target_classes() -> None:
|
|||||||
assert verify_bookings(gym, results).is_success
|
assert verify_bookings(gym, results).is_success
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_bookings_lists_target_entries_after_the_window_as_also_booked() -> None:
|
||||||
|
gym = FakeGym(bookings=[make_entry(), make_entry("Thu, Aug 21, 6:00 PM")])
|
||||||
|
results = [ClassResult(make_card("Tue, Aug 12"), Outcome.NEW_BOOKING)]
|
||||||
|
|
||||||
|
verification = verify_bookings(gym, results)
|
||||||
|
|
||||||
|
assert verification.is_success
|
||||||
|
assert verification.also_booked == ["Spin Class on Thu, Aug 21"]
|
||||||
|
|
||||||
|
|
||||||
def test_verify_bookings_reports_a_missing_booking() -> None:
|
def test_verify_bookings_reports_a_missing_booking() -> None:
|
||||||
gym = FakeGym(bookings=[make_entry("Wed, Aug 13, 7:00 AM")])
|
gym = FakeGym(bookings=[make_entry("Wed, Aug 13, 7:00 AM")])
|
||||||
results = [ClassResult(make_card("Tue, Aug 12"), Outcome.NEW_BOOKING)]
|
results = [ClassResult(make_card("Tue, Aug 12"), Outcome.NEW_BOOKING)]
|
||||||
|
|||||||
@@ -182,6 +182,21 @@ def test_main_logs_the_verification_and_returns_0_on_success(
|
|||||||
assert "Expected: 1 booking\nFound: 1 booking\n✅ SUCCESS" in caplog.text
|
assert "Expected: 1 booking\nFound: 1 booking\n✅ SUCCESS" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("credentials_env")
|
||||||
|
def test_main_notes_bookings_after_the_handled_classes_and_returns_0(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
gym = FakeGym(
|
||||||
|
cards=[make_card("Tue, Aug 19")],
|
||||||
|
bookings=[make_entry("Tue, Aug 19, 6:00 PM"), make_entry("Tue, Aug 26, 6:00 PM")],
|
||||||
|
)
|
||||||
|
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
assert main([], lambda *_: gym) == 0
|
||||||
|
|
||||||
|
assert " i Also booked (not checked): Spin Class on Tue, Aug 26\n✅ SUCCESS" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("credentials_env")
|
@pytest.mark.usefixtures("credentials_env")
|
||||||
def test_main_returns_3_on_a_mismatch(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
def test_main_returns_3_on_a_mismatch(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
||||||
gym = FakeGym(cards=[make_card("Tue, Aug 19")], bookings=[make_entry("Wed, Aug 20, 7:00 AM")])
|
gym = FakeGym(cards=[make_card("Tue, Aug 19")], bookings=[make_entry("Wed, Aug 20, 7:00 AM")])
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
@brief The comparison of the bot's results with the My Bookings page, and its text.
|
@brief The comparison of the bot's results with the My Bookings page, and its text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
from tests.unit.test_booking_entry import make_entry
|
from tests.unit.test_booking_entry import make_entry
|
||||||
from tests.unit.test_models import make_card
|
from tests.unit.test_models import make_card
|
||||||
|
|
||||||
|
from snack_and_lift.domain.errors import PageFormatError
|
||||||
from snack_and_lift.domain.models import ClassResult, Outcome
|
from snack_and_lift.domain.models import ClassResult, Outcome
|
||||||
from snack_and_lift.domain.verification import build_verification, format_verification
|
from snack_and_lift.domain.verification import build_verification, format_verification
|
||||||
|
|
||||||
@@ -58,16 +60,154 @@ def test_missing_entry_is_a_mismatch_with_a_line() -> None:
|
|||||||
assert text.endswith(" ✗ Missing: Spin Class (Waitlist) on Thu, Aug 14")
|
assert text.endswith(" ✗ Missing: Spin Class (Waitlist) on Thu, Aug 14")
|
||||||
|
|
||||||
|
|
||||||
def test_unexpected_entry_gives_a_negative_missing_count() -> None:
|
def test_unexpected_entry_inside_the_window_is_a_mismatch() -> None:
|
||||||
result = build_verification(
|
result = build_verification(
|
||||||
[BOOKED], [make_entry("Tue, Aug 12, 6:00 PM"), make_entry("Thu, Aug 14, 6:00 PM")]
|
[BOOKED, WAITLISTED],
|
||||||
|
[
|
||||||
|
make_entry("Tue, Aug 12, 6:00 PM"),
|
||||||
|
make_entry("Thu, Aug 14, 6:00 PM", waitlisted=True),
|
||||||
|
make_entry("Thu, Aug 14, 6:00 PM", name="Yoga Class"),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
text = format_verification(result)
|
text = format_verification(result)
|
||||||
|
|
||||||
assert result.unexpected == ["Spin Class on Thu, Aug 14"]
|
assert not result.is_success
|
||||||
assert "❌ MISMATCH: Missing -1 bookings" in text
|
assert result.unexpected == ["Yoga Class on Thu, Aug 14"]
|
||||||
assert text.endswith(" ? Unexpected: Spin Class on Thu, Aug 14")
|
assert "❌ MISMATCH: Missing 0 bookings" in text
|
||||||
|
assert text.endswith(" ? Unexpected: Yoga Class on Thu, Aug 14")
|
||||||
|
|
||||||
|
|
||||||
|
def test_entries_outside_the_window_are_a_note_and_a_success() -> None:
|
||||||
|
result = build_verification(
|
||||||
|
[BOOKED, WAITLISTED],
|
||||||
|
[
|
||||||
|
make_entry("Tue, Aug 12, 6:00 PM"),
|
||||||
|
make_entry("Thu, Aug 14, 6:00 PM", waitlisted=True),
|
||||||
|
make_entry("Thu, Aug 7, 6:00 PM"),
|
||||||
|
make_entry("Tue, Aug 19, 6:00 PM", waitlisted=True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.is_success
|
||||||
|
assert (result.expected, result.found) == (2, 2)
|
||||||
|
assert result.also_booked == [
|
||||||
|
"Spin Class on Thu, Aug 7",
|
||||||
|
"Spin Class (Waitlist) on Tue, Aug 19",
|
||||||
|
]
|
||||||
|
assert format_verification(result) == (
|
||||||
|
"--- VERIFYING ON MY BOOKINGS PAGE ---\n"
|
||||||
|
" ✓ Verified: Spin Class\n"
|
||||||
|
" ✓ Verified: Spin Class (Waitlist)\n"
|
||||||
|
"\n"
|
||||||
|
"--- VERIFICATION RESULT ---\n"
|
||||||
|
"Expected: 2 bookings\n"
|
||||||
|
"Found: 2 bookings\n"
|
||||||
|
" i Also booked (not checked): Spin Class on Thu, Aug 7\n"
|
||||||
|
" i Also booked (not checked): Spin Class (Waitlist) on Tue, Aug 19\n"
|
||||||
|
"✅ SUCCESS: All bookings verified!"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_window_ends_are_inclusive() -> None:
|
||||||
|
result = build_verification(
|
||||||
|
[BOOKED, WAITLISTED],
|
||||||
|
[make_entry("Tue, Aug 12, 6:00 PM"), make_entry("Thu, Aug 14, 6:00 PM", waitlisted=True)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.also_booked == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_count_is_the_number_of_missing_classes_with_notes_outside() -> None:
|
||||||
|
result = build_verification(
|
||||||
|
[BOOKED, WAITLISTED],
|
||||||
|
[make_entry("Tue, Aug 12, 6:00 PM"), make_entry("Tue, Aug 26, 6:00 PM")],
|
||||||
|
)
|
||||||
|
|
||||||
|
text = format_verification(result)
|
||||||
|
|
||||||
|
assert result.missing == ["Spin Class (Waitlist) on Thu, Aug 14"]
|
||||||
|
assert "Expected: 2 bookings\nFound: 1 booking\n" in text
|
||||||
|
assert (
|
||||||
|
" i Also booked (not checked): Spin Class on Tue, Aug 26\n❌ MISMATCH: Missing 1 bookings"
|
||||||
|
in text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_count_is_never_negative() -> None:
|
||||||
|
result = build_verification(
|
||||||
|
[BOOKED, WAITLISTED],
|
||||||
|
[
|
||||||
|
make_entry("Tue, Aug 12, 6:00 PM"),
|
||||||
|
make_entry("Wed, Aug 13, 6:00 PM"),
|
||||||
|
make_entry("Wed, Aug 13, 6:00 PM", name="Yoga Class"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Missing 1 bookings" in format_verification(result)
|
||||||
|
assert len(result.unexpected) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_run_with_bookings_after_the_window_is_a_success() -> None:
|
||||||
|
first = ClassResult(make_card("Thu, Oct 29"), Outcome.ALREADY_BOOKED)
|
||||||
|
second = ClassResult(make_card("Tue, Nov 3", card_id="2"), Outcome.ALREADY_BOOKED)
|
||||||
|
entries = [
|
||||||
|
make_entry("Thu, Oct 29, 6:00 PM"),
|
||||||
|
make_entry("Tue, Nov 3, 6:00 PM"),
|
||||||
|
make_entry("Thu, Nov 12, 6:00 PM"),
|
||||||
|
make_entry("Tue, Nov 17, 6:00 PM"),
|
||||||
|
make_entry("Tue, Nov 24, 6:00 PM"),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = build_verification([first, second], entries)
|
||||||
|
|
||||||
|
assert result.is_success
|
||||||
|
assert (result.expected, result.found) == (2, 2)
|
||||||
|
assert result.also_booked == [
|
||||||
|
"Spin Class on Thu, Nov 12",
|
||||||
|
"Spin Class on Tue, Nov 17",
|
||||||
|
"Spin Class on Tue, Nov 24",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_results_makes_every_entry_a_note_and_no_mismatch() -> None:
|
||||||
|
result = build_verification([], [make_entry("Tue, Aug 12, 6:00 PM")])
|
||||||
|
|
||||||
|
assert result.is_success
|
||||||
|
assert (result.expected, result.found) == (0, 0)
|
||||||
|
assert result.also_booked == ["Spin Class on Tue, Aug 12"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_window_holds_over_the_turn_of_the_year() -> None:
|
||||||
|
december = ClassResult(make_card("Tue, Dec 30"), Outcome.NEW_BOOKING)
|
||||||
|
january = ClassResult(make_card("Thu, Jan 1", card_id="2"), Outcome.NEW_BOOKING)
|
||||||
|
|
||||||
|
result = build_verification(
|
||||||
|
[december, january],
|
||||||
|
[
|
||||||
|
make_entry("Tue, Dec 30, 6:00 PM"),
|
||||||
|
make_entry("Thu, Jan 1, 6:00 PM"),
|
||||||
|
make_entry("Thu, Jan 8, 6:00 PM"),
|
||||||
|
make_entry("Thu, Dec 18, 6:00 PM"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.is_success
|
||||||
|
assert result.also_booked == ["Spin Class on Thu, Jan 8", "Spin Class on Thu, Dec 18"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_leap_day_is_a_valid_date() -> None:
|
||||||
|
leap = ClassResult(make_card("Thu, Feb 29"), Outcome.NEW_BOOKING)
|
||||||
|
|
||||||
|
assert build_verification([leap], [make_entry("Thu, Feb 29, 6:00 PM")]).is_success
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("date_text", ["Tue, Foo 12", "Tue, Feb 30"])
|
||||||
|
def test_an_invalid_date_is_a_page_format_error(date_text: str) -> None:
|
||||||
|
bad = ClassResult(make_card(date_text), Outcome.NEW_BOOKING)
|
||||||
|
|
||||||
|
with pytest.raises(PageFormatError):
|
||||||
|
build_verification([bad], [])
|
||||||
|
|
||||||
|
|
||||||
def test_waitlist_flag_must_match() -> None:
|
def test_waitlist_flag_must_match() -> None:
|
||||||
@@ -75,7 +215,7 @@ def test_waitlist_flag_must_match() -> None:
|
|||||||
|
|
||||||
assert result.missing == ["Spin Class on Tue, Aug 12"]
|
assert result.missing == ["Spin Class on Tue, Aug 12"]
|
||||||
assert result.unexpected == ["Spin Class (Waitlist) on Tue, Aug 12"]
|
assert result.unexpected == ["Spin Class (Waitlist) on Tue, Aug 12"]
|
||||||
assert "Missing 0 bookings" in format_verification(result)
|
assert "Missing 1 bookings" in format_verification(result)
|
||||||
|
|
||||||
|
|
||||||
def test_date_must_match() -> None:
|
def test_date_must_match() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user