diff --git a/docs/milestones/mil-002-nato-converter.md b/docs/milestones/mil-002-nato-converter.md new file mode 100644 index 0000000..c0c4b05 --- /dev/null +++ b/docs/milestones/mil-002-nato-converter.md @@ -0,0 +1,76 @@ +# MIL-002 NATO converter + +## Metadata +| Key | Value | +| --- | --- | +| ID | MIL-002 | +| CrossReference | [BC-001] | +| Language | en | +| Domain | it | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, reviewed in RC-004 | pending | + +--- + +## Purpose + +Decide whether the console program does what the Day 30 assignment asks: read a word, print the NATO code word of each letter, and handle invalid input with exception handling (a `try`/`except` on the failed dictionary lookup) inside `generate_phonetic`, which keeps asking until the input is valid. Tested with pytest. + +## Deliverable + +The `nato_phonetic_alphabet` package with a runnable entry point, a test suite that passes, and Doxygen comments on every public item; plus the user story, use case and system sequence diagram of "spell a word". + +## Go / No-Go Criteria + +| # | Criterion (objectively checkable) | Go | No-Go | +| --- | --- | --- | --- | +| 1 | All 26 letters map to their code word and input is case-insensitive | pytest passes | Any test fails | +| 2 | A character that is not a letter raises a caught `KeyError`; the program tells the user to enter letters of the alphabet only and asks again instead of crashing | pytest covers it and passes | Missing or failing | +| 3 | The loader uses a dictionary comprehension, the converter a list comprehension, and the invalid input is handled with `try`/`except`, not with a pre-check | Code review confirms | Loops or pre-checks used instead | +| 4 | The assignment's function names are kept in the public API | Names present in the module | Names missing | +| 5 | The code review against `qc-programming-python.md` is a `Go` | `RC-*` verdict `Go` | `No-Go` | +| 6 | `doxygen Doxyfile` reports no warnings | 0 warnings | Any warning | +| 7 | Constants live in `constants.py`; `.env` is not imported by `src/` or `tests/` | Search confirms | Any hit | + +## Dependencies + +| Depends on | Reason | +| --- | --- | +| [MIL-001] | Project configuration, skeleton, CI and Doxyfile | + +## Traceability + +| Business Case objective / KPI / user story | Reference | +| --- | --- | +| O1, O3, O4 | [BC-001] | +| Spell a word | US-001, UC-001 | + +## Ownership + +| Role | Stakeholder ID (SA) | +| --- | --- | +| Owner | S01 | +| Approving reviewer | S01 | + +## Target Date + +2026-10-15 — inside the plan's window (2026-10-14 to 2026-10-16). + +## Tasks + +| # | Task | Summary | Needs its own Use Case/User Story? | Reference | +| --- | --- | --- | --- | --- | +| 1 | Model "spell a word" | Write US-001, UC-001 (brief) and the system sequence diagram for the user who types a word and receives its NATO code words, including the retry on invalid input. Domain concepts are only letter and code word, so no domain model is added. | Yes | US-001, UC-001 | +| 2 | Load the NATO alphabet | Read the CSV with the standard library and build the letter-to-code-word dictionary with a dictionary comprehension (`load_nato_dict`), taking the file name from `constants.py`. | No | | +| 3 | Convert a word to code words | Implement the assignment's `generate_phonetic`: it prompts for a word, builds the code words with a list comprehension inside a `try` block, catches the `KeyError` of a character that is not a letter, tells the user to enter letters of the alphabet only, and loops until the input is valid. The lookup and the prompt sit behind ports so the layers stay separate. | Yes | UC-001 | +| 4 | Implement the console entry point | Prompt for a word, print the code words, ask again on invalid input; runnable as `python -m nato_phonetic_alphabet` and as the `nato-phonetic-alphabet` command. | Yes | UC-001 | +| 5 | Write the pytest suite | Test all letters, mixed case, unknown characters, the loader (with a temporary CSV) and the entry point (with patched input and captured output). | No | | +| 6 | Add Doxygen comments | Document every module, function and constant with Doxygen comments and confirm the build has no warnings. | No | | + +--- + +[BC-001]: ../business-case.md +[MIL-001]: ./mil-001-repository-foundation.md diff --git a/docs/milestones/mil-003-documentation-and-publication.md b/docs/milestones/mil-003-documentation-and-publication.md new file mode 100644 index 0000000..ee06f6b --- /dev/null +++ b/docs/milestones/mil-003-documentation-and-publication.md @@ -0,0 +1,75 @@ +# MIL-003 Documentation and publication + +## Metadata +| Key | Value | +| --- | --- | +| ID | MIL-003 | +| CrossReference | [BC-001] | +| Language | en | +| Domain | it | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, adapted from the 026 NATO alphabet project | pending | + +--- + +## Purpose + +Decide whether S02 and S03 can understand, set up and run the project from its README and wiki without help, in English, Danish and Bahasa Malaysia. + +## Deliverable + +The `README.md` built from the framework template with screenshots, and a wiki in English, Danish and Bahasa Malaysia with the same pages and screenshots, published on the git host. + +## Go / No-Go Criteria + +| # | Criterion (objectively checkable) | Go | No-Go | +| --- | --- | --- | --- | +| 1 | `check-readme.sh` reports no finding for the README | Exit code 0 | Any finding | +| 2 | The README steps (create `.venv`, upgrade pip, install, run, test) work on a fresh clone on Windows PowerShell | The run produces the expected output | A step fails | +| 3 | Every wiki page exists in en, da and ms | `check-languages.sh` and a file listing agree | A page is missing | +| 4 | The README and each wiki language show a screenshot | Image files exist and are linked | Missing | +| 5 | The wiki is published and readable on the host | Page requests return success | Any page not found | +| 6 | `doxygen Doxyfile` builds and the README says how | Exit code 0 and the section exists | Missing | + +## Dependencies + +| Depends on | Reason | +| --- | --- | +| [MIL-002] | The documentation describes the finished program | + +## Traceability + +| Business Case objective / KPI / user story | Reference | +| --- | --- | +| O2, O4, O5 | [BC-001] | + +## Ownership + +| Role | Stakeholder ID (SA) | +| --- | --- | +| Owner | S01 | +| Approving reviewer | S01 | + +## Target Date + +2026-10-16 — the end of the plan's window. + +## Tasks + +| # | Task | Summary | Needs its own Use Case/User Story? | Reference | +| --- | --- | --- | --- | --- | +| 1 | Capture screenshots | Run the program and render its real output as PNG images (a word spelled, an invalid input retried) stored under `docs/images/` for the README and the wiki. | No | | +| 2 | Write the README | Fill the framework README template: set-up on Windows PowerShell, Linux Debian and macOS with a local `.venv` and `python -m pip install --upgrade pip`, run, tests, continuous integration, Doxygen build, layout and licence. | No | | +| 3 | Write the English wiki | Pages Home, Install, Usage and Development in English with screenshots, in the layout of the framework wiki. | No | | +| 4 | Write the Danish wiki | The same pages in Danish with the `-da` suffix. | No | | +| 5 | Write the Bahasa Malaysia wiki | The same pages in Bahasa Malaysia with the `-ms` suffix. | No | | +| 6 | Publish the wiki | Push the pages to the repository's wiki on the git host and check that each page is readable. | No | | +| 7 | Verify the set-up instructions | Follow the README from a fresh clone in a new `.venv`, run the program and the tests, and record the result in the review. | No | | + +--- + +[BC-001]: ../business-case.md +[MIL-002]: ./mil-002-nato-converter.md diff --git a/docs/sqa/reviews/rc-004-mil-002.md b/docs/sqa/reviews/rc-004-mil-002.md new file mode 100644 index 0000000..bc8fdfd --- /dev/null +++ b/docs/sqa/reviews/rc-004-mil-002.md @@ -0,0 +1,65 @@ +# SQA Review Record RC-004: MIL-002 + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-004 | +| CrossReference | [MIL-002], [QC-MIL-001], [QC-LANG-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of MIL-002 | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [MIL-002] +- Checklist used: [QC-MIL-001] and [QC-LANG-001] +- Scope: full review +- Language and domain: en / it +- Language reviewer: none (the reviewer reads the language and knows the domain) +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | A concrete deliverable is defined for every gate | Pass | Deliverable section names concrete output: working console program, tests, Doxygen comments, user story and use case. | +| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Go / No-Go table has objectively checkable rows (commands and counts). | +| 3 | Dependencies on other milestones are explicitly mapped | Pass | Dependencies table is filled. | +| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Traceability cites [BC-001] objectives. | +| 5 | Milestone owner and approving reviewer are identified | Pass | Owner and approving reviewer are S01. | +| 6 | Milestone has a defined target date consistent with project constraints | Pass | Target date lies inside the Project Plan window; the Business Case sets no calendar constraint. | + +## 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 | `Language | en` and `Domain | it` are in the Metadata table. | +| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code; `it` is in the registry's domain list. | +| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English; the course title is quoted source text. Checked by the stand-in reviewer, who reads English. | +| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives IT Executive English for this type; the text matches it. | +| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | No dictionary (`DICT`) exists yet; the terms letter, code word and NATO alphabet are used consistently with no synonyms. Checked by the stand-in; open to challenge. | +| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. | +| 7 | No translated twin (`..md`) exists beside the document | Pass | No translated twin of the file exists. | +| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain. | +| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The domain is software and IT and the language English; the reviewer stood in for (S01) reads both. A person may challenge this in the run summary. | +| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out on first use or are identifiers (IDs, tool names). | + +## Overall Verdict + +Go — every Mandatory criterion of QC-MIL-001 and of QC-LANG-001 passes. The review is a stand-in review by the author's AI and is not independent; it is open to challenge in the run summary. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | S01 | n/a | + +--- + +[MIL-002]: ../../milestones/mil-002-nato-converter.md +[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md +[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md diff --git a/docs/sqa/reviews/rc-010-ucd-001.md b/docs/sqa/reviews/rc-010-ucd-001.md new file mode 100644 index 0000000..0854145 --- /dev/null +++ b/docs/sqa/reviews/rc-010-ucd-001.md @@ -0,0 +1,66 @@ +# SQA Review Record RC-010: UCD-001 + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-010 | +| CrossReference | [UCD-001], [QC-UCD-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of UCD-001 | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [UCD-001] +- Checklist used: [QC-UCD-001] +- Scope: full review +- Language and domain: en / it +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Actors are defined with correct UML stereotypes (e.g. `<>`, `<>`) | Pass | Actor `<>` and system `<>` in the diagram and actor table. | +| 2 | System boundary is clearly drawn and labeled | Pass | The rectangle is labelled "NATO alphabet tool". | +| 3 | Include/extend relationships are used correctly per UML 2.5.1, not as generic "uses" arrows | Pass | No include or extend is used; the Relationships table says so. | +| 4 | Every actor participates in at least one use case (no orphan actors) | Pass | The User takes part in Spell a Word. | +| 5 | Diagram is traceable to a documented stakeholder need | Pass | The actor table cites S01 and S02 from [SA-001]. | +| 6 | Use case names are verb phrases describing actor goals, not internal system operations | Pass | "Spell a Word" is a verb phrase describing the actor's goal. | +| 7 | Diagram is free of implementation detail (e.g. UI widgets, database tables) | Pass | No interface or storage detail. | +| 8 | Actor and use case naming is consistent with corresponding Use Case and User Story documents | Pass | Actor `User` and use case `Spell a Word` are named alike in US-001 and UC-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 | `Language | en` and `Domain | it` are in the Metadata table. | +| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code; `it` is in the registry's domain list. | +| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English. Checked by the stand-in reviewer, who reads English. | +| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives IT Professional English for this type; the text matches it. | +| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | No dictionary (`DICT`) exists; the terms user, word, letter and code word are used consistently with no synonyms. Checked by the stand-in; open to challenge. | +| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. | +| 7 | No translated twin (`..md`) exists beside the document | Pass | No translated twin exists. | +| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain. | +| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | Software and IT domain, English language; the reviewer stood in for (S01) reads both. A person may challenge this in the run summary. | +| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out on first use or are identifiers. | + +## Overall Verdict + +Go — every Mandatory criterion of QC-UCD-001 and QC-LANG-001 passes. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | S01 | n/a | + +--- + +[UCD-001]: ../../use-case-diagram.md +[QC-UCD-001]: ../../../framework/qc/artifacts/qc-use-case-diagram.md diff --git a/docs/sqa/reviews/rc-011-us-001.md b/docs/sqa/reviews/rc-011-us-001.md new file mode 100644 index 0000000..fbee282 --- /dev/null +++ b/docs/sqa/reviews/rc-011-us-001.md @@ -0,0 +1,65 @@ +# SQA Review Record RC-011: US-001 + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-011 | +| CrossReference | [US-001], [QC-US-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of US-001 | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [US-001] +- Checklist used: [QC-US-001] +- Scope: full review +- Language and domain: en / it +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Follows INVEST criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable) | Pass | INVEST check line in the document; one story, one console interaction. | +| 2 | Written in "As a / I want / So that" form | Pass | As a / I want / so that. | +| 3 | Clear, testable acceptance criteria are included | Pass | Four Given/When/Then criteria, each a pytest case. | +| 4 | Traceable to a use case or epic | Pass | Traces to [UC-001] and the epic [MIL-002]. | +| 5 | Story is sized to fit within a single iteration | Pass | Fits one iteration. | +| 6 | Story statement avoids technical implementation detail | Pass | No implementation detail. | +| 7 | Role named in the story matches an actor defined in the Use Case Diagram | Pass | Role `User` is the actor of [UCD-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 | `Language | en` and `Domain | it` are in the Metadata table. | +| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code; `it` is in the registry's domain list. | +| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English. Checked by the stand-in reviewer, who reads English. | +| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives IT Professional English for this type; the text matches it. | +| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | No dictionary (`DICT`) exists; the terms user, word, letter and code word are used consistently with no synonyms. Checked by the stand-in; open to challenge. | +| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. | +| 7 | No translated twin (`..md`) exists beside the document | Pass | No translated twin exists. | +| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain. | +| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | Software and IT domain, English language; the reviewer stood in for (S01) reads both. A person may challenge this in the run summary. | +| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out on first use or are identifiers. | + +## Overall Verdict + +Go — every Mandatory criterion of QC-US-001 and QC-LANG-001 passes. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | S01 | n/a | + +--- + +[US-001]: ../../user-stories.md +[QC-US-001]: ../../../framework/qc/artifacts/qc-user-story.md diff --git a/docs/sqa/reviews/rc-012-uc-001.md b/docs/sqa/reviews/rc-012-uc-001.md new file mode 100644 index 0000000..16f52a7 --- /dev/null +++ b/docs/sqa/reviews/rc-012-uc-001.md @@ -0,0 +1,70 @@ +# SQA Review Record RC-012: UC-001 + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-012 | +| CrossReference | [UC-001], [QC-UC-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of UC-001 | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [UC-001] +- Checklist used: [QC-UC-001] +- Scope: full review +- Language and domain: en / it +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Consists of a single, concise paragraph summarizing only the primary success scenario | N-A | Format is Fully Dressed, not Brief. | +| 2 | Written as an informal multi-paragraph narrative; may mention some alternate flows without formal structure | N-A | Format is Fully Dressed, not Casual. | +| 3 | All standard sections are present: actors, preconditions, postconditions, main success scenario, alternative/exception flows | Pass | Scope, level, actor, stakeholders, pre- and postconditions, main scenario, extensions, rules and open issues are present. | +| 4 | Preconditions and postconditions are explicitly defined | Pass | Preconditions and postconditions are stated. | +| 5 | Primary actor is explicitly stated | Pass | Primary actor: User. | +| 6 | Stakeholders and their interests are stated | Pass | S01 and S02 with their interests. | +| 7 | Main success scenario is written as clear, numbered steps | Pass | Four numbered steps. | +| 8 | Alternative/exception flows correctly reference `<>`/`<>` use cases where relevant, per UML 2.5.1 | N-A | No other use case exists, so no include or extend is relevant; the alternative flow 3a is a plain extension. | +| 9 | Explicit business rules are captured per step where applicable, rather than embedded loosely in narrative text | Pass | A Special Requirements table gives a rule per step. | +| 10 | Naming of actors and use case title is consistent with the corresponding Use Case Diagram and User Stories | Pass | Actor and title match [UCD-001] and [US-001]. | +| 11 | Scope/level (e.g. summary, user-goal, subfunction) is explicitly stated | Pass | Level: user-goal; scope: NATO alphabet tool. | +| 12 | Use case is written from the actor's goal perspective, free of UI or implementation detail | Pass | Written from the goal of the User, with no interface detail. | + +## 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 | `Language | en` and `Domain | it` are in the Metadata table. | +| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code; `it` is in the registry's domain list. | +| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English. Checked by the stand-in reviewer, who reads English. | +| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives IT Professional English for this type; the text matches it. | +| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | No dictionary (`DICT`) exists; the terms user, word, letter and code word are used consistently with no synonyms. Checked by the stand-in; open to challenge. | +| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. | +| 7 | No translated twin (`..md`) exists beside the document | Pass | No translated twin exists. | +| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain. | +| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | Software and IT domain, English language; the reviewer stood in for (S01) reads both. A person may challenge this in the run summary. | +| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out on first use or are identifiers. | + +## Overall Verdict + +Go — every applicable Mandatory criterion passes. The task summary of MIL-002 said "brief"; the use case is Fully Dressed so that the system sequence diagram can follow its steps one by one. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | S01 | n/a | + +--- + +[UC-001]: ../../uc-001/uc.md +[QC-UC-001]: ../../../framework/qc/artifacts/qc-use-case.md diff --git a/docs/sqa/reviews/rc-013-ssd-001.md b/docs/sqa/reviews/rc-013-ssd-001.md new file mode 100644 index 0000000..2f22009 --- /dev/null +++ b/docs/sqa/reviews/rc-013-ssd-001.md @@ -0,0 +1,65 @@ +# SQA Review Record RC-013: SSD-001 + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-013 | +| CrossReference | [SSD-001], [QC-SSD-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of SSD-001 | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [SSD-001] +- Checklist used: [QC-SSD-001] +- Scope: full review +- Language and domain: en / it +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Input/output messages match the corresponding Use Case's main success scenario step-for-step | Pass | Messages requestWord and spellWord cover steps 1 to 4; the table maps each message to its use case steps. | +| 2 | Actor and System are treated strictly as black boxes (system shown as `:System`) | Pass | The system is `:System`; no internal objects. | +| 3 | Object creation/destruction of the System instance handled explicitly where relevant | Pass | Lifecycle notes state one session per start. | +| 4 | Return values are shown for operations that produce one, using dashed return arrows | Pass | Dashed return arrows for prompt and codeWords. | +| 5 | Alternate/exceptional flows are represented separately (or explicitly out of scope noted) | Pass | The alternate flow 3a has its own diagram. | +| 6 | Message names are verb phrases consistent with the use case's system responsibilities | Pass | requestWord and spellWord are verb phrases. | +| 7 | Diagram references the specific Use Case (name and ID) it depicts | Pass | The Source Use Case section names Spell a Word and [UC-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 | `Language | en` and `Domain | it` are in the Metadata table. | +| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code; `it` is in the registry's domain list. | +| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English. Checked by the stand-in reviewer, who reads English. | +| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives IT Professional English for this type; the text matches it. | +| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | No dictionary (`DICT`) exists; the terms user, word, letter and code word are used consistently with no synonyms. Checked by the stand-in; open to challenge. | +| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. | +| 7 | No translated twin (`..md`) exists beside the document | Pass | No translated twin exists. | +| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain. | +| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | Software and IT domain, English language; the reviewer stood in for (S01) reads both. A person may challenge this in the run summary. | +| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out on first use or are identifiers. | + +## Overall Verdict + +Go — every Mandatory criterion of QC-SSD-001 and QC-LANG-001 passes. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | S01 | n/a | + +--- + +[SSD-001]: ../../uc-001/ssd.md +[QC-SSD-001]: ../../../framework/qc/artifacts/qc-ssd.md diff --git a/docs/sqa/reviews/rc-014-python-converter.md b/docs/sqa/reviews/rc-014-python-converter.md new file mode 100644 index 0000000..0ba3366 --- /dev/null +++ b/docs/sqa/reviews/rc-014-python-converter.md @@ -0,0 +1,59 @@ +# SQA Review Record RC-014: PY-002 + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-014 | +| CrossReference | [PY-002], [QC-PY-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of PY-002 | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: [PY-002] +- Checklist used: [QC-PY-001] +- Scope: full review +- Language and domain: n/a (technical type) +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | ruff rule set N passes; modules snake_case, classes PascalCase, `InvalidWordError` ends in Error; ruff rule set N passes. | +| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose: `generate_phonetic`, `spell_word`, `load_nato_dict`, `TerminalConsole`. | +| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | ruff 0.17.0 check and format pass; no suppressions. | +| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | All signatures annotated; mypy --strict passes. | +| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `spell_word` catches the specific `KeyError` and raises `InvalidWordError` `from` it; `generate_phonetic` catches only `InvalidWordError`; the console adapter maps only EOFError and KeyboardInterrupt to `InputClosedError`; no bare except. | +| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults; `path` defaults to None. | +| 7 | Files, locks and connections are managed with context managers | Pass | The CSV is read inside a `with` block. | +| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Every module, class, function and constant has a Doxygen docstring; doxygen reports 0 warnings. | +| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | `print` and `input` are confined to `adapters/console.py` because the console is the program's output; there is no logging and no secret or personal data. | +| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No design class diagram exists; MIL-002 plans none (the SSD and use case are the design artifacts). Classes follow the ports in `application/ports.py`. | +| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 49 tests named for behaviour; `tmp_path` and fakes; no network; order independent. | +| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | mypy --strict: no issues in 18 files; no `Any`. | +| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | No runtime dependencies; development tools pinned with `==`. | +| 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 literal is defined once in `constants.py`; the letter mapping is defined once in the CSV. | +| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | import-linter contract `Dependencies point inward` is kept; no cycles. | +| 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 | `WordConsole` is a narrow Protocol; `generate_phonetic` depends on it and `TerminalConsole` implements it. | + +## Overall Verdict + +Go — every Mandatory criterion of QC-PY-001 passes or is not applicable (criterion 10: no design class diagram is planned). `generate_phonetic` takes the alphabet and the console as parameters because the application layer may not read files or call `input`; the assignment's name is kept. The `try`/`except KeyError` is in the domain function `spell_word` and the retry loop is in `generate_phonetic`. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | S01 | n/a | + +--- + +[PY-002]: ../../../src/nato_phonetic_alphabet/main.py +[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md diff --git a/docs/uc-001/ssd.md b/docs/uc-001/ssd.md new file mode 100644 index 0000000..b0ccaa0 --- /dev/null +++ b/docs/uc-001/ssd.md @@ -0,0 +1,60 @@ +# SSD-001 Spell a Word + +## Metadata +| Key | Value | +| --- | --- | +| ID | SSD-001 | +| CrossReference | [UC-001] | +| Language | en | +| Domain | it | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, reviewed in RC-013 | pending | + +--- + +## Source Use Case + +Spell a Word ([UC-001]) — scenario: main success scenario. The alternate flow 3a (invalid character) is drawn separately below. + +## Diagram + +```plantuml +@startuml +actor User as U +participant ":System" as S +U -> S : requestWord() +S --> U : prompt +U -> S : spellWord(word) +S --> U : codeWords +@enduml +``` + +Alternate flow 3a, a word with a character that is not a letter: + +```plantuml +@startuml +actor User as U +participant ":System" as S +U -> S : spellWord(word) +S --> U : rejection(only letters allowed) +S --> U : prompt +@enduml +``` + +## System Operations + +| Step | Message | Parameters | Return | Use case step | +| --- | --- | --- | --- | --- | +| 1 | requestWord | none | prompt asking for a word | 1, 2 | +| 2 | spellWord | word | the list of code words, or a rejection for a non-letter | 3, 4 and 3a | + +## Lifecycle Notes + +The tool runs one session per start; it keeps no state between words. The data is loaded once when the session starts. + +--- + +[UC-001]: ./uc.md diff --git a/docs/uc-001/uc.md b/docs/uc-001/uc.md new file mode 100644 index 0000000..8a1f6c4 --- /dev/null +++ b/docs/uc-001/uc.md @@ -0,0 +1,59 @@ +# UC-001 Spell a Word + +## Metadata +| Key | Value | +| --- | --- | +| ID | UC-001 | +| CrossReference | [UCD-001], [US-001], [SA-001] | +| Language | en | +| Domain | it | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, reviewed in RC-012 | pending | + +--- + +**Format:** Fully Dressed + +## Fully Dressed + +- **Scope:** NATO alphabet tool +- **Level:** user-goal +- **Primary Actor:** User, as named in [UCD-001] +- **Stakeholders and Interests:** + - S01 — a correct result for every letter, kept simple + - S02 — a readable behaviour that can be compared with their own solution +- **Preconditions:** the NATO alphabet data is available to the tool +- **Postconditions (success guarantee):** the User has seen the NATO code word of each letter of the word, in the order of the letters + +### Main Success Scenario + +1. The User starts the tool. +2. The tool asks for a word. +3. The User enters a word made of letters. +4. The tool shows the NATO code word of each letter, in order. + +### Extensions (Alternative / Exception Flows) + +- 3a. The word contains a character that is not a letter: + 1. The tool catches the failed lookup (exception handling), tells the User to enter only letters of the alphabet and does not crash. + 2. The tool asks for a word again (back to step 2). + +### Special Requirements / Business Rules + +| Step | Rule | +| --- | --- | +| 3 | Upper-case and lower-case letters give the same result | +| 4 | Each letter A to Z maps to exactly one code word from the supplied alphabet | + +### Open Issues + +- None. + +--- + +[UCD-001]: ../use-case-diagram.md +[US-001]: ../user-stories.md +[SA-001]: ../stakeholder-analysis.md diff --git a/docs/use-case-diagram.md b/docs/use-case-diagram.md index c7d338b..7f7f3f5 100644 --- a/docs/use-case-diagram.md +++ b/docs/use-case-diagram.md @@ -11,7 +11,8 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, adapted from the 026 NATO alphabet project | pending | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, adapted from the 026 NATO alphabet project | [1da8981] | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Reviewed in RC-010 | pending | --- @@ -56,3 +57,4 @@ U --> UC1 [BC-001]: ./business-case.md [US-001]: ./user-stories.md [UC-001]: ./uc-001/uc.md +[1da8981]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-nato-phonetic-alphabet/commit/1da8981 diff --git a/docs/user-stories.md b/docs/user-stories.md index 891a11d..e465747 100644 --- a/docs/user-stories.md +++ b/docs/user-stories.md @@ -11,7 +11,8 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, adapted from the 026 NATO alphabet project | pending | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, adapted from the 026 NATO alphabet project | [1da8981] | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Reviewed in RC-011 | pending | --- @@ -46,3 +47,4 @@ Independent (no other story is needed), Negotiable (wording of the messages is o [BC-001]: ./business-case.md [MIL-002]: ./milestones/mil-002-nato-converter.md [UC-001]: ./uc-001/uc.md +[1da8981]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-nato-phonetic-alphabet/commit/1da8981 diff --git a/src/nato_phonetic_alphabet/__main__.py b/src/nato_phonetic_alphabet/__main__.py new file mode 100644 index 0000000..42144d8 --- /dev/null +++ b/src/nato_phonetic_alphabet/__main__.py @@ -0,0 +1,5 @@ +"""@brief Entry point of the command `python -m nato_phonetic_alphabet`.""" + +from nato_phonetic_alphabet.main import main + +raise SystemExit(main()) diff --git a/src/nato_phonetic_alphabet/adapters/__init__.py b/src/nato_phonetic_alphabet/adapters/__init__.py new file mode 100644 index 0000000..4460049 --- /dev/null +++ b/src/nato_phonetic_alphabet/adapters/__init__.py @@ -0,0 +1 @@ +"""@brief Adapters of the NATO alphabet tool: data file and console.""" diff --git a/src/nato_phonetic_alphabet/adapters/console.py b/src/nato_phonetic_alphabet/adapters/console.py new file mode 100644 index 0000000..c368c4c --- /dev/null +++ b/src/nato_phonetic_alphabet/adapters/console.py @@ -0,0 +1,31 @@ +"""@brief Adapter that talks to the user through the terminal.""" + +from nato_phonetic_alphabet import constants +from nato_phonetic_alphabet.application.ports import InputClosedError + + +class TerminalConsole: + """@brief WordConsole port implemented on standard input and output.""" + + def ask_word(self) -> str: + """@brief Prompt for a word. + + @return The word as typed. + @throws InputClosedError Input ended or the user interrupted it. + """ + try: + return input(constants.PROMPT_TEXT) + except (EOFError, KeyboardInterrupt) as error: + raise InputClosedError from error + + def show_invalid(self) -> None: + """@brief Print that the word was rejected.""" + print(constants.INVALID_INPUT_TEXT) + + def show_code_words(self, code_words: list[str]) -> None: + """@brief Print the result. + + @param code_words The code words to print. + """ + print(constants.RESULT_TEXT) + print(*code_words, sep="\n") diff --git a/src/nato_phonetic_alphabet/adapters/csv_alphabet.py b/src/nato_phonetic_alphabet/adapters/csv_alphabet.py new file mode 100644 index 0000000..f7ebf6b --- /dev/null +++ b/src/nato_phonetic_alphabet/adapters/csv_alphabet.py @@ -0,0 +1,33 @@ +"""@brief Adapter that reads the NATO alphabet from a CSV file.""" + +import csv +from importlib import resources +from importlib.resources.abc import Traversable + +from nato_phonetic_alphabet import constants + + +def packaged_data_file() -> Traversable: + """@brief Locate the data file shipped with the package. + + @return The packaged CSV file. + """ + return ( + resources.files(constants.DATA_PACKAGE) + / constants.DATA_DIRECTORY + / constants.DATA_FILE_NAME + ) + + +def load_nato_dict(path: Traversable | None = None) -> dict[str, str]: + """@brief Read the NATO alphabet from a CSV file. + + @param path The CSV file; the packaged file when omitted. + @return Code word per upper-case letter. + """ + source = path if path is not None else packaged_data_file() + with source.open("r", encoding=constants.DATA_ENCODING) as stream: + return { + row[constants.LETTER_COLUMN].upper(): row[constants.CODE_COLUMN] + for row in csv.DictReader(stream) + } diff --git a/src/nato_phonetic_alphabet/application/__init__.py b/src/nato_phonetic_alphabet/application/__init__.py new file mode 100644 index 0000000..1e78bbb --- /dev/null +++ b/src/nato_phonetic_alphabet/application/__init__.py @@ -0,0 +1 @@ +"""@brief Use case and ports of the NATO alphabet tool.""" diff --git a/src/nato_phonetic_alphabet/application/phonetic.py b/src/nato_phonetic_alphabet/application/phonetic.py new file mode 100644 index 0000000..c9365c5 --- /dev/null +++ b/src/nato_phonetic_alphabet/application/phonetic.py @@ -0,0 +1,26 @@ +"""@brief Use case: ask for a word until it can be spelled.""" + +from collections.abc import Mapping + +from nato_phonetic_alphabet.application.ports import WordConsole +from nato_phonetic_alphabet.domain.spelling import InvalidWordError, spell_word + + +def generate_phonetic(alphabet: Mapping[str, str], console: WordConsole) -> list[str]: + """@brief Ask for a word until it is valid, then return its code words. + + This is the assignment's `generate_phonetic`: it wraps the input and the + error handling. An invalid word is reported to the user and asked for + again, so the function returns only for a valid word. + + @param alphabet Code word per upper-case letter. + @param console Conversation with the user. + @return The code words of the word the user typed. + @throws InputClosedError The user can give no more input. + """ + while True: + word = console.ask_word() + try: + return spell_word(word, alphabet) + except InvalidWordError: + console.show_invalid() diff --git a/src/nato_phonetic_alphabet/application/ports.py b/src/nato_phonetic_alphabet/application/ports.py new file mode 100644 index 0000000..7a7eca9 --- /dev/null +++ b/src/nato_phonetic_alphabet/application/ports.py @@ -0,0 +1,30 @@ +"""@brief Ports the use case needs from the outside world.""" + +from typing import Protocol + + +class InputClosedError(Exception): + """@brief Raised when the user can give no more input (end of input, abort).""" + + +class WordConsole(Protocol): + """@brief Port for the conversation with the user.""" + + def ask_word(self) -> str: + """@brief Ask the user for a word. + + @return The word as typed. + @throws InputClosedError The user can give no more input. + """ + ... + + def show_invalid(self) -> None: + """@brief Tell the user that the word was rejected.""" + ... + + def show_code_words(self, code_words: list[str]) -> None: + """@brief Show the result to the user. + + @param code_words The code words to show. + """ + ... diff --git a/src/nato_phonetic_alphabet/domain/__init__.py b/src/nato_phonetic_alphabet/domain/__init__.py new file mode 100644 index 0000000..5e9796f --- /dev/null +++ b/src/nato_phonetic_alphabet/domain/__init__.py @@ -0,0 +1 @@ +"""@brief Business rules of the NATO alphabet tool, free of input and output.""" diff --git a/src/nato_phonetic_alphabet/domain/spelling.py b/src/nato_phonetic_alphabet/domain/spelling.py new file mode 100644 index 0000000..1d72875 --- /dev/null +++ b/src/nato_phonetic_alphabet/domain/spelling.py @@ -0,0 +1,33 @@ +"""@brief Rule that spells a word with the NATO phonetic alphabet.""" + +from collections.abc import Mapping + + +class InvalidWordError(ValueError): + """@brief Raised when a word cannot be spelled. + + A word is invalid when it is empty or holds a character that is not a + letter A to Z. + """ + + +def spell_word(word: str, alphabet: Mapping[str, str]) -> list[str]: + """@brief Spell a word with the code word of each of its letters. + + The list comprehension looks every letter up in the alphabet. A character + that is not in the alphabet makes the lookup fail with a `KeyError`, which + is caught here and turned into an `InvalidWordError`. The match is + case-insensitive. + + @param word The word to spell. + @param alphabet Code word per upper-case letter. + @return The code words, one per letter of the word, in order. + @throws InvalidWordError The word is empty or holds a character that is + not a letter of the alphabet. + """ + if not word: + raise InvalidWordError("The word is empty.") + try: + return [alphabet[letter.upper()] for letter in word] + except KeyError as error: + raise InvalidWordError(f"Not a letter: {error.args[0]!r}") from error diff --git a/src/nato_phonetic_alphabet/main.py b/src/nato_phonetic_alphabet/main.py new file mode 100644 index 0000000..c9cddcd --- /dev/null +++ b/src/nato_phonetic_alphabet/main.py @@ -0,0 +1,32 @@ +"""@brief Composition root: wires the adapters into the use case.""" + +from nato_phonetic_alphabet import constants +from nato_phonetic_alphabet.adapters.console import TerminalConsole +from nato_phonetic_alphabet.adapters.csv_alphabet import load_nato_dict +from nato_phonetic_alphabet.application.phonetic import generate_phonetic +from nato_phonetic_alphabet.application.ports import InputClosedError +from nato_phonetic_alphabet.domain.spelling import InvalidWordError, spell_word + +__all__ = [ + "InvalidWordError", + "generate_phonetic", + "load_nato_dict", + "main", + "spell_word", +] + + +def main() -> int: + """@brief Run the interactive tool. + + Ends without a result when the user can give no more input. + + @return The exit status of the program. + """ + console = TerminalConsole() + try: + code_words = generate_phonetic(load_nato_dict(), console) + except InputClosedError: + return constants.EXIT_OK + console.show_code_words(code_words) + return constants.EXIT_OK diff --git a/tests/adapters/test_console.py b/tests/adapters/test_console.py new file mode 100644 index 0000000..e9fc173 --- /dev/null +++ b/tests/adapters/test_console.py @@ -0,0 +1,71 @@ +import pytest + +from nato_phonetic_alphabet import constants +from nato_phonetic_alphabet.adapters.console import TerminalConsole +from nato_phonetic_alphabet.application.ports import InputClosedError +from nato_phonetic_alphabet.main import main + + +def _feed(monkeypatch: pytest.MonkeyPatch, answers: list[str]) -> list[str]: + prompts: list[str] = [] + + def fake_input(prompt: str = "") -> str: + prompts.append(prompt) + if not answers: + raise EOFError + return answers.pop(0) + + monkeypatch.setattr("builtins.input", fake_input) + return prompts + + +def test_main_prints_code_words_for_valid_word( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + _feed(monkeypatch, ["Hello"]) + + status = main() + + lines = capsys.readouterr().out.splitlines() + assert status == constants.EXIT_OK + assert lines == [constants.RESULT_TEXT, "Hotel", "Echo", "Lima", "Lima", "Oscar"] + + +def test_main_asks_again_after_invalid_word( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + prompts = _feed(monkeypatch, ["h3llo", "Hi"]) + + main() + + lines = capsys.readouterr().out.splitlines() + assert prompts == [constants.PROMPT_TEXT, constants.PROMPT_TEXT] + assert lines == [ + constants.INVALID_INPUT_TEXT, + constants.RESULT_TEXT, + "Hotel", + "India", + ] + + +def test_main_ends_cleanly_on_end_of_input( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + _feed(monkeypatch, []) + + status = main() + + assert status == constants.EXIT_OK + assert capsys.readouterr().out == "" + + +def test_terminal_console_raises_input_closed_on_interrupt( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def interrupted(prompt: str = "") -> str: + raise KeyboardInterrupt + + monkeypatch.setattr("builtins.input", interrupted) + + with pytest.raises(InputClosedError): + TerminalConsole().ask_word() diff --git a/tests/adapters/test_csv_alphabet.py b/tests/adapters/test_csv_alphabet.py new file mode 100644 index 0000000..ccb0f2b --- /dev/null +++ b/tests/adapters/test_csv_alphabet.py @@ -0,0 +1,19 @@ +from pathlib import Path + +from nato_phonetic_alphabet.adapters.csv_alphabet import load_nato_dict + +EXPECTED_ENTRIES = 26 + + +def test_load_nato_dict_reads_temporary_csv(tmp_path: Path) -> None: + data_file = tmp_path / "alphabet.csv" + data_file.write_text("letter,code\na,Alfa\nB,Bravo\n", encoding="utf-8") + + assert load_nato_dict(data_file) == {"A": "Alfa", "B": "Bravo"} + + +def test_load_nato_dict_builds_all_entries_from_packaged_file() -> None: + alphabet = load_nato_dict() + + assert len(alphabet) == EXPECTED_ENTRIES + assert alphabet["Z"] == "Zulu" diff --git a/tests/application/test_phonetic.py b/tests/application/test_phonetic.py new file mode 100644 index 0000000..4a0c5da --- /dev/null +++ b/tests/application/test_phonetic.py @@ -0,0 +1,49 @@ +import pytest + +from nato_phonetic_alphabet.application.phonetic import generate_phonetic +from nato_phonetic_alphabet.application.ports import InputClosedError + +ALPHABET = {"A": "Alfa", "B": "Bravo"} + + +class FakeConsole: + def __init__(self, words: list[str]) -> None: + self._words = words + self.asked = 0 + self.invalid = 0 + + def ask_word(self) -> str: + self.asked += 1 + if not self._words: + raise InputClosedError + return self._words.pop(0) + + def show_invalid(self) -> None: + self.invalid += 1 + + def show_code_words(self, code_words: list[str]) -> None: + raise AssertionError("generate_phonetic must not print the result") + + +def test_generate_phonetic_returns_code_words_for_valid_word() -> None: + console = FakeConsole(["ab"]) + + assert generate_phonetic(ALPHABET, console) == ["Alfa", "Bravo"] + assert console.asked == 1 + assert console.invalid == 0 + + +def test_generate_phonetic_asks_again_after_invalid_words() -> None: + console = FakeConsole(["a1", "", "b"]) + + assert generate_phonetic(ALPHABET, console) == ["Bravo"] + assert console.invalid == 2 + assert console.asked == 3 + + +def test_generate_phonetic_lets_closed_input_escape() -> None: + console = FakeConsole(["x"]) + + with pytest.raises(InputClosedError): + generate_phonetic(ALPHABET, console) + assert console.invalid == 1 diff --git a/tests/domain/test_spelling.py b/tests/domain/test_spelling.py new file mode 100644 index 0000000..2a8d1f4 --- /dev/null +++ b/tests/domain/test_spelling.py @@ -0,0 +1,43 @@ +import pytest + +from nato_phonetic_alphabet.adapters.csv_alphabet import load_nato_dict +from nato_phonetic_alphabet.domain.spelling import InvalidWordError, spell_word + +ALPHABET = load_nato_dict() +LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + +@pytest.mark.parametrize("letter", list(LETTERS)) +def test_spell_word_maps_letter_to_its_code_word(letter: str) -> None: + assert spell_word(letter, ALPHABET) == [ALPHABET[letter]] + + +def test_spell_word_uses_known_code_words() -> None: + assert spell_word("ADZ", ALPHABET) == ["Alfa", "Delta", "Zulu"] + + +def test_spell_word_covers_all_26_letters() -> None: + assert len(spell_word(LETTERS, ALPHABET)) == len(LETTERS) + + +def test_spell_word_is_case_insensitive_for_mixed_case() -> None: + assert spell_word("Hello", ALPHABET) == [ + "Hotel", + "Echo", + "Lima", + "Lima", + "Oscar", + ] + + +@pytest.mark.parametrize("word", ["a1", "two words", "hi!", "", "ß", "æ"]) +def test_spell_word_raises_for_non_letter_or_empty(word: str) -> None: + with pytest.raises(InvalidWordError): + spell_word(word, ALPHABET) + + +def test_spell_word_keeps_cause_of_invalid_character() -> None: + with pytest.raises(InvalidWordError) as info: + spell_word("a1", ALPHABET) + + assert isinstance(info.value.__cause__, KeyError) diff --git a/tests/test_main_module.py b/tests/test_main_module.py new file mode 100644 index 0000000..587d110 --- /dev/null +++ b/tests/test_main_module.py @@ -0,0 +1,30 @@ +import subprocess +import sys + +from nato_phonetic_alphabet import constants + + +def _run_module(stdin_text: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, "-m", "nato_phonetic_alphabet"], + input=stdin_text, + capture_output=True, + text=True, + check=False, + timeout=30, + ) + + +def test_python_dash_m_spells_word_from_piped_input() -> None: + result = _run_module("Hi\n") + + assert result.returncode == 0 + assert constants.RESULT_TEXT in result.stdout + assert result.stdout.splitlines()[-2:] == ["Hotel", "India"] + + +def test_python_dash_m_exits_cleanly_on_end_of_input() -> None: + result = _run_module("") + + assert result.returncode == 0 + assert result.stderr == ""