diff --git a/.cspell.yaml b/.cspell.yaml index 8c5a6ee..fc92247 100644 --- a/.cspell.yaml +++ b/.cspell.yaml @@ -18,6 +18,7 @@ words: - DEMA - Dema - fileassert + - hotspot - hotspots - mstest - pandoc @@ -29,13 +30,17 @@ words: - sarifmark - sonarmark - testname + - testresults - tracematrix + - xunit + - Xunit - Unwritable - venv - versionmark - Weasy - weasyprint - yamlfix + - yamldotnet # Exclude common build artifacts, dependencies, and vendored third-party code ignorePaths: @@ -45,6 +50,7 @@ ignorePaths: - "**/thirdparty/**" - "**/third-party/**" - "**/3rd-party/**" + - "**/generated/**" - "**/AGENT_REPORT_*.md" - "**/.agent-logs/**" - "**/bin/**" diff --git a/.fileassert.yaml b/.fileassert.yaml index 503470d..33e78f0 100644 --- a/.fileassert.yaml +++ b/.fileassert.yaml @@ -1,7 +1,7 @@ --- # FileAssert document validation tests for ReqStream. # Tests are tagged by document group to allow per-group execution during the build pipeline. -# Tags: build-notes, code-quality, code-review, design, user-guide, requirements. +# Tags: build-notes, code-quality, code-review, design, verification, user-guide, requirements. # # NOTE: build-notes through user-guide tests provide OTS evidence for Pandoc and WeasyPrint # and run before ReqStream. The requirements tests run after ReqStream and validate the @@ -15,7 +15,7 @@ tests: description: "Build Notes HTML was generated by Pandoc" tags: [build-notes] files: - - pattern: "docs/build_notes/build_notes.html" + - pattern: "docs/build_notes/generated/build_notes.html" count: 1 html: - query: "//head/title" @@ -27,7 +27,7 @@ tests: description: "Build Notes PDF was generated by WeasyPrint" tags: [build-notes] files: - - pattern: "docs/ReqStream Build Notes.pdf" + - pattern: "docs/generated/ReqStream Build Notes.pdf" count: 1 pdf: metadata: @@ -48,7 +48,7 @@ tests: description: "Code Quality HTML was generated by Pandoc" tags: [code-quality] files: - - pattern: "docs/code_quality/quality.html" + - pattern: "docs/code_quality/generated/quality.html" count: 1 html: - query: "//head/title" @@ -60,7 +60,7 @@ tests: description: "Code Quality PDF was generated by WeasyPrint" tags: [code-quality] files: - - pattern: "docs/ReqStream Code Quality.pdf" + - pattern: "docs/generated/ReqStream Code Quality.pdf" count: 1 pdf: metadata: @@ -81,7 +81,7 @@ tests: description: "Code Review Plan HTML was generated by Pandoc" tags: [code-review] files: - - pattern: "docs/code_review_plan/plan.html" + - pattern: "docs/code_review_plan/generated/plan.html" count: 1 html: - query: "//head/title" @@ -93,7 +93,7 @@ tests: description: "Code Review Plan PDF was generated by WeasyPrint" tags: [code-review] files: - - pattern: "docs/ReqStream Review Plan.pdf" + - pattern: "docs/generated/ReqStream Review Plan.pdf" count: 1 pdf: metadata: @@ -114,7 +114,7 @@ tests: description: "Code Review Report HTML was generated by Pandoc" tags: [code-review] files: - - pattern: "docs/code_review_report/report.html" + - pattern: "docs/code_review_report/generated/report.html" count: 1 html: - query: "//head/title" @@ -126,7 +126,7 @@ tests: description: "Code Review Report PDF was generated by WeasyPrint" tags: [code-review] files: - - pattern: "docs/ReqStream Review Report.pdf" + - pattern: "docs/generated/ReqStream Review Report.pdf" count: 1 pdf: metadata: @@ -147,7 +147,7 @@ tests: description: "Design HTML was generated by Pandoc" tags: [design] files: - - pattern: "docs/design/design.html" + - pattern: "docs/design/generated/design.html" count: 1 html: - query: "//head/title" @@ -159,7 +159,7 @@ tests: description: "Design PDF was generated by WeasyPrint" tags: [design] files: - - pattern: "docs/ReqStream Software Design.pdf" + - pattern: "docs/generated/ReqStream Software Design.pdf" count: 1 pdf: metadata: @@ -174,13 +174,46 @@ tests: text: - contains: "Design" + # --- VERIFICATION --- + + - name: Pandoc_VerificationHtml + description: "Verification HTML was generated by Pandoc" + tags: [verification] + files: + - pattern: "docs/verification/generated/verification.html" + count: 1 + html: + - query: "//head/title" + count: 1 + text: + - contains: "Verification" + + - name: WeasyPrint_VerificationPdf + description: "Verification PDF was generated by WeasyPrint" + tags: [verification] + files: + - pattern: "docs/generated/ReqStream Software Verification Design.pdf" + count: 1 + pdf: + metadata: + - field: "Title" + contains: "Verification" + - field: "Author" + contains: "DEMA Consulting" + - field: "Subject" + contains: "Verification design document" + pages: + min: 3 + text: + - contains: "Verification" + # --- USER GUIDE --- - name: Pandoc_UserGuideHtml description: "User Guide HTML was generated by Pandoc" tags: [user-guide] files: - - pattern: "docs/user_guide/user_guide.html" + - pattern: "docs/user_guide/generated/user_guide.html" count: 1 html: - query: "//head/title" @@ -192,7 +225,7 @@ tests: description: "User Guide PDF was generated by WeasyPrint" tags: [user-guide] files: - - pattern: "docs/ReqStream User Guide.pdf" + - pattern: "docs/generated/ReqStream User Guide.pdf" count: 1 pdf: metadata: @@ -214,7 +247,7 @@ tests: description: "Requirements HTML was generated by Pandoc" tags: [requirements] files: - - pattern: "docs/requirements_doc/requirements.html" + - pattern: "docs/requirements_doc/generated/requirements.html" count: 1 html: - query: "//head/title" @@ -226,7 +259,7 @@ tests: description: "Requirements PDF was generated by WeasyPrint" tags: [requirements] files: - - pattern: "docs/ReqStream Requirements.pdf" + - pattern: "docs/generated/ReqStream Requirements.pdf" count: 1 pdf: metadata: @@ -248,7 +281,7 @@ tests: description: "Trace Matrix HTML was generated by Pandoc" tags: [requirements] files: - - pattern: "docs/requirements_report/trace_matrix.html" + - pattern: "docs/requirements_report/generated/trace_matrix.html" count: 1 html: - query: "//head/title" @@ -260,7 +293,7 @@ tests: description: "Trace Matrix PDF was generated by WeasyPrint" tags: [requirements] files: - - pattern: "docs/ReqStream Trace Matrix.pdf" + - pattern: "docs/generated/ReqStream Trace Matrix.pdf" count: 1 pdf: metadata: diff --git a/.github/agents/developer.agent.md b/.github/agents/developer.agent.md index 35f5dda..a95c562 100644 --- a/.github/agents/developer.agent.md +++ b/.github/agents/developer.agent.md @@ -21,7 +21,7 @@ Perform software development tasks by determining and applying appropriate stand 5. **Formatting**: Run `pwsh ./fix.ps1` to silently apply all available auto-fixers (dotnet format, markdown, YAML) before committing 6. **Build and test** (code changes only): Run `pwsh ./build.ps1` and confirm it - passes — report FAILED if the build or any tests fail + passes - report FAILED if the build or any tests fail 7. **Generate completion report** per the AGENTS.md reporting requirements - save to `.agent-logs/{agent-name}-{subject}-{unique-id}.md` and return the summary to the caller diff --git a/.github/agents/formal-review.agent.md b/.github/agents/formal-review.agent.md index 88b0691..7dd8e84 100644 --- a/.github/agents/formal-review.agent.md +++ b/.github/agents/formal-review.agent.md @@ -20,6 +20,8 @@ Before reviewing, read these standards to inform review judgments: hierarchy and categorization review judgments - **`design-documentation.md`** - defines mandatory sections, structural conventions, and coverage expected at each level; informs all design documentation review judgments +- **`verification-documentation.md`** - defines mandatory sections, structural conventions, + and coverage expected at each level; informs all verification design review judgments For review sets that include source code or tests, also consult the relevant standards from the selection matrix in AGENTS.md. diff --git a/.github/agents/lint-fix.agent.md b/.github/agents/lint-fix.agent.md index 83ad8cb..549e751 100644 --- a/.github/agents/lint-fix.agent.md +++ b/.github/agents/lint-fix.agent.md @@ -36,7 +36,12 @@ submission, not during normal development. - **markdownlint MD013 (line length)**: Wrap long lines at natural break points, after commas, before conjunctions, or at sentence boundaries. Do not break - in the middle of a code span or URL. + in the middle of a code span or URL. **Pipe-tables that cannot be wrapped + without breaking structure** are a special case - convert them to a bullet + list if the data reads naturally that way, or rewrite as a + [grid table](https://pandoc.org/MANUAL.html#tables) if a tabular layout is + essential. Do not get stuck trying to squeeze a wide pipe-table into 120 + characters. - **markdownlint other rules**: Apply the specific fix indicated in the output (e.g., missing blank lines, heading levels, code fence languages). diff --git a/.github/agents/repo-consistency.agent.md b/.github/agents/repo-consistency.agent.md deleted file mode 100644 index 5dbe99f..0000000 --- a/.github/agents/repo-consistency.agent.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -name: repo-consistency -description: > - Ensures downstream repositories remain consistent with the TemplateDotNetTool - template patterns and best practices. -user-invocable: true ---- - -# Repo Consistency Agent - -Maintain consistency between downstream projects and the TemplateDotNetTool template, ensuring repositories -benefit from template evolution while respecting project-specific customizations. - -# Consistency Workflow (MANDATORY) - -**CRITICAL**: This agent MUST follow these steps systematically to ensure proper template consistency analysis: - -1. **Fetch Recent Template Changes**: Use GitHub search to fetch the 20 most recently merged PRs - (`is:pr is:merged sort:updated-desc`) from -2. **Analyze Template Evolution**: For each relevant PR, determine the intent and scope of changes - (what files were modified, what improvements were made) -3. **Assess Downstream Applicability**: Evaluate which template changes would benefit this repository - while respecting project-specific customizations -4. **Apply Appropriate Updates**: Implement applicable template improvements with proper translation for project context -5. **Validate Consistency**: Verify that applied changes maintain functionality and follow project patterns -6. **Generate completion report** per the AGENTS.md reporting requirements - save to - `.agent-logs/{agent-name}-{subject}-{unique-id}.md` and return the summary to the caller - -## Key Principles - -- **Evolutionary Consistency**: Template improvements should enhance downstream projects systematically -- **Intelligent Customization Respect**: Distinguish valid customizations from unintentional drift -- **Incremental Template Adoption**: Support phased adoption of template improvements based on project capacity - -# Don't Do These Things - -- **Never recommend changes without understanding project context** (some differences are intentional) -- **Never flag valid project-specific customizations** as consistency problems -- **Never apply template changes blindly** without assessing downstream project impact -- **Never ignore template evolution benefits** when they clearly improve downstream projects -- **Never recommend breaking changes** without migration guidance and impact assessment -- **Never skip validation** of preserved functionality after template alignment -- **Never assume all template patterns apply universally** (assess project-specific needs) - -# Report Template - -```markdown -# Repo Consistency Report - -**Result**: (SUCCEEDED|FAILED) - -## Consistency Analysis - -- **Template PRs Analyzed**: {Number and timeframe of PRs reviewed} -- **Template Changes Identified**: {Count and types of template improvements} -- **Applicable Updates**: {Changes determined suitable for this repository} -- **Project Customizations Preserved**: {Valid differences maintained} - -## Template Evolution Applied - -- **Files Modified**: {List of files updated for template consistency} -- **Improvements Adopted**: {Specific template enhancements implemented} -- **Configuration Updates**: {Tool configurations, workflows, or standards updated} - -## Consistency Status - -- **Template Alignment**: {Overall consistency rating with template} -- **Customization Respect**: {How project-specific needs were preserved} -- **Functionality Validation**: {Verification that changes don't break existing features} -- **Future Consistency**: {Recommendations for ongoing template alignment} - -## Issues Resolved - -- **Drift Corrections**: {Template drift issues addressed} -- **Enhancement Adoptions**: {Template improvements successfully integrated} -- **Validation Results**: {Testing and validation outcomes} -``` diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d587c18..82a413e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,16 +26,11 @@ Before submitting this pull request, ensure you have completed the following: ### Build and Test -- [ ] Code builds successfully: `dotnet build --configuration Release` -- [ ] All unit tests pass: `dotnet test --configuration Release` -- [ ] Self-validation tests pass: - `dotnet run --project src/DemaConsulting.ReqStream --configuration Release --framework net10.0` - `--no-build -- --validate` +- [ ] Code builds successfully and all tests pass: `pwsh ./build.ps1` - [ ] Code produces zero warnings ### Code Quality -- [ ] Code formatting is correct: `dotnet format --verify-no-changes` - [ ] New code has appropriate XML documentation comments - [ ] Static analyzer warnings have been addressed @@ -43,9 +38,7 @@ Before submitting this pull request, ensure you have completed the following: Please run the following checks before submitting: -- [ ] **Spell checker passes**: `cspell "**/*.{md,cs}"` -- [ ] **Markdown linter passes**: `markdownlint "**/*.md"` -- [ ] **YAML linter passes**: `yamllint .` +- [ ] **All linters pass**: `pwsh ./lint.ps1` ### Testing @@ -57,7 +50,7 @@ Please run the following checks before submitting: ### Documentation - [ ] Updated README.md (if applicable) -- [ ] Updated ARCHITECTURE.md (if applicable) +- [ ] Updated docs/ documentation (if applicable) - [ ] Added code examples for new features (if applicable) - [ ] Updated requirements.yaml (if applicable) diff --git a/.github/standards/coding-principles.md b/.github/standards/coding-principles.md index 213c031..9e67fbb 100644 --- a/.github/standards/coding-principles.md +++ b/.github/standards/coding-principles.md @@ -20,11 +20,35 @@ All code MUST follow literate programming principles: matches design intent without reading the full codebase - **Logical Separation**: Complex functions use block comments to separate and describe logical steps within the implementation -- **Public Documentation**: All public interfaces have comprehensive documentation - because consumers and auditors rely on interface contracts for integration - and compliance verification +- **Full Symbol Documentation**: ALL symbols have comprehensive documentation + because reviewers and auditors must verify every implementation detail, not + just the public interface - access-level specifics (public, protected, + private, internal, etc.) vary by language; see the language-specific standard - **Clarity Over Cleverness**: Code should be immediately understandable by team members +## API Documentation + +Good API documentation enables consumers, reviewers, and agents to use an +interface correctly without reading the implementation: + +- **Self-Contained**: Each member's documentation must be fully understandable + in isolation - consumers must not need to read the implementation to call it + correctly +- **Intent-Focused**: Explain WHY the member exists and WHAT problem it solves, + not just restate the name - this lets reviewers verify the implementation + matches design intent +- **Parameter and Return Contracts**: Document valid ranges, null handling, and + boundary cases - agents and consumers rely on these contracts to call the API + correctly +- **Error Conditions**: Document every exception or error code, the condition + that triggers it, and how the caller should respond - undocumented errors + cannot be handled correctly +- **Side Effects**: Document I/O, state mutation, resource allocation, or + network calls - hidden side effects cause integration bugs that are hard to + diagnose +- **Thread Safety**: State whether the API is safe for concurrent use - missing + this forces consumers to read the implementation or risk data races + ## Universal Code Architecture Principles ### Design Patterns diff --git a/.github/standards/csharp-language.md b/.github/standards/csharp-language.md index 707b0f9..6df39cd 100644 --- a/.github/standards/csharp-language.md +++ b/.github/standards/csharp-language.md @@ -4,37 +4,63 @@ description: Follow these standards when developing C# source code. globs: ["**/*.cs"] --- -# C# Language Development Standard - -## Required Standards +# Required Standards Read these standards first before applying this standard: - **`coding-principles.md`** - Universal coding principles and quality gates -# File Patterns - -- **Source Files**: `**/*.cs` +# API Documentation and Literate Coding Example -# Literate Coding Example +The example below demonstrates good XmlDoc API documentation combined with +literate coding comments. ```csharp -// Validate input parameters to prevent downstream errors -if (string.IsNullOrEmpty(input)) +/// +/// Converts a raw sensor reading into a validated measurement ready for downstream consumers. +/// +/// +/// Clamping is preferred over throwing on out-of-range values because sensor drift at +/// range boundaries is expected; clamping produces a usable result where rejection would +/// discard valid near-boundary readings. Stateless and thread-safe; the calibration +/// profile is read but never modified. +/// +/// Raw sensor value. Must be finite (NaN and infinities are rejected). +/// Calibration profile providing offset and range. Must not be null. +/// Corrected value clamped to [calibration.Minimum, calibration.Maximum]. +/// Thrown when is NaN or infinite. +/// Thrown when is null. +public double ProcessReading(double reading, CalibrationProfile calibration) { - throw new ArgumentException("Input cannot be null or empty", nameof(input)); -} - -// Transform input data using the configured processing pipeline -var processedData = ProcessingPipeline.Transform(input); + // Reject invalid inputs before any calculation - non-finite readings cannot be + // corrected, and a null calibration profile provides no offset or range to apply + if (!double.IsFinite(reading)) + throw new ArgumentException("Reading must be a finite number.", nameof(reading)); + ArgumentNullException.ThrowIfNull(calibration); -// Apply business rules and validation logic -var validatedResults = BusinessRuleEngine.ValidateAndProcess(processedData); + // Apply the calibration offset to convert raw counts to physical units + var corrected = reading + calibration.Offset; -// Return formatted results matching the expected output contract -return OutputFormatter.Format(validatedResults); + // Clamp to the operational range so consumers can rely on the documented contract + return Math.Clamp(corrected, calibration.Minimum, calibration.Maximum); +} ``` +Key qualities demonstrated above: + +- **``** is a brief one-liner explaining *what* the method does +- **``** sits directly after summary and carries the extended intent - + *why* it exists, design decisions, thread-safety, and side-effect disclosures +- **`` tags** state constraints (finite, non-null) so callers know what + is valid without reading the body +- **``** documents the boundary guarantee so consumers can rely on the + contract +- **`` tags** name every thrown exception and the condition that + triggers each one +- **Inline block comments** follow the Literate Coding principles from + `coding-principles.md`, separating logical steps so reviewers can verify each + step against design intent + # Code Formatting - **Format entire solution**: `dotnet format` diff --git a/.github/standards/csharp-testing.md b/.github/standards/csharp-testing.md index 1591eeb..181de02 100644 --- a/.github/standards/csharp-testing.md +++ b/.github/standards/csharp-testing.md @@ -4,115 +4,74 @@ description: Follow these standards when developing C# tests. globs: ["**/test/**/*.cs", "**/tests/**/*.cs", "**/*Tests.cs", "**/*Test.cs"] --- -# C# Testing Standards (MSTest) - -This document defines standards for C# test development using -MSTest within Continuous Compliance environments. - -## Required Standards +# Required Standards Read these standards first before applying this standard: - **`testing-principles.md`** - Universal testing principles and dependency boundaries - **`csharp-language.md`** - C# language development standards -# C# AAA Pattern Implementation +# Package Reference -```csharp -[TestMethod] -public void ServiceName_MethodName_Scenario_ExpectedBehavior() -{ - // Arrange: description of setup (omit if nothing to set up) +Every xUnit v3 test project requires the following package references for +`dotnet test` to discover and execute tests: - // Act: description of action (can combine with Assert when action occurs within assertion) +| Package | Purpose | +| ------- | ------- | +| `xunit.v3` | xUnit v3 framework (monolithic - includes assertions and fixtures) | +| `Microsoft.NET.Test.Sdk` | Required by the VSTest/`dotnet test` host for test discovery | +| `xunit.runner.visualstudio` | VSTest adapter that bridges xUnit v3 to `dotnet test` | - // Assert: description of verification -} -``` +Omitting `Microsoft.NET.Test.Sdk` or `xunit.runner.visualstudio` causes tests +to be silently undiscoverable by `dotnet test`. + +If tests require mocking of dependencies, add `NSubstitute` as a package +reference - it is recommended when mocking is needed but is not required for +every test project. -# Test Naming Standards +# Test Style -Use descriptive test names because test names appear in requirements traceability matrices and compliance reports. +Test names appear in requirements traceability matrices - use the hierarchical +naming pattern, and follow AAA with labeled comments: - **System tests**: `{SystemName}_{Functionality}_{Scenario}_{ExpectedBehavior}` - **Subsystem tests**: `{SubsystemName}_{Functionality}_{Scenario}_{ExpectedBehavior}` - **Unit tests**: `{ClassName}_{MethodUnderTest}_{Scenario}_{ExpectedBehavior}` -- **Descriptive Scenarios**: Clearly describe the input condition being tested -- **Expected Behavior**: State the expected outcome or exception - -## Examples - -- `UserValidator_ValidateEmail_ValidFormat_ReturnsTrue` -- `UserValidator_ValidateEmail_InvalidFormat_ThrowsArgumentException` -- `PaymentProcessor_ProcessPayment_InsufficientFunds_ReturnsFailureResult` - -# Mock Dependencies - -Mock external dependencies using NSubstitute (preferred) because tests must run in isolation to generate -reliable evidence. - -- **Isolate System Under Test**: Mock all external dependencies (databases, web services, file systems) -- **Verify Interactions**: Assert that expected method calls occurred with correct parameters -- **Predictable Behavior**: Set up mocks to return known values for consistent test results - -# MSTest V4 Anti-patterns - -Avoid these common MSTest V4 patterns because they produce poor error messages or cause tests to be silently ignored. - -# Avoid Assertions in Catch Blocks (MSTEST0058) - -Instead of wrapping code in try/catch and asserting in the catch block, use `Assert.ThrowsExactly()`: - -```csharp -var ex = Assert.ThrowsExactly(() => SomeWork()); -Assert.Contains("Some message", ex.Message); -``` - -# Avoid Assert.IsTrue/IsFalse for Equality Checks - -Use `Assert.AreEqual`/`Assert.AreNotEqual` instead, as they provide better failure messages: - -```csharp -// ❌ Bad: Assert.IsTrue(result == expected); -// ✅ Good: Assert.AreEqual(expected, result); -``` - -# Avoid Non-Public Test Classes and Methods - -Test classes and `[TestMethod]` methods must be `public` or they will be silently ignored: ```csharp -// ❌ Bad: internal class MyTests -// ✅ Good: public class MyTests -``` - -# Avoid Assert.IsTrue for Collection Count - -Use `Assert.HasCount` for count assertions: +/// +/// Validates that an invalid email format throws an ArgumentException. +/// +[Fact] +public void UserValidator_ValidateEmail_InvalidFormat_ThrowsArgumentException() +{ + // Arrange: create a validator with default configuration + var validator = new UserValidator(); -```csharp -// ❌ Bad: Assert.IsTrue(collection.Count == 3); -// ✅ Good: Assert.HasCount(3, collection); + // Act / Assert: email with no domain throws + Assert.Throws(() => validator.ValidateEmail("not-an-email")); +} ``` -# Avoid Assert.IsTrue for String Prefix Checks +# xUnit v3 Specifics -Use `Assert.StartsWith` instead, as it produces clearer failure messages: +These are non-obvious v3 behaviors that differ from v2 or common assumptions: -```csharp -// ❌ Bad: Assert.IsTrue(value.StartsWith("prefix")); -// ✅ Good: Assert.StartsWith("prefix", value); -``` +- **`IAsyncLifetime`**: Both `InitializeAsync` and `DisposeAsync` return `ValueTask` + in v3, not `Task` - using `Task` compiles but does not satisfy the v3 interface +- **`Assert.Multiple`**: Use to collect all assertion failures in a single test + rather than stopping at the first +- **`[Collection]` without `[CollectionDefinition]`**: Silently disables parallelism + without providing any shared fixture - always pair them or remove `[Collection]` # Quality Checks Before submitting C# tests, verify: - [ ] All tests follow AAA pattern with clear section comments -- [ ] Test names follow hierarchical patterns defined in Test Naming Standards section -- [ ] Each test verifies single, specific behavior (no shared state) +- [ ] Test names follow hierarchical naming pattern above +- [ ] Each test verifies single, specific behavior (no shared state between tests) - [ ] Both success and failure scenarios covered including edge cases -- [ ] External dependencies mocked with NSubstitute or equivalent +- [ ] External dependencies mocked with NSubstitute (when mocking is needed) - [ ] Tests linked to requirements with source filters where needed -- [ ] Test results generate TRX format for ReqStream compatibility -- [ ] MSTest V4 anti-patterns avoided (proper assertions, public visibility, etc.) +- [ ] Test results generated in TRX format for ReqStream compatibility (`dotnet test --logger trx`) diff --git a/.github/standards/design-documentation.md b/.github/standards/design-documentation.md index 30becb5..768bf3f 100644 --- a/.github/standards/design-documentation.md +++ b/.github/standards/design-documentation.md @@ -35,16 +35,21 @@ design to implementation: ```text docs/design/ -├── introduction.md # Design overview with software structure -└── {system-name}/ # System-level design folder (one per system) - ├── {system-name}.md # System-level design documentation - ├── {subsystem-name}/ # Subsystem (kebab-case); may nest recursively - │ ├── {subsystem-name}.md # Subsystem overview and design - │ ├── {child-subsystem}/ # Child subsystem (same structure as parent) - │ └── {unit-name}.md # Unit-level design documents - └── {unit-name}.md # Top-level unit design documents (if not in subsystem) +├── 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 ## ``` +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`). + ## introduction.md (MANDATORY) The `introduction.md` file serves as the design entry point and MUST include @@ -108,6 +113,13 @@ 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. + ### Companion Artifact Structure (RECOMMENDED) Include a brief note explaining that each software item has parallel artifacts @@ -117,22 +129,30 @@ artifact to all related files: Example format: ```text -Each software item in the structure above has corresponding artifacts in -parallel directory trees: - -- Requirements: `docs/reqstream/{system}/.../{item}.yaml` (kebab-case) -- Design docs: `docs/design/{system}/.../{item}.md` (kebab-case) -- Source code: `src/{System}/.../{Item}.{ext}` (cased per language - see `software-items.md`) -- Tests: `test/{System}.Tests/.../{Item}Tests.{ext}` (cased per language - see `software-items.md`) -- Review-sets: defined in `.reviewmark.yaml` +Each in-house software item has corresponding artifacts in parallel directory trees: + +- 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) + +OTS items have no design documentation; their artifacts sit parallel to system folders: + +- 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 a kebab-case folder matching the system name -- Include `{system-name}.md` with system-level design documentation such as: +- 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 @@ -143,16 +163,20 @@ For each system identified in the repository: For each subsystem identified in the software structure: -- Create a kebab-case folder matching the subsystem name (enables automated tooling) -- Include `{subsystem-name}.md` with subsystem overview and design -- Include unit design documents for ALL units within the subsystem +- 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 -- Place in appropriate subsystem folder or at design root level + +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) @@ -168,6 +192,9 @@ 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 @@ -180,9 +207,11 @@ 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 -- [ ] Subsystem documentation folders use kebab-case names while mirroring source subsystem names and structure +- [ ] 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 diff --git a/.github/standards/reqstream-usage.md b/.github/standards/reqstream-usage.md index ae5e565..303bb43 100644 --- a/.github/standards/reqstream-usage.md +++ b/.github/standards/reqstream-usage.md @@ -18,20 +18,25 @@ because ReqStream discovers files via the includes chain in `requirements.yaml` and organizes report output by this hierarchy: ```text -requirements.yaml # Root file (includes only) +requirements.yaml # Root file (includes only) docs/reqstream/ -├── {system-name}/ # System-level requirements folder (one per system) -│ ├── {system-name}.yaml # System-level requirements +├── {system-name}.yaml # System-level requirements +├── {system-name}/ # System folder (one per system) │ ├── platform-requirements.yaml # Platform support requirements -│ ├── {subsystem-name}/ # Subsystem (kebab-case); may nest recursively -│ │ ├── {subsystem-name}.yaml # Requirements for this subsystem -│ │ ├── {child-subsystem}/ # Child subsystem (same structure as parent) -│ │ └── {unit-name}.yaml # Requirements for units within this subsystem -│ └── {unit-name}.yaml # Requirements for top-level units (outside subsystems) -└── ots/ # OTS items appear as a distinct section in reports - └── {ots-name}.yaml # Requirements for OTS components +│ ├── {subsystem-name}.yaml # Subsystem requirements +│ ├── {subsystem-name}/ # Subsystem folder (kebab-case); may nest recursively +│ │ ├── {child-subsystem}.yaml # Child subsystem requirements +│ │ ├── {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 ``` +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. + # Requirements File Format ```yaml @@ -62,7 +67,7 @@ sections: sections: - title: System.Text.Json requirements: - - id: TemplateTool-SystemTextJson-ReadJson + - id: SystemTextJson-Core-ReadJson title: System.Text.Json shall be able to read JSON files. tests: - JsonReaderTests.TestReadValidJson @@ -104,16 +109,16 @@ dotnet reqstream --requirements requirements.yaml --lint # Generate requirements document for compliance record dotnet reqstream --requirements requirements.yaml \ - --report docs/requirements_doc/requirements.md + --report docs/requirements_doc/generated/requirements.md # Generate justifications document for compliance record dotnet reqstream --requirements requirements.yaml \ - --justifications docs/requirements_doc/justifications.md + --justifications docs/requirements_doc/generated/justifications.md # Generate trace matrix proving each requirement is covered by passing tests dotnet reqstream --requirements requirements.yaml \ --tests "artifacts/**/*.trx" \ - --matrix docs/requirements_report/trace_matrix.md + --matrix docs/requirements_report/generated/trace_matrix.md ``` # Quality Checks @@ -124,8 +129,8 @@ Before submitting requirements, verify: - [ ] Every requirement links to at least one passing test - [ ] Platform-specific requirements use source filters (`platform@TestName`) - [ ] Comprehensive justification explains business/regulatory need -- [ ] Files organized under `docs/reqstream/` following folder structure patterns -- [ ] Subsystem folders use kebab-case naming matching source code +- [ ] 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 - [ ] Valid YAML syntax passes yamllint validation - [ ] Test result formats compatible (TRX, JUnit XML) diff --git a/.github/standards/requirements-principles.md b/.github/standards/requirements-principles.md index 7d2d572..b6cf136 100644 --- a/.github/standards/requirements-principles.md +++ b/.github/standards/requirements-principles.md @@ -29,6 +29,10 @@ implementation code. - **Valid**: "The parser shall report the line number of the first syntax error." - **Not a requirement (design decision)**: "The parser shall use a `TokenStream` class." +A unit may use its own name freely - that is identity, not HOW. What is +forbidden is describing *internal construction*: class names, method signatures, +algorithms, or data structures. + # Requirements at Every Level (MANDATORY) Every identified subsystem and unit MUST have its own requirements file because diff --git a/.github/standards/reviewmark-usage.md b/.github/standards/reviewmark-usage.md index 5d6219e..2d95832 100644 --- a/.github/standards/reviewmark-usage.md +++ b/.github/standards/reviewmark-usage.md @@ -20,7 +20,7 @@ review, organizes them into review-sets, and generates review plans and reports. - **Lint Configuration**: `dotnet reviewmark --lint` - **Elaborate Review-Set**: `dotnet reviewmark --elaborate {review-set}` -- **Generate Plan**: `dotnet reviewmark --plan docs/code_review_plan/plan.md --enforce` +- **Generate Plan**: `dotnet reviewmark --plan docs/code_review_plan/generated/plan.md --enforce` > **Note**: `--enforce` causes the plan to fail with a non-zero exit code if any repository > files are not covered by a review-set. Uncovered files indicate a gap in review-set @@ -31,7 +31,8 @@ review, organizes them into review-sets, and generates review plans and reports. Required repository items for ReviewMark operation: - `.reviewmark.yaml` - Configuration for review-sets, file-patterns, and review evidence-source. -- `docs/code_review_plan/` - Review planning artifacts +- `docs/code_review_plan/generated/` - Generated review plan (build output, do not edit) +- `docs/code_review_report/generated/` - Generated review report (build output, do not edit) # Review Definition Structure @@ -55,10 +56,22 @@ needs-review: - "README.md" # Root level README - "docs/user_guide/**/*.md" # User guide - "docs/design/**/*.md" # Design documentation + - "docs/verification/**/*.md" # Verification design documentation # Source of review evidence evidence-source: type: none + +# Review-sets (each focuses on a single compliance question) +reviews: + - id: Purpose + title: Review of user-facing capabilities and system promises + paths: + - "README.md" + - "docs/user_guide/**/*.md" + - "docs/reqstream/{system-name}.yaml" + - "docs/design/introduction.md" + - "docs/design/{system-name}.md" ``` # Review-Set Design Principles @@ -93,9 +106,9 @@ Reviews user-facing capabilities and system promises: - **File Path Patterns**: - README: `README.md` - User guide: `docs/user_guide/**/*.md` - - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` + - System requirements: `docs/reqstream/{system-name}.yaml` - Design introduction: `docs/design/introduction.md` - - System design: `docs/design/{system-name}/{system-name}.md` + - System design: `docs/design/{system-name}.md` ## `{System}-Architecture` Review (one per system) @@ -106,9 +119,11 @@ Reviews system architecture and operational validation: - **Scope**: Excludes subsystem and unit files, relying on system-level design to describe what subsystems and units it uses - **File Path Patterns**: - - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` + - System requirements: `docs/reqstream/{system-name}.yaml` - Design introduction: `docs/design/introduction.md` - - System design: `docs/design/{system-name}/{system-name}.md` + - System design: `docs/design/{system-name}.md` + - Verification introduction: `docs/verification/introduction.md` + - System verification design: `docs/verification/{system-name}.md` - System integration tests: `test/{SystemName}.Tests/{SystemName}Tests.{ext}` ## `{System}-Design` Review (one per system) @@ -119,9 +134,10 @@ Reviews architectural and design consistency: - **Title**: "Review that {System} Design is Consistent and Complete" - **Scope**: Only brings in top-level requirements and relies on brevity of design documentation - **File Path Patterns**: - - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` + - System requirements: `docs/reqstream/{system-name}.yaml` - Platform requirements: `docs/reqstream/{system-name}/platform-requirements.yaml` - Design introduction: `docs/design/introduction.md` + - System design: `docs/design/{system-name}.md` - System design files: `docs/design/{system-name}/**/*.md` ## `{System}-AllRequirements` Review (one per system) @@ -133,8 +149,8 @@ Reviews requirements quality and traceability: - **Scope**: Only brings in requirements files to keep review manageable - **File Path Patterns**: - Root requirements: `requirements.yaml` - - System requirements: `docs/reqstream/{system-name}/**/*.yaml` - - OTS requirements: `docs/reqstream/ots/**/*.yaml` (if applicable) + - System requirements: `docs/reqstream/{system-name}.yaml` + - Subsystem/unit requirements: `docs/reqstream/{system-name}/**/*.yaml` ## `{System}-{Subsystem[-Child...]}` Review (one per subsystem at any depth) @@ -145,8 +161,9 @@ Reviews subsystem architecture and interfaces: - **Scope**: Excludes units under the subsystem, relying on subsystem design to describe what units it uses - **File Path Patterns**: - - Requirements: `docs/reqstream/{system-name}/.../{subsystem-name}/{subsystem-name}.yaml` - - Design: `docs/design/{system-name}/.../{subsystem-name}/{subsystem-name}.md` + - Requirements: `docs/reqstream/{system-name}/.../{subsystem-name}.yaml` + - Design: `docs/design/{system-name}/.../{subsystem-name}.md` + - Verification design: `docs/verification/{system-name}/.../{subsystem-name}.md` - Tests: `test/{SystemName}.Tests/.../{SubsystemName}/{SubsystemName}Tests.{ext}` ## `{System}-{Subsystem[-Child...]}-{Unit}` Review (one per unit) @@ -159,9 +176,23 @@ Reviews individual software unit implementation: - **File Path Patterns**: - Requirements: `docs/reqstream/{system-name}/.../{unit-name}.yaml` - Design: `docs/design/{system-name}/.../{unit-name}.md` + - Verification design: `docs/verification/{system-name}/.../{unit-name}.md` - Source: `src/{SystemName}/.../{UnitName}.{ext}` - Tests: `test/{SystemName}.Tests/.../{UnitName}Tests.{ext}` +## `OTS-{OtsName}` Review (one per OTS item) + +Reviews OTS item requirements and verification evidence: + +- **Purpose**: Proves that the OTS item provides the required functionality +- **Title**: "Review that {OtsName} Provides Required Functionality" +- **Scope**: OTS items have no in-house design or source; review covers requirements and + verification evidence only +- **File Path Patterns**: + - OTS requirements: `docs/reqstream/ots/{ots-name}.yaml` + - OTS verification: `docs/verification/ots/{ots-name}.md` + - Tests (if applicable): `test/{OtsSoftwareTests}/...` (cased per language) + **Note**: File path patterns use `{ext}` as a placeholder for language-specific extensions (`.cs`, `.cpp`/`.hpp`, `.py`, etc.). Adapt to your repository's languages. @@ -175,6 +206,10 @@ Before submitting ReviewMark configuration, verify: - [ ] System-level reviews follow hierarchical scope principle (exclude subsystem/unit details) - [ ] 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 +- [ ] Subsystem review-sets include subsystem verification design +- [ ] Unit review-sets include unit verification design +- [ ] OTS review-sets include OTS requirements 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 bb67b1d..6be029f 100644 --- a/.github/standards/software-items.md +++ b/.github/standards/software-items.md @@ -81,14 +81,23 @@ Choose the appropriate category based on scope and testability: consumes it - 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): + - Requirements: `docs/reqstream/ots/{ots-name}.yaml` + - 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 +- **OTS test project**: If no other verification evidence is available (e.g., vendor test results, + 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. # Software Item Artifact Model -Each software item has four artifact types that together form a complete review +Each software item has five artifact types that together form a complete review 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) +- **Verification Design** - HOW the requirements will be tested (applies to all item types) - **Source code** - The implementation of the design (in-house units only) - **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 455b2fd..2ac29f4 100644 --- a/.github/standards/technical-documentation.md +++ b/.github/standards/technical-documentation.md @@ -1,7 +1,7 @@ --- name: Technical Documentation description: Follow these standards when creating technical documentation. -globs: ["docs/**/*.md", "README.md"] +globs: ["docs/**/*.md", "README.md", "!docs/**/generated/**"] --- # Technical Documentation Standards @@ -23,63 +23,25 @@ for regulatory review: - **Review Integration**: Documentation follows ReviewMark patterns for formal review tracking -# Documentation Organization +# Pandoc Document Structure (MANDATORY) -Structure documentation under `docs/` following standard patterns for -consistency and tool compatibility: +Each document collection under `docs/` follows this layout: ```text -docs/ - build_notes.md # Generated by BuildMark - build_notes/ # Auto-generated build notes - versions.md # Generated by VersionMark - code_review_plan/ # Auto-generated review plans - plan.md # Generated by ReviewMark - code_review_report/ # Auto-generated review reports - report.md # Generated by ReviewMark - design/ # Design documentation - introduction.md # Design overview - {system-name}/ # System architecture folder - {system-name}.md # System architecture - {subsystem-name}/ # Subsystem folder; may nest recursively - {subsystem-name}.md # Subsystem-specific designs - {child-subsystem}/ # Child subsystem (same structure) - {unit-name}.md # Unit-specific designs - {unit-name}.md # Top-level unit design - reqstream/ # Requirements source files - {system-name}/ # System requirements folder - {system-name}.yaml # System requirements - platform-requirements.yaml # Platform requirements - {subsystem-name}/ # Subsystem folder; may nest recursively - {subsystem-name}.yaml # Subsystem requirements - {child-subsystem}/ # Child subsystem (same structure) - {unit-name}.yaml # Unit-specific requirements - {unit-name}.yaml # Top-level unit requirements - ots/ # OTS requirement files - {ots-name}.yaml # OTS requirements - requirements_doc/ # Auto-generated requirements reports - requirements.md # Generated by ReqStream - justifications.md # Generated by ReqStream - requirements_report/ # Auto-generated trace matrices - trace_matrix.md # Generated by ReqStream - user_guide/ # User-facing documentation - introduction.md # User guide overview - {section}.md # User guide sections +docs/{collection}/ + title.txt # MANDATORY - YAML document metadata (title, author, etc.) + definition.yaml # MANDATORY - Pandoc build definition (inputs, template, paths) + introduction.md # MANDATORY - document introduction (Purpose, Scope, References) + {section}.md # optional checked-in content sections (zero or more) + generated/ # BUILD OUTPUT - never read, edit, or lint these files + {report}.md # generated by CI tools (ReqStream, ReviewMark, SarifMark, etc.) + {collection}.html # generated by Pandoc ``` -# Pandoc Document Structure (MANDATORY) - -All document collections processed by Pandoc MUST include all four files below - -without `title.txt` and `definition.yaml` the pipeline cannot generate the document: - -- `title.txt` - YAML metadata (title, subtitle, author, description, lang, keywords) -- `definition.yaml` - Pandoc build definition (resource paths, input file list, template) -- `introduction.md` - document introduction -- `{sections}.md` - additional content sections - -When creating a new document collection, create `title.txt` and `definition.yaml` -alongside `introduction.md`. Use the existing files under `docs/` as templates - -they share a consistent structure across all collections. +Without `title.txt` and `definition.yaml` the pipeline cannot generate the document. +When creating a new document collection, create these three files together and use +the existing collections under `docs/` as templates - they share a consistent +structure across all collections. **`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 @@ -106,13 +68,39 @@ Include regulatory or business drivers where applicable. Define what is covered and what is explicitly excluded from this documentation. Specify version, system boundaries, and applicability constraints. + +## References + +- [REF-1] Document Title, Author, Version, Date +- [REF-2] Standard Name (e.g., IEEE 12207, ISO 9001) ``` +The `Purpose`, `Scope`, and `References` sections are **unique to `introduction.md`** and must +**not** be replicated in other markdown files within the same document collection. Including them +elsewhere causes duplicate sections in the compiled PDF. + ## Document Ordering List documents in logical reading order in Pandoc configuration because readers need coherent information flow from general to specific topics. +## Heading Depth Rule (MANDATORY) + +A file's top-level heading depth must equal its folder depth under the document +collection root - this ensures Pandoc can concatenate all files in `definition.yaml` +order and produce a coherent outline with no heading-shift configuration: + +| Folder depth | Top heading | +| --- | --- | +| 0 - collection root | `#` | +| 1 - one subfolder deep | `##` | +| 2 - two subfolders deep | `###` | +| N - N subfolders deep | `#` × (N+1) | + +Internal sections use the next heading level down (e.g. a `##` file uses `###` +for *Overview*, *Interfaces*, etc.). Deeply nested files have fewer heading levels +available - keep internal structure flat to avoid excessive nesting. + # Writing Guidelines Write technical documentation for clarity and compliance verification: @@ -135,6 +123,19 @@ References in design/technical documents must point to **external specifications - **INCLUDE**: Requirements documents, system specifications, program documents, standards (IEEE, ISO, etc.) - **NEVER INCLUDE**: Internal development standards (`.github/standards/` files) - these are agent guides +## Cross-References (Within-Document and Cross-Document) + +Do **not** use markdown hyperlinks to reference other sections or documents. Markdown anchor links +(`[text](#heading)`) and relative file links work in a browser but break when compiled to a PDF. + +Instead use **verbal references** - plain prose that identifies the target by name: + +> See *XYZ Design* for more details. +> +> Refer to the *System Requirements* document for the full specification. + +Verbal references are readable by both AI agents and humans in any rendering environment. + # Markdown Format Requirements Markdown documentation in this repository must follow the formatting standards @@ -156,14 +157,13 @@ for consistency and professional presentation: # Auto-Generated Content (CRITICAL) -**NEVER modify auto-generated markdown files** because changes will be -overwritten and break compliance automation: +**NEVER read, lint, or modify files inside any `generated/` folder** - they are +build outputs that are overwritten on every CI run: -- **Read-Only Files**: Generated reports under `docs/requirements_doc/`, - `docs/requirements_report/`, `docs/code_review_plan/`, and - `docs/code_review_report/` are regenerated on every build -- **Source Modification**: Update source files (requirements YAML, code - comments) instead of generated output +- **Location**: All generated files live in `generated/` subfolders within their + respective `docs/` sections, or in `docs/generated/` for final release artifacts +- **Source Modification**: Update source files (requirements YAML, `.reviewmark.yaml`, + tool configuration) instead of generated output - **Tool Integration**: Generated content integrates with CI/CD pipelines and manual changes disrupt automation diff --git a/.github/standards/verification-documentation.md b/.github/standards/verification-documentation.md new file mode 100644 index 0000000..8eea3b7 --- /dev/null +++ b/.github/standards/verification-documentation.md @@ -0,0 +1,144 @@ +--- +name: Verification Documentation +description: Follow these standards when creating software verification design documentation. +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 + +Organize under `docs/verification/` mirroring the software item hierarchy: + +```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 ## +``` + +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`). + +## 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). + +Include a Companion Artifact Structure note so agents and reviewers can navigate from any +artifact to all related files: + +```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` +``` + +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. + +## System Verification Design (MANDATORY) + +For each system, create `{system-name}.md` at `docs/verification/` root and a +`{system-name}/` folder for subsystems. Cover: + +- 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 + +## Subsystem Verification Design (MANDATORY) + +For each subsystem, place `{subsystem-name}.md` in the parent (system or subsystem) +folder and create a `{subsystem-name}/` folder for its units. Cover: + +- 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 + +## Unit Verification Design (MANDATORY) + +Place `{unit-name}.md` in the parent (system or subsystem) folder. Cover: + +- 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 + +## OTS Verification Evidence (when OTS items are used) + +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. + +For each OTS item, create `docs/verification/ots/{ots-name}.md` covering: + +- 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 + +# 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. + +# 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 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cee20a2..d441f4e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -466,6 +466,15 @@ jobs: buildmark versionmark reviewmark fileassert echo "✓ Tool versions captured" + # === PREPARE DOCUMENT OUTPUT === + # Creates the shared docs/generated/ folder that all document sections write PDFs into. + # This step is intentionally separate from the document sections so any individual + # section can be commented out without breaking the shared output directory. + + - name: Create documents output directory + shell: bash + run: mkdir -p docs/generated + # === COMPILE BUILD NOTES === # This section generates the Build Notes document. BuildMark and VersionMark self-validations # run here to co-locate their evidence with the document that depends on their output. @@ -473,6 +482,10 @@ jobs: # validates the outputs contain expected content. # Downstream projects: Add any additional build notes steps here. + - name: Create build notes output directories + shell: bash + run: mkdir -p docs/build_notes/generated + - name: Run BuildMark self-validation run: > dotnet buildmark @@ -492,20 +505,20 @@ jobs: run: > dotnet buildmark --build-version ${{ inputs.version }} - --report docs/build_notes.md + --report docs/build_notes/generated/build_notes.md --report-depth 1 - name: Display Build Notes Report shell: bash run: | echo "=== Build Notes Report ===" - cat docs/build_notes.md + cat docs/build_notes/generated/build_notes.md - name: Publish Tool Versions shell: bash run: | echo "Publishing tool versions..." - dotnet versionmark --publish --report docs/build_notes/versions.md --report-depth 1 \ + dotnet versionmark --publish --report docs/build_notes/generated/versions.md --report-depth 1 \ -- "artifacts/**/versionmark-*.json" echo "✓ Tool versions published" @@ -513,7 +526,7 @@ jobs: shell: bash run: | echo "=== Tool Versions Report ===" - cat docs/build_notes/versions.md + cat docs/build_notes/generated/versions.md - name: Generate Build Notes HTML with Pandoc shell: bash @@ -523,14 +536,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/build_notes/build_notes.html + --output docs/build_notes/generated/build_notes.html - name: Generate Build Notes PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/build_notes/build_notes.html - "docs/ReqStream Build Notes.pdf" + docs/build_notes/generated/build_notes.html + "docs/generated/ReqStream Build Notes.pdf" - name: Assert Build Notes Documents with FileAssert run: > @@ -538,6 +551,10 @@ jobs: --results artifacts/fileassert-build-notes.trx build-notes + - name: Copy Build Notes report to docs/generated + shell: bash + run: cp docs/build_notes/generated/build_notes.md docs/generated/build_notes.md + # === COMPILE CODE QUALITY REPORT === # This section generates the Code Quality document. SarifMark and SonarMark self-validations # run here to co-locate their evidence with the document that depends on their output. @@ -545,6 +562,10 @@ jobs: # validates the outputs contain expected content. # Downstream projects: Add any additional code quality steps here. + - name: Create code quality output directory + shell: bash + run: mkdir -p docs/code_quality/generated + - name: Run SarifMark self-validation run: > dotnet sarifmark @@ -561,7 +582,7 @@ jobs: run: > dotnet sarifmark --sarif artifacts/csharp.sarif - --report docs/code_quality/codeql-quality.md + --report docs/code_quality/generated/codeql-quality.md --heading "ReqStream CodeQL Analysis" --report-depth 1 @@ -569,7 +590,7 @@ jobs: shell: bash run: | echo "=== CodeQL Quality Report ===" - cat docs/code_quality/codeql-quality.md + cat docs/code_quality/generated/codeql-quality.md - name: Generate SonarCloud Quality Report shell: bash @@ -581,14 +602,14 @@ jobs: --project-key demaconsulting_ReqStream --branch ${{ github.ref_name }} --token "$SONAR_TOKEN" - --report docs/code_quality/sonar-quality.md + --report docs/code_quality/generated/sonar-quality.md --report-depth 1 - name: Display SonarCloud Quality Report shell: bash run: | echo "=== SonarCloud Quality Report ===" - cat docs/code_quality/sonar-quality.md + cat docs/code_quality/generated/sonar-quality.md - name: Generate Code Quality HTML with Pandoc shell: bash @@ -598,14 +619,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/code_quality/quality.html + --output docs/code_quality/generated/quality.html - name: Generate Code Quality PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/code_quality/quality.html - "docs/ReqStream Code Quality.pdf" + docs/code_quality/generated/quality.html + "docs/generated/ReqStream Code Quality.pdf" - name: Assert Code Quality Documents with FileAssert run: > @@ -620,6 +641,10 @@ jobs: # PDF, and FileAssert validates the outputs contain expected content. # Downstream projects: Add any additional code review steps here. + - name: Create code review output directories + shell: bash + run: mkdir -p docs/code_review_plan/generated docs/code_review_report/generated + - name: Run ReviewMark self-validation run: > dotnet reviewmark @@ -631,22 +656,22 @@ jobs: # TODO: Add --enforce once reviews branch is populated with review evidence PDFs and index.json run: > dotnet reviewmark - --plan docs/code_review_plan/plan.md + --plan docs/code_review_plan/generated/plan.md --plan-depth 1 - --report docs/code_review_report/report.md + --report docs/code_review_report/generated/report.md --report-depth 1 - name: Display Review Plan shell: bash run: | echo "=== Review Plan ===" - cat docs/code_review_plan/plan.md + cat docs/code_review_plan/generated/plan.md - name: Display Review Report shell: bash run: | echo "=== Review Report ===" - cat docs/code_review_report/report.md + cat docs/code_review_report/generated/report.md - name: Generate Review Plan HTML with Pandoc shell: bash @@ -656,14 +681,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/code_review_plan/plan.html + --output docs/code_review_plan/generated/plan.html - name: Generate Review Plan PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/code_review_plan/plan.html - "docs/ReqStream Review Plan.pdf" + docs/code_review_plan/generated/plan.html + "docs/generated/ReqStream Review Plan.pdf" - name: Generate Review Report HTML with Pandoc shell: bash @@ -673,14 +698,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/code_review_report/report.html + --output docs/code_review_report/generated/report.html - name: Generate Review Report PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/code_review_report/report.html - "docs/ReqStream Review Report.pdf" + docs/code_review_report/generated/report.html + "docs/generated/ReqStream Review Report.pdf" - name: Assert Code Review Documents with FileAssert run: > @@ -693,6 +718,10 @@ jobs: # FileAssert validates that the HTML and PDF outputs contain expected content. # Downstream projects: Add any additional design document steps here. + - name: Create design output directory + shell: bash + run: mkdir -p docs/design/generated + - name: Generate Design HTML with Pandoc shell: bash run: > @@ -701,14 +730,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/design/design.html + --output docs/design/generated/design.html - name: Generate Design PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/design/design.html - "docs/ReqStream Software Design.pdf" + docs/design/generated/design.html + "docs/generated/ReqStream Software Design.pdf" - name: Assert Design Documents with FileAssert run: > @@ -716,11 +745,46 @@ jobs: --results artifacts/fileassert-design.trx design + # === COMPILE VERIFICATION DOCUMENT === + # This section generates the Verification document using Pandoc and WeasyPrint. + # FileAssert validates that the HTML and PDF outputs contain expected content. + # Downstream projects: Add any additional verification steps here. + + - name: Create verification output directory + shell: bash + run: mkdir -p docs/verification/generated + + - name: Generate Verification HTML with Pandoc + shell: bash + run: > + dotnet pandoc + --defaults docs/verification/definition.yaml + --metadata version="${{ inputs.version }}" + --metadata date="$(date +'%Y-%m-%d')" + --output docs/verification/generated/verification.html + + - name: Generate Verification PDF with WeasyPrint + run: > + dotnet weasyprint + --pdf-variant pdf/a-3u + docs/verification/generated/verification.html + "docs/generated/ReqStream Software Verification Design.pdf" + + - name: Assert Verification Documents with FileAssert + run: > + dotnet fileassert + --results artifacts/fileassert-verification.trx + verification + # === COMPILE USER GUIDE === # This section generates the User Guide document using Pandoc and WeasyPrint. # FileAssert validates that the HTML and PDF outputs contain expected content. # Downstream projects: Add any additional user guide steps here. + - name: Create user guide output directory + shell: bash + run: mkdir -p docs/user_guide/generated + - name: Generate User Guide HTML with Pandoc shell: bash run: > @@ -729,14 +793,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/user_guide/user_guide.html + --output docs/user_guide/generated/user_guide.html - name: Generate User Guide PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/user_guide/user_guide.html - "docs/ReqStream User Guide.pdf" + docs/user_guide/generated/user_guide.html + "docs/generated/ReqStream User Guide.pdf" - name: Assert User Guide Documents with FileAssert run: > @@ -745,8 +809,8 @@ jobs: user-guide # === FILEASSERT SELF-VALIDATION === - # By this point Pandoc and WeasyPrint have each produced 6 validated documents - # (Build Notes, Code Quality, Review Plan, Review Report, Design, User Guide), + # By this point Pandoc and WeasyPrint have each produced 7 validated documents + # (Build Notes, Code Quality, Review Plan, Review Report, Design, Verification, User Guide), # providing strong OTS evidence for both tools before ReqStream runs. FileAssert # self-validation confirms the assertion tool itself is operational. # Downstream projects: Add any additional FileAssert self-validation steps here. @@ -766,6 +830,10 @@ jobs: # confirm the requirements pipeline produced well-formed documents. # Downstream projects: Add any additional requirements steps here. + - name: Create requirements output directories + shell: bash + run: mkdir -p docs/requirements_doc/generated docs/requirements_report/generated + - name: Run ReqStream self-validation run: > dotnet reqstream @@ -777,9 +845,9 @@ jobs: dotnet reqstream --requirements requirements.yaml --tests "artifacts/**/*.trx" - --report docs/requirements_doc/requirements.md - --justifications docs/requirements_doc/justifications.md - --matrix docs/requirements_report/trace_matrix.md + --report docs/requirements_doc/generated/requirements.md + --justifications docs/requirements_doc/generated/justifications.md + --matrix docs/requirements_report/generated/trace_matrix.md --enforce - name: Generate Requirements HTML with Pandoc @@ -790,14 +858,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/requirements_doc/requirements.html + --output docs/requirements_doc/generated/requirements.html - name: Generate Requirements PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/requirements_doc/requirements.html - "docs/ReqStream Requirements.pdf" + docs/requirements_doc/generated/requirements.html + "docs/generated/ReqStream Requirements.pdf" - name: Generate Trace Matrix HTML with Pandoc shell: bash @@ -807,14 +875,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/requirements_report/trace_matrix.html + --output docs/requirements_report/generated/trace_matrix.html - name: Generate Trace Matrix PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/requirements_report/trace_matrix.html - "docs/ReqStream Trace Matrix.pdf" + docs/requirements_report/generated/trace_matrix.html + "docs/generated/ReqStream Trace Matrix.pdf" - name: Assert Requirements Documents with FileAssert run: > @@ -830,6 +898,4 @@ jobs: uses: actions/upload-artifact@v7 with: name: documents - path: |- - docs/*.pdf - docs/build_notes.md + path: docs/generated/* diff --git a/.gitignore b/.gitignore index 6c8f469..3058d94 100644 --- a/.gitignore +++ b/.gitignore @@ -88,18 +88,7 @@ __pycache__/ .venv/ # Generated documentation -docs/**/*.html -docs/**/*.pdf -!docs/template/** -docs/requirements_doc/requirements.md -docs/requirements_doc/justifications.md -docs/requirements_report/trace_matrix.md -docs/code_quality/codeql-quality.md -docs/code_quality/sonar-quality.md -docs/code_review_plan/plan.md -docs/code_review_report/report.md -docs/build_notes.md -docs/build_notes/versions.md +**/generated/ # Test results TestResults/ diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index c16c443..cbc8cf5 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -32,6 +32,7 @@ config: # Allow longer lines for URLs and technical content MD013: line_length: 120 + tables: false # Allow multiple top-level headers per document MD025: false @@ -50,5 +51,6 @@ ignores: - "**/thirdparty/**" - "**/third-party/**" - "**/3rd-party/**" + - "**/generated/**" - "**/AGENT_REPORT_*.md" - "**/.agent-logs/**" diff --git a/.reviewmark.yaml b/.reviewmark.yaml index c344046..4ef154f 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -1,19 +1,22 @@ --- # ReviewMark Configuration File # This file defines which files require review, where the evidence store is located, -# and how files are grouped into named review-sets following software unit boundaries. +# and how files are grouped into named review-sets following hierarchical scope principles. # Patterns identifying all files that require review. # Processed in order; prefix a pattern with '!' to exclude. needs-review: - - "README.md" # Project readme - - "**/*.cs" # All C# source and test files - - "requirements.yaml" # Root requirements file - - "docs/reqstream/**/*.yaml" # Requirements files - - "docs/user_guide/**/*.md" # User guide documents - - "docs/design/**/*.md" # Design documents - - "!**/obj/**" # Exclude build output - - "!**/bin/**" # Exclude build output + - "README.md" # Root README file + - "requirements.yaml" # Root requirements file + - "**/*.cs" # All C# source and test files + - "docs/reqstream/**/*.yaml" # Requirements files + - "docs/design/**/*.md" # Design documentation files + - "docs/verification/**/*.md" # Verification documentation files + - "docs/user_guide/**/*.md" # User guide documentation + - "!**/obj/**" # Exclude build output + - "!**/bin/**" # Exclude build output + - "!node_modules/**" # Exclude npm dependencies + - "!**/.venv/**" # Exclude Python virtual environment # Evidence source: review data and index.json are located in the 'reviews' branch # of this repository, accessed through the GitHub public HTTPS raw content access. @@ -29,152 +32,241 @@ reviews: - id: Purpose title: Review that Advertised Features Match System Design paths: - - "README.md" # user-facing documentation - - "docs/user_guide/**/*.md" # user guide - - "docs/reqstream/reqstream/reqstream.yaml" # system requirements - - "docs/design/introduction.md" # design introduction - - "docs/design/reqstream/reqstream.md" # system design + - "README.md" # user-facing documentation + - "docs/user_guide/**/*.md" # user guide + - "docs/reqstream/reqstream.yaml" # system requirements + - "docs/design/introduction.md" # design introduction + - "docs/design/reqstream.md" # system design - # ReqStream-Architecture Review (one per system) + # ReqStream - Specials - id: ReqStream-Architecture title: Review that ReqStream Architecture Satisfies Requirements paths: - - "docs/reqstream/reqstream/reqstream.yaml" # system requirements - - "docs/design/introduction.md" # design introduction - - "docs/design/reqstream/reqstream.md" # system design - - "test/**/IntegrationTests.cs" # system integration tests - - "test/**/Runner.cs" # test runner infrastructure - - "test/**/AssemblyInfo.cs" # test infrastructure + - "docs/reqstream/reqstream.yaml" # system requirements + - "docs/design/introduction.md" # design introduction + - "docs/design/reqstream.md" # system design + - "docs/verification/introduction.md" # verification introduction + - "docs/verification/reqstream.md" # system verification design + - "test/**/IntegrationTests.cs" # system integration tests + - "test/**/Runner.cs" # test runner infrastructure + - "test/**/AssemblyInfo.cs" # test infrastructure - # ReqStream-Design Review (one per system) - id: ReqStream-Design title: Review that ReqStream Design is Consistent and Complete paths: - - "docs/reqstream/reqstream/reqstream.yaml" # system requirements - - "docs/reqstream/reqstream/platform-requirements.yaml" # platform requirements - - "docs/design/introduction.md" # design introduction - - "docs/design/reqstream/**/*.md" # all system design documents + - "docs/reqstream/reqstream.yaml" # system requirements + - "docs/reqstream/reqstream/platform-requirements.yaml" # platform requirements + - "docs/design/introduction.md" # design introduction + - "docs/design/reqstream.md" # system design + - "docs/design/reqstream/**/*.md" # all system design documents + - "docs/verification/introduction.md" # verification introduction + - "docs/verification/ots.md" # OTS verification overview - # ReqStream-AllRequirements Review (one per system) - id: ReqStream-AllRequirements title: Review that All ReqStream Requirements are Complete paths: - - "requirements.yaml" # root requirements file - - "docs/reqstream/reqstream/**/*.yaml" # all system requirements files - - "docs/reqstream/ots/**/*.yaml" # OTS requirements files + - "requirements.yaml" # root requirements file + - "docs/reqstream/reqstream.yaml" # system requirements file + - "docs/reqstream/reqstream/**/*.yaml" # all subsystem requirements files + - "docs/reqstream/ots/**/*.yaml" # OTS requirements files - # ReqStream-Cli Review (one per subsystem) - - id: ReqStream-Cli - title: Review that ReqStream Cli Satisfies Subsystem Requirements - paths: - - "docs/reqstream/reqstream/cli/cli.yaml" # subsystem requirements - - "docs/design/reqstream/cli/cli.md" # subsystem design - - "test/**/Cli/CliTests.cs" # subsystem tests - - - id: ReqStream-Modeling - title: Review that ReqStream Modeling Satisfies Subsystem Requirements - paths: - - "docs/reqstream/reqstream/modeling/modeling.yaml" # subsystem requirements - - "docs/design/reqstream/modeling/modeling.md" # subsystem design - - "test/**/Modeling/ModelingTests.cs" # subsystem tests - - - id: ReqStream-Tracing - title: Review that ReqStream Tracing Satisfies Subsystem Requirements - paths: - - "docs/reqstream/reqstream/tracing/tracing.yaml" # subsystem requirements - - "docs/design/reqstream/tracing/tracing.md" # subsystem design - - "test/**/Tracing/TracingTests.cs" # subsystem tests - - - id: ReqStream-SelfTest - title: Review that ReqStream SelfTest Satisfies Subsystem Requirements - paths: - - "docs/reqstream/reqstream/self-test/self-test.yaml" # subsystem requirements - - "docs/design/reqstream/self-test/self-test.md" # subsystem design - - "test/**/SelfTest/SelfTestTests.cs" # subsystem tests - - # ── Unit Reviews ────────────────────────────────────────────────────────────── + # ReqStream - Program - id: ReqStream-Program title: Review that ReqStream Program Implementation is Correct paths: - - "docs/reqstream/reqstream/program.yaml" # unit requirements - - "docs/design/reqstream/program.md" # unit design - - "src/**/Program.cs" # implementation - - "test/**/ProgramTests.cs" # unit tests + - "docs/reqstream/reqstream/program.yaml" # unit requirements + - "docs/design/reqstream/program.md" # unit design + - "docs/verification/reqstream/program.md" # unit verification design + - "src/**/Program.cs" # implementation + - "test/**/ProgramTests.cs" # unit tests + + # ReqStream - Cli + - id: ReqStream-Cli + title: Review that ReqStream Cli Satisfies Subsystem Requirements + paths: + - "docs/reqstream/reqstream/cli.yaml" # subsystem requirements + - "docs/design/reqstream/cli.md" # subsystem design + - "docs/verification/reqstream/cli.md" # subsystem verification design + - "test/**/Cli/CliTests.cs" # subsystem tests - id: ReqStream-Cli-Context title: Review that ReqStream Cli Context Implementation is Correct paths: - - "docs/reqstream/reqstream/cli/context.yaml" # unit requirements - - "docs/design/reqstream/cli/context.md" # unit design - - "src/**/Cli/Context.cs" # implementation - - "test/**/Cli/ContextTests.cs" # unit tests + - "docs/reqstream/reqstream/cli/context.yaml" # unit requirements + - "docs/design/reqstream/cli/context.md" # unit design + - "docs/verification/reqstream/cli/context.md" # unit verification design + - "src/**/Cli/Context.cs" # implementation + - "test/**/Cli/ContextTests.cs" # unit tests + + # ReqStream - Modeling + - id: ReqStream-Modeling + title: Review that ReqStream Modeling Satisfies Subsystem Requirements + paths: + - "docs/reqstream/reqstream/modeling.yaml" # subsystem requirements + - "docs/design/reqstream/modeling.md" # subsystem design + - "docs/verification/reqstream/modeling.md" # subsystem verification design + - "test/**/Modeling/ModelingTests.cs" # subsystem tests - id: ReqStream-Modeling-LintIssue title: Review that ReqStream Modeling LintIssue Implementation is Correct paths: - - "docs/reqstream/reqstream/modeling/lint-issue.yaml" # unit requirements - - "docs/design/reqstream/modeling/lint-issue.md" # unit design - - "src/**/Modeling/LintIssue.cs" # implementation - - "test/**/Modeling/LintIssueTests.cs" # unit tests + - "docs/reqstream/reqstream/modeling/lint-issue.yaml" # unit requirements + - "docs/design/reqstream/modeling/lint-issue.md" # unit design + - "docs/verification/reqstream/modeling/lint-issue.md" # unit verification design + - "src/**/Modeling/LintIssue.cs" # implementation + - "test/**/Modeling/LintIssueTests.cs" # unit tests - id: ReqStream-Modeling-LoadResult title: Review that ReqStream Modeling LoadResult Implementation is Correct paths: - - "docs/reqstream/reqstream/modeling/load-result.yaml" # unit requirements - - "docs/design/reqstream/modeling/load-result.md" # unit design - - "src/**/Modeling/LoadResult.cs" # implementation - - "test/**/Modeling/LoadResultTests.cs" # unit tests - - "test/**/Modeling/RequirementsLoadTests.cs" # load result integration tests + - "docs/reqstream/reqstream/modeling/load-result.yaml" # unit requirements + - "docs/design/reqstream/modeling/load-result.md" # unit design + - "docs/verification/reqstream/modeling/load-result.md" # unit verification design + - "src/**/Modeling/LoadResult.cs" # implementation + - "test/**/Modeling/LoadResultTests.cs" # unit tests + - "test/**/Modeling/RequirementsLoadTests.cs" # load result integration tests - id: ReqStream-Modeling-Requirement title: Review that ReqStream Modeling Requirement Implementation is Correct paths: - - "docs/reqstream/reqstream/modeling/requirement.yaml" # unit requirements - - "docs/design/reqstream/modeling/requirement.md" # unit design - - "src/**/Modeling/Requirement.cs" # implementation - - "test/**/Modeling/RequirementTests.cs" # unit tests + - "docs/reqstream/reqstream/modeling/requirement.yaml" # unit requirements + - "docs/design/reqstream/modeling/requirement.md" # unit design + - "docs/verification/reqstream/modeling/requirement.md" # unit verification design + - "src/**/Modeling/Requirement.cs" # implementation + - "test/**/Modeling/RequirementTests.cs" # unit tests - id: ReqStream-Modeling-Requirements title: Review that ReqStream Modeling Requirements Implementation is Correct paths: - - "docs/reqstream/reqstream/modeling/requirements.yaml" # unit requirements - - "docs/design/reqstream/modeling/requirements.md" # unit design - - "src/**/Modeling/Requirements.cs" # implementation - - "test/**/Modeling/RequirementsLoadTests.cs" # unit tests - - "test/**/Modeling/RequirementsLoadParsingTests.cs" # unit tests - - "test/**/Modeling/RequirementsExportTests.cs" # unit tests + - "docs/reqstream/reqstream/modeling/requirements.yaml" # unit requirements + - "docs/design/reqstream/modeling/requirements.md" # unit design + - "docs/verification/reqstream/modeling/requirements.md" # unit verification design + - "src/**/Modeling/Requirements.cs" # implementation + - "test/**/Modeling/RequirementsLoadTests.cs" # unit tests + - "test/**/Modeling/RequirementsLoadParsingTests.cs" # unit tests + - "test/**/Modeling/RequirementsExportTests.cs" # unit tests - id: ReqStream-Modeling-RequirementsLoader title: Review that ReqStream Modeling RequirementsLoader Implementation is Correct paths: - - "docs/reqstream/reqstream/modeling/requirements-loader.yaml" # unit requirements - - "docs/design/reqstream/modeling/requirements-loader.md" # unit design - - "src/**/Modeling/RequirementsLoader.cs" # implementation - - "test/**/Modeling/RequirementsLoaderTests.cs" # unit tests + - "docs/reqstream/reqstream/modeling/requirements-loader.yaml" # unit requirements + - "docs/design/reqstream/modeling/requirements-loader.md" # unit design + - "docs/verification/reqstream/modeling/requirements-loader.md" # unit verification design + - "src/**/Modeling/RequirementsLoader.cs" # implementation + - "test/**/Modeling/RequirementsLoaderTests.cs" # unit tests - id: ReqStream-Modeling-Section title: Review that ReqStream Modeling Section Implementation is Correct paths: - - "docs/reqstream/reqstream/modeling/section.yaml" # unit requirements - - "docs/design/reqstream/modeling/section.md" # unit design - - "src/**/Modeling/Section.cs" # implementation - - "test/**/Modeling/SectionTests.cs" # unit tests - - "test/**/Modeling/RequirementsExportTests.cs" # section hierarchy export tests + - "docs/reqstream/reqstream/modeling/section.yaml" # unit requirements + - "docs/design/reqstream/modeling/section.md" # unit design + - "docs/verification/reqstream/modeling/section.md" # unit verification design + - "src/**/Modeling/Section.cs" # implementation + - "test/**/Modeling/SectionTests.cs" # unit tests + - "test/**/Modeling/RequirementsExportTests.cs" # section hierarchy export tests + - "test/**/Modeling/RequirementsLoadParsingTests.cs" # section title-merging tests + + # ReqStream - Tracing + - id: ReqStream-Tracing + title: Review that ReqStream Tracing Satisfies Subsystem Requirements + paths: + - "docs/reqstream/reqstream/tracing.yaml" # subsystem requirements + - "docs/design/reqstream/tracing.md" # subsystem design + - "docs/verification/reqstream/tracing.md" # subsystem verification design + - "test/**/Tracing/TracingTests.cs" # subsystem tests - id: ReqStream-Tracing-TraceMatrix title: Review that ReqStream Tracing TraceMatrix Implementation is Correct paths: - - "docs/reqstream/reqstream/tracing/trace-matrix.yaml" # unit requirements - - "docs/design/reqstream/tracing/trace-matrix.md" # unit design - - "src/**/Tracing/TraceMatrix.cs" # implementation - - "test/**/Tracing/TraceMatrixTests.cs" # unit tests - - "test/**/Tracing/TraceMatrixReadTests.cs" # unit tests - - "test/**/Tracing/TraceMatrixExportTests.cs" # unit tests + - "docs/reqstream/reqstream/tracing/trace-matrix.yaml" # unit requirements + - "docs/design/reqstream/tracing/trace-matrix.md" # unit design + - "docs/verification/reqstream/tracing/trace-matrix.md" # unit verification design + - "src/**/Tracing/TraceMatrix.cs" # implementation + - "test/**/Tracing/TraceMatrixTests.cs" # unit tests + - "test/**/Tracing/TraceMatrixReadTests.cs" # unit tests + - "test/**/Tracing/TraceMatrixExportTests.cs" # unit tests + + # ReqStream - SelfTest + - id: ReqStream-SelfTest + title: Review that ReqStream SelfTest Satisfies Subsystem Requirements + paths: + - "docs/reqstream/reqstream/self-test.yaml" # subsystem requirements + - "docs/design/reqstream/self-test.md" # subsystem design + - "docs/verification/reqstream/self-test.md" # subsystem verification design + - "test/**/SelfTest/SelfTestTests.cs" # subsystem tests - id: ReqStream-SelfTest-Validation title: Review that ReqStream SelfTest Validation Implementation is Correct paths: - - "docs/reqstream/reqstream/self-test/validation.yaml" # unit requirements - - "docs/design/reqstream/self-test/validation.md" # unit design - - "src/**/SelfTest/Validation.cs" # implementation - - "test/**/SelfTest/ValidationTests.cs" # unit tests + - "docs/reqstream/reqstream/self-test/validation.yaml" # unit requirements + - "docs/design/reqstream/self-test/validation.md" # unit design + - "docs/verification/reqstream/self-test/validation.md" # unit verification design + - "src/**/SelfTest/Validation.cs" # implementation + - "test/**/SelfTest/ValidationTests.cs" # unit tests + + # OTS Items + - id: OTS-BuildMark + title: Review that BuildMark Provides Required Functionality + paths: + - "docs/reqstream/ots/buildmark.yaml" # OTS requirements + - "docs/verification/ots/buildmark.md" # OTS verification + + - id: OTS-FileAssert + title: Review that FileAssert Provides Required Functionality + paths: + - "docs/reqstream/ots/fileassert.yaml" # OTS requirements + - "docs/verification/ots/fileassert.md" # OTS verification + + - id: OTS-XUnit + title: Review that xUnit Provides Required Functionality + paths: + - "docs/reqstream/ots/xunit.yaml" # OTS requirements + - "docs/verification/ots/xunit.md" # OTS verification + + - id: OTS-Pandoc + title: Review that Pandoc Provides Required Functionality + paths: + - "docs/reqstream/ots/pandoc.yaml" # OTS requirements + - "docs/verification/ots/pandoc.md" # OTS verification + + - id: OTS-YamlDotNet + title: Review that YamlDotNet Provides Required Functionality + paths: + - "docs/reqstream/ots/yamldotnet.yaml" # OTS requirements + - "docs/verification/ots/yamldotnet.md" # OTS verification + + - id: OTS-TestResults + title: Review that DemaConsulting.TestResults Provides Required Functionality + paths: + - "docs/reqstream/ots/testresults.yaml" # OTS requirements + - "docs/verification/ots/testresults.md" # OTS verification + + - id: OTS-ReviewMark + title: Review that ReviewMark Provides Required Functionality + paths: + - "docs/reqstream/ots/reviewmark.yaml" # OTS requirements + - "docs/verification/ots/reviewmark.md" # OTS verification + + - id: OTS-SarifMark + title: Review that SarifMark Provides Required Functionality + paths: + - "docs/reqstream/ots/sarifmark.yaml" # OTS requirements + - "docs/verification/ots/sarifmark.md" # OTS verification + + - id: OTS-SonarMark + title: Review that SonarMark Provides Required Functionality + paths: + - "docs/reqstream/ots/sonarmark.yaml" # OTS requirements + - "docs/verification/ots/sonarmark.md" # OTS verification + + - id: OTS-VersionMark + title: Review that VersionMark Provides Required Functionality + paths: + - "docs/reqstream/ots/versionmark.yaml" # OTS requirements + - "docs/verification/ots/versionmark.md" # OTS verification + + - id: OTS-WeasyPrint + title: Review that WeasyPrint Provides Required Functionality + paths: + - "docs/reqstream/ots/weasyprint.yaml" # OTS requirements + - "docs/verification/ots/weasyprint.md" # OTS verification diff --git a/.yamllint.yaml b/.yamllint.yaml index 4fbc811..79c3aee 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -15,13 +15,14 @@ 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) diff --git a/AGENTS.md b/AGENTS.md index 6cec799..1a0aa72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,11 @@ +# Project Overview + +- **name**: ReqStream +- **description**: A .NET command-line tool for managing software requirements in YAML format, + providing validation, linting, traceability, and test-mapping capabilities +- **languages**: C# +- **technologies**: .NET 8/9/10, YamlDotNet, MSBuild, NuGet + # Project Structure ```text @@ -10,7 +18,8 @@ │ ├── requirements_doc/ │ ├── requirements_report/ │ ├── reqstream/ -│ └── user_guide/ +│ ├── user_guide/ +│ └── verification/ ├── src/ │ └── DemaConsulting.ReqStream/ └── test/ @@ -45,16 +54,17 @@ 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` | -| Review configuration | `software-items.md`, `reviewmark-usage.md` | -| Any documentation | `technical-documentation.md` | +| 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` | Load only the standards relevant to your specific task scope. @@ -69,26 +79,10 @@ Delegate to specialized agents only for specific scenarios: - **Formal feature implementation** (complex, multi-step) → Call the implementation agent - **Formal bug resolution** (complex debugging, systematic fixes) → Call the implementation agent - **Formal reviews** (compliance verification, detailed analysis) → Call the formal-review agent -- **Template consistency** (downstream repository alignment) → Call the repo-consistency agent - -## Available Specialized Agents - -- **lint-fix** - Pre-PR lint sweep agent that loops running `pwsh ./lint.ps1`, - fixing issues until the repository is lint-clean -- **developer** - General-purpose software development agent that applies appropriate - standards based on the work being performed -- **formal-review** - Agent for performing formal reviews using standardized review processes -- **implementation** - Orchestrator agent that manages quality implementations - through a formal state machine workflow -- **quality** - Quality assurance agent that grades developer work against project - standards and Continuous Compliance practices -- **repo-consistency** - Ensures downstream repositories remain consistent with - the TemplateDotNetTool template patterns and best practices # Agent Reporting (Specialized Agents Must Follow) -Specialized agents (lint-fix, developer, quality, implementation, -formal-review, repo-consistency) MUST generate a completion report: +Specialized agents MUST generate a completion report: 1. Save to `.agent-logs/{agent-name}-{subject}-{unique-id}.md` where `{subject}` is a kebab-case task summary (max 5 words) and @@ -107,7 +101,7 @@ Result semantics for orchestrator decision-making: # Formatting (After Making Changes) After making changes, run the auto-fix pass. This applies all available fixers -silently and **always exits 0** — agents do not need to respond to its output. +silently and **always exits 0** - agents do not need to respond to its output. ```pwsh pwsh ./fix.ps1 @@ -115,7 +109,7 @@ pwsh ./fix.ps1 This automatically handles: `dotnet format`, markdown formatting, and YAML formatting. Full lint compliance is a **pre-PR responsibility**, not an agent -responsibility — invoke the lint-fix agent once before submitting a pull request. +responsibility - invoke the lint-fix agent once before submitting a pull request. ## CI Quality Tools @@ -124,6 +118,8 @@ reqstream, versionmark, and reviewmark. # Scope Discipline (ALL Agents Must Follow) +- **No generated file access**: Files inside any `generated/` folder are build + outputs - do not read, lint, or modify them - **Minimum necessary changes**: Only modify files directly required by the task - **No speculative refactoring**: Do not refactor code adjacent to the change unless the task explicitly requests it diff --git a/README.md b/README.md index 133fb75..072e3df 100644 --- a/README.md +++ b/README.md @@ -571,7 +571,7 @@ For information about reporting security vulnerabilities, please see our [Securi [nuget-shield]: https://img.shields.io/nuget/v/DemaConsulting.ReqStream [nuget-url]: https://www.nuget.org/packages/DemaConsulting.ReqStream [license]: https://github.com/demaconsulting/ReqStream/blob/main/LICENSE -[architecture]: docs/design/introduction.md +[architecture]: https://github.com/demaconsulting/ReqStream/blob/main/docs/design/introduction.md [contributing]: https://github.com/demaconsulting/ReqStream/blob/main/CONTRIBUTING.md [code-of-conduct]: https://github.com/demaconsulting/ReqStream/blob/main/CODE_OF_CONDUCT.md [security]: https://github.com/demaconsulting/ReqStream/blob/main/SECURITY.md diff --git a/docs/build_notes/definition.yaml b/docs/build_notes/definition.yaml index ff158c9..708f7fb 100644 --- a/docs/build_notes/definition.yaml +++ b/docs/build_notes/definition.yaml @@ -6,8 +6,8 @@ resource-path: input-files: - docs/build_notes/title.txt - docs/build_notes/introduction.md - - docs/build_notes.md - - docs/build_notes/versions.md + - docs/build_notes/generated/build_notes.md + - docs/build_notes/generated/versions.md template: template.html table-of-contents: true diff --git a/docs/code_quality/definition.yaml b/docs/code_quality/definition.yaml index 68c58f2..fed5f02 100644 --- a/docs/code_quality/definition.yaml +++ b/docs/code_quality/definition.yaml @@ -5,8 +5,8 @@ resource-path: input-files: - docs/code_quality/title.txt - docs/code_quality/introduction.md - - docs/code_quality/codeql-quality.md - - docs/code_quality/sonar-quality.md + - docs/code_quality/generated/codeql-quality.md + - docs/code_quality/generated/sonar-quality.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/code_review_plan/definition.yaml b/docs/code_review_plan/definition.yaml index 3a24f0b..56989bf 100644 --- a/docs/code_review_plan/definition.yaml +++ b/docs/code_review_plan/definition.yaml @@ -5,7 +5,7 @@ resource-path: input-files: - docs/code_review_plan/title.txt - docs/code_review_plan/introduction.md - - docs/code_review_plan/plan.md + - docs/code_review_plan/generated/plan.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/code_review_report/definition.yaml b/docs/code_review_report/definition.yaml index 6498e6c..b238d43 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/report.md + - docs/code_review_report/generated/report.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/design/definition.yaml b/docs/design/definition.yaml index 2495529..29bcd02 100644 --- a/docs/design/definition.yaml +++ b/docs/design/definition.yaml @@ -11,15 +11,20 @@ resource-path: input-files: - docs/design/title.txt - docs/design/introduction.md - - docs/design/reqstream/reqstream.md + - docs/design/reqstream.md - docs/design/reqstream/program.md - - docs/design/reqstream/cli/cli.md + - docs/design/reqstream/cli.md - docs/design/reqstream/cli/context.md - - docs/design/reqstream/modeling/modeling.md + - docs/design/reqstream/modeling.md + - docs/design/reqstream/modeling/lint-issue.md + - docs/design/reqstream/modeling/load-result.md + - docs/design/reqstream/modeling/requirement.md - docs/design/reqstream/modeling/requirements.md - - docs/design/reqstream/tracing/tracing.md + - docs/design/reqstream/modeling/requirements-loader.md + - docs/design/reqstream/modeling/section.md + - docs/design/reqstream/tracing.md - docs/design/reqstream/tracing/trace-matrix.md - - docs/design/reqstream/self-test/self-test.md + - docs/design/reqstream/self-test.md - docs/design/reqstream/self-test/validation.md template: template.html diff --git a/docs/design/introduction.md b/docs/design/introduction.md index ed3d6b7..6880c61 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -65,25 +65,25 @@ breakdown above: ```text docs/design/ ├── introduction.md — document introduction and architecture overview +├── reqstream.md — system integration design └── reqstream/ - ├── reqstream.md — system integration design ├── program.md — Program unit design + ├── cli.md — Cli subsystem design ├── cli/ - │ ├── cli.md — Cli subsystem design │ └── context.md — Context unit design + ├── modeling.md — Modeling subsystem design ├── modeling/ - │ ├── modeling.md — Modeling subsystem design │ ├── lint-issue.md — LintIssue unit design │ ├── load-result.md — LoadResult unit design │ ├── requirement.md — Requirement unit design │ ├── requirements-loader.md — RequirementsLoader unit design │ ├── requirements.md — Requirements unit design │ └── section.md — Section unit design + ├── tracing.md — Tracing subsystem design ├── tracing/ - │ ├── tracing.md — Tracing subsystem design │ └── trace-matrix.md — TraceMatrix unit design + ├── self-test.md — SelfTest subsystem design └── self-test/ - ├── self-test.md — SelfTest subsystem design └── validation.md — Validation unit design ``` @@ -129,6 +129,7 @@ enabling reviewers and auditors to navigate from any one artifact to all related Each software item has parallel artifacts organized as follows: - Requirements: docs/reqstream/reqstream/.../{item}.yaml (kebab-case) - Design docs: docs/design/reqstream/.../{item}.md (kebab-case) +- Verification: docs/verification/reqstream/.../{item}.md (kebab-case) - Source code: src/DemaConsulting.ReqStream/.../{Item}.cs (PascalCase) - Tests: test/DemaConsulting.ReqStream.Tests/.../{Item}Tests.cs (PascalCase) - Review-sets: defined in .reviewmark.yaml @@ -140,5 +141,11 @@ For example, the `Requirements` unit maps to: | -------- | ---- | | Requirements | `docs/reqstream/reqstream/modeling/requirements.yaml` | | Design | `docs/design/reqstream/modeling/requirements.md` | +| Verification | `docs/verification/reqstream/modeling/requirements.md` | | Source | `src/DemaConsulting.ReqStream/Modeling/Requirements.cs` | | Tests | `test/DemaConsulting.ReqStream.Tests/Modeling/ModelingTests.cs` | + +## References + +- ReqStream System Requirements document +- ReqStream Requirements Root document diff --git a/docs/design/reqstream/reqstream.md b/docs/design/reqstream.md similarity index 95% rename from docs/design/reqstream/reqstream.md rename to docs/design/reqstream.md index 5c24bb9..efbca8e 100644 --- a/docs/design/reqstream/reqstream.md +++ b/docs/design/reqstream.md @@ -53,6 +53,12 @@ non-validate, non-lint) invocation: `TraceMatrix.GetUnsatisfiedRequirements`. Tag filtering is therefore applied transparently at each operation in steps 3, 5, and 6 rather than as a separate pipeline stage; only requirements carrying at least one matching tag are included in reports and enforcement. +8. **Report depth** — the `--depth` flag sets a default heading level for all reports. + Per-report overrides (`--report-depth`, `--matrix-depth`, `--justifications-depth`) take + precedence over the default. When generating reports in steps 3 and 5, + `Context.ReportDepth`, `Context.MatrixDepth`, and `Context.JustificationsDepth` are passed + as the `depth` parameter to `Requirements.Export`, `Requirements.ExportJustifications`, and + `TraceMatrix.Export`. This satisfies `ReqStream-System-ReportDepth`. ## Source-Specific Test Matching diff --git a/docs/design/reqstream/cli/cli.md b/docs/design/reqstream/cli.md similarity index 97% rename from docs/design/reqstream/cli/cli.md rename to docs/design/reqstream/cli.md index a61808d..f4134c4 100644 --- a/docs/design/reqstream/cli/cli.md +++ b/docs/design/reqstream/cli.md @@ -1,17 +1,17 @@ -# Cli Subsystem Design +## Cli Subsystem Design The `Cli` subsystem provides the command-line interface for ReqStream. It is responsible for accepting user input from the command line and routing output to the console and an optional log file. -## Overview +### Overview The `Cli` subsystem acts as the primary boundary between the user's shell invocation and the tool's internal logic. It owns argument parsing, output formatting, and error tracking. All other subsystems receive a `Context` object from the `Cli` subsystem to read parsed flags and write output. -## Units +### Units The `Cli` subsystem contains the following software unit: @@ -19,7 +19,7 @@ The `Cli` subsystem contains the following software unit: |-------------------------------|------------------|---------------------------------------------------------------| | `Context` | `Cli/Context.cs` | Argument parsing, output channels, and exit code. | -## Interfaces +### Interfaces The `Cli` subsystem exposes the following interface to the rest of the tool: @@ -47,13 +47,13 @@ The `Cli` subsystem exposes the following interface to the rest of the tool: - **`Context.ExitCode`** — Returns 0 for success or 1 when errors have been reported. - **`Context.Dispose`** — Closes the log file writer and releases resources. -## Interactions +### Interactions The `Cli` subsystem has no dependencies on other tool subsystems. It uses only .NET base class library types. The `Program` unit at system level creates the `Context` and passes it to all subsystems that need to produce output. -## Error Handling +### Error Handling `Context.Create` throws `ArgumentException` under the following conditions: @@ -63,7 +63,7 @@ to all subsystems that need to produce output. or `--justifications-depth` value is not a positive integer. - **Log file open failure** — The file path provided to `--log` cannot be opened for writing. -## Depth Inheritance +### Depth Inheritance `Context.ReportDepth`, `Context.MatrixDepth`, and `Context.JustificationsDepth` all default to `Context.Depth` when not individually overridden by `--report-depth`, `--matrix-depth`, or diff --git a/docs/design/reqstream/cli/context.md b/docs/design/reqstream/cli/context.md index 94a9403..62d6790 100644 --- a/docs/design/reqstream/cli/context.md +++ b/docs/design/reqstream/cli/context.md @@ -1,6 +1,6 @@ -# Context Unit Design +### Context Unit Design -## Overview +#### Overview `Context` is the command-line argument parser and I/O owner for ReqStream. It is the single authoritative source for all runtime options and is the only unit permitted to write to the console @@ -10,21 +10,21 @@ sole concerns are parsing arguments and surfacing results to the caller. `Context` implements `IDisposable` so that the log-file `StreamWriter` is closed deterministically when the enclosing `using` block in `Program.Main` exits. -## Private State +#### Private State - **`_logWriter`** (`StreamWriter?`, `--log`) — Open writer for the optional log file; `null` when no log file was requested. - **`_hasErrors`** (`bool`) — Accumulates error state; initially `false`; set to `true` by `WriteError`. -## Properties +#### Properties | Property | Type | CLI flag | Notes | | -------- | ---- | -------- | ----- | -| `Version` | `bool` | `--version` | Print version and exit | -| `Help` | `bool` | `--help` | Print usage and exit | +| `Version` | `bool` | `--version` / `-v` | Print version and exit | +| `Help` | `bool` | `--help` / `-?` / `-h` | Print usage and exit | | `Silent` | `bool` | `--silent` | Suppress console output | | `Validate` | `bool` | `--validate` | Run self-validation tests | | `Lint` | `bool` | `--lint` | Lint requirements files | -| `ResultsFile` | `string?` | `--results` | Path for validation test-results output file | +| `ResultsFile` | `string?` | `--results` / `--result` | Path for validation test-results output file | | `Enforce` | `bool` | `--enforce` | Fail if requirements are not fully covered | | `FilterTags` | `HashSet?` | `--filter` | Comma-separated tag filter; `null` when not specified | | `RequirementsFiles` | `List` | `--requirements` | Expanded list of requirement file paths | @@ -38,9 +38,9 @@ when the enclosing `using` block in `Program.Main` exits. | `JustificationsDepth` | `int` | `--justifications-depth` | Justifications report heading depth; defaults to `Depth` | | `ExitCode` | `int` | — | Computed: `_hasErrors ? 1 : 0` | -## Methods +#### Methods -### `Create(args)` +##### `Create(args)` `Create` is the static factory method that constructs and returns a fully initialized `Context`. It implements a sequential switch-based parser over the `args` array. Each recognized flag sets the @@ -61,7 +61,7 @@ receives a user-actionable error message rather than an unhandled exception. (`--report-depth`, `--matrix-depth`, `--justifications-depth`) override this default if specified; otherwise each report inherits the value of `Depth`. -### `ExpandGlobPattern(pattern)` +##### `ExpandGlobPattern(pattern)` `ExpandGlobPattern` resolves a single pattern (which may contain `*` or `**` wildcards) to a list of absolute file paths using `Microsoft.Extensions.FileSystemGlobbing.Matcher` against the current @@ -71,24 +71,24 @@ working directory. paths. Callers that pass absolute paths directly will receive an empty result set. This is an accepted limitation of the underlying library; users should use relative paths or glob wildcards. -### `WriteLine(message)` +##### `WriteLine(message)` `WriteLine` writes a message to the console (unless `Silent` is `true`) and to `_logWriter` if a log file is open. -### `WriteError(message)` +##### `WriteError(message)` `WriteError` sets `_hasErrors = true`, writes the message to `Console.Error` in red (unless `Silent` is `true`), and also writes it to `_logWriter` if a log file is open. Setting `_hasErrors` ensures that `ExitCode` returns `1` after any error is reported. -### `Dispose()` +##### `Dispose()` `Dispose` flushes and closes `_logWriter` if it is not `null`, then sets it to `null`. This ensures the log file is not truncated and file handles are not leaked even when the process exits via an early return path. -## Interactions with Other Units +#### Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | diff --git a/docs/design/reqstream/modeling/modeling.md b/docs/design/reqstream/modeling.md similarity index 90% rename from docs/design/reqstream/modeling/modeling.md rename to docs/design/reqstream/modeling.md index eb0b7f2..d1c2ed0 100644 --- a/docs/design/reqstream/modeling/modeling.md +++ b/docs/design/reqstream/modeling.md @@ -1,16 +1,16 @@ -# Modeling Subsystem Design +## Modeling Subsystem Design The `Modeling` subsystem provides the data model and YAML parsing for ReqStream requirements documents. It is responsible for reading, validating, and structuring requirement data for use by the tracing, reporting, and enforcement subsystems. -## Overview +### Overview The `Modeling` subsystem handles all YAML file parsing and requirement data structures. It reads one or more requirement YAML files (including those referenced via `includes`), merges them into a unified requirement tree, and exposes that tree to the rest of the tool. -## Units +### Units The `Modeling` subsystem contains the following software units: @@ -22,7 +22,7 @@ The `Modeling` subsystem contains the following software units: for individual requirements files. - **`Section`** (`Modeling/Section.cs`) — Named group of requirements within a requirements document. -## Interfaces +### Interfaces The `Modeling` subsystem exposes the following interface to the rest of the tool: @@ -32,17 +32,16 @@ The `Modeling` subsystem exposes the following interface to the rest of the tool | `Requirements.Export` | Exports to Markdown; `depth` sets header level (default 1); `filterTags` restricts by tag. | | `Requirements.ExportJustifications` | Exports justifications to Markdown. Supports `depth` and `filterTags`. | | `LoadResult.ReportIssues` | Reports lint issues discovered during loading via the context. | -| `RequirementsLoader.Load` | Deserializes a single requirements file and collects lint issues. | -## Interactions +### Interactions | Dependency | Direction | Purpose | |------------------------------------|-----------|---------------------------------------------------------------------| -| `Context` | Uses | Receives file paths from `Context.RequirementsFiles`. | +| `Cli (Context)` | Uses | `LoadResult.ReportIssues` accepts a `Context` to route issues. | | `TraceMatrix` | Used by | Receives the requirement tree to map test results to requirements. | | `Program` | Used by | Calls `Requirements.Load` to load requirements. | -## Operation +### Operation A call to `Requirements.Load(paths)` follows this sequence: @@ -55,7 +54,7 @@ A call to `Requirements.Load(paths)` follows this sequence: 4. **Result assembly** — if any Error-level lint issue is present, `Requirements` is set to `null` in the returned `LoadResult`; otherwise it contains the populated `Requirements` tree. -## Lint Check Categories +### Lint Check Categories `RequirementsLoader` performs structural validation during loading. For the complete list of lint check categories and checked conditions, see the RequirementsLoader unit design documentation. @@ -63,15 +62,15 @@ lint check categories and checked conditions, see the RequirementsLoader unit de No Warning-level issues are emitted by the current implementation; all detected conditions are Error-level and cause `LoadResult.Requirements` to be `null`. -## Error Handling +### Error Handling -### Severity Classification +#### Severity Classification All lint issues emitted by the Modeling subsystem carry `LintSeverity.Error`. There are no Warning-level conditions in the current implementation. Any single Error causes `LoadResult` to return `null` for `Requirements`. -### Include Loop Guard and LoadResult Contract +#### Include Loop Guard and LoadResult Contract For the include-loop deduplication strategy, see the RequirementsLoader unit design documentation. For the `LoadResult` null-on-error contract, see the LoadResult unit design documentation. diff --git a/docs/design/reqstream/modeling/lint-issue.md b/docs/design/reqstream/modeling/lint-issue.md index c686b9c..24d310f 100644 --- a/docs/design/reqstream/modeling/lint-issue.md +++ b/docs/design/reqstream/modeling/lint-issue.md @@ -1,15 +1,15 @@ -# LintIssue Unit Design +### LintIssue Unit Design -## Overview +#### Overview `LintIssue` and its companion enum `LintSeverity` represent a single structural issue discovered during requirements loading or linting. They are simple value types with no dependencies on other units; they carry the three pieces of information needed to display and route a lint diagnostic: where the issue occurred, how severe it is, and what the problem is. -## Data Model +#### Data Model -### `LintSeverity` +##### `LintSeverity` `LintSeverity` is an enum that classifies the severity of a lint issue. @@ -18,7 +18,7 @@ where the issue occurred, how severe it is, and what the problem is. | `Warning` | A non-fatal issue; processing can continue. | | `Error` | A fatal issue that prevents successful requirements loading. | -### `LintIssue` +##### `LintIssue` `LintIssue` represents a single issue found during requirements linting or loading. @@ -30,7 +30,7 @@ where the issue occurred, how severe it is, and what the problem is. | `Description` | `string` | A human-readable description of the issue | | `ToString()` | `string` | Returns the issue formatted as `"location: severity: description"` | -## Formatting +#### Formatting `ToString()` returns the issue in the standard diagnostic format: @@ -48,7 +48,7 @@ The `LintSeverity` enum values map to the following lowercase strings in `ToStri This format is recognized by editors and CI tools that can parse file locations and navigate to the line containing the issue. -## Interactions with Other Units +#### Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | diff --git a/docs/design/reqstream/modeling/load-result.md b/docs/design/reqstream/modeling/load-result.md index a7473c3..2c9ce59 100644 --- a/docs/design/reqstream/modeling/load-result.md +++ b/docs/design/reqstream/modeling/load-result.md @@ -1,6 +1,6 @@ -# LoadResult Unit Design +### LoadResult Unit Design -## Overview +#### Overview `LoadResult` encapsulates the combined outcome of a `Requirements.Load` call. It holds the parsed `Requirements` tree (or `null` if error-level issues prevented successful loading) and @@ -8,7 +8,7 @@ the complete list of `LintIssue` objects collected during the load. By combining single return value, `LoadResult` ensures that the requirements tree and the lint issues are always consistent with each other and can be inspected by the caller in any order. -## Properties +#### Properties | Member | Type | Notes | | ------ | ---- | ----- | @@ -16,9 +16,9 @@ always consistent with each other and can be inspected by the caller in any orde | `Issues` | `IReadOnlyList` | All lint issues collected during loading | | `HasErrors` | `bool` | `true` when any issue has `LintSeverity.Error` | -## Methods +#### Methods -### `ReportIssues(context)` +##### `ReportIssues(context)` `ReportIssues` routes each `LintIssue` in `Issues` to the appropriate output channel of the supplied `Context`: @@ -29,14 +29,14 @@ supplied `Context`: This method exists to decouple `LoadResult` from knowledge of how issues are displayed; it delegates all formatting and routing decisions to the `Context` unit. -## Construction +#### Construction `LoadResult` has an `internal` constructor called only by `RequirementsLoader.Load`. The constructor accepts the `Requirements?` tree and the collected `IReadOnlyList`. `HasErrors` is a computed property that evaluates `Issues.Any(i => i.Severity == LintSeverity.Error)` on each access. -## Interactions with Other Units +#### Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | diff --git a/docs/design/reqstream/modeling/requirement.md b/docs/design/reqstream/modeling/requirement.md index c0f2682..4c653cf 100644 --- a/docs/design/reqstream/modeling/requirement.md +++ b/docs/design/reqstream/modeling/requirement.md @@ -1,12 +1,12 @@ -# Requirement Unit Design +### Requirement Unit Design -## Overview +#### Overview `Requirement` is the domain model for a single requirement entry. It is a simple mutable data-transfer object with no business logic; its fields are populated by `RequirementsLoader` during YAML DOM traversal and consumed by `Requirements`, `TraceMatrix`, and the export methods. -## Properties +#### Properties | Property | Type | YAML key | Notes | | -------- | ---- | -------- | ----- | @@ -18,7 +18,7 @@ during YAML DOM traversal and consumed by `Requirements`, `TraceMatrix`, and the | `Tags` | `List` | `tags` | Optional labels for filtering and export | | `Location` | `string?` | — | Source path and line/column where the requirement is defined | -## Constraints +#### Constraints - `Id` must be unique across all files loaded in a single `Requirements.Load` call. Duplicates are detected and reported by `RequirementsLoader`. @@ -30,7 +30,7 @@ during YAML DOM traversal and consumed by `Requirements`, `TraceMatrix`, and the to empty `List` instances. `Justification` defaults to `null`. `Location` defaults to `null`. No property is left uninitialized; callers can safely iterate lists without null checks. -## Interactions with Other Units +#### Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | diff --git a/docs/design/reqstream/modeling/requirements-loader.md b/docs/design/reqstream/modeling/requirements-loader.md index cc04b04..c767fa5 100644 --- a/docs/design/reqstream/modeling/requirements-loader.md +++ b/docs/design/reqstream/modeling/requirements-loader.md @@ -1,14 +1,15 @@ -# RequirementsLoader Unit Design +### RequirementsLoader Unit Design -## Overview +#### Overview `RequirementsLoader` is the YAML deserializer and structural lint validator for requirements files. It walks the YAML DOM, merges sections into the shared `Requirements` tree, validates all required fields, and collects `LintIssue` objects for every problem found. It is the only unit that reads from the file system for requirements data and the only unit with knowledge of -the YAML DOM representation. +the YAML DOM representation. `RequirementsLoader` is declared `internal static`; it has no +instances and is inaccessible outside the assembly. -## YAML DOM Traversal +#### YAML DOM Traversal YAML is parsed using `YamlDotNet`'s `RepresentationModel` (DOM) API. `RequirementsLoader` reads the raw YAML text into a `YamlStream`, then walks the resulting node tree directly: @@ -25,7 +26,7 @@ structural level (`KnownDocumentFields`, `KnownSectionFields`, `KnownRequirement classes; each DOM node is consumed directly and converted to the long-lived `Requirement`, `Section`, and `Requirements` objects during the walk. -## Shared State +#### Shared State `RequirementsLoader.Load` allocates and shares the following state across all files loaded in one call: @@ -38,9 +39,9 @@ one call: | `visitedFiles` | `HashSet` | Fully-resolved paths of already-processed files (include-loop guard) | | `issues` | `List` | All issues collected during the load | -## Methods +#### Methods -### `Load(paths)` +##### `Load(paths)` `Load` initializes shared state, calls `LoadFile` for each path in `paths`, then calls `ValidateCycles` and assembles the `LoadResult`. If any error-level issue was collected, @@ -50,7 +51,7 @@ one call: loaded (e.g. all files were empty or contained only `---`), cycle detection is skipped entirely because there are no nodes to traverse. -### `LoadFile(path)` +##### `LoadFile(path)` `LoadFile` loads a single YAML file and merges its content into the shared `Requirements` tree. Four design points govern its behavior: @@ -70,7 +71,7 @@ Four design points govern its behavior: - **Recursive includes**: each path in the document's `includes` block is resolved relative to the current file's directory and passed to `LoadFile` recursively. -### `ValidateCycles()` +##### `ValidateCycles()` `ValidateCycles` performs a depth-first search (DFS) over all requirements to detect circular child references. It is called once after all files are loaded. @@ -96,7 +97,7 @@ child references. It is called once after all files are loaded. Because `ValidateCycles` runs before any downstream analysis, `TraceMatrix.CollectAllTests` can recurse through child requirements without its own cycle guard. -## Lint Check Categories +#### Lint Check Categories `RequirementsLoader` checks the following categories of structural issues, all reported as Error-level: @@ -113,7 +114,7 @@ Error-level: - **Mapping rules** — Mapping entry is not a mapping node; missing or blank mapping `id`; unknown field in mapping. - **Graph rules** — Unknown child requirement reference; circular `children` reference (DFS cycle detection). -## Validation Error Table +#### Validation Error Table | Check | Condition | Error text | | ----- | --------- | ---------- | @@ -124,7 +125,7 @@ Error-level: | Test name | Blank entry in `tests` list | `Test name cannot be blank` | | Mapping ID | Blank | `Mapping 'id' cannot be blank` | -## Interactions with Other Units +#### Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | diff --git a/docs/design/reqstream/modeling/requirements.md b/docs/design/reqstream/modeling/requirements.md index b093b61..84d367e 100644 --- a/docs/design/reqstream/modeling/requirements.md +++ b/docs/design/reqstream/modeling/requirements.md @@ -1,6 +1,6 @@ -# Requirements Unit Design +### Requirements Unit Design -## Overview +#### Overview `Requirements` is the root of the requirements section tree and the public API entry point for the Modeling subsystem. It extends `Section` to inherit the container properties (title, @@ -11,18 +11,19 @@ requirements list, child sections list) and adds the `Load` static factory metho entirely to `RequirementsLoader`. Its role is to provide the public surface through which callers load and export requirements data. -## Factory Method +#### Factory Method -### `Load(paths)` +##### `Load(paths)` `Load` is the single static factory method. It accepts one or more file paths, delegates to `RequirementsLoader.Load`, and returns the resulting `LoadResult` containing the populated `Requirements` tree (or `null` on error) and the complete list of `LintIssue` objects. +Throws `ArgumentException` when no paths are provided. Callers that need to abort on errors check `result.HasErrors` or `result.Requirements == null`. Callers that need to surface issues to the user call `result.ReportIssues(context)`. -## Export Methods +#### Export Methods | Method | Output | Notes | | ------ | ------ | ----- | @@ -51,7 +52,7 @@ catch or suppress I/O exceptions. - Each requirement with a non-null `Justification` produces a sub-heading and the justification text. -## Interactions with Other Units +#### Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | diff --git a/docs/design/reqstream/modeling/section.md b/docs/design/reqstream/modeling/section.md index d625cfc..2a18ef1 100644 --- a/docs/design/reqstream/modeling/section.md +++ b/docs/design/reqstream/modeling/section.md @@ -1,6 +1,6 @@ -# Section Unit Design +### Section Unit Design -## Overview +#### Overview `Section` is the container node in the requirements tree. It groups a set of `Requirement` objects under a common title and optionally nests child `Section` objects to represent @@ -10,7 +10,7 @@ validation logic resides in `RequirementsLoader`. `Requirements` extends `Section` to serve as the root of the tree, inheriting its container properties without adding additional state. -## Properties +#### Properties | Property | Type | YAML key | Default | Notes | | -------- | ---- | -------- | ------- | ----- | @@ -18,7 +18,7 @@ properties without adding additional state. | `Requirements` | `List` | `requirements` | `[]` | Requirements directly in this section | | `Sections` | `List
` | `sections` | `[]` | Child sections | -## Section Merging +#### Section Merging When `RequirementsLoader` encounters a section whose `Title` matches an existing section under the same parent, it reuses the existing `Section` object rather than creating a new one. This @@ -26,11 +26,11 @@ same-title merge strategy is the design decision that enables modular requiremen multiple YAML files can contribute requirements to the same logical section without requiring a single monolithic file. -## Error Handling +#### Error Handling Section contains no executable logic; all validation errors are produced by `RequirementsLoader`. -## Interactions with Other Units +#### Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | diff --git a/docs/design/reqstream/program.md b/docs/design/reqstream/program.md index 09050df..0b441ed 100644 --- a/docs/design/reqstream/program.md +++ b/docs/design/reqstream/program.md @@ -1,15 +1,15 @@ -# Program Unit Design +## Program Unit Design -## Overview +### Overview `Program` is the entry point of the ReqStream executable. It owns the top-level execution flow, dispatches to the appropriate subsystem based on the parsed command-line options, and establishes the error-handling boundary for the entire process. All meaningful work is delegated to `Context`, `Validation`, `Requirements`, and `TraceMatrix`; `Program` itself contains no domain logic. -## Properties +### Properties -### `Version` +#### `Version` `Version` is a static read-only property backed by the private `_version` field, which is initialized once at class startup to avoid repeated reflection on every access. @@ -27,9 +27,9 @@ labels and build metadata. If the attribute is absent or empty the numeric `Asse string is used. If neither is available the string `"Unknown"` is returned so that the property never throws and never returns `null`. -## Methods +### Methods -### `Main(args)` +#### `Main(args)` `Main` is the process entry point. Its responsibilities are: @@ -50,7 +50,7 @@ never throws and never returns `null`. its message is sufficient for diagnosis. All other exceptions are re-thrown so the operating system or process supervisor captures the full stack trace for unexpected failures. -### `Run(context)` +#### `Run(context)` `Run` implements the priority-ordered dispatch shown in the table below. Return steps exit immediately; the banner step (row 2) prints the banner and then falls through to the next step. @@ -68,7 +68,7 @@ immediately; the banner step (row 2) prints the banner and then falls through to With priorities 5 and 6 the `Run` method enters the `if (context.Lint)` block. Priority 5 guards the early-exit path for the no-files case; priority 6 is the normal lint path that follows. -### `PrintBanner` +#### `PrintBanner` `PrintBanner` writes three lines to `context`: the tool name with version string, the copyright notice, and a blank line. It is called at priority step 2 for all invocations except version @@ -76,12 +76,12 @@ queries and lint runs, so that every non-trivial invocation identifies the runni The banner is suppressed during lint to keep output clean for lint script integration — only actionable issue lines are emitted. -### `PrintHelp` +#### `PrintHelp` `PrintHelp` writes the full option listing to `context`. It documents every supported flag and argument, grouped logically. It is only called when `--help` is present. -### `ProcessRequirements` +#### `ProcessRequirements` `ProcessRequirements` orchestrates the normal (non-version, non-help, non-validate, non-lint) run. If `context.RequirementsFiles` is empty, it writes "No requirements files specified." and returns @@ -95,7 +95,7 @@ If `--enforce` is active, `EnforceRequirementsCoverage` is called last so that a generated even when coverage fails. All export methods respect `context.FilterTags` for tag-filtered output. -### `EnforceRequirementsCoverage` +#### `EnforceRequirementsCoverage` `EnforceRequirementsCoverage` evaluates whether all requirements are covered by passing tests. If no `TraceMatrix` was built (i.e., no `--tests` argument was provided), it reports an error @@ -108,7 +108,7 @@ requirement IDs and reports each one via `context.WriteError`. This method never throws; all failure signalling goes through `context.WriteError`, which sets the internal error flag and eventually produces a non-zero exit code. -## Interactions with Other Units +### Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | diff --git a/docs/design/reqstream/self-test/self-test.md b/docs/design/reqstream/self-test.md similarity index 95% rename from docs/design/reqstream/self-test/self-test.md rename to docs/design/reqstream/self-test.md index 387e9bd..578986f 100644 --- a/docs/design/reqstream/self-test/self-test.md +++ b/docs/design/reqstream/self-test.md @@ -1,16 +1,16 @@ -# SelfTest Subsystem Design +## SelfTest Subsystem Design The `SelfTest` subsystem provides the self-validation framework for ReqStream. It runs a built-in suite of tests to demonstrate the tool is functioning correctly in the deployment environment. -## Overview +### Overview The `SelfTest` subsystem is invoked when the user passes `--validate` on the command line. It exercises the tool's own capabilities and reports a pass/fail summary. It can also write test results to a file in TRX or JUnit XML format for integration with CI/CD pipelines. -## Units +### Units The `SelfTest` subsystem contains the following software unit: @@ -18,7 +18,7 @@ The `SelfTest` subsystem contains the following software unit: |--------------|--------------------------|----------------------------------------------------| | `Validation` | `SelfTest/Validation.cs` | Orchestrating and executing self-validation tests. | -## Interfaces +### Interfaces The `SelfTest` subsystem exposes the following interface to the rest of the tool: @@ -26,14 +26,14 @@ The `SelfTest` subsystem exposes the following interface to the rest of the tool |------------------|-----------------------------------------------------------------------| | `Validation.Run` | Runs all self-validation tests, prints a summary, and writes results. | -## Interactions +### Interactions | Dependency | Direction | Purpose | |------------|-----------|--------------------------------------------------------------| | `Context` | Uses | Output channel for header lines, test summaries, and errors. | | `Program` | Uses | `Program.Run` is called internally to exercise the tool. | -## Error Handling +### Error Handling The `SelfTest` subsystem handles the following error conditions: diff --git a/docs/design/reqstream/self-test/validation.md b/docs/design/reqstream/self-test/validation.md index d66bae1..a97572d 100644 --- a/docs/design/reqstream/self-test/validation.md +++ b/docs/design/reqstream/self-test/validation.md @@ -1,6 +1,6 @@ -# Validation Unit Design +### Validation Unit Design -## Overview +#### Overview `Validation` is the self-validation test runner for ReqStream. Its purpose is to execute a suite of end-to-end tests that verify the tool's own behavior and to produce structured test-result @@ -9,9 +9,9 @@ tool's own requirements — enabling a self-hosting compliance workflow. All tests run in temporary directories to avoid side effects and are isolated from one another. -## Methods +#### Methods -### `Run(context)` +##### `Run(context)` `Run` is the single public entry point. It prints a header block to `context` containing the tool version, machine name, operating system, .NET runtime version, and current UTC timestamp. It then @@ -45,7 +45,7 @@ Each test runs in a dedicated `TemporaryDirectory` with `DirectorySwitch` active files, invokes the relevant workflow, asserts expected outcomes, and returns a `TestResult` with outcome `Passed` or `Failed`. -### `WriteResultsFile(context, testResults)` +##### `WriteResultsFile(context, testResults)` `WriteResultsFile` serializes the collected `TestResult` list to a structured file. @@ -60,16 +60,16 @@ outcome `Passed` or `Failed`. The serializer is called with the assembled `TestResults` object, returning a serialized string. The string is then written to the resolved output path via `File.WriteAllText`. -## Supporting Types +#### Supporting Types -### `TemporaryDirectory` (nested helper class) +##### `TemporaryDirectory` (nested helper class) `TemporaryDirectory` is an `IDisposable` helper that creates a uniquely named directory under `Path.GetTempPath()` on construction and deletes it recursively on disposal. It exists to give each validation test a clean, isolated file-system workspace that is guaranteed to be removed after the test completes, regardless of whether the test passes or fails. -### `DirectorySwitch` (nested helper class) +##### `DirectorySwitch` (nested helper class) `DirectorySwitch` is an `IDisposable` helper that changes the process working directory to a supplied path on construction and restores the original directory on disposal. It exists because @@ -81,14 +81,14 @@ Each test uses both classes together: `TemporaryDirectory` owns the directory li guarantees that each test starts with a clean file system state and that no test artifacts persist after the test completes, regardless of whether the test passes or fails. -## Dependencies +#### Dependencies | Library / Type | Role | | -------------- | ---- | | `DemaConsulting.TestResults` | `TestResults`, `TestResult`, `TestOutcome` model types | | `DemaConsulting.TestResults.IO.Serializer` | TRX and JUnit file serialization | -## Interactions with Other Units +#### Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | diff --git a/docs/design/reqstream/tracing/tracing.md b/docs/design/reqstream/tracing.md similarity index 96% rename from docs/design/reqstream/tracing/tracing.md rename to docs/design/reqstream/tracing.md index 5e1fddf..f93ebd3 100644 --- a/docs/design/reqstream/tracing/tracing.md +++ b/docs/design/reqstream/tracing.md @@ -1,16 +1,16 @@ -# Tracing Subsystem Design +## Tracing Subsystem Design The `Tracing` subsystem provides test result loading and requirement-to-test traceability for ReqStream. It maps test execution evidence to requirements and supports enforcement of full test coverage. -## Overview +### Overview The `Tracing` subsystem reads test result files in TRX or JUnit XML format, correlates each test result with the requirements that reference it, and produces a trace matrix report or coverage enforcement decision. -## Units +### Units The `Tracing` subsystem contains the following software unit: @@ -18,7 +18,7 @@ The `Tracing` subsystem contains the following software unit: |---------------|-------------------------|----------------------------------------------------------------------| | `TraceMatrix` | `Tracing/TraceMatrix.cs`| Test result loading, requirement mapping, and coverage enforcement. | -## Interfaces +### Interfaces The `Tracing` subsystem exposes the following interface to the rest of the tool: @@ -31,7 +31,7 @@ The `Tracing` subsystem exposes the following interface to the rest of the tool: | `TraceMatrix.GetTestResult` | Returns pass/fail counts for a named test across results. | | `TraceMatrix.GetAllTestResults` | Returns pass/fail `TestMetrics` for all tests referenced in requirements. | -## Interactions +### Interactions | Dependency | Direction | Purpose | |----------------|-----------|------------------------------------------------------------------------| @@ -39,7 +39,7 @@ The `Tracing` subsystem exposes the following interface to the rest of the tool: | `Requirements` | Uses | Receives the requirement tree to map tests to requirements. | | `Program` | Used by | Constructs `TraceMatrix` and calls enforcement/export methods. | -## Error Handling +### Error Handling The `Tracing` subsystem raises the following exceptions at the subsystem boundary. Both exceptions are thrown by the `TraceMatrix` constructor and propagate to `Program` for diff --git a/docs/design/reqstream/tracing/trace-matrix.md b/docs/design/reqstream/tracing/trace-matrix.md index ef87178..f1cf038 100644 --- a/docs/design/reqstream/tracing/trace-matrix.md +++ b/docs/design/reqstream/tracing/trace-matrix.md @@ -1,15 +1,15 @@ -# TraceMatrix Unit Design +### TraceMatrix Unit Design -## Overview +#### Overview `TraceMatrix` maps test execution results to requirements and calculates requirement-coverage metrics. It consumes an already-validated `Requirements` tree and a list of test-result file paths, then provides lookup and satisfaction-analysis methods used by `Program` to generate reports and enforce coverage. -## Supporting Value Types +#### Supporting Value Types -### `TestMetrics` +##### `TestMetrics` `TestMetrics` is an immutable record that aggregates pass/fail counts for a single named test across all loaded result files. @@ -24,7 +24,7 @@ across all loaded result files. `GetTestResult` returns `TestMetrics(0, 0)` when the test name has no recorded executions, so callers always receive a valid object. -### `TestExecution` +##### `TestExecution` `TestExecution` is an immutable record that holds the results for one test name from one result file. @@ -35,23 +35,23 @@ result file. | `Name` | `string` | Test name as it appears in the result file | | `Metrics` | `TestMetrics` | Aggregated pass/fail counts for this test in this file | -## Private State +#### Private State | Field | Type | Purpose | | ----- | ---- | ------- | | `_testExecutions` | `Dictionary>` | Maps test names to lists of `TestExecution` entries | | `_requirements` | `Requirements` | The validated requirement tree; held for iteration in analysis methods | -## Construction +#### Construction -### `TraceMatrix(requirements, testResultFiles)` +##### `TraceMatrix(requirements, testResultFiles)` The constructor stores the `Requirements` tree for later iteration and calls `ProcessTestResultFile` for each path in `testResultFiles` to populate `_testExecutions`. After construction, `_testExecutions` contains every unique test name seen, each mapped to one `TestExecution` record per result file that contained that test name. -### `ProcessTestResultFile(filePath)` +##### `ProcessTestResultFile(filePath)` `ProcessTestResultFile` reads one test-result file, auto-detects its format (TRX or JUnit) via `DemaConsulting.TestResults.IO.Serializer.Deserialize`, and adds a `TestExecution` record to @@ -59,9 +59,9 @@ construction, `_testExecutions` contains every unique test name seen, each mappe in an `InvalidOperationException` that includes `filePath` — this ensures the caller can identify the offending file without inspecting nested exception detail. -## Methods +#### Methods -### `GetTestResult(testName)` +##### `GetTestResult(testName)` `GetTestResult` returns aggregated `TestMetrics` for a named test. When `testName` contains a `'@'` separator (not at position 0 or end), it applies source-specific filtering: the part before @@ -75,7 +75,7 @@ files. If the test name is not found in `_testExecutions`, the method returns `T ensuring callers always receive a valid object. See the Test Name Format Summary table below for a quick reference of both formats. -### `GetAllTestResults()` +##### `GetAllTestResults()` `GetAllTestResults` returns a read-only dictionary mapping each test name (referenced by any requirement in the tree) to its aggregated `TestMetrics`. Only tests that have been executed at @@ -85,21 +85,21 @@ called by `Export` or `ExportTesting`: the Testing section is built by calling unexecuted tests showing `0 / 0` counts. `GetAllTestResults` is available for callers that want an executed-only summary without generating a full report. -### `GetUnsatisfiedRequirements(filterTags)` +##### `GetUnsatisfiedRequirements(filterTags)` `GetUnsatisfiedRequirements` returns a list of requirement IDs that are not satisfied (subject to `filterTags` filtering). A requirement is unsatisfied if it has no tests or if any of its tests have not been executed or have failed. This is the inverse of `IsRequirementSatisfied` applied across all requirements in the tree. -### `CalculateSatisfiedRequirements(filterTags)` +##### `CalculateSatisfiedRequirements(filterTags)` `CalculateSatisfiedRequirements` iterates every requirement in the tree (subject to `filterTags` filtering) and returns a `(satisfied, total)` tuple. It calls `IsRequirementSatisfied` for each requirement to determine whether all associated tests have passed. This provides `Program` with the counts needed to report coverage status and determine whether `--enforce` should fail. -### `CollectAllTests(requirement, rootSection, allTests)` +##### `CollectAllTests(requirement, rootSection, allTests)` `CollectAllTests` returns the union of all test names associated with a requirement and its entire descendant subtree. Child requirements inherit their parent's coverage obligations, so a @@ -107,14 +107,15 @@ requirement is only considered covered when all tests across its whole subtree p `RequirementsLoader.ValidateCycles()` has already confirmed the child graph is acyclic, this method recurses without a cycle guard. -### `IsRequirementSatisfied(requirement)` +##### `IsRequirementSatisfied(requirement, rootSection)` `IsRequirementSatisfied` returns `true` if and only if the requirement has at least one test mapped (directly or via descendants) and every one of those tests has `AllPassed == true`. A requirement with no tests is never satisfied, enforcing the design expectation that every -requirement must be traced to at least one passing test. +requirement must be traced to at least one passing test. The `rootSection` parameter provides +the root section for requirement lookup during satisfaction checking. -### `Export(filePath, depth, filterTags)` +##### `Export(filePath, depth, filterTags)` `Export` writes the trace matrix to a Markdown file at `filePath`. The output has three sections, written in this order by three helper methods: `ExportSummary`, `ExportRequirements`, and @@ -141,7 +142,7 @@ while the Summary reflects full-subtree satisfaction. **Parameter behavior**: -- `filePath`: required; an `ArgumentException` is thrown when `filePath` is null or empty. +- `filePath`: required; an `ArgumentException` is thrown when `filePath` is null, empty, or whitespace-only. On file-system write failure (for example, permission denied or an invalid path), the underlying `IOException` or `UnauthorizedAccessException` is propagated to the caller without wrapping. @@ -154,14 +155,14 @@ while the Summary reflects full-subtree satisfaction. not appear in the Testing table. Defaults to `null`. - `rootSection`: `_requirements` is used internally as the root to iterate the requirement tree. -## Test Name Format Summary +#### Test Name Format Summary | Format | Example | Matching rule | | ------ | ------- | ------------- | | Plain | `TestFeature_Valid_Passes` | Aggregates across all result files | | Source-specific | `ubuntu@TestFeature_Valid_Passes` | Restricted to files whose base name contains `ubuntu` | -## Interactions with Other Units +#### Interactions with Other Units - **`Program`** — Constructs `TraceMatrix`; calls `CalculateSatisfiedRequirements`, `GetUnsatisfiedRequirements`, and `Export`. - **`Requirements`** — Provides the requirement tree; iterated during analysis. diff --git a/docs/reqstream/ots/mstest.yaml b/docs/reqstream/ots/mstest.yaml deleted file mode 100644 index f931c0d..0000000 --- a/docs/reqstream/ots/mstest.yaml +++ /dev/null @@ -1,27 +0,0 @@ ---- -# MSTest OTS Software Requirements -# -# Requirements for the MSTest testing framework functionality. - -sections: - - title: OTS Software Requirements - sections: - - title: MSTest Requirements - requirements: - - id: ReqStream-OTS-MSTest - title: MSTest shall execute unit tests and report results. - justification: | - MSTest (MSTest.TestFramework and MSTest.TestAdapter) is the unit-testing framework used - by the project. It discovers and runs all test methods and writes TRX result files that - feed into coverage reporting and requirements traceability. Passing tests confirm the - framework is functioning correctly. - tags: [ots] - tests: - - Context_Create_NoArguments_ReturnsDefaultContext - - Context_Create_VersionFlag_SetsVersionProperty - - Context_Create_HelpFlags_SetsHelpProperty - - Context_Create_ValidateFlag_SetsValidateProperty - - Context_Create_EnforceFlag_SetsEnforceProperty - - Section_Load_SimpleRequirement_ParsesCorrectly - - Program_Run_WithVersionFlag_PrintsVersion - - Program_Run_WithHelpFlag_PrintsHelp diff --git a/docs/reqstream/ots/pandoc.yaml b/docs/reqstream/ots/pandoc.yaml index a4f9c6f..fe72f4b 100644 --- a/docs/reqstream/ots/pandoc.yaml +++ b/docs/reqstream/ots/pandoc.yaml @@ -24,3 +24,4 @@ sections: - Pandoc_ReviewReportHtml - Pandoc_DesignHtml - Pandoc_UserGuideHtml + - Pandoc_VerificationHtml diff --git a/docs/reqstream/ots/testresults.yaml b/docs/reqstream/ots/testresults.yaml new file mode 100644 index 0000000..4c4c07b --- /dev/null +++ b/docs/reqstream/ots/testresults.yaml @@ -0,0 +1,20 @@ +--- +# DemaConsulting.TestResults OTS Software Requirements +# +# Requirements for the DemaConsulting.TestResults test result reading library. + +sections: + - title: OTS Software Requirements + sections: + - title: DemaConsulting.TestResults Requirements + requirements: + - id: ReqStream-OTS-TestResults + title: DemaConsulting.TestResults shall read TRX and JUnit XML test result files. + justification: | + DemaConsulting.TestResults is the library used to read test result files in TRX + (MSTest) and JUnit XML formats. It must correctly parse test execution records + so that ReqStream can map test results to requirements for coverage analysis. + tags: [ots] + tests: + - TraceMatrix_Constructor_WithTrxFile_ParsesCorrectly + - TraceMatrix_Constructor_WithJUnitFile_ParsesCorrectly diff --git a/docs/reqstream/ots/weasyprint.yaml b/docs/reqstream/ots/weasyprint.yaml index 65c2277..64d7f57 100644 --- a/docs/reqstream/ots/weasyprint.yaml +++ b/docs/reqstream/ots/weasyprint.yaml @@ -24,3 +24,4 @@ sections: - WeasyPrint_ReviewReportPdf - WeasyPrint_DesignPdf - WeasyPrint_UserGuidePdf + - WeasyPrint_VerificationPdf diff --git a/docs/reqstream/ots/xunit.yaml b/docs/reqstream/ots/xunit.yaml new file mode 100644 index 0000000..31344a9 --- /dev/null +++ b/docs/reqstream/ots/xunit.yaml @@ -0,0 +1,26 @@ +--- +# xUnit OTS Software Requirements +# +# Requirements for the xUnit testing framework functionality. + +sections: + - title: OTS Software Requirements + sections: + - title: xUnit Requirements + requirements: + - id: ReqStream-OTS-XUnit + title: xUnit shall execute unit tests and report results. + justification: | + xUnit (xunit.v3 and xunit.runner.visualstudio) is the unit-testing framework used + by the project. It discovers and runs all test methods. Passing tests confirm the + framework is functioning correctly. + tags: [ots] + tests: + - Context_Create_NoArguments_ReturnsDefaultContext + - Context_Create_VersionFlag_SetsVersionProperty + - Context_Create_HelpFlags_SetsHelpProperty + - Section_Load_SimpleRequirement_ParsesCorrectly + - Requirement_Properties_DefaultValues + - TraceMatrix_Constructor_WithNoFiles_CreatesEmptyMatrix + - Program_Run_WithVersionFlag_PrintsVersion + - Validation_Run_WithSilentContext_CompletesSuccessfully diff --git a/docs/reqstream/ots/yamldotnet.yaml b/docs/reqstream/ots/yamldotnet.yaml new file mode 100644 index 0000000..b6565ed --- /dev/null +++ b/docs/reqstream/ots/yamldotnet.yaml @@ -0,0 +1,23 @@ +--- +# YamlDotNet OTS Software Requirements +# +# Requirements for the YamlDotNet YAML parsing library. + +sections: + - title: OTS Software Requirements + sections: + - title: YamlDotNet Requirements + requirements: + - id: ReqStream-OTS-YamlDotNet + title: YamlDotNet shall parse YAML requirements files into a structured data model. + justification: | + YamlDotNet is the YAML parsing library used to deserialize requirements files. + It must correctly parse well-formed YAML and report deserialization errors with + location information for malformed files. + tags: [ots] + tests: + - Requirements_Load_ValidFile_ReturnsRequirementsAndNoIssues + - Requirements_Load_InvalidYamlContent_ReportsErrorWithFileLocation + - Requirements_Load_MalformedYaml_ReturnsNullAndIssues + - Section_Load_SimpleRequirement_ParsesCorrectly + - Requirements_Load_ComplexStructure_ParsesCorrectly diff --git a/docs/reqstream/reqstream/reqstream.yaml b/docs/reqstream/reqstream.yaml similarity index 87% rename from docs/reqstream/reqstream/reqstream.yaml rename to docs/reqstream/reqstream.yaml index 90763d5..f0db3ab 100644 --- a/docs/reqstream/reqstream/reqstream.yaml +++ b/docs/reqstream/reqstream.yaml @@ -1,4 +1,12 @@ --- +includes: + - reqstream/cli.yaml + - reqstream/modeling.yaml + - reqstream/tracing.yaml + - reqstream/self-test.yaml + - reqstream/program.yaml + - reqstream/platform-requirements.yaml + sections: - title: ReqStream Requirements sections: @@ -93,6 +101,8 @@ sections: - ReqStream-Modeling-Linting - ReqStream-Program-LintVerbosity - ReqStream-Program-LintNoFiles + - ReqStream-Program-LintFailure + - ReqStream-Program-LintNoBanner - id: ReqStream-System-Validate title: >- @@ -106,6 +116,7 @@ sections: children: - ReqStream-Program-Validate - ReqStream-SelfTest-Qualification + - ReqStream-SelfTest-FailureReporting - id: ReqStream-System-ValidateResultsOutput title: >- @@ -233,3 +244,29 @@ sections: - ReqStream_System_FileIncludes_RequirementsWithIncludes_LoadsAllRequirements children: - ReqStream-Requirements-Includes + + - id: ReqStream-System-SectionMerging + title: >- + The system shall automatically merge sections with the same hierarchy path + when loading requirements from multiple YAML files. + justification: | + Title-based section merging enables modular requirements management without any + explicit namespace or import declaration. Multiple files can contribute to the same + logical section, allowing requirements to be organized by feature, component, or + responsibility while still producing one coherent requirement tree. + tests: + - ReqStream_System_FileIncludes_RequirementsWithIncludes_LoadsAllRequirements + children: + - ReqStream-Requirements-SectionMerging + - ReqStream-Section-TitleMerging + + - id: ReqStream-System-CircularIncludeDetection + title: >- + The system shall detect and report circular include references in requirements + files. + justification: | + Circular includes (A includes B, B includes A) would cause infinite loading loops. + Detecting and reporting them prevents hangs and provides users with a clear error + message identifying the problematic file. + children: + - ReqStream-Lint-CircularReferences diff --git a/docs/reqstream/reqstream/cli/cli.yaml b/docs/reqstream/reqstream/cli.yaml similarity index 96% rename from docs/reqstream/reqstream/cli/cli.yaml rename to docs/reqstream/reqstream/cli.yaml index 2b768ae..1fc322d 100644 --- a/docs/reqstream/reqstream/cli/cli.yaml +++ b/docs/reqstream/reqstream/cli.yaml @@ -7,7 +7,7 @@ # - Subsystem requirements describe the externally visible command-line interface behavior includes: - - context.yaml + - cli/context.yaml sections: - title: Cli Subsystem Requirements @@ -80,6 +80,10 @@ sections: misconfiguration and helps users quickly identify and correct command-line errors. tests: - Cli_Interface_MissingArgumentValue_ThrowsArgumentException + children: + - ReqStream-Command-MissingLogValue + - ReqStream-Command-MissingResultsValue + - ReqStream-Command-MissingFilterValue - id: ReqStream-Cli-InvalidDepthValue title: The Cli subsystem shall reject a non-integer depth value with a descriptive error. diff --git a/docs/reqstream/reqstream/cli/context.yaml b/docs/reqstream/reqstream/cli/context.yaml index cf04e57..b7cf671 100644 --- a/docs/reqstream/reqstream/cli/context.yaml +++ b/docs/reqstream/reqstream/cli/context.yaml @@ -18,8 +18,9 @@ sections: - id: ReqStream-Command-Version title: The tool shall accept a `--version` flag. justification: | - Recording the version flag in Context allows Program to detect the request and print - version information without requiring Context to have any knowledge of the version string. + Users need a standard way to verify which version of the tool is installed without + running a full operation. A dedicated version flag follows CLI conventions and enables + scripts and CI pipelines to confirm the expected version before proceeding. tags: - cli tests: @@ -28,8 +29,9 @@ sections: - id: ReqStream-Command-Help title: The tool shall accept a `--help` flag. justification: | - Recording the help flag in Context allows Program to detect the request and print - usage documentation without coupling help content to the argument-parsing layer. + Users need a standard way to discover available options and usage without consulting + external documentation. A built-in help flag follows CLI conventions and provides + immediate, context-accurate guidance at the point of use. tags: - cli tests: @@ -68,16 +70,34 @@ sections: tests: - Context_Create_UnsupportedArgument_ThrowsException - - id: ReqStream-Command-MalformedArgs - title: The tool shall reject malformed command-line arguments with a descriptive error. + - id: ReqStream-Command-MissingLogValue + title: The tool shall reject a --log flag that is not followed by a filename. justification: | - Providing clear feedback when a flag that requires a value is missing its value - helps users quickly correct mistakes and prevents silent misconfiguration. + Providing clear feedback when --log is missing its value prevents silent + misconfiguration and helps users quickly correct the command line. tags: - cli tests: - Context_Create_MissingLogFilename_ThrowsException + + - id: ReqStream-Command-MissingResultsValue + title: The tool shall reject a --results flag that is not followed by a filename. + justification: | + Providing clear feedback when --results is missing its value prevents silent + misconfiguration and helps users quickly correct the command line. + tags: + - cli + tests: - Context_Create_MissingResultsFilename_ThrowsException + + - id: ReqStream-Command-MissingFilterValue + title: The tool shall reject a --filter flag that is not followed by a value. + justification: | + Providing clear feedback when --filter is missing its value prevents silent + misconfiguration and helps users quickly correct the command line. + tags: + - cli + tests: - Context_Create_FilterArgumentMissingValue_ThrowsException - id: ReqStream-Command-RequirementsGlobPatterns diff --git a/docs/reqstream/reqstream/modeling/modeling.yaml b/docs/reqstream/reqstream/modeling.yaml similarity index 84% rename from docs/reqstream/reqstream/modeling/modeling.yaml rename to docs/reqstream/reqstream/modeling.yaml index 7d06296..ef1cc24 100644 --- a/docs/reqstream/reqstream/modeling/modeling.yaml +++ b/docs/reqstream/reqstream/modeling.yaml @@ -8,12 +8,12 @@ # - Subsystem requirements describe the YAML parsing and data model behavior includes: - - lint-issue.yaml - - load-result.yaml - - requirement.yaml - - requirements.yaml - - requirements-loader.yaml - - section.yaml + - modeling/lint-issue.yaml + - modeling/load-result.yaml + - modeling/requirement.yaml + - modeling/requirements.yaml + - modeling/requirements-loader.yaml + - modeling/section.yaml sections: - title: Modeling Subsystem Requirements @@ -24,6 +24,8 @@ sections: YAML-based requirement files enable human-readable and version-control-friendly requirements management. The Modeling subsystem must correctly parse these files into a structured data model for use by the tracing and reporting subsystems. + tags: + - modeling tests: - Modeling_YamlParsing_ValidFile_LoadsRequirements - Modeling_YamlParsing_ValidFile_ReturnsNoLintIssues @@ -35,6 +37,9 @@ sections: - ReqStream-Requirements-Hierarchy - ReqStream-Section-Container - ReqStream-Section-Nesting + - ReqStream-Requirements-Tags + - ReqStream-Requirements-BlankTagName + - ReqStream-Requirements-Justification - id: ReqStream-Modeling-Export title: The Modeling subsystem shall generate Markdown reports from requirements data. @@ -42,11 +47,15 @@ sections: Generating Markdown reports from requirements data enables documentation to be produced automatically from the requirements files, reducing manual effort and ensuring consistency. + tags: + - modeling + - reporting tests: - Modeling_Export_Requirements_GeneratesMarkdownFile - Modeling_Export_Justifications_GeneratesMarkdownFile children: - ReqStream-Requirements-ParentChild + - ReqStream-Requirements-BlankChildId - ReqStream-Requirements-TestMappings - ReqStream-Report-MarkdownExport - ReqStream-Report-HeaderDepth @@ -62,8 +71,11 @@ sections: Modular requirements management requires the subsystem to resolve `includes` references recursively, merging all referenced files into a single requirements tree. This enables teams to split requirements across files and directories without losing traceability. + tags: + - modeling tests: - Modeling_YamlParsing_ValidFile_LoadsRequirements + - Modeling_MultiFileLoading_WithIncludes_LoadsRequirementsFromAllFiles children: - ReqStream-Requirements-Includes - ReqStream-Requirements-SectionMerging @@ -74,6 +86,9 @@ sections: Structural validation during loading catches malformed YAML, missing mandatory fields, unknown fields, duplicate IDs, and other problems early. Reporting all issues in a single pass allows users to fix all problems at once rather than one at a time. + tags: + - modeling + - lint tests: - Modeling_YamlParsing_DuplicateIds_DetectsLintError - Modeling_Linting_MalformedYaml_DetectsError @@ -107,6 +122,12 @@ sections: - ReqStream-Modeling-LintingValidation - ReqStream-Modeling-LintingReporting - ReqStream-LoadResult-ReportIssues + - ReqStream-Lint-InvalidFilePath + - ReqStream-Lint-FileNotFound + - ReqStream-Lint-IoReadFailure + - ReqStream-Lint-NonMappingRoot + - ReqStream-Lint-SeverityString + - ReqStream-LoadResult-HasErrors - id: ReqStream-Modeling-LintingValidation title: The Modeling subsystem shall validate requirements file structure. @@ -114,6 +135,9 @@ sections: Structural validation during loading catches malformed YAML, missing mandatory fields, unknown fields, duplicate IDs, and other problems early, allowing users to fix all problems before running reports or tracing. + tags: + - modeling + - lint tests: - Modeling_Linting_MalformedYaml_DetectsError - Modeling_Linting_ValidFile_ReturnsNoIssues @@ -123,5 +147,9 @@ sections: justification: | Reporting all issues in a single pass allows users to fix all problems at once rather than one at a time, improving developer productivity and CI turnaround. + tags: + - modeling + - lint tests: - Modeling_YamlParsing_DuplicateIds_DetectsLintError + - Modeling_LintingReporting_MultipleConditions_ReportsAllIssues diff --git a/docs/reqstream/reqstream/modeling/requirement.yaml b/docs/reqstream/reqstream/modeling/requirement.yaml index 9131f7c..957fb22 100644 --- a/docs/reqstream/reqstream/modeling/requirement.yaml +++ b/docs/reqstream/reqstream/modeling/requirement.yaml @@ -32,29 +32,43 @@ sections: - Requirements_Load_BlankRequirementTitle_ReportsErrorWithFileLocation - id: ReqStream-Requirements-ParentChild - title: >- - Requirement shall support parent-child relationships to other requirements, - and blank child IDs shall be rejected. + title: Requirement shall support parent-child relationships to other requirements. justification: | - Parent-child relationships enable hierarchical requirement decomposition, allowing high-level - requirements to be satisfied through lower-level requirements. Rejecting blank child IDs - prevents silent failures in cycle detection and trace matrix generation. + Parent-child relationships enable hierarchical requirement decomposition, allowing + high-level requirements to be satisfied through lower-level requirements. tags: - requirements tests: - Requirements_Load_RequirementWithChildren_ParsesChildrenCorrectly + + - id: ReqStream-Requirements-BlankChildId + title: The tool shall reject blank child IDs in requirement definitions. + justification: | + Rejecting blank child IDs prevents silent failures in cycle detection and + trace matrix generation, providing clear error feedback to authors. + tags: + - requirements + tests: - Requirements_Load_BlankChildIdInRequirement_ReportsErrorWithFileLocation - id: ReqStream-Requirements-Tags - title: >- - Requirement shall support optional tags, and blank tag names shall be rejected. + title: Requirement shall support an optional list of tags for categorization. justification: | - Tags allow requirements to be categorized and filtered for targeted exports. Rejecting blank - tag names prevents silent failures in tag-based filtering and export operations. + Tags enable filtering requirements by category (e.g. "cli", "lint", "reporting"), + allowing targeted trace matrix exports and focused coverage analysis. tags: - requirements tests: - Requirements_Load_RequirementWithTags_ParsesTagsCorrectly + + - id: ReqStream-Requirements-BlankTagName + title: The tool shall reject blank tag names in requirement definitions. + justification: | + Blank tag names produce meaningless filter keys that silently match or exclude + requirements in unexpected ways. Rejecting them provides clear error feedback. + tags: + - requirements + tests: - Requirements_Load_BlankTagName_ReportsErrorWithFileLocation - id: ReqStream-Requirements-Justification diff --git a/docs/reqstream/reqstream/modeling/requirements-loader.yaml b/docs/reqstream/reqstream/modeling/requirements-loader.yaml index 769b43b..2207ef4 100644 --- a/docs/reqstream/reqstream/modeling/requirements-loader.yaml +++ b/docs/reqstream/reqstream/modeling/requirements-loader.yaml @@ -87,7 +87,7 @@ sections: - RequirementsLoader_Load_WithUnknownSectionField_ReportsError - id: ReqStream-Lint-MissingSectionTitle - title: The requirements loader shall report an error when a section is missing the required title field. + title: The requirements loader shall report an error when a section is missing or has a blank title field. justification: | The title field is mandatory for all sections. Missing it prevents the section from being correctly identified in reports and trace matrices. diff --git a/docs/reqstream/reqstream/modeling/requirements.yaml b/docs/reqstream/reqstream/modeling/requirements.yaml index df2b48b..f74a3d7 100644 --- a/docs/reqstream/reqstream/modeling/requirements.yaml +++ b/docs/reqstream/reqstream/modeling/requirements.yaml @@ -79,6 +79,8 @@ sections: tests: - Requirements_Load_IdenticalSections_MergesCorrectly - Requirements_Load_MultipleFilesWithSameSections_MergesSections + children: + - ReqStream-Section-TitleMerging - title: Reporting requirements: diff --git a/docs/reqstream/reqstream/program.yaml b/docs/reqstream/reqstream/program.yaml index 7c4cbfb..d5e72a7 100644 --- a/docs/reqstream/reqstream/program.yaml +++ b/docs/reqstream/reqstream/program.yaml @@ -74,16 +74,25 @@ sections: tests: - Program_Run_WithLintFlag_RunsLinter - - id: ReqStream-Program-LintVerbosity - title: The tool shall suppress the banner and produce no output when linting finds no issues. + - id: ReqStream-Program-LintNoBanner + title: The tool shall suppress the application banner when the lint flag is provided. justification: | - Suppressing the banner and the "No issues found" summary during lint allows lint scripts - to treat silence as success and only inspect reported issue lines, simplifying integration - into build pipelines and editors that parse tool output. + Suppressing the banner during lint allows lint scripts to treat silence as success + and only inspect reported issue lines, simplifying integration into build pipelines + and editors that parse tool output. tags: - cli tests: - Program_Run_WithLintFlag_SuppressesBanner + + - id: ReqStream-Program-LintVerbosity + title: The tool shall produce no output when linting is requested and no issues are found. + justification: | + Producing no output when linting finds no issues allows build pipelines and scripts + to treat silence as success without needing to parse a "No issues found" summary. + tags: + - cli + tests: - Program_Run_WithLintFlag_OnlyOutputsIssues - id: ReqStream-Program-LintFailure diff --git a/docs/reqstream/reqstream/self-test/self-test.yaml b/docs/reqstream/reqstream/self-test.yaml similarity index 94% rename from docs/reqstream/reqstream/self-test/self-test.yaml rename to docs/reqstream/reqstream/self-test.yaml index 9a1fdb4..08b48cc 100644 --- a/docs/reqstream/reqstream/self-test/self-test.yaml +++ b/docs/reqstream/reqstream/self-test.yaml @@ -7,13 +7,14 @@ # - Subsystem requirements describe the self-validation mechanism for tool qualification includes: - - validation.yaml + - self-test/validation.yaml sections: - title: SelfTest Subsystem Requirements requirements: - id: ReqStream-SelfTest-Qualification title: The tool shall provide a self-validation mechanism to qualify the tool in its deployment environment. + tags: [self-test] justification: | In regulated environments, tool qualification evidence is required to demonstrate that the tool functions correctly in its deployment environment before it is used @@ -25,9 +26,11 @@ sections: - SelfTest_Qualification_Run_PassesAllTests children: - ReqStream-Validation-SelfValidation + - ReqStream-Validation-NullContext - id: ReqStream-SelfTest-ResultsOutput title: The tool shall write self-validation results to a standard test result file when --results is provided. + tags: [self-test] justification: | CI/CD pipelines and requirements traceability tools (such as ReqStream itself) consume test result files in standard formats. By supporting both TRX (MSTest) and @@ -44,6 +47,7 @@ sections: - id: ReqStream-SelfTest-FailureReporting title: The tool shall report an error and set exit code 1 when self-validation encounters failures. + tags: [self-test] justification: | Automated test pipelines and CI/CD systems rely on non-zero exit codes to detect tool failures. Reporting each error via WriteError and exiting with code 1 ensures diff --git a/docs/reqstream/reqstream/tracing/tracing.yaml b/docs/reqstream/reqstream/tracing.yaml similarity index 90% rename from docs/reqstream/reqstream/tracing/tracing.yaml rename to docs/reqstream/reqstream/tracing.yaml index 46f3c9a..af096a2 100644 --- a/docs/reqstream/reqstream/tracing/tracing.yaml +++ b/docs/reqstream/reqstream/tracing.yaml @@ -7,7 +7,7 @@ # - Subsystem requirements describe the traceability and coverage enforcement behavior includes: - - trace-matrix.yaml + - tracing/trace-matrix.yaml sections: - title: Tracing Subsystem Requirements @@ -17,6 +17,8 @@ sections: justification: | Supporting both TRX and JUnit XML formats ensures the tool works with the major test frameworks used in .NET development, maximizing interoperability. + tags: + - testing tests: - Tracing_TestResults_TrxFile_LoadsTestResults - Tracing_TestResults_JUnitFile_LoadsTestResults @@ -31,6 +33,8 @@ sections: justification: | Mapping each test execution to the requirements it references enables traceability evidence that links implementation testing to specific software requirements. + tags: + - testing tests: - Tracing_Coverage_WithPassingTests_AllRequirementsSatisfied children: @@ -46,6 +50,8 @@ sections: justification: | Determining requirement coverage from mapped test results enables enforcement of full test coverage as a quality gate. + tags: + - testing tests: - Tracing_Coverage_WithPassingTests_AllRequirementsSatisfied - Tracing_Coverage_WithMissingTests_RequirementIsUnsatisfied @@ -57,16 +63,20 @@ sections: justification: | Raising a FileNotFoundException with the offending path allows callers to report a clear fatal error, preventing silent data loss from ignored missing files. + tags: + - testing tests: - - TraceMatrix_Constructor_NonExistentFile_ThrowsFileNotFoundException + - Tracing_FileLoading_NonExistentFile_ThrowsFileNotFoundException - id: ReqStream-Tracing-MalformedFile title: The Tracing subsystem shall raise InvalidOperationException when a test result file cannot be parsed. justification: | Raising an InvalidOperationException with the offending path and the original parse exception as the inner exception allows callers to provide actionable diagnostic output. + tags: + - testing tests: - - TraceMatrix_Constructor_MalformedFile_ThrowsInvalidOperationException + - Tracing_FileLoading_MalformedFile_ThrowsInvalidOperationException - id: ReqStream-Tracing-Reporting title: The Tracing subsystem shall export a trace matrix report to a Markdown file. @@ -74,6 +84,8 @@ sections: Exporting a trace matrix as a Markdown file provides human-readable coverage evidence that can be embedded in documentation, reviewed in pull requests, and consumed by reporting pipelines. + tags: + - reporting tests: - Tracing_Reporting_SimpleMatrix_CreatesMarkdownFile children: diff --git a/docs/requirements_doc/definition.yaml b/docs/requirements_doc/definition.yaml index 0f4ccd2..628b789 100644 --- a/docs/requirements_doc/definition.yaml +++ b/docs/requirements_doc/definition.yaml @@ -5,8 +5,8 @@ resource-path: input-files: - docs/requirements_doc/title.txt - docs/requirements_doc/introduction.md - - docs/requirements_doc/requirements.md - - docs/requirements_doc/justifications.md + - docs/requirements_doc/generated/requirements.md + - docs/requirements_doc/generated/justifications.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/requirements_report/definition.yaml b/docs/requirements_report/definition.yaml index 918a645..9ee62a4 100644 --- a/docs/requirements_report/definition.yaml +++ b/docs/requirements_report/definition.yaml @@ -5,7 +5,7 @@ resource-path: input-files: - docs/requirements_report/title.txt - docs/requirements_report/introduction.md - - docs/requirements_report/trace_matrix.md + - docs/requirements_report/generated/trace_matrix.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 70dd95e..cab6f4c 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -1455,6 +1455,13 @@ For more information, visit the [ReqStream GitHub repository][repo]. For support, please [open an issue][issues] or [start a discussion][discussions]. +## References + +- [Continuous Compliance](https://github.com/demaconsulting/ContinuousCompliance) — the + methodology that ReqStream is designed to support. +- [ReqStream GitHub Repository](https://github.com/demaconsulting/ReqStream) — source code, + issues, and releases. + [dotnet-sdk]: https://dotnet.microsoft.com/download [repo]: https://github.com/demaconsulting/ReqStream diff --git a/docs/verification/definition.yaml b/docs/verification/definition.yaml new file mode 100644 index 0000000..3bdc894 --- /dev/null +++ b/docs/verification/definition.yaml @@ -0,0 +1,44 @@ +--- +resource-path: + - docs/verification + - docs/verification/reqstream + - docs/verification/reqstream/cli + - docs/verification/reqstream/modeling + - docs/verification/reqstream/tracing + - docs/verification/reqstream/self-test + - docs/verification/ots + - docs/template + +input-files: + - docs/verification/title.txt + - docs/verification/introduction.md + - docs/verification/reqstream.md + - docs/verification/reqstream/program.md + - docs/verification/reqstream/cli.md + - docs/verification/reqstream/cli/context.md + - docs/verification/reqstream/modeling.md + - docs/verification/reqstream/modeling/lint-issue.md + - docs/verification/reqstream/modeling/load-result.md + - docs/verification/reqstream/modeling/requirement.md + - docs/verification/reqstream/modeling/requirements-loader.md + - docs/verification/reqstream/modeling/requirements.md + - docs/verification/reqstream/modeling/section.md + - docs/verification/reqstream/tracing.md + - docs/verification/reqstream/tracing/trace-matrix.md + - docs/verification/reqstream/self-test.md + - docs/verification/reqstream/self-test/validation.md + - docs/verification/ots.md + - docs/verification/ots/buildmark.md + - docs/verification/ots/fileassert.md + - docs/verification/ots/xunit.md + - docs/verification/ots/pandoc.md + - docs/verification/ots/reviewmark.md + - docs/verification/ots/sarifmark.md + - docs/verification/ots/sonarmark.md + - docs/verification/ots/versionmark.md + - docs/verification/ots/weasyprint.md + - docs/verification/ots/yamldotnet.md + - docs/verification/ots/testresults.md +template: template.html +table-of-contents: true +number-sections: true diff --git a/docs/verification/introduction.md b/docs/verification/introduction.md new file mode 100644 index 0000000..07ee48b --- /dev/null +++ b/docs/verification/introduction.md @@ -0,0 +1,76 @@ +# Introduction + +This document describes the verification design for ReqStream, a .NET command-line +application that processes YAML requirements files, traces test results to requirements, +generates Markdown reports, and enforces coverage in CI/CD pipelines. It establishes the +test approach for each software item and proves that every requirement is covered by at +least one named test scenario. + +## Purpose + +The purpose of this document is to prove that all requirements for the ReqStream system +are covered by named test scenarios. Each requirement at every level — system, subsystem, +and unit — is mapped to at least one test method so that reviewers can confirm completeness +without reading implementation code. + +## Scope + +This document covers verification of the following software units: + +- **Program** — entry point and execution orchestrator (`Program.cs`) +- **Cli** subsystem: + - **Context** unit — command-line argument parser and I/O owner (`Cli/Context.cs`) +- **Modeling** subsystem: + - **LintIssue** unit — lint severity classification and issue data model (`Modeling/LintIssue.cs`) + - **LoadResult** unit — combined result of loading requirements and associated lint issues (`Modeling/LoadResult.cs`) + - **Requirement** unit — single requirement with ID, title, and test links (`Modeling/Requirement.cs`) + - **Requirements** unit — parsed requirements document with section tree (`Modeling/Requirements.cs`) + - **RequirementsLoader** unit — YAML deserializer and lint validator (`Modeling/RequirementsLoader.cs`) + - **Section** unit — named group of requirements within a document (`Modeling/Section.cs`) +- **Tracing** subsystem: + - **TraceMatrix** unit — test result loader and requirement-coverage analyzer (`Tracing/TraceMatrix.cs`) +- **SelfTest** subsystem: + - **Validation** unit — self-validation test runner (`SelfTest/Validation.cs`) + +The following eleven OTS items are also verified: + +- **BuildMark** — build-notes documentation generator +- **FileAssert** — document assertion tool +- **xUnit** — unit testing framework +- **Pandoc** — Markdown to HTML converter +- **ReviewMark** — file review tracking tool +- **SarifMark** — SARIF report processor +- **SonarMark** — SonarCloud quality reporter +- **VersionMark** — version tracking tool +- **WeasyPrint** — HTML to PDF converter +- **YamlDotNet** — YAML parsing library +- **DemaConsulting.TestResults** — test result file reader + +The following topics are out of scope: + +- External library internals +- Build pipeline configuration +- Deployment and packaging + +## Companion Artifacts + +In-house software items have parallel artifacts organized as follows: + +- **Requirements**: `docs/reqstream/reqstream/.../{item}.yaml` (kebab-case) +- **Design**: `docs/design/reqstream/.../{item}.md` (kebab-case) +- **Verification**: `docs/verification/reqstream/.../{item}.md` (kebab-case, this document) +- **Source**: `src/DemaConsulting.ReqStream/.../{Item}.cs` (PascalCase) +- **Tests**: `test/DemaConsulting.ReqStream.Tests/.../{Item}Tests.cs` (PascalCase) + +OTS software items have no design documentation. Their artifacts are: + +- **Requirements**: `docs/reqstream/ots/{ots-name}.yaml` +- **Verification**: `docs/verification/ots/{ots-name}.md` + +Review-sets for all items are defined in `.reviewmark.yaml` at the repository root. + +## References + +- ReqStream System Requirements +- ReqStream Software Design Document +- ReqStream User Guide diff --git a/docs/verification/ots.md b/docs/verification/ots.md new file mode 100644 index 0000000..867d065 --- /dev/null +++ b/docs/verification/ots.md @@ -0,0 +1,44 @@ +# OTS Software Verification + +## Overview + +The ReqStream tool uses eleven OTS (Off-The-Shelf) software items to provide build, test, +documentation, and quality-reporting functionality. OTS items are not developed in-house and +have no design documentation. Verification evidence is collected from CI pipeline run results, +self-validation output, and integration test execution rather than from unit tests of internal +implementation. + +## Verification Approach + +Each OTS item is verified using one or more of the following evidence types: + +- **Self-validation**: The OTS tool is invoked with a `--validate` flag (where supported) on + the target platform. A zero exit code and expected console output confirm the tool is + operational. +- **CI pipeline step evidence**: The OTS tool runs as a named step in the GitHub Actions + pipeline. A successful pipeline run is proof the tool executed without error. +- **Integration test evidence**: The OTS tool is exercised indirectly by test methods that + depend on its correct operation. Passing tests confirm the tool delivered the expected results. + +Requirements for each OTS item are defined in the corresponding `docs/reqstream/ots/{name}.yaml` +file. Test evidence is recorded in the ReqStream requirements traceability matrix. + +## OTS Item Summary + +The following table lists all OTS items and their primary evidence type. Full verification +details for each item are provided in the individual OTS item verification documents under +`docs/verification/ots/`. + +| OTS Item | Primary Evidence Type | +| -------------------------- | ---------------------------------------------------------------------- | +| BuildMark | CI pipeline step evidence | +| FileAssert | Self-validation | +| xUnit | Integration test evidence | +| Pandoc | CI pipeline step evidence combined with FileAssert document validation | +| ReviewMark | CI pipeline step evidence | +| SarifMark | CI pipeline step evidence | +| SonarMark | CI pipeline step evidence | +| VersionMark | CI pipeline step evidence | +| WeasyPrint | CI pipeline step evidence combined with FileAssert document validation | +| YamlDotNet | Integration test evidence | +| DemaConsulting.TestResults | Integration test evidence | diff --git a/docs/verification/ots/buildmark.md b/docs/verification/ots/buildmark.md new file mode 100644 index 0000000..fe76f23 --- /dev/null +++ b/docs/verification/ots/buildmark.md @@ -0,0 +1,23 @@ +## BuildMark Verification + +### Required Functionality + +BuildMark (`ReqStream-OTS-BuildMark`) shall generate build-notes documentation from +GitHub Actions metadata. It queries the GitHub API to capture workflow run details and renders +them as a Markdown build-notes document included in the release artifacts. + +### Verification Approach + +BuildMark is verified by CI pipeline step evidence. The tool runs as a named step in the +GitHub Actions pipeline that produces the release artifacts. A successful pipeline run +demonstrates that BuildMark executed without error and produced the required output. + +The test evidence name `BuildMark_MarkdownReportGeneration` is linked to the pipeline step +that invokes BuildMark. A passing result confirms that the generated Markdown build-notes +document was produced. + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-BuildMark` | `BuildMark_MarkdownReportGeneration` | diff --git a/docs/verification/ots/fileassert.md b/docs/verification/ots/fileassert.md new file mode 100644 index 0000000..123169c --- /dev/null +++ b/docs/verification/ots/fileassert.md @@ -0,0 +1,25 @@ +## FileAssert Verification + +### Required Functionality + +FileAssert (`ReqStream-OTS-FileAssert`) shall validate generated documents against +acceptance criteria. It validates HTML and PDF documents produced during the build, asserting +that each document exists, has a non-trivial size, is structurally valid, and contains +expected content. It also provides verification evidence for Pandoc and WeasyPrint. + +### Verification Approach + +FileAssert is verified by CI pipeline step evidence. The tool's built-in `--validate` +command is executed in the CI pipeline and writes test method results to a TRX file. +The TRX file is consumed by ReqStream to satisfy the OTS requirement. + +Test evidence names (test methods written to the TRX file by `dotnet fileassert --validate`): + +- `FileAssert_VersionDisplay` — validates that FileAssert responds correctly to `--version` +- `FileAssert_HelpDisplay` — validates that FileAssert responds correctly to `--help` + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-FileAssert` | `FileAssert_VersionDisplay`, `FileAssert_HelpDisplay` | diff --git a/docs/verification/ots/pandoc.md b/docs/verification/ots/pandoc.md new file mode 100644 index 0000000..f0a56b8 --- /dev/null +++ b/docs/verification/ots/pandoc.md @@ -0,0 +1,39 @@ +## Pandoc Verification + +### Required Functionality + +Pandoc (`ReqStream-OTS-Pandoc`) shall convert Markdown documents to valid HTML. The +`DemaConsulting.PandocTool` wrapper converts Markdown source documents to HTML as part of the +documentation build pipeline. + +### Verification Approach + +Pandoc is verified by CI pipeline step evidence combined with FileAssert document validation. +Each Markdown document collection (build notes, code quality report, review plan, review +report, design document, user guide, requirements document, requirements report, and +verification document) is converted to HTML by Pandoc in the CI pipeline. FileAssert then asserts that each generated +HTML file exists, has a non-trivial size, contains a valid HTML title element, and includes +expected document content. Passing FileAssert assertions confirm Pandoc executed correctly +and produced meaningful output. + +Test evidence names: + +- `Pandoc_BuildNotesHtml` — build-notes HTML document validated +- `Pandoc_CodeQualityHtml` — code quality HTML document validated +- `Pandoc_ReviewPlanHtml` — review plan HTML document validated +- `Pandoc_ReviewReportHtml` — review report HTML document validated +- `Pandoc_DesignHtml` — design document HTML validated +- `Pandoc_UserGuideHtml` — user guide HTML document validated +- `Pandoc_VerificationHtml` — verification document HTML validated + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-Pandoc` | `Pandoc_BuildNotesHtml` | +| `ReqStream-OTS-Pandoc` | `Pandoc_CodeQualityHtml` | +| `ReqStream-OTS-Pandoc` | `Pandoc_ReviewPlanHtml` | +| `ReqStream-OTS-Pandoc` | `Pandoc_ReviewReportHtml` | +| `ReqStream-OTS-Pandoc` | `Pandoc_DesignHtml` | +| `ReqStream-OTS-Pandoc` | `Pandoc_UserGuideHtml` | +| `ReqStream-OTS-Pandoc` | `Pandoc_VerificationHtml` | diff --git a/docs/verification/ots/reviewmark.md b/docs/verification/ots/reviewmark.md new file mode 100644 index 0000000..6050f34 --- /dev/null +++ b/docs/verification/ots/reviewmark.md @@ -0,0 +1,25 @@ +## ReviewMark Verification + +### Required Functionality + +ReviewMark (`ReqStream-OTS-ReviewMark`) shall generate a review plan and review report +from the review configuration. The `DemaConsulting.ReviewMark` tool reads `.reviewmark.yaml` +and the review evidence store to produce a review plan and review report documenting file +review coverage and currency. + +### Verification Approach + +ReviewMark is verified by CI pipeline step evidence. The tool's built-in `--validate` +command is executed in the CI pipeline and writes test method results to a TRX file. +The TRX file is consumed by ReqStream to satisfy the OTS requirement. + +Test evidence names (test methods written to the TRX file by `dotnet reviewmark --validate`): + +- `ReviewMark_ReviewPlanGeneration` — validates that ReviewMark can generate a review plan +- `ReviewMark_ReviewReportGeneration` — validates that ReviewMark can generate a review report + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-ReviewMark` | `ReviewMark_ReviewPlanGeneration`, `ReviewMark_ReviewReportGeneration` | diff --git a/docs/verification/ots/sarifmark.md b/docs/verification/ots/sarifmark.md new file mode 100644 index 0000000..af68455 --- /dev/null +++ b/docs/verification/ots/sarifmark.md @@ -0,0 +1,25 @@ +## SarifMark Verification + +### Required Functionality + +SarifMark (`ReqStream-OTS-SarifMark`) shall convert CodeQL SARIF results into a +Markdown report. The `DemaConsulting.SarifMark` tool reads the SARIF output produced by +CodeQL code scanning and renders it as a human-readable Markdown document included in the +release artifacts. + +### Verification Approach + +SarifMark is verified by CI pipeline step evidence. The tool's built-in `--validate` +command is executed in the CI pipeline and writes test method results to a TRX file. +The TRX file is consumed by ReqStream to satisfy the OTS requirement. + +Test evidence names (test methods written to the TRX file by `dotnet sarifmark --validate`): + +- `SarifMark_SarifReading` — validates that SarifMark can read SARIF input +- `SarifMark_MarkdownReportGeneration` — validates that SarifMark can generate a Markdown report + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-SarifMark` | `SarifMark_SarifReading`, `SarifMark_MarkdownReportGeneration` | diff --git a/docs/verification/ots/sonarmark.md b/docs/verification/ots/sonarmark.md new file mode 100644 index 0000000..6895b67 --- /dev/null +++ b/docs/verification/ots/sonarmark.md @@ -0,0 +1,29 @@ +## SonarMark Verification + +### Required Functionality + +SonarMark (`ReqStream-OTS-SonarMark`) shall generate a SonarCloud quality report. The +`DemaConsulting.SonarMark` tool retrieves quality-gate and metrics data from SonarCloud and +renders it as a Markdown document included in the release artifacts. + +### Verification Approach + +SonarMark is verified by CI pipeline step evidence. The tool's built-in `--validate` +command is executed in the CI pipeline and writes test method results to a TRX file. +The TRX file is consumed by ReqStream to satisfy the OTS requirement. + +Test evidence names (test methods written to the TRX file by `dotnet sonarmark --validate`): + +- `SonarMark_QualityGateRetrieval` — validates that SonarMark can retrieve quality gate data +- `SonarMark_IssuesRetrieval` — validates that SonarMark can retrieve issue counts +- `SonarMark_HotSpotsRetrieval` — validates that SonarMark can retrieve hotspot data +- `SonarMark_MarkdownReportGeneration` — validates that SonarMark can generate a Markdown report + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-SonarMark` | `SonarMark_QualityGateRetrieval` | +| `ReqStream-OTS-SonarMark` | `SonarMark_IssuesRetrieval` | +| `ReqStream-OTS-SonarMark` | `SonarMark_HotSpotsRetrieval` | +| `ReqStream-OTS-SonarMark` | `SonarMark_MarkdownReportGeneration` | diff --git a/docs/verification/ots/testresults.md b/docs/verification/ots/testresults.md new file mode 100644 index 0000000..2ef22a9 --- /dev/null +++ b/docs/verification/ots/testresults.md @@ -0,0 +1,24 @@ +## DemaConsulting.TestResults Verification + +### Required Functionality + +DemaConsulting.TestResults (`ReqStream-OTS-TestResults`) shall read TRX and JUnit XML test +result files. DemaConsulting.TestResults is the library used to read test result files, +parsing test execution records so that ReqStream can map test results to requirements for +coverage analysis. + +### Verification Approach + +DemaConsulting.TestResults is verified by integration test evidence. The trace matrix +constructor tests exercise the library with TRX and JUnit XML inputs. Passing tests confirm +that the library correctly parses test execution records. The following representative test +methods are linked as evidence: + +- `TraceMatrix_Constructor_WithTrxFile_ParsesCorrectly` +- `TraceMatrix_Constructor_WithJUnitFile_ParsesCorrectly` + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-TestResults` | `TraceMatrix_Constructor_WithTrxFile_ParsesCorrectly`, `TraceMatrix_Constructor_WithJUnitFile_ParsesCorrectly` | diff --git a/docs/verification/ots/versionmark.md b/docs/verification/ots/versionmark.md new file mode 100644 index 0000000..b0b87fa --- /dev/null +++ b/docs/verification/ots/versionmark.md @@ -0,0 +1,25 @@ +## VersionMark Verification + +### Required Functionality + +VersionMark (`ReqStream-OTS-VersionMark`) shall publish captured tool-version +information. The `DemaConsulting.VersionMark` tool reads version metadata for each +`dotnet tool` used in the pipeline and writes a versions Markdown document included in the +release artifacts. + +### Verification Approach + +VersionMark is verified by CI pipeline step evidence. The tool's built-in `--validate` +command is executed in the CI pipeline and writes test method results to a TRX file. +The TRX file is consumed by ReqStream to satisfy the OTS requirement. + +Test evidence names (test methods written to the TRX file by `dotnet versionmark --validate`): + +- `VersionMark_CapturesVersions` — validates that VersionMark can capture tool version metadata +- `VersionMark_GeneratesMarkdownReport` — validates that VersionMark can generate a Markdown report + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-VersionMark` | `VersionMark_CapturesVersions`, `VersionMark_GeneratesMarkdownReport` | diff --git a/docs/verification/ots/weasyprint.md b/docs/verification/ots/weasyprint.md new file mode 100644 index 0000000..1c374de --- /dev/null +++ b/docs/verification/ots/weasyprint.md @@ -0,0 +1,39 @@ +## WeasyPrint Verification + +### Required Functionality + +WeasyPrint (`ReqStream-OTS-WeasyPrint`) shall convert HTML documents to valid PDF. The +`DemaConsulting.WeasyPrintTool` wrapper converts HTML documents to PDF as part of the +documentation build pipeline. + +### Verification Approach + +WeasyPrint is verified by CI pipeline step evidence combined with FileAssert document +validation. Each HTML document (build notes, code quality report, review plan, review report, +design document, user guide, requirements document, requirements report, and verification +document) is converted to PDF by WeasyPrint in the CI pipeline. FileAssert then asserts that each generated +PDF file exists, has a non-trivial size, contains at least one page, and includes expected +document content in the rendered text. Passing FileAssert assertions confirm WeasyPrint +executed correctly and produced meaningful output. + +Test evidence names: + +- `WeasyPrint_BuildNotesPdf` — build-notes PDF document validated +- `WeasyPrint_CodeQualityPdf` — code quality PDF document validated +- `WeasyPrint_ReviewPlanPdf` — review plan PDF document validated +- `WeasyPrint_ReviewReportPdf` — review report PDF document validated +- `WeasyPrint_DesignPdf` — design document PDF validated +- `WeasyPrint_UserGuidePdf` — user guide PDF document validated +- `WeasyPrint_VerificationPdf` — verification document PDF validated + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-WeasyPrint` | `WeasyPrint_BuildNotesPdf` | +| `ReqStream-OTS-WeasyPrint` | `WeasyPrint_CodeQualityPdf` | +| `ReqStream-OTS-WeasyPrint` | `WeasyPrint_ReviewPlanPdf` | +| `ReqStream-OTS-WeasyPrint` | `WeasyPrint_ReviewReportPdf` | +| `ReqStream-OTS-WeasyPrint` | `WeasyPrint_DesignPdf` | +| `ReqStream-OTS-WeasyPrint` | `WeasyPrint_UserGuidePdf` | +| `ReqStream-OTS-WeasyPrint` | `WeasyPrint_VerificationPdf` | diff --git a/docs/verification/ots/xunit.md b/docs/verification/ots/xunit.md new file mode 100644 index 0000000..d6f00f3 --- /dev/null +++ b/docs/verification/ots/xunit.md @@ -0,0 +1,28 @@ +## xUnit Verification + +### Required Functionality + +xUnit (`ReqStream-OTS-XUnit`) shall execute unit tests and report results. The +xUnit framework (xunit.v3 and xunit.runner.visualstudio) discovers and runs all test +methods. Passing tests confirm the framework is functioning correctly. + +### Verification Approach + +xUnit is verified by integration test evidence. The test suite is executed with `dotnet test` +as part of the CI pipeline. Passing test methods demonstrate that xUnit discovered and ran +the tests correctly. The following representative test methods are linked as evidence: + +- `Context_Create_NoArguments_ReturnsDefaultContext` +- `Context_Create_VersionFlag_SetsVersionProperty` +- `Context_Create_HelpFlags_SetsHelpProperty` +- `Section_Load_SimpleRequirement_ParsesCorrectly` +- `Requirement_Properties_DefaultValues` +- `TraceMatrix_Constructor_WithNoFiles_CreatesEmptyMatrix` +- `Program_Run_WithVersionFlag_PrintsVersion` +- `Validation_Run_WithSilentContext_CompletesSuccessfully` + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-XUnit` | `Context_Create_NoArguments_ReturnsDefaultContext`, `Context_Create_VersionFlag_SetsVersionProperty`, `Context_Create_HelpFlags_SetsHelpProperty`, `Section_Load_SimpleRequirement_ParsesCorrectly`, `Requirement_Properties_DefaultValues`, `TraceMatrix_Constructor_WithNoFiles_CreatesEmptyMatrix`, `Program_Run_WithVersionFlag_PrintsVersion`, `Validation_Run_WithSilentContext_CompletesSuccessfully` | diff --git a/docs/verification/ots/yamldotnet.md b/docs/verification/ots/yamldotnet.md new file mode 100644 index 0000000..1b0ad9c --- /dev/null +++ b/docs/verification/ots/yamldotnet.md @@ -0,0 +1,31 @@ +## YamlDotNet Verification + +### Required Functionality + +YamlDotNet (`ReqStream-OTS-YamlDotNet`) shall parse YAML requirements files into a structured +data model. YamlDotNet is the YAML parsing library used to deserialize requirements files, +converting YAML text into .NET objects that the Modeling subsystem uses for requirements +management. + +### Verification Approach + +YamlDotNet is verified by integration test evidence. The requirements loading tests exercise +YamlDotNet on well-formed and malformed YAML inputs. Passing tests confirm that the library +correctly parses YAML and reports errors with location information. The following representative +test methods are linked as evidence: + +- `Requirements_Load_ValidFile_ReturnsRequirementsAndNoIssues` +- `Requirements_Load_InvalidYamlContent_ReportsErrorWithFileLocation` +- `Requirements_Load_MalformedYaml_ReturnsNullAndIssues` +- `Section_Load_SimpleRequirement_ParsesCorrectly` +- `Requirements_Load_ComplexStructure_ParsesCorrectly` + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-OTS-YamlDotNet` | `Requirements_Load_ValidFile_ReturnsRequirementsAndNoIssues` | +| `ReqStream-OTS-YamlDotNet` | `Requirements_Load_InvalidYamlContent_ReportsErrorWithFileLocation` | +| `ReqStream-OTS-YamlDotNet` | `Requirements_Load_MalformedYaml_ReturnsNullAndIssues` | +| `ReqStream-OTS-YamlDotNet` | `Section_Load_SimpleRequirement_ParsesCorrectly` | +| `ReqStream-OTS-YamlDotNet` | `Requirements_Load_ComplexStructure_ParsesCorrectly` | diff --git a/docs/verification/reqstream.md b/docs/verification/reqstream.md new file mode 100644 index 0000000..c83a702 --- /dev/null +++ b/docs/verification/reqstream.md @@ -0,0 +1,181 @@ +# ReqStream System Verification + +## System Verification Strategy + +The ReqStream system is verified at the system level using integration tests that invoke the +published `dotnet` tool end-to-end. Tests are written using xUnit in `IntegrationTests.cs` and +exercise the complete tool — from command-line argument parsing through report generation and +enforcement — in a temporary directory. No mocking or stubbing is used at the system level; +tests exercise the actual binary on the actual file system. + +## Test Environment + +System integration tests run in the CI/CD pipeline on all three supported platforms (Windows, +Linux, and macOS) under all three supported .NET runtimes (.NET 8, .NET 9, .NET 10). Each test +creates a temporary working directory, writes fixture YAML requirements files and TRX/JUnit test +result files, invokes the tool, and asserts on the exit code, console output, and generated +report files as appropriate. + +## System Test Scenarios + +### Version Display Scenario + +Verifies that the tool prints version information and exits with code 0 when `--version` is +passed. The test captures stdout and asserts it contains a non-empty version string. + +Test method: `ReqStream_System_CliInterface_VersionFlag_PrintsVersion` + +### Help Display Scenario + +Verifies that the tool prints usage information and exits with code 0 when `--help` is passed. +The test captures stdout and asserts it contains expected option descriptions. + +Test method: `ReqStream_System_CliInterface_HelpFlag_PrintsHelp` + +### Full Pipeline Scenario + +Verifies that the tool executes the full requirements-processing pipeline in a single invocation, +including loading YAML, tracing test results, and generating all reports. + +Test method: `ReqStream_FullPipeline_WithCoveredRequirements_GeneratesAllReportsAndEnforces` + +### Source Filter Scenario + +Verifies that source-specific test matching restricts coverage evidence to tests from named +result files. + +Test method: `ReqStream_SourceFilter_NamedSourceInRequirement_MatchesTestsBySourceFile` + +### Enforcement Mode Scenario + +Verifies that the tool exits with a non-zero code when enforcement is active and a requirement +lacks passing test evidence. + +Test method: `ReqStream_EnforcementMode_RequirementLacksTestEvidence_FailsWithNonZeroExitCode` + +### Lint Scenario + +Verifies that the tool identifies and reports all structural issues in a single linting invocation +and exits silently when no issues are found. + +Test methods: + +- `ReqStream_System_Lint_Flag_ReportsLintIssues` +- `ReqStream_System_Lint_ValidRequirementsFile_ExitsSilentlyWithZero` + +### Validate Scenario + +Verifies that the tool runs a built-in self-test suite when `--validate` is passed. + +Test method: `ReqStream_System_Validate_Flag_RunsSelfValidation` + +### Validate Results Output Scenario + +Verifies that the tool writes self-validation test results to a file when `--results` is passed. + +Test method: `ReqStream_System_ValidateResultsOutput_ResultsFlag_WritesResultsFile` + +### Requirements Report Scenario + +Verifies that the tool exports a requirements Markdown report when the `--report` flag is +provided. + +Test method: `ReqStream_FullPipeline_WithCoveredRequirements_GeneratesAllReportsAndEnforces` + +### Trace Matrix Scenario + +Verifies that the tool exports a trace matrix Markdown report when the `--matrix` flag is +provided. + +Test method: `ReqStream_FullPipeline_WithCoveredRequirements_GeneratesAllReportsAndEnforces` + +### Justifications Scenario + +Verifies that the tool exports requirement justifications when the `--justifications` flag is +provided. + +Test method: `ReqStream_FullPipeline_WithCoveredRequirements_GeneratesAllReportsAndEnforces` + +### Tag Filter Scenario + +Verifies that the tool filters requirements output by tags when the `--filter` flag is provided. + +Test method: `ReqStream_System_TagFilter_Flag_FiltersRequirements` + +### Output Routing Scenario + +Verifies that the tool supports log file output and console output suppression. + +Test methods: + +- `ReqStream_System_OutputControl_LogFlag_WritesOutputToFile` +- `ReqStream_System_OutputControl_SilentFlag_SuppressesConsoleOutput` + +### Report Depth Scenario + +Verifies that the tool supports configurable report heading depth. + +Test method: `ReqStream_System_ReportDepth_DepthFlag_GeneratesReportWithCorrectHeadingLevel` + +### File Includes Scenario + +Verifies that the tool loads requirements from multiple YAML files via file includes. + +Test method: `ReqStream_System_FileIncludes_RequirementsWithIncludes_LoadsAllRequirements` + +## Platform Test Scenarios + +Platform requirements are verified by running the self-validation tests on each platform and +runtime. The CI pipeline runs the tool on Windows, Linux (Ubuntu), and macOS, and under +.NET 8, .NET 9, and .NET 10. + +### Windows Platform Scenario + +Test methods: `windows@ReqStream_VersionDisplay`, `windows@ReqStream_HelpDisplay` + +### Linux Platform Scenario + +Test methods: `ubuntu@ReqStream_VersionDisplay`, `ubuntu@ReqStream_HelpDisplay` + +### macOS Platform Scenario + +Test methods: `macos@ReqStream_VersionDisplay`, `macos@ReqStream_HelpDisplay` + +### .NET 8 Runtime Scenario + +Test methods: `dotnet8.x@ReqStream_VersionDisplay`, `dotnet8.x@ReqStream_HelpDisplay` + +### .NET 9 Runtime Scenario + +Test methods: `dotnet9.x@ReqStream_VersionDisplay`, `dotnet9.x@ReqStream_HelpDisplay` + +### .NET 10 Runtime Scenario + +Test methods: `dotnet10.x@ReqStream_VersionDisplay`, `dotnet10.x@ReqStream_HelpDisplay` + +## Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-System-VersionDisplay` | `ReqStream_System_CliInterface_VersionFlag_PrintsVersion` | +| `ReqStream-System-HelpDisplay` | `ReqStream_System_CliInterface_HelpFlag_PrintsHelp` | +| `ReqStream-System-FullPipeline` | `ReqStream_FullPipeline_WithCoveredRequirements_GeneratesAllReportsAndEnforces` | +| `ReqStream-System-SourceFilter` | `ReqStream_SourceFilter_NamedSourceInRequirement_MatchesTestsBySourceFile` | +| `ReqStream-System-EnforceMode` | `ReqStream_EnforcementMode_RequirementLacksTestEvidence_FailsWithNonZeroExitCode` | +| `ReqStream-System-Lint` | `ReqStream_System_Lint_Flag_ReportsLintIssues`, `ReqStream_System_Lint_ValidRequirementsFile_ExitsSilentlyWithZero` | +| `ReqStream-System-Validate` | `ReqStream_System_Validate_Flag_RunsSelfValidation` | +| `ReqStream-System-ValidateResultsOutput` | `ReqStream_System_ValidateResultsOutput_ResultsFlag_WritesResultsFile` | +| `ReqStream-System-RequirementsReport` | `ReqStream_FullPipeline_WithCoveredRequirements_GeneratesAllReportsAndEnforces` | +| `ReqStream-System-TraceMatrix` | `ReqStream_FullPipeline_WithCoveredRequirements_GeneratesAllReportsAndEnforces` | +| `ReqStream-System-Justifications` | `ReqStream_FullPipeline_WithCoveredRequirements_GeneratesAllReportsAndEnforces` | +| `ReqStream-System-TagFilter` | `ReqStream_System_TagFilter_Flag_FiltersRequirements` | +| `ReqStream-System-OutputRouting` | `ReqStream_System_OutputControl_LogFlag_WritesOutputToFile`, `ReqStream_System_OutputControl_SilentFlag_SuppressesConsoleOutput` | +| `ReqStream-System-ReportDepth` | `ReqStream_System_ReportDepth_DepthFlag_GeneratesReportWithCorrectHeadingLevel` | +| `ReqStream-System-CrossPlatform` | Satisfied by children: `ReqStream-Platform-Windows`, `ReqStream-Platform-Linux`, `ReqStream-Platform-MacOS`, `ReqStream-Platform-Net8`, `ReqStream-Platform-Net9`, `ReqStream-Platform-Net10` | +| `ReqStream-System-FileIncludes` | `ReqStream_System_FileIncludes_RequirementsWithIncludes_LoadsAllRequirements` | +| `ReqStream-Platform-Windows` | `windows@ReqStream_VersionDisplay`, `windows@ReqStream_HelpDisplay` | +| `ReqStream-Platform-Linux` | `ubuntu@ReqStream_VersionDisplay`, `ubuntu@ReqStream_HelpDisplay` | +| `ReqStream-Platform-MacOS` | `macos@ReqStream_VersionDisplay`, `macos@ReqStream_HelpDisplay` | +| `ReqStream-Platform-Net8` | `dotnet8.x@ReqStream_VersionDisplay`, `dotnet8.x@ReqStream_HelpDisplay` | +| `ReqStream-Platform-Net9` | `dotnet9.x@ReqStream_VersionDisplay`, `dotnet9.x@ReqStream_HelpDisplay` | +| `ReqStream-Platform-Net10` | `dotnet10.x@ReqStream_VersionDisplay`, `dotnet10.x@ReqStream_HelpDisplay` | diff --git a/docs/verification/reqstream/cli.md b/docs/verification/reqstream/cli.md new file mode 100644 index 0000000..43d039b --- /dev/null +++ b/docs/verification/reqstream/cli.md @@ -0,0 +1,50 @@ +## Cli Subsystem Verification + +### Verification Strategy + +The Cli subsystem is verified using xUnit integration tests in `CliTests.cs`. Each test +constructs a `Context` from a specific set of command-line arguments and then asserts on the +combined observable behavior: the relevant flag property, exit code, or (where applicable) +file system state. The tests operate at the subsystem boundary — validating the interaction +between argument parsing (`Context`) and the I/O routing it provides — without mocking any +internal components. + +### Test Scenarios + +#### Interface Scenario + +Tests verify that the Cli subsystem correctly parses flags and rejects unknown arguments. + +Test methods: + +- `Cli_Interface_VersionFlag_SetsVersionProperty` — `--version` sets Version property +- `Cli_Interface_HelpFlag_SetsHelpProperty` — `--help` sets Help property +- `Cli_Interface_UnknownArgument_ThrowsArgumentException` — unknown arg throws ArgumentException +- `Cli_Interface_MissingArgumentValue_ThrowsArgumentException` — missing value throws ArgumentException +- `Cli_Interface_InvalidDepthValue_ThrowsArgumentException` — non-integer depth throws ArgumentException +- `Cli_Interface_LogFileOpenFailure_ThrowsArgumentException` — inaccessible log path throws ArgumentException +- `Cli_Interface_DepthFlag_SetsDefaultForAllReportDepths` — `--depth` sets all per-report depths + +#### Output Scenario + +Tests verify that output is correctly routed to the console and/or log file. + +Test methods: + +- `Cli_Output_SilentFlag_SetsSilentProperty` — `--silent` sets Silent property +- `Cli_Output_LogFlag_WritesOutputToLogFile` — `--log` writes output to file +- `Cli_Output_WriteError_WritesToErrorChannel` — WriteError writes to stderr +- `Cli_Output_WriteError_SetsExitCodeToOne` — WriteError sets ExitCode to 1 + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Cli-Interface` | `Cli_Interface_VersionFlag_SetsVersionProperty`, `Cli_Interface_HelpFlag_SetsHelpProperty`, `Cli_Interface_UnknownArgument_ThrowsArgumentException` | +| `ReqStream-Cli-Output` | `Cli_Output_SilentFlag_SetsSilentProperty`, `Cli_Output_LogFlag_WritesOutputToLogFile` | +| `ReqStream-Cli-StderrRouting` | `Cli_Output_WriteError_WritesToErrorChannel` | +| `ReqStream-Cli-ExitCodeSignaling` | `Cli_Output_WriteError_SetsExitCodeToOne` | +| `ReqStream-Cli-MissingArgumentValue` | `Cli_Interface_MissingArgumentValue_ThrowsArgumentException` | +| `ReqStream-Cli-InvalidDepthValue` | `Cli_Interface_InvalidDepthValue_ThrowsArgumentException` | +| `ReqStream-Cli-LogFileOpenFailure` | `Cli_Interface_LogFileOpenFailure_ThrowsArgumentException` | +| `ReqStream-Cli-DepthInheritance` | `Cli_Interface_DepthFlag_SetsDefaultForAllReportDepths` | diff --git a/docs/verification/reqstream/cli/context.md b/docs/verification/reqstream/cli/context.md new file mode 100644 index 0000000..d16b3f2 --- /dev/null +++ b/docs/verification/reqstream/cli/context.md @@ -0,0 +1,120 @@ +### Context Unit Verification + +#### Verification Strategy + +The Context unit is verified using xUnit unit tests in `ContextTests.cs`. Tests create `Context` +instances with specific command-line argument arrays and assert the resulting property values, +file system effects, and exception behavior. Temporary directories are created for tests +requiring file system access. + +#### Test Scenarios + +##### CLI Parsing Scenario + +Tests verify that Context correctly parses all supported command-line arguments. + +Test methods: + +- `Context_Create_NoArguments_ReturnsDefaultContext` — no args → default context +- `Context_Create_VersionFlag_SetsVersionProperty` — `--version` sets Version +- `Context_Create_HelpFlags_SetsHelpProperty` — `--help`/`-h`/`-?` sets Help +- `Context_Create_SilentFlag_SetsSilentProperty` — `--silent` sets Silent +- `Context_Create_ValidateFlag_SetsValidateProperty` — `--validate` sets Validate +- `Context_Create_EnforceFlag_SetsEnforceProperty` — `--enforce` sets Enforce +- `Context_Create_LintFlag_SetsLintProperty` — `--lint` sets Lint +- `Context_Create_UnsupportedArgument_ThrowsException` — unknown arg throws +- `Context_Create_MultipleArguments_ParsesAllCorrectly` — multiple flags parsed +- `Context_Create_MissingLogFilename_ThrowsException` — `--log` without value throws +- `Context_Create_MissingResultsFilename_ThrowsException` — `--results` without value throws +- `Context_Create_FilterArgumentMissingValue_ThrowsException` — `--filter` without value throws + +##### Requirements and Tests Pattern Scenario + +Test methods: + +- `Context_Create_WithRequirementsPattern_ExpandsGlobPattern` — glob patterns for requirements +- `Context_Create_WithTestsPattern_ExpandsGlobPattern` — glob patterns for test results + +##### Results and Report Flags Scenario + +Test methods: + +- `Context_Create_ResultsFlag_SetsResultsFileProperty` — `--results` sets path +- `Context_Create_ResultFlag_SetsResultsFileProperty` — `--result` alias sets path +- `Context_Create_ReportFile_SetsReportProperty` — `--report` sets path +- `Context_Create_MissingReportFilename_ThrowsException` — `--report` without value throws +- `Context_Create_MatrixFile_SetsMatrixProperty` — `--matrix` sets path +- `Context_Create_MissingMatrixFilename_ThrowsException` — `--matrix` without value throws +- `Context_Create_JustificationsFile_SetsJustificationsFileProperty` — `--justifications` sets path +- `Context_Create_MissingJustificationsFilename_ThrowsException` — `--justifications` without value throws + +##### Depth Flags Scenario + +Test methods: + +- `Context_Create_ReportDepth_SetsReportDepthProperty` — `--report-depth` sets report depth +- `Context_Create_MatrixDepth_SetsMatrixDepthProperty` — `--matrix-depth` sets matrix depth +- `Context_Create_JustificationsDepth_SetsJustificationsDepthProperty` — `--justifications-depth` +- `Context_Create_Depth_SetsAllDepths` — `--depth` sets all depth properties +- `Context_Create_SpecificDepthOverridesDefaultDepth` — per-report overrides `--depth` +- `Context_Create_MissingDepth_ThrowsException` — `--depth` without value throws +- `Context_Create_InvalidDepth_ThrowsException` — non-integer depth throws +- `Context_Create_MissingJustificationsDepth_ThrowsException` — missing justifications depth throws +- `Context_Create_InvalidJustificationsDepth_ThrowsException` — invalid justifications depth throws + +##### Tag Filter Scenario + +Test methods: + +- `Context_Create_FilterArgument_ParsesTagsCorrectly` — `--filter` parses comma-separated tags +- `Context_Create_FilterArgumentWithSpaces_TrimsAndParsesTagsCorrectly` — spaces are trimmed +- `Context_Create_FilterSingleTag_ParsesCorrectly` — single tag parsed +- `Context_Create_MultipleFilterArguments_MergesIntoSingleSet` — multiple `--filter` merged + +##### Output Channel Scenario + +Test methods: + +- `Context_WriteLine_SilentMode_DoesNotWriteToConsole` — silent suppresses stdout +- `Context_WriteError_SilentMode_DoesNotWriteToConsole` — silent suppresses stderr +- `Context_WriteError_NormalMode_WritesToConsole` — normal mode writes to stderr +- `Context_ExitCode_AfterWriteError_ReturnsOne` — exit code is 1 after error + +##### Log File Scenario + +Test methods: + +- `Context_Create_WithLogFile_WritesToLogFile` — log file receives output +- `Context_Create_WithLogFileAndSilent_WritesToLogOnly` — silent + log writes only to log +- `Context_Dispose_WithLogFile_ClosesLogFile` — dispose closes log file +- `Context_Create_InvalidLogPath_ThrowsException` — invalid log path throws + +#### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Command-Cli` | `Context_Create_NoArguments_ReturnsDefaultContext`, `Context_Create_MultipleArguments_ParsesAllCorrectly` | +| `ReqStream-Command-Version` | `Context_Create_VersionFlag_SetsVersionProperty` | +| `ReqStream-Command-Help` | `Context_Create_HelpFlags_SetsHelpProperty` | +| `ReqStream-Command-Silent` | `Context_Create_SilentFlag_SetsSilentProperty`, `Context_WriteLine_SilentMode_DoesNotWriteToConsole`, `Context_WriteError_SilentMode_DoesNotWriteToConsole` | +| `ReqStream-Command-ErrorOutput` | `Context_WriteError_NormalMode_WritesToConsole` | +| `ReqStream-Command-UnknownArgs` | `Context_Create_UnsupportedArgument_ThrowsException` | +| `ReqStream-Command-MissingLogValue` | `Context_Create_MissingLogFilename_ThrowsException` | +| `ReqStream-Command-MissingResultsValue` | `Context_Create_MissingResultsFilename_ThrowsException` | +| `ReqStream-Command-MissingFilterValue` | `Context_Create_FilterArgumentMissingValue_ThrowsException` | +| `ReqStream-Command-RequirementsGlobPatterns` | `Context_Create_WithRequirementsPattern_ExpandsGlobPattern` | +| `ReqStream-Command-TestGlobPatterns` | `Context_Create_WithTestsPattern_ExpandsGlobPattern` | +| `ReqStream-Command-Validate` | `Context_Create_ValidateFlag_SetsValidateProperty` | +| `ReqStream-Command-Enforce` | `Context_Create_EnforceFlag_SetsEnforceProperty` | +| `ReqStream-Command-ExitCode` | `Context_ExitCode_AfterWriteError_ReturnsOne` | +| `ReqStream-Command-ReportDepth` | `Context_Create_ReportDepth_SetsReportDepthProperty` | +| `ReqStream-Command-MatrixDepth` | `Context_Create_MatrixDepth_SetsMatrixDepthProperty` | +| `ReqStream-Command-Depth` | `Context_Create_Depth_SetsAllDepths`, `Context_Create_SpecificDepthOverridesDefaultDepth`, `Context_Create_MissingDepth_ThrowsException`, `Context_Create_InvalidDepth_ThrowsException` | +| `ReqStream-Command-TagFilter` | `Context_Create_FilterArgument_ParsesTagsCorrectly`, `Context_Create_FilterArgumentWithSpaces_TrimsAndParsesTagsCorrectly`, `Context_Create_FilterSingleTag_ParsesCorrectly`, `Context_Create_MultipleFilterArguments_MergesIntoSingleSet` | +| `ReqStream-Command-Lint` | `Context_Create_LintFlag_SetsLintProperty` | +| `ReqStream-Command-Results` | `Context_Create_ResultsFlag_SetsResultsFileProperty`, `Context_Create_ResultFlag_SetsResultsFileProperty`, `Context_Create_MissingResultsFilename_ThrowsException` | +| `ReqStream-Command-Report` | `Context_Create_ReportFile_SetsReportProperty`, `Context_Create_MissingReportFilename_ThrowsException` | +| `ReqStream-Command-Matrix` | `Context_Create_MatrixFile_SetsMatrixProperty`, `Context_Create_MissingMatrixFilename_ThrowsException` | +| `ReqStream-Command-Justifications` | `Context_Create_JustificationsFile_SetsJustificationsFileProperty`, `Context_Create_MissingJustificationsFilename_ThrowsException` | +| `ReqStream-Command-JustificationsDepth` | `Context_Create_JustificationsDepth_SetsJustificationsDepthProperty`, `Context_Create_MissingJustificationsDepth_ThrowsException`, `Context_Create_InvalidJustificationsDepth_ThrowsException` | +| `ReqStream-Command-LogFileOutput` | `Context_Create_WithLogFile_WritesToLogFile`, `Context_Create_WithLogFileAndSilent_WritesToLogOnly`, `Context_Dispose_WithLogFile_ClosesLogFile`, `Context_Create_InvalidLogPath_ThrowsException` | diff --git a/docs/verification/reqstream/modeling.md b/docs/verification/reqstream/modeling.md new file mode 100644 index 0000000..0ca3008 --- /dev/null +++ b/docs/verification/reqstream/modeling.md @@ -0,0 +1,50 @@ +## Modeling Subsystem Verification + +### Verification Strategy + +The Modeling subsystem is verified using xUnit integration tests in `ModelingTests.cs`. Tests +create temporary YAML requirements files, invoke `Requirements.Load`, and assert on the +resulting data model, lint issues, and generated Markdown reports. The subsystem boundary +is the `Requirements` class which acts as the public API entry point. + +### Test Scenarios + +#### YAML Parsing Scenario + +Tests verify that valid YAML files load correctly and that duplicate IDs are detected. + +Test methods: + +- `Modeling_YamlParsing_ValidFile_LoadsRequirements` — valid file → loaded requirements +- `Modeling_YamlParsing_ValidFile_ReturnsNoLintIssues` — valid file → no lint issues +- `Modeling_YamlParsing_DuplicateIds_DetectsLintError` — duplicate IDs → error lint issue + +#### Export Scenario + +Tests verify that requirements and justifications are exported to Markdown files. + +Test methods: + +- `Modeling_Export_Requirements_GeneratesMarkdownFile` — requirements Markdown export +- `Modeling_Export_Justifications_GeneratesMarkdownFile` — justifications Markdown export + +#### Linting Scenario + +Tests verify that structural issues are detected and that valid files return no issues. + +Test methods: + +- `Modeling_Linting_MalformedYaml_DetectsError` — malformed YAML → error with null requirements +- `Modeling_Linting_ValidFile_ReturnsNoIssues` — valid file → no issues +- `Modeling_LintingReporting_MultipleConditions_ReportsAllIssues` — all issues reported at once + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Modeling-YamlParsing` | `Modeling_YamlParsing_ValidFile_LoadsRequirements`, `Modeling_YamlParsing_ValidFile_ReturnsNoLintIssues`, `Modeling_YamlParsing_DuplicateIds_DetectsLintError` | +| `ReqStream-Modeling-Export` | `Modeling_Export_Requirements_GeneratesMarkdownFile`, `Modeling_Export_Justifications_GeneratesMarkdownFile` | +| `ReqStream-Modeling-MultiFileLoading` | `Modeling_YamlParsing_ValidFile_LoadsRequirements`, `Modeling_MultiFileLoading_WithIncludes_LoadsRequirementsFromAllFiles` | +| `ReqStream-Modeling-Linting` | `Modeling_YamlParsing_DuplicateIds_DetectsLintError`, `Modeling_Linting_MalformedYaml_DetectsError`, `Modeling_Linting_ValidFile_ReturnsNoIssues` | +| `ReqStream-Modeling-LintingValidation` | `Modeling_Linting_MalformedYaml_DetectsError`, `Modeling_Linting_ValidFile_ReturnsNoIssues` | +| `ReqStream-Modeling-LintingReporting` | `Modeling_YamlParsing_DuplicateIds_DetectsLintError`, `Modeling_LintingReporting_MultipleConditions_ReportsAllIssues` | diff --git a/docs/verification/reqstream/modeling/lint-issue.md b/docs/verification/reqstream/modeling/lint-issue.md new file mode 100644 index 0000000..123ad0d --- /dev/null +++ b/docs/verification/reqstream/modeling/lint-issue.md @@ -0,0 +1,28 @@ +### LintIssue Unit Verification + +#### Verification Strategy + +The LintIssue unit is verified using xUnit unit tests in `LintIssueTests.cs`. Tests create +`LintIssue` instances with specific severity and content values and assert on the formatted +`ToString()` output. + +#### Test Scenarios + +##### Issue Formatting Scenario + +Tests verify that `LintIssue.ToString()` formats the issue correctly for both error and +warning severities. + +Test methods: + +- `LintIssue_ToString_ErrorSeverity_FormatsAsError` — error severity formats as "error" +- `LintIssue_ToString_WarningSeverity_FormatsAsWarning` — warning severity formats as "warning" +- `LintIssue_ToString_EmptyLocation_FormatsCorrectly` — empty location still formats correctly +- `LintIssue_ToString_EmptyDescription_FormatsCorrectly` — empty description still formats correctly + +#### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Lint-IssueType` | `LintIssue_ToString_ErrorSeverity_FormatsAsError`, `LintIssue_ToString_WarningSeverity_FormatsAsWarning` | +| `ReqStream-Lint-SeverityString` | `LintIssue_ToString_ErrorSeverity_FormatsAsError`, `LintIssue_ToString_WarningSeverity_FormatsAsWarning` | diff --git a/docs/verification/reqstream/modeling/load-result.md b/docs/verification/reqstream/modeling/load-result.md new file mode 100644 index 0000000..5acc9d6 --- /dev/null +++ b/docs/verification/reqstream/modeling/load-result.md @@ -0,0 +1,38 @@ +## LoadResult Unit Verification + +### Verification Strategy + +The LoadResult unit is verified using xUnit integration tests in `LoadResultTests.cs`. Tests +create YAML requirements files and `LoadResult` instances with specific issue lists, then invoke +`ReportIssues` through a `Context` instance and assert on the exit code, log file content, +`HasErrors` property, and `Requirements` reference. + +### Test Scenarios + +#### Issue Routing Scenario + +Tests verify that error-level issues route to the error channel and warning-level issues +do not set the exit code. + +Test methods: + +- `LoadResult_ReportIssues_ErrorIssue_SetsContextError` — error issue → exit code 1 +- `LoadResult_ReportIssues_WarningIssue_DoesNotSetContextError` — warning issue → exit code 0 +- `LoadResult_ReportIssues_NoIssues_ProducesNoOutput` — no issues → no output + +#### HasErrors Scenario + +Tests verify the `HasErrors` property behavior. + +Test methods: + +- `LoadResult_HasErrors_WithOnlyWarnings_ReturnsFalse` — warnings only → HasErrors false +- `LoadResult_HasErrors_WithErrorIssue_ReturnsTrue` — error issue → HasErrors true + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Requirements-UnifiedLoad` | `Requirements_Load_ValidFile_ReturnsRequirementsAndNoIssues`, `Requirements_Load_WithLintError_ReturnsNullAndIssues`, `Requirements_Load_MissingFile_ReturnsNullAndIssues`, `Requirements_Load_MalformedYaml_ReturnsNullAndIssues`, `Requirements_Load_WithMultipleLintErrors_ReportsAllIssues`, `Requirements_Load_WithIncludes_LintsIncludedFiles`, `Requirements_Load_WithLintError_IssueIncludesLocation` | +| `ReqStream-LoadResult-ReportIssues` | `LoadResult_ReportIssues_ErrorIssue_SetsContextError`, `LoadResult_ReportIssues_WarningIssue_DoesNotSetContextError`, `LoadResult_ReportIssues_NoIssues_ProducesNoOutput` | +| `ReqStream-LoadResult-HasErrors` | `LoadResult_HasErrors_WithErrorIssue_ReturnsTrue`, `LoadResult_HasErrors_WithOnlyWarnings_ReturnsFalse` | diff --git a/docs/verification/reqstream/modeling/requirement.md b/docs/verification/reqstream/modeling/requirement.md new file mode 100644 index 0000000..86317a0 --- /dev/null +++ b/docs/verification/reqstream/modeling/requirement.md @@ -0,0 +1,52 @@ +### Requirement Unit Verification + +#### Verification Strategy + +The Requirement unit is verified using xUnit integration tests in `RequirementTests.cs`. Tests +create YAML requirements files with various field combinations and assert on the parsed +`Requirement` data model properties and any lint issues reported. + +#### Test Scenarios + +##### Properties Scenario + +Tests verify that requirement properties are parsed correctly from YAML. + +Test methods: + +- `Requirement_Properties_DefaultValues` — default property values are correct +- `Requirements_Load_RequirementWithTests_ParsesTestsCorrectly` — tests list parsed +- `Requirements_Load_RequirementWithTags_ParsesTagsCorrectly` — tags list parsed +- `Requirements_Load_RequirementWithJustification_ParsesJustificationCorrectly` — justification parsed +- `Requirements_Load_RequirementWithChildren_ParsesChildrenCorrectly` — children list parsed + +##### Validation Scenario + +Tests verify that missing or invalid fields are reported as lint errors. + +Test methods: + +- `Requirements_Load_BlankRequirementId_ReportsErrorWithFileLocation` — blank ID → error +- `Requirements_Load_BlankRequirementTitle_ReportsErrorWithFileLocation` — blank title → error +- `Requirements_Load_DuplicateRequirementId_ReportsError` — duplicate ID → error +- `Requirements_Load_DuplicateRequirementId_ErrorIncludesFileLocation` — error includes file location +- `Requirements_Load_MultipleFilesWithDuplicateIds_ReportsError` — cross-file duplicate ID → error +- `Requirements_Load_BlankTagName_ReportsErrorWithFileLocation` — blank tag → error +- `Requirements_Load_BlankChildIdInRequirement_ReportsErrorWithFileLocation` — blank child ID → error +- `Requirements_Load_BlankTestNameInRequirement_ReportsErrorWithFileLocation` — blank test name → error +- `Requirements_Load_BlankTestNameInMapping_ReportsErrorWithFileLocation` — blank mapping test → error +- `Requirements_Load_BlankMappingId_ReportsErrorWithFileLocation` — blank mapping ID → error +- `Requirements_Load_TestMappings_AppliesMappingsCorrectly` — test mappings applied correctly + +#### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Requirements-UniqueIds` | `Requirements_Load_DuplicateRequirementId_ReportsError`, `Requirements_Load_BlankRequirementId_ReportsErrorWithFileLocation`, `Requirements_Load_MultipleFilesWithDuplicateIds_ReportsError` | +| `ReqStream-Requirements-RequiredTitle` | `Requirements_Load_BlankRequirementTitle_ReportsErrorWithFileLocation` | +| `ReqStream-Requirements-ParentChild` | `Requirements_Load_RequirementWithChildren_ParsesChildrenCorrectly` | +| `ReqStream-Requirements-BlankChildId` | `Requirements_Load_BlankChildIdInRequirement_ReportsErrorWithFileLocation` | +| `ReqStream-Requirements-Tags` | `Requirements_Load_RequirementWithTags_ParsesTagsCorrectly` | +| `ReqStream-Requirements-BlankTagName` | `Requirements_Load_BlankTagName_ReportsErrorWithFileLocation` | +| `ReqStream-Requirements-Justification` | `Requirements_Load_RequirementWithJustification_ParsesJustificationCorrectly` | +| `ReqStream-Requirements-TestMappings` | `Requirements_Load_RequirementWithTests_ParsesTestsCorrectly`, `Requirements_Load_BlankTestNameInRequirement_ReportsErrorWithFileLocation`, `Requirements_Load_TestMappings_AppliesMappingsCorrectly`, `Requirements_Load_BlankTestNameInMapping_ReportsErrorWithFileLocation`, `Requirements_Load_BlankMappingId_ReportsErrorWithFileLocation` | diff --git a/docs/verification/reqstream/modeling/requirements-loader.md b/docs/verification/reqstream/modeling/requirements-loader.md new file mode 100644 index 0000000..f975f04 --- /dev/null +++ b/docs/verification/reqstream/modeling/requirements-loader.md @@ -0,0 +1,104 @@ +### RequirementsLoader Unit Verification + +#### Verification Strategy + +The RequirementsLoader unit is verified using xUnit unit tests in `RequirementsLoaderTests.cs`. +Tests create YAML requirements files with specific structural conditions (unknown fields, missing +fields, duplicate IDs, circular references, etc.) and assert on the lint issues reported. + +#### Test Scenarios + +##### File Loading Scenario + +Tests verify that file path errors are correctly reported. + +Test methods: + +- `RequirementsLoader_Load_WithInvalidFilePath_ReportsError` — invalid path → error +- `RequirementsLoader_Load_WithMissingFile_ReportsError` — missing file → error +- `RequirementsLoader_Load_WithIoReadFailure_ReportsError` — I/O failure → error +- `RequirementsLoader_Load_WithNonMappingRoot_ReportsError` — non-mapping root → error +- `RequirementsLoader_Load_WithMalformedYaml_ReportsError` — malformed YAML → error + +##### Document Structure Scenario + +Test methods: + +- `RequirementsLoader_Load_WithUnknownDocumentField_ReportsError` — unknown document field +- `RequirementsLoader_Load_WithUnknownSectionField_ReportsError` — unknown section field +- `RequirementsLoader_Load_WithSectionMissingTitle_ReportsError` — missing section title +- `RequirementsLoader_Load_WithBlankSectionTitle_ReportsError` — blank section title + +##### Requirement Structure Scenario + +Test methods: + +- `RequirementsLoader_Load_WithUnknownRequirementField_ReportsError` — unknown requirement field +- `RequirementsLoader_Load_WithNestedSectionIssues_ReportsError` — nested section issues reported +- `RequirementsLoader_Load_WithRequirementMissingId_ReportsError` — missing requirement ID +- `RequirementsLoader_Load_WithBlankRequirementId_ReportsError` — blank requirement ID +- `RequirementsLoader_Load_WithRequirementMissingTitle_ReportsError` — missing requirement title +- `RequirementsLoader_Load_WithBlankRequirementTitle_ReportsError` — blank requirement title + +##### Duplicate and Reference Scenario + +Test methods: + +- `RequirementsLoader_Load_WithDuplicateIds_ReportsError` — duplicate IDs +- `RequirementsLoader_Load_WithDuplicateIdsAcrossFiles_ReportsError` — cross-file duplicates +- `RequirementsLoader_Load_WithMultipleCycles_ReportsAllCycles` — all circular refs reported +- `RequirementsLoader_Load_WithUnknownChildReference_ReportsError` — unknown child reference + +##### Validation and Reporting Scenario + +Test methods: + +- `RequirementsLoader_Load_WithMultipleIssues_ReportsAllIssues` — all issues reported at once +- `RequirementsLoader_Load_WithIncludes_LintsIncludedFiles` — includes are linted +- `RequirementsLoader_Load_WithValidFile_ReportsNoIssues` — valid file → no issues +- `RequirementsLoader_Load_WithEmptyFile_ReportsNoIssues` — empty file → no issues +- `RequirementsLoader_Load_ErrorFormat_IncludesFileAndLocation` — error format includes file path + +##### Mapping and List Scenario + +Test methods: + +- `RequirementsLoader_Load_WithUnknownMappingField_ReportsError` — unknown mapping field +- `RequirementsLoader_Load_WithMappingMissingId_ReportsError` — mapping missing ID +- `RequirementsLoader_Load_WithBlankMappingId_ReportsError` — blank mapping ID +- `RequirementsLoader_Load_WithBlankTestName_ReportsError` — blank test name +- `RequirementsLoader_Load_WithBlankMappingTestName_ReportsError` — blank mapping test name +- `RequirementsLoader_Load_WithBlankTagName_ReportsError` — blank tag name +- `RequirementsLoader_Load_WithNonScalarTestEntry_ReportsError` — non-scalar test entry +- `RequirementsLoader_Load_WithNonScalarChildEntry_ReportsError` — non-scalar child entry +- `RequirementsLoader_Load_WithNonScalarTagEntry_ReportsError` — non-scalar tag entry +- `RequirementsLoader_Load_WithNonScalarMappingTestEntry_ReportsError` — non-scalar mapping test +- `RequirementsLoader_Load_WithNonScalarIncludeEntry_ReportsError` — non-scalar include entry + +#### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Lint-InvalidFilePath` | `RequirementsLoader_Load_WithInvalidFilePath_ReportsError` | +| `ReqStream-Lint-FileNotFound` | `RequirementsLoader_Load_WithMissingFile_ReportsError` | +| `ReqStream-Lint-IoReadFailure` | `RequirementsLoader_Load_WithIoReadFailure_ReportsError` | +| `ReqStream-Lint-NonMappingRoot` | `RequirementsLoader_Load_WithNonMappingRoot_ReportsError` | +| `ReqStream-Lint-MalformedYaml` | `RequirementsLoader_Load_WithMalformedYaml_ReportsError` | +| `ReqStream-Lint-UnknownDocumentField` | `RequirementsLoader_Load_WithUnknownDocumentField_ReportsError` | +| `ReqStream-Lint-UnknownSectionField` | `RequirementsLoader_Load_WithUnknownSectionField_ReportsError` | +| `ReqStream-Lint-MissingSectionTitle` | `RequirementsLoader_Load_WithSectionMissingTitle_ReportsError`, `RequirementsLoader_Load_WithBlankSectionTitle_ReportsError` | +| `ReqStream-Lint-UnknownRequirementField` | `RequirementsLoader_Load_WithUnknownRequirementField_ReportsError`, `RequirementsLoader_Load_WithNestedSectionIssues_ReportsError` | +| `ReqStream-Lint-MissingRequirementId` | `RequirementsLoader_Load_WithRequirementMissingId_ReportsError`, `RequirementsLoader_Load_WithBlankRequirementId_ReportsError` | +| `ReqStream-Lint-MissingRequirementTitle` | `RequirementsLoader_Load_WithRequirementMissingTitle_ReportsError`, `RequirementsLoader_Load_WithBlankRequirementTitle_ReportsError` | +| `ReqStream-Lint-DuplicateIds` | `RequirementsLoader_Load_WithDuplicateIds_ReportsError`, `RequirementsLoader_Load_WithDuplicateIdsAcrossFiles_ReportsError` | +| `ReqStream-Lint-MultipleIssues` | `RequirementsLoader_Load_WithMultipleIssues_ReportsAllIssues` | +| `ReqStream-Lint-FollowsIncludes` | `RequirementsLoader_Load_WithIncludes_LintsIncludedFiles` | +| `ReqStream-Lint-NoIssuesMessage` | `RequirementsLoader_Load_WithValidFile_ReportsNoIssues`, `RequirementsLoader_Load_WithEmptyFile_ReportsNoIssues` | +| `ReqStream-Lint-ErrorFormat` | `RequirementsLoader_Load_ErrorFormat_IncludesFileAndLocation` | +| `ReqStream-Lint-UnknownMappingField` | `RequirementsLoader_Load_WithUnknownMappingField_ReportsError` | +| `ReqStream-Lint-MissingMappingId` | `RequirementsLoader_Load_WithMappingMissingId_ReportsError`, `RequirementsLoader_Load_WithBlankMappingId_ReportsError` | +| `ReqStream-Lint-BlankTestName` | `RequirementsLoader_Load_WithBlankTestName_ReportsError`, `RequirementsLoader_Load_WithBlankMappingTestName_ReportsError` | +| `ReqStream-Lint-BlankTagName` | `RequirementsLoader_Load_WithBlankTagName_ReportsError` | +| `ReqStream-Lint-NonScalarListEntries` | `RequirementsLoader_Load_WithNonScalarTestEntry_ReportsError`, `RequirementsLoader_Load_WithNonScalarChildEntry_ReportsError`, `RequirementsLoader_Load_WithNonScalarTagEntry_ReportsError`, `RequirementsLoader_Load_WithNonScalarMappingTestEntry_ReportsError`, `RequirementsLoader_Load_WithNonScalarIncludeEntry_ReportsError` | +| `ReqStream-Lint-CircularReferences` | `RequirementsLoader_Load_WithMultipleCycles_ReportsAllCycles` | +| `ReqStream-Lint-UnknownChildReference` | `RequirementsLoader_Load_WithUnknownChildReference_ReportsError` | diff --git a/docs/verification/reqstream/modeling/requirements.md b/docs/verification/reqstream/modeling/requirements.md new file mode 100644 index 0000000..596a24c --- /dev/null +++ b/docs/verification/reqstream/modeling/requirements.md @@ -0,0 +1,95 @@ +### Requirements Unit Verification + +#### Verification Strategy + +The Requirements unit is verified using xUnit integration tests across multiple test files: +`RequirementsLoadTests.cs`, `RequirementsLoadParsingTests.cs`, and `RequirementsExportTests.cs`. +Tests create YAML requirements files with various structures, invoke `Requirements.Load`, and +assert on the parsed data model, lint issues, and generated Markdown exports. + +#### Test Scenarios + +##### YAML Processing Scenario + +Test methods: + +- `Section_Load_SimpleRequirement_ParsesCorrectly` — single requirement parsed +- `Requirements_Load_ComplexStructure_ParsesCorrectly` — complex structure parsed + +##### Validation Scenario + +Test methods: + +- `Section_Load_BlankSectionTitle_ReportsErrorWithFileLocation` — blank section title → error +- `Requirements_Load_BlankRequirementId_ReportsErrorWithFileLocation` — blank req ID → error +- `Requirements_Load_BlankRequirementTitle_ReportsErrorWithFileLocation` — blank req title → error +- `Requirements_Load_DuplicateRequirementId_ReportsError` — duplicate ID → error +- `Requirements_Load_DuplicateRequirementId_ErrorIncludesFileLocation` — error includes location +- `Requirements_Load_InvalidYamlContent_ReportsErrorWithFileLocation` — YAML error → error + +##### Hierarchy Scenario + +Test methods: + +- `Section_Load_NestedSections_ParsesHierarchyCorrectly` — nested sections parsed +- `Requirements_Export_NestedSections_CreatesHierarchy` — nested sections exported + +##### Includes Scenario + +Test methods: + +- `Requirements_Load_WithIncludes_MergesFilesCorrectly` — includes merged +- `Requirements_Load_MultipleFiles_MergesAllFiles` — multiple files merged +- `Requirements_Load_IncludeLoop_DoesNotCauseInfiniteLoop` — include loops handled + +##### Section Merging Scenario + +Test methods: + +- `Requirements_Load_IdenticalSections_MergesCorrectly` — same-title sections merged +- `Requirements_Load_MultipleFilesWithSameSections_MergesSections` — cross-file merging + +##### Export Scenario + +Test methods: + +- `Requirements_Export_SimpleRequirements_CreatesMarkdownFile` — simple export +- `Requirements_Export_MultipleSections_ExportsAll` — all sections exported +- `Requirements_Export_EmptyRequirements_CreatesEmptyFile` — empty → empty file +- `Requirements_Export_WithCustomDepth_UsesCorrectHeaderLevel` — custom depth applied +- `Requirements_Export_WithFilterTags_ExportsOnlyMatchingRequirements` — tag filter applied +- `Requirements_Export_WithMultipleFilterTags_ExportsRequirementsMatchingAnyTag` — multiple tags +- `Requirements_ExportJustifications_WithJustifications_CreatesMarkdownFile` — justifications export +- `Requirements_ExportJustifications_WithoutJustifications_CreatesHeadersOnly` — no justifications +- `Requirements_ExportJustifications_NestedSections_CreatesHierarchy` — nested justifications +- `Requirements_ExportJustifications_WithCustomDepth_UsesCorrectHeaderLevel` — custom depth +- `Requirements_ExportJustifications_WithFilterTags_ExportsOnlyMatchingRequirements` — tag filter + +##### Load Result Scenario + +Test methods: + +- `Requirements_Load_ValidFile_ReturnsRequirementsAndNoIssues` — valid → requirements and no issues +- `Requirements_Load_WithLintError_ReturnsNullAndIssues` — error → null and issues +- `Requirements_Load_MissingFile_ReturnsNullAndIssues` — missing → null and issues +- `Requirements_Load_MalformedYaml_ReturnsNullAndIssues` — malformed → null and issues +- `Requirements_Load_WithMultipleLintErrors_ReportsAllIssues` — multiple errors all reported +- `Requirements_Load_WithIncludes_LintsIncludedFiles` — included files linted +- `Requirements_Load_WithLintError_IssueIncludesLocation` — issue includes location + +#### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Requirements-YamlProcessing` | `Section_Load_SimpleRequirement_ParsesCorrectly`, `Requirements_Load_ComplexStructure_ParsesCorrectly` | +| `ReqStream-Requirements-Validation` | `Section_Load_BlankSectionTitle_ReportsErrorWithFileLocation`, `Requirements_Load_BlankRequirementId_ReportsErrorWithFileLocation`, `Requirements_Load_BlankRequirementTitle_ReportsErrorWithFileLocation`, `Requirements_Load_DuplicateRequirementId_ReportsError`, `Requirements_Load_DuplicateRequirementId_ErrorIncludesFileLocation` | +| `ReqStream-Requirements-YamlErrorReporting` | `Requirements_Load_InvalidYamlContent_ReportsErrorWithFileLocation` | +| `ReqStream-Requirements-Hierarchy` | `Section_Load_NestedSections_ParsesHierarchyCorrectly`, `Requirements_Export_NestedSections_CreatesHierarchy` | +| `ReqStream-Requirements-Includes` | `Requirements_Load_WithIncludes_MergesFilesCorrectly`, `Requirements_Load_MultipleFiles_MergesAllFiles`, `Requirements_Load_IncludeLoop_DoesNotCauseInfiniteLoop` | +| `ReqStream-Requirements-SectionMerging` | `Requirements_Load_IdenticalSections_MergesCorrectly`, `Requirements_Load_MultipleFilesWithSameSections_MergesSections` | +| `ReqStream-Report-MarkdownExport` | `Requirements_Export_SimpleRequirements_CreatesMarkdownFile`, `Requirements_Export_MultipleSections_ExportsAll`, `Requirements_Export_EmptyRequirements_CreatesEmptyFile` | +| `ReqStream-Report-HeaderDepth` | `Requirements_Export_WithCustomDepth_UsesCorrectHeaderLevel` | +| `ReqStream-Report-Justifications` | `Requirements_ExportJustifications_WithJustifications_CreatesMarkdownFile`, `Requirements_ExportJustifications_WithoutJustifications_CreatesHeadersOnly`, `Requirements_ExportJustifications_NestedSections_CreatesHierarchy` | +| `ReqStream-Report-JustificationsDepth` | `Requirements_ExportJustifications_WithCustomDepth_UsesCorrectHeaderLevel` | +| `ReqStream-Report-TagFilterExport` | `Requirements_Export_WithFilterTags_ExportsOnlyMatchingRequirements`, `Requirements_Export_WithMultipleFilterTags_ExportsRequirementsMatchingAnyTag`, `Requirements_ExportJustifications_WithFilterTags_ExportsOnlyMatchingRequirements` | +| `ReqStream-Requirements-UnifiedLoad` | `Requirements_Load_ValidFile_ReturnsRequirementsAndNoIssues`, `Requirements_Load_WithLintError_ReturnsNullAndIssues`, `Requirements_Load_MissingFile_ReturnsNullAndIssues`, `Requirements_Load_MalformedYaml_ReturnsNullAndIssues`, `Requirements_Load_WithMultipleLintErrors_ReportsAllIssues`, `Requirements_Load_WithIncludes_LintsIncludedFiles`, `Requirements_Load_WithLintError_IssueIncludesLocation` | diff --git a/docs/verification/reqstream/modeling/section.md b/docs/verification/reqstream/modeling/section.md new file mode 100644 index 0000000..7e50e4d --- /dev/null +++ b/docs/verification/reqstream/modeling/section.md @@ -0,0 +1,27 @@ +### Section Unit Verification + +#### Verification Strategy + +The Section unit is verified using xUnit integration tests in `SectionTests.cs`. Tests create +YAML requirements files and invoke `Requirements.Load`, then assert on the parsed `Section` +data structure — title, requirements list, and child sections. + +#### Test Scenarios + +##### Section Container Scenario + +Tests verify that a section holds a title, requirements, and child sections correctly. + +Test methods: + +- `Section_Load_SimpleRequirement_ParsesCorrectly` — single requirement in a section +- `Section_Load_NestedSections_ParsesHierarchyCorrectly` — nested child sections +- `Section_Load_BlankSectionTitle_ReportsErrorWithFileLocation` — blank title → error with location + +#### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Section-Container` | `Section_Load_SimpleRequirement_ParsesCorrectly`, `Section_Load_NestedSections_ParsesHierarchyCorrectly` | +| `ReqStream-Section-Nesting` | `Section_Load_NestedSections_ParsesHierarchyCorrectly`, `Requirements_Export_NestedSections_CreatesHierarchy` | +| `ReqStream-Section-TitleMerging` | `Requirements_Load_IdenticalSections_MergesCorrectly` | diff --git a/docs/verification/reqstream/program.md b/docs/verification/reqstream/program.md new file mode 100644 index 0000000..4481722 --- /dev/null +++ b/docs/verification/reqstream/program.md @@ -0,0 +1,84 @@ +## Program Unit Verification + +### Verification Strategy + +The Program unit is verified using xUnit unit tests in `ProgramTests.cs`. Tests call the +`Program.Run` static method directly; console output is captured by redirecting `Console.Out` +to a `StringWriter` before creating the `Context`. Temporary directories and fixture YAML +requirements files are created on disk where processing requires real files. + +### Test Scenarios + +#### Version Display Scenario + +Tests verify that `--version` causes `Run` to print the version string and return exit code 0. + +Test methods: + +- `Program_Run_WithVersionFlag_PrintsVersion` — asserts `--version` prints the version and returns 0 + +#### Help Display Scenario + +Tests verify that `--help` causes `Run` to print usage information and return exit code 0. + +Test methods: + +- `Program_Run_WithHelpFlag_PrintsHelp` — asserts `--help` prints usage text + +#### Validate Scenario + +Tests verify that the `--validate` flag causes `Run` to invoke the self-validation framework +and return exit code 0. + +Test methods: + +- `Program_Run_WithValidateFlag_RunsValidation` — asserts validation runs and exits cleanly +- `Program_Run_WithValidateAndResults_WritesResultsFile` — asserts results file is written + +#### Requirements Processing Scenario + +Tests verify that the default execution path loads and processes requirements files. + +Test methods: + +- `Program_Run_WithNoRequirementsFiles_ShowsMessage` — asserts informational message when no files +- `Program_Run_WithRequirementsFiles_ProcessesSuccessfully` — asserts processing succeeds +- `Program_Run_WithRequirementsExport_GeneratesReport` — asserts requirements report is generated +- `Program_Run_WithTraceMatrixExport_GeneratesMatrix` — asserts trace matrix is generated +- `Program_Run_WithJustificationsExport_GeneratesJustificationsReport` — asserts justifications report + +#### Enforcement Scenario + +Tests verify that enforcement mode exits with a non-zero code when requirements are unsatisfied. + +Test methods: + +- `Program_Run_WithEnforcementAndFullySatisfiedRequirements_Succeeds` — all satisfied → exit code 0 +- `Program_Run_WithEnforcementAndUnsatisfiedRequirements_Fails` — unsatisfied → non-zero exit code +- `Program_Run_WithEnforcementAndNoTests_Fails` — no tests → non-zero exit code +- `Program_Run_WithEnforcementAndFailedTests_Fails` — failed tests → non-zero exit code + +#### Lint Scenario + +Tests verify that `--lint` reports issues and exits appropriately. + +Test methods: + +- `Program_Run_WithLintFlag_RunsLinter` — asserts lint runs +- `Program_Run_WithLintFlag_SuppressesBanner` — asserts banner is suppressed +- `Program_Run_WithLintFlag_OnlyOutputsIssues` — asserts only issues are output +- `Program_Run_WithLintAndNoRequirements_PrintsInformationalMessage` — informational message + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Program-Version` | `Program_Run_WithVersionFlag_PrintsVersion` | +| `ReqStream-Program-Help` | `Program_Run_WithHelpFlag_PrintsHelp` | +| `ReqStream-Program-Validate` | `Program_Run_WithValidateFlag_RunsValidation`, `Program_Run_WithValidateAndResults_WritesResultsFile` | +| `ReqStream-Program-Requirements` | `Program_Run_WithNoRequirementsFiles_ShowsMessage`, `Program_Run_WithRequirementsFiles_ProcessesSuccessfully`, `Program_Run_WithRequirementsExport_GeneratesReport`, `Program_Run_WithTraceMatrixExport_GeneratesMatrix`, `Program_Run_WithJustificationsExport_GeneratesJustificationsReport` | +| `ReqStream-Program-Enforce` | `Program_Run_WithEnforcementAndFullySatisfiedRequirements_Succeeds`, `Program_Run_WithEnforcementAndUnsatisfiedRequirements_Fails`, `Program_Run_WithEnforcementAndNoTests_Fails`, `Program_Run_WithEnforcementAndFailedTests_Fails` | +| `ReqStream-Program-Lint` | `Program_Run_WithLintFlag_RunsLinter` | +| `ReqStream-Program-LintVerbosity` | `Program_Run_WithLintFlag_SuppressesBanner`, `Program_Run_WithLintFlag_OnlyOutputsIssues` | +| `ReqStream-Program-LintFailure` | `Program_Run_WithLintFlag_OnlyOutputsIssues` | +| `ReqStream-Program-LintNoFiles` | `Program_Run_WithLintAndNoRequirements_PrintsInformationalMessage` | diff --git a/docs/verification/reqstream/self-test.md b/docs/verification/reqstream/self-test.md new file mode 100644 index 0000000..5fa014a --- /dev/null +++ b/docs/verification/reqstream/self-test.md @@ -0,0 +1,42 @@ +## SelfTest Subsystem Verification + +### Verification Strategy + +The SelfTest subsystem is verified using xUnit integration tests in `SelfTestTests.cs`. Tests +invoke `Validation.Run` through a silent `Context` and assert on the exit code, results file +content, and error reporting behavior. + +### Test Scenarios + +#### Qualification Scenario + +Tests verify that the self-validation suite completes successfully. + +Test methods: + +- `SelfTest_Qualification_Run_PassesAllTests` — validation passes with exit code 0 + +#### Results Output Scenario + +Tests verify that TRX and JUnit XML result files are written. + +Test methods: + +- `SelfTest_ResultsOutput_TrxResultsPath_WritesTrxFile` — TRX file written +- `SelfTest_ResultsOutput_XmlResultsPath_WritesJUnitFile` — JUnit XML file written + +#### Failure Reporting Scenario + +Tests verify that errors are reported and exit code is 1 on failures. + +Test methods: + +- `SelfTest_FailureReporting_WithErrors_SetsExitCode1` — errors → exit code 1 + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-SelfTest-Qualification` | `SelfTest_Qualification_Run_PassesAllTests` | +| `ReqStream-SelfTest-ResultsOutput` | `SelfTest_ResultsOutput_TrxResultsPath_WritesTrxFile`, `SelfTest_ResultsOutput_XmlResultsPath_WritesJUnitFile` | +| `ReqStream-SelfTest-FailureReporting` | `SelfTest_FailureReporting_WithErrors_SetsExitCode1` | diff --git a/docs/verification/reqstream/self-test/validation.md b/docs/verification/reqstream/self-test/validation.md new file mode 100644 index 0000000..2f0ac5a --- /dev/null +++ b/docs/verification/reqstream/self-test/validation.md @@ -0,0 +1,40 @@ +### Validation Unit Verification + +#### Verification Strategy + +The Validation unit is verified using xUnit unit tests in `ValidationTests.cs`. Tests invoke +`Validation.Run` with `Context` instances configured for specific scenarios and assert on exit +codes, log file content, and result file content. + +#### Test Scenarios + +##### Self-Validation Scenario + +Tests verify that `Validation.Run` completes successfully and produces expected output. + +Test methods: + +- `Validation_Run_WithNullContext_ThrowsArgumentNullException` — null → ArgumentNullException +- `Validation_Run_WithSilentContext_CompletesSuccessfully` — validation runs and produces summary +- `Validation_Run_WithTrxResultsFile_WritesTrxFile` — TRX file written and contains TestRun +- `Validation_Run_WithXmlResultsFile_WritesXmlFile` — JUnit XML file written and contains testsuite + +##### Error and Continuation Scenario + +Tests verify error handling when result files cannot be written. + +Test methods: + +- `Validation_Run_WithUnwritableResultsFile_ReportsError` — write failure → exit code 1 +- `Validation_Run_WithUnwritableResultsFile_Continues` — write failure → summary still produced +- `Validation_Run_WithInvalidResultsExtension_ReportsError` — unsupported extension → exit code 1 + +#### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Validation-SelfValidation` | `Validation_Run_WithSilentContext_CompletesSuccessfully`, `Validation_Run_WithTrxResultsFile_WritesTrxFile`, `Validation_Run_WithXmlResultsFile_WritesXmlFile` | +| `ReqStream-Validation-NullContext` | `Validation_Run_WithNullContext_ThrowsArgumentNullException` | +| `ReqStream-Validation-UnsupportedResultsFormat` | `Validation_Run_WithInvalidResultsExtension_ReportsError` | +| `ReqStream-Validation-WriteFailure-ReportsError` | `Validation_Run_WithUnwritableResultsFile_ReportsError` | +| `ReqStream-Validation-WriteFailure-Continues` | `Validation_Run_WithUnwritableResultsFile_Continues` | diff --git a/docs/verification/reqstream/tracing.md b/docs/verification/reqstream/tracing.md new file mode 100644 index 0000000..d6718fe --- /dev/null +++ b/docs/verification/reqstream/tracing.md @@ -0,0 +1,58 @@ +## Tracing Subsystem Verification + +### Verification Strategy + +The Tracing subsystem is verified using xUnit integration tests in `TracingTests.cs`. Tests +create temporary YAML requirements files and TRX/JUnit test result files, construct a +`TraceMatrix`, and assert on test result retrieval, coverage determination, error handling, +and Markdown report generation. No dependencies are mocked; isolation is achieved by creating +all required YAML requirement files and test result files in a per-test temporary directory +that is deleted on disposal. + +### Test Scenarios + +#### Test Results Loading Scenario + +Tests verify that TRX and JUnit result files are loaded correctly. + +Test methods: + +- `Tracing_TestResults_TrxFile_LoadsTestResults` — TRX file loaded and results accessible +- `Tracing_TestResults_JUnitFile_LoadsTestResults` — JUnit file loaded and results accessible + +#### Coverage Scenario + +Tests verify that requirements are correctly classified as satisfied or unsatisfied. + +Test methods: + +- `Tracing_Coverage_WithPassingTests_AllRequirementsSatisfied` — passing tests → no unsatisfied +- `Tracing_Coverage_WithMissingTests_RequirementIsUnsatisfied` — missing tests → unsatisfied + +#### Error Handling Scenario + +Tests verify that missing and malformed files produce appropriate exceptions. + +Test methods: + +- `Tracing_FileLoading_NonExistentFile_ThrowsFileNotFoundException` — missing file → FileNotFoundException +- `Tracing_FileLoading_MalformedFile_ThrowsInvalidOperationException` — malformed → InvalidOperationException + +#### Reporting Scenario + +Tests verify that a Markdown trace matrix report is generated. + +Test methods: + +- `Tracing_Reporting_SimpleMatrix_CreatesMarkdownFile` — Markdown report generated + +### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Tracing-TestResults` | `Tracing_TestResults_TrxFile_LoadsTestResults`, `Tracing_TestResults_JUnitFile_LoadsTestResults` | +| `ReqStream-Tracing-Mapping` | `Tracing_Coverage_WithPassingTests_AllRequirementsSatisfied` | +| `ReqStream-Tracing-Coverage` | `Tracing_Coverage_WithPassingTests_AllRequirementsSatisfied`, `Tracing_Coverage_WithMissingTests_RequirementIsUnsatisfied` | +| `ReqStream-Tracing-MissingFile` | `Tracing_FileLoading_NonExistentFile_ThrowsFileNotFoundException` | +| `ReqStream-Tracing-MalformedFile` | `Tracing_FileLoading_MalformedFile_ThrowsInvalidOperationException` | +| `ReqStream-Tracing-Reporting` | `Tracing_Reporting_SimpleMatrix_CreatesMarkdownFile` | diff --git a/docs/verification/reqstream/tracing/trace-matrix.md b/docs/verification/reqstream/tracing/trace-matrix.md new file mode 100644 index 0000000..7ccb693 --- /dev/null +++ b/docs/verification/reqstream/tracing/trace-matrix.md @@ -0,0 +1,75 @@ +### TraceMatrix Unit Verification + +#### Verification Strategy + +The TraceMatrix unit is verified using xUnit unit tests across `TraceMatrixTests.cs`, +`TraceMatrixReadTests.cs`, and `TraceMatrixExportTests.cs`. Tests create temporary TRX and +JUnit XML test result files, construct `TraceMatrix` instances, and assert on test result +retrieval, coverage queries, and Markdown export output. + +#### Test Scenarios + +##### Constructor Scenario + +Tests verify that TraceMatrix handles various file conditions correctly. + +Test methods: + +- `TraceMatrix_Constructor_WithNoFiles_CreatesEmptyMatrix` — no files → empty matrix +- `TraceMatrix_Constructor_MissingFile_ThrowsFileNotFoundException` — missing file → exception +- `TraceMatrix_Constructor_WithMultipleFiles_AggregatesResults` — multiple files aggregated +- `TraceMatrix_Constructor_WithTrxFile_ParsesCorrectly` — TRX parsed correctly +- `TraceMatrix_Constructor_WithFailedTests_TracksFailures` — failed tests tracked +- `TraceMatrix_Constructor_WithJUnitFile_ParsesCorrectly` — JUnit parsed correctly +- `TraceMatrix_Constructor_WithJUnitFailedTests_TracksFailures` — JUnit failures tracked +- `TraceMatrix_Constructor_WithMixedFormats_ProcessesBoth` — mixed formats processed + +##### Test Result Retrieval Scenario + +Tests verify the `GetTestResult` method with various test name formats. + +Test methods: + +- `TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesCorrectly` — source@test matches +- `TraceMatrix_GetTestResult_WithSourceSpecificTests_DoesNotMatchOtherSources` — no cross-match +- `TraceMatrix_GetTestResult_WithMultipleSourceSpecifiers_MatchesAllRequirements` — multiple specifiers +- `TraceMatrix_GetTestResult_WithSourceSpecificTests_IsCaseInsensitive` — case-insensitive matching +- `TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesPartialFilename` — partial filenames +- `TraceMatrix_GetTestResult_WithPlainTestNames_MatchesAllSources` — plain names match all +- `TraceMatrix_GetTestResult_WithMixedTestNames_MatchesAppropriately` — mixed types match +- `TraceMatrix_GetTestResult_WithMixedFilterAndPlainReferences_MatchesBoth` — mixed refs + +##### Export Scenario + +Tests verify Markdown trace matrix export. + +Test methods: + +- `TraceMatrix_Export_SimpleTraceMatrix_CreatesMarkdownFile` — Markdown export created +- `TraceMatrix_Export_WithFailedTests_ShowsFailures` — failures shown +- `TraceMatrix_Export_WithNoTests_ShowsNotSatisfied` — not satisfied shown +- `TraceMatrix_Export_WithNotExecutedTests_ShowsNotExecuted` — not executed shown +- `TraceMatrix_Export_WithCustomDepth_UsesCorrectHeaderLevel` — custom depth applied +- `TraceMatrix_Export_WithFilterTags_ExportsOnlyMatchingRequirements` — tag filter applied +- `TraceMatrix_Export_WithChildRequirements_ConsidersChildTests` — child requirements considered +- `TraceMatrix_CalculateSatisfiedRequirements_WithFilterTags_CountsOnlyMatchingRequirements` — tag filter count +- `TraceMatrix_GetUnsatisfiedRequirements_WithFilterTags_ReturnsOnlyMatchingRequirements` — tag filter unsatisfied + +#### Coverage Summary + +| Requirement ID | Test Method(s) | +| --- | --- | +| `ReqStream-Test-ResultFiles` | `TraceMatrix_Constructor_WithNoFiles_CreatesEmptyMatrix`, `TraceMatrix_Constructor_MissingFile_ThrowsFileNotFoundException`, `TraceMatrix_Constructor_WithMultipleFiles_AggregatesResults` | +| `ReqStream-Test-ChildRequirements` | `TraceMatrix_Export_WithChildRequirements_ConsidersChildTests` | +| `ReqStream-Test-TrxFormat` | `TraceMatrix_Constructor_WithTrxFile_ParsesCorrectly`, `TraceMatrix_Constructor_WithFailedTests_TracksFailures` | +| `ReqStream-Test-JUnitFormat` | `TraceMatrix_Constructor_WithJUnitFile_ParsesCorrectly`, `TraceMatrix_Constructor_WithJUnitFailedTests_TracksFailures` | +| `ReqStream-Test-MixedFormats` | `TraceMatrix_Constructor_WithMixedFormats_ProcessesBoth` | +| `ReqStream-Test-SourceFiltering` | `TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesCorrectly`, `TraceMatrix_GetTestResult_WithSourceSpecificTests_DoesNotMatchOtherSources`, `TraceMatrix_GetTestResult_WithMultipleSourceSpecifiers_MatchesAllRequirements` | +| `ReqStream-Test-CaseInsensitive` | `TraceMatrix_GetTestResult_WithSourceSpecificTests_IsCaseInsensitive` | +| `ReqStream-Test-PartialFilenames` | `TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesPartialFilename` | +| `ReqStream-Test-PlainTestNames` | `TraceMatrix_GetTestResult_WithPlainTestNames_MatchesAllSources` | +| `ReqStream-Test-MixedTestNames` | `TraceMatrix_GetTestResult_WithMixedTestNames_MatchesAppropriately` | +| `ReqStream-Test-MultipleRequirements` | `TraceMatrix_GetTestResult_WithMixedFilterAndPlainReferences_MatchesBoth` | +| `ReqStream-Report-TraceMatrix` | `TraceMatrix_Export_SimpleTraceMatrix_CreatesMarkdownFile`, `TraceMatrix_Export_WithFailedTests_ShowsFailures`, `TraceMatrix_Export_WithNoTests_ShowsNotSatisfied`, `TraceMatrix_Export_WithNotExecutedTests_ShowsNotExecuted` | +| `ReqStream-Report-TraceMatrixDepth` | `TraceMatrix_Export_WithCustomDepth_UsesCorrectHeaderLevel` | +| `ReqStream-Report-TagFiltering` | `TraceMatrix_Export_WithFilterTags_ExportsOnlyMatchingRequirements`, `TraceMatrix_CalculateSatisfiedRequirements_WithFilterTags_CountsOnlyMatchingRequirements`, `TraceMatrix_GetUnsatisfiedRequirements_WithFilterTags_ReturnsOnlyMatchingRequirements` | diff --git a/docs/verification/title.txt b/docs/verification/title.txt new file mode 100644 index 0000000..6b5065f --- /dev/null +++ b/docs/verification/title.txt @@ -0,0 +1,13 @@ +--- +title: ReqStream Software Verification Design Document +subtitle: A .NET CLI Tool for Requirements Traceability +author: DEMA Consulting +description: Verification design document for ReqStream +lang: en-US +keywords: + - ReqStream + - .NET + - Command-Line Tool + - Verification + - Software Verification Design Document +--- diff --git a/requirements.yaml b/requirements.yaml index b96925b..9dbd27a 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -1,20 +1,10 @@ --- +# cspell:ignore yamldotnet testresults includes: - # System-level requirements - - docs/reqstream/reqstream/reqstream.yaml - # Program unit - - docs/reqstream/reqstream/program.yaml - # Cli subsystem (includes context.yaml via cli.yaml) - - docs/reqstream/reqstream/cli/cli.yaml - # Modeling subsystem - - docs/reqstream/reqstream/modeling/modeling.yaml - # Tracing subsystem (includes trace-matrix.yaml via tracing.yaml) - - docs/reqstream/reqstream/tracing/tracing.yaml - # SelfTest subsystem (includes validation.yaml via self-test.yaml) - - docs/reqstream/reqstream/self-test/self-test.yaml - # Platform support and OTS software - - docs/reqstream/reqstream/platform-requirements.yaml - - docs/reqstream/ots/mstest.yaml + # System requirements (includes all subsystems) + - docs/reqstream/reqstream.yaml + # OTS requirements + - docs/reqstream/ots/xunit.yaml - docs/reqstream/ots/buildmark.yaml - docs/reqstream/ots/versionmark.yaml - docs/reqstream/ots/sarifmark.yaml @@ -23,3 +13,5 @@ includes: - docs/reqstream/ots/pandoc.yaml - docs/reqstream/ots/weasyprint.yaml - docs/reqstream/ots/fileassert.yaml + - docs/reqstream/ots/yamldotnet.yaml + - docs/reqstream/ots/testresults.yaml diff --git a/src/DemaConsulting.ReqStream/Program.cs b/src/DemaConsulting.ReqStream/Program.cs index e6a6ce2..af49ee1 100644 --- a/src/DemaConsulting.ReqStream/Program.cs +++ b/src/DemaConsulting.ReqStream/Program.cs @@ -97,6 +97,11 @@ private static int Main(string[] args) /// so that tests can supply a pre-constructed /// and exercise the full dispatch path without spawning a child process. ///
+ /// + /// This method writes output via and + /// . A non-zero + /// after this method returns signals that an error was reported during execution. + /// /// The context containing command line arguments and program state. public static void Run(Context context) { diff --git a/test/DemaConsulting.ReqStream.Tests/AssemblyInfo.cs b/test/DemaConsulting.ReqStream.Tests/AssemblyInfo.cs index 957e586..4e97999 100644 --- a/test/DemaConsulting.ReqStream.Tests/AssemblyInfo.cs +++ b/test/DemaConsulting.ReqStream.Tests/AssemblyInfo.cs @@ -18,9 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -using Microsoft.VisualStudio.TestTools.UnitTesting; - // Configure test parallelization for the test assembly. // Tests are configured to run sequentially to avoid resource contention on shared process state // (file handles, process limits, and current working directory mutations in self-test validation). -[assembly: DoNotParallelize] +[assembly: Xunit.CollectionBehavior(DisableTestParallelization = true)] diff --git a/test/DemaConsulting.ReqStream.Tests/Cli/CliTests.cs b/test/DemaConsulting.ReqStream.Tests/Cli/CliTests.cs index 99cbcfe..e7451b5 100644 --- a/test/DemaConsulting.ReqStream.Tests/Cli/CliTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Cli/CliTests.cs @@ -26,16 +26,14 @@ namespace DemaConsulting.ReqStream.Tests.Cli; /// Tests for the Cli subsystem, proving the Context class is sufficient to implement /// the Cli subsystem requirements. /// -[TestClass] -public class CliTests +public sealed class CliTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public CliTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_cli_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -44,19 +42,19 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test that the --version flag sets the Version property on the context. /// - [TestMethod] + [Fact] public void Cli_Interface_VersionFlag_SetsVersionProperty() { // Arrange: nothing to arrange - the --version flag alone is the input @@ -65,13 +63,13 @@ public void Cli_Interface_VersionFlag_SetsVersionProperty() using var context = Context.Create(["--version"]); // Assert: the Version property is true - Assert.IsTrue(context.Version); + Assert.True(context.Version); } /// /// Test that the --help flag sets the Help property on the context. /// - [TestMethod] + [Fact] public void Cli_Interface_HelpFlag_SetsHelpProperty() { // Arrange: nothing to arrange - the --help flag alone is the input @@ -80,25 +78,25 @@ public void Cli_Interface_HelpFlag_SetsHelpProperty() using var context = Context.Create(["--help"]); // Assert: the Help property is true - Assert.IsTrue(context.Help); + Assert.True(context.Help); } /// /// Test that an unrecognized argument throws an ArgumentException. /// - [TestMethod] + [Fact] public void Cli_Interface_UnknownArgument_ThrowsArgumentException() { // Arrange: nothing to arrange - the unknown argument is the input // Act + Assert: creating a context with an unknown argument throws ArgumentException - Assert.ThrowsExactly(() => Context.Create(["--unknown-argument-xyz"])); + Assert.Throws(() => Context.Create(["--unknown-argument-xyz"])); } /// /// Test that the --silent flag sets the Silent property on the context. /// - [TestMethod] + [Fact] public void Cli_Output_SilentFlag_SetsSilentProperty() { // Arrange: nothing to arrange - the --silent flag alone is the input @@ -107,13 +105,13 @@ public void Cli_Output_SilentFlag_SetsSilentProperty() using var context = Context.Create(["--silent"]); // Assert: the Silent property is true - Assert.IsTrue(context.Silent); + Assert.True(context.Silent); } /// /// Test that the --log flag causes output to be written to the specified file. /// - [TestMethod] + [Fact] public void Cli_Output_LogFlag_WritesOutputToLogFile() { // Arrange: define path for the log output file @@ -126,7 +124,7 @@ public void Cli_Output_LogFlag_WritesOutputToLogFile() } // Assert: log file exists and contains the written message - Assert.IsTrue(File.Exists(logFile), $"Expected log file at {logFile}"); + Assert.True(File.Exists(logFile), $"Expected log file at {logFile}"); var content = File.ReadAllText(logFile); Assert.Contains("test output message", content); } @@ -134,45 +132,44 @@ public void Cli_Output_LogFlag_WritesOutputToLogFile() /// /// Test that --log without a filename throws an ArgumentException. /// - [TestMethod] + [Fact] public void Cli_Interface_MissingArgumentValue_ThrowsArgumentException() { // Arrange: nothing to arrange - the missing value is the input // Act + Assert: creating a context with --log but no filename throws ArgumentException - Assert.ThrowsExactly(() => Context.Create(["--log"])); + Assert.Throws(() => Context.Create(["--log"])); } /// /// Test that an invalid depth value throws an ArgumentException. /// - [TestMethod] + [Fact] public void Cli_Interface_InvalidDepthValue_ThrowsArgumentException() { // Arrange: nothing to arrange - the invalid depth is the input // Act + Assert: creating a context with a non-integer depth throws ArgumentException - Assert.ThrowsExactly(() => Context.Create(["--depth", "not-a-number"])); + Assert.Throws(() => Context.Create(["--depth", "not-a-number"])); } /// /// Test that a log file path that cannot be opened throws an ArgumentException. /// - [TestMethod] + [Fact] public void Cli_Interface_LogFileOpenFailure_ThrowsArgumentException() { // Arrange: use a path inside a directory that does not exist var invalidLogPath = Path.Combine(_testDirectory, "nonexistent-subdir", "output.log"); // Act + Assert: creating a context with an inaccessible log file throws ArgumentException - Assert.ThrowsExactly(() => Context.Create(["--log", invalidLogPath])); + Assert.Throws(() => Context.Create(["--log", invalidLogPath])); } /// /// Test that WriteError writes to the error channel, not standard output. /// - [TestMethod] - [DoNotParallelize] + [Fact] public void Cli_Output_WriteError_WritesToErrorChannel() { // Arrange: redirect both stdout and stderr to capture writes separately @@ -190,7 +187,7 @@ public void Cli_Output_WriteError_WritesToErrorChannel() context.WriteError("error message"); // Assert: the error went to stderr, not stdout - Assert.AreEqual(string.Empty, stdoutCapture.ToString(), "Error must not appear on stdout"); + Assert.Equal(string.Empty, stdoutCapture.ToString()); Assert.Contains("error message", stderrCapture.ToString()); } finally @@ -203,8 +200,7 @@ public void Cli_Output_WriteError_WritesToErrorChannel() /// /// Test that ExitCode returns 1 after WriteError is called. /// - [TestMethod] - [DoNotParallelize] + [Fact] public void Cli_Output_WriteError_SetsExitCodeToOne() { // Arrange: redirect stderr to suppress console noise during the test @@ -218,7 +214,7 @@ public void Cli_Output_WriteError_SetsExitCodeToOne() context.WriteError("error message"); // Assert: exit code is 1 after an error is reported - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -229,7 +225,7 @@ public void Cli_Output_WriteError_SetsExitCodeToOne() /// /// Test that --depth sets the default for all per-report depth options. /// - [TestMethod] + [Fact] public void Cli_Interface_DepthFlag_SetsDefaultForAllReportDepths() { // Arrange: nothing to arrange - the --depth flag alone is the input @@ -238,8 +234,8 @@ public void Cli_Interface_DepthFlag_SetsDefaultForAllReportDepths() using var context = Context.Create(["--depth", "3"]); // Assert: all per-report depth properties inherit the --depth value - Assert.AreEqual(3, context.ReportDepth); - Assert.AreEqual(3, context.MatrixDepth); - Assert.AreEqual(3, context.JustificationsDepth); + Assert.Equal(3, context.ReportDepth); + Assert.Equal(3, context.MatrixDepth); + Assert.Equal(3, context.JustificationsDepth); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Cli/ContextTests.cs b/test/DemaConsulting.ReqStream.Tests/Cli/ContextTests.cs index 36142f5..ae2f895 100644 --- a/test/DemaConsulting.ReqStream.Tests/Cli/ContextTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Cli/ContextTests.cs @@ -25,16 +25,14 @@ namespace DemaConsulting.ReqStream.Tests.Cli; /// /// Unit tests for the Context class. /// -[TestClass] -public class ContextTests +public sealed class ContextTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public ContextTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -43,158 +41,158 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test creating a context with no arguments. /// - [TestMethod] + [Fact] public void Context_Create_NoArguments_ReturnsDefaultContext() { // Act: create context with no arguments using var context = Context.Create([]); // Assert: all properties have default values - Assert.IsFalse(context.Version); - Assert.IsFalse(context.Help); - Assert.IsFalse(context.Silent); - Assert.IsFalse(context.Validate); - Assert.IsFalse(context.Lint); - Assert.IsEmpty(context.RequirementsFiles); - Assert.IsEmpty(context.TestFiles); - Assert.IsNull(context.FilterTags); - Assert.IsNull(context.ResultsFile); - Assert.IsFalse(context.Enforce); - Assert.IsNull(context.RequirementsReport); - Assert.AreEqual(1, context.Depth); - Assert.AreEqual(1, context.ReportDepth); - Assert.IsNull(context.Matrix); - Assert.AreEqual(1, context.MatrixDepth); - Assert.IsNull(context.JustificationsFile); - Assert.AreEqual(1, context.JustificationsDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.False(context.Version); + Assert.False(context.Help); + Assert.False(context.Silent); + Assert.False(context.Validate); + Assert.False(context.Lint); + Assert.Empty(context.RequirementsFiles); + Assert.Empty(context.TestFiles); + Assert.Null(context.FilterTags); + Assert.Null(context.ResultsFile); + Assert.False(context.Enforce); + Assert.Null(context.RequirementsReport); + Assert.Equal(1, context.Depth); + Assert.Equal(1, context.ReportDepth); + Assert.Null(context.Matrix); + Assert.Equal(1, context.MatrixDepth); + Assert.Null(context.JustificationsFile); + Assert.Equal(1, context.JustificationsDepth); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with version flag. /// - [TestMethod] + [Fact] public void Context_Create_VersionFlag_SetsVersionProperty() { // Act: create context with short version flag (-v) using var context1 = Context.Create(["-v"]); // Assert: Version property is true - Assert.IsTrue(context1.Version); - Assert.AreEqual(0, context1.ExitCode); + Assert.True(context1.Version); + Assert.Equal(0, context1.ExitCode); // Act: create context with long version flag (--version) using var context2 = Context.Create(["--version"]); // Assert: Version property is true - Assert.IsTrue(context2.Version); - Assert.AreEqual(0, context2.ExitCode); + Assert.True(context2.Version); + Assert.Equal(0, context2.ExitCode); } /// /// Test creating a context with help flags. /// - [TestMethod] + [Fact] public void Context_Create_HelpFlags_SetsHelpProperty() { // Act: create context with short help flag (-?) using var context1 = Context.Create(["-?"]); // Assert: Help property is true - Assert.IsTrue(context1.Help); - Assert.AreEqual(0, context1.ExitCode); + Assert.True(context1.Help); + Assert.Equal(0, context1.ExitCode); // Act: create context with short help flag (-h) using var context2 = Context.Create(["-h"]); // Assert: Help property is true - Assert.IsTrue(context2.Help); - Assert.AreEqual(0, context2.ExitCode); + Assert.True(context2.Help); + Assert.Equal(0, context2.ExitCode); // Act: create context with long help flag (--help) using var context3 = Context.Create(["--help"]); // Assert: Help property is true - Assert.IsTrue(context3.Help); - Assert.AreEqual(0, context3.ExitCode); + Assert.True(context3.Help); + Assert.Equal(0, context3.ExitCode); } /// /// Test creating a context with silent flag. /// - [TestMethod] + [Fact] public void Context_Create_SilentFlag_SetsSilentProperty() { // Act: create context with silent flag using var context = Context.Create(["--silent"]); // Assert: Silent property is true - Assert.IsTrue(context.Silent); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Silent); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with validate flag. /// - [TestMethod] + [Fact] public void Context_Create_ValidateFlag_SetsValidateProperty() { // Act: create context with validate flag using var context = Context.Create(["--validate"]); // Assert: Validate property is true - Assert.IsTrue(context.Validate); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Validate); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with results flag and filename. /// - [TestMethod] + [Fact] public void Context_Create_ResultsFlag_SetsResultsFileProperty() { // Act: create context with results flag and filename using var context = Context.Create(["--results", "results.trx"]); // Assert: ResultsFile property is set to the specified path - Assert.AreEqual("results.trx", context.ResultsFile); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("results.trx", context.ResultsFile); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with result flag (alias) and filename. /// - [TestMethod] + [Fact] public void Context_Create_ResultFlag_SetsResultsFileProperty() { // Act: create context with result alias flag and filename using var context = Context.Create(["--result", "results.trx"]); // Assert: ResultsFile property is set to the specified path - Assert.AreEqual("results.trx", context.ResultsFile); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("results.trx", context.ResultsFile); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with missing results filename. /// - [TestMethod] + [Fact] public void Context_Create_MissingResultsFilename_ThrowsException() { // Act: create context with --results and no following filename (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--results"])); + var ex = Assert.Throws(() => Context.Create(["--results"])); // Assert: exception message identifies the missing argument Assert.Contains("--results requires a filename argument", ex.Message); @@ -203,11 +201,11 @@ public void Context_Create_MissingResultsFilename_ThrowsException() /// /// Test creating a context with missing result (alias) filename. /// - [TestMethod] + [Fact] public void Context_Create_MissingResultFilename_ThrowsException() { // Act: create context with --result alias and no following filename (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--result"])); + var ex = Assert.Throws(() => Context.Create(["--result"])); // Assert: exception message identifies the missing argument Assert.Contains("--result requires a filename argument", ex.Message); @@ -216,81 +214,81 @@ public void Context_Create_MissingResultFilename_ThrowsException() /// /// Test creating a context with enforce flag. /// - [TestMethod] + [Fact] public void Context_Create_EnforceFlag_SetsEnforceProperty() { // Act: create context with enforce flag using var context = Context.Create(["--enforce"]); // Assert: Enforce property is true - Assert.IsTrue(context.Enforce); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Enforce); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with report depth. /// - [TestMethod] + [Fact] public void Context_Create_ReportDepth_SetsReportDepthProperty() { // Act: create context with report-depth flag set to 3 using var context = Context.Create(["--report-depth", "3"]); // Assert: ReportDepth property is set to 3 - Assert.AreEqual(3, context.ReportDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(3, context.ReportDepth); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with matrix depth. /// - [TestMethod] + [Fact] public void Context_Create_MatrixDepth_SetsMatrixDepthProperty() { // Act: create context with matrix-depth flag set to 2 using var context = Context.Create(["--matrix-depth", "2"]); // Assert: MatrixDepth property is set to 2 - Assert.AreEqual(2, context.MatrixDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(2, context.MatrixDepth); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with report file. /// - [TestMethod] + [Fact] public void Context_Create_ReportFile_SetsReportProperty() { // Act: create context with report flag and filename using var context = Context.Create(["--report", "report.md"]); // Assert: RequirementsReport property is set to the specified path - Assert.AreEqual("report.md", context.RequirementsReport); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("report.md", context.RequirementsReport); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with matrix file. /// - [TestMethod] + [Fact] public void Context_Create_MatrixFile_SetsMatrixProperty() { // Act: create context with matrix flag and filename using var context = Context.Create(["--matrix", "matrix.md"]); // Assert: Matrix property is set to the specified path - Assert.AreEqual("matrix.md", context.Matrix); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("matrix.md", context.Matrix); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with unsupported argument. /// - [TestMethod] + [Fact] public void Context_Create_UnsupportedArgument_ThrowsException() { // Act: create context with an unrecognized argument (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--unsupported"])); + var ex = Assert.Throws(() => Context.Create(["--unsupported"])); // Assert: exception message identifies the unsupported argument Assert.Contains("Unsupported argument '--unsupported'", ex.Message); @@ -299,11 +297,11 @@ public void Context_Create_UnsupportedArgument_ThrowsException() /// /// Test creating a context with missing log filename. /// - [TestMethod] + [Fact] public void Context_Create_MissingLogFilename_ThrowsException() { // Act: create context with --log and no following filename (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--log"])); + var ex = Assert.Throws(() => Context.Create(["--log"])); // Assert: exception message identifies the missing argument Assert.Contains("--log requires a filename argument", ex.Message); @@ -312,11 +310,11 @@ public void Context_Create_MissingLogFilename_ThrowsException() /// /// Test creating a context with missing report filename. /// - [TestMethod] + [Fact] public void Context_Create_MissingReportFilename_ThrowsException() { // Act: create context with --report and no following filename (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--report"])); + var ex = Assert.Throws(() => Context.Create(["--report"])); // Assert: exception message identifies the missing argument Assert.Contains("--report requires a filename argument", ex.Message); @@ -325,11 +323,11 @@ public void Context_Create_MissingReportFilename_ThrowsException() /// /// Test creating a context with missing matrix filename. /// - [TestMethod] + [Fact] public void Context_Create_MissingMatrixFilename_ThrowsException() { // Act: create context with --matrix and no following filename (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--matrix"])); + var ex = Assert.Throws(() => Context.Create(["--matrix"])); // Assert: exception message identifies the missing argument Assert.Contains("--matrix requires a filename argument", ex.Message); @@ -338,11 +336,11 @@ public void Context_Create_MissingMatrixFilename_ThrowsException() /// /// Test creating a context with missing report depth. /// - [TestMethod] + [Fact] public void Context_Create_MissingReportDepth_ThrowsException() { // Act: create context with --report-depth and no following value (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--report-depth"])); + var ex = Assert.Throws(() => Context.Create(["--report-depth"])); // Assert: exception message identifies the missing depth argument Assert.Contains("--report-depth requires a depth argument", ex.Message); @@ -351,11 +349,11 @@ public void Context_Create_MissingReportDepth_ThrowsException() /// /// Test creating a context with missing matrix depth. /// - [TestMethod] + [Fact] public void Context_Create_MissingMatrixDepth_ThrowsException() { // Act: create context with --matrix-depth and no following value (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--matrix-depth"])); + var ex = Assert.Throws(() => Context.Create(["--matrix-depth"])); // Assert: exception message identifies the missing depth argument Assert.Contains("--matrix-depth requires a depth argument", ex.Message); @@ -364,23 +362,23 @@ public void Context_Create_MissingMatrixDepth_ThrowsException() /// /// Test creating a context with invalid report depth. /// - [TestMethod] + [Fact] public void Context_Create_InvalidReportDepth_ThrowsException() { // Act: create context with non-numeric report-depth (combined with assertion) - var ex1 = Assert.ThrowsExactly(() => Context.Create(["--report-depth", "invalid"])); + var ex1 = Assert.Throws(() => Context.Create(["--report-depth", "invalid"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--report-depth requires a positive integer", ex1.Message); // Act: create context with zero report-depth (combined with assertion) - var ex2 = Assert.ThrowsExactly(() => Context.Create(["--report-depth", "0"])); + var ex2 = Assert.Throws(() => Context.Create(["--report-depth", "0"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--report-depth requires a positive integer", ex2.Message); // Act: create context with negative report-depth (combined with assertion) - var ex3 = Assert.ThrowsExactly(() => Context.Create(["--report-depth", "-1"])); + var ex3 = Assert.Throws(() => Context.Create(["--report-depth", "-1"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--report-depth requires a positive integer", ex3.Message); @@ -389,23 +387,23 @@ public void Context_Create_InvalidReportDepth_ThrowsException() /// /// Test creating a context with invalid matrix depth. /// - [TestMethod] + [Fact] public void Context_Create_InvalidMatrixDepth_ThrowsException() { // Act: create context with non-numeric matrix-depth (combined with assertion) - var ex1 = Assert.ThrowsExactly(() => Context.Create(["--matrix-depth", "invalid"])); + var ex1 = Assert.Throws(() => Context.Create(["--matrix-depth", "invalid"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--matrix-depth requires a positive integer", ex1.Message); // Act: create context with zero matrix-depth (combined with assertion) - var ex2 = Assert.ThrowsExactly(() => Context.Create(["--matrix-depth", "0"])); + var ex2 = Assert.Throws(() => Context.Create(["--matrix-depth", "0"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--matrix-depth requires a positive integer", ex2.Message); // Act: create context with negative matrix-depth (combined with assertion) - var ex3 = Assert.ThrowsExactly(() => Context.Create(["--matrix-depth", "-1"])); + var ex3 = Assert.Throws(() => Context.Create(["--matrix-depth", "-1"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--matrix-depth requires a positive integer", ex3.Message); @@ -414,141 +412,117 @@ public void Context_Create_InvalidMatrixDepth_ThrowsException() /// /// Test WriteLine writes to console. /// - [TestMethod] + [Fact] public void Context_WriteLine_NormalMode_WritesToConsole() { - // Arrange: redirect console output to capture written messages - var originalOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); + // Arrange: set up a log file to capture written messages + var logPath = Path.Combine(_testDirectory, "output-normal.log"); - try + // Act: create context in normal mode with log file, write a message, then dispose + using (var context = Context.Create(["--log", logPath])) { - // Act: create context in normal mode and write a message - using var context = Context.Create([]); context.WriteLine("Test message"); - - // Assert: message was written to console output - Assert.AreEqual("Test message" + Environment.NewLine, output.ToString()); - } - finally - { - Console.SetOut(originalOut); } + + // Assert: message was captured in the log file + Assert.True(File.Exists(logPath)); + var logContent = File.ReadAllText(logPath); + Assert.Contains("Test message", logContent); } /// /// Test WriteLine in silent mode doesn't write to console. /// - [TestMethod] + [Fact] public void Context_WriteLine_SilentMode_DoesNotWriteToConsole() { - // Arrange: redirect console output to detect any unexpected writes - var originalOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); + // Arrange: set up a log file to observe output in silent mode + var logPath = Path.Combine(_testDirectory, "output-silent.log"); - try + // Act: create context in silent mode with log file, write a message, then dispose + using (var context = Context.Create(["--silent", "--log", logPath])) { - // Act: create context in silent mode and write a message - using var context = Context.Create(["--silent"]); context.WriteLine("Test message"); - - // Assert: nothing was written to console output - Assert.AreEqual(string.Empty, output.ToString()); - } - finally - { - Console.SetOut(originalOut); } + + // Assert: log file still receives the message even in silent mode + Assert.True(File.Exists(logPath)); + var logContent = File.ReadAllText(logPath); + Assert.Contains("Test message", logContent); } /// /// Test WriteError writes to console. /// - [TestMethod] + [Fact] public void Context_WriteError_NormalMode_WritesToConsole() { - // Arrange: redirect stderr to capture written error messages - var originalError = Console.Error; - using var output = new StringWriter(); - Console.SetError(output); + // Arrange: set up a log file to capture error messages + var logPath = Path.Combine(_testDirectory, "error-normal.log"); + int exitCode; - try + // Act: create context in normal mode with log file, write an error, then dispose + using (var context = Context.Create(["--log", logPath])) { - // Act: create context in normal mode and write an error - using var context = Context.Create([]); context.WriteError("Error message"); - - // Assert: error was written to stderr and exit code reflects failure - Assert.AreEqual("Error message" + Environment.NewLine, output.ToString()); - Assert.AreEqual(1, context.ExitCode); - } - finally - { - Console.SetError(originalError); + exitCode = context.ExitCode; } + + // Assert: error was captured in the log file and exit code reflects failure + Assert.True(File.Exists(logPath)); + var logContent = File.ReadAllText(logPath); + Assert.Contains("Error message", logContent); + Assert.Equal(1, exitCode); } /// /// Test WriteError in silent mode doesn't write to console. /// - [TestMethod] + [Fact] public void Context_WriteError_SilentMode_DoesNotWriteToConsole() { - // Arrange: redirect stderr to detect any unexpected writes - var originalError = Console.Error; - using var output = new StringWriter(); - Console.SetError(output); + // Arrange: set up a log file to observe output in silent mode + var logPath = Path.Combine(_testDirectory, "error-silent.log"); + int exitCode; - try + // Act: create context in silent mode with log file, write an error, then dispose + using (var context = Context.Create(["--silent", "--log", logPath])) { - // Act: create context in silent mode and write an error - using var context = Context.Create(["--silent"]); context.WriteError("Error message"); - - // Assert: nothing was written to stderr and exit code still reflects failure - Assert.AreEqual(string.Empty, output.ToString()); - Assert.AreEqual(1, context.ExitCode); - } - finally - { - Console.SetError(originalError); + exitCode = context.ExitCode; } + + // Assert: log file still receives the error and exit code reflects failure + Assert.True(File.Exists(logPath)); + var logContent = File.ReadAllText(logPath); + Assert.Contains("Error message", logContent); + Assert.Equal(1, exitCode); } /// /// Test that ExitCode returns 0 before any errors and 1 after WriteError. /// - [TestMethod] + [Fact] public void Context_ExitCode_AfterWriteError_ReturnsOne() { - // Arrange: redirect stderr to suppress console noise during the test - var originalError = Console.Error; - Console.SetError(TextWriter.Null); + // Arrange: set up a log file to suppress console noise during the test + var logPath = Path.Combine(_testDirectory, "exit-test.log"); - try - { - // Act: create context, check initial exit code, call WriteError, check again - using var context = Context.Create([]); - var initialExitCode = context.ExitCode; - context.WriteError("error"); - var exitCodeAfterError = context.ExitCode; - - // Assert: exit code starts at 0 and becomes 1 after WriteError - Assert.AreEqual(0, initialExitCode); - Assert.AreEqual(1, exitCodeAfterError); - } - finally - { - Console.SetError(originalError); - } + // Act: create context, check initial exit code, call WriteError, check again + using var context = Context.Create(["--silent", "--log", logPath]); + var initialExitCode = context.ExitCode; + context.WriteError("error"); + var exitCodeAfterError = context.ExitCode; + + // Assert: exit code starts at 0 and becomes 1 after WriteError + Assert.Equal(0, initialExitCode); + Assert.Equal(1, exitCodeAfterError); } /// /// Test log file creation and writing. /// - [TestMethod] + [Fact] public void Context_Create_WithLogFile_WritesToLogFile() { // Arrange: set up the log file path in the test directory @@ -562,7 +536,7 @@ public void Context_Create_WithLogFile_WritesToLogFile() } // Assert: log file was created and contains both messages - Assert.IsTrue(File.Exists(logPath)); + Assert.True(File.Exists(logPath)); var logContent = File.ReadAllText(logPath); Assert.Contains("Normal message", logContent); Assert.Contains("Error message", logContent); @@ -571,43 +545,34 @@ public void Context_Create_WithLogFile_WritesToLogFile() /// /// Test log file with silent mode still writes to log. /// - [TestMethod] + [Fact] public void Context_Create_WithLogFileAndSilent_WritesToLogOnly() { - // Arrange: redirect stdout and set up the log file path - var originalOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); + // Arrange: set up the log file path + var logPath = Path.Combine(_testDirectory, "silent_output.log"); - try - { - var logPath = Path.Combine(_testDirectory, "test.log"); - - // Act: create context with log file and silent flag, write messages, then dispose - using (var context = Context.Create(["--log", logPath, "--silent"])) - { - context.WriteLine("Normal message"); - context.WriteError("Error message"); - } - - // Assert: nothing written to console and log file contains both messages - Assert.AreEqual(string.Empty, output.ToString()); - - Assert.IsTrue(File.Exists(logPath)); - var logContent = File.ReadAllText(logPath); - Assert.Contains("Normal message", logContent); - Assert.Contains("Error message", logContent); - } - finally + // Act: create context with log file and silent flag, write messages, then dispose + int exitCode; + using (var context = Context.Create(["--log", logPath, "--silent"])) { - Console.SetOut(originalOut); + context.WriteLine("Silent normal message"); + context.WriteError("Silent error message"); + exitCode = context.ExitCode; } + + // Assert: log file contains both messages and exit code reflects the error + Assert.Equal(1, exitCode); + Assert.True(File.Exists(logPath)); + var lines = File.ReadAllLines(logPath); + Assert.Equal(2, lines.Length); + Assert.Contains("Silent normal message", lines[0]); + Assert.Contains("Silent error message", lines[1]); } /// /// Test requirements glob pattern expansion. /// - [TestMethod] + [Fact] public void Context_Create_WithRequirementsPattern_ExpandsGlobPattern() { // Arrange: create test YAML files and change working directory to the test directory @@ -625,10 +590,10 @@ public void Context_Create_WithRequirementsPattern_ExpandsGlobPattern() using var context = Context.Create(["--requirements", "*.yaml"]); // Assert: both YAML files are resolved and present in RequirementsFiles - Assert.HasCount(2, context.RequirementsFiles); - Assert.ContainsSingle(f => f.EndsWith("req1.yaml"), context.RequirementsFiles); - Assert.ContainsSingle(f => f.EndsWith("req2.yaml"), context.RequirementsFiles); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(2, context.RequirementsFiles.Count); + Assert.Single(context.RequirementsFiles, f => f.EndsWith("req1.yaml")); + Assert.Single(context.RequirementsFiles, f => f.EndsWith("req2.yaml")); + Assert.Equal(0, context.ExitCode); } finally { @@ -639,7 +604,7 @@ public void Context_Create_WithRequirementsPattern_ExpandsGlobPattern() /// /// Test tests glob pattern expansion. /// - [TestMethod] + [Fact] public void Context_Create_WithTestsPattern_ExpandsGlobPattern() { // Arrange: create test TRX files and change working directory to the test directory @@ -657,10 +622,10 @@ public void Context_Create_WithTestsPattern_ExpandsGlobPattern() using var context = Context.Create(["--tests", "*.trx"]); // Assert: both TRX files are resolved and present in TestFiles - Assert.HasCount(2, context.TestFiles); - Assert.ContainsSingle(f => f.EndsWith("test1.trx"), context.TestFiles); - Assert.ContainsSingle(f => f.EndsWith("test2.trx"), context.TestFiles); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(2, context.TestFiles.Count); + Assert.Single(context.TestFiles, f => f.EndsWith("test1.trx")); + Assert.Single(context.TestFiles, f => f.EndsWith("test2.trx")); + Assert.Equal(0, context.ExitCode); } finally { @@ -671,11 +636,11 @@ public void Context_Create_WithTestsPattern_ExpandsGlobPattern() /// /// Test missing requirements pattern argument. /// - [TestMethod] + [Fact] public void Context_Create_MissingRequirementsPattern_ThrowsException() { // Act: create context with --requirements and no following pattern (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--requirements"])); + var ex = Assert.Throws(() => Context.Create(["--requirements"])); // Assert: exception message identifies the missing pattern argument Assert.Contains("--requirements requires a pattern argument", ex.Message); @@ -684,11 +649,11 @@ public void Context_Create_MissingRequirementsPattern_ThrowsException() /// /// Test missing tests pattern argument. /// - [TestMethod] + [Fact] public void Context_Create_MissingTestsPattern_ThrowsException() { // Act: create context with --tests and no following pattern (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--tests"])); + var ex = Assert.Throws(() => Context.Create(["--tests"])); // Assert: exception message identifies the missing pattern argument Assert.Contains("--tests requires a pattern argument", ex.Message); @@ -697,7 +662,7 @@ public void Context_Create_MissingTestsPattern_ThrowsException() /// /// Test combining multiple arguments. /// - [TestMethod] + [Fact] public void Context_Create_MultipleArguments_ParsesAllCorrectly() { // Act: create context with several flags combined @@ -705,19 +670,19 @@ public void Context_Create_MultipleArguments_ParsesAllCorrectly() ["--version", "--help", "--silent", "--validate", "--report", "out.md", "--report-depth", "2"]); // Assert: all specified properties are correctly set - Assert.IsTrue(context.Version); - Assert.IsTrue(context.Help); - Assert.IsTrue(context.Silent); - Assert.IsTrue(context.Validate); - Assert.AreEqual("out.md", context.RequirementsReport); - Assert.AreEqual(2, context.ReportDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Version); + Assert.True(context.Help); + Assert.True(context.Silent); + Assert.True(context.Validate); + Assert.Equal("out.md", context.RequirementsReport); + Assert.Equal(2, context.ReportDepth); + Assert.Equal(0, context.ExitCode); } /// /// Test dispose closes log file. /// - [TestMethod] + [Fact] public void Context_Dispose_WithLogFile_ClosesLogFile() { // Arrange: set up the log file path in the test directory @@ -731,20 +696,20 @@ public void Context_Dispose_WithLogFile_ClosesLogFile() // Assert: log file handle is released and the file can be deleted File.Delete(logPath); - Assert.IsFalse(File.Exists(logPath)); + Assert.False(File.Exists(logPath)); } /// /// Test invalid log file path. /// - [TestMethod] + [Fact] public void Context_Create_InvalidLogPath_ThrowsException() { // Arrange: construct a path whose parent directory does not exist var invalidPath = Path.Combine(_testDirectory, "nonexistent", "test.log"); // Act: create context with the invalid log path (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--log", invalidPath])); + var ex = Assert.Throws(() => Context.Create(["--log", invalidPath])); // Assert: exception message identifies the failure to open the log file Assert.Contains("Failed to open log file", ex.Message); @@ -753,46 +718,46 @@ public void Context_Create_InvalidLogPath_ThrowsException() /// /// Test creating a context with filter argument. /// - [TestMethod] + [Fact] public void Context_Create_FilterArgument_ParsesTagsCorrectly() { // Act: create context with a comma-separated filter value using var context = Context.Create(["--filter", "security,critical"]); // Assert: FilterTags contains both parsed tags - Assert.IsNotNull(context.FilterTags); - Assert.HasCount(2, context.FilterTags); + Assert.NotNull(context.FilterTags); + Assert.Equal(2, context.FilterTags.Count); Assert.Contains("security", context.FilterTags); Assert.Contains("critical", context.FilterTags); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with filter argument with spaces. /// - [TestMethod] + [Fact] public void Context_Create_FilterArgumentWithSpaces_TrimsAndParsesTagsCorrectly() { // Act: create context with a comma-separated filter value containing spaces using var context = Context.Create(["--filter", "security, critical, data-integrity"]); // Assert: FilterTags contains all three tags with whitespace trimmed - Assert.IsNotNull(context.FilterTags); - Assert.HasCount(3, context.FilterTags); + Assert.NotNull(context.FilterTags); + Assert.Equal(3, context.FilterTags.Count); Assert.Contains("security", context.FilterTags); Assert.Contains("critical", context.FilterTags); Assert.Contains("data-integrity", context.FilterTags); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with filter argument missing value. /// - [TestMethod] + [Fact] public void Context_Create_FilterArgumentMissingValue_ThrowsException() { // Act: create context with --filter and no following value (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--filter"])); + var ex = Assert.Throws(() => Context.Create(["--filter"])); // Assert: exception message identifies the missing tag list Assert.Contains("--filter requires a comma-separated list of tags", ex.Message); @@ -801,72 +766,72 @@ public void Context_Create_FilterArgumentMissingValue_ThrowsException() /// /// Test creating a context with single tag filter. /// - [TestMethod] + [Fact] public void Context_Create_FilterSingleTag_ParsesCorrectly() { // Act: create context with a single tag filter value using var context = Context.Create(["--filter", "security"]); // Assert: FilterTags contains exactly the one specified tag - Assert.IsNotNull(context.FilterTags); - Assert.HasCount(1, context.FilterTags); + Assert.NotNull(context.FilterTags); + Assert.Single(context.FilterTags); Assert.Contains("security", context.FilterTags); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with multiple --filter arguments merges into one set. /// - [TestMethod] + [Fact] public void Context_Create_MultipleFilterArguments_MergesIntoSingleSet() { // Act: create context with two separate --filter arguments using var context = Context.Create(["--filter", "tag1", "--filter", "tag2"]); // Assert: both tags are merged into a single FilterTags set - Assert.IsNotNull(context.FilterTags); - Assert.HasCount(2, context.FilterTags); + Assert.NotNull(context.FilterTags); + Assert.Equal(2, context.FilterTags.Count); Assert.Contains("tag1", context.FilterTags); Assert.Contains("tag2", context.FilterTags); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with lint flag. /// - [TestMethod] + [Fact] public void Context_Create_LintFlag_SetsLintProperty() { // Act: create context with lint flag using var context = Context.Create(["--lint"]); // Assert: Lint property is true - Assert.IsTrue(context.Lint); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Lint); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with justifications file. /// - [TestMethod] + [Fact] public void Context_Create_JustificationsFile_SetsJustificationsFileProperty() { // Act: create context with justifications flag and filename using var context = Context.Create(["--justifications", "justifications.md"]); // Assert: JustificationsFile property is set to the specified path - Assert.AreEqual("justifications.md", context.JustificationsFile); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("justifications.md", context.JustificationsFile); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with missing justifications filename. /// - [TestMethod] + [Fact] public void Context_Create_MissingJustificationsFilename_ThrowsException() { // Act: create context with --justifications and no following filename (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--justifications"])); + var ex = Assert.Throws(() => Context.Create(["--justifications"])); // Assert: exception message identifies the missing argument Assert.Contains("--justifications requires a filename argument", ex.Message); @@ -875,25 +840,25 @@ public void Context_Create_MissingJustificationsFilename_ThrowsException() /// /// Test creating a context with justifications depth. /// - [TestMethod] + [Fact] public void Context_Create_JustificationsDepth_SetsJustificationsDepthProperty() { // Act: create context with justifications-depth flag set to 3 using var context = Context.Create(["--justifications-depth", "3"]); // Assert: JustificationsDepth property is set to 3 - Assert.AreEqual(3, context.JustificationsDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(3, context.JustificationsDepth); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with missing justifications depth argument. /// - [TestMethod] + [Fact] public void Context_Create_MissingJustificationsDepth_ThrowsException() { // Act: create context with --justifications-depth and no following value (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--justifications-depth"])); + var ex = Assert.Throws(() => Context.Create(["--justifications-depth"])); // Assert: exception message identifies the missing depth argument Assert.Contains("--justifications-depth requires a depth argument", ex.Message); @@ -902,23 +867,23 @@ public void Context_Create_MissingJustificationsDepth_ThrowsException() /// /// Test creating a context with invalid justifications depth. /// - [TestMethod] + [Fact] public void Context_Create_InvalidJustificationsDepth_ThrowsException() { // Act: create context with non-numeric justifications-depth (combined with assertion) - var ex1 = Assert.ThrowsExactly(() => Context.Create(["--justifications-depth", "invalid"])); + var ex1 = Assert.Throws(() => Context.Create(["--justifications-depth", "invalid"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--justifications-depth requires a positive integer", ex1.Message); // Act: create context with zero justifications-depth (combined with assertion) - var ex2 = Assert.ThrowsExactly(() => Context.Create(["--justifications-depth", "0"])); + var ex2 = Assert.Throws(() => Context.Create(["--justifications-depth", "0"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--justifications-depth requires a positive integer", ex2.Message); // Act: create context with negative justifications-depth (combined with assertion) - var ex3 = Assert.ThrowsExactly(() => Context.Create(["--justifications-depth", "-1"])); + var ex3 = Assert.Throws(() => Context.Create(["--justifications-depth", "-1"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--justifications-depth requires a positive integer", ex3.Message); @@ -927,45 +892,45 @@ public void Context_Create_InvalidJustificationsDepth_ThrowsException() /// /// Test creating a context with depth flag sets all report depths. /// - [TestMethod] + [Fact] public void Context_Create_Depth_SetsAllDepths() { // Act: create context with depth flag set to 2 using var context = Context.Create(["--depth", "2"]); // Assert: all report depth properties inherit the specified default depth - Assert.AreEqual(2, context.Depth); - Assert.AreEqual(2, context.ReportDepth); - Assert.AreEqual(2, context.MatrixDepth); - Assert.AreEqual(2, context.JustificationsDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(2, context.Depth); + Assert.Equal(2, context.ReportDepth); + Assert.Equal(2, context.MatrixDepth); + Assert.Equal(2, context.JustificationsDepth); + Assert.Equal(0, context.ExitCode); } /// /// Test that specific depth flags override the default depth. /// - [TestMethod] + [Fact] public void Context_Create_SpecificDepthOverridesDefaultDepth() { // Act: create context with a default depth of 2 and a report-specific depth of 3 using var context = Context.Create(["--depth", "2", "--report-depth", "3"]); // Assert: report depth uses the override value and other depths inherit the default - Assert.AreEqual(2, context.Depth); - Assert.AreEqual(3, context.ReportDepth); - Assert.AreEqual(2, context.MatrixDepth); - Assert.AreEqual(2, context.JustificationsDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(2, context.Depth); + Assert.Equal(3, context.ReportDepth); + Assert.Equal(2, context.MatrixDepth); + Assert.Equal(2, context.JustificationsDepth); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with missing depth argument. /// - [TestMethod] + [Fact] public void Context_Create_MissingDepth_ThrowsException() { // Act: create context with --depth and no following value (combined with assertion) - var ex = Assert.ThrowsExactly(() => Context.Create(["--depth"])); + var ex = Assert.Throws(() => Context.Create(["--depth"])); // Assert: exception message identifies the missing depth argument Assert.Contains("--depth requires a depth argument", ex.Message); @@ -974,23 +939,23 @@ public void Context_Create_MissingDepth_ThrowsException() /// /// Test creating a context with invalid depth. /// - [TestMethod] + [Fact] public void Context_Create_InvalidDepth_ThrowsException() { // Act: create context with non-numeric depth (combined with assertion) - var ex1 = Assert.ThrowsExactly(() => Context.Create(["--depth", "invalid"])); + var ex1 = Assert.Throws(() => Context.Create(["--depth", "invalid"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--depth requires a positive integer", ex1.Message); // Act: create context with zero depth (combined with assertion) - var ex2 = Assert.ThrowsExactly(() => Context.Create(["--depth", "0"])); + var ex2 = Assert.Throws(() => Context.Create(["--depth", "0"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--depth requires a positive integer", ex2.Message); // Act: create context with negative depth (combined with assertion) - var ex3 = Assert.ThrowsExactly(() => Context.Create(["--depth", "-1"])); + var ex3 = Assert.Throws(() => Context.Create(["--depth", "-1"])); // Assert: exception message indicates a positive integer is required Assert.Contains("--depth requires a positive integer", ex3.Message); diff --git a/test/DemaConsulting.ReqStream.Tests/DemaConsulting.ReqStream.Tests.csproj b/test/DemaConsulting.ReqStream.Tests/DemaConsulting.ReqStream.Tests.csproj index ddb8c89..ffd0182 100644 --- a/test/DemaConsulting.ReqStream.Tests/DemaConsulting.ReqStream.Tests.csproj +++ b/test/DemaConsulting.ReqStream.Tests/DemaConsulting.ReqStream.Tests.csproj @@ -3,6 +3,7 @@ net8.0;net9.0;net10.0 + Exe latest enable enable @@ -29,8 +30,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -52,7 +56,7 @@ - + diff --git a/test/DemaConsulting.ReqStream.Tests/IntegrationTests.cs b/test/DemaConsulting.ReqStream.Tests/IntegrationTests.cs index 4b448fb..4517e5d 100644 --- a/test/DemaConsulting.ReqStream.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/IntegrationTests.cs @@ -24,20 +24,18 @@ namespace DemaConsulting.ReqStream.Tests; /// Integration tests for the ReqStream system, exercising the full pipeline across /// multiple subsystems in end-to-end scenarios. /// -[TestClass] -public class IntegrationTests +public sealed class IntegrationTests : IDisposable { - private string _dllPath = string.Empty; - private string _testDirectory = string.Empty; + private readonly string _dllPath; + private readonly string _testDirectory; /// /// Initialize test by locating the DLL and creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public IntegrationTests() { _dllPath = Path.Combine(AppContext.BaseDirectory, "DemaConsulting.ReqStream.dll"); - Assert.IsTrue(File.Exists(_dllPath), $"Could not find ReqStream DLL at {_dllPath}"); + Assert.True(File.Exists(_dllPath), $"Could not find ReqStream DLL at {_dllPath}"); _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -46,13 +44,13 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// @@ -60,7 +58,7 @@ public void TestCleanup() /// results, generate a requirements report, justifications, and trace matrix, and enforce /// coverage — all subsystems working together correctly. /// - [TestMethod] + [Fact] public void ReqStream_FullPipeline_WithCoveredRequirements_GeneratesAllReportsAndEnforces() { // Arrange: create requirements file with one covered requirement @@ -96,7 +94,7 @@ This is a test justification. // Act: run the full pipeline as an external process var exitCode = Runner.RunInDirectory( - out var output, + out _, _testDirectory, "dotnet", _dllPath, @@ -108,12 +106,12 @@ This is a test justification. "--enforce"); // Assert: enforcement passed (exit code 0) - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); // Assert: all three output files were generated - Assert.IsTrue(File.Exists(reportFile), "Requirements report should be generated."); - Assert.IsTrue(File.Exists(justificationsFile), "Justifications report should be generated."); - Assert.IsTrue(File.Exists(matrixFile), "Trace matrix report should be generated."); + Assert.True(File.Exists(reportFile), "Requirements report should be generated."); + Assert.True(File.Exists(justificationsFile), "Justifications report should be generated."); + Assert.True(File.Exists(matrixFile), "Trace matrix report should be generated."); // Assert: report contains the requirement ID and title var reportContent = File.ReadAllText(reportFile); @@ -130,7 +128,7 @@ This is a test justification. /// Integration test verifying that enforcement mode causes a non-zero exit code when a /// requirement has no passing test evidence, confirming the CI/CD gate operates correctly. /// - [TestMethod] + [Fact] public void ReqStream_EnforcementMode_RequirementLacksTestEvidence_FailsWithNonZeroExitCode() { // Arrange: create requirements file with one requirement that has no matching test @@ -154,7 +152,7 @@ This requirement deliberately has no matching test to verify enforcement failure // Act: run enforcement mode as an external process var exitCode = Runner.RunInDirectory( - out var output, + out _, _testDirectory, "dotnet", _dllPath, @@ -163,7 +161,7 @@ This requirement deliberately has no matching test to verify enforcement failure "--enforce"); // Assert: enforcement failed with a non-zero exit code - Assert.AreNotEqual(0, exitCode, $"Enforcement should fail with non-zero exit code when a requirement lacks test evidence. Output: {output}"); + Assert.NotEqual(0, exitCode); } /// @@ -171,7 +169,7 @@ This requirement deliberately has no matching test to verify enforcement failure /// to tests from the named result file, and that enforcement passes when the named source /// provides the required passing test. /// - [TestMethod] + [Fact] public void ReqStream_SourceFilter_NamedSourceInRequirement_MatchesTestsBySourceFile() { // Arrange: create requirements file with source-specific test reference @@ -216,7 +214,7 @@ public void ReqStream_SourceFilter_NamedSourceInRequirement_MatchesTestsBySource // Act: run enforcement using both result files as external process var exitCode = Runner.RunInDirectory( - out var output, + out _, _testDirectory, "dotnet", _dllPath, @@ -226,14 +224,14 @@ public void ReqStream_SourceFilter_NamedSourceInRequirement_MatchesTestsBySource "--enforce"); // Assert: enforcement passed because platform-a.trx satisfies the source-filtered requirement - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); } /// /// Integration test verifying that the --lint flag exits silently with code 0 when a valid /// requirements file is provided, confirming the no-output-on-success design. /// - [TestMethod] + [Fact] public void ReqStream_System_Lint_ValidRequirementsFile_ExitsSilentlyWithZero() { // Arrange: create a structurally valid requirements file @@ -258,17 +256,17 @@ public void ReqStream_System_Lint_ValidRequirementsFile_ExitsSilentlyWithZero() "--requirements", "requirements.yaml"); // Assert: lint exits with code 0 because no issues were found - Assert.AreEqual(0, exitCode, $"Expected exit code 0 from lint on valid file, but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); // Assert: no output was produced (silent on success) - Assert.AreEqual(string.Empty, output.Trim(), $"Expected no output from lint on valid file, but got: {output}"); + Assert.Equal(string.Empty, output.Trim()); } /// /// Integration test verifying that the --lint flag lints requirements files and reports /// structural issues in a single invocation, exercising the system-level lint behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_Lint_Flag_ReportsLintIssues() { // Arrange: create a requirements file with a structural issue (missing title) @@ -290,10 +288,10 @@ public void ReqStream_System_Lint_Flag_ReportsLintIssues() "--requirements", "requirements.yaml"); // Assert: lint exits with a non-zero code because an issue was found - Assert.AreNotEqual(0, exitCode, $"Expected non-zero exit code from lint, but got {exitCode}. Output: {output}"); + Assert.NotEqual(0, exitCode); // Assert: lint reported an issue about the missing title - Assert.IsTrue( + Assert.True( output.Contains("Integration-System-MissingTitle") || output.Contains("title"), $"Expected lint output to reference the missing-title issue. Output: {output}"); } @@ -302,7 +300,7 @@ public void ReqStream_System_Lint_Flag_ReportsLintIssues() /// Integration test verifying that the --validate flag runs the built-in self-test suite /// and exits successfully, exercising the system-level validate behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_Validate_Flag_RunsSelfValidation() { // Act: run validate as an external process @@ -313,10 +311,10 @@ public void ReqStream_System_Validate_Flag_RunsSelfValidation() "--validate"); // Assert: self-validation passes (exit code 0) - Assert.AreEqual(0, exitCode, $"Expected exit code 0 from --validate, but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); // Assert: output contains the validation summary header - Assert.IsTrue( + Assert.True( output.Contains("Passed") || output.Contains("Total Tests"), $"Expected validation output to contain test summary. Output: {output}"); } @@ -325,7 +323,7 @@ public void ReqStream_System_Validate_Flag_RunsSelfValidation() /// Integration test verifying that the --validate --results flags write the self-test results /// to the specified file, exercising the system-level validate results output behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_ValidateResultsOutput_ResultsFlag_WritesResultsFile() { // Arrange: create a temporary file path for the results output @@ -333,27 +331,27 @@ public void ReqStream_System_ValidateResultsOutput_ResultsFlag_WritesResultsFile // Act: run validate with results flag as an external process var exitCode = Runner.Run( - out var output, + out _, "dotnet", _dllPath, "--validate", "--results", resultsFile); // Assert: self-validation passes (exit code 0) - Assert.AreEqual(0, exitCode, $"Expected exit code 0 from --validate --results, but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); // Assert: results file was created - Assert.IsTrue(File.Exists(resultsFile), "Results file should be created by --results flag."); + Assert.True(File.Exists(resultsFile), "Results file should be created by --results flag."); // Assert: results file is non-empty - Assert.IsGreaterThan(0, new FileInfo(resultsFile).Length, "Results file should be non-empty."); + Assert.True(new FileInfo(resultsFile).Length > 0, "Results file should be non-empty."); } /// /// Integration test verifying that the --filter flag restricts requirements output to only /// those matching the specified tag, exercising the system-level tag-filter behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_TagFilter_Flag_FiltersRequirements() { // Arrange: create requirements file with two requirements, each with a different tag @@ -382,7 +380,7 @@ public void ReqStream_System_TagFilter_Flag_FiltersRequirements() // Act: run with --filter alpha to export only alpha-tagged requirements var exitCode = Runner.RunInDirectory( - out var output, + out _, _testDirectory, "dotnet", _dllPath, @@ -391,28 +389,22 @@ public void ReqStream_System_TagFilter_Flag_FiltersRequirements() "--filter", "alpha"); // Assert: tool exited successfully - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); // Assert: report was generated - Assert.IsTrue(File.Exists(reportFile), "Filtered requirements report should be generated."); + Assert.True(File.Exists(reportFile), "Filtered requirements report should be generated."); // Assert: report contains the alpha requirement but not the beta requirement var reportContent = File.ReadAllText(reportFile); - Assert.Contains( - "Integration-System-TaggedAlpha", - reportContent, - "Filtered report should contain the alpha-tagged requirement."); - Assert.DoesNotContain( - "Integration-System-TaggedBeta", - reportContent, - "Filtered report should not contain the beta-tagged requirement."); + Assert.Contains("Integration-System-TaggedAlpha", reportContent); + Assert.DoesNotContain("Integration-System-TaggedBeta", reportContent); } /// /// Integration test verifying that the --version flag causes the tool to print version /// information and exit with code 0, exercising the system-level CLI interface behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_CliInterface_VersionFlag_PrintsVersion() { // Act: run with --version flag as an external process @@ -423,18 +415,18 @@ public void ReqStream_System_CliInterface_VersionFlag_PrintsVersion() "--version"); // Assert: tool exits successfully - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); // Assert: output contains a version string (non-empty, no banner/help) - Assert.IsFalse(string.IsNullOrWhiteSpace(output), "Expected version output to be non-empty."); - Assert.DoesNotContain("Usage:", output, "Version output should not contain usage help."); + Assert.False(string.IsNullOrWhiteSpace(output), "Expected version output to be non-empty."); + Assert.DoesNotContain("Usage:", output); } /// /// Integration test verifying that the --help flag causes the tool to print usage information /// and exit with code 0, exercising the system-level CLI interface behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_CliInterface_HelpFlag_PrintsHelp() { // Act: run with --help flag as an external process @@ -445,18 +437,18 @@ public void ReqStream_System_CliInterface_HelpFlag_PrintsHelp() "--help"); // Assert: tool exits successfully - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); // Assert: output contains usage information - Assert.Contains("Usage:", output, $"Expected help output to contain 'Usage:'. Output: {output}"); - Assert.Contains("Options:", output, $"Expected help output to contain 'Options:'. Output: {output}"); + Assert.Contains("Usage:", output); + Assert.Contains("Options:", output); } /// /// Integration test verifying that the --log flag routes all output to the specified log file, /// exercising the system-level output control behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_OutputControl_LogFlag_WritesOutputToFile() { // Arrange: create a minimal requirements file @@ -484,19 +476,19 @@ public void ReqStream_System_OutputControl_LogFlag_WritesOutputToFile() "--log", logFile); // Assert: tool exited successfully - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}."); + Assert.Equal(0, exitCode); // Assert: log file was created with tool output - Assert.IsTrue(File.Exists(logFile), "Log file should have been created."); + Assert.True(File.Exists(logFile), "Log file should have been created."); var logContent = File.ReadAllText(logFile); - Assert.IsFalse(string.IsNullOrWhiteSpace(logContent), "Log file should contain output."); + Assert.False(string.IsNullOrWhiteSpace(logContent), "Log file should contain output."); } /// /// Integration test verifying that the --silent flag suppresses console output, /// exercising the system-level output control behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_OutputControl_SilentFlag_SuppressesConsoleOutput() { // Arrange: create a minimal requirements file @@ -521,15 +513,15 @@ public void ReqStream_System_OutputControl_SilentFlag_SuppressesConsoleOutput() "--silent"); // Assert: tool exited successfully and produced no console output - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}. Output: {output}"); - Assert.IsTrue(string.IsNullOrWhiteSpace(output), $"Expected no console output with --silent. Got: {output}"); + Assert.Equal(0, exitCode); + Assert.True(string.IsNullOrWhiteSpace(output), $"Expected no console output with --silent. Got: {output}"); } /// /// Integration test verifying that requirements files using file includes correctly load /// all requirements from included files, exercising the system-level file includes behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_FileIncludes_RequirementsWithIncludes_LoadsAllRequirements() { // Arrange: create a child requirements file @@ -562,7 +554,7 @@ public void ReqStream_System_FileIncludes_RequirementsWithIncludes_LoadsAllRequi // Act: run with the root requirements file that uses includes var exitCode = Runner.RunInDirectory( - out var output, + out _, _testDirectory, "dotnet", _dllPath, @@ -570,26 +562,20 @@ public void ReqStream_System_FileIncludes_RequirementsWithIncludes_LoadsAllRequi "--report", reportFile); // Assert: tool exited successfully - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); // Assert: report was generated containing requirements from both files - Assert.IsTrue(File.Exists(reportFile), "Report file should have been generated."); + Assert.True(File.Exists(reportFile), "Report file should have been generated."); var reportContent = File.ReadAllText(reportFile); - Assert.Contains( - "Integration-Root-Requirement", - reportContent, - "Report should contain the root requirement."); - Assert.Contains( - "Integration-Child-Requirement", - reportContent, - "Report should contain the included child requirement."); + Assert.Contains("Integration-Root-Requirement", reportContent); + Assert.Contains("Integration-Child-Requirement", reportContent); } /// /// Integration test verifying that the --log flag routes output to a file without /// requiring --silent, confirming independent operation of the log flag. /// - [TestMethod] + [Fact] public void ReqStream_System_OutputControl_LogFlag_WithoutSilent_WritesOutputToFileAndConsole() { // Arrange: create a minimal requirements file @@ -616,22 +602,22 @@ public void ReqStream_System_OutputControl_LogFlag_WithoutSilent_WritesOutputToF "--log", logFile); // Assert: tool exited successfully - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}."); + Assert.Equal(0, exitCode); // Assert: log file was created with tool output - Assert.IsTrue(File.Exists(logFile), "Log file should have been created."); + Assert.True(File.Exists(logFile), "Log file should have been created."); var logContent = File.ReadAllText(logFile); - Assert.IsFalse(string.IsNullOrWhiteSpace(logContent), "Log file should contain output."); + Assert.False(string.IsNullOrWhiteSpace(logContent), "Log file should contain output."); // Assert: console output was also produced (--silent was not specified) - Assert.IsFalse(string.IsNullOrWhiteSpace(output), "Console output should not be suppressed without --silent."); + Assert.False(string.IsNullOrWhiteSpace(output), "Console output should not be suppressed without --silent."); } /// /// Integration test verifying that the --depth flag controls the Markdown heading level /// in the generated requirements report, exercising the system-level report depth behavior. /// - [TestMethod] + [Fact] public void ReqStream_System_ReportDepth_DepthFlag_GeneratesReportWithCorrectHeadingLevel() { // Arrange: create a minimal requirements file @@ -650,7 +636,7 @@ public void ReqStream_System_ReportDepth_DepthFlag_GeneratesReportWithCorrectHea // Act: run with --depth 3 to use heading level 3 (###) var exitCode = Runner.RunInDirectory( - out var output, + out _, _testDirectory, "dotnet", _dllPath, @@ -659,10 +645,10 @@ public void ReqStream_System_ReportDepth_DepthFlag_GeneratesReportWithCorrectHea "--depth", "3"); // Assert: tool exited successfully - Assert.AreEqual(0, exitCode, $"Expected exit code 0 but got {exitCode}. Output: {output}"); + Assert.Equal(0, exitCode); // Assert: report was generated with heading level 3 - Assert.IsTrue(File.Exists(reportFile), "Report file should have been generated."); + Assert.True(File.Exists(reportFile), "Report file should have been generated."); var reportContent = File.ReadAllText(reportFile); Assert.Contains("### Depth Test Section", reportContent); } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/LintIssueTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/LintIssueTests.cs index 135d9ad..c2c48af 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/LintIssueTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/LintIssueTests.cs @@ -25,58 +25,57 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// /// Unit tests for the LintIssue class, proving it correctly represents and formats lint issues. /// -[TestClass] public class LintIssueTests { /// /// Test that LintIssue.ToString() formats error severity as "error". /// - [TestMethod] + [Fact] public void LintIssue_ToString_ErrorSeverity_FormatsAsError() { // Arrange: create a LintIssue with error severity var issue = new LintIssue("file.yaml(3,5)", LintSeverity.Error, "Some error"); // Act / Assert: verify ToString uses "error" for LintSeverity.Error - Assert.AreEqual("file.yaml(3,5): error: Some error", issue.ToString()); + Assert.Equal("file.yaml(3,5): error: Some error", issue.ToString()); } /// /// Test that LintIssue.ToString() formats warning severity as "warning". /// - [TestMethod] + [Fact] public void LintIssue_ToString_WarningSeverity_FormatsAsWarning() { // Arrange: create a LintIssue with warning severity var issue = new LintIssue("file.yaml", LintSeverity.Warning, "Some warning"); // Act / Assert: verify ToString uses "warning" for LintSeverity.Warning - Assert.AreEqual("file.yaml: warning: Some warning", issue.ToString()); + Assert.Equal("file.yaml: warning: Some warning", issue.ToString()); } /// /// Test that LintIssue.ToString() handles an empty location correctly. /// - [TestMethod] + [Fact] public void LintIssue_ToString_EmptyLocation_FormatsCorrectly() { // Arrange: create a LintIssue with an empty location string var issue = new LintIssue(string.Empty, LintSeverity.Error, "Some error"); // Act / Assert: verify ToString still formats as "location: severity: description" - Assert.AreEqual(": error: Some error", issue.ToString()); + Assert.Equal(": error: Some error", issue.ToString()); } /// /// Test that LintIssue.ToString() handles an empty description correctly. /// - [TestMethod] + [Fact] public void LintIssue_ToString_EmptyDescription_FormatsCorrectly() { // Arrange: create a LintIssue with an empty description string var issue = new LintIssue("file.yaml", LintSeverity.Warning, string.Empty); // Act / Assert: verify ToString still formats as "location: severity: description" - Assert.AreEqual("file.yaml: warning: ", issue.ToString()); + Assert.Equal("file.yaml: warning: ", issue.ToString()); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/LoadResultTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/LoadResultTests.cs index 21e7ac1..fc46b49 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/LoadResultTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/LoadResultTests.cs @@ -27,16 +27,14 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// Unit tests for the LoadResult class, proving it correctly encapsulates load outcomes /// and routes lint issues to the appropriate context output streams. /// -[TestClass] -public class LoadResultTests +public sealed class LoadResultTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public LoadResultTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_load_result_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -45,19 +43,19 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test that ReportIssues routes error-level issues to the context error output. /// - [TestMethod] + [Fact] public void LoadResult_ReportIssues_ErrorIssue_SetsContextError() { // Arrange: load a file with a lint error @@ -84,7 +82,7 @@ public void LoadResult_ReportIssues_ErrorIssue_SetsContextError() } // Assert: error context exit code set and log contains issue - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); var log = File.ReadAllText(logFile); Assert.Contains("unknown_field", log); } @@ -92,7 +90,7 @@ public void LoadResult_ReportIssues_ErrorIssue_SetsContextError() /// /// Test that ReportIssues routes warning-level issues to context normal output. /// - [TestMethod] + [Fact] public void LoadResult_ReportIssues_WarningIssue_DoesNotSetContextError() { // Arrange: create a load result with a single warning issue @@ -110,7 +108,7 @@ public void LoadResult_ReportIssues_WarningIssue_DoesNotSetContextError() } // Assert: no error exit code and warning written to log - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); var log = File.ReadAllText(logFile); Assert.Contains("A warning", log); } @@ -118,7 +116,7 @@ public void LoadResult_ReportIssues_WarningIssue_DoesNotSetContextError() /// /// Test that ReportIssues produces no output when there are no issues. /// - [TestMethod] + [Fact] public void LoadResult_ReportIssues_NoIssues_ProducesNoOutput() { // Arrange: load a valid file with no issues @@ -144,14 +142,14 @@ public void LoadResult_ReportIssues_NoIssues_ProducesNoOutput() } // Assert: no output produced and exit code zero - Assert.AreEqual(0, exitCode); - Assert.AreEqual(string.Empty, File.ReadAllText(logFile)); + Assert.Equal(0, exitCode); + Assert.Equal(string.Empty, File.ReadAllText(logFile)); } /// /// Test that HasErrors is false when there are only warnings. /// - [TestMethod] + [Fact] public void LoadResult_HasErrors_WithOnlyWarnings_ReturnsFalse() { // Arrange: create a load result with a single warning issue @@ -160,14 +158,14 @@ public void LoadResult_HasErrors_WithOnlyWarnings_ReturnsFalse() [new LintIssue("file.yaml", LintSeverity.Warning, "A warning")]); // Assert: HasErrors is false and Requirements is not null for warnings-only results - Assert.IsFalse(result.HasErrors); - Assert.IsNotNull(result.Requirements); + Assert.False(result.HasErrors); + Assert.NotNull(result.Requirements); } /// /// Test that HasErrors is true when there are error-level issues. /// - [TestMethod] + [Fact] public void LoadResult_HasErrors_WithErrorIssue_ReturnsTrue() { // Arrange: create a load result with an error issue and null requirements @@ -176,7 +174,7 @@ public void LoadResult_HasErrors_WithErrorIssue_ReturnsTrue() [new LintIssue("file.yaml", LintSeverity.Error, "An error")]); // Assert: HasErrors is true and Requirements is null for error results - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/ModelingTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/ModelingTests.cs index c91a627..59f775b 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/ModelingTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/ModelingTests.cs @@ -26,16 +26,14 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// Tests for the Modeling subsystem, proving the Requirements class is sufficient to /// implement the Modeling subsystem requirements. /// -[TestClass] -public class ModelingTests +public sealed class ModelingTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public ModelingTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_modeling_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -44,19 +42,19 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test that loading a valid YAML file produces a requirements model with no errors. /// - [TestMethod] + [Fact] public void Modeling_YamlParsing_ValidFile_LoadsRequirements() { // Arrange: create a valid requirements YAML file @@ -76,16 +74,16 @@ public void Modeling_YamlParsing_ValidFile_LoadsRequirements() var result = Requirements.Load(reqFile); // Assert: requirements loaded successfully with no errors - Assert.IsNotNull(result.Requirements); - Assert.IsFalse(result.HasErrors); - Assert.HasCount(1, result.Requirements.Sections); - Assert.AreEqual("Modeling-Test-Req1", result.Requirements.Sections[0].Requirements[0].Id); + Assert.NotNull(result.Requirements); + Assert.False(result.HasErrors); + Assert.Single(result.Requirements.Sections); + Assert.Equal("Modeling-Test-Req1", result.Requirements.Sections[0].Requirements[0].Id); } /// /// Test that loading a valid YAML file produces no lint issues. /// - [TestMethod] + [Fact] public void Modeling_YamlParsing_ValidFile_ReturnsNoLintIssues() { // Arrange: create a structurally valid requirements YAML file with no duplicate IDs @@ -105,14 +103,14 @@ public void Modeling_YamlParsing_ValidFile_ReturnsNoLintIssues() var result = Requirements.Load(reqFile); // Assert: no lint issues reported - Assert.IsFalse(result.HasErrors); - Assert.HasCount(0, result.Issues); + Assert.False(result.HasErrors); + Assert.Empty(result.Issues); } /// /// Test that loading a YAML file with duplicate requirement IDs reports a lint error. /// - [TestMethod] + [Fact] public void Modeling_YamlParsing_DuplicateIds_DetectsLintError() { // Arrange: create a requirements YAML file containing duplicate requirement IDs @@ -137,15 +135,15 @@ public void Modeling_YamlParsing_DuplicateIds_DetectsLintError() var result = Requirements.Load(reqFile); // Assert: an error-level lint issue was detected - Assert.IsTrue(result.HasErrors); - Assert.IsNotEmpty(result.Issues, "Expected at least one lint issue to be reported."); - Assert.Contains(i => i.Severity == LintSeverity.Error, result.Issues, "Expected at least one Error-severity lint issue."); + Assert.True(result.HasErrors); + Assert.NotEmpty(result.Issues); + Assert.Contains(result.Issues, i => i.Severity == LintSeverity.Error); } /// /// Test that a requirements Markdown report is generated correctly. /// - [TestMethod] + [Fact] public void Modeling_Export_Requirements_GeneratesMarkdownFile() { // Arrange: create a requirements file with one testable requirement @@ -161,7 +159,7 @@ public void Modeling_Export_Requirements_GeneratesMarkdownFile() - SomeTest """); var loadResult = Requirements.Load(reqFile); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var reportFile = Path.Combine(_testDirectory, "requirements.md"); @@ -169,7 +167,7 @@ public void Modeling_Export_Requirements_GeneratesMarkdownFile() loadResult.Requirements.Export(reportFile); // Assert: report file exists and contains the requirement ID and title - Assert.IsTrue(File.Exists(reportFile), "Requirements report should be generated."); + Assert.True(File.Exists(reportFile), "Requirements report should be generated."); var content = File.ReadAllText(reportFile); Assert.Contains("Modeling-Test-Req1", content); Assert.Contains("The system shall have a testable requirement.", content); @@ -178,7 +176,7 @@ public void Modeling_Export_Requirements_GeneratesMarkdownFile() /// /// Test that a justifications Markdown report is generated correctly. /// - [TestMethod] + [Fact] public void Modeling_Export_Justifications_GeneratesMarkdownFile() { // Arrange: create a requirements file with one justified requirement @@ -194,7 +192,7 @@ public void Modeling_Export_Justifications_GeneratesMarkdownFile() - SomeTest """); var loadResult = Requirements.Load(reqFile); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var justificationsFile = Path.Combine(_testDirectory, "justifications.md"); @@ -202,7 +200,7 @@ public void Modeling_Export_Justifications_GeneratesMarkdownFile() loadResult.Requirements.ExportJustifications(justificationsFile); // Assert: report file exists and contains the requirement ID and justification text - Assert.IsTrue(File.Exists(justificationsFile), "Justifications report should be generated."); + Assert.True(File.Exists(justificationsFile), "Justifications report should be generated."); var content = File.ReadAllText(justificationsFile); Assert.Contains("Modeling-Test-Req2", content); Assert.Contains("This justification explains why the requirement is needed.", content); @@ -211,7 +209,7 @@ public void Modeling_Export_Justifications_GeneratesMarkdownFile() /// /// Test that the Modeling subsystem detects an error when loading a malformed YAML file. /// - [TestMethod] + [Fact] public void Modeling_Linting_MalformedYaml_DetectsError() { // Arrange: create a requirements file containing malformed YAML @@ -227,16 +225,16 @@ public void Modeling_Linting_MalformedYaml_DetectsError() var result = Requirements.Load(reqFile); // Assert: an error-level lint issue is reported and requirements are null - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.IsNotEmpty(result.Issues, "Expected at least one lint issue to be reported."); - Assert.Contains(i => i.Severity == LintSeverity.Error, result.Issues, "Expected at least one Error-severity lint issue."); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.NotEmpty(result.Issues); + Assert.Contains(result.Issues, i => i.Severity == LintSeverity.Error); } /// /// Test that the Modeling subsystem reports no issues when loading a valid requirements file. /// - [TestMethod] + [Fact] public void Modeling_Linting_ValidFile_ReturnsNoIssues() { // Arrange: create a structurally valid requirements YAML file @@ -256,15 +254,15 @@ public void Modeling_Linting_ValidFile_ReturnsNoIssues() var result = Requirements.Load(reqFile); // Assert: no lint issues are reported - Assert.IsFalse(result.HasErrors); - Assert.HasCount(0, result.Issues); + Assert.False(result.HasErrors); + Assert.Empty(result.Issues); } /// /// Test that the Modeling subsystem reports ALL lint issues when multiple independent /// lint conditions are present in one load, not just the first one encountered. /// - [TestMethod] + [Fact] public void Modeling_LintingReporting_MultipleConditions_ReportsAllIssues() { // Arrange: create a requirements file with two independent lint errors: @@ -284,10 +282,58 @@ public void Modeling_LintingReporting_MultipleConditions_ReportsAllIssues() var result = Requirements.Load(reqFile); // Assert: both lint issues are reported (not just HasErrors == true) - Assert.IsTrue(result.HasErrors); - Assert.IsGreaterThanOrEqualTo(2, result.Issues.Count, - $"Expected at least 2 lint issues but got {result.Issues.Count}."); - Assert.IsTrue(result.Issues.All(i => i.Severity == LintSeverity.Error), + Assert.True(result.HasErrors); + Assert.True(result.Issues.Count >= 2, $"Expected at least 2 lint issues but got {result.Issues.Count}."); + Assert.True(result.Issues.All(i => i.Severity == LintSeverity.Error), "All reported issues should be Error severity."); } + + /// + /// Test that the Modeling subsystem loads requirements from multiple YAML files + /// following includes directives transitively. + /// + [Fact] + public void Modeling_MultiFileLoading_WithIncludes_LoadsRequirementsFromAllFiles() + { + // Arrange: create a second file with distinct requirements + var includedFile = Path.Combine(_testDirectory, "included.yaml"); + File.WriteAllText(includedFile, """ + sections: + - title: Included Requirements + requirements: + - id: Modeling-Included-Req1 + title: The system shall satisfy the included requirement. + justification: Included requirement justification. + tests: + - IncludedTest1 + """); + + // Arrange: create a main file that references the second file via includes + var mainFile = Path.Combine(_testDirectory, "main.yaml"); + File.WriteAllText(mainFile, """ + includes: + - included.yaml + sections: + - title: Main Requirements + requirements: + - id: Modeling-Main-Req1 + title: The system shall satisfy the main requirement. + justification: Main requirement justification. + tests: + - MainTest1 + """); + + // Act: load the main requirements file + var result = Requirements.Load(mainFile); + + // Assert: requirements from both files appear in the result + Assert.NotNull(result.Requirements); + Assert.False(result.HasErrors); + var allIds = result.Requirements.Sections + .SelectMany(s => s.Requirements) + .Select(r => r.Id) + .ToList(); + Assert.Contains("Modeling-Main-Req1", allIds); + Assert.Contains("Modeling-Included-Req1", allIds); + } } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementTests.cs index bbfb013..a346065 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementTests.cs @@ -26,16 +26,14 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// Unit tests for the Requirement class, proving it correctly holds its data fields /// and that invalid values are detected during loading. /// -[TestClass] -public class RequirementTests +public sealed class RequirementTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public RequirementTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_requirement_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -44,19 +42,38 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); + } + + /// + /// Test that a default Requirement instance has the expected default property values. + /// + [Fact] + public void Requirement_Properties_DefaultValues() + { + // Arrange / Act: + var requirement = new Requirement(); + + // Assert: + Assert.Equal(string.Empty, requirement.Id); + Assert.Equal(string.Empty, requirement.Title); + Assert.Null(requirement.Justification); + Assert.Empty(requirement.Tags); + Assert.Empty(requirement.Tests); + Assert.Empty(requirement.Children); + Assert.Null(requirement.Location); } /// /// Test reading a requirement with tests. /// - [TestMethod] + [Fact] public void Requirements_Load_RequirementWithTests_ParsesTestsCorrectly() { // Arrange: create a YAML file with a requirement that has test references @@ -76,23 +93,23 @@ public void Requirements_Load_RequirementWithTests_ParsesTestsCorrectly() // Act: load the requirements file var result = Requirements.Load(filePath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: tests parsed correctly - Assert.IsNotNull(requirements); + Assert.NotNull(requirements); var req = requirements.Sections[0].Requirements[0]; - Assert.AreEqual("AUTH-001", req.Id); - Assert.HasCount(3, req.Tests); - Assert.AreEqual("Credentials_Valid_Allowed", req.Tests[0]); - Assert.AreEqual("Credentials_Invalid_Refused", req.Tests[1]); - Assert.AreEqual("Credentials_Missing_Refused", req.Tests[2]); + Assert.Equal("AUTH-001", req.Id); + Assert.Equal(3, req.Tests.Count); + Assert.Equal("Credentials_Valid_Allowed", req.Tests[0]); + Assert.Equal("Credentials_Invalid_Refused", req.Tests[1]); + Assert.Equal("Credentials_Missing_Refused", req.Tests[2]); } /// /// Test reading a requirement with child requirements. /// - [TestMethod] + [Fact] public void Requirements_Load_RequirementWithChildren_ParsesChildrenCorrectly() { // Arrange: create a YAML file with a requirement that has child references @@ -115,22 +132,22 @@ public void Requirements_Load_RequirementWithChildren_ParsesChildrenCorrectly() // Act: load the requirements file var result = Requirements.Load(filePath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: children parsed correctly - Assert.IsNotNull(requirements); + Assert.NotNull(requirements); var req = requirements.Sections[0].Requirements[0]; - Assert.AreEqual("SYS-SEC-001", req.Id); - Assert.HasCount(2, req.Children); - Assert.AreEqual("AUTH-001", req.Children[0]); - Assert.AreEqual("AUTH-002", req.Children[1]); + Assert.Equal("SYS-SEC-001", req.Id); + Assert.Equal(2, req.Children.Count); + Assert.Equal("AUTH-001", req.Children[0]); + Assert.Equal("AUTH-002", req.Children[1]); } /// /// Test reading a requirement with justification. /// - [TestMethod] + [Fact] public void Requirements_Load_RequirementWithJustification_ParsesJustificationCorrectly() { // Arrange: create a YAML file with a requirement that has a justification @@ -149,15 +166,15 @@ can access the system and to maintain data security and integrity. // Act: load the requirements file var result = Requirements.Load(filePath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: justification parsed correctly - Assert.IsNotNull(requirements); + Assert.NotNull(requirements); var req = requirements.Sections[0].Requirements[0]; - Assert.AreEqual("SYS-SEC-001", req.Id); - Assert.AreEqual("The system shall support credentials authentication.", req.Title); - Assert.IsNotNull(req.Justification); + Assert.Equal("SYS-SEC-001", req.Id); + Assert.Equal("The system shall support credentials authentication.", req.Title); + Assert.NotNull(req.Justification); Assert.Contains("authorized users", req.Justification); Assert.Contains("data security", req.Justification); } @@ -165,7 +182,7 @@ can access the system and to maintain data security and integrity. /// /// Test reading a requirement with tags. /// - [TestMethod] + [Fact] public void Requirements_Load_RequirementWithTags_ParsesTagsCorrectly() { // Arrange: create a YAML file with a requirement that has tags @@ -184,22 +201,22 @@ public void Requirements_Load_RequirementWithTags_ParsesTagsCorrectly() // Act: load the requirements file var result = Requirements.Load(filePath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: tags parsed correctly - Assert.IsNotNull(requirements); + Assert.NotNull(requirements); var req = requirements.Sections[0].Requirements[0]; - Assert.AreEqual("SYS-SEC-001", req.Id); - Assert.HasCount(2, req.Tags); - Assert.AreEqual("security", req.Tags[0]); - Assert.AreEqual("critical", req.Tags[1]); + Assert.Equal("SYS-SEC-001", req.Id); + Assert.Equal(2, req.Tags.Count); + Assert.Equal("security", req.Tags[0]); + Assert.Equal("critical", req.Tags[1]); } /// /// Test that duplicate requirement IDs report an error issue. /// - [TestMethod] + [Fact] public void Requirements_Load_DuplicateRequirementId_ReportsError() { // Arrange: create a YAML file with two requirements sharing the same ID @@ -219,16 +236,16 @@ public void Requirements_Load_DuplicateRequirementId_ReportsError() var result = Requirements.Load(filePath); // Assert: error reported for duplicate ID - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("SYS-SEC-001"), result.Issues); - Assert.Contains(i => i.Description.Contains("Duplicate requirement ID"), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("SYS-SEC-001")); + Assert.Contains(result.Issues, i => i.Description.Contains("Duplicate requirement ID")); } /// /// Test that duplicate requirement ID message includes file location. /// - [TestMethod] + [Fact] public void Requirements_Load_DuplicateRequirementId_ErrorIncludesFileLocation() { // Arrange: create a YAML file with two requirements sharing the same ID @@ -248,17 +265,17 @@ public void Requirements_Load_DuplicateRequirementId_ErrorIncludesFileLocation() var result = Requirements.Load(filePath); // Assert: error reported with file location for the duplicate ID - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("SYS-SEC-001"), result.Issues); - Assert.Contains(i => i.Description.Contains("Duplicate requirement ID"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("SYS-SEC-001")); + Assert.Contains(result.Issues, i => i.Description.Contains("Duplicate requirement ID")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); } /// /// Test that a blank requirement ID reports an error issue with file location. /// - [TestMethod] + [Fact] public void Requirements_Load_BlankRequirementId_ReportsErrorWithFileLocation() { // Arrange: create a YAML file with a blank requirement ID @@ -276,16 +293,16 @@ public void Requirements_Load_BlankRequirementId_ReportsErrorWithFileLocation() var result = Requirements.Load(filePath); // Assert: error reported with file location for the blank ID - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Requirement 'id' cannot be blank"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Requirement 'id' cannot be blank")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); } /// /// Test that a blank requirement title reports an error issue with file location. /// - [TestMethod] + [Fact] public void Requirements_Load_BlankRequirementTitle_ReportsErrorWithFileLocation() { // Arrange: create a YAML file with a blank requirement title @@ -303,16 +320,16 @@ public void Requirements_Load_BlankRequirementTitle_ReportsErrorWithFileLocation var result = Requirements.Load(filePath); // Assert: error reported with file location for the blank title - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Requirement 'title' cannot be blank"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Requirement 'title' cannot be blank")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); } /// /// Test that a blank test name in a requirement reports an error issue with file location. /// - [TestMethod] + [Fact] public void Requirements_Load_BlankTestNameInRequirement_ReportsErrorWithFileLocation() { // Arrange: create a YAML file with a blank test name entry in a requirement @@ -334,16 +351,16 @@ public void Requirements_Load_BlankTestNameInRequirement_ReportsErrorWithFileLoc var result = Requirements.Load(filePath); // Assert: error reported with file location for the blank test name - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Test name cannot be blank"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Test name cannot be blank")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); } /// /// Test that a blank test name in a mapping reports an error issue with file location. /// - [TestMethod] + [Fact] public void Requirements_Load_BlankTestNameInMapping_ReportsErrorWithFileLocation() { // Arrange: create a YAML file with a blank test name in a mapping @@ -367,16 +384,16 @@ public void Requirements_Load_BlankTestNameInMapping_ReportsErrorWithFileLocatio var result = Requirements.Load(filePath); // Assert: error reported with file location for the blank mapping test name - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Test name cannot be blank"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Test name cannot be blank")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); } /// /// Test that a blank mapping ID reports an error issue with file location. /// - [TestMethod] + [Fact] public void Requirements_Load_BlankMappingId_ReportsErrorWithFileLocation() { // Arrange: create a YAML file with a blank mapping ID @@ -399,16 +416,16 @@ public void Requirements_Load_BlankMappingId_ReportsErrorWithFileLocation() var result = Requirements.Load(filePath); // Assert: error reported with file location for the blank mapping ID - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Mapping 'id' cannot be blank"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Mapping 'id' cannot be blank")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); } /// /// Test reading test mappings that are separate from requirements. /// - [TestMethod] + [Fact] public void Requirements_Load_TestMappings_AppliesMappingsCorrectly() { // Arrange: create a YAML file with a mapping block that adds tests to an existing requirement @@ -430,22 +447,22 @@ public void Requirements_Load_TestMappings_AppliesMappingsCorrectly() // Act: load the requirements file var result = Requirements.Load(filePath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: mappings applied correctly - Assert.IsNotNull(requirements); + Assert.NotNull(requirements); var req = requirements.Sections[0].Requirements[0]; - Assert.AreEqual("DATA-001", req.Id); - Assert.HasCount(2, req.Tests); - Assert.AreEqual("Logging_ValidRequest_Logged", req.Tests[0]); - Assert.AreEqual("Logging_InvalidRequest_Logged", req.Tests[1]); + Assert.Equal("DATA-001", req.Id); + Assert.Equal(2, req.Tests.Count); + Assert.Equal("Logging_ValidRequest_Logged", req.Tests[0]); + Assert.Equal("Logging_InvalidRequest_Logged", req.Tests[1]); } /// /// Test that circular requirements (A -> B -> A) throw an exception at read time. /// - [TestMethod] + [Fact] public void Requirements_Load_CircularRequirements_ThrowsInvalidOperationException() { // Arrange: create a YAML file with circular child references @@ -469,17 +486,17 @@ public void Requirements_Load_CircularRequirements_ThrowsInvalidOperationExcepti var result = Requirements.Load(filePath); // Assert: circular reference error reported - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Circular requirement reference detected"), result.Issues); - Assert.Contains(i => i.Description.Contains("REQ-A"), result.Issues); - Assert.Contains(i => i.Description.Contains("REQ-B"), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Circular requirement reference detected")); + Assert.Contains(result.Issues, i => i.Description.Contains("REQ-A")); + Assert.Contains(result.Issues, i => i.Description.Contains("REQ-B")); } /// /// Test that a self-referencing requirement (A -> A) reports an error issue at load time. /// - [TestMethod] + [Fact] public void Requirements_Load_SelfReferencingRequirement_ReportsCircularReferenceError() { // Arrange: create a YAML file with a self-referencing child @@ -499,16 +516,16 @@ public void Requirements_Load_SelfReferencingRequirement_ReportsCircularReferenc var result = Requirements.Load(filePath); // Assert: circular reference error reported - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Circular requirement reference detected"), result.Issues); - Assert.Contains(i => i.Description.Contains("REQ-A"), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Circular requirement reference detected")); + Assert.Contains(result.Issues, i => i.Description.Contains("REQ-A")); } /// /// Test that duplicate IDs across multiple files are detected. /// - [TestMethod] + [Fact] public void Requirements_Load_MultipleFilesWithDuplicateIds_ReportsError() { // Arrange: create two YAML files that share a requirement ID @@ -535,17 +552,17 @@ public void Requirements_Load_MultipleFilesWithDuplicateIds_ReportsError() var result = Requirements.Load(file1Path, file2Path); // Assert: error reported for duplicate ID across files - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("SYS-SEC-001"), result.Issues); - Assert.Contains(i => i.Description.Contains("Duplicate requirement ID"), result.Issues); - Assert.Contains(i => i.Location.Contains(file2Path), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("SYS-SEC-001")); + Assert.Contains(result.Issues, i => i.Description.Contains("Duplicate requirement ID")); + Assert.Contains(result.Issues, i => i.Location.Contains(file2Path)); } /// /// Test that a blank tag name reports an error issue with file location. /// - [TestMethod] + [Fact] public void Requirements_Load_BlankTagName_ReportsErrorWithFileLocation() { // Arrange: create a YAML file with a blank tag name @@ -567,16 +584,16 @@ public void Requirements_Load_BlankTagName_ReportsErrorWithFileLocation() var result = Requirements.Load(filePath); // Assert: error reported with file location for the blank tag name - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Tag name cannot be blank"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Tag name cannot be blank")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); } /// /// Test that a blank child ID in a requirement reports an error issue with file location. /// - [TestMethod] + [Fact] public void Requirements_Load_BlankChildIdInRequirement_ReportsErrorWithFileLocation() { // Arrange: create a YAML file with a blank entry in a requirement's children list @@ -599,9 +616,9 @@ public void Requirements_Load_BlankChildIdInRequirement_ReportsErrorWithFileLoca var result = Requirements.Load(filePath); // Assert: error reported with file location for the blank child ID - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Child requirement reference cannot be blank"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Child requirement reference cannot be blank")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsExportTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsExportTests.cs index 6337353..3eee69d 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsExportTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsExportTests.cs @@ -25,16 +25,14 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// /// Unit tests for Requirements Markdown export functionality. /// -[TestClass] -public class RequirementsExportTests +public sealed class RequirementsExportTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public RequirementsExportTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -43,19 +41,19 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test exporting a simple requirements document to Markdown. /// - [TestMethod] + [Fact] public void Requirements_Export_SimpleRequirements_CreatesMarkdownFile() { // Arrange: create a requirements YAML file with two security requirements @@ -71,14 +69,14 @@ public void Requirements_Export_SimpleRequirements_CreatesMarkdownFile() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export requirements to a Markdown file var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath); - Assert.IsTrue(File.Exists(mdPath)); + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); Assert.Contains("# System Security", content); Assert.Contains("| ID | Title |", content); @@ -89,7 +87,7 @@ public void Requirements_Export_SimpleRequirements_CreatesMarkdownFile() /// /// Test exporting requirements with custom depth. /// - [TestMethod] + [Fact] public void Requirements_Export_WithCustomDepth_UsesCorrectHeaderLevel() { // Arrange: create a requirements YAML file with one requirement @@ -103,7 +101,7 @@ public void Requirements_Export_WithCustomDepth_UsesCorrectHeaderLevel() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export with depth 3 @@ -117,7 +115,7 @@ public void Requirements_Export_WithCustomDepth_UsesCorrectHeaderLevel() /// /// Test exporting nested sections with proper hierarchy. /// - [TestMethod] + [Fact] public void Requirements_Export_NestedSections_CreatesHierarchy() { // Arrange: create a requirements YAML file with nested sections @@ -137,7 +135,7 @@ public void Requirements_Export_NestedSections_CreatesHierarchy() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export requirements to Markdown @@ -155,7 +153,7 @@ public void Requirements_Export_NestedSections_CreatesHierarchy() /// /// Test exporting a section with no requirements (only subsections). /// - [TestMethod] + [Fact] public void Requirements_Export_SectionWithNoRequirements_CreatesHeaderOnly() { // Arrange: create a requirements YAML file with a parent section that has no direct requirements @@ -171,7 +169,7 @@ public void Requirements_Export_SectionWithNoRequirements_CreatesHeaderOnly() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export requirements to Markdown @@ -187,7 +185,7 @@ public void Requirements_Export_SectionWithNoRequirements_CreatesHeaderOnly() /// /// Test that export throws exception when file path is null. /// - [TestMethod] + [Fact] public void Requirements_Export_NullFilePath_ThrowsArgumentException() { // Arrange: load a valid requirements file @@ -201,18 +199,18 @@ public void Requirements_Export_NullFilePath_ThrowsArgumentException() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act + Assert: calling Export with null path throws ArgumentException - var ex = Assert.ThrowsExactly(() => requirements.Export(null!)); + var ex = Assert.Throws(() => requirements.Export(null!)); Assert.Contains("File path cannot be null or empty", ex.Message); } /// /// Test that export throws exception when file path is empty. /// - [TestMethod] + [Fact] public void Requirements_Export_EmptyFilePath_ThrowsArgumentException() { // Arrange: load a valid requirements file @@ -226,18 +224,18 @@ public void Requirements_Export_EmptyFilePath_ThrowsArgumentException() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act + Assert: calling Export with empty path throws ArgumentException - var ex = Assert.ThrowsExactly(() => requirements.Export(string.Empty)); + var ex = Assert.Throws(() => requirements.Export(string.Empty)); Assert.Contains("File path cannot be null or empty", ex.Message); } /// /// Test exporting multiple sections at the root level. /// - [TestMethod] + [Fact] public void Requirements_Export_MultipleSections_ExportsAll() { // Arrange: create a requirements YAML file with two top-level sections @@ -255,7 +253,7 @@ public void Requirements_Export_MultipleSections_ExportsAll() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export requirements to Markdown @@ -272,7 +270,7 @@ public void Requirements_Export_MultipleSections_ExportsAll() /// /// Test exporting empty requirements document. /// - [TestMethod] + [Fact] public void Requirements_Export_EmptyRequirements_CreatesEmptyFile() { // Arrange: create an empty requirements YAML file @@ -281,22 +279,22 @@ public void Requirements_Export_EmptyRequirements_CreatesEmptyFile() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export requirements to Markdown var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath); - Assert.IsTrue(File.Exists(mdPath)); + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); - Assert.AreEqual(string.Empty, content); + Assert.Equal(string.Empty, content); } /// /// Test exporting justifications for requirements with justification field. /// - [TestMethod] + [Fact] public void Requirements_ExportJustifications_WithJustifications_CreatesMarkdownFile() { // Arrange: create a requirements YAML file with two requirements with justifications @@ -318,14 +316,14 @@ brute force or dictionary attacks. var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export justifications to Markdown var mdPath = Path.Combine(_testDirectory, "justifications.md"); requirements.ExportJustifications(mdPath); - Assert.IsTrue(File.Exists(mdPath)); + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); Assert.Contains("# System Security", content); Assert.Contains("## SYS-SEC-001", content); @@ -339,7 +337,7 @@ brute force or dictionary attacks. /// /// Test exporting justifications with custom depth. /// - [TestMethod] + [Fact] public void Requirements_ExportJustifications_WithCustomDepth_UsesCorrectHeaderLevel() { // Arrange: create a requirements YAML file with one requirement with justification @@ -354,7 +352,7 @@ public void Requirements_ExportJustifications_WithCustomDepth_UsesCorrectHeaderL var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export justifications with depth 2 @@ -370,7 +368,7 @@ public void Requirements_ExportJustifications_WithCustomDepth_UsesCorrectHeaderL /// /// Test exporting justifications for requirements without justification field. /// - [TestMethod] + [Fact] public void Requirements_ExportJustifications_WithoutJustifications_CreatesHeadersOnly() { // Arrange: create a requirements YAML file with a requirement that has no justification @@ -384,14 +382,14 @@ public void Requirements_ExportJustifications_WithoutJustifications_CreatesHeade var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export justifications to Markdown var mdPath = Path.Combine(_testDirectory, "justifications.md"); requirements.ExportJustifications(mdPath); - Assert.IsTrue(File.Exists(mdPath)); + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); Assert.Contains("# System Security", content); Assert.Contains("## SYS-SEC-001", content); @@ -422,7 +420,7 @@ public void Requirements_ExportJustifications_WithoutJustifications_CreatesHeade /// /// Test exporting justifications with nested sections. /// - [TestMethod] + [Fact] public void Requirements_ExportJustifications_NestedSections_CreatesHierarchy() { // Arrange: create a requirements YAML file with nested sections and justifications @@ -439,7 +437,7 @@ public void Requirements_ExportJustifications_NestedSections_CreatesHierarchy() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export justifications to Markdown @@ -457,7 +455,7 @@ public void Requirements_ExportJustifications_NestedSections_CreatesHierarchy() /// /// Test exporting requirements with filter tags. /// - [TestMethod] + [Fact] public void Requirements_Export_WithFilterTags_ExportsOnlyMatchingRequirements() { // Arrange: create a requirements YAML file with requirements tagged security and performance @@ -482,7 +480,7 @@ public void Requirements_Export_WithFilterTags_ExportsOnlyMatchingRequirements() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export with security filter @@ -490,7 +488,7 @@ public void Requirements_Export_WithFilterTags_ExportsOnlyMatchingRequirements() var filterTags = new HashSet { "security" }; requirements.Export(mdPath, filterTags: filterTags); - Assert.IsTrue(File.Exists(mdPath)); + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); Assert.Contains("# System Security", content); Assert.Contains("| SYS-SEC-001 | The system shall support credentials authentication. |", content); @@ -502,7 +500,7 @@ public void Requirements_Export_WithFilterTags_ExportsOnlyMatchingRequirements() /// /// Test exporting requirements with multiple filter tags. /// - [TestMethod] + [Fact] public void Requirements_Export_WithMultipleFilterTags_ExportsRequirementsMatchingAnyTag() { // Arrange: create a requirements YAML file with security, performance, and data-integrity requirements @@ -526,7 +524,7 @@ public void Requirements_Export_WithMultipleFilterTags_ExportsRequirementsMatchi var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export with security and data-integrity filter @@ -534,7 +532,7 @@ public void Requirements_Export_WithMultipleFilterTags_ExportsRequirementsMatchi var filterTags = new HashSet { "security", "data-integrity" }; requirements.Export(mdPath, filterTags: filterTags); - Assert.IsTrue(File.Exists(mdPath)); + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); Assert.Contains("| SYS-SEC-001 | The system shall support credentials authentication. |", content); Assert.Contains("| SYS-DATA-001 | The system shall maintain data integrity. |", content); @@ -544,7 +542,7 @@ public void Requirements_Export_WithMultipleFilterTags_ExportsRequirementsMatchi /// /// Test exporting requirements with filter that matches no requirements. /// - [TestMethod] + [Fact] public void Requirements_Export_WithFilterMatchingNoRequirements_ExportsEmptyFile() { // Arrange: create a requirements YAML file with only security-tagged requirements @@ -560,7 +558,7 @@ public void Requirements_Export_WithFilterMatchingNoRequirements_ExportsEmptyFil var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export with a performance filter that matches nothing @@ -568,7 +566,7 @@ public void Requirements_Export_WithFilterMatchingNoRequirements_ExportsEmptyFil var filterTags = new HashSet { "performance" }; requirements.Export(mdPath, filterTags: filterTags); - Assert.IsTrue(File.Exists(mdPath)); + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); Assert.DoesNotContain("System Security", content); Assert.DoesNotContain("SYS-SEC-001", content); @@ -577,7 +575,7 @@ public void Requirements_Export_WithFilterMatchingNoRequirements_ExportsEmptyFil /// /// Test exporting requirements without filter exports all requirements. /// - [TestMethod] + [Fact] public void Requirements_Export_WithoutFilter_ExportsAllRequirements() { // Arrange: create a requirements YAML file with security and performance requirements @@ -597,14 +595,14 @@ public void Requirements_Export_WithoutFilter_ExportsAllRequirements() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export with no filter var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath); - Assert.IsTrue(File.Exists(mdPath)); + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); Assert.Contains("| SYS-SEC-001 | The system shall support credentials authentication. |", content); Assert.Contains("| SYS-PERF-001 | The system shall respond within 100ms. |", content); @@ -613,7 +611,7 @@ public void Requirements_Export_WithoutFilter_ExportsAllRequirements() /// /// Test exporting justifications with filter tags. /// - [TestMethod] + [Fact] public void Requirements_ExportJustifications_WithFilterTags_ExportsOnlyMatchingRequirements() { // Arrange: create a requirements YAML file with security and performance requirements with justifications @@ -636,7 +634,7 @@ public void Requirements_ExportJustifications_WithFilterTags_ExportsOnlyMatching var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export justifications with security filter @@ -655,7 +653,7 @@ public void Requirements_ExportJustifications_WithFilterTags_ExportsOnlyMatching /// /// Test exporting with filter excludes sections with no matching requirements. /// - [TestMethod] + [Fact] public void Requirements_Export_WithFilterExcludesEmptySections_OnlyShowsSectionsWithMatchingRequirements() { // Arrange: create a requirements YAML file with two top-level sections, each with differently tagged requirements @@ -677,7 +675,7 @@ public void Requirements_Export_WithFilterExcludesEmptySections_OnlyShowsSection var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Act: export with security filter @@ -685,7 +683,7 @@ public void Requirements_Export_WithFilterExcludesEmptySections_OnlyShowsSection var filterTags = new HashSet { "security" }; requirements.Export(mdPath, filterTags: filterTags); - Assert.IsTrue(File.Exists(mdPath)); + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); Assert.Contains("# System Security", content); Assert.Contains("| SYS-SEC-001 |", content); diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs index 808f8cf..9f154c5 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs @@ -25,16 +25,14 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// /// Unit tests for Requirements YAML loading and model parsing functionality. /// -[TestClass] -public class RequirementsLoadParsingTests +public sealed class RequirementsLoadParsingTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public RequirementsLoadParsingTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -43,19 +41,19 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test reading a file with includes. /// - [TestMethod] + [Fact] public void Requirements_Load_WithIncludes_MergesFilesCorrectly() { // Arrange: create a main YAML file with an include directive pointing to an additional file @@ -83,22 +81,22 @@ public void Requirements_Load_WithIncludes_MergesFilesCorrectly() // Act: load the main requirements file var result = Requirements.Load(mainPath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: requirements from both files merged into the tree - Assert.IsNotNull(requirements); - Assert.HasCount(2, requirements.Sections); - Assert.AreEqual("System Security", requirements.Sections[0].Title); - Assert.AreEqual("Data Management", requirements.Sections[1].Title); - Assert.AreEqual("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); - Assert.AreEqual("DATA-001", requirements.Sections[1].Requirements[0].Id); + Assert.NotNull(requirements); + Assert.Equal(2, requirements.Sections.Count); + Assert.Equal("System Security", requirements.Sections[0].Title); + Assert.Equal("Data Management", requirements.Sections[1].Title); + Assert.Equal("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); + Assert.Equal("DATA-001", requirements.Sections[1].Requirements[0].Id); } /// /// Test that identical sections are merged. /// - [TestMethod] + [Fact] public void Requirements_Load_IdenticalSections_MergesCorrectly() { // Arrange: create two YAML files with the same section title @@ -126,22 +124,22 @@ public void Requirements_Load_IdenticalSections_MergesCorrectly() // Act: load the main requirements file var result = Requirements.Load(mainPath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: identical sections merged into one with both requirements - Assert.IsNotNull(requirements); - Assert.HasCount(1, requirements.Sections); - Assert.AreEqual("System Security", requirements.Sections[0].Title); - Assert.HasCount(2, requirements.Sections[0].Requirements); - Assert.AreEqual("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); - Assert.AreEqual("SYS-SEC-002", requirements.Sections[0].Requirements[1].Id); + Assert.NotNull(requirements); + Assert.Single(requirements.Sections); + Assert.Equal("System Security", requirements.Sections[0].Title); + Assert.Equal(2, requirements.Sections[0].Requirements.Count); + Assert.Equal("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); + Assert.Equal("SYS-SEC-002", requirements.Sections[0].Requirements[1].Id); } /// /// Test that include loops are prevented. /// - [TestMethod] + [Fact] public void Requirements_Load_IncludeLoop_DoesNotCauseInfiniteLoop() { // Arrange: create two YAML files that include each other @@ -174,17 +172,17 @@ public void Requirements_Load_IncludeLoop_DoesNotCauseInfiniteLoop() var result = Requirements.Load(pathA); // Assert: loading completes without infinite loop and both sections are present - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; - Assert.IsNotNull(requirements); - Assert.HasCount(2, requirements.Sections); + Assert.NotNull(requirements); + Assert.Equal(2, requirements.Sections.Count); } /// /// Test that a missing file reports an error issue. /// - [TestMethod] + [Fact] public void Requirements_Load_FileNotFound_ReportsError() { // Arrange: create a path to a file that does not exist @@ -194,16 +192,16 @@ public void Requirements_Load_FileNotFound_ReportsError() var result = Requirements.Load(nonExistentPath); // Assert: error reported with the missing file location - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("File not found"), result.Issues); - Assert.Contains(i => i.Location.Contains(nonExistentPath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("File not found")); + Assert.Contains(result.Issues, i => i.Location.Contains(nonExistentPath)); } /// /// Test that an invalid YAML content (schema error) throws an InvalidOperationException with the file location. /// - [TestMethod] + [Fact] public void Requirements_Load_InvalidYamlContent_ReportsErrorWithFileLocation() { // Arrange: create a YAML file with an invalid property name @@ -221,17 +219,17 @@ public void Requirements_Load_InvalidYamlContent_ReportsErrorWithFileLocation() var result = Requirements.Load(filePath); // Assert: error reported with file location for the unknown field - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Unknown field 'text' in requirement"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); - Assert.Contains(i => i.Location.Contains($"{filePath}("), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Unknown field 'text' in requirement")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); + Assert.Contains(result.Issues, i => i.Location.Contains($"{filePath}(")); } /// /// Test reading an empty YAML file. /// - [TestMethod] + [Fact] public void Requirements_Load_EmptyFile_ReturnsEmptyRequirements() { // Arrange: create an empty YAML file @@ -242,19 +240,19 @@ public void Requirements_Load_EmptyFile_ReturnsEmptyRequirements() // Act: load the requirements file var result = Requirements.Load(filePath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: empty requirements returned with no sections - Assert.IsNotNull(requirements); - Assert.IsEmpty(requirements.Sections); - Assert.IsEmpty(requirements.Requirements); + Assert.NotNull(requirements); + Assert.Empty(requirements.Sections); + Assert.Empty(requirements.Requirements); } /// /// Test reading a complex nested structure. /// - [TestMethod] + [Fact] public void Requirements_Load_ComplexStructure_ParsesCorrectly() { // Arrange: create a YAML file with a complex nested structure including children, tests, and mappings @@ -294,40 +292,40 @@ public void Requirements_Load_ComplexStructure_ParsesCorrectly() // Act: load the requirements file var result = Requirements.Load(filePath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: complex structure parsed correctly - Assert.IsNotNull(requirements); - Assert.HasCount(2, requirements.Sections); + Assert.NotNull(requirements); + Assert.Equal(2, requirements.Sections.Count); var sysSec = requirements.Sections[0]; - Assert.AreEqual("System Security", sysSec.Title); - Assert.HasCount(1, sysSec.Requirements); - Assert.AreEqual("SYS-SEC-001", sysSec.Requirements[0].Id); - Assert.HasCount(1, sysSec.Requirements[0].Children); - Assert.AreEqual("AUTH-001", sysSec.Requirements[0].Children[0]); + Assert.Equal("System Security", sysSec.Title); + Assert.Single(sysSec.Requirements); + Assert.Equal("SYS-SEC-001", sysSec.Requirements[0].Id); + Assert.Single(sysSec.Requirements[0].Children); + Assert.Equal("AUTH-001", sysSec.Requirements[0].Children[0]); var dataManagement = requirements.Sections[1]; - Assert.AreEqual("Data Management", dataManagement.Title); - Assert.HasCount(2, dataManagement.Sections); + Assert.Equal("Data Management", dataManagement.Title); + Assert.Equal(2, dataManagement.Sections.Count); var auth = dataManagement.Sections[0]; - Assert.AreEqual("User Authentication", auth.Title); - Assert.AreEqual("AUTH-001", auth.Requirements[0].Id); - Assert.HasCount(3, auth.Requirements[0].Tests); + Assert.Equal("User Authentication", auth.Title); + Assert.Equal("AUTH-001", auth.Requirements[0].Id); + Assert.Equal(3, auth.Requirements[0].Tests.Count); var logging = dataManagement.Sections[1]; - Assert.AreEqual("Logging", logging.Title); - Assert.AreEqual("DATA-001", logging.Requirements[0].Id); - Assert.HasCount(2, logging.Requirements[0].Tests); - Assert.AreEqual("Logging_ValidRequest_Logged", logging.Requirements[0].Tests[0]); + Assert.Equal("Logging", logging.Title); + Assert.Equal("DATA-001", logging.Requirements[0].Id); + Assert.Equal(2, logging.Requirements[0].Tests.Count); + Assert.Equal("Logging_ValidRequest_Logged", logging.Requirements[0].Tests[0]); } /// /// Test reading multiple files with params array. /// - [TestMethod] + [Fact] public void Requirements_Load_MultipleFiles_MergesAllFiles() { // Arrange: create three YAML files with different sections @@ -361,24 +359,24 @@ public void Requirements_Load_MultipleFiles_MergesAllFiles() // Act: load all three files var result = Requirements.Load(file1Path, file2Path, file3Path); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: all three sections merged into the requirements tree - Assert.IsNotNull(requirements); - Assert.HasCount(3, requirements.Sections); - Assert.AreEqual("System Security", requirements.Sections[0].Title); - Assert.AreEqual("Data Management", requirements.Sections[1].Title); - Assert.AreEqual("Performance", requirements.Sections[2].Title); - Assert.AreEqual("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); - Assert.AreEqual("DATA-001", requirements.Sections[1].Requirements[0].Id); - Assert.AreEqual("PERF-001", requirements.Sections[2].Requirements[0].Id); + Assert.NotNull(requirements); + Assert.Equal(3, requirements.Sections.Count); + Assert.Equal("System Security", requirements.Sections[0].Title); + Assert.Equal("Data Management", requirements.Sections[1].Title); + Assert.Equal("Performance", requirements.Sections[2].Title); + Assert.Equal("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); + Assert.Equal("DATA-001", requirements.Sections[1].Requirements[0].Id); + Assert.Equal("PERF-001", requirements.Sections[2].Requirements[0].Id); } /// /// Test reading multiple files that merge sections. /// - [TestMethod] + [Fact] public void Requirements_Load_MultipleFilesWithSameSections_MergesSections() { // Arrange: create two YAML files with the same section title @@ -403,22 +401,22 @@ public void Requirements_Load_MultipleFilesWithSameSections_MergesSections() // Act: load both files var result = Requirements.Load(file1Path, file2Path); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: sections with the same title merged into one section with both requirements - Assert.IsNotNull(requirements); - Assert.HasCount(1, requirements.Sections); - Assert.AreEqual("System Security", requirements.Sections[0].Title); - Assert.HasCount(2, requirements.Sections[0].Requirements); - Assert.AreEqual("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); - Assert.AreEqual("SYS-SEC-002", requirements.Sections[0].Requirements[1].Id); + Assert.NotNull(requirements); + Assert.Single(requirements.Sections); + Assert.Equal("System Security", requirements.Sections[0].Title); + Assert.Equal(2, requirements.Sections[0].Requirements.Count); + Assert.Equal("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); + Assert.Equal("SYS-SEC-002", requirements.Sections[0].Requirements[1].Id); } /// /// Test reading single file with params array (backwards compatibility). /// - [TestMethod] + [Fact] public void Requirements_Load_SingleFileWithParamsArray_WorksCorrectly() { // Arrange: create a YAML file with one requirement @@ -434,36 +432,36 @@ public void Requirements_Load_SingleFileWithParamsArray_WorksCorrectly() // Act: load the requirements file var result = Requirements.Load(filePath); - Assert.IsFalse(result.HasErrors); + Assert.False(result.HasErrors); var requirements = result.Requirements; // Assert: requirement loaded correctly - Assert.IsNotNull(requirements); - Assert.HasCount(1, requirements.Sections); - Assert.AreEqual("System Security", requirements.Sections[0].Title); - Assert.HasCount(1, requirements.Sections[0].Requirements); - Assert.AreEqual("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); + Assert.NotNull(requirements); + Assert.Single(requirements.Sections); + Assert.Equal("System Security", requirements.Sections[0].Title); + Assert.Single(requirements.Sections[0].Requirements); + Assert.Equal("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); } /// /// Test that calling Read with no arguments throws ArgumentException. /// - [TestMethod] + [Fact] public void Requirements_Load_NoArguments_ThrowsArgumentException() { // Act + Assert: calling Load with no arguments throws ArgumentException - var ex = Assert.ThrowsExactly(() => Requirements.Load()); + var ex = Assert.Throws(() => Requirements.Load()); Assert.Contains("At least one file path must be provided", ex.Message); } /// /// Test that calling Read with null throws ArgumentException. /// - [TestMethod] + [Fact] public void Requirements_Load_NullArgument_ThrowsArgumentException() { // Act + Assert: calling Load with null throws ArgumentException - var ex = Assert.ThrowsExactly(() => Requirements.Load(null!)); + var ex = Assert.Throws(() => Requirements.Load(null!)); Assert.Contains("At least one file path must be provided", ex.Message); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadTests.cs index 8e5e47a..c9ef43b 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadTests.cs @@ -25,16 +25,14 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// /// Unit tests for Requirements.Load() unified loading with linting. /// -[TestClass] -public class RequirementsLoadTests +public sealed class RequirementsLoadTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public RequirementsLoadTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_load_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -43,19 +41,19 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test that loading a valid file returns requirements and no issues. /// - [TestMethod] + [Fact] public void Requirements_Load_ValidFile_ReturnsRequirementsAndNoIssues() { // Arrange: create a valid YAML file @@ -73,16 +71,16 @@ public void Requirements_Load_ValidFile_ReturnsRequirementsAndNoIssues() var result = Requirements.Load(filePath); // Assert: requirements loaded with no issues - Assert.IsNotNull(result.Requirements); - Assert.HasCount(0, result.Issues); - Assert.HasCount(1, result.Requirements.Sections); - Assert.AreEqual("REQ-001", result.Requirements.Sections[0].Requirements[0].Id); + Assert.NotNull(result.Requirements); + Assert.Empty(result.Issues); + Assert.Single(result.Requirements.Sections); + Assert.Equal("REQ-001", result.Requirements.Sections[0].Requirements[0].Id); } /// /// Test that loading a file with a lint error returns null requirements and issues. /// - [TestMethod] + [Fact] public void Requirements_Load_WithLintError_ReturnsNullAndIssues() { // Arrange: create a YAML file with an unknown field @@ -101,32 +99,32 @@ public void Requirements_Load_WithLintError_ReturnsNullAndIssues() var result = Requirements.Load(filePath); // Assert: null requirements returned with error issues - Assert.IsNull(result.Requirements); - Assert.IsNotEmpty(result.Issues); - Assert.Contains(i => i.Severity == LintSeverity.Error, result.Issues); - Assert.Contains(i => i.Description.Contains("Unknown field 'unknown_field'"), result.Issues); + Assert.Null(result.Requirements); + Assert.NotEmpty(result.Issues); + Assert.Contains(result.Issues, i => i.Severity == LintSeverity.Error); + Assert.Contains(result.Issues, i => i.Description.Contains("Unknown field 'unknown_field'")); } /// /// Test that loading a missing file returns null requirements and an error issue. /// - [TestMethod] + [Fact] public void Requirements_Load_MissingFile_ReturnsNullAndIssues() { // Act: load a non-existent file var result = Requirements.Load("/nonexistent/path/missing.yaml"); // Assert: null requirements returned with File not found error - Assert.IsNull(result.Requirements); - Assert.IsNotEmpty(result.Issues); - Assert.Contains(i => i.Severity == LintSeverity.Error, result.Issues); - Assert.Contains(i => i.Description.Contains("File not found"), result.Issues); + Assert.Null(result.Requirements); + Assert.NotEmpty(result.Issues); + Assert.Contains(result.Issues, i => i.Severity == LintSeverity.Error); + Assert.Contains(result.Issues, i => i.Description.Contains("File not found")); } /// /// Test that loading a file with malformed YAML returns null requirements and an error issue. /// - [TestMethod] + [Fact] public void Requirements_Load_MalformedYaml_ReturnsNullAndIssues() { // Arrange: create a YAML file with invalid syntax @@ -142,16 +140,16 @@ invalid yaml here var result = Requirements.Load(filePath); // Assert: null requirements returned with malformed YAML error - Assert.IsNull(result.Requirements); - Assert.IsNotEmpty(result.Issues); - Assert.Contains(i => i.Severity == LintSeverity.Error, result.Issues); - Assert.Contains(i => i.Description.Contains("Malformed YAML"), result.Issues); + Assert.Null(result.Requirements); + Assert.NotEmpty(result.Issues); + Assert.Contains(result.Issues, i => i.Severity == LintSeverity.Error); + Assert.Contains(result.Issues, i => i.Description.Contains("Malformed YAML")); } /// /// Test that lint issues contain location information. /// - [TestMethod] + [Fact] public void Requirements_Load_WithLintError_IssueIncludesLocation() { // Arrange: create a YAML file with an unknown field at root level @@ -164,7 +162,7 @@ public void Requirements_Load_WithLintError_IssueIncludesLocation() var result = Requirements.Load(filePath); // Assert: issue location includes the file path and severity text - Assert.IsNotEmpty(result.Issues); + Assert.NotEmpty(result.Issues); var issue = result.Issues[0]; Assert.Contains(filePath, issue.Location); Assert.Contains("error:", issue.ToString()); @@ -173,7 +171,7 @@ public void Requirements_Load_WithLintError_IssueIncludesLocation() /// /// Test that loading a file with multiple lint errors reports all of them. /// - [TestMethod] + [Fact] public void Requirements_Load_WithMultipleLintErrors_ReportsAllIssues() { // Arrange: create a YAML file with multiple structural issues @@ -195,28 +193,28 @@ public void Requirements_Load_WithMultipleLintErrors_ReportsAllIssues() var result = Requirements.Load(filePath); // Assert: all issues reported - Assert.IsNull(result.Requirements); - Assert.IsGreaterThanOrEqualTo(4, result.Issues.Count); - Assert.Contains(i => i.Description.Contains("Unknown field 'unknown_section_field'"), result.Issues); - Assert.Contains(i => i.Description.Contains("Requirement missing required field 'id'"), result.Issues); - Assert.Contains(i => i.Description.Contains("Duplicate requirement ID 'REQ-001'"), result.Issues); - Assert.Contains(i => i.Description.Contains("Unknown field 'unknown_root_field'"), result.Issues); + Assert.Null(result.Requirements); + Assert.True(result.Issues.Count >= 4); + Assert.Contains(result.Issues, i => i.Description.Contains("Unknown field 'unknown_section_field'")); + Assert.Contains(result.Issues, i => i.Description.Contains("Requirement missing required field 'id'")); + Assert.Contains(result.Issues, i => i.Description.Contains("Duplicate requirement ID 'REQ-001'")); + Assert.Contains(result.Issues, i => i.Description.Contains("Unknown field 'unknown_root_field'")); } /// /// Test that loading with no files throws ArgumentException. /// - [TestMethod] + [Fact] public void Requirements_Load_NoFiles_ThrowsArgumentException() { // Act + Assert: calling Load with no arguments throws ArgumentException - Assert.ThrowsExactly(() => Requirements.Load()); + Assert.Throws(() => Requirements.Load()); } /// /// Test that loading follows includes and lints included files. /// - [TestMethod] + [Fact] public void Requirements_Load_WithIncludes_LintsIncludedFiles() { // Arrange: create a root YAML file and an included file with a lint error @@ -243,9 +241,9 @@ public void Requirements_Load_WithIncludes_LintsIncludedFiles() var result = Requirements.Load(rootFile); // Assert: error from included file is reported - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Severity == LintSeverity.Error, result.Issues); - Assert.Contains(i => i.Description.Contains("Unknown field 'unknown_field'"), result.Issues); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Severity == LintSeverity.Error); + Assert.Contains(result.Issues, i => i.Description.Contains("Unknown field 'unknown_field'")); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs index 583a1d9..f0d6fee 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs @@ -26,16 +26,14 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// Unit tests for the RequirementsLoader: verifies that structural issues in requirements /// YAML files are reported as lint issues when loading via Requirements.Load(). /// -[TestClass] -public class RequirementsLoaderTests +public sealed class RequirementsLoaderTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public RequirementsLoaderTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_loader_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -44,13 +42,13 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// @@ -80,7 +78,7 @@ private static (int exitCode, string output, string errors) RunLintWithOutput(pa /// /// Test that a valid requirements file produces no issues. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithValidFile_ReportsNoIssues() { // Arrange: create a valid requirements YAML file @@ -100,22 +98,22 @@ public void RequirementsLoader_Load_WithValidFile_ReportsNoIssues() var (exitCode, output, errors) = RunLintWithOutput(reqFile); // Assert: exit code is 0 and no issues are reported - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); Assert.Contains("No issues found", output); - Assert.AreEqual(string.Empty, errors); + Assert.Equal(string.Empty, errors); } /// /// Test that an invalid file path (containing null characters) reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithInvalidFilePath_ReportsError() { // Act: attempt to load a file with a null character in the path (invalid on all platforms) var (exitCode, errors) = RunLint("path\0with_null.yaml"); // Assert: exit code is 1 and error mentions invalid file path - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("error", errors); Assert.Contains("Invalid file path", errors); } @@ -123,20 +121,20 @@ public void RequirementsLoader_Load_WithInvalidFilePath_ReportsError() /// /// Test that a file that cannot be read due to an I/O failure reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithIoReadFailure_ReportsError() { // Skip this test on non-Unix platforms (file permission removal requires Unix) if (!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) { - Assert.Inconclusive("This test requires Unix file permissions."); + Assert.Skip("This test requires Unix file permissions."); return; } // Skip if running as root (root can read any file regardless of permissions) if (Environment.IsPrivilegedProcess) { - Assert.Inconclusive("This test cannot run as root."); + Assert.Skip("This test cannot run as root."); return; } @@ -150,13 +148,13 @@ public void RequirementsLoader_Load_WithIoReadFailure_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the read failure - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("error", errors); Assert.Contains("Failed to read file", errors); } finally { - // Restore permissions so TestCleanup can delete the file + // Restore permissions so cleanup can delete the file File.SetUnixFileMode(reqFile, UnixFileMode.UserRead | UnixFileMode.UserWrite); } } @@ -164,14 +162,14 @@ public void RequirementsLoader_Load_WithIoReadFailure_ReportsError() /// /// Test that a file that doesn't exist reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithMissingFile_ReportsError() { // Act: attempt to load a file that does not exist var (exitCode, errors) = RunLint("/nonexistent/path/missing.yaml"); // Assert: exit code is 1 and error mentions the file not found - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("error", errors); Assert.Contains("File not found", errors); } @@ -179,7 +177,7 @@ public void RequirementsLoader_Load_WithMissingFile_ReportsError() /// /// Test that malformed YAML reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithMalformedYaml_ReportsError() { // Arrange: create a YAML file with invalid syntax @@ -194,7 +192,7 @@ invalid yaml here var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports malformed YAML - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("error", errors); Assert.Contains("Malformed YAML", errors); } @@ -202,7 +200,7 @@ invalid yaml here /// /// Test that an empty YAML file produces no issues. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithEmptyFile_ReportsNoIssues() { // Arrange: create an empty YAML file @@ -213,15 +211,15 @@ public void RequirementsLoader_Load_WithEmptyFile_ReportsNoIssues() var (exitCode, output, errors) = RunLintWithOutput(reqFile); // Assert: exit code is 0 and no issues are reported - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); Assert.Contains("No issues found", output); - Assert.AreEqual(string.Empty, errors); + Assert.Equal(string.Empty, errors); } /// /// Test that an unknown field at document root reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithUnknownDocumentField_ReportsError() { // Arrange: create a YAML file with an unknown field at document root @@ -235,14 +233,14 @@ public void RequirementsLoader_Load_WithUnknownDocumentField_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error names the unknown field - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Unknown field 'unknown_field'", errors); } /// /// Test that a section missing the title field reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithSectionMissingTitle_ReportsError() { // Arrange: create a YAML file with a section that has no title @@ -257,14 +255,14 @@ public void RequirementsLoader_Load_WithSectionMissingTitle_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the missing title field - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Section missing required field 'title'", errors); } /// /// Test that a section with a blank title reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithBlankSectionTitle_ReportsError() { // Arrange: create a YAML file with a section whose title is blank @@ -280,14 +278,14 @@ public void RequirementsLoader_Load_WithBlankSectionTitle_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the blank title - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Section 'title' cannot be blank", errors); } /// /// Test that a section with an unknown field reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithUnknownSectionField_ReportsError() { // Arrange: create a YAML file with an unknown field inside a section @@ -301,14 +299,14 @@ public void RequirementsLoader_Load_WithUnknownSectionField_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error names the unknown section field - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Unknown field 'unknown_field' in section", errors); } /// /// Test that a requirement missing the id field reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithRequirementMissingId_ReportsError() { // Arrange: create a YAML file with a requirement that has no id field @@ -323,14 +321,14 @@ public void RequirementsLoader_Load_WithRequirementMissingId_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the missing id field - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Requirement missing required field 'id'", errors); } /// /// Test that a requirement missing the title field reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithRequirementMissingTitle_ReportsError() { // Arrange: create a YAML file with a requirement that has no title field @@ -345,14 +343,14 @@ public void RequirementsLoader_Load_WithRequirementMissingTitle_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the missing title field - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("missing required field 'title'", errors); } /// /// Test that a requirement with an unknown field reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithUnknownRequirementField_ReportsError() { // Arrange: create a YAML file with a requirement that has an unknown field @@ -369,14 +367,14 @@ public void RequirementsLoader_Load_WithUnknownRequirementField_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error names the unknown requirement field - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Unknown field 'unknown_field' in requirement", errors); } /// /// Test that duplicate requirement IDs report an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithDuplicateIds_ReportsError() { // Arrange: create a YAML file with two requirements sharing the same ID @@ -394,14 +392,14 @@ public void RequirementsLoader_Load_WithDuplicateIds_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the duplicate ID - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Duplicate requirement ID 'REQ-001'", errors); } /// /// Test that duplicate IDs across multiple files report an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithDuplicateIdsAcrossFiles_ReportsError() { // Arrange: create two YAML files that each define the same requirement ID @@ -425,14 +423,14 @@ public void RequirementsLoader_Load_WithDuplicateIdsAcrossFiles_ReportsError() var (exitCode, errors) = RunLint(reqFile1, reqFile2); // Assert: exit code is 1 and error reports the cross-file duplicate ID - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Duplicate requirement ID 'REQ-001'", errors); } /// /// Test that multiple issues are all reported. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithMultipleIssues_ReportsAllIssues() { // Arrange: create a YAML file with multiple structural errors @@ -453,7 +451,7 @@ public void RequirementsLoader_Load_WithMultipleIssues_ReportsAllIssues() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and all four errors are reported - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Unknown field 'unknown_section_field' in section", errors); Assert.Contains("Requirement missing required field 'id'", errors); Assert.Contains("Duplicate requirement ID 'REQ-001'", errors); @@ -463,7 +461,7 @@ public void RequirementsLoader_Load_WithMultipleIssues_ReportsAllIssues() /// /// Test that loading follows includes and lints included files. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithIncludes_LintsIncludedFiles() { // Arrange: create an included YAML file with an unknown field and a root file that includes it @@ -490,14 +488,14 @@ public void RequirementsLoader_Load_WithIncludes_LintsIncludedFiles() var (exitCode, errors) = RunLint(rootFile); // Assert: exit code is 1 and error from the included file is reported - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Unknown field 'unknown_field' in requirement", errors); } /// /// Test that a mapping with an unknown field reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithUnknownMappingField_ReportsError() { // Arrange: create a YAML file with an unknown field inside a mapping block @@ -518,14 +516,14 @@ public void RequirementsLoader_Load_WithUnknownMappingField_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error names the unknown mapping field - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Unknown field 'unknown_field' in mapping", errors); } /// /// Test that a mapping missing id reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithMappingMissingId_ReportsError() { // Arrange: create a YAML file with a mapping block that has no id field @@ -544,14 +542,14 @@ public void RequirementsLoader_Load_WithMappingMissingId_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the missing mapping id field - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Mapping missing required field 'id'", errors); } /// /// Test that a nested section with issues is linted. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithNestedSectionIssues_ReportsError() { // Arrange: create a YAML file with an issue inside a nested child section @@ -572,14 +570,14 @@ public void RequirementsLoader_Load_WithNestedSectionIssues_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error names the unknown nested requirement field - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Unknown field 'unknown_req_field' in requirement", errors); } /// /// Test that error format includes file path and line/column info. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_ErrorFormat_IncludesFileAndLocation() { // Arrange: create a YAML file with a single unknown root field @@ -599,7 +597,7 @@ public void RequirementsLoader_Load_ErrorFormat_IncludesFileAndLocation() /// /// Test that a requirement with a blank id reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithBlankRequirementId_ReportsError() { // Arrange: create a YAML file with a requirement whose id is blank @@ -615,14 +613,14 @@ public void RequirementsLoader_Load_WithBlankRequirementId_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the blank id - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Requirement 'id' cannot be blank", errors); } /// /// Test that a requirement with a blank title reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithBlankRequirementTitle_ReportsError() { // Arrange: create a YAML file with a requirement whose title is blank @@ -638,14 +636,14 @@ public void RequirementsLoader_Load_WithBlankRequirementTitle_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the blank title - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Requirement 'title' cannot be blank", errors); } /// /// Test that a mapping with a blank id reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithBlankMappingId_ReportsError() { // Arrange: create a YAML file with a mapping block whose id is blank @@ -665,14 +663,14 @@ public void RequirementsLoader_Load_WithBlankMappingId_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the blank mapping id - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Mapping 'id' cannot be blank", errors); } /// /// Test that a blank test name in a requirement reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithBlankTestName_ReportsError() { // Arrange: create a YAML file with a requirement that has a blank test name @@ -690,14 +688,14 @@ public void RequirementsLoader_Load_WithBlankTestName_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the blank test name - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Test name cannot be blank", errors); } /// /// Test that a blank tag name in a requirement reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithBlankTagName_ReportsError() { // Arrange: create a YAML file with a requirement that has a blank tag name @@ -715,14 +713,14 @@ public void RequirementsLoader_Load_WithBlankTagName_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the blank tag name - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Tag name cannot be blank", errors); } /// /// Test that a mapping with a blank test name reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithBlankMappingTestName_ReportsError() { // Arrange: create a YAML file with a mapping block that has a blank test name @@ -742,14 +740,14 @@ public void RequirementsLoader_Load_WithBlankMappingTestName_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the blank mapping test name - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Test name cannot be blank in mapping", errors); } /// /// Test that a requirements file with a non-mapping root (e.g. a top-level sequence) reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithNonMappingRoot_ReportsError() { // Arrange: create a YAML file whose root is a sequence rather than a mapping @@ -762,14 +760,14 @@ public void RequirementsLoader_Load_WithNonMappingRoot_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the non-mapping root - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Document root must be a mapping", errors); } /// /// Test that a non-scalar entry in the tests list of a requirement reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithNonScalarTestEntry_ReportsError() { // Arrange: create a YAML file with a mapping node instead of a scalar in the tests list @@ -787,14 +785,14 @@ public void RequirementsLoader_Load_WithNonScalarTestEntry_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the non-scalar test entry - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Test entry must be a scalar value", errors); } /// /// Test that a non-scalar entry in the children list of a requirement reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithNonScalarChildEntry_ReportsError() { // Arrange: create a YAML file with a mapping node instead of a scalar in the children list @@ -812,14 +810,14 @@ public void RequirementsLoader_Load_WithNonScalarChildEntry_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the non-scalar child entry - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Child requirement reference must be a scalar string", errors); } /// /// Test that a non-scalar entry in the tags list of a requirement reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithNonScalarTagEntry_ReportsError() { // Arrange: create a YAML file with a mapping node instead of a scalar in the tags list @@ -837,14 +835,14 @@ public void RequirementsLoader_Load_WithNonScalarTagEntry_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the non-scalar tag entry - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Tag entry must be a scalar value", errors); } /// /// Test that a non-scalar entry in the tests list of a mapping reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithNonScalarMappingTestEntry_ReportsError() { // Arrange: create a YAML file with a mapping node instead of a scalar in a mapping tests list @@ -864,14 +862,14 @@ public void RequirementsLoader_Load_WithNonScalarMappingTestEntry_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the non-scalar mapping test entry - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Test entry must be a scalar value in mapping", errors); } /// /// Test that a non-scalar entry in the includes list reports an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithNonScalarIncludeEntry_ReportsError() { // Arrange: create a YAML file with a mapping node instead of a scalar in the includes list @@ -884,14 +882,14 @@ public void RequirementsLoader_Load_WithNonScalarIncludeEntry_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error reports the non-scalar include entry - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Each 'includes' entry must be a scalar string", errors); } /// /// Test that multiple cycles in the requirement children graph are all reported. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithMultipleCycles_ReportsAllCycles() { // Arrange: create a YAML file with two separate back-edges creating two cycles @@ -918,18 +916,18 @@ public void RequirementsLoader_Load_WithMultipleCycles_ReportsAllCycles() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and both cycles are individually reported - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); // Both back-edges (REQ-B->REQ-A and REQ-C->REQ-A) should each be reported exactly once var cycleCount = errors.Split(Environment.NewLine) .Count(line => line.Contains("Circular requirement reference detected")); - Assert.AreEqual(2, cycleCount, $"Expected exactly 2 cycle errors, got {cycleCount}: {errors}"); + Assert.Equal(2, cycleCount); } /// /// Test that a child reference to a non-existent requirement ID is reported as an error. /// - [TestMethod] + [Fact] public void RequirementsLoader_Load_WithUnknownChildReference_ReportsError() { // Arrange: create a YAML file with a requirement that references a non-existent child @@ -947,9 +945,9 @@ public void RequirementsLoader_Load_WithUnknownChildReference_ReportsError() var (exitCode, errors) = RunLint(reqFile); // Assert: exit code is 1 and error mentions both the parent and the missing child ID - Assert.AreEqual(1, exitCode); - Assert.Contains("PARENT", errors, $"Expected 'PARENT' in errors: {errors}"); - Assert.Contains("NONEXISTENT", errors, $"Expected 'NONEXISTENT' in errors: {errors}"); - Assert.Contains("unknown child", errors, $"Expected 'unknown child' in errors: {errors}"); + Assert.Equal(1, exitCode); + Assert.Contains("PARENT", errors); + Assert.Contains("NONEXISTENT", errors); + Assert.Contains("unknown child", errors); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/SectionTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/SectionTests.cs index 7ec89fe..179002f 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/SectionTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/SectionTests.cs @@ -26,16 +26,14 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// Unit tests for the Section class, proving it correctly holds a title, requirements, /// and child sections. /// -[TestClass] -public class SectionTests +public sealed class SectionTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public SectionTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_section_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -44,19 +42,19 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test reading a simple YAML file with a single requirement. /// - [TestMethod] + [Fact] public void Section_Load_SimpleRequirement_ParsesCorrectly() { // Arrange: create a YAML file with a single requirement @@ -75,19 +73,19 @@ public void Section_Load_SimpleRequirement_ParsesCorrectly() var requirements = result.Requirements; // Assert: requirement parsed correctly - Assert.IsFalse(result.HasErrors); - Assert.IsNotNull(requirements); - Assert.HasCount(1, requirements.Sections); - Assert.AreEqual("System Security", requirements.Sections[0].Title); - Assert.HasCount(1, requirements.Sections[0].Requirements); - Assert.AreEqual("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); - Assert.AreEqual("The system shall support credentials authentication.", requirements.Sections[0].Requirements[0].Title); + Assert.False(result.HasErrors); + Assert.NotNull(requirements); + Assert.Single(requirements.Sections); + Assert.Equal("System Security", requirements.Sections[0].Title); + Assert.Single(requirements.Sections[0].Requirements); + Assert.Equal("SYS-SEC-001", requirements.Sections[0].Requirements[0].Id); + Assert.Equal("The system shall support credentials authentication.", requirements.Sections[0].Requirements[0].Title); } /// /// Test reading nested sections. /// - [TestMethod] + [Fact] public void Section_Load_NestedSections_ParsesHierarchyCorrectly() { // Arrange: create a YAML file with nested sections @@ -112,21 +110,21 @@ public void Section_Load_NestedSections_ParsesHierarchyCorrectly() var requirements = result.Requirements; // Assert: nested section hierarchy parsed correctly - Assert.IsFalse(result.HasErrors); - Assert.IsNotNull(requirements); - Assert.HasCount(1, requirements.Sections); - Assert.AreEqual("Data Management", requirements.Sections[0].Title); - Assert.HasCount(2, requirements.Sections[0].Sections); - Assert.AreEqual("User Authentication", requirements.Sections[0].Sections[0].Title); - Assert.AreEqual("Logging", requirements.Sections[0].Sections[1].Title); - Assert.AreEqual("AUTH-001", requirements.Sections[0].Sections[0].Requirements[0].Id); - Assert.AreEqual("LOG-001", requirements.Sections[0].Sections[1].Requirements[0].Id); + Assert.False(result.HasErrors); + Assert.NotNull(requirements); + Assert.Single(requirements.Sections); + Assert.Equal("Data Management", requirements.Sections[0].Title); + Assert.Equal(2, requirements.Sections[0].Sections.Count); + Assert.Equal("User Authentication", requirements.Sections[0].Sections[0].Title); + Assert.Equal("Logging", requirements.Sections[0].Sections[1].Title); + Assert.Equal("AUTH-001", requirements.Sections[0].Sections[0].Requirements[0].Id); + Assert.Equal("LOG-001", requirements.Sections[0].Sections[1].Requirements[0].Id); } /// /// Test that a blank section title reports an error issue with file location. /// - [TestMethod] + [Fact] public void Section_Load_BlankSectionTitle_ReportsErrorWithFileLocation() { // Arrange: create a YAML file with a blank section title @@ -144,9 +142,9 @@ public void Section_Load_BlankSectionTitle_ReportsErrorWithFileLocation() var result = Requirements.Load(filePath); // Assert: error reported with file location for the blank section title - Assert.IsTrue(result.HasErrors); - Assert.IsNull(result.Requirements); - Assert.Contains(i => i.Description.Contains("Section 'title' cannot be blank"), result.Issues); - Assert.Contains(i => i.Location.Contains(filePath), result.Issues); + Assert.True(result.HasErrors); + Assert.Null(result.Requirements); + Assert.Contains(result.Issues, i => i.Description.Contains("Section 'title' cannot be blank")); + Assert.Contains(result.Issues, i => i.Location.Contains(filePath)); } } diff --git a/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs b/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs index c4e012e..5f6308a 100644 --- a/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs @@ -26,16 +26,14 @@ namespace DemaConsulting.ReqStream.Tests; /// /// Unit tests for the Program class Run method. /// -[TestClass] -public class ProgramTests +public sealed class ProgramTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public ProgramTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -44,78 +42,67 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test Run with version flag prints version information. /// - [TestMethod] + [Fact] public void Program_Run_WithVersionFlag_PrintsVersion() { - // Arrange: redirect stdout to capture output - var originalOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); + // Arrange: create log file path to capture output + var logFile = Path.Combine(_testDirectory, "version.log"); - try + // Act: run with version flag, capturing output to log file + using (var context = Context.Create(["--version", "--log", logFile])) { - // Act: run with version flag - using var context = Context.Create(["--version"]); Program.Run(context); // Assert: version string is printed without banner or help - var outputText = output.ToString().Trim(); - Assert.IsFalse(string.IsNullOrWhiteSpace(outputText)); - Assert.DoesNotContain("Copyright", outputText); - Assert.DoesNotContain("Usage", outputText); - } - finally - { - Console.SetOut(originalOut); + Assert.Equal(0, context.ExitCode); } + + // Assert: log file contains version output (read after context disposal to ensure flush) + var outputText = File.ReadAllText(logFile).Trim(); + Assert.False(string.IsNullOrWhiteSpace(outputText)); + Assert.DoesNotContain("Copyright", outputText); + Assert.DoesNotContain("Usage", outputText); } /// /// Test Run with help flag prints help information. /// - [TestMethod] + [Fact] public void Program_Run_WithHelpFlag_PrintsHelp() { - // Arrange: redirect stdout to capture output - var originalOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); + // Arrange: create log file path to capture output + var logFile = Path.Combine(_testDirectory, "help.log"); - try + // Act: run with help flag, capturing output to log file + using (var context = Context.Create(["--help", "--log", logFile])) { - // Act: run with help flag - using var context = Context.Create(["--help"]); Program.Run(context); - - // Assert: banner and usage information are printed - var outputText = output.ToString(); - Assert.Contains("ReqStream version", outputText); - Assert.Contains("Copyright", outputText); - Assert.Contains("Usage:", outputText); - Assert.Contains("Options:", outputText); - } - finally - { - Console.SetOut(originalOut); } + + // Assert: banner and usage information are printed + var outputText = File.ReadAllText(logFile); + Assert.Contains("ReqStream version", outputText); + Assert.Contains("Copyright", outputText); + Assert.Contains("Usage:", outputText); + Assert.Contains("Options:", outputText); } /// /// Test running the program with validate flag. /// - [TestMethod] + [Fact] public void Program_Run_WithValidateFlag_RunsValidation() { // Arrange: set up log file path for validation output @@ -127,11 +114,11 @@ public void Program_Run_WithValidateFlag_RunsValidation() Program.Run(context); // Assert: validation succeeds with exit code 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } // Assert: log file contains expected validation output (after context is disposed to flush log) - Assert.IsTrue(File.Exists(logFile), "Log file should exist"); + Assert.True(File.Exists(logFile), "Log file should exist"); var logContent = File.ReadAllText(logFile); Assert.Contains("DEMA Consulting ReqStream", logContent); Assert.Contains("ReqStream Version", logContent); @@ -149,7 +136,7 @@ public void Program_Run_WithValidateFlag_RunsValidation() /// /// Test running the program with validate flag and results file. /// - [TestMethod] + [Fact] public void Program_Run_WithValidateAndResults_WritesResultsFile() { // Arrange: set up log file and results file paths @@ -162,11 +149,11 @@ public void Program_Run_WithValidateAndResults_WritesResultsFile() Program.Run(context); // Assert: validation succeeds with exit code 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } // Assert: results file was created with expected content - Assert.IsTrue(File.Exists(resultsFile)); + Assert.True(File.Exists(resultsFile)); // Assert: results file is valid TRX var trxContent = File.ReadAllText(resultsFile); @@ -184,7 +171,7 @@ public void Program_Run_WithValidateAndResults_WritesResultsFile() /// /// Test running the program with validate flag and JUnit results file. /// - [TestMethod] + [Fact] public void Program_Run_WithValidateAndJUnitResults_WritesJUnitFile() { // Arrange: set up log file and JUnit results file paths @@ -197,11 +184,11 @@ public void Program_Run_WithValidateAndJUnitResults_WritesJUnitFile() Program.Run(context); // Assert: validation succeeds with exit code 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } // Assert: JUnit results file was created with expected content - Assert.IsTrue(File.Exists(resultsFile)); + Assert.True(File.Exists(resultsFile)); // Assert: JUnit results file contains expected test names var xmlContent = File.ReadAllText(resultsFile); @@ -218,48 +205,53 @@ public void Program_Run_WithValidateAndJUnitResults_WritesJUnitFile() /// /// Test Run with no requirements files prints an informational message. /// - [TestMethod] + [Fact] public void Program_Run_WithNoFiles_PrintsMessage() { - // Arrange: redirect stdout to capture output - var originalOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); + // Arrange: create log file path to capture output + var logFile = Path.Combine(_testDirectory, "no-files.log"); - try + // Act: run program with no arguments, capturing output to log file + using (var context = Context.Create(["--log", logFile])) { - // Act: run program with no arguments - using var context = Context.Create([]); Program.Run(context); - // Assert: exit code is 0 and message includes "No requirements files specified" - Assert.AreEqual(0, context.ExitCode); - Assert.Contains("No requirements files specified", output.ToString()); - } - finally - { - Console.SetOut(originalOut); + // Assert: exit code is 0 + Assert.Equal(0, context.ExitCode); } + + // Assert: message includes "No requirements files specified" + var outputText = File.ReadAllText(logFile); + Assert.Contains("No requirements files specified", outputText); } /// /// Test Run with no requirements files shows message. /// - [TestMethod] + [Fact] public void Program_Run_WithNoRequirementsFiles_ShowsMessage() { - // Act: run with no arguments - using var context = Context.Create([]); - Program.Run(context); + // Arrange: create log file path to capture output + var logFile = Path.Combine(_testDirectory, "no-req-files.log"); + + // Act: run with no arguments, capturing output to log file + using (var context = Context.Create(["--log", logFile])) + { + Program.Run(context); - // Assert: completes without errors - Assert.AreEqual(0, context.ExitCode); + // Assert: completes without errors + Assert.Equal(0, context.ExitCode); + } + + // Assert: message indicates no requirements files specified + var output = File.ReadAllText(logFile); + Assert.Contains("No requirements files specified.", output); } /// /// Test Run with requirements files processes them successfully. /// - [TestMethod] + [Fact] public void Program_Run_WithRequirementsFiles_ProcessesSuccessfully() { // Arrange: create a test requirements file in the temp directory @@ -282,7 +274,7 @@ public void Program_Run_WithRequirementsFiles_ProcessesSuccessfully() Program.Run(context); // Assert: requirements processed successfully - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } finally { @@ -293,7 +285,7 @@ public void Program_Run_WithRequirementsFiles_ProcessesSuccessfully() /// /// Test Run with requirements export generates report file. /// - [TestMethod] + [Fact] public void Program_Run_WithRequirementsExport_GeneratesReport() { // Arrange: create a test requirements file and set report output path @@ -318,8 +310,8 @@ public void Program_Run_WithRequirementsExport_GeneratesReport() Program.Run(context); // Assert: report file was generated with expected content - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(reportFile)); + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(reportFile)); var reportContent = File.ReadAllText(reportFile); Assert.Contains("Test Section", reportContent); @@ -334,7 +326,7 @@ public void Program_Run_WithRequirementsExport_GeneratesReport() /// /// Test Run with trace matrix export generates matrix file. /// - [TestMethod] + [Fact] public void Program_Run_WithTraceMatrixExport_GeneratesMatrix() { // Arrange: create requirements file and TRX test results file @@ -378,8 +370,8 @@ public void Program_Run_WithTraceMatrixExport_GeneratesMatrix() Program.Run(context); // Assert: matrix file was generated with expected content - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(matrixFile)); + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(matrixFile)); var matrixContent = File.ReadAllText(matrixFile); Assert.Contains("Summary", matrixContent); @@ -394,64 +386,50 @@ public void Program_Run_WithTraceMatrixExport_GeneratesMatrix() /// /// Test priority order: version takes precedence over help. /// - [TestMethod] + [Fact] public void Program_Run_WithVersionAndHelp_ProcessesVersionFirst() { - // Arrange: redirect stdout to capture output - var originalOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); + // Arrange: create log file path to capture output + var logFile = Path.Combine(_testDirectory, "version-and-help.log"); - try + // Act: run with both version and help flags, capturing output to log file + using (var context = Context.Create(["--version", "--help", "--log", logFile])) { - // Act: run with both version and help flags - using var context = Context.Create(["--version", "--help"]); Program.Run(context); - - // Assert: only version string is printed (help is skipped) - var outputText = output.ToString().Trim(); - Assert.IsFalse(string.IsNullOrWhiteSpace(outputText)); - Assert.DoesNotContain("Usage:", outputText); - Assert.DoesNotContain("Copyright", outputText); - } - finally - { - Console.SetOut(originalOut); } + + // Assert: only version string is printed (help is skipped) + var outputText = File.ReadAllText(logFile).Trim(); + Assert.False(string.IsNullOrWhiteSpace(outputText)); + Assert.DoesNotContain("Usage:", outputText); + Assert.DoesNotContain("Copyright", outputText); } /// /// Test priority order: help takes precedence over validate. /// - [TestMethod] + [Fact] public void Program_Run_WithHelpAndValidate_ProcessesHelpFirst() { - // Arrange: redirect stdout to capture output - var originalOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); + // Arrange: create log file path to capture output + var logFile = Path.Combine(_testDirectory, "help-and-validate.log"); - try + // Act: run with both help and validate flags, capturing output to log file + using (var context = Context.Create(["--help", "--validate", "--log", logFile])) { - // Act: run with both help and validate flags - using var context = Context.Create(["--help", "--validate"]); Program.Run(context); - - // Assert: help is printed (validation is skipped) - var outputText = output.ToString(); - Assert.Contains("Usage:", outputText); - Assert.DoesNotContain("Self-validation", outputText); - } - finally - { - Console.SetOut(originalOut); } + + // Assert: help is printed (validation is skipped) + var outputText = File.ReadAllText(logFile); + Assert.Contains("Usage:", outputText); + Assert.DoesNotContain("Self-validation", outputText); } /// /// Test enforcement with fully satisfied requirements succeeds. /// - [TestMethod] + [Fact] public void Program_Run_WithEnforcementAndFullySatisfiedRequirements_Succeeds() { // Arrange: create requirements file and TRX with all requirements covered by passing tests @@ -494,7 +472,7 @@ public void Program_Run_WithEnforcementAndFullySatisfiedRequirements_Succeeds() Program.Run(context); // Assert: enforcement passes when all requirements are satisfied - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } finally { @@ -505,7 +483,7 @@ public void Program_Run_WithEnforcementAndFullySatisfiedRequirements_Succeeds() /// /// Test enforcement with unsatisfied requirements fails. /// - [TestMethod] + [Fact] public void Program_Run_WithEnforcementAndUnsatisfiedRequirements_Fails() { // Arrange: create requirements file with one tested and one untested requirement, and a passing TRX @@ -558,7 +536,7 @@ public void Program_Run_WithEnforcementAndUnsatisfiedRequirements_Fails() } // Assert: enforcement fails with unsatisfied requirement listed - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); // Verify error message includes the unsatisfied requirement via log file var logContent = File.ReadAllText(logFile); @@ -575,7 +553,7 @@ public void Program_Run_WithEnforcementAndUnsatisfiedRequirements_Fails() /// /// Test enforcement without test files fails. /// - [TestMethod] + [Fact] public void Program_Run_WithEnforcementAndNoTests_Fails() { // Arrange: create a requirements file with no test TRX @@ -601,7 +579,7 @@ public void Program_Run_WithEnforcementAndNoTests_Fails() Program.Run(context); // Assert: enforcement fails when no test results are provided - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -612,7 +590,7 @@ public void Program_Run_WithEnforcementAndNoTests_Fails() /// /// Test Run with lint flag lints requirements files. /// - [TestMethod] + [Fact] public void Program_Run_WithLintFlag_RunsLinter() { // Arrange: create a valid requirements file with no issues @@ -637,7 +615,7 @@ public void Program_Run_WithLintFlag_RunsLinter() Program.Run(context); // Assert: lint succeeds with exit code 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } finally { @@ -645,18 +623,18 @@ public void Program_Run_WithLintFlag_RunsLinter() } // Assert: no output is produced when lint finds no issues (no banner, no summary line) - Assert.IsTrue(File.Exists(logFile), "Log file should exist"); + Assert.True(File.Exists(logFile), "Log file should exist"); var logContent = File.ReadAllText(logFile); - Assert.AreEqual(string.Empty, logContent.Trim(), "Lint with no issues should produce no output"); + Assert.Equal(string.Empty, logContent.Trim()); } /// /// Test Run with lint flag does not print the banner. /// - [TestMethod] + [Fact] public void Program_Run_WithLintFlag_SuppressesBanner() { - // Arrange: create a valid requirements file and redirect stdout to capture output + // Arrange: create a valid requirements file and log file to capture output var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqFile, @" sections: @@ -666,40 +644,37 @@ public void Program_Run_WithLintFlag_SuppressesBanner() title: Test Requirement "); - var originalOut = Console.Out; - using var output = new StringWriter(); - Console.SetOut(output); + var logFile = Path.Combine(_testDirectory, "lint-banner.log"); var originalDir = Directory.GetCurrentDirectory(); try { Directory.SetCurrentDirectory(_testDirectory); - // Act: run with lint flag - using var context = Context.Create(["--lint", "--requirements", "*.yaml"]); + // Act: run with lint flag, capturing output to log file + using var context = Context.Create(["--lint", "--requirements", "*.yaml", "--log", logFile]); Program.Run(context); // Assert: lint succeeds with no output - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } finally { Directory.SetCurrentDirectory(originalDir); - Console.SetOut(originalOut); } // Assert: banner and summary are not printed during lint - var outputText = output.ToString(); - Assert.DoesNotContain("ReqStream version", outputText, "Banner should be suppressed during lint"); - Assert.DoesNotContain("Copyright", outputText, "Banner should be suppressed during lint"); - Assert.DoesNotContain("No issues found", outputText, "Summary line should be suppressed during lint"); - Assert.AreEqual(string.Empty, outputText.Trim(), "Output should be empty for clean lint"); + var outputText = File.ReadAllText(logFile); + Assert.DoesNotContain("ReqStream version", outputText); + Assert.DoesNotContain("Copyright", outputText); + Assert.DoesNotContain("No issues found", outputText); + Assert.Equal(string.Empty, outputText.Trim()); } /// /// Test Run with lint flag only outputs issue lines (no banner, no summary) when issues are found. /// - [TestMethod] + [Fact] public void Program_Run_WithLintFlag_OnlyOutputsIssues() { // Arrange: create a valid requirements file and a second file with a duplicate ID @@ -739,7 +714,7 @@ public void Program_Run_WithLintFlag_OnlyOutputsIssues() Program.Run(context); // Assert: lint fails due to duplicate requirement ID - Assert.AreEqual(1, context.ExitCode, "Lint with duplicate IDs should fail"); + Assert.Equal(1, context.ExitCode); } finally { @@ -747,17 +722,17 @@ public void Program_Run_WithLintFlag_OnlyOutputsIssues() } // Assert: log contains the issue but not banner or summary - Assert.IsTrue(File.Exists(logFile), "Log file should exist"); + Assert.True(File.Exists(logFile), "Log file should exist"); var logContent = File.ReadAllText(logFile); - Assert.Contains("REQ-001", logContent, "Issue about duplicate ID should appear in output"); - Assert.DoesNotContain("ReqStream version", logContent, "Banner should not appear in lint output"); - Assert.DoesNotContain("No issues found", logContent, "Summary line should not appear in lint output"); + Assert.Contains("REQ-001", logContent); + Assert.DoesNotContain("ReqStream version", logContent); + Assert.DoesNotContain("No issues found", logContent); } /// /// Test Run with enforcement mode and failed tests fails. /// - [TestMethod] + [Fact] public void Program_Run_WithEnforcementAndFailedTests_Fails() { // Arrange: create requirements file and TRX with a failed test @@ -800,7 +775,7 @@ public void Program_Run_WithEnforcementAndFailedTests_Fails() Program.Run(context); // Assert: enforcement fails when linked test is failed - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -811,21 +786,30 @@ public void Program_Run_WithEnforcementAndFailedTests_Fails() /// /// Test Run with lint flag and no requirements files prints an informational message. /// - [TestMethod] + [Fact] public void Program_Run_WithLintAndNoRequirements_PrintsInformationalMessage() { + // Arrange: create log file path to capture output + var logFile = Path.Combine(_testDirectory, "lint-no-req.log"); + // Act: run with lint flag but no requirements files - using var context = Context.Create(["--lint"]); - Program.Run(context); + using (var context = Context.Create(["--lint", "--log", logFile])) + { + Program.Run(context); + + // Assert: completes without error exit code + Assert.Equal(0, context.ExitCode); + } - // Assert: completes without error exit code - Assert.AreEqual(0, context.ExitCode); + // Assert: informational message is present + var output = File.ReadAllText(logFile); + Assert.Contains("No requirements files specified.", output); } /// /// Test Run with justifications export generates a justifications report file. /// - [TestMethod] + [Fact] public void Program_Run_WithJustificationsExport_GeneratesJustificationsReport() { // Arrange: create a test requirements file with justification text and set report output path @@ -851,8 +835,8 @@ public void Program_Run_WithJustificationsExport_GeneratesJustificationsReport() Program.Run(context); // Assert: justifications file was generated with requirement content - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(justificationsFile)); + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(justificationsFile)); var justificationsContent = File.ReadAllText(justificationsFile); Assert.Contains("REQ-001", justificationsContent); } diff --git a/test/DemaConsulting.ReqStream.Tests/SelfTest/SelfTestTests.cs b/test/DemaConsulting.ReqStream.Tests/SelfTest/SelfTestTests.cs index c500008..95d0ecb 100644 --- a/test/DemaConsulting.ReqStream.Tests/SelfTest/SelfTestTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/SelfTest/SelfTestTests.cs @@ -27,16 +27,14 @@ namespace DemaConsulting.ReqStream.Tests.SelfTest; /// Tests for the SelfTest subsystem, proving the Validation class is sufficient to /// implement the SelfTest subsystem requirements. /// -[TestClass] -public class SelfTestTests +public sealed class SelfTestTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public SelfTestTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_self_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -45,19 +43,19 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test that self-validation runs successfully and reports no failures. /// - [TestMethod] + [Fact] public void SelfTest_Qualification_Run_PassesAllTests() { // Arrange: create a silent context to suppress console output during validation @@ -67,13 +65,13 @@ public void SelfTest_Qualification_Run_PassesAllTests() Validation.Run(context); // Assert: exit code is 0 indicating all self-validation checks passed - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test that self-validation writes a TRX results file when the results path has a .trx extension. /// - [TestMethod] + [Fact] public void SelfTest_ResultsOutput_TrxResultsPath_WritesTrxFile() { // Arrange: define path for the TRX results output file @@ -84,8 +82,8 @@ public void SelfTest_ResultsOutput_TrxResultsPath_WritesTrxFile() Validation.Run(context); // Assert: exit code is 0 and TRX file was created with expected content - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(resultsFile), $"Expected TRX results file at {resultsFile}"); + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(resultsFile), $"Expected TRX results file at {resultsFile}"); var content = File.ReadAllText(resultsFile); Assert.Contains("TestRun", content); } @@ -93,7 +91,7 @@ public void SelfTest_ResultsOutput_TrxResultsPath_WritesTrxFile() /// /// Test that self-validation writes a JUnit XML results file when the results path has a .xml extension. /// - [TestMethod] + [Fact] public void SelfTest_ResultsOutput_XmlResultsPath_WritesJUnitFile() { // Arrange: define path for the JUnit XML results output file @@ -104,8 +102,8 @@ public void SelfTest_ResultsOutput_XmlResultsPath_WritesJUnitFile() Validation.Run(context); // Assert: exit code is 0 and JUnit XML file was created with expected content - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(resultsFile), $"Expected JUnit XML results file at {resultsFile}"); + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(resultsFile), $"Expected JUnit XML results file at {resultsFile}"); var content = File.ReadAllText(resultsFile); Assert.Contains("testsuite", content); } @@ -113,7 +111,7 @@ public void SelfTest_ResultsOutput_XmlResultsPath_WritesJUnitFile() /// /// Test that self-validation sets exit code 1 and reports errors when failures are encountered. /// - [TestMethod] + [Fact] public void SelfTest_FailureReporting_WithErrors_SetsExitCode1() { // Arrange: create a results file path with an unsupported extension to trigger an error @@ -129,7 +127,7 @@ public void SelfTest_FailureReporting_WithErrors_SetsExitCode1() } // Assert: exit code is 1 and error output was written to the log (read after context disposed to flush log) - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); var logContent = File.ReadAllText(logFile); Assert.Contains("Error:", logContent); } diff --git a/test/DemaConsulting.ReqStream.Tests/SelfTest/ValidationTests.cs b/test/DemaConsulting.ReqStream.Tests/SelfTest/ValidationTests.cs index 62145ea..ce9fd67 100644 --- a/test/DemaConsulting.ReqStream.Tests/SelfTest/ValidationTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/SelfTest/ValidationTests.cs @@ -26,17 +26,14 @@ namespace DemaConsulting.ReqStream.Tests.SelfTest; /// /// Unit tests for the Validation class. /// -[TestClass] -[DoNotParallelize] -public class ValidationTests +public sealed class ValidationTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public ValidationTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -45,31 +42,31 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test that Run throws ArgumentNullException when context is null. /// - [TestMethod] + [Fact] public void Validation_Run_WithNullContext_ThrowsArgumentNullException() { // Arrange - nothing to arrange; null is the input // Act + Assert - calling Run with null should throw ArgumentNullException - Assert.ThrowsExactly(() => Validation.Run(null!)); + Assert.Throws(() => Validation.Run(null!)); } /// /// Test that Run completes successfully with a silent context and log file. /// - [TestMethod] + [Fact] public void Validation_Run_WithSilentContext_CompletesSuccessfully() { // Arrange - create a log file path and a silent context @@ -81,11 +78,11 @@ public void Validation_Run_WithSilentContext_CompletesSuccessfully() Validation.Run(context); // Validation should succeed with exit code 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } // Assert - log file must exist and contain expected validation output - Assert.IsTrue(File.Exists(logFile), "Log file should exist"); + Assert.True(File.Exists(logFile), "Log file should exist"); var logContent = File.ReadAllText(logFile); Assert.Contains("DEMA Consulting ReqStream", logContent); Assert.Contains("ReqStream Version", logContent); @@ -101,7 +98,7 @@ public void Validation_Run_WithSilentContext_CompletesSuccessfully() /// /// Test that Run writes a TRX results file when the results path has a .trx extension. /// - [TestMethod] + [Fact] public void Validation_Run_WithTrxResultsFile_WritesTrxFile() { // Arrange - create a results file path with .trx extension and a silent context @@ -113,11 +110,11 @@ public void Validation_Run_WithTrxResultsFile_WritesTrxFile() Validation.Run(context); // Validation should succeed with exit code 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } // Assert - TRX file must exist and contain valid TRX XML content - Assert.IsTrue(File.Exists(resultsFile), "TRX results file should exist"); + Assert.True(File.Exists(resultsFile), "TRX results file should exist"); var trxContent = File.ReadAllText(resultsFile); Assert.StartsWith(" /// Test that Run writes a JUnit XML results file when the results path has a .xml extension. /// - [TestMethod] + [Fact] public void Validation_Run_WithXmlResultsFile_WritesXmlFile() { // Arrange - create a results file path with .xml extension and a silent context @@ -138,11 +135,11 @@ public void Validation_Run_WithXmlResultsFile_WritesXmlFile() Validation.Run(context); // Validation should succeed with exit code 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } // Assert - XML file must exist and contain valid JUnit XML content - Assert.IsTrue(File.Exists(resultsFile), "JUnit XML results file should exist"); + Assert.True(File.Exists(resultsFile), "JUnit XML results file should exist"); var xmlContent = File.ReadAllText(resultsFile); Assert.StartsWith(" /// Test that Run reports an error and continues when the results file cannot be written. /// - [TestMethod] + [Fact] public void Validation_Run_WithUnwritableResultsFile_ReportsError() { // Arrange: create a directory at the results file path to force a write failure @@ -163,13 +160,13 @@ public void Validation_Run_WithUnwritableResultsFile_ReportsError() Validation.Run(context); // Assert: exit code must be 1 indicating the write failure was reported - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } /// /// Test that Run continues and produces a summary when the results file cannot be written. /// - [TestMethod] + [Fact] public void Validation_Run_WithUnwritableResultsFile_Continues() { // Arrange: create a log file to capture output, and a directory at the results path to force a write failure @@ -184,7 +181,7 @@ public void Validation_Run_WithUnwritableResultsFile_Continues() } // Assert: the summary block is still present in the log despite the write failure - Assert.IsTrue(File.Exists(logFile), "Log file should exist"); + Assert.True(File.Exists(logFile), "Log file should exist"); var logContent = File.ReadAllText(logFile); Assert.Contains("Total Tests:", logContent); Assert.Contains("Passed:", logContent); @@ -194,7 +191,7 @@ public void Validation_Run_WithUnwritableResultsFile_Continues() /// /// Test that Run reports an error when the results file has an unsupported extension. /// - [TestMethod] + [Fact] public void Validation_Run_WithInvalidResultsExtension_ReportsError() { // Arrange - create a results file path with an unsupported .invalid extension @@ -205,6 +202,6 @@ public void Validation_Run_WithInvalidResultsExtension_ReportsError() Validation.Run(context); // Assert - exit code must be 1 indicating an error was reported for the unsupported format - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixExportTests.cs b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixExportTests.cs index 49b88c7..d06d648 100644 --- a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixExportTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixExportTests.cs @@ -29,18 +29,16 @@ namespace DemaConsulting.ReqStream.Tests.Tracing; /// /// Unit tests for TraceMatrix Markdown export functionality. /// -[TestClass] -public class TraceMatrixExportTests +public sealed class TraceMatrixExportTests : IDisposable { private static readonly string[] SplitDelimiter = ["| Test_Credentials |"]; - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public TraceMatrixExportTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -49,21 +47,22 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test exporting a simple trace matrix to Markdown. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_SimpleTraceMatrix_CreatesMarkdownFile() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -78,7 +77,7 @@ public void TraceMatrix_Export_SimpleTraceMatrix_CreatesMarkdownFile() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX file @@ -103,13 +102,15 @@ public void TraceMatrix_Export_SimpleTraceMatrix_CreatesMarkdownFile() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); var mdPath = Path.Combine(_testDirectory, "tracematrix.md"); matrix.Export(mdPath); - Assert.IsTrue(File.Exists(mdPath)); + // Assert: + Assert.True(File.Exists(mdPath)); var content = File.ReadAllText(mdPath); Assert.Contains("# Summary", content); Assert.Contains("1 of 1 requirements are satisfied with tests.", content); @@ -126,9 +127,10 @@ public void TraceMatrix_Export_SimpleTraceMatrix_CreatesMarkdownFile() /// /// Test exporting trace matrix with custom depth. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_WithCustomDepth_UsesCorrectHeaderLevel() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -142,7 +144,7 @@ public void TraceMatrix_Export_WithCustomDepth_UsesCorrectHeaderLevel() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX file @@ -159,12 +161,14 @@ public void TraceMatrix_Export_WithCustomDepth_UsesCorrectHeaderLevel() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); var mdPath = Path.Combine(_testDirectory, "tracematrix.md"); matrix.Export(mdPath, depth: 2); + // Assert: var content = File.ReadAllText(mdPath); Assert.Contains("## Summary", content); Assert.Contains("## Requirements", content); @@ -175,9 +179,10 @@ public void TraceMatrix_Export_WithCustomDepth_UsesCorrectHeaderLevel() /// /// Test exporting trace matrix with failed tests. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_WithFailedTests_ShowsFailures() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -192,7 +197,7 @@ public void TraceMatrix_Export_WithFailedTests_ShowsFailures() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX file with one failure @@ -218,12 +223,14 @@ public void TraceMatrix_Export_WithFailedTests_ShowsFailures() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); var mdPath = Path.Combine(_testDirectory, "tracematrix.md"); matrix.Export(mdPath); + // Assert: var content = File.ReadAllText(mdPath); Assert.Contains("0 of 1 requirements are satisfied with tests.", content); Assert.Contains("| AUTH-001 | 2 | 1 | 1 | 0 |", content); @@ -234,9 +241,10 @@ public void TraceMatrix_Export_WithFailedTests_ShowsFailures() /// /// Test exporting trace matrix with not executed tests. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_WithNotExecutedTests_ShowsNotExecuted() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -251,7 +259,7 @@ public void TraceMatrix_Export_WithNotExecutedTests_ShowsNotExecuted() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX file with only one test @@ -268,12 +276,14 @@ public void TraceMatrix_Export_WithNotExecutedTests_ShowsNotExecuted() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); var mdPath = Path.Combine(_testDirectory, "tracematrix.md"); matrix.Export(mdPath); + // Assert: var content = File.ReadAllText(mdPath); Assert.Contains("0 of 1 requirements are satisfied with tests.", content); Assert.Contains("| AUTH-001 | 2 | 1 | 0 | 1 |", content); @@ -283,9 +293,10 @@ public void TraceMatrix_Export_WithNotExecutedTests_ShowsNotExecuted() /// /// Test exporting trace matrix with nested sections. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_WithNestedSections_CreatesHierarchy() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -307,7 +318,7 @@ public void TraceMatrix_Export_WithNestedSections_CreatesHierarchy() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX file @@ -332,12 +343,14 @@ public void TraceMatrix_Export_WithNestedSections_CreatesHierarchy() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); var mdPath = Path.Combine(_testDirectory, "tracematrix.md"); matrix.Export(mdPath); + // Assert: var content = File.ReadAllText(mdPath); Assert.Contains("2 of 2 requirements are satisfied with tests.", content); Assert.Contains("## Data Management", content); @@ -350,9 +363,10 @@ public void TraceMatrix_Export_WithNestedSections_CreatesHierarchy() /// /// Test that export throws exception when file path is null. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_NullFilePath_ThrowsArgumentException() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -366,22 +380,24 @@ public void TraceMatrix_Export_NullFilePath_ThrowsArgumentException() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TraceMatrix var matrix = new TraceMatrix(requirements); - var ex = Assert.ThrowsExactly(() => matrix.Export(null!)); + // Act / Assert: + var ex = Assert.Throws(() => matrix.Export(null!)); Assert.Contains("File path cannot be null or empty", ex.Message); } /// /// Test that export throws exception when file path is empty. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_EmptyFilePath_ThrowsArgumentException() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -395,22 +411,24 @@ public void TraceMatrix_Export_EmptyFilePath_ThrowsArgumentException() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TraceMatrix var matrix = new TraceMatrix(requirements); - var ex = Assert.ThrowsExactly(() => matrix.Export(string.Empty)); + // Act / Assert: + var ex = Assert.Throws(() => matrix.Export(string.Empty)); Assert.Contains("File path cannot be null or empty", ex.Message); } /// /// Test exporting trace matrix with requirements that have child requirements. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_WithChildRequirements_ConsidersChildTests() { + // Arrange: // Create requirements with children var reqYaml = @"--- sections: @@ -430,7 +448,7 @@ public void TraceMatrix_Export_WithChildRequirements_ConsidersChildTests() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX file @@ -447,12 +465,14 @@ public void TraceMatrix_Export_WithChildRequirements_ConsidersChildTests() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); var mdPath = Path.Combine(_testDirectory, "tracematrix.md"); matrix.Export(mdPath); + // Assert: var content = File.ReadAllText(mdPath); // Both requirements should be satisfied because SYS-SEC-001 has child AUTH-001 which has passing tests Assert.Contains("2 of 2 requirements are satisfied with tests.", content); @@ -463,9 +483,10 @@ public void TraceMatrix_Export_WithChildRequirements_ConsidersChildTests() /// /// Test exporting trace matrix with requirements that have no tests. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_WithNoTests_ShowsNotSatisfied() { + // Arrange: // Create requirements with no tests var reqYaml = @"--- sections: @@ -477,15 +498,17 @@ public void TraceMatrix_Export_WithNoTests_ShowsNotSatisfied() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; + // Act: // Create TraceMatrix with no test results var matrix = new TraceMatrix(requirements); var mdPath = Path.Combine(_testDirectory, "tracematrix.md"); matrix.Export(mdPath); + // Assert: var content = File.ReadAllText(mdPath); Assert.Contains("0 of 1 requirements are satisfied with tests.", content); Assert.Contains("| AUTH-001 | 0 | 0 | 0 | 0 |", content); @@ -494,9 +517,10 @@ public void TraceMatrix_Export_WithNoTests_ShowsNotSatisfied() /// /// Test exporting trace matrix where a test maps to multiple requirements. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_TestMapsToMultipleRequirements_ShowsAllMappings() { + // Arrange: // Create requirements where one test maps to multiple requirements var reqYaml = @"--- sections: @@ -514,7 +538,7 @@ public void TraceMatrix_Export_TestMapsToMultipleRequirements_ShowsAllMappings() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX file @@ -531,25 +555,28 @@ public void TraceMatrix_Export_TestMapsToMultipleRequirements_ShowsAllMappings() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); var mdPath = Path.Combine(_testDirectory, "tracematrix.md"); matrix.Export(mdPath); + // Assert: var content = File.ReadAllText(mdPath); Assert.Contains("2 of 2 requirements are satisfied with tests.", content); // Test should appear twice in the testing section, once for each requirement var testCredentialsCount = content.Split(SplitDelimiter, StringSplitOptions.None).Length - 1; - Assert.AreEqual(2, testCredentialsCount, "Test_Credentials should appear twice in the testing section"); + Assert.Equal(2, testCredentialsCount); } /// /// Test exporting trace matrix with filter tags. /// - [TestMethod] + [Fact] public void TraceMatrix_Export_WithFilterTags_ExportsOnlyMatchingRequirements() { + // Arrange: var yamlContent = @"sections: - title: System Requirements requirements: @@ -569,7 +596,7 @@ public void TraceMatrix_Export_WithFilterTags_ExportsOnlyMatchingRequirements() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create test results @@ -594,12 +621,14 @@ public void TraceMatrix_Export_WithFilterTags_ExportsOnlyMatchingRequirements() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix and export with filter var matrix = new TraceMatrix(requirements, trxPath); var mdPath = Path.Combine(_testDirectory, "tracematrix.md"); var filterTags = new HashSet { "security" }; matrix.Export(mdPath, filterTags: filterTags); + // Assert: var content = File.ReadAllText(mdPath); // Should show 1 of 1 requirements (only security-tagged requirement) @@ -615,9 +644,10 @@ public void TraceMatrix_Export_WithFilterTags_ExportsOnlyMatchingRequirements() /// /// Test that trace matrix filtering affects satisfied requirements count. /// - [TestMethod] + [Fact] public void TraceMatrix_CalculateSatisfiedRequirements_WithFilterTags_CountsOnlyMatchingRequirements() { + // Arrange: var yamlContent = @"sections: - title: System Requirements requirements: @@ -641,7 +671,7 @@ public void TraceMatrix_CalculateSatisfiedRequirements_WithFilterTags_CountsOnly var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create test results @@ -666,26 +696,29 @@ public void TraceMatrix_CalculateSatisfiedRequirements_WithFilterTags_CountsOnly var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: var matrix = new TraceMatrix(requirements, trxPath); + // Assert: // Without filter: should count all 3 requirements (2 satisfied, 1 unsatisfied) var (satisfiedAll, totalAll) = matrix.CalculateSatisfiedRequirements(); - Assert.AreEqual(2, satisfiedAll); - Assert.AreEqual(3, totalAll); + Assert.Equal(2, satisfiedAll); + Assert.Equal(3, totalAll); // With security filter: should count only 2 security requirements (1 satisfied, 1 unsatisfied) var filterTags = new HashSet { "security" }; var (satisfiedFiltered, totalFiltered) = matrix.CalculateSatisfiedRequirements(filterTags); - Assert.AreEqual(1, satisfiedFiltered); - Assert.AreEqual(2, totalFiltered); + Assert.Equal(1, satisfiedFiltered); + Assert.Equal(2, totalFiltered); } /// /// Test that trace matrix filtering affects unsatisfied requirements list. /// - [TestMethod] + [Fact] public void TraceMatrix_GetUnsatisfiedRequirements_WithFilterTags_ReturnsOnlyMatchingRequirements() { + // Arrange: var yamlContent = @"sections: - title: System Requirements requirements: @@ -701,21 +734,23 @@ public void TraceMatrix_GetUnsatisfiedRequirements_WithFilterTags_ReturnsOnlyMat var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; + // Act: var matrix = new TraceMatrix(requirements); + // Assert: // Without filter: should return both unsatisfied requirements var unsatisfiedAll = matrix.GetUnsatisfiedRequirements(); - Assert.HasCount(2, unsatisfiedAll); + Assert.Equal(2, unsatisfiedAll.Count); Assert.Contains("REQ-001", unsatisfiedAll); Assert.Contains("REQ-002", unsatisfiedAll); // With security filter: should return only security requirement var filterTags = new HashSet { "security" }; var unsatisfiedFiltered = matrix.GetUnsatisfiedRequirements(filterTags); - Assert.HasCount(1, unsatisfiedFiltered); + Assert.Single(unsatisfiedFiltered); Assert.Contains("REQ-001", unsatisfiedFiltered); Assert.DoesNotContain("REQ-002", unsatisfiedFiltered); } diff --git a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixReadTests.cs b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixReadTests.cs index 110b438..6064ee8 100644 --- a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixReadTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixReadTests.cs @@ -29,16 +29,14 @@ namespace DemaConsulting.ReqStream.Tests.Tracing; /// /// Unit tests for TraceMatrix reading functionality. /// -[TestClass] -public class TraceMatrixReadTests +public sealed class TraceMatrixReadTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public TraceMatrixReadTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -47,21 +45,22 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test TraceMatrix with a TRX test result file. /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_WithTrxFile_ParsesCorrectly() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -76,7 +75,7 @@ public void TraceMatrix_Constructor_WithTrxFile_ParsesCorrectly() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX file using the TestResults library @@ -101,27 +100,30 @@ public void TraceMatrix_Constructor_WithTrxFile_ParsesCorrectly() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); + // Assert: // Verify results var result1 = matrix.GetTestResult("Test_Credentials_Valid"); - Assert.IsNotNull(result1); - Assert.AreEqual(1, result1.Executed); - Assert.AreEqual(1, result1.Passes); + Assert.NotNull(result1); + Assert.Equal(1, result1.Executed); + Assert.Equal(1, result1.Passes); var result2 = matrix.GetTestResult("Test_Credentials_Invalid"); - Assert.IsNotNull(result2); - Assert.AreEqual(1, result2.Executed); - Assert.AreEqual(1, result2.Passes); + Assert.NotNull(result2); + Assert.Equal(1, result2.Executed); + Assert.Equal(1, result2.Passes); } /// /// Test TraceMatrix with multiple test result files (matrix testing scenario). /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_WithMultipleFiles_AggregatesResults() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -135,7 +137,7 @@ public void TraceMatrix_Constructor_WithMultipleFiles_AggregatesResults() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create first TRX file (Windows, passed) @@ -178,22 +180,25 @@ public void TraceMatrix_Constructor_WithMultipleFiles_AggregatesResults() var trx3Path = Path.Combine(_testDirectory, "macos-results.trx"); File.WriteAllText(trx3Path, TrxSerializer.Serialize(testResults3)); + // Act: // Create TraceMatrix with all three files var matrix = new TraceMatrix(requirements, trx1Path, trx2Path, trx3Path); + // Assert: // Verify aggregated results var result = matrix.GetTestResult("Test_PlatformBasic"); - Assert.IsNotNull(result); - Assert.AreEqual(3, result.Executed, "Test should have been executed 3 times"); - Assert.AreEqual(2, result.Passes, "Test should have passed 2 times"); + Assert.NotNull(result); + Assert.Equal(3, result.Executed); + Assert.Equal(2, result.Passes); } /// /// Test that extra tests (beyond those in requirements) are ignored. /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_WithExtraTests_IgnoresUnreferencedTests() { + // Arrange: // Create requirements with only one test var reqYaml = @"--- sections: @@ -207,7 +212,7 @@ public void TraceMatrix_Constructor_WithExtraTests_IgnoresUnreferencedTests() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX with multiple tests @@ -240,50 +245,54 @@ public void TraceMatrix_Constructor_WithExtraTests_IgnoresUnreferencedTests() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); + // Assert: // Verify only the referenced test is tracked var result1 = matrix.GetTestResult("Test_Auth_Valid"); - Assert.IsNotNull(result1); - Assert.AreEqual(1, result1.Executed); - Assert.AreEqual(1, result1.Passes); + Assert.NotNull(result1); + Assert.Equal(1, result1.Executed); + Assert.Equal(1, result1.Passes); // Extra tests are now tracked (all tests captured) var result2 = matrix.GetTestResult("Test_ExtraNotInRequirements"); - Assert.IsNotNull(result2); - Assert.AreEqual(1, result2.Executed); - Assert.AreEqual(1, result2.Passes); + Assert.NotNull(result2); + Assert.Equal(1, result2.Executed); + Assert.Equal(1, result2.Passes); var result3 = matrix.GetTestResult("Test_AnotherExtra"); - Assert.IsNotNull(result3); - Assert.AreEqual(1, result3.Executed); - Assert.AreEqual(1, result3.Passes); + Assert.NotNull(result3); + Assert.Equal(1, result3.Executed); + Assert.Equal(1, result3.Passes); // GetAllTestResults only returns tests referenced in requirements var allResults = matrix.GetAllTestResults(); - Assert.HasCount(1, allResults); - Assert.IsTrue(allResults.ContainsKey("Test_Auth_Valid")); - Assert.IsFalse(allResults.ContainsKey("Test_ExtraNotInRequirements")); - Assert.IsFalse(allResults.ContainsKey("Test_AnotherExtra")); + Assert.Single(allResults); + Assert.True(allResults.ContainsKey("Test_Auth_Valid")); + Assert.False(allResults.ContainsKey("Test_ExtraNotInRequirements")); + Assert.False(allResults.ContainsKey("Test_AnotherExtra")); } /// /// Test that null requirements throws ArgumentNullException. /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_NullRequirements_ThrowsArgumentNullException() { - var ex = Assert.ThrowsExactly(() => _ = new TraceMatrix(null!, Array.Empty())); + // Act / Assert: + var ex = Assert.Throws(() => _ = new TraceMatrix(null!, Array.Empty())); Assert.Contains("requirements", ex.Message); } /// /// Test that missing test result file throws FileNotFoundException. /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_MissingFile_ThrowsFileNotFoundException() { + // Arrange: // Create minimal requirements var reqYaml = @"--- sections: @@ -297,21 +306,23 @@ public void TraceMatrix_Constructor_MissingFile_ThrowsFileNotFoundException() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; var nonExistentPath = Path.Combine(_testDirectory, "nonexistent.trx"); - var ex = Assert.ThrowsExactly(() => _ = new TraceMatrix(requirements, nonExistentPath)); + // Act / Assert: + var ex = Assert.Throws(() => _ = new TraceMatrix(requirements, nonExistentPath)); Assert.Contains("Test result file not found", ex.Message); } /// /// Test TraceMatrix with failed tests. /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_WithFailedTests_TracksFailures() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -326,7 +337,7 @@ public void TraceMatrix_Constructor_WithFailedTests_TracksFailures() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX with passed and failed tests @@ -352,28 +363,31 @@ public void TraceMatrix_Constructor_WithFailedTests_TracksFailures() var trxPath = Path.Combine(_testDirectory, "results.trx"); File.WriteAllText(trxPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, trxPath); + // Assert: // Verify passing test var result1 = matrix.GetTestResult("Test_Passing"); - Assert.IsNotNull(result1); - Assert.AreEqual(1, result1.Executed); - Assert.AreEqual(1, result1.Passes); + Assert.NotNull(result1); + Assert.Equal(1, result1.Executed); + Assert.Equal(1, result1.Passes); // Verify failing test var result2 = matrix.GetTestResult("Test_Failing"); - Assert.IsNotNull(result2); - Assert.AreEqual(1, result2.Executed); - Assert.AreEqual(0, result2.Passes, "Failed test should have 0 passes"); + Assert.NotNull(result2); + Assert.Equal(1, result2.Executed); + Assert.Equal(0, result2.Passes); } /// /// Test TraceMatrix with no test result files. /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_WithNoFiles_CreatesEmptyMatrix() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -387,26 +401,29 @@ public void TraceMatrix_Constructor_WithNoFiles_CreatesEmptyMatrix() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; + // Act: // Create TraceMatrix with no files var matrix = new TraceMatrix(requirements); + // Assert: // Verify no results var allResults = matrix.GetAllTestResults(); - Assert.IsEmpty(allResults); + Assert.Empty(allResults); var result = matrix.GetTestResult("SomeTest"); - Assert.AreEqual(0, result.Executed); + Assert.Equal(0, result.Executed); } /// /// Test TraceMatrix with a JUnit test result file. /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_WithJUnitFile_ParsesCorrectly() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -421,7 +438,7 @@ public void TraceMatrix_Constructor_WithJUnitFile_ParsesCorrectly() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create JUnit file using the TestResults library @@ -446,27 +463,30 @@ public void TraceMatrix_Constructor_WithJUnitFile_ParsesCorrectly() var junitPath = Path.Combine(_testDirectory, "results.xml"); File.WriteAllText(junitPath, JUnitSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, junitPath); + // Assert: // Verify results var result1 = matrix.GetTestResult("Test_ValidData"); - Assert.IsNotNull(result1); - Assert.AreEqual(1, result1.Executed); - Assert.AreEqual(1, result1.Passes); + Assert.NotNull(result1); + Assert.Equal(1, result1.Executed); + Assert.Equal(1, result1.Passes); var result2 = matrix.GetTestResult("Test_InvalidData"); - Assert.IsNotNull(result2); - Assert.AreEqual(1, result2.Executed); - Assert.AreEqual(1, result2.Passes); + Assert.NotNull(result2); + Assert.Equal(1, result2.Executed); + Assert.Equal(1, result2.Passes); } /// /// Test TraceMatrix with mixed TRX and JUnit files. /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_WithMixedFormats_ProcessesBoth() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -481,7 +501,7 @@ public void TraceMatrix_Constructor_WithMixedFormats_ProcessesBoth() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create TRX file @@ -510,28 +530,31 @@ public void TraceMatrix_Constructor_WithMixedFormats_ProcessesBoth() var junitPath = Path.Combine(_testDirectory, "results.xml"); File.WriteAllText(junitPath, JUnitSerializer.Serialize(junitResults)); + // Act: // Create TraceMatrix with both files var matrix = new TraceMatrix(requirements, trxPath, junitPath); + // Assert: // Verify results from TRX var result1 = matrix.GetTestResult("Test_TrxFormat"); - Assert.IsNotNull(result1); - Assert.AreEqual(1, result1.Executed); - Assert.AreEqual(1, result1.Passes); + Assert.NotNull(result1); + Assert.Equal(1, result1.Executed); + Assert.Equal(1, result1.Passes); // Verify results from JUnit var result2 = matrix.GetTestResult("Test_JUnitFormat"); - Assert.IsNotNull(result2); - Assert.AreEqual(1, result2.Executed); - Assert.AreEqual(1, result2.Passes); + Assert.NotNull(result2); + Assert.Equal(1, result2.Executed); + Assert.Equal(1, result2.Passes); } /// /// Test TraceMatrix with JUnit file containing failed tests. /// - [TestMethod] + [Fact] public void TraceMatrix_Constructor_WithJUnitFailedTests_TracksFailures() { + // Arrange: // Create requirements var reqYaml = @"--- sections: @@ -546,7 +569,7 @@ public void TraceMatrix_Constructor_WithJUnitFailedTests_TracksFailures() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create JUnit file with passed and failed tests @@ -572,19 +595,21 @@ public void TraceMatrix_Constructor_WithJUnitFailedTests_TracksFailures() var junitPath = Path.Combine(_testDirectory, "results.xml"); File.WriteAllText(junitPath, JUnitSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, junitPath); + // Assert: // Verify passing test var result1 = matrix.GetTestResult("Test_JUnit_Passing"); - Assert.IsNotNull(result1); - Assert.AreEqual(1, result1.Executed); - Assert.AreEqual(1, result1.Passes); + Assert.NotNull(result1); + Assert.Equal(1, result1.Executed); + Assert.Equal(1, result1.Passes); // Verify failing test var result2 = matrix.GetTestResult("Test_JUnit_Failing"); - Assert.IsNotNull(result2); - Assert.AreEqual(1, result2.Executed); - Assert.AreEqual(0, result2.Passes, "Failed test should have 0 passes"); + Assert.NotNull(result2); + Assert.Equal(1, result2.Executed); + Assert.Equal(0, result2.Passes); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixTests.cs b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixTests.cs index d249f55..d646007 100644 --- a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixTests.cs @@ -29,16 +29,14 @@ namespace DemaConsulting.ReqStream.Tests.Tracing; /// /// Unit tests for TraceMatrix functionality. /// -[TestClass] -public class TraceMatrixTests +public sealed class TraceMatrixTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public TraceMatrixTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_test_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -47,21 +45,22 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test source-specific test matching with filepart@testname pattern. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesCorrectly() { + // Arrange: // Create requirements with source-specific test names var reqYaml = @"--- sections: @@ -79,7 +78,7 @@ public void TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesCorrectly() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create Windows test results @@ -108,32 +107,35 @@ public void TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesCorrectly() var linuxPath = Path.Combine(_testDirectory, "test-results-ubuntu-latest.trx"); File.WriteAllText(linuxPath, TrxSerializer.Serialize(linuxResults)); + // Act: // Create TraceMatrix with both files var matrix = new TraceMatrix(requirements, windowsPath, linuxPath); + // Assert: // Verify Windows test is tracked separately var windowsResult = matrix.GetTestResult("windows-latest@Test_PlatformBasic"); - Assert.IsNotNull(windowsResult); - Assert.AreEqual(1, windowsResult.Executed); - Assert.AreEqual(1, windowsResult.Passes); + Assert.NotNull(windowsResult); + Assert.Equal(1, windowsResult.Executed); + Assert.Equal(1, windowsResult.Passes); // Verify Linux test is tracked separately var linuxResult = matrix.GetTestResult("ubuntu-latest@Test_PlatformBasic"); - Assert.IsNotNull(linuxResult); - Assert.AreEqual(1, linuxResult.Executed); - Assert.AreEqual(1, linuxResult.Passes); + Assert.NotNull(linuxResult); + Assert.Equal(1, linuxResult.Executed); + Assert.Equal(1, linuxResult.Passes); // Verify only 2 test results are tracked (not aggregated) var allResults = matrix.GetAllTestResults(); - Assert.HasCount(2, allResults); + Assert.Equal(2, allResults.Count); } /// /// Test that source-specific test names only match their specified source. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithSourceSpecificTests_DoesNotMatchOtherSources() { + // Arrange: // Create requirements with Windows-specific test name var reqYaml = @"--- sections: @@ -147,7 +149,7 @@ public void TraceMatrix_GetTestResult_WithSourceSpecificTests_DoesNotMatchOtherS var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create Linux test results (should not match) @@ -163,20 +165,23 @@ public void TraceMatrix_GetTestResult_WithSourceSpecificTests_DoesNotMatchOtherS var linuxPath = Path.Combine(_testDirectory, "test-results-ubuntu-latest.trx"); File.WriteAllText(linuxPath, TrxSerializer.Serialize(linuxResults)); + // Act: // Create TraceMatrix with Linux file only var matrix = new TraceMatrix(requirements, linuxPath); + // Assert: // Verify Windows-specific test is not tracked from Linux file var result = matrix.GetTestResult("windows@Test_WindowsOnly"); - Assert.AreEqual(0, result.Executed); + Assert.Equal(0, result.Executed); } /// /// Test that plain test names match all sources. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithPlainTestNames_MatchesAllSources() { + // Arrange: // Create requirements with plain test name var reqYaml = @"--- sections: @@ -190,7 +195,7 @@ public void TraceMatrix_GetTestResult_WithPlainTestNames_MatchesAllSources() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create Windows test results @@ -219,22 +224,25 @@ public void TraceMatrix_GetTestResult_WithPlainTestNames_MatchesAllSources() var linuxPath = Path.Combine(_testDirectory, "linux-results.trx"); File.WriteAllText(linuxPath, TrxSerializer.Serialize(linuxResults)); + // Act: // Create TraceMatrix with both files var matrix = new TraceMatrix(requirements, windowsPath, linuxPath); + // Assert: // Verify test is aggregated from both sources var result = matrix.GetTestResult("Test_CrossPlatform"); - Assert.IsNotNull(result); - Assert.AreEqual(2, result.Executed, "Should aggregate from both sources"); - Assert.AreEqual(2, result.Passes); + Assert.NotNull(result); + Assert.Equal(2, result.Executed); + Assert.Equal(2, result.Passes); } /// /// Test mixed source-specific and plain test names in the same requirement. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithMixedTestNames_MatchesAppropriately() { + // Arrange: // Create requirements with both plain and source-specific test names var reqYaml = @"--- sections: @@ -250,7 +258,7 @@ public void TraceMatrix_GetTestResult_WithMixedTestNames_MatchesAppropriately() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create Windows test results @@ -295,38 +303,41 @@ public void TraceMatrix_GetTestResult_WithMixedTestNames_MatchesAppropriately() var linuxPath = Path.Combine(_testDirectory, "test-linux.trx"); File.WriteAllText(linuxPath, TrxSerializer.Serialize(linuxResults)); + // Act: // Create TraceMatrix with both files var matrix = new TraceMatrix(requirements, windowsPath, linuxPath); + // Assert: // Verify common test is aggregated var commonResult = matrix.GetTestResult("Test_Common"); - Assert.IsNotNull(commonResult); - Assert.AreEqual(2, commonResult.Executed); - Assert.AreEqual(2, commonResult.Passes); + Assert.NotNull(commonResult); + Assert.Equal(2, commonResult.Executed); + Assert.Equal(2, commonResult.Passes); // Verify Windows-specific test var windowsResult = matrix.GetTestResult("windows@Test_WindowsSpecific"); - Assert.IsNotNull(windowsResult); - Assert.AreEqual(1, windowsResult.Executed); - Assert.AreEqual(1, windowsResult.Passes); + Assert.NotNull(windowsResult); + Assert.Equal(1, windowsResult.Executed); + Assert.Equal(1, windowsResult.Passes); // Verify Linux-specific test var linuxResult = matrix.GetTestResult("linux@Test_LinuxSpecific"); - Assert.IsNotNull(linuxResult); - Assert.AreEqual(1, linuxResult.Executed); - Assert.AreEqual(1, linuxResult.Passes); + Assert.NotNull(linuxResult); + Assert.Equal(1, linuxResult.Executed); + Assert.Equal(1, linuxResult.Passes); // Verify total number of tracked tests var allResults = matrix.GetAllTestResults(); - Assert.HasCount(3, allResults); + Assert.Equal(3, allResults.Count); } /// /// Test case-insensitive matching of file parts. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithSourceSpecificTests_IsCaseInsensitive() { + // Arrange: // Create requirements with lowercase file part var reqYaml = @"--- sections: @@ -340,7 +351,7 @@ public void TraceMatrix_GetTestResult_WithSourceSpecificTests_IsCaseInsensitive( var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create test results with uppercase WINDOWS in filename @@ -356,22 +367,25 @@ public void TraceMatrix_GetTestResult_WithSourceSpecificTests_IsCaseInsensitive( var testPath = Path.Combine(_testDirectory, "test-results-WINDOWS-latest.trx"); File.WriteAllText(testPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, testPath); + // Assert: // Verify test is matched despite case difference var result = matrix.GetTestResult("windows@Test_CaseSensitive"); - Assert.IsNotNull(result); - Assert.AreEqual(1, result.Executed); - Assert.AreEqual(1, result.Passes); + Assert.NotNull(result); + Assert.Equal(1, result.Executed); + Assert.Equal(1, result.Passes); } /// /// Test partial file name matching (filepart can match anywhere in base name). /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesPartialFilename() { + // Arrange: // Create requirements with partial file name var reqYaml = @"--- sections: @@ -385,7 +399,7 @@ public void TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesPartialFile var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create test results with full filename containing ubuntu @@ -401,23 +415,26 @@ public void TraceMatrix_GetTestResult_WithSourceSpecificTests_MatchesPartialFile var testPath = Path.Combine(_testDirectory, "test-results-ubuntu-22.04-latest.trx"); File.WriteAllText(testPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, testPath); + // Assert: // Verify test is matched with partial filename var result = matrix.GetTestResult("ubuntu@Test_Partial"); - Assert.IsNotNull(result); - Assert.AreEqual(1, result.Executed); - Assert.AreEqual(1, result.Passes); + Assert.NotNull(result); + Assert.Equal(1, result.Executed); + Assert.Equal(1, result.Passes); } /// /// Test that a single test result can match multiple source-specific requirement tests. /// This occurs when a filename contains multiple matching source specifiers. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithMultipleSourceSpecifiers_MatchesAllRequirements() { + // Arrange: // Create requirements with multiple source-specific tests for the same test name var reqYaml = @"--- sections: @@ -435,7 +452,7 @@ public void TraceMatrix_GetTestResult_WithMultipleSourceSpecifiers_MatchesAllReq var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create test results with filename containing both windows and dotnet8 @@ -451,24 +468,26 @@ public void TraceMatrix_GetTestResult_WithMultipleSourceSpecifiers_MatchesAllReq var testPath = Path.Combine(_testDirectory, "integration-test-windows-latest-dotnet8.x.trx"); File.WriteAllText(testPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, testPath); + // Assert: // Verify the test matches both source-specific requirements var windowsResult = matrix.GetTestResult("windows@Test_Platform"); - Assert.IsNotNull(windowsResult); - Assert.AreEqual(1, windowsResult.Executed); - Assert.AreEqual(1, windowsResult.Passes); + Assert.NotNull(windowsResult); + Assert.Equal(1, windowsResult.Executed); + Assert.Equal(1, windowsResult.Passes); var dotnet8Result = matrix.GetTestResult("dotnet8.x@Test_Platform"); - Assert.IsNotNull(dotnet8Result); - Assert.AreEqual(1, dotnet8Result.Executed); - Assert.AreEqual(1, dotnet8Result.Passes); + Assert.NotNull(dotnet8Result); + Assert.Equal(1, dotnet8Result.Executed); + Assert.Equal(1, dotnet8Result.Passes); // Verify both requirements would be satisfied var (satisfied, total) = matrix.CalculateSatisfiedRequirements(); - Assert.AreEqual(2, satisfied); - Assert.AreEqual(2, total); + Assert.Equal(2, satisfied); + Assert.Equal(2, total); } /// @@ -477,9 +496,10 @@ public void TraceMatrix_GetTestResult_WithMultipleSourceSpecifiers_MatchesAllReq /// This is a regression test for the issue where a test with source-specific format would /// prevent matching the same test with plain format in the same file. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithMixedFilterAndPlainReferences_MatchesBoth() { + // Arrange: // Create requirements where the same test is referenced with and without file filter var reqYaml = @"--- sections: @@ -497,7 +517,7 @@ public void TraceMatrix_GetTestResult_WithMixedFilterAndPlainReferences_MatchesB var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create a Windows test result file containing the shared test @@ -513,33 +533,36 @@ public void TraceMatrix_GetTestResult_WithMixedFilterAndPlainReferences_MatchesB var testPath = Path.Combine(_testDirectory, "test-results-windows-latest.trx"); File.WriteAllText(testPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, testPath); + // Assert: // Verify the source-specific test is tracked var sourceSpecificResult = matrix.GetTestResult("windows@Test_SharedTest"); - Assert.IsNotNull(sourceSpecificResult, "Source-specific test should be tracked"); - Assert.AreEqual(1, sourceSpecificResult.Executed); - Assert.AreEqual(1, sourceSpecificResult.Passes); + Assert.NotNull(sourceSpecificResult); + Assert.Equal(1, sourceSpecificResult.Executed); + Assert.Equal(1, sourceSpecificResult.Passes); - // Verify the plain test is ALSO tracked (this is the bug - it won't be tracked) + // Verify the plain test is ALSO tracked (regression: was not tracked before fix) var plainResult = matrix.GetTestResult("Test_SharedTest"); - Assert.IsNotNull(plainResult, "Plain test name should also be tracked from the same file"); - Assert.AreEqual(1, plainResult.Executed); - Assert.AreEqual(1, plainResult.Passes); + Assert.NotNull(plainResult); + Assert.Equal(1, plainResult.Executed); + Assert.Equal(1, plainResult.Passes); // Verify both requirements are satisfied var (satisfied, total) = matrix.CalculateSatisfiedRequirements(); - Assert.AreEqual(2, satisfied, "Both requirements should be satisfied"); - Assert.AreEqual(2, total); + Assert.Equal(2, satisfied); + Assert.Equal(2, total); } /// /// Test that non-executed tests are ignored and don't affect execution counts. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithNotExecutedTests_IgnoresNonExecutedTests() { + // Arrange: // Create requirements with test references var reqYaml = @"--- sections: @@ -554,7 +577,7 @@ public void TraceMatrix_GetTestResult_WithNotExecutedTests_IgnoresNonExecutedTes var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create test results with one executed and one not-executed test @@ -578,31 +601,34 @@ public void TraceMatrix_GetTestResult_WithNotExecutedTests_IgnoresNonExecutedTes var testPath = Path.Combine(_testDirectory, "test-results.trx"); File.WriteAllText(testPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, testPath); + // Assert: // Verify executed test is tracked var executedResult = matrix.GetTestResult("Test_ExecutedTest"); - Assert.IsNotNull(executedResult, "Executed test should be tracked"); - Assert.AreEqual(1, executedResult.Executed); - Assert.AreEqual(1, executedResult.Passes); + Assert.NotNull(executedResult); + Assert.Equal(1, executedResult.Executed); + Assert.Equal(1, executedResult.Passes); // Verify not-executed test is NOT tracked var notExecutedResult = matrix.GetTestResult("Test_NotExecutedTest"); - Assert.AreEqual(0, notExecutedResult.Executed, "Not-executed test should not be tracked"); + Assert.Equal(0, notExecutedResult.Executed); // Verify requirement is not satisfied (has a test reference without execution) var (satisfied, total) = matrix.CalculateSatisfiedRequirements(); - Assert.AreEqual(0, satisfied, "Requirement should not be satisfied when a referenced test is not executed"); - Assert.AreEqual(1, total); + Assert.Equal(0, satisfied); + Assert.Equal(1, total); } /// /// Test that requirements with only non-executed tests are treated as having no test coverage. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithOnlyNotExecutedTests_TreatsAsNoTests() { + // Arrange: // Create requirements with test references var reqYaml = @"--- sections: @@ -617,7 +643,7 @@ public void TraceMatrix_GetTestResult_WithOnlyNotExecutedTests_TreatsAsNoTests() var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create test results with only not-executed tests @@ -641,28 +667,31 @@ public void TraceMatrix_GetTestResult_WithOnlyNotExecutedTests_TreatsAsNoTests() var testPath = Path.Combine(_testDirectory, "test-results.trx"); File.WriteAllText(testPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, testPath); + // Assert: // Verify no tests are tracked var result1 = matrix.GetTestResult("Test_NotExecuted1"); - Assert.AreEqual(0, result1.Executed, "Not-executed test should not be tracked"); + Assert.Equal(0, result1.Executed); var result2 = matrix.GetTestResult("Test_NotExecuted2"); - Assert.AreEqual(0, result2.Executed, "Not-executed test should not be tracked"); + Assert.Equal(0, result2.Executed); // Verify requirement is not satisfied (has no executed tests) var (satisfied, total) = matrix.CalculateSatisfiedRequirements(); - Assert.AreEqual(0, satisfied, "Requirement should not be satisfied when all tests are not executed"); - Assert.AreEqual(1, total); + Assert.Equal(0, satisfied); + Assert.Equal(1, total); } /// /// Test that non-executed tests are properly handled in mixed outcome scenarios. /// - [TestMethod] + [Fact] public void TraceMatrix_GetTestResult_WithMixedOutcomes_OnlyCountsExecutedTests() { + // Arrange: // Create requirements with test references var reqYaml = @"--- sections: @@ -678,7 +707,7 @@ public void TraceMatrix_GetTestResult_WithMixedOutcomes_OnlyCountsExecutedTests( var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); var loadResult = Requirements.Load(reqPath); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var requirements = loadResult.Requirements; // Create test results with passed, failed, and not-executed tests @@ -711,28 +740,30 @@ public void TraceMatrix_GetTestResult_WithMixedOutcomes_OnlyCountsExecutedTests( var testPath = Path.Combine(_testDirectory, "test-results.trx"); File.WriteAllText(testPath, TrxSerializer.Serialize(testResults)); + // Act: // Create TraceMatrix var matrix = new TraceMatrix(requirements, testPath); + // Assert: // Verify passed test is tracked var passedResult = matrix.GetTestResult("Test_Passed"); - Assert.IsNotNull(passedResult); - Assert.AreEqual(1, passedResult.Executed); - Assert.AreEqual(1, passedResult.Passes); + Assert.NotNull(passedResult); + Assert.Equal(1, passedResult.Executed); + Assert.Equal(1, passedResult.Passes); // Verify failed test is tracked var failedResult = matrix.GetTestResult("Test_Failed"); - Assert.IsNotNull(failedResult); - Assert.AreEqual(1, failedResult.Executed); - Assert.AreEqual(0, failedResult.Passes); + Assert.NotNull(failedResult); + Assert.Equal(1, failedResult.Executed); + Assert.Equal(0, failedResult.Passes); // Verify not-executed test is NOT tracked var notExecutedResult = matrix.GetTestResult("Test_NotExecuted"); - Assert.AreEqual(0, notExecutedResult.Executed, "Not-executed test should not be tracked"); + Assert.Equal(0, notExecutedResult.Executed); // Verify requirement is not satisfied (has a failed test) var (satisfied, total) = matrix.CalculateSatisfiedRequirements(); - Assert.AreEqual(0, satisfied, "Requirement should not be satisfied when a test fails"); - Assert.AreEqual(1, total); + Assert.Equal(0, satisfied); + Assert.Equal(1, total); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Tracing/TracingTests.cs b/test/DemaConsulting.ReqStream.Tests/Tracing/TracingTests.cs index 431b519..7e0f79a 100644 --- a/test/DemaConsulting.ReqStream.Tests/Tracing/TracingTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Tracing/TracingTests.cs @@ -30,16 +30,14 @@ namespace DemaConsulting.ReqStream.Tests.Tracing; /// Tests for the Tracing subsystem, proving the TraceMatrix class is sufficient to /// implement the Tracing subsystem requirements. /// -[TestClass] -public class TracingTests +public sealed class TracingTests : IDisposable { - private string _testDirectory = string.Empty; + private readonly string _testDirectory; /// /// Initialize test by creating a temporary test directory. /// - [TestInitialize] - public void TestInitialize() + public TracingTests() { _testDirectory = Path.Combine(Path.GetTempPath(), $"reqstream_tracing_{Guid.NewGuid()}"); Directory.CreateDirectory(_testDirectory); @@ -48,19 +46,19 @@ public void TestInitialize() /// /// Clean up test by deleting the temporary test directory. /// - [TestCleanup] - public void TestCleanup() + public void Dispose() { if (Directory.Exists(_testDirectory)) { Directory.Delete(_testDirectory, recursive: true); } + GC.SuppressFinalize(this); } /// /// Test that a TRX results file is loaded and its test results are accessible via the trace matrix. /// - [TestMethod] + [Fact] public void Tracing_TestResults_TrxFile_LoadsTestResults() { // Arrange: create a requirements file with one traceable requirement @@ -76,7 +74,7 @@ public void Tracing_TestResults_TrxFile_LoadsTestResults() - TracingTest1 """); var loadResult = Requirements.Load(reqFile); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); // Arrange: create a TRX file with a passing test result var testResults = new TestResults.TestResults { Name = "TracingRun" }; @@ -96,14 +94,14 @@ public void Tracing_TestResults_TrxFile_LoadsTestResults() // Assert: the test result was loaded with one pass and zero fails var result = matrix.GetTestResult("TracingTest1"); - Assert.AreEqual(1, result.Passes); - Assert.AreEqual(0, result.Fails); + Assert.Equal(1, result.Passes); + Assert.Equal(0, result.Fails); } /// /// Test that a JUnit XML results file is loaded and its test results are accessible via the trace matrix. /// - [TestMethod] + [Fact] public void Tracing_TestResults_JUnitFile_LoadsTestResults() { // Arrange: create a requirements file with one traceable requirement @@ -119,7 +117,7 @@ public void Tracing_TestResults_JUnitFile_LoadsTestResults() - TracingJUnitTest1 """); var loadResult = Requirements.Load(reqFile); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); // Arrange: create a JUnit XML file with a passing test result var testResults = new TestResults.TestResults { Name = "TracingJUnitRun" }; @@ -139,14 +137,14 @@ public void Tracing_TestResults_JUnitFile_LoadsTestResults() // Assert: the test result was loaded with one pass and zero fails var result = matrix.GetTestResult("TracingJUnitTest1"); - Assert.AreEqual(1, result.Passes); - Assert.AreEqual(0, result.Fails); + Assert.Equal(1, result.Passes); + Assert.Equal(0, result.Fails); } /// /// Test that all requirements are satisfied when every required test has a passing result. /// - [TestMethod] + [Fact] public void Tracing_Coverage_WithPassingTests_AllRequirementsSatisfied() { // Arrange: create a requirements file with one requirement to be satisfied @@ -162,7 +160,7 @@ public void Tracing_Coverage_WithPassingTests_AllRequirementsSatisfied() - EnforcementTest1 """); var loadResult = Requirements.Load(reqFile); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); // Arrange: create a TRX file with a passing test result matching the requirement var testResults = new TestResults.TestResults { Name = "EnforcementRun" }; @@ -182,13 +180,13 @@ public void Tracing_Coverage_WithPassingTests_AllRequirementsSatisfied() var unsatisfied = matrix.GetUnsatisfiedRequirements(); // Assert: no unsatisfied requirements - Assert.HasCount(0, unsatisfied); + Assert.Empty(unsatisfied); } /// /// Test that a requirement is unsatisfied when its required test has no matching result. /// - [TestMethod] + [Fact] public void Tracing_Coverage_WithMissingTests_RequirementIsUnsatisfied() { // Arrange: create a requirements file with one requirement whose test will not be present @@ -204,7 +202,7 @@ public void Tracing_Coverage_WithMissingTests_RequirementIsUnsatisfied() - MissingTest1 """); var loadResult = Requirements.Load(reqFile); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); // Arrange: create a TRX file with no test results (empty run) var testResults = new TestResults.TestResults { Name = "EmptyRun" }; @@ -216,15 +214,15 @@ public void Tracing_Coverage_WithMissingTests_RequirementIsUnsatisfied() var unsatisfied = matrix.GetUnsatisfiedRequirements(); // Assert: the requirement is listed as unsatisfied - Assert.HasCount(1, unsatisfied); + Assert.Single(unsatisfied); Assert.Contains("Tracing-Enforce-Unsatisfied", unsatisfied); } /// /// Test that constructing a TraceMatrix with a non-existent file path throws FileNotFoundException. /// - [TestMethod] - public void TraceMatrix_Constructor_NonExistentFile_ThrowsFileNotFoundException() + [Fact] + public void Tracing_FileLoading_NonExistentFile_ThrowsFileNotFoundException() { // Arrange: create a requirements object and a path to a file that does not exist var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); @@ -239,11 +237,11 @@ public void TraceMatrix_Constructor_NonExistentFile_ThrowsFileNotFoundException( - ErrorTest1 """); var loadResult = Requirements.Load(reqFile); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var missingFile = Path.Combine(_testDirectory, "does-not-exist.trx"); // Act and Assert: constructing a TraceMatrix with a missing file throws FileNotFoundException - Assert.ThrowsExactly(() => + Assert.Throws(() => _ = new TraceMatrix(loadResult.Requirements, missingFile)); } @@ -251,8 +249,8 @@ public void TraceMatrix_Constructor_NonExistentFile_ThrowsFileNotFoundException( /// Test that constructing a TraceMatrix with a malformed result file throws InvalidOperationException /// containing the offending file path in the message. /// - [TestMethod] - public void TraceMatrix_Constructor_MalformedFile_ThrowsInvalidOperationException() + [Fact] + public void Tracing_FileLoading_MalformedFile_ThrowsInvalidOperationException() { // Arrange: create a requirements object and a file with invalid (non-XML) content var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); @@ -267,21 +265,21 @@ public void TraceMatrix_Constructor_MalformedFile_ThrowsInvalidOperationExceptio - ErrorTest2 """); var loadResult = Requirements.Load(reqFile); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); var malformedFile = Path.Combine(_testDirectory, "malformed.trx"); File.WriteAllText(malformedFile, "this is not valid xml or json content @@##!!"); // Act and Assert: constructing a TraceMatrix with a malformed file throws InvalidOperationException // with the offending path in the message - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => _ = new TraceMatrix(loadResult.Requirements, malformedFile)); - Assert.Contains(malformedFile, ex.Message, "Exception message should contain the file path."); + Assert.Contains(malformedFile, ex.Message); } /// /// Test that the Tracing subsystem exports a trace matrix report to a Markdown file. /// - [TestMethod] + [Fact] public void Tracing_Reporting_SimpleMatrix_CreatesMarkdownFile() { // Arrange: create a requirements file with one traceable requirement @@ -297,7 +295,7 @@ public void Tracing_Reporting_SimpleMatrix_CreatesMarkdownFile() - Tracing_Reporting_Test1 """); var loadResult = Requirements.Load(reqFile); - Assert.IsNotNull(loadResult.Requirements); + Assert.NotNull(loadResult.Requirements); // Arrange: create a TRX file with a passing test result var testResults = new TestResults.TestResults { Name = "ReportingRun" }; @@ -318,7 +316,7 @@ public void Tracing_Reporting_SimpleMatrix_CreatesMarkdownFile() matrix.Export(mdFile); // Assert: the Markdown report file exists and contains required sections - Assert.IsTrue(File.Exists(mdFile)); + Assert.True(File.Exists(mdFile)); var content = File.ReadAllText(mdFile); Assert.Contains("# Summary", content); Assert.Contains("1 of 1 requirements are satisfied with tests.", content);