Compare commits

..
Author SHA1 Message Date
Tirsvad 034f792c1a Add step 8 screenshot after the verification fix (MIL-009) 2026-10-28 21:48:36 +08:00
Tirsvad fedbc39165 Merge pull request 'MIL-009: verification fix' (#54) from mil-009-verification-fix into main
Publish accepted milestones / publish (push) Failing after 4s
Publish wiki / publish-wiki (push) Successful in 15s
Publish wiki / wiki (push) Successful in 15s
TirSystem/git-workflows: Publish wiki / publish-wiki (push) Successful in 11s
TirSystem/git-workflows: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
2026-10-28 14:46:15 +01:00
Tirsvad 85f5eecd07 Resolve pending commit links 2026-10-28 21:46:06 +08:00
Tirsvad 52427ca69c Add review record RC-024 and update the step 8 notes (MIL-009) 2026-10-28 21:46:04 +08:00
Tirsvad 353d157e22 Compare only the days of the run in the verification
Task: MIL-009#1
Task: MIL-009#2
2026-10-28 21:45:48 +08:00
Tirsvad 3b9e381cd4 Plan MIL-009: verification fix (issue #50) 2026-10-28 21:39:22 +08:00
Tirsvad ca6a706abe Merge pull request 'MIL-008: real screenshots' (#49) from mil-008-real-screenshots into main
TirSystem/git-workflows: Sync GitHub mirror metadata / sync-metadata (push) Successful in 7s
TirSystem/git-workflows: Publish wiki / publish-wiki (push) Successful in 36s
Publish wiki / publish-wiki (push) Successful in 12s
Publish wiki / wiki (push) Successful in 13s
2026-10-28 14:34:50 +01:00
Tirsvad bd28943482 Add real screenshots of steps 1 to 9 (MIL-008) 2026-10-28 21:34:38 +08:00
Tirsvad 2c568f1fa2 Merge pull request 'MIL-008: add step images' (#48) from mil-008-screenshots into main
TirSystem/git-workflows: Publish wiki / publish-wiki (push) Successful in 24s
TirSystem/git-workflows: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Publish wiki / publish-wiki (push) Successful in 15s
Publish wiki / wiki (push) Successful in 15s
2026-10-28 12:04:24 +01:00
Tirsvad cbdd553394 Add step 2 to 9 illustrations and label them (MIL-008) 2026-10-28 19:04:15 +08:00
Tirsvad 7a8412ad6b Merge pull request 'MIL-008: documentation, wiki and release' (#47) from mil-008-documentation into main
Publish wiki / publish-wiki (push) Successful in 36s
TirSystem/git-workflows: Publish wiki / publish-wiki (push) Successful in 11s
TirSystem/git-workflows: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
Publish accepted milestones / publish (push) Failing after 6s
Publish wiki / wiki (push) Successful in 36s
2026-10-28 11:20:58 +01:00
49 changed files with 500 additions and 67 deletions
+1
View File
@@ -43,3 +43,4 @@ Thumbs.db
# Selenium Chrome profile created by the bot (holds the practice site data)
/chrome_profile/
.env.gym
+1 -1
View File
@@ -8,7 +8,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
SNACK_AND_LIFT_E2E=1
RUN apt-get update \
&& apt-get install -y --no-install-recommends chromium chromium-driver fonts-liberation fonts-noto-color-emoji \
&& apt-get install -y --no-install-recommends chromium chromium-driver fonts-liberation fonts-dejavu-core fonts-noto-color-emoji \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
+1 -1
View File
@@ -115,7 +115,7 @@ docker run --rm snack-and-lift
## 📸 Lecture steps and screenshots
Each step has a wiki page that explains what it teaches, in [English](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Home), [Dansk](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Home-da) and [Bahasa Melayu](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Home-ms). Only the screenshot of step 1 is in the repository yet: the others need a run with a real gym account, which the maintainer makes with `--screenshot-dir` (see [Getting started](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Getting-Started)). Until then their images do not show.
Each step has a wiki page that explains what it teaches, in [English](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Home), [Dansk](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Home-da) and [Bahasa Melayu](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Home-ms). All nine images are screenshots of a real run against the live site. Step 8 ends in SUCCESS; the My Bookings page also lists bookings from earlier runs, which the check prints as "Also booked (not checked)".
| Step | Wiki page | Screenshot |
| --- | --- | --- |
+2 -2
View File
@@ -14,9 +14,9 @@ document of a type. `Primary File` may contain a glob (e.g.
| BC | Business Case | docs/business-case.md | 002 |
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
| PP | Project Plan | docs/project-plan.md | 002 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 009 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 010 |
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0004 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 023 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 024 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

@@ -0,0 +1,74 @@
# MIL-009 Verification fix: only compare the bookings of the run
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-009 |
| CrossReference | [BC-001], [MIL-006] |
| Language | en |
| Domain | it |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Initial version, added after a real run (issue #50) | [3b9e381] |
---
## Purpose
Decide whether the My Bookings check ignores bookings that lie outside the window the bot worked on, so that a real account with bookings from earlier runs ends in SUCCESS, and whether the report never prints a negative count.
## Deliverable
A corrected comparison in `domain/verification.py` and its use in `application/verification.py`, unit tests and an end-to-end fixture with extra future bookings, new screenshots for lecture steps 7 to 9 from a real run, and updated README and wiki notes.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | Unit tests show that entries outside the window of the run are listed as a note and do not cause MISMATCH | 0 failures; a test uses entries after the last handled class | A test fails or is missing |
| 2 | Unit tests show that the missing count is never negative and that a class the bot booked but the page lacks is still a MISMATCH | 0 failures | A test fails or is missing |
| 3 | Unit and container tests of the existing steps still pass with at least 80% coverage of the logic modules | Both true | Either false |
| 4 | The code review against `qc-programming-python.md` is `Go` | `RC-*` is `Go` | `No-Go` |
| 5 | `docs/images/step-8.png` from a real run shows SUCCESS | S01 reran the bot with their own account and the image is committed | The image shows MISMATCH or is not replaced |
## Dependencies
| Depends on | Reason |
| --- | --- |
| MIL-006 | Changes the verification that MIL-006 built |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| O5, success criterion 3 | [BC-001] |
| CN-001, CN-003 | [SA-001] |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S01 |
| Approving reviewer | S02 (AI as stand-in, autonomous mode, not independent) |
## Target Date
2026-11-06 — inside the planned window 2026-10-28 to 2026-11-06 of [PP-001]; the Business Case sets no deadline.
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Limit the comparison to the window of the run | In `build_verification` count only page entries dated from the first to the last class the bot handled; list the other entries as "also booked, not checked". The missing count never goes below zero. | No | O5 |
| 2 | Test the corrected comparison | Unit tests for entries outside the window, a booked class that is missing on the page, and the count; an end-to-end fixture with extra future bookings. | No | O5 |
| 3 | Refresh the step 7 to 9 screenshots and the notes | S01 reruns the bot with their own account and `--screenshot-dir`; the AI commits the images and removes the MISMATCH note from the README and the step 8 wiki pages in three languages. | No | O5, O8 |
---
[BC-001]: ../business-case.md
[MIL-006]: ./mil-006-verify-and-time-travel.md
[PP-001]: ../project-plan.md
[SA-001]: ../stakeholder-analysis.md
[3b9e381]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/commit/3b9e381cd42c56cc4543869c5b80c8e0014b4948
+7 -3
View File
@@ -4,7 +4,7 @@
| Key | Value |
| --- | --- |
| ID | PP-001 |
| CrossReference | [BC-001], [SA-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008] |
| CrossReference | [BC-001], [SA-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009] |
| Language | en |
| Domain | it |
@@ -12,12 +12,13 @@
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version: eight gateways | [50e8331] |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | S01 | Add MIL-009, a verification fix found in a real run (S01 accepted the plan in chat) | [3b9e381] |
---
## Purpose
This plan schedules eight gateways (`MIL-001` to `MIL-008`) that take the Snack and Lift booking bot from an accepted baseline to a documented, tested and published repository. The Business Case sets no deadline, so the window 2026-10-28 to 2026-11-06 is a planning assumption of this plan. Each gateway ends in a decision and a deliverable and is delivered by one pull request on its own branch.
This plan schedules nine gateways (`MIL-001` to `MIL-009`) that take the Snack and Lift booking bot from an accepted baseline to a documented, tested and published repository. The Business Case sets no deadline, so the window 2026-10-28 to 2026-11-06 is a planning assumption of this plan. Each gateway ends in a decision and a deliverable and is delivered by one pull request on its own branch.
## Planning Assumptions
@@ -39,6 +40,7 @@ This plan schedules eight gateways (`MIL-001` to `MIL-008`) that take the Snack
| Lecture steps 7 and 8 | [MIL-006] | 2026-11-01 to 2026-11-02 | 2026-11-02 | S01 | none (technical tasks) | My Bookings verification, time travel, screenshots 7 and 8 | |
| Lecture step 9 | [MIL-007] | 2026-11-02 to 2026-11-04 | 2026-11-04 | S01 | none (technical tasks) | `retry`, resilient `login`, `book_class`, `get_my_bookings`, screenshot 9 | |
| Documentation, wiki and release | [MIL-008] | 2026-11-04 to 2026-11-06 | 2026-11-06 | S01 | none (documentation) | README, Doxygen, wiki in en, da and ms, final checks | |
| Verification fix | [MIL-009] | 2026-11-04 to 2026-11-06 | 2026-11-06 | S01 | none (technical tasks) | Verification compares only the window of the run; new screenshots 7 to 9 | |
```plantuml
@startgantt
@@ -77,7 +79,7 @@ Project starts 2026-10-28
## Dependencies
```
MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007 → MIL-008
MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007 → MIL-008 → MIL-009
```
A No-Go on a gateway stops all later gateways until it is fixed and re-reviewed. The end date then moves by the days spent on the rework; S01 decides a move of the end date.
@@ -109,4 +111,6 @@ A No-Go on a gateway stops all later gateways until it is fixed and re-reviewed.
[MIL-006]: ./milestones/mil-006-verify-and-time-travel.md
[MIL-007]: ./milestones/mil-007-resilience.md
[MIL-008]: ./milestones/mil-008-documentation.md
[MIL-009]: ./milestones/mil-009-verification-fix.md
[50e8331]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/commit/50e8331bd21910a72ae56b74c9ff6a3677a709b3
[3b9e381]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/commit/3b9e381cd42c56cc4543869c5b80c8e0014b4948
@@ -0,0 +1,65 @@
# SQA Review Record
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-023 |
| CrossReference | [MIL-009], [QC-MIL-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Review record, full review | [3b9e381] |
---
## Artifact Under Review
- Instance reviewed: [MIL-009]
- Checklist used: [QC-MIL-001] (`QC-MIL-001`)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S02`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | Deliverable section names concrete files, runs or documents. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Go/No-Go table has objectively checkable criteria with Go and No-Go columns. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Dependencies table names the preceding milestone and the reason. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Traceability cites O5 of BC-001 and the concerns of SA-001. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Ownership names S01 as owner and S02 (AI as stand-in) as approving reviewer. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Target Date lies inside the window 2026-10-28 to 2026-11-06 of PP-001. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata has `Language` = en and `Domain` = it. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is BCP 47; `it` is in the registry domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells of MIL-009 are English (stand-in assessment, see the verdict). |
| 4 | The register matches the one the registry gives for the artifact type | Pass | Register of the type is given in the registry (Languages); the text keeps to it. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | Terms follow DICT-001: bot, booking, verification (stand-in assessment, see the verdict). |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists in `docs/`. |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | N-A | First version; no language change. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The stand-in reviewer reads English and checked the IT terms in context (stand-in assessment, see the verdict). |
| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out or defined where first used, or are the framework's own IDs. |
## Overall Verdict
Go — MIL-009 meets every Mandatory criterion. No blocking finding. The review is not independent: the AI authored and reviewed it as stand-in for S02, and S01 can challenge it in the end-of-run summary. Criteria 3, 5 and 9 of QC-LANG-001 were assessed by the stand-in only; the framework would hold them `Pending` for an independent language reviewer. S01 is asked to confirm them in the end-of-run summary.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | S01 | - |
---
[MIL-009]: ../../milestones/mil-009-verification-fix.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[3b9e381]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/commit/3b9e381cd42c56cc4543869c5b80c8e0014b4948
@@ -0,0 +1,62 @@
# SQA Review Record
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-024 |
| CrossReference | [MIL-009], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Review record, full review of the source and tests of MIL-009 | [52427ca] |
---
## Artifact Under Review
- Instance reviewed: [MIL-009]
- Checklist used: [QC-PY-001] (`QC-PY-001`)
- Scope: full review of the source and tests of MIL-009
- Language and domain: n/a
- Language reviewer: none
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S02`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Modules, functions and variables are `snake_case`, classes `PascalCase`, constants `UPPER_SNAKE`; `ruff check` with the N rules passes. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (for example `retry`, `login`, `book_class`, `get_my_bookings`); no single-letter names outside comprehensions. |
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff format` leaves 0 files changed and `ruff check` reports 0 findings; there is no suppression. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every function and method has annotations (ruff ANN rules pass), including `-> None`. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No bare `except`; specific exceptions are caught, and re-raised with `raise ... from` where the cause matters. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable default arguments; no builtin is shadowed (ruff B and A-style checks pass). |
| 7 | Files, locks and connections are managed with context managers | Pass | Files and the browser are managed with `with`/`try-finally`; no unmanaged file handle exists. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Public modules, classes and functions have Doxygen docstrings (`@brief`, `@param`, `@return`). |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No print calls; messages go through `logging`. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No Design Class Diagram exists; ADR-0001 records that design artifacts are skipped. Classes follow ADR-0002. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 249 unit tests need no browser or network (new: entries outside the window, inclusive ends, missing class, unexpected entry, no results, New Year wrap-around, invalid dates); 21 container tests include a fixture with extra future bookings; coverage of the non-infrastructure modules is 100%. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | N-A | No strict type checker is configured; annotations are complete and checked by ruff. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `pyproject.toml` declares Selenium (runtime) and pytest, pytest-cov, ruff (dev) with lower bounds; none is unused. |
| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | Each rule, constant and message exists once (constants in `constants.py`, rules in `domain/`). |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `domain` and `application` import no Selenium; only `infrastructure` imports it; no import cycle exists. |
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | One reason to change per module; the adapter hides Selenium behind a port; fakes honour the port contract. |
| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | Layout follows `src/snack_and_lift/{domain,application,infrastructure}` and `tests/{unit,integration,e2e}`; there is no presentation layer for a command-line bot (ADR-0002). |
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | All file and folder names are English lowercase with `_`, `-` and `.` only. |
## Overall Verdict
Go-with-conditions - the code of MIL-009 meets every Mandatory criterion of QC-PY-001 and there is no blocking finding, but part of the gate evidence cannot be produced by the stand-in. Gate criteria 1 to 3 are met in tests. Criterion 5 is not met yet: `docs/images/step-8.png` still shows the MISMATCH of the run before the fix, because a new image needs a run with S01's own account, which the stand-in may not log in to. The README and the wiki say that the image predates the fix. The review is not independent: the AI wrote and reviewed it as stand-in for S02, and S01 can challenge it in the end-of-run summary.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Rerun `python -m snack_and_lift --screenshot-dir docs/images` with your own account and tell the AI, which then commits the new images and removes the note | S01 | when S01 has time |
---
[MIL-009]: ../../milestones/mil-009-verification-fix.md
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[52427ca]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/commit/52427ca69cc0193ba9734b548f31a33549606249
+3 -1
View File
@@ -23,7 +23,7 @@ Tracks backward and forward links between artifact instances so that the Busines
| --- | --- | --- | --- | --- | --- | --- |
| [BC-001] | Business Case | en | it | - | [SA-001], [PP-001] | RC-001 |
| [SA-001] | Stakeholder Analysis | en | it | [BC-001] | [PP-001] | RC-002 |
| [PP-001] | Project Plan | en | it | [BC-001], [SA-001] | [MIL-001] to [MIL-008] | RC-003 |
| [PP-001] | Project Plan | en | it | [BC-001], [SA-001] | [MIL-001] to [MIL-009] | RC-003 |
| [DICT-001] | Domain Dictionary | en | it | [BC-001], [SA-001] | - | RC-004 |
| [ADR-0001] | ADR | - | - | [BC-001] | [PP-001] | RC-005 |
| [ADR-0002] | ADR | - | - | [BC-001], [SA-001] | code from MIL-002 onward | RC-006 |
@@ -36,6 +36,7 @@ Tracks backward and forward links between artifact instances so that the Busines
| [MIL-006] | Milestone | en | it | [BC-001], [PP-001] | [MIL-007] | RC-013 |
| [MIL-007] | Milestone | en | it | [BC-001], [PP-001] | [MIL-008] | RC-014 |
| [MIL-008] | Milestone | en | it | [BC-001], [PP-001] | - | RC-015 |
| [MIL-009] | Milestone | en | it | [BC-001], [MIL-006] | - | RC-023 |
| Source and tests of [MIL-002] | Python source code | - | - | [MIL-002], [ADR-0002], [ADR-0003] | - | RC-016 |
| Source and tests of [MIL-003] | Python source code | - | - | [MIL-003], [ADR-0002], [ADR-0003] | - | RC-017 (Go-with-conditions) |
| Source and tests of [MIL-004] | Python source code | - | - | [MIL-004], [ADR-0002] | - | RC-018 (Go-with-conditions) |
@@ -75,4 +76,5 @@ Tracks backward and forward links between artifact instances so that the Busines
[MIL-006]: ../milestones/mil-006-verify-and-time-travel.md
[MIL-007]: ../milestones/mil-007-resilience.md
[MIL-008]: ../milestones/mil-008-documentation.md
[MIL-009]: ../milestones/mil-009-verification-fix.md
[50e8331]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/commit/50e8331bd21910a72ae56b74c9ff6a3677a709b3
-2
View File
@@ -14,6 +14,4 @@ Du lærer eksplicitte ventetider (`WebDriverWait`) og at udfylde en formular. `l
![Trin 2: Automatisk login](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-2.png)
> Dette skærmbillede kræver en kørsel med en rigtig gym-konto. Vedligeholderen tilføjer det med `--screenshot-dir` (se [Kom i gang](Getting-Started-da)); indtil da mangler filen `docs/images/step-2.png`, og billedet ovenfor vises ikke.
[Forrige: 1](Step-1-da) · [Forside](Home-da) · [Næste: 3](Step-3-da)
-2
View File
@@ -14,6 +14,4 @@ Anda belajar menunggu secara eksplisit (`WebDriverWait`) dan mengisi borang. `lo
![Langkah 2: Log masuk automatik](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-2.png)
> Tangkapan skrin ini memerlukan larian dengan akaun gim sebenar. Penyelenggara menambahnya dengan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)); sehingga itu fail `docs/images/step-2.png` tiada dan imej di atas tidak dipaparkan.
[Sebelumnya: 1](Step-1-ms) · [Utama](Home-ms) · [Seterusnya: 3](Step-3-ms)
-2
View File
@@ -14,6 +14,4 @@ You learn explicit waits (`WebDriverWait`) and how to fill a form. `login()` typ
![Step 2: Automated login](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-2.png)
> This screenshot needs a run with a real gym account. The maintainer adds it with `--screenshot-dir` (see [Getting started](Getting-Started)); until then the file `docs/images/step-2.png` is missing and the image above does not show.
[Previous: 1](Step-1) · [Home](Home) · [Next: 3](Step-3)
-2
View File
@@ -14,6 +14,4 @@ Du lærer at finde et element relativt til et andet: næste tirsdags dagsgruppe
![Trin 3: Book næste tirsdagshold](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-3.png)
> Dette skærmbillede kræver en kørsel med en rigtig gym-konto. Vedligeholderen tilføjer det med `--screenshot-dir` (se [Kom i gang](Getting-Started-da)); indtil da mangler filen `docs/images/step-3.png`, og billedet ovenfor vises ikke.
[Forrige: 2](Step-2-da) · [Forside](Home-da) · [Næste: 4](Step-4-da)
-2
View File
@@ -14,6 +14,4 @@ Anda belajar mencari elemen berdasarkan elemen lain: kumpulan hari Selasa seteru
![Langkah 3: Tempah kelas Selasa seterusnya](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-3.png)
> Tangkapan skrin ini memerlukan larian dengan akaun gim sebenar. Penyelenggara menambahnya dengan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)); sehingga itu fail `docs/images/step-3.png` tiada dan imej di atas tidak dipaparkan.
[Sebelumnya: 2](Step-2-ms) · [Utama](Home-ms) · [Seterusnya: 4](Step-4-ms)
-2
View File
@@ -14,6 +14,4 @@ You learn to find an element relative to another one: the day group of the next
![Step 3: Book the next Tuesday class](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-3.png)
> This screenshot needs a run with a real gym account. The maintainer adds it with `--screenshot-dir` (see [Getting started](Getting-Started)); until then the file `docs/images/step-3.png` is missing and the image above does not show.
[Previous: 2](Step-2) · [Home](Home) · [Next: 4](Step-4)
-2
View File
@@ -14,6 +14,4 @@ Du lærer at læse en knaps tilstand før du handler: "Book Class", "Join Waitli
![Trin 4: Tjek om et hold allerede er booket](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-4.png)
> Dette skærmbillede kræver en kørsel med en rigtig gym-konto. Vedligeholderen tilføjer det med `--screenshot-dir` (se [Kom i gang](Getting-Started-da)); indtil da mangler filen `docs/images/step-4.png`, og billedet ovenfor vises ikke.
[Forrige: 3](Step-3-da) · [Forside](Home-da) · [Næste: 5](Step-5-da)
-2
View File
@@ -14,6 +14,4 @@ Anda belajar membaca keadaan butang sebelum bertindak: "Book Class", "Join Waitl
![Langkah 4: Semak sama ada kelas sudah ditempah](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-4.png)
> Tangkapan skrin ini memerlukan larian dengan akaun gim sebenar. Penyelenggara menambahnya dengan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)); sehingga itu fail `docs/images/step-4.png` tiada dan imej di atas tidak dipaparkan.
[Sebelumnya: 3](Step-3-ms) · [Utama](Home-ms) · [Seterusnya: 5](Step-5-ms)
-2
View File
@@ -14,6 +14,4 @@ You learn to read the state of a button before acting: "Book Class", "Join Waitl
![Step 4: Check if a class is already booked](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-4.png)
> This screenshot needs a run with a real gym account. The maintainer adds it with `--screenshot-dir` (see [Getting started](Getting-Started)); until then the file `docs/images/step-4.png` is missing and the image above does not show.
[Previous: 3](Step-3) · [Home](Home) · [Next: 5](Step-5)
-2
View File
@@ -14,6 +14,4 @@ Du lærer at føre tællere mens en løkke kører og at udskrive en rapport: BOO
![Trin 5: Tællere og et resumé](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-5.png)
> Dette skærmbillede kræver en kørsel med en rigtig gym-konto. Vedligeholderen tilføjer det med `--screenshot-dir` (se [Kom i gang](Getting-Started-da)); indtil da mangler filen `docs/images/step-5.png`, og billedet ovenfor vises ikke.
[Forrige: 4](Step-4-da) · [Forside](Home-da) · [Næste: 6](Step-6-da)
-2
View File
@@ -14,6 +14,4 @@ Anda belajar menyimpan pembilang semasa gelung berjalan dan mencetak laporan: BO
![Langkah 5: Pembilang dan ringkasan](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-5.png)
> Tangkapan skrin ini memerlukan larian dengan akaun gim sebenar. Penyelenggara menambahnya dengan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)); sehingga itu fail `docs/images/step-5.png` tiada dan imej di atas tidak dipaparkan.
[Sebelumnya: 4](Step-4-ms) · [Utama](Home-ms) · [Seterusnya: 6](Step-6-ms)
-2
View File
@@ -14,6 +14,4 @@ You learn to keep counters while a loop runs and to print a report: the BOOKING
![Step 5: Counters and a summary](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-5.png)
> This screenshot needs a run with a real gym account. The maintainer adds it with `--screenshot-dir` (see [Getting started](Getting-Started)); until then the file `docs/images/step-5.png` is missing and the image above does not show.
[Previous: 4](Step-4) · [Home](Home) · [Next: 6](Step-6)
-2
View File
@@ -14,6 +14,4 @@ Du lærer at gennemløbe alle matchende elementer: hvert tirsdags- og torsdagsho
![Trin 6: Alle tirsdags- og torsdagshold](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-6.png)
> Dette skærmbillede kræver en kørsel med en rigtig gym-konto. Vedligeholderen tilføjer det med `--screenshot-dir` (se [Kom i gang](Getting-Started-da)); indtil da mangler filen `docs/images/step-6.png`, og billedet ovenfor vises ikke.
[Forrige: 5](Step-5-da) · [Forside](Home-da) · [Næste: 7](Step-7-da)
-2
View File
@@ -14,6 +14,4 @@ Anda belajar mengulang semua elemen yang sepadan: setiap kelas Selasa dan Khamis
![Langkah 6: Semua kelas Selasa dan Khamis](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-6.png)
> Tangkapan skrin ini memerlukan larian dengan akaun gim sebenar. Penyelenggara menambahnya dengan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)); sehingga itu fail `docs/images/step-6.png` tiada dan imej di atas tidak dipaparkan.
[Sebelumnya: 5](Step-5-ms) · [Utama](Home-ms) · [Seterusnya: 7](Step-7-ms)
-2
View File
@@ -14,6 +14,4 @@ You learn to loop over all matching elements: every Tuesday and Thursday 6pm cla
![Step 6: Every Tuesday and Thursday class](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-6.png)
> This screenshot needs a run with a real gym account. The maintainer adds it with `--screenshot-dir` (see [Getting started](Getting-Started)); until then the file `docs/images/step-6.png` is missing and the image above does not show.
[Previous: 5](Step-5) · [Home](Home) · [Next: 7](Step-7)
-2
View File
@@ -14,6 +14,4 @@ Du lærer at kontrollere dit eget arbejde fra en anden kilde: `get_my_bookings()
![Trin 7: Bekræft på siden My Bookings](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-7.png)
> Dette skærmbillede kræver en kørsel med en rigtig gym-konto. Vedligeholderen tilføjer det med `--screenshot-dir` (se [Kom i gang](Getting-Started-da)); indtil da mangler filen `docs/images/step-7.png`, og billedet ovenfor vises ikke.
[Forrige: 6](Step-6-da) · [Forside](Home-da) · [Næste: 8](Step-8-da)
-2
View File
@@ -14,6 +14,4 @@ Anda belajar menyemak kerja sendiri daripada sumber kedua: `get_my_bookings()` m
![Langkah 7: Sahkan di halaman My Bookings](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-7.png)
> Tangkapan skrin ini memerlukan larian dengan akaun gim sebenar. Penyelenggara menambahnya dengan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)); sehingga itu fail `docs/images/step-7.png` tiada dan imej di atas tidak dipaparkan.
[Sebelumnya: 6](Step-6-ms) · [Utama](Home-ms) · [Seterusnya: 8](Step-8-ms)
-2
View File
@@ -14,6 +14,4 @@ You learn to check your own work from a second source: `get_my_bookings()` reads
![Step 7: Verify on the My Bookings page](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-7.png)
> This screenshot needs a run with a real gym account. The maintainer adds it with `--screenshot-dir` (see [Getting started](Getting-Started)); until then the file `docs/images/step-7.png` is missing and the image above does not show.
[Previous: 6](Step-6) · [Home](Home) · [Next: 8](Step-8)
+1 -1
View File
@@ -14,6 +14,6 @@ Du lærer at datoer er den almindelige fejlkilde: sidens admin-knap "advance 3 d
![Trin 8: Kvalitetssikring med tidsrejse](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-8.png)
> Dette skærmbillede kræver en kørsel med en rigtig gym-konto. Vedligeholderen tilføjer det med `--screenshot-dir` (se [Kom i gang](Getting-Started-da)); indtil da mangler filen `docs/images/step-8.png`, og billedet ovenfor vises ikke.
> Siden My Bookings viser også bookinger fra tidligere kørsler uden for denne kørsels dage. Kontrollen sammenligner kun kørslens dage og skriver de øvrige som "Also booked (not checked)".
[Forrige: 7](Step-7-da) · [Forside](Home-da) · [Næste: 9](Step-9-da)
+1 -1
View File
@@ -14,6 +14,6 @@ Anda belajar bahawa tarikh ialah punca pepijat biasa: kawalan admin laman "advan
![Langkah 8: Jaminan kualiti dengan perjalanan masa](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-8.png)
> Tangkapan skrin ini memerlukan larian dengan akaun gim sebenar. Penyelenggara menambahnya dengan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)); sehingga itu fail `docs/images/step-8.png` tiada dan imej di atas tidak dipaparkan.
> Halaman My Bookings turut menyenaraikan tempahan daripada larian terdahulu di luar hari larian ini. Semakan hanya membandingkan hari larian dan mencetak yang lain sebagai "Also booked (not checked)".
[Sebelumnya: 7](Step-7-ms) · [Utama](Home-ms) · [Seterusnya: 9](Step-9-ms)
+1 -1
View File
@@ -14,6 +14,6 @@ You learn that dates are the usual bug: the site's admin control "advance 3 days
![Step 8: Time travel quality assurance](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-8.png)
> This screenshot needs a run with a real gym account. The maintainer adds it with `--screenshot-dir` (see [Getting started](Getting-Started)); until then the file `docs/images/step-8.png` is missing and the image above does not show.
> The My Bookings page also lists bookings from earlier runs outside the days of this run. The check compares only the days of the run and prints the others as "Also booked (not checked)".
[Previous: 7](Step-7) · [Home](Home) · [Next: 9](Step-9)
-2
View File
@@ -14,6 +14,4 @@ Du lærer at prøve igen: `retry(func, retries=7, description=None)` gentager et
![Trin 9: Robusthed mod netværksfejl](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-9.png)
> Dette skærmbillede kræver en kørsel med en rigtig gym-konto. Vedligeholderen tilføjer det med `--screenshot-dir` (se [Kom i gang](Getting-Started-da)); indtil da mangler filen `docs/images/step-9.png`, og billedet ovenfor vises ikke.
[Forrige: 8](Step-8-da) · [Forside](Home-da)
-2
View File
@@ -14,6 +14,4 @@ Anda belajar mencuba semula: `retry(func, retries=7, description=None)` mengulan
![Langkah 9: Ketahanan terhadap kegagalan rangkaian](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-9.png)
> Tangkapan skrin ini memerlukan larian dengan akaun gim sebenar. Penyelenggara menambahnya dengan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)); sehingga itu fail `docs/images/step-9.png` tiada dan imej di atas tidak dipaparkan.
[Sebelumnya: 8](Step-8-ms) · [Utama](Home-ms)
-2
View File
@@ -14,6 +14,4 @@ You learn to retry: `retry(func, retries=7, description=None)` repeats a call th
![Step 9: Resilience against network failures](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/raw/branch/main/docs/images/step-9.png)
> This screenshot needs a run with a real gym account. The maintainer adds it with `--screenshot-dir` (see [Getting started](Getting-Started)); until then the file `docs/images/step-9.png` is missing and the image above does not show.
[Previous: 8](Step-8) · [Home](Home)
+23
View File
@@ -228,3 +228,26 @@ VERIFY_SUCCESS: Final[str] = "✅ SUCCESS: All bookings verified!"
VERIFY_MISMATCH: Final[str] = "❌ MISMATCH: Missing {count} bookings"
VERIFY_LINE_MISSING: Final[str] = " ✗ Missing: {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
+60 -6
View File
@@ -5,9 +5,11 @@
from collections import Counter
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import date
from snack_and_lift import constants
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
_WAITLISTED_OUTCOMES = (Outcome.NEW_WAITLIST, Outcome.ALREADY_WAITLISTED)
@@ -20,8 +22,11 @@ _Key = tuple[str, str, bool]
class VerificationResult:
"""@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
waitlist entry; `missing` and `unexpected` also say the day (`Spin Class on Tue, Aug 12`).
`verified`, `missing`, `unexpected` and `also_booked` hold display names, with the waitlist
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
@@ -29,6 +34,7 @@ class VerificationResult:
verified: list[str]
missing: list[str]
unexpected: list[str]
also_booked: list[str]
@property
def is_success(self) -> bool:
@@ -57,6 +63,46 @@ def _describe(key: _Key) -> str:
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(
results: Sequence[ClassResult], entries: Sequence[BookingEntry]
) -> VerificationResult:
@@ -64,11 +110,16 @@ def build_verification(
@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.
@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.
"""
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] = []
missing: list[str] = []
for key in expected:
@@ -78,7 +129,9 @@ def build_verification(
else:
missing.append(_describe(key))
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:
@@ -99,10 +152,11 @@ def format_verification(result: VerificationResult) -> str:
lines.append(constants.VERIFY_RESULT_TITLE)
lines.append(_count_text(constants.VERIFY_EXPECTED, result.expected))
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:
lines.append(constants.VERIFY_SUCCESS)
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_UNEXPECTED.format(entry=text) for text in result.unexpected)
return "\n".join(lines)
+13 -2
View File
@@ -10,7 +10,8 @@
</div>
<script>
// 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 state = params.get("state");
// ?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));
// 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;
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 content = document.getElementById("content");
setTimeout(function () {
@@ -30,7 +39,8 @@
} else if (state === "empty") {
content.innerHTML = '<div id="empty-bookings-state">You have no bookings yet</div>';
} 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";
content.innerHTML =
'<div id="confirmed-bookings-section">' +
@@ -40,6 +50,7 @@
'<div id="booking-card-12" data-booking-status="confirmed">' +
'<h3 id="booking-class-name-12">Yoga Class</h3>' +
'<p><strong>When:</strong> Wed, Aug 13, 7:00 AM</p></div>' +
extra +
'</div>' +
'<div id="waitlist-section">' +
'<div id="waitlist-card-21" data-booking-status="waitlisted">' +
+14
View File
@@ -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(
open_my_bookings_page: Callable[[str], SeleniumGym],
) -> None:
@@ -53,6 +53,16 @@ def test_verify_bookings_ignores_entries_that_are_not_target_classes() -> None:
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:
gym = FakeGym(bookings=[make_entry("Wed, Aug 13, 7:00 AM")])
results = [ClassResult(make_card("Tue, Aug 12"), Outcome.NEW_BOOKING)]
+15
View File
@@ -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
@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")
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")])
+146 -6
View File
@@ -2,9 +2,11 @@
@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_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.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")
def test_unexpected_entry_gives_a_negative_missing_count() -> None:
def test_unexpected_entry_inside_the_window_is_a_mismatch() -> None:
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)
assert result.unexpected == ["Spin Class on Thu, Aug 14"]
assert "❌ MISMATCH: Missing -1 bookings" in text
assert text.endswith(" ? Unexpected: Spin Class on Thu, Aug 14")
assert not result.is_success
assert result.unexpected == ["Yoga 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:
@@ -75,7 +215,7 @@ def test_waitlist_flag_must_match() -> None:
assert result.missing == ["Spin Class 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: