diff --git a/.cspell.yaml b/.cspell.yaml index 1b319f18..8a47c5cf 100644 --- a/.cspell.yaml +++ b/.cspell.yaml @@ -9,10 +9,11 @@ # - NEVER add a misspelled word to the 'words' list # - PROPOSE only genuine technical terms/names as needed -version: "0.2" +version: '0.2' + language: en -# Project-specific technical terms and tool names +# TODO: Add project-specific technical terms and tool names words: - ACCESSTOKEN - buildmark @@ -42,17 +43,15 @@ words: - workitems - yamlfix -# Exclude common build artifacts, dependencies, and vendored third-party code ignorePaths: - - "**/.git/**" - - "**/node_modules/**" - - "**/.venv/**" - - "**/thirdparty/**" - - "**/third-party/**" - - "**/3rd-party/**" - - "**/generated/**" - - "**/AGENT_REPORT_*.md" - - "**/.agent-logs/**" - - "**/bin/**" - - "**/obj/**" + - '**/.git/**' + - '**/node_modules/**' + - '**/.venv/**' + - '**/thirdparty/**' + - '**/third-party/**' + - '**/3rd-party/**' + - '**/.agent-logs/**' + - '**/bin/**' + - '**/obj/**' + - '**/generated/**' - package-lock.json diff --git a/.github/agents/developer.agent.md b/.github/agents/developer.agent.md index a95c562f..5f208eb1 100644 --- a/.github/agents/developer.agent.md +++ b/.github/agents/developer.agent.md @@ -14,6 +14,10 @@ Perform software development tasks by determining and applying appropriate stand 2. **Read relevant standards** using the selection matrix in AGENTS.md 3. **Pre-flight verification** before making any changes: - List files that will be created, modified, or deleted + - For each file to be **created**, check whether a counterpart exists in the + template (URL in the `# Reference Template` section of `AGENTS.md`). + If one exists, fetch it as the starting point; adjust placeholder names and heading + depth to match the target path before writing the file - For each modified file, identify which companion artifacts need updating (requirements, design docs, tests, review-sets) - Include companion artifact updates in the work plan diff --git a/.github/agents/quality.agent.md b/.github/agents/quality.agent.md index da467d40..380d11f1 100644 --- a/.github/agents/quality.agent.md +++ b/.github/agents/quality.agent.md @@ -54,21 +54,13 @@ Priority-ordered list of issues that MUST be resolved for the next retry: ## Requirements Compliance: (PASS|FAIL|N/A) -- Were requirements updated to reflect functional changes? -- Were new requirements created for new features? -- Do requirement IDs follow semantic naming standards? -- Do requirement files follow kebab-case naming convention? -- Are requirement files organized under `docs/reqstream/` with proper folder structure? -- Are OTS requirements properly placed in `docs/reqstream/ots/` subfolder? -- Were source filters applied appropriately for platform-specific requirements? +- Were requirements created/updated for all functional changes? +- Were source filters applied for platform-specific requirements? - Is requirements traceability maintained to tests? ## Design Documentation Compliance: (PASS|FAIL|N/A) -- Were design documents updated for architectural changes? -- Were new design artifacts created for new components? -- Do design folder names use kebab-case convention matching source structure? -- Are design files properly named ({subsystem-name}.md, {unit-name}.md patterns)? +- Were design artifacts created/updated for all new or changed components? - Is `docs/design/introduction.md` present with required Software Structure section? - Are design decisions documented with rationale? - Is system/subsystem/unit categorization maintained? @@ -76,55 +68,50 @@ Priority-ordered list of issues that MUST be resolved for the next retry: ## Code Quality Compliance: (PASS|FAIL|N/A) -- Are language-specific standards followed (from applicable standards files)? -- Are quality checks from standards files satisfied? -- Is code properly categorized (system/subsystem/unit/OTS)? -- Is appropriate separation of concerns maintained? -- Was language-specific build tooling executed and passing? +- Do language-specific quality checks from loaded standards pass? +- Is code properly categorized (system/subsystem/unit/OTS/Shared Package)? +- Does the build pass? ## Testing Compliance: (PASS|FAIL|N/A) - Were tests created/updated for all functional changes? - Is test coverage maintained for all requirements? -- Are testing standards followed (AAA pattern, etc.)? -- Do tests respect software item hierarchy boundaries (System/Subsystem/Unit scope)? +- Do tests respect software item hierarchy boundaries? - Are cross-hierarchy test dependencies documented in design docs? -- Does test categorization align with code structure? -- Do all tests pass without failures? +- Do all tests pass? ## Review Management Compliance: (PASS|FAIL|N/A) -- Were review-sets updated for structural changes (new/deleted systems, subsystems, or units)? -- Do file patterns follow include-then-exclude approach? +- Were review-sets updated for structural changes? - Is review scope appropriate for change magnitude? -- Was ReviewMark tooling executed and passing? -- Were review artifacts generated correctly? +- Does ReviewMark pass? ## Documentation Compliance: (PASS|FAIL|N/A) -- Was README.md updated for user-facing changes? -- Were user guides updated for feature changes? +- Were README.md and user guides updated for user-facing changes? - Does API documentation reflect code changes? - Was compliance documentation generated? -- Does documentation follow standards formatting? -- Is documentation organized under `docs/` following standard folder structure? -- Do Pandoc collections include proper `introduction.md` with Purpose and Scope sections? - Are auto-generated markdown files left unmodified? -- Do README.md files use absolute URLs and include concrete examples? -- Is documentation integrated into ReviewMark review-sets for formal review? +- Is documentation integrated into ReviewMark review-sets? ## Software Item Completeness: (PASS|FAIL|N/A) +- Load `software-items.md` before evaluating this section. + - Does every identified software unit have its own requirements file? - Does every identified software unit have its own design document? - Does every identified subsystem have its own requirements file? - Does every identified subsystem have its own design document? +## Repository Structure Compliance: (PASS|FAIL|N/A) + +- Load `repository-map.md` from the template URL in the `# Reference Template` + section of `AGENTS.md` before evaluating this section. + +- Are parallel artifact trees in sync (reqstream/design/verification/src/test)? +- Does the repository conform to the template `repository-map.md`? + ## Process Compliance: (PASS|FAIL|N/A) -- Was Continuous Compliance workflow followed? -- Did all quality gates execute successfully? -- Were appropriate tools used for validation? -- Were standards consistently applied across work? -- Was compliance evidence generated and preserved? +- Was compliance evidence (test results, review artifacts, generated docs) generated and preserved? ``` diff --git a/.github/agents/software-architect.agent.md b/.github/agents/software-architect.agent.md index 494568df..de5efa2a 100644 --- a/.github/agents/software-architect.agent.md +++ b/.github/agents/software-architect.agent.md @@ -13,7 +13,7 @@ Interview the user and produce evolving architecture documentation with prioriti # Standards Read `.github/standards/software-items.md` before starting. Use its definitions -(Software Package, System, Subsystem, Unit, OTS) as vocabulary throughout. +(Software Package, System, Subsystem, Unit, OTS, Shared Package) as vocabulary throughout. # Approach diff --git a/.github/agents/template-sync.agent.md b/.github/agents/template-sync.agent.md new file mode 100644 index 00000000..44d29ff5 --- /dev/null +++ b/.github/agents/template-sync.agent.md @@ -0,0 +1,77 @@ +--- +name: template-sync +description: Audits or synchronizes repository files against the canonical template. + Supports four modes - Audit, Sync, Scaffold, and Recreate. +user-invocable: true +--- + +# Template Sync Agent + +This agent is an orchestrator supporting four modes: + +- **Audit** - report structural deviations; no changes +- **Sync** - patch missing sections into existing files +- **Scaffold** - create files that do not yet exist; skip existing files +- **Recreate** - rebuild existing files from the template, migrating old content + +Read the template URL and `repository-map.md` from the `# Reference Template` +section in `AGENTS.md`, then map the requested scope onto the work groups below. +Delegate each group to a sub-agent. + +# Work Groups + +- **Root config files** - all non-collection files at the repository root +- **One group per flat `docs/` folder** - e.g. `docs/build_notes/`, `docs/user_guide/` +- **One group per system subtree** in `docs/design/`, `docs/verification/`, `docs/reqstream/` - + each subtree and all its descendants is one group + +# Orchestration + +For each group intersecting the requested scope, call a sub-agent with: + +- **context**: + - Group scope and template URL from the `# Reference Template` section in `AGENTS.md` + - Project-specific names substitute for placeholders at matching path depth + (e.g. `SystemName` → `{SystemName}`, `system-name` → `{system-name}`) + - If a template counterpart cannot be fetched, skip the file and report it +- **goal**: + - Based on the given mode: + - **Audit** - fetch each template counterpart; compare headings; report missing + sections and depth mismatches; no changes + - **Sync** - as Audit, then insert each missing section; run `pwsh ./fix.ps1` + - **Scaffold** - fetch `repository-map.md` from the template URL in `AGENTS.md` + to identify files that should exist but don't; for each, fetch the template, + populate all sections, write the file; run `pwsh ./fix.ps1` + - **Recreate** - read the existing file in full, then fetch the template; use + full semantic understanding to map old content onto template sections, + splitting or consolidating as needed; create extra sections for any content + that has no template home; write the rebuilt file; run `pwsh ./fix.ps1` + - When writing any section: HTML comments and TODO placeholders in the template + are instructions - always resolve them to real content; infer from README, + related files, sibling docs, and path; if confident write directly; if + ambiguous offer 2–3 concrete options and ask the user; keep asking until they + answer - never leave a TODO unless the user explicitly requests it + +Collect sub-agent results and assemble the final report. + +# Report Template + +```markdown +# Template Sync Report + +**Result**: (SUCCEEDED|FAILED) +**Mode**: (Audit|Sync|Scaffold|Recreate) + +## Files + +### {file-path} + +- **Template**: {template path} +- **Missing sections**: {list or "none"} +- **Heading depth issues**: {list or "none"} +- **Action**: (Reported | Sections added | Created | Rebuilt | No template found) + +## Summary + +- **Conformant**: {count} | **Deviations**: {count} | **Updated**: {count} +``` diff --git a/.github/standards/design-documentation.md b/.github/standards/design-documentation.md index 768bf3f2..635cb6d5 100644 --- a/.github/standards/design-documentation.md +++ b/.github/standards/design-documentation.md @@ -4,214 +4,106 @@ description: Follow these standards when creating design documentation. globs: ["docs/design/**/*.md"] --- -# Design Documentation Standards - -This document defines standards for design documentation within Continuous -Compliance environments, extending the general technical documentation -standards with specific requirements for software design artifacts. - -## Required Standards - -Read these standards first before applying this standard: +# Required Standards - **`technical-documentation.md`** - General technical documentation standards -- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS) - -# Core Principles - -Design documentation serves as the bridge between requirements and -implementation, providing detailed technical specifications that enable: +- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS/Shared Package) -- **Formal Code Review**: Reviewers can verify implementation matches design -- **Compliance Evidence**: Auditors can trace requirements through design to code -- **Maintenance Support**: Developers can understand system structure and interactions -- **Quality Assurance**: Testing teams can validate against detailed specifications - -# Required Structure and Documents - -Design documentation must be organized under `docs/design/` with folder structure -mirroring source code organization because reviewers need clear navigation from -design to implementation: +# Folder Structure ```text docs/design/ -├── introduction.md # Document overview - heading depth # -├── {system-name}.md # System-level design - heading depth # -└── {system-name}/ # System folder (one per system) - ├── {subsystem-name}.md # Subsystem overview - heading depth ## - ├── {subsystem-name}/ # Subsystem folder (kebab-case); may nest recursively - │ ├── {child-subsystem}.md # Child subsystem overview - heading depth ### - │ ├── {child-subsystem}/ # Child subsystem folder (same structure as parent) - │ └── {unit-name}.md # Unit design - heading depth ### - └── {unit-name}.md # System-level unit design - heading depth ## +├── introduction.md # heading depth # +├── {system-name}.md # heading depth # +├── {system-name}/ +│ ├── {subsystem-name}.md # heading depth ## +│ ├── {subsystem-name}/ +│ │ └── {unit-name}.md # heading depth ### +│ └── {unit-name}.md # heading depth ## +├── ots.md # heading depth # (if OTS items exist) +├── ots/ +│ └── {ots-name}.md # heading depth ## +├── shared.md # heading depth # (if Shared Packages exist) +└── shared/ + └── {package-name}.md # heading depth ## ``` -Each scope's overview file lives in its **parent** folder, not inside the scope's own -subfolder - this aligns heading depth with folder depth so the compiled PDF has a -meaningful multi-level outline (see Heading Depth Rule in `technical-documentation.md`). +Subsystems may nest recursively. Each file's heading depth equals its folder depth under `docs/design/`. -## introduction.md (MANDATORY) +# introduction.md (MANDATORY) -The `introduction.md` file serves as the design entry point and MUST include -these sections because auditors need clear scope boundaries and architectural -overview: +Must include: -### Purpose Section +- **Purpose**: audience and compliance drivers +- **Scope**: items covered and explicitly excluded (no test projects) +- **Software Structure**: text tree showing all Systems/Subsystems/Units/OTS/Shared items +- **Folder Layout**: text tree showing source folder structure +- **Companion Artifact Structure**: parallel paths for requirements, design, verification, source, tests +- **References** _(if applicable)_: external standards or specifications - only in `introduction.md` -Clear statement of the design document's purpose, audience, and regulatory -or compliance drivers. +# System Design (MANDATORY) -### Scope Section +Create `{system-name}.md` (`#` heading) and `{system-name}/` folder. All sections mandatory; +write "N/A - {justification}" rather than removing any section: -Define what software items are covered and what is explicitly excluded. -Design documentation must NOT include test projects, test classes, or test -infrastructure because design documentation documents the architecture of -shipping product code, not ancillary content used to validate it. +- **Architecture**: software items, relationships, and collaboration +- **External Interfaces**: name, direction, format, constraints +- **Dependencies**: OTS and Shared Packages used; cross-reference their design docs +- **Risk Control Measures**: segregation required for risk control (IEC 62304 §5.3.3) +- **Data Flow**: inputs to outputs +- **Design Constraints**: platform, performance, security, regulatory -### Software Structure Section (MANDATORY) +# Subsystem Design (MANDATORY) -Include a text-based tree diagram showing the software organization across -System, Subsystem, and Unit levels. Agents MUST read `software-items.md` -to understand these classifications before creating this section. +Place `{subsystem-name}.md` in the **parent** folder; create `{subsystem-name}/` for children. +All sections mandatory; write "N/A - {justification}" rather than removing any section: -Example format: +- **Overview**: responsibility, boundaries, contained units +- **Interfaces**: what it exposes and consumes +- **Design**: how internal units collaborate -```text -Project1Name (System) -├── ComponentA (Subsystem) -│ ├── SubComponentP (Subsystem) -│ │ └── ClassW (Unit) -│ ├── ClassX (Unit) -│ └── ClassY (Unit) -├── ComponentB (Subsystem) -│ └── ClassZ (Unit) -└── UtilityClass (Unit) - -Project2Name (System) -└── HelperClass (Unit) -``` +# Unit Design (MANDATORY) -### Folder Layout Section (MANDATORY) +Place `{unit-name}.md` in the **parent** folder. All sections mandatory; +write "N/A - {justification}" rather than removing any section: -Include a text-based tree diagram showing how the source code folders -mirror the software structure, with file paths and brief descriptions. +- **Purpose**: single responsibility +- **Data Model**: fields, properties, types, invariants (IEC 62304 §5.4.2) +- **Key Methods**: name, purpose, algorithm, preconditions, postconditions, parameter types +- **Error Handling**: detection and handling; what is propagated vs. handled locally +- **Interactions**: dependencies on other units/subsystems/OTS; who calls this unit -Example format: +# OTS Integration Design (when OTS items exist) -```text -src/Project1Name/ -├── ComponentA/ -│ ├── SubComponentP/ -│ │ └── ClassW.cs - Specialized processing engine -│ ├── ClassX.cs - Core business logic handler -│ └── ClassY.cs - Data validation service -├── ComponentB/ -│ └── ClassZ.cs - Integration interface -└── UtilityClass.cs - Common utility functions - -src/Project2Name/ -└── HelperClass.cs - Helper functions -``` - -### References Section (RECOMMENDED) - -If the design references external documents (standards, specifications), include -a `## References` section in `introduction.md`. This is the **only** place in the -design document collection where a References section should appear - do not add -one to any other design file. +Create `docs/design/ots.md` (`#` heading) covering the overall OTS integration strategy. -### Companion Artifact Structure (RECOMMENDED) - -Include a brief note explaining that each software item has parallel artifacts -across the repository, so agents and reviewers can navigate from any one -artifact to all related files: - -Example format: - -```text -Each in-house software item has corresponding artifacts in parallel directory trees: +For each OTS item, create `docs/design/ots/{ots-name}.md` (`##` heading) covering: +why chosen, which features/APIs used, integration patterns, version constraints. -- Requirements: `docs/reqstream/{system-name}.yaml`, `docs/reqstream/{system-name}/.../{item}.yaml` -- Design docs: `docs/design/{system-name}.md`, `docs/design/{system-name}/.../{item}.md` -- Verification: `docs/verification/{system-name}.md`, `docs/verification/{system-name}/.../{item}.md` -- Source code: `src/{SystemName}/.../{Item}.{ext}` (cased per language - see `software-items.md`) -- Tests: `test/{SystemName}.Tests/.../{Item}Tests.{ext}` (cased per language) +# Shared Package Integration Design (when Shared Packages exist) -OTS items have no design documentation; their artifacts sit parallel to system folders: +Create `docs/design/shared.md` (`#` heading) covering the overall consumption strategy. -- Requirements: `docs/reqstream/ots/{ots-name}.yaml` -- Verification: `docs/verification/ots/{ots-name}.md` -- Tests (optional): `test/{OtsSoftwareTests}/...` (cased per language - see `software-items.md`) - -Review-sets: defined in `.reviewmark.yaml` -``` - -## System Design Documentation (MANDATORY) - -For each system identified in the repository: - -- Create `{system-name}.md` directly under `docs/design/` (heading depth `#`) -- Create a kebab-case folder `{system-name}/` to hold its subsystems and units -- `{system-name}.md` must cover: - - System architecture and major components - - External interfaces and dependencies - - Data flow and control flow - - System-wide design constraints and decisions - - Integration patterns and communication protocols - -## Subsystem and Unit Design Documents - -For each subsystem identified in the software structure: - -- Place `{subsystem-name}.md` inside the **parent** folder (the system folder, or parent - subsystem folder) - not inside its own subfolder -- Create a kebab-case folder `{subsystem-name}/` to hold its child units and subsystems -- `{subsystem-name}.md` must cover subsystem overview and design - -For every unit identified in the software structure: - -- Place `{unit-name}.md` inside its parent scope's folder (system or subsystem folder) -- Document data models, algorithms, and key methods -- Describe interactions with other units -- Include sufficient detail for formal code review - -Follow the Heading Depth Rule from `technical-documentation.md` - a file's top-level -heading depth equals its folder depth under `docs/design/`. - -# Software Items Integration (CRITICAL) - -Read `software-items.md` before creating design documentation - correct -System/Subsystem/Unit categorization is required for software structure -diagrams and folder layout. +For each Shared Package, create `docs/design/shared/{package-name}.md` (`##` heading) covering: +which advertised features are consumed, integration pattern, configuration/initialization. # Writing Guidelines -Design documentation must be technical and specific because it serves as the -implementation specification for formal code review: - -- **Implementation Detail**: Provide sufficient detail for code review and implementation -- **Architectural Clarity**: Clearly define component boundaries and interfaces -- **Traceability**: Link to requirements where applicable using ReqStream patterns -- **Verbal Cross-References**: Reference other parts of the design by name (e.g., - "See *Parser Design* for more details") - do not use markdown hyperlinks, which - break in compiled PDFs - -# Mermaid Diagram Integration - -Use Mermaid diagrams to supplement text descriptions (diagrams must not replace text content). +- Use Mermaid diagrams to supplement (not replace) text +- Use verbal cross-references ("see _Parser Design_") - not markdown hyperlinks (break in PDF) +- Provide sufficient detail for formal code review # Quality Checks -Before submitting design documentation, verify: - -- [ ] `introduction.md` includes both Software Structure and Folder Layout sections -- [ ] Software structure correctly categorizes items as System/Subsystem/Unit per `software-items.md` -- [ ] Folder layout mirrors software structure organization -- [ ] Files organized under `docs/design/` following the folder structure pattern above -- [ ] Each file's top-level heading depth matches its folder depth per the Heading Depth Rule -- [ ] Design documents provide sufficient detail for code review -- [ ] System documentation provides comprehensive system-level design -- [ ] All documentation folders use kebab-case names mirroring source code structure -- [ ] All documents follow technical documentation formatting standards -- [ ] Content is current with implementation and requirements -- [ ] Documents are integrated into ReviewMark review-sets for formal review +- [ ] `introduction.md` includes Software Structure, Folder Layout, and Companion Artifact Structure +- [ ] Software structure correctly categorizes items per `software-items.md` +- [ ] Each file's heading depth matches its folder depth +- [ ] All folders use kebab-case mirroring source structure +- [ ] System design includes all mandatory sections (Architecture, External Interfaces, Dependencies, + Risk Control Measures, Data Flow, Design Constraints) +- [ ] Subsystem design includes all mandatory sections (Overview, Interfaces, Design) +- [ ] Unit design includes all mandatory sections (Purpose, Data Model, Key Methods, Error Handling, Interactions) +- [ ] Non-applicable mandatory sections contain "N/A - {justification}" +- [ ] `docs/design/ots.md` and `docs/design/ots/{ots-name}.md` exist when OTS items are present +- [ ] `docs/design/shared.md` and `docs/design/shared/{package-name}.md` exist when Shared Packages are present +- [ ] Documents are integrated into ReviewMark review-sets diff --git a/.github/standards/reqstream-usage.md b/.github/standards/reqstream-usage.md index 303bb43f..95d36a10 100644 --- a/.github/standards/reqstream-usage.md +++ b/.github/standards/reqstream-usage.md @@ -9,7 +9,7 @@ globs: ["requirements.yaml", "docs/reqstream/**/*.yaml"] Read these standards first before applying this standard: - **`requirements-principles.md`** - Requirements principles and unidirectionality -- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS) +- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS/Shared Package) # Requirements Organization @@ -29,13 +29,16 @@ docs/reqstream/ │ │ ├── {child-subsystem}/ # Child subsystem folder │ │ └── {unit-name}.yaml # Unit requirements │ └── {unit-name}.yaml # System-level unit requirements -└── ots/ # OTS items appear as a distinct section in reports - └── {ots-name}.yaml # Requirements for OTS components +├── ots/ # OTS items appear as a distinct section in reports +│ └── {ots-name}.yaml # Requirements for OTS components +└── shared/ # Shared Packages appear as a distinct section in reports + └── {package-name}.yaml # Requirements for Shared Package dependencies ``` -In-house items have matching relative paths across `docs/reqstream/`, `docs/design/`, and -`docs/verification/`. OTS items appear only in `docs/reqstream/ots/` and -`docs/verification/ots/` - they have no design documentation. +Local items have matching relative paths across `docs/reqstream/`, `docs/design/`, and +`docs/verification/`. OTS items appear in `docs/reqstream/ots/`, `docs/design/ots/`, and +`docs/verification/ots/`. Shared Packages appear in `docs/reqstream/shared/`, +`docs/design/shared/`, and `docs/verification/shared/`. # Requirements File Format @@ -58,7 +61,7 @@ sections: # OTS Software Requirements Use nested sections in `docs/reqstream/ots/` because ReqStream renders the `ots/` -subtree as a distinct section in generated reports, separate from in-house +subtree as a distinct section in generated reports, separate from local system requirements: ```yaml @@ -73,6 +76,23 @@ sections: - JsonReaderTests.TestReadValidJson ``` +# Shared Package Requirements + +Use nested sections in `docs/reqstream/shared/` - ReqStream renders the `shared/` +subtree as a distinct section in reports, separate from local and OTS requirements: + +```yaml +sections: + - title: Shared Package Requirements + sections: + - title: MyOrg.SharedLibrary + requirements: + - id: SharedLibrary-Core-ParseConfig + title: MyOrg.SharedLibrary shall parse configuration files. + tests: + - SharedLibraryIntegrationTests.TestParseValidConfig +``` + # Semantic IDs (MANDATORY) Use the `System-Component-Feature` pattern because ReqStream uses IDs as-is in @@ -132,5 +152,6 @@ Before submitting requirements, verify: - [ ] Files organized under `docs/reqstream/` following the folder structure pattern above - [ ] All documentation folders use kebab-case names matching source code structure - [ ] OTS requirements placed in `ots/` subfolder +- [ ] Shared Package requirements placed in `shared/` subfolder - [ ] Valid YAML syntax passes yamllint validation - [ ] Test result formats compatible (TRX, JUnit XML) diff --git a/.github/standards/reviewmark-usage.md b/.github/standards/reviewmark-usage.md index 2d958320..990d707e 100644 --- a/.github/standards/reviewmark-usage.md +++ b/.github/standards/reviewmark-usage.md @@ -9,7 +9,7 @@ description: Follow these standards when configuring file reviews with ReviewMar Read these standards first before applying this standard: -- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS) +- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS/Shared Package) ## Purpose @@ -139,6 +139,23 @@ Reviews architectural and design consistency: - Design introduction: `docs/design/introduction.md` - System design: `docs/design/{system-name}.md` - System design files: `docs/design/{system-name}/**/*.md` + - OTS overview: `docs/design/ots.md` _(only if OTS items exist)_ + - Shared Package overview: `docs/design/shared.md` _(only if Shared Package items exist)_ + +## `{System}-Verification` Review (one per system) + +Reviews verification completeness and consistency: + +- **Purpose**: Proves the system verification design is consistent and covers all requirements +- **Title**: "Review that {System} Verification is Consistent and Complete" +- **Scope**: Only brings in top-level requirements and all verification docs for the system +- **File Path Patterns**: + - System requirements: `docs/reqstream/{system-name}.yaml` + - Verification introduction: `docs/verification/introduction.md` + - System verification: `docs/verification/{system-name}.md` + - System verification files: `docs/verification/{system-name}/**/*.md` + - OTS overview: `docs/verification/ots.md` _(only if OTS items exist)_ + - Shared Package overview: `docs/verification/shared.md` _(only if Shared Package items exist)_ ## `{System}-AllRequirements` Review (one per system) @@ -182,17 +199,29 @@ Reviews individual software unit implementation: ## `OTS-{OtsName}` Review (one per OTS item) -Reviews OTS item requirements and verification evidence: +Reviews OTS item integration design, requirements, and verification evidence: -- **Purpose**: Proves that the OTS item provides the required functionality +- **Purpose**: Proves that the OTS item provides the required functionality and is correctly integrated - **Title**: "Review that {OtsName} Provides Required Functionality" -- **Scope**: OTS items have no in-house design or source; review covers requirements and - verification evidence only +- **Scope**: No local source code; review covers integration design, requirements, and verification evidence - **File Path Patterns**: - OTS requirements: `docs/reqstream/ots/{ots-name}.yaml` + - OTS integration design: `docs/design/ots/{ots-name}.md` - OTS verification: `docs/verification/ots/{ots-name}.md` - Tests (if applicable): `test/{OtsSoftwareTests}/...` (cased per language) +## `Shared-{PackageName}` Review (one per Shared Package) + +Reviews Shared Package integration design, requirements, and verification evidence: + +- **Purpose**: Proves that the Shared Package provides the required advertised features and is correctly integrated +- **Title**: "Review that {PackageName} Provides Required Features" +- **Scope**: No local source code; review covers integration design, requirements, and verification evidence +- **File Path Patterns**: + - Shared Package requirements: `docs/reqstream/shared/{package-name}.yaml` + - Shared Package integration design: `docs/design/shared/{package-name}.md` + - Shared Package verification: `docs/verification/shared/{package-name}.md` + **Note**: File path patterns use `{ext}` as a placeholder for language-specific extensions (`.cs`, `.cpp`/`.hpp`, `.py`, etc.). Adapt to your repository's languages. @@ -207,9 +236,12 @@ Before submitting ReviewMark configuration, verify: - [ ] Subsystem reviews follow hierarchical scope principle (exclude unit source code) - [ ] Only unit reviews include actual source code files - [ ] Architecture review-sets include system verification design alongside system design +- [ ] Design review-sets include all system design files +- [ ] Verification review-sets include all system verification files - [ ] Subsystem review-sets include subsystem verification design - [ ] Unit review-sets include unit verification design -- [ ] OTS review-sets include OTS requirements and verification evidence +- [ ] OTS review-sets include OTS requirements, integration design, and verification evidence +- [ ] Shared Package review-sets include Shared Package requirements, integration design, and verification evidence - [ ] Each review-set focuses on a single compliance question (single focus principle) - [ ] File patterns use correct glob syntax and match intended files - [ ] Review-set file counts remain manageable (context management principle) diff --git a/.github/standards/software-items.md b/.github/standards/software-items.md index 6be029f9..328a08e2 100644 --- a/.github/standards/software-items.md +++ b/.github/standards/software-items.md @@ -11,12 +11,14 @@ requirements management approach, testing strategy, and review scope. # Software Item Categories -Categorize all software into five primary groups: +Categorize all software into six primary groups: - **Software Package**: Distributable unit delivered to end users or dependent systems, containing one software system with all its components. All software - systems are delivered as a software package. When consumed by another system, - our software package is treated as an OTS Software Item by that system. + systems are delivered as a software package. When consumed by a system outside + the producing program, our software package is treated as an OTS Software Item + by that system. When consumed by another repository within the same program, + it is treated as a Shared Package. - **Software System**: Complete deliverable product including all components and external interfaces, contained within a software package - **Software Subsystem**: Major architectural component with well-defined @@ -24,7 +26,11 @@ Categorize all software into five primary groups: - **Software Unit**: Individual class, function, or tightly coupled set of functions that can be tested in isolation - **OTS Software Item**: Third-party component (library, framework, tool, or - published software package) providing functionality not developed in-house + published software package) providing functionality not developed within the program +- **Shared Package**: A software package produced by a different repository within + the same program, consumed as a dependency. Referenced by its advertised features + rather than internal design; traceability to program-level requirements runs + through the top-level project. **Naming**: When names collide in hierarchy, add descriptive suffix to higher-level entity: @@ -75,14 +81,15 @@ Choose the appropriate category based on scope and testability: ## OTS Software Item -- External dependency not developed in-house - typically a third-party published +- External dependency from outside the program - typically a third-party published software package (NuGet, npm, etc.), hosted service, or tool -- Our own published software package becomes an OTS item to any system that - consumes it +- A package produced by an unrelated program (inside or outside the organization) + is treated as OTS by any consuming system - Tested through integration tests proving required functionality works - Examples: System.Text.Json, Entity Framework, third-party APIs -- **Artifact locations** (OTS items have no design documentation): +- **Artifact locations** (OTS items have no internal design documentation): - Requirements: `docs/reqstream/ots/{ots-name}.yaml` + - Design: `docs/design/ots/{ots-name}.md` (integration/usage design - how the local system uses this item) - Verification: `docs/verification/ots/{ots-name}.md` - These folders sit parallel to system folders (not inside any system folder) - System design documentation records which OTS items each system depends on @@ -90,6 +97,21 @@ Choose the appropriate category based on scope and testability: published compliance reports), a dedicated test project (`OtsSoftwareTests` / `ots_software_tests`, cased per language) holds OTS integration tests - one test file per OTS item requiring tests. +## Shared Package + +- A software package produced by a different repository within the same program +- The consuming repository references advertised features, not internal design or source +- Traceability to program-level requirements runs through the top-level project, + not directly between repositories +- Verified through any appropriate approach in the consuming repository - most commonly + downstream integration tests that transitively prove the advertised features are functional +- **Artifact locations** (no internal design documentation in the consuming repository): + - Requirements: `docs/reqstream/shared/{package-name}.yaml` + - Design: `docs/design/shared/{package-name}.md` (integration/usage design - which features are consumed and how) + - Verification: `docs/verification/shared/{package-name}.md` + - These folders sit parallel to system and OTS folders +- System design documentation records which Shared Packages each system depends on + # Software Item Artifact Model Each software item has five artifact types that together form a complete review @@ -97,7 +119,8 @@ unit - because reviewing any one artifact in isolation cannot determine whether the item is correct, well-designed, and proven to work: - **Requirements** - WHAT the item must do (drives all other artifacts; applies to all item types) -- **Design** - HOW the item satisfies its requirements (in-house items only: system, subsystem, unit) +- **Design** - HOW the item satisfies its requirements (full design for local items: system, + subsystem, unit; integration/usage design for OTS and Shared Package) - **Verification Design** - HOW the requirements will be tested (applies to all item types) -- **Source code** - The implementation of the design (in-house units only) +- **Source code** - The implementation of the design (local units only; not applicable to OTS or Shared Package) - **Tests** - PROOF the item does WHAT it is required to do (applies to all item types) diff --git a/.github/standards/technical-documentation.md b/.github/standards/technical-documentation.md index 2ac29f47..0dc44556 100644 --- a/.github/standards/technical-documentation.md +++ b/.github/standards/technical-documentation.md @@ -43,6 +43,10 @@ When creating a new document collection, create these three files together and u the existing collections under `docs/` as templates - they share a consistent structure across all collections. +The `generated/` folder is **never committed** to the repository - it is created +locally and in CI by the build pipeline. Do not flag its absence as a conformance +issue. + **`title.txt`** - YAML front matter with document metadata. Use the existing files under `docs/` as a pattern and keep fields consistent with the rest of the repository. diff --git a/.github/standards/verification-documentation.md b/.github/standards/verification-documentation.md index 8eea3b70..8dc44084 100644 --- a/.github/standards/verification-documentation.md +++ b/.github/standards/verification-documentation.md @@ -6,139 +6,105 @@ globs: ["docs/verification/**/*.md"] # Required Standards -Read these standards first before applying this standard: - - **`technical-documentation.md`** - General technical documentation standards -- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS) - -# Core Principles - -Verification design is the bridge between requirements and tests - it documents HOW -requirements will be verified, enabling reviewers to confirm test completeness without -reading implementation code. - -# Required Structure and Documents +- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS/Shared Package) -Organize under `docs/verification/` mirroring the software item hierarchy: +# Folder Structure ```text docs/verification/ -├── introduction.md # Document overview - heading depth # -├── {system-name}.md # System-level verification - heading depth # -├── {system-name}/ # System folder (one per system) -│ ├── {subsystem-name}.md # Subsystem verification - heading depth ## -│ ├── {subsystem-name}/ # Subsystem folder (kebab-case); may nest recursively -│ │ ├── {child-subsystem}.md # Child subsystem verification - heading depth ### -│ │ ├── {child-subsystem}/ # Child subsystem folder (same structure as parent) -│ │ └── {unit-name}.md # Unit verification - heading depth ### -│ └── {unit-name}.md # System-level unit verification - heading depth ## -├── ots.md # OTS section overview - heading depth # (MANDATORY if OTS items exist) -└── ots/ # OTS items - parallel to system folders (not inside them) - └── {ots-name}.md # OTS item verification evidence - heading depth ## +├── introduction.md # heading depth # +├── {system-name}.md # heading depth # +├── {system-name}/ +│ ├── {subsystem-name}.md # heading depth ## +│ ├── {subsystem-name}/ +│ │ └── {unit-name}.md # heading depth ### +│ └── {unit-name}.md # heading depth ## +├── ots.md # heading depth # (if OTS items exist) +├── ots/ +│ └── {ots-name}.md # heading depth ## +├── shared.md # heading depth # (if Shared Packages exist) +└── shared/ + └── {package-name}.md # heading depth ## ``` -Each scope's overview file lives in its **parent** folder, not inside the scope's own -subfolder - this keeps artifact locations consistent with design and requirements trees -so any item's files are deterministically locatable, and aligns heading depth with folder -depth for correct PDF structure (see Heading Depth Rule in `technical-documentation.md`). +Subsystems may nest recursively. Each file's heading depth equals its folder depth under `docs/verification/`. -## introduction.md (MANDATORY) +# introduction.md (MANDATORY) -Follow the standard `introduction.md` format from `technical-documentation.md`. Scope -covers all software items including OTS items (via self-validation if appropriate). +Must include: -Include a Companion Artifact Structure note so agents and reviewers can navigate from any -artifact to all related files: +- **Purpose**: audience and compliance drivers +- **Scope**: items covered and explicitly excluded (no test projects) +- **Companion Artifact Structure**: parallel paths for requirements, design, verification, source, tests +- **References** _(if applicable)_: external standards or specifications - only in `introduction.md` -```text -In-house items have parallel artifacts in: -- Requirements: `docs/reqstream/{system-name}.yaml`, `docs/reqstream/{system-name}/.../{item}.yaml` -- Design: `docs/design/{system-name}.md`, `docs/design/{system-name}/.../{item}.md` -- Verification: `docs/verification/{system-name}.md`, `docs/verification/{system-name}/.../{item}.md` -- Source: `src/{SystemName}/.../{Item}.{ext}` (cased per language) -- Tests: `test/{SystemName}.Tests/.../{Item}Tests.{ext}` (cased per language) - -OTS items (no design documentation) have artifacts parallel to system folders: -- Requirements: `docs/reqstream/ots/{ots-name}.yaml` -- Verification: `docs/verification/ots/{ots-name}.md` -- Tests (if required): `test/{OtsSoftwareTests}/...` (cased per language - see `software-items.md`) - -Review-sets: defined in `.reviewmark.yaml` -``` +# System Verification Design (MANDATORY) -If the verification design references external documents (standards, specifications), include -a `## References` section in `introduction.md` only - do not add one to any other verification file. +Create `{system-name}.md` (`#` heading) and `{system-name}/` folder. All sections mandatory; +write "N/A - {justification}" rather than removing any section: -## System Verification Design (MANDATORY) +- **Verification Strategy**: test types (unit, integration, end-to-end), framework, project structure +- **Test Environment**: OS, runtime, external services, files, or configuration required +- **Acceptance Criteria**: what constitutes a passing system test (IEC 62304 §5.7.2) +- **System-Level Test Scenarios**: named scenarios for each system requirement +- **Requirements Coverage**: requirement → scenario(s) → test method(s) mapping -For each system, create `{system-name}.md` at `docs/verification/` root and a -`{system-name}/` folder for subsystems. Cover: +# Subsystem Verification Design (MANDATORY) -- System verification strategy and overall test approach -- Test environments and configuration required -- External interface simulation and test-harness design -- End-to-end and integration test scenarios covering system requirements -- Acceptance criteria and pass/fail conditions at the system boundary -- Coverage mapping of system requirements to system-level test scenarios +Place `{subsystem-name}.md` in the **parent** folder; create `{subsystem-name}/` for children. +All sections mandatory; write "N/A - {justification}" rather than removing any section: -## Subsystem Verification Design (MANDATORY) +- **Verification Strategy**: integration test approach and mocking at subsystem boundary +- **Test Environment**: any environment setup beyond the standard test runner +- **Acceptance Criteria**: what constitutes a passing subsystem test (IEC 62304 §5.5.2) +- **Test Scenarios**: named scenarios including boundary conditions, error paths, and normal operation +- **Requirements Coverage**: requirement → scenario(s) → test method(s) mapping -For each subsystem, place `{subsystem-name}.md` in the parent (system or subsystem) -folder and create a `{subsystem-name}/` folder for its units. Cover: +# Unit Verification Design (MANDATORY) -- Subsystem verification strategy and integration test approach -- Dependencies that must be mocked or stubbed at the subsystem boundary -- Integration test scenarios covering subsystem requirements -- Coverage mapping of subsystem requirements to subsystem-level test scenarios +Place `{unit-name}.md` in the **parent** folder. All sections mandatory; +write "N/A - {justification}" rather than removing any section: -## Unit Verification Design (MANDATORY) +- **Verification Approach**: what is mocked/stubbed and why; injected vs. real dependencies +- **Test Environment**: any environment setup beyond the standard test runner +- **Acceptance Criteria**: what constitutes passing unit tests (IEC 62304 §5.5.2) +- **Test Scenarios**: named scenarios including boundary values, error paths, and normal operation +- **Requirements Coverage**: requirement → scenario(s) → test method(s) mapping -Place `{unit-name}.md` in the parent (system or subsystem) folder. Cover: +# OTS Verification Evidence (when OTS items exist) -- Verification approach for each unit requirement -- Named test scenarios including boundary conditions, error paths, and normal-operation cases -- Which dependencies are mocked and how they are configured -- Coverage mapping of every unit requirement to at least one named test scenario +Create `docs/verification/ots.md` (`#` heading) covering the overall OTS verification strategy. -## OTS Verification Evidence (when OTS items are used) +For each OTS item, create `docs/verification/ots/{ots-name}.md` (`##` heading) covering: +verification approach (self-validation, integration tests, vendor evidence) and requirements coverage. -Create `docs/verification/ots.md` at the collection root with a `#` top-level heading. This -file introduces the OTS verification approach and ensures OTS items compile as a top-level -section in the PDF rather than as subsystems of the last in-house system. +# Shared Package Verification Evidence (when Shared Packages exist) -For each OTS item, create `docs/verification/ots/{ots-name}.md` covering: +Create `docs/verification/shared.md` (`#` heading) covering the overall Shared Package verification strategy. -- The OTS item's required functionality (reference `docs/reqstream/ots/{ots-name}.yaml`) -- Verification of each requirement (using self-validation evidence if appropriate) -- Coverage mapping of OTS requirements to test scenarios +For each Shared Package, create `docs/verification/shared/{package-name}.md` (`##` heading) covering: +verification approach and requirements coverage. # Writing Guidelines -- **Test Coverage**: Map every requirement to at least one named test scenario so - reviewers can verify completeness without reading test code -- **Scenario Clarity**: Name each scenario clearly - "Valid input returns parsed result" not "Test 1" -- **Boundary Conditions**: Call out boundary values, error inputs, and edge cases explicitly -- **Isolation Strategy**: Describe what is mocked or stubbed and why at each level -- **Traceability**: Link to requirements where applicable using ReqStream patterns -- **Verbal Cross-References**: Reference other documents by name - do not use markdown - hyperlinks, which break in compiled PDFs - -Mermaid diagrams may supplement text descriptions where test flow benefits from visual -representation, but must not replace text content. +- Name scenarios clearly ("Valid input returns parsed result", not "Test 1") +- Use verbal cross-references - not markdown hyperlinks (break in PDF) +- Use Mermaid diagrams to supplement (not replace) text # Quality Checks -Before submitting verification documentation, verify: - -- [ ] Every requirement at each level is mapped to at least one named test scenario -- [ ] System verification documents cover end-to-end and integration scenarios -- [ ] Subsystem verification documents identify mocked boundaries and integration scenarios -- [ ] Unit verification documents identify individual scenarios including boundary and error paths -- [ ] Files organized under `docs/verification/` following the folder structure pattern above -- [ ] Each file's top-level heading depth matches its folder depth per the Heading Depth Rule -- [ ] All documentation folders use kebab-case names mirroring source code structure -- [ ] All documents follow technical documentation formatting standards -- [ ] Content is current with requirements and test implementation -- [ ] Every OTS item has `docs/verification/ots/{ots-name}.md` with requirement coverage -- [ ] `docs/verification/ots.md` exists with a `#` heading when OTS items are present -- [ ] Documents are integrated into ReviewMark review-sets for formal review +- [ ] `introduction.md` includes Companion Artifact Structure +- [ ] Each file's heading depth matches its folder depth +- [ ] All folders use kebab-case mirroring source structure +- [ ] System verification includes all mandatory sections (Verification Strategy, Test Environment, + Acceptance Criteria, System-Level Test Scenarios, Requirements Coverage) +- [ ] Subsystem verification includes all mandatory sections (Verification Strategy, Test Environment, + Acceptance Criteria, Test Scenarios, Requirements Coverage) +- [ ] Unit verification includes all mandatory sections (Verification Approach, Test Environment, + Acceptance Criteria, Test Scenarios, Requirements Coverage) +- [ ] Non-applicable mandatory sections contain "N/A - {justification}" +- [ ] Every requirement is mapped to at least one named test scenario +- [ ] `docs/verification/ots.md` and `docs/verification/ots/{ots-name}.md` exist when OTS items are present +- [ ] `docs/verification/shared.md` and `docs/verification/shared/{package-name}.md` exist when Shared Packages are present +- [ ] Documents are integrated into ReviewMark review-sets diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 49427468..60f37d9d 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -1,55 +1,43 @@ --- -# Markdown Linting Standards +# Markdown Linting Configuration # -# PURPOSE: -# - Maintain professional technical documentation standards -# - Ensure consistent formatting for readability and maintenance -# - Support automated documentation generation and publishing -# -# DO NOT MODIFY: These rules represent coding standards -# - If files fail linting, fix the files to meet these standards -# - Do not relax rules to accommodate existing non-compliant files -# - Consistency across repositories is critical for documentation quality - -# Disable the banner message (e.g., version info) on stdout -noBanner: true +# DO NOT MODIFY: These rules represent coding standards. -# Disable the progress indicator on stdout -noProgress: true +ignores: + - '**/.git/**' + - '**/node_modules/**' + - '**/.venv/**' + - '**/thirdparty/**' + - '**/third-party/**' + - '**/3rd-party/**' + - '**/generated/**' + - '**/.agent-logs/**' config: - # Enable all default rules - default: true - - # Require ATX-style headers (# Header) instead of Setext-style + # Require ATX-style headers MD003: style: atx + # Consistent unordered list markers + MD004: + style: dash + # Set consistent indentation for nested lists MD007: indent: 2 - # Allow longer lines for URLs and technical content - MD013: - line_length: 120 + # Line length - disabled (no limit enforced) + MD013: false # Allow multiple top-level headers per document MD025: false - # Allow inline HTML for enhanced documentation + # Allow inline HTML (XML comments used in templates) MD033: false - # Allow documents without top-level header (for fragments) + # Disable first-line-heading: this project uses the "fragment markdown" method where + # subsystem, unit, OTS, and shared-package files intentionally start with ## or ### + # (not #) because they are assembled by Pandoc into a single document via definition.yaml. + # In the assembled document the heading hierarchy is correct; markdownlint sees fragments + # in isolation and would otherwise raise false positives on every sub-document file. MD041: false - -# Exclude common build artifacts, dependencies, and vendored third-party code -ignores: - - "**/.git/**" - - "**/node_modules/**" - - "**/.venv/**" - - "**/thirdparty/**" - - "**/third-party/**" - - "**/3rd-party/**" - - "**/generated/**" - - "**/AGENT_REPORT_*.md" - - "**/.agent-logs/**" diff --git a/.reviewmark.yaml b/.reviewmark.yaml index 602b2ae7..551b4938 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -35,18 +35,19 @@ reviews: paths: - "README.md" - "docs/user_guide/**/*.md" - - "docs/reqstream/build-mark/build-mark.yaml" + - "docs/reqstream/build-mark.yaml" - "docs/design/introduction.md" - - "docs/design/build-mark/build-mark.md" + - "docs/design/build-mark.md" # BuildMark - Specials - id: BuildMark-Architecture title: Review of BuildMark system architecture and operational validation paths: - - "docs/reqstream/build-mark/build-mark.yaml" + - "docs/reqstream/build-mark.yaml" - "docs/design/introduction.md" - - "docs/design/build-mark/build-mark.md" - - "docs/verification/build-mark/build-mark.md" + - "docs/design/build-mark.md" + - "docs/verification/introduction.md" + - "docs/verification/build-mark.md" - "test/**/IntegrationTests.cs" - "test/**/Runner.cs" - "test/**/AssemblyInfo.cs" @@ -54,17 +55,26 @@ reviews: - id: BuildMark-Design title: Review of BuildMark architectural and design consistency paths: - - "docs/reqstream/build-mark/build-mark.yaml" + - "docs/reqstream/build-mark.yaml" - "docs/reqstream/build-mark/platform-requirements.yaml" - "docs/design/introduction.md" + - "docs/design/build-mark.md" - "docs/design/build-mark/**/*.md" + + - id: BuildMark-Verification + title: Review that BuildMark Verification is Consistent and Complete + paths: + - "docs/reqstream/build-mark.yaml" - "docs/verification/introduction.md" + - "docs/verification/build-mark.md" + - "docs/verification/build-mark/**/*.md" - "docs/verification/ots.md" - id: BuildMark-AllRequirements title: Review of All BuildMark requirements quality and traceability paths: - "requirements.yaml" + - "docs/reqstream/build-mark.yaml" - "docs/reqstream/build-mark/**/*.yaml" - "docs/reqstream/ots/**/*.yaml" @@ -84,7 +94,7 @@ reviews: paths: - "docs/reqstream/build-mark/cli/cli.yaml" - "docs/design/build-mark/cli/cli.md" - - "docs/verification/build-mark/cli/cli.md" + - "docs/verification/build-mark/cli.md" - "test/**/Cli/CliTests.cs" - id: BuildMark-Cli-Context @@ -102,7 +112,7 @@ reviews: paths: - "docs/reqstream/build-mark/self-test/self-test.yaml" - "docs/design/build-mark/self-test/self-test.md" - - "docs/verification/build-mark/self-test/self-test.md" + - "docs/verification/build-mark/self-test.md" - "test/**/SelfTest/SelfTestTests.cs" - id: BuildMark-SelfTest-Validation @@ -120,7 +130,7 @@ reviews: paths: - "docs/reqstream/build-mark/utilities/utilities.yaml" - "docs/design/build-mark/utilities/utilities.md" - - "docs/verification/build-mark/utilities/utilities.md" + - "docs/verification/build-mark/utilities.md" - "test/**/Utilities/UtilitiesTests.cs" - id: BuildMark-Utilities-PathHelpers @@ -141,13 +151,22 @@ reviews: - "src/**/Utilities/ProcessRunner.cs" - "test/**/Utilities/ProcessRunnerTests.cs" + - id: BuildMark-Utilities-TemporaryDirectory + title: Review of BuildMark Utilities TemporaryDirectory unit implementation + paths: + - "docs/reqstream/build-mark/utilities/temporary-directory.yaml" + - "docs/design/build-mark/utilities/temporary-directory.md" + - "docs/verification/build-mark/utilities/temporary-directory.md" + - "src/**/Utilities/TemporaryDirectory.cs" + - "test/**/Utilities/TemporaryDirectoryTests.cs" + # BuildMark - Version - id: BuildMark-Version title: Review that BuildMark Version Satisfies Subsystem Requirements paths: - "docs/reqstream/build-mark/version/version.yaml" - "docs/design/build-mark/version/version.md" - - "docs/verification/build-mark/version/version.md" + - "docs/verification/build-mark/version.md" - "test/**/Version/VersionTests.cs" - id: BuildMark-Version-VersionComparable @@ -204,7 +223,7 @@ reviews: paths: - "docs/reqstream/build-mark/build-notes/build-notes.yaml" - "docs/design/build-mark/build-notes/build-notes.md" - - "docs/verification/build-mark/build-notes/build-notes.md" + - "docs/verification/build-mark/build-notes.md" - "test/**/BuildNotes/BuildNotesTests.cs" - id: BuildMark-BuildNotes-BuildInformation @@ -239,7 +258,7 @@ reviews: paths: - "docs/reqstream/build-mark/configuration/configuration.yaml" - "docs/design/build-mark/configuration/configuration.md" - - "docs/verification/build-mark/configuration/configuration.md" + - "docs/verification/build-mark/configuration.md" - "test/**/Configuration/ConfigurationSubsystemTests.cs" - id: BuildMark-Configuration-BuildMarkConfigReader @@ -290,7 +309,7 @@ reviews: paths: - "docs/reqstream/build-mark/repo-connectors/repo-connectors.yaml" - "docs/design/build-mark/repo-connectors/repo-connectors.md" - - "docs/verification/build-mark/repo-connectors/repo-connectors.md" + - "docs/verification/build-mark/repo-connectors.md" - "test/**/RepoConnectors/RepoConnectorsTests.cs" - id: BuildMark-RepoConnectors-RepoConnectorBase @@ -392,7 +411,7 @@ reviews: - id: BuildMark-RepoConnectors-AzureDevOps title: Review that BuildMark RepoConnectors AzureDevOps Satisfies Sub-Subsystem Requirements paths: - - "docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml" + - "docs/reqstream/build-mark/repo-connectors/azure-devops.yaml" - "docs/design/build-mark/repo-connectors/azure-devops.md" - "docs/verification/build-mark/repo-connectors/azure-devops.md" - "test/**/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs" diff --git a/.yamllint.yaml b/.yamllint.yaml index 79c3aee4..1cde40ec 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -1,19 +1,11 @@ --- # YAML Linting Standards # -# PURPOSE: -# - Maintain consistent code quality and readability standards -# - Support CI/CD workflows with reliable YAML parsing -# - Ensure professional documentation and configuration files -# -# DO NOT MODIFY: These rules represent coding standards -# - If files fail linting, fix the files to meet these standards -# - Do not relax rules to accommodate existing non-compliant files -# - Consistency across repositories is critical for maintainability +# DO NOT MODIFY: These rules represent coding standards. +# If files fail linting, fix the files to meet these standards. extends: default -# Exclude common build artifacts, dependencies, and vendored third-party code ignore: | **/.git/** **/node_modules/** @@ -27,12 +19,11 @@ ignore: | rules: # Allow 'on:' in GitHub Actions workflows (not a boolean value) truthy: - allowed-values: ['true', 'false', 'on', 'off'] + allowed-values: ['true', 'false'] + check-keys: false - # Allow longer lines for URLs and complex expressions - line-length: - max: 120 - level: error + # Disable line-length rule + line-length: disable # Ensure proper indentation indentation: diff --git a/AGENTS.md b/AGENTS.md index 443a0853..3eb2e952 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,14 @@ └── DemaConsulting.BuildMark.Tests/ ``` +# Reference Template + +This repository follows a reference template for structure and file conventions. + +- **Template URL**: `https://github.com/demaconsulting/Agents/raw/refs/heads/template` +- **Repository map**: `{template-url}/repository-map.md` +- **Template files**: `{template-url}/{file-path}` for files described in the map + # Codebase Navigation (ALL Agents) When working with source code, design, or requirements artifacts, read @@ -53,17 +61,16 @@ before searching the filesystem. Before performing any work, agents must read and apply the relevant standards from `.github/standards/`. Use this matrix to determine which to load: -| Work involves... | Load these standards | -|----------------------|------------------------------------------------------------------------------------| -| Any code | `coding-principles.md` | -| C# code | `coding-principles.md`, `csharp-language.md` | -| Any tests | `testing-principles.md` | -| C# tests | `testing-principles.md`, `csharp-testing.md` | -| Requirements | `requirements-principles.md`, `software-items.md`, `reqstream-usage.md` | -| Design docs | `software-items.md`, `design-documentation.md`, `technical-documentation.md` | -| Verification docs | `software-items.md`, `verification-documentation.md`, `technical-documentation.md` | -| Review configuration | `software-items.md`, `reviewmark-usage.md` | -| Any documentation | `technical-documentation.md` | +- **Any code**: `coding-principles.md` +- **C# code**: `coding-principles.md`, `csharp-language.md` +- **Any tests**: `testing-principles.md` +- **C# tests**: `testing-principles.md`, `csharp-testing.md` +- **Requirements**: `requirements-principles.md`, `software-items.md`, `reqstream-usage.md` +- **Design docs**: `software-items.md`, `design-documentation.md`, `technical-documentation.md` +- **Verification docs**: `software-items.md`, `verification-documentation.md`, `technical-documentation.md` +- **Review configuration**: `software-items.md`, `reviewmark-usage.md` +- **Any documentation**: `technical-documentation.md` +- **Structural audit**: `template-sync` agent Load only the standards relevant to your specific task scope. diff --git a/README.md b/README.md index 7d74d489..a39d4b15 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ BuildMark is a .NET command-line tool that generates comprehensive markdown buil history and issue-tracking systems. It analyzes commits, pull requests, and issues to create human-readable build notes, making it easy to integrate release documentation into your CI/CD pipelines and documentation workflows. -For detailed documentation, see the [User Guide](https://github.com/demaconsulting/BuildMark/blob/main/docs/user_guide/introduction.md). -For command-line options, see the [CLI Reference](https://github.com/demaconsulting/BuildMark/blob/main/docs/user_guide/cli-reference.md). +For detailed documentation, see the [BuildMark releases page](https://github.com/demaconsulting/BuildMark/releases). ## Features @@ -151,6 +150,16 @@ buildmark --validate buildmark --validate --results validation-results.trx ``` +## Building + +```pwsh +pwsh ./build.ps1 +``` + +## User Guide + +The BuildMark User Guide is available on the [BuildMark releases page](https://github.com/demaconsulting/BuildMark/releases). + ## Configuration File BuildMark can be configured via a `.buildmark.yaml` file placed in the repository root, with @@ -160,9 +169,6 @@ When no configuration file is present, BuildMark applies built-in defaults that Changes, Bugs Fixed, and Dependency Updates sections with pre-wired routing rules for common label and work-item patterns. -For configuration details and examples, see the -[Configuration Guide](https://github.com/demaconsulting/BuildMark/blob/main/docs/user_guide/configuration.md). - ### Authentication Authentication tokens are not configured in `.buildmark.yaml`. BuildMark resolves them automatically @@ -174,7 +180,8 @@ from environment variables at runtime. `AZURE_DEVOPS_EXT_PAT`, then `SYSTEM_ACCESSTOKEN` (Azure Pipelines), then `az account get-access-token` (Azure CLI). -For more detail see the [Authentication Guide](https://github.com/demaconsulting/BuildMark/blob/main/docs/user_guide/introduction.md#with-github-token). +The `token-variable` option in `.buildmark.yaml` can be used to override the automatic token +resolution chain. ## Self Validation @@ -210,19 +217,15 @@ Each test in the report proves: - **`BuildMark_KnownIssuesReporting`** - Known issues are correctly included when requested. - **`BuildMark_RulesRouting`** - Rules-based item routing assigns items to the correct report sections. -See the [CLI Reference][cli-ref] for more details on the self-validation tests. - -[cli-ref]: https://github.com/demaconsulting/BuildMark/blob/main/docs/user_guide/cli-reference.md#self-validation - On validation failure the tool will exit with a non-zero exit code. ## Extended Item Controls BuildMark supports an optional `buildmark` code block in issue and pull request descriptions to control visibility, type classification, and affected-version ranges. Azure DevOps work items -additionally support native custom fields for the same controls. - -For details, see the [Item Controls Guide](https://github.com/demaconsulting/BuildMark/blob/main/docs/user_guide/item-controls.md). +additionally support native custom fields as an alternative mechanism for visibility and +affected-versions controls. Note: type classification cannot be overridden via Azure DevOps +custom fields; use a `buildmark` block in the item description instead. ## Report Format diff --git a/docs/build_notes/introduction.md b/docs/build_notes/introduction.md index bbf681e9..f489e664 100644 --- a/docs/build_notes/introduction.md +++ b/docs/build_notes/introduction.md @@ -31,3 +31,7 @@ This document is intended for: - Users evaluating what has changed in this release - Project stakeholders tracking progress - Contributors understanding recent changes + +## References + +- [BuildMark Releases](https://github.com/demaconsulting/BuildMark/releases) — compiled compliance documents for each release diff --git a/docs/code_quality/introduction.md b/docs/code_quality/introduction.md index e2f29965..7218e575 100644 --- a/docs/code_quality/introduction.md +++ b/docs/code_quality/introduction.md @@ -33,3 +33,8 @@ This document is intended for: - Quality assurance teams reviewing code quality - Project stakeholders evaluating project health - Contributors understanding quality standards + +## References + +- [BuildMark Releases](https://github.com/demaconsulting/BuildMark/releases) — compiled compliance documents for each release +- [SonarCloud Dashboard](https://sonarcloud.io/dashboard?id=demaconsulting_BuildMark) — live code quality and security analysis diff --git a/docs/code_review_plan/introduction.md b/docs/code_review_plan/introduction.md index a2668a04..3cc5225d 100644 --- a/docs/code_review_plan/introduction.md +++ b/docs/code_review_plan/introduction.md @@ -31,3 +31,9 @@ This document is intended for: - Quality assurance teams validating review coverage - Project stakeholders reviewing compliance status - Auditors verifying that all required files have been reviewed + +## References + +- [BuildMark Releases](https://github.com/demaconsulting/BuildMark/releases) — compiled compliance documents for each release +- [Continuous Compliance](https://github.com/demaconsulting/ContinuousCompliance) — + methodology for continuous compliance evidence generation diff --git a/docs/code_review_report/introduction.md b/docs/code_review_report/introduction.md index 6d428331..8b6657bb 100644 --- a/docs/code_review_report/introduction.md +++ b/docs/code_review_report/introduction.md @@ -31,3 +31,9 @@ This document is intended for: - Quality assurance teams validating review currency - Project stakeholders reviewing compliance status - Auditors verifying that all reviews remain valid for the current release + +## References + +- [BuildMark Releases](https://github.com/demaconsulting/BuildMark/releases) — compiled compliance documents for each release +- [Continuous Compliance](https://github.com/demaconsulting/ContinuousCompliance) — + methodology for continuous compliance evidence generation diff --git a/docs/design/build-mark.md b/docs/design/build-mark.md index 1b0e72bf..d7257f54 100644 --- a/docs/design/build-mark.md +++ b/docs/design/build-mark.md @@ -9,7 +9,7 @@ retrieve commits, issues or work items, pull requests, and version tags, then formats the results as a structured markdown report suitable for embedding in release documentation. -## System Architecture +## Architecture BuildMark is composed of seven subsystems and a top-level entry point: @@ -82,7 +82,22 @@ BuildMark is composed of seven subsystems and a top-level entry point: [Markdown Report File] ``` -## System-Wide Design Constraints +## Dependencies + +- **YamlDotNet**: used for parsing `.buildmark.yaml` configuration files via the + `YamlStream` representation model - see *YamlDotNet Integration Design* +- **System.Net.Http / System.Net.Http.Json** (.NET built-in): used by + `GitHubGraphQLClient` and `AzureDevOpsRestClient` for HTTPS communication with + the GitHub GraphQL and Azure DevOps REST APIs + +## Risk Control Measures + +N/A - BuildMark is a build-tooling utility with no safety-critical functions and +no subsystems that require isolation from each other for risk-control purposes. All +subsystems run in the same process and share the same memory address space; no +inter-process or memory-boundary segregation is required. + +## Design Constraints - **Target framework**: .NET 8, .NET 9, and .NET 10 - **Platform support**: Windows, Linux, macOS diff --git a/docs/design/build-mark/build-notes.md b/docs/design/build-mark/build-notes.md index 84c03957..4b294801 100644 --- a/docs/design/build-mark/build-notes.md +++ b/docs/design/build-mark/build-notes.md @@ -18,6 +18,32 @@ calls `BuildInformation.ToMarkdown` to write the final report file. | `ItemInfo` | `BuildNotes/ItemInfo.cs` | Single issue or pull request in the report | | `WebLink` | `BuildNotes/WebLink.cs` | Hyperlink used for the full-changelog entry | +### Interfaces + +The primary interface consumed by `Program` is: + +| Member | Kind | Description | +| --- | --- | --- | +| `BuildInformation.ToMarkdown(depth, includeKnownIssues)` | Method | Renders assembled build data as markdown string | + +The data records `BuildInformation`, `ItemInfo`, and `WebLink` are the shared +output types exposed to all connectors for assembly, and to `Program` and +`SelfTest` for consumption. + +### Design + +The BuildNotes subsystem is a pure data and rendering layer. Connectors in the +`RepoConnectors` subsystem assemble a `BuildInformation` record by populating its +version tags and item lists (`Changes`, `Bugs`, `KnownIssues`, and optionally +`RoutedSections`). `Program` then calls `BuildInformation.ToMarkdown` to convert +the in-memory model into the final markdown report file. + +`ItemInfo` and `WebLink` are simple immutable records; their construction and +consumption require no additional coordination. The rendering logic in +`ToMarkdown` selects between routed and legacy output modes based on whether +`RoutedSections` is populated, requiring no knowledge of which connector produced +the data. + ### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/build-notes/build-information.md b/docs/design/build-mark/build-notes/build-information.md index 2bca0735..e18a60fd 100644 --- a/docs/design/build-mark/build-notes/build-information.md +++ b/docs/design/build-mark/build-notes/build-information.md @@ -1,6 +1,6 @@ ### BuildInformation -#### Overview +#### Purpose `BuildInformation` is a record in the BuildNotes subsystem that holds all data needed to produce one markdown build-notes report. It is assembled by connectors @@ -30,7 +30,7 @@ public record BuildInformation( optional ordered list of custom report sections populated by `RepoConnectorBase.ApplyRules` when routing rules are configured; `null` when no rules are active -#### Methods +#### Key Methods ##### `ToMarkdown(headingDepth, includeKnownIssues) → string` @@ -61,6 +61,11 @@ The rendered output contains the following sections: 4. **Full Changelog** *(optional)* - hyperlink from `CompleteChangelogLink`, emitted only when the link is non-null. +#### Error Handling + +N/A — `BuildInformation` is an immutable data record. `ToMarkdown` renders content from +already-validated data and does not throw under normal operation. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/build-notes/item-info.md b/docs/design/build-mark/build-notes/item-info.md index 16cc055d..fc92fdc7 100644 --- a/docs/design/build-mark/build-notes/item-info.md +++ b/docs/design/build-mark/build-notes/item-info.md @@ -1,6 +1,6 @@ ### ItemInfo -#### Overview +#### Purpose `ItemInfo` is a record in the BuildNotes subsystem that represents a single issue or pull request entry in the build report. It is produced by connectors and stored @@ -27,6 +27,15 @@ public record ItemInfo( | `Index` | `int` | Numeric issue/PR number for deterministic sorting | | `AffectedVersions` | `VersionIntervalSet?` | Interval set from the `affected-versions` field, or `null` | +#### Key Methods + +N/A — `ItemInfo` is an immutable data record with no methods beyond those auto-generated +by C#. + +#### Error Handling + +N/A — This is an immutable data record with no methods that detect or propagate errors. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/build-notes/web-link.md b/docs/design/build-mark/build-notes/web-link.md index eaa948b1..48123da4 100644 --- a/docs/design/build-mark/build-notes/web-link.md +++ b/docs/design/build-mark/build-notes/web-link.md @@ -1,6 +1,6 @@ ### WebLink -#### Overview +#### Purpose `WebLink` is a record in the BuildNotes subsystem that represents a hyperlink. It is used by `BuildInformation` to carry the optional complete-changelog link that @@ -17,6 +17,15 @@ public record WebLink( - `LinkText` (`string`) is the human-readable link label shown in the report. - `TargetUrl` (`string`) is the fully-qualified URL that the link points to. +#### Key Methods + +N/A — `WebLink` is an immutable data record with no methods beyond those auto-generated +by C#. + +#### Error Handling + +N/A — This is an immutable data record with no methods that detect or propagate errors. + #### Interactions - `BuildInformation` holds a `WebLink?` as `CompleteChangelogLink`. diff --git a/docs/design/build-mark/cli.md b/docs/design/build-mark/cli.md index 993b99c8..33d1286b 100644 --- a/docs/design/build-mark/cli.md +++ b/docs/design/build-mark/cli.md @@ -40,6 +40,15 @@ subsystems receive a `Context` from the caller rather than creating one themselv > `Create(args)` throws `ArgumentException` for invalid or malformed arguments. +### Design + +The Cli subsystem contains a single unit (`Context`), so there is no inter-unit +collaboration to describe. `Context.Create` parses the argument array and opens +the optional log file. The resulting `Context` object is then passed by `Program` +to every other subsystem that needs to write output, read flags, or set the exit +code. No other subsystem creates a `Context`; all output and exit-code management +flows through the single instance created at startup. + ### Interactions The Cli subsystem has no dependencies on other BuildMark subsystems. `Program` diff --git a/docs/design/build-mark/cli/context.md b/docs/design/build-mark/cli/context.md index 2778724e..754b2585 100644 --- a/docs/design/build-mark/cli/context.md +++ b/docs/design/build-mark/cli/context.md @@ -1,6 +1,6 @@ ### Context -#### Overview +#### Purpose `Context` is the sole unit in the Cli subsystem. It owns the lifecycle of command-line argument parsing, console and log-file output, and exit-code tracking. @@ -39,7 +39,7 @@ so that any open log file is properly flushed and closed. |------------|-------|---------------------------------------------------| | `ExitCode` | `int` | `0` if no errors have been written; `1` otherwise | -#### Methods +#### Key Methods ##### `Create(string[] args) → Context` @@ -87,3 +87,17 @@ iterates over the argument array and classifies each token: - `--depth` additionally validates that the value is a positive integer. - The legacy `--report-depth` argument is also accepted as an undocumented alias for `--depth`. - Any unrecognized token causes `ArgumentException` to be thrown. + +#### Error Handling + +`Create(string[] args)` throws `ArgumentException` when an unrecognized flag or missing +value argument is encountered, and `InvalidOperationException` if the log file path cannot +be opened. `OpenLogFile` throws `InvalidOperationException` if the target directory does +not exist. `WriteError` sets the internal error flag so that `ExitCode` returns `1` but +does not throw. + +#### Interactions + +`Program` creates exactly one `Context` per invocation using `Context.Create(args)` and +passes it to `Validation`, `BuildMarkConfigReader`, and all connector subsystems. `Context` +must be disposed after use to flush and close any open log file. diff --git a/docs/design/build-mark/configuration.md b/docs/design/build-mark/configuration.md index 8489b712..3bbb44aa 100644 --- a/docs/design/build-mark/configuration.md +++ b/docs/design/build-mark/configuration.md @@ -129,6 +129,21 @@ problem with its file path, line number, severity, and description. | `Label` | Property | List of label values; the rule matches when any label is present | | `WorkItemType` | Property | List of work-item type values; rule matches when any type matches | +### Design + +`BuildMarkConfigReader.ReadAsync` is the subsystem's only entry point. It uses +the YamlDotNet `YamlStream` representation model to parse the raw YAML content, +then walks the resulting node tree manually to populate the strongly-typed +configuration objects. Each node walk is guarded so that a missing or invalid +node creates a `ConfigurationIssue` record rather than throwing an exception. + +All constructed objects — `BuildMarkConfig`, `ConnectorConfig`, +`GitHubConnectorConfig`, `AzureDevOpsConnectorConfig`, `ReportConfig`, +`SectionConfig`, `RuleConfig`, and `RuleMatchConfig` — are assembled in a single +pass and returned wrapped in a `ConfigurationLoadResult`. `Program` calls +`result.ReportTo(context)` to surface issues to the user and checks `HasErrors` +before continuing to the connector and report-generation steps. + ### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/configuration/build-mark-config-reader.md b/docs/design/build-mark/configuration/build-mark-config-reader.md index 4b75fe11..dde9fe9e 100644 --- a/docs/design/build-mark/configuration/build-mark-config-reader.md +++ b/docs/design/build-mark/configuration/build-mark-config-reader.md @@ -1,6 +1,6 @@ ### BuildMarkConfigReader -#### Overview +#### Purpose `BuildMarkConfigReader` is a static utility class responsible for reading and deserializing the optional `.buildmark.yaml` file from the repository root. It uses the YamlDotNet library's @@ -10,7 +10,11 @@ to produce a strongly-typed `BuildMarkConfig` object. The method always returns a `ConfigurationLoadResult` and never throws. Parse errors and validation warnings are captured as `ConfigurationIssue` records within the result. -#### Interface +#### Data Model + +N/A — `BuildMarkConfigReader` is a static utility class with no instance state. + +#### Key Methods | Member | Kind | Description | |-------------------|---------------|---------------------------------------------------------------------------| @@ -26,6 +30,12 @@ Looks for a `.buildmark.yaml` file at the supplied path (normally the repository - If the file is valid, returns a result with a fully populated `BuildMarkConfig` and an empty issues list. +#### Error Handling + +`ReadAsync` never throws. All YAML parse errors and validation warnings are captured as +`ConfigurationIssue` records within the returned `ConfigurationLoadResult`. This ensures +that `Program` can always inspect issues via `ReportTo` regardless of the file content. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/configuration/build-mark-config.md b/docs/design/build-mark/configuration/build-mark-config.md index 22f5cf9e..93d7dbf1 100644 --- a/docs/design/build-mark/configuration/build-mark-config.md +++ b/docs/design/build-mark/configuration/build-mark-config.md @@ -1,6 +1,6 @@ ### BuildMarkConfig -#### Overview +#### Purpose `BuildMarkConfig` is the top-level configuration data model for BuildMark. It holds all settings read from the `.buildmark.yaml` file, including connector configuration, report @@ -18,6 +18,17 @@ to obtain a default configuration with built-in section and rule definitions. | `Sections` | `IList` | Ordered list of report section definitions | | `Rules` | `IList` | List of item routing rules | +#### Key Methods + +- `CreateDefault()` — Static factory returning a `BuildMarkConfig` populated with + built-in section and rule definitions, used when no `.buildmark.yaml` file is + present + +#### Error Handling + +N/A — `BuildMarkConfig` is a configuration data record. No methods on this type detect +or propagate errors; all validation occurs in `BuildMarkConfigReader`. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/configuration/configuration-issue.md b/docs/design/build-mark/configuration/configuration-issue.md index caf93347..e5b94edb 100644 --- a/docs/design/build-mark/configuration/configuration-issue.md +++ b/docs/design/build-mark/configuration/configuration-issue.md @@ -1,6 +1,6 @@ ### ConfigurationIssue -#### Overview +#### Purpose `ConfigurationIssue` is an immutable record representing a single problem found while reading or validating the `.buildmark.yaml` file. Each issue carries a file path, line number, @@ -22,6 +22,14 @@ severity, and human-readable description. | `Warning` | Non-fatal issue; tool continues and exit code is 0 | | `Error` | Fatal issue; tool reports all errors, exits with code 1 | +#### Key Methods + +N/A — `ConfigurationIssue` is an immutable data record with no methods beyond those auto-generated by C#. + +#### Error Handling + +N/A — This is an immutable data record with no methods that detect or propagate errors. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/configuration/configuration-load-result.md b/docs/design/build-mark/configuration/configuration-load-result.md index 942fcd66..6dcb8b48 100644 --- a/docs/design/build-mark/configuration/configuration-load-result.md +++ b/docs/design/build-mark/configuration/configuration-load-result.md @@ -1,6 +1,6 @@ ### ConfigurationLoadResult -#### Overview +#### Purpose `ConfigurationLoadResult` is an immutable record that carries the output of `BuildMarkConfigReader.ReadAsync`. It holds the parsed configuration (or `null` if parsing @@ -18,11 +18,24 @@ surface any issues to the user and set the exit code when errors are present. | `HasErrors` | Property | `true` when any issue has `Severity` of `Error` | | `ReportTo(context)` | Method | Writes all issues to `Context`; sets exit code on errors | -##### `ReportTo(Context context)` +##### ReportTo Overview Iterates `Issues` and writes each one to the context output. If any issue has severity `Error`, sets `context.ExitCode` to 1. +#### Key Methods + +##### `ReportTo(Context context)` + +Iterates `Issues` and writes each one to the context output. Issues with `Error` severity +are written via `context.WriteError`, which sets the exit code to `1`. Non-error issues are +written via `context.WriteLine`. Does not throw exceptions. + +#### Error Handling + +N/A — `ConfigurationLoadResult` is an immutable record. `ReportTo(context)` writes issues +to the context output and may set the exit code to `1`, but does not throw exceptions. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/configuration/connector-config.md b/docs/design/build-mark/configuration/connector-config.md index 3bb4a153..bce86399 100644 --- a/docs/design/build-mark/configuration/connector-config.md +++ b/docs/design/build-mark/configuration/connector-config.md @@ -1,6 +1,6 @@ ### ConnectorConfig, GitHubConnectorConfig, AzureDevOpsConnectorConfig -#### Overview +#### Purpose These three configuration data models define the connector selection and per-connector settings read from the `connector:` section of `.buildmark.yaml`. @@ -9,7 +9,7 @@ settings read from the `connector:` section of `.buildmark.yaml`. settings. `GitHubConnectorConfig` holds GitHub-specific overrides. `AzureDevOpsConnectorConfig` holds Azure DevOps-specific connection details. -#### Data Models +#### Data Model ##### ConnectorConfig @@ -39,6 +39,15 @@ holds Azure DevOps-specific connection details. | `TokenVariable` | `string?` | Name of the env variable holding the access token; `null` uses well-known names | | `AreaPath` | `string?` | Scopes WIQL queries to area path; defaults to `{Project}` when `null`; empty = all | +#### Key Methods + +N/A — `ConnectorConfig`, `GitHubConnectorConfig`, and `AzureDevOpsConnectorConfig` are +immutable configuration data records with no methods beyond those auto-generated by C#. + +#### Error Handling + +N/A — These are immutable configuration data records with no methods that detect or propagate errors. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/configuration/report-config.md b/docs/design/build-mark/configuration/report-config.md index e3e36b6e..a58fd1d4 100644 --- a/docs/design/build-mark/configuration/report-config.md +++ b/docs/design/build-mark/configuration/report-config.md @@ -1,6 +1,6 @@ ### ReportConfig -#### Overview +#### Purpose `ReportConfig` is a configuration data model holding the optional report output settings read from the `report:` section of `.buildmark.yaml`. All properties are nullable; when @@ -14,6 +14,15 @@ absent, `Program` uses CLI argument values or built-in defaults. | `Depth` | `int?` | Optional heading depth for report sections; `null` defaults to 1 | | `IncludeKnownIssues` | `bool?` | Optional flag to include known issues; `null` defaults to `false` | +#### Key Methods + +N/A — `ReportConfig` is an immutable configuration data record with no methods beyond those +auto-generated by C#. + +#### Error Handling + +N/A — This is an immutable configuration data record with no methods that detect or propagate errors. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/configuration/rule-config.md b/docs/design/build-mark/configuration/rule-config.md index 61ac6cf2..b550c15b 100644 --- a/docs/design/build-mark/configuration/rule-config.md +++ b/docs/design/build-mark/configuration/rule-config.md @@ -1,6 +1,6 @@ ### RuleConfig and RuleMatchConfig -#### Overview +#### Purpose `RuleConfig` is a configuration data model representing a single item routing rule read from the `rules:` list in `.buildmark.yaml`. Each rule carries match conditions and a destination @@ -8,7 +8,7 @@ section ID. `RuleMatchConfig` holds the conditions that must be satisfied for a Both types are defined in `Configuration/RuleConfig.cs`. -#### Data Models +#### Data Model ##### RuleConfig @@ -24,6 +24,15 @@ Both types are defined in `Configuration/RuleConfig.cs`. | `Label` | `IList` | List of label values; rule matches when any label is present | | `WorkItemType` | `IList` | List of work-item type values; rule matches when any type matches | +#### Key Methods + +N/A — `RuleConfig` and `RuleMatchConfig` are immutable configuration data records with no +methods beyond those auto-generated by C#. + +#### Error Handling + +N/A — These are immutable configuration data records with no methods that detect or propagate errors. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/configuration/section-config.md b/docs/design/build-mark/configuration/section-config.md index 21b3e74e..eb5535a9 100644 --- a/docs/design/build-mark/configuration/section-config.md +++ b/docs/design/build-mark/configuration/section-config.md @@ -1,6 +1,6 @@ ### SectionConfig -#### Overview +#### Purpose `SectionConfig` is a configuration data model representing a single report section definition read from the `sections:` list in `.buildmark.yaml`. Each section has a unique identifier @@ -13,6 +13,15 @@ used by routing rules and a display title used as the markdown heading. | `Id` | `string` | Unique identifier for the section | | `Title` | `string` | Display title for the report section | +#### Key Methods + +N/A — `SectionConfig` is an immutable configuration data record with no methods beyond those +auto-generated by C#. + +#### Error Handling + +N/A — This is an immutable configuration data record with no methods that detect or propagate errors. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/program.md b/docs/design/build-mark/program.md index 785d84ee..a779cf66 100644 --- a/docs/design/build-mark/program.md +++ b/docs/design/build-mark/program.md @@ -1,6 +1,6 @@ ## Program -### Overview +### Purpose `Program` is the top-level unit of BuildMark. It owns the application entry point, creates the `Context` object from command-line arguments, and dispatches execution @@ -23,7 +23,7 @@ The version is resolved at startup by inspecting `AssemblyInformationalVersionAt first, then falling back to `assembly.GetName().Version`, and finally defaulting to `"0.0.0"` if neither attribute is present. -### Methods +### Key Methods #### `Main(string[] args) → int` @@ -97,6 +97,15 @@ Synchronously invokes `BuildMarkConfigReader.ReadAsync(Environment.CurrentDirect Called from both the lint branch of `Run` and from `ProcessBuildNotes` to keep the async-to-sync bridging in one place. +### Error Handling + +`Main` catches `ArgumentException` and `InvalidOperationException` from `Context` +construction and writes them directly to `Console.Error`, returning exit code `1`. Any +other exception is re-thrown. `ProcessBuildNotes` catches file-system exceptions during +report writing and reports them via `context.WriteError` without propagating the exception; +`InvalidOperationException` from `connector.GetBuildInformationAsync` is caught, reported, +and causes early return. + ### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/repo-connectors.md b/docs/design/build-mark/repo-connectors.md index 0388c307..c00c8225 100644 --- a/docs/design/build-mark/repo-connectors.md +++ b/docs/design/build-mark/repo-connectors.md @@ -45,6 +45,32 @@ self-test. |-------------------------------------|--------|----------------------------------| | `GetBuildInformationAsync(version)` | Method | Fetch complete build information | +### Design + +The RepoConnectors subsystem separates the connector contract, shared +infrastructure, and concrete implementations into three layers: + +1. **Contract layer**: `IRepoConnector` defines the single public method all + connectors must implement. `RepoConnectorFactory` resolves the appropriate + concrete connector at runtime without the caller needing to know which + platform is in use. + +2. **Base layer**: `RepoConnectorBase` provides shared behavior inherited by + all production connectors — token resolution is handled in the concrete + classes, while `Configure`, `HasRules`, `ApplyRules`, `FindVersionIndex`, and + `RunCommandAsync` are provided by the base. `ItemControlsParser` and + `ItemControlsInfo` are shared utilities called by every connector to apply + buildmark block overrides per item. `ItemRouter` is the central routing + engine called by `RepoConnectorBase.ApplyRules` to distribute items into + configured report sections. + +3. **Implementation layer**: `GitHub`, `AzureDevOps`, and `Mock` child subsystems + each contain a connector that inherits from `RepoConnectorBase` together with + platform-specific client and type definitions. Each connector fetches platform + data, normalizes it into `ItemInfo` records, applies item-controls overrides, + calls `ApplyRules` when routing is configured, and returns a `BuildInformation` + record. + ### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/repo-connectors/azure-devops.md b/docs/design/build-mark/repo-connectors/azure-devops.md index 312dd3e7..e801eb0f 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops.md +++ b/docs/design/build-mark/repo-connectors/azure-devops.md @@ -45,6 +45,31 @@ both buildmark code blocks in the work item description and Azure DevOps custom (`Custom.Visibility`, `Custom.AffectedVersions`). Custom fields take precedence over buildmark blocks when both are present. +#### Interfaces + +The AzureDevOps subsystem exposes `AzureDevOpsRepoConnector`, which implements +`IRepoConnector`. All other types in the subsystem are internal. + +| Member | Kind | Description | +| --- | --- | --- | +| `AzureDevOpsRepoConnector(config)` | Constructor | Create the connector with optional configuration overrides | +| `GetBuildInformationAsync(version)` | Method | Fetch complete build information from the Azure DevOps REST API | + +#### Design + +`AzureDevOpsRepoConnector` orchestrates the subsystem's data flow. It uses +`AzureDevOpsRestClient` for all HTTPS communication, `AzureDevOpsApiTypes` +records as serialization targets, and `WorkItemMapper` to transform raw work +item responses into `ItemInfo` records. + +The connector calls `ItemControlsParser.Parse` on each work item description +body and also reads `Custom.Visibility` and `Custom.AffectedVersions` custom +fields directly. `WorkItemMapper` merges the two sources (custom fields take +precedence) and returns the final `ItemInfo` record or `null` when the item +should be excluded. When routing rules have been configured, the connector passes +all collected items to `ApplyRules` (inherited from `RepoConnectorBase`) to +populate `BuildInformation.RoutedSections`. + #### Interactions | Unit / Subsystem | Role | @@ -56,3 +81,13 @@ buildmark blocks when both are present. | `AzureDevOpsConnectorConfig` | Supplies organization URL, project, and repository overrides | | `ItemControlsParser` | Parses buildmark blocks from work item description bodies | | `BuildInformation` | The output record assembled and returned by `AzureDevOpsRepoConnector` | + +#### Error Handling + +`AzureDevOpsRestClient` propagates HTTP errors and JSON deserialization failures as +`InvalidOperationException`. The exception message includes the Azure DevOps error +message read via `TryReadAdoErrorMessageAsync` when the response body contains an +Azure DevOps error object; otherwise the raw HTTP status code is included. + +`AzureDevOpsRepoConnector` does not suppress these exceptions; a failed REST API call +therefore aborts `GetBuildInformationAsync` and propagates the exception to the caller. diff --git a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md index b3605f6f..27eb8cee 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md +++ b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md @@ -1,6 +1,6 @@ #### AzureDevOpsApiTypes -##### Overview +##### Purpose `AzureDevOpsApiTypes` is the collection of internal record types used by the Azure DevOps subsystem to represent REST API request and response payloads. These @@ -18,7 +18,7 @@ process safely and predictably. `AzureDevOpsRestClient` can deserialize API responses without reflection workarounds or third-party JSON libraries -##### Key Types +##### Data Model All record types are defined as C# `record` types with init-only properties using conventional PascalCase property names. No `[JsonPropertyName]` attributes are @@ -110,6 +110,24 @@ Generic wrapper for paginated collection responses from the Azure DevOps REST AP Fields: `count`, `value` (list of `T`) +###### `AzureDevOpsApiError` + +Error response body returned by the Azure DevOps REST API when a request fails. +Used by `AzureDevOpsRestClient.TryReadAdoErrorMessageAsync` to extract a human-readable +error message from non-success HTTP responses. + +Fields: `message`, `typeKey` + +##### Key Methods + +N/A — `AzureDevOpsApiTypes` is a collection of immutable record types used purely for JSON +deserialization. No methods beyond C#-generated record members are defined. + +##### Error Handling + +N/A — These are immutable data record types used purely for JSON deserialization. No +methods detect or propagate errors. + ##### Interactions - `AzureDevOpsRestClient` uses these records as serialization and deserialization diff --git a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md index 7bf0b98d..68cb2a22 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md +++ b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md @@ -1,6 +1,6 @@ #### AzureDevOpsRepoConnector -##### Overview +##### Purpose `AzureDevOpsRepoConnector` is the production unit in the RepoConnectors/AzureDevOps subsystem. It implements `RepoConnectorBase` and uses `AzureDevOpsRestClient` to @@ -107,7 +107,7 @@ The `AzureDevOpsRestClient` returns the following record types: - **`AzureDevOpsCollectionResponse`** - wraps paginated responses with a count and value list. -##### Methods +##### Key Methods ###### `ParseAzureDevOpsUrl(url) → (organizationUrl, project, repository)` @@ -173,6 +173,14 @@ Main entry point. Performs the following steps: are configured, items remain in the legacy `Changes`, `Bugs`, and `KnownIssues` lists. Return the assembled `BuildInformation` record. +##### Error Handling + +`GetBuildInformationAsync` throws `InvalidOperationException` when no Azure DevOps token +can be resolved, when `ParseAzureDevOpsUrl` receives an unsupported URL format (propagated +as `ArgumentException`), or when a git command fails. These exceptions propagate to +`Program.ProcessBuildNotes`, which catches them, writes an error message via +`context.WriteError`, and returns early without generating a report. + ##### Interactions - `AzureDevOpsConnectorConfig` is received from `RepoConnectorFactory` and overrides diff --git a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md index e793d887..f478894e 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md +++ b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md @@ -1,6 +1,6 @@ #### AzureDevOpsRestClient -##### Overview +##### Purpose `AzureDevOpsRestClient` is the Azure DevOps subsystem unit responsible for issuing paginated REST API requests to the Azure DevOps API and translating the responses @@ -38,7 +38,13 @@ The sole exception is the `AzureDevOpsWorkItem.Fields` dictionary - its keys are Azure DevOps field reference names (e.g. `System.WorkItemType`, `Custom.Visibility`) and are preserved as-is without any naming transformation. -##### Methods +##### Data Model + +`AzureDevOpsRestClient` holds a single `HttpClient` instance configured with the +organization URL as the base address and the resolved authentication header (Basic for +PAT tokens, Bearer for Entra ID tokens). + +##### Key Methods The client provides the following methods for retrieving the repository data needed to build a `BuildInformation` record: @@ -50,7 +56,7 @@ Fetches repository metadata for the specified repository. Endpoint: `GET /{organization}/{project}/_apis/git/repositories/{repository}?api-version=6.0` Returns an `AzureDevOpsRepository` record containing the repository id, name, and -remote URL. +remote URL. Throws when the request is unsuccessful (including HTTP 404). ###### `GetTagsAsync(repositoryId)` @@ -111,6 +117,24 @@ Endpoint: `POST /{organization}/{project}/_apis/wit/wiql?api-version=6.0` Returns an `AzureDevOpsWorkItemQuery` record containing the list of matching work item id references. +##### Error Handling + +HTTP and deserialization errors from the underlying `HttpClient` are propagated to +`AzureDevOpsRepoConnector`. Network failures, authentication errors (HTTP 401/403), and +malformed JSON responses result in exceptions that propagate up to +`Program.ProcessBuildNotes`. + +`GetRepositoryAsync` additionally throws `InvalidOperationException` when the HTTP +response is successful but JSON deserialization returns `null` (the `result ?? throw` +pattern). This guards against a response body that is valid JSON but does not +deserialize into the expected `AzureDevOpsRepository` record. + +When an HTTP error response includes a JSON body in the Azure DevOps error format, the +internal `TryReadAdoErrorMessageAsync` helper deserializes the body into an +`AzureDevOpsApiError` record and extracts the `message` field to include in the thrown +`InvalidOperationException`. This provides a human-readable error description (e.g., +`"The area path does not exist."`) rather than a raw HTTP status code. + ##### Interactions - `AzureDevOpsRepoConnector` creates and calls `AzureDevOpsRestClient`. diff --git a/docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md b/docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md index 7bffe670..653c874d 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md +++ b/docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md @@ -1,6 +1,6 @@ #### WorkItemMapper -##### Overview +##### Purpose `WorkItemMapper` maps `AzureDevOpsWorkItem` records from the Azure DevOps REST API into `ItemInfo` records for the `BuildInformation` model. It centralizes work item @@ -53,7 +53,7 @@ Item controls are extracted from two sources and merged: present for the same control. If a custom field value is non-null, it supersedes the corresponding value from the buildmark block. -##### Methods +##### Key Methods ###### `MapWorkItemToItemInfo(workItem)` @@ -67,7 +67,8 @@ Steps: 3. Apply work item type mapping to determine the normalized type. 4. Call `ExtractItemControls(workItem)` to obtain any item controls overrides. 5. If item controls specify a visibility of `internal`, return `null` to signal that - the item should be excluded. + the item should be excluded. Items with `visibility: public` (or no visibility + override) are included normally; there is no separate "force include" logic. 6. If item controls specify a type override, apply it to the normalized type. 7. Construct and return the `ItemInfo` record with the title, url, type, and affected versions. @@ -100,6 +101,12 @@ Steps: block result. 4. Return the merged `ItemControlsInfo`, or `null` if no controls were found. +##### Error Handling + +`MapWorkItemToItemInfo` returns `null` rather than throwing when a work item should be +excluded (suppressed state or `visibility: internal`). Missing or unexpected field values +are handled defensively; no exceptions are thrown for absent dictionary keys. + ##### Interactions - `AzureDevOpsRepoConnector` calls `WorkItemMapper` to convert REST API work item diff --git a/docs/design/build-mark/repo-connectors/github.md b/docs/design/build-mark/repo-connectors/github.md index 29b011d8..f3bb7f0a 100644 --- a/docs/design/build-mark/repo-connectors/github.md +++ b/docs/design/build-mark/repo-connectors/github.md @@ -33,6 +33,30 @@ supports GitHub Enterprise by accepting an alternative base URL. Internal C# records that mirror the GraphQL schema types returned by GitHub. Used as the deserialization target for responses from `GitHubGraphQLClient`. +#### Interfaces + +The GitHub subsystem exposes `GitHubRepoConnector`, which implements +`IRepoConnector`. All other types in the subsystem are internal. + +| Member | Kind | Description | +| --- | --- | --- | +| `GitHubRepoConnector(config)` | Constructor | Create the connector with optional configuration overrides | +| `GetBuildInformationAsync(version)` | Method | Fetch complete build information from the GitHub GraphQL API | + +#### Design + +`GitHubRepoConnector` orchestrates the subsystem's data flow. It uses +`GitHubGraphQLClient` for all HTTPS communication, `GitHubGraphQLTypes` records +as GraphQL deserialization targets, and `ItemControlsParser` to extract buildmark +block overrides from issue and pull request description bodies. + +The connector calls `ItemControlsParser.Parse` on the `body` field of each issue +and pull request. If a non-null `ItemControlsInfo` is returned, the connector +applies visibility, type, and affected-versions overrides before adding the item +to the appropriate list. When routing rules have been configured, the connector +passes all collected items to `ApplyRules` (inherited from `RepoConnectorBase`) +to populate `BuildInformation.RoutedSections`. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/repo-connectors/github/github-graphql-client.md b/docs/design/build-mark/repo-connectors/github/github-graphql-client.md index d3544bcd..a226c4d8 100644 --- a/docs/design/build-mark/repo-connectors/github/github-graphql-client.md +++ b/docs/design/build-mark/repo-connectors/github/github-graphql-client.md @@ -1,6 +1,6 @@ #### GitHubGraphQLClient -##### Overview +##### Purpose `GitHubGraphQLClient` is the GitHub subsystem unit responsible for issuing paginated GraphQL requests to the GitHub API and translating the responses into @@ -11,31 +11,59 @@ GitHub API communication to this client. The class provides two constructors: -- **Public constructor** - accepts a GitHub authentication token and creates an - owned `HttpClient` configured with the token. Used by `GitHubRepoConnector` in - production. -- **Internal constructor** - accepts an existing `HttpClient` directly. Used by - tests to inject a mock `HttpClient` without network access. +- **Public constructor** - accepts a GitHub authentication token and an optional + `graphqlEndpoint` URL, then creates an owned `HttpClient` configured with the + token and wraps it in a `GraphQLHttpClient` instance (from the `GraphQL.Client.Http` + package). Used by `GitHubRepoConnector` in production. When `graphqlEndpoint` is + omitted, the default GitHub GraphQL API endpoint (`https://api.github.com/graphql`) + is used. For GitHub Enterprise Server, supply the enterprise-specific endpoint + (e.g., `https://your-github-enterprise/api/graphql`). +- **Internal constructor** - accepts an existing `HttpClient` directly and an optional + `graphqlEndpoint` URL. Used by tests to inject a mock `HttpClient` without network + access. ##### Lifecycle `GitHubGraphQLClient` implements `IDisposable`. When created via the public -constructor, the instance owns its `HttpClient` and disposes it when the client -is disposed. When created via the internal constructor, the caller retains -ownership of the `HttpClient` and the client does not dispose it. +constructor, the instance owns its `GraphQLHttpClient` (and the `HttpClient` it wraps) +and disposes them when the client is disposed. When created via the internal +constructor, the caller retains ownership of the `HttpClient` and the client does +not dispose it. Callers that construct `GitHubGraphQLClient` via the public constructor must wrap usage in a `using` statement or otherwise dispose the instance to release the underlying HTTP connection resources. -##### Error Strategy +##### Error Handling -All API methods catch exceptions from the underlying `HttpClient` and return +All API methods catch exceptions from the underlying `GraphQLHttpClient` and return empty lists rather than propagating the exception to the caller. This allows the connector to continue with partial data when the GitHub API is transiently unavailable. -##### Methods +> **Note**: Because exceptions are silently swallowed and an empty list is returned, +> runtime failures (network errors, authentication failures, malformed responses) are +> not observable by the caller. Diagnostics require inspecting log output or +> correlating an unexpectedly empty result set with network or authentication issues. + +##### Data Model + +`GitHubGraphQLClient` holds a single `GraphQLHttpClient` instance (from the external +`GraphQL.Client.Http` NuGet package). The `GraphQLHttpClient` internally manages an +`HttpClient` for HTTPS transport. When constructed via the public constructor, the +client owns both the `GraphQLHttpClient` and the underlying `HttpClient` and disposes +them on disposal. When constructed via the internal constructor (for test injection), +the caller retains ownership of the `HttpClient` and the client does not dispose +the injected instance. + +##### Dependencies + +| Dependency | Package | Purpose | +| -------------------------- | ------------------------------------------ | ---------------------------------------- | +| `GraphQLHttpClient` | `GraphQL.Client.Http` | Sends GraphQL queries over HTTPS | +| `SystemTextJsonSerializer` | `GraphQL.Client.Serializer.SystemTextJson` | Serializes/deserializes GraphQL payloads | + +##### Key Methods The client provides methods for retrieving the repository data needed to build a `BuildInformation` record: diff --git a/docs/design/build-mark/repo-connectors/github/github-graphql-types.md b/docs/design/build-mark/repo-connectors/github/github-graphql-types.md index 0e095357..26fea4f0 100644 --- a/docs/design/build-mark/repo-connectors/github/github-graphql-types.md +++ b/docs/design/build-mark/repo-connectors/github/github-graphql-types.md @@ -1,6 +1,6 @@ #### GitHubGraphQLTypes -##### Overview +##### Purpose `GitHubGraphQLTypes` is the collection of internal record types used by the GitHub subsystem to represent GraphQL request and response payloads. These types @@ -14,6 +14,28 @@ typed objects that `GitHubRepoConnector` can process safely and predictably. - Preserve issue and pull request description body fields for item-controls parsing +##### Data Model + +The following record types are defined for GitHub GraphQL request and response serialization: + +- **Tag and release nodes**: types representing GitHub tag and release response objects, + including tag names and associated commit SHAs +- **Issue and pull request nodes**: `IssueNode` and `PullRequestNode` records carrying + number, title, URL, state, label connections, and description body fields +- **Commit nodes**: records carrying commit SHAs from GraphQL commit range queries +- **Pagination types**: `PageInfo`, connection, and edge wrapper types carrying `endCursor` + and `hasNextPage` fields for cursor-based pagination across all paginated queries + +##### Key Methods + +N/A — `GitHubGraphQLTypes` is a collection of record type definitions with no methods +beyond C# record-generated members. + +##### Error Handling + +N/A — These are immutable record types used purely for JSON deserialization. No methods +detect or propagate errors. + ##### Interactions - `GitHubGraphQLClient` uses these records as serialization and deserialization diff --git a/docs/design/build-mark/repo-connectors/github/github-repo-connector.md b/docs/design/build-mark/repo-connectors/github/github-repo-connector.md index 44b3078c..f14861d5 100644 --- a/docs/design/build-mark/repo-connectors/github/github-repo-connector.md +++ b/docs/design/build-mark/repo-connectors/github/github-repo-connector.md @@ -1,6 +1,6 @@ #### GitHubRepoConnector -##### Overview +##### Purpose `GitHubRepoConnector` is the production unit in the RepoConnectors/GitHub subsystem. It implements `RepoConnectorBase` and uses `GitHubGraphQLClient` to @@ -108,7 +108,7 @@ overrides are applied: When no `buildmark` block is present, the existing label-based rules apply unchanged. -##### Methods +##### Key Methods ###### `GetBuildInformationAsync(Version? version) → BuildInformation` @@ -153,6 +153,14 @@ Main entry point. Performs the following steps: 13. Generate the full changelog URL from the baseline and target tags. 14. Return the assembled `BuildInformation` record. +##### Error Handling + +`GetBuildInformationAsync` throws `InvalidOperationException` when no GitHub token can be +resolved, when no release matches the current commit hash and no version is specified +explicitly, or when a git command fails. These exceptions propagate to +`Program.ProcessBuildNotes`, which catches them, writes an error message via +`context.WriteError`, and returns early without generating a report. + ##### Interactions - `GitHubConnectorConfig` is received from `RepoConnectorFactory` and overrides diff --git a/docs/design/build-mark/repo-connectors/item-controls-info.md b/docs/design/build-mark/repo-connectors/item-controls-info.md index 628131ec..77a9b327 100644 --- a/docs/design/build-mark/repo-connectors/item-controls-info.md +++ b/docs/design/build-mark/repo-connectors/item-controls-info.md @@ -1,6 +1,6 @@ ### ItemControlsInfo -#### Overview +#### Purpose `ItemControlsInfo` is the data record used by the RepoConnectors subsystem to carry the controls extracted from a `buildmark` block. It stores the optional @@ -22,6 +22,15 @@ public record ItemControlsInfo( - `AffectedVersions` (`VersionIntervalSet?`) stores the optional affected-version interval set. +#### Key Methods + +N/A — `ItemControlsInfo` is an immutable data record with no methods beyond those +auto-generated by C#. + +#### Error Handling + +N/A — This is an immutable data record with no methods that detect or propagate errors. + #### Interactions - `ItemControlsParser` creates an `ItemControlsInfo` instance when a `buildmark` diff --git a/docs/design/build-mark/repo-connectors/item-controls-parser.md b/docs/design/build-mark/repo-connectors/item-controls-parser.md index 8b59adf7..a1760e0b 100644 --- a/docs/design/build-mark/repo-connectors/item-controls-parser.md +++ b/docs/design/build-mark/repo-connectors/item-controls-parser.md @@ -1,6 +1,6 @@ ### ItemControlsParser and ItemControlsInfo -#### Overview +#### Purpose `ItemControlsParser` is a static utility class that extracts a `buildmark` fenced code block from an issue or pull request description and parses its @@ -48,11 +48,11 @@ Recognized keys: Unrecognized values for a known key are silently ignored (the field remains `null`). -#### Data Model - ItemControlsInfo +#### Data Model See `item-controls-info.md` for the `ItemControlsInfo` data model definition. -#### Methods +#### Key Methods ##### `ItemControlsParser.Parse(string? description) → ItemControlsInfo?` @@ -68,6 +68,12 @@ Entry point for the parser. Steps: 7. Build and return an `ItemControlsInfo` from the recognized keys, or `null` if no recognized keys were found. +#### Error Handling + +`Parse` returns `null` rather than throwing for any of: null or empty input, missing +`buildmark` code block, unrecognized key-value pairs, or unrecognized values. No exceptions +propagate to callers; invalid or unrecognized content is silently discarded. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/repo-connectors/item-router.md b/docs/design/build-mark/repo-connectors/item-router.md index d55b18b9..5b53eea8 100644 --- a/docs/design/build-mark/repo-connectors/item-router.md +++ b/docs/design/build-mark/repo-connectors/item-router.md @@ -1,6 +1,6 @@ ### ItemRouter -#### Overview +#### Purpose `ItemRouter` is a shared static utility in the RepoConnectors subsystem that routes a list of `ItemInfo` objects into report sections. It applies a list of `RuleConfig` @@ -10,7 +10,11 @@ routing logic across multiple connector implementations. All connectors (GitHub, Azure DevOps, Mock) call `ItemRouter` rather than each implementing their own routing. -#### Methods +#### Data Model + +N/A — `ItemRouter` is a static utility class with no instance state. + +#### Key Methods ##### `Route(items, rules, sections) → Dictionary>` @@ -43,7 +47,7 @@ ad-hoc sections without requiring them to be pre-declared. Both lists are matched case-insensitively against the item's `Type` field. All non-empty filter lists must match for the rule to apply. -###### Error Handling +#### Error Handling No explicit error handling is performed. Callers are responsible for passing valid, non-null arguments. Duplicate section IDs in the `sections` list will cause an `ArgumentException` from diff --git a/docs/design/build-mark/repo-connectors/mock.md b/docs/design/build-mark/repo-connectors/mock.md index c10a8ef0..0b3b8d25 100644 --- a/docs/design/build-mark/repo-connectors/mock.md +++ b/docs/design/build-mark/repo-connectors/mock.md @@ -15,6 +15,27 @@ assembly or external tooling. |----------------------|---------------------------------------------|----------------------------------------------| | `MockRepoConnector` | `RepoConnectors/Mock/MockRepoConnector.cs` | In-memory connector for self-validation | +#### Interfaces + +The Mock subsystem exposes `MockRepoConnector`, which implements `IRepoConnector`. + +| Member | Kind | Description | +| --- | --- | --- | +| `MockRepoConnector()` | Constructor | Create the connector with hard-coded in-memory data | +| `GetBuildInformationAsync(version)` | Method | Return a deterministic `BuildInformation` record from in-memory data | + +#### Design + +The Mock subsystem contains a single unit, so there is no inter-unit +collaboration to describe. `MockRepoConnector` overrides +`GetBuildInformationAsync` entirely with an in-memory implementation that +mirrors the production connector logic but operates on hard-coded dictionaries +instead of live API responses. The `RunCommandAsync` method inherited from +`RepoConnectorBase` is not called, as the mock does not execute any shell +commands. When routing rules have been configured via `Configure`, the connector +calls `ApplyRules` (inherited from `RepoConnectorBase`) to populate +`BuildInformation.RoutedSections`. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md b/docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md index 12c9018b..fcf7a8f6 100644 --- a/docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md +++ b/docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md @@ -1,6 +1,6 @@ #### MockRepoConnector -##### Overview +##### Purpose `MockRepoConnector` is an in-memory implementation of `IRepoConnector` used for self-validation and unit testing. It returns a fixed, deterministic dataset @@ -23,7 +23,7 @@ The connector holds hard-coded mappings used to build the `BuildInformation` res | `_openIssues` | `List` | IDs of issues that remain open | | `_issueAffectedVersions` | `Dictionary` | Issue ID -> declared affected-versions range | -##### Methods +##### Key Methods ###### `GetBuildInformationAsync(version?) → BuildInformation` @@ -44,7 +44,7 @@ collects all items and passes them to `ApplyRules` (inherited from `RepoConnecto to produce the `RoutedSections` list. If no rules are configured, the legacy categorization into `Changes` and `Bugs` is used. -##### Error Conditions +##### Error Handling `GetBuildInformationAsync` throws `InvalidOperationException` in the following scenarios: diff --git a/docs/design/build-mark/repo-connectors/repo-connector-base.md b/docs/design/build-mark/repo-connectors/repo-connector-base.md index b0fb977f..3fb610ba 100644 --- a/docs/design/build-mark/repo-connectors/repo-connector-base.md +++ b/docs/design/build-mark/repo-connectors/repo-connector-base.md @@ -1,11 +1,34 @@ ### IRepoConnector and RepoConnectorBase -#### Overview +#### Purpose `IRepoConnector` defines the contract that all repository connectors must satisfy. `RepoConnectorBase` is an abstract class implementing `IRepoConnector` and providing shared utilities used by concrete connectors. +#### Data Model + +`IRepoConnector` is a pure interface with no instance state. `RepoConnectorBase` stores +the routing rules and section definitions supplied via `Configure(rules, sections)`; these +are held as internal fields accessible to subclasses via `HasRules` and `ApplyRules`. + +#### Key Methods + +- `Configure(rules, sections)` — Stores routing rules and section definitions on the + connector instance; called by `Program.ProcessBuildNotes` before the first + `GetBuildInformationAsync` call +- `HasRules` — Internal property returning `true` when at least one rule has been + stored via `Configure` +- `ApplyRules(allItems)` — Protected method routing items via `ItemRouter.Route`, + then assembling an ordered list of `(SectionId, SectionTitle, Items)` tuples + following configured section order +- `FindVersionIndex(versions, targetVersion)` — Protected static method finding the + index of `targetVersion` in a `VersionTag` list using semantic `VersionComparable` + equality; returns `-1` if not found +- `RunCommandAsync(command, args)` — Protected virtual method delegating shell commands + to `ProcessRunner.RunAsync`; `virtual` so test subclasses can override without + spawning real processes + #### Interface `IRepoConnector` exposes a single method: @@ -21,7 +44,7 @@ shared utilities used by concrete connectors. | Member | Kind | Description | |------------------------------------------|-------------------|-------------------------------------------------------| | `Configure(rules, sections)` | Public method | Stores routing rules and section definitions | -| `HasRules` | Protected bool | True when at least one rule has been configured | +| `HasRules` | Internal bool | True when at least one rule has been configured | | `ApplyRules(allItems)` | Protected method | Routes items into sections using configured rules | | `RunCommandAsync(command, args)` | Protected virtual | Delegates shell commands to ProcessRunner | | `FindVersionIndex(versions, target)` | Protected static | Locates version using semantic equality | @@ -34,7 +57,7 @@ by `Program.ProcessBuildNotes` after the connector is created, passing `Rules` a ##### `HasRules` -Protected boolean property that returns `true` when at least one rule has been +Internal boolean property that returns `true` when at least one rule has been stored via `Configure`. Concrete connectors use this in `GetBuildInformationAsync` to decide whether to call `ApplyRules` or use legacy categorization. @@ -62,6 +85,13 @@ The `RunCommandAsync` method accepts a command and a `params string[]` arguments array, and is `virtual` so that test subclasses can override it with mock implementations that return fixed strings without spawning real processes. +#### Error Handling + +`RunCommandAsync` propagates `InvalidOperationException` from `ProcessRunner.RunAsync` +when a shell command fails. `FindVersionIndex` returns `-1` rather than throwing when no +matching version is found. Error handling for `GetBuildInformationAsync` is delegated to +concrete implementations. + #### Interactions - `ProcessRunner` is used by `RunCommandAsync` to execute shell commands in the diff --git a/docs/design/build-mark/repo-connectors/repo-connector-factory.md b/docs/design/build-mark/repo-connectors/repo-connector-factory.md index 80b2b44a..d7c70a73 100644 --- a/docs/design/build-mark/repo-connectors/repo-connector-factory.md +++ b/docs/design/build-mark/repo-connectors/repo-connector-factory.md @@ -1,11 +1,15 @@ ### RepoConnectorFactory -#### Overview +#### Purpose `RepoConnectorFactory` is a static factory class that creates the appropriate `IRepoConnector` implementation based on the runtime environment. -#### Methods +#### Data Model + +N/A — `RepoConnectorFactory` is a static factory class with no instance state. + +#### Key Methods ##### `Create(ConnectorConfig? config) → IRepoConnector` @@ -52,6 +56,13 @@ exercise the URL-based detection logic without requiring a real git process. - If `remoteUrl` is `null` or does not match any known host, defaults to a `GitHubRepoConnector` initialized with `config?.GitHub`. +#### Error Handling + +`Create` never throws. If the git remote URL cannot be determined (e.g., git is unavailable), +`ProcessRunner.TryRunAsync` returns `null` and the factory silently defaults to a +`GitHubRepoConnector`. Connector type detection errors are suppressed to avoid failing +tool startup on environment differences. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/self-test.md b/docs/design/build-mark/self-test.md index 8fafa56a..a8e81d66 100644 --- a/docs/design/build-mark/self-test.md +++ b/docs/design/build-mark/self-test.md @@ -23,6 +23,15 @@ as its input parameter. |----------------|--------|------------------------------------------------------------| | `Run(context)` | Method | Execute all self-tests and optionally write a results file | +### Design + +The SelfTest subsystem contains a single unit (`Validation`), so there is no +inter-unit collaboration to describe. `Validation.Run` executes each self-test +method in sequence using a `MockRepoConnector` for deterministic data, accumulates +`TestResult` records, and writes the results file in TRX or JUnit XML format at +the end of the run. The test methods are independent of each other and share no +mutable state; each creates its own `MockRepoConnector` instance. + ### Interactions | Unit / Subsystem | Role | @@ -42,9 +51,3 @@ as its input parameter. | `BuildMark_IssueTracking` | GitHub issue and pull request tracking works correctly | | `BuildMark_KnownIssuesReporting` | Known issues are correctly included in the report when requested | | `BuildMark_RulesRouting` | Rules-based item routing assigns items to the correct report sections | - -### Error Handling - -If `--results` is provided with an unsupported file extension (i.e., neither `.trx` nor `.xml`), -`Validation.Run` writes an error message via `context.WriteError` and returns without writing a -file. No exception is propagated to the caller. diff --git a/docs/design/build-mark/self-test/validation.md b/docs/design/build-mark/self-test/validation.md index f6d88445..d8d4e869 100644 --- a/docs/design/build-mark/self-test/validation.md +++ b/docs/design/build-mark/self-test/validation.md @@ -1,6 +1,6 @@ ### Validation -#### Overview +#### Purpose `Validation` is the sole unit in the SelfTest subsystem. It runs a fixed set of self-tests that exercise the core functionality of BuildMark without requiring @@ -15,12 +15,7 @@ The unit is invoked by `Program.Run` when the `--validate` flag is set. its helpers. Test results are accumulated in a list of `TestResult` records that are written to a file at the end of the run. -##### `TemporaryDirectory` Helper - -A private nested class that creates a temporary directory on construction and -deletes it (with all contents) on disposal. Used to isolate test artifacts. - -#### Methods +#### Key Methods ##### `Run(Context context)` @@ -63,11 +58,17 @@ Creates a `MockRepoConnector` configured with routing rules that direct items la `BuildInformation` record, calls `ToMarkdown`, writes the output to a temporary file, and verifies the file contains both `## Features` and `## Bugs` section headings. +#### Error Handling + +If `--results` is provided with an unsupported file extension (i.e., neither `.trx` nor +`.xml`), `Validation.Run` writes an error message via `context.WriteError` and returns +without writing a file. No exception is propagated to the caller. + #### Interactions - `Context` provides output methods, `ResultsFile`, and the exit code sink. - `MockRepoConnector` supplies deterministic data for all tests in the `RepoConnectors/Mock` subsystem. - `BuildInformation` is the test target validated against expected content. -- `PathHelpers` is used directly, for example through `SafePathCombine`, to - build temporary, log, and report file paths. +- `TemporaryDirectory` (Utilities subsystem) provides temporary directory management + for test artifact isolation in `RunMarkdownReportGeneration` and `RunRulesRouting`. diff --git a/docs/design/build-mark/utilities.md b/docs/design/build-mark/utilities.md index f339d2e7..81c26ebc 100644 --- a/docs/design/build-mark/utilities.md +++ b/docs/design/build-mark/utilities.md @@ -7,16 +7,18 @@ BuildMark system: - `PathHelpers` for safe path combination with traversal prevention - `ProcessRunner` for executing external shell commands +- `TemporaryDirectory` for disposable temporary directory management Version parsing, comparison, tag handling, and interval logic are implemented in the separate `Version` subsystem. ### Units -| Unit | File | Responsibility | -|-----------------|-----------------------------|----------------------------| -| `PathHelpers` | `Utilities/PathHelpers.cs` | Safe path combination | -| `ProcessRunner` | `Utilities/ProcessRunner.cs`| External process execution | +| Unit | File | Responsibility | +|----------------------|------------------------------------|-----------------------------------| +| `PathHelpers` | `Utilities/PathHelpers.cs` | Safe path combination | +| `ProcessRunner` | `Utilities/ProcessRunner.cs` | External process execution | +| `TemporaryDirectory` | `Utilities/TemporaryDirectory.cs` | Disposable temporary directory | ### Interfaces @@ -33,10 +35,44 @@ the separate `Version` subsystem. | `RunAsync(command, params arguments)` | Method | Run a process and return stdout; throws on failure | | `TryRunAsync(command, params arguments)`| Method | Run a process and return stdout, or null on any failure | +`TemporaryDirectory` exposes the following members: + +| Member | Kind | Description | +|--------------------------------|-------------|--------------------------------------------------------------| +| `TemporaryDirectory()` | Constructor | Creates a uniquely-named directory under `CurrentDirectory` | +| `DirectoryPath` | Property | Absolute path to the temporary directory on disk | +| `GetFilePath(relativePath)` | Method | Resolve a relative path, creating intermediate directories | +| `Dispose()` | Method | Delete the directory and all contents; suppress I/O errors | + +### Design + +The Utilities subsystem contains three units. `PathHelpers` and `ProcessRunner` are +stateless; `TemporaryDirectory` is instance-based and manages directory lifetime: + +- `PathHelpers.SafePathCombine` is a pure function that combines a base path and + a relative path, normalizes both to absolute form, and rejects the result if the + combined path escapes the base directory. It is consumed by any unit that needs + to write output files to user-supplied paths safely. + +- `ProcessRunner.RunAsync` and `TryRunAsync` are async wrappers over + `System.Diagnostics.Process` that capture stdout and return it as a trimmed + string. On Windows, commands are routed through `cmd /c` to handle `.cmd` + and `.bat` scripts; on other platforms they are invoked directly. All connector + and factory code that needs to run Git, `gh`, or `az` CLI commands delegates + to `ProcessRunner` via `RepoConnectorBase.RunCommandAsync`. + +- `TemporaryDirectory` creates a uniquely-named directory under + `Environment.CurrentDirectory` on construction and deletes it recursively on + disposal. `GetFilePath` delegates path validation to `PathHelpers.SafePathCombine` + before creating any missing intermediate directories, ensuring all paths remain + within the temporary directory. + ### Interactions `PathHelpers` and `ProcessRunner` have no dependencies on other BuildMark -subsystems. They are consumed by any unit that needs safe path combination or -external process execution. +subsystems. `TemporaryDirectory` depends on `PathHelpers.SafePathCombine` for +path validation in `GetFilePath`. All three units are consumed by any subsystem +that needs safe path combination, external process execution, or temporary directory +management. Version-specific consumers should use the separate `Version` subsystem. diff --git a/docs/design/build-mark/utilities/path-helpers.md b/docs/design/build-mark/utilities/path-helpers.md index 8a381aa7..c769eaef 100644 --- a/docs/design/build-mark/utilities/path-helpers.md +++ b/docs/design/build-mark/utilities/path-helpers.md @@ -1,6 +1,6 @@ ### PathHelpers Design -#### Overview +#### Purpose `PathHelpers` is a static utility class that provides a safe path-combination method. It protects callers against path-traversal attacks by verifying the resolved combined path stays @@ -8,7 +8,11 @@ within the base directory. Note that `Path.GetFullPath` normalizes `.`/`..` segm not resolve symlinks or reparse points, so this check guards against string-level traversal only. -#### Class Structure +#### Data Model + +N/A — `PathHelpers` is a static utility class with no instance state. + +#### Key Methods ##### SafePathCombine Method @@ -30,7 +34,7 @@ the base directory. or `Path.AltDirectorySeparatorChar`, or is itself rooted (absolute), which would indicate the combined path escapes the base directory. -#### Design Decisions +**Implementation rationale:** - **`Path.GetRelativePath` for containment check**: Using `GetRelativePath` to verify containment handles root paths (e.g. `/`, `C:\`), platform case-sensitivity, and @@ -45,6 +49,12 @@ the base directory. - **No logging or error accumulation**: `SafePathCombine` is a pure utility method that throws on invalid input; it does not interact with the `Context` or any output mechanism. +#### Error Handling + +`SafePathCombine` throws `ArgumentNullException` when either argument is `null` and +`ArgumentException` (identifying `relativePath` as the problematic parameter) when the +resolved combined path escapes the base directory. No other exceptions are thrown. + #### Interactions `PathHelpers` has no dependencies on other BuildMark units or subsystems. diff --git a/docs/design/build-mark/utilities/process-runner.md b/docs/design/build-mark/utilities/process-runner.md index 8ac2156a..0cc42c1d 100644 --- a/docs/design/build-mark/utilities/process-runner.md +++ b/docs/design/build-mark/utilities/process-runner.md @@ -1,13 +1,17 @@ ### ProcessRunner -#### Overview +#### Purpose `ProcessRunner` is a static helper class in the Utilities subsystem that executes external shell commands and captures their standard output. It provides two public methods: `RunAsync`, which throws on failure, and `TryRunAsync`, which returns `null` on failure. -#### Methods +#### Data Model + +N/A — `ProcessRunner` is a static utility class with no instance state. + +#### Key Methods ##### `RunAsync(command, params arguments) → string` @@ -42,6 +46,13 @@ the command is invoked directly without a shell wrapper. Empty or whitespace-only commands are not routed through `cmd /c`, preserving the exception behavior for invalid commands. +#### Error Handling + +`RunAsync` throws `InvalidOperationException` when the process exits with a non-zero exit +code or when the command is not found (wrapping `Win32Exception` with a descriptive message). +`TryRunAsync` catches all exceptions and returns `null` on any failure, never propagating +exceptions to callers. + #### Interactions | Unit / Subsystem | Role | diff --git a/docs/design/build-mark/utilities/temporary-directory.md b/docs/design/build-mark/utilities/temporary-directory.md new file mode 100644 index 00000000..3509a992 --- /dev/null +++ b/docs/design/build-mark/utilities/temporary-directory.md @@ -0,0 +1,75 @@ +### TemporaryDirectory Design + +#### Purpose + +`TemporaryDirectory` is an `internal sealed` class implementing `IDisposable` that +creates a uniquely-named directory under `Environment.CurrentDirectory` on construction +and deletes it recursively on disposal. Using `Environment.CurrentDirectory` as the base +avoids the macOS `/tmp` → `/private/tmp` symlink mismatch that can cause path-containment +checks to fail when the real path and the requested path differ. + +#### Data Model + +`TemporaryDirectory` is instance-based and owns a single directory for its lifetime: + +| Property | Type | Description | +| --------------- | -------- | ------------------------------------------------------- | +| `DirectoryPath` | `string` | Absolute path to the temporary directory on disk | + +#### Key Methods + +##### Constructor + +```csharp +internal TemporaryDirectory() +``` + +Generates a unique name by combining a fixed prefix with a GUID string, creates the +directory under `Environment.CurrentDirectory`, and stores the resulting path in +`DirectoryPath`. Throws `InvalidOperationException` wrapping the original exception if +directory creation fails due to an I/O error. + +##### GetFilePath Method + +```csharp +internal string GetFilePath(string relativePath) +``` + +Returns the absolute path to a file inside the temporary directory identified by +`relativePath`. + +**Steps:** + +1. Delegates to `PathHelpers.SafePathCombine(DirectoryPath, relativePath)` to produce a + validated absolute path. `SafePathCombine` rejects traversal sequences (`..`) and + absolute-path overrides, throwing `ArgumentException` on invalid input. +2. Creates all intermediate directories between `DirectoryPath` and the resolved file path + using `Directory.CreateDirectory` so that callers can write the file immediately. +3. Returns the validated path string. + +##### Dispose Method + +```csharp +public void Dispose() +``` + +Deletes the temporary directory and all its contents by calling +`Directory.Delete(DirectoryPath, recursive: true)`. Both `IOException` and +`UnauthorizedAccessException` are caught and silently discarded so that callers in +`using` statements are not disrupted when the directory has already been removed or +when a file lock prevents deletion. + +#### Error Handling + +| Situation | Behavior | +| ------------------------------------------ | -------------------------------------------------- | +| Directory creation fails (I/O error) | Throws `InvalidOperationException` (wraps original)| +| Traversal or absolute path in GetFilePath | Throws `ArgumentException` (from `SafePathCombine`)| +| Directory already deleted before Dispose | Silently suppressed | +| File lock prevents deletion in Dispose | Silently suppressed | + +#### Interactions + +- `PathHelpers.SafePathCombine` (Utilities subsystem) is called by `GetFilePath` to + validate and resolve the caller-supplied relative path before creating intermediate + directories. diff --git a/docs/design/build-mark/version.md b/docs/design/build-mark/version.md index 1e997cad..31967f41 100644 --- a/docs/design/build-mark/version.md +++ b/docs/design/build-mark/version.md @@ -35,8 +35,9 @@ VersionCommitTag (version + commit hash) #### Semantic Versioning Compliance -All version processing strictly adheres to Semantic Versioning 2.0.0 () specification -to ensure predictable and industry-standard behavior. +All version processing substantially adheres to Semantic Versioning 2.0.0 (). +Pre-release identifiers are compared case-insensitively rather than using the ASCII +case-sensitive sort defined by SemVer 2.0.0. #### Performance Optimization @@ -53,6 +54,20 @@ Each version type serves a specific purpose with clear boundaries: - **VersionInterval**: Range queries and filtering - **VersionCommitTag**: Build metadata association +### Interfaces + +The Version subsystem exposes all six unit types as public records or classes. +The primary interface consumed by other subsystems is: + +| Member | Kind | Description | +| --- | --- | --- | +| `VersionTag.Create(tag)` | Static method | Parse a repository tag; throws on invalid input | +| `VersionTag.TryCreate(tag)` | Static method | Parse a repository tag; returns `null` on invalid input | +| `VersionIntervalSet.Parse(text)` | Static method | Parse a comma-separated set of version intervals | +| `VersionIntervalSet.Contains(version)` | Method | Test whether a version falls in any interval in the set | +| `VersionComparable.Create(version)` | Static method | Parse semantic version for comparison; throws on invalid input | +| `VersionComparable.TryCreate(version)` | Static method | Parse a semantic version; returns `null` on invalid input | + ### External Interfaces | Interface | Direction | Protocol / Format | @@ -86,6 +101,25 @@ Version processing provides two parsing patterns: - Malformed version ranges are rejected during parsing - Version comparison operations are guaranteed to be consistent and transitive +### Design + +The units in the Version subsystem form a directed processing hierarchy. Raw +repository tag strings flow in from connectors and pass through `VersionTag`, +which strips tag prefixes and extracts a `VersionSemantic`. `VersionSemantic` +wraps a `VersionComparable` that exposes numeric major/minor/patch fields and +pre-release segments for sorting. `VersionCommitTag` pairs a `VersionTag` with +its Git commit hash so that `BuildInformation` can record the exact commit at +each version boundary. + +Version range expressions flow in from `ItemControlsParser` as text and are +parsed by `VersionIntervalSet.Parse`, which delegates to `VersionInterval.Parse` +for each token. `VersionInterval.Contains` tests a candidate version using +`VersionComparable.TryCreate` for the bound comparisons. + +No unit in the subsystem holds mutable state; all types are records or +effectively immutable classes instantiated via `Create`/`TryCreate` factory +methods. + ### Performance Characteristics #### Version Comparison diff --git a/docs/design/build-mark/version/version-commit-tag.md b/docs/design/build-mark/version/version-commit-tag.md index ca1a9f55..7c4f32ba 100644 --- a/docs/design/build-mark/version/version-commit-tag.md +++ b/docs/design/build-mark/version/version-commit-tag.md @@ -1,6 +1,6 @@ ### VersionCommitTag -#### Overview +#### Purpose `VersionCommitTag` is a record in the Version subsystem that pairs a parsed `VersionTag` value with the Git commit hash at which that tag was created. It is @@ -20,6 +20,14 @@ public record VersionCommitTag( | `VersionTag` | `VersionTag` | Parsed version information for this tag | | `CommitHash` | `string` | Git commit hash at the point this tag was made | +#### Key Methods + +N/A — `VersionCommitTag` is an immutable data record with no methods beyond those auto-generated by C#. + +#### Error Handling + +N/A — `VersionCommitTag` is an immutable data record with no methods that detect or propagate errors. + #### Interactions - `VersionTag` supplies the parsed tag and semantic version details. diff --git a/docs/design/build-mark/version/version-comparable.md b/docs/design/build-mark/version/version-comparable.md index 57614cdd..f952496d 100644 --- a/docs/design/build-mark/version/version-comparable.md +++ b/docs/design/build-mark/version/version-comparable.md @@ -6,7 +6,7 @@ The `VersionComparable` class provides core semantic version comparison function It handles versions in the format `major.minor.patch[-pre-release]` and implements proper semantic version ordering rules with optimized performance for pre-release comparison. -#### Structure +#### Data Model | Property | Type | Description | | -------- | ---- | ----------- | @@ -18,6 +18,21 @@ proper semantic version ordering rules with optimized performance for pre-releas | IsPreRelease | bool | Whether this is a pre-release version | | CompareVersion | string | Normalized comparison string (major.minor.patch[-pre-release]) | +#### Key Methods + +- `Create(string version)` — Parses a `major.minor.patch[-pre-release]` string; + throws `ArgumentException` on invalid input +- `TryCreate(string version)` — Parses a version string; returns `null` on invalid + input instead of throwing +- `CompareTo(VersionComparable other)` — Implements `IComparable` + using numeric major/minor/patch comparison followed by SemVer pre-release ordering + +`Create` and `TryCreate` use a source-generated `Regex` pattern to validate and parse the +input. Pre-release segments are split at construction time by `ParsePreReleaseSegments` into a +cached `PreReleaseSegment[]` array so that repeated comparisons (`CompareTo`) avoid +re-parsing the pre-release string. Operator overloads (`<`, `<=`, `>`, `>=`) delegate to +`CompareTo`. + #### Performance Optimization ##### Pre-Release Parsing @@ -93,3 +108,14 @@ This pattern matches: - Required: major.minor.patch numbers - Optional: hyphen followed by pre-release identifier + +#### Error Handling + +`Create(string version)` throws `ArgumentException` when the input string does not match +the expected `major.minor.patch[-pre-release]` format. `TryCreate(string version)` returns +`null` instead of throwing, allowing callers to test validity without exception handling. +Comparison operations on a valid instance never fail. + +#### Interactions + +Consumed by `VersionSemantic`, `VersionInterval`, and `VersionIntervalSet` for range evaluation. diff --git a/docs/design/build-mark/version/version-interval-set.md b/docs/design/build-mark/version/version-interval-set.md index 8c64289c..72637199 100644 --- a/docs/design/build-mark/version/version-interval-set.md +++ b/docs/design/build-mark/version/version-interval-set.md @@ -1,5 +1,51 @@ ### VersionIntervalSet -The `VersionIntervalSet` unit is documented in `version-interval.md`, which covers both -`VersionInterval` and `VersionIntervalSet` comprehensively, including the data model, parsing -algorithm, `Contains` overloads, and interaction table. +#### Purpose + +`VersionIntervalSet` is an ordered, immutable collection of one or more `VersionInterval` +instances. It is parsed from the `affected-versions` field of a `buildmark` block and tests +whether a version falls inside any contained interval. The full parsing algorithm and detailed +method descriptions are documented together with `VersionInterval` in +_VersionInterval and VersionIntervalSet Design_. + +#### Data Model + +```csharp +public record VersionIntervalSet( + IReadOnlyList Intervals); +``` + +| Property | Type | Description | +|-------------|----------------------------------|----------------------------------| +| `Intervals` | `IReadOnlyList` | Ordered list of parsed intervals | + +#### Key Methods + +- `Parse(string text)` — Parses a comma-separated list of interval expressions into a + `VersionIntervalSet`; discards unrecognized tokens silently +- `Contains(string version)` — Tests whether a semantic version string falls within + any interval in the set; returns `false` for invalid version strings +- `Contains(VersionComparable version)` — Tests whether a `VersionComparable` instance + falls within any interval in the set +- `Contains(VersionTag version)` — Convenience overload delegating to + `Contains(VersionComparable)` using `version.Semantic.Comparable` + +See _VersionInterval and VersionIntervalSet Design_ for the full algorithmic descriptions of +each method. + +#### Error Handling + +`Parse` silently discards interval tokens that do not conform to the expected bracket-bound +format, returning a set containing only the valid intervals. `Contains(string version)` returns +`false` when the candidate version string is not a valid semantic version, rather than +propagating a parse error. + +#### Interactions + +| Unit / Subsystem | Role | +|----------------------|--------------------------------------------------------------------------| +| `VersionInterval` | Each element of `Intervals`; `Contains` overloads delegate to it | +| `VersionComparable` | Used by `Contains(VersionComparable)` for ordered semantic comparison | +| `VersionTag` | Accepted by the `Contains(VersionTag)` convenience overload | +| `ItemControlsParser` | Creates `VersionIntervalSet` from the `affected-versions` field value | +| `ItemControlsInfo` | Holds the `VersionIntervalSet` for the `affected-versions` field | diff --git a/docs/design/build-mark/version/version-interval.md b/docs/design/build-mark/version/version-interval.md index b36c322e..b061b47c 100644 --- a/docs/design/build-mark/version/version-interval.md +++ b/docs/design/build-mark/version/version-interval.md @@ -1,6 +1,6 @@ ### VersionInterval and VersionIntervalSet -#### Overview +#### Purpose `VersionInterval` represents a single mathematical version interval using inclusive or exclusive bounds, where either bound may be omitted to indicate @@ -56,7 +56,7 @@ public record VersionIntervalSet( |-------------|-----------------------------------|-----------------------------------| | `Intervals` | `IReadOnlyList` | Ordered list of parsed intervals | -#### Methods +#### Key Methods ##### `VersionInterval.Parse(string text) → VersionInterval?` @@ -151,6 +151,13 @@ Convenience overload for callers that already hold a parsed BuildMark | `(,1.0.1],[1.1.0,1.2.0)` | Two intervals | | `[3.0.0,)` | One interval: `3.0.0` and later | +#### Error Handling + +`VersionInterval.Parse` returns `null` for malformed interval tokens rather than throwing. +`VersionIntervalSet.Parse` silently discards tokens that do not parse successfully, returning +a set containing only the valid intervals. `Contains(string version)` returns `false` if the +candidate version string is not a valid semantic version, rather than propagating a parse error. + #### Interactions `VersionInterval` and `VersionIntervalSet` are general-purpose utility types. diff --git a/docs/design/build-mark/version/version-semantic.md b/docs/design/build-mark/version/version-semantic.md index 8fe9531d..f569e036 100644 --- a/docs/design/build-mark/version/version-semantic.md +++ b/docs/design/build-mark/version/version-semantic.md @@ -7,7 +7,7 @@ support. As a C# `record`, it provides structural equality by default - two `Ver instances are equal when all their properties compare equal. It provides the full semantic version structure including build metadata while preserving comparison functionality. -#### Structure +#### Data Model | Property | Type | Description | | -------- | ---- | ----------- | @@ -15,6 +15,19 @@ version structure including build metadata while preserving comparison functiona | Metadata | string? | Build metadata (+metadata), or `null` when absent | | FullVersion | string | Complete version string (major.minor.patch\[-pre-release\]\[+metadata\]) | +#### Key Methods + +- `Create(string version)` — Parses a full semantic version string including optional + `+metadata`; throws `ArgumentException` on invalid input +- `TryCreate(string version)` — Parses a full semantic version string; returns `null` + on invalid input instead of throwing + +Both methods split the input on `+` (using `Split('+', 2)`) to separate the core version from +optional build metadata, then delegate the core version part to `VersionComparable.TryCreate`. +An empty metadata segment is normalized to `null`. Comparison operations delegate entirely to +the wrapped `Comparable` instance; build metadata does not affect ordering per the SemVer +specification. + #### Delegated Properties For convenience, the following properties delegate to the `Comparable` instance: @@ -53,3 +66,13 @@ var version = VersionSemantic.Create("1.2.3-beta.1+build.123"); // version.FullVersion = "1.2.3-beta.1+build.123" // version.CompareVersion = "1.2.3-beta.1" ``` + +#### Error Handling + +`Create(string version)` throws `ArgumentException` for invalid input. `TryCreate(string version)` +returns `null` instead of throwing. Once constructed, property access and comparison operations +cannot fail. + +#### Interactions + +Consumed by `VersionTag` for version extraction from Git tag strings. diff --git a/docs/design/build-mark/version/version-tag.md b/docs/design/build-mark/version/version-tag.md index 3a121029..e9336bff 100644 --- a/docs/design/build-mark/version/version-tag.md +++ b/docs/design/build-mark/version/version-tag.md @@ -8,13 +8,28 @@ original tag and parsed semantic version. **Critically, VersionTag instances are compared based on their semantic version content (VersionComparable), not their tag strings, enabling version equality across different tag formats.** -#### Structure +#### Data Model | Property | Type | Description | |----------|-----------------|------------------------------| | Tag | string | Original repository tag | | Semantic | VersionSemantic | Parsed semantic version info | +#### Key Methods + +- `Create(string tag)` — Parses a repository tag string and extracts the embedded + semantic version; throws `ArgumentException` when no recognizable semantic version + can be found +- `TryCreate(string tag)` — Parses a repository tag string; returns `null` when no + semantic version can be extracted instead of throwing +- `ToString()` — Returns the original `Tag` string verbatim, preserving the repository + tag format for display and logging + +The parsing algorithm strips known prefix patterns (e.g., `v`, `ver`, `release/`) and then +attempts `VersionSemantic.TryCreate` on the remainder. Equality between `VersionTag` +instances is based on `Semantic.Comparable` rather than the raw `Tag` string, so tags with +different prefixes but identical semantic versions compare as equal. + #### Delegated Properties For convenience, the following properties delegate to the `Semantic.Comparable` instance: @@ -83,3 +98,13 @@ var tag1 = VersionTag.Create("v1.2.3"); var tag2 = VersionTag.Create("VER1.2.3"); // tag1.Semantic.Comparable.Equals(tag2.Semantic.Comparable) == true ``` + +#### Error Handling + +`Create(string tag)` throws `ArgumentException` when the tag cannot be parsed into a +recognizable semantic version format. `TryCreate(string tag)` returns `null` instead of +throwing. Once constructed, property access and `ToString()` cannot fail. + +#### Interactions + +Consumed by `VersionCommitTag`, RepoConnectors (for tag parsing), and `Program` (for filtering). diff --git a/docs/design/introduction.md b/docs/design/introduction.md index b8009e9d..44c7ddaa 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -143,6 +143,18 @@ src/DemaConsulting.BuildMark/ The test project mirrors the same layout under `test/DemaConsulting.BuildMark.Tests/`. +## Companion Artifact Structure + +Each local software item has corresponding artifacts in parallel directory trees: + +- Requirements: `docs/reqstream/build-mark.yaml`, `docs/reqstream/build-mark/.../{item}.yaml` +- Design: `docs/design/build-mark.md`, `docs/design/build-mark/.../{item}.md` +- Verification: `docs/verification/build-mark.md`, `docs/verification/build-mark/.../{item}.md` +- Source: `src/DemaConsulting.BuildMark/.../{Item}.cs` +- Tests: `test/DemaConsulting.BuildMark.Tests/.../{Item}Tests.cs` + +Review-sets: defined in `.reviewmark.yaml` + ## Document Conventions Throughout this document: @@ -155,5 +167,5 @@ Throughout this document: ## References -- See the BuildMark User Guide for user-facing documentation. +- [BuildMark releases](https://github.com/demaconsulting/BuildMark/releases) — compiled user guide and documentation - See the BuildMark repository at . diff --git a/docs/design/ots/yaml-dot-net.md b/docs/design/ots/yaml-dot-net.md new file mode 100644 index 00000000..8df503c8 --- /dev/null +++ b/docs/design/ots/yaml-dot-net.md @@ -0,0 +1,49 @@ +## YamlDotNet Integration Design + +### Why YamlDotNet Was Chosen + +YamlDotNet is the established YAML parsing library for .NET. It provides a +representation model (`YamlStream`, `YamlMappingNode`, `YamlSequenceNode`, +`YamlScalarNode`) that allows walking the YAML node tree without requiring a +pre-defined schema. This is important for BuildMark because the configuration +file is optional and partially structured: the reader must tolerate absent +sections gracefully and convert malformed content into `ConfigurationIssue` +records rather than throwing unhandled exceptions. + +### APIs Used + +BuildMark uses the YamlDotNet **representation model** exclusively: + +| Type | Usage | +|---------------------|------------------------------------------------------------------| +| `YamlStream` | Top-level container; parsed from the raw file text | +| `YamlMappingNode` | Key-value mapping; used for all object nodes in the config | +| `YamlSequenceNode` | Ordered list; used for `sections` and `rules` arrays | +| `YamlScalarNode` | Leaf node value; represents a string, boolean, or number value | + +The serializer/deserializer (`YamlDotNet.Serialization`) is **not** used +because it requires a fixed schema and throws on unknown keys, which would +prevent forward-compatible configuration files. + +### Integration Pattern + +`BuildMarkConfigReader.ReadAsync` reads the file text and passes it to +`YamlStream.Load(reader)`. If parsing succeeds, the first document's root +node is cast to `YamlMappingNode` and walked recursively. Each expected key +is looked up by name; absent keys produce `null` and are handled with +`ConfigurationIssue` creation when they are required, or silently ignored +when optional. + +The node walk is deliberately defensive: + +- Every cast is guarded; an unexpected node type generates a `ConfigurationIssue` + with `Severity.Warning` so the tool can continue with partial configuration. +- Line numbers are extracted from `YamlNode.Start.Line` and included in every + issue for precise user feedback. + +### Version Constraints + +BuildMark targets the version of YamlDotNet specified in the project file +(`src/DemaConsulting.BuildMark/DemaConsulting.BuildMark.csproj`). No version +below 13.x is supported because earlier versions used a different namespace +layout for the representation model types. diff --git a/docs/reqstream/build-mark.yaml b/docs/reqstream/build-mark.yaml index 6b24105c..cfa51a6c 100644 --- a/docs/reqstream/build-mark.yaml +++ b/docs/reqstream/build-mark.yaml @@ -207,7 +207,6 @@ sections: - BuildMark_Report_ConsumesConfigurationFileDuringGeneration children: - BuildMark-Configuration-Read - - BuildMark-Program-Lint - BuildMark-ReportConfig-Properties - id: BuildMark-Config-Connector diff --git a/docs/reqstream/build-mark/program.yaml b/docs/reqstream/build-mark/program.yaml index be80c613..e7a59b1b 100644 --- a/docs/reqstream/build-mark/program.yaml +++ b/docs/reqstream/build-mark/program.yaml @@ -59,7 +59,6 @@ sections: Silent mode enables automated build pipelines to invoke BuildMark without cluttering their output logs when only the exit code or report file is needed. tests: - - BuildMark_SilentFlag_SuppressesOutput - Program_Run_WithSilentFlag_SuppressesOutput - id: BuildMark-Program-Log @@ -68,7 +67,6 @@ sections: Log file support enables persistent capture of BuildMark output for audit trails and post-build analysis in automated environments. tests: - - BuildMark_LogParameter_IsAccepted - Program_Run_WithLogFlag_WritesToLogFile - id: BuildMark-Program-Results @@ -77,7 +75,6 @@ sections: Writing results to standard test result formats (TRX or JUnit XML) enables integration with CI/CD platforms that process test result files. tests: - - BuildMark_ResultsParameter_IsAccepted - Program_Run_WithResultsFlag_WritesResultsFile - id: BuildMark-Program-BuildVersion @@ -86,7 +83,6 @@ sections: Specifying the build version allows users to override or supplement version information from source control, enabling flexible version management strategies. tests: - - BuildMark_BuildVersionParameter_IsAccepted - Program_Run_WithBuildVersionFlag_AcceptsBuildVersion - id: BuildMark-Program-Depth @@ -95,8 +91,6 @@ sections: Configurable heading depth allows BuildMark reports to be embedded at any level within a larger document without disrupting the document hierarchy. tests: - - BuildMark_DepthParameter_IsAccepted - - BuildMark_Report_DepthTwo_UsesLevelTwoHeadings - Program_Run_WithDepthFlag_SetsHeadingDepth - id: BuildMark-Program-IncludeKnownIssues diff --git a/docs/reqstream/build-mark/repo-connectors.yaml b/docs/reqstream/build-mark/repo-connectors.yaml index 5d847669..71dd49fc 100644 --- a/docs/reqstream/build-mark/repo-connectors.yaml +++ b/docs/reqstream/build-mark/repo-connectors.yaml @@ -11,26 +11,37 @@ sections: - title: RepoConnectors Subsystem Requirements requirements: - - id: BuildMark-RepoConnectors-ConnectorBase + - id: BuildMark-RepoConnectors-ConnectorInterface title: >- - The RepoConnectors subsystem shall provide a common interface and base class for all repository connectors. + The RepoConnectors subsystem shall provide a common interface for all repository connectors. justification: | - A shared interface and base class ensure that all connector implementations - are interchangeable and benefit from common utilities such as process - execution delegation and version-list search. + A shared interface ensures that all connector implementations are interchangeable, + enabling the factory and program to work with any connector implementation + without depending on concrete types. tests: - RepoConnectors_ConnectorBase_MockConnector_ImplementsInterface - RepoConnectors_ConnectorBase_GitHubConnector_ImplementsInterface + children: + - BuildMark-RepoConnectorBase-Interface + - BuildMark-RepoConnectorBase-RulesRouting + - BuildMark-RepoConnectorBase-FindVersionIndex + + - id: BuildMark-RepoConnectors-ProcessRunner + title: >- + The RepoConnectors subsystem shall provide a process execution utility for running shell commands. + justification: | + Repository connectors rely on shell commands (git, gh CLI) to retrieve repository + information. A shared process-runner utility centralizes subprocess management, + ensuring consistent command execution and output handling across all connector + implementations. + tests: - RepoConnectors_ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput - RepoConnectors_ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull - RepoConnectors_ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull - RepoConnectors_ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput - RepoConnectors_ProcessRunner_RunAsync_WithFailingCommand_ThrowsException children: - - BuildMark-RepoConnectorBase-Interface - BuildMark-RepoConnectorBase-CommandExecution - - BuildMark-RepoConnectorBase-RulesRouting - - BuildMark-RepoConnectorBase-FindVersionIndex - id: BuildMark-RepoConnectors-Factory title: The RepoConnectors subsystem shall provide a factory for creating the appropriate repository connector. diff --git a/docs/reqstream/build-mark/repo-connectors/azure-devops.yaml b/docs/reqstream/build-mark/repo-connectors/azure-devops.yaml index ca3d2ab6..3b6c14ab 100644 --- a/docs/reqstream/build-mark/repo-connectors/azure-devops.yaml +++ b/docs/reqstream/build-mark/repo-connectors/azure-devops.yaml @@ -21,7 +21,7 @@ sections: requests, and work items, apply item-controls overrides from buildmark blocks and custom fields, and assemble them into structured build information. tests: - - AzureDevOps_ImplementsInterface_ReturnsTrue + - AzureDevOps_IRepoConnector_ConnectorInstance_ImplementsInterface - AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation - AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges - AzureDevOps_GetBuildInformation_WithOpenWorkItems_IdentifiesKnownIssues @@ -35,3 +35,4 @@ sections: - BuildMark-AzureDevOps-Rules - BuildMark-AzureDevOps-RestClient - BuildMark-AzureDevOps-WorkItemMapper + - BuildMark-AzureDevOps-TokenVariable diff --git a/docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml b/docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml index 351ca0cd..fcf947aa 100644 --- a/docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml +++ b/docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml @@ -25,6 +25,8 @@ sections: - WorkItemMapper_IsWorkItemResolved_ResolvedState_ReturnsTrue - WorkItemMapper_IsWorkItemResolved_ActiveState_ReturnsFalse - WorkItemMapper_GetWorkItemTypeForRuleMatching_ReturnsWorkItemTypeName + children: + - BuildMark-AzureDevOps-SuppressRemovedWorkItems - id: BuildMark-AzureDevOps-SuppressRemovedWorkItems title: >- diff --git a/docs/reqstream/build-mark/repo-connectors/github.yaml b/docs/reqstream/build-mark/repo-connectors/github.yaml index a4efb5c6..9bbfa3ea 100644 --- a/docs/reqstream/build-mark/repo-connectors/github.yaml +++ b/docs/reqstream/build-mark/repo-connectors/github.yaml @@ -34,3 +34,4 @@ sections: - BuildMark-GitHub-DescriptionBody - BuildMark-GitHub-GraphQLClient - BuildMark-GitHub-Rules + - BuildMark-GitHub-TokenVariable diff --git a/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml b/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml index c9620bc5..70632aba 100644 --- a/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml +++ b/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml @@ -18,6 +18,12 @@ sections: follow pagination cursors to retrieve all commits, releases, tags, pull requests, issues, and linked issue IDs so that build information reflects the complete repository history rather than only the first page of results. + + This requirement serves as a parent summary requirement. Its children + (BuildMark-GitHub-GetCommits, BuildMark-GitHub-GetReleases, BuildMark-GitHub-GetTags, + BuildMark-GitHub-GetPullRequests, BuildMark-GitHub-GetIssues, and + BuildMark-GitHub-ErrorHandling) each cover one specific aspect of the client's + observable behavior and carry the detailed test mappings. tests: - GitHubGraphQLClient_GetCommitsAsync_ValidResponse_ReturnsCommitShas - GitHubGraphQLClient_GetCommitsAsync_WithPagination_ReturnsAllCommits @@ -37,6 +43,7 @@ sections: - BuildMark-GitHub-GetTags - BuildMark-GitHub-GetPullRequests - BuildMark-GitHub-GetIssues + - BuildMark-GitHub-ErrorHandling - id: BuildMark-GitHub-DescriptionBody title: >- @@ -99,3 +106,25 @@ sections: - GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssuesWithBody - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_ValidResponse_ReturnsIssueIds - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_WithPagination_ReturnsAllIssues + + - id: BuildMark-GitHub-ErrorHandling + title: >- + The GitHubGraphQLClient shall return an empty list rather than propagating + exceptions when a GitHub API query fails. + justification: | + Transient network failures, HTTP error responses, and malformed JSON payloads + must not crash the build-notes pipeline. Returning an empty list on any query + failure allows the connector to produce a partial result or a meaningful error + message rather than an unhandled exception. + tests: + - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_HttpError_ReturnsEmptyList + - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_InvalidJson_ReturnsEmptyList + - GitHubGraphQLClient_GetAllIssuesAsync_Exception_ReturnsEmptyList + - GitHubGraphQLClient_GetAllTagsAsync_HttpError_ReturnsEmptyList + - GitHubGraphQLClient_GetAllTagsAsync_InvalidJson_ReturnsEmptyList + - GitHubGraphQLClient_GetCommitsAsync_HttpError_ReturnsEmptyList + - GitHubGraphQLClient_GetCommitsAsync_InvalidJson_ReturnsEmptyList + - GitHubGraphQLClient_GetPullRequestsAsync_HttpError_ReturnsEmptyList + - GitHubGraphQLClient_GetPullRequestsAsync_InvalidJson_ReturnsEmptyList + - GitHubGraphQLClient_GetReleasesAsync_HttpError_ReturnsEmptyList + - GitHubGraphQLClient_GetReleasesAsync_InvalidJson_ReturnsEmptyList diff --git a/docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml b/docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml index 60365043..5177df3b 100644 --- a/docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml +++ b/docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml @@ -22,13 +22,14 @@ sections: - id: BuildMark-RepoConnectorBase-CommandExecution title: >- - The RepoConnectorBase class shall allow shell command execution to be - substituted for testing purposes without spawning real processes. + Repository connectors shall execute shell commands and return their output + to retrieve repository information. justification: | - Repository connectors run git and gh CLI commands to retrieve repository - information. Tests must be able to substitute controlled responses for those - commands to produce deterministic, repeatable results without network or - filesystem dependencies. + Repository connectors run git and CLI commands to retrieve repository + information such as version tags, commit hashes, and remote URLs. The base + class centralizes process execution so that all connector implementations + consistently invoke commands and propagate their output without duplicating + subprocess-management logic. tests: - GitHubRepoConnector_GetBuildInformationAsync_WithMockedData_ReturnsValidBuildInformation - GitHubRepoConnector_GetBuildInformationAsync_WithMultipleVersions_SelectsCorrectPreviousVersionAndGeneratesChangelogLink diff --git a/docs/reqstream/build-mark/utilities.yaml b/docs/reqstream/build-mark/utilities.yaml index 10c89915..97ec6291 100644 --- a/docs/reqstream/build-mark/utilities.yaml +++ b/docs/reqstream/build-mark/utilities.yaml @@ -3,7 +3,7 @@ # # PURPOSE: # - Define requirements for the BuildMark Utilities subsystem -# - The Utilities subsystem spans PathHelpers.cs and ProcessRunner.cs +# - The Utilities subsystem spans PathHelpers.cs, ProcessRunner.cs, and TemporaryDirectory.cs # - Subsystem requirements describe the shared utility behavior available to all other subsystems sections: @@ -37,3 +37,19 @@ sections: - Utilities_ProcessRunner_FailingCommand_ThrowsException children: - BuildMark-ProcessRunner-RunAsync + + - id: BuildMark-Utilities-TemporaryDirectory + title: The Utilities subsystem shall provide a disposable temporary directory for isolating file-system artifacts. + justification: | + Multiple subsystems need a safe, isolated location to write temporary files during + self-test and integration operations. Placing TemporaryDirectory in the shared + Utilities subsystem ensures consistent creation, path-safety, and cleanup behavior + across all consumers, without each caller reimplementing ad-hoc temporary-file logic. + tests: + - TemporaryDirectory_Constructor_CreatesDirectory + - TemporaryDirectory_Dispose_DeletesDirectory + children: + - BuildMark-TemporaryDirectory-Creation + - BuildMark-TemporaryDirectory-FilePath + - BuildMark-TemporaryDirectory-Traversal + - BuildMark-TemporaryDirectory-Cleanup diff --git a/docs/reqstream/build-mark/utilities/temporary-directory.yaml b/docs/reqstream/build-mark/utilities/temporary-directory.yaml new file mode 100644 index 00000000..c3e8ed64 --- /dev/null +++ b/docs/reqstream/build-mark/utilities/temporary-directory.yaml @@ -0,0 +1,51 @@ +--- +# Software Unit Requirements for the TemporaryDirectory Class +# +# The TemporaryDirectory class provides a disposable temporary directory that +# creates an isolated file-system location under Environment.CurrentDirectory, +# offers safe path resolution via PathHelpers, and deletes all contents on disposal. + +sections: + - title: TemporaryDirectory Unit Requirements + requirements: + - id: BuildMark-TemporaryDirectory-Creation + title: The TemporaryDirectory class shall create a uniquely-named temporary directory on construction. + justification: | + Each instance must own a distinct directory so that concurrent or sequential + uses of TemporaryDirectory do not interfere with each other. Creating under + Environment.CurrentDirectory avoids the macOS /tmp → /private/tmp symlink + mismatch that can break path-containment checks. + tests: + - TemporaryDirectory_Constructor_CreatesDirectory + - TemporaryDirectory_Constructor_CreatesUniqueDirectories + + - id: BuildMark-TemporaryDirectory-FilePath + title: The TemporaryDirectory class shall resolve relative paths and create intermediate directories via GetFilePath. + justification: | + Callers need a simple way to obtain a fully-qualified path for a file inside + the temporary directory without having to create parent directories themselves. + GetFilePath delegates path validation to PathHelpers.SafePathCombine and + creates any missing intermediate directories before returning the path. + tests: + - TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory + - TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories + + - id: BuildMark-TemporaryDirectory-Traversal + title: The TemporaryDirectory class shall reject path-traversal attempts in GetFilePath. + justification: | + Path inputs supplied by callers must not be allowed to escape the temporary + directory. GetFilePath delegates to PathHelpers.SafePathCombine, which rejects + any relative path containing ".." segments or an absolute path override. + tests: + - TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException + + - id: BuildMark-TemporaryDirectory-Cleanup + title: The TemporaryDirectory class shall delete the directory and all its contents on disposal. + justification: | + Temporary directories must not accumulate on disk across runs. Dispose must + remove the directory tree and suppress I/O errors (IOException, + UnauthorizedAccessException) so that callers in using-statements are not + disrupted even when the directory has already been deleted externally. + tests: + - TemporaryDirectory_Dispose_DeletesDirectory + - TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow diff --git a/docs/requirements_doc/introduction.md b/docs/requirements_doc/introduction.md index 2f0fc0bc..133e7073 100644 --- a/docs/requirements_doc/introduction.md +++ b/docs/requirements_doc/introduction.md @@ -30,3 +30,8 @@ This document is intended for: - Quality assurance teams validating requirements - Project stakeholders reviewing project scope - Users understanding the tool's capabilities + +## References + +- [BuildMark Releases](https://github.com/demaconsulting/BuildMark/releases) — + compiled requirements, trace matrix, and quality reports diff --git a/docs/requirements_report/introduction.md b/docs/requirements_report/introduction.md index 83adda5c..e1e6cf35 100644 --- a/docs/requirements_report/introduction.md +++ b/docs/requirements_report/introduction.md @@ -7,6 +7,11 @@ This document contains the requirements trace matrix for the BuildMark project. The trace matrix links requirements to their corresponding test cases, ensuring complete test coverage and traceability from requirements to implementation. +## Scope + +This document covers all requirements defined in `docs/reqstream/` for BuildMark and their +corresponding test evidence from unit, integration, and self-validation test runs. + ## Test Sources Requirements traceability in BuildMark uses two types of tests: @@ -46,3 +51,8 @@ All requirements must have: - At least one test case mapped to verify the requirement - All mapped tests must pass for the requirement to be satisfied - Tests must execute on supported platforms and .NET runtimes + +## References + +- [BuildMark Releases](https://github.com/demaconsulting/BuildMark/releases) — + compiled requirements, trace matrix, and quality reports diff --git a/docs/user_guide/advanced-topics.md b/docs/user_guide/advanced-topics.md index 55ce2821..0edfa120 100644 --- a/docs/user_guide/advanced-topics.md +++ b/docs/user_guide/advanced-topics.md @@ -1,5 +1,3 @@ - - # Common Use Cases ## CI/CD Integration @@ -152,12 +150,12 @@ whose `affected-versions` field includes the current build version: - [#51](https://github.com/owner/repo/issues/51): UI glitch in dark mode ``` -## Complete Changelog +## Full Changelog Provides a link to the full changelog on GitHub comparing the baseline and current versions: ```markdown -## Complete Changelog +## Full Changelog [View Full Changelog](https://github.com/owner/repo/compare/v1.2.0...v1.2.3) ``` diff --git a/docs/user_guide/cli-reference.md b/docs/user_guide/cli-reference.md index ca43d445..8d71ebfb 100644 --- a/docs/user_guide/cli-reference.md +++ b/docs/user_guide/cli-reference.md @@ -1,5 +1,3 @@ - - # Command-Line Options | Option | Description | diff --git a/docs/user_guide/configuration.md b/docs/user_guide/configuration.md index 6bd972f8..978aed3d 100644 --- a/docs/user_guide/configuration.md +++ b/docs/user_guide/configuration.md @@ -1,5 +1,3 @@ - - # Configuration File BuildMark can be configured with a `.buildmark.yaml` file placed in the repository root. This file diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 3eb7e274..bbda3495 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -34,6 +34,12 @@ The following topics are out of scope: - **Self-Validation** - Built-in qualification tests without external tooling - **Dependency Updates** - Built-in tracking of dependency changes from Dependabot and Renovate +## References + +- [BuildMark Releases](https://github.com/demaconsulting/BuildMark/releases) — compiled user guide and documentation +- [.NET SDK Download][dotnet-download] +- [Continuous Compliance][continuous-compliance] + # Continuous Compliance BuildMark follows the [Continuous Compliance][continuous-compliance] methodology, which ensures diff --git a/docs/user_guide/item-controls.md b/docs/user_guide/item-controls.md index 157f27a5..ff5a31c5 100644 --- a/docs/user_guide/item-controls.md +++ b/docs/user_guide/item-controls.md @@ -1,5 +1,3 @@ - - # Extended Item Controls BuildMark supports an optional `buildmark` code block embedded in GitHub and Azure DevOps issue and diff --git a/docs/verification/build-mark.md b/docs/verification/build-mark.md index 98aceae3..c95f1e15 100644 --- a/docs/verification/build-mark.md +++ b/docs/verification/build-mark.md @@ -1,19 +1,26 @@ # BuildMark -## Verification Approach +## Verification Strategy -BuildMark is verified at the system level through a set of integration and end-to-end -tests that exercise the full pipeline from CLI invocation to build notes generation. -The `ProgramTests.cs` file exercises the entry point with all supported flags and -validates both exit codes and console output. The `RepoConnectorsTests.cs` file -exercises the full data pipeline, from connector factory creation through -`GetBuildInformationAsync`, using mock data to cover GitHub, Azure DevOps, and Mock -connector paths. +BuildMark is verified at two levels: -Self-test (`--validate`) is covered by `Program_Run_ValidateFlag_OutputsValidationMessage` -and the self-test suite in `ValidationTests.cs`. The CI pipeline additionally runs -the full build notes generation chain with live GitHub metadata to confirm end-to-end -operation. +**System-level (integration)** testing is provided by `IntegrationTests.cs`, which runs the +BuildMark executable end-to-end via `Runner.Run()` (`dotnet `). These tests invoke the +full compiled binary, exercising the complete pipeline from CLI argument parsing through build +notes generation, and validate exit codes and console output without any in-process mocking. + +**Unit-level** testing is provided by `ProgramTests.cs`, which calls `Program.Run()` directly +with a controlled `Context` object. These tests validate individual flags and error conditions +with fast, isolated, in-process invocations. + +The `RepoConnectorsTests.cs` file exercises the full data pipeline, from connector factory +creation through `GetBuildInformationAsync`, using mock data to cover GitHub, Azure DevOps, and +Mock connector paths. + +Self-test (`--validate`) is covered by `BuildMark_ValidateFlag_RunsSelfValidation` in +`IntegrationTests.cs` and the self-test suite in `ValidationTests.cs`. The CI pipeline +additionally runs the full build notes generation chain with live GitHub metadata to confirm +end-to-end operation. ## Dependencies @@ -23,15 +30,117 @@ operation. | `MockHttpMessageHandler` | Used by GraphQL/REST client unit tests | | Context output capture | Replaces `Console.Out` with `StringWriter` for assertion | +## Test Environment + +Tests run via `dotnet test` on the CI matrix (Windows, Ubuntu, macOS) against .NET 8, 9, +and 10. No external services are required for unit and integration tests; all HTTP +communication is intercepted by `MockHttpMessageHandler`. A live GitHub Actions environment +is used for the end-to-end CI validation of the report generation pipeline. + +## Acceptance Criteria + +The system-level test run passes when: all automated tests in `ProgramTests.cs` and +`RepoConnectorsTests.cs` complete with zero failures; the CI pipeline executes the +end-to-end build notes generation step without error; and there are no unresolved +anomalies of Error severity. + ## Test Scenarios (System-Level) +### BuildMark_VersionFlag_OutputsVersion + +**Scenario**: BuildMark executable is invoked with the `--version` flag via `Runner.Run()`. + +**Expected**: Version string is written to output; exit code is 0. + +**Requirement coverage**: `BuildMark-Command-Version` + +### BuildMark_HelpFlag_OutputsUsageInformation + +**Scenario**: BuildMark executable is invoked with the `--help` flag via `Runner.Run()`. + +**Expected**: Usage information including available options is written to output; exit code is 0. + +**Requirement coverage**: `BuildMark-Command-Help` + +### BuildMark_SilentFlag_SuppressesOutput + +**Scenario**: BuildMark executable is invoked with `--silent --help` flags via `Runner.Run()`. + +**Expected**: No banner is written to output; exit code is 0. + +**Requirement coverage**: `BuildMark-Command-Silent` + +### BuildMark_InvalidArgument_ShowsError + +**Scenario**: BuildMark executable is invoked with an unrecognized argument via `Runner.Run()`. + +**Expected**: An error message containing "Unsupported argument" is written to output; exit code is 1. + +**Requirement coverage**: `BuildMark-Command-ExitCode` + +### BuildMark_ValidateFlag_RunsSelfValidation + +**Scenario**: BuildMark executable is invoked with the `--validate` flag via `Runner.Run()`. + +**Expected**: Self-validation output is written; exit code is 0. + +**Requirement coverage**: `BuildMark-Validation-SelfValidation` + +### BuildMark_LogParameter_IsAccepted + +**Scenario**: BuildMark executable is invoked with `--log test.log --help` via `Runner.Run()`. + +**Expected**: Exit code is 0; no "Unsupported argument" error in output. + +**Requirement coverage**: `BuildMark-Command-Log` + +### BuildMark_ReportParameter_IsAccepted + +**Scenario**: BuildMark executable is invoked with `--report output.md --help` via `Runner.Run()`. + +**Expected**: Exit code is 0; no "Unsupported argument" error in output. + +**Requirement coverage**: `BuildMark-Report-Markdown` + +### BuildMark_DepthParameter_IsAccepted + +**Scenario**: BuildMark executable is invoked with `--depth 2 --help` via `Runner.Run()`. + +**Expected**: Exit code is 0; no "Unsupported argument" error in output. + +**Requirement coverage**: `BuildMark-Command-Depth` + +### BuildMark_BuildVersionParameter_IsAccepted + +**Scenario**: BuildMark executable is invoked with `--build-version 1.0.0 --help` via `Runner.Run()`. + +**Expected**: Exit code is 0; no "Unsupported argument" error in output. + +**Requirement coverage**: `BuildMark-Command-BuildVersion` + +### BuildMark_ResultsParameter_IsAccepted + +**Scenario**: BuildMark executable is invoked with `--results results.trx --help` via `Runner.Run()`. + +**Expected**: Exit code is 0; no "Unsupported argument" error in output. + +**Requirement coverage**: `BuildMark-Command-Results` + +### BuildMark_LintFlag_IsAccepted + +**Scenario**: BuildMark executable is invoked with the `--lint` flag via `Runner.Run()`. + +**Expected**: Exit code is 0; no "Unsupported argument" error in output. + +**Requirement coverage**: `BuildMark-Config-Lint` + ### Program_Version_ReturnsValidVersion **Scenario**: `Program.Version` property is accessed. **Expected**: Returns a non-null, non-empty version string in semver format. -**Requirement coverage**: `BuildMark-Program-Version` +**Requirement coverage**: `BuildMark-Command-Version` ### Program_Run_VersionFlag_OutputsVersionToConsole @@ -39,7 +148,7 @@ operation. **Expected**: Version string is written to context output; exit code is 0. -**Requirement coverage**: `BuildMark-Program-Version` +**Requirement coverage**: `BuildMark-Command-Version` ### Program_Run_HelpFlag_OutputsHelpMessage @@ -47,7 +156,7 @@ operation. **Expected**: Help text is written to context output; exit code is 0. -**Requirement coverage**: `BuildMark-Program-Help` +**Requirement coverage**: `BuildMark-Command-Help` ### Program_Run_QuestionMarkFlag_OutputsHelpMessage @@ -55,7 +164,7 @@ operation. **Expected**: Help text is written to context output; exit code is 0. -**Requirement coverage**: `BuildMark-Program-Help` +**Requirement coverage**: `BuildMark-Command-Help` ### Program_Run_LongHelpFlag_OutputsHelpMessage @@ -63,7 +172,7 @@ operation. **Expected**: Help text is written to context output; exit code is 0. -**Requirement coverage**: `BuildMark-Program-Help` +**Requirement coverage**: `BuildMark-Command-Help` ### Program_Run_ValidateFlag_OutputsValidationMessage @@ -71,7 +180,7 @@ operation. **Expected**: Validation output is written; self-test completes; exit code is 0. -**Requirement coverage**: `BuildMark-Program-Validate` +**Requirement coverage**: `BuildMark-Validation-SelfValidation` ### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues @@ -79,7 +188,7 @@ operation. **Expected**: Build notes report is generated including known issues section; exit code is 0. -**Requirement coverage**: `BuildMark-Program-Report` +**Requirement coverage**: `BuildMark-Report-Markdown` ### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero @@ -87,7 +196,7 @@ operation. **Expected**: Exit code remains 0 (lint with no config is not an error). -**Requirement coverage**: `BuildMark-Program-Lint` +**Requirement coverage**: `BuildMark-Config-Lint` ### Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode @@ -95,7 +204,7 @@ operation. **Expected**: Error message is written to stderr; exit code is 1. -**Requirement coverage**: `BuildMark-Program-ErrorHandling` +**Requirement coverage**: `BuildMark-Program-ErrorHandling-InvalidBuildVersion` ### Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode @@ -103,16 +212,23 @@ operation. **Expected**: Error message is written to stderr; exit code is 1. -**Requirement coverage**: `BuildMark-Program-ErrorHandling` +**Requirement coverage**: `BuildMark-Program-ErrorHandling-ConnectorFailure` ## Requirements Coverage -- **BuildMark-Program-Version**: Program_Version_ReturnsValidVersion, - Program_Run_VersionFlag_OutputsVersionToConsole -- **BuildMark-Program-Help**: Program_Run_HelpFlag_OutputsHelpMessage, +- **BuildMark-Command-Version**: BuildMark_VersionFlag_OutputsVersion, + Program_Version_ReturnsValidVersion, Program_Run_VersionFlag_OutputsVersionToConsole +- **BuildMark-Command-Help**: BuildMark_HelpFlag_OutputsUsageInformation, + Program_Run_HelpFlag_OutputsHelpMessage, Program_Run_QuestionMarkFlag_OutputsHelpMessage, Program_Run_LongHelpFlag_OutputsHelpMessage -- **BuildMark-Program-Validate**: Program_Run_ValidateFlag_OutputsValidationMessage -- **BuildMark-Program-Report**: Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues -- **BuildMark-Program-Lint**: Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero -- **BuildMark-Program-ErrorHandling**: Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode, +- **BuildMark-Validation-SelfValidation**: BuildMark_ValidateFlag_RunsSelfValidation, + Program_Run_ValidateFlag_OutputsValidationMessage +- **BuildMark-Report-Markdown**: BuildMark_ReportParameter_IsAccepted, + Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues +- **BuildMark-Config-Lint**: BuildMark_LintFlag_IsAccepted, + Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero +- **BuildMark-Command-ExitCode**: BuildMark_InvalidArgument_ShowsError +- **BuildMark-Program-ErrorHandling-InvalidBuildVersion**: + Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode +- **BuildMark-Program-ErrorHandling-ConnectorFailure**: Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode diff --git a/docs/verification/build-mark/build-notes.md b/docs/verification/build-mark/build-notes.md index 39e81173..9d0b978e 100644 --- a/docs/verification/build-mark/build-notes.md +++ b/docs/verification/build-mark/build-notes.md @@ -1,6 +1,6 @@ ## BuildNotes -### Verification Approach +### Verification Strategy `BuildNotes` is the subsystem encompassing `BuildInformation`, `ItemInfo`, and `WebLink`. It is verified with dedicated subsystem tests in `BuildNotesTests.cs`. The subsystem uses @@ -13,6 +13,17 @@ doubles are required. | ------------------- | -------------------------------------------------------------------- | | `MockRepoConnector` | Provides deterministic `BuildInformation` for subsystem-level tests. | +### Test Environment + +N/A - standard test environment. `BuildNotesTests.cs` runs within the standard +`dotnet test` host; no external services, live network, or special configuration +are required. + +### Acceptance Criteria + +All tests in `BuildNotesTests.cs` pass with zero failures. All `BuildMark-BuildNotes-*` +requirements have at least one test in the Requirements Coverage mapping. + ### Test Scenarios #### BuildNotes_ReportModel_GeneratesCorrectMarkdown diff --git a/docs/verification/build-mark/build-notes/build-information.md b/docs/verification/build-mark/build-notes/build-information.md index 055e0ff6..d68243b0 100644 --- a/docs/verification/build-mark/build-notes/build-information.md +++ b/docs/verification/build-mark/build-notes/build-information.md @@ -15,6 +15,14 @@ further mocking is needed. | `MockRepoConnector` | Supplies deterministic `BuildInformation` instances for rendering tests. | | `NSubstitute` (`IRepoConnector`) | Simulates error conditions that `MockRepoConnector` cannot reproduce. | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildInformation_GetBuildInformationAsync_ThrowsWhenNoVersionAndNoTags diff --git a/docs/verification/build-mark/build-notes/item-info.md b/docs/verification/build-mark/build-notes/item-info.md index 2384558b..bfa5e219 100644 --- a/docs/verification/build-mark/build-notes/item-info.md +++ b/docs/verification/build-mark/build-notes/item-info.md @@ -13,6 +13,14 @@ needed. | ------------------- | -------------------------------------------------------------------- | | `MockRepoConnector` | Provides `BuildInformation` instances with known `ItemInfo` entries. | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildInformation_GetBuildInformationAsync_OrdersChangesByIndex diff --git a/docs/verification/build-mark/build-notes/web-link.md b/docs/verification/build-mark/build-notes/web-link.md index 42b00e47..40a1dc66 100644 --- a/docs/verification/build-mark/build-notes/web-link.md +++ b/docs/verification/build-mark/build-notes/web-link.md @@ -12,6 +12,14 @@ No mocking is required. | ----------- | --------------- | | None | No mocks needed | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### WebLink_Constructor_StoresTextAndUrl diff --git a/docs/verification/build-mark/cli.md b/docs/verification/build-mark/cli.md index db51ad20..c6ce6499 100644 --- a/docs/verification/build-mark/cli.md +++ b/docs/verification/build-mark/cli.md @@ -1,6 +1,6 @@ ## Cli -### Verification Approach +### Verification Strategy The Cli subsystem is verified through `CliTests.cs`, which exercises the `Context` class directly by constructing instances with various argument combinations and @@ -15,6 +15,18 @@ and output behavior. | `StringWriter` | Captures context output for assertion without console side effects | | In-process arguments | Passed directly to `Context` constructor instead of `args[]` | +### Test Environment + +N/A - standard test environment. `CliTests.cs` runs within the standard `dotnet test` +host; no external services, live network, or file system side effects beyond an +in-process `StringWriter` are required. + +### Acceptance Criteria + +All tests in `CliTests.cs` pass with zero failures. All `BuildMark-Cli-*` and referenced +`BuildMark-Program-*` requirements have at least one test in the Requirements Coverage +mapping. + ### Test Scenarios #### Cli_Context_EmptyArguments_CreatesValidContext diff --git a/docs/verification/build-mark/cli/context.md b/docs/verification/build-mark/cli/context.md index a0df0c33..054195b8 100644 --- a/docs/verification/build-mark/cli/context.md +++ b/docs/verification/build-mark/cli/context.md @@ -15,6 +15,14 @@ properties and exit codes, and verify output written to captured streams. `Context` has no dependencies on other tool units. All dependencies are real .NET BCL types; no mocking is needed at this level. +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### Context_Create_EmptyArguments_CreatesValidContext diff --git a/docs/verification/build-mark/configuration.md b/docs/verification/build-mark/configuration.md index b0b66875..5e59e6a2 100644 --- a/docs/verification/build-mark/configuration.md +++ b/docs/verification/build-mark/configuration.md @@ -1,6 +1,6 @@ ## Configuration -### Verification Approach +### Verification Strategy The Configuration subsystem is verified with dedicated subsystem tests in `ConfigurationSubsystemTests.cs`. Tests create temporary `.buildmark.yaml` files, call @@ -9,9 +9,22 @@ is required; the real file system is used with temporary directories. ### Dependencies -| Mock / Stub | Reason | -| ----------- | -------------------------------------------------------------------- | -| File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` | +| Mock / Stub | Reason | +| ----------- | --------------------------------------------------------------------------------------------------------- | +| File system | Tests create temporary `.buildmark.yaml` files via `TemporaryDirectory` in the current working directory. | + +### Test Environment + +Tests create temporary `.buildmark.yaml` files through `TemporaryDirectory`, which +creates a unique `tmp-*` subdirectory under the current working directory. Write +access to the current working directory is required. No network access or external +services are needed. + +### Acceptance Criteria + +All tests in `ConfigurationSubsystemTests.cs` pass with zero failures. All +`BuildMark-Configuration-*` requirements have at least one test in the Requirements +Coverage mapping. ### Test Scenarios diff --git a/docs/verification/build-mark/configuration/azure-devops-connector-config.md b/docs/verification/build-mark/configuration/azure-devops-connector-config.md index d998a887..63baa547 100644 --- a/docs/verification/build-mark/configuration/azure-devops-connector-config.md +++ b/docs/verification/build-mark/configuration/azure-devops-connector-config.md @@ -13,6 +13,14 @@ alias key support. No mocking is required. | ----------- | -------------------------------------------------------------------- | | File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildMarkConfigReader_ReadAsync_ValidAzureDevOpsConnector_ReturnsParsedConfiguration diff --git a/docs/verification/build-mark/configuration/build-mark-config-reader.md b/docs/verification/build-mark/configuration/build-mark-config-reader.md index e8ad5b00..8e094988 100644 --- a/docs/verification/build-mark/configuration/build-mark-config-reader.md +++ b/docs/verification/build-mark/configuration/build-mark-config-reader.md @@ -13,6 +13,14 @@ is used. | ----------- | -------------------------------------------------------------------- | | File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildMarkConfigReader_ReadAsync_MissingFile_ReturnsEmptyResult diff --git a/docs/verification/build-mark/configuration/build-mark-config.md b/docs/verification/build-mark/configuration/build-mark-config.md index ec9c98b4..c76b50a8 100644 --- a/docs/verification/build-mark/configuration/build-mark-config.md +++ b/docs/verification/build-mark/configuration/build-mark-config.md @@ -13,6 +13,14 @@ mocking is required. | ----------- | --------------- | | None | No mocks needed | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection diff --git a/docs/verification/build-mark/configuration/configuration-issue.md b/docs/verification/build-mark/configuration/configuration-issue.md index 8ef9d0ec..cbb5a94e 100644 --- a/docs/verification/build-mark/configuration/configuration-issue.md +++ b/docs/verification/build-mark/configuration/configuration-issue.md @@ -12,6 +12,14 @@ their `FilePath`, `Line`, `Severity`, and `Description` properties. No mocking i | ----------- | --------------- | | None | No mocks needed | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### ConfigurationLoadResult_ReportTo_ErrorIssue_SetsExitCode diff --git a/docs/verification/build-mark/configuration/configuration-load-result.md b/docs/verification/build-mark/configuration/configuration-load-result.md index c22d845b..484ec33f 100644 --- a/docs/verification/build-mark/configuration/configuration-load-result.md +++ b/docs/verification/build-mark/configuration/configuration-load-result.md @@ -12,6 +12,14 @@ and assert on the behavior of `ReportTo(context)`. No mocking is required. | ----------- | --------------- | | None | No mocks needed | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### ConfigurationLoadResult_ReportTo_ErrorIssue_SetsExitCode diff --git a/docs/verification/build-mark/configuration/connector-config.md b/docs/verification/build-mark/configuration/connector-config.md index 23419d67..7a7876d7 100644 --- a/docs/verification/build-mark/configuration/connector-config.md +++ b/docs/verification/build-mark/configuration/connector-config.md @@ -13,6 +13,14 @@ mocking is required. | ----------- | -------------------------------------------------------------------- | | File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration diff --git a/docs/verification/build-mark/configuration/git-hub-connector-config.md b/docs/verification/build-mark/configuration/git-hub-connector-config.md index 32c2cf0b..79f633d2 100644 --- a/docs/verification/build-mark/configuration/git-hub-connector-config.md +++ b/docs/verification/build-mark/configuration/git-hub-connector-config.md @@ -12,6 +12,14 @@ correctly parsed. No mocking is required. | ----------- | -------------------------------------------------------------------- | | File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration diff --git a/docs/verification/build-mark/configuration/report-config.md b/docs/verification/build-mark/configuration/report-config.md index b0e68e0d..ac584ae8 100644 --- a/docs/verification/build-mark/configuration/report-config.md +++ b/docs/verification/build-mark/configuration/report-config.md @@ -12,6 +12,14 @@ correctly parsed or that invalid values produce error issues. No mocking is requ | ----------- | -------------------------------------------------------------------- | | File system | Tests create temporary `.buildmark.yaml` files in `Path.GetTempPath` | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildMarkConfigReader_ReadAsync_ValidReportSection_ReturnsParsedReportConfig diff --git a/docs/verification/build-mark/configuration/rule-config.md b/docs/verification/build-mark/configuration/rule-config.md index 526498a3..e0e3e848 100644 --- a/docs/verification/build-mark/configuration/rule-config.md +++ b/docs/verification/build-mark/configuration/rule-config.md @@ -13,6 +13,14 @@ returned rules. No mocking is required. | ----------- | --------------- | | None | No mocks needed | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection diff --git a/docs/verification/build-mark/configuration/rule-match-config.md b/docs/verification/build-mark/configuration/rule-match-config.md index b747ca34..f265faf9 100644 --- a/docs/verification/build-mark/configuration/rule-match-config.md +++ b/docs/verification/build-mark/configuration/rule-match-config.md @@ -13,6 +13,14 @@ the match comparison behavior. | ----------- | ---------- | | None | Data class | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios (Integration) ##### ItemRouter_Route_WithWorkItemTypeMatch_RoutesMatchingItem diff --git a/docs/verification/build-mark/configuration/section-config.md b/docs/verification/build-mark/configuration/section-config.md index 5a21cc66..080c82bd 100644 --- a/docs/verification/build-mark/configuration/section-config.md +++ b/docs/verification/build-mark/configuration/section-config.md @@ -13,6 +13,14 @@ sections. No mocking is required. | ----------- | --------------- | | None | No mocks needed | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection diff --git a/docs/verification/build-mark/program.md b/docs/verification/build-mark/program.md index 11ec4eaf..cd6b8898 100644 --- a/docs/verification/build-mark/program.md +++ b/docs/verification/build-mark/program.md @@ -14,6 +14,14 @@ to avoid live API calls where needed. | `Context` | Constructed with controlled arguments and output capture | | Connector factory mock | Injected to avoid live API calls | +### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + ### Test Scenarios #### Program_Version_ReturnsValidVersion @@ -88,6 +96,8 @@ to avoid live API calls where needed. **Requirement coverage**: `BuildMark-Program-ErrorHandling-InvalidBuildVersion` +#### Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode + **Scenario**: `Program.Run` is called but connector factory throws `InvalidOperationException`. **Expected**: Error is written to stderr; exit code is 1. diff --git a/docs/verification/build-mark/repo-connectors.md b/docs/verification/build-mark/repo-connectors.md index ca6c16de..8d3c6abc 100644 --- a/docs/verification/build-mark/repo-connectors.md +++ b/docs/verification/build-mark/repo-connectors.md @@ -1,6 +1,6 @@ ## RepoConnectors -### Verification Approach +### Verification Strategy The RepoConnectors subsystem is verified through `RepoConnectorsTests.cs`, which contains 33 subsystem-level integration tests. These tests exercise the connector @@ -16,6 +16,18 @@ unit tests for sub-components are described in the unit-level chapters. | `MockRepoConnector` | Used directly for factory and base class tests | | `ProcessRunner` (real) | Used by ProcessRunner tests with actual OS commands | +### Test Environment + +No external network access is required; all HTTP calls to the GitHub GraphQL API and +Azure DevOps REST API are intercepted by `MockHttpMessageHandler`. Tests run within +the standard `dotnet test` host. + +### Acceptance Criteria + +All 33 integration tests in `RepoConnectorsTests.cs` pass with zero failures. All +`BuildMark-RepoConnectors-*` requirements have at least one test in the Requirements +Coverage mapping. + ### Test Scenarios #### RepoConnectors_GitHubConnector_ImplementsInterface_ReturnsTrue diff --git a/docs/verification/build-mark/repo-connectors/azure-devops.md b/docs/verification/build-mark/repo-connectors/azure-devops.md index 1602c2be..4e217633 100644 --- a/docs/verification/build-mark/repo-connectors/azure-devops.md +++ b/docs/verification/build-mark/repo-connectors/azure-devops.md @@ -1,10 +1,10 @@ ### Azure DevOps -#### Verification Approach +#### Verification Strategy The Azure DevOps sub-subsystem is verified through `AzureDevOpsTests.cs` (5 subsystem- -level tests), `AzureDevOpsRepoConnectorTests.cs` (25 unit tests), -`AzureDevOpsRestClientTests.cs` (8 unit tests), and `WorkItemMapperTests.cs` (10 unit +level tests), `AzureDevOpsRepoConnectorTests.cs` (32 unit tests), +`AzureDevOpsRestClientTests.cs` (12 unit tests), and `WorkItemMapperTests.cs` (13 unit tests). The subsystem tests exercise the full Azure DevOps data pipeline through mock HTTP responses. The unit tests are described in the individual unit chapters. @@ -14,15 +14,26 @@ HTTP responses. The unit tests are described in the individual unit chapters. | ------------------------ | -------------------------------------------------- | | `MockHttpMessageHandler` | Intercepts HTTP calls to the Azure DevOps REST API | +#### Test Environment + +N/A - standard test environment. All HTTP calls to the Azure DevOps REST API are +intercepted by `MockHttpMessageHandler`; no live network access is required. + +#### Acceptance Criteria + +All 5 subsystem tests in `AzureDevOpsTests.cs` pass with zero failures. All +`BuildMark-AzureDevOps-SubSystem` requirements have at least one test in the +Requirements Coverage mapping. + #### Test Scenarios (Subsystem-Level, AzureDevOpsTests.cs) -##### AzureDevOps_ImplementsInterface_ReturnsTrue +##### AzureDevOps_IRepoConnector_ConnectorInstance_ImplementsInterface **Scenario**: `AzureDevOpsRepoConnector` is checked against `IRepoConnector`. **Expected**: Implements the interface. -**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` +**Requirement coverage**: `BuildMark-AzureDevOps-SubSystem` ##### AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation @@ -30,23 +41,23 @@ HTTP responses. The unit tests are described in the individual unit chapters. **Expected**: Returns valid `BuildInformation` with correct fields. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` +**Requirement coverage**: `BuildMark-AzureDevOps-SubSystem` -##### AzureDevOps_GetBuildInformation_WithWorkItems_GathersChanges +##### AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges -**Scenario**: Mock data includes work items linked to commits. +**Scenario**: Mock data includes work items linked to pull requests. **Expected**: Work items appear in `BuildInformation.Changes`. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` +**Requirement coverage**: `BuildMark-AzureDevOps-SubSystem` -##### AzureDevOps_GetBuildInformation_WithOpenBugs_IdentifiesKnownIssues +##### AzureDevOps_GetBuildInformation_WithOpenWorkItems_IdentifiesKnownIssues **Scenario**: Mock data includes open bug work items. **Expected**: Bugs appear in `BuildInformation.KnownIssues`. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` +**Requirement coverage**: `BuildMark-AzureDevOps-SubSystem` ##### AzureDevOps_GetBuildInformation_ReleaseVersion_SkipsPreReleases @@ -54,12 +65,12 @@ HTTP responses. The unit tests are described in the individual unit chapters. **Expected**: Baseline is the previous release tag. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` +**Requirement coverage**: `BuildMark-AzureDevOps-SubSystem` #### Requirements Coverage -- **BuildMark-RepoConnectors-IRepoConnector**: AzureDevOps_ImplementsInterface_ReturnsTrue -- **BuildMark-RepoConnectors-AzureDevOps**: AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation, - AzureDevOps_GetBuildInformation_WithWorkItems_GathersChanges, - AzureDevOps_GetBuildInformation_WithOpenBugs_IdentifiesKnownIssues, +- **BuildMark-AzureDevOps-SubSystem**: AzureDevOps_IRepoConnector_ConnectorInstance_ImplementsInterface, + AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation, + AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges, + AzureDevOps_GetBuildInformation_WithOpenWorkItems_IdentifiesKnownIssues, AzureDevOps_GetBuildInformation_ReleaseVersion_SkipsPreReleases diff --git a/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md b/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md index ea3fa3dd..39f722fc 100644 --- a/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md +++ b/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md @@ -14,6 +14,14 @@ deserialized data. | ------------------------ | ------------------------------------------------------------ | | `MockHttpMessageHandler` | Provides JSON payloads whose structure matches the DTO types | +##### Test Environment + +Tests use `MockHttpMessageHandler` to intercept HTTP calls. No real network access or Azure DevOps token is required. + +##### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + ##### Test Scenarios (via AzureDevOpsRestClientTests.cs) ###### AzureDevOpsRestClient_GetTagsAsync_ValidResponse_ReturnsTags @@ -22,7 +30,7 @@ deserialized data. **Expected**: Tag DTO fields are populated correctly. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsApiTypes` +**Requirement coverage**: `BuildMark-AzureDevOps-ApiTypes` ###### AzureDevOpsRestClient_GetCommitsAsync_ValidResponse_ReturnsCommits @@ -30,7 +38,7 @@ deserialized data. **Expected**: Commit DTO fields are populated correctly. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsApiTypes` +**Requirement coverage**: `BuildMark-AzureDevOps-ApiTypes` ###### AzureDevOpsRestClient_GetWorkItemsAsync_ValidResponse_ReturnsWorkItems @@ -38,18 +46,17 @@ deserialized data. **Expected**: Work item DTO fields are populated correctly. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsApiTypes` +**Requirement coverage**: `BuildMark-AzureDevOps-ApiTypes` -###### AzureDevOpsRestClient_GetWorkItemLinksAsync_ValidResponse_ReturnsWorkItemLinks +###### AzureDevOpsRestClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests -**Scenario**: Work item links REST response is deserialized into work item link DTOs. +**Scenario**: Pull requests REST response is deserialized into pull request DTOs. -**Expected**: Work item link DTO fields are populated correctly. +**Expected**: Pull request DTO fields are populated correctly. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsApiTypes` +**Requirement coverage**: `BuildMark-AzureDevOps-ApiTypes` ##### Requirements Coverage -- **BuildMark-RepoConnectors-AzureDevOpsApiTypes**: Verified indirectly through all - 8 tests in `AzureDevOpsRestClientTests.cs` and all 10 tests in - `WorkItemMapperTests.cs` +- **BuildMark-AzureDevOps-ApiTypes**: Verified indirectly through all 12 tests in + `AzureDevOpsRestClientTests.cs` and all 13 tests in `WorkItemMapperTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md b/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md index 3f9c3bd1..56dc6ba1 100644 --- a/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md +++ b/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md @@ -14,6 +14,14 @@ work item deduplication and version tag handling. | ------------------------ | ----------------------------------------------------------- | | `MockHttpMessageHandler` | Intercepts all HTTP calls to the Azure DevOps REST endpoint | +##### Test Environment + +Tests use `MockHttpMessageHandler` to intercept HTTP calls. No real network access or Azure DevOps token is required. + +##### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + ##### Test Scenarios ###### AzureDevOpsRepoConnector_Constructor_CreatesInstance diff --git a/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md b/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md index d58e7287..f390f72f 100644 --- a/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md +++ b/docs/verification/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md @@ -3,9 +3,9 @@ ##### Verification Approach `AzureDevOpsRestClient` is tested through `AzureDevOpsRestClientTests.cs`, which -contains 8 unit tests. The tests cover successful data retrieval for tags, commits, -work items, and work item links, as well as HTTP error handling and invalid JSON -handling. +contains 12 unit tests. The tests cover successful data retrieval for repository +metadata, tags, commits, pull requests, work items, and WIQL queries, as well as +response variants (string-valued ids) and error handling (HTTP errors, empty inputs). ##### Dependencies @@ -13,23 +13,31 @@ handling. | ------------------------ | ------------------------------------------------------------ | | `MockHttpMessageHandler` | Intercepts all HTTP calls to the Azure DevOps REST endpoints | +##### Test Environment + +Tests use `MockHttpMessageHandler` to intercept HTTP calls. No real network access or Azure DevOps token is required. + +##### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + ##### Test Scenarios -###### AzureDevOpsRestClient_GetTagsAsync_ValidResponse_ReturnsTags +###### AzureDevOpsRestClient_GetRepositoryAsync_ValidResponse_ReturnsRepository -**Scenario**: Valid REST API response for the tags endpoint. +**Scenario**: Valid REST API response for the repository metadata endpoint. -**Expected**: Returns the list of tags from the response. +**Expected**: Returns a repository record with correct `Id`, `Name`, and `RemoteUrl`. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` -###### AzureDevOpsRestClient_GetTagsAsync_HttpError_ReturnsEmptyList +###### AzureDevOpsRestClient_GetRepositoryAsync_HttpError_ThrowsHttpRequestException -**Scenario**: Tags endpoint returns a non-success status code. +**Scenario**: Repository metadata endpoint returns an HTTP error response (e.g., 404 Not Found). -**Expected**: Returns an empty list without throwing. +**Expected**: `GetRepositoryAsync` throws `HttpRequestException`. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` ###### AzureDevOpsRestClient_GetCommitsAsync_ValidResponse_ReturnsCommits @@ -37,15 +45,39 @@ handling. **Expected**: Returns the list of commits from the response. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` + +###### AzureDevOpsRestClient_GetTagsAsync_ValidResponse_ReturnsTags + +**Scenario**: Valid REST API response for the tags endpoint. + +**Expected**: Returns the list of tags from the response. + +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` + +###### AzureDevOpsRestClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests + +**Scenario**: Valid REST API response for the pull requests endpoint. + +**Expected**: Returns the list of pull requests from the response. + +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` + +###### AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_ValidResponse_ReturnsWorkItemRefs + +**Scenario**: Valid REST API response for the PR work items endpoint. -###### AzureDevOpsRestClient_GetCommitsAsync_HttpError_ReturnsEmptyList +**Expected**: Returns the list of work item references from the response. -**Scenario**: Commits endpoint returns a non-success status code. +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` -**Expected**: Returns an empty list without throwing. +###### AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_StringValuedIds_DeserializesCorrectly -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` +**Scenario**: PR work items endpoint returns ids serialized as JSON strings rather than numbers. + +**Expected**: Work item ids are deserialized as integers. + +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` ###### AzureDevOpsRestClient_GetWorkItemsAsync_ValidResponse_ReturnsWorkItems @@ -53,33 +85,40 @@ handling. **Expected**: Returns the list of work items from the response. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` + +###### AzureDevOpsRestClient_QueryWorkItemsAsync_ValidWiql_ReturnsWorkItemIds + +**Scenario**: Valid REST API response for the WIQL query endpoint. + +**Expected**: Returns the list of work item ids from the WIQL query result. + +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` -###### AzureDevOpsRestClient_GetWorkItemsAsync_HttpError_ReturnsEmptyList +###### AzureDevOpsRestClient_QueryWorkItemsAsync_StringValuedIds_DeserializesCorrectly -**Scenario**: Work items endpoint returns a non-success status code. +**Scenario**: WIQL query endpoint returns ids serialized as JSON strings rather than numbers. -**Expected**: Returns an empty list without throwing. +**Expected**: Work item ids are deserialized as integers. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` -###### AzureDevOpsRestClient_GetWorkItemLinksAsync_ValidResponse_ReturnsWorkItemLinks +###### AzureDevOpsRestClient_QueryWorkItemsAsync_WithHttpError_ThrowsInvalidOperationException -**Scenario**: Valid REST API response for the work item links endpoint. +**Scenario**: WIQL query endpoint returns an HTTP error response (e.g., 400 Bad Request). -**Expected**: Returns the list of work item links from the response. +**Expected**: `QueryWorkItemsAsync` throws `InvalidOperationException`. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` -###### AzureDevOpsRestClient_GetWorkItemLinksAsync_HttpError_ReturnsEmptyList +###### AzureDevOpsRestClient_GetWorkItemsAsync_WithEmptyInput_ReturnsEmptyList -**Scenario**: Work item links endpoint returns a non-success status code. +**Scenario**: `GetWorkItemsAsync` is called with an empty list of work item ids. -**Expected**: Returns an empty list without throwing. +**Expected**: Returns an empty list without making any HTTP call. -**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` +**Requirement coverage**: `BuildMark-AzureDevOps-RestClient` ##### Requirements Coverage -- **BuildMark-RepoConnectors-AzureDevOpsRestClient**: All 8 tests in - `AzureDevOpsRestClientTests.cs` +- **BuildMark-AzureDevOps-RestClient**: All 12 tests in `AzureDevOpsRestClientTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/azure-devops/work-item-mapper.md b/docs/verification/build-mark/repo-connectors/azure-devops/work-item-mapper.md index 97d62fb2..1cfd34d3 100644 --- a/docs/verification/build-mark/repo-connectors/azure-devops/work-item-mapper.md +++ b/docs/verification/build-mark/repo-connectors/azure-devops/work-item-mapper.md @@ -2,7 +2,7 @@ ##### Verification Approach -`WorkItemMapper` is tested through `WorkItemMapperTests.cs`, which contains 11 unit +`WorkItemMapper` is tested through `WorkItemMapperTests.cs`, which contains 13 unit tests. The tests verify mapping of Azure DevOps work items to the BuildMark model - classification of features and bugs, type normalization, suppression of Removed work items, resolved-state identification, rule-matching type retrieval, and custom field @@ -14,6 +14,14 @@ extraction for visibility and affected-versions controls. | -------------------- | ----------------------------------------------------------- | | `WorkItem` test data | Constructed in-line with specific types, states, and fields | +##### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +##### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + ##### Test Scenarios ###### WorkItemMapper_MapWorkItemToItemInfo_BugType_ReturnsBugItem @@ -24,6 +32,14 @@ extraction for visibility and affected-versions controls. **Requirement coverage**: `BuildMark-AzureDevOps-WorkItemMapper` +###### WorkItemMapper_MapWorkItemToItemInfo_IssueType_ReturnsBugItem + +**Scenario**: A work item with type `"Issue"` is mapped. + +**Expected**: `ItemInfo.Type` is `"bug"`. + +**Requirement coverage**: `BuildMark-AzureDevOps-WorkItemMapper` + ###### WorkItemMapper_MapWorkItemToItemInfo_UserStoryType_ReturnsFeatureItem **Scenario**: A work item with type `"User Story"` is mapped. @@ -32,6 +48,14 @@ extraction for visibility and affected-versions controls. **Requirement coverage**: `BuildMark-AzureDevOps-WorkItemMapper` +###### WorkItemMapper_MapWorkItemToItemInfo_FeatureType_ReturnsFeatureItem + +**Scenario**: A work item with type `"Feature"` is mapped. + +**Expected**: `ItemInfo.Type` is `"feature"`. + +**Requirement coverage**: `BuildMark-AzureDevOps-WorkItemMapper` + ###### WorkItemMapper_MapWorkItemToItemInfo_EpicType_ReturnsFeatureItem **Scenario**: A work item with type `"Epic"` is mapped. @@ -111,7 +135,9 @@ block value. ##### Requirements Coverage - **BuildMark-AzureDevOps-WorkItemMapper**: `WorkItemMapper_MapWorkItemToItemInfo_BugType_ReturnsBugItem`, + `WorkItemMapper_MapWorkItemToItemInfo_IssueType_ReturnsBugItem`, `WorkItemMapper_MapWorkItemToItemInfo_UserStoryType_ReturnsFeatureItem`, + `WorkItemMapper_MapWorkItemToItemInfo_FeatureType_ReturnsFeatureItem`, `WorkItemMapper_MapWorkItemToItemInfo_EpicType_ReturnsFeatureItem`, `WorkItemMapper_MapWorkItemToItemInfo_TaskType_ReturnsTaskItem`, `WorkItemMapper_IsWorkItemResolved_ResolvedState_ReturnsTrue`, diff --git a/docs/verification/build-mark/repo-connectors/github.md b/docs/verification/build-mark/repo-connectors/github.md index 3be43356..daaeae8e 100644 --- a/docs/verification/build-mark/repo-connectors/github.md +++ b/docs/verification/build-mark/repo-connectors/github.md @@ -1,6 +1,6 @@ ### GitHub -#### Verification Approach +#### Verification Strategy The GitHub sub-subsystem is verified through `GitHubTests.cs` (6 subsystem-level tests), `GitHubRepoConnectorTests.cs` (22 unit tests), and 5 `GitHubGraphQLClient*Tests.cs` @@ -13,6 +13,17 @@ mock HTTP responses. The unit tests are described in the individual unit chapter | ------------------------ | ----------------------------------------------- | | `MockHttpMessageHandler` | Intercepts HTTP calls to the GitHub GraphQL API | +#### Test Environment + +N/A - standard test environment. All HTTP calls to the GitHub GraphQL API are +intercepted by `MockHttpMessageHandler`; no live network access is required. + +#### Acceptance Criteria + +All 6 subsystem tests in `GitHubTests.cs` pass with zero failures. All +`BuildMark-RepoConnectors-GitHub` requirements have at least one test in the +Requirements Coverage mapping. + #### Test Scenarios (Subsystem-Level, GitHubTests.cs) ##### GitHub_ImplementsInterface_ReturnsTrue diff --git a/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-client.md b/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-client.md index 24333d31..8f9b9176 100644 --- a/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-client.md +++ b/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-client.md @@ -2,7 +2,7 @@ ##### Verification Approach -`GitHubGraphQLClient` is tested through five dedicated test files, each covering one +`GitHubGraphQLClient` is tested through six dedicated test files, each covering one query method. All tests use `MockHttpMessageHandler` to intercept HTTP requests and return controlled JSON responses. Tests cover successful responses, empty data, missing required properties, HTTP errors, invalid JSON, single-item responses, and @@ -23,6 +23,14 @@ pagination. | ------------------------ | ------------------------------------------------------- | | `MockHttpMessageHandler` | Intercepts HTTP calls; returns controlled JSON payloads | +##### Test Environment + +Tests use `MockHttpMessageHandler` to intercept HTTP calls. No real network access or GitHub token is required. + +##### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + ##### Test Scenarios ###### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_ValidResponse_ReturnsIssueIds @@ -31,7 +39,7 @@ pagination. **Expected**: Returns the list of issue IDs extracted from the response. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_NoIssues_ReturnsEmptyList @@ -39,7 +47,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_MissingData_ReturnsEmptyList @@ -47,7 +55,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_HttpError_ReturnsEmptyList @@ -55,7 +63,7 @@ pagination. **Expected**: Returns an empty list without throwing. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_InvalidJson_ReturnsEmptyList @@ -63,7 +71,7 @@ pagination. **Expected**: Returns an empty list without throwing. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_SingleIssue_ReturnsOneIssueId @@ -71,7 +79,7 @@ pagination. **Expected**: Returns a list with one issue ID. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_MissingNumberProperty_SkipsInvalidNodes @@ -79,7 +87,7 @@ pagination. **Expected**: Invalid node is skipped; valid nodes are returned. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_WithPagination_ReturnsAllIssues @@ -87,7 +95,7 @@ pagination. **Expected**: All pages are fetched and all issue IDs are returned. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssues @@ -95,7 +103,7 @@ pagination. **Expected**: Returns all issues from the response. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllIssuesAsync_NoIssues_ReturnsEmptyList @@ -103,7 +111,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllIssuesAsync_MissingData_ReturnsEmptyList @@ -111,7 +119,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllIssuesAsync_NullNodes_ReturnsEmptyList @@ -119,7 +127,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllIssuesAsync_InvalidIssues_FiltersThemOut @@ -127,7 +135,7 @@ pagination. **Expected**: Invalid issues are filtered out; valid issues are returned. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllIssuesAsync_WithPagination_ReturnsAllIssues @@ -135,7 +143,7 @@ pagination. **Expected**: All pages are fetched and all issues are returned. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllIssuesAsync_Exception_ReturnsEmptyList @@ -143,7 +151,7 @@ pagination. **Expected**: Returns an empty list without re-throwing. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssuesWithBody @@ -151,7 +159,7 @@ pagination. **Expected**: Returned issues include the body content. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllTagsAsync_ValidResponse_ReturnsTagNodes @@ -159,7 +167,7 @@ pagination. **Expected**: Returns all tag nodes from the response. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllTagsAsync_NoTags_ReturnsEmptyList @@ -167,7 +175,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllTagsAsync_MissingData_ReturnsEmptyList @@ -175,7 +183,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllTagsAsync_HttpError_ReturnsEmptyList @@ -183,7 +191,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllTagsAsync_InvalidJson_ReturnsEmptyList @@ -191,7 +199,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllTagsAsync_SingleTag_ReturnsOneTagNode @@ -199,7 +207,7 @@ pagination. **Expected**: Returns a list with one tag node. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllTagsAsync_MissingNameProperty_SkipsInvalidNodes @@ -207,7 +215,7 @@ pagination. **Expected**: Invalid node is skipped. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllTagsAsync_WithPagination_ReturnsAllTags @@ -215,7 +223,7 @@ pagination. **Expected**: All pages are fetched and all tags are returned. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetCommitsAsync_ValidResponse_ReturnsCommitShas @@ -223,7 +231,7 @@ pagination. **Expected**: Returns all commit SHAs. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetCommitsAsync_NoCommits_ReturnsEmptyList @@ -231,7 +239,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetCommitsAsync_MissingData_ReturnsEmptyList @@ -239,7 +247,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetCommitsAsync_HttpError_ReturnsEmptyList @@ -247,7 +255,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetCommitsAsync_InvalidJson_ReturnsEmptyList @@ -255,7 +263,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetCommitsAsync_SingleCommit_ReturnsOneCommitSha @@ -263,7 +271,7 @@ pagination. **Expected**: Returns a list with one commit SHA. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetCommitsAsync_MissingOidProperty_SkipsInvalidNodes @@ -271,7 +279,7 @@ pagination. **Expected**: Invalid node is skipped. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetCommitsAsync_WithPagination_ReturnsAllCommits @@ -279,7 +287,7 @@ pagination. **Expected**: All pages are fetched and all commit SHAs are returned. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests @@ -287,7 +295,7 @@ pagination. **Expected**: Returns all pull requests. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_NoPullRequests_ReturnsEmptyList @@ -295,7 +303,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_MissingData_ReturnsEmptyList @@ -303,7 +311,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_HttpError_ReturnsEmptyList @@ -311,7 +319,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_InvalidJson_ReturnsEmptyList @@ -319,7 +327,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_SinglePullRequest_ReturnsOnePullRequest @@ -327,7 +335,7 @@ pagination. **Expected**: Returns a list with one pull request. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_MissingNumberOrTitle_SkipsInvalidNodes @@ -335,7 +343,7 @@ pagination. **Expected**: Invalid node is skipped. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_WithPagination_ReturnsAllPullRequests @@ -343,7 +351,7 @@ pagination. **Expected**: All pages are fetched and all pull requests are returned. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequestsWithBody @@ -351,7 +359,7 @@ pagination. **Expected**: Returned pull requests include the body content. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetReleasesAsync_ValidResponse_ReturnsReleaseTagNames @@ -359,7 +367,7 @@ pagination. **Expected**: Returns all release tag names. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetReleasesAsync_NoReleases_ReturnsEmptyList @@ -367,7 +375,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetReleasesAsync_MissingData_ReturnsEmptyList @@ -375,7 +383,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetReleasesAsync_HttpError_ReturnsEmptyList @@ -383,7 +391,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetReleasesAsync_InvalidJson_ReturnsEmptyList @@ -391,7 +399,7 @@ pagination. **Expected**: Returns an empty list. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetReleasesAsync_SingleRelease_ReturnsOneTagName @@ -399,7 +407,7 @@ pagination. **Expected**: Returns a list with one tag name. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetReleasesAsync_MissingTagNameProperty_SkipsInvalidNodes @@ -407,7 +415,7 @@ pagination. **Expected**: Invalid node is skipped. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetReleasesAsync_WithPagination_ReturnsAllReleases @@ -415,9 +423,11 @@ pagination. **Expected**: All pages are fetched and all release tag names are returned. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ##### Requirements Coverage -- **BuildMark-RepoConnectors-GitHubGraphQLClient**: All 41 tests across the five +- **BuildMark-GitHub-GraphQLClient**: All 49 tests across the six `GitHubGraphQLClient*Tests.cs` files +- **BuildMark-GitHub-ErrorHandling**: HTTP error, invalid JSON, and exception tests + across all six `GitHubGraphQLClient*Tests.cs` files diff --git a/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-types.md b/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-types.md index c225c1d8..60317e8b 100644 --- a/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-types.md +++ b/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-types.md @@ -13,6 +13,14 @@ JSON deserialization of mocked API responses. | ------------------------ | ------------------------------------------------------------ | | `MockHttpMessageHandler` | Provides JSON payloads whose structure matches the DTO types | +##### Test Environment + +Tests use `MockHttpMessageHandler` to intercept HTTP calls. No real network access or GitHub token is required. + +##### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + ##### Test Scenarios (via GitHubGraphQLClient*Tests.cs) ###### GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssuesWithBody @@ -21,7 +29,7 @@ JSON deserialization of mocked API responses. **Expected**: Issue DTOs contain the expected fields including `body`. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLTypes` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequestsWithBody @@ -29,7 +37,7 @@ JSON deserialization of mocked API responses. **Expected**: Pull request DTOs contain the expected fields including `body`. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLTypes` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ###### GitHubGraphQLClient_GetAllTagsAsync_ValidResponse_ReturnsTagNodes @@ -37,9 +45,9 @@ JSON deserialization of mocked API responses. **Expected**: Tag node DTOs contain `name` and target commit hash fields. -**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLTypes` +**Requirement coverage**: `BuildMark-GitHub-GraphQLClient` ##### Requirements Coverage -- **BuildMark-RepoConnectors-GitHubGraphQLTypes**: Verified indirectly through all - 41 tests in the `GitHubGraphQLClient*Tests.cs` files +- **BuildMark-GitHub-GraphQLClient**: Verified indirectly through all + 49 tests in the `GitHubGraphQLClient*Tests.cs` files diff --git a/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md b/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md index dde24997..e30b0e85 100644 --- a/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md +++ b/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md @@ -3,7 +3,7 @@ ##### Verification Approach `GitHubRepoConnector` is tested through `GitHubRepoConnectorTests.cs`, which contains -22 unit tests. The tests exercise constructor behavior (with and without config), +25 unit tests. The tests exercise constructor behavior (with and without config), the full `GetBuildInformationAsync` pipeline with various scenarios, visibility and type overrides, routing configuration, known issues filtering by affected versions, and edge cases such as duplicate commit SHAs and substring label matching. @@ -14,6 +14,14 @@ and edge cases such as duplicate commit SHAs and substring label matching. | ------------------------ | -------------------------------------------------------- | | `MockHttpMessageHandler` | Intercepts all HTTP calls to the GitHub GraphQL endpoint | +##### Test Environment + +Tests use `MockHttpMessageHandler` to intercept HTTP calls. No real network access or GitHub token is required. + +##### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + ##### Test Scenarios ###### GitHubRepoConnector_Constructor_CreatesInstance @@ -40,7 +48,7 @@ equals `"https://api.github.com"`. **Expected**: Returns `true`. -**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` +**Requirement coverage**: `BuildMark-RepoConnectorBase-Interface` ###### GitHubRepoConnector_GetBuildInformationAsync_WithMockedData_ReturnsValidBuildInformation @@ -196,7 +204,58 @@ classification. **Requirement coverage**: `BuildMark-RepoConnectors-GitHub` +###### GitHubRepoConnector_GetBuildInformationAsync_WithTokenVariable_UsesCustomVariable + +**Scenario**: `GetBuildInformationAsync` is called with a `GitHubConnectorConfig` that +specifies a `TokenVariable` name; the environment variable is set to a non-empty token. + +**Expected**: Build information is returned successfully, confirming the custom token +variable was resolved and used without throwing. + +**Requirement coverage**: `BuildMark-GitHub-TokenVariable` + +###### GitHubRepoConnector_GetBuildInformationAsync_WithTokenVariable_EmptyValue_ThrowsInvalidOperationException + +**Scenario**: `GetBuildInformationAsync` is called with a `TokenVariable` config whose +corresponding environment variable is set to an empty string. + +**Expected**: `InvalidOperationException` is thrown. + +**Requirement coverage**: `BuildMark-GitHub-TokenVariable` + +###### GitHubRepoConnector_GetBuildInformationAsync_WithTokenVariable_NotSet_ThrowsInvalidOperationException + +**Scenario**: `GetBuildInformationAsync` is called with a `TokenVariable` config whose +corresponding environment variable is not set (null). + +**Expected**: `InvalidOperationException` is thrown. + +**Requirement coverage**: `BuildMark-GitHub-TokenVariable` + ##### Requirements Coverage -- **BuildMark-RepoConnectors-IRepoConnector**: GitHubRepoConnector_ImplementsInterface_ReturnsTrue -- **BuildMark-RepoConnectors-GitHub**: All remaining 21 tests in `GitHubRepoConnectorTests.cs` +- **BuildMark-RepoConnectorBase-Interface**: GitHubRepoConnector_ImplementsInterface_ReturnsTrue +- **BuildMark-GitHub-ConnectorConfig**: GitHubRepoConnector_Constructor_CreatesInstance, + GitHubRepoConnector_Constructor_WithConfig_StoresConfigurationOverrides +- **BuildMark-GitHub-BuildInformation**: GitHubRepoConnector_GetBuildInformationAsync_WithMockedData_ReturnsValidBuildInformation, + GitHubRepoConnector_GetBuildInformationAsync_WithMultipleVersions_SelectsCorrectPreviousVersionAndGeneratesChangelogLink, + GitHubRepoConnector_GetBuildInformationAsync_WithPullRequests_GathersChangesCorrectly, + GitHubRepoConnector_GetBuildInformationAsync_WithOpenIssues_IdentifiesKnownIssues, + GitHubRepoConnector_GetBuildInformationAsync_PreReleaseWithSameCommitHash_SkipsToNextDifferentHash, + GitHubRepoConnector_GetBuildInformationAsync_ReleaseVersion_SkipsAllPreReleases, + GitHubRepoConnector_GetBuildInformationAsync_PreReleaseNotInHistory_UsesLatestDifferentHash, + GitHubRepoConnector_GetBuildInformationAsync_PreReleaseAllPreviousSameHash_ReturnsNullBaseline, + GitHubRepoConnector_GetBuildInformationAsync_WithDuplicateMergeCommitSha_DoesNotThrow, + GitHubRepoConnector_GetBuildInformationAsync_PrWithSubstringMatchLabel_NotClassifiedAsBug, + GitHubRepoConnector_GetBuildInformationAsync_IssueWithSubstringMatchLabel_NotClassifiedAsKnownIssue, + GitHubRepoConnector_GetBuildInformationAsync_KnownIssues_FilteredByAffectedVersions, + GitHubRepoConnector_GetBuildInformationAsync_ClosedBugWithMatchingAffectedVersions_IsKnownIssue +- **BuildMark-GitHub-ItemControls**: GitHubRepoConnector_GetBuildInformationAsync_VisibilityInternal_ExcludesItem, + GitHubRepoConnector_GetBuildInformationAsync_VisibilityPublic_IncludesItem, + GitHubRepoConnector_GetBuildInformationAsync_TypeBugOverride_ClassifiesAsBug, + GitHubRepoConnector_GetBuildInformationAsync_TypeFeatureOverride_ClassifiesAsFeature +- **BuildMark-GitHub-Rules**: GitHubRepoConnector_Configure_WithRules_HasRulesReturnsTrue, + GitHubRepoConnector_GetBuildInformationAsync_WithConfiguredRules_PopulatesRoutedSections +- **BuildMark-GitHub-TokenVariable**: GitHubRepoConnector_GetBuildInformationAsync_WithTokenVariable_UsesCustomVariable, + GitHubRepoConnector_GetBuildInformationAsync_WithTokenVariable_EmptyValue_ThrowsInvalidOperationException, + GitHubRepoConnector_GetBuildInformationAsync_WithTokenVariable_NotSet_ThrowsInvalidOperationException diff --git a/docs/verification/build-mark/repo-connectors/i-repo-connector.md b/docs/verification/build-mark/repo-connectors/i-repo-connector.md index 36b3be7d..7f6d537e 100644 --- a/docs/verification/build-mark/repo-connectors/i-repo-connector.md +++ b/docs/verification/build-mark/repo-connectors/i-repo-connector.md @@ -14,6 +14,14 @@ implements `IRepoConnector`. | ----------- | --------- | | None | Interface | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios (Integration via Implementations) ##### RepoConnectors_GitHubConnector_ImplementsInterface_ReturnsTrue diff --git a/docs/verification/build-mark/repo-connectors/item-controls-info.md b/docs/verification/build-mark/repo-connectors/item-controls-info.md index 9d30184c..255a7823 100644 --- a/docs/verification/build-mark/repo-connectors/item-controls-info.md +++ b/docs/verification/build-mark/repo-connectors/item-controls-info.md @@ -13,6 +13,14 @@ type, and affected versions fields are populated correctly. | ----------- | ---------- | | None | Data class | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios (via ItemControlsTests.cs - 13 tests) ##### ItemControls_Parse_WithVisibilityPublic_ReturnsPublicVisibility diff --git a/docs/verification/build-mark/repo-connectors/item-controls-parser.md b/docs/verification/build-mark/repo-connectors/item-controls-parser.md index bd357090..dd0d9bd2 100644 --- a/docs/verification/build-mark/repo-connectors/item-controls-parser.md +++ b/docs/verification/build-mark/repo-connectors/item-controls-parser.md @@ -14,6 +14,14 @@ unrecognized values are tested for graceful ignorance. | ----------- | ---------- | | None | Pure logic | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### ItemControlsParser_Parse_WithNullDescription_ReturnsNull diff --git a/docs/verification/build-mark/repo-connectors/item-router.md b/docs/verification/build-mark/repo-connectors/item-router.md index bab1d9e2..001aa356 100644 --- a/docs/verification/build-mark/repo-connectors/item-router.md +++ b/docs/verification/build-mark/repo-connectors/item-router.md @@ -15,6 +15,14 @@ and default section fallback. | `SectionConfig` | Provided as test input to define available sections | | `RuleConfig` | Provided as test input to define routing rules | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### ItemRouter_Route_MatchingRuleRoutesItemToConfiguredSection diff --git a/docs/verification/build-mark/repo-connectors/mock.md b/docs/verification/build-mark/repo-connectors/mock.md index f4edccd6..9f4b0eff 100644 --- a/docs/verification/build-mark/repo-connectors/mock.md +++ b/docs/verification/build-mark/repo-connectors/mock.md @@ -1,6 +1,6 @@ ### Mock -#### Verification Approach +#### Verification Strategy The Mock sub-subsystem is verified through `MockTests.cs` (3 subsystem-level tests) and `MockRepoConnectorTests.cs` (11 unit tests). The subsystem tests confirm @@ -13,6 +13,17 @@ described in the individual unit chapter. | ----------- | --------- | | None | Pure mock | +#### Test Environment + +N/A - standard test environment. The Mock sub-subsystem has no external dependencies; +`MockRepoConnector` operates entirely in memory. + +#### Acceptance Criteria + +All 3 subsystem tests in `MockTests.cs` pass with zero failures. All +`BuildMark-RepoConnectors-Mock` requirements have at least one test in the Requirements +Coverage mapping. + #### Test Scenarios (Subsystem-Level, MockTests.cs) ##### Mock_ImplementsInterface_ReturnsTrue diff --git a/docs/verification/build-mark/repo-connectors/mock/mock-repo-connector.md b/docs/verification/build-mark/repo-connectors/mock/mock-repo-connector.md index 6e55edf4..735e8bf6 100644 --- a/docs/verification/build-mark/repo-connectors/mock/mock-repo-connector.md +++ b/docs/verification/build-mark/repo-connectors/mock/mock-repo-connector.md @@ -13,9 +13,17 @@ correctly implements all members of `IRepoConnector`. | ----------- | ---------------------------------- | | None | Self-contained in-memory connector | -##### Test Scenarios +##### Test Environment -###### MockRepoConnector_ImplementsInterface +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + +#### Test Scenarios + +##### MockRepoConnector_ImplementsInterface **Scenario**: `MockRepoConnector` is checked against `IRepoConnector`. diff --git a/docs/verification/build-mark/repo-connectors/repo-connector-base.md b/docs/verification/build-mark/repo-connectors/repo-connector-base.md index 483cd265..24647103 100644 --- a/docs/verification/build-mark/repo-connectors/repo-connector-base.md +++ b/docs/verification/build-mark/repo-connectors/repo-connector-base.md @@ -13,6 +13,14 @@ a version tag in a list, including cross-prefix equality). | ------------------------- | -------------------------------------------------------------- | | Concrete subclass fixture | Tests instantiate a minimal concrete subclass for testing base | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### RepoConnectorBase_Configure_StoresRulesAndSections_HasRulesReturnsTrue diff --git a/docs/verification/build-mark/repo-connectors/repo-connector-factory.md b/docs/verification/build-mark/repo-connectors/repo-connector-factory.md index 7e83264d..78287713 100644 --- a/docs/verification/build-mark/repo-connectors/repo-connector-factory.md +++ b/docs/verification/build-mark/repo-connectors/repo-connector-factory.md @@ -14,6 +14,14 @@ and from remote URL detection. | Environment variables | Tests set/clear `GITHUB_ACTIONS` and `TF_BUILD` env vars | | Git remote URL (process) | Factory may invoke Git to detect the remote URL | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### RepoConnectorFactory_Create_ReturnsConnector diff --git a/docs/verification/build-mark/self-test.md b/docs/verification/build-mark/self-test.md index a8f4a16e..8776f53b 100644 --- a/docs/verification/build-mark/self-test.md +++ b/docs/verification/build-mark/self-test.md @@ -1,6 +1,6 @@ ## SelfTest -### Verification Approach +### Verification Strategy The SelfTest subsystem is verified with dedicated subsystem tests in `SelfTestTests.cs`. Tests invoke `Validation.Run` through a `Context` constructed with controlled argument arrays, then @@ -13,6 +13,19 @@ repository access is needed. | --- | --- | | `MockRepoConnector` | Provides a real connector factory that does not require external network access. | +### Test Environment + +Tests create temporary directories and results files (`.trx`, `.xml`) through +`TemporaryDirectory`, which creates a unique `tmp-*` subdirectory under the current +working directory. Write access to the current working directory is required. No +network access or external API calls are made; `MockRepoConnector` provides all +repository data. + +### Acceptance Criteria + +All tests in `SelfTestTests.cs` pass with zero failures. All `BuildMark-SelfTest-*` +requirements have at least one test in the Requirements Coverage mapping. + ### Test Scenarios #### SelfTest_Validation_WithTrxFile_WritesResults diff --git a/docs/verification/build-mark/self-test/validation.md b/docs/verification/build-mark/self-test/validation.md index 6e53b7a3..0e334e07 100644 --- a/docs/verification/build-mark/self-test/validation.md +++ b/docs/verification/build-mark/self-test/validation.md @@ -13,6 +13,14 @@ factory; no further mocking is required. | ------------------- | --------------------------------------------------------------------- | | `MockRepoConnector` | Supplies connector factory so self-check runs without network access. | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### Validation_Run_WithTrxResultsFile_WritesTrxFile diff --git a/docs/verification/build-mark/utilities.md b/docs/verification/build-mark/utilities.md index 9a576d7f..420ae486 100644 --- a/docs/verification/build-mark/utilities.md +++ b/docs/verification/build-mark/utilities.md @@ -1,12 +1,12 @@ ## Utilities -### Verification Approach +### Verification Strategy -The Utilities subsystem is verified through `RepoConnectorsTests.cs` (for -`ProcessRunner`) and indirectly through `CliTests.cs` (for `PathHelpers` via -path-related flag handling). There is no dedicated `UtilitiesTests.cs` file; -coverage is provided by integration-level tests that exercise the utility classes -as they are used by other units. +The Utilities subsystem is verified through `PathHelpersTests.cs` (7 unit tests for +`PathHelpers`), `TemporaryDirectoryTests.cs` (7 unit tests for `TemporaryDirectory`), +and through `RepoConnectorsTests.cs` (for `ProcessRunner`). There is no +dedicated `UtilitiesTests.cs` subsystem file; unit coverage is provided by the +dedicated class-level test files described above. ### Dependencies @@ -14,6 +14,18 @@ as they are used by other units. | ------------- | --------------------------------------------------------------------- | | None required | `ProcessRunner` tests use real processes; `PathHelpers` is pure logic | +### Test Environment + +`ProcessRunner` tests invoke real OS commands (e.g., `git --version`) and therefore +require a working shell and a `git` executable on the host. Tests run on Windows, +Ubuntu, and macOS in the CI matrix. `PathHelpers` tests have no external dependencies. + +### Acceptance Criteria + +All `ProcessRunner` integration tests in `RepoConnectorsTests.cs` pass with zero +failures on all supported operating systems. All `BuildMark-Utilities-*` requirements +have at least one test in the Requirements Coverage mapping. + ### Test Scenarios (Integration) The following integration tests in `RepoConnectorsTests.cs` exercise `ProcessRunner`: @@ -58,6 +70,72 @@ The following integration tests in `RepoConnectorsTests.cs` exercise `ProcessRun **Requirement coverage**: `BuildMark-Utilities-ProcessRunner` +### Test Scenarios (Unit) + +The following unit tests in `TemporaryDirectoryTests.cs` exercise `TemporaryDirectory`: + +#### TemporaryDirectory_Constructor_CreatesDirectory + +**Scenario**: A `TemporaryDirectory` instance is constructed. + +**Expected**: `DirectoryPath` refers to a directory that exists on disk. + +**Requirement coverage**: `BuildMark-Utilities-TemporaryDirectory` + +#### TemporaryDirectory_Constructor_CreatesUniqueDirectories + +**Scenario**: Two `TemporaryDirectory` instances are constructed in sequence. + +**Expected**: Each instance has a distinct `DirectoryPath`; neither directory collides +with the other. + +**Requirement coverage**: `BuildMark-Utilities-TemporaryDirectory` + +#### TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory + +**Scenario**: `GetFilePath` is called with a simple filename such as `"file.txt"`. + +**Expected**: Returns a path that starts with `DirectoryPath` and ends with the +supplied filename. + +**Requirement coverage**: `BuildMark-Utilities-TemporaryDirectory` + +#### TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories + +**Scenario**: `GetFilePath` is called with a nested relative path such as +`"sub/dir/file.txt"`. + +**Expected**: Returns the expected absolute path and the intermediate directory +`sub/dir` is created on disk. + +**Requirement coverage**: `BuildMark-Utilities-TemporaryDirectory` + +#### TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException + +**Scenario**: `GetFilePath` is called with a traversal path such as `"../outside.txt"`. + +**Expected**: `ArgumentException` is thrown; no file is created outside the temporary +directory. + +**Requirement coverage**: `BuildMark-Utilities-TemporaryDirectory` + +#### TemporaryDirectory_Dispose_DeletesDirectory + +**Scenario**: A `TemporaryDirectory` instance is disposed. + +**Expected**: The directory referred to by `DirectoryPath` no longer exists on disk +after `Dispose` returns. + +**Requirement coverage**: `BuildMark-Utilities-TemporaryDirectory` + +#### TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow + +**Scenario**: The temporary directory is manually deleted before `Dispose` is called. + +**Expected**: `Dispose` completes without throwing any exception. + +**Requirement coverage**: `BuildMark-Utilities-TemporaryDirectory` + ### Requirements Coverage - **BuildMark-Utilities-ProcessRunner**: @@ -66,3 +144,19 @@ The following integration tests in `RepoConnectorsTests.cs` exercise `ProcessRun RepoConnectors_ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull, RepoConnectors_ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput, RepoConnectors_ProcessRunner_RunAsync_WithFailingCommand_ThrowsException +- **BuildMark-Utilities-PathHelpers**: + PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly, + PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException, + PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullException, + PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException, + PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentException, + PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException, + PathHelpers_SafePathCombine_PathStartingWithDots_CombinesCorrectly +- **BuildMark-Utilities-TemporaryDirectory**: + TemporaryDirectory_Constructor_CreatesDirectory, + TemporaryDirectory_Constructor_CreatesUniqueDirectories, + TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory, + TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories, + TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException, + TemporaryDirectory_Dispose_DeletesDirectory, + TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow diff --git a/docs/verification/build-mark/utilities/path-helpers.md b/docs/verification/build-mark/utilities/path-helpers.md index 60b2ece4..3310d1a7 100644 --- a/docs/verification/build-mark/utilities/path-helpers.md +++ b/docs/verification/build-mark/utilities/path-helpers.md @@ -2,14 +2,10 @@ #### Verification Approach -`PathHelpers` is a pure utility class with no dedicated test class. It is verified -indirectly through CLI and program tests that exercise path-related flag handling -(`--log`, `--report`, `--results`) and through the overall build pipeline where -paths are resolved during document generation. - -No direct unit tests exist for `PathHelpers` because the class provides straightforward -path combination logic with no branching that requires isolated testing. Its behavior -is validated through the integration tests that consume it. +`PathHelpers` is a pure utility class. It is verified through dedicated unit tests in +`PathHelpersTests.cs`, which contains 7 tests covering valid path combination, null +argument rejection, path traversal prevention (double-dots), absolute path rejection, +and acceptance of valid dot-prefixed directory names such as `"..data"`. #### Dependencies @@ -17,27 +13,87 @@ is validated through the integration tests that consume it. | ----------- | ---------- | | None | Pure logic | -#### Test Scenarios (Integration) +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + +#### Test Scenarios + +##### PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly + +**Scenario**: `PathHelpers.SafePathCombine` is called with a valid base path and a +relative path. + +**Expected**: Returns `Path.Combine(basePath, relativePath)`. + +**Requirement coverage**: `BuildMark-Utilities-PathHelpers` + +##### PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException + +**Scenario**: `PathHelpers.SafePathCombine` is called with a `null` base path. + +**Expected**: `ArgumentNullException` is thrown with `ParamName` equal to `"basePath"`. + +**Requirement coverage**: `BuildMark-Utilities-PathHelpers` + +##### PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullException + +**Scenario**: `PathHelpers.SafePathCombine` is called with a `null` relative path. + +**Expected**: `ArgumentNullException` is thrown with `ParamName` equal to +`"relativePath"`. + +**Requirement coverage**: `BuildMark-Utilities-PathHelpers` + +##### PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException + +**Scenario**: `PathHelpers.SafePathCombine` is called with `"../etc/passwd"` as the +relative path. + +**Expected**: `ArgumentException` is thrown with a message containing +`"Invalid path component"`. + +**Requirement coverage**: `BuildMark-Utilities-PathHelpers` + +##### PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentException + +**Scenario**: `PathHelpers.SafePathCombine` is called with `"subfolder/../../../etc/passwd"`. + +**Expected**: `ArgumentException` is thrown with a message containing +`"Invalid path component"`. + +**Requirement coverage**: `BuildMark-Utilities-PathHelpers` -##### Cli_LogFlag_CreatesLogFile +##### PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException -**Scenario**: `Context` is created with a log file path; `PathHelpers` is used to -resolve the file path. +**Scenario**: `PathHelpers.SafePathCombine` is called with an absolute path as the +relative argument. -**Expected**: Log file is created at the resolved path. +**Expected**: `ArgumentException` is thrown with a message containing +`"Invalid path component"`. **Requirement coverage**: `BuildMark-Utilities-PathHelpers` -##### Cli_ReportFlags_SetProperties +##### PathHelpers_SafePathCombine_PathStartingWithDots_CombinesCorrectly -**Scenario**: `Context` is created with `--report` flag; path is processed by the -context/utilities layer. +**Scenario**: `PathHelpers.SafePathCombine` is called with `"..data/file.txt"` (a valid +directory name that begins with dots but is not a traversal component). -**Expected**: `ReportFile` property contains the resolved path. +**Expected**: Returns `Path.Combine(basePath, "..data/file.txt")` without error. **Requirement coverage**: `BuildMark-Utilities-PathHelpers` #### Requirements Coverage -- **BuildMark-Utilities-PathHelpers**: Cli_LogFlag_CreatesLogFile, - Cli_ReportFlags_SetProperties +- **BuildMark-Utilities-PathHelpers**: + PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly, + PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException, + PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullException, + PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException, + PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentException, + PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException, + PathHelpers_SafePathCombine_PathStartingWithDots_CombinesCorrectly diff --git a/docs/verification/build-mark/utilities/process-runner.md b/docs/verification/build-mark/utilities/process-runner.md index 0cd62507..5c0ae798 100644 --- a/docs/verification/build-mark/utilities/process-runner.md +++ b/docs/verification/build-mark/utilities/process-runner.md @@ -13,6 +13,15 @@ correct on the target operating system. | ----------- | ----------------------------------------------------------- | | None | Real OS processes are used to test actual process execution | +#### Test Environment + +Tests invoke real OS processes. A working shell and `git` executable must be present on +the host. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### RepoConnectors_ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput diff --git a/docs/verification/build-mark/utilities/temporary-directory.md b/docs/verification/build-mark/utilities/temporary-directory.md new file mode 100644 index 00000000..f39c7396 --- /dev/null +++ b/docs/verification/build-mark/utilities/temporary-directory.md @@ -0,0 +1,102 @@ +### TemporaryDirectory + +#### Verification Approach + +`TemporaryDirectory` is verified through dedicated unit tests in +`TemporaryDirectoryTests.cs`, which contains 7 tests covering directory creation, +uniqueness, path resolution, intermediate-directory creation, traversal prevention, +disposal cleanup, and idempotent disposal when the directory has already been deleted. + +#### Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------------------------------------------------------- | +| None | Tests use the real file system under a temporary directory | + +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. +Tests create and delete real directories on the host file system. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + +#### Test Scenarios + +##### TemporaryDirectory_Constructor_CreatesDirectory + +**Scenario**: A `TemporaryDirectory` instance is constructed. + +**Expected**: `DirectoryPath` refers to a directory that exists on disk. + +**Requirement coverage**: `BuildMark-TemporaryDirectory-Creation` + +##### TemporaryDirectory_Constructor_CreatesUniqueDirectories + +**Scenario**: Two `TemporaryDirectory` instances are constructed in sequence. + +**Expected**: Each instance has a distinct `DirectoryPath`; neither directory collides +with the other. + +**Requirement coverage**: `BuildMark-TemporaryDirectory-Creation` + +##### TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory + +**Scenario**: `GetFilePath` is called with a simple filename such as `"file.txt"`. + +**Expected**: Returns a path that starts with `DirectoryPath` and ends with the +supplied filename. + +**Requirement coverage**: `BuildMark-TemporaryDirectory-FilePath` + +##### TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories + +**Scenario**: `GetFilePath` is called with a nested relative path such as +`"sub/dir/file.txt"`. + +**Expected**: Returns the expected absolute path and the intermediate directory +`sub/dir` is created on disk. + +**Requirement coverage**: `BuildMark-TemporaryDirectory-FilePath` + +##### TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException + +**Scenario**: `GetFilePath` is called with a traversal path such as +`"../outside.txt"`. + +**Expected**: `ArgumentException` is thrown; no file is created outside the +temporary directory. + +**Requirement coverage**: `BuildMark-TemporaryDirectory-Traversal` + +##### TemporaryDirectory_Dispose_DeletesDirectory + +**Scenario**: A `TemporaryDirectory` instance is disposed. + +**Expected**: The directory referred to by `DirectoryPath` no longer exists on disk +after `Dispose` returns. + +**Requirement coverage**: `BuildMark-TemporaryDirectory-Cleanup` + +##### TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow + +**Scenario**: The temporary directory is manually deleted before `Dispose` is called. + +**Expected**: `Dispose` completes without throwing any exception. + +**Requirement coverage**: `BuildMark-TemporaryDirectory-Cleanup` + +#### Requirements Coverage + +- **BuildMark-TemporaryDirectory-Creation**: + TemporaryDirectory_Constructor_CreatesDirectory, + TemporaryDirectory_Constructor_CreatesUniqueDirectories +- **BuildMark-TemporaryDirectory-FilePath**: + TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory, + TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories +- **BuildMark-TemporaryDirectory-Traversal**: + TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException +- **BuildMark-TemporaryDirectory-Cleanup**: + TemporaryDirectory_Dispose_DeletesDirectory, + TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow diff --git a/docs/verification/build-mark/version.md b/docs/verification/build-mark/version.md index da6422a0..c011e33b 100644 --- a/docs/verification/build-mark/version.md +++ b/docs/verification/build-mark/version.md @@ -1,6 +1,6 @@ ## Version -### Verification Approach +### Verification Strategy The Version subsystem is verified through `VersionTests.cs` (subsystem integration tests), plus dedicated unit test files for each version class. The subsystem tests @@ -14,6 +14,16 @@ are described in the individual unit chapters. | ----------- | ---------- | | None | Pure logic | +### Test Environment + +N/A - standard test environment. `VersionTests.cs` contains pure logic tests with no +external dependencies, network access, or file system requirements. + +### Acceptance Criteria + +All tests in `VersionTests.cs` pass with zero failures. All `BuildMark-Version-*` +requirements have at least one test in the Requirements Coverage mapping. + ### Test Scenarios #### VersionComparable_Create_ValidVersions_ReturnsVersionComparable @@ -48,6 +58,14 @@ are described in the individual unit chapters. **Requirement coverage**: `BuildMark-Version-VersionInterval` +#### VersionIntervalSet_Parse_SingleInterval_ReturnsOneInterval + +**Scenario**: `VersionIntervalSet.Parse` is called with a single interval string. + +**Expected**: Returns a set containing exactly one `VersionInterval`. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + #### VersionCommitTag_Constructor_ValidParameters_CreatesInstance **Scenario**: `VersionCommitTag` is constructed with a valid tag and commit hash. @@ -94,4 +112,5 @@ are described in the individual unit chapters. Version_Subsystem_CreateAllVersionTypes_WorksCorrectly, Version_Subsystem_TagToComparableIntegration_WorksCorrectly - **BuildMark-Version-VersionInterval**: VersionInterval_Create_ValidInterval_ReturnsVersionInterval +- **BuildMark-Version-VersionIntervalSet**: VersionIntervalSet_Parse_SingleInterval_ReturnsOneInterval - **BuildMark-Version-VersionCommitTag**: VersionCommitTag_Constructor_ValidParameters_CreatesInstance diff --git a/docs/verification/build-mark/version/version-commit-tag.md b/docs/verification/build-mark/version/version-commit-tag.md index 5374f8d5..98874417 100644 --- a/docs/verification/build-mark/version/version-commit-tag.md +++ b/docs/verification/build-mark/version/version-commit-tag.md @@ -13,6 +13,14 @@ instance and asserts that the tag and commit-hash properties are stored correctl | ----------- | ---------- | | None | Data class | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### VersionCommitTag_Constructor_ValidParameters_CreatesInstance diff --git a/docs/verification/build-mark/version/version-comparable.md b/docs/verification/build-mark/version/version-comparable.md index ae3e9088..a4e41224 100644 --- a/docs/verification/build-mark/version/version-comparable.md +++ b/docs/verification/build-mark/version/version-comparable.md @@ -13,6 +13,14 @@ Semantic Versioning specification. | ----------- | ---------- | | None | Pure logic | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### VersionComparable_Create_ValidVersion_ReturnsInstance diff --git a/docs/verification/build-mark/version/version-interval-set.md b/docs/verification/build-mark/version/version-interval-set.md index 5c7aa144..047050bd 100644 --- a/docs/verification/build-mark/version/version-interval-set.md +++ b/docs/verification/build-mark/version/version-interval-set.md @@ -14,6 +14,14 @@ internal commas in interval strings, empty input, discarding invalid tokens, and | ----------- | ---------- | | None | Pure logic | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### VersionIntervalSet_Parse_SingleInterval_ReturnsOneInterval diff --git a/docs/verification/build-mark/version/version-interval.md b/docs/verification/build-mark/version/version-interval.md index 39874a09..c161d0ad 100644 --- a/docs/verification/build-mark/version/version-interval.md +++ b/docs/verification/build-mark/version/version-interval.md @@ -13,6 +13,14 @@ versions, and pre-release versions. | ----------- | ---------- | | None | Pure logic | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### VersionInterval_Parse_InclusiveLower_IsInclusive diff --git a/docs/verification/build-mark/version/version-semantic.md b/docs/verification/build-mark/version/version-semantic.md index fa8e4b72..b4d3ced4 100644 --- a/docs/verification/build-mark/version/version-semantic.md +++ b/docs/verification/build-mark/version/version-semantic.md @@ -12,6 +12,14 @@ to the underlying `VersionComparable`, string formatting, and comparison. | ----------- | ---------- | | None | Pure logic | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### VersionSemantic_Create_WithBuildMetadata_ReturnsInstance diff --git a/docs/verification/build-mark/version/version-tag.md b/docs/verification/build-mark/version/version-tag.md index 589d0c58..f6b0ab14 100644 --- a/docs/verification/build-mark/version/version-tag.md +++ b/docs/verification/build-mark/version/version-tag.md @@ -13,6 +13,14 @@ and error handling for invalid tags. | ----------- | ---------- | | None | Pure logic | +#### Test Environment + +Standard dotnet test host; no external dependencies or environment setup required. + +#### Acceptance Criteria + +All tests in the test class pass with no errors or warnings. + #### Test Scenarios ##### VersionTag_Create_ValidTag_ReturnsVersionTag diff --git a/docs/verification/introduction.md b/docs/verification/introduction.md index ee693539..72c04e25 100644 --- a/docs/verification/introduction.md +++ b/docs/verification/introduction.md @@ -137,6 +137,6 @@ match identifiers defined in the ReqStream YAML files under `docs/reqstream/`. ## References -- See the *BuildMark Software Design* document for implementation details of each unit. -- See the *BuildMark Requirements* document for the full requirements specification. +- [BuildMark releases](https://github.com/demaconsulting/BuildMark/releases) — + compiled design, requirements, and compliance documents - See the BuildMark repository at . diff --git a/docs/verification/ots/buildmark.md b/docs/verification/ots/buildmark.md index 4cbf090e..172829e5 100644 --- a/docs/verification/ots/buildmark.md +++ b/docs/verification/ots/buildmark.md @@ -18,6 +18,12 @@ dotnet buildmark --validate --results artifacts/buildmark-self-validation.trx The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. +### Test Scenarios + +- *BuildMark self-validation*: CI pipeline executes + `dotnet buildmark --validate --results artifacts/buildmark-self-validation.trx`; + expects exit code 0 and a non-empty TRX file containing self-test results. + ### Requirements Coverage - **BuildMark-OTS-BuildMark**: CI pipeline self-validation TRX evidence from diff --git a/docs/verification/ots/fileassert.md b/docs/verification/ots/fileassert.md index 4f13bb2f..e1271eab 100644 --- a/docs/verification/ots/fileassert.md +++ b/docs/verification/ots/fileassert.md @@ -23,6 +23,16 @@ dotnet fileassert --validate --results artifacts/fileassert-self-validation.trx The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. +### Test Scenarios + +- *FileAssert self-validation*: CI pipeline executes + `dotnet fileassert --validate --results artifacts/fileassert-self-validation.trx`; + expects exit code 0 and a non-empty TRX file containing self-test results. +- *FileAssert document assertions*: FileAssert is invoked for each generated document + collection (Build Notes, Code Quality, Review Plan, Review Report, Design, User Guide, + Verification); expects that each assertion set passes and a non-empty TRX file is + produced per collection. + ### Requirements Coverage - **BuildMark-OTS-FileAssert**: CI pipeline self-validation TRX evidence from diff --git a/docs/verification/ots/pandoc.md b/docs/verification/ots/pandoc.md index 918810af..e4969753 100644 --- a/docs/verification/ots/pandoc.md +++ b/docs/verification/ots/pandoc.md @@ -23,6 +23,13 @@ FileAssert TRX files (`fileassert-build-notes.trx`, `fileassert-code-quality.trx `fileassert-code-review.trx`, `fileassert-design.trx`, `fileassert-user-guide.trx`, `fileassert-verification.trx`) are consumed by ReqStream to satisfy the OTS requirement. +### Test Scenarios + +- *Pandoc document conversion*: CI pipeline invokes Pandoc for each document collection + (Build Notes, Code Quality, Review Plan, Review Report, Design, User Guide, Verification); + FileAssert validates the generated HTML output; expects exit code 0 for each invocation + and non-empty HTML files containing expected content markers. + ### Requirements Coverage - **BuildMark-OTS-Pandoc**: CI pipeline document generation evidence from multiple diff --git a/docs/verification/ots/reqstream.md b/docs/verification/ots/reqstream.md index 9ea38541..6b5ac961 100644 --- a/docs/verification/ots/reqstream.md +++ b/docs/verification/ots/reqstream.md @@ -23,6 +23,15 @@ dotnet reqstream --validate --results artifacts/reqstream-self-validation.trx The resulting TRX file is consumed by ReqStream itself to satisfy the OTS requirement. +### Test Scenarios + +- *ReqStream self-validation*: CI pipeline executes + `dotnet reqstream --validate --results artifacts/reqstream-self-validation.trx`; + expects exit code 0 and a non-empty TRX file containing self-test results. +- *ReqStream requirements enforcement*: CI pipeline executes ReqStream in enforcement + mode (`--enforce`) against the project requirements YAML files and evidence TRX files; + expects exit code 0 confirming all requirements are fully satisfied. + ### Requirements Coverage - **BuildMark-OTS-ReqStream**: CI pipeline self-validation TRX evidence from diff --git a/docs/verification/ots/reviewmark.md b/docs/verification/ots/reviewmark.md index e0565968..34e691b0 100644 --- a/docs/verification/ots/reviewmark.md +++ b/docs/verification/ots/reviewmark.md @@ -22,6 +22,15 @@ dotnet reviewmark --validate --results artifacts/reviewmark-self-validation.trx The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. +### Test Scenarios + +- *ReviewMark self-validation*: CI pipeline executes + `dotnet reviewmark --validate --results artifacts/reviewmark-self-validation.trx`; + expects exit code 0 and a non-empty TRX file containing self-test results. +- *ReviewMark document generation*: CI pipeline uses ReviewMark to generate the Review + Plan and Review Report documents from the `.reviewmark.yaml` configuration; expects + exit code 0 and non-empty output documents for both the plan and report. + ### Requirements Coverage - **BuildMark-OTS-ReviewMark**: CI pipeline self-validation TRX evidence from diff --git a/docs/verification/ots/sarifmark.md b/docs/verification/ots/sarifmark.md index 064b2b71..bd392b0d 100644 --- a/docs/verification/ots/sarifmark.md +++ b/docs/verification/ots/sarifmark.md @@ -22,6 +22,15 @@ dotnet sarifmark --validate --results artifacts/sarifmark-self-validation.trx The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. +### Test Scenarios + +- *SarifMark self-validation*: CI pipeline executes + `dotnet sarifmark --validate --results artifacts/sarifmark-self-validation.trx`; + expects exit code 0 and a non-empty TRX file containing self-test results. +- *SarifMark SARIF report generation*: CI pipeline uses SarifMark to process the CodeQL + SARIF output and generate the code quality markdown report; expects exit code 0 and a + non-empty markdown report. + ### Requirements Coverage - **BuildMark-OTS-SarifMark**: CI pipeline self-validation TRX evidence from diff --git a/docs/verification/ots/sonarmark.md b/docs/verification/ots/sonarmark.md index e6f02bbe..062341bb 100644 --- a/docs/verification/ots/sonarmark.md +++ b/docs/verification/ots/sonarmark.md @@ -22,6 +22,15 @@ dotnet sonarmark --validate --results artifacts/sonarmark-self-validation.trx The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. +### Test Scenarios + +- *SonarMark self-validation*: CI pipeline executes + `dotnet sonarmark --validate --results artifacts/sonarmark-self-validation.trx`; + expects exit code 0 and a non-empty TRX file containing self-test results. +- *SonarMark quality report generation*: CI pipeline uses SonarMark to query the + SonarCloud API and generate the SonarCloud quality report markdown; expects exit + code 0 and a non-empty markdown report. + ### Requirements Coverage - **BuildMark-OTS-SonarMark**: CI pipeline self-validation TRX evidence from diff --git a/docs/verification/ots/versionmark.md b/docs/verification/ots/versionmark.md index 41de0ffa..11b94440 100644 --- a/docs/verification/ots/versionmark.md +++ b/docs/verification/ots/versionmark.md @@ -23,6 +23,18 @@ dotnet versionmark --validate --results artifacts/versionmark-self-validation.tr The resulting TRX files are consumed by ReqStream to satisfy the OTS requirement. +### Test Scenarios + +- *VersionMark self-validation (quality job)*: CI pipeline executes + `dotnet versionmark --validate --results artifacts/versionmark-self-validation-quality.trx`; + expects exit code 0 and a non-empty TRX file. +- *VersionMark self-validation (main job)*: CI pipeline executes + `dotnet versionmark --validate --results artifacts/versionmark-self-validation.trx`; + expects exit code 0 and a non-empty TRX file. +- *VersionMark operational use*: VersionMark is invoked in every CI job to capture and + publish tool version information; expects exit code 0 and correct version output for + each job. + ### Requirements Coverage - **BuildMark-OTS-VersionMark**: CI pipeline self-validation TRX evidence from diff --git a/docs/verification/ots/weasyprint.md b/docs/verification/ots/weasyprint.md index e2dd86bd..7ed865e9 100644 --- a/docs/verification/ots/weasyprint.md +++ b/docs/verification/ots/weasyprint.md @@ -24,6 +24,13 @@ FileAssert TRX files (`fileassert-build-notes.trx`, `fileassert-code-quality.trx `fileassert-code-review.trx`, `fileassert-design.trx`, `fileassert-user-guide.trx`, `fileassert-verification.trx`) are consumed by ReqStream to satisfy the OTS requirement. +### Test Scenarios + +- *WeasyPrint PDF rendering*: CI pipeline invokes WeasyPrint for each document collection + (Build Notes, Code Quality, Review Plan, Review Report, Design, User Guide, Verification); + FileAssert validates the generated PDF output; expects exit code 0 for each invocation + and non-empty PDF files confirmed by FileAssert to contain expected content and metadata. + ### Requirements Coverage - **BuildMark-OTS-WeasyPrint**: CI pipeline document generation evidence from multiple diff --git a/docs/verification/ots/xunit.md b/docs/verification/ots/xunit.md index 41633ec9..d67150cd 100644 --- a/docs/verification/ots/xunit.md +++ b/docs/verification/ots/xunit.md @@ -23,6 +23,12 @@ The resulting TRX files are consumed by ReqStream to satisfy unit test requireme The matrix of operating systems (Windows, Ubuntu, macOS) and .NET versions (8, 9, 10) provides broad platform coverage evidence. +### Test Scenarios + +- *xUnit test discovery and execution*: CI pipeline executes `dotnet test` across the + full OS and .NET version matrix (Windows, Ubuntu, macOS × .NET 8, 9, 10); expects zero + test failures and a non-empty TRX result file for each platform/version combination. + ### Requirements Coverage - **BuildMark-OTS-xUnit**: CI pipeline test execution TRX evidence confirming that diff --git a/requirements.yaml b/requirements.yaml index 4c62363d..c5553052 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -14,6 +14,7 @@ includes: - docs/reqstream/build-mark/utilities.yaml - docs/reqstream/build-mark/utilities/path-helpers.yaml - docs/reqstream/build-mark/utilities/process-runner.yaml + - docs/reqstream/build-mark/utilities/temporary-directory.yaml - docs/reqstream/build-mark/version.yaml - docs/reqstream/build-mark/repo-connectors.yaml - docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/AzureDevOps/WorkItemMapper.cs b/src/DemaConsulting.BuildMark/RepoConnectors/AzureDevOps/WorkItemMapper.cs index 4ced68e7..1a207b26 100644 --- a/src/DemaConsulting.BuildMark/RepoConnectors/AzureDevOps/WorkItemMapper.cs +++ b/src/DemaConsulting.BuildMark/RepoConnectors/AzureDevOps/WorkItemMapper.cs @@ -92,8 +92,7 @@ internal static class WorkItemMapper var controls = ExtractItemControls(workItem); // Exclude item if visibility is "internal" - var forceInclude = controls?.Visibility == "public"; - if (!forceInclude && controls?.Visibility == "internal") + if (controls?.Visibility == "internal") { return null; } diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs index d9b69280..1da2a8f4 100644 --- a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs +++ b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs @@ -175,7 +175,8 @@ ... on Commit { } catch { - // If GraphQL query fails, return empty list + // Intentional silent-fail: errors return an empty list rather than propagating + // to callers. If logging infrastructure is added in future, log the exception here. return []; } } @@ -246,7 +247,8 @@ public async Task> GetReleasesAsync( } catch { - // If GraphQL query fails, return empty list + // Intentional silent-fail: errors return an empty list rather than propagating + // to callers. If logging infrastructure is added in future, log the exception here. return []; } } @@ -323,7 +325,8 @@ public async Task> FindIssueIdsLinkedToPullRequestAsync( } catch { - // If GraphQL query fails, return empty list + // Intentional silent-fail: errors return an empty list rather than propagating + // to callers. If logging infrastructure is added in future, log the exception here. return []; } } @@ -397,7 +400,8 @@ public async Task> GetAllTagsAsync( } catch { - // If GraphQL query fails, return empty list + // Intentional silent-fail: errors return an empty list rather than propagating + // to callers. If logging infrastructure is added in future, log the exception here. return []; } } @@ -481,7 +485,8 @@ public async Task> GetPullRequestsAsync( } catch { - // If GraphQL query fails, return empty list + // Intentional silent-fail: errors return an empty list rather than propagating + // to callers. If logging infrastructure is added in future, log the exception here. return []; } } @@ -561,7 +566,8 @@ public async Task> GetAllIssuesAsync( } catch { - // If GraphQL query fails, return empty list + // Intentional silent-fail: errors return an empty list rather than propagating + // to callers. If logging infrastructure is added in future, log the exception here. return []; } } diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubRepoConnector.cs b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubRepoConnector.cs index 6ab8bf36..62c9699a 100644 --- a/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubRepoConnector.cs +++ b/src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubRepoConnector.cs @@ -121,10 +121,20 @@ public override async Task GetBuildInformationAsync(VersionTag var branch = await RunCommandAsync("git", "rev-parse", "--abbrev-ref", "HEAD"); var currentCommitHash = await RunCommandAsync("git", "rev-parse", "HEAD"); - // Parse owner and repo from URL - var (parsedOwner, parsedRepo) = ParseGitHubUrl(repoUrl); - var owner = _config?.Owner ?? parsedOwner; - var repo = _config?.Repo ?? parsedRepo; + // Parse owner and repo from URL, using non-empty config overrides when available + string owner, repo; + if (!string.IsNullOrWhiteSpace(_config?.Owner) && !string.IsNullOrWhiteSpace(_config?.Repo)) + { + owner = _config.Owner; + repo = _config.Repo; + } + else + { + // Fall back to parsing owner and repo from the remote URL + var (parsedOwner, parsedRepo) = ParseGitHubUrl(repoUrl); + owner = string.IsNullOrWhiteSpace(_config?.Owner) ? parsedOwner : _config.Owner; + repo = string.IsNullOrWhiteSpace(_config?.Repo) ? parsedRepo : _config.Repo; + } // Get GitHub token var token = await GetGitHubTokenAsync(); @@ -937,16 +947,7 @@ private static string ApplyTypeOverride(string type, ItemControlsInfo? controls) /// Item type string. private static string GetTypeFromLabels(IReadOnlyList labels) { - // Find first matching label type by checking label names against the type map - var matchingType = labels - .Select(label => label.Name.ToLowerInvariant()) - .SelectMany(lowerLabel => LabelTypeMap - .Where(kvp => lowerLabel == kvp.Key) - .Select(kvp => kvp.Value)) - .FirstOrDefault(); - - // Return matched type or default to "other" - return matchingType ?? "other"; + return GetTypeFromLabelNames(labels.Select(l => l.Name)); } /// @@ -955,10 +956,20 @@ private static string GetTypeFromLabels(IReadOnlyList labels) /// List of pull request labels. /// Item type string. private static string GetTypeFromLabels(IReadOnlyList labels) + { + return GetTypeFromLabelNames(labels.Select(l => l.Name)); + } + + /// + /// Determines item type from a sequence of label names. + /// + /// Label name strings. + /// Item type string, defaulting to "other" when no match is found. + private static string GetTypeFromLabelNames(IEnumerable names) { // Find first matching label type by checking label names against the type map - var matchingType = labels - .Select(label => label.Name.ToLowerInvariant()) + var matchingType = names + .Select(name => name.ToLowerInvariant()) .SelectMany(lowerLabel => LabelTypeMap .Where(kvp => lowerLabel == kvp.Key) .Select(kvp => kvp.Value)) diff --git a/src/DemaConsulting.BuildMark/RepoConnectors/RepoConnectorBase.cs b/src/DemaConsulting.BuildMark/RepoConnectors/RepoConnectorBase.cs index df00eec2..9b5da121 100644 --- a/src/DemaConsulting.BuildMark/RepoConnectors/RepoConnectorBase.cs +++ b/src/DemaConsulting.BuildMark/RepoConnectors/RepoConnectorBase.cs @@ -43,7 +43,7 @@ public abstract class RepoConnectorBase : IRepoConnector /// /// Gets a value indicating whether routing rules have been configured. /// - protected bool HasRules => _rules.Count > 0; + internal bool HasRules => _rules.Count > 0; /// /// Configures the routing rules and section definitions for this connector. diff --git a/src/DemaConsulting.BuildMark/SelfTest/Validation.cs b/src/DemaConsulting.BuildMark/SelfTest/Validation.cs index bd74ccf5..cb099196 100644 --- a/src/DemaConsulting.BuildMark/SelfTest/Validation.cs +++ b/src/DemaConsulting.BuildMark/SelfTest/Validation.cs @@ -307,8 +307,8 @@ private static void RunValidationTest( try { using var tempDir = new TemporaryDirectory(); - var logFile = PathHelpers.SafePathCombine(tempDir.DirectoryPath, $"{testName}.log"); - var reportFile = reportFileName != null ? PathHelpers.SafePathCombine(tempDir.DirectoryPath, reportFileName) : null; + var logFile = tempDir.GetFilePath($"{testName}.log"); + var reportFile = reportFileName != null ? tempDir.GetFilePath(reportFileName) : null; // Build command line arguments List args = @@ -467,50 +467,4 @@ private static void HandleTestException( test.ErrorMessage = $"Exception: {ex.Message}"; context.WriteError($"✗ {displayName} - Failed: {ex.Message}"); } - - /// - /// Represents a temporary directory that is automatically deleted when disposed. - /// - private sealed class TemporaryDirectory : IDisposable - { - /// - /// Gets the path to the temporary directory. - /// - public string DirectoryPath { get; } - - /// - /// Initializes a new instance of the class. - /// - public TemporaryDirectory() - { - DirectoryPath = PathHelpers.SafePathCombine(Path.GetTempPath(), $"buildmark_validation_{Guid.NewGuid()}"); - - try - { - Directory.CreateDirectory(DirectoryPath); - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException) - { - throw new InvalidOperationException($"Failed to create temporary directory: {ex.Message}", ex); - } - } - - /// - /// Deletes the temporary directory and all its contents. - /// - public void Dispose() - { - try - { - if (Directory.Exists(DirectoryPath)) - { - Directory.Delete(DirectoryPath, true); - } - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) - { - // Ignore cleanup errors during disposal - } - } - } } diff --git a/src/DemaConsulting.BuildMark/Utilities/TemporaryDirectory.cs b/src/DemaConsulting.BuildMark/Utilities/TemporaryDirectory.cs new file mode 100644 index 00000000..387176aa --- /dev/null +++ b/src/DemaConsulting.BuildMark/Utilities/TemporaryDirectory.cs @@ -0,0 +1,119 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DemaConsulting.BuildMark.Utilities; + +/// +/// A disposable temporary directory that is automatically deleted when disposed. +/// +/// +/// The temporary directory is created under +/// rather than . This avoids OS symlink issues such as +/// /tmp resolving to /private/tmp on macOS, which can cause +/// path-comparison failures when the OS returns the real (resolved) path instead +/// of the symlink path used to construct it. +/// +internal sealed class TemporaryDirectory : IDisposable +{ + /// + /// Gets the full path to the temporary directory. + /// + public string DirectoryPath { get; } + + /// + /// Initializes a new instance of the class, + /// creating a uniquely-named subdirectory under . + /// + /// + /// Thrown when the temporary directory cannot be created due to an + /// , , or + /// from the underlying file-system call. + /// + public TemporaryDirectory() + { + var effectiveBase = Environment.CurrentDirectory; + DirectoryPath = PathHelpers.SafePathCombine(effectiveBase, $"tmp-{Guid.NewGuid():N}"); + + // Create the directory and surface any failure as InvalidOperationException so + // callers receive a consistent, descriptive error without having to handle + // low-level I/O exceptions from this constructor. + try + { + Directory.CreateDirectory(DirectoryPath); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException) + { + throw new InvalidOperationException($"Failed to create temporary directory: {ex.Message}", ex); + } + } + + /// + /// Returns the full path to a file within the temporary directory, + /// creating any required intermediate subdirectories. + /// + /// + /// A relative path (file name or subdirectory/file) within the temporary directory. + /// Must not be . + /// + /// The combined full path within the temporary directory. + /// + /// Thrown when is . + /// + /// + /// Thrown when would escape the temporary directory. + /// + public string GetFilePath(string relativePath) + { + // Validate and combine the relative path within the temporary directory boundary + var path = PathHelpers.SafePathCombine(DirectoryPath, relativePath); + + // Ensure any intermediate subdirectories exist before the caller tries to write + var directory = Path.GetDirectoryName(path); + if (directory != null) + { + Directory.CreateDirectory(directory); + } + + return path; + } + + /// + /// Deletes the temporary directory and all its contents. + /// + /// + /// , , and + /// are intentionally suppressed during + /// disposal. Cleanup failures are non-fatal: the operating system or the user's + /// temp-folder maintenance process will eventually reclaim the directory, and + /// allowing an exception to escape from Dispose would break + /// using blocks and mask the original outcome. + /// + public void Dispose() + { + try + { + Directory.Delete(DirectoryPath, recursive: true); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or DirectoryNotFoundException) + { + // Ignore cleanup errors during disposal + } + } +} diff --git a/test/DemaConsulting.BuildMark.Tests/AssemblyInfo.cs b/test/DemaConsulting.BuildMark.Tests/AssemblyInfo.cs index 15dd94b1..aca89f16 100644 --- a/test/DemaConsulting.BuildMark.Tests/AssemblyInfo.cs +++ b/test/DemaConsulting.BuildMark.Tests/AssemblyInfo.cs @@ -20,6 +20,7 @@ using Xunit; +// Disabled globally because ProgramTests redirect shared Console streams; must run serially. [assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/test/DemaConsulting.BuildMark.Tests/Cli/CliTests.cs b/test/DemaConsulting.BuildMark.Tests/Cli/CliTests.cs index 52e0453b..25d9bbb1 100644 --- a/test/DemaConsulting.BuildMark.Tests/Cli/CliTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Cli/CliTests.cs @@ -19,6 +19,7 @@ // SOFTWARE. using DemaConsulting.BuildMark.Cli; +using DemaConsulting.BuildMark.Utilities; namespace DemaConsulting.BuildMark.Tests.Cli; @@ -151,29 +152,19 @@ public void Cli_ReportFlags_SetProperties() public void Cli_LogFlag_CreatesLogFile() { // Arrange: create a temporary log file path - var logFile = Path.GetTempFileName(); + using var tempDir = new TemporaryDirectory(); + var logFile = tempDir.GetFilePath("output.log"); - try + // Act: create context with --log argument and write a message + using (var context = Context.Create(["--log", logFile])) { - // Act: create context with --log argument and write a message - using (var context = Context.Create(["--log", logFile])) - { - context.WriteLine("Subsystem log test"); - } - - // Assert: log file exists and contains the written message - Assert.True(File.Exists(logFile)); - var logContent = File.ReadAllText(logFile); - Assert.Contains("Subsystem log test", logContent); - } - finally - { - // Clean up log file - if (File.Exists(logFile)) - { - File.Delete(logFile); - } + context.WriteLine("Subsystem log test"); } + + // Assert: log file exists and contains the written message + Assert.True(File.Exists(logFile)); + var logContent = File.ReadAllText(logFile); + Assert.Contains("Subsystem log test", logContent); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/Cli/ContextTests.cs b/test/DemaConsulting.BuildMark.Tests/Cli/ContextTests.cs index 9a060d0e..df80b817 100644 --- a/test/DemaConsulting.BuildMark.Tests/Cli/ContextTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Cli/ContextTests.cs @@ -19,6 +19,7 @@ // SOFTWARE. using DemaConsulting.BuildMark.Cli; +using DemaConsulting.BuildMark.Utilities; namespace DemaConsulting.BuildMark.Tests.Cli; @@ -251,24 +252,15 @@ public void Context_Create_ResultArgument_SetsResultsFileProperty() [Fact] public void Context_Create_LogArgument_CreatesLogFile() { - // Create temporary log file path - var logFile = Path.GetTempFileName(); - try - { - // Create context with --log argument - using var context = Context.Create(["--log", logFile]); + // Arrange: create a temporary log file path + using var tempDir = new TemporaryDirectory(); + var logFile = tempDir.GetFilePath("output.log"); - // Verify log file exists - Assert.True(File.Exists(logFile)); - } - finally - { - // Clean up log file - if (File.Exists(logFile)) - { - File.Delete(logFile); - } - } + // Act: create context with --log argument + using var context = Context.Create(["--log", logFile]); + + // Assert: log file exists + Assert.True(File.Exists(logFile)); } /// @@ -498,29 +490,20 @@ public void Context_WriteLine_Silent_DoesNotWriteToConsole() [Fact] public void Context_WriteLine_WithLogFile_WritesToLogFile() { - // Create temporary log file path - var logFile = Path.GetTempFileName(); - try - { - // Create context with log file and write a message - using (var context = Context.Create(["--log", logFile])) - { - // Write a line - context.WriteLine("Test message"); - } - - // Verify message was written to log file - var logContent = File.ReadAllText(logFile); - Assert.Equal("Test message" + Environment.NewLine, logContent); - } - finally + // Arrange: create a temporary log file path + using var tempDir = new TemporaryDirectory(); + var logFile = tempDir.GetFilePath("output.log"); + + // Act: create context with log file and write a message + using (var context = Context.Create(["--log", logFile])) { - // Clean up log file - if (File.Exists(logFile)) - { - File.Delete(logFile); - } + // Write a line + context.WriteLine("Test message"); } + + // Assert: message was written to log file + var logContent = File.ReadAllText(logFile); + Assert.Equal("Test message" + Environment.NewLine, logContent); } /// @@ -587,29 +570,20 @@ public void Context_WriteError_Silent_DoesNotWriteToConsole() [Fact] public void Context_WriteError_WithLogFile_WritesToLogFile() { - // Create temporary log file path - var logFile = Path.GetTempFileName(); - try - { - // Create context with log file and write an error - using (var context = Context.Create(["--log", logFile])) - { - // Write an error - context.WriteError("Error message"); - } - - // Verify message was written to log file - var logContent = File.ReadAllText(logFile); - Assert.Equal("Error message" + Environment.NewLine, logContent); - } - finally + // Arrange: create a temporary log file path + using var tempDir = new TemporaryDirectory(); + var logFile = tempDir.GetFilePath("output.log"); + + // Act: create context with log file and write an error + using (var context = Context.Create(["--log", logFile])) { - // Clean up log file - if (File.Exists(logFile)) - { - File.Delete(logFile); - } + // Write an error + context.WriteError("Error message"); } + + // Assert: message was written to log file + var logContent = File.ReadAllText(logFile); + Assert.Equal("Error message" + Environment.NewLine, logContent); } /// @@ -680,28 +654,19 @@ public void Context_ExitCode_NoErrors_RemainsZero() [Fact] public void Context_Dispose_ClosesLogFileProperly() { - // Create temporary log file path - var logFile = Path.GetTempFileName(); - try - { - // Create and dispose context with log file - using (var context = Context.Create(["--log", logFile])) - { - context.WriteLine("Test message"); - } - - // Verify we can delete the log file (it's been closed) - File.Delete(logFile); - Assert.False(File.Exists(logFile)); - } - finally + // Arrange: create a temporary log file path + using var tempDir = new TemporaryDirectory(); + var logFile = tempDir.GetFilePath("output.log"); + + // Act: create and dispose context with log file + using (var context = Context.Create(["--log", logFile])) { - // Clean up log file if it still exists - if (File.Exists(logFile)) - { - File.Delete(logFile); - } + context.WriteLine("Test message"); } + + // Assert: log file can be deleted (it's been closed by Dispose) + File.Delete(logFile); + Assert.False(File.Exists(logFile)); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationSubsystemTests.cs b/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationSubsystemTests.cs index 0d294c11..9fc2e605 100644 --- a/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationSubsystemTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationSubsystemTests.cs @@ -20,6 +20,7 @@ using DemaConsulting.BuildMark.Cli; using DemaConsulting.BuildMark.Configuration; +using DemaConsulting.BuildMark.Utilities; namespace DemaConsulting.BuildMark.Tests.Configuration; @@ -39,9 +40,8 @@ public class ConfigurationSubsystemTests public async Task Configuration_ReadAsync_ValidFile_ReturnsConfiguration() { // Arrange: create a temporary directory with a valid .buildmark.yaml file - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -60,25 +60,17 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act: read the configuration - var result = await BuildMarkConfigReader.ReadAsync(directory); + // Act: read the configuration + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); - // Assert: configuration is returned with expected structure - Assert.NotNull(result.Config); - Assert.False(result.HasErrors); - Assert.Equal("github", result.Config.Connector?.Type); - Assert.Equal("test-owner", result.Config.Connector?.GitHub?.Owner); - Assert.Equal("test-repo", result.Config.Connector?.GitHub?.Repo); - Assert.Single(result.Config.Sections); - Assert.Single(result.Config.Rules); - } - finally - { - // Cleanup temporary directory - Directory.Delete(directory, recursive: true); - } + // Assert: configuration is returned with expected structure + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("github", result.Config.Connector?.Type); + Assert.Equal("test-owner", result.Config.Connector?.GitHub?.Owner); + Assert.Equal("test-repo", result.Config.Connector?.GitHub?.Repo); + Assert.Single(result.Config.Sections); + Assert.Single(result.Config.Rules); } /// @@ -88,24 +80,15 @@ await File.WriteAllTextAsync( public async Task Configuration_ReadAsync_MissingFile_ReturnsEmptyResult() { // Arrange: create a temporary directory with no .buildmark.yaml file - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); + using var tempDir = new TemporaryDirectory(); - try - { - // Act: read configuration from a directory without the file - var result = await BuildMarkConfigReader.ReadAsync(directory); + // Act: read configuration from a directory without the file + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); - // Assert: result has null Config and no errors - Assert.Null(result.Config); - Assert.False(result.HasErrors); - Assert.Empty(result.Issues); - } - finally - { - // Cleanup temporary directory - Directory.Delete(directory, recursive: true); - } + // Assert: result has null Config and no errors + Assert.Null(result.Config); + Assert.False(result.HasErrors); + Assert.Empty(result.Issues); } /// @@ -115,30 +98,21 @@ public async Task Configuration_ReadAsync_MissingFile_ReturnsEmptyResult() public async Task Configuration_ReadAsync_MalformedFile_ReportsError() { // Arrange: create a temporary directory with a malformed .buildmark.yaml file - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, "connector:\n\ttype: github\n", TestContext.Current.CancellationToken); - try - { - // Act: read the malformed configuration - var result = await BuildMarkConfigReader.ReadAsync(directory); + // Act: read the malformed configuration + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); - // Assert: result contains errors and Config is null - Assert.Null(result.Config); - Assert.True(result.HasErrors); - Assert.NotEmpty(result.Issues); - Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); - } - finally - { - // Cleanup temporary directory - Directory.Delete(directory, recursive: true); - } + // Assert: result contains errors and Config is null + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.NotEmpty(result.Issues); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); } // ───────────────────────────────────────────────────────────────────────── @@ -202,9 +176,8 @@ public void Configuration_Issues_WarningIssue_DoesNotSetExitCode() public async Task Configuration_Issues_ValidationError_ReportsAccurateLine() { // Arrange: create a YAML file where the unsupported key is on a known line number - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -214,22 +187,14 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act: read the configuration - var result = await BuildMarkConfigReader.ReadAsync(directory); + // Act: read the configuration + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); - // Assert: the error is reported with the correct line number (3) - Assert.NotNull(result.Issues); - Assert.True(result.HasErrors); - var issue = result.Issues[0]; - Assert.True(issue.Line == 3, $"Expected line 3 for 'unsupported-key' but got {issue.Line}"); - } - finally - { - // Cleanup temporary directory - Directory.Delete(directory, recursive: true); - } + // Assert: the error is reported with the correct line number (3) + Assert.NotNull(result.Issues); + Assert.True(result.HasErrors); + var issue = result.Issues[0]; + Assert.True(issue.Line == 3, $"Expected line 3 for 'unsupported-key' but got {issue.Line}"); } // ───────────────────────────────────────────────────────────────────────── @@ -243,9 +208,8 @@ await File.WriteAllTextAsync( public async Task Configuration_ConnectorConfig_ValidFile_ParsesConnectorSettings() { // Arrange: create a temporary directory with a valid .buildmark.yaml containing connector settings - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -258,24 +222,16 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act: read the configuration - var result = await BuildMarkConfigReader.ReadAsync(directory); + // Act: read the configuration + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); - // Assert: connector settings are parsed correctly - Assert.NotNull(result.Config); - Assert.False(result.HasErrors); - Assert.Equal("github", result.Config.Connector?.Type); - Assert.Equal("acme-org", result.Config.Connector?.GitHub?.Owner); - Assert.Equal("my-project", result.Config.Connector?.GitHub?.Repo); - Assert.Equal("https://api.github.example.com", result.Config.Connector?.GitHub?.BaseUrl); - } - finally - { - // Cleanup temporary directory - Directory.Delete(directory, recursive: true); - } + // Assert: connector settings are parsed correctly + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("github", result.Config.Connector?.Type); + Assert.Equal("acme-org", result.Config.Connector?.GitHub?.Owner); + Assert.Equal("my-project", result.Config.Connector?.GitHub?.Repo); + Assert.Equal("https://api.github.example.com", result.Config.Connector?.GitHub?.BaseUrl); } /// @@ -285,9 +241,8 @@ await File.WriteAllTextAsync( public async Task Configuration_ConnectorConfig_ValidFile_ParsesAzureDevOpsSettings() { // Arrange: create a temporary directory with a valid .buildmark.yaml containing Azure DevOps settings - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -301,24 +256,16 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act: read the configuration - var result = await BuildMarkConfigReader.ReadAsync(directory); + // Act: read the configuration + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); - // Assert: Azure DevOps connector settings are parsed correctly - Assert.NotNull(result.Config); - Assert.False(result.HasErrors); - Assert.Equal("azure-devops", result.Config.Connector?.Type); - Assert.Equal("https://dev.azure.com/acme", result.Config.Connector?.AzureDevOps?.OrganizationUrl); - Assert.Equal("acme", result.Config.Connector?.AzureDevOps?.Organization); - Assert.Equal("my-project", result.Config.Connector?.AzureDevOps?.Project); - Assert.Equal("my-repo", result.Config.Connector?.AzureDevOps?.Repository); - } - finally - { - // Cleanup temporary directory - Directory.Delete(directory, recursive: true); - } + // Assert: Azure DevOps connector settings are parsed correctly + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("azure-devops", result.Config.Connector?.Type); + Assert.Equal("https://dev.azure.com/acme", result.Config.Connector?.AzureDevOps?.OrganizationUrl); + Assert.Equal("acme", result.Config.Connector?.AzureDevOps?.Organization); + Assert.Equal("my-project", result.Config.Connector?.AzureDevOps?.Project); + Assert.Equal("my-repo", result.Config.Connector?.AzureDevOps?.Repository); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationTests.cs b/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationTests.cs index 49933f97..f8550ba5 100644 --- a/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationTests.cs @@ -20,6 +20,7 @@ using DemaConsulting.BuildMark.Cli; using DemaConsulting.BuildMark.Configuration; +using DemaConsulting.BuildMark.Utilities; namespace DemaConsulting.BuildMark.Tests.Configuration; @@ -35,23 +36,15 @@ public class ConfigurationTests public async Task BuildMarkConfigReader_ReadAsync_MissingFile_ReturnsEmptyResult() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.Null(result.Config); - Assert.False(result.HasErrors); - Assert.Empty(result.Issues); - } - finally - { - Directory.Delete(directory, recursive: true); - } + using var tempDir = new TemporaryDirectory(); + + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.Null(result.Config); + Assert.False(result.HasErrors); + Assert.Empty(result.Issues); } /// @@ -61,9 +54,8 @@ public async Task BuildMarkConfigReader_ReadAsync_MissingFile_ReturnsEmptyResult public async Task BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -83,28 +75,21 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.NotNull(result.Config); - Assert.False(result.HasErrors); - Assert.Equal("github", result.Config.Connector?.Type); - Assert.Equal("example-owner", result.Config.Connector?.GitHub?.Owner); - Assert.Equal("hello-world", result.Config.Connector?.GitHub?.Repo); - Assert.Equal("https://api.github.com", result.Config.Connector?.GitHub?.BaseUrl); - Assert.Single(result.Config.Sections); - Assert.Equal("changes", result.Config.Sections[0].Id); - Assert.Single(result.Config.Rules); - Assert.Equal("changes", result.Config.Rules[0].Route); - Assert.Equal("feature", result.Config.Rules[0].Match?.Label[0]); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("github", result.Config.Connector?.Type); + Assert.Equal("example-owner", result.Config.Connector?.GitHub?.Owner); + Assert.Equal("hello-world", result.Config.Connector?.GitHub?.Repo); + Assert.Equal("https://api.github.com", result.Config.Connector?.GitHub?.BaseUrl); + Assert.Single(result.Config.Sections); + Assert.Equal("changes", result.Config.Sections[0].Id); + Assert.Single(result.Config.Rules); + Assert.Equal("changes", result.Config.Rules[0].Route); + Assert.Equal("feature", result.Config.Rules[0].Match?.Label[0]); } /// @@ -114,9 +99,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_InvalidRepositoryValue_ReturnsErrorIssue() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -127,21 +111,14 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.Null(result.Config); - Assert.True(result.HasErrors); - Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); - Assert.Contains("owner/repo", result.Issues[0].Description); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Contains("owner/repo", result.Issues[0].Description); } /// @@ -151,29 +128,21 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_MalformedFile_ReturnsErrorIssue() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, "connector:\n\ttype: github\n", TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.Null(result.Config); - Assert.True(result.HasErrors); - Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); - Assert.Contains("tab", result.Issues[0].Description, StringComparison.OrdinalIgnoreCase); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Contains("tab", result.Issues[0].Description, StringComparison.OrdinalIgnoreCase); } /// @@ -183,9 +152,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_ValidAzureDevOpsConnector_ReturnsParsedConfiguration() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -202,24 +170,17 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.NotNull(result.Config); - Assert.False(result.HasErrors); - Assert.Equal("azure-devops", result.Config.Connector?.Type); - Assert.Equal("https://dev.azure.com/myorg", result.Config.Connector?.AzureDevOps?.OrganizationUrl); - Assert.Equal("myorg", result.Config.Connector?.AzureDevOps?.Organization); - Assert.Equal("myproject", result.Config.Connector?.AzureDevOps?.Project); - Assert.Equal("myrepo", result.Config.Connector?.AzureDevOps?.Repository); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("azure-devops", result.Config.Connector?.Type); + Assert.Equal("https://dev.azure.com/myorg", result.Config.Connector?.AzureDevOps?.OrganizationUrl); + Assert.Equal("myorg", result.Config.Connector?.AzureDevOps?.Organization); + Assert.Equal("myproject", result.Config.Connector?.AzureDevOps?.Project); + Assert.Equal("myrepo", result.Config.Connector?.AzureDevOps?.Repository); } /// @@ -229,9 +190,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsConnectorAliases_ReturnsParsedConfiguration() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -248,23 +208,16 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.NotNull(result.Config); - Assert.False(result.HasErrors); - Assert.Equal("https://dev.azure.com/myorg", result.Config.Connector?.AzureDevOps?.OrganizationUrl); - Assert.Equal("myorg", result.Config.Connector?.AzureDevOps?.Organization); - Assert.Equal("myproject", result.Config.Connector?.AzureDevOps?.Project); - Assert.Equal("myrepo", result.Config.Connector?.AzureDevOps?.Repository); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("https://dev.azure.com/myorg", result.Config.Connector?.AzureDevOps?.OrganizationUrl); + Assert.Equal("myorg", result.Config.Connector?.AzureDevOps?.Organization); + Assert.Equal("myproject", result.Config.Connector?.AzureDevOps?.Project); + Assert.Equal("myrepo", result.Config.Connector?.AzureDevOps?.Repository); } /// @@ -274,9 +227,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsConnectorAreaPath_ReturnsParsedConfiguration() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -293,20 +245,13 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.NotNull(result.Config); - Assert.False(result.HasErrors); - Assert.Equal(@"myproject\myrepo", result.Config.Connector?.AzureDevOps?.AreaPath); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal(@"myproject\myrepo", result.Config.Connector?.AzureDevOps?.AreaPath); } /// @@ -317,9 +262,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsConnectorEmptyAreaPath_ReturnsParsedConfiguration() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -336,20 +280,13 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert: AreaPath must be an empty string, not null, so that filtering is disabled - Assert.NotNull(result.Config); - Assert.False(result.HasErrors); - Assert.Equal(string.Empty, result.Config.Connector?.AzureDevOps?.AreaPath); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert: AreaPath must be an empty string, not null, so that filtering is disabled + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal(string.Empty, result.Config.Connector?.AzureDevOps?.AreaPath); } /// @@ -359,9 +296,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsNonScalarAreaPath_ReturnsErrorIssue() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -379,21 +315,14 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.Null(result.Config); - Assert.True(result.HasErrors); - Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); - Assert.Contains("Azure DevOps area-path must be a scalar string value", result.Issues[0].Description); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Contains("Azure DevOps area-path must be a scalar string value", result.Issues[0].Description); } /// @@ -403,9 +332,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsUnsupportedKey_ReturnsErrorIssue() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -416,21 +344,14 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.Null(result.Config); - Assert.True(result.HasErrors); - Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); - Assert.Contains("Unsupported Azure DevOps connector key", result.Issues[0].Description); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Contains("Unsupported Azure DevOps connector key", result.Issues[0].Description); } /// @@ -440,9 +361,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsNonMapping_ReturnsErrorIssue() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -452,21 +372,14 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.Null(result.Config); - Assert.True(result.HasErrors); - Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); - Assert.Contains("YAML mapping", result.Issues[0].Description); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Contains("YAML mapping", result.Issues[0].Description); } /// @@ -603,9 +516,8 @@ public void BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection() public async Task BuildMarkConfigReader_ReadAsync_ValidReportSection_ReturnsParsedReportConfig() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -616,23 +528,16 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.NotNull(result.Config); - Assert.False(result.HasErrors); - Assert.NotNull(result.Config.Report); - Assert.Equal("build-notes.md", result.Config.Report.File); - Assert.Equal(2, result.Config.Report.Depth); - Assert.True(result.Config.Report.IncludeKnownIssues); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.NotNull(result.Config.Report); + Assert.Equal("build-notes.md", result.Config.Report.File); + Assert.Equal(2, result.Config.Report.Depth); + Assert.True(result.Config.Report.IncludeKnownIssues); } /// @@ -642,9 +547,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_InvalidReportDepth_ReturnsErrorIssue() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -653,20 +557,13 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.Null(result.Config); - Assert.True(result.HasErrors); - Assert.Contains("positive integer", result.Issues[0].Description); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Contains("positive integer", result.Issues[0].Description); } /// @@ -676,9 +573,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_GitHubEmptyTokenVariable_ReturnsErrorIssue() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -690,21 +586,14 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.Null(result.Config); - Assert.True(result.HasErrors); - Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); - Assert.Contains("token-variable", result.Issues[0].Description); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Contains("token-variable", result.Issues[0].Description); } /// @@ -714,9 +603,8 @@ await File.WriteAllTextAsync( public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsEmptyTokenVariable_ReturnsErrorIssue() { // Arrange - var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - Directory.CreateDirectory(directory); - var filePath = Path.Combine(directory, ".buildmark.yaml"); + using var tempDir = new TemporaryDirectory(); + var filePath = tempDir.GetFilePath(".buildmark.yaml"); await File.WriteAllTextAsync( filePath, """ @@ -730,20 +618,13 @@ await File.WriteAllTextAsync( """, TestContext.Current.CancellationToken); - try - { - // Act - var result = await BuildMarkConfigReader.ReadAsync(directory); - - // Assert - Assert.Null(result.Config); - Assert.True(result.HasErrors); - Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); - Assert.Contains("token-variable", result.Issues[0].Description); - } - finally - { - Directory.Delete(directory, recursive: true); - } + // Act + var result = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + + // Assert + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Contains("token-variable", result.Issues[0].Description); } } diff --git a/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs b/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs index d35200cc..20e1088d 100644 --- a/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs @@ -56,17 +56,17 @@ public IntegrationTests() [Fact] public void BuildMark_VersionFlag_OutputsVersion() { - // Run the application with --version flag + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", _dllPath, "--version"); - // Verify success + // Assert: Assert.Equal(0, exitCode); - - // Verify version is output Assert.False(string.IsNullOrWhiteSpace(output)); Assert.DoesNotContain("Error", output); } @@ -77,17 +77,17 @@ public void BuildMark_VersionFlag_OutputsVersion() [Fact] public void BuildMark_HelpFlag_OutputsUsageInformation() { - // Run the application with --help flag + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", _dllPath, "--help"); - // Verify success + // Assert: Assert.Equal(0, exitCode); - - // Verify usage information Assert.Contains("Usage: buildmark", output); Assert.Contains("Options:", output); Assert.Contains("--version", output); @@ -100,7 +100,9 @@ public void BuildMark_HelpFlag_OutputsUsageInformation() [Fact] public void BuildMark_SilentFlag_SuppressesOutput() { - // Run the application with --silent and --help flags + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", @@ -108,10 +110,8 @@ public void BuildMark_SilentFlag_SuppressesOutput() "--silent", "--help"); - // Verify success + // Assert: Assert.Equal(0, exitCode); - - // Verify no banner in output Assert.DoesNotContain("BuildMark version", output); } @@ -121,17 +121,17 @@ public void BuildMark_SilentFlag_SuppressesOutput() [Fact] public void BuildMark_InvalidArgument_ShowsError() { - // Run the application with invalid argument + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", _dllPath, "--invalid-argument"); - // Verify error exit code + // Assert: Assert.Equal(1, exitCode); - - // Verify error message Assert.Contains("Error:", output); Assert.Contains("Unsupported argument", output); } @@ -179,17 +179,17 @@ public void BuildMark_InvalidReportPath_ShowsError() [Fact] public void BuildMark_ValidateFlag_RunsSelfValidation() { - // Run the application with --validate flag + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", _dllPath, "--validate"); - // Verify success + // Assert: Assert.Equal(0, exitCode); - - // Verify validation runs Assert.False(string.IsNullOrWhiteSpace(output)); } @@ -199,7 +199,9 @@ public void BuildMark_ValidateFlag_RunsSelfValidation() [Fact] public void BuildMark_LogParameter_IsAccepted() { - // Run the application with log parameter + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", @@ -207,10 +209,8 @@ public void BuildMark_LogParameter_IsAccepted() "--log", "test.log", "--help"); - // Verify success + // Assert: Assert.Equal(0, exitCode); - - // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); } @@ -220,7 +220,9 @@ public void BuildMark_LogParameter_IsAccepted() [Fact] public void BuildMark_ReportParameter_IsAccepted() { - // Run the application with report parameter + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", @@ -228,10 +230,8 @@ public void BuildMark_ReportParameter_IsAccepted() "--report", "output.md", "--help"); - // Verify success + // Assert: Assert.Equal(0, exitCode); - - // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); } @@ -241,7 +241,9 @@ public void BuildMark_ReportParameter_IsAccepted() [Fact] public void BuildMark_DepthParameter_IsAccepted() { - // Run the application with depth parameter + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", @@ -249,10 +251,8 @@ public void BuildMark_DepthParameter_IsAccepted() "--depth", "2", "--help"); - // Verify success + // Assert: Assert.Equal(0, exitCode); - - // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); } @@ -262,7 +262,9 @@ public void BuildMark_DepthParameter_IsAccepted() [Fact] public void BuildMark_BuildVersionParameter_IsAccepted() { - // Run the application with build-version parameter + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", @@ -270,10 +272,8 @@ public void BuildMark_BuildVersionParameter_IsAccepted() "--build-version", "1.0.0", "--help"); - // Verify success + // Assert: Assert.Equal(0, exitCode); - - // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); } @@ -283,7 +283,9 @@ public void BuildMark_BuildVersionParameter_IsAccepted() [Fact] public void BuildMark_ResultsParameter_IsAccepted() { - // Run the application with results parameter + // Arrange: (no setup required) + + // Act: var exitCode = Runner.Run( out var output, "dotnet", @@ -291,10 +293,8 @@ public void BuildMark_ResultsParameter_IsAccepted() "--results", "results.trx", "--help"); - // Verify success + // Assert: Assert.Equal(0, exitCode); - - // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); } @@ -305,31 +305,23 @@ public void BuildMark_ResultsParameter_IsAccepted() public void BuildMark_Report_GeneratesMarkdownWithVersionInformation() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with mock connector injected for deterministic output - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with mock connector injected for deterministic output + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => new MockRepoConnector()); - // Assert: report file contains markdown title and version information - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("# Build Report", content); - Assert.Contains("## Version Information", content); - Assert.Contains("2.0.0", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report file contains markdown title and version information + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("# Build Report", content); + Assert.Contains("## Version Information", content); + Assert.Contains("2.0.0", content); } /// @@ -339,31 +331,23 @@ public void BuildMark_Report_GeneratesMarkdownWithVersionInformation() public void BuildMark_Report_ContainsChangesAndBugFixesWithHyperlinks() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with mock connector injected for deterministic output - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with mock connector injected for deterministic output + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => new MockRepoConnector()); - // Assert: report contains changes and bug fixes sections with linked items - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("## Changes", content); - Assert.Contains("## Bugs Fixed", content); - Assert.Contains("](", content); // markdown hyperlink syntax [text](url) - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report contains changes and bug fixes sections with linked items + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("## Changes", content); + Assert.Contains("## Bugs Fixed", content); + Assert.Contains("](", content); // markdown hyperlink syntax [text](url) } /// @@ -373,30 +357,22 @@ public void BuildMark_Report_ContainsChangesAndBugFixesWithHyperlinks() public void BuildMark_Report_ShowsVersionRangeFromPreviousRelease() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with mock connector injected for deterministic output - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with mock connector injected for deterministic output + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => new MockRepoConnector()); - // Assert: report identifies the previous version as the baseline of the version range - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("Previous Version", content); - Assert.Contains("ver-1.1.0", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report identifies the previous version as the baseline of the version range + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("Previous Version", content); + Assert.Contains("ver-1.1.0", content); } /// @@ -407,31 +383,23 @@ public void BuildMark_Report_ShowsVersionRangeFromPreviousRelease() public void BuildMark_Report_BaselinePreRelease_SkipsSameCommitPredecessor() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with a connector that provides a pre-release where the immediate - // predecessor shares the same commit hash (should be skipped), leaving v1.1.0 as baseline - using var context = Context.Create( - ["--build-version", "1.2.0-beta.2", "--report", reportFile, "--silent"], - () => new PreReleaseSameCommitConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with a connector that provides a pre-release where the immediate + // predecessor shares the same commit hash (should be skipped), leaving v1.1.0 as baseline + using var context = Context.Create( + ["--build-version", "1.2.0-beta.2", "--report", reportFile, "--silent"], + () => new PreReleaseSameCommitConnector()); - // Assert: report uses v1.1.0 as the baseline (same-commit predecessor was skipped) - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("v1.1.0", content); - Assert.Contains("v1.1.0...1.2.0-beta.2", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report uses v1.1.0 as the baseline (same-commit predecessor was skipped) + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("v1.1.0", content); + Assert.Contains("v1.1.0...1.2.0-beta.2", content); } /// @@ -442,30 +410,22 @@ public void BuildMark_Report_BaselinePreRelease_SkipsSameCommitPredecessor() public void BuildMark_Report_BaselineFirstRelease_GeneratesFromBeginningOfHistory() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with a connector that provides a first release with no baseline - using var context = Context.Create( - ["--build-version", "1.0.0", "--report", reportFile, "--silent"], - () => new FirstReleaseConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with a connector that provides a first release with no baseline + using var context = Context.Create( + ["--build-version", "1.0.0", "--report", reportFile, "--silent"], + () => new FirstReleaseConnector()); - // Assert: report generates successfully and shows N/A for the missing baseline - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("1.0.0", content); - Assert.Contains("N/A", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report generates successfully and shows N/A for the missing baseline + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("1.0.0", content); + Assert.Contains("N/A", content); } /// @@ -475,29 +435,21 @@ public void BuildMark_Report_BaselineFirstRelease_GeneratesFromBeginningOfHistor public void BuildMark_Report_IncludesKnownIssues_WhenFlagIsSet() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with mock connector and include-known-issues flag - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--include-known-issues", "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with mock connector and include-known-issues flag + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--include-known-issues", "--silent"], + () => new MockRepoConnector()); - // Assert: report includes a known issues section - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("## Known Issues", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report includes a known issues section + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("## Known Issues", content); } /// @@ -507,30 +459,22 @@ public void BuildMark_Report_IncludesKnownIssues_WhenFlagIsSet() public void BuildMark_Report_DepthTwo_UsesLevelTwoHeadings() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with mock connector and report depth 2 - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--depth", "2", "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with mock connector and report depth 2 + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--depth", "2", "--silent"], + () => new MockRepoConnector()); - // Assert: report uses level-two heading for the title and level-three for sections - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("## Build Report", content); - Assert.Contains("### Version Information", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report uses level-two heading for the title and level-three for sections + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("## Build Report", content); + Assert.Contains("### Version Information", content); } /// @@ -539,6 +483,8 @@ public void BuildMark_Report_DepthTwo_UsesLevelTwoHeadings() [Fact] public void BuildMark_LintFlag_IsAccepted() { + // Arrange: (no setup required) + // Act: run the application with --lint flag var exitCode = Runner.Run( out var output, @@ -558,30 +504,22 @@ public void BuildMark_LintFlag_IsAccepted() public void BuildMark_Report_ConsumesConfigurationFileDuringGeneration() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with mock connector (configuration loading executes as part of report generation) - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program which loads configuration before generating the report - Program.Run(context); + // Create context with mock connector (configuration loading executes as part of report generation) + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => new MockRepoConnector()); - // Assert: tool succeeds and produces a report (configuration was loaded without error) - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.False(string.IsNullOrWhiteSpace(content)); - Assert.Contains("# Build Report", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program which loads configuration before generating the report + Program.Run(context); + + // Assert: tool succeeds and produces a report (configuration was loaded without error) + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.False(string.IsNullOrWhiteSpace(content)); + Assert.Contains("# Build Report", content); } /// @@ -591,30 +529,22 @@ public void BuildMark_Report_ConsumesConfigurationFileDuringGeneration() public void BuildMark_Report_UsesConnectorForBuildData() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with mock connector injected via connector factory - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with mock connector injected via connector factory + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => new MockRepoConnector()); - // Assert: report contains data sourced from the mock connector - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("Update documentation", content); - Assert.Contains("Fix bug in Y", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report contains data sourced from the mock connector + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("Update documentation", content); + Assert.Contains("Fix bug in Y", content); } /// @@ -624,31 +554,23 @@ public void BuildMark_Report_UsesConnectorForBuildData() public void BuildMark_Report_ContainsSectionDefinitions() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with mock connector - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with mock connector + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => new MockRepoConnector()); - // Assert: report contains the expected section headings - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("## Version Information", content); - Assert.Contains("## Changes", content); - Assert.Contains("## Bugs Fixed", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report contains the expected section headings + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("## Version Information", content); + Assert.Contains("## Changes", content); + Assert.Contains("## Bugs Fixed", content); } /// @@ -658,40 +580,32 @@ public void BuildMark_Report_ContainsSectionDefinitions() public void BuildMark_Report_RoutesItemsToCorrectSections() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Create context with mock connector (which provides items of different types) - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + // Create context with mock connector (which provides items of different types) + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => new MockRepoConnector()); - // Assert: bug items appear in Bugs Fixed section; non-bug items appear in Changes section - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - var changesStart = content.IndexOf("## Changes", StringComparison.Ordinal); - var bugsStart = content.IndexOf("## Bugs Fixed", StringComparison.Ordinal); - Assert.True(changesStart >= 0, "Report must contain Changes section"); - Assert.True(bugsStart >= 0, "Report must contain Bugs Fixed section"); - - // Bug-typed item "Fix bug in Y" should be in Bugs Fixed section - var bugsSection = content[bugsStart..]; - Assert.Contains("Fix bug in Y", bugsSection); - - // Non-bug item "Update documentation" should be in Changes section - var changesSection = content[changesStart..bugsStart]; - Assert.Contains("Update documentation", changesSection); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: bug items appear in Bugs Fixed section; non-bug items appear in Changes section + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + var changesStart = content.IndexOf("## Changes", StringComparison.Ordinal); + var bugsStart = content.IndexOf("## Bugs Fixed", StringComparison.Ordinal); + Assert.True(changesStart >= 0, "Report must contain Changes section"); + Assert.True(bugsStart >= 0, "Report must contain Bugs Fixed section"); + + // Bug-typed item "Fix bug in Y" should be in Bugs Fixed section + var bugsSection = content[bugsStart..]; + Assert.Contains("Fix bug in Y", bugsSection); + + // Non-bug item "Update documentation" should be in Changes section + var changesSection = content[changesStart..bugsStart]; + Assert.Contains("Update documentation", changesSection); } /// @@ -862,41 +776,33 @@ public void BuildMark_Report_HiddenBuildmarkBlock_IsRecognized() public void BuildMark_AzureDevOps_Report_GeneratesMarkdownWithVersionInformation() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Set up a mocked REST handler with a single version tag and commit - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddTagsResponse(new MockAdoTag("v1.0.0", "abc123")) - .AddCommitsResponse(new MockAdoCommit("abc123")) - .AddPullRequestsResponse() - .AddWiqlResponse(); - - using var mockHttpClient = new HttpClient(mockHandler); - var adoConnector = CreateMockAdoConnector(mockHttpClient, "abc123"); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Create context with Azure DevOps connector injected for deterministic output - using var context = Context.Create( - ["--build-version", "1.0.0", "--report", reportFile, "--silent"], - () => adoConnector); + // Set up a mocked REST handler with a single version tag and commit + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse(new MockAdoTag("v1.0.0", "abc123")) + .AddCommitsResponse(new MockAdoCommit("abc123")) + .AddPullRequestsResponse() + .AddWiqlResponse(); - // Act: run the program - Program.Run(context); + using var mockHttpClient = new HttpClient(mockHandler); + var adoConnector = CreateMockAdoConnector(mockHttpClient, "abc123"); - // Assert: report file contains markdown title and version information - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("# Build Report", content); - Assert.Contains("## Version Information", content); - Assert.Contains("1.0.0", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Create context with Azure DevOps connector injected for deterministic output + using var context = Context.Create( + ["--build-version", "1.0.0", "--report", reportFile, "--silent"], + () => adoConnector); + + // Act: run the program + Program.Run(context); + + // Assert: report file contains markdown title and version information + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("# Build Report", content); + Assert.Contains("## Version Information", content); + Assert.Contains("1.0.0", content); } /// @@ -907,53 +813,45 @@ public void BuildMark_AzureDevOps_Report_GeneratesMarkdownWithVersionInformation public void BuildMark_AzureDevOps_Report_ContainsChangesAndBugFixesWithHyperlinks() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Set up mocked REST data with two versions, two pull requests, and linked work items - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddTagsResponse( - new MockAdoTag("v1.1.0", "commit3"), - new MockAdoTag("v1.0.0", "commit1")) - .AddCommitsResponse( - new MockAdoCommit("commit3"), - new MockAdoCommit("commit2"), - new MockAdoCommit("commit1")) - .AddPullRequestsResponse( - new MockAdoPullRequest(101, "Add new feature", "completed", "commit3"), - new MockAdoPullRequest(100, "Fix critical bug", "completed", "commit2")) - .AddPullRequestWorkItemsResponse("repo", 101, 201) - .AddPullRequestWorkItemsResponse("repo", 100, 200) - .AddWorkItemsResponse( - new MockAdoWorkItem(201, "New feature work item", "User Story"), - new MockAdoWorkItem(200, "Bug work item", "Bug")) - .AddWiqlResponse(); - - using var mockHttpClient = new HttpClient(mockHandler); - var adoConnector = CreateMockAdoConnector(mockHttpClient, "commit3"); - - // Create context with Azure DevOps connector injected for deterministic output - using var context = Context.Create( - ["--build-version", "1.1.0", "--report", reportFile, "--silent"], - () => adoConnector); - - // Act: run the program - Program.Run(context); - - // Assert: report contains changes and bug fixes sections with linked items - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("## Changes", content); - Assert.Contains("## Bugs Fixed", content); - Assert.Contains("](", content); // markdown hyperlink syntax [text](url) - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); + + // Set up mocked REST data with two versions, two pull requests, and linked work items + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse( + new MockAdoTag("v1.1.0", "commit3"), + new MockAdoTag("v1.0.0", "commit1")) + .AddCommitsResponse( + new MockAdoCommit("commit3"), + new MockAdoCommit("commit2"), + new MockAdoCommit("commit1")) + .AddPullRequestsResponse( + new MockAdoPullRequest(101, "Add new feature", "completed", "commit3"), + new MockAdoPullRequest(100, "Fix critical bug", "completed", "commit2")) + .AddPullRequestWorkItemsResponse("repo", 101, 201) + .AddPullRequestWorkItemsResponse("repo", 100, 200) + .AddWorkItemsResponse( + new MockAdoWorkItem(201, "New feature work item", "User Story"), + new MockAdoWorkItem(200, "Bug work item", "Bug")) + .AddWiqlResponse(); + + using var mockHttpClient = new HttpClient(mockHandler); + var adoConnector = CreateMockAdoConnector(mockHttpClient, "commit3"); + + // Create context with Azure DevOps connector injected for deterministic output + using var context = Context.Create( + ["--build-version", "1.1.0", "--report", reportFile, "--silent"], + () => adoConnector); + + // Act: run the program + Program.Run(context); + + // Assert: report contains changes and bug fixes sections with linked items + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("## Changes", content); + Assert.Contains("## Bugs Fixed", content); + Assert.Contains("](", content); // markdown hyperlink syntax [text](url) } /// @@ -964,46 +862,38 @@ public void BuildMark_AzureDevOps_Report_ContainsChangesAndBugFixesWithHyperlink public void BuildMark_AzureDevOps_Report_ShowsVersionRangeFromPreviousRelease() { // Arrange: create a temporary report file path - var reportFile = Path.GetTempFileName(); - try - { - // Set up mocked REST data with three version tags so previous version selection is exercised - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddTagsResponse( - new MockAdoTag("v2.0.0", "commit3"), - new MockAdoTag("v1.1.0", "commit2"), - new MockAdoTag("v1.0.0", "commit1")) - .AddCommitsResponse( - new MockAdoCommit("commit3"), - new MockAdoCommit("commit2"), - new MockAdoCommit("commit1")) - .AddPullRequestsResponse() - .AddWiqlResponse(); - - using var mockHttpClient = new HttpClient(mockHandler); - var adoConnector = CreateMockAdoConnector(mockHttpClient, "commit3"); - - // Create context with Azure DevOps connector injected for deterministic output - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--silent"], - () => adoConnector); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); + + // Set up mocked REST data with three version tags so previous version selection is exercised + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse( + new MockAdoTag("v2.0.0", "commit3"), + new MockAdoTag("v1.1.0", "commit2"), + new MockAdoTag("v1.0.0", "commit1")) + .AddCommitsResponse( + new MockAdoCommit("commit3"), + new MockAdoCommit("commit2"), + new MockAdoCommit("commit1")) + .AddPullRequestsResponse() + .AddWiqlResponse(); + + using var mockHttpClient = new HttpClient(mockHandler); + var adoConnector = CreateMockAdoConnector(mockHttpClient, "commit3"); + + // Create context with Azure DevOps connector injected for deterministic output + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => adoConnector); - // Act: run the program - Program.Run(context); + // Act: run the program + Program.Run(context); - // Assert: report identifies the previous version as the baseline of the version range - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("Previous Version", content); - Assert.Contains("v1.1.0", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Assert: report identifies the previous version as the baseline of the version range + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("Previous Version", content); + Assert.Contains("v1.1.0", content); } /// @@ -1013,31 +903,23 @@ public void BuildMark_AzureDevOps_Report_ShowsVersionRangeFromPreviousRelease() public void BuildMark_ResultsParameter_WritesTrxFile() { // Arrange: create a temporary TRX results file path - var resultsFile = Path.ChangeExtension(Path.GetTempFileName(), ".trx"); - try - { - // Act: run the application with --validate and --results pointing to a TRX file - var exitCode = Runner.Run( - out _, - "dotnet", - _dllPath, - "--validate", - "--results", resultsFile, - "--silent"); - - // Assert: tool succeeds and writes a TRX file containing the TestRun XML element - Assert.Equal(0, exitCode); - Assert.True(File.Exists(resultsFile), "TRX results file should have been created"); - var content = File.ReadAllText(resultsFile); - Assert.Contains(" @@ -1047,31 +929,23 @@ public void BuildMark_ResultsParameter_WritesTrxFile() public void BuildMark_ResultsParameter_WritesJUnitFile() { // Arrange: create a temporary JUnit XML results file path - var resultsFile = Path.ChangeExtension(Path.GetTempFileName(), ".xml"); - try - { - // Act: run the application with --validate and --results pointing to an XML file - var exitCode = Runner.Run( - out _, - "dotnet", - _dllPath, - "--validate", - "--results", resultsFile, - "--silent"); - - // Assert: tool succeeds and writes an XML file containing the testsuites element - Assert.Equal(0, exitCode); - Assert.True(File.Exists(resultsFile), "JUnit XML results file should have been created"); - var content = File.ReadAllText(resultsFile); - Assert.Contains(" @@ -1081,33 +955,23 @@ public void BuildMark_ResultsParameter_WritesJUnitFile() public async Task BuildMark_Config_ConnectorType_ReadFromConfigFile() { // Arrange: create a temporary directory containing a .buildmark.yaml with an explicit connector type - var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(tempDir); - try - { - // Write a configuration file that explicitly selects the azure-devops connector type - var configContent = "connector:\n type: azure-devops\n"; - await File.WriteAllTextAsync( - Path.Combine(tempDir, ".buildmark.yaml"), - configContent, - TestContext.Current.CancellationToken); - - // Act: read configuration and create the connector through the factory - var loadResult = await BuildMarkConfigReader.ReadAsync(tempDir); - var connector = RepoConnectorFactory.Create(loadResult.Config?.Connector); - - // Assert: configuration was parsed without errors and the factory created an Azure DevOps connector - Assert.False(loadResult.HasErrors, "Configuration file should load without errors"); - Assert.NotNull(connector); - Assert.IsAssignableFrom(connector); - } - finally - { - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, recursive: true); - } - } + using var tempDir = new TemporaryDirectory(); + + // Write a configuration file that explicitly selects the azure-devops connector type + var configContent = "connector:\n type: azure-devops\n"; + await File.WriteAllTextAsync( + tempDir.GetFilePath(".buildmark.yaml"), + configContent, + TestContext.Current.CancellationToken); + + // Act: read configuration and create the connector through the factory + var loadResult = await BuildMarkConfigReader.ReadAsync(tempDir.DirectoryPath); + var connector = RepoConnectorFactory.Create(loadResult.Config?.Connector); + + // Assert: configuration was parsed without errors and the factory created an Azure DevOps connector + Assert.False(loadResult.HasErrors, "Configuration file should load without errors"); + Assert.NotNull(connector); + Assert.IsAssignableFrom(connector); } /// @@ -1138,28 +1002,20 @@ private static MockableAzureDevOpsRepoConnector CreateMockAdoConnector( /// The markdown report content string. private static string GenerateControlsMockReport() { - var reportFile = Path.GetTempFileName(); - try - { - // Create context with controls mock connector - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--silent"], - () => new ControlsMockConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Run the program - Program.Run(context); + // Create context with controls mock connector + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => new ControlsMockConnector()); - // Verify success and return content - Assert.Equal(0, context.ExitCode); - return File.ReadAllText(reportFile); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Run the program + Program.Run(context); + + // Verify success and return content + Assert.Equal(0, context.ExitCode); + return File.ReadAllText(reportFile); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs b/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs index e7a48519..4b086e14 100644 --- a/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs @@ -22,6 +22,7 @@ using DemaConsulting.BuildMark.Cli; using DemaConsulting.BuildMark.RepoConnectors; using DemaConsulting.BuildMark.RepoConnectors.Mock; +using DemaConsulting.BuildMark.Utilities; using DemaConsulting.BuildMark.Version; namespace DemaConsulting.BuildMark.Tests; @@ -173,46 +174,37 @@ public void Program_Run_ValidateFlag_OutputsValidationMessage() public void Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues() { // Create temporary report file path - var reportFile = Path.GetTempFileName(); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); + + // Create context with report and include-known-issues flags + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--include-known-issues", "--silent"], + () => new MockRepoConnector()); + + // Verify IncludeKnownIssues property is set + Assert.True(context.IncludeKnownIssues); + + // Capture console output + var originalOut = Console.Out; + using var writer = new StringWriter(); + Console.SetOut(writer); + try { - // Create context with report and include-known-issues flags - using var context = Context.Create( - ["--build-version", "2.0.0", "--report", reportFile, "--include-known-issues", "--silent"], - () => new MockRepoConnector()); + // Run program + Program.Run(context); - // Verify IncludeKnownIssues property is set - Assert.True(context.IncludeKnownIssues); + // Verify report file was created + Assert.True(File.Exists(reportFile)); - // Capture console output - var originalOut = Console.Out; - using var writer = new StringWriter(); - Console.SetOut(writer); - - try - { - // Run program - Program.Run(context); - - // Verify report file was created - Assert.True(File.Exists(reportFile)); - - // Verify the context flag was set correctly - Assert.True(context.IncludeKnownIssues); - } - finally - { - // Restore console output - Console.SetOut(originalOut); - } + // Verify the context flag was set correctly + Assert.True(context.IncludeKnownIssues); } finally { - // Clean up report file - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } + // Restore console output + Console.SetOut(originalOut); } } @@ -268,28 +260,20 @@ public void Program_Run_WithSilentFlag_SuppressesOutput() public void Program_Run_WithLogFlag_WritesToLogFile() { // Arrange: create a temporary log file path and context with --log flag - var logFile = Path.ChangeExtension(Path.GetTempFileName(), ".log"); - try - { - // Dispose context before reading file so the log file handle is released - using (var context = Context.Create(["--log", logFile, "--help"])) - { - // Act: run the program (help output is written to log file via context) - Program.Run(context); - } - - // Assert: log file exists and contains output (checked after context is disposed) - Assert.True(File.Exists(logFile), "Log file should have been created"); - var logContent = File.ReadAllText(logFile); - Assert.False(string.IsNullOrWhiteSpace(logContent), "Log file should contain output"); - } - finally + using var tempDir = new TemporaryDirectory(); + var logFile = tempDir.GetFilePath("output.log"); + + // Dispose context before reading file so the log file handle is released + using (var context = Context.Create(["--log", logFile, "--help"])) { - if (File.Exists(logFile)) - { - File.Delete(logFile); - } + // Act: run the program (help output is written to log file via context) + Program.Run(context); } + + // Assert: log file exists and contains output (checked after context is disposed) + Assert.True(File.Exists(logFile), "Log file should have been created"); + var logContent = File.ReadAllText(logFile); + Assert.False(string.IsNullOrWhiteSpace(logContent), "Log file should contain output"); } /// @@ -299,25 +283,17 @@ public void Program_Run_WithLogFlag_WritesToLogFile() public void Program_Run_WithResultsFlag_WritesResultsFile() { // Arrange: create a temporary TRX results file path and context with --validate and --results flags - var resultsFile = Path.ChangeExtension(Path.GetTempFileName(), ".trx"); - try - { - using var context = Context.Create(["--validate", "--results", resultsFile, "--silent"]); + using var tempDir = new TemporaryDirectory(); + var resultsFile = tempDir.GetFilePath("results.trx"); - // Act: run the program in validate mode - Program.Run(context); + using var context = Context.Create(["--validate", "--results", resultsFile, "--silent"]); - // Assert: results file was created by the validation run - Assert.Equal(0, context.ExitCode); - Assert.True(File.Exists(resultsFile), "Results file should have been created"); - } - finally - { - if (File.Exists(resultsFile)) - { - File.Delete(resultsFile); - } - } + // Act: run the program in validate mode + Program.Run(context); + + // Assert: results file was created by the validation run + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(resultsFile), "Results file should have been created"); } /// @@ -327,28 +303,20 @@ public void Program_Run_WithResultsFlag_WritesResultsFile() public void Program_Run_WithBuildVersionFlag_AcceptsBuildVersion() { // Arrange: create a temporary report file path and context with --build-version flag - var reportFile = Path.GetTempFileName(); - try - { - using var context = Context.Create( - ["--build-version", "3.2.1", "--report", reportFile, "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + using var context = Context.Create( + ["--build-version", "3.2.1", "--report", reportFile, "--silent"], + () => new MockRepoConnector()); - // Assert: program succeeds and the build version appears in the report - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("3.2.1", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: program succeeds and the build version appears in the report + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("3.2.1", content); } /// @@ -358,28 +326,20 @@ public void Program_Run_WithBuildVersionFlag_AcceptsBuildVersion() public void Program_Run_WithDepthFlag_SetsHeadingDepth() { // Arrange: create a temporary report file path and context with --depth 3 flag - var reportFile = Path.GetTempFileName(); - try - { - using var context = Context.Create( - ["--build-version", "1.0.0", "--report", reportFile, "--depth", "3", "--silent"], - () => new MockRepoConnector()); + using var tempDir = new TemporaryDirectory(); + var reportFile = tempDir.GetFilePath("report.md"); - // Act: run the program - Program.Run(context); + using var context = Context.Create( + ["--build-version", "1.0.0", "--report", reportFile, "--depth", "3", "--silent"], + () => new MockRepoConnector()); - // Assert: report uses level-three heading for the title (depth 3 = ###) - Assert.Equal(0, context.ExitCode); - var content = File.ReadAllText(reportFile); - Assert.Contains("### Build Report", content); - } - finally - { - if (File.Exists(reportFile)) - { - File.Delete(reportFile); - } - } + // Act: run the program + Program.Run(context); + + // Assert: report uses level-three heading for the title (depth 3 = ###) + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("### Build Report", content); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRestClientTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRestClientTests.cs index 1d57b5ff..0aad520a 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRestClientTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRestClientTests.cs @@ -250,4 +250,63 @@ public async Task AzureDevOpsRestClient_QueryWorkItemsAsync_StringValuedIds_Dese Assert.Equal(300, query.WorkItems[0].Id); Assert.Equal(301, query.WorkItems[1].Id); } + + /// + /// Verify that QueryWorkItemsAsync throws InvalidOperationException when the + /// Azure DevOps API returns an HTTP error response. + /// + [Fact] + public async Task AzureDevOpsRestClient_QueryWorkItemsAsync_WithHttpError_ThrowsInvalidOperationException() + { + // Arrange: + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddWiqlErrorResponse(); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act: + var act = async () => await client.QueryWorkItemsAsync( + "SELECT [System.Id] FROM workitems WHERE [System.AreaPath] = 'NonExistent'"); + + // Assert: + await Assert.ThrowsAsync(act); + } + + /// + /// Verify that GetRepositoryAsync throws HttpRequestException when the REST API returns an HTTP error. + /// + [Fact] + public async Task AzureDevOpsRestClient_GetRepositoryAsync_HttpError_ThrowsHttpRequestException() + { + // Arrange: + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddResponse("git/repositories/", "{}", System.Net.HttpStatusCode.NotFound); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act: + var act = async () => await client.GetRepositoryAsync("NonExistent"); + + // Assert: + await Assert.ThrowsAsync(act); + } + + /// + /// Verify that GetWorkItemsAsync returns an empty list when given an empty id list. + /// + [Fact] + public async Task AzureDevOpsRestClient_GetWorkItemsAsync_WithEmptyInput_ReturnsEmptyList() + { + // Arrange: + using var mockHandler = new MockAzureDevOpsHttpMessageHandler(); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act: + var workItems = await client.GetWorkItemsAsync([]); + + // Assert: + Assert.NotNull(workItems); + Assert.Empty(workItems); + } } diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs index bd520dc7..4ce6223d 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs @@ -39,7 +39,7 @@ public class AzureDevOpsTests /// Test that the AzureDevOps sub-subsystem provides a connector that implements IRepoConnector. /// [Fact] - public void AzureDevOps_ImplementsInterface_ReturnsTrue() + public void AzureDevOps_IRepoConnector_ConnectorInstance_ImplementsInterface() { // Arrange: create an AzureDevOpsRepoConnector instance from the AzureDevOps sub-subsystem var connector = new AzureDevOpsRepoConnector(); diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/WorkItemMapperTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/WorkItemMapperTests.cs index c01fc1d0..4b3faee5 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/WorkItemMapperTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/WorkItemMapperTests.cs @@ -22,8 +22,9 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.AzureDevOps; -/// -/// +/// +/// Unit tests for the WorkItemMapper class. +/// public class WorkItemMapperTests { // ───────────────────────────────────────────────────────────────────────── @@ -49,6 +50,25 @@ public void WorkItemMapper_MapWorkItemToItemInfo_BugType_ReturnsBugItem() Assert.Equal("bug", itemInfo.Type); } + /// + /// Verify that Issue work item type maps to a bug ItemInfo. + /// + [Fact] + public void WorkItemMapper_MapWorkItemToItemInfo_IssueType_ReturnsBugItem() + { + // Arrange: + var workItem = CreateWorkItem(100, "An issue", "Issue", "Active"); + + // Act: + var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/100", 1); + + // Assert: + Assert.NotNull(itemInfo); + Assert.Equal("100", itemInfo.Id); + Assert.Equal("An issue", itemInfo.Title); + Assert.Equal("bug", itemInfo.Type); + } + /// /// Verify that User Story work item type maps to a feature ItemInfo. /// @@ -68,6 +88,25 @@ public void WorkItemMapper_MapWorkItemToItemInfo_UserStoryType_ReturnsFeatureIte Assert.Equal("feature", itemInfo.Type); } + /// + /// Verify that Feature work item type maps to a feature ItemInfo. + /// + [Fact] + public void WorkItemMapper_MapWorkItemToItemInfo_FeatureType_ReturnsFeatureItem() + { + // Arrange: + var workItem = CreateWorkItem(104, "A feature", "Feature", "Active"); + + // Act: + var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/104", 5); + + // Assert: + Assert.NotNull(itemInfo); + Assert.Equal("104", itemInfo.Id); + Assert.Equal("A feature", itemInfo.Title); + Assert.Equal("feature", itemInfo.Type); + } + /// /// Verify that Epic work item type maps to a feature ItemInfo. /// @@ -267,3 +306,4 @@ private static AzureDevOpsWorkItem CreateWorkItem( return new AzureDevOpsWorkItem(id, fields); } } + diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs index 08310a49..7b4c77ea 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs @@ -166,6 +166,80 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_WithMultipleVersi Assert.Contains("v1.1.0...v2.0.0", buildInfo.CompleteChangelogLink.TargetUrl); } + /// + /// Test that GetBuildInformationAsync falls back to the remote owner when the configured owner is blank. + /// + [Fact] + public async Task GitHubRepoConnector_GetBuildInformationAsync_WithBlankConfiguredOwner_FallsBackPerField() + { + // Arrange - Create mock responses with a previous version so a changelog link is generated + using var mockHandler = new MockGitHubGraphQLHttpMessageHandler() + .AddCommitsResponse("commit2", "commit1") + .AddReleasesResponse( + new MockRelease("v1.1.0", "2024-02-01T00:00:00Z"), + new MockRelease("v1.0.0", "2024-01-01T00:00:00Z")) + .AddPullRequestsResponse() + .AddIssuesResponse() + .AddTagsResponse( + new MockTag("v1.1.0", "commit2"), + new MockTag("v1.0.0", "commit1")); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = new MockableGitHubRepoConnector( + new GitHubConnectorConfig { Owner = " ", Repo = "configured-repo" }, + mockHttpClient); + + // Set up mock command responses + connector.SetCommandResponse("git remote get-url origin", "https://github.com/parsed-owner/parsed-repo.git"); + connector.SetCommandResponse("git rev-parse --abbrev-ref HEAD", "main"); + connector.SetCommandResponse("git rev-parse HEAD", "commit2"); + connector.SetCommandResponse("gh auth token", "test-token"); + + // Act + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); + + // Assert + Assert.NotNull(buildInfo.CompleteChangelogLink); + Assert.Contains("https://github.com/parsed-owner/configured-repo/compare/", buildInfo.CompleteChangelogLink.TargetUrl); + } + + /// + /// Test that GetBuildInformationAsync falls back to the remote repo when the configured repo is blank. + /// + [Fact] + public async Task GitHubRepoConnector_GetBuildInformationAsync_WithBlankConfiguredRepo_FallsBackPerField() + { + // Arrange - Create mock responses with a previous version so a changelog link is generated + using var mockHandler = new MockGitHubGraphQLHttpMessageHandler() + .AddCommitsResponse("commit2", "commit1") + .AddReleasesResponse( + new MockRelease("v1.1.0", "2024-02-01T00:00:00Z"), + new MockRelease("v1.0.0", "2024-01-01T00:00:00Z")) + .AddPullRequestsResponse() + .AddIssuesResponse() + .AddTagsResponse( + new MockTag("v1.1.0", "commit2"), + new MockTag("v1.0.0", "commit1")); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = new MockableGitHubRepoConnector( + new GitHubConnectorConfig { Owner = "configured-owner", Repo = "" }, + mockHttpClient); + + // Set up mock command responses + connector.SetCommandResponse("git remote get-url origin", "https://github.com/parsed-owner/parsed-repo.git"); + connector.SetCommandResponse("git rev-parse --abbrev-ref HEAD", "main"); + connector.SetCommandResponse("git rev-parse HEAD", "commit2"); + connector.SetCommandResponse("gh auth token", "test-token"); + + // Act + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); + + // Assert + Assert.NotNull(buildInfo.CompleteChangelogLink); + Assert.Contains("https://github.com/configured-owner/parsed-repo/compare/", buildInfo.CompleteChangelogLink.TargetUrl); + } + /// /// Test that GetBuildInformationAsync correctly gathers changes from PRs with labels. /// @@ -773,6 +847,7 @@ public void GitHubRepoConnector_Configure_WithRules_HasRulesReturnsTrue() // Assert - Connector is still a valid instance after configuration Assert.NotNull(connector); Assert.IsAssignableFrom(connector); + Assert.True(connector.HasRules, "HasRules should return true after Configure is called with rules"); } /// @@ -1083,4 +1158,3 @@ await Assert.ThrowsAsync( } - diff --git a/test/DemaConsulting.BuildMark.Tests/SelfTest/SelfTestTests.cs b/test/DemaConsulting.BuildMark.Tests/SelfTest/SelfTestTests.cs index c6a884d9..9888652c 100644 --- a/test/DemaConsulting.BuildMark.Tests/SelfTest/SelfTestTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/SelfTest/SelfTestTests.cs @@ -21,6 +21,7 @@ using DemaConsulting.BuildMark.Cli; using DemaConsulting.BuildMark.RepoConnectors.Mock; using DemaConsulting.BuildMark.SelfTest; +using DemaConsulting.BuildMark.Utilities; namespace DemaConsulting.BuildMark.Tests.SelfTest; @@ -36,32 +37,19 @@ public class SelfTestTests public void SelfTest_Validation_WithTrxFile_WritesResults() { // Arrange: create a temporary directory and define a TRX results file path - var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_subsystem_test_{Guid.NewGuid()}"); - Directory.CreateDirectory(tempDir); - - try - { - var trxFile = Path.Combine(tempDir, "results.trx"); - var args = new[] { "--validate", "--results", trxFile, "--silent" }; - - // Act: run the validation subsystem - using var context = Context.Create(args, () => new MockRepoConnector()); - Validation.Run(context); - - // Assert: TRX file was created and contains expected content - Assert.True(File.Exists(trxFile), "TRX file should be created"); - var trxContent = File.ReadAllText(trxFile); - Assert.Contains("TestRun", trxContent); - Assert.Contains("BuildMark Self-Validation", trxContent); - } - finally - { - // Cleanup temporary directory - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } + using var tempDir = new TemporaryDirectory(); + var trxFile = tempDir.GetFilePath("results.trx"); + var args = new[] { "--validate", "--results", trxFile, "--silent" }; + + // Act: run the validation subsystem + using var context = Context.Create(args, () => new MockRepoConnector()); + Validation.Run(context); + + // Assert: TRX file was created and contains expected content + Assert.True(File.Exists(trxFile), "TRX file should be created"); + var trxContent = File.ReadAllText(trxFile); + Assert.Contains("TestRun", trxContent); + Assert.Contains("BuildMark Self-Validation", trxContent); } /// @@ -71,32 +59,19 @@ public void SelfTest_Validation_WithTrxFile_WritesResults() public void SelfTest_Validation_WithXmlFile_WritesResults() { // Arrange: create a temporary directory and define an XML results file path - var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_subsystem_test_{Guid.NewGuid()}"); - Directory.CreateDirectory(tempDir); - - try - { - var xmlFile = Path.Combine(tempDir, "results.xml"); - var args = new[] { "--validate", "--results", xmlFile, "--silent" }; - - // Act: run the validation subsystem - using var context = Context.Create(args, () => new MockRepoConnector()); - Validation.Run(context); - - // Assert: XML file was created and contains expected content - Assert.True(File.Exists(xmlFile), "XML file should be created"); - var xmlContent = File.ReadAllText(xmlFile); - Assert.Contains("testsuites", xmlContent); - Assert.Contains("BuildMark Self-Validation", xmlContent); - } - finally - { - // Cleanup temporary directory - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } + using var tempDir = new TemporaryDirectory(); + var xmlFile = tempDir.GetFilePath("results.xml"); + var args = new[] { "--validate", "--results", xmlFile, "--silent" }; + + // Act: run the validation subsystem + using var context = Context.Create(args, () => new MockRepoConnector()); + Validation.Run(context); + + // Assert: XML file was created and contains expected content + Assert.True(File.Exists(xmlFile), "XML file should be created"); + var xmlContent = File.ReadAllText(xmlFile); + Assert.Contains("testsuites", xmlContent); + Assert.Contains("BuildMark Self-Validation", xmlContent); } /// @@ -106,31 +81,18 @@ public void SelfTest_Validation_WithXmlFile_WritesResults() public void SelfTest_ResultsOutput_WithTrxFile_CreatesFile() { // Arrange: create a temporary directory and define a TRX results file path - var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_subsystem_test_{Guid.NewGuid()}"); - Directory.CreateDirectory(tempDir); - - try - { - var trxFile = Path.Combine(tempDir, "output.trx"); - var args = new[] { "--validate", "--results", trxFile, "--silent" }; - - // Act: run validation and check results file creation - using var context = Context.Create(args, () => new MockRepoConnector()); - Validation.Run(context); - - // Assert: results file exists and has non-zero content - Assert.True(File.Exists(trxFile), "TRX results file should be created"); - var fileInfo = new FileInfo(trxFile); - Assert.True(fileInfo.Length > 0, "TRX results file should have content"); - } - finally - { - // Cleanup temporary directory - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } + using var tempDir = new TemporaryDirectory(); + var trxFile = tempDir.GetFilePath("output.trx"); + var args = new[] { "--validate", "--results", trxFile, "--silent" }; + + // Act: run validation and check results file creation + using var context = Context.Create(args, () => new MockRepoConnector()); + Validation.Run(context); + + // Assert: results file exists and has non-zero content + Assert.True(File.Exists(trxFile), "TRX results file should be created"); + var fileInfo = new FileInfo(trxFile); + Assert.True(fileInfo.Length > 0, "TRX results file should have content"); } /// @@ -140,31 +102,18 @@ public void SelfTest_ResultsOutput_WithTrxFile_CreatesFile() public void SelfTest_ResultsOutput_WithXmlFile_CreatesFile() { // Arrange: create a temporary directory and define an XML results file path - var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_subsystem_test_{Guid.NewGuid()}"); - Directory.CreateDirectory(tempDir); - - try - { - var xmlFile = Path.Combine(tempDir, "output.xml"); - var args = new[] { "--validate", "--results", xmlFile, "--silent" }; - - // Act: run validation and check results file creation - using var context = Context.Create(args, () => new MockRepoConnector()); - Validation.Run(context); - - // Assert: results file exists and has non-zero content - Assert.True(File.Exists(xmlFile), "XML results file should be created"); - var fileInfo = new FileInfo(xmlFile); - Assert.True(fileInfo.Length > 0, "XML results file should have content"); - } - finally - { - // Cleanup temporary directory - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } + using var tempDir = new TemporaryDirectory(); + var xmlFile = tempDir.GetFilePath("output.xml"); + var args = new[] { "--validate", "--results", xmlFile, "--silent" }; + + // Act: run validation and check results file creation + using var context = Context.Create(args, () => new MockRepoConnector()); + Validation.Run(context); + + // Assert: results file exists and has non-zero content + Assert.True(File.Exists(xmlFile), "XML results file should be created"); + var fileInfo = new FileInfo(xmlFile); + Assert.True(fileInfo.Length > 0, "XML results file should have content"); } /// @@ -174,34 +123,21 @@ public void SelfTest_ResultsOutput_WithXmlFile_CreatesFile() public void SelfTest_Qualification_WithoutResultsFile_Succeeds() { // Arrange: create a temporary directory and define a log file path (no results file) - var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_subsystem_test_{Guid.NewGuid()}"); - Directory.CreateDirectory(tempDir); + using var tempDir = new TemporaryDirectory(); + var logFile = tempDir.GetFilePath("validation.log"); + var args = new[] { "--validate", "--log", logFile, "--silent" }; - try + // Act: run the validation subsystem without specifying --results. + // Dispose the context before reading the log file to release the file lock. + using (var context = Context.Create(args, () => new MockRepoConnector())) { - var logFile = Path.Combine(tempDir, "validation.log"); - var args = new[] { "--validate", "--log", logFile, "--silent" }; - - // Act: run the validation subsystem without specifying --results. - // Dispose the context before reading the log file to release the file lock. - using (var context = Context.Create(args, () => new MockRepoConnector())) - { - Validation.Run(context); - } - - // Assert: validation ran and produced log output; no results file was created - Assert.True(File.Exists(logFile), "Log file should be created"); - var logContent = File.ReadAllText(logFile); - Assert.Contains("BuildMark Self-validation", logContent); - Assert.Contains("Total Tests:", logContent); - } - finally - { - // Cleanup temporary directory - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } + Validation.Run(context); } + + // Assert: validation ran and produced log output; no results file was created + Assert.True(File.Exists(logFile), "Log file should be created"); + var logContent = File.ReadAllText(logFile); + Assert.Contains("BuildMark Self-validation", logContent); + Assert.Contains("Total Tests:", logContent); } } diff --git a/test/DemaConsulting.BuildMark.Tests/SelfTest/ValidationTests.cs b/test/DemaConsulting.BuildMark.Tests/SelfTest/ValidationTests.cs index c3b17700..ccf1a745 100644 --- a/test/DemaConsulting.BuildMark.Tests/SelfTest/ValidationTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/SelfTest/ValidationTests.cs @@ -21,6 +21,7 @@ using DemaConsulting.BuildMark.Cli; using DemaConsulting.BuildMark.RepoConnectors.Mock; using DemaConsulting.BuildMark.SelfTest; +using DemaConsulting.BuildMark.Utilities; namespace DemaConsulting.BuildMark.Tests.SelfTest; @@ -36,51 +37,38 @@ public class ValidationTests public void Validation_Run_WithTrxResultsFile_WritesTrxFile() { // Arrange - var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_test_{Guid.NewGuid()}"); - Directory.CreateDirectory(tempDir); + using var tempDir = new TemporaryDirectory(); + var trxFile = tempDir.GetFilePath("results.trx"); + var args = new[] { "--validate", "--results", trxFile }; + using var outputWriter = new StringWriter(); + using var errorWriter = new StringWriter(); + + var originalOut = Console.Out; + var originalError = Console.Error; try { - var trxFile = Path.Combine(tempDir, "results.trx"); - var args = new[] { "--validate", "--results", trxFile }; - - using var outputWriter = new StringWriter(); - using var errorWriter = new StringWriter(); - - var originalOut = Console.Out; - var originalError = Console.Error; - try - { - // Capture console output - Console.SetOut(outputWriter); - Console.SetError(errorWriter); - - // Act - using var context = Context.Create(args, () => new MockRepoConnector()); - Validation.Run(context); - - // Assert - Verify TRX file was created - Assert.True(File.Exists(trxFile), "TRX file should be created"); - - // Verify TRX file contains expected content - var trxContent = File.ReadAllText(trxFile); - Assert.Contains("TestRun", trxContent); - Assert.Contains("BuildMark Self-Validation", trxContent); - } - finally - { - // Restore console output - Console.SetOut(originalOut); - Console.SetError(originalError); - } + // Capture console output + Console.SetOut(outputWriter); + Console.SetError(errorWriter); + + // Act + using var context = Context.Create(args, () => new MockRepoConnector()); + Validation.Run(context); + + // Assert - Verify TRX file was created + Assert.True(File.Exists(trxFile), "TRX file should be created"); + + // Verify TRX file contains expected content + var trxContent = File.ReadAllText(trxFile); + Assert.Contains("TestRun", trxContent); + Assert.Contains("BuildMark Self-Validation", trxContent); } finally { - // Cleanup - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } + // Restore console output + Console.SetOut(originalOut); + Console.SetError(originalError); } } @@ -91,51 +79,38 @@ public void Validation_Run_WithTrxResultsFile_WritesTrxFile() public void Validation_Run_WithXmlResultsFile_WritesJUnitFile() { // Arrange - var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_test_{Guid.NewGuid()}"); - Directory.CreateDirectory(tempDir); + using var tempDir = new TemporaryDirectory(); + var xmlFile = tempDir.GetFilePath("results.xml"); + var args = new[] { "--validate", "--results", xmlFile }; + + using var outputWriter = new StringWriter(); + using var errorWriter = new StringWriter(); + var originalOut = Console.Out; + var originalError = Console.Error; try { - var xmlFile = Path.Combine(tempDir, "results.xml"); - var args = new[] { "--validate", "--results", xmlFile }; - - using var outputWriter = new StringWriter(); - using var errorWriter = new StringWriter(); - - var originalOut = Console.Out; - var originalError = Console.Error; - try - { - // Capture console output - Console.SetOut(outputWriter); - Console.SetError(errorWriter); - - // Act - using var context = Context.Create(args, () => new MockRepoConnector()); - Validation.Run(context); - - // Assert - Verify XML file was created - Assert.True(File.Exists(xmlFile), "XML file should be created"); - - // Verify XML file contains expected content - var xmlContent = File.ReadAllText(xmlFile); - Assert.Contains("testsuites", xmlContent); - Assert.Contains("BuildMark Self-Validation", xmlContent); - } - finally - { - // Restore console output - Console.SetOut(originalOut); - Console.SetError(originalError); - } + // Capture console output + Console.SetOut(outputWriter); + Console.SetError(errorWriter); + + // Act + using var context = Context.Create(args, () => new MockRepoConnector()); + Validation.Run(context); + + // Assert - Verify XML file was created + Assert.True(File.Exists(xmlFile), "XML file should be created"); + + // Verify XML file contains expected content + var xmlContent = File.ReadAllText(xmlFile); + Assert.Contains("testsuites", xmlContent); + Assert.Contains("BuildMark Self-Validation", xmlContent); } finally { - // Cleanup - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } + // Restore console output + Console.SetOut(originalOut); + Console.SetError(originalError); } } @@ -146,46 +121,33 @@ public void Validation_Run_WithXmlResultsFile_WritesJUnitFile() public void Validation_Run_WithUnsupportedResultsFileExtension_ShowsError() { // Arrange - var tempDir = Path.Combine(Path.GetTempPath(), $"buildmark_test_{Guid.NewGuid()}"); - Directory.CreateDirectory(tempDir); + using var tempDir = new TemporaryDirectory(); + var unsupportedFile = tempDir.GetFilePath("results.json"); + var args = new[] { "--validate", "--results", unsupportedFile }; + + using var errorWriter = new StringWriter(); + var originalError = Console.Error; try { - var unsupportedFile = Path.Combine(tempDir, "results.json"); - var args = new[] { "--validate", "--results", unsupportedFile }; - - using var errorWriter = new StringWriter(); - - var originalError = Console.Error; - try - { - // Capture console error output - Console.SetError(errorWriter); - - // Act - using var context = Context.Create(args, () => new MockRepoConnector()); - Validation.Run(context); - - // Assert - Verify error message in error output (WriteError writes to Console.Error) - var output = errorWriter.ToString(); - Assert.Contains("Unsupported results file format", output); - - // Assert - Verify exit code is 1 when an error is reported - Assert.True(context.ExitCode == 1); - } - finally - { - // Restore console error output - Console.SetError(originalError); - } + // Capture console error output + Console.SetError(errorWriter); + + // Act + using var context = Context.Create(args, () => new MockRepoConnector()); + Validation.Run(context); + + // Assert - Verify error message in error output (WriteError writes to Console.Error) + var output = errorWriter.ToString(); + Assert.Contains("Unsupported results file format", output); + + // Assert - Verify exit code is 1 when an error is reported + Assert.True(context.ExitCode == 1); } finally { - // Cleanup - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } + // Restore console error output + Console.SetError(originalError); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Utilities/TemporaryDirectoryTests.cs b/test/DemaConsulting.BuildMark.Tests/Utilities/TemporaryDirectoryTests.cs new file mode 100644 index 00000000..ae50f04e --- /dev/null +++ b/test/DemaConsulting.BuildMark.Tests/Utilities/TemporaryDirectoryTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.BuildMark.Utilities; + +namespace DemaConsulting.BuildMark.Tests.Utilities; + +/// +/// Unit tests for the TemporaryDirectory class. +/// +[Collection("Sequential")] +public class TemporaryDirectoryTests +{ + /// + /// Test that the constructor creates the directory on disk. + /// + [Fact] + public void TemporaryDirectory_Constructor_CreatesDirectory() + { + // Act + using var tmpDir = new TemporaryDirectory(); + + // Assert + Assert.True(Directory.Exists(tmpDir.DirectoryPath), + "Directory should exist after construction."); + } + + /// + /// Test that two instances produce distinct directory paths. + /// + [Fact] + public void TemporaryDirectory_Constructor_CreatesUniqueDirectories() + { + // Act + using var tmpDir1 = new TemporaryDirectory(); + using var tmpDir2 = new TemporaryDirectory(); + + // Assert + Assert.NotEqual(tmpDir1.DirectoryPath, tmpDir2.DirectoryPath); + } + + /// + /// Test that GetFilePath returns a path located under the temporary directory. + /// + [Fact] + public void TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory() + { + // Arrange + using var tmpDir = new TemporaryDirectory(); + + // Act + var filePath = tmpDir.GetFilePath("output.md"); + + // Assert + Assert.StartsWith(tmpDir.DirectoryPath, filePath, StringComparison.OrdinalIgnoreCase); + Assert.EndsWith("output.md", filePath, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Test that GetFilePath with a nested relative path creates intermediate subdirectories. + /// + [Fact] + public void TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories() + { + // Arrange + using var tmpDir = new TemporaryDirectory(); + + // Act + var filePath = tmpDir.GetFilePath(Path.Combine("sub", "nested", "output.md")); + + // Assert: intermediate directories were created + Assert.True(Directory.Exists(Path.GetDirectoryName(filePath)), + "Intermediate subdirectories should be created by GetFilePath."); + } + + /// + /// Test that GetFilePath rejects a path-traversal attempt with ArgumentException. + /// + [Fact] + public void TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException() + { + // Arrange + using var tmpDir = new TemporaryDirectory(); + + // Act + Assert + Assert.Throws(() => tmpDir.GetFilePath("../escaped.txt")); + } + + /// + /// Test that Dispose deletes the temporary directory and its contents. + /// + [Fact] + public void TemporaryDirectory_Dispose_DeletesDirectory() + { + // Arrange + string dirPath; + using (var tmpDir = new TemporaryDirectory()) + { + dirPath = tmpDir.DirectoryPath; + File.WriteAllText(tmpDir.GetFilePath("file.txt"), "content"); + } + + // Assert + Assert.False(Directory.Exists(dirPath), + "Directory should be deleted after disposal."); + } + + /// + /// Test that Dispose is safe to call when the directory has already been deleted. + /// + [Fact] + public void TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow() + { + // Arrange + var tmpDir = new TemporaryDirectory(); + Directory.Delete(tmpDir.DirectoryPath, recursive: true); + + // Act + Assert: second disposal should not throw + var exception = Record.Exception(() => tmpDir.Dispose()); + Assert.Null(exception); + } +}