From c3b47b6bf52ed22fcf3df7ba6b8b1ea5a46366c5 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Wed, 20 May 2026 17:00:13 -0400 Subject: [PATCH 1/8] Template sync, formal reviews, and lint config alignment - Sync repository with template (README, docs introductions, OTS docs, design/verification unit sections) - Fix all issues from formal reviews of first 5 review sets (requirements gaps, design gaps, missing integration tests) - Binary-copy .markdownlint-cli2.yaml and .yamllint.yaml from template - Update .cspell.yaml to match template format and exclude paths - Fix MD060 table alignment in 7 design docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .cspell.yaml | 55 +++- .github/agents/developer.agent.md | 4 + .github/agents/quality.agent.md | 59 ++--- .github/agents/software-architect.agent.md | 2 +- .github/agents/template-sync.agent.md | 77 ++++++ .github/standards/design-documentation.md | 250 +++++------------- .github/standards/reqstream-usage.md | 35 ++- .github/standards/reviewmark-usage.md | 44 ++- .github/standards/software-items.md | 43 ++- .github/standards/technical-documentation.md | 4 + .../standards/verification-documentation.md | 176 +++++------- .markdownlint-cli2.yaml | 59 ++--- .yamllint.yaml | 36 +-- AGENTS.md | 29 +- README.md | 23 ++ docs/build_notes/introduction.md | 4 + docs/code_quality/introduction.md | 4 + docs/code_review_plan/introduction.md | 4 + docs/code_review_report/definition.yaml | 2 +- docs/code_review_report/introduction.md | 4 + docs/design/definition.yaml | 11 + docs/design/file-assert.md | 119 +++++++++ docs/design/file-assert/cli.md | 57 ++++ docs/design/file-assert/cli/context.md | 59 +++++ .../configuration/file-assert-config.md | 42 +++ .../configuration/file-assert-data.md | 58 ++++ .../file-assert/modeling/file-assert-file.md | 57 ++++ .../modeling/file-assert-html-assert.md | 64 ++++- .../modeling/file-assert-json-assert.md | 45 ++++ .../modeling/file-assert-pdf-assert.md | 62 +++++ .../file-assert/modeling/file-assert-rule.md | 44 +++ .../file-assert/modeling/file-assert-test.md | 40 +++ .../modeling/file-assert-text-assert.md | 35 +++ .../modeling/file-assert-xml-assert.md | 45 ++++ .../modeling/file-assert-yaml-assert.md | 45 ++++ .../modeling/file-assert-zip-assert.md | 88 ++++-- docs/design/file-assert/program.md | 50 +++- .../design/file-assert/selftest/validation.md | 65 ++++- .../file-assert/utilities/path-helpers.md | 44 +++ docs/design/introduction.md | 2 +- docs/design/ots.md | 113 ++++++++ docs/design/ots/buildmark.md | 51 ++++ docs/design/ots/fileassert.md | 54 ++++ docs/design/ots/pandoc.md | 53 ++++ docs/design/ots/reqstream.md | 52 ++++ docs/design/ots/reviewmark.md | 55 ++++ docs/design/ots/sarifmark.md | 48 ++++ docs/design/ots/sonarmark.md | 48 ++++ docs/design/ots/versionmark.md | 57 ++++ docs/design/ots/weasyprint.md | 54 ++++ docs/design/ots/xunit.md | 65 +++++ docs/reqstream/file-assert.yaml | 53 +++- docs/reqstream/file-assert/cli.yaml | 32 ++- docs/reqstream/file-assert/configuration.yaml | 5 + .../configuration/file-assert-data.yaml | 16 -- .../modeling/file-assert-file.yaml | 6 +- .../modeling/file-assert-html-assert.yaml | 8 +- docs/reqstream/file-assert/selftest.yaml | 8 + docs/reqstream/file-assert/utilities.yaml | 3 + docs/requirements_doc/introduction.md | 4 + docs/requirements_report/introduction.md | 4 + docs/user_guide/introduction.md | 21 ++ docs/verification/file-assert.md | 51 +++- docs/verification/file-assert/cli.md | 29 +- docs/verification/file-assert/cli/context.md | 11 + .../verification/file-assert/configuration.md | 13 + .../configuration/file-assert-config.md | 11 + .../configuration/file-assert-data.md | 13 +- docs/verification/file-assert/modeling.md | 13 + .../file-assert/modeling/file-assert-file.md | 11 + .../modeling/file-assert-html-assert.md | 11 + .../modeling/file-assert-json-assert.md | 11 + .../modeling/file-assert-pdf-assert.md | 11 + .../file-assert/modeling/file-assert-rule.md | 11 + .../file-assert/modeling/file-assert-test.md | 11 + .../modeling/file-assert-text-assert.md | 11 + .../modeling/file-assert-xml-assert.md | 11 + .../modeling/file-assert-yaml-assert.md | 11 + .../modeling/file-assert-zip-assert.md | 11 + docs/verification/file-assert/program.md | 13 + docs/verification/file-assert/selftest.md | 13 + .../file-assert/selftest/validation.md | 11 + docs/verification/file-assert/utilities.md | 13 + .../file-assert/utilities/path-helpers.md | 11 + docs/verification/introduction.md | 9 +- docs/verification/ots.md | 57 ++++ docs/verification/ots/buildmark.md | 6 + docs/verification/ots/fileassert.md | 6 + docs/verification/ots/pandoc.md | 6 + docs/verification/ots/reqstream.md | 6 + docs/verification/ots/reviewmark.md | 6 + docs/verification/ots/sarifmark.md | 6 + docs/verification/ots/sonarmark.md | 6 + docs/verification/ots/versionmark.md | 6 + docs/verification/ots/weasyprint.md | 6 + docs/verification/ots/xunit.md | 6 + .../Cli/CliTests.cs | 41 ++- .../IntegrationTests.cs | 228 ++++++++++++++++ 98 files changed, 2949 insertions(+), 514 deletions(-) create mode 100644 .github/agents/template-sync.agent.md create mode 100644 docs/design/ots.md create mode 100644 docs/design/ots/buildmark.md create mode 100644 docs/design/ots/fileassert.md create mode 100644 docs/design/ots/pandoc.md create mode 100644 docs/design/ots/reqstream.md create mode 100644 docs/design/ots/reviewmark.md create mode 100644 docs/design/ots/sarifmark.md create mode 100644 docs/design/ots/sonarmark.md create mode 100644 docs/design/ots/versionmark.md create mode 100644 docs/design/ots/weasyprint.md create mode 100644 docs/design/ots/xunit.md diff --git a/.cspell.yaml b/.cspell.yaml index 66e400a..0ff9e28 100644 --- a/.cspell.yaml +++ b/.cspell.yaml @@ -9,38 +9,67 @@ # - 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 words: - appsettings + - artifacts + - artefacts + - behavior + - behaviour - buildmark - Dema + - demaconsulting + - deserialize + - deserializes + - dogfooding - fileassert + - fontconfig + - initialise + - initialized + - initialisation - Linq - - xunit - - Pandoc + - materialize + - materialise + - Pango + - pandoc + - pandoctool + - recognized + - recognise + - recognises + - recognised - reqstream - reviewmark - Sarif - sarifmark - selftest + - serialize + - serializes + - serialise + - serialises + - sonarcloud - sonarmark + - unrecognized + - unrecognised - versionmark - Weasyprint + - weasyprinttool + - xunit - yamlfix # Exclude common build artifacts, dependencies, and vendored third-party code ignorePaths: - - "**/.git/**" - - "**/node_modules/**" - - "**/.venv/**" - - "**/thirdparty/**" - - "**/third-party/**" - - "**/3rd-party/**" - - "**/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 a95c562..5f208eb 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 da467d4..380d11f 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 494568d..de5efa2 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 0000000..44d29ff --- /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 768bf3f..635cb6d 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 303bb43..95d36a1 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 2d95832..990d707 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 6be029f..328a08e 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 2ac29f4..0dc4455 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 8eea3b7..8dc4408 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 c16c443..60f37d9 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -1,54 +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/**" - - "**/AGENT_REPORT_*.md" - - "**/.agent-logs/**" diff --git a/.yamllint.yaml b/.yamllint.yaml index 4fbc811..1cde40e 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -1,37 +1,29 @@ --- # 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/ - .venv/ - thirdparty/ - third-party/ - 3rd-party/ - .agent-logs/ + **/.git/** + **/node_modules/** + **/.venv/** + **/thirdparty/** + **/third-party/** + **/3rd-party/** + **/generated/** + **/.agent-logs/** 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 c83827c..cb80fe9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,14 @@ └── DemaConsulting.FileAssert.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 13b5fca..b3e53de 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ [![Security][badge-security]][link-security] [![NuGet][badge-nuget]][link-nuget] +## Overview + FileAssert is a .NET CLI tool for asserting file properties using YAML-defined test suites. It validates files against acceptance criteria such as size constraints, content requirements, and pattern matching, making it ideal for CI/CD pipelines and compliance workflows. @@ -72,6 +74,17 @@ fileassert --validate fileassert --silent --log output.log ``` +## Building + +```pwsh +pwsh ./build.ps1 +``` + +## User Guide + +The DemaConsulting.FileAssert User Guide is available on the +[DemaConsulting.FileAssert releases page](https://github.com/demaconsulting/FileAssert/releases). + ## Command-Line Options | Option | Description | @@ -286,6 +299,16 @@ Generated documentation includes: - **Requirements Justifications**: Detailed requirement rationale - **Trace Matrix**: Requirements to test traceability +## Contributing + +See [CONTRIBUTING.md](https://github.com/demaconsulting/FileAssert/blob/main/CONTRIBUTING.md) for +guidelines on reporting bugs, suggesting features, and submitting pull requests. + +## Support + +- [Report a bug or request a feature](https://github.com/demaconsulting/FileAssert/issues) +- [Ask a question or start a discussion](https://github.com/demaconsulting/FileAssert/discussions) + ## License Copyright (c) DEMA Consulting. Licensed under the MIT License. See [LICENSE][link-license] for details. diff --git a/docs/build_notes/introduction.md b/docs/build_notes/introduction.md index dae75b4..056f49d 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 + +- [DemaConsulting.FileAssert releases](https://github.com/demaconsulting/FileAssert/releases) diff --git a/docs/code_quality/introduction.md b/docs/code_quality/introduction.md index 0dcbd1d..c4eff73 100644 --- a/docs/code_quality/introduction.md +++ b/docs/code_quality/introduction.md @@ -33,3 +33,7 @@ This document is intended for: - Quality assurance teams reviewing code quality - Project stakeholders evaluating project health - Contributors understanding quality standards + +## References + +- [DemaConsulting.FileAssert releases](https://github.com/demaconsulting/FileAssert/releases) diff --git a/docs/code_review_plan/introduction.md b/docs/code_review_plan/introduction.md index 7fa7d06..89cc2f2 100644 --- a/docs/code_review_plan/introduction.md +++ b/docs/code_review_plan/introduction.md @@ -31,3 +31,7 @@ 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 + +- [DemaConsulting.FileAssert releases](https://github.com/demaconsulting/FileAssert/releases) diff --git a/docs/code_review_report/definition.yaml b/docs/code_review_report/definition.yaml index b238d43..b097a50 100644 --- a/docs/code_review_report/definition.yaml +++ b/docs/code_review_report/definition.yaml @@ -5,7 +5,7 @@ resource-path: input-files: - docs/code_review_report/title.txt - docs/code_review_report/introduction.md - - docs/code_review_report/generated/report.md + - docs/code_review_report/generated/report.md # Generated by ReviewMark (completed review records) template: template.html table-of-contents: true number-sections: true diff --git a/docs/code_review_report/introduction.md b/docs/code_review_report/introduction.md index 41f3bff..de1bc74 100644 --- a/docs/code_review_report/introduction.md +++ b/docs/code_review_report/introduction.md @@ -31,3 +31,7 @@ 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 + +- [DemaConsulting.FileAssert releases](https://github.com/demaconsulting/FileAssert/releases) diff --git a/docs/design/definition.yaml b/docs/design/definition.yaml index 6510a04..ec7f78c 100644 --- a/docs/design/definition.yaml +++ b/docs/design/definition.yaml @@ -27,6 +27,17 @@ input-files: - docs/design/file-assert/selftest.md - docs/design/file-assert/selftest/validation.md - docs/design/file-assert/ots-dependencies.md + - docs/design/ots.md + - docs/design/ots/buildmark.md + - docs/design/ots/fileassert.md + - docs/design/ots/pandoc.md + - docs/design/ots/reqstream.md + - docs/design/ots/reviewmark.md + - docs/design/ots/sarifmark.md + - docs/design/ots/sonarmark.md + - docs/design/ots/versionmark.md + - docs/design/ots/weasyprint.md + - docs/design/ots/xunit.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/design/file-assert.md b/docs/design/file-assert.md index 93de5f7..a65efcb 100644 --- a/docs/design/file-assert.md +++ b/docs/design/file-assert.md @@ -85,6 +85,125 @@ The following sequence describes the normal execution path: 10. When `--validate` is used with `--results`, `Validation.Run` writes TRX or JUnit XML results to the file specified by `context.ResultsFile`. +## Architecture + +The FileAssert system contains one top-level unit and five subsystems. There is no +system-level code; the system boundary is defined by the combination of its parts. + +| Item | Level | Responsibility | +| :------------ | :-------- | :------------------------------------------------------------------------- | +| Program | Unit | Entry point; creates `Context`; dispatches to validation or config logic. | +| Cli | Subsystem | Contains `Context`; owns arg parsing, I/O references, filter list, exit. | +| Configuration | Subsystem | Contains `FileAssertConfig`/`FileAssertData`; YAML deserialization, tests. | +| Modeling | Subsystem | Contains assertion classes; pure domain objects evaluating file rules. | +| Utilities | Subsystem | Contains `PathHelpers`; stateless path helper used by Modeling subsystem. | +| SelfTest | Subsystem | Contains `Validation`; runs built-in assertions when `--validate` passed. | + +All subsystems receive a `Context` instance (created by `Program`) rather than reading +command-line arguments directly. This removes argument-parsing concerns from every +subsystem and makes error recording consistent — all violations flow through `Context.WriteError`. + +## External Interfaces + +| Interface | Direction | Description | +| :--------------------------- | :-------- | :----------------------------------------------------------------------- | +| Command-line arguments | Input | POSIX flags and positional filters; unrecognized flags produce an error. | +| YAML configuration file | Input | YAML matching `FileAssertData` schema; default `.fileassert.yaml`. | +| File system (asserted files) | Input | Any file type; glob patterns relative to config file. Read-only. | +| Results file | Output | TRX or JUnit XML; written only when `--results` is specified. | +| Standard output | Output | UTF-8 text; one line per test pass or summary. | +| Standard error | Output | UTF-8 text; one line per rule violation or parse failure. | +| Log file | Output | Plain text mirroring stdout/stderr; written when `--log` is specified. | + +## Dependencies + +Runtime library dependencies used by the FileAssert system: + +- **YamlDotNet**: YAML configuration file deserialization and YAML-document content assertions. +- **PdfPig**: PDF document parsing for metadata, page count, and body text extraction. +- **HtmlAgilityPack**: lenient HTML document parsing for XPath node-count assertions. +- **Microsoft.Extensions.FileSystemGlobbing**: cross-platform glob pattern evaluation for file discovery. +- **DemaConsulting.TestResults**: serialization of test results to TRX and JUnit XML formats. +- **System.Xml.Linq / System.Xml.XPath** (.NET BCL): XML document parsing and XPath node-count assertions. +- **System.Text.Json** (.NET BCL): JSON document parsing for dot-notation path assertions. +- **System.IO.Compression** (.NET BCL): zip archive entry enumeration and glob-based count assertions. + +Test-project dependency: + +- **xUnit**: unit test discovery, execution, and TRX reporting — see _xUnit Integration Design_. + +## Risk Control Measures + +N/A — FileAssert is a development and CI tool. It carries no safety-critical function +and has no requirement for hardware or software item segregation for risk control +purposes. The tool runs with standard user-level operating-system permissions and +imposes no memory or process isolation boundaries beyond standard .NET process containment. + +## Data Flow + +```text +Command-line arguments + └─► Context (parsed flags, filter list, ResultsFile path) + │ + ├─► --version / --help ──────────────────────────────► stdout → exit 0 + │ + ├─► --validate ──────────────────────────────────────► Validation.Run + │ │ + │ └─► (same assertion pipeline below) + │ + └─► RunToolLogic + │ + ├─► config file path (from Context.ConfigFile) + │ + └─► FileAssertConfig.ReadFromFile + │ YAML file ──────────────────────────► FileAssertData deserialization + │ │ + │ FileAssertTest[] hierarchy + │ + └─► FileAssertConfig.Run (filtered by Context.Filters) + │ + └─► per test: FileAssertTest.Run + │ + └─► per file pattern: FileAssertFile.Run + │ + ├─► glob match ──────► file system (read-only) + │ │ + │ matched file paths + │ + ├─► size check ──────► Context.WriteError (on violation) + └─► content assertion (text / pdf / xml / html / yaml / + json / zip) ──────► Context.WriteError (on violation) + +Outputs: + Context.WriteError ─────────────────────────────────────► stderr (per violation) + pass/fail lines ─────────────────────────────────────────► stdout + Context.ResultsFile (optional) ──────────────────────────► TRX or JUnit XML file + Context.ExitCode ────────────────────────────────────────► process exit code (0 = pass, 1 = fail) +``` + +## Design Constraints + +- **Platform**: .NET (cross-platform); supported on Windows, macOS, and Linux via the .NET runtime. No + platform-specific code paths; all file system operations use `Path.Combine` and + `Microsoft.Extensions.FileSystemGlobbing` to normalize path separators per the host OS. +- **Distribution**: packaged as a .NET global tool; installed with `dotnet tool install` and invoked + as `fileassert` on the `PATH`. +- **Assembly scope**: all production logic is compiled into a single assembly. No external native DLLs + beyond those shipped with the .NET runtime. +- **Permissions**: runs with standard user-level operating-system permissions. Does not require + elevated privileges. +- **File system access**: read-only for the configuration file and all asserted files. Write access is + required only for the optional results file and optional log file. +- **Memory**: no explicit memory limit is defined. PDF, XML, HTML, YAML, and JSON files are parsed + in-process; very large files may exhaust available heap, which is an accepted limitation of the + current design. +- **Exit code contract**: `0` when all assertions pass or `--help`/`--version` is used; `1` when at + least one assertion violation or self-validation failure is recorded. +- **Missing default configuration file**: When no `--config` flag is provided and the + default `.fileassert.yaml` file does not exist, the tool prints guidance and exits with + code 0 (not an error condition). When an explicit `--config` path is missing, the tool + reports an error and exits with code 1. + ## Design Decisions - **Cross-platform portability**: All file system operations use `Path.Combine`, `Path.GetFullPath`, diff --git a/docs/design/file-assert/cli.md b/docs/design/file-assert/cli.md index bb99cc6..a7af625 100644 --- a/docs/design/file-assert/cli.md +++ b/docs/design/file-assert/cli.md @@ -21,6 +21,63 @@ and execution decisions. - Write output to stdout and the log file; write errors to stderr and the log file. - Expose an exit code that reflects whether any errors have been reported. +### Interfaces + +The `Context` unit exposes the following public interface: + +#### Properties + +| Property | Type | Description | +| :------------- | :---------------------- | :-------------------------------------------------------------- | +| `Silent` | `bool` | `true` when `--silent` was given; suppresses console output. | +| `Validate` | `bool` | `true` when `--validate` was specified. | +| `Version` | `bool` | `true` when `--version`/`-v` was specified. | +| `Help` | `bool` | `true` when `--help`/`-h`/`-?` was specified. | +| `Depth` | `int` | Heading depth for validation output (1–6, default 1). | +| `ConfigFile` | `string` | Configuration file path (default `.fileassert.yaml`). | +| `ResultsFile` | `string?` | Results file path; `null` if `--results` was not specified. | +| `Filters` | `IReadOnlyList` | Positional name-or-tag filter arguments. | +| `ExitCode` | `int` | `0` when no errors have been reported; `1` otherwise. | + +#### Methods + +| Method | Signature | Description | +| :----------- | :------------------------------------- | :----------------------------------------------------------- | +| `Create` | `static Context Create(string[] args)` | Parses args; opens log file when `--log` is specified. | +| `WriteLine` | `void WriteLine(string message)` | Writes to stdout (unless silent) and to the log file. | +| `WriteError` | `void WriteError(string message)` | Sets `_hasErrors`; writes to stderr (unless silent) and log. | +| `Dispose` | `void Dispose()` | Closes the log file writer. | + +#### Environmental Resources + +| Resource | Direction | Description | +| :-------------------- | :-------- | :-------------------------------------------------------------------- | +| Standard output | Output | Receives all `WriteLine` messages when `--silent` is not set. | +| Standard error | Output | Receives all `WriteError` messages when `--silent` is not set. | +| Command-line arguments| Input | Parsed by the internal `ArgumentParser` nested class. | +| File system (log) | Output | Log file opened for writing when `--log ` is specified. | +| File system (config) | Input | Configuration file path resolved from `--config` or default. | +| File system (results) | Output | Results file path; written by the Configuration subsystem. | + +### Design + +The `Context` class uses the following collaboration flow: + +1. `Context.Create` is called with the raw `string[]` argument array. +2. An internal `ArgumentParser` instance iterates the array, setting boolean flags + (`Silent`, `Validate`, `Version`, `Help`) and collecting string values (`ConfigFile`, + `ResultsFile`, `LogFile`) and positional filter arguments. +3. All recognized values are transferred to the immutable `Context` instance via + `private init` property accessors; unrecognized flags starting with `-` throw + an `ArgumentException`. +4. If a log file path was provided, `OpenLogFile` opens a `StreamWriter` with + `AutoFlush = true` on the specified path. +5. All subsequent output dispatches through `WriteLine` and `WriteError`: + - If `Silent` is `false`, messages are written to `Console.Out` / `Console.Error`. + - If a log writer is open, messages are always written to it regardless of `Silent`. +6. `WriteError` additionally sets the internal `_hasErrors` flag, causing `ExitCode` + to return `1` for the remainder of the context's lifetime. + ### Interactions with Other Subsystems | Consumer | Usage | diff --git a/docs/design/file-assert/cli/context.md b/docs/design/file-assert/cli/context.md index ec5c417..2d1f3a1 100644 --- a/docs/design/file-assert/cli/context.md +++ b/docs/design/file-assert/cli/context.md @@ -66,3 +66,62 @@ The private nested class `ArgumentParser` processes each argument in order: - **ErrorCount for per-test tracking**: The `ErrorCount` property increments monotonically so that callers running multiple named tests can snapshot the count before each test and compare after to derive a per-test pass/fail outcome without requiring a separate context per test. + +#### Purpose + +`Context` is the single owner of all command-line argument state, log-file I/O, and +error-count bookkeeping for a FileAssert execution. It provides a unified output interface +so that every assertion unit writes errors through one reporting path rather than directly +to the console. + +#### Data Model + +| Field / Property | Type | Description | +| :--------------------- | :---------------------- | :------------------------------------------------------------ | +| `_logWriter` | `StreamWriter?` | Open log-file stream; `null` when `--log` was not specified. | +| `_hasErrors` | `bool` | Set on the first `WriteError` call; drives `ExitCode`. | +| `_errorCount` | `int` | Monotonically increasing count of `WriteError` calls. | +| `Version` | `bool` | `true` when `--version` / `-v` is present. | +| `Help` | `bool` | `true` when `--help` / `-h` / `-?` is present. | +| `Silent` | `bool` | `true` when `--silent` is present. | +| `Validate` | `bool` | `true` when `--validate` is present. | +| `Depth` | `int` | Markdown heading depth; defaults to `1`. | +| `ResultsFile` | `string?` | Path for TRX/JUnit results output; `null` if not specified. | +| `ConfigFile` | `string` | Config file path; defaults to `.fileassert.yaml`. | +| `IsConfigFileExplicit` | `bool` | `true` when `--config` was explicitly provided. | +| `Filters` | `IReadOnlyList` | Positional arguments treated as test name/tag filters. | +| `ExitCode` | `int` | `1` if any errors have been reported; otherwise `0`. | +| `ErrorCount` | `int` | Read-only view of `_errorCount`. | + +#### Key Methods + +| Method | Purpose | +| :---------------------------------------- | :------------------------------------------------------------------ | +| `Context.Create(string[])` | Factory: parses args, opens log file, returns initialized instance. | +| `WriteLine(string)` | Writes to stdout (unless silent) and log file. | +| `WriteError(string)` | Sets error flag and counter; writes to stderr/log (unless silent). | +| `Dispose()` | Closes and disposes the log-file stream writer. | +| `ArgumentParser.ParseArguments(string[])` | Inner class: translates argument array into named parser state. | + +#### Error Handling + +| Scenario | Handling | +| :---------------------------------------- | :------------------------------------------------------------ | +| Null `args` passed to `Create` | `ArgumentNullException` thrown before parsing begins. | +| Unknown flag argument (starts with `-`) | `ArgumentException` propagated to the caller of `Create`. | +| Value-requiring flag with no value | `ArgumentException` propagated to the caller of `Create`. | +| `--depth` value not in 1–6 range | `ArgumentException` propagated to the caller of `Create`. | +| Log file cannot be opened | `InvalidOperationException` wrapping the underlying I/O. | +| Assertion or rule failure at runtime | Handled by `WriteError`; no throw — errors in `_errorCount`. | + +#### Interactions + +- **Callers**: `Program.RunToolLogic` constructs a `Context` via `Context.Create` and passes it + to all execution paths. `Validation.Run` constructs additional `Context` instances to drive + self-test scenarios. +- **Consumers**: `FileAssertConfig.Run`, `FileAssertTest.Run`, `FileAssertFile.Run`, and every + assert unit (`FileAssertTextAssert`, `FileAssertPdfAssert`, `FileAssertXmlAssert`, + `FileAssertHtmlAssert`, `FileAssertYamlAssert`, `FileAssertJsonAssert`, `FileAssertZipAssert`) + receive a `Context` reference and call `WriteLine` / `WriteError` to report results. +- **Internal dependency**: `ArgumentParser` (private nested class) is used exclusively by + `Context.Create`. diff --git a/docs/design/file-assert/configuration/file-assert-config.md b/docs/design/file-assert/configuration/file-assert-config.md index 3af15ca..564dbba 100644 --- a/docs/design/file-assert/configuration/file-assert-config.md +++ b/docs/design/file-assert/configuration/file-assert-config.md @@ -108,3 +108,45 @@ file is absent, the tool reports an error and exits with a non-zero code. a per-test sub-context, `Run` snapshots `context.ErrorCount` before each test and compares after. This reuses the existing error-reporting path without additional abstraction. Tests that were skipped by the filter are not recorded in the results. + +#### Purpose + +`FileAssertConfig` is the top-level configuration object for a FileAssert execution. Its single +responsibility is to load a YAML configuration file, materialize the full test/file/rule +hierarchy, and drive test execution with optional name or tag filtering and results +serialization. + +#### Data Model + +| Field / Property | Type | Description | +| :--------------- | :------------------------------ | :----------------------------------------------------- | +| `_configPath` | `string` | Path to the YAML config file; base for glob patterns. | +| `Tests` | `IReadOnlyList` | Ordered list of tests loaded from the configuration. | + +#### Key Methods + +| Method | Purpose | +| :--------------------------------------------------- | :----------------------------------------------------------- | +| `ReadFromFile(string path)` | Reads YAML; validates path; returns new `FileAssertConfig`. | +| `Run(Context context, IEnumerable filters)` | Iterates matching tests, executes each, writes results file. | +| `WriteResultsFile(Context, TestResults)` *(private)* | Serializes results to TRX or JUnit XML by file extension. | + +#### Error Handling + +| Scenario | Handling | +| :------------------------------------------ | :------------------------------------------------------------ | +| Null `path` passed to `ReadFromFile` | `ArgumentNullException` propagated to caller. | +| Configuration file does not exist | `FileNotFoundException` propagated to `Program.RunToolLogic`. | +| Invalid YAML in configuration file | `YamlDotNet` exceptions propagate to `Program.RunToolLogic`. | +| Null `context` or `filters` passed to `Run` | `ArgumentNullException` propagated to caller. | +| Unsupported results file extension | Error written via `context.WriteError`; no exception. | +| Results file write failure | Exception caught; error written via `context.WriteError`. | +| Individual test assertion failures | Accumulated in `context` via `WriteError`; run continues. | + +- **Caller**: `Program.RunToolLogic` calls `ReadFromFile` with `context.ConfigFile`, then calls + `Run(context, context.Filters)`. +- **Creates**: `FileAssertTest` instances via `FileAssertTest.Create` during `ReadFromFile`. +- **Calls**: `FileAssertTest.MatchesFilter` and `FileAssertTest.Run` for each qualifying test. +- **Uses**: `Context` for output and error reporting; `DemaConsulting.TestResults.IO.TrxSerializer` + and `JUnitSerializer` for results serialization; `YamlDotNet.Serialization.DeserializerBuilder` + for configuration parsing. diff --git a/docs/design/file-assert/configuration/file-assert-data.md b/docs/design/file-assert/configuration/file-assert-data.md index a9b530e..32723ed 100644 --- a/docs/design/file-assert/configuration/file-assert-data.md +++ b/docs/design/file-assert/configuration/file-assert-data.md @@ -102,9 +102,67 @@ assertion blocks. #### Design Decisions +#### Design Constraints + +The `FileAssertData` classes contain no validation or business logic, delegating all +validation to the factory methods in the Modeling subsystem. This maintains a clean +separation between deserialization and domain object construction. + - **Nullable reference type properties**: All properties are nullable to correctly represent absent YAML keys without throwing during deserialization. - **No validation logic in DTOs**: Validation and construction of domain objects is the responsibility of the factory methods in the Modeling subsystem, keeping DTOs simple. - **YamlMember aliases**: Explicit `[YamlMember(Alias = "...")]` attributes tie each property to its YAML key, decoupling C# naming conventions from the YAML schema. + +#### Purpose + +The `FileAssertData` file defines the complete set of YAML data transfer objects (DTOs) used +by `FileAssertConfig.ReadFromFile` to deserialize the `.fileassert.yaml` configuration. Each +class carries no business logic and serves exclusively as a container for raw values produced +by YamlDotNet. + +#### Data Model + +| DTO Class | Fields | +| :------------------------------ | :-------------------------------------------------------------------------- | +| `FileAssertConfigData` | `Tests: List?` | +| `FileAssertTestData` | `Name?, Tags (list)?, Files (list)?` | +| `FileAssertFileData` | Pattern, Min, Max, Count, MinSize, MaxSize; Text/Pdf/Xml/Html/Yaml/Json/Zip | +| `FileAssertRuleData` | `Contains?, DoesNotContain?, Matches?, DoesNotContainRegex?` | +| `FileAssertPdfData` | `Metadata (list)?, Pages?, Text (list)?` | +| `FileAssertPdfMetadataRuleData` | `Field?, Contains?, Matches?` | +| `FileAssertPdfPagesData` | `Min: int?`, `Max: int?` | +| `FileAssertQueryData` | `Query?, Count?, Min?, Max?` | +| `FileAssertZipData` | `Entries: List?` | +| `FileAssertZipEntryData` | `Pattern?, Min?, Max?` | + +All properties are nullable so that absent YAML keys deserialize cleanly to `null`. + +#### Key Methods + +N/A — DTOs are pure data containers. All properties are public `get`/`set`. No methods +are defined. + +#### Error Handling + +N/A — DTOs contain no validation logic. Any malformed YAML causes `YamlDotNet` to throw +a `YamlException` that propagates directly to `FileAssertConfig.ReadFromFile`. Constraint +validation (e.g. exactly one rule type per `FileAssertRuleData`) is the responsibility of +the Modeling subsystem factory methods. + +#### Interactions + +- **Populated by**: `YamlDotNet.Serialization.Deserializer` inside `FileAssertConfig.ReadFromFile` + via `DeserializerBuilder().IgnoreUnmatchedProperties().Build()`. +- **Consumed by**: + - `FileAssertTest.Create(FileAssertTestData)` in the Modeling subsystem. + - `FileAssertFile.Create(FileAssertFileData)` in the Modeling subsystem. + - `FileAssertRule.Create(FileAssertRuleData)` in the Modeling subsystem. + - `FileAssertTextAssert.Create(IEnumerable)` in the Modeling subsystem. + - `FileAssertPdfAssert.Create(FileAssertPdfData)` in the Modeling subsystem. + - `FileAssertHtmlAssert.Create(IEnumerable)` in the Modeling subsystem. + - `FileAssertJsonAssert.Create(IEnumerable)` in the Modeling subsystem. + - `FileAssertXmlAssert.Create(IEnumerable)` in the Modeling subsystem. + - `FileAssertYamlAssert.Create(IEnumerable)` in the Modeling subsystem. + - `FileAssertZipAssert.Create(FileAssertZipData)` in the Modeling subsystem. diff --git a/docs/design/file-assert/modeling/file-assert-file.md b/docs/design/file-assert/modeling/file-assert-file.md index f0cd58d..7230005 100644 --- a/docs/design/file-assert/modeling/file-assert-file.md +++ b/docs/design/file-assert/modeling/file-assert-file.md @@ -24,6 +24,7 @@ file-type-specific assert units. | `HtmlAssert` | `FileAssertHtmlAssert?` | HTML node assertions (null if not declared). | | `YamlAssert` | `FileAssertYamlAssert?` | YAML node assertions (null if not declared). | | `JsonAssert` | `FileAssertJsonAssert?` | JSON node assertions (null if not declared). | +| `ZipAssert` | `FileAssertZipAssert?` | Zip archive entry assertions; `null` if absent. | ##### Factory Method @@ -122,3 +123,59 @@ All properties except `pattern` are optional. least one text rule is defined. File-type parsing (PDF, XML, HTML, YAML, JSON) is attempted only when the corresponding assertion block is declared, avoiding unnecessary I/O and third-party library invocations. + +#### Purpose + +`FileAssertFile` is responsible for a single file-pattern assertion within a test. It +discovers files on disk via a glob pattern, enforces count and size constraints, and +delegates per-file content validation to file-type-specific assert units. + +#### Data Model + +| Property | Type | Description | +| :----------- | :---------------------- | :--------------------------------------------------- | +| `Pattern` | `string` | Glob pattern used to discover files (required). | +| `Min` | `int?` | Minimum number of matching files; `null` = no bound. | +| `Max` | `int?` | Maximum number of matching files; `null` = no bound. | +| `Count` | `int?` | Exact number of matching files; `null` = no bound. | +| `MinSize` | `long?` | Minimum file size in bytes; `null` = no bound. | +| `MaxSize` | `long?` | Maximum file size in bytes; `null` = no bound. | +| `TextAssert` | `FileAssertTextAssert?` | Text content assert unit; `null` if not declared. | +| `PdfAssert` | `FileAssertPdfAssert?` | PDF assert unit; `null` if not declared. | +| `XmlAssert` | `FileAssertXmlAssert?` | XML assert unit; `null` if not declared. | +| `HtmlAssert` | `FileAssertHtmlAssert?` | HTML assert unit; `null` if not declared. | +| `YamlAssert` | `FileAssertYamlAssert?` | YAML assert unit; `null` if not declared. | +| `JsonAssert` | `FileAssertJsonAssert?` | JSON assert unit; `null` if not declared. | +| `ZipAssert` | `FileAssertZipAssert?` | Zip archive assert unit; `null` if not declared. | + +#### Key Methods + +| Method | Purpose | +| :--------------------------------------- | :----------------------------------------------------------------- | +| `Create(FileAssertFileData data)` | Factory: validates pattern, builds assert units, returns instance. | +| `Run(Context context, string basePath)` | Discovers files, checks count/size, delegates to assert units. | + +#### Error Handling + +| Scenario | Handling | +| :------------------------------------- | :-------------------------------------------------------------------- | +| Null or whitespace `Pattern` in data | `InvalidOperationException` thrown by `Create`. | +| Count below `Min` | Error written via `context.WriteError`; `Run` returns immediately. | +| Count above `Max` | Error written via `context.WriteError`; `Run` returns immediately. | +| Count not equal to `Count` constraint | Error written via `context.WriteError`; `Run` returns immediately. | +| File size outside `MinSize`/`MaxSize` | Error written via `context.WriteError`; per-file assertions continue. | +| Parse errors in assert units | Assert units catch parse exceptions and call `context.WriteError`. | + +#### Interactions + +- **Caller**: `FileAssertTest.Run` iterates the `Files` collection and calls `Run` on each instance. +- **Created by**: `FileAssertTest.Create` via `FileAssertFile.Create`. +- **Delegates to**: + - `FileAssertTextAssert.Run` for text content rules. + - `FileAssertPdfAssert.Run` for PDF document rules. + - `FileAssertXmlAssert.Run` for XML XPath rules. + - `FileAssertHtmlAssert.Run` for HTML XPath rules. + - `FileAssertYamlAssert.Run` for YAML path rules. + - `FileAssertJsonAssert.Run` for JSON path rules. + - `FileAssertZipAssert.Run` for zip archive entry rules. +- **OTS dependency**: `Microsoft.Extensions.FileSystemGlobbing.Matcher` for file discovery. diff --git a/docs/design/file-assert/modeling/file-assert-html-assert.md b/docs/design/file-assert/modeling/file-assert-html-assert.md index bda9c1d..550c7b3 100644 --- a/docs/design/file-assert/modeling/file-assert-html-assert.md +++ b/docs/design/file-assert/modeling/file-assert-html-assert.md @@ -15,18 +15,18 @@ The main class coordinating XPath-based node count assertions for an HTML file. ###### FileAssertHtmlAssert Properties -| Property | Type | Description | -| :-------- | :----------------------------------- | :---------------------- | -| `Queries` | `IReadOnlyList` | XPath query assertions. | +| Property | +| :-------- | +| `Queries` | Each `FileAssertHtmlQuery` entry holds: -| Property | Type | Description | -| :------- | :------- | :------------------------------- | -| `Query` | `string` | XPath expression to evaluate. | -| `Count` | `int?` | Exact number of matched nodes. | -| `Min` | `int?` | Minimum number of matched nodes. | -| `Max` | `int?` | Maximum number of matched nodes. | +| Property | +| :------- | +| `Query` | +| `Count` | +| `Min` | +| `Max` | ###### FileAssertHtmlAssert Factory @@ -87,3 +87,49 @@ files: parse failure immediately gives users a clear, actionable error message. - **Independent query model**: `FileAssertHtmlQuery` is private to this unit so that HTML assertion behavior can evolve independently of the other structured-document assert units. + +#### Purpose + +`FileAssertHtmlAssert` is responsible for validating one HTML file against a list of XPath +queries. It parses the file with HtmlAgilityPack and enforces min, max, and exact node-count +constraints per query. + +#### Data Model + +| Field / Property | +| :--------------- | +| `Queries` | + +Each `FileAssertHtmlQuery` (private nested record) holds: + +| Property | +| :------- | +| `Query` | +| `Count` | +| `Min` | +| `Max` | + +#### Key Methods + +| Method | +| :---------------------------------------------- | +| `Create(IEnumerable data)` | +| `Run(Context context, string fileName)` | + +#### Error Handling + +| Scenario | +| :-------------------------------------------- | +| HtmlAgilityPack reports critical parse errors | +| Query result below `Min` | +| Query result above `Max` | +| Query result not equal to `Count` | + +#### Interactions + +- **Caller**: `FileAssertFile.Run` calls `HtmlAssert.Run(context, fileName)` when the `html:` + assertion block is declared. +- **Created by**: `FileAssertFile.Create` via `FileAssertHtmlAssert.Create`. +- **OTS dependency**: `HtmlAgilityPack.HtmlDocument` for lenient HTML parsing and + `HtmlDocument.DocumentNode.SelectNodes` for XPath evaluation. +- **Configuration dependency**: `FileAssertQueryData` DTOs from the Configuration subsystem. diff --git a/docs/design/file-assert/modeling/file-assert-json-assert.md b/docs/design/file-assert/modeling/file-assert-json-assert.md index 958f5e9..8ed8f55 100644 --- a/docs/design/file-assert/modeling/file-assert-json-assert.md +++ b/docs/design/file-assert/modeling/file-assert-json-assert.md @@ -89,3 +89,48 @@ files: and cardinality of array-valued keys. - **Independent query model**: `FileAssertJsonQuery` is private to this unit so that JSON assertion behavior can evolve independently of the other structured-document assert units. + +#### Purpose + +`FileAssertJsonAssert` is responsible for validating one JSON file against a list of +dot-notation path queries. It parses the file with `System.Text.Json.JsonDocument` and +enforces min, max, and exact element-count constraints per path. + +#### Data Model + +| Field / Property | Type | Description | +| :--------------- | :----------------------------------- | :-------------------------------------------- | +| `Queries` | `IReadOnlyList` | Ordered list of dot-notation path assertions. | + +Each `FileAssertJsonQuery` (private nested record) holds: + +| Property | Type | Description | +| :------- | :------- | :------------------------------------------------------- | +| `Query` | `string` | Dot-notation path to traverse. | +| `Count` | `int?` | Expected exact element count; `null` = N/A. | +| `Min` | `int?` | Minimum element count; `null` = no bound. | +| `Max` | `int?` | Maximum element count; `null` = no bound. | + +#### Key Methods + +| Method | Purpose | +| :---------------------------------------------- | :--------------------------------------------------------------- | +| `Create(IEnumerable data)` | Factory: converts query DTOs to `FileAssertJsonQuery` instances. | +| `Run(Context context, string fileName)` | Parses the JSON file and evaluates each dot-notation path query. | + +#### Error Handling + +| Scenario | Handling | +| :------------------------------------------ | :------------------------------------------------------------------- | +| `JsonException` during `JsonDocument.Parse` | Error written via `context.WriteError`; `Run` returns immediately. | +| Query result below `Min` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result above `Max` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result not equal to `Count` | Error written via `context.WriteError`; subsequent queries continue. | + +#### Interactions + +- **Caller**: `FileAssertFile.Run` calls `JsonAssert.Run(context, fileName)` when the `json:` + assertion block is declared. +- **Created by**: `FileAssertFile.Create` via `FileAssertJsonAssert.Create`. +- **OTS dependency**: `System.Text.Json.JsonDocument` (BCL) for parsing and element traversal. +- **Configuration dependency**: `FileAssertQueryData` DTOs from the Configuration subsystem. diff --git a/docs/design/file-assert/modeling/file-assert-pdf-assert.md b/docs/design/file-assert/modeling/file-assert-pdf-assert.md index ca2b314..3abca11 100644 --- a/docs/design/file-assert/modeling/file-assert-pdf-assert.md +++ b/docs/design/file-assert/modeling/file-assert-pdf-assert.md @@ -167,3 +167,65 @@ files: - **Shared text rule hierarchy**: Body text assertions delegate to the same `FileAssertRule` hierarchy used by `FileAssertTextAssert`, ensuring consistent rule behavior across all assertion types. + +#### Purpose + +`FileAssertPdfAssert` is responsible for validating one PDF file. It coordinates three +assertion categories — metadata field rules, page count constraints, and body text rules — +using PdfPig as the PDF parsing library. + +#### Data Model + +| Field | Type | Description | +| :---------- | :------------------------------- | :---------------------------------------------- | +| `_metadata` | `IReadOnlyList` | Ordered list of metadata field assertions. | +| `_pages` | `PdfPages?` | Page count constraints; `null` if not declared. | +| `_text` | `IReadOnlyList` | Body text rules applied to extracted page text. | + +Inner class `PdfMetadataRule` holds: + +| Property | Type | Description | +| :--------- | :-------- | :------------------------------------------------ | +| `Field` | `string` | Metadata field name (Title, Author, Subject, etc.)| +| `Contains` | `string?` | Substring the field value must contain. | +| `Matches` | `string?` | Regex the field value must match. | + +Inner class `PdfPages` holds: + +| Property | Type | Description | +| :------- | :----- | :----------------------- | +| `Min` | `int?` | Minimum number of pages. | +| `Max` | `int?` | Maximum number of pages. | + +#### Key Methods + +| Method | Purpose | +| :------------------------------------------------------------------ | :-------------------------------------------- | +| `Create(FileAssertPdfData data)` | Builds metadata/page/text rules from DTO. | +| `Run(Context context, string fileName)` | Opens PDF; applies metadata/page/text rules. | +| `GetMetadataField(PdfDocument doc, string field)` *(private)* | Maps field to `DocumentInformation` property. | +| `PdfMetadataRule.FromData(FileAssertPdfMetadataRuleData)` *(inner)* | Creates a `PdfMetadataRule` from DTO. | +| `PdfMetadataRule.Apply(Context, string, string?)` *(inner)* | Applies `Contains`/`Matches` to field value. | +| `PdfPages.FromData(FileAssertPdfPagesData)` *(inner)* | Creates `PdfPages` from DTO. | +| `PdfPages.Apply(Context, string, int)` *(inner)* | Checks `Min`/`Max` against actual page count. | + +#### Error Handling + +| Scenario | Handling | +| :------------------------------------------ | :------------------------------------------------------------------- | +| PdfPig throws on `PdfDocument.Open` | Error written via `context.WriteError`; `Run` returns immediately. | +| Metadata field value fails `Contains` check | Error written via `context.WriteError`; other rules continue. | +| Metadata field value fails `Matches` check | Error written via `context.WriteError`; other rules continue. | +| Page count below `Min` or above `Max` | Error written via `context.WriteError`; text rules continue. | +| Body text rule failure | Delegated to `FileAssertRule.Apply`; errors reported individually. | +| Unrecognised metadata field name | Result is `null`; `Contains`/`Matches` run against null. | + +#### Interactions + +- **Caller**: `FileAssertFile.Run` calls `PdfAssert.Run(context, fileName)` when the `pdf:` + assertion block is declared. +- **Created by**: `FileAssertFile.Create` via `FileAssertPdfAssert.Create`. +- **Delegates to**: `FileAssertRule.Apply` for body text validation. +- **OTS dependency**: `PdfPig.PdfDocument` for PDF parsing and text extraction. +- **Configuration dependency**: `FileAssertPdfData`, `FileAssertPdfMetadataRuleData`, + `FileAssertPdfPagesData`, and `FileAssertRuleData` DTOs from the Configuration subsystem. diff --git a/docs/design/file-assert/modeling/file-assert-rule.md b/docs/design/file-assert/modeling/file-assert-rule.md index 4155847..5a4f6fb 100644 --- a/docs/design/file-assert/modeling/file-assert-rule.md +++ b/docs/design/file-assert/modeling/file-assert-rule.md @@ -114,3 +114,47 @@ passed to `FileAssertRule.Create` to produce the concrete rule instance. - **No exception on failure**: Rules report failures via `context.WriteError` rather than throwing, so all rules are applied to all files and all failures are reported in a single run. + +#### Purpose + +`FileAssertRule` defines the abstract rule interface and provides the static factory that +creates the correct concrete implementation from a YAML DTO. Concrete subclasses perform +specific content checks (substring presence/absence and regex match/non-match) against +a file's text content. + +#### Data Model + +| Class | Field | Type | Description | +| :----------------------------- | :--------- | :------ | :---------------------------------------------- | +| `FileAssertContainsRule` | `Value` | `string`| Substring the file content must contain. | +| `FileAssertDoesNotContainRule` | `Value` | `string`| Substring the file content must NOT contain. | +| `FileAssertMatchesRule` | `Pattern` | `Regex` | Compiled regex the file content must match. | +| `FileAssertDoesNotMatchRule` | `Pattern` | `Regex` | Compiled regex the file content must NOT match. | + +Regex objects are compiled at construction with a ten-second evaluation timeout. + +#### Key Methods + +| Method | Purpose | +| :--------------------------------------------------------------------- | :----------------------------------------- | +| `Create(FileAssertRuleData data)` *(static on base)* | Returns concrete rule subclass for `data`. | +| `Apply(Context context, string fileName, string content)` *(abstract)* | Runs rule-specific check on file content. | + +#### Error Handling + +| Scenario | Handling | +| :-------------------------------------------- | :------------------------------------------------------------------- | +| No rule type set in `FileAssertRuleData` | `InvalidOperationException` thrown by `Create`. | +| Invalid regex pattern in `FileAssertRuleData` | `ArgumentException` from `Regex` constructor in `Create`. | +| Regex evaluation timeout (>10 seconds) | `RegexMatchTimeoutException` propagated to `Apply` caller. | +| Rule check fails at `Apply` time | Error written via `context.WriteError`; no exception thrown. | + +#### Interactions + +- **Created by**: + - `FileAssertTextAssert.Create` for text content rules. + - `FileAssertPdfAssert.Create` for PDF body text rules. +- **Called by**: + - `FileAssertTextAssert.Run` — iterates rules and calls `Apply(context, fileName, content)`. + - `FileAssertPdfAssert.Run` — iterates `_text` rules and calls `Apply(context, fileName, pdfText)`. +- **Configuration dependency**: `FileAssertRuleData` DTOs from the Configuration subsystem. diff --git a/docs/design/file-assert/modeling/file-assert-test.md b/docs/design/file-assert/modeling/file-assert-test.md index 596df3b..f6ff6be 100644 --- a/docs/design/file-assert/modeling/file-assert-test.md +++ b/docs/design/file-assert/modeling/file-assert-test.md @@ -73,3 +73,43 @@ tests: casing of test names or tags when running from the command line. - **Empty filter runs all tests**: Following the principle of least surprise, omitting filters from the command line executes the full suite rather than nothing. + +#### Purpose + +`FileAssertTest` represents a single named, tagged test within a FileAssert configuration. +Its single responsibility is to group a set of `FileAssertFile` assertions, evaluate +filter criteria for selective execution, and drive execution of its assertions. + +#### Data Model + +| Property | Type | Description | +| :------- | :------------------------------ | :---------------------------------------------- | +| `Name` | `string` | Required human-readable test identifier. | +| `Tags` | `IReadOnlyList` | Tags used for command-line filter selection. | +| `Files` | `IReadOnlyList` | Ordered file assertions belonging to this test. | + +#### Key Methods + +| Method | Purpose | +| :------------------------------------------- | :----------------------------------------------------------- | +| `Create(FileAssertTestData data)` | Validates `Name`; builds `FileAssertFile` list. | +| `MatchesFilter(IEnumerable filters)` | Returns `true` if filters empty or any matches name or tag. | +| `Run(Context context, string basePath)` | Iterates `Files` and calls `Run(context, basePath)` on each. | + +#### Error Handling + +| Scenario | Handling | +| :------------------------------------ | :--------------------------------------------------- | +| Null `data` passed to `Create` | `ArgumentNullException` thrown. | +| Null or whitespace `Name` in data | `InvalidOperationException` thrown by `Create`. | +| Null `context` or `basePath` in `Run` | `ArgumentNullException` thrown. | +| Individual file assertion failures | Accumulated in `context`; subsequent files continue. | + +#### Interactions + +- **Created by**: `FileAssertConfig.ReadFromFile` via `FileAssertTest.Create` for each + `FileAssertTestData` entry. +- **Called by**: `FileAssertConfig.Run` — calls `MatchesFilter(filterList)` then + `Run(context, basePath)` for each qualifying test. +- **Creates and owns**: `FileAssertFile` instances via `FileAssertFile.Create`. +- **Calls**: `FileAssertFile.Run(context, basePath)` for each file assertion. diff --git a/docs/design/file-assert/modeling/file-assert-text-assert.md b/docs/design/file-assert/modeling/file-assert-text-assert.md index 1745502..95b2a06 100644 --- a/docs/design/file-assert/modeling/file-assert-text-assert.md +++ b/docs/design/file-assert/modeling/file-assert-text-assert.md @@ -83,3 +83,38 @@ files: - **Delegates to `FileAssertRule.Apply`**: Each rule is applied independently via the abstract `Apply` method, so all rule violations in a single file are reported in one pass without short-circuiting on the first failure. + +#### Purpose + +`FileAssertTextAssert` wraps a list of `FileAssertRule` instances and applies them to the +UTF-8 text content of a matched file. It provides the same structural pattern as the other +file-type assert units, keeping `FileAssertFile` free of rule-application logic. + +#### Data Model + +| Property | Type | Description | +| :------- | :------------------------------ | :---------------------------------------- | +| `Rules` | `IReadOnlyList` | Content rules applied to the file's text. | + +#### Key Methods + +| Method | Purpose | +| :--------------------------------------------- | :----------------------------------------------------------------- | +| `Create(IEnumerable data)` | Static factory: creates a `FileAssertRule` for each DTO entry. | +| `Run(Context context, string fileName)` | Reads the file as UTF-8 text and applies each rule to the content. | + +#### Error Handling + +| Scenario | Handling | +| :----------------------------------------------------- | :--------------------------------------------------- | +| Null `data` passed to `Create` | `ArgumentNullException` thrown. | +| `IOException` or `UnauthorizedAccessException` on read | Error via `context.WriteError`; `Run` returns. | +| Individual rule check fails | Error via `context` in `Rule.Apply`; rules continue. | + +#### Interactions + +- **Caller**: `FileAssertFile.Run` calls `TextAssert.Run(context, fileName)` when the `text:` + assertion block is declared. +- **Created by**: `FileAssertFile.Create` via `FileAssertTextAssert.Create`. +- **Delegates to**: `FileAssertRule.Apply` for each content rule. +- **Configuration dependency**: `FileAssertRuleData` DTOs from the Configuration subsystem. diff --git a/docs/design/file-assert/modeling/file-assert-xml-assert.md b/docs/design/file-assert/modeling/file-assert-xml-assert.md index cac4afc..44a7bec 100644 --- a/docs/design/file-assert/modeling/file-assert-xml-assert.md +++ b/docs/design/file-assert/modeling/file-assert-xml-assert.md @@ -83,3 +83,48 @@ files: failure immediately gives users a clear, actionable error message. - **Independent query model**: `FileAssertXmlQuery` is private to this unit so that XML assertion behavior can evolve independently of the other structured-document assert units. + +#### Purpose + +`FileAssertXmlAssert` is responsible for validating one XML file against a list of XPath +queries. It parses the file with `System.Xml.Linq.XDocument` and enforces min, max, and +exact node-count constraints per query. + +#### Data Model + +| Field / Property | Type | Description | +| :--------------- | :---------------------------------- | :-------------------------------------- | +| `Queries` | `IReadOnlyList` | Ordered list of XPath query assertions. | + +Each `FileAssertXmlQuery` (private nested record) holds: + +| Property | Type | Description | +| :------- | :------- | :--------------------------------------- | +| `Query` | `string` | XPath expression to evaluate. | +| `Count` | `int?` | Expected exact node count; `null` = N/A. | +| `Min` | `int?` | Minimum node count; `null` = no bound. | +| `Max` | `int?` | Maximum node count; `null` = no bound. | + +#### Key Methods + +| Method | Purpose | +| :---------------------------------------------- | :------------------------------------------------------------ | +| `Create(IEnumerable data)` | Converts query DTOs to `FileAssertXmlQuery` instances. | +| `Run(Context context, string fileName)` | Loads the XML file and evaluates each XPath query against it. | + +#### Error Handling + +| Scenario | Handling | +| :------------------------------------------ | :-------------------------------------------------------------------- | +| `XDocument.Load` throws on parse failure | Error written via `context.WriteError`; `Run` returns immediately. | +| Query result below `Min` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result above `Max` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result not equal to `Count` | Error written via `context.WriteError`; subsequent queries continue. | + +#### Interactions + +- **Caller**: `FileAssertFile.Run` calls `XmlAssert.Run(context, fileName)` when the `xml:` + assertion block is declared. +- **Created by**: `FileAssertFile.Create` via `FileAssertXmlAssert.Create`. +- **OTS dependency**: `System.Xml.Linq.XDocument` and `System.Xml.XPath` extension methods (BCL). +- **Configuration dependency**: `FileAssertQueryData` DTOs from the Configuration subsystem. diff --git a/docs/design/file-assert/modeling/file-assert-yaml-assert.md b/docs/design/file-assert/modeling/file-assert-yaml-assert.md index 50e1be5..b793d07 100644 --- a/docs/design/file-assert/modeling/file-assert-yaml-assert.md +++ b/docs/design/file-assert/modeling/file-assert-yaml-assert.md @@ -88,3 +88,48 @@ files: the presence and cardinality of sequence keys. - **Independent query model**: `FileAssertYamlQuery` is private to this unit so that YAML assertion behavior can evolve independently of the other structured-document assert units. + +#### Purpose + +`FileAssertYamlAssert` is responsible for validating one YAML file against a list of +dot-notation path queries. It parses the file with YamlDotNet's `YamlStream` and enforces +min, max, and exact node-count constraints per path. + +#### Data Model + +| Field / Property | Type | Description | +| :--------------- | :----------------------------------- | :-------------------------------------------- | +| `Queries` | `IReadOnlyList` | Ordered list of dot-notation path assertions. | + +Each `FileAssertYamlQuery` (private nested record) holds: + +| Property | Type | Description | +| :------- | :------- | :---------------------------------------- | +| `Query` | `string` | Dot-notation path to traverse. | +| `Count` | `int?` | Expected exact node count; `null` = N/A. | +| `Min` | `int?` | Minimum node count; `null` = no bound. | +| `Max` | `int?` | Maximum node count; `null` = no bound. | + +#### Key Methods + +| Method | Purpose | +| :---------------------------------------------- | :--------------------------------------------------------------- | +| `Create(IEnumerable data)` | Converts query DTOs to `FileAssertYamlQuery` instances. | +| `Run(Context context, string fileName)` | Parses the YAML file and evaluates each dot-notation path query. | + +#### Error Handling + +| Scenario | Handling | +| :------------------------------------------ | :-------------------------------------------------------------------- | +| `YamlException` during `YamlStream.Load` | Error written via `context.WriteError`; `Run` returns immediately. | +| Query result below `Min` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result above `Max` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result not equal to `Count` | Error written via `context.WriteError`; subsequent queries continue. | + +#### Interactions + +- **Caller**: `FileAssertFile.Run` calls `YamlAssert.Run(context, fileName)` when the `yaml:` + assertion block is declared. +- **Created by**: `FileAssertFile.Create` via `FileAssertYamlAssert.Create`. +- **OTS dependency**: `YamlDotNet.RepresentationModel.YamlStream` for parsing and traversal. +- **Configuration dependency**: `FileAssertQueryData` DTOs from the Configuration subsystem. diff --git a/docs/design/file-assert/modeling/file-assert-zip-assert.md b/docs/design/file-assert/modeling/file-assert-zip-assert.md index ec783f2..40677be 100644 --- a/docs/design/file-assert/modeling/file-assert-zip-assert.md +++ b/docs/design/file-assert/modeling/file-assert-zip-assert.md @@ -15,17 +15,17 @@ assert units. The `Entry` nested class holds the compiled state for a single entry constraint: -| Property | Type | Description | -| :-------- | :------- | :------------------------------------------------------------------ | -| `Pattern` | `string` | Glob pattern used to match zip entry names. | -| `Min` | `int?` | Minimum number of entries that must match, or null for no bound. | -| `Max` | `int?` | Maximum number of entries that may match, or null for no bound. | +| Property | +| :-------- | +| `Pattern` | +| `Min` | +| `Max` | ##### Properties -| Property | Type | Description | -| :-------- | :-------------------------- | :---------------------------------------------------- | -| `Entries` | `IReadOnlyList` | Entry constraints applied to the zip archive. | +| Property | +| :-------- | +| `Entries` | ##### Factory Method @@ -36,15 +36,15 @@ internal static FileAssertZipAssert Create(FileAssertZipData data) Converts each `FileAssertZipEntryData` DTO into an `Entry` instance after validating that a pattern is specified. -| Parameter | Type | Description | -| :-------- | :-------------------- | :------------------------------------------------------- | -| `data` | `FileAssertZipData` | Zip assertion block data from YAML configuration. | +| Parameter | +| :-------- | +| `data` | -| Return / Exception | Description | -| :--------------------------- | :---------------------------------------------------------- | -| Returns | A new `FileAssertZipAssert` instance. | -| `ArgumentNullException` | Thrown when `data` is null. | -| `InvalidOperationException` | Thrown when any entry does not specify a pattern. | +| Return / Exception | +| :-------------------------- | +| Returns | +| `ArgumentNullException` | +| `InvalidOperationException` | ##### Run Method @@ -81,10 +81,10 @@ Zip '' entry pattern '' matched entry(s), but expected at most ``` -| Parameter | Type | Description | -| :--------- | :-------- | :------------------------------------- | -| `context` | `Context` | Reporting sink used to record errors. | -| `fileName` | `string` | Full path to the zip file to validate. | +| Parameter | +| :--------- | +| `context` | +| `fileName` | #### YAML Configuration @@ -118,3 +118,51 @@ files: - **Immediate failure on parse error**: If the file cannot be opened as a zip archive, an error is written immediately and no entry constraints are evaluated, consistent with the behavior of all other file-type assert units. + +#### Purpose + +`FileAssertZipAssert` is responsible for validating the contents of a zip archive. It enumerates +the archive's file entries, matches them against glob patterns, and enforces min and max +count constraints per pattern. + +#### Data Model + +| Property | +| :-------- | +| `Entries` | + +The `Entry` nested class holds: + +| Property | +| :-------- | +| `Pattern` | +| `Min` | +| `Max` | + +#### Key Methods + +| Method | +| :-------------------------------------- | +| `Create(FileAssertZipData data)` | +| `Run(Context context, string fileName)` | + +#### Error Handling + +| Scenario | +| :-------------------------------------------------------------------------------------------- | +| Null `data` passed to `Create` | +| Entry with null or whitespace pattern in `Create` | +| `IOException`, `InvalidDataException`, or `UnauthorizedAccessException` on `ZipFile.OpenRead` | +| Entry match count below `Min` | +| Entry match count above `Max` | + +#### Interactions + +- **Caller**: `FileAssertFile.Run` calls `ZipAssert.Run(context, fileName)` when the `zip:` + assertion block is declared. +- **Created by**: `FileAssertFile.Create` via `FileAssertZipAssert.Create`. +- **OTS dependencies**: + - `System.IO.Compression.ZipFile` (BCL) for opening the archive. + - `Microsoft.Extensions.FileSystemGlobbing.Matcher` for matching entry names against glob patterns. +- **Configuration dependency**: `FileAssertZipData` and `FileAssertZipEntryData` DTOs from the + Configuration subsystem. diff --git a/docs/design/file-assert/program.md b/docs/design/file-assert/program.md index 8f10e16..18e5138 100644 --- a/docs/design/file-assert/program.md +++ b/docs/design/file-assert/program.md @@ -1,5 +1,15 @@ ## Program Design +### Purpose + +`Program` is the static entry-point class for the FileAssert tool. It constructs the execution +`Context` from command-line arguments, dispatches to the appropriate handler (version display, +help, self-validation, or main tool logic), and returns the final process exit code. + +### Data Model + +N/A – `Program` is a static entry-point class with no instance state. + ### Overview `Program` is the entry point for the FileAssert tool. It owns the `Main` method, constructs a @@ -37,12 +47,12 @@ public static void Run(Context context) Inspects context flags in the following priority order: -| Priority | Condition | Action | -| :------- | :-------------------- | :------------------------------------------ | -| 1 | `context.Version` | Print version string; return. | -| 2 | `context.Help` | Print banner and usage; return. | -| 3 | `context.Validate` | Print banner; delegate to `Validation.Run`. | -| 4 | Default | Print banner; delegate to `RunToolLogic`. | +| Priority | +| :------- | +| 1 | +| 2 | +| 3 | +| 4 | #### RunToolLogic Method @@ -59,11 +69,11 @@ arguments) to `config.Run` so that only matching tests are executed. ### Interactions with Other Units -| Dependency | Usage | -| :------------------ | :------------------------------------------------------------ | -| `Context` | Created by `Context.Create`; owns all I/O and exit code. | -| `Validation` | Invoked by `Run` when `--validate` is set. | -| `FileAssertConfig` | Loaded from file and executed by `RunToolLogic`. | +| Dependency | +| :----------------- | +| `Context` | +| `Validation` | +| `FileAssertConfig` | ### Design Decisions @@ -73,3 +83,21 @@ arguments) to `config.Run` so that only matching tests are executed. expected error conditions; all other exceptions propagate to generate crash reports. - **Version from assembly attribute**: Using `AssemblyInformationalVersionAttribute` allows the CI pipeline to inject the exact package version (including pre-release labels) at build time. + +### Key Methods + +| Method | +| :------------- | +| `Main` | +| `Run` | +| `PrintBanner` | +| `PrintHelp` | +| `RunToolLogic` | + +### Error Handling + +| Exception | +| :-------------------------- | +| `ArgumentException` | +| `InvalidOperationException` | +| All other exceptions | diff --git a/docs/design/file-assert/selftest/validation.md b/docs/design/file-assert/selftest/validation.md index 52bbd4e..ea26fea 100644 --- a/docs/design/file-assert/selftest/validation.md +++ b/docs/design/file-assert/selftest/validation.md @@ -26,13 +26,13 @@ Entry point for self-validation. Executes the following steps: ##### Built-in Tests -| Test Name | Description | -| :-------------------------- | :----------------------------------------------------------------------------------- | -| `FileAssert_VersionDisplay` | Runs `--version`; verifies log contains a version string. | -| `FileAssert_HelpDisplay` | Runs `--help`; verifies log contains `"Usage:"` and `"Options:"`. | -| `FileAssert_Results` | Runs tests with passes and fails; verifies non-zero exit code and results file. | -| `FileAssert_Exists` | Runs a glob-pattern existence check; verifies zero exit code. | -| `FileAssert_Contains` | Runs a text-contains check; verifies zero exit code. | +| Test Name | +| :-------------------------- | +| `FileAssert_VersionDisplay` | +| `FileAssert_HelpDisplay` | +| `FileAssert_Results` | +| `FileAssert_Exists` | +| `FileAssert_Contains` | Each test is dispatched via `RunValidationTest`, which handles the common boilerplate: @@ -92,3 +92,54 @@ directory path under `Path.GetTempPath()`. complete, so the summary reflects the full run. - **`Program.Run` as the test target**: Using the public `Run` method rather than the private `Main` method allows tests to capture the log output without spawning a subprocess. + +#### Purpose + +`Validation` is the self-validation test runner for FileAssert. Its single responsibility is +to exercise the tool's core functionality using built-in test scenarios, print a structured +pass/fail report, and optionally serialize the results to a TRX or JUnit XML file. + +#### Data Model + +N/A — `Validation` is a `static` class with no instance fields. All state is local to the +`Run` method call. The private nested `TemporaryDirectory` class holds a single field: + +| Field | +| :-------------- | +| `DirectoryPath` | + +#### Key Methods + +| Method | +| :--------------------------------------------------------------------------- | +| `Run(Context context)` *(public)* | +| `RunVersionTest(Context, TestResults)` *(private)* | +| `RunHelpTest(Context, TestResults)` *(private)* | +| `RunResultsTest(Context, TestResults)` *(private)* | +| `RunExistsTest(Context, TestResults)` *(private)* | +| `RunContainsTest(Context, TestResults)` *(private)* | +| `RunValidationTest(Context, TestResults, string, Func)` *(private)* | +| `WriteResultsFile(Context, TestResults)` *(private)* | + +#### Error Handling + +| Scenario | +| :-------------------------------------- | +| Null `context` passed to `Run` | +| Test body throws an unhandled exception | +| Unsupported results file extension | +| Results file write failure | +| Temporary directory creation failure | +| Temporary directory deletion failure | + +#### Interactions + +- **Caller**: `Program.Run` calls `Validation.Run(context)` when `context.Validate` is `true`. +- **Calls internally**: + - `Program.Run(Context)` to execute each built-in test scenario in-process. + - `Context.Create(string[])` to construct per-test contexts with `--silent` and `--config`. + - `PathHelpers.SafePathCombine` to build all fixture and log file paths safely. + - `DemaConsulting.TestResults.IO.TrxSerializer.Serialize` and `JUnitSerializer.Serialize` for + results serialization. +- **OTS dependencies**: `System.Runtime.InteropServices.RuntimeInformation` for system info + output; `System.Text.RegularExpressions.Regex` (source-generated) for version string matching. diff --git a/docs/design/file-assert/utilities/path-helpers.md b/docs/design/file-assert/utilities/path-helpers.md index 2796d4a..8c80ef6 100644 --- a/docs/design/file-assert/utilities/path-helpers.md +++ b/docs/design/file-assert/utilities/path-helpers.md @@ -44,3 +44,47 @@ the base directory. identifying `relativePath` as the problematic parameter, making debugging straightforward. - **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. + +#### Purpose + +`PathHelpers` provides a single safe path-combination utility. Its responsibility is to +prevent path-traversal attacks by verifying that the resolved combined path remains within +the specified base directory before returning it. + +#### Data Model + +N/A — `PathHelpers` is a `static` class with no instance state or fields. + +#### Key Methods + +| Method | +| :------------------------------------------------------ | +| `SafePathCombine(string basePath, string relativePath)` | + +**Algorithm:** + +1. Reject null inputs via `ArgumentNullException.ThrowIfNull`. +2. Produce `combinedPath = Path.Combine(basePath, relativePath)`. +3. Resolve both `basePath` and `combinedPath` to absolute form with `Path.GetFullPath`. +4. Compute `Path.GetRelativePath(absoluteBase, absoluteCombined)`. +5. Throw `ArgumentException` if the relative result equals `".."`, starts with `"../"` or + `"..\\"`, or is itself rooted. + +#### Error Handling + +| Scenario | +| :------------------------------------------------------- | +| Null `basePath` or `relativePath` | +| Combined path escapes base directory via `../` traversal | +| Path contains unsupported format | +| Combined or resolved path exceeds system maximum length | + +#### Interactions + +- **Callers**: + - `Validation.TemporaryDirectory` — uses `SafePathCombine(Path.GetTempPath(), guid-name)` to + create a temp directory path. + - `Validation` built-in tests — uses `SafePathCombine(tempDir.DirectoryPath, fileName)` to + build fixture file paths. +- **No internal FileAssert dependencies**: `PathHelpers` is a self-contained utility with no + references to other units in the system. diff --git a/docs/design/introduction.md b/docs/design/introduction.md index 3b991d7..d229f50 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -138,5 +138,5 @@ Review-sets: defined in `.reviewmark.yaml` - [FileAssert User Guide][guide] - [FileAssert Repository][repo] -[guide]: ../../README.md +[guide]: https://github.com/demaconsulting/FileAssert/blob/main/README.md [repo]: https://github.com/demaconsulting/FileAssert diff --git a/docs/design/ots.md b/docs/design/ots.md new file mode 100644 index 0000000..01cd593 --- /dev/null +++ b/docs/design/ots.md @@ -0,0 +1,113 @@ +# OTS Integration Design + +FileAssert relies on ten off-the-shelf (OTS) tools that support CI/CD automation, documentation +generation, and compliance checking. This chapter describes the overall OTS integration strategy +and introduces each tool's role in the pipeline. + +## Integration Strategy + +The OTS items used by this project fall into three functional groups: + +- **Build pipeline tools** — dotnet global tools installed via `.config/dotnet-tools.json` and + invoked by the GitHub Actions workflow. Each tool performs a distinct step such as capturing + build metadata, asserting document correctness, or publishing compliance reports. +- **Test framework** — the xUnit NuGet packages referenced by the test project and consumed + automatically by `dotnet test`. +- **Document generation** — Pandoc converts Markdown to HTML and WeasyPrint converts HTML to + PDF; both are installed as dotnet global tools via `.config/dotnet-tools.json`. + +All dotnet global tools are pinned to specific versions in `.config/dotnet-tools.json` and +restored with `dotnet tool restore` at the beginning of each CI job. This ensures reproducible +builds and provides an audit record of which tool version produced each release artifact. + +## OTS Item Summary + +| OTS Item | Role | +| :---------- | :--------------------------------------------------------------------------- | +| BuildMark | Generates build-notes documentation from GitHub Actions metadata | +| FileAssert | Validates generated HTML and PDF documents against acceptance criteria | +| Pandoc | Converts Markdown source documents to HTML for each document collection | +| ReqStream | Enforces requirements traceability against TRX test-result files | +| ReviewMark | Generates review plan and review report from the review configuration | +| SarifMark | Converts CodeQL SARIF results into a human-readable Markdown report | +| SonarMark | Generates a SonarCloud quality and security metrics report | +| VersionMark | Captures and publishes tool-version information for each CI job | +| WeasyPrint | Converts HTML documents to PDF for release artifact archiving | +| xUnit | Discovers, executes, and reports unit tests; produces TRX output | + +## Per-Item Design + +Detailed design for each OTS item is provided in the following sections of this document: + +- See _BuildMark OTS Design_ for the build-notes generation tool. +- See _FileAssert OTS Design_ for the document assertion tool. +- See _Pandoc OTS Design_ for the Markdown-to-HTML conversion tool. +- See _ReqStream OTS Design_ for the requirements traceability enforcement tool. +- See _ReviewMark OTS Design_ for the review plan and report generation tool. +- See _SarifMark OTS Design_ for the CodeQL SARIF report tool. +- See _SonarMark OTS Design_ for the SonarCloud quality report tool. +- See _VersionMark OTS Design_ for the tool-version capture and publish tool. +- See _WeasyPrint OTS Design_ for the HTML-to-PDF conversion tool. +- See _xUnit OTS Design_ for the unit-testing framework. + +## Selection Criteria + +OTS items are selected for use in this project against the following criteria: + +- **License compatibility**: the item's license must permit use in the build pipeline and, where + applicable, distribution of the produced artifacts, without imposing reciprocal licensing + requirements on this project's source code. +- **Active maintenance**: the item must have recent release activity and an active public issue + tracker, indicating ongoing support and timely security responses. +- **Community adoption**: preference is given to items with broad .NET or general-purpose community + adoption, which reduces abandonment risk and simplifies onboarding for contributors. +- **Security track record**: critical vulnerabilities must be publicly disclosed and patched in a + timely manner by the vendor or maintainer. +- **Functional fit**: the item must address the required capability with a documented API and without + requiring significant wrapper code to adapt its interface. + +## Version Management Policy + +All dotnet global tools are pinned to specific versions in `.config/dotnet-tools.json` and restored +with `dotnet tool restore` at the start of each CI job. This ensures reproducible builds and provides +an audit record of which tool version produced each release artifact. + +OTS items that are NuGet package dependencies are managed via the project file (`*.csproj`). Version +changes are reviewed in pull requests and must be accompanied by a passing build and full test suite +before merging. + +Version numbers are not recorded in design documentation — they are managed in SBOMs and the tool +manifest outside of the design artifact set. + +## General Integration Approach + +OTS items in this project fall into two integration categories: + +- **dotnet global tools** (BuildMark, FileAssert, Pandoc, ReqStream, ReviewMark, SarifMark, + SonarMark, VersionMark, WeasyPrint) — installed globally in the CI environment via + `dotnet tool restore` from `.config/dotnet-tools.json` and invoked as command-line executables + within GitHub Actions workflow steps. No wrapper code is written; tools are invoked directly + with documented command-line flags. A non-zero exit code from any tool step causes the CI job + to fail immediately, consistent with the GitHub Actions default `fail-fast` behaviour. +- **NuGet package** (xUnit) — referenced in the test project file and consumed through standard + .NET package restore. No explicit initialisation or configuration code is required beyond the + test-project target framework declaration. + +Error handling across all tool invocations relies on exit-code contracts documented in each tool's +own user guide. No custom error-recovery logic is applied. + +## Qualification Strategy + +OTS items are qualified for use in this project through a combination of the following approaches: + +- **Vendor self-validation**: all OTS tools publish their own test suites and release notes. Vendor + evidence is reviewed before a major version upgrade is accepted into this project. +- **Pipeline integration tests**: for OTS tools whose outputs are consumed by downstream steps + (e.g., Pandoc HTML output validated by FileAssert assertions, ReqStream traceability checked + against TRX results), the CI pipeline acts as an integration harness — a tool that produces + incorrect output will cause a downstream step to fail. +- **Local unit tests** (xUnit): the xUnit framework is implicitly qualified by the passing of all + unit and integration tests in each CI run. A test framework that cannot discover, execute, or + report tests correctly would cause the CI pipeline to fail before any artifacts are published. +- **Release-note review**: before accepting any OTS version upgrade, the team reviews the vendor + release notes for breaking changes, deprecations, and security advisories. diff --git a/docs/design/ots/buildmark.md b/docs/design/ots/buildmark.md new file mode 100644 index 0000000..ab8dafb --- /dev/null +++ b/docs/design/ots/buildmark.md @@ -0,0 +1,51 @@ +## BuildMark OTS Design + +DemaConsulting.BuildMark is a .NET dotnet global tool that queries the GitHub Actions API to +capture workflow run details and renders them as a Markdown build-notes document included in +the release artifacts. + +### Purpose + +BuildMark provides an automated build-notes report for each CI pipeline run. It captures the +GitHub Actions workflow run details — including the workflow name, run number, trigger, and +associated commit — and renders them as a Markdown document. This document is compiled into the +Build Notes PDF artifact, giving reviewers a permanent record of the build provenance for each +release. + +BuildMark is chosen because it integrates directly with the GitHub Actions event context, +requiring no manual input, and produces Markdown that is compatible with the Pandoc pipeline +already used for all other document collections. + +### Integration + +BuildMark is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the +package name `demaconsulting.buildmark` and restored with `dotnet tool restore`. The tool is +invoked in the CI pipeline's build-docs job with the `--output` argument pointing to the +generated Markdown path. The generated file is placed in +`docs/build_notes/generated/build_notes.md`, which Pandoc incorporates into the Build Notes +HTML document. Version constraint: `1.1.0` (pinned in `.config/dotnet-tools.json`). + +### Configuration + +BuildMark requires the GitHub Actions environment variables `GITHUB_TOKEN`, `GITHUB_RUN_ID`, +`GITHUB_REPOSITORY`, and `GITHUB_SERVER_URL`, which GitHub Actions provides automatically in +every job. No additional configuration files are used; all options are supplied as command-line +arguments. + +### Interfaces + +The project uses the following BuildMark command-line interface: + +| Invocation | Effect | +| :------------------------------------------ | :---------------------------------------------------------------- | +| `dotnet buildmark --output ` | Queries the GitHub API and writes a Markdown build-notes document | + +The output Markdown file is the sole artifact consumed downstream. No programmatic API or SDK +is used; all interaction occurs through the command-line interface. + +### Dependencies + +BuildMark operates as an isolated tool process. Its internal dependencies do not propagate to +the main source project or the published NuGet package. The tool requires network access to the +GitHub REST API during the CI job that invokes it. The generated Markdown file has no runtime +dependency on BuildMark after it is produced. diff --git a/docs/design/ots/fileassert.md b/docs/design/ots/fileassert.md new file mode 100644 index 0000000..7cf5d99 --- /dev/null +++ b/docs/design/ots/fileassert.md @@ -0,0 +1,54 @@ +## FileAssert OTS Design + +DemaConsulting.FileAssert is the tool developed by this project and is also consumed as an OTS +item within its own CI pipeline to validate the correctness of the documents it generates and to +produce self-validation evidence. + +### Purpose + +FileAssert validates HTML and PDF documents produced during the build pipeline, asserting that +each document exists, has a non-trivial size, is structurally valid, and contains expected +content. Its built-in self-validation suite (`--validate`) is run as a CI step to produce TRX +test evidence satisfying the `FileAssert-OTS-FileAssert` requirement. + +FileAssert is chosen because it directly implements the project's own document-assertion +capability, making its CI use a natural dogfooding exercise that simultaneously validates the +tool and provides compliance evidence for the document pipeline. + +### Integration + +FileAssert is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the +package name `demaconsulting.fileassert` and restored with `dotnet tool restore`. The CI +pipeline uses it in two ways: + +- **Self-validation**: `fileassert --validate --results artifacts/fileassert-self-validation.trx` + runs the built-in self-validation suite and writes TRX output for ReqStream consumption. +- **Document assertion**: `fileassert --config --results ` validates specific + generated HTML and PDF files throughout the pipeline using YAML configuration files. + +Version constraint: `0.3.0` (pinned in `.config/dotnet-tools.json`). + +### Configuration + +When used for document validation, FileAssert reads `.fileassert.yaml` configuration files that +define the glob patterns and acceptance criteria for each document type. These YAML files are +checked in alongside the documents they validate. When run with `--validate`, no configuration +file is required; the built-in test suite is used. + +### Interfaces + +The project uses the following FileAssert command-line interfaces: + +| Invocation | Effect | +| :----------------------------------------------------------- | :------------------------------------------------- | +| `dotnet fileassert --validate --results ` | Runs self-validation and writes TRX results | +| `dotnet fileassert --config --results ` | Runs document assertions from a YAML configuration | + +The TRX output from `--results` is consumed by ReqStream to satisfy `FileAssert-OTS-FileAssert`. + +### Dependencies + +FileAssert operates as an isolated tool process. Its NuGet dependencies — PdfPig, +HtmlAgilityPack, YamlDotNet, Microsoft.Extensions.FileSystemGlobbing, and +DemaConsulting.TestResults — are bundled within the tool and do not affect the main project's +dependency graph or the published NuGet package. diff --git a/docs/design/ots/pandoc.md b/docs/design/ots/pandoc.md new file mode 100644 index 0000000..ebea2d2 --- /dev/null +++ b/docs/design/ots/pandoc.md @@ -0,0 +1,53 @@ +## Pandoc OTS Design + +DemaConsulting.PandocTool is a .NET dotnet global tool wrapper around the Pandoc document +converter. It converts Markdown source documents into HTML as part of the documentation build +pipeline, producing the intermediate output that WeasyPrint then renders to PDF. + +### Purpose + +Pandoc converts the ordered set of Markdown content files for each document collection into a +single HTML document using a project-specific HTML template. The HTML output is then rendered +to PDF by WeasyPrint. Pandoc is used for all document collections: Build Notes, Code Quality, +Review Plan, Review Report, Design, Verification, and User Guide. + +Pandoc is chosen because it handles multi-file document assembly natively, supports a custom +HTML template for consistent styling, and produces numbered sections and table-of-contents +entries without additional tooling. + +### Integration + +Pandoc is installed as a .NET local tool via the package `demaconsulting.pandoctool` in +`.config/dotnet-tools.json` and restored with `dotnet tool restore`. The tool is invoked as +`dotnet pandoc` with a `definition.yaml` argument that lists the input Markdown files, template, +and output path. Each document collection provides its own `definition.yaml`. +Version constraint: `3.9.0.2` (pinned in `.config/dotnet-tools.json`). + +### Configuration + +Each document collection provides a `definition.yaml` file at `docs/{collection}/definition.yaml` +that specifies: + +- `resource-path` — directories containing the HTML template and CSS assets +- `input-files` — ordered list of Markdown source files to concatenate +- `template` — the shared `template.html` located in `docs/template/` +- `table-of-contents: true` — generates a navigation table of contents +- `number-sections: true` — produces numbered headings in the output HTML + +### Interfaces + +The project uses the following Pandoc command-line interface: + +| Invocation | +| :--------------------------------------------- | +| `dotnet pandoc --definition ` | + +The generated HTML file is placed in `docs/{collection}/generated/{collection}.html` and is +passed directly to WeasyPrint for PDF conversion. + +### Dependencies + +The PandocTool wrapper bundles the Pandoc executable internally; no separate Pandoc installation +is required. The project supplies a `docs/template/` directory containing the shared +`template.html` and CSS assets used by all document collections. No additional runtime +dependencies are required beyond the tool installation. diff --git a/docs/design/ots/reqstream.md b/docs/design/ots/reqstream.md new file mode 100644 index 0000000..b06d091 --- /dev/null +++ b/docs/design/ots/reqstream.md @@ -0,0 +1,52 @@ +## ReqStream OTS Design + +DemaConsulting.ReqStream is a .NET dotnet global tool that processes requirements YAML files and +TRX test-result files to generate requirements reports and enforce that every requirement is +linked to at least one passing test. + +### Purpose + +ReqStream reads the project's `requirements.yaml` root file — which includes all subsystem and +OTS requirements YAML files — together with the TRX files accumulated by the CI pipeline, and +produces three generated Markdown reports: a requirements report, a justifications document, and +a traceability matrix. When invoked with `--enforce`, ReqStream exits with a non-zero exit code +if any requirement lacks at least one passing test result, making incomplete traceability a +build-breaking condition. + +ReqStream is chosen because it understands the project's YAML requirements format natively, +processes TRX files directly, and integrates with the `--enforce` flag as a quality gate without +requiring a separate CI plugin or external service. + +### Integration + +ReqStream is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the +package name `demaconsulting.reqstream` and restored with `dotnet tool restore`. The CI pipeline +invokes it in the build-docs job after all test and self-validation TRX files have been +accumulated as workflow artifacts. Version constraint: `1.9.0` (pinned in +`.config/dotnet-tools.json`). + +### Configuration + +ReqStream is configured through the `requirements.yaml` root file, which uses an `includes:` +list to compose all subsystem and OTS requirements YAML files. Each YAML file defines a +hierarchy of sections and requirements where each requirement has an `id`, `title`, +`justification`, `tags`, and a list of `tests` that must pass. Requirements files are located +under `docs/reqstream/`. + +### Interfaces + +The project uses the following ReqStream command-line interface: + +| Invocation | Effect | +| :------------------------------------------------------------------- | :--------------------------------------------- | +| `dotnet reqstream --enforce ` | Generates reports; fails if coverage is broken | + +The three generated Markdown files (`requirements.md`, `justifications.md`, `trace_matrix.md`) +are written to `docs/requirements/generated/` and consumed by Pandoc to produce the +Requirements PDF. + +### Dependencies + +ReqStream has no transitive NuGet dependencies that propagate to the main source project. It +requires TRX files produced by `dotnet test --logger trx` and by `fileassert --results`. The +tool operates entirely at the file system level and requires no network access. diff --git a/docs/design/ots/reviewmark.md b/docs/design/ots/reviewmark.md new file mode 100644 index 0000000..2c477f0 --- /dev/null +++ b/docs/design/ots/reviewmark.md @@ -0,0 +1,55 @@ +## ReviewMark OTS Design + +DemaConsulting.ReviewMark is a .NET dotnet global tool that reads a review configuration and +evidence store to generate a review plan and review report documenting formal file review +coverage. + +### Purpose + +ReviewMark provides continuous compliance evidence for formal code review. It reads the +`.reviewmark.yaml` configuration, which defines review-sets (named groups of files that must be +reviewed together), and the review evidence store to produce two Markdown documents: a review +plan that lists all files included in review-sets, and a review report that records which files +have been reviewed, by whom, and when. + +ReviewMark is chosen because it integrates directly with the repository-level review evidence +pattern used by this project's Continuous Compliance methodology and produces Markdown output +compatible with the Pandoc pipeline. + +### Integration + +ReviewMark is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the +package name `demaconsulting.reviewmark` and restored with `dotnet tool restore`. The CI +pipeline invokes ReviewMark in two separate steps: + +- `dotnet reviewmark --plan docs/code_review_plan/generated/plan.md` — generates the review plan +- `dotnet reviewmark --report docs/code_review_report/generated/report.md` — generates the review + report + +Version constraint: `1.2.0` (pinned in `.config/dotnet-tools.json`). + +### Configuration + +ReviewMark reads its configuration from `.reviewmark.yaml` at the repository root. This file +defines review-sets, each consisting of a name, description, and a list of file glob patterns +identifying the files that belong to the set. The review evidence store, which contains committed +review records in the repository, provides the historical evidence that ReviewMark uses to +determine which files have been reviewed and when. + +### Interfaces + +The project uses the following ReviewMark command-line interfaces: + +| Invocation | Effect | +| :------------------------------------------------- | :---------------------------------------- | +| `dotnet reviewmark --plan ` | Generates a Markdown review plan | +| `dotnet reviewmark --report ` | Generates a Markdown review report | + +Both generated Markdown files are consumed by Pandoc to produce the Review Plan PDF and the +Review Report PDF, respectively. + +### Dependencies + +ReviewMark has no transitive NuGet dependencies that propagate to the main source project. It +reads the `.reviewmark.yaml` configuration and the repository's review evidence directory. No +network access is required. diff --git a/docs/design/ots/sarifmark.md b/docs/design/ots/sarifmark.md new file mode 100644 index 0000000..33d8dda --- /dev/null +++ b/docs/design/ots/sarifmark.md @@ -0,0 +1,48 @@ +## SarifMark OTS Design + +DemaConsulting.SarifMark is a .NET dotnet global tool that reads SARIF (Static Analysis Results +Interchange Format) files produced by CodeQL code scanning and renders them as a human-readable +Markdown report included in the Code Quality PDF artifact. + +### Purpose + +SarifMark converts the CodeQL SARIF output into a Markdown document that provides a persistent, +human-readable record of any security findings identified by CodeQL for each release. This +document is compiled into the Code Quality PDF artifact alongside the SonarCloud quality report, +giving reviewers a unified view of static analysis results. + +SarifMark is chosen because it understands the SARIF format natively and produces Markdown +output compatible with the Pandoc pipeline, requiring no custom scripting to transform CodeQL +output into the document format. + +### Integration + +SarifMark is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the +package name `demaconsulting.sarifmark` and restored with `dotnet tool restore`. The CI pipeline +invokes SarifMark in the build-docs job after the CodeQL scanning step has completed and the +SARIF file has been downloaded as a workflow artifact. Version constraint: `1.3.2` (pinned in +`.config/dotnet-tools.json`). + +### Configuration + +SarifMark is configured entirely through command-line arguments: the SARIF input file path and +the output Markdown file path. No additional configuration files are required. The SARIF file is +produced by the `github/codeql-action/analyze` GitHub Actions step, downloaded as a workflow +artifact, and passed directly to SarifMark. + +### Interfaces + +The project uses the following SarifMark command-line interface: + +| Invocation | Effect | +| :----------------------------------------------------------- | :------------------------------------------- | +| `dotnet sarifmark --sarif --output ` | Converts SARIF results to a Markdown report | + +The generated Markdown file is written to `docs/code_quality/generated/codeql-quality.md` and +consumed by Pandoc to produce the Code Quality HTML and subsequently the Code Quality PDF. + +### Dependencies + +SarifMark has no transitive NuGet dependencies that propagate to the main source project. It +requires the SARIF file produced by CodeQL as input and produces a standalone Markdown file as +output. No network access is required. diff --git a/docs/design/ots/sonarmark.md b/docs/design/ots/sonarmark.md new file mode 100644 index 0000000..25c5b56 --- /dev/null +++ b/docs/design/ots/sonarmark.md @@ -0,0 +1,48 @@ +## SonarMark OTS Design + +DemaConsulting.SonarMark is a .NET dotnet global tool that retrieves quality-gate status, +issues, and security hot-spots from the SonarCloud API and renders them as a Markdown report +included in the Code Quality PDF artifact. + +### Purpose + +SonarMark surfaces the SonarCloud quality-gate result and detailed metrics — issues, code +smells, coverage, duplications, and security hot-spots — as a Markdown document for inclusion +in the Code Quality PDF. This gives reviewers a persistent, artifact-bound quality snapshot for +each release, independent of the SonarCloud web dashboard. + +SonarMark is chosen because it integrates with the SonarCloud REST API to retrieve data that +is not available from the CI runner's standard output, and it produces Markdown output +compatible with the Pandoc pipeline. + +### Integration + +SonarMark is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the +package name `demaconsulting.sonarmark` and restored with `dotnet tool restore`. The CI pipeline +invokes SonarMark in the build-docs job after the SonarCloud analysis has completed. A +`SONAR_TOKEN` secret is provided to the job for authenticated API access. Version constraint: +`1.5.0` (pinned in `.config/dotnet-tools.json`). + +### Configuration + +SonarMark is configured entirely through command-line arguments: the SonarCloud project key and +the output Markdown file path. The SonarCloud project key for this project is +`demaconsulting_FileAssert`. Authentication is supplied via the `SONAR_TOKEN` environment +variable injected by the CI job. No additional configuration files are required. + +### Interfaces + +The project uses the following SonarMark command-line interface: + +| Invocation | Effect | +| :-------------------------------------------------------------- | :---------------------------------------------- | +| `dotnet sonarmark --project --output ` | Queries SonarCloud and writes a Markdown report | + +The generated Markdown file is written to `docs/code_quality/generated/sonar-quality.md` and +consumed by Pandoc to produce the Code Quality HTML and subsequently the Code Quality PDF. + +### Dependencies + +SonarMark requires authenticated network access to the SonarCloud REST API at +`sonarcloud.io`. The `SONAR_TOKEN` secret must be available as an environment variable. SonarMark +has no transitive NuGet dependencies that propagate to the main source project. diff --git a/docs/design/ots/versionmark.md b/docs/design/ots/versionmark.md new file mode 100644 index 0000000..35777a8 --- /dev/null +++ b/docs/design/ots/versionmark.md @@ -0,0 +1,57 @@ +## VersionMark OTS Design + +DemaConsulting.VersionMark is a .NET dotnet global tool that captures installed tool version +information during each CI job and publishes it as a Markdown document included in the Build +Notes PDF artifact. + +### Purpose + +VersionMark provides an audit record of the exact tool versions used to produce each release. +Each CI job captures the versions of the dotnet tools and runtime components it uses; the +build-docs job then merges all captured data and publishes a Markdown document included in the +Build Notes artifact. This supports reproducibility and compliance traceability for all build +tools. + +VersionMark is chosen because it operates as a local dotnet tool alongside the other pipeline +tools, requires no external service, and produces Markdown output compatible with the Pandoc +pipeline. + +### Integration + +VersionMark is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the +package name `demaconsulting.versionmark` and restored with `dotnet tool restore`. It is used +in two modes in the CI pipeline: + +- **Capture mode** (each CI job): invoked with `--capture --job-id --output ` to + interrogate installed tool versions and write a JSON file to the artifacts folder. +- **Publish mode** (build-docs job): invoked with `--publish --output ` to + merge all captured JSON files and write the consolidated Markdown versions document. +- **Self-validation**: invoked with `--validate --results ` to run the built-in + validation suite and write TRX evidence for ReqStream consumption. + +Version constraint: `1.3.0` (pinned in `.config/dotnet-tools.json`). + +### Configuration + +VersionMark is configured entirely through command-line arguments. In capture mode, `--job-id` +identifies the CI job and `--output` specifies the JSON output path. In publish mode, the input +JSON files and the output Markdown path are provided as positional and named arguments +respectively. No external configuration files are required. + +### Interfaces + +The project uses the following VersionMark command-line interfaces: + +| Invocation | Effect | +| :----------------------------------------------------------------------- | :------------------------------------------- | +| `dotnet versionmark --capture --job-id --output ` | Captures tool versions to a JSON file | +| `dotnet versionmark --publish --output ` | Publishes merged version info as Markdown | +| `dotnet versionmark --validate --results ` | Runs self-validation and writes TRX evidence | + +The generated Markdown file is included in the Build Notes document and consumed by Pandoc. + +### Dependencies + +VersionMark has no transitive NuGet dependencies that propagate to the main source project. It +reads installed tool metadata from the local environment and writes JSON and Markdown files. No +network access is required. diff --git a/docs/design/ots/weasyprint.md b/docs/design/ots/weasyprint.md new file mode 100644 index 0000000..8c4b612 --- /dev/null +++ b/docs/design/ots/weasyprint.md @@ -0,0 +1,54 @@ +## WeasyPrint OTS Design + +DemaConsulting.WeasyPrintTool is a .NET dotnet global tool wrapper around the WeasyPrint Python +library. It converts HTML documents produced by Pandoc into PDF artifacts for release +distribution and compliance archiving. + +### Purpose + +WeasyPrint renders the HTML output produced by Pandoc into PDF documents for all document +collections: Build Notes, Code Quality, Review Plan, Review Report, Design, Verification, and +User Guide. PDF output enables FileAssert to assert PDF metadata and content during CI +validation, and provides a format suitable for long-term archivability and distribution to +reviewers. + +WeasyPrint is chosen because it applies CSS-based page layout, supports custom fonts and headers +via the shared `docs/template/` CSS, and is available as a dotnet global tool wrapper that +integrates with the existing pipeline without requiring a separate Python invocation step. + +### Integration + +WeasyPrint is installed as a .NET local tool via the package `demaconsulting.weasyprinttool` in +`.config/dotnet-tools.json` and restored with `dotnet tool restore`. The tool is invoked as +`dotnet weasyprint` with the input HTML path and the output PDF path for each document +collection. The CI workflow installs Python via `actions/setup-python` to satisfy the +WeasyPrintTool's internal Python dependency. Version constraint: `68.1.0` (pinned in +`.config/dotnet-tools.json`). + +### Configuration + +WeasyPrint is configured entirely through command-line arguments: the input HTML file path and +the output PDF file path. The HTML files produced by Pandoc embed the shared CSS from +`docs/template/`, which WeasyPrint uses to apply page layout, fonts, and document styling. No +separate WeasyPrint configuration files are used in this project. + +### Interfaces + +The project uses the following WeasyPrint command-line interface: + +| Invocation | +| :---------------------------------------------------------- | +| `dotnet weasyprint --input --output ` | + +The generated PDF files are validated by FileAssert assertions (`WeasyPrint_BuildNotesPdf`, +`WeasyPrint_CodeQualityPdf`, `WeasyPrint_ReviewPlanPdf`, `WeasyPrint_ReviewReportPdf`, +`WeasyPrint_DesignPdf`, `WeasyPrint_VerificationPdf`, `WeasyPrint_UserGuidePdf`) and uploaded +as release artifacts. + +### Dependencies + +WeasyPrintTool requires Python to be available in the CI environment. The GitHub Actions +workflow installs Python via `actions/setup-python`. The WeasyPrint Python library and its +CSS rendering dependencies (Pango, Cairo, fontconfig) must be present in the runner image. +No WeasyPrint NuGet dependencies are propagated to the main source project or the published +NuGet package. diff --git a/docs/design/ots/xunit.md b/docs/design/ots/xunit.md new file mode 100644 index 0000000..45e42e9 --- /dev/null +++ b/docs/design/ots/xunit.md @@ -0,0 +1,65 @@ +## xUnit OTS Design + +xUnit is the .NET unit-testing framework used by the FileAssert test project. It provides test +discovery, execution, and result reporting including TRX output for requirements traceability. + +### Purpose + +xUnit discovers all test methods annotated with `[Fact]` in the test project, executes them, and +reports pass/fail results. The xunit.runner.visualstudio adapter generates TRX result files that +ReqStream consumes to verify requirements coverage. Passing tests provide continuous traceability +evidence that FileAssert's functional requirements are implemented correctly. + +xUnit v3 is chosen because it provides a modern, self-contained test runner with +`OutputType: Exe` support for .NET 8/9/10, strong assertion APIs, and the +`xunit.runner.visualstudio` adapter for TRX output format that ReqStream requires. + +### Integration + +xUnit is integrated via NuGet package references in the test project +(`DemaConsulting.FileAssert.Tests.csproj`): + +- `xunit.v3` version `3.2.2` — the core test framework providing `[Fact]`, assertions, and + test runner infrastructure for .NET 8, 9, and 10. +- `xunit.runner.visualstudio` version `3.1.5` — the Visual Studio and `dotnet test` adapter + that enables TRX result file output. + +Tests are executed by `dotnet test` with the `--logger trx;LogFileName=.trx` argument to +produce TRX files for ReqStream. The test project targets `net8.0`, `net9.0`, and `net10.0` +matching the supported runtime targets of the main project. + +### Configuration + +xUnit behavior is controlled through `dotnet test` command-line arguments. The test project +is configured with: + +- `OutputType: Exe` — required for xUnit v3 self-contained test executables. +- `IsTestProject: true` — marks the project for MSBuild and the .NET test SDK. +- `TreatWarningsAsErrors: true` — enforces code quality at compile time. + +No `xunit.runner.json` file is required; default discovery and execution settings are used. +`Microsoft.NET.Test.Sdk` version `18.5.1` provides the test SDK integration layer. + +### Interfaces + +xUnit exposes the following APIs consumed by the project: + +| API | Usage | +| :----------------------------------- | :------------------------------------------------------------- | +| `[Fact]` attribute | Marks a method as a test case for discovery and execution | +| `[Collection]` attribute | Groups tests that share a fixture or must not run in parallel | +| `Assert.Equal`, `Assert.True`, etc. | Assertion methods used throughout all test methods | +| `dotnet test --logger trx` | Produces TRX output consumed by ReqStream | + +### Dependencies + +xUnit brings the following dependencies into the test project: + +- `xunit.v3.core` — the test execution engine and assertion library. +- `xunit.v3.common` — shared abstractions used by the xUnit framework. +- `xunit.runner.visualstudio` — the `dotnet test` integration adapter. +- `Microsoft.NET.Test.Sdk` version `18.5.1` — the test SDK integration layer. + +All xUnit and runner dependencies are scoped to the test project via `PrivateAssets` settings +and do not propagate to the main `DemaConsulting.FileAssert` project or its NuGet package +consumers. diff --git a/docs/reqstream/file-assert.yaml b/docs/reqstream/file-assert.yaml index f2f58d8..9047b5b 100644 --- a/docs/reqstream/file-assert.yaml +++ b/docs/reqstream/file-assert.yaml @@ -255,6 +255,8 @@ sections: Text content assertions allow users to verify that files contain required strings, do not contain forbidden strings, or match regular expression patterns. These assertions are the primary mechanism for content validation in CI/CD pipelines. + The supported assertion types are: `contains`, `does-not-contain`, `matches` + (regex), and `does-not-match` (regex). children: - FileAssert-Modeling-FileTypeParsing tests: @@ -276,6 +278,7 @@ sections: - FileAssert-Modeling-FileTypeParsing tests: - IntegrationTest_PdfAssert_InvalidFile_ReturnsNonZero + - IntegrationTest_PdfAssert_FailingAssertion_ReturnsNonZero - id: FileAssert-System-XmlAssertions title: | @@ -309,6 +312,7 @@ sections: - FileAssert-Modeling-QueryAssertions tests: - IntegrationTest_HtmlAssert_PassingQuery_ReturnsZero + - IntegrationTest_HtmlAssert_InvalidFile_ReturnsNonZero - id: FileAssert-System-YamlAssertions title: | @@ -325,6 +329,7 @@ sections: - FileAssert-Modeling-QueryAssertions tests: - IntegrationTest_YamlAssert_PassingQuery_ReturnsZero + - IntegrationTest_YamlAssert_InvalidFile_ReturnsNonZero - id: FileAssert-System-JsonAssertions title: | @@ -341,6 +346,7 @@ sections: - FileAssert-Modeling-QueryAssertions tests: - IntegrationTest_JsonAssert_PassingQuery_ReturnsZero + - IntegrationTest_JsonAssert_InvalidFile_ReturnsNonZero - id: FileAssert-System-ZipAssertions title: | @@ -356,5 +362,48 @@ sections: - FileAssert-Modeling-FileTypeParsing - FileAssert-FileAssertZipAssert-EntryMatching tests: - - FileAssertZipAssert_Run_MatchingEntriesMeetConstraints_NoError - - FileAssertZipAssert_Run_TooFewMatchingEntries_WritesError + - IntegrationTest_ZipAssert_PassingQuery_ReturnsZero + - IntegrationTest_ZipAssert_InvalidFile_ReturnsNonZero + + - id: FileAssert-System-DefaultBehavior + title: | + The FileAssert tool shall display version and copyright information when invoked + with no recognized flags. + justification: | + Displaying the application banner on default invocation follows standard CLI conventions + and confirms to users that the tool is installed and running correctly. + children: + - FileAssert-Program-DefaultBehavior + tests: + - IntegrationTest_ValidateFlag_RunsValidation + + - id: FileAssert-System-DepthFlag + title: | + The FileAssert tool shall accept a --depth argument in the range 1-6 that controls + the heading level of self-validation output, defaulting to 1. + justification: | + The --depth flag allows users to embed self-validation output at an appropriate + heading level within existing Markdown documents. A default of 1 ensures top-level + output without explicit configuration. + children: + - FileAssert-Context-Depth + tests: + - IntegrationTest_ValidateFlag_RunsValidation + + - id: FileAssert-System-MultiPlatform + title: | + The FileAssert tool shall run correctly on Windows, Linux, and macOS operating systems. + justification: | + CI/CD pipelines operate across heterogeneous environments. Supporting all three major + platforms ensures the tool can be used consistently regardless of the build agent OS. + tests: + - IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero + + - id: FileAssert-System-MultiRuntime + title: | + The FileAssert tool shall run correctly on .NET 8, .NET 9, and .NET 10 runtimes. + justification: | + Supporting multiple .NET runtimes allows projects targeting different LTS and current + versions to adopt FileAssert without being forced to upgrade their runtime. + tests: + - IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero diff --git a/docs/reqstream/file-assert/cli.yaml b/docs/reqstream/file-assert/cli.yaml index fd4de2b..7fe8896 100644 --- a/docs/reqstream/file-assert/cli.yaml +++ b/docs/reqstream/file-assert/cli.yaml @@ -9,26 +9,45 @@ sections: - title: Cli Subsystem Requirements requirements: - id: FileAssert-Cli-ArgumentParsing - title: The Cli subsystem shall parse command-line arguments and expose them as structured properties. + title: The Cli subsystem shall parse command-line arguments into recognized flags and values. justification: | A dedicated argument-parsing subsystem provides a single point of truth for all command-line interface logic, keeping the rest of the tool free of raw argument handling. It also enables consistent unit and integration testing of argument behavior independently of business logic. + children: + - FileAssert-Context-ArgumentParsing + - FileAssert-Context-InvalidArgs tests: - Cli_CreateContext_ParsesSilentValidateAndLogFlags - Cli_CreateContext_ParsesVersionHelpConfigResultsFlags - - Cli_CreateContext_WithFilters_ParsesPositionalArguments - Cli_CreateContext_UnknownArgument_ThrowsArgumentException + - id: FileAssert-Cli-ArgumentExposure + title: The Cli subsystem shall expose parsed argument values as typed properties on the Context object. + justification: | + Exposing parsed arguments as typed properties provides a strongly-typed interface + for downstream subsystems, eliminating repeated string parsing and making argument + access self-documenting. + children: + - FileAssert-Context-ConfigFile + - FileAssert-Context-Filters + - FileAssert-Context-Depth + tests: + - Cli_CreateContext_ParsesSilentValidateAndLogFlags + - Cli_CreateContext_ParsesVersionHelpConfigResultsFlags + - Cli_CreateContext_WithFilters_ParsesPositionalArguments + - id: FileAssert-Cli-ErrorReporting title: The Cli subsystem shall set the exit code to 1 when errors are reported. justification: | A non-zero exit code is the standard Unix convention for indicating failure. Setting the exit code to 1 as soon as any error is written ensures that CI/CD pipelines and calling scripts can reliably detect failures. + children: + - FileAssert-Context-ExitCode tests: - - Cli_WriteError_ChangesExitCodeToOne + - Cli_WriteError_AfterSuccessfulCreate_ChangesExitCodeToOne - id: FileAssert-Cli-OutputPipeline title: The Cli subsystem shall route output and errors to the console and optional log file. @@ -36,5 +55,10 @@ sections: A unified output pipeline ensures that all messages, whether informational or error-level, are consistently written to both the console and any active log file. This provides a reliable audit trail for automated environments and CI/CD pipelines. + children: + - FileAssert-Context-Output + - FileAssert-Context-Silent + - FileAssert-Context-ErrorOutput tests: - - Cli_OutputPipeline_WritesMessagesToLogFile + - Cli_OutputPipeline_WithLogPathAndSilentFlag_WritesMessagesToLogFile + - Cli_OutputPipeline_WithoutSilentFlag_WritesMessagesToConsole diff --git a/docs/reqstream/file-assert/configuration.yaml b/docs/reqstream/file-assert/configuration.yaml index d0fb878..7328433 100644 --- a/docs/reqstream/file-assert/configuration.yaml +++ b/docs/reqstream/file-assert/configuration.yaml @@ -17,6 +17,9 @@ sections: the Modeling subsystem can execute them. Verifying this end-to-end pipeline at the subsystem level ensures that the deserialization and domain-object construction stages interact correctly. + children: + - FileAssert-FileAssertConfig-ReadFromFile + - FileAssert-FileAssertData-YamlSchema tests: - Configuration_LoadYaml_BuildsCompleteTestHierarchy @@ -26,6 +29,8 @@ sections: Selective test execution based on names and tags is a core feature of the Configuration subsystem. Testing this capability at the subsystem level verifies that filtering works correctly across the full load-and-run pipeline. + children: + - FileAssert-FileAssertConfig-Run tests: - Configuration_RunWithFilter_ExecutesOnlyMatchingTests - Configuration_RunWithTagFilter_ExecutesOnlyMatchingTests diff --git a/docs/reqstream/file-assert/configuration/file-assert-data.yaml b/docs/reqstream/file-assert/configuration/file-assert-data.yaml index 3f523a8..71db537 100644 --- a/docs/reqstream/file-assert/configuration/file-assert-data.yaml +++ b/docs/reqstream/file-assert/configuration/file-assert-data.yaml @@ -20,19 +20,3 @@ sections: tests: - FileAssertConfig_ReadFromFile_ValidFile_ReturnsConfig - FileAssertConfig_ReadFromFile_PdfAssertConfig_ParsesCorrectly - - - id: FileAssert-FileAssertData-NoBusinessLogic - title: | - The FileAssertData classes shall contain no validation or business logic, delegating - all validation to the factory methods in the Modeling subsystem. - justification: | - Keeping DTOs free of business logic maintains a clean separation between - deserialization (Configuration subsystem) and domain object construction (Modeling - subsystem). This makes the DTOs simple, predictable, and easy to test indirectly - through the classes that consume them. The test FileAssertConfig_ReadFromFile_ValidFile_ReturnsConfig - sufficiently covers this requirement by verifying that the DTO is transparently - consumed by FileAssertConfig.ReadFromFile without imposing any constraints of its - own — if the DTO contained validation logic, the round-trip deserialization would - either throw or silently discard data, which would cause this test to fail. - tests: - - FileAssertConfig_ReadFromFile_ValidFile_ReturnsConfig diff --git a/docs/reqstream/file-assert/modeling/file-assert-file.yaml b/docs/reqstream/file-assert/modeling/file-assert-file.yaml index f27dafe..56c79c7 100644 --- a/docs/reqstream/file-assert/modeling/file-assert-file.yaml +++ b/docs/reqstream/file-assert/modeling/file-assert-file.yaml @@ -72,10 +72,8 @@ sections: - id: FileAssert-FileAssertFile-FileTypeAssertDelegation title: | - The FileAssertFile class shall delegate per-file assertions to the appropriate - file-type assert unit (FileAssertTextAssert, FileAssertPdfAssert, FileAssertXmlAssert, - FileAssertHtmlAssert, FileAssertYamlAssert, or FileAssertJsonAssert) when the - corresponding assertion block is declared. + The FileAssertFile class shall evaluate the file-type-specific assertion + block configured for each matched file. justification: | Delegating file-type assertions to dedicated units keeps FileAssertFile free of format-specific parsing and assertion logic, making each unit independently testable diff --git a/docs/reqstream/file-assert/modeling/file-assert-html-assert.yaml b/docs/reqstream/file-assert/modeling/file-assert-html-assert.yaml index 2850a1d..59479b2 100644 --- a/docs/reqstream/file-assert/modeling/file-assert-html-assert.yaml +++ b/docs/reqstream/file-assert/modeling/file-assert-html-assert.yaml @@ -17,12 +17,12 @@ sections: - FileAssertHtmlAssert_Create_NullData_ThrowsArgumentNullException - id: FileAssert-FileAssertHtmlAssert-ParseError - title: | + title: >- The FileAssertHtmlAssert class shall report an immediate error and skip remaining - assertions when a matched file cannot be parsed as a valid HTML document. + assertions when a matched file cannot be read (e.g., file does not exist or is inaccessible). justification: | - Attempting to evaluate XPath queries against a file that cannot be parsed as HTML - would produce meaningless or misleading results. Reporting the parse failure + HtmlAgilityPack parses HTML leniently without throwing exceptions. The primary error + condition handled is a missing or inaccessible file. Reporting the I/O failure immediately gives users a clear, actionable error message. tests: - FileAssertHtmlAssert_Run_NonExistentFile_WritesError diff --git a/docs/reqstream/file-assert/selftest.yaml b/docs/reqstream/file-assert/selftest.yaml index 7667073..5e80c05 100644 --- a/docs/reqstream/file-assert/selftest.yaml +++ b/docs/reqstream/file-assert/selftest.yaml @@ -15,6 +15,14 @@ sections: subsystems to produce a complete self-validation report. Testing the full pipeline at the subsystem level verifies that test execution, result collection, and summary output all work together correctly. + children: + - FileAssert-Validation-Run + - FileAssert-Validation-Results + - FileAssert-Validation-ResultsTest + - FileAssert-Validation-ExistsTest + - FileAssert-Validation-ContainsTest + - FileAssert-Validation-NullContext + - FileAssert-Validation-Depth tests: - SelfTest_Run_ExecutesBuiltInTestsAndProducesSummary - SelfTest_Run_WhenInvoked_PrintsSystemInfoHeader diff --git a/docs/reqstream/file-assert/utilities.yaml b/docs/reqstream/file-assert/utilities.yaml index c91dc6d..4a80600 100644 --- a/docs/reqstream/file-assert/utilities.yaml +++ b/docs/reqstream/file-assert/utilities.yaml @@ -15,5 +15,8 @@ sections: that they can be verified in isolation and reused across the codebase without duplicating path-safety logic. Testing at the subsystem level confirms that the utility functions are accessible to and correctly used by dependent subsystems. + children: + - FileAssert-PathHelpers-SafeCombine + - FileAssert-PathHelpers-NullValidation tests: - Utilities_SafePathCombine_PreventsPathTraversalToFileSystem diff --git a/docs/requirements_doc/introduction.md b/docs/requirements_doc/introduction.md index ba780be..bcd9563 100644 --- a/docs/requirements_doc/introduction.md +++ b/docs/requirements_doc/introduction.md @@ -20,6 +20,10 @@ This requirements document covers: - Documentation generation - CI/CD integration +## References + +- [DemaConsulting.FileAssert releases](https://github.com/demaconsulting/FileAssert/releases) + ## Audience This document is intended for: diff --git a/docs/requirements_report/introduction.md b/docs/requirements_report/introduction.md index f45aeef..c7fa09a 100644 --- a/docs/requirements_report/introduction.md +++ b/docs/requirements_report/introduction.md @@ -25,3 +25,7 @@ This document is intended for: - Quality assurance teams validating requirements coverage - Project stakeholders reviewing test coverage - Auditors verifying requirements traceability + +## References + +- [DemaConsulting.FileAssert releases](https://github.com/demaconsulting/FileAssert/releases) diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 9406fcc..d42306d 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -1,5 +1,7 @@ # Introduction +This guide describes how to install, configure, and use FileAssert. + ## Purpose FileAssert is a .NET CLI tool for asserting file properties using YAML-defined test suites. It validates @@ -16,6 +18,11 @@ This user guide covers: - Command-line options reference - Practical examples for various scenarios +## References + +- [DemaConsulting.FileAssert releases](https://github.com/demaconsulting/FileAssert/releases) +- [Continuous Compliance](https://github.com/demaconsulting/ContinuousCompliance) + # Continuous Compliance This tool follows the @@ -253,6 +260,16 @@ tests: json: - query: "ConnectionStrings" count: 1 + + - name: TestProject_PackageValid + # Zip archive contains required artifacts + tags: [release] + files: + - pattern: "output/package.zip" + zip: + entries: + - pattern: "**/*.dll" + min: 1 ``` ## Acceptance Criteria Reference @@ -299,6 +316,10 @@ tests: | `json[].count` | Exact number of matched JSON nodes | | `json[].min` | Minimum number of matched JSON nodes | | `json[].max` | Maximum number of matched JSON nodes | +| `zip:` | Zip archive assertion block | +| `zip.entries[].pattern` | Glob pattern matching zip entry names | +| `zip.entries[].min` | Minimum number of matching entries | +| `zip.entries[].max` | Maximum number of matching entries | # Command-Line Options diff --git a/docs/verification/file-assert.md b/docs/verification/file-assert.md index 47b77d5..87127d4 100644 --- a/docs/verification/file-assert.md +++ b/docs/verification/file-assert.md @@ -38,6 +38,19 @@ multi-platform requirements: All integration test scenarios are expected to produce identical results on all supported runtime and platform combinations. +## Acceptance Criteria + +The system-level verification is considered complete and passing when: + +1. All integration test scenarios defined in `IntegrationTests.cs` execute and report + a passing result in the CI pipeline across all supported runtime and platform combinations + listed in the Test Environments table. +2. No integration test exits with an unexpected exit code or fails an assertion. +3. The `--validate` flag integration test passes, confirming that the installed tool + is self-consistent. + +Failure of any single integration test scenario constitutes a system-level acceptance failure. + ## External Interface Simulation At the system level, no interfaces are mocked. All external interfaces are exercised with real @@ -237,6 +250,37 @@ fewer matching entries than required. **Expected**: Exit code non-zero. +### IntegrationTest_ZipAssert_PassingQuery_ReturnsZero + +**Scenario**: A zip assertion is configured and the archive contains entries that satisfy +the declared constraints. + +**Expected**: Exit code 0. + +### IntegrationTest_ZipAssert_InvalidFile_ReturnsNonZero + +**Scenario**: A zip assertion is configured but the target file is not a valid zip archive. + +**Expected**: Exit code non-zero. + +### IntegrationTest_HtmlAssert_InvalidFile_ReturnsNonZero + +**Scenario**: An HTML assertion is configured but the target file is not valid HTML. + +**Expected**: Exit code non-zero. + +### IntegrationTest_YamlAssert_InvalidFile_ReturnsNonZero + +**Scenario**: A YAML assertion is configured but the target file is not valid YAML. + +**Expected**: Exit code non-zero. + +### IntegrationTest_JsonAssert_InvalidFile_ReturnsNonZero + +**Scenario**: A JSON assertion is configured but the target file is not valid JSON. + +**Expected**: Exit code non-zero. + ## Requirements Coverage - **Version display**: IntegrationTest_VersionFlag_OutputsVersion @@ -264,8 +308,13 @@ fewer matching entries than required. - **Structured file assertions**: IntegrationTest_XmlAssert_PassingQuery_ReturnsZero, IntegrationTest_XmlAssert_InvalidFile_ReturnsNonZero, IntegrationTest_HtmlAssert_PassingQuery_ReturnsZero, + IntegrationTest_HtmlAssert_InvalidFile_ReturnsNonZero, IntegrationTest_YamlAssert_PassingQuery_ReturnsZero, + IntegrationTest_YamlAssert_InvalidFile_ReturnsNonZero, IntegrationTest_JsonAssert_PassingQuery_ReturnsZero, + IntegrationTest_JsonAssert_InvalidFile_ReturnsNonZero, IntegrationTest_PdfAssert_InvalidFile_ReturnsNonZero - **Zip archive assertions**: FileAssertZipAssert_Run_MatchingEntriesMeetConstraints_NoError, - FileAssertZipAssert_Run_TooFewMatchingEntries_WritesError + FileAssertZipAssert_Run_TooFewMatchingEntries_WritesError, + IntegrationTest_ZipAssert_PassingQuery_ReturnsZero, + IntegrationTest_ZipAssert_InvalidFile_ReturnsNonZero diff --git a/docs/verification/file-assert/cli.md b/docs/verification/file-assert/cli.md index 346a761..3a4654f 100644 --- a/docs/verification/file-assert/cli.md +++ b/docs/verification/file-assert/cli.md @@ -21,6 +21,19 @@ not reach `Validation` at all. No mocking is applied at this level; all collaborators within and directly adjacent to the subsystem use their real implementations. +### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner against +the .NET runtime specified by the build matrix. No special hardware, peripherals, or +environment configuration is required beyond the standard build toolchain. + +### Acceptance Criteria + +The Cli subsystem verification passes when all test scenarios listed in +this document execute and pass in the CI pipeline without any test failures, unexpected +exceptions, or assertion errors. Each named scenario must pass on all supported runtime +and platform combinations. + ### Integration Test Scenarios The following integration test scenarios are defined in `CliTests.cs`. @@ -51,24 +64,32 @@ through `Context.Create`. **Expected**: An `ArgumentException` is thrown. -#### Cli_WriteError_ChangesExitCodeToOne +#### Cli_WriteError_AfterSuccessfulCreate_ChangesExitCodeToOne **Scenario**: `Context.WriteError` is called with an error message. **Expected**: `ExitCode` becomes 1 after the call. -#### Cli_OutputPipeline_WritesMessagesToLogFile +#### Cli_OutputPipeline_WithLogPathAndSilentFlag_WritesMessagesToLogFile **Scenario**: A context with both `--silent` and `--log ` flags is created; `Context.WriteLine` is called with a message. **Expected**: The message appears in the log file; exit code is 0. +#### Cli_OutputPipeline_WithoutSilentFlag_WritesMessagesToConsole + +**Scenario**: A context without the `--silent` flag is created; `Context.WriteLine` is +called with a message. + +**Expected**: The message appears on standard output; exit code is 0. + ### Requirements Coverage - **Argument parsing**: Cli_CreateContext_ParsesSilentValidateAndLogFlags, Cli_CreateContext_ParsesVersionHelpConfigResultsFlags, Cli_CreateContext_WithFilters_ParsesPositionalArguments - **Unknown argument rejection**: Cli_CreateContext_UnknownArgument_ThrowsArgumentException -- **Error exit code**: Cli_WriteError_ChangesExitCodeToOne -- **Log file output**: Cli_OutputPipeline_WritesMessagesToLogFile +- **Error exit code**: Cli_WriteError_AfterSuccessfulCreate_ChangesExitCodeToOne +- **Log file output**: Cli_OutputPipeline_WithLogPathAndSilentFlag_WritesMessagesToLogFile +- **Console output**: Cli_OutputPipeline_WithoutSilentFlag_WritesMessagesToConsole diff --git a/docs/verification/file-assert/cli/context.md b/docs/verification/file-assert/cli/context.md index bfe137d..ceb40cd 100644 --- a/docs/verification/file-assert/cli/context.md +++ b/docs/verification/file-assert/cli/context.md @@ -11,6 +11,17 @@ test scenarios, dependency usage, and requirement coverage for `Cli/Context.cs`. `Context.Create` with controlled argument arrays, inspect the resulting properties and exit codes, and verify output written to captured streams. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies `Context` has no dependencies on other tool units. All dependencies are real .NET BCL types; diff --git a/docs/verification/file-assert/configuration.md b/docs/verification/file-assert/configuration.md index 6c40417..b7bcc20 100644 --- a/docs/verification/file-assert/configuration.md +++ b/docs/verification/file-assert/configuration.md @@ -16,6 +16,19 @@ All collaborators at the subsystem boundary (`Context`, `FileAssertConfig`, `Pat their real implementations. Temporary directories are used for configuration files and test artifacts so that tests remain isolated and leave no permanent file-system side-effects. +### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner against +the .NET runtime specified by the build matrix. No special hardware, peripherals, or +environment configuration is required beyond the standard build toolchain. + +### Acceptance Criteria + +The Configuration subsystem verification passes when all test scenarios listed in +this document execute and pass in the CI pipeline without any test failures, unexpected +exceptions, or assertion errors. Each named scenario must pass on all supported runtime +and platform combinations. + ### Integration Test Scenarios The following integration test scenarios are defined in `ConfigurationTests.cs`. diff --git a/docs/verification/file-assert/configuration/file-assert-config.md b/docs/verification/file-assert/configuration/file-assert-config.md index f15a49c..4e6b8b6 100644 --- a/docs/verification/file-assert/configuration/file-assert-config.md +++ b/docs/verification/file-assert/configuration/file-assert-config.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for YAML configuration files in temporary directories and assert on the resulting object state, exit codes, and results files. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/configuration/file-assert-data.md b/docs/verification/file-assert/configuration/file-assert-data.md index cd86e21..899d22c 100644 --- a/docs/verification/file-assert/configuration/file-assert-data.md +++ b/docs/verification/file-assert/configuration/file-assert-data.md @@ -10,6 +10,17 @@ carry no logic and are exercised indirectly through `FileAssertConfig.ReadFromFi `FileAssertConfigTests.cs`. No dedicated test file exists; all coverage is inherited from the `FileAssertConfig` tests. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies `FileAssertData` depends only on YamlDotNet deserialization annotations. No mocking is needed. @@ -23,4 +34,4 @@ fields confirms the data-transfer objects are correctly annotated and deserializ #### Requirements Coverage All `FileAssertData` requirements are satisfied indirectly by the `FileAssertConfig` test -scenarios. See [FileAssertConfig Verification](file-assert-config.md) for details. +scenarios. See the FileAssertConfig Verification document for details. diff --git a/docs/verification/file-assert/modeling.md b/docs/verification/file-assert/modeling.md index 78ebaf7..3dd80b5 100644 --- a/docs/verification/file-assert/modeling.md +++ b/docs/verification/file-assert/modeling.md @@ -15,6 +15,19 @@ evaluating constraints, and reporting results through a real `Context`. All collaborators at the subsystem boundary use their real implementations. Temporary directories are used for test files so that tests remain isolated. +### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner against +the .NET runtime specified by the build matrix. No special hardware, peripherals, or +environment configuration is required beyond the standard build toolchain. + +### Acceptance Criteria + +The Modeling subsystem verification passes when all test scenarios listed in +this document execute and pass in the CI pipeline without any test failures, unexpected +exceptions, or assertion errors. Each named scenario must pass on all supported runtime +and platform combinations. + ### Integration Test Scenarios The following integration test scenarios are defined in `ModelingTests.cs`. diff --git a/docs/verification/file-assert/modeling/file-assert-file.md b/docs/verification/file-assert/modeling/file-assert-file.md index b818e98..fd3b041 100644 --- a/docs/verification/file-assert/modeling/file-assert-file.md +++ b/docs/verification/file-assert/modeling/file-assert-file.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for temporary directories with controlled file sets and assert on constraint evaluation and error reporting behavior. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/modeling/file-assert-html-assert.md b/docs/verification/file-assert/modeling/file-assert-html-assert.md index 0c9b480..2dc8ad9 100644 --- a/docs/verification/file-assert/modeling/file-assert-html-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-html-assert.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for create temporary HTML files with controlled content and assert on XPath query results, count constraints, and text matching. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/modeling/file-assert-json-assert.md b/docs/verification/file-assert/modeling/file-assert-json-assert.md index 4132098..b49e9e4 100644 --- a/docs/verification/file-assert/modeling/file-assert-json-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-json-assert.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for create temporary JSON files with controlled content and assert on path query results and count constraints. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/modeling/file-assert-pdf-assert.md b/docs/verification/file-assert/modeling/file-assert-pdf-assert.md index 3abc879..2fd228a 100644 --- a/docs/verification/file-assert/modeling/file-assert-pdf-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-pdf-assert.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for use PDF files in test fixtures and assert on page-count constraints, metadata field assertions, and text content assertions. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/modeling/file-assert-rule.md b/docs/verification/file-assert/modeling/file-assert-rule.md index 26d30f7..3e12c03 100644 --- a/docs/verification/file-assert/modeling/file-assert-rule.md +++ b/docs/verification/file-assert/modeling/file-assert-rule.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for controlled rule data objects and string content, asserting on rule type creation, application results, and error reporting. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/modeling/file-assert-test.md b/docs/verification/file-assert/modeling/file-assert-test.md index 0ccaa06..1747918 100644 --- a/docs/verification/file-assert/modeling/file-assert-test.md +++ b/docs/verification/file-assert/modeling/file-assert-test.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for controlled `FileAssertTestData` instances and assert on filter matching behavior, creation validation, and execution delegation. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/modeling/file-assert-text-assert.md b/docs/verification/file-assert/modeling/file-assert-text-assert.md index 9787941..7046751 100644 --- a/docs/verification/file-assert/modeling/file-assert-text-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-text-assert.md @@ -9,6 +9,17 @@ defines the test scenarios, dependency usage, and requirement coverage for `FileAssertTextAssert` is verified with unit tests defined in `FileAssertTextAssertTests.cs`. Tests create temporary files with controlled content and assert on rule evaluation and error reporting. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/modeling/file-assert-xml-assert.md b/docs/verification/file-assert/modeling/file-assert-xml-assert.md index ad5ccb9..2fd49f0 100644 --- a/docs/verification/file-assert/modeling/file-assert-xml-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-xml-assert.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for create temporary XML files with controlled content and assert on XPath query results, count constraints, and text matching. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/modeling/file-assert-yaml-assert.md b/docs/verification/file-assert/modeling/file-assert-yaml-assert.md index 87a80d7..9de3f05 100644 --- a/docs/verification/file-assert/modeling/file-assert-yaml-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-yaml-assert.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for create temporary YAML files with controlled content and assert on path query results and count constraints. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/modeling/file-assert-zip-assert.md b/docs/verification/file-assert/modeling/file-assert-zip-assert.md index 82bc1b3..8110c1a 100644 --- a/docs/verification/file-assert/modeling/file-assert-zip-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-zip-assert.md @@ -10,6 +10,17 @@ defines the test scenarios, dependency usage, and requirement coverage for create actual zip archives in a temporary file using `System.IO.Compression.ZipFile`, then invoke `FileAssertZipAssert.Run` and assert on the resulting context state. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/program.md b/docs/verification/file-assert/program.md index 064febc..e1ee898 100644 --- a/docs/verification/file-assert/program.md +++ b/docs/verification/file-assert/program.md @@ -19,6 +19,19 @@ codes. No test doubles are introduced at the `Program` level; all collaborators execute their real logic. +### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner against +the .NET runtime specified by the build matrix. No special hardware, peripherals, or +environment configuration is required beyond the standard build toolchain. + +### Acceptance Criteria + +The Program subsystem verification passes when all test scenarios listed in +this document execute and pass in the CI pipeline without any test failures, unexpected +exceptions, or assertion errors. Each named scenario must pass on all supported runtime +and platform combinations. + ### Test Scenarios #### Program_Run_WithVersionFlag_DisplaysVersionOnly diff --git a/docs/verification/file-assert/selftest.md b/docs/verification/file-assert/selftest.md index 14349c8..af60ab7 100644 --- a/docs/verification/file-assert/selftest.md +++ b/docs/verification/file-assert/selftest.md @@ -16,6 +16,19 @@ At the subsystem boundary, `Context` (from the `Cli` subsystem) and `PathHelpers `Utilities` subsystem) are used with their real implementations. No mocking is applied. Temporary directories are used for result file output so that tests remain isolated. +### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner against +the .NET runtime specified by the build matrix. No special hardware, peripherals, or +environment configuration is required beyond the standard build toolchain. + +### Acceptance Criteria + +The Selftest subsystem verification passes when all test scenarios listed in +this document execute and pass in the CI pipeline without any test failures, unexpected +exceptions, or assertion errors. Each named scenario must pass on all supported runtime +and platform combinations. + ### Integration Test Scenarios The following integration test scenarios are defined in `SelfTestTests.cs`. diff --git a/docs/verification/file-assert/selftest/validation.md b/docs/verification/file-assert/selftest/validation.md index 072a4a4..d845a2d 100644 --- a/docs/verification/file-assert/selftest/validation.md +++ b/docs/verification/file-assert/selftest/validation.md @@ -10,6 +10,17 @@ the test scenarios, dependency usage, and requirement coverage for `SelfTest/Val content, and result files. Temporary directories are used for result file paths to keep tests isolated. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies | Dependency | Usage in Tests | diff --git a/docs/verification/file-assert/utilities.md b/docs/verification/file-assert/utilities.md index 8a959a1..53fed09 100644 --- a/docs/verification/file-assert/utilities.md +++ b/docs/verification/file-assert/utilities.md @@ -15,6 +15,19 @@ valid paths are resolved correctly and traversal attacks are rejected. `PathHelpers` depends only on .NET BCL types for path manipulation. No external dependencies require mocking at the subsystem level. +### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner against +the .NET runtime specified by the build matrix. No special hardware, peripherals, or +environment configuration is required beyond the standard build toolchain. + +### Acceptance Criteria + +The Utilities subsystem verification passes when all test scenarios listed in +this document execute and pass in the CI pipeline without any test failures, unexpected +exceptions, or assertion errors. Each named scenario must pass on all supported runtime +and platform combinations. + ### Integration Test Scenarios The following integration test scenarios are defined in `UtilitiesTests.cs`. diff --git a/docs/verification/file-assert/utilities/path-helpers.md b/docs/verification/file-assert/utilities/path-helpers.md index 600a521..b73f36b 100644 --- a/docs/verification/file-assert/utilities/path-helpers.md +++ b/docs/verification/file-assert/utilities/path-helpers.md @@ -10,6 +10,17 @@ performs pure path manipulation using only .NET BCL types, no mocking or test do Tests call `PathHelpers.SafePathCombine` directly with controlled base and relative path arguments and assert on the returned string or the thrown exception. +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. No +special hardware, peripherals, or environment configuration is required. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + #### Dependencies `PathHelpers` has no dependencies on other tool units. All path operations use .NET BCL types diff --git a/docs/verification/introduction.md b/docs/verification/introduction.md index 835d487..192302c 100644 --- a/docs/verification/introduction.md +++ b/docs/verification/introduction.md @@ -33,6 +33,7 @@ This document covers the verification design for the same software items describ - **FileAssertHtmlAssert** — HTML XPath query assertions - **FileAssertYamlAssert** — YAML path query assertions - **FileAssertJsonAssert** — JSON path query assertions + - **FileAssertZipAssert** — Zip archive entry assertions - **Utilities** — shared utility subsystem - **PathHelpers** — safe path combination utilities - **SelfTest** — self-validation subsystem @@ -77,7 +78,8 @@ FileAssert (System) │ ├── FileAssertXmlAssert (Unit) │ ├── FileAssertHtmlAssert (Unit) │ ├── FileAssertYamlAssert (Unit) -│ └── FileAssertJsonAssert (Unit) +│ ├── FileAssertJsonAssert (Unit) +│ └── FileAssertZipAssert (Unit) ├── Utilities (Subsystem) │ └── PathHelpers (Unit) └── SelfTest (Subsystem) @@ -112,3 +114,8 @@ OTS items have parallel artifacts in: - Verification: `docs/verification/ots/{ots-name}.md` Review-sets: defined in `.reviewmark.yaml` + +## References + +- [FileAssert releases](https://github.com/demaconsulting/FileAssert/releases) — compiled release + artifacts for FileAssert, including the Verification Design Document PDF diff --git a/docs/verification/ots.md b/docs/verification/ots.md index 667ea79..9bd45c8 100644 --- a/docs/verification/ots.md +++ b/docs/verification/ots.md @@ -5,6 +5,57 @@ the FileAssert build pipeline. OTS items are third-party tools and frameworks th in-house; each is verified by demonstrating that it performs its required functionality within the FileAssert CI pipeline. +## Verification Strategy + +OTS software items used by FileAssert are verified by one of two complementary approaches, +depending on the nature of the item: + +- **Authored integration tests**: Items whose correct behaviour cannot be inferred purely from + pipeline success (xUnit, FileAssert) are verified by named test scenarios that exercise the + specific features required by this project. Each scenario identifies a test method, the expected + outcome, and the requirement it covers. + +- **CI pipeline evidence**: Items whose primary function is to produce artefacts consumed by + downstream pipeline steps (BuildMark, Pandoc, ReqStream, ReviewMark, SarifMark, SonarMark, + VersionMark, WeasyPrint) are verified by a chain of transitive evidence: the item executes in + CI, produces its expected output, and a subsequent step (typically a FileAssert assertion or a + ReqStream enforce check) fails the build if the output is absent or malformed. A passing + pipeline run therefore constitutes evidence that each item performed its required function. + +Per-item verification details, test scenarios, and requirements mappings are documented in the +individual files under `docs/verification/ots/`. + +## Qualification Evidence + +The following evidence is collected and retained for each OTS item: + +- **xUnit**: TRX result files generated by `dotnet test` during CI. Each TRX file is consumed by + `reqstream --enforce` to confirm that all named test scenarios have a recorded passing result. + +- **FileAssert**: The self-validation TRX file (`artifacts/fileassert-self-validation.trx`) + produced by `fileassert --validate --results` during CI. This file is also consumed by + `reqstream --enforce`. + +- **All other OTS items**: A passing CI pipeline run, including Pandoc HTML generation, WeasyPrint + PDF rendering, FileAssert assertions on every generated document, and `reqstream --enforce` with + no unmet requirements. The combined CI log and artefacts constitute the qualification record. + +All CI artefacts are retained as GitHub Actions run artefacts and are accessible from the +FileAssert releases page. + +## Regression Approach + +When an OTS item is updated to a new version, the following steps apply: + +1. Update the version reference in the tool manifest or project file. +2. Run the full CI pipeline. All authored integration tests and FileAssert assertions must pass. +3. Confirm that `reqstream --enforce` reports no unmet requirements. +4. If any step fails, investigate whether the version change introduced a breaking change affecting + the features used by this project, and resolve before merging the update. + +No additional re-qualification steps are required beyond a clean CI pipeline run, because all +verification scenarios exercise the tool at the integration boundary used in production. + ## OTS Items The following OTS software items are used by FileAssert and are verified in this section: @@ -38,3 +89,9 @@ The following OTS software items are used by FileAssert and are verified in this - **xUnit** — discovers and executes unit tests and writes TRX result files; verified by self-validation test scenarios that confirm test discovery, execution, and reporting. + +## Acceptance Criteria + +OTS item verification passes when all self-validation runs and integration test scenarios +that exercise OTS functionality complete without failure across the supported runtime and +platform combinations. diff --git a/docs/verification/ots/buildmark.md b/docs/verification/ots/buildmark.md index 021562a..1866b24 100644 --- a/docs/verification/ots/buildmark.md +++ b/docs/verification/ots/buildmark.md @@ -32,3 +32,9 @@ in the release artifacts. ### Requirements Coverage - **`FileAssert-OTS-BuildMark`**: BuildMark_MarkdownReportGeneration + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/docs/verification/ots/fileassert.md b/docs/verification/ots/fileassert.md index 3f707e1..dfeea68 100644 --- a/docs/verification/ots/fileassert.md +++ b/docs/verification/ots/fileassert.md @@ -43,3 +43,9 @@ pipeline. ### Requirements Coverage - **`FileAssert-OTS-FileAssert`**: FileAssert_VersionDisplay, FileAssert_HelpDisplay + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/docs/verification/ots/pandoc.md b/docs/verification/ots/pandoc.md index 8e6d904..87649ac 100644 --- a/docs/verification/ots/pandoc.md +++ b/docs/verification/ots/pandoc.md @@ -85,3 +85,9 @@ a valid HTML title element, and includes expected document content. - **`FileAssert-OTS-Pandoc`**: Pandoc_BuildNotesHtml, Pandoc_CodeQualityHtml, Pandoc_ReviewPlanHtml, Pandoc_ReviewReportHtml, Pandoc_DesignHtml, Pandoc_VerificationHtml, Pandoc_UserGuideHtml + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/docs/verification/ots/reqstream.md b/docs/verification/ots/reqstream.md index 95e57a4..bb4dea5 100644 --- a/docs/verification/ots/reqstream.md +++ b/docs/verification/ots/reqstream.md @@ -34,3 +34,9 @@ generated. ### Requirements Coverage - **`FileAssert-OTS-ReqStream`**: ReqStream_EnforcementMode + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/docs/verification/ots/reviewmark.md b/docs/verification/ots/reviewmark.md index 37c5445..165e07a 100644 --- a/docs/verification/ots/reviewmark.md +++ b/docs/verification/ots/reviewmark.md @@ -42,3 +42,9 @@ evidence that ReviewMark did not produce the required review documents. ### Requirements Coverage - **`FileAssert-OTS-ReviewMark`**: ReviewMark_ReviewPlanGeneration, ReviewMark_ReviewReportGeneration + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/docs/verification/ots/sarifmark.md b/docs/verification/ots/sarifmark.md index fe21c1c..b2f7fa0 100644 --- a/docs/verification/ots/sarifmark.md +++ b/docs/verification/ots/sarifmark.md @@ -40,3 +40,9 @@ artifacts. ### Requirements Coverage - **`FileAssert-OTS-SarifMark`**: SarifMark_SarifReading, SarifMark_MarkdownReportGeneration + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/docs/verification/ots/sonarmark.md b/docs/verification/ots/sonarmark.md index 2e775d1..91a091d 100644 --- a/docs/verification/ots/sonarmark.md +++ b/docs/verification/ots/sonarmark.md @@ -56,3 +56,9 @@ failure at any step is evidence that SonarMark did not retrieve and render quali - **`FileAssert-OTS-SonarMark`**: SonarMark_QualityGateRetrieval, SonarMark_IssuesRetrieval, SonarMark_HotSpotsRetrieval, SonarMark_MarkdownReportGeneration + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/docs/verification/ots/versionmark.md b/docs/verification/ots/versionmark.md index b0b1710..985fe59 100644 --- a/docs/verification/ots/versionmark.md +++ b/docs/verification/ots/versionmark.md @@ -40,3 +40,9 @@ evidence that VersionMark did not execute correctly. ### Requirements Coverage - **`FileAssert-OTS-VersionMark`**: VersionMark_CapturesVersions, VersionMark_GeneratesMarkdownReport + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/docs/verification/ots/weasyprint.md b/docs/verification/ots/weasyprint.md index e08edb1..0672203 100644 --- a/docs/verification/ots/weasyprint.md +++ b/docs/verification/ots/weasyprint.md @@ -87,3 +87,9 @@ at least one page, and includes expected document content. - **`FileAssert-OTS-WeasyPrint`**: WeasyPrint_BuildNotesPdf, WeasyPrint_CodeQualityPdf, WeasyPrint_ReviewPlanPdf, WeasyPrint_ReviewReportPdf, WeasyPrint_DesignPdf, WeasyPrint_VerificationPdf, WeasyPrint_UserGuidePdf + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/docs/verification/ots/xunit.md b/docs/verification/ots/xunit.md index 9cb5fa6..a0fcd3d 100644 --- a/docs/verification/ots/xunit.md +++ b/docs/verification/ots/xunit.md @@ -101,3 +101,9 @@ summary even when the context is configured for silent operation. Context_Create_LogFlag_OpensLogFile, Context_Create_UnknownArgument_ThrowsArgumentException, PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly, Program_Run_WithVersionFlag_DisplaysVersionOnly, Validation_Run_WithSilentContext_PrintsSummary + +### Acceptance Criteria + +N/A – Acceptance criteria are managed at the system integration level. This OTS item is +considered verified when the integration test scenarios that exercise its functionality +pass in the CI pipeline. diff --git a/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs b/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs index b8ce265..bdf4c51 100644 --- a/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs @@ -123,10 +123,10 @@ public void Cli_CreateContext_UnknownArgument_ThrowsArgumentException() } /// - /// Verifies that WriteError changes the context exit code from 0 to 1. + /// Verifies that WriteError changes the context exit code from 0 to 1 after a successful Create. /// [Fact] - public void Cli_WriteError_ChangesExitCodeToOne() + public void Cli_WriteError_AfterSuccessfulCreate_ChangesExitCodeToOne() { // Arrange using var context = Context.Create(["--silent"]); @@ -139,12 +139,47 @@ public void Cli_WriteError_ChangesExitCodeToOne() Assert.Equal(1, context.ExitCode); } + /// + /// Verifies that WriteLine and WriteError messages reach the console when --silent is absent. + /// + [Fact] + public void Cli_OutputPipeline_WithoutSilentFlag_WritesMessagesToConsole() + { + // Arrange + var originalOut = Console.Out; + var originalError = Console.Error; + var outWriter = new System.IO.StringWriter(); + var errorWriter = new System.IO.StringWriter(); + + try + { + Console.SetOut(outWriter); + Console.SetError(errorWriter); + + // Act - create a context WITHOUT --silent, write messages + using (var context = Context.Create(Array.Empty())) + { + context.WriteLine("informational message"); + context.WriteError("error message"); + } + + // Assert - messages appear on the respective streams + Assert.Contains("informational message", outWriter.ToString()); + Assert.Contains("error message", errorWriter.ToString()); + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalError); + } + } + /// /// Verifies that the Cli subsystem routes both informational and error messages /// through the log file when a log path is specified. /// [Fact] - public void Cli_OutputPipeline_WritesMessagesToLogFile() + public void Cli_OutputPipeline_WithLogPathAndSilentFlag_WritesMessagesToLogFile() { // Arrange var tempDir = Directory.CreateTempSubdirectory("fileassert_cli_"); diff --git a/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs b/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs index b94076b..bad2a5b 100644 --- a/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs @@ -18,9 +18,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System.IO.Compression; using System.Text.RegularExpressions; using DemaConsulting.FileAssert.Utilities; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.Writer; namespace DemaConsulting.FileAssert.Tests; @@ -1039,6 +1042,231 @@ public void IntegrationTest_JsonAssert_PassingQuery_ReturnsZero() } } + /// + /// Test that a ZIP assert with a passing entry query returns a zero exit code. + /// + [Fact] + public void IntegrationTest_ZipAssert_PassingQuery_ReturnsZero() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); + try + { + var zipPath = Path.Combine(tempDir.FullName, "archive.zip"); + using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create)) + { + zip.CreateEntry("readme.txt"); + } + + var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: ZipCheck + files: + - pattern: "*.zip" + zip: + entries: + - pattern: "*.txt" + min: 1 + """); + + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.Equal(0, exitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } + + /// + /// Test that a ZIP assert with an invalid zip file returns a non-zero exit code. + /// + [Fact] + public void IntegrationTest_ZipAssert_InvalidFile_ReturnsNonZero() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); + try + { + var invalidZipPath = Path.Combine(tempDir.FullName, "invalid.zip"); + File.WriteAllText(invalidZipPath, "this is not a zip file"); + + var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: ZipInvalidCheck + files: + - pattern: "*.zip" + zip: + entries: + - pattern: "*.txt" + min: 1 + """); + + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } + + /// + /// Test that an HTML assert with a non-existent XPath query and min:1 returns a non-zero exit code. + /// + [Fact] + public void IntegrationTest_HtmlAssert_InvalidFile_ReturnsNonZero() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); + try + { + File.WriteAllText(Path.Combine(tempDir.FullName, "index.html"), """ + + Test Page +

Hello

+ + """); + + var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "HtmlInvalidCheck" + files: + - pattern: "*.html" + html: + - query: "//nonexistentElement" + min: 1 + """); + + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } + + /// + /// Test that a YAML assert with an invalid YAML file returns a non-zero exit code. + /// + [Fact] + public void IntegrationTest_YamlAssert_InvalidFile_ReturnsNonZero() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); + try + { + File.WriteAllText(Path.Combine(tempDir.FullName, "invalid.yaml"), ": invalid yaml\n - bad"); + + var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "YamlInvalidCheck" + files: + - pattern: "invalid.yaml" + yaml: + - query: "server.host" + count: 1 + """); + + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } + + /// + /// Test that a JSON assert with an invalid JSON file returns a non-zero exit code. + /// + [Fact] + public void IntegrationTest_JsonAssert_InvalidFile_ReturnsNonZero() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); + try + { + File.WriteAllText(Path.Combine(tempDir.FullName, "invalid.json"), "{invalid json"); + + var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "JsonInvalidCheck" + files: + - pattern: "invalid.json" + json: + - query: "ConnectionStrings" + count: 1 + """); + + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } + + /// + /// Test that a PDF assert with a failing page-count assertion returns a non-zero exit code. + /// + [Fact] + public void IntegrationTest_PdfAssert_FailingAssertion_ReturnsNonZero() + { + // Arrange - build a valid single-page PDF and assert a minimum of 2 pages (will fail) + var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); + try + { + var pdfPath = Path.Combine(tempDir.FullName, "report.pdf"); + using var builder = new PdfDocumentBuilder(); + builder.AddPage(PageSize.A4); + File.WriteAllBytes(pdfPath, builder.Build()); + + var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "PdfCheck" + files: + - pattern: "*.pdf" + pdf: + pages: + min: 2 + """); + + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } + /// /// Test that a PDF assert with an invalid file returns a non-zero exit code. /// From fd7da7e8a4cee9cba64876da81a75ffe106aca94 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Wed, 20 May 2026 18:52:10 -0400 Subject: [PATCH 2/8] Updates from template and reviews. --- .reviewmark.yaml | 10 + .../design/file-assert/selftest/validation.md | 15 +- .../file-assert/utilities/path-helpers.md | 9 +- .../utilities/temporary-directory.md | 133 ++ docs/design/introduction.md | 7 +- .../utilities/temporary-directory.yaml | 37 + docs/verification/file-assert/utilities.md | 22 + .../utilities/temporary-directory.md | 114 ++ requirements.yaml | 1 + .../SelfTest/Validation.cs | 63 +- .../Utilities/TemporaryDirectory.cs | 106 ++ .../Cli/CliTests.cs | 115 +- .../Configuration/ConfigurationTests.cs | 189 ++- .../Configuration/FileAssertConfigTests.cs | 377 ++--- .../IntegrationTests.cs | 1366 ++++++++--------- .../Modeling/FileAssertFileTests.cs | 353 ++--- .../Modeling/FileAssertTestTests.cs | 37 +- .../Modeling/ModelingTests.cs | 253 ++- .../SelfTest/SelfTestTests.cs | 101 +- .../Utilities/TemporaryDirectoryTests.cs | 139 ++ .../Utilities/UtilitiesTests.cs | 22 +- 21 files changed, 1809 insertions(+), 1660 deletions(-) create mode 100644 docs/design/file-assert/utilities/temporary-directory.md create mode 100644 docs/reqstream/file-assert/utilities/temporary-directory.yaml create mode 100644 docs/verification/file-assert/utilities/temporary-directory.md create mode 100644 src/DemaConsulting.FileAssert/Utilities/TemporaryDirectory.cs create mode 100644 test/DemaConsulting.FileAssert.Tests/Utilities/TemporaryDirectoryTests.cs diff --git a/.reviewmark.yaml b/.reviewmark.yaml index 8cad381..c82ab31 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -155,6 +155,16 @@ reviews: - "src/**/Utilities/PathHelpers.cs" # implementation - "test/**/Utilities/PathHelpersTests.cs" # unit tests + # FileAssert-Utilities-TemporaryDirectory Review (one per unit) + - id: FileAssert-Utilities-TemporaryDirectory + title: Review that FileAssert Utilities TemporaryDirectory Implementation is Correct + paths: + - "docs/reqstream/file-assert/utilities/temporary-directory.yaml" # requirements + - "docs/design/file-assert/utilities/temporary-directory.md" # design + - "docs/verification/file-assert/utilities/temporary-directory.md" # verification + - "src/**/Utilities/TemporaryDirectory.cs" # implementation + - "test/**/Utilities/TemporaryDirectoryTests.cs" # unit tests + # FileAssert-Modeling-FileAssertRule Review (one per unit) - id: FileAssert-Modeling-FileAssertRule title: Review that FileAssert Modeling FileAssertRule Implementation is Correct diff --git a/docs/design/file-assert/selftest/validation.md b/docs/design/file-assert/selftest/validation.md index ea26fea..359d10d 100644 --- a/docs/design/file-assert/selftest/validation.md +++ b/docs/design/file-assert/selftest/validation.md @@ -77,12 +77,6 @@ Writes the collected results to the file specified by `context.ResultsFile`: - Other extensions → error written to context. - Any I/O or other exception is caught and an error message is written to context. -##### TemporaryDirectory Helper - -A private nested `IDisposable` class that creates a unique temporary directory on construction -and deletes it recursively on disposal. Uses `PathHelpers.SafePathCombine` to build the -directory path under `Path.GetTempPath()`. - #### Design Decisions - **`RunValidationTest` dispatcher**: All built-in tests share a single helper that owns the @@ -102,11 +96,8 @@ pass/fail report, and optionally serialize the results to a TRX or JUnit XML fil #### Data Model N/A — `Validation` is a `static` class with no instance fields. All state is local to the -`Run` method call. The private nested `TemporaryDirectory` class holds a single field: - -| Field | -| :-------------- | -| `DirectoryPath` | +`Run` method call. Temporary directory management is delegated to the standalone +`TemporaryDirectory` utility class. #### Key Methods @@ -138,7 +129,7 @@ N/A — `Validation` is a `static` class with no instance fields. All state is l - **Calls internally**: - `Program.Run(Context)` to execute each built-in test scenario in-process. - `Context.Create(string[])` to construct per-test contexts with `--silent` and `--config`. - - `PathHelpers.SafePathCombine` to build all fixture and log file paths safely. + - `TemporaryDirectory.GetFilePath` to build all fixture and log file paths safely. - `DemaConsulting.TestResults.IO.TrxSerializer.Serialize` and `JUnitSerializer.Serialize` for results serialization. - **OTS dependencies**: `System.Runtime.InteropServices.RuntimeInformation` for system info diff --git a/docs/design/file-assert/utilities/path-helpers.md b/docs/design/file-assert/utilities/path-helpers.md index 8c80ef6..63ae6dd 100644 --- a/docs/design/file-assert/utilities/path-helpers.md +++ b/docs/design/file-assert/utilities/path-helpers.md @@ -82,9 +82,10 @@ N/A — `PathHelpers` is a `static` class with no instance state or fields. #### Interactions - **Callers**: - - `Validation.TemporaryDirectory` — uses `SafePathCombine(Path.GetTempPath(), guid-name)` to - create a temp directory path. - - `Validation` built-in tests — uses `SafePathCombine(tempDir.DirectoryPath, fileName)` to - build fixture file paths. + - `TemporaryDirectory` — uses `SafePathCombine(Environment.CurrentDirectory, guid-name)` to + create a temp directory path, and `SafePathCombine(DirectoryPath, relativePath)` inside + `GetFilePath`. + - `Validation` built-in tests — uses `tempDir.GetFilePath(fileName)` (which internally calls + `SafePathCombine`) to build fixture file paths. - **No internal FileAssert dependencies**: `PathHelpers` is a self-contained utility with no references to other units in the system. diff --git a/docs/design/file-assert/utilities/temporary-directory.md b/docs/design/file-assert/utilities/temporary-directory.md new file mode 100644 index 0000000..f3dc7c7 --- /dev/null +++ b/docs/design/file-assert/utilities/temporary-directory.md @@ -0,0 +1,133 @@ +### TemporaryDirectory Design + +#### Overview + +`TemporaryDirectory` is a disposable utility class that creates a uniquely-named temporary +directory under `Environment.CurrentDirectory` and deletes it automatically on disposal. +It uses `PathHelpers.SafePathCombine` internally to prevent path-traversal attacks on any +relative paths supplied by callers. + +The directory is rooted at `Environment.CurrentDirectory` rather than `Path.GetTempPath()` +to avoid the macOS symlink issue where `/tmp` resolves to `/private/tmp`, which can cause +path-comparison failures when the OS returns the resolved (real) path instead of the symlink +path used to construct it. + +#### Class Structure + +##### Constructor + +```csharp +public TemporaryDirectory() +``` + +Creates a uniquely-named subdirectory under `Environment.CurrentDirectory` using the pattern +`tmp-{Guid:N}`. The directory name is derived from a fresh `Guid` formatted with the `N` +specifier (32 lowercase hex digits, no hyphens). + +**Steps:** + +1. Capture `Environment.CurrentDirectory` as the effective base directory. +2. Call `PathHelpers.SafePathCombine(effectiveBase, $"tmp-{Guid.NewGuid():N}")` to produce + the directory path. +3. Call `Directory.CreateDirectory(DirectoryPath)`. +4. Wrap any `IOException`, `UnauthorizedAccessException`, or `ArgumentException` in an + `InvalidOperationException` with a descriptive message. + +**Exceptions thrown:** + +| Exception | Condition | +| :------------------------- | :---------------------------------------------------------- | +| `InvalidOperationException`| Directory could not be created (wraps the underlying cause) | + +##### GetFilePath Method + +```csharp +public string GetFilePath(string relativePath) +``` + +Returns the full path to a file within the temporary directory, creating any required +intermediate subdirectories. + +**Steps:** + +1. Call `PathHelpers.SafePathCombine(DirectoryPath, relativePath)` to produce the full path + and guard against traversal attacks. +2. Obtain the parent directory with `Path.GetDirectoryName(path)`. +3. Call `Directory.CreateDirectory(directory)` if the parent directory is non-null. +4. Return the full path. + +**Exceptions thrown:** + +| Exception | Condition | +| :--------------------- | :----------------------------------------------------------- | +| `ArgumentNullException`| `relativePath` is `null` | +| `ArgumentException` | `relativePath` would escape the temporary directory | + +##### Dispose Method + +```csharp +public void Dispose() +``` + +Deletes the temporary directory and all its contents recursively. Cleanup failures are +treated as non-fatal: `IOException` and `UnauthorizedAccessException` are caught and +silently suppressed so that a failed cleanup does not propagate as an exception out of a +`using` block. + +#### Design Decisions + +- **`Environment.CurrentDirectory` over `Path.GetTempPath()`**: On macOS, `/tmp` is a + symlink to `/private/tmp`. When paths are constructed with `/tmp` as a base but the OS + later returns the real path `/private/tmp`, string-equality comparisons fail. Using + `Environment.CurrentDirectory`, which is already fully resolved, avoids this class of + failure entirely. +- **`tmp-{Guid:N}` name pattern**: The `N` format specifier produces 32 lowercase hex + digits with no hyphens, which is a valid directory name on all supported platforms and + is highly unlikely to collide with existing directories. +- **`InvalidOperationException` on construction failure**: Wrapping the underlying I/O + exception in `InvalidOperationException` gives callers a single catch target for setup + failures without exposing the low-level exception hierarchy as part of the public + contract. +- **Suppressed exceptions in `Dispose`**: Temporary-directory cleanup is best-effort. + Propagating cleanup errors out of a `using` block would mask the real exception that + caused the `using` block to exit, so `IOException` and `UnauthorizedAccessException` + are silently swallowed. + +#### Purpose + +`TemporaryDirectory` provides a safe, self-cleaning workspace for built-in tests and any +other caller that needs a short-lived directory isolated to the current working directory. +Its single responsibility is to own the lifecycle — creation and deletion — of that +directory. + +#### Data Model + +| Field | Type | Description | +| :-------------- | :------- | :--------------------------------------- | +| `DirectoryPath` | `string` | Full path to the temporary directory | + +#### Key Methods + +| Method | +| :-------------------------------------- | +| `TemporaryDirectory()` *(constructor)* | +| `GetFilePath(string relativePath)` | +| `Dispose()` | + +#### Error Handling + +| Scenario | +| :------------------------------------------------ | +| Temporary directory creation failure | +| Null or traversal-escaping `relativePath` | +| Temporary directory deletion failure (suppressed) | + +#### Interactions + +- **Uses**: `PathHelpers.SafePathCombine` for all path construction, ensuring traversal + safety on both the directory name and every relative path passed to `GetFilePath`. +- **Callers**: + - `Validation` built-in tests — each test creates a `TemporaryDirectory` instance, + writes fixture files via `GetFilePath`, and disposes it at the end of the test body. + - Test project files (`*Tests.cs`) — use `TemporaryDirectory` for isolated file + system fixtures in unit and integration tests. diff --git a/docs/design/introduction.md b/docs/design/introduction.md index d229f50..2d4143e 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -29,6 +29,7 @@ This document covers the detailed design of the following software units: - **FileAssertJsonAssert** — JSON document assertions (`FileAssertJsonAssert.cs`) - **FileAssertZipAssert** — zip archive entry assertions (`FileAssertZipAssert.cs`) - **PathHelpers** — safe path-combination utility (`PathHelpers.cs`) +- **TemporaryDirectory** — disposable temporary directory utility (`TemporaryDirectory.cs`) - **Validation** — self-validation test runner (`Validation.cs`) The following cross-cutting design topics are also covered: @@ -67,7 +68,8 @@ FileAssert (System) │ ├── FileAssertJsonAssert (Unit) │ └── FileAssertZipAssert (Unit) ├── Utilities (Subsystem) -│ └── PathHelpers (Unit) +│ ├── PathHelpers (Unit) +│ └── TemporaryDirectory (Unit) └── SelfTest (Subsystem) └── Validation (Unit) ``` @@ -99,7 +101,8 @@ src/DemaConsulting.FileAssert/ │ ├── FileAssertJsonAssert.cs — JSON document assertions (System.Text.Json) │ └── FileAssertZipAssert.cs — zip archive entry assertions (System.IO.Compression) ├── Utilities/ -│ └── PathHelpers.cs — safe path-combination utility +│ ├── PathHelpers.cs — safe path-combination utility +│ └── TemporaryDirectory.cs — disposable temporary directory utility └── SelfTest/ └── Validation.cs — self-validation test runner ``` diff --git a/docs/reqstream/file-assert/utilities/temporary-directory.yaml b/docs/reqstream/file-assert/utilities/temporary-directory.yaml new file mode 100644 index 0000000..88b8fbb --- /dev/null +++ b/docs/reqstream/file-assert/utilities/temporary-directory.yaml @@ -0,0 +1,37 @@ +--- +# Software Unit Requirements for the TemporaryDirectory Class +# +# The TemporaryDirectory class manages the lifecycle of a disposable temporary +# directory rooted under Environment.CurrentDirectory. It provides safe path +# construction via GetFilePath and guarantees cleanup on Dispose. + +sections: + - title: TemporaryDirectory Unit Requirements + requirements: + - id: FileAssert-TemporaryDirectory-Lifecycle + title: The TemporaryDirectory class shall create a unique directory on construction and delete it on disposal. + justification: | + Tests that rely on isolated scratch space must not interfere with one another + or leave artifacts on disk. Creating a unique directory per instance prevents + collisions between concurrent or sequential tests, and deleting it on disposal + ensures the working directory is left clean even if the test fails or throws. + tests: + - TemporaryDirectory_Constructor_CreatesDirectory + - TemporaryDirectory_Constructor_CreatesUniqueDirectories + - TemporaryDirectory_Dispose_DeletesDirectory + - TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow + + - id: FileAssert-TemporaryDirectory-SafePath + title: The TemporaryDirectory class shall construct safe file paths under the temporary directory and create intermediate + subdirectories. + justification: | + Callers supply relative file paths to GetFilePath to resolve output locations + inside the temporary directory. Using PathHelpers.SafePathCombine ensures that + path-traversal attempts (e.g., "../escaped.txt") are rejected with a clear + ArgumentException, preventing accidental or malicious writes outside the + sandbox. Creating intermediate subdirectories automatically removes boilerplate + from callers that write to nested paths. + tests: + - TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory + - TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories + - TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException diff --git a/docs/verification/file-assert/utilities.md b/docs/verification/file-assert/utilities.md index 53fed09..a2cd05e 100644 --- a/docs/verification/file-assert/utilities.md +++ b/docs/verification/file-assert/utilities.md @@ -42,3 +42,25 @@ The following integration test scenarios are defined in `UtilitiesTests.cs`. ### Requirements Coverage - **Path traversal prevention**: Utilities_SafePathCombine_PreventsPathTraversalToFileSystem + +### TemporaryDirectory Verification + +The `TemporaryDirectory` unit is verified by the unit tests defined in `TemporaryDirectoryTests.cs`. +Each test exercises construction, path resolution, and disposal against the real file system. + +#### TemporaryDirectory Test Scenarios + +- **TemporaryDirectory_Constructor_CreatesDirectory** – confirms the directory exists on disk + immediately after construction. +- **TemporaryDirectory_Constructor_CreatesUniqueDirectories** – confirms two instances produce + distinct directory paths. +- **TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory** – confirms that a simple + relative filename resolves to a path under the temporary directory. +- **TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories** – confirms that + intermediate subdirectories are created automatically for nested relative paths. +- **TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException** – confirms that a + path-traversal attempt (e.g., `"../escaped.txt"`) is rejected with `ArgumentException`. +- **TemporaryDirectory_Dispose_DeletesDirectory** – confirms the directory and its contents are + deleted when the instance is disposed. +- **TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow** – confirms that disposal does not + throw when the directory has already been removed externally. diff --git a/docs/verification/file-assert/utilities/temporary-directory.md b/docs/verification/file-assert/utilities/temporary-directory.md new file mode 100644 index 0000000..d9c606b --- /dev/null +++ b/docs/verification/file-assert/utilities/temporary-directory.md @@ -0,0 +1,114 @@ +### TemporaryDirectory Verification + +This document describes the unit-level verification design for the `TemporaryDirectory` unit. It +defines the test scenarios, dependency usage, and requirement coverage for +`Utilities/TemporaryDirectory.cs`. + +#### Verification Approach + +`TemporaryDirectory` is verified with unit tests defined in `TemporaryDirectoryTests.cs`. Tests +exercise construction, path resolution, and disposal against the real file system rooted under +`Environment.CurrentDirectory`. No mocking or test doubles are needed because the class interacts +only with the local file system and delegates traversal protection to `PathHelpers.SafePathCombine`. + +#### Test Environment + +Tests execute in the standard CI pipeline environment using the xUnit test runner. The test +collection is marked `[Collection("Sequential")]` to prevent parallel execution of tests that +mutate the working directory. No special hardware, peripherals, or environment configuration is +required beyond the standard build toolchain. + +#### Acceptance Criteria + +N/A – Acceptance criteria are managed at the subsystem and system integration levels. +Unit tests provide fine-grained coverage evidence; formal acceptance is declared at the +subsystem level when all unit tests supporting a subsystem requirement pass. + +#### Dependencies + +`TemporaryDirectory` depends on `PathHelpers.SafePathCombine` for traversal-safe path +construction and on .NET BCL types (`Directory`, `Path`, `Environment`) for file system +operations. No mocking is needed at this level. + +#### Test Scenarios + +##### TemporaryDirectory_Constructor_CreatesDirectory + +**Scenario**: A `TemporaryDirectory` instance is constructed inside a `using` block. + +**Expected**: `Directory.Exists(tmpDir.DirectoryPath)` returns `true` immediately after +construction. + +**Requirement coverage**: Lifecycle creation requirement. + +##### TemporaryDirectory_Constructor_CreatesUniqueDirectories + +**Scenario**: Two `TemporaryDirectory` instances are constructed sequentially without disposal +between them. + +**Expected**: The two `DirectoryPath` values are not equal. + +**Boundary / error path**: Ensures uniqueness under rapid successive construction. + +**Requirement coverage**: Lifecycle uniqueness requirement. + +##### TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory + +**Scenario**: `GetFilePath("output.md")` is called on a live `TemporaryDirectory` instance. + +**Expected**: The returned path starts with `tmpDir.DirectoryPath` and ends with `"output.md"`. +No exception is thrown. + +**Requirement coverage**: Safe path construction requirement. + +##### TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories + +**Scenario**: `GetFilePath(Path.Combine("sub", "nested", "output.md"))` is called on a live +`TemporaryDirectory` instance. + +**Expected**: `Directory.Exists` on the parent directory of the returned path returns `true`, +confirming that intermediate subdirectories were created automatically. No exception is thrown. + +**Requirement coverage**: Intermediate subdirectory creation requirement. + +##### TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException + +**Scenario**: `GetFilePath("../escaped.txt")` is called on a live `TemporaryDirectory` instance. + +**Expected**: An `ArgumentException` is thrown; no file is created outside the temporary +directory. + +**Boundary / error path**: Path-traversal attempt using leading `../`. + +**Requirement coverage**: Traversal rejection requirement. + +##### TemporaryDirectory_Dispose_DeletesDirectory + +**Scenario**: A `TemporaryDirectory` is constructed, a file is written inside it via +`GetFilePath`, and then the instance is disposed by exiting a `using` block. + +**Expected**: `Directory.Exists` on the captured `DirectoryPath` returns `false` after disposal, +confirming that the directory and its contents were deleted. + +**Requirement coverage**: Lifecycle cleanup requirement. + +##### TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow + +**Scenario**: The underlying directory is manually deleted before `Dispose()` is called on the +`TemporaryDirectory` instance. + +**Expected**: `Dispose()` completes without throwing any exception. + +**Boundary / error path**: Cleanup error suppression when the directory no longer exists. + +**Requirement coverage**: Resilient disposal requirement. + +#### Requirements Coverage + +- (directory created on construction): TemporaryDirectory_Constructor_CreatesDirectory +- (unique directory per instance): TemporaryDirectory_Constructor_CreatesUniqueDirectories +- (directory deleted on disposal): TemporaryDirectory_Dispose_DeletesDirectory +- (disposal safe when already deleted): TemporaryDirectory_Dispose_AlreadyDeleted_DoesNotThrow +- (simple file path under directory): TemporaryDirectory_GetFilePath_SimpleFile_ReturnsPathUnderDirectory +- (nested path creates subdirectories): TemporaryDirectory_GetFilePath_NestedPath_CreatesIntermediateDirectories +- (traversal attempt rejected): TemporaryDirectory_GetFilePath_TraversalAttempt_ThrowsArgumentException diff --git a/requirements.yaml b/requirements.yaml index cf4a649..42cb09e 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -21,6 +21,7 @@ includes: - docs/reqstream/file-assert/modeling/file-assert-zip-assert.yaml - docs/reqstream/file-assert/utilities.yaml - docs/reqstream/file-assert/utilities/path-helpers.yaml + - docs/reqstream/file-assert/utilities/temporary-directory.yaml - docs/reqstream/file-assert/selftest.yaml - docs/reqstream/file-assert/selftest/validation.yaml - docs/reqstream/file-assert/platform-requirements.yaml diff --git a/src/DemaConsulting.FileAssert/SelfTest/Validation.cs b/src/DemaConsulting.FileAssert/SelfTest/Validation.cs index d88e086..c6313a8 100644 --- a/src/DemaConsulting.FileAssert/SelfTest/Validation.cs +++ b/src/DemaConsulting.FileAssert/SelfTest/Validation.cs @@ -110,7 +110,7 @@ private static void RunVersionTest(Context context, DemaConsulting.TestResults.T RunValidationTest(context, testResults, "FileAssert_VersionDisplay", () => { using var tempDir = new TemporaryDirectory(); - var logFile = PathHelpers.SafePathCombine(tempDir.DirectoryPath, "version-test.log"); + var logFile = tempDir.GetFilePath("version-test.log"); // Run the program capturing output to a log file int exitCode; @@ -142,7 +142,7 @@ private static void RunHelpTest(Context context, DemaConsulting.TestResults.Test RunValidationTest(context, testResults, "FileAssert_HelpDisplay", () => { using var tempDir = new TemporaryDirectory(); - var logFile = PathHelpers.SafePathCombine(tempDir.DirectoryPath, "help-test.log"); + var logFile = tempDir.GetFilePath("help-test.log"); // Run the program capturing output to a log file int exitCode; @@ -174,11 +174,11 @@ private static void RunResultsTest(Context context, DemaConsulting.TestResults.T RunValidationTest(context, testResults, "FileAssert_Results", () => { using var tempDir = new TemporaryDirectory(); - var configFile = PathHelpers.SafePathCombine(tempDir.DirectoryPath, ".fileassert.yaml"); - var resultsFile = PathHelpers.SafePathCombine(tempDir.DirectoryPath, "results.trx"); + var configFile = tempDir.GetFilePath(".fileassert.yaml"); + var resultsFile = tempDir.GetFilePath("results.trx"); // Create a file that will satisfy the passing test - File.WriteAllText(PathHelpers.SafePathCombine(tempDir.DirectoryPath, "present.txt"), "present"); + File.WriteAllText(tempDir.GetFilePath("present.txt"), "present"); // Write a config with one passing test (present.txt exists) and one failing test (absent.txt missing) File.WriteAllText(configFile, @@ -223,10 +223,10 @@ private static void RunExistsTest(Context context, DemaConsulting.TestResults.Te RunValidationTest(context, testResults, "FileAssert_Exists", () => { using var tempDir = new TemporaryDirectory(); - var configFile = PathHelpers.SafePathCombine(tempDir.DirectoryPath, ".fileassert.yaml"); + var configFile = tempDir.GetFilePath(".fileassert.yaml"); // Create a text file that the glob pattern should match - File.WriteAllText(PathHelpers.SafePathCombine(tempDir.DirectoryPath, "hello.txt"), "Hello World"); + File.WriteAllText(tempDir.GetFilePath("hello.txt"), "Hello World"); // Write a config that verifies exactly one .txt file exists in the directory File.WriteAllText(configFile, @@ -260,10 +260,10 @@ private static void RunContainsTest(Context context, DemaConsulting.TestResults. RunValidationTest(context, testResults, "FileAssert_Contains", () => { using var tempDir = new TemporaryDirectory(); - var configFile = PathHelpers.SafePathCombine(tempDir.DirectoryPath, ".fileassert.yaml"); + var configFile = tempDir.GetFilePath(".fileassert.yaml"); // Create a text file with known content for the contains assertion - File.WriteAllText(PathHelpers.SafePathCombine(tempDir.DirectoryPath, "hello.txt"), "Hello World"); + File.WriteAllText(tempDir.GetFilePath("hello.txt"), "Hello World"); // Write a config that verifies the file contains the expected text File.WriteAllText(configFile, @@ -394,51 +394,6 @@ private static DemaConsulting.TestResults.TestResult CreateTestResult(string tes }; } - /// - /// 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(), $"fileassert_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 - } - } - } /// /// Source-generated regex for matching semantic version strings (N.N.N format). diff --git a/src/DemaConsulting.FileAssert/Utilities/TemporaryDirectory.cs b/src/DemaConsulting.FileAssert/Utilities/TemporaryDirectory.cs new file mode 100644 index 0000000..996719b --- /dev/null +++ b/src/DemaConsulting.FileAssert/Utilities/TemporaryDirectory.cs @@ -0,0 +1,106 @@ +// 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.FileAssert.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, creating a uniquely-named subdirectory under + /// . + /// + /// + /// Thrown when the temporary directory cannot be created due to an + /// , , or + /// . + /// + public TemporaryDirectory() + { + var effectiveBase = Environment.CurrentDirectory; + DirectoryPath = PathHelpers.SafePathCombine(effectiveBase, $"tmp-{Guid.NewGuid():N}"); + + 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 within the temporary directory. Must not be null. + /// + /// The combined full path within the temporary directory. + /// Thrown when relativePath is null. + /// Thrown when relativePath would escape the temporary directory. + public string GetFilePath(string relativePath) + { + var path = PathHelpers.SafePathCombine(DirectoryPath, relativePath); + + var directory = Path.GetDirectoryName(path); + if (directory != null) + { + Directory.CreateDirectory(directory); + } + + return path; + } + + /// + /// Deletes the temporary directory and all its contents. + /// + /// + /// IOException and UnauthorizedAccessException are intentionally suppressed during + /// disposal. Cleanup failures are non-fatal. + /// + public void Dispose() + { + try + { + if (Directory.Exists(DirectoryPath)) + { + Directory.Delete(DirectoryPath, recursive: true); + } + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + // Ignore cleanup errors during disposal + } + } +} diff --git a/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs b/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs index bdf4c51..c79655e 100644 --- a/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs @@ -20,6 +20,7 @@ using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Utilities; namespace DemaConsulting.FileAssert.Tests.Cli; /// @@ -35,32 +36,26 @@ public class CliTests public void Cli_CreateContext_ParsesSilentValidateAndLogFlags() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_cli_"); - try + using var tempDir = new TemporaryDirectory(); + var logPath = tempDir.GetFilePath("out.log"); + + // Act - create a context with the silent, validate, and log flags + using (var context = Context.Create( + [ + "--silent", + "--validate", + "--log", logPath + ])) { - var logPath = Path.Combine(tempDir.FullName, "out.log"); - - // Act - create a context with the silent, validate, and log flags - using (var context = Context.Create( - [ - "--silent", - "--validate", - "--log", logPath - ])) - { - // Assert - all flags are reflected in the context properties - Assert.True(context.Silent); - Assert.True(context.Validate); - Assert.False(context.Version); - Assert.False(context.Help); - Assert.Equal(".fileassert.yaml", context.ConfigFile); - Assert.Equal(0, context.ExitCode); - } - } - finally - { - tempDir.Delete(recursive: true); + // Assert - all flags are reflected in the context properties + Assert.True(context.Silent); + Assert.True(context.Validate); + Assert.False(context.Version); + Assert.False(context.Help); + Assert.Equal(".fileassert.yaml", context.ConfigFile); + Assert.Equal(0, context.ExitCode); } + } /// @@ -70,31 +65,25 @@ public void Cli_CreateContext_ParsesSilentValidateAndLogFlags() public void Cli_CreateContext_ParsesVersionHelpConfigResultsFlags() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_cli_"); - try - { - var configPath = Path.Combine(tempDir.FullName, "custom.yaml"); - var resultsPath = Path.Combine(tempDir.FullName, "results.trx"); - - // Act - create a context with the version, help, config, and results flags - using var context = Context.Create( - [ - "--version", - "--help", - "--config", configPath, - "--results", resultsPath - ]); + using var tempDir = new TemporaryDirectory(); + var configPath = tempDir.GetFilePath("custom.yaml"); + var resultsPath = tempDir.GetFilePath("results.trx"); + + // Act - create a context with the version, help, config, and results flags + using var context = Context.Create( + [ + "--version", + "--help", + "--config", configPath, + "--results", resultsPath + ]); + + // Assert - all flags are reflected in the context properties + Assert.True(context.Version); + Assert.True(context.Help); + Assert.Equal(configPath, context.ConfigFile); + Assert.Equal(resultsPath, context.ResultsFile); - // Assert - all flags are reflected in the context properties - Assert.True(context.Version); - Assert.True(context.Help); - Assert.Equal(configPath, context.ConfigFile); - Assert.Equal(resultsPath, context.ResultsFile); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -182,26 +171,20 @@ public void Cli_OutputPipeline_WithoutSilentFlag_WritesMessagesToConsole() public void Cli_OutputPipeline_WithLogPathAndSilentFlag_WritesMessagesToLogFile() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_cli_"); - try - { - var logPath = Path.Combine(tempDir.FullName, "out.log"); + using var tempDir = new TemporaryDirectory(); + var logPath = tempDir.GetFilePath("out.log"); - // Act - create a silent context with logging, write messages, dispose to flush - using (var context = Context.Create(["--silent", "--log", logPath])) - { - context.WriteLine("informational message"); - context.WriteError("error message"); - } - - // Assert - both messages appear in the log file - var logContent = File.ReadAllText(logPath); - Assert.Contains("informational message", logContent); - Assert.Contains("error message", logContent); - } - finally + // Act - create a silent context with logging, write messages, dispose to flush + using (var context = Context.Create(["--silent", "--log", logPath])) { - tempDir.Delete(recursive: true); + context.WriteLine("informational message"); + context.WriteError("error message"); } + + // Assert - both messages appear in the log file + var logContent = File.ReadAllText(logPath); + Assert.Contains("informational message", logContent); + Assert.Contains("error message", logContent); + } } diff --git a/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationTests.cs b/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationTests.cs index 39b1d48..f3df2ef 100644 --- a/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationTests.cs @@ -21,6 +21,7 @@ using DemaConsulting.FileAssert.Cli; using DemaConsulting.FileAssert.Configuration; +using DemaConsulting.FileAssert.Utilities; namespace DemaConsulting.FileAssert.Tests.Configuration; /// @@ -37,41 +38,35 @@ public class ConfigurationTests public void Configuration_LoadYaml_BuildsCompleteTestHierarchy() { // Arrange - write a YAML configuration with nested test, file, and rule entries - var tempDir = Directory.CreateTempSubdirectory("fileassert_config_"); - try - { - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "License Check" - tags: - - license - files: - - pattern: "**/*.txt" - min: 1 - text: - - contains: "Copyright" - """); - - // Act - var config = FileAssertConfig.ReadFromFile(configPath); - - // Assert - the full hierarchy is correctly constructed - Assert.Single(config.Tests); - var test = config.Tests[0]; - Assert.Equal("License Check", test.Name); - Assert.Single(test.Tags); - Assert.Equal("license", test.Tags[0]); - Assert.Single(test.Files); - var file = test.Files[0]; - Assert.Equal("**/*.txt", file.Pattern); - Assert.Equal(1, file.Min); - Assert.Single(file.TextAssert!.Rules); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "License Check" + tags: + - license + files: + - pattern: "**/*.txt" + min: 1 + text: + - contains: "Copyright" + """); + + // Act + var config = FileAssertConfig.ReadFromFile(configPath); + + // Assert - the full hierarchy is correctly constructed + Assert.Single(config.Tests); + var test = config.Tests[0]; + Assert.Equal("License Check", test.Name); + Assert.Single(test.Tags); + Assert.Equal("license", test.Tags[0]); + Assert.Single(test.Files); + var file = test.Files[0]; + Assert.Equal("**/*.txt", file.Pattern); + Assert.Equal(1, file.Min); + Assert.Single(file.TextAssert!.Rules); + } /// @@ -82,38 +77,32 @@ public void Configuration_LoadYaml_BuildsCompleteTestHierarchy() public void Configuration_RunWithFilter_ExecutesOnlyMatchingTests() { // Arrange - two tests in config; only one file exists so only that test should pass - var tempDir = Directory.CreateTempSubdirectory("fileassert_config_"); - try - { - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "Alpha" - files: - - pattern: "alpha.txt" - min: 1 - - name: "Beta" - files: - - pattern: "beta.txt" - min: 1 - """); - - // Create only alpha.txt so the Alpha test passes and Beta would fail - File.WriteAllText(Path.Combine(tempDir.FullName, "alpha.txt"), "content"); - - var config = FileAssertConfig.ReadFromFile(configPath); - using var context = Context.Create(["--silent"]); - - // Act - run with the "Alpha" filter only - config.Run(context, ["Alpha"]); - - // Assert - no errors because only Alpha ran (and alpha.txt exists) - Assert.Equal(0, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "Alpha" + files: + - pattern: "alpha.txt" + min: 1 + - name: "Beta" + files: + - pattern: "beta.txt" + min: 1 + """); + + // Create only alpha.txt so the Alpha test passes and Beta would fail + File.WriteAllText(tempDir.GetFilePath("alpha.txt"), "content"); + + var config = FileAssertConfig.ReadFromFile(configPath); + using var context = Context.Create(["--silent"]); + + // Act - run with the "Alpha" filter only + config.Run(context, ["Alpha"]); + + // Assert - no errors because only Alpha ran (and alpha.txt exists) + Assert.Equal(0, context.ExitCode); + } /// @@ -124,41 +113,35 @@ public void Configuration_RunWithFilter_ExecutesOnlyMatchingTests() public void Configuration_RunWithTagFilter_ExecutesOnlyMatchingTests() { // Arrange - two tests with different tags; only one file exists so only that test passes - var tempDir = Directory.CreateTempSubdirectory("fileassert_config_"); - try - { - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "Alpha" - tags: - - smoke - files: - - pattern: "alpha.txt" - min: 1 - - name: "Beta" - tags: - - regression - files: - - pattern: "beta.txt" - min: 1 - """); - - // Create only alpha.txt so the Alpha test passes and Beta would fail - File.WriteAllText(Path.Combine(tempDir.FullName, "alpha.txt"), "content"); - - var config = FileAssertConfig.ReadFromFile(configPath); - using var context = Context.Create(["--silent"]); - - // Act - run with the "smoke" tag filter only - config.Run(context, ["smoke"]); - - // Assert - no errors because only Alpha ran (matching the smoke tag) and alpha.txt exists - Assert.Equal(0, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "Alpha" + tags: + - smoke + files: + - pattern: "alpha.txt" + min: 1 + - name: "Beta" + tags: + - regression + files: + - pattern: "beta.txt" + min: 1 + """); + + // Create only alpha.txt so the Alpha test passes and Beta would fail + File.WriteAllText(tempDir.GetFilePath("alpha.txt"), "content"); + + var config = FileAssertConfig.ReadFromFile(configPath); + using var context = Context.Create(["--silent"]); + + // Act - run with the "smoke" tag filter only + config.Run(context, ["smoke"]); + + // Assert - no errors because only Alpha ran (matching the smoke tag) and alpha.txt exists + Assert.Equal(0, context.ExitCode); + } } diff --git a/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs b/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs index b052218..9009742 100644 --- a/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs @@ -22,6 +22,7 @@ using DemaConsulting.FileAssert.Configuration; using DemaConsulting.FileAssert.Modeling; +using DemaConsulting.FileAssert.Utilities; namespace DemaConsulting.FileAssert.Tests.Configuration; /// @@ -50,28 +51,22 @@ public class FileAssertConfigTests public void FileAssertConfig_ReadFromFile_ValidFile_ReturnsConfig() { // Arrange - write a minimal config to a temp file - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "Sample Test" - files: - - pattern: "*.txt" - """); - - // Act - var config = FileAssertConfig.ReadFromFile(configPath); - - // Assert - Assert.Single(config.Tests); - Assert.Equal("Sample Test", config.Tests[0].Name); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "Sample Test" + files: + - pattern: "*.txt" + """); + + // Act + var config = FileAssertConfig.ReadFromFile(configPath); + + // Assert + Assert.Single(config.Tests); + Assert.Equal("Sample Test", config.Tests[0].Name); + } /// @@ -104,28 +99,22 @@ public void FileAssertConfig_ReadFromFile_NullPath_ThrowsArgumentNullException() public void FileAssertConfig_Run_WithNoFilter_RunsAllTests() { // Arrange - create temp directory with files that satisfy both test patterns - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "alpha.txt"), "alpha content"); - File.WriteAllText(Path.Combine(tempDir.FullName, "beta.txt"), "beta content"); - - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, SimpleYaml); - - var config = FileAssertConfig.ReadFromFile(configPath); - using var context = Context.Create(["--silent"]); - - // Act - config.Run(context, []); - - // Assert - both tests ran and found their files with no errors - Assert.Equal(0, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("alpha.txt"), "alpha content"); + File.WriteAllText(tempDir.GetFilePath("beta.txt"), "beta content"); + + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, SimpleYaml); + + var config = FileAssertConfig.ReadFromFile(configPath); + using var context = Context.Create(["--silent"]); + + // Act + config.Run(context, []); + + // Assert - both tests ran and found their files with no errors + Assert.Equal(0, context.ExitCode); + } /// @@ -135,37 +124,31 @@ public void FileAssertConfig_Run_WithNoFilter_RunsAllTests() public void FileAssertConfig_Run_WithMatchingFilter_RunsMatchingTest() { // Arrange - only create alpha.txt; beta.txt missing would cause an error if Test Beta ran - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "alpha.txt"), "alpha content"); - - // beta.txt is intentionally absent; Test Beta with min=1 would fail if executed - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "Test Alpha" - files: - - pattern: "alpha.txt" - - name: "Test Beta" - files: - - pattern: "beta.txt" - min: 1 - """); - - var config = FileAssertConfig.ReadFromFile(configPath); - using var context = Context.Create(["--silent"]); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("alpha.txt"), "alpha content"); + + // beta.txt is intentionally absent; Test Beta with min=1 would fail if executed + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "Test Alpha" + files: + - pattern: "alpha.txt" + - name: "Test Beta" + files: + - pattern: "beta.txt" + min: 1 + """); + + var config = FileAssertConfig.ReadFromFile(configPath); + using var context = Context.Create(["--silent"]); + + // Act - filter to only "Test Alpha" + config.Run(context, ["Test Alpha"]); + + // Assert - no error because Test Beta was skipped + Assert.Equal(0, context.ExitCode); - // Act - filter to only "Test Alpha" - config.Run(context, ["Test Alpha"]); - - // Assert - no error because Test Beta was skipped - Assert.Equal(0, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -175,35 +158,29 @@ public void FileAssertConfig_Run_WithMatchingFilter_RunsMatchingTest() public void FileAssertConfig_Run_WithNonMatchingFilter_SkipsTests() { // Arrange - both patterns would fail if executed (files are absent with min=1) - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "Test Alpha" - files: - - pattern: "alpha.txt" - min: 1 - - name: "Test Beta" - files: - - pattern: "beta.txt" - min: 1 - """); - - var config = FileAssertConfig.ReadFromFile(configPath); - using var context = Context.Create(["--silent"]); - - // Act - filter that matches nothing - config.Run(context, ["No Match"]); + using var tempDir = new TemporaryDirectory(); + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "Test Alpha" + files: + - pattern: "alpha.txt" + min: 1 + - name: "Test Beta" + files: + - pattern: "beta.txt" + min: 1 + """); + + var config = FileAssertConfig.ReadFromFile(configPath); + using var context = Context.Create(["--silent"]); + + // Act - filter that matches nothing + config.Run(context, ["No Match"]); + + // Assert - no error because all tests were skipped by the filter + Assert.Equal(0, context.ExitCode); - // Assert - no error because all tests were skipped by the filter - Assert.Equal(0, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -213,43 +190,33 @@ public void FileAssertConfig_Run_WithNonMatchingFilter_SkipsTests() public void FileAssertConfig_Run_WithResultsFile_WritesTrxWithPassedOutcome() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - var trxFile = Path.Combine(Path.GetTempPath(), $"config_test_{Guid.NewGuid()}.trx"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "Copyright"); - - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "LicenseCheck" - files: - - pattern: "sample.txt" - text: - - contains: "Copyright" - """); - - var config = FileAssertConfig.ReadFromFile(configPath); - using var context = Context.Create(["--silent", "--results", trxFile]); - - // Act - config.Run(context, []); - - // Assert - TRX file contains the test name with Passed outcome - Assert.Equal(0, context.ExitCode); - Assert.True(File.Exists(trxFile)); - var trxContent = File.ReadAllText(trxFile); - Assert.Contains("LicenseCheck", trxContent); - Assert.Contains("outcome=\"Passed\"", trxContent); - } - finally - { - tempDir.Delete(recursive: true); - if (File.Exists(trxFile)) - { - File.Delete(trxFile); - } - } + using var tempDir = new TemporaryDirectory(); + var trxFile = tempDir.GetFilePath("results.trx"); + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "Copyright"); + + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "LicenseCheck" + files: + - pattern: "sample.txt" + text: + - contains: "Copyright" + """); + + var config = FileAssertConfig.ReadFromFile(configPath); + using var context = Context.Create(["--silent", "--results", trxFile]); + + // Act + config.Run(context, []); + + // Assert - TRX file contains the test name with Passed outcome + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(trxFile)); + var trxContent = File.ReadAllText(trxFile); + Assert.Contains("LicenseCheck", trxContent); + Assert.Contains("outcome=\"Passed\"", trxContent); + } /// @@ -259,44 +226,34 @@ public void FileAssertConfig_Run_WithResultsFile_WritesTrxWithPassedOutcome() public void FileAssertConfig_Run_WithResultsFile_WritesJUnitWithFailedOutcome() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - var xmlFile = Path.Combine(Path.GetTempPath(), $"config_test_{Guid.NewGuid()}.xml"); - try - { - // File does NOT contain the expected text - assertion will fail - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "no license here"); - - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "LicenseCheck" - files: - - pattern: "sample.txt" - text: - - contains: "Copyright" - """); - - var config = FileAssertConfig.ReadFromFile(configPath); - using var context = Context.Create(["--silent", "--results", xmlFile]); - - // Act - config.Run(context, []); - - // Assert - JUnit file contains the test name with a failure entry - Assert.NotEqual(0, context.ExitCode); - Assert.True(File.Exists(xmlFile)); - var xmlContent = File.ReadAllText(xmlFile); - Assert.Contains("LicenseCheck", xmlContent); - Assert.Contains("failures=\"1\"", xmlContent); - } - finally - { - tempDir.Delete(recursive: true); - if (File.Exists(xmlFile)) - { - File.Delete(xmlFile); - } - } + using var tempDir = new TemporaryDirectory(); + var xmlFile = tempDir.GetFilePath("results.xml"); + // File does NOT contain the expected text - assertion will fail + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "no license here"); + + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "LicenseCheck" + files: + - pattern: "sample.txt" + text: + - contains: "Copyright" + """); + + var config = FileAssertConfig.ReadFromFile(configPath); + using var context = Context.Create(["--silent", "--results", xmlFile]); + + // Act + config.Run(context, []); + + // Assert - JUnit file contains the test name with a failure entry + Assert.NotEqual(0, context.ExitCode); + Assert.True(File.Exists(xmlFile)); + var xmlContent = File.ReadAllText(xmlFile); + Assert.Contains("LicenseCheck", xmlContent); + Assert.Contains("failures=\"1\"", xmlContent); + } /// @@ -306,41 +263,35 @@ public void FileAssertConfig_Run_WithResultsFile_WritesJUnitWithFailedOutcome() public void FileAssertConfig_ReadFromFile_PdfAssertConfig_ParsesCorrectly() { // Arrange - write a config with a pdf: block containing metadata and pages - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - var configPath = Path.Combine(tempDir.FullName, "config.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "PDF Check" - files: - - pattern: "report.pdf" - pdf: - metadata: - - field: "Title" - contains: "Annual Report" - pages: - min: 1 - max: 100 - text: - - contains: "Summary" - """); - - // Act - var config = FileAssertConfig.ReadFromFile(configPath); - - // Assert - one test was parsed with one file assertion and populated PDF settings - Assert.Single(config.Tests); - Assert.Equal("PDF Check", config.Tests[0].Name); - Assert.Single(config.Tests[0].Files); - - var fileAssertion = config.Tests[0].Files[0]; - Assert.Equal("report.pdf", fileAssertion.Pattern); - Assert.NotNull(fileAssertion.PdfAssert); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + var configPath = tempDir.GetFilePath("config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "PDF Check" + files: + - pattern: "report.pdf" + pdf: + metadata: + - field: "Title" + contains: "Annual Report" + pages: + min: 1 + max: 100 + text: + - contains: "Summary" + """); + + // Act + var config = FileAssertConfig.ReadFromFile(configPath); + + // Assert - one test was parsed with one file assertion and populated PDF settings + Assert.Single(config.Tests); + Assert.Equal("PDF Check", config.Tests[0].Name); + Assert.Single(config.Tests[0].Files); + + var fileAssertion = config.Tests[0].Files[0]; + Assert.Equal("report.pdf", fileAssertion.Pattern); + Assert.NotNull(fileAssertion.PdfAssert); + } } diff --git a/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs b/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs index bad2a5b..42e7633 100644 --- a/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs @@ -256,45 +256,39 @@ public void IntegrationTest_UnknownArgument_ReturnsError() public void IntegrationTest_TestFiltering_OnlyRunsMatchingTests() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create a file that the smoke test will assert against - File.WriteAllText(Path.Combine(tempDir.FullName, "smoke.txt"), "smoke content"); - - // Write a config with a passing "smoke" test and a failing "regression" test - // (regression test references a file that does not exist with min: 1) - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "SmokeTest" - tags: [smoke] - files: - - pattern: "smoke.txt" - min: 1 - - name: "RegressionTest" - tags: [regression] - files: - - pattern: "missing.txt" - min: 1 - """); + using var tempDir = new TemporaryDirectory(); + // Create a file that the smoke test will assert against + File.WriteAllText(tempDir.GetFilePath("smoke.txt"), "smoke content"); + + // Write a config with a passing "smoke" test and a failing "regression" test + // (regression test references a file that does not exist with min: 1) + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "SmokeTest" + tags: [smoke] + files: + - pattern: "smoke.txt" + min: 1 + - name: "RegressionTest" + tags: [regression] + files: + - pattern: "missing.txt" + min: 1 + """); + + // Act - filter to only the "smoke" tag; the regression test should not run + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--config", + configPath, + "smoke"); - // Act - filter to only the "smoke" tag; the regression test should not run - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--config", - configPath, - "smoke"); + // Assert - exit code 0 because the failing regression test was skipped by the filter + Assert.Equal(0, exitCode); - // Assert - exit code 0 because the failing regression test was skipped by the filter - Assert.Equal(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -304,39 +298,33 @@ public void IntegrationTest_TestFiltering_OnlyRunsMatchingTests() public void IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create a file that satisfies the assertion - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "Copyright (c) DEMA Consulting"); - - // Write a config that asserts the file exists and contains the expected text - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "License Check" - files: - - pattern: "*.txt" - min: 1 - text: - - contains: "Copyright" - """); + using var tempDir = new TemporaryDirectory(); + // Create a file that satisfies the assertion + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "Copyright (c) DEMA Consulting"); + + // Write a config that asserts the file exists and contains the expected text + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "License Check" + files: + - pattern: "*.txt" + min: 1 + text: + - contains: "Copyright" + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--config", + configPath); + + // Assert + Assert.Equal(0, exitCode); - // Assert - Assert.Equal(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -346,39 +334,33 @@ public void IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero() public void IntegrationTest_ValidConfig_FailingAssertions_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create a file that does NOT satisfy the assertion - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "no license here"); - - // Write a config that asserts the file contains text it does not contain - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "License Check" - files: - - pattern: "*.txt" - text: - - contains: "Copyright" - """); + using var tempDir = new TemporaryDirectory(); + // Create a file that does NOT satisfy the assertion + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "no license here"); + + // Write a config that asserts the file contains text it does not contain + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "License Check" + files: + - pattern: "*.txt" + text: + - contains: "Copyright" + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero exit code indicates assertion failure + Assert.NotEqual(0, exitCode); - // Assert - non-zero exit code indicates assertion failure - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -388,49 +370,39 @@ public void IntegrationTest_ValidConfig_FailingAssertions_ReturnsNonZero() public void IntegrationTest_PassingAssertions_WritesTrxWithPassedResults() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - var resultsFile = Path.Combine(Path.GetTempPath(), $"integration_test_{Guid.NewGuid()}.trx"); - try - { - // Create a file that satisfies the assertion - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "Copyright (c) DEMA Consulting"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "LicenseCheck" - files: - - pattern: "*.txt" - min: 1 - text: - - contains: "Copyright" - """); + using var tempDir = new TemporaryDirectory(); + var resultsFile = tempDir.GetFilePath("results.trx"); + // Create a file that satisfies the assertion + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "Copyright (c) DEMA Consulting"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "LicenseCheck" + files: + - pattern: "*.txt" + min: 1 + text: + - contains: "Copyright" + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--config", - configPath, - "--results", - resultsFile); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--config", + configPath, + "--results", + resultsFile); + + // Assert - exit code 0 and TRX file contains LicenseCheck with Passed outcome + Assert.Equal(0, exitCode); + Assert.True(File.Exists(resultsFile), "Results file was not created"); + var trxContent = File.ReadAllText(resultsFile); + Assert.Contains("LicenseCheck", trxContent); + Assert.Contains("outcome=\"Passed\"", trxContent); - // Assert - exit code 0 and TRX file contains LicenseCheck with Passed outcome - Assert.Equal(0, exitCode); - Assert.True(File.Exists(resultsFile), "Results file was not created"); - var trxContent = File.ReadAllText(resultsFile); - Assert.Contains("LicenseCheck", trxContent); - Assert.Contains("outcome=\"Passed\"", trxContent); - } - finally - { - tempDir.Delete(recursive: true); - if (File.Exists(resultsFile)) - { - File.Delete(resultsFile); - } - } } /// @@ -440,49 +412,39 @@ public void IntegrationTest_PassingAssertions_WritesTrxWithPassedResults() public void IntegrationTest_FailingAssertions_WritesJUnitWithFailedResults() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - var resultsFile = Path.Combine(Path.GetTempPath(), $"integration_test_{Guid.NewGuid()}.xml"); - try - { - // Create a file that does NOT satisfy the assertion - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "no license here"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "LicenseCheck" - files: - - pattern: "*.txt" - text: - - contains: "Copyright" - """); + using var tempDir = new TemporaryDirectory(); + var resultsFile = tempDir.GetFilePath("results.xml"); + // Create a file that does NOT satisfy the assertion + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "no license here"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "LicenseCheck" + files: + - pattern: "*.txt" + text: + - contains: "Copyright" + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath, - "--results", - resultsFile); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath, + "--results", + resultsFile); + + // Assert - non-zero exit code and JUnit file contains LicenseCheck with a failure entry + Assert.NotEqual(0, exitCode); + Assert.True(File.Exists(resultsFile), "Results file was not created"); + var xmlContent = File.ReadAllText(resultsFile); + Assert.Contains("LicenseCheck", xmlContent); + Assert.Contains("failures=\"1\"", xmlContent); - // Assert - non-zero exit code and JUnit file contains LicenseCheck with a failure entry - Assert.NotEqual(0, exitCode); - Assert.True(File.Exists(resultsFile), "Results file was not created"); - var xmlContent = File.ReadAllText(resultsFile); - Assert.Contains("LicenseCheck", xmlContent); - Assert.Contains("failures=\"1\"", xmlContent); - } - finally - { - tempDir.Delete(recursive: true); - if (File.Exists(resultsFile)) - { - File.Delete(resultsFile); - } - } } /// @@ -492,35 +454,29 @@ public void IntegrationTest_FailingAssertions_WritesJUnitWithFailedResults() public void IntegrationTest_MinCountConstraint_TooFewFiles_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create no files when the test requires at least one - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "RequiredFileCheck" - files: - - pattern: "*.txt" - min: 1 - """); + using var tempDir = new TemporaryDirectory(); + // Create no files when the test requires at least one + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "RequiredFileCheck" + files: + - pattern: "*.txt" + min: 1 + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero exit code because the min count constraint was not met + Assert.NotEqual(0, exitCode); - // Assert - non-zero exit code because the min count constraint was not met - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -530,38 +486,32 @@ public void IntegrationTest_MinCountConstraint_TooFewFiles_ReturnsNonZero() public void IntegrationTest_MaxCountConstraint_TooManyFiles_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create two files when the test asserts at most one - File.WriteAllText(Path.Combine(tempDir.FullName, "a.txt"), "content a"); - File.WriteAllText(Path.Combine(tempDir.FullName, "b.txt"), "content b"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "UniqueFileCheck" - files: - - pattern: "*.txt" - max: 1 - """); + using var tempDir = new TemporaryDirectory(); + // Create two files when the test asserts at most one + File.WriteAllText(tempDir.GetFilePath("a.txt"), "content a"); + File.WriteAllText(tempDir.GetFilePath("b.txt"), "content b"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "UniqueFileCheck" + files: + - pattern: "*.txt" + max: 1 + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero exit code because the max count constraint was exceeded + Assert.NotEqual(0, exitCode); - // Assert - non-zero exit code because the max count constraint was exceeded - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -571,37 +521,31 @@ public void IntegrationTest_MaxCountConstraint_TooManyFiles_ReturnsNonZero() public void IntegrationTest_RegexRule_MatchingContent_ReturnsZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create a file whose content matches the regex pattern - File.WriteAllText(Path.Combine(tempDir.FullName, "version.txt"), "Version: 1.2.3"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "VersionFormatCheck" - files: - - pattern: "version.txt" - text: - - matches: "\\d+\\.\\d+\\.\\d+" - """); + using var tempDir = new TemporaryDirectory(); + // Create a file whose content matches the regex pattern + File.WriteAllText(tempDir.GetFilePath("version.txt"), "Version: 1.2.3"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "VersionFormatCheck" + files: + - pattern: "version.txt" + text: + - matches: "\\d+\\.\\d+\\.\\d+" + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--config", + configPath); + + // Assert + Assert.Equal(0, exitCode); - // Assert - Assert.Equal(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -611,38 +555,32 @@ public void IntegrationTest_RegexRule_MatchingContent_ReturnsZero() public void IntegrationTest_RegexRule_NonMatchingContent_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create a file whose content does NOT match the version regex - File.WriteAllText(Path.Combine(tempDir.FullName, "version.txt"), "no version here"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "VersionFormatCheck" - files: - - pattern: "version.txt" - text: - - matches: "\\d+\\.\\d+\\.\\d+" - """); + using var tempDir = new TemporaryDirectory(); + // Create a file whose content does NOT match the version regex + File.WriteAllText(tempDir.GetFilePath("version.txt"), "no version here"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "VersionFormatCheck" + files: + - pattern: "version.txt" + text: + - matches: "\\d+\\.\\d+\\.\\d+" + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero because the file does not match the version pattern + Assert.NotEqual(0, exitCode); - // Assert - non-zero because the file does not match the version pattern - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -652,38 +590,32 @@ public void IntegrationTest_RegexRule_NonMatchingContent_ReturnsNonZero() public void IntegrationTest_ExactCountConstraint_WrongCount_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create two files when the test asserts exactly one - File.WriteAllText(Path.Combine(tempDir.FullName, "a.txt"), "content a"); - File.WriteAllText(Path.Combine(tempDir.FullName, "b.txt"), "content b"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "ExactCountCheck" - files: - - pattern: "*.txt" - count: 1 - """); + using var tempDir = new TemporaryDirectory(); + // Create two files when the test asserts exactly one + File.WriteAllText(tempDir.GetFilePath("a.txt"), "content a"); + File.WriteAllText(tempDir.GetFilePath("b.txt"), "content b"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "ExactCountCheck" + files: + - pattern: "*.txt" + count: 1 + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero exit code because the exact count constraint was not met + Assert.NotEqual(0, exitCode); - // Assert - non-zero exit code because the exact count constraint was not met - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -693,37 +625,31 @@ public void IntegrationTest_ExactCountConstraint_WrongCount_ReturnsNonZero() public void IntegrationTest_FileSizeConstraints_TooSmall_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create an empty file when the test requires at least 10 bytes - File.WriteAllText(Path.Combine(tempDir.FullName, "empty.txt"), string.Empty); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "MinSizeCheck" - files: - - pattern: "*.txt" - min-size: 10 - """); + using var tempDir = new TemporaryDirectory(); + // Create an empty file when the test requires at least 10 bytes + File.WriteAllText(tempDir.GetFilePath("empty.txt"), string.Empty); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "MinSizeCheck" + files: + - pattern: "*.txt" + min-size: 10 + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero exit code because the file is smaller than the minimum size + Assert.NotEqual(0, exitCode); - // Assert - non-zero exit code because the file is smaller than the minimum size - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -733,37 +659,31 @@ public void IntegrationTest_FileSizeConstraints_TooSmall_ReturnsNonZero() public void IntegrationTest_FileSizeConstraints_TooLarge_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create a file with content larger than 5 bytes - File.WriteAllText(Path.Combine(tempDir.FullName, "large.txt"), "this content is more than five bytes"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "MaxSizeCheck" - files: - - pattern: "*.txt" - max-size: 5 - """); + using var tempDir = new TemporaryDirectory(); + // Create a file with content larger than 5 bytes + File.WriteAllText(tempDir.GetFilePath("large.txt"), "this content is more than five bytes"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "MaxSizeCheck" + files: + - pattern: "*.txt" + max-size: 5 + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero exit code because the file exceeds the maximum size + Assert.NotEqual(0, exitCode); - // Assert - non-zero exit code because the file exceeds the maximum size - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -773,38 +693,32 @@ public void IntegrationTest_FileSizeConstraints_TooLarge_ReturnsNonZero() public void IntegrationTest_DoesNotContainRule_ForbiddenTextPresent_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create a file that contains the forbidden text - File.WriteAllText(Path.Combine(tempDir.FullName, "config.txt"), "password123=secret"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "NoSecretsCheck" - files: - - pattern: "*.txt" - text: - - does-not-contain: "password123" - """); + using var tempDir = new TemporaryDirectory(); + // Create a file that contains the forbidden text + File.WriteAllText(tempDir.GetFilePath("config.txt"), "password123=secret"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "NoSecretsCheck" + files: + - pattern: "*.txt" + text: + - does-not-contain: "password123" + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero exit code because the forbidden text is present + Assert.NotEqual(0, exitCode); - // Assert - non-zero exit code because the forbidden text is present - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -814,38 +728,32 @@ public void IntegrationTest_DoesNotContainRule_ForbiddenTextPresent_ReturnsNonZe public void IntegrationTest_DoesNotContainRegexRule_ForbiddenPatternMatches_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - // Create a file that matches the forbidden pattern - File.WriteAllText(Path.Combine(tempDir.FullName, "app.log"), "FATAL: unexpected error occurred"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "NoFatalErrorsCheck" - files: - - pattern: "*.log" - text: - - does-not-contain-regex: "FATAL|ERROR" - """); + using var tempDir = new TemporaryDirectory(); + // Create a file that matches the forbidden pattern + File.WriteAllText(tempDir.GetFilePath("app.log"), "FATAL: unexpected error occurred"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "NoFatalErrorsCheck" + files: + - pattern: "*.log" + text: + - does-not-contain-regex: "FATAL|ERROR" + """); - // Act - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--silent", - "--config", - configPath); + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero exit code because the forbidden pattern matched + Assert.NotEqual(0, exitCode); - // Assert - non-zero exit code because the forbidden pattern matched - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -855,36 +763,30 @@ public void IntegrationTest_DoesNotContainRegexRule_ForbiddenPatternMatches_Retu public void IntegrationTest_XmlAssert_PassingQuery_ReturnsZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "config.xml"), """ - - production - - """); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "XmlCheck" - files: - - pattern: "*.xml" - xml: - - query: "//configuration/setting" - min: 1 - """); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("config.xml"), """ + + production + + """); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "XmlCheck" + files: + - pattern: "*.xml" + xml: + - query: "//configuration/setting" + min: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.Equal(0, exitCode); - // Assert - Assert.Equal(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -894,32 +796,26 @@ public void IntegrationTest_XmlAssert_PassingQuery_ReturnsZero() public void IntegrationTest_XmlAssert_InvalidFile_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "config.xml"), "this is not xml <<>>"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "XmlCheck" - files: - - pattern: "*.xml" - xml: - - query: "//configuration" - min: 1 - """); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("config.xml"), "this is not xml <<>>"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "XmlCheck" + files: + - pattern: "*.xml" + xml: + - query: "//configuration" + min: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); - // Assert - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -929,37 +825,31 @@ public void IntegrationTest_XmlAssert_InvalidFile_ReturnsNonZero() public void IntegrationTest_HtmlAssert_PassingQuery_ReturnsZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "index.html"), """ - - Test Page -

Hello

- - """); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "HtmlCheck" - files: - - pattern: "*.html" - html: - - query: "//head/title" - count: 1 - """); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("index.html"), """ + + Test Page +

Hello

+ + """); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "HtmlCheck" + files: + - pattern: "*.html" + html: + - query: "//head/title" + count: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.Equal(0, exitCode); - // Assert - Assert.Equal(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -969,36 +859,30 @@ public void IntegrationTest_HtmlAssert_PassingQuery_ReturnsZero() public void IntegrationTest_YamlAssert_PassingQuery_ReturnsZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "appsettings.yaml"), """ - server: - host: localhost - port: 8080 - """); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "YamlCheck" - files: - - pattern: "appsettings.yaml" - yaml: - - query: "server.host" - count: 1 - """); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("appsettings.yaml"), """ + server: + host: localhost + port: 8080 + """); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "YamlCheck" + files: + - pattern: "appsettings.yaml" + yaml: + - query: "server.host" + count: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.Equal(0, exitCode); - // Assert - Assert.Equal(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -1008,38 +892,32 @@ public void IntegrationTest_YamlAssert_PassingQuery_ReturnsZero() public void IntegrationTest_JsonAssert_PassingQuery_ReturnsZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "appsettings.json"), """ - { - "ConnectionStrings": { - "DefaultConnection": "Server=localhost" - } - } - """); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "JsonCheck" - files: - - pattern: "appsettings.json" - json: - - query: "ConnectionStrings" - count: 1 - """); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("appsettings.json"), """ + { + "ConnectionStrings": { + "DefaultConnection": "Server=localhost" + } + } + """); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "JsonCheck" + files: + - pattern: "appsettings.json" + json: + - query: "ConnectionStrings" + count: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.Equal(0, exitCode); - // Assert - Assert.Equal(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -1049,37 +927,31 @@ public void IntegrationTest_JsonAssert_PassingQuery_ReturnsZero() public void IntegrationTest_ZipAssert_PassingQuery_ReturnsZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try + using var tempDir = new TemporaryDirectory(); + var zipPath = tempDir.GetFilePath("archive.zip"); + using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create)) { - var zipPath = Path.Combine(tempDir.FullName, "archive.zip"); - using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create)) - { - zip.CreateEntry("readme.txt"); - } + zip.CreateEntry("readme.txt"); + } - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: ZipCheck - files: - - pattern: "*.zip" - zip: - entries: - - pattern: "*.txt" - min: 1 - """); + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: ZipCheck + files: + - pattern: "*.zip" + zip: + entries: + - pattern: "*.txt" + min: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.Equal(0, exitCode); - // Assert - Assert.Equal(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -1089,34 +961,28 @@ public void IntegrationTest_ZipAssert_PassingQuery_ReturnsZero() public void IntegrationTest_ZipAssert_InvalidFile_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - var invalidZipPath = Path.Combine(tempDir.FullName, "invalid.zip"); - File.WriteAllText(invalidZipPath, "this is not a zip file"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: ZipInvalidCheck - files: - - pattern: "*.zip" - zip: - entries: - - pattern: "*.txt" - min: 1 - """); + using var tempDir = new TemporaryDirectory(); + var invalidZipPath = tempDir.GetFilePath("invalid.zip"); + File.WriteAllText(invalidZipPath, "this is not a zip file"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: ZipInvalidCheck + files: + - pattern: "*.zip" + zip: + entries: + - pattern: "*.txt" + min: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); - // Assert - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -1126,37 +992,31 @@ public void IntegrationTest_ZipAssert_InvalidFile_ReturnsNonZero() public void IntegrationTest_HtmlAssert_InvalidFile_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "index.html"), """ - - Test Page -

Hello

- - """); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "HtmlInvalidCheck" - files: - - pattern: "*.html" - html: - - query: "//nonexistentElement" - min: 1 - """); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("index.html"), """ + + Test Page +

Hello

+ + """); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "HtmlInvalidCheck" + files: + - pattern: "*.html" + html: + - query: "//nonexistentElement" + min: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); - // Assert - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -1166,32 +1026,26 @@ public void IntegrationTest_HtmlAssert_InvalidFile_ReturnsNonZero() public void IntegrationTest_YamlAssert_InvalidFile_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "invalid.yaml"), ": invalid yaml\n - bad"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "YamlInvalidCheck" - files: - - pattern: "invalid.yaml" - yaml: - - query: "server.host" - count: 1 - """); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("invalid.yaml"), ": invalid yaml\n - bad"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "YamlInvalidCheck" + files: + - pattern: "invalid.yaml" + yaml: + - query: "server.host" + count: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); - // Assert - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -1201,32 +1055,26 @@ public void IntegrationTest_YamlAssert_InvalidFile_ReturnsNonZero() public void IntegrationTest_JsonAssert_InvalidFile_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "invalid.json"), "{invalid json"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "JsonInvalidCheck" - files: - - pattern: "invalid.json" - json: - - query: "ConnectionStrings" - count: 1 - """); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("invalid.json"), "{invalid json"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "JsonInvalidCheck" + files: + - pattern: "invalid.json" + json: + - query: "ConnectionStrings" + count: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); - // Assert - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -1236,35 +1084,29 @@ public void IntegrationTest_JsonAssert_InvalidFile_ReturnsNonZero() public void IntegrationTest_PdfAssert_FailingAssertion_ReturnsNonZero() { // Arrange - build a valid single-page PDF and assert a minimum of 2 pages (will fail) - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - var pdfPath = Path.Combine(tempDir.FullName, "report.pdf"); - using var builder = new PdfDocumentBuilder(); - builder.AddPage(PageSize.A4); - File.WriteAllBytes(pdfPath, builder.Build()); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "PdfCheck" - files: - - pattern: "*.pdf" - pdf: - pages: - min: 2 - """); + using var tempDir = new TemporaryDirectory(); + var pdfPath = tempDir.GetFilePath("report.pdf"); + using var builder = new PdfDocumentBuilder(); + builder.AddPage(PageSize.A4); + File.WriteAllBytes(pdfPath, builder.Build()); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "PdfCheck" + files: + - pattern: "*.pdf" + pdf: + pages: + min: 2 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); - // Assert - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -1274,31 +1116,25 @@ public void IntegrationTest_PdfAssert_FailingAssertion_ReturnsNonZero() public void IntegrationTest_PdfAssert_InvalidFile_ReturnsNonZero() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "report.pdf"), "not a real pdf"); - - var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); - File.WriteAllText(configPath, """ - tests: - - name: "PdfCheck" - files: - - pattern: "*.pdf" - pdf: - pages: - min: 1 - """); + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("report.pdf"), "not a real pdf"); + + var configPath = tempDir.GetFilePath(".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "PdfCheck" + files: + - pattern: "*.pdf" + pdf: + pages: + min: 1 + """); - // Act - var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + // Act + var exitCode = Runner.Run(out var _, "dotnet", _dllPath, "--silent", "--config", configPath); + + // Assert + Assert.NotEqual(0, exitCode); - // Assert - Assert.NotEqual(0, exitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } } diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs index e90cc07..f11ce03 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs @@ -22,6 +22,7 @@ using DemaConsulting.FileAssert.Configuration; using DemaConsulting.FileAssert.Modeling; +using DemaConsulting.FileAssert.Utilities; namespace DemaConsulting.FileAssert.Tests.Modeling; /// @@ -94,23 +95,17 @@ public void FileAssertFile_Create_BlankPattern_ThrowsInvalidOperationException() public void FileAssertFile_Run_NoMatchingFiles_NoConstraints_NoError() { // Arrange - use an empty temp directory so the pattern matches nothing - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - var data = new FileAssertFileData { Pattern = "*.txt" }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); + using var tempDir = new TemporaryDirectory(); + var data = new FileAssertFileData { Pattern = "*.txt" }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); - // Act - file.Run(context, tempDir.FullName); + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert + Assert.Equal(0, context.ExitCode); - // Assert - Assert.Equal(0, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -120,24 +115,18 @@ public void FileAssertFile_Run_NoMatchingFiles_NoConstraints_NoError() public void FileAssertFile_Run_WithMatchingFiles_NoConstraints_NoError() { // Arrange - create a temp file for the pattern to match - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "content"); - var data = new FileAssertFileData { Pattern = "*.txt" }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - file.Run(context, tempDir.FullName); - - // Assert - Assert.Equal(0, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "content"); + var data = new FileAssertFileData { Pattern = "*.txt" }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert + Assert.Equal(0, context.ExitCode); + } /// @@ -147,23 +136,17 @@ public void FileAssertFile_Run_WithMatchingFiles_NoConstraints_NoError() public void FileAssertFile_Run_TooFewFiles_WritesError() { // Arrange - empty directory so zero files match, but min requires at least 1 - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - var data = new FileAssertFileData { Pattern = "*.txt", Min = 1 }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); + using var tempDir = new TemporaryDirectory(); + var data = new FileAssertFileData { Pattern = "*.txt", Min = 1 }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); - // Act - file.Run(context, tempDir.FullName); + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert + Assert.Equal(1, context.ExitCode); - // Assert - Assert.Equal(1, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -173,25 +156,19 @@ public void FileAssertFile_Run_TooFewFiles_WritesError() public void FileAssertFile_Run_TooManyFiles_WritesError() { // Arrange - create two files but constrain max to 1 - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "a.txt"), "content a"); - File.WriteAllText(Path.Combine(tempDir.FullName, "b.txt"), "content b"); - var data = new FileAssertFileData { Pattern = "*.txt", Max = 1 }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - file.Run(context, tempDir.FullName); - - // Assert - Assert.Equal(1, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("a.txt"), "content a"); + File.WriteAllText(tempDir.GetFilePath("b.txt"), "content b"); + var data = new FileAssertFileData { Pattern = "*.txt", Max = 1 }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert + Assert.Equal(1, context.ExitCode); + } /// @@ -201,28 +178,22 @@ public void FileAssertFile_Run_TooManyFiles_WritesError() public void FileAssertFile_Run_WithContentRule_ContentContainsValue_NoError() { // Arrange - create a file that satisfies the contains rule - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("check.txt"), "expected content here"); + var data = new FileAssertFileData { - File.WriteAllText(Path.Combine(tempDir.FullName, "check.txt"), "expected content here"); - var data = new FileAssertFileData - { - Pattern = "*.txt", - Text = [new FileAssertRuleData { Contains = "expected content" }] - }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - file.Run(context, tempDir.FullName); - - // Assert - Assert.Equal(0, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + Pattern = "*.txt", + Text = [new FileAssertRuleData { Contains = "expected content" }] + }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert + Assert.Equal(0, context.ExitCode); + } /// @@ -232,28 +203,22 @@ public void FileAssertFile_Run_WithContentRule_ContentContainsValue_NoError() public void FileAssertFile_Run_WithContentRule_ContentMissingValue_WritesError() { // Arrange - create a file that does NOT satisfy the contains rule - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("check.txt"), "unrelated content"); + var data = new FileAssertFileData { - File.WriteAllText(Path.Combine(tempDir.FullName, "check.txt"), "unrelated content"); - var data = new FileAssertFileData - { - Pattern = "*.txt", - Text = [new FileAssertRuleData { Contains = "expected content" }] - }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - file.Run(context, tempDir.FullName); - - // Assert - Assert.Equal(1, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + Pattern = "*.txt", + Text = [new FileAssertRuleData { Contains = "expected content" }] + }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert + Assert.Equal(1, context.ExitCode); + } /// @@ -263,25 +228,19 @@ public void FileAssertFile_Run_WithContentRule_ContentMissingValue_WritesError() public void FileAssertFile_Run_WrongCount_WritesError() { // Arrange - create two files but constrain count to exactly 1 - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "a.txt"), "content a"); - File.WriteAllText(Path.Combine(tempDir.FullName, "b.txt"), "content b"); - var data = new FileAssertFileData { Pattern = "*.txt", Count = 1 }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - file.Run(context, tempDir.FullName); - - // Assert - Assert.Equal(1, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("a.txt"), "content a"); + File.WriteAllText(tempDir.GetFilePath("b.txt"), "content b"); + var data = new FileAssertFileData { Pattern = "*.txt", Count = 1 }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert + Assert.Equal(1, context.ExitCode); + } /// @@ -291,24 +250,18 @@ public void FileAssertFile_Run_WrongCount_WritesError() public void FileAssertFile_Run_TooSmall_WritesError() { // Arrange - create an empty file and require at least 10 bytes - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "small.txt"), string.Empty); - var data = new FileAssertFileData { Pattern = "*.txt", MinSize = 10 }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - file.Run(context, tempDir.FullName); - - // Assert - Assert.Equal(1, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("small.txt"), string.Empty); + var data = new FileAssertFileData { Pattern = "*.txt", MinSize = 10 }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert + Assert.Equal(1, context.ExitCode); + } /// @@ -318,24 +271,18 @@ public void FileAssertFile_Run_TooSmall_WritesError() public void FileAssertFile_Run_TooLarge_WritesError() { // Arrange - create a file with content larger than 5 bytes - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "large.txt"), "this content is more than five bytes"); - var data = new FileAssertFileData { Pattern = "*.txt", MaxSize = 5 }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - file.Run(context, tempDir.FullName); - - // Assert - Assert.Equal(1, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("large.txt"), "this content is more than five bytes"); + var data = new FileAssertFileData { Pattern = "*.txt", MaxSize = 5 }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert + Assert.Equal(1, context.ExitCode); + } /// @@ -346,27 +293,21 @@ public void FileAssertFile_Run_TooLarge_WritesError() public void FileAssertFile_Run_MultipleFiles_MultipleViolateSizeConstraints_WritesErrorForEachViolation() { // Arrange - three files: one within bounds, one too small, one too large - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "ok.txt"), "valid"); - File.WriteAllText(Path.Combine(tempDir.FullName, "small.txt"), string.Empty); - File.WriteAllText(Path.Combine(tempDir.FullName, "large.txt"), "this file is too large"); - var data = new FileAssertFileData { Pattern = "*.txt", MinSize = 2, MaxSize = 10 }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - file.Run(context, tempDir.FullName); - - // Assert - both invalid files should trigger errors regardless of enumeration order - Assert.Equal(1, context.ExitCode); - Assert.Equal(2, context.ErrorCount); - } - finally - { - tempDir.Delete(recursive: true); - } + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("ok.txt"), "valid"); + File.WriteAllText(tempDir.GetFilePath("small.txt"), string.Empty); + File.WriteAllText(tempDir.GetFilePath("large.txt"), "this file is too large"); + var data = new FileAssertFileData { Pattern = "*.txt", MinSize = 2, MaxSize = 10 }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert - both invalid files should trigger errors regardless of enumeration order + Assert.Equal(1, context.ExitCode); + Assert.Equal(2, context.ErrorCount); + } /// @@ -377,30 +318,24 @@ public void FileAssertFile_Run_MultipleFiles_MultipleViolateSizeConstraints_Writ public void FileAssertFile_Run_MultipleFiles_MultipleFailContentRule_WritesErrorForEachViolation() { // Arrange - three files: one with the required content, two without - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("good.txt"), "expected content here"); + File.WriteAllText(tempDir.GetFilePath("bad1.txt"), "unrelated content"); + File.WriteAllText(tempDir.GetFilePath("bad2.txt"), "also unrelated"); + var data = new FileAssertFileData { - File.WriteAllText(Path.Combine(tempDir.FullName, "good.txt"), "expected content here"); - File.WriteAllText(Path.Combine(tempDir.FullName, "bad1.txt"), "unrelated content"); - File.WriteAllText(Path.Combine(tempDir.FullName, "bad2.txt"), "also unrelated"); - var data = new FileAssertFileData - { - Pattern = "*.txt", - Text = [new FileAssertRuleData { Contains = "expected content" }] - }; - var file = FileAssertFile.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - file.Run(context, tempDir.FullName); - - // Assert - both bad files should trigger errors regardless of enumeration order - Assert.Equal(1, context.ExitCode); - Assert.Equal(2, context.ErrorCount); - } - finally - { - tempDir.Delete(recursive: true); - } + Pattern = "*.txt", + Text = [new FileAssertRuleData { Contains = "expected content" }] + }; + var file = FileAssertFile.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + file.Run(context, tempDir.DirectoryPath); + + // Assert - both bad files should trigger errors regardless of enumeration order + Assert.Equal(1, context.ExitCode); + Assert.Equal(2, context.ErrorCount); + } } diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs index babbe4b..3ac003a 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs @@ -22,6 +22,7 @@ using DemaConsulting.FileAssert.Configuration; using DemaConsulting.FileAssert.Modeling; +using DemaConsulting.FileAssert.Utilities; namespace DemaConsulting.FileAssert.Tests.Modeling; /// @@ -205,28 +206,22 @@ public void FileAssertTest_MatchesFilter_CaseInsensitiveTag_ReturnsTrue() public void FileAssertTest_Run_RunsAllFiles() { // Arrange - create a temp directory with a file matching the pattern - var tempDir = Directory.CreateTempSubdirectory("fileassert_test_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "content"); - var data = new FileAssertTestData - { - Name = "Run Test", - Files = [new FileAssertFileData { Pattern = "*.txt", Min = 1 }] - }; - var test = FileAssertTest.Create(data); - using var context = Context.Create(["--silent"]); - - // Act - test.Run(context, tempDir.FullName); - - // Assert - min=1 would have produced an error if the file had not been found - Assert.Equal(0, context.ExitCode); - } - finally + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "content"); + var data = new FileAssertTestData { - tempDir.Delete(recursive: true); - } + Name = "Run Test", + Files = [new FileAssertFileData { Pattern = "*.txt", Min = 1 }] + }; + var test = FileAssertTest.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + test.Run(context, tempDir.DirectoryPath); + + // Assert - min=1 would have produced an error if the file had not been found + Assert.Equal(0, context.ExitCode); + } /// diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingTests.cs index b93f056..159e796 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingTests.cs @@ -22,6 +22,7 @@ using DemaConsulting.FileAssert.Configuration; using DemaConsulting.FileAssert.Modeling; +using DemaConsulting.FileAssert.Utilities; namespace DemaConsulting.FileAssert.Tests.Modeling; /// @@ -38,41 +39,35 @@ public class ModelingTests public void Modeling_ExecuteChain_PassesWhenAllConstraintsMet() { // Arrange - create a real file with content that satisfies all rules - var tempDir = Directory.CreateTempSubdirectory("fileassert_modeling_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "Copyright (c) DEMA Consulting"); - - var testData = new FileAssertTestData - { - Name = "License Check", - Files = - [ - new FileAssertFileData - { - Pattern = "*.txt", - Min = 1, - Text = - [ - new FileAssertRuleData { Contains = "Copyright" } - ] - } - ] - }; - - var test = FileAssertTest.Create(testData); - using var context = Context.Create(["--silent"]); - - // Act - test.Run(context, tempDir.FullName); - - // Assert - no errors reported - Assert.Equal(0, context.ExitCode); - } - finally + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "Copyright (c) DEMA Consulting"); + + var testData = new FileAssertTestData { - tempDir.Delete(recursive: true); - } + Name = "License Check", + Files = + [ + new FileAssertFileData + { + Pattern = "*.txt", + Min = 1, + Text = + [ + new FileAssertRuleData { Contains = "Copyright" } + ] + } + ] + }; + + var test = FileAssertTest.Create(testData); + using var context = Context.Create(["--silent"]); + + // Act + test.Run(context, tempDir.DirectoryPath); + + // Assert - no errors reported + Assert.Equal(0, context.ExitCode); + } /// @@ -83,40 +78,34 @@ public void Modeling_ExecuteChain_PassesWhenAllConstraintsMet() public void Modeling_ExecuteChain_ReportsFailuresThroughContext() { // Arrange - create a file that does NOT contain the required text - var tempDir = Directory.CreateTempSubdirectory("fileassert_modeling_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "no license header here"); - - var testData = new FileAssertTestData - { - Name = "License Check", - Files = - [ - new FileAssertFileData - { - Pattern = "*.txt", - Text = - [ - new FileAssertRuleData { Contains = "Copyright" } - ] - } - ] - }; - - var test = FileAssertTest.Create(testData); - using var context = Context.Create(["--silent"]); - - // Act - test.Run(context, tempDir.FullName); - - // Assert - an error was reported and the exit code is non-zero - Assert.Equal(1, context.ExitCode); - } - finally + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("sample.txt"), "no license header here"); + + var testData = new FileAssertTestData { - tempDir.Delete(recursive: true); - } + Name = "License Check", + Files = + [ + new FileAssertFileData + { + Pattern = "*.txt", + Text = + [ + new FileAssertRuleData { Contains = "Copyright" } + ] + } + ] + }; + + var test = FileAssertTest.Create(testData); + using var context = Context.Create(["--silent"]); + + // Act + test.Run(context, tempDir.DirectoryPath); + + // Assert - an error was reported and the exit code is non-zero + Assert.Equal(1, context.ExitCode); + } /// @@ -127,40 +116,34 @@ public void Modeling_ExecuteChain_ReportsFailuresThroughContext() public void Modeling_FileTypeParsing_InvalidXml_ReportsParseError() { // Arrange - create a file with invalid XML content - var tempDir = Directory.CreateTempSubdirectory("fileassert_modeling_"); - try - { - File.WriteAllText(Path.Combine(tempDir.FullName, "config.xml"), "this is not valid xml <<>>"); - - var testData = new FileAssertTestData - { - Name = "XmlCheck", - Files = - [ - new FileAssertFileData - { - Pattern = "*.xml", - Xml = - [ - new FileAssertQueryData { Query = "//root", Count = 1 } - ] - } - ] - }; - - var test = FileAssertTest.Create(testData); - using var context = Context.Create(["--silent"]); - - // Act - test.Run(context, tempDir.FullName); - - // Assert - an error was reported because the file could not be parsed as XML - Assert.Equal(1, context.ExitCode); - } - finally + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("config.xml"), "this is not valid xml <<>>"); + + var testData = new FileAssertTestData { - tempDir.Delete(recursive: true); - } + Name = "XmlCheck", + Files = + [ + new FileAssertFileData + { + Pattern = "*.xml", + Xml = + [ + new FileAssertQueryData { Query = "//root", Count = 1 } + ] + } + ] + }; + + var test = FileAssertTest.Create(testData); + using var context = Context.Create(["--silent"]); + + // Act + test.Run(context, tempDir.DirectoryPath); + + // Assert - an error was reported because the file could not be parsed as XML + Assert.Equal(1, context.ExitCode); + } /// @@ -171,44 +154,38 @@ public void Modeling_FileTypeParsing_InvalidXml_ReportsParseError() public void Modeling_QueryAssertions_XmlQueryMeetsCount_NoError() { // Arrange - create a valid XML file with elements the query will match - var tempDir = Directory.CreateTempSubdirectory("fileassert_modeling_"); - try + using var tempDir = new TemporaryDirectory(); + File.WriteAllText(tempDir.GetFilePath("config.xml"), """ + + production + false + + """); + + var testData = new FileAssertTestData { - File.WriteAllText(Path.Combine(tempDir.FullName, "config.xml"), """ - - production - false - - """); - - var testData = new FileAssertTestData - { - Name = "XmlQueryCheck", - Files = - [ - new FileAssertFileData - { - Pattern = "*.xml", - Xml = - [ - new FileAssertQueryData { Query = "//configuration/setting", Min = 1 } - ] - } - ] - }; - - var test = FileAssertTest.Create(testData); - using var context = Context.Create(["--silent"]); - - // Act - test.Run(context, tempDir.FullName); - - // Assert - no errors reported because the query matched the expected count - Assert.Equal(0, context.ExitCode); - } - finally - { - tempDir.Delete(recursive: true); - } + Name = "XmlQueryCheck", + Files = + [ + new FileAssertFileData + { + Pattern = "*.xml", + Xml = + [ + new FileAssertQueryData { Query = "//configuration/setting", Min = 1 } + ] + } + ] + }; + + var test = FileAssertTest.Create(testData); + using var context = Context.Create(["--silent"]); + + // Act + test.Run(context, tempDir.DirectoryPath); + + // Assert - no errors reported because the query matched the expected count + Assert.Equal(0, context.ExitCode); + } } diff --git a/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestTests.cs b/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestTests.cs index 41c4200..bcd1fc7 100644 --- a/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestTests.cs @@ -21,6 +21,7 @@ using DemaConsulting.FileAssert.Cli; using DemaConsulting.FileAssert.SelfTest; +using DemaConsulting.FileAssert.Utilities; namespace DemaConsulting.FileAssert.Tests.SelfTest; /// @@ -37,33 +38,27 @@ public class SelfTestTests public void SelfTest_Run_ExecutesBuiltInTestsAndProducesSummary() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_selftest_"); - try + using var tempDir = new TemporaryDirectory(); + var logPath = tempDir.GetFilePath("validation.log"); + int exitCode; + + using (var context = Context.Create(["--silent", "--log", logPath])) { - var logPath = Path.Combine(tempDir.FullName, "validation.log"); - int exitCode; + // Act + Validation.Run(context); - using (var context = Context.Create(["--silent", "--log", logPath])) - { - // Act - Validation.Run(context); + // Capture exit code before disposal + exitCode = context.ExitCode; + } - // Capture exit code before disposal - exitCode = context.ExitCode; - } + // Assert - context is disposed above so the log file is fully flushed and closed + Assert.Equal(0, exitCode); - // Assert - context is disposed above so the log file is fully flushed and closed - Assert.Equal(0, exitCode); + var logContent = File.ReadAllText(logPath); + Assert.Contains("Total Tests:", logContent); + Assert.Contains("Passed:", logContent); + Assert.Contains("Failed:", logContent); - var logContent = File.ReadAllText(logPath); - Assert.Contains("Total Tests:", logContent); - Assert.Contains("Passed:", logContent); - Assert.Contains("Failed:", logContent); - } - finally - { - tempDir.Delete(recursive: true); - } } /// @@ -73,27 +68,21 @@ public void SelfTest_Run_ExecutesBuiltInTestsAndProducesSummary() public void SelfTest_Run_WhenInvoked_PrintsSystemInfoHeader() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_selftest_"); - try - { - var logPath = Path.Combine(tempDir.FullName, "validation.log"); - - using (var context = Context.Create(["--silent", "--log", logPath])) - { - // Act - Validation.Run(context); - } - - // Assert - system information header must appear in the log - var logContent = File.ReadAllText(logPath); - Assert.Contains("Tool Version", logContent); - Assert.Contains("Machine Name", logContent); - Assert.Contains("OS Version", logContent); - } - finally + using var tempDir = new TemporaryDirectory(); + var logPath = tempDir.GetFilePath("validation.log"); + + using (var context = Context.Create(["--silent", "--log", logPath])) { - tempDir.Delete(recursive: true); + // Act + Validation.Run(context); } + + // Assert - system information header must appear in the log + var logContent = File.ReadAllText(logPath); + Assert.Contains("Tool Version", logContent); + Assert.Contains("Machine Name", logContent); + Assert.Contains("OS Version", logContent); + } /// @@ -103,25 +92,19 @@ public void SelfTest_Run_WhenInvoked_PrintsSystemInfoHeader() public void SelfTest_Run_WithResultsFile_WritesTrxResultsFile() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_selftest_"); - try - { - var resultsPath = Path.Combine(tempDir.FullName, "results.trx"); - - using (var context = Context.Create(["--silent", "--results", resultsPath])) - { - // Act - Validation.Run(context); - } - - // Assert - TRX results file must exist and contain test result content - Assert.True(File.Exists(resultsPath), "TRX results file should be created"); - var content = File.ReadAllText(resultsPath); - Assert.Contains("TestRun", content); - } - finally + using var tempDir = new TemporaryDirectory(); + var resultsPath = tempDir.GetFilePath("results.trx"); + + using (var context = Context.Create(["--silent", "--results", resultsPath])) { - tempDir.Delete(recursive: true); + // Act + Validation.Run(context); } + + // Assert - TRX results file must exist and contain test result content + Assert.True(File.Exists(resultsPath), "TRX results file should be created"); + var content = File.ReadAllText(resultsPath); + Assert.Contains("TestRun", content); + } } diff --git a/test/DemaConsulting.FileAssert.Tests/Utilities/TemporaryDirectoryTests.cs b/test/DemaConsulting.FileAssert.Tests/Utilities/TemporaryDirectoryTests.cs new file mode 100644 index 0000000..534a120 --- /dev/null +++ b/test/DemaConsulting.FileAssert.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.FileAssert.Utilities; + +namespace DemaConsulting.FileAssert.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); + } +} diff --git a/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs b/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs index d011ca2..70c1c4e 100644 --- a/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs @@ -36,20 +36,14 @@ public class UtilitiesTests public void Utilities_SafePathCombine_PreventsPathTraversalToFileSystem() { // Arrange - var tempDir = Directory.CreateTempSubdirectory("fileassert_util_"); - try - { - // Act & Assert - a traversal attempt is rejected with ArgumentException - Assert.Throws( - () => PathHelpers.SafePathCombine(tempDir.FullName, "../escape.txt")); + using var tempDir = new TemporaryDirectory(); + // Act & Assert - a traversal attempt is rejected with ArgumentException + Assert.Throws( + () => PathHelpers.SafePathCombine(tempDir.DirectoryPath, "../escape.txt")); + + // Act & Assert - a valid relative path within the base is accepted + var combined = PathHelpers.SafePathCombine(tempDir.DirectoryPath, "nested/file.txt"); + Assert.StartsWith(tempDir.DirectoryPath, combined); - // Act & Assert - a valid relative path within the base is accepted - var combined = PathHelpers.SafePathCombine(tempDir.FullName, "nested/file.txt"); - Assert.StartsWith(tempDir.FullName, combined); - } - finally - { - tempDir.Delete(recursive: true); - } } } From e25f6446d4ed988c81bf44ae3f26cfbe892f4040 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 23:42:50 +0000 Subject: [PATCH 3/8] Address PR review thread feedback Agent-Logs-Url: https://github.com/demaconsulting/FileAssert/sessions/1ab3be61-d076-4c8a-ac4f-74db00024030 Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --- docs/design/file-assert/program.md | 22 +++++++++---------- docs/design/ots/buildmark.md | 2 +- docs/design/ots/fileassert.md | 2 -- docs/design/ots/pandoc.md | 1 - docs/design/ots/reqstream.md | 3 +-- docs/design/ots/reviewmark.md | 2 -- docs/design/ots/sarifmark.md | 3 +-- docs/design/ots/sonarmark.md | 3 +-- docs/design/ots/versionmark.md | 2 -- docs/design/ots/weasyprint.md | 3 +-- docs/design/ots/xunit.md | 12 +++++----- docs/reqstream/file-assert.yaml | 4 ++-- .../Utilities/UtilitiesTests.cs | 4 ++-- 13 files changed, 26 insertions(+), 37 deletions(-) diff --git a/docs/design/file-assert/program.md b/docs/design/file-assert/program.md index 18e5138..78f84e2 100644 --- a/docs/design/file-assert/program.md +++ b/docs/design/file-assert/program.md @@ -47,12 +47,12 @@ public static void Run(Context context) Inspects context flags in the following priority order: -| Priority | -| :------- | -| 1 | -| 2 | -| 3 | -| 4 | +| Priority | Condition | Action | +| :------- | :----------------- | :------------------------------- | +| 1 | `context.Version` | Write `Version` and return. | +| 2 | `context.Help` | Print banner, print help, return.| +| 3 | `context.Validate` | Print banner, run validation. | +| 4 | Otherwise | Print banner, run tool logic. | #### RunToolLogic Method @@ -69,11 +69,11 @@ arguments) to `config.Run` so that only matching tests are executed. ### Interactions with Other Units -| Dependency | -| :----------------- | -| `Context` | -| `Validation` | -| `FileAssertConfig` | +- `Context` provides the parsed flags, config path, filters, and output methods used by every + execution path. +- `Validation` runs the built-in self-validation flow when `context.Validate` is set. +- `FileAssertConfig` loads the YAML configuration and executes matching assertions during normal + tool runs. ### Design Decisions diff --git a/docs/design/ots/buildmark.md b/docs/design/ots/buildmark.md index ab8dafb..9fd3eef 100644 --- a/docs/design/ots/buildmark.md +++ b/docs/design/ots/buildmark.md @@ -23,7 +23,7 @@ package name `demaconsulting.buildmark` and restored with `dotnet tool restore`. invoked in the CI pipeline's build-docs job with the `--output` argument pointing to the generated Markdown path. The generated file is placed in `docs/build_notes/generated/build_notes.md`, which Pandoc incorporates into the Build Notes -HTML document. Version constraint: `1.1.0` (pinned in `.config/dotnet-tools.json`). +HTML document. ### Configuration diff --git a/docs/design/ots/fileassert.md b/docs/design/ots/fileassert.md index 7cf5d99..f6fe021 100644 --- a/docs/design/ots/fileassert.md +++ b/docs/design/ots/fileassert.md @@ -26,8 +26,6 @@ pipeline uses it in two ways: - **Document assertion**: `fileassert --config --results ` validates specific generated HTML and PDF files throughout the pipeline using YAML configuration files. -Version constraint: `0.3.0` (pinned in `.config/dotnet-tools.json`). - ### Configuration When used for document validation, FileAssert reads `.fileassert.yaml` configuration files that diff --git a/docs/design/ots/pandoc.md b/docs/design/ots/pandoc.md index ebea2d2..5cf97b6 100644 --- a/docs/design/ots/pandoc.md +++ b/docs/design/ots/pandoc.md @@ -21,7 +21,6 @@ Pandoc is installed as a .NET local tool via the package `demaconsulting.pandoct `.config/dotnet-tools.json` and restored with `dotnet tool restore`. The tool is invoked as `dotnet pandoc` with a `definition.yaml` argument that lists the input Markdown files, template, and output path. Each document collection provides its own `definition.yaml`. -Version constraint: `3.9.0.2` (pinned in `.config/dotnet-tools.json`). ### Configuration diff --git a/docs/design/ots/reqstream.md b/docs/design/ots/reqstream.md index b06d091..a9d828b 100644 --- a/docs/design/ots/reqstream.md +++ b/docs/design/ots/reqstream.md @@ -22,8 +22,7 @@ requiring a separate CI plugin or external service. ReqStream is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the package name `demaconsulting.reqstream` and restored with `dotnet tool restore`. The CI pipeline invokes it in the build-docs job after all test and self-validation TRX files have been -accumulated as workflow artifacts. Version constraint: `1.9.0` (pinned in -`.config/dotnet-tools.json`). +accumulated as workflow artifacts. ### Configuration diff --git a/docs/design/ots/reviewmark.md b/docs/design/ots/reviewmark.md index 2c477f0..e4f48a0 100644 --- a/docs/design/ots/reviewmark.md +++ b/docs/design/ots/reviewmark.md @@ -26,8 +26,6 @@ pipeline invokes ReviewMark in two separate steps: - `dotnet reviewmark --report docs/code_review_report/generated/report.md` — generates the review report -Version constraint: `1.2.0` (pinned in `.config/dotnet-tools.json`). - ### Configuration ReviewMark reads its configuration from `.reviewmark.yaml` at the repository root. This file diff --git a/docs/design/ots/sarifmark.md b/docs/design/ots/sarifmark.md index 33d8dda..f4942de 100644 --- a/docs/design/ots/sarifmark.md +++ b/docs/design/ots/sarifmark.md @@ -20,8 +20,7 @@ output into the document format. SarifMark is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the package name `demaconsulting.sarifmark` and restored with `dotnet tool restore`. The CI pipeline invokes SarifMark in the build-docs job after the CodeQL scanning step has completed and the -SARIF file has been downloaded as a workflow artifact. Version constraint: `1.3.2` (pinned in -`.config/dotnet-tools.json`). +SARIF file has been downloaded as a workflow artifact. ### Configuration diff --git a/docs/design/ots/sonarmark.md b/docs/design/ots/sonarmark.md index 25c5b56..d18d742 100644 --- a/docs/design/ots/sonarmark.md +++ b/docs/design/ots/sonarmark.md @@ -20,8 +20,7 @@ compatible with the Pandoc pipeline. SonarMark is installed as a .NET local tool defined in `.config/dotnet-tools.json` under the package name `demaconsulting.sonarmark` and restored with `dotnet tool restore`. The CI pipeline invokes SonarMark in the build-docs job after the SonarCloud analysis has completed. A -`SONAR_TOKEN` secret is provided to the job for authenticated API access. Version constraint: -`1.5.0` (pinned in `.config/dotnet-tools.json`). +`SONAR_TOKEN` secret is provided to the job for authenticated API access. ### Configuration diff --git a/docs/design/ots/versionmark.md b/docs/design/ots/versionmark.md index 35777a8..d446a23 100644 --- a/docs/design/ots/versionmark.md +++ b/docs/design/ots/versionmark.md @@ -29,8 +29,6 @@ in two modes in the CI pipeline: - **Self-validation**: invoked with `--validate --results ` to run the built-in validation suite and write TRX evidence for ReqStream consumption. -Version constraint: `1.3.0` (pinned in `.config/dotnet-tools.json`). - ### Configuration VersionMark is configured entirely through command-line arguments. In capture mode, `--job-id` diff --git a/docs/design/ots/weasyprint.md b/docs/design/ots/weasyprint.md index 8c4b612..ec989e8 100644 --- a/docs/design/ots/weasyprint.md +++ b/docs/design/ots/weasyprint.md @@ -22,8 +22,7 @@ WeasyPrint is installed as a .NET local tool via the package `demaconsulting.wea `.config/dotnet-tools.json` and restored with `dotnet tool restore`. The tool is invoked as `dotnet weasyprint` with the input HTML path and the output PDF path for each document collection. The CI workflow installs Python via `actions/setup-python` to satisfy the -WeasyPrintTool's internal Python dependency. Version constraint: `68.1.0` (pinned in -`.config/dotnet-tools.json`). +WeasyPrintTool's internal Python dependency. ### Configuration diff --git a/docs/design/ots/xunit.md b/docs/design/ots/xunit.md index 45e42e9..aece0c3 100644 --- a/docs/design/ots/xunit.md +++ b/docs/design/ots/xunit.md @@ -19,10 +19,10 @@ xUnit v3 is chosen because it provides a modern, self-contained test runner with xUnit is integrated via NuGet package references in the test project (`DemaConsulting.FileAssert.Tests.csproj`): -- `xunit.v3` version `3.2.2` — the core test framework providing `[Fact]`, assertions, and - test runner infrastructure for .NET 8, 9, and 10. -- `xunit.runner.visualstudio` version `3.1.5` — the Visual Studio and `dotnet test` adapter - that enables TRX result file output. +- `xunit.v3` — the core test framework providing `[Fact]`, assertions, and test runner + infrastructure for .NET 8, 9, and 10. +- `xunit.runner.visualstudio` — the Visual Studio and `dotnet test` adapter that enables + TRX result file output. Tests are executed by `dotnet test` with the `--logger trx;LogFileName=.trx` argument to produce TRX files for ReqStream. The test project targets `net8.0`, `net9.0`, and `net10.0` @@ -38,7 +38,7 @@ is configured with: - `TreatWarningsAsErrors: true` — enforces code quality at compile time. No `xunit.runner.json` file is required; default discovery and execution settings are used. -`Microsoft.NET.Test.Sdk` version `18.5.1` provides the test SDK integration layer. +`Microsoft.NET.Test.Sdk` provides the test SDK integration layer. ### Interfaces @@ -58,7 +58,7 @@ xUnit brings the following dependencies into the test project: - `xunit.v3.core` — the test execution engine and assertion library. - `xunit.v3.common` — shared abstractions used by the xUnit framework. - `xunit.runner.visualstudio` — the `dotnet test` integration adapter. -- `Microsoft.NET.Test.Sdk` version `18.5.1` — the test SDK integration layer. +- `Microsoft.NET.Test.Sdk` — the test SDK integration layer. All xUnit and runner dependencies are scoped to the test project via `PrivateAssets` settings and do not propagate to the main `DemaConsulting.FileAssert` project or its NuGet package diff --git a/docs/reqstream/file-assert.yaml b/docs/reqstream/file-assert.yaml index 9047b5b..5074598 100644 --- a/docs/reqstream/file-assert.yaml +++ b/docs/reqstream/file-assert.yaml @@ -375,7 +375,7 @@ sections: children: - FileAssert-Program-DefaultBehavior tests: - - IntegrationTest_ValidateFlag_RunsValidation + - Program_Run_NoArguments_DisplaysDefaultBehavior - id: FileAssert-System-DepthFlag title: | @@ -388,7 +388,7 @@ sections: children: - FileAssert-Context-Depth tests: - - IntegrationTest_ValidateFlag_RunsValidation + - Validation_Run_WithDepth_UsesSpecifiedHeadingDepth - id: FileAssert-System-MultiPlatform title: | diff --git a/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs b/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs index 70c1c4e..cc84dd8 100644 --- a/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs @@ -43,7 +43,7 @@ public void Utilities_SafePathCombine_PreventsPathTraversalToFileSystem() // Act & Assert - a valid relative path within the base is accepted var combined = PathHelpers.SafePathCombine(tempDir.DirectoryPath, "nested/file.txt"); - Assert.StartsWith(tempDir.DirectoryPath, combined); - + var relativePath = Path.GetRelativePath(tempDir.DirectoryPath, combined); + Assert.Equal(Path.Combine("nested", "file.txt"), relativePath); } } From 2eaa47cc355a4f4abd2b6ece25a0b6159bc14583 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 00:34:06 +0000 Subject: [PATCH 4/8] docs: address remaining PR #49 review feedback Agent-Logs-Url: https://github.com/demaconsulting/FileAssert/sessions/f2d2e6ed-5f15-4a3d-a691-3fb761978a11 Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --- .../verification/file-assert/utilities/temporary-directory.md | 4 ++-- docs/verification/introduction.md | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/verification/file-assert/utilities/temporary-directory.md b/docs/verification/file-assert/utilities/temporary-directory.md index d9c606b..1d2d004 100644 --- a/docs/verification/file-assert/utilities/temporary-directory.md +++ b/docs/verification/file-assert/utilities/temporary-directory.md @@ -15,8 +15,8 @@ only with the local file system and delegates traversal protection to `PathHelpe Tests execute in the standard CI pipeline environment using the xUnit test runner. The test collection is marked `[Collection("Sequential")]` to prevent parallel execution of tests that -mutate the working directory. No special hardware, peripherals, or environment configuration is -required beyond the standard build toolchain. +share `Console` state. No special hardware, peripherals, or environment configuration is required +beyond the standard build toolchain. #### Acceptance Criteria diff --git a/docs/verification/introduction.md b/docs/verification/introduction.md index 192302c..46fcac9 100644 --- a/docs/verification/introduction.md +++ b/docs/verification/introduction.md @@ -36,6 +36,7 @@ This document covers the verification design for the same software items describ - **FileAssertZipAssert** — Zip archive entry assertions - **Utilities** — shared utility subsystem - **PathHelpers** — safe path combination utilities + - **TemporaryDirectory** — temporary workspace lifecycle utility - **SelfTest** — self-validation subsystem - **Validation** — self-validation test runner @@ -81,7 +82,8 @@ FileAssert (System) │ ├── FileAssertJsonAssert (Unit) │ └── FileAssertZipAssert (Unit) ├── Utilities (Subsystem) -│ └── PathHelpers (Unit) +│ ├── PathHelpers (Unit) +│ └── TemporaryDirectory (Unit) └── SelfTest (Subsystem) └── Validation (Unit) From 6febdd7e465d8e61b343fc40a04e892a0cee42c8 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Thu, 21 May 2026 06:47:58 -0400 Subject: [PATCH 5/8] Updates from formal reviews. --- docs/design/file-assert.md | 6 +- docs/design/file-assert/configuration.md | 35 ++++- .../configuration/file-assert-config.md | 2 + .../configuration/file-assert-data.md | 59 +++++--- docs/design/file-assert/modeling.md | 36 ++++- .../file-assert/modeling/file-assert-file.md | 6 +- .../modeling/file-assert-html-assert.md | 12 +- .../modeling/file-assert-json-assert.md | 20 +-- .../modeling/file-assert-pdf-assert.md | 2 +- .../file-assert/modeling/file-assert-rule.md | 14 +- .../file-assert/modeling/file-assert-test.md | 13 +- .../modeling/file-assert-text-assert.md | 13 +- .../modeling/file-assert-xml-assert.md | 42 +++--- .../modeling/file-assert-yaml-assert.md | 39 +++--- docs/design/file-assert/program.md | 127 ++++++++++++------ docs/design/file-assert/selftest.md | 30 +++++ .../design/file-assert/selftest/validation.md | 2 + docs/design/file-assert/utilities.md | 51 ++++++- .../file-assert/utilities/path-helpers.md | 15 ++- docs/reqstream/file-assert.yaml | 10 +- docs/reqstream/file-assert/cli.yaml | 2 + docs/reqstream/file-assert/configuration.yaml | 14 ++ .../configuration/file-assert-config.yaml | 1 + docs/reqstream/file-assert/modeling.yaml | 25 +++- .../modeling/file-assert-file.yaml | 2 + .../modeling/file-assert-json-assert.yaml | 2 + .../modeling/file-assert-xml-assert.yaml | 2 + docs/reqstream/file-assert/utilities.yaml | 13 ++ docs/reqstream/ots/fileassert.yaml | 3 + docs/reqstream/ots/versionmark.yaml | 2 +- docs/reqstream/ots/weasyprint.yaml | 6 +- docs/user_guide/introduction.md | 2 +- docs/verification/file-assert.md | 28 ++-- docs/verification/file-assert/cli.md | 9 +- docs/verification/file-assert/cli/context.md | 4 +- .../verification/file-assert/configuration.md | 9 ++ .../configuration/file-assert-config.md | 2 +- .../configuration/file-assert-data.md | 15 +++ docs/verification/file-assert/modeling.md | 4 +- .../file-assert/modeling/file-assert-file.md | 3 + .../modeling/file-assert-text-assert.md | 12 +- .../modeling/file-assert-xml-assert.md | 22 ++- .../modeling/file-assert-yaml-assert.md | 13 +- docs/verification/file-assert/selftest.md | 12 +- .../file-assert/selftest/validation.md | 25 ++-- docs/verification/file-assert/utilities.md | 1 + .../file-assert/utilities/path-helpers.md | 23 ++-- docs/verification/ots/fileassert.md | 30 ++++- docs/verification/ots/pandoc.md | 6 +- docs/verification/ots/reviewmark.md | 2 +- docs/verification/ots/versionmark.md | 7 +- src/DemaConsulting.FileAssert/Cli/Context.cs | 33 ++--- .../Modeling/FileAssertHtmlAssert.cs | 3 + .../Modeling/FileAssertTest.cs | 1 + .../Modeling/FileAssertTextAssert.cs | 3 + .../Modeling/FileAssertXmlAssert.cs | 7 + .../Modeling/FileAssertZipAssert.cs | 3 + src/DemaConsulting.FileAssert/Program.cs | 14 ++ .../SelfTest/Validation.cs | 1 - .../Utilities/PathHelpers.cs | 18 ++- .../Cli/CliTests.cs | 20 ++- .../Configuration/ConfigurationTests.cs | 32 ++++- .../Configuration/FileAssertConfigTests.cs | 2 +- .../IntegrationTests.cs | 10 ++ .../Modeling/FileAssertFileTests.cs | 1 + .../Modeling/FileAssertTestTests.cs | 2 +- .../Modeling/FileAssertTextAssertTests.cs | 2 + .../Modeling/FileAssertXmlAssertTests.cs | 56 +++++++- .../Modeling/FileAssertYamlAssertTests.cs | 27 ++++ .../Modeling/FileAssertZipAssertTests.cs | 2 + .../Modeling/ModelingTests.cs | 2 +- .../ProgramTests.cs | 4 + .../SelfTest/SelfTestTests.cs | 3 +- .../Utilities/PathHelpersTests.cs | 7 +- .../Utilities/UtilitiesTests.cs | 25 ++++ 75 files changed, 856 insertions(+), 257 deletions(-) diff --git a/docs/design/file-assert.md b/docs/design/file-assert.md index a65efcb..45931c8 100644 --- a/docs/design/file-assert.md +++ b/docs/design/file-assert.md @@ -38,7 +38,7 @@ one or more units: | | | FileAssertTextAssert, FileAssertPdfAssert, FileAssertXmlAssert, | | | | FileAssertHtmlAssert, FileAssertYamlAssert, FileAssertJsonAssert, | | | | FileAssertZipAssert | -| Utilities | Subsystem | PathHelpers | +| Utilities | Subsystem | PathHelpers, TemporaryDirectory | | SelfTest | Subsystem | Validation | ## Execution Flow @@ -96,7 +96,7 @@ system-level code; the system boundary is defined by the combination of its part | Cli | Subsystem | Contains `Context`; owns arg parsing, I/O references, filter list, exit. | | Configuration | Subsystem | Contains `FileAssertConfig`/`FileAssertData`; YAML deserialization, tests. | | Modeling | Subsystem | Contains assertion classes; pure domain objects evaluating file rules. | -| Utilities | Subsystem | Contains `PathHelpers`; stateless path helper used by Modeling subsystem. | +| Utilities | Subsystem | Contains `PathHelpers` and `TemporaryDirectory`; shared utilities. | | SelfTest | Subsystem | Contains `Validation`; runs built-in assertions when `--validate` passed. | All subsystems receive a `Context` instance (created by `Program`) rather than reading @@ -219,7 +219,7 @@ Outputs: - **Error accumulation**: Failures are accumulated via `Context.WriteError` rather than exceptions, so all assertions in a run are reported in a single pass. - **Lazy file-type parsing**: A file is only parsed as a structured document (PDF, XML, HTML, - YAML, or JSON) if the corresponding assertion block is declared in the YAML configuration. + YAML, JSON, or zip archive) if the corresponding assertion block is declared in the YAML configuration. This avoids unnecessary I/O and third-party library invocations for files that are only checked for size or text content. - **Immediate failure on parse error**: If a file-type assertion block is declared and the file diff --git a/docs/design/file-assert/configuration.md b/docs/design/file-assert/configuration.md index 949df45..a7ba79c 100644 --- a/docs/design/file-assert/configuration.md +++ b/docs/design/file-assert/configuration.md @@ -1,4 +1,4 @@ -## Configuration Subsystem Design +## Configuration Subsystem Design ### Overview @@ -23,6 +23,39 @@ and runs the tests. - Resolve the base directory for glob patterns from the configuration file path. - Filter tests by name or tag before execution. +### Interfaces + +#### Exposed + +| Member / Class | Description | +| :------------------------------------------- | :-------------------------------------------------------------------------------- | +| `FileAssertConfig.ReadFromFile(path)` | Reads and deserializes the YAML file; returns a `FileAssertConfig` instance. | +| `FileAssertConfig.Run(context, filters)` | Executes filtered tests; writes results when `context.ResultsFile` is set. | +| `FileAssertData` DTO classes | Intermediate data holders produced during deserialization; consumed by Modeling. | + +#### Consumed + +| Dependency | Usage | +| :---------------------------------- | :---------------------------------------------------------------------- | +| `Context` (Cli subsystem) | Receives error and progress output; provides filter list and config path. | +| `FileAssertTest.Create` (Modeling) | Converts each `FileAssertTestData` DTO into a domain object. | +| `FileAssertTest.Run` (Modeling) | Executes each selected test. | +| YamlDotNet | Deserializes the YAML configuration file into DTO objects. | +| `DemaConsulting.TestResults` | Serializes test outcomes to TRX or JUnit XML when results are requested. | + +### Design + +`FileAssertConfig.ReadFromFile` and `FileAssertConfig.Run` collaborate as follows: + +1. `ReadFromFile` opens and deserializes the YAML file via YamlDotNet into + `FileAssertConfigData` (the top-level DTO), then calls `FileAssertTest.Create` for each + `FileAssertTestData` entry to produce the domain-object list. +2. `Run` materializes the filter list, derives the base directory from the config file path, + and iterates the test list — calling `FileAssertTest.MatchesFilter` to skip non-matching + tests and `FileAssertTest.Run` to execute each selected test. +3. `FileAssertData` classes are pure data holders: they carry no logic and are used only during + deserialization. Once `ReadFromFile` returns, the DTOs are discarded. + ### Interactions with Other Subsystems | Dependency | Usage | diff --git a/docs/design/file-assert/configuration/file-assert-config.md b/docs/design/file-assert/configuration/file-assert-config.md index 564dbba..e002c17 100644 --- a/docs/design/file-assert/configuration/file-assert-config.md +++ b/docs/design/file-assert/configuration/file-assert-config.md @@ -143,6 +143,8 @@ serialization. | Results file write failure | Exception caught; error written via `context.WriteError`. | | Individual test assertion failures | Accumulated in `context` via `WriteError`; run continues. | +#### Interactions + - **Caller**: `Program.RunToolLogic` calls `ReadFromFile` with `context.ConfigFile`, then calls `Run(context, context.Filters)`. - **Creates**: `FileAssertTest` instances via `FileAssertTest.Create` during `ReadFromFile`. diff --git a/docs/design/file-assert/configuration/file-assert-data.md b/docs/design/file-assert/configuration/file-assert-data.md index 32723ed..742385f 100644 --- a/docs/design/file-assert/configuration/file-assert-data.md +++ b/docs/design/file-assert/configuration/file-assert-data.md @@ -26,20 +26,21 @@ Exactly one property shall be set per rule. The `FileAssertRule.Create` factory Represents a file pattern assertion within a test. -| Property | YAML alias | Type | Description | -| :--------- | :--------- | :-------------------------- | :----------------------------------------------------------- | -| `Pattern` | `pattern` | `string?` | Glob pattern used to locate files. | -| `Min` | `min` | `int?` | Minimum number of matching files; null means no lower bound. | -| `Max` | `max` | `int?` | Maximum number of matching files; null means no upper bound. | -| `Count` | `count` | `int?` | Exact number of matching files; null means no exact bound. | -| `MinSize` | `min-size` | `long?` | Minimum file size in bytes; null means no lower bound. | -| `MaxSize` | `max-size` | `long?` | Maximum file size in bytes; null means no upper bound. | -| `Text` | `text` | `List?` | Text content rules (used by `FileAssertTextAssert`). | -| `Pdf` | `pdf` | `FileAssertPdfData?` | PDF document assertions. | -| `Xml` | `xml` | `List?`| XML node count assertions using XPath. | -| `Html` | `html` | `List?`| HTML node count assertions using XPath. | -| `Yaml` | `yaml` | `List?`| YAML node count assertions using dot-notation. | -| `Json` | `json` | `List?`| JSON node count assertions using dot-notation. | +| Property | YAML alias | Type | Description | +| :--------- | :--------- | :-------------------------- | :----------------------------------------------------------- | +| `Pattern` | `pattern` | `string?` | Glob pattern used to locate files. | +| `Min` | `min` | `int?` | Minimum number of matching files; null means no lower bound. | +| `Max` | `max` | `int?` | Maximum number of matching files; null means no upper bound. | +| `Count` | `count` | `int?` | Exact number of matching files; null means no exact bound. | +| `MinSize` | `min-size` | `long?` | Minimum file size in bytes; null means no lower bound. | +| `MaxSize` | `max-size` | `long?` | Maximum file size in bytes; null means no upper bound. | +| `Text` | `text` | `List?` | Text content rules (used by `FileAssertTextAssert`). | +| `Pdf` | `pdf` | `FileAssertPdfData?` | PDF document assertions. | +| `Xml` | `xml` | `List?` | XML node count assertions using XPath. | +| `Html` | `html` | `List?` | HTML node count assertions using XPath. | +| `Yaml` | `yaml` | `List?` | YAML node count assertions using dot-notation. | +| `Json` | `json` | `List?` | JSON node count assertions using dot-notation. | +| `Zip` | `zip` | `FileAssertZipData?` | Zip archive entry count assertions. | ##### FileAssertTestData @@ -100,13 +101,27 @@ assertion blocks. | `Min` | `min` | `int?` | Minimum number of matched nodes. | | `Max` | `max` | `int?` | Maximum number of matched nodes. | -#### Design Decisions +##### FileAssertZipData -#### Design Constraints +Represents the `zip:` assertion block for a file entry. -The `FileAssertData` classes contain no validation or business logic, delegating all -validation to the factory methods in the Modeling subsystem. This maintains a clean -separation between deserialization and domain object construction. +| Property | YAML alias | Type | Description | +| :--------- | :---------- | :--------------------------------- | :------------------------------------- | +| `Entries` | `entries` | `List?` | Entry glob pattern constraints. | + +##### FileAssertZipEntryData + +Represents a single zip archive entry count constraint. + +| Property | YAML alias | Type | Description | +| :-------- | :--------- | :-------- | :------------------------------------------------------ | +| `Pattern` | `pattern` | `string?` | Glob pattern matched against normalized entry names. | +| `Min` | `min` | `int?` | Minimum number of matching entries; null means no bound. | +| `Max` | `max` | `int?` | Maximum number of matching entries; null means no bound. | + +#### Design Decisions + +The following decisions were made to keep the DTO layer simple and decoupled: - **Nullable reference type properties**: All properties are nullable to correctly represent absent YAML keys without throwing during deserialization. @@ -115,6 +130,12 @@ separation between deserialization and domain object construction. - **YamlMember aliases**: Explicit `[YamlMember(Alias = "...")]` attributes tie each property to its YAML key, decoupling C# naming conventions from the YAML schema. +#### Design Constraints + +The `FileAssertData` classes contain no validation or business logic, delegating all +validation to the factory methods in the Modeling subsystem. This maintains a clean +separation between deserialization and domain object construction. + #### Purpose The `FileAssertData` file defines the complete set of YAML data transfer objects (DTOs) used diff --git a/docs/design/file-assert/modeling.md b/docs/design/file-assert/modeling.md index dcab85b..a5041ab 100644 --- a/docs/design/file-assert/modeling.md +++ b/docs/design/file-assert/modeling.md @@ -1,4 +1,4 @@ -## Modeling Subsystem Design +## Modeling Subsystem Design ### Overview @@ -53,6 +53,40 @@ FileAssertTest └── FileAssertZipAssert? (zero or one) ``` +### Interfaces + +#### Exposed + +| Member / Class | Description | +| :-------------------------- | :------------------------------------------------------------------------------------------ | +| `FileAssertTest.Create` | Factory method: builds a domain test object from a `FileAssertTestData` DTO. | +| `FileAssertTest.MatchesFilter` | Returns whether the test name or tags match the provided filter list. | +| `FileAssertTest.Run` | Executes all file assertions within the test and reports results via `Context`. | +| `FileAssertFile.Create` | Factory method: builds a domain file-pattern object from a `FileAssertFileData` DTO. | +| `FileAssertRule.Create` | Factory method: selects and returns the correct concrete rule subclass. | + +#### Consumed + +| Dependency | Usage | +| :------------------------- | :----------------------------------------------------------------------------- | +| `Context` (Cli subsystem) | Receives assertion failure messages and error exit code. | +| `FileAssertData` DTOs | Input types for all `Create` factory methods. | +| Microsoft.Extensions.FileSystemGlobbing | Cross-platform glob evaluation for file discovery. | +| YamlDotNet, PdfPig, HtmlAgilityPack, System.Xml.Linq, System.Text.Json, System.IO.Compression | Format-specific parsing libraries. | + +### Design + +Domain objects are constructed and executed in the following layers: + +1. `FileAssertTest.Create` iterates the `FileAssertTestData.Files` list, calling + `FileAssertFile.Create` for each entry. `FileAssertFile.Create` in turn creates any + declared assert units (`FileAssertTextAssert`, `FileAssertPdfAssert`, etc.). +2. `FileAssertTextAssert.Create` iterates rule data, calling `FileAssertRule.Create` for + each entry to produce the correct concrete rule subclass. +3. During execution, `FileAssertConfig.Run` calls `FileAssertTest.Run` → `FileAssertFile.Run` + → assert unit `Run` methods, threading `Context` through every layer so all failures are + reported via a single path. + ### Interactions with Other Subsystems | Dependency | Usage | diff --git a/docs/design/file-assert/modeling/file-assert-file.md b/docs/design/file-assert/modeling/file-assert-file.md index 7230005..6d441a6 100644 --- a/docs/design/file-assert/modeling/file-assert-file.md +++ b/docs/design/file-assert/modeling/file-assert-file.md @@ -1,4 +1,4 @@ -### FileAssertFile Design +### FileAssertFile Design #### Overview @@ -78,6 +78,10 @@ Execution proceeds in five phases: g. If `JsonAssert` is defined, attempts to parse the file using `System.Text.Json`; reports an immediate error if parsing fails, otherwise applies dot-notation path count assertions. + h. If `ZipAssert` is defined, attempts to open the file as a zip archive using + `System.IO.Compression.ZipFile`; reports an immediate error if the archive cannot + be opened, otherwise matches entry names against each configured glob pattern and + enforces the declared minimum and maximum count constraints. ##### Count Constraint Error Messages diff --git a/docs/design/file-assert/modeling/file-assert-html-assert.md b/docs/design/file-assert/modeling/file-assert-html-assert.md index 550c7b3..feccfa1 100644 --- a/docs/design/file-assert/modeling/file-assert-html-assert.md +++ b/docs/design/file-assert/modeling/file-assert-html-assert.md @@ -43,8 +43,8 @@ internal void Run(Context context, string fileName) Execution proceeds in the following steps: 1. Loads the file using `HtmlDocument.Load`. -2. If `ParseErrors` contains critical errors, writes the error below and returns - immediately. +2. If `Load` throws an `IOException` or `UnauthorizedAccessException`, writes the + error below and returns immediately. 3. For each query entry: selects nodes via `HtmlDocument.DocumentNode.SelectNodes(xpathQuery)`, counts the result, and applies `Count`, `Min`, and `Max` constraints against the match count. @@ -82,9 +82,11 @@ files: - **HtmlAgilityPack chosen**: HtmlAgilityPack is the de-facto standard for lenient HTML parsing in .NET. It handles malformed HTML gracefully, making it appropriate for asserting generated documentation and static site outputs that may not be strict XHTML. -- **Immediate failure on critical parse errors**: When HtmlAgilityPack reports critical - parse errors, applying XPath assertions would produce meaningless results. Reporting the - parse failure immediately gives users a clear, actionable error message. +- **Immediate failure on I/O errors**: HtmlAgilityPack is lenient and parses malformed + HTML without throwing exceptions. The primary error condition handled is a missing or + inaccessible file; `IOException` and `UnauthorizedAccessException` from `Load` are + caught and reported immediately so users receive a clear, actionable error message + rather than silent XPath results against an empty document. - **Independent query model**: `FileAssertHtmlQuery` is private to this unit so that HTML assertion behavior can evolve independently of the other structured-document assert units. diff --git a/docs/design/file-assert/modeling/file-assert-json-assert.md b/docs/design/file-assert/modeling/file-assert-json-assert.md index 8ed8f55..b22acd4 100644 --- a/docs/design/file-assert/modeling/file-assert-json-assert.md +++ b/docs/design/file-assert/modeling/file-assert-json-assert.md @@ -16,11 +16,11 @@ The main class coordinating dot-notation path assertions for a JSON file. ###### FileAssertJsonAssert Properties -| Property | Type | Description | -| :-------- | :----------------------------------- | :---------------------------------- | -| `Queries` | `IReadOnlyList` | Dot-notation path query assertions. | +| Field | Type | Description | +| :--------- | :---------------------------- | :---------------------------------- | +| `_queries` | `IReadOnlyList` | Dot-notation path query assertions. | -Each `FileAssertJsonQuery` entry holds: +Each `JsonQuery` entry holds: | Property | Type | Description | | :------- | :------- | :------------------------------- | @@ -87,7 +87,7 @@ files: - **Dot-notation path traversal**: Segment-by-segment descent through JSON object properties. Array elements are counted at the terminal segment, allowing users to assert the presence and cardinality of array-valued keys. -- **Independent query model**: `FileAssertJsonQuery` is private to this unit so that JSON +- **Independent query model**: `JsonQuery` is private to this unit so that JSON assertion behavior can evolve independently of the other structured-document assert units. #### Purpose @@ -98,11 +98,11 @@ enforces min, max, and exact element-count constraints per path. #### Data Model -| Field / Property | Type | Description | -| :--------------- | :----------------------------------- | :-------------------------------------------- | -| `Queries` | `IReadOnlyList` | Ordered list of dot-notation path assertions. | +| Field / Property | Type | Description | +| :--------------- | :------------------------- | :-------------------------------------------- | +| `_queries` | `IReadOnlyList` | Ordered list of dot-notation path assertions. | -Each `FileAssertJsonQuery` (private nested record) holds: +Each `JsonQuery` (private nested record) holds: | Property | Type | Description | | :------- | :------- | :------------------------------------------------------- | @@ -115,7 +115,7 @@ Each `FileAssertJsonQuery` (private nested record) holds: | Method | Purpose | | :---------------------------------------------- | :--------------------------------------------------------------- | -| `Create(IEnumerable data)` | Factory: converts query DTOs to `FileAssertJsonQuery` instances. | +| `Create(IEnumerable data)` | Factory: converts query DTOs to `JsonQuery` instances. | | `Run(Context context, string fileName)` | Parses the JSON file and evaluates each dot-notation path query. | #### Error Handling diff --git a/docs/design/file-assert/modeling/file-assert-pdf-assert.md b/docs/design/file-assert/modeling/file-assert-pdf-assert.md index 3abca11..41dbf04 100644 --- a/docs/design/file-assert/modeling/file-assert-pdf-assert.md +++ b/docs/design/file-assert/modeling/file-assert-pdf-assert.md @@ -29,7 +29,7 @@ internal static PdfMetadataRule FromData(FileAssertPdfMetadataRuleData data) ###### PdfMetadataRule Apply ```csharp -internal void Apply(Context context, string fileName, string? fieldValue) +internal void Apply(Context context, string fileName, string? value) ``` Checks `Contains` substring presence (ordinal) and `Matches` regex against `fieldValue`. diff --git a/docs/design/file-assert/modeling/file-assert-rule.md b/docs/design/file-assert/modeling/file-assert-rule.md index 5a4f6fb..cad0cdc 100644 --- a/docs/design/file-assert/modeling/file-assert-rule.md +++ b/docs/design/file-assert/modeling/file-assert-rule.md @@ -124,12 +124,14 @@ a file's text content. #### Data Model -| Class | Field | Type | Description | -| :----------------------------- | :--------- | :------ | :---------------------------------------------- | -| `FileAssertContainsRule` | `Value` | `string`| Substring the file content must contain. | -| `FileAssertDoesNotContainRule` | `Value` | `string`| Substring the file content must NOT contain. | -| `FileAssertMatchesRule` | `Pattern` | `Regex` | Compiled regex the file content must match. | -| `FileAssertDoesNotMatchRule` | `Pattern` | `Regex` | Compiled regex the file content must NOT match. | +| Class | Field | Type | Description | +| :----------------------------- | :--------- | :------ | :--------------------------------------------------------------- | +| `FileAssertContainsRule` | `Value` | `string`| Substring the file content must contain. | +| `FileAssertDoesNotContainRule` | `Value` | `string`| Substring the file content must NOT contain. | +| `FileAssertMatchesRule` | `Pattern` | `string`| Raw pattern string; used in error messages. | +| `FileAssertMatchesRule` | `_regex` | `Regex` | Compiled regex (private) the file content must match. | +| `FileAssertDoesNotMatchRule` | `Pattern` | `string`| Raw pattern string; used in error messages. | +| `FileAssertDoesNotMatchRule` | `_regex` | `Regex` | Compiled regex (private) the file content must NOT match. | Regex objects are compiled at construction with a ten-second evaluation timeout. diff --git a/docs/design/file-assert/modeling/file-assert-test.md b/docs/design/file-assert/modeling/file-assert-test.md index f6ff6be..fbd071b 100644 --- a/docs/design/file-assert/modeling/file-assert-test.md +++ b/docs/design/file-assert/modeling/file-assert-test.md @@ -98,12 +98,13 @@ filter criteria for selective execution, and drive execution of its assertions. #### Error Handling -| Scenario | Handling | -| :------------------------------------ | :--------------------------------------------------- | -| Null `data` passed to `Create` | `ArgumentNullException` thrown. | -| Null or whitespace `Name` in data | `InvalidOperationException` thrown by `Create`. | -| Null `context` or `basePath` in `Run` | `ArgumentNullException` thrown. | -| Individual file assertion failures | Accumulated in `context`; subsequent files continue. | +| Scenario | Handling | +| :--------------------------------------- | :--------------------------------------------------- | +| Null `data` passed to `Create` | `ArgumentNullException` thrown. | +| Null or whitespace `Name` in data | `InvalidOperationException` thrown by `Create`. | +| Null `filters` passed to `MatchesFilter` | `ArgumentNullException` thrown. | +| Null `context` or `basePath` in `Run` | `ArgumentNullException` thrown. | +| Individual file assertion failures | Accumulated in `context`; subsequent files continue. | #### Interactions diff --git a/docs/design/file-assert/modeling/file-assert-text-assert.md b/docs/design/file-assert/modeling/file-assert-text-assert.md index 95b2a06..dfd0deb 100644 --- a/docs/design/file-assert/modeling/file-assert-text-assert.md +++ b/docs/design/file-assert/modeling/file-assert-text-assert.md @@ -55,10 +55,14 @@ Execution proceeds in the following steps: File '' could not be read as text ``` -| Parameter | Type | Description | -| :----------- | :-------- | :--------------------------------------- | -| `context` | `Context` | Reporting sink used to record errors. | -| `fileName` | `string` | Full path to the file to validate. | +| Parameter | Type | Description | +| :----------- | :-------- | :------------------------------------------------------ | +| `context` | `Context` | Reporting sink used to record errors. Must not be null. | +| `fileName` | `string` | Full path to the file to validate. Must not be null. | + +| Exception | Condition | +| :-------------------------- | :----------------------------------------------------- | +| `ArgumentNullException` | Thrown when `context` or `fileName` is null. | #### YAML Configuration @@ -108,6 +112,7 @@ file-type assert units, keeping `FileAssertFile` free of rule-application logic. | Scenario | Handling | | :----------------------------------------------------- | :--------------------------------------------------- | | Null `data` passed to `Create` | `ArgumentNullException` thrown. | +| Null `context` or `fileName` passed to `Run` | `ArgumentNullException` thrown. | | `IOException` or `UnauthorizedAccessException` on read | Error via `context.WriteError`; `Run` returns. | | Individual rule check fails | Error via `context` in `Rule.Apply`; rules continue. | diff --git a/docs/design/file-assert/modeling/file-assert-xml-assert.md b/docs/design/file-assert/modeling/file-assert-xml-assert.md index 44a7bec..d982e8a 100644 --- a/docs/design/file-assert/modeling/file-assert-xml-assert.md +++ b/docs/design/file-assert/modeling/file-assert-xml-assert.md @@ -13,13 +13,13 @@ applies min, max, and exact count constraints to the number of matching nodes. The main class coordinating XPath-based node count assertions for an XML file. -###### FileAssertXmlAssert Properties +###### FileAssertXmlAssert Fields -| Property | Type | Description | -| :-------- | :---------------------------------- | :---------------------- | -| `Queries` | `IReadOnlyList` | XPath query assertions. | +| Field | Type | Description | +| :--------- | :------------------------------- | :---------------------- | +| `_queries` | `IReadOnlyList` | XPath query assertions. | -Each `FileAssertXmlQuery` entry holds: +Each `XmlQuery` entry holds: | Property | Type | Description | | :------- | :------- | :------------------------------- | @@ -45,8 +45,10 @@ Execution proceeds in the following steps: 1. Attempts to load the file using `XDocument.Load(fileName)`. 2. If an exception is thrown, writes the error below and returns immediately. 3. For each query entry: evaluates the XPath expression against the document using - `System.Xml.XPath` extension methods, counts the matching nodes, and applies - `Count`, `Min`, and `Max` constraints against the match count. + `System.Xml.XPath` extension methods. If the XPath expression is malformed and throws + an `XPathException`, writes the error below and continues to the next query. Otherwise, + counts the matching nodes and applies `Count`, `Min`, and `Max` constraints against the + match count. ###### FileAssertXmlAssert Parse Error Message @@ -57,6 +59,7 @@ File '' could not be parsed as an XML document ###### FileAssertXmlAssert Query Error Messages ```text +File '' query '' is not a valid XPath expression File '' query '' returned result(s) which is below the minimum of File '' query '' returned result(s) which exceeds the maximum of File '' query '' returned result(s) but expected exactly @@ -81,7 +84,7 @@ files: - **Immediate failure on parse error**: Attempting to evaluate XPath queries against a file that is not valid XML would produce meaningless or misleading results. Reporting the parse failure immediately gives users a clear, actionable error message. -- **Independent query model**: `FileAssertXmlQuery` is private to this unit so that XML +- **Independent query model**: `XmlQuery` is private to this unit so that XML assertion behavior can evolve independently of the other structured-document assert units. #### Purpose @@ -92,11 +95,11 @@ exact node-count constraints per query. #### Data Model -| Field / Property | Type | Description | -| :--------------- | :---------------------------------- | :-------------------------------------- | -| `Queries` | `IReadOnlyList` | Ordered list of XPath query assertions. | +| Field | Type | Description | +| :--------- | :------------------------ | :-------------------------------------- | +| `_queries` | `IReadOnlyList` | Ordered list of XPath query assertions. | -Each `FileAssertXmlQuery` (private nested record) holds: +Each `XmlQuery` (private nested record) holds: | Property | Type | Description | | :------- | :------- | :--------------------------------------- | @@ -109,17 +112,18 @@ Each `FileAssertXmlQuery` (private nested record) holds: | Method | Purpose | | :---------------------------------------------- | :------------------------------------------------------------ | -| `Create(IEnumerable data)` | Converts query DTOs to `FileAssertXmlQuery` instances. | +| `Create(IEnumerable data)` | Converts query DTOs to `XmlQuery` instances. | | `Run(Context context, string fileName)` | Loads the XML file and evaluates each XPath query against it. | #### Error Handling -| Scenario | Handling | -| :------------------------------------------ | :-------------------------------------------------------------------- | -| `XDocument.Load` throws on parse failure | Error written via `context.WriteError`; `Run` returns immediately. | -| Query result below `Min` | Error written via `context.WriteError`; subsequent queries continue. | -| Query result above `Max` | Error written via `context.WriteError`; subsequent queries continue. | -| Query result not equal to `Count` | Error written via `context.WriteError`; subsequent queries continue. | +| Scenario | Handling | +| :----------------------------------------------- | :-------------------------------------------------------------------------------- | +| `XDocument.Load` throws on parse failure | Error written via `context.WriteError`; `Run` returns immediately. | +| XPath expression malformed (`XPathException`) | Error written via `context.WriteError`; evaluation continues with the next query. | +| Query result below `Min` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result above `Max` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result not equal to `Count` | Error written via `context.WriteError`; subsequent queries continue. | #### Interactions diff --git a/docs/design/file-assert/modeling/file-assert-yaml-assert.md b/docs/design/file-assert/modeling/file-assert-yaml-assert.md index b793d07..c9fcdc1 100644 --- a/docs/design/file-assert/modeling/file-assert-yaml-assert.md +++ b/docs/design/file-assert/modeling/file-assert-yaml-assert.md @@ -13,13 +13,13 @@ max, and exact count constraints to the number of matching nodes. The main class coordinating dot-notation path assertions for a YAML file. -###### FileAssertYamlAssert Properties +###### FileAssertYamlAssert Fields -| Property | Type | Description | -| :-------- | :----------------------------------- | :---------------------------------- | -| `Queries` | `IReadOnlyList` | Dot-notation path query assertions. | +| Field | Type | Description | +| :--------- | :--------------------------------- | :---------------------------------- | +| `_queries` | `IReadOnlyList` | Dot-notation path query assertions. | -Each `FileAssertYamlQuery` entry holds: +Each `YamlQuery` private nested record holds: | Property | Type | Description | | :------- | :------- | :------------------------------- | @@ -43,8 +43,11 @@ internal void Run(Context context, string fileName) Execution proceeds in the following steps: 1. Parses the file using YamlDotNet's `YamlStream.Load`. -2. If a `YamlException` is thrown, writes the error below and returns immediately. -3. For each query entry: traverses the YAML document tree following the dot-notation +2. If a `YamlException`, `IOException`, or `UnauthorizedAccessException` is thrown, + writes the error below and returns immediately. +3. If the parsed stream contains no documents (empty file), evaluates all queries + with a count of 0 and applies constraints, then returns. +4. For each query entry: traverses the YAML document tree following the dot-notation path segments, counts the matched nodes, and applies `Count`, `Min`, and `Max` constraints against the match count. @@ -86,7 +89,7 @@ files: - **Dot-notation path traversal**: Segment-by-segment descent through YAML mapping nodes. Sequences count as zero or more items at the terminal segment, allowing users to assert the presence and cardinality of sequence keys. -- **Independent query model**: `FileAssertYamlQuery` is private to this unit so that YAML +- **Independent query model**: `YamlQuery` is a private nested record within this unit so that YAML assertion behavior can evolve independently of the other structured-document assert units. #### Purpose @@ -97,11 +100,11 @@ min, max, and exact node-count constraints per path. #### Data Model -| Field / Property | Type | Description | -| :--------------- | :----------------------------------- | :-------------------------------------------- | -| `Queries` | `IReadOnlyList` | Ordered list of dot-notation path assertions. | +| Field / Property | Type | Description | +| :--------------- | :--------------------------------- | :-------------------------------------------- | +| `_queries` | `IReadOnlyList` | Ordered list of dot-notation path assertions. | -Each `FileAssertYamlQuery` (private nested record) holds: +Each `YamlQuery` (private nested record) holds: | Property | Type | Description | | :------- | :------- | :---------------------------------------- | @@ -119,12 +122,12 @@ Each `FileAssertYamlQuery` (private nested record) holds: #### Error Handling -| Scenario | Handling | -| :------------------------------------------ | :-------------------------------------------------------------------- | -| `YamlException` during `YamlStream.Load` | Error written via `context.WriteError`; `Run` returns immediately. | -| Query result below `Min` | Error written via `context.WriteError`; subsequent queries continue. | -| Query result above `Max` | Error written via `context.WriteError`; subsequent queries continue. | -| Query result not equal to `Count` | Error written via `context.WriteError`; subsequent queries continue. | +| Scenario | Handling | +| :------------------------------------------------------------------ | :-------------------------------------------------------------------- | +| `YamlException`, `IOException`, or `UnauthorizedAccessException` | Error written via `context.WriteError`; `Run` returns immediately. | +| Query result below `Min` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result above `Max` | Error written via `context.WriteError`; subsequent queries continue. | +| Query result not equal to `Count` | Error written via `context.WriteError`; subsequent queries continue. | #### Interactions diff --git a/docs/design/file-assert/program.md b/docs/design/file-assert/program.md index 78f84e2..4ca401d 100644 --- a/docs/design/file-assert/program.md +++ b/docs/design/file-assert/program.md @@ -4,20 +4,14 @@ `Program` is the static entry-point class for the FileAssert tool. It constructs the execution `Context` from command-line arguments, dispatches to the appropriate handler (version display, -help, self-validation, or main tool logic), and returns the final process exit code. +help, self-validation, or main tool logic), and returns the final process exit code. It also +exposes a `Version` property used by both the version display path and the self-validation header. ### Data Model N/A – `Program` is a static entry-point class with no instance state. -### Overview - -`Program` is the entry point for the FileAssert tool. It owns the `Main` method, constructs a -`Context` from command-line arguments, dispatches to the appropriate handler based on context -flags, and returns the final exit code. It also exposes a `Version` property used by both the -version display path and the self-validation header. - -### Class Structure +### Key Methods #### Version Property @@ -25,8 +19,15 @@ version display path and the self-validation header. public static string Version { get; } ``` -Reads the informational version from the executing assembly's -`AssemblyInformationalVersionAttribute`. Falls back to the assembly version, then to `"0.0.0"`. +**Purpose**: Provides the application version string for display and self-test headers. + +**Algorithm**: Reads the informational version from the executing assembly's +`AssemblyInformationalVersionAttribute`. Falls back to the assembly version string if the +attribute is absent, then to `"0.0.0"` if neither is available. + +**Preconditions**: None. + +**Postconditions**: Returns a non-null, non-empty string representing the application version. #### Main Method @@ -34,10 +35,18 @@ Reads the informational version from the executing assembly's private static int Main(string[] args) ``` -Creates a `Context`, calls `Run`, and returns `context.ExitCode`. Catches `ArgumentException` -and `InvalidOperationException` to print expected error messages and return exit code `1`. -Unexpected exceptions are re-thrown after printing the message so that the runtime generates -an event-log entry. +**Purpose**: Operating-system entry point; creates execution context, delegates to `Run`, and +returns the process exit code. + +**Algorithm**: Creates a `Context` via `Context.Create(args)`, calls `Run(context)`, and returns +`context.ExitCode`. Catches `ArgumentException` and `InvalidOperationException` to print their +messages to standard error and return exit code `1`. Any other exception is printed to standard +error and re-thrown so that the runtime generates a crash-report event-log entry. + +**Preconditions**: `args` is the raw command-line argument array supplied by the runtime (may be +empty; must not be null). + +**Postconditions**: Returns `0` for success or a non-zero value for failure. #### Run Method @@ -45,7 +54,11 @@ an event-log entry. public static void Run(Context context) ``` -Inspects context flags in the following priority order: +**Purpose**: Dispatches to the appropriate execution path based on the parsed context flags. +Declared `public` so that unit tests and the self-validation suite can exercise it without +spawning a child process. + +**Algorithm**: Inspects context flags in the following priority order: | Priority | Condition | Action | | :------- | :----------------- | :------------------------------- | @@ -54,20 +67,70 @@ Inspects context flags in the following priority order: | 3 | `context.Validate` | Print banner, run validation. | | 4 | Otherwise | Print banner, run tool logic. | +**Preconditions**: `context` must not be null. + +**Postconditions**: Appropriate output has been written to `context`; `context.ExitCode` reflects +the outcome of the dispatched path. + +#### PrintBanner Method + +```csharp +private static void PrintBanner(Context context) +``` + +**Purpose**: Writes the application name, version, and copyright notice to the context output. + +**Algorithm**: Writes three lines: the versioned tool name, the copyright notice, and a blank +separator line. + +**Preconditions**: `context` must not be null. + +**Postconditions**: Banner lines have been written to `context`. + +#### PrintHelp Method + +```csharp +private static void PrintHelp(Context context) +``` + +**Purpose**: Writes usage information and available options to the context output. + +**Algorithm**: Writes the usage line followed by a formatted table of all supported options. + +**Preconditions**: `context` must not be null. + +**Postconditions**: Help text has been written to `context`. + #### RunToolLogic Method ```csharp private static void RunToolLogic(Context context) ``` -Resolves the configuration file from `context.ConfigFile` (defaulting to `.fileassert.yaml`; -overridden by `--config`). When the default configuration file is absent, it prints guidance -without setting an error. When an explicitly specified file is absent, it calls -`context.WriteError` to signal failure. When the file exists, it calls -`FileAssertConfig.ReadFromFile` and then passes `context.Filters` (the positional name-or-tag -arguments) to `config.Run` so that only matching tests are executed. +**Purpose**: Loads the YAML configuration file and runs all matching assertions. + +**Algorithm**: Resolves the configuration file from `context.ConfigFile` (defaulting to +`.fileassert.yaml`; overridden by `--config`). When the default configuration file is absent, +prints guidance without setting an error and returns. When an explicitly specified file is absent, +calls `context.WriteError` to signal failure. When the file exists, calls +`FileAssertConfig.ReadFromFile` and then passes `context.Filters` to `config.Run` so that only +matching tests are executed. + +**Preconditions**: `context` must not be null. -### Interactions with Other Units +**Postconditions**: Either an error has been reported via `context.WriteError`, guidance has been +written for a missing default config, or all matching assertions have been executed and their +outcomes reflected in `context.ExitCode`. + +### Error Handling + +| Exception | Detection | Handling | +| :-------------------------- | :---------------------------------------------------------- | :--------------------------------------------------------------------- | +| `ArgumentException` | Invalid arguments detected by `Context.Create` or callee | Caught in `Main`; message printed to standard error; exit code `1` | +| `InvalidOperationException` | Invalid state detected by downstream code | Caught in `Main`; message printed to standard error; exit code `1` | +| All other exceptions | Unexpected runtime failure (programming error or I/O fault) | Printed to standard error and re-thrown; runtime generates crash log | + +### Interactions - `Context` provides the parsed flags, config path, filters, and output methods used by every execution path. @@ -83,21 +146,3 @@ arguments) to `config.Run` so that only matching tests are executed. expected error conditions; all other exceptions propagate to generate crash reports. - **Version from assembly attribute**: Using `AssemblyInformationalVersionAttribute` allows the CI pipeline to inject the exact package version (including pre-release labels) at build time. - -### Key Methods - -| Method | -| :------------- | -| `Main` | -| `Run` | -| `PrintBanner` | -| `PrintHelp` | -| `RunToolLogic` | - -### Error Handling - -| Exception | -| :-------------------------- | -| `ArgumentException` | -| `InvalidOperationException` | -| All other exceptions | diff --git a/docs/design/file-assert/selftest.md b/docs/design/file-assert/selftest.md index d2cdc5d..c98b8b9 100644 --- a/docs/design/file-assert/selftest.md +++ b/docs/design/file-assert/selftest.md @@ -19,6 +19,36 @@ produces structured test results that can be written to a TRX or JUnit XML file. - Optionally serialize results to TRX or JUnit XML format. - Report a system information header before running tests. +### Interfaces + +#### Exposed + +| Member / Class | Description | +| :----------------------- | :----------------------------------------------------------------------------------- | +| `Validation.Run(Context)` | Executes all built-in self-validation tests and writes results via `Context`. | + +#### Consumed + +| Dependency | Usage | +| :------------------------- | :---------------------------------------------------------------------------- | +| `Context` (Cli subsystem) | Receives test output and result file path; exposes `ResultsFile`. | +| `Program.Version` | Included in the system information header printed before tests run. | +| `Program.Run` | Called within each built-in test to exercise the tool's execution logic. | +| `TemporaryDirectory` (Utilities) | Provides isolated, self-cleaning workspaces for test fixture files. | +| `DemaConsulting.TestResults` | Serializes validation outcomes to TRX or JUnit XML when requested. | + +### Design + +`Validation.Run` executes a set of built-in test cases in a self-contained loop: + +1. A system information header (`Program.Version`, OS, runtime) is written via `Context.WriteLine`. +2. For each built-in test, a fresh `TemporaryDirectory` is created to hold fixture files, and a + dedicated `Context` is constructed from controlled argument arrays so the test runs in isolation. +3. `Program.Run` is invoked with the per-test context; pass or fail is determined by whether + `context.ExitCode` changed. +4. Results are accumulated and, when `context.ResultsFile` is non-null, serialized to TRX or JUnit + XML using `DemaConsulting.TestResults`. + ### Interactions with Other Subsystems | Dependency | Usage | diff --git a/docs/design/file-assert/selftest/validation.md b/docs/design/file-assert/selftest/validation.md index 359d10d..d3ba3e5 100644 --- a/docs/design/file-assert/selftest/validation.md +++ b/docs/design/file-assert/selftest/validation.md @@ -104,6 +104,7 @@ N/A — `Validation` is a `static` class with no instance fields. All state is l | Method | | :--------------------------------------------------------------------------- | | `Run(Context context)` *(public)* | +| `PrintValidationHeader(Context)` *(private)* | | `RunVersionTest(Context, TestResults)` *(private)* | | `RunHelpTest(Context, TestResults)` *(private)* | | `RunResultsTest(Context, TestResults)` *(private)* | @@ -111,6 +112,7 @@ N/A — `Validation` is a `static` class with no instance fields. All state is l | `RunContainsTest(Context, TestResults)` *(private)* | | `RunValidationTest(Context, TestResults, string, Func)` *(private)* | | `WriteResultsFile(Context, TestResults)` *(private)* | +| `CreateTestResult(string)` *(private)* | #### Error Handling diff --git a/docs/design/file-assert/utilities.md b/docs/design/file-assert/utilities.md index de370a8..7b353aa 100644 --- a/docs/design/file-assert/utilities.md +++ b/docs/design/file-assert/utilities.md @@ -8,24 +8,61 @@ domain subsystem. ### Subsystem Contents -| Unit | File | Responsibility | -| :------------ | :--------------- | :------------------------------------------------------------ | -| `PathHelpers` | `PathHelpers.cs` | Safe path-combination utility with path-traversal protection. | +| Unit | File | Responsibility | +| :------------------- | :---------------------- | :--------------------------------------------------------------------- | +| `PathHelpers` | `PathHelpers.cs` | Safe path-combination utility with path-traversal protection. | +| `TemporaryDirectory` | `TemporaryDirectory.cs` | Disposable temporary directory with safe path resolution and clean-up. | ### Subsystem Responsibilities - Provide path utilities that safely combine paths while preventing path-traversal attacks. - Reject relative paths containing `..` or absolute paths when a relative path is expected. +- Create uniquely-named temporary directories and delete them automatically on disposal. +- Ensure all file paths within a temporary directory remain within its boundary. + +### Interfaces + +#### Exposed + +| Class / Member | Description | +| :-------------------------------------------- | :--------------------------------------------------------------------------------------- | +| `PathHelpers.SafePathCombine(base, relative)` | Combines `base` and `relative`; throws `ArgumentException` if the result escapes `base`. | +| `TemporaryDirectory` *(constructor)* | Creates a uniquely-named subdirectory under `Environment.CurrentDirectory`. | +| `TemporaryDirectory.DirectoryPath` | Full path to the temporary directory. | +| `TemporaryDirectory.GetFilePath(relative)` | Resolves a relative path within the directory; creates intermediate subdirectories. | +| `TemporaryDirectory.Dispose()` | Deletes the temporary directory and all its contents. | + +#### Consumed + +| Dependency | Usage | +| :---------------------------- | :---------------------------------------------------------------------------- | +| .NET BCL (`Path`, `Directory`) | All path manipulation and file-system operations within both units. | + +### Design + +`PathHelpers` and `TemporaryDirectory` collaborate in a layered pattern: + +1. `TemporaryDirectory` delegates all path construction to `PathHelpers.SafePathCombine`, ensuring + that both the directory name itself and every relative path passed to `GetFilePath` are safe. +2. `PathHelpers` performs validation independently of `TemporaryDirectory`, so it can be used + directly by other subsystems (such as `SelfTest`) without going through `TemporaryDirectory`. + +Neither unit holds references to `Context` or any other subsystem; they are pure utilities with no +awareness of the tool's execution state. ### Interactions with Other Subsystems -| Consumer | Usage | -| :-------- | :----------------------------------------------------------------------- | -| SelfTest | Uses `PathHelpers.SafePathCombine` when creating temporary log files. | +| Consumer | Usage | +| :-------- | :---------------------------------------------------------------------------------------- | +| SelfTest | Uses `TemporaryDirectory` and `PathHelpers.SafePathCombine` for fixture file management. | +| Tests | Uses `TemporaryDirectory` for isolated file-system fixtures in all test projects. | ### Design Decisions -- **Static class**: `PathHelpers` is a static utility class with no instance state, suitable +- **Static class for PathHelpers**: `PathHelpers` is a static utility class with no instance state, suitable for use anywhere in the codebase without injection. - **Defense-in-depth validation**: Path safety is validated both before and after combining paths, guarding against edge cases that might bypass the initial checks. +- **`Environment.CurrentDirectory` over `Path.GetTempPath()`**: On macOS, `/tmp` is a symlink + to `/private/tmp`. Using the current directory avoids path-comparison failures caused by + symlink resolution. See *TemporaryDirectory Design* for details. diff --git a/docs/design/file-assert/utilities/path-helpers.md b/docs/design/file-assert/utilities/path-helpers.md index 63ae6dd..9c8df57 100644 --- a/docs/design/file-assert/utilities/path-helpers.md +++ b/docs/design/file-assert/utilities/path-helpers.md @@ -72,12 +72,15 @@ N/A — `PathHelpers` is a `static` class with no instance state or fields. #### Error Handling -| Scenario | -| :------------------------------------------------------- | -| Null `basePath` or `relativePath` | -| Combined path escapes base directory via `../` traversal | -| Path contains unsupported format | -| Combined or resolved path exceeds system maximum length | +- **Null `basePath` or `relativePath`**: `ArgumentNullException` thrown immediately by + `ArgumentNullException.ThrowIfNull`; not propagated further. +- **Combined path escapes base directory via `../` traversal**: `ArgumentException` thrown + with message `"Invalid path component: {relativePath}"`; `relativePath` is named as the + offending parameter so callers can identify the cause. +- **Path contains unsupported format**: `NotSupportedException` propagated from + `Path.GetFullPath` or `Path.Combine`; not caught by this method. +- **Combined or resolved path exceeds system maximum length**: `PathTooLongException` + propagated from `Path.GetFullPath`; not caught by this method. #### Interactions diff --git a/docs/reqstream/file-assert.yaml b/docs/reqstream/file-assert.yaml index 5074598..fff6a29 100644 --- a/docs/reqstream/file-assert.yaml +++ b/docs/reqstream/file-assert.yaml @@ -256,7 +256,7 @@ sections: do not contain forbidden strings, or match regular expression patterns. These assertions are the primary mechanism for content validation in CI/CD pipelines. The supported assertion types are: `contains`, `does-not-contain`, `matches` - (regex), and `does-not-match` (regex). + (regex), and `does-not-contain-regex` (regex). children: - FileAssert-Modeling-FileTypeParsing tests: @@ -396,6 +396,10 @@ sections: justification: | CI/CD pipelines operate across heterogeneous environments. Supporting all three major platforms ensures the tool can be used consistently regardless of the build agent OS. + children: + - FileAssert-Platform-Windows + - FileAssert-Platform-Linux + - FileAssert-Platform-MacOS tests: - IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero @@ -405,5 +409,9 @@ sections: justification: | Supporting multiple .NET runtimes allows projects targeting different LTS and current versions to adopt FileAssert without being forced to upgrade their runtime. + children: + - FileAssert-Platform-Net8 + - FileAssert-Platform-Net9 + - FileAssert-Platform-Net10 tests: - IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero diff --git a/docs/reqstream/file-assert/cli.yaml b/docs/reqstream/file-assert/cli.yaml index 7fe8896..18bc164 100644 --- a/docs/reqstream/file-assert/cli.yaml +++ b/docs/reqstream/file-assert/cli.yaml @@ -37,6 +37,7 @@ sections: - Cli_CreateContext_ParsesSilentValidateAndLogFlags - Cli_CreateContext_ParsesVersionHelpConfigResultsFlags - Cli_CreateContext_WithFilters_ParsesPositionalArguments + - Cli_CreateContext_ParsesDepthFlag - id: FileAssert-Cli-ErrorReporting title: The Cli subsystem shall set the exit code to 1 when errors are reported. @@ -46,6 +47,7 @@ sections: CI/CD pipelines and calling scripts can reliably detect failures. children: - FileAssert-Context-ExitCode + - FileAssert-Context-ErrorCount tests: - Cli_WriteError_AfterSuccessfulCreate_ChangesExitCodeToOne diff --git a/docs/reqstream/file-assert/configuration.yaml b/docs/reqstream/file-assert/configuration.yaml index 7328433..79b252a 100644 --- a/docs/reqstream/file-assert/configuration.yaml +++ b/docs/reqstream/file-assert/configuration.yaml @@ -34,3 +34,17 @@ sections: tests: - Configuration_RunWithFilter_ExecutesOnlyMatchingTests - Configuration_RunWithTagFilter_ExecutesOnlyMatchingTests + + - id: FileAssert-Configuration-ResultsFiles + title: The Configuration subsystem shall write per-test pass/fail results to a TRX or JUnit XML file when a results + file path is provided. + justification: | + CI/CD pipelines and regulated environments require machine-readable test result files + to collect evidence of assertion outcomes. Supporting both TRX and JUnit XML formats + at the subsystem level confirms that the configuration load, test execution, and results + output stages integrate correctly end-to-end. + children: + - FileAssert-FileAssertConfig-ResultsTrx + - FileAssert-FileAssertConfig-ResultsJUnit + tests: + - Configuration_Run_WithResultsFile_WritesTrxResultsFile diff --git a/docs/reqstream/file-assert/configuration/file-assert-config.yaml b/docs/reqstream/file-assert/configuration/file-assert-config.yaml index d7234f4..fd8d18f 100644 --- a/docs/reqstream/file-assert/configuration/file-assert-config.yaml +++ b/docs/reqstream/file-assert/configuration/file-assert-config.yaml @@ -19,6 +19,7 @@ sections: - FileAssertConfig_ReadFromFile_ValidFile_ReturnsConfig - FileAssertConfig_ReadFromFile_FileNotFound_ThrowsFileNotFoundException - FileAssertConfig_ReadFromFile_NullPath_ThrowsArgumentNullException + - FileAssertConfig_ReadFromFile_PdfAssertConfig_ParsesCorrectly - id: FileAssert-FileAssertConfig-Run title: | diff --git a/docs/reqstream/file-assert/modeling.yaml b/docs/reqstream/file-assert/modeling.yaml index 4ed8537..9807a36 100644 --- a/docs/reqstream/file-assert/modeling.yaml +++ b/docs/reqstream/file-assert/modeling.yaml @@ -14,6 +14,23 @@ sections: The Modeling subsystem integrates three unit types (FileAssertTest, FileAssertFile, FileAssertRule) into a single execution chain. Verifying the chain end-to-end at the subsystem level ensures that inter-unit delegation and data flow work correctly. + children: + - FileAssert-FileAssertTest-Creation + - FileAssert-FileAssertTest-Filtering + - FileAssert-FileAssertTest-Execution + - FileAssert-FileAssertTest-RunValidation + - FileAssert-FileAssertFile-Creation + - FileAssert-FileAssertFile-CountConstraints + - FileAssert-FileAssertFile-ContentRules + - FileAssert-FileAssertFile-ExactCount + - FileAssert-FileAssertFile-SizeConstraints + - FileAssert-FileAssertFile-FileTypeAssertDelegation + - FileAssert-FileAssertRule-Factory + - FileAssert-FileAssertRule-ContainsRule + - FileAssert-FileAssertRule-MatchesRule + - FileAssert-FileAssertRule-DoesNotContainRule + - FileAssert-FileAssertRule-DoesNotContainRegexRule + - FileAssert-FileAssertZipAssert-EntryMatching tests: - Modeling_ExecuteChain_PassesWhenAllConstraintsMet @@ -23,13 +40,18 @@ sections: Assertion failures must propagate from the innermost rule through the file and test layers to the context, setting the error exit code. Testing this at the subsystem level verifies that the full error-reporting pipeline within the subsystem works. + children: + - FileAssert-FileAssertTest-Execution + - FileAssert-FileAssertFile-CountConstraints + - FileAssert-FileAssertFile-ContentRules + - FileAssert-FileAssertFile-SizeConstraints tests: - Modeling_ExecuteChain_ReportsFailuresThroughContext - id: FileAssert-Modeling-FileTypeParsing title: | The Modeling subsystem shall parse matched files as structured documents (PDF, XML, - HTML, YAML, JSON) when the corresponding assertion block is declared, and report + HTML, YAML, JSON, ZIP) when the corresponding assertion block is declared, and report an immediate error if the file cannot be parsed. justification: | File-type parsing enables structured-document assertions (metadata, XPath, dot-notation @@ -38,6 +60,7 @@ sections: children: - FileAssert-FileAssertTextAssert-Creation - FileAssert-FileAssertTextAssert-RuleApplication + - FileAssert-FileAssertTextAssert-IOError - FileAssert-FileAssertPdfAssert-Creation - FileAssert-FileAssertPdfAssert-ParseError - FileAssert-FileAssertPdfAssert-MetadataAssertions diff --git a/docs/reqstream/file-assert/modeling/file-assert-file.yaml b/docs/reqstream/file-assert/modeling/file-assert-file.yaml index 56c79c7..1ee977e 100644 --- a/docs/reqstream/file-assert/modeling/file-assert-file.yaml +++ b/docs/reqstream/file-assert/modeling/file-assert-file.yaml @@ -80,3 +80,5 @@ sections: and the overall design extensible to future formats. tests: - FileAssertFile_Create_ValidData_CreatesFile + - FileAssertFile_Run_WithContentRule_ContentContainsValue_NoError + - FileAssertFile_Run_WithContentRule_ContentMissingValue_WritesError diff --git a/docs/reqstream/file-assert/modeling/file-assert-json-assert.yaml b/docs/reqstream/file-assert/modeling/file-assert-json-assert.yaml index cc6c08d..373827e 100644 --- a/docs/reqstream/file-assert/modeling/file-assert-json-assert.yaml +++ b/docs/reqstream/file-assert/modeling/file-assert-json-assert.yaml @@ -41,3 +41,5 @@ sections: - FileAssertJsonAssert_Run_ArrayCount_Mismatch_WritesError - FileAssertJsonAssert_Run_MinMaxCount_WithinBounds_NoError - FileAssertJsonAssert_Run_ScalarValue_CountsAsOne_NoError + - FileAssertJsonAssert_Run_MinCount_BelowMinimum_WritesError + - FileAssertJsonAssert_Run_MaxCount_ExceedsMaximum_WritesError diff --git a/docs/reqstream/file-assert/modeling/file-assert-xml-assert.yaml b/docs/reqstream/file-assert/modeling/file-assert-xml-assert.yaml index 19be843..3b6c6dd 100644 --- a/docs/reqstream/file-assert/modeling/file-assert-xml-assert.yaml +++ b/docs/reqstream/file-assert/modeling/file-assert-xml-assert.yaml @@ -41,6 +41,8 @@ sections: - FileAssertXmlAssert_Run_ExactCount_Matches_NoError - FileAssertXmlAssert_Run_ExactCount_Mismatch_WritesError - FileAssertXmlAssert_Run_MinMaxCount_WithinBounds_NoError + - FileAssertXmlAssert_Run_MinCount_NotMet_WritesError + - FileAssertXmlAssert_Run_MaxCount_Exceeded_WritesError - FileAssertXmlAssert_Run_InvalidXPathQuery_WritesError - FileAssertXmlAssert_Run_XPathExactTextMatch_Matches_NoError - FileAssertXmlAssert_Run_XPathExactTextMatch_NoMatch_WritesError diff --git a/docs/reqstream/file-assert/utilities.yaml b/docs/reqstream/file-assert/utilities.yaml index 4a80600..c596c39 100644 --- a/docs/reqstream/file-assert/utilities.yaml +++ b/docs/reqstream/file-assert/utilities.yaml @@ -20,3 +20,16 @@ sections: - FileAssert-PathHelpers-NullValidation tests: - Utilities_SafePathCombine_PreventsPathTraversalToFileSystem + + - id: FileAssert-Utilities-TemporaryDirectory + title: The Utilities subsystem shall provide a disposable temporary directory for isolated file operations. + justification: | + Tests and other consumers that write scratch files need an isolated location that + is automatically created on demand and cleaned up on disposal. Providing this as a + shared utility prevents ad-hoc temp-file management scattered across the codebase + and ensures cleanup even when an operation fails. + children: + - FileAssert-TemporaryDirectory-Lifecycle + - FileAssert-TemporaryDirectory-SafePath + tests: + - Utilities_TemporaryDirectory_IsolatesAndCleansUpScratchSpace diff --git a/docs/reqstream/ots/fileassert.yaml b/docs/reqstream/ots/fileassert.yaml index 25a390e..4734daf 100644 --- a/docs/reqstream/ots/fileassert.yaml +++ b/docs/reqstream/ots/fileassert.yaml @@ -20,3 +20,6 @@ sections: tests: - FileAssert_VersionDisplay - FileAssert_HelpDisplay + - FileAssert_Results + - FileAssert_Exists + - FileAssert_Contains diff --git a/docs/reqstream/ots/versionmark.yaml b/docs/reqstream/ots/versionmark.yaml index 9dfed9b..22a017e 100644 --- a/docs/reqstream/ots/versionmark.yaml +++ b/docs/reqstream/ots/versionmark.yaml @@ -9,7 +9,7 @@ sections: - title: VersionMark Requirements requirements: - id: FileAssert-OTS-VersionMark - title: VersionMark shall publish captured tool-version information. + title: VersionMark shall capture and publish tool-version information. justification: | DemaConsulting.VersionMark reads version metadata for each dotnet tool used in the pipeline and writes a versions markdown document included in the release artifacts. diff --git a/docs/reqstream/ots/weasyprint.yaml b/docs/reqstream/ots/weasyprint.yaml index 054bbf8..751218b 100644 --- a/docs/reqstream/ots/weasyprint.yaml +++ b/docs/reqstream/ots/weasyprint.yaml @@ -13,9 +13,9 @@ sections: justification: | DemaConsulting.WeasyPrintTool converts HTML documents to PDF as part of the documentation build pipeline. FileAssert validates that each generated PDF file - exists, contains at least one page, has correct document metadata, and includes - expected content in the rendered text. Passing FileAssert assertions for each - document type proves WeasyPrint executed correctly and produced meaningful output. + exists, has a non-trivial size, contains at least one page, and includes expected + content in the rendered text. Passing FileAssert assertions for each document type + proves WeasyPrint executed correctly and produced meaningful output. tags: [ots] tests: - WeasyPrint_BuildNotesPdf diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index d42306d..66cb315 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -316,7 +316,7 @@ tests: | `json[].count` | Exact number of matched JSON nodes | | `json[].min` | Minimum number of matched JSON nodes | | `json[].max` | Maximum number of matched JSON nodes | -| `zip:` | Zip archive assertion block | +| `zip:` | Zip archive entry assertions (fails if not a valid zip) | | `zip.entries[].pattern` | Glob pattern matching zip entry names | | `zip.entries[].min` | Minimum number of matching entries | | `zip.entries[].max` | Maximum number of matching entries | diff --git a/docs/verification/file-assert.md b/docs/verification/file-assert.md index 87127d4..36b623a 100644 --- a/docs/verification/file-assert.md +++ b/docs/verification/file-assert.md @@ -1,4 +1,4 @@ -# System Verification +# FileAssert System Verification This document describes the system-level verification design for FileAssert. It defines the overall verification strategy, test environments, interface simulation approach, and end-to-end integration @@ -236,20 +236,6 @@ a positional filter argument. **Expected**: Exit code non-zero. -### FileAssertZipAssert_Run_MatchingEntriesMeetConstraints_NoError - -**Scenario**: A zip assertion is configured and the archive contains entries that satisfy -the declared minimum and maximum count constraints. - -**Expected**: Exit code 0. - -### FileAssertZipAssert_Run_TooFewMatchingEntries_WritesError - -**Scenario**: A zip assertion is configured with a minimum count but the archive contains -fewer matching entries than required. - -**Expected**: Exit code non-zero. - ### IntegrationTest_ZipAssert_PassingQuery_ReturnsZero **Scenario**: A zip assertion is configured and the archive contains entries that satisfy @@ -265,7 +251,10 @@ the declared constraints. ### IntegrationTest_HtmlAssert_InvalidFile_ReturnsNonZero -**Scenario**: An HTML assertion is configured but the target file is not valid HTML. +**Scenario**: An HTML assertion is configured with an XPath query that yields no matching elements +and a `min: 1` constraint. Note: HtmlAgilityPack is intentionally lenient and does not raise a +parse error for malformed HTML; this test exercises the assertion-failure path (zero matching +elements) rather than a parse-failure path. **Expected**: Exit code non-zero. @@ -313,8 +302,7 @@ the declared constraints. IntegrationTest_YamlAssert_InvalidFile_ReturnsNonZero, IntegrationTest_JsonAssert_PassingQuery_ReturnsZero, IntegrationTest_JsonAssert_InvalidFile_ReturnsNonZero, - IntegrationTest_PdfAssert_InvalidFile_ReturnsNonZero -- **Zip archive assertions**: FileAssertZipAssert_Run_MatchingEntriesMeetConstraints_NoError, - FileAssertZipAssert_Run_TooFewMatchingEntries_WritesError, - IntegrationTest_ZipAssert_PassingQuery_ReturnsZero, + IntegrationTest_PdfAssert_InvalidFile_ReturnsNonZero, + IntegrationTest_PdfAssert_FailingAssertion_ReturnsNonZero +- **Zip archive assertions**: IntegrationTest_ZipAssert_PassingQuery_ReturnsZero, IntegrationTest_ZipAssert_InvalidFile_ReturnsNonZero diff --git a/docs/verification/file-assert/cli.md b/docs/verification/file-assert/cli.md index 3a4654f..ed2d229 100644 --- a/docs/verification/file-assert/cli.md +++ b/docs/verification/file-assert/cli.md @@ -4,7 +4,7 @@ This document describes the subsystem-level verification design for the `Cli` su defines the integration test approach, subsystem boundary, mocking strategy, and test scenarios that together verify the `Cli` subsystem requirements. -### Verification Approach +### Verification Strategy The `Cli` subsystem boundary at `Program` is verified by integration tests defined in `CliTests.cs`. Each test exercises `Context.Create` and `Program.Run` together, treating the pair @@ -77,6 +77,12 @@ through `Context.Create`. **Expected**: The message appears in the log file; exit code is 0. +#### Cli_CreateContext_ParsesDepthFlag + +**Scenario**: Arguments containing `--depth 3` are passed through `Context.Create`. + +**Expected**: The `Depth` property is set to `3`; exit code is 0. + #### Cli_OutputPipeline_WithoutSilentFlag_WritesMessagesToConsole **Scenario**: A context without the `--silent` flag is created; `Context.WriteLine` is @@ -90,6 +96,7 @@ called with a message. Cli_CreateContext_ParsesVersionHelpConfigResultsFlags, Cli_CreateContext_WithFilters_ParsesPositionalArguments - **Unknown argument rejection**: Cli_CreateContext_UnknownArgument_ThrowsArgumentException +- **Typed property exposure (depth)**: Cli_CreateContext_ParsesDepthFlag - **Error exit code**: Cli_WriteError_AfterSuccessfulCreate_ChangesExitCodeToOne - **Log file output**: Cli_OutputPipeline_WithLogPathAndSilentFlag_WritesMessagesToLogFile - **Console output**: Cli_OutputPipeline_WithoutSilentFlag_WritesMessagesToConsole diff --git a/docs/verification/file-assert/cli/context.md b/docs/verification/file-assert/cli/context.md index ceb40cd..dd60a73 100644 --- a/docs/verification/file-assert/cli/context.md +++ b/docs/verification/file-assert/cli/context.md @@ -203,7 +203,7 @@ with a test message. **Scenario**: `Context.Create` is called with `["--depth", "3"]`. -**Expected**: `HeadingDepth` property equals 3. +**Expected**: `Depth` property equals 3. **Requirement coverage**: Depth flag parsing requirement. @@ -211,7 +211,7 @@ with a test message. **Scenario**: `Context.Create` is called with an empty argument array. -**Expected**: `HeadingDepth` property equals 1 (the default). +**Expected**: `Depth` property equals 1 (the default). **Requirement coverage**: Default heading depth requirement. diff --git a/docs/verification/file-assert/configuration.md b/docs/verification/file-assert/configuration.md index b7bcc20..b6f8839 100644 --- a/docs/verification/file-assert/configuration.md +++ b/docs/verification/file-assert/configuration.md @@ -55,8 +55,17 @@ a filter naming one tag is passed to `FileAssertConfig.Run`. **Expected**: Only the test matching the tag runs; exit code is 0. +#### Configuration_Run_WithResultsFile_WritesTrxResultsFile + +**Scenario**: A configuration file with one test is loaded. A results file path with a `.trx` +extension is provided to the context via `--results`. + +**Expected**: `FileAssertConfig.Run` completes and a TRX results file is written to the specified +path. + ### Requirements Coverage - **YAML loading and hierarchy construction**: Configuration_LoadYaml_BuildsCompleteTestHierarchy - **Test name filtering**: Configuration_RunWithFilter_ExecutesOnlyMatchingTests - **Tag filtering**: Configuration_RunWithTagFilter_ExecutesOnlyMatchingTests +- **Results file output (TRX/JUnit XML)**: Configuration_Run_WithResultsFile_WritesTrxResultsFile diff --git a/docs/verification/file-assert/configuration/file-assert-config.md b/docs/verification/file-assert/configuration/file-assert-config.md index 4e6b8b6..a6c6e23 100644 --- a/docs/verification/file-assert/configuration/file-assert-config.md +++ b/docs/verification/file-assert/configuration/file-assert-config.md @@ -115,4 +115,4 @@ assertion configuration (pages, metadata, text rules). - **Non-matching filter**: FileAssertConfig_Run_WithNonMatchingFilter_SkipsTests - **TRX results output**: FileAssertConfig_Run_WithResultsFile_WritesTrxWithPassedOutcome - **JUnit results output**: FileAssertConfig_Run_WithResultsFile_WritesJUnitWithFailedOutcome -- **PDF config parsing**: FileAssertConfig_ReadFromFile_PdfAssertConfig_ParsesCorrectly +- **Configuration file reading (PDF variant)**: FileAssertConfig_ReadFromFile_PdfAssertConfig_ParsesCorrectly diff --git a/docs/verification/file-assert/configuration/file-assert-data.md b/docs/verification/file-assert/configuration/file-assert-data.md index 899d22c..676ab9f 100644 --- a/docs/verification/file-assert/configuration/file-assert-data.md +++ b/docs/verification/file-assert/configuration/file-assert-data.md @@ -25,6 +25,21 @@ subsystem level when all unit tests supporting a subsystem requirement pass. `FileAssertData` depends only on YamlDotNet deserialization annotations. No mocking is needed. +#### Test Scenarios + +The following named scenarios exercise the `FileAssertData` schema indirectly via +`FileAssertConfig.ReadFromFile`: + +- **Valid minimal YAML schema deserializes correctly** — A YAML file containing a single + `tests` entry with a `name` and a `files` list with one `pattern` is parsed without error + and the resulting domain object reflects the expected values. + Covered by: `FileAssertConfig_ReadFromFile_ValidFile_ReturnsConfig`. + +- **PDF assertion block deserializes correctly** — A YAML file containing a `pdf:` block + with `metadata`, `pages`, and `text` sub-keys is parsed without error and the resulting + domain object contains a non-null PDF assertion. + Covered by: `FileAssertConfig_ReadFromFile_PdfAssertConfig_ParsesCorrectly`. + #### Coverage `FileAssertData` objects are verified indirectly by every `FileAssertConfig_ReadFromFile_*` test diff --git a/docs/verification/file-assert/modeling.md b/docs/verification/file-assert/modeling.md index 3dd80b5..35829dd 100644 --- a/docs/verification/file-assert/modeling.md +++ b/docs/verification/file-assert/modeling.md @@ -4,7 +4,7 @@ This document describes the subsystem-level verification design for the `Modelin defines the integration test approach, subsystem boundary, mocking strategy, and test scenarios that together verify the `Modeling` subsystem requirements. -### Verification Approach +### Verification Strategy The `Modeling` subsystem is verified by integration tests defined in `ModelingTests.cs`. Each test exercises the assertion execution pipeline — creating a `FileAssertTest`, resolving file patterns, @@ -66,5 +66,3 @@ satisfying the query and count constraints is provided. Modeling_ExecuteChain_ReportsFailuresThroughContext - **XML parsing error reporting**: Modeling_FileTypeParsing_InvalidXml_ReportsParseError - **XML query assertion**: Modeling_QueryAssertions_XmlQueryMeetsCount_NoError -- **Zip assert creation and parse error**: FileAssertZipAssert_Create_ValidData_CreatesZipAssert, - FileAssertZipAssert_Run_InvalidZipFile_WritesError diff --git a/docs/verification/file-assert/modeling/file-assert-file.md b/docs/verification/file-assert/modeling/file-assert-file.md index fd3b041..47c4655 100644 --- a/docs/verification/file-assert/modeling/file-assert-file.md +++ b/docs/verification/file-assert/modeling/file-assert-file.md @@ -150,3 +150,6 @@ file is larger than the maximum. - **Content rules**: FileAssertFile_Run_WithContentRule_ContentContainsValue_NoError, FileAssertFile_Run_WithContentRule_ContentMissingValue_WritesError, FileAssertFile_Run_MultipleFiles_MultipleFailContentRule_WritesErrorForEachViolation +- **File-type assert delegation**: FileAssertFile_Create_ValidData_CreatesFile, + FileAssertFile_Run_WithContentRule_ContentContainsValue_NoError, + FileAssertFile_Run_WithContentRule_ContentMissingValue_WritesError diff --git a/docs/verification/file-assert/modeling/file-assert-text-assert.md b/docs/verification/file-assert/modeling/file-assert-text-assert.md index 7046751..d275060 100644 --- a/docs/verification/file-assert/modeling/file-assert-text-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-text-assert.md @@ -80,9 +80,9 @@ subsystem level when all unit tests supporting a subsystem requirement pass. #### Requirements Coverage -- **Text assert creation**: FileAssertTextAssert_Create_ValidData_CreatesTextAssert -- **Null guard**: FileAssertTextAssert_Create_NullData_ThrowsArgumentNullException -- **Pass**: FileAssertTextAssert_Run_FileContainsText_NoError -- **Fail**: FileAssertTextAssert_Run_FileMissingText_WritesError -- **Missing file**: FileAssertTextAssert_Run_NonExistentFile_WritesError -- **Multiple violations**: FileAssertTextAssert_Run_MultipleRulesMultipleViolations_WritesMultipleErrors +- **FileAssert-FileAssertTextAssert-Creation**: FileAssertTextAssert_Create_ValidData_CreatesTextAssert, + FileAssertTextAssert_Create_NullData_ThrowsArgumentNullException +- **FileAssert-FileAssertTextAssert-RuleApplication**: FileAssertTextAssert_Run_FileContainsText_NoError, + FileAssertTextAssert_Run_FileMissingText_WritesError, + FileAssertTextAssert_Run_MultipleRulesMultipleViolations_WritesMultipleErrors +- **FileAssert-FileAssertTextAssert-IOError**: FileAssertTextAssert_Run_NonExistentFile_WritesError diff --git a/docs/verification/file-assert/modeling/file-assert-xml-assert.md b/docs/verification/file-assert/modeling/file-assert-xml-assert.md index 2fd49f0..9f1380a 100644 --- a/docs/verification/file-assert/modeling/file-assert-xml-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-xml-assert.md @@ -80,6 +80,24 @@ query result count is within bounds. **Requirement coverage**: Min/max count constraint pass requirement. +##### FileAssertXmlAssert_Run_MinCount_NotMet_WritesError + +**Scenario**: `FileAssertXmlAssert.Run` is called with a `min` count constraint and the XPath +query returns fewer nodes than the minimum. + +**Expected**: An error is written to the context; exit code is non-zero. + +**Requirement coverage**: Min count constraint violation requirement. + +##### FileAssertXmlAssert_Run_MaxCount_Exceeded_WritesError + +**Scenario**: `FileAssertXmlAssert.Run` is called with a `max` count constraint and the XPath +query returns more nodes than the maximum. + +**Expected**: An error is written to the context; exit code is non-zero. + +**Requirement coverage**: Max count constraint violation requirement. + ##### FileAssertXmlAssert_Run_InvalidXPathQuery_WritesError **Scenario**: `FileAssertXmlAssert.Run` is called with a malformed XPath query string. @@ -132,7 +150,9 @@ result does not contain the expected value. - **Invalid query**: FileAssertXmlAssert_Run_InvalidXPathQuery_WritesError - **Count constraints**: FileAssertXmlAssert_Run_ExactCount_Matches_NoError, FileAssertXmlAssert_Run_ExactCount_Mismatch_WritesError, - FileAssertXmlAssert_Run_MinMaxCount_WithinBounds_NoError + FileAssertXmlAssert_Run_MinMaxCount_WithinBounds_NoError, + FileAssertXmlAssert_Run_MinCount_NotMet_WritesError, + FileAssertXmlAssert_Run_MaxCount_Exceeded_WritesError - **Text assertions**: FileAssertXmlAssert_Run_XPathExactTextMatch_Matches_NoError, FileAssertXmlAssert_Run_XPathExactTextMatch_NoMatch_WritesError, FileAssertXmlAssert_Run_XPathContainsText_Matches_NoError, diff --git a/docs/verification/file-assert/modeling/file-assert-yaml-assert.md b/docs/verification/file-assert/modeling/file-assert-yaml-assert.md index 9de3f05..a2863aa 100644 --- a/docs/verification/file-assert/modeling/file-assert-yaml-assert.md +++ b/docs/verification/file-assert/modeling/file-assert-yaml-assert.md @@ -121,6 +121,16 @@ a count of 1 is asserted. **Requirement coverage**: Scalar value counts as one requirement. +##### FileAssertYamlAssert_Run_EmptyDocument_ReportsZeroCount + +**Scenario**: `FileAssertYamlAssert.Run` is called with an empty YAML file (a stream with no +documents). + +**Expected**: The query returns a count of 0; if the assertion specifies `min: 1` an error is +written; if no lower bound is set no error is written. + +**Boundary / error path**: Empty YAML document edge case. + ##### FileAssertYamlAssert_Run_MinCount_BelowMinimum_WritesError **Scenario**: `FileAssertYamlAssert.Run` is called with a minimum count constraint that is not @@ -153,4 +163,5 @@ exceeded. FileAssertYamlAssert_Run_MinMaxCount_WithinBounds_NoError, FileAssertYamlAssert_Run_ScalarValue_CountsAsOne_NoError, FileAssertYamlAssert_Run_MinCount_BelowMinimum_WritesError, - FileAssertYamlAssert_Run_MaxCount_ExceedsMaximum_WritesError + FileAssertYamlAssert_Run_MaxCount_ExceedsMaximum_WritesError, + FileAssertYamlAssert_Run_EmptyDocument_ReportsZeroCount diff --git a/docs/verification/file-assert/selftest.md b/docs/verification/file-assert/selftest.md index af60ab7..9bf5648 100644 --- a/docs/verification/file-assert/selftest.md +++ b/docs/verification/file-assert/selftest.md @@ -4,7 +4,7 @@ This document describes the subsystem-level verification design for the `SelfTes defines the integration test approach, subsystem boundary, mocking strategy, and test scenarios that together verify the `SelfTest` subsystem requirements. -### Verification Approach +### Verification Strategy The `SelfTest` subsystem is verified by integration tests defined in `SelfTestTests.cs`. Each test exercises the `Validation.Run` method with a real `Context` to confirm that the subsystem @@ -29,7 +29,7 @@ this document execute and pass in the CI pipeline without any test failures, une exceptions, or assertion errors. Each named scenario must pass on all supported runtime and platform combinations. -### Integration Test Scenarios +### Test Scenarios The following integration test scenarios are defined in `SelfTestTests.cs`. @@ -56,6 +56,8 @@ element; exit code is 0. ### Requirements Coverage -- **Self-validation execution**: SelfTest_Run_ExecutesBuiltInTestsAndProducesSummary -- **System info header**: SelfTest_Run_WhenInvoked_PrintsSystemInfoHeader -- **TRX results output**: SelfTest_Run_WithResultsFile_WritesTrxResultsFile +| Requirement | Scenario | Test Method(s) | +| :---------- | :------- | :------------- | +| FileAssert-SelfTest-ValidationPipeline | SelfTest runs all built-in tests and produces a summary | SelfTest_Run_ExecutesBuiltInTestsAndProducesSummary | +| FileAssert-SelfTest-ValidationPipeline | SelfTest prints a system information header | SelfTest_Run_WhenInvoked_PrintsSystemInfoHeader | +| FileAssert-SelfTest-ValidationPipeline | SelfTest writes a TRX results file when requested | SelfTest_Run_WithResultsFile_WritesTrxResultsFile | diff --git a/docs/verification/file-assert/selftest/validation.md b/docs/verification/file-assert/selftest/validation.md index d845a2d..bf1c230 100644 --- a/docs/verification/file-assert/selftest/validation.md +++ b/docs/verification/file-assert/selftest/validation.md @@ -40,7 +40,7 @@ No test doubles are introduced at the `Validation` unit level. **Boundary / error path**: Null guard at the unit boundary. -**Coverage type**: Defensive/boundary test — no formal requirement. +**Requirement coverage**: `FileAssert-Validation-NullContext` — null context rejection requirement. ##### Validation_Run_WithSilentContext_PrintsSummary @@ -86,7 +86,8 @@ is written to `context` indicating the unsupported format. **Boundary / error path**: Tests the unsupported-format error path. -**Coverage type**: Defensive/boundary test — no formal requirement. +**Requirement coverage**: `FileAssert-Validation-Results` — results file output requirement +(boundary condition: unsupported extension is rejected without creating a file). ##### Validation_Run_WithSilentContext_LogContainsFileAssertResults @@ -124,13 +125,13 @@ is written to `context` indicating the unsupported format. | Requirement | Test Scenario | |--------------------------------------|-----------------------------------------------------------------| -| Defensive boundary (no req.) | Validation_Run_NullContext_ThrowsArgumentNullException | -| Summary output | Validation_Run_WithSilentContext_PrintsSummary | -| Successful exit code | Validation_Run_WithSilentContext_ExitCodeIsZero | -| TRX results output | Validation_Run_WithTrxResultsFile_WritesTrxFile | -| JUnit results output | Validation_Run_WithXmlResultsFile_WritesXmlFile | -| Defensive boundary (no req.) | Validation_Run_WithUnsupportedResultsFormat_DoesNotWriteFile | -| Logging | Validation_Run_WithSilentContext_LogContainsFileAssertResults | -| Self-validation content | Validation_Run_WithSilentContext_LogContainsFileAssertExists, | -| | Validation_Run_WithSilentContext_LogContainsFileAssertContains | -| Heading depth | Validation_Run_WithDepth_UsesSpecifiedHeadingDepth | +| FileAssert-Validation-NullContext | Validation_Run_NullContext_ThrowsArgumentNullException | +| FileAssert-Validation-Run | Validation_Run_WithSilentContext_PrintsSummary | +| FileAssert-Validation-Run | Validation_Run_WithSilentContext_ExitCodeIsZero | +| FileAssert-Validation-Results | Validation_Run_WithTrxResultsFile_WritesTrxFile | +| FileAssert-Validation-Results | Validation_Run_WithXmlResultsFile_WritesXmlFile | +| FileAssert-Validation-Results | Validation_Run_WithUnsupportedResultsFormat_DoesNotWriteFile | +| FileAssert-Validation-ResultsTest | Validation_Run_WithSilentContext_LogContainsFileAssertResults | +| FileAssert-Validation-ExistsTest | Validation_Run_WithSilentContext_LogContainsFileAssertExists | +| FileAssert-Validation-ContainsTest | Validation_Run_WithSilentContext_LogContainsFileAssertContains | +| FileAssert-Validation-Depth | Validation_Run_WithDepth_UsesSpecifiedHeadingDepth | diff --git a/docs/verification/file-assert/utilities.md b/docs/verification/file-assert/utilities.md index a2cd05e..1f8e8ee 100644 --- a/docs/verification/file-assert/utilities.md +++ b/docs/verification/file-assert/utilities.md @@ -42,6 +42,7 @@ The following integration test scenarios are defined in `UtilitiesTests.cs`. ### Requirements Coverage - **Path traversal prevention**: Utilities_SafePathCombine_PreventsPathTraversalToFileSystem +- **Temporary directory isolation and cleanup**: Utilities_TemporaryDirectory_IsolatesAndCleansUpScratchSpace ### TemporaryDirectory Verification diff --git a/docs/verification/file-assert/utilities/path-helpers.md b/docs/verification/file-assert/utilities/path-helpers.md index b73f36b..e7f3fe4 100644 --- a/docs/verification/file-assert/utilities/path-helpers.md +++ b/docs/verification/file-assert/utilities/path-helpers.md @@ -132,13 +132,16 @@ base path. #### Requirements Coverage -- (valid path combination): PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly -- (leading traversal rejection): PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException -- (embedded traversal rejection): PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentException -- (absolute path rejection): PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException -- (current-directory prefix): PathHelpers_SafePathCombine_CurrentDirectoryReference_CombinesCorrectly -- (nested path combination): PathHelpers_SafePathCombine_NestedPaths_CombinesCorrectly -- (empty relative path): PathHelpers_SafePathCombine_EmptyRelativePath_ReturnsBasePath -- (dot-dot filename, not traversal): PathHelpers_SafePathCombine_DoubleDotInFilename_CombinesCorrectly -- (null basePath rejection): PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException -- (null relativePath rejection): PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullException +- **FileAssert-PathHelpers-SafeCombine** (safe path combination): + - PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly + - PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException + - PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentException + - PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException + - PathHelpers_SafePathCombine_CurrentDirectoryReference_CombinesCorrectly + - PathHelpers_SafePathCombine_NestedPaths_CombinesCorrectly + - PathHelpers_SafePathCombine_EmptyRelativePath_ReturnsBasePath + - PathHelpers_SafePathCombine_DoubleDotInFilename_CombinesCorrectly + +- **FileAssert-PathHelpers-NullValidation** (null input rejection): + - PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException + - PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullException diff --git a/docs/verification/ots/fileassert.md b/docs/verification/ots/fileassert.md index dfeea68..83c7dfd 100644 --- a/docs/verification/ots/fileassert.md +++ b/docs/verification/ots/fileassert.md @@ -40,9 +40,37 @@ pipeline. **Requirement coverage**: `FileAssert-OTS-FileAssert`. +#### FileAssert_Results + +**Scenario**: FileAssert self-validation exercises the `--results` flag by running a configuration with one +passing test and one deliberately failing test, then verifying that a TRX results file is written. + +**Expected**: Exits non-zero (due to the failing test) and creates a TRX results file at the specified path. + +**Requirement coverage**: `FileAssert-OTS-FileAssert`. + +#### FileAssert_Exists + +**Scenario**: FileAssert self-validation exercises file-existence checking by matching a glob pattern against +a temporary directory containing a single `.txt` file. + +**Expected**: Exits 0, confirming that the glob-based file-existence assertion passes. + +**Requirement coverage**: `FileAssert-OTS-FileAssert`. + +#### FileAssert_Contains + +**Scenario**: FileAssert self-validation exercises file-content checking by asserting that a temporary `.txt` +file contains a known string. + +**Expected**: Exits 0, confirming that the text `contains` assertion passes. + +**Requirement coverage**: `FileAssert-OTS-FileAssert`. + ### Requirements Coverage -- **`FileAssert-OTS-FileAssert`**: FileAssert_VersionDisplay, FileAssert_HelpDisplay +- **`FileAssert-OTS-FileAssert`**: FileAssert_VersionDisplay, FileAssert_HelpDisplay, FileAssert_Results, + FileAssert_Exists, FileAssert_Contains ### Acceptance Criteria diff --git a/docs/verification/ots/pandoc.md b/docs/verification/ots/pandoc.md index 87649ac..b59e9b7 100644 --- a/docs/verification/ots/pandoc.md +++ b/docs/verification/ots/pandoc.md @@ -11,9 +11,9 @@ assertions for each document type proves Pandoc executed correctly and produced ### Verification Approach -Pandoc is verified by self-validation evidence from the CI pipeline. Each scenario is a FileAssert -assertion that runs after Pandoc converts a specific Markdown document to HTML. A passing pipeline -run for all scenarios constitutes evidence that the requirement is satisfied. +Pandoc is verified by CI pipeline evidence. Each scenario is a FileAssert assertion that runs after +Pandoc converts a specific Markdown document to HTML. A passing pipeline run for all scenarios +constitutes evidence that the requirement is satisfied. ### Test Scenarios diff --git a/docs/verification/ots/reviewmark.md b/docs/verification/ots/reviewmark.md index 165e07a..7557fd1 100644 --- a/docs/verification/ots/reviewmark.md +++ b/docs/verification/ots/reviewmark.md @@ -1,6 +1,6 @@ ## ReviewMark Verification -This document provides the verification evidence for the ReviewMark OTS software item. Requirements +This document provides the verification evidence for the `ReviewMark` OTS software item. Requirements for this OTS item are defined in the ReviewMark OTS Software Requirements document. ### Required Functionality diff --git a/docs/verification/ots/versionmark.md b/docs/verification/ots/versionmark.md index 985fe59..f6c0820 100644 --- a/docs/verification/ots/versionmark.md +++ b/docs/verification/ots/versionmark.md @@ -4,10 +4,9 @@ This document provides the verification evidence for the `VersionMark` OTS softw ### Required Functionality -DemaConsulting.VersionMark reads version metadata for each dotnet tool used in the pipeline and -writes a versions markdown document included in the release artifacts. It runs in the same CI -pipeline that produces the TRX test results, so a successful pipeline run is evidence that -VersionMark executed without error. +DemaConsulting.VersionMark captures version metadata for each dotnet tool used in the pipeline +via `--capture`, then publishes that metadata as a versions markdown document via `--publish`. +The published document is included in the Build Notes release artifact. ### Verification Approach diff --git a/src/DemaConsulting.FileAssert/Cli/Context.cs b/src/DemaConsulting.FileAssert/Cli/Context.cs index 9c43edc..0dbc548 100644 --- a/src/DemaConsulting.FileAssert/Cli/Context.cs +++ b/src/DemaConsulting.FileAssert/Cli/Context.cs @@ -139,9 +139,10 @@ public static Context Create(string[] args) } /// - /// Opens the log file for writing + /// Opens the log file for writing. /// - /// Log file path + /// Log file path. + /// Thrown when the log file cannot be opened due to an I/O or access error. private void OpenLogFile(string logFile) { try @@ -160,7 +161,7 @@ private void OpenLogFile(string logFile) } /// - /// Helper class for parsing command-line arguments + /// Helper class for parsing command-line arguments. /// private sealed class ArgumentParser { @@ -215,7 +216,7 @@ private sealed class ArgumentParser public List Filters { get; } = []; /// - /// Parses command-line arguments + /// Parses command-line arguments. /// /// Command-line arguments. public void ParseArguments(string[] args) @@ -232,12 +233,13 @@ public void ParseArguments(string[] args) } /// - /// Parses a single argument + /// Parses a single argument. /// - /// Argument to parse - /// All arguments - /// Current index - /// Updated index + /// Argument to parse. + /// All arguments. + /// Current index. + /// Updated index. + /// Thrown when an unknown flag or a missing required value is encountered. private int ParseArgument(string arg, string[] args, int index) { switch (arg) @@ -298,13 +300,14 @@ private int ParseArgument(string arg, string[] args, int index) } /// - /// Gets a required string argument value + /// Gets a required string argument value. /// - /// Argument name - /// All arguments - /// Current index - /// Description of what's required - /// Argument value + /// Argument name. + /// All arguments. + /// Current index. + /// Description of what's required. + /// Argument value. + /// Thrown when no value follows the argument flag. private static string GetRequiredStringArgument(string arg, string[] args, int index, string description) { if (index >= args.Length) diff --git a/src/DemaConsulting.FileAssert/Modeling/FileAssertHtmlAssert.cs b/src/DemaConsulting.FileAssert/Modeling/FileAssertHtmlAssert.cs index 47c1c8a..8bf99f0 100644 --- a/src/DemaConsulting.FileAssert/Modeling/FileAssertHtmlAssert.cs +++ b/src/DemaConsulting.FileAssert/Modeling/FileAssertHtmlAssert.cs @@ -39,6 +39,9 @@ internal sealed class FileAssertHtmlAssert /// The maximum node count expected, or null for no upper-bound constraint. private sealed record HtmlQuery(string Query, int? Count, int? Min, int? Max); + /// + /// The ordered list of XPath query assertions to evaluate against the parsed HTML document. + /// private readonly IReadOnlyList _queries; /// diff --git a/src/DemaConsulting.FileAssert/Modeling/FileAssertTest.cs b/src/DemaConsulting.FileAssert/Modeling/FileAssertTest.cs index 2f67fa2..ffa9b19 100644 --- a/src/DemaConsulting.FileAssert/Modeling/FileAssertTest.cs +++ b/src/DemaConsulting.FileAssert/Modeling/FileAssertTest.cs @@ -98,6 +98,7 @@ internal static FileAssertTest Create(FileAssertTestData data) /// true if is empty or contains the test name or any tag /// (case-insensitive); otherwise false. /// + /// Thrown when is null. internal bool MatchesFilter(IEnumerable filters) { // Validate required parameter diff --git a/src/DemaConsulting.FileAssert/Modeling/FileAssertTextAssert.cs b/src/DemaConsulting.FileAssert/Modeling/FileAssertTextAssert.cs index b0b5e3e..593f080 100644 --- a/src/DemaConsulting.FileAssert/Modeling/FileAssertTextAssert.cs +++ b/src/DemaConsulting.FileAssert/Modeling/FileAssertTextAssert.cs @@ -60,6 +60,9 @@ internal static FileAssertTextAssert Create(IEnumerable data /// /// The context used for reporting errors. /// The full path to the file to validate. + /// + /// Thrown when or is null. + /// internal void Run(Context context, string fileName) { ArgumentNullException.ThrowIfNull(context); diff --git a/src/DemaConsulting.FileAssert/Modeling/FileAssertXmlAssert.cs b/src/DemaConsulting.FileAssert/Modeling/FileAssertXmlAssert.cs index 4ff9d25..33760c7 100644 --- a/src/DemaConsulting.FileAssert/Modeling/FileAssertXmlAssert.cs +++ b/src/DemaConsulting.FileAssert/Modeling/FileAssertXmlAssert.cs @@ -34,8 +34,15 @@ internal sealed class FileAssertXmlAssert /// /// Represents a single XPath query assertion with count constraints. /// + /// XPath expression to evaluate against the document. + /// Expected exact node count; means no exact count constraint. + /// Minimum node count; means no lower bound. + /// Maximum node count; means no upper bound. private sealed record XmlQuery(string Query, int? Count, int? Min, int? Max); + /// + /// The ordered list of XPath query assertions to apply when this rule runs. + /// private readonly IReadOnlyList _queries; /// diff --git a/src/DemaConsulting.FileAssert/Modeling/FileAssertZipAssert.cs b/src/DemaConsulting.FileAssert/Modeling/FileAssertZipAssert.cs index ceda4f1..1dc5e10 100644 --- a/src/DemaConsulting.FileAssert/Modeling/FileAssertZipAssert.cs +++ b/src/DemaConsulting.FileAssert/Modeling/FileAssertZipAssert.cs @@ -125,6 +125,9 @@ internal static FileAssertZipAssert Create(FileAssertZipData data) /// /// The context used for reporting errors. Must not be null. /// The full path to the zip file to validate. Must not be null. + /// + /// Thrown when or is null. + /// internal void Run(Context context, string fileName) { ArgumentNullException.ThrowIfNull(context); diff --git a/src/DemaConsulting.FileAssert/Program.cs b/src/DemaConsulting.FileAssert/Program.cs index d38813a..8033a50 100644 --- a/src/DemaConsulting.FileAssert/Program.cs +++ b/src/DemaConsulting.FileAssert/Program.cs @@ -33,6 +33,14 @@ internal static class Program /// /// Gets the application version string. /// + /// + /// Reads the informational version from so + /// that CI-injected pre-release labels are preserved. Falls back to the assembly version, then + /// to the hard-coded sentinel "0.0.0" if neither attribute is present. + /// + /// + /// A non-null, non-empty version string; falls back through assembly version then "0.0.0". + /// public static string Version { get @@ -88,6 +96,12 @@ private static int Main(string[] args) /// /// Runs the program logic based on the provided context. /// + /// + /// Declared public so that unit tests and self-validation tests can invoke it + /// directly without spawning a child process. Dispatches in priority order: version query, + /// help, self-validation, then main tool logic. Each path writes its output through + /// and leaves the exit code in . + /// /// The context containing command line arguments and program state. public static void Run(Context context) { diff --git a/src/DemaConsulting.FileAssert/SelfTest/Validation.cs b/src/DemaConsulting.FileAssert/SelfTest/Validation.cs index c6313a8..94851e3 100644 --- a/src/DemaConsulting.FileAssert/SelfTest/Validation.cs +++ b/src/DemaConsulting.FileAssert/SelfTest/Validation.cs @@ -394,7 +394,6 @@ private static DemaConsulting.TestResults.TestResult CreateTestResult(string tes }; } - /// /// Source-generated regex for matching semantic version strings (N.N.N format). /// diff --git a/src/DemaConsulting.FileAssert/Utilities/PathHelpers.cs b/src/DemaConsulting.FileAssert/Utilities/PathHelpers.cs index bef9213..094fbcc 100644 --- a/src/DemaConsulting.FileAssert/Utilities/PathHelpers.cs +++ b/src/DemaConsulting.FileAssert/Utilities/PathHelpers.cs @@ -36,9 +36,21 @@ internal static class PathHelpers /// absolute form and compared against the resolved base directory to detect escape /// attempts using ../ sequences, absolute paths, or other traversal vectors. /// - /// The base path. - /// The relative path to combine. - /// The combined path. + /// + /// The root directory that the combined result must remain within. Must not be + /// ; must be a valid file-system path (relative or absolute). + /// + /// + /// The path to append to . Must not be . + /// Must not contain traversal segments (../) or be an absolute path, as these + /// would cause the resolved result to escape . + /// An empty string is accepted and causes the method to return unchanged. + /// + /// + /// The combined path string as returned by . + /// The resolved (absolute) form of this path is guaranteed to reside within the resolved + /// form of . + /// /// Thrown when or is . /// /// Thrown when the resolved combined path escapes the base directory, or when a supplied path is invalid. diff --git a/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs b/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs index c79655e..70e9288 100644 --- a/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs @@ -19,8 +19,8 @@ // SOFTWARE. using DemaConsulting.FileAssert.Cli; - using DemaConsulting.FileAssert.Utilities; + namespace DemaConsulting.FileAssert.Tests.Cli; /// @@ -107,7 +107,7 @@ public void Cli_CreateContext_WithFilters_ParsesPositionalArguments() [Fact] public void Cli_CreateContext_UnknownArgument_ThrowsArgumentException() { - // Arrange & Act & Assert + // Act / Assert - an unrecognized flag starting with '-' must throw Assert.Throws(() => Context.Create(["--unknown-flag"])); } @@ -185,6 +185,22 @@ public void Cli_OutputPipeline_WithLogPathAndSilentFlag_WritesMessagesToLogFile( var logContent = File.ReadAllText(logPath); Assert.Contains("informational message", logContent); Assert.Contains("error message", logContent); + } + /// + /// Verifies that the Cli subsystem exposes the --depth flag value as the typed Depth property. + /// + [Fact] + public void Cli_CreateContext_ParsesDepthFlag() + { + // Arrange: a non-default depth value to confirm the flag is parsed and exposed + const int expectedDepth = 3; + + // Act + using var context = Context.Create(["--silent", "--depth", "3"]); + + // Assert - the Depth property reflects the parsed flag value + Assert.Equal(expectedDepth, context.Depth); + Assert.Equal(0, context.ExitCode); } } diff --git a/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationTests.cs b/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationTests.cs index f3df2ef..bb00232 100644 --- a/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationTests.cs @@ -22,6 +22,7 @@ using DemaConsulting.FileAssert.Configuration; using DemaConsulting.FileAssert.Utilities; + namespace DemaConsulting.FileAssert.Tests.Configuration; /// @@ -66,7 +67,6 @@ public void Configuration_LoadYaml_BuildsCompleteTestHierarchy() Assert.Equal("**/*.txt", file.Pattern); Assert.Equal(1, file.Min); Assert.Single(file.TextAssert!.Rules); - } /// @@ -144,4 +144,34 @@ public void Configuration_RunWithTagFilter_ExecutesOnlyMatchingTests() Assert.Equal(0, context.ExitCode); } + + /// + /// Verifies that the Configuration subsystem writes a TRX results file + /// when a .trx results path is provided to the context. + /// + [Fact] + public void Configuration_Run_WithResultsFile_WritesTrxResultsFile() + { + // Arrange + using var tempDir = new TemporaryDirectory(); + var configPath = tempDir.GetFilePath("config.yaml"); + var resultsPath = tempDir.GetFilePath("results.trx"); + File.WriteAllText(configPath, """ + tests: + - name: "Exists Check" + files: + - pattern: "*.yaml" + min: 1 + """); + + var config = FileAssertConfig.ReadFromFile(configPath); + using var context = Context.Create(["--silent", "--results", resultsPath]); + + // Act + config.Run(context, []); + + // Assert - results file was written + Assert.True(File.Exists(resultsPath), + "TRX results file should be written when --results is provided."); + } } diff --git a/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs b/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs index 9009742..4d0c60c 100644 --- a/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs @@ -21,8 +21,8 @@ using DemaConsulting.FileAssert.Cli; using DemaConsulting.FileAssert.Configuration; using DemaConsulting.FileAssert.Modeling; - using DemaConsulting.FileAssert.Utilities; + namespace DemaConsulting.FileAssert.Tests.Configuration; /// diff --git a/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs b/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs index 42e7633..03281c2 100644 --- a/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs @@ -54,6 +54,8 @@ public IntegrationTests() [Fact] public void IntegrationTest_VersionFlag_OutputsVersion() { + // Arrange: no test-specific state required; _dllPath is resolved in the constructor + // Act var exitCode = Runner.Run( out var output, @@ -72,6 +74,8 @@ public void IntegrationTest_VersionFlag_OutputsVersion() [Fact] public void IntegrationTest_HelpFlag_OutputsUsageInformation() { + // Arrange: no test-specific state required; _dllPath is resolved in the constructor + // Act var exitCode = Runner.Run( out var output, @@ -92,6 +96,8 @@ public void IntegrationTest_HelpFlag_OutputsUsageInformation() [Fact] public void IntegrationTest_ValidateFlag_RunsValidation() { + // Arrange: no test-specific state required; _dllPath is resolved in the constructor + // Act var exitCode = Runner.Run( out var output, @@ -148,6 +154,8 @@ public void IntegrationTest_ValidateWithResults_GeneratesTrxFile() [Fact] public void IntegrationTest_SilentFlag_SuppressesOutput() { + // Arrange: no test-specific state required; _dllPath is resolved in the constructor + // Act var exitCode = Runner.Run( out var output, @@ -237,6 +245,8 @@ public void IntegrationTest_ValidateWithResults_GeneratesJUnitFile() [Fact] public void IntegrationTest_UnknownArgument_ReturnsError() { + // Arrange: no test-specific state required; _dllPath is resolved in the constructor + // Act var exitCode = Runner.Run( out var output, diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs index f11ce03..1e36689 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs @@ -23,6 +23,7 @@ using DemaConsulting.FileAssert.Modeling; using DemaConsulting.FileAssert.Utilities; + namespace DemaConsulting.FileAssert.Tests.Modeling; /// diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs index 3ac003a..e844e7d 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs @@ -23,6 +23,7 @@ using DemaConsulting.FileAssert.Modeling; using DemaConsulting.FileAssert.Utilities; + namespace DemaConsulting.FileAssert.Tests.Modeling; /// @@ -221,7 +222,6 @@ public void FileAssertTest_Run_RunsAllFiles() // Assert - min=1 would have produced an error if the file had not been found Assert.Equal(0, context.ExitCode); - } /// diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTextAssertTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTextAssertTests.cs index e6177d2..7e2aa1b 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTextAssertTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTextAssertTests.cs @@ -56,6 +56,8 @@ public void FileAssertTextAssert_Create_ValidData_CreatesTextAssert() [Fact] public void FileAssertTextAssert_Create_NullData_ThrowsArgumentNullException() { + // Arrange: no setup required - null is passed directly to the method under test + // Act & Assert Assert.Throws(() => FileAssertTextAssert.Create(null!)); } diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertXmlAssertTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertXmlAssertTests.cs index 837bd59..c3c0f3b 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertXmlAssertTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertXmlAssertTests.cs @@ -231,7 +231,61 @@ public void FileAssertXmlAssert_Run_XPathExactTextMatch_Matches_NoError() } /// - /// Verifies that Run reports an error when an XPath exact text query finds no matching nodes. + /// Verifies that Run reports an error when the XPath query result exceeds the maximum count. + /// + [Fact] + public void FileAssertXmlAssert_Run_MaxCount_Exceeded_WritesError() + { + // Arrange - sample XML has 3 items; assert max=1 so 3 > 1 is a violation + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, SampleXml); + var data = new List { new() { Query = "//item", Max = 1 } }; + var xmlAssert = FileAssertXmlAssert.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + xmlAssert.Run(context, tempFile); + + // Assert + Assert.Equal(1, context.ExitCode); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Verifies that Run reports an error when the XPath query result is below the minimum count. + /// + [Fact] + public void FileAssertXmlAssert_Run_MinCount_NotMet_WritesError() + { + // Arrange - sample XML has 3 items; assert min=10 so 3 < 10 is a violation + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, SampleXml); + var data = new List { new() { Query = "//item", Min = 10 } }; + var xmlAssert = FileAssertXmlAssert.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + xmlAssert.Run(context, tempFile); + + // Assert + Assert.Equal(1, context.ExitCode); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Verifies that Run produces no error when an XPath exact text query finds no matching nodes. /// [Fact] public void FileAssertXmlAssert_Run_XPathExactTextMatch_NoMatch_WritesError() diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertYamlAssertTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertYamlAssertTests.cs index f729f0a..a486ffb 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertYamlAssertTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertYamlAssertTests.cs @@ -307,4 +307,31 @@ public void FileAssertYamlAssert_Create_ConsecutiveDotsQuery_ThrowsInvalidOperat // Act & Assert Assert.Throws(() => FileAssertYamlAssert.Create(data)); } + + /// + /// Verifies that Run reports zero matches for all queries when the YAML file has no documents. + /// + [Fact] + public void FileAssertYamlAssert_Run_EmptyDocument_ReportsZeroCount() + { + // Arrange - write an empty file; YamlStream.Load produces no documents + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, string.Empty); + var data = new List { new() { Query = "tools", Min = 1 } }; + var yamlAssert = FileAssertYamlAssert.Create(data); + using var context = Context.Create(["--silent"]); + + // Act + yamlAssert.Run(context, tempFile); + + // Assert - no documents means 0 matches; min=1 constraint is violated + Assert.Equal(1, context.ExitCode); + } + finally + { + File.Delete(tempFile); + } + } } diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertZipAssertTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertZipAssertTests.cs index e70b89b..d17cffa 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertZipAssertTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertZipAssertTests.cs @@ -134,6 +134,7 @@ public void FileAssertZipAssert_Run_MatchingEntriesMeetConstraints_NoError() // Assert Assert.Equal(0, context.ExitCode); + Assert.Equal(0, context.ErrorCount); } finally { @@ -168,6 +169,7 @@ public void FileAssertZipAssert_Run_GlobPatternMatchesMultipleEntries_NoError() // Assert Assert.Equal(0, context.ExitCode); + Assert.Equal(0, context.ErrorCount); } finally { diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingTests.cs index 159e796..b33ad96 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingTests.cs @@ -23,6 +23,7 @@ using DemaConsulting.FileAssert.Modeling; using DemaConsulting.FileAssert.Utilities; + namespace DemaConsulting.FileAssert.Tests.Modeling; /// @@ -67,7 +68,6 @@ public void Modeling_ExecuteChain_PassesWhenAllConstraintsMet() // Assert - no errors reported Assert.Equal(0, context.ExitCode); - } /// diff --git a/test/DemaConsulting.FileAssert.Tests/ProgramTests.cs b/test/DemaConsulting.FileAssert.Tests/ProgramTests.cs index 66024a7..2a9906f 100644 --- a/test/DemaConsulting.FileAssert.Tests/ProgramTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/ProgramTests.cs @@ -50,6 +50,7 @@ public void Program_Run_WithVersionFlag_DisplaysVersionOnly() Assert.Contains(Program.Version, output); Assert.DoesNotContain("Copyright", output); Assert.DoesNotContain("FileAssert version", output); + Assert.Equal(0, context.ExitCode); } finally { @@ -80,6 +81,7 @@ public void Program_Run_WithHelpFlag_DisplaysUsageInformation() Assert.Contains("Options:", output); Assert.Contains("--version", output); Assert.Contains("--help", output); + Assert.Equal(0, context.ExitCode); } finally { @@ -107,6 +109,7 @@ public void Program_Run_WithValidateFlag_RunsValidation() // Assert var output = outWriter.ToString(); Assert.Contains("Total Tests:", output); + Assert.Equal(0, context.ExitCode); } finally { @@ -135,6 +138,7 @@ public void Program_Run_NoArguments_DisplaysDefaultBehavior() var output = outWriter.ToString(); Assert.Contains("FileAssert version", output); Assert.Contains("Copyright", output); + Assert.Equal(0, context.ExitCode); } finally { diff --git a/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestTests.cs b/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestTests.cs index bcd1fc7..404fc5e 100644 --- a/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestTests.cs @@ -22,6 +22,7 @@ using DemaConsulting.FileAssert.SelfTest; using DemaConsulting.FileAssert.Utilities; + namespace DemaConsulting.FileAssert.Tests.SelfTest; /// @@ -58,7 +59,6 @@ public void SelfTest_Run_ExecutesBuiltInTestsAndProducesSummary() Assert.Contains("Total Tests:", logContent); Assert.Contains("Passed:", logContent); Assert.Contains("Failed:", logContent); - } /// @@ -82,7 +82,6 @@ public void SelfTest_Run_WhenInvoked_PrintsSystemInfoHeader() Assert.Contains("Tool Version", logContent); Assert.Contains("Machine Name", logContent); Assert.Contains("OS Version", logContent); - } /// diff --git a/test/DemaConsulting.FileAssert.Tests/Utilities/PathHelpersTests.cs b/test/DemaConsulting.FileAssert.Tests/Utilities/PathHelpersTests.cs index b3a7969..60eea7e 100644 --- a/test/DemaConsulting.FileAssert.Tests/Utilities/PathHelpersTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Utilities/PathHelpersTests.cs @@ -25,7 +25,6 @@ namespace DemaConsulting.FileAssert.Tests.Utilities; /// /// Tests for the PathHelpers class. /// -[Collection("Sequential")] public class PathHelpersTests { /// @@ -158,9 +157,11 @@ public void PathHelpers_SafePathCombine_EmptyRelativePath_ReturnsBasePath() [Fact] public void PathHelpers_SafePathCombine_DoubleDotInFilename_CombinesCorrectly() { - // Arrange - filename with ".." as substring, not a path traversal component + // Arrange - filename starting with ".." but it is a directory name, not a traversal component. + // This exercises the design requirement that "..data" (dot-dot followed by a non-separator + // character) must not be misidentified as an escaping "../" sequence. var basePath = "/home/user/project"; - var relativePath = "my..file.txt"; + var relativePath = "..data/file.txt"; // Act var result = PathHelpers.SafePathCombine(basePath, relativePath); diff --git a/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs b/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs index cc84dd8..a1613be 100644 --- a/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesTests.cs @@ -46,4 +46,29 @@ public void Utilities_SafePathCombine_PreventsPathTraversalToFileSystem() var relativePath = Path.GetRelativePath(tempDir.DirectoryPath, combined); Assert.Equal(Path.Combine("nested", "file.txt"), relativePath); } + + /// + /// Verifies that the Utilities subsystem's temporary directory provides an isolated + /// scratch space that is created on construction, accessible during the lifetime, + /// and fully removed on disposal. + /// + [Fact] + public void Utilities_TemporaryDirectory_IsolatesAndCleansUpScratchSpace() + { + // Arrange & Act: create a temp directory, write a file inside it, then dispose + string filePath; + using (var tempDir = new TemporaryDirectory()) + { + filePath = tempDir.GetFilePath("scratch.txt"); + File.WriteAllText(filePath, "scratch content"); + + // Assert: file is accessible within the temporary directory lifetime + Assert.True(File.Exists(filePath), + "Scratch file should be accessible within the temporary directory lifetime."); + } + + // Assert: directory and its contents are removed after disposal + Assert.False(File.Exists(filePath), + "Scratch file should be removed after the temporary directory is disposed."); + } } From b8a9d77a18e95a0d79a42ce429e95a03ca9a6d2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 12:08:02 +0000 Subject: [PATCH 6/8] docs: address remaining PR review comments in design docs Agent-Logs-Url: https://github.com/demaconsulting/FileAssert/sessions/9ae953bd-f996-404a-8e40-c99c0f5b8b6e Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --- docs/design/file-assert/configuration.md | 2 +- docs/design/file-assert/modeling.md | 2 +- .../file-assert/modeling/file-assert-file.md | 2 +- .../modeling/file-assert-html-assert.md | 26 ++++++++++--------- .../modeling/file-assert-yaml-assert.md | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/design/file-assert/configuration.md b/docs/design/file-assert/configuration.md index a7ba79c..2247392 100644 --- a/docs/design/file-assert/configuration.md +++ b/docs/design/file-assert/configuration.md @@ -1,4 +1,4 @@ -## Configuration Subsystem Design +## Configuration Subsystem Design ### Overview diff --git a/docs/design/file-assert/modeling.md b/docs/design/file-assert/modeling.md index a5041ab..656d74c 100644 --- a/docs/design/file-assert/modeling.md +++ b/docs/design/file-assert/modeling.md @@ -1,4 +1,4 @@ -## Modeling Subsystem Design +## Modeling Subsystem Design ### Overview diff --git a/docs/design/file-assert/modeling/file-assert-file.md b/docs/design/file-assert/modeling/file-assert-file.md index 6d441a6..4a48776 100644 --- a/docs/design/file-assert/modeling/file-assert-file.md +++ b/docs/design/file-assert/modeling/file-assert-file.md @@ -1,4 +1,4 @@ -### FileAssertFile Design +### FileAssertFile Design #### Overview diff --git a/docs/design/file-assert/modeling/file-assert-html-assert.md b/docs/design/file-assert/modeling/file-assert-html-assert.md index feccfa1..47624ae 100644 --- a/docs/design/file-assert/modeling/file-assert-html-assert.md +++ b/docs/design/file-assert/modeling/file-assert-html-assert.md @@ -3,9 +3,10 @@ #### Overview The `FileAssertHtmlAssert` class attempts to parse a matched file as an HTML document using -`HtmlAgilityPack`. If parsing produces critical errors, an error is reported and no further -assertions are evaluated. Otherwise it evaluates each XPath query against the document and -applies min, max, and exact count constraints to the number of matching nodes. +`HtmlAgilityPack`. If loading the file fails due to I/O or permission errors, an error is +reported and no further assertions are evaluated. Otherwise it evaluates each XPath query +against the document and applies min, max, and exact count constraints to the number of +matching nodes. #### Class Structure @@ -19,7 +20,7 @@ The main class coordinating XPath-based node count assertions for an HTML file. | :-------- | | `Queries` | -Each `FileAssertHtmlQuery` entry holds: +Each `HtmlQuery` entry holds: | Property | | :------- | @@ -87,7 +88,7 @@ files: inaccessible file; `IOException` and `UnauthorizedAccessException` from `Load` are caught and reported immediately so users receive a clear, actionable error message rather than silent XPath results against an empty document. -- **Independent query model**: `FileAssertHtmlQuery` is private to this unit so that HTML +- **Independent query model**: `HtmlQuery` is a private nested record in this unit so that HTML assertion behavior can evolve independently of the other structured-document assert units. #### Purpose @@ -102,7 +103,7 @@ constraints per query. | :--------------- | | `Queries` | -Each `FileAssertHtmlQuery` (private nested record) holds: +Each `HtmlQuery` (private nested record) holds: | Property | | :------- | @@ -120,12 +121,13 @@ Each `FileAssertHtmlQuery` (private nested record) holds: #### Error Handling -| Scenario | -| :-------------------------------------------- | -| HtmlAgilityPack reports critical parse errors | -| Query result below `Min` | -| Query result above `Max` | -| Query result not equal to `Count` | +| Scenario | +| :----------------------------------------------------------------------- | +| `IOException` or `UnauthorizedAccessException` while loading the file | +| Query XPath expression is invalid (`XPathException`) | +| Query result below `Min` | +| Query result above `Max` | +| Query result not equal to `Count` | #### Interactions diff --git a/docs/design/file-assert/modeling/file-assert-yaml-assert.md b/docs/design/file-assert/modeling/file-assert-yaml-assert.md index c9fcdc1..7793c73 100644 --- a/docs/design/file-assert/modeling/file-assert-yaml-assert.md +++ b/docs/design/file-assert/modeling/file-assert-yaml-assert.md @@ -117,7 +117,7 @@ Each `YamlQuery` (private nested record) holds: | Method | Purpose | | :---------------------------------------------- | :--------------------------------------------------------------- | -| `Create(IEnumerable data)` | Converts query DTOs to `FileAssertYamlQuery` instances. | +| `Create(IEnumerable data)` | Converts query DTOs to `YamlQuery` instances. | | `Run(Context context, string fileName)` | Parses the YAML file and evaluates each dot-notation path query. | #### Error Handling From 8abad3171d71df93d82214612c4802099d7d4b9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 14:36:55 +0000 Subject: [PATCH 7/8] fix review comments from latest PR thread Agent-Logs-Url: https://github.com/demaconsulting/FileAssert/sessions/3a7529d9-6fbb-4bd5-a76f-1cb9562e623f Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --- docs/verification/file-assert/selftest.md | 2 +- .../Modeling/FileAssertXmlAssertTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/verification/file-assert/selftest.md b/docs/verification/file-assert/selftest.md index 9bf5648..67bbe1d 100644 --- a/docs/verification/file-assert/selftest.md +++ b/docs/verification/file-assert/selftest.md @@ -24,7 +24,7 @@ environment configuration is required beyond the standard build toolchain. ### Acceptance Criteria -The Selftest subsystem verification passes when all test scenarios listed in +The SelfTest subsystem verification passes when all test scenarios listed in this document execute and pass in the CI pipeline without any test failures, unexpected exceptions, or assertion errors. Each named scenario must pass on all supported runtime and platform combinations. diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertXmlAssertTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertXmlAssertTests.cs index c3c0f3b..07a56d0 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertXmlAssertTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertXmlAssertTests.cs @@ -285,7 +285,7 @@ public void FileAssertXmlAssert_Run_MinCount_NotMet_WritesError() } /// - /// Verifies that Run produces no error when an XPath exact text query finds no matching nodes. + /// Verifies that Run reports an error when an XPath exact text query finds no matching nodes. /// [Fact] public void FileAssertXmlAssert_Run_XPathExactTextMatch_NoMatch_WritesError() From 53a82422f67da86c8dc9197c3a2943c9badbef03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 14:41:29 +0000 Subject: [PATCH 8/8] chore: complete lint-fix wording cleanup Agent-Logs-Url: https://github.com/demaconsulting/FileAssert/sessions/3a7529d9-6fbb-4bd5-a76f-1cb9562e623f Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --- docs/design/file-assert/program.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/design/file-assert/program.md b/docs/design/file-assert/program.md index 4ca401d..3e1e6ec 100644 --- a/docs/design/file-assert/program.md +++ b/docs/design/file-assert/program.md @@ -27,7 +27,7 @@ attribute is absent, then to `"0.0.0"` if neither is available. **Preconditions**: None. -**Postconditions**: Returns a non-null, non-empty string representing the application version. +**Post-conditions**: Returns a non-null, non-empty string representing the application version. #### Main Method @@ -46,7 +46,7 @@ error and re-thrown so that the runtime generates a crash-report event-log entry **Preconditions**: `args` is the raw command-line argument array supplied by the runtime (may be empty; must not be null). -**Postconditions**: Returns `0` for success or a non-zero value for failure. +**Post-conditions**: Returns `0` for success or a non-zero value for failure. #### Run Method @@ -69,7 +69,7 @@ spawning a child process. **Preconditions**: `context` must not be null. -**Postconditions**: Appropriate output has been written to `context`; `context.ExitCode` reflects +**Post-conditions**: Appropriate output has been written to `context`; `context.ExitCode` reflects the outcome of the dispatched path. #### PrintBanner Method @@ -85,7 +85,7 @@ separator line. **Preconditions**: `context` must not be null. -**Postconditions**: Banner lines have been written to `context`. +**Post-conditions**: Banner lines have been written to `context`. #### PrintHelp Method @@ -99,7 +99,7 @@ private static void PrintHelp(Context context) **Preconditions**: `context` must not be null. -**Postconditions**: Help text has been written to `context`. +**Post-conditions**: Help text has been written to `context`. #### RunToolLogic Method @@ -118,7 +118,7 @@ matching tests are executed. **Preconditions**: `context` must not be null. -**Postconditions**: Either an error has been reported via `context.WriteError`, guidance has been +**Post-conditions**: Either an error has been reported via `context.WriteError`, guidance has been written for a missing default config, or all matching assertions have been executed and their outcomes reflected in `context.ExitCode`.