diff --git a/.github/agents/software-quality-enforcer.md b/.github/agents/software-quality-enforcer.md index 478b83f..e532b64 100644 --- a/.github/agents/software-quality-enforcer.md +++ b/.github/agents/software-quality-enforcer.md @@ -84,16 +84,16 @@ Based on `.editorconfig` and project preferences: - SonarAnalyzer.CSharp enabled - EnforceCodeStyleInBuild enabled - AnalysisLevel set to latest -- **Analyzer Suppressions**: Use `.globalconfig` file to disable rules globally when needed (e.g., for serializer - DTOs), rather than SuppressMessage attributes ### Test Requirements - **Test Framework**: MSTest (Microsoft.VisualStudio.TestTools.UnitTesting) -- **Test File Naming**: `[Component]Tests.cs` (e.g., `BasicTests.cs`) +- **Test File Naming**: `[Component]Tests.cs` (e.g., `ContextTests.cs`, `ProgramTests.cs`) - **Test Class Naming**: Descriptive names ending with `Tests` -- **Test Method Naming**: `TestMethod_Scenario_ExpectedBehavior` - - Examples: `Parse_ValidYaml_ReturnsDocument()`, `Validate_MissingRequiredField_ThrowsException()` +- **Test Method Naming**: `ClassName_MethodUnderTest_Scenario_ExpectedBehavior` + - Example: `Context_Create_NoArguments_ReturnsDefaultContext` clearly indicates testing the `Context.Create` method + - Example: `Program_Run_WithVersionFlag_PrintsVersion` clearly indicates testing the `Program.Run` method + - This pattern makes test intent clear for requirements traceability and linking - **All tests must pass** before merging - **No warnings allowed** in test builds diff --git a/AGENTS.md b/AGENTS.md index 6b18354..cb58c17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,8 +55,11 @@ ReqStream/ ## Testing Guidelines - **Test Framework**: MSTest v4 (Microsoft.VisualStudio.TestTools.UnitTesting) -- **Test File Naming**: `[Component]Tests.cs` (e.g., `BasicTests.cs`) -- **Test Method Naming**: `TestMethod_Scenario_ExpectedBehavior` format +- **Test File Naming**: `[Component]Tests.cs` (e.g., `ContextTests.cs`, `ProgramTests.cs`) +- **Test Method Naming**: `ClassName_MethodUnderTest_Scenario_ExpectedBehavior` format + - Example: `Context_Create_NoArguments_ReturnsDefaultContext` clearly indicates testing the `Context.Create` method + - Example: `Context_WriteLine_NormalMode_WritesToConsole` clearly indicates testing the `Context.WriteLine` method + - This pattern makes test intent clear for requirements traceability - **MSTest v4 APIs**: Use modern assertions: - `Assert.HasCount(collection, expectedCount)` instead of `Assert.AreEqual(count, collection.Count)` - `Assert.IsEmpty(collection)` instead of `Assert.AreEqual(0, collection.Count)` diff --git a/README.md b/README.md index 4c286f0..b06cf40 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ Options: --tests Test result files glob pattern (TRX or JUnit) --matrix Export trace matrix to markdown file --matrix-depth Markdown header depth for trace matrix (default: 1) + --enforce Fail if requirements are not fully tested ``` ## YAML Format @@ -201,6 +202,57 @@ requirements: - File part matching is case-insensitive and supports partial filename matching - Both plain and source-specific test names can be mixed in the same requirement +## Requirements Enforcement + +ReqStream can enforce that all requirements have adequate test coverage, making it ideal for use in CI/CD pipelines +to ensure quality gates are met. + +### Enforcement Mode + +Use the `--enforce` flag to fail the build if any requirements are not fully satisfied with tests: + +```bash +reqstream --requirements "**/*.yaml" --tests "**/*.trx" --enforce +``` + +When enforcement mode is enabled: + +- All requirements must have at least one test mapped (either directly or through child requirements) +- All mapped tests must be present in the test results +- All mapped tests must pass +- If any requirement is not satisfied, an error is reported and the exit code is non-zero + +### CI/CD Integration + +Enforcement mode is designed for CI/CD pipelines. The error message is printed after all reports are generated, +allowing you to review the reports for failure analysis: + +```bash +# GitHub Actions example +- name: Validate Requirements Coverage + run: | + dotnet reqstream \ + --requirements "docs/**/*.yaml" \ + --tests "test-results/**/*.trx" \ + --matrix trace-matrix.md \ + --enforce +``` + +If requirements are not fully satisfied, the tool will print: + +```text +Error: Only X of Y requirements are satisfied with tests. +``` + +And exit with code 1, failing the build. + +### Best Practices + +- Use `--enforce` in CI/CD to prevent merging code that reduces requirements coverage +- Generate the trace matrix (`--matrix`) alongside enforcement to review coverage details +- Start without enforcement initially, then enable it once baseline coverage is established +- Use transitive coverage through child requirements for high-level requirements that don't have direct tests + ## Development ### Requirements diff --git a/docs/guide/guide.md b/docs/guide/guide.md index 326c67d..8cb4bc4 100644 --- a/docs/guide/guide.md +++ b/docs/guide/guide.md @@ -444,6 +444,7 @@ ReqStream supports the following command-line options: | `--tests ` | Glob pattern for test result files (TRX or JUnit format) | | `--matrix ` | Export trace matrix to markdown file | | `--matrix-depth ` | Starting header depth for trace matrix (default: 1) | +| `--enforce` | Fail if requirements are not fully tested | ### Examples @@ -498,6 +499,22 @@ reqstream --requirements "**/*.yaml" \ --tests "test-results/**/*.xml" ``` +**Requirements enforcement in CI/CD:** + +```bash +# Enforce that all requirements have passing tests +reqstream --requirements "**/*.yaml" \ + --tests "test-results/**/*.trx" \ + --enforce + +# Generate reports and enforce coverage +reqstream --requirements "**/*.yaml" \ + --tests "test-results/**/*.trx" \ + --report requirements.md \ + --matrix trace-matrix.md \ + --enforce +``` + ## Exporting ReqStream can export requirements and test trace matrices to markdown format for documentation and review. @@ -621,6 +638,312 @@ reqstream --silent \ --report requirements.md ``` +## Requirements Enforcement + +ReqStream can enforce that all requirements have adequate test coverage, making it ideal for use in CI/CD pipelines +as a quality gate to ensure requirements are properly verified. + +### Overview + +Requirements enforcement validates that: + +- Every requirement has at least one test mapped (either directly or through child requirements) +- All mapped tests are present in the test result files +- All mapped tests pass + +If any requirement doesn't meet these criteria, the tool reports an error and exits with a non-zero status code, +causing CI/CD builds to fail. + +### Usage + +Enable enforcement with the `--enforce` flag: + +```bash +reqstream --requirements "**/*.yaml" \ + --tests "test-results/**/*.trx" \ + --enforce +``` + +### How It Works + +Requirements can be satisfied in two ways: + +1. **Direct tests**: Tests mapped directly to the requirement via the `tests` field +2. **Transitive tests**: Tests mapped to child requirements + +For a requirement to be satisfied: + +- It must have at least one test (direct or via children) +- All tests must have been executed in the test results +- All tests must have passed + +**Example:** + +```yaml +sections: + - title: "System Security" + requirements: + - id: "SYS-SEC-001" + title: "The system shall support authentication." + children: + - "AUTH-001" + - "AUTH-002" + + - id: "AUTH-001" + title: "Users shall authenticate with username and password." + tests: + - "Test_UsernamePassword_Valid" + - "Test_UsernamePassword_Invalid" + + - id: "AUTH-002" + title: "Failed authentication attempts shall be logged." + tests: + - "Test_FailedAuth_Logged" +``` + +In this example: + +- `AUTH-001` is satisfied if both its tests pass +- `AUTH-002` is satisfied if its test passes +- `SYS-SEC-001` is satisfied transitively through its children (if both `AUTH-001` and `AUTH-002` are satisfied) + +### Enforcement Output + +When enforcement mode is enabled, ReqStream processes normally and generates any requested reports. After all +processing is complete, it checks requirement satisfaction. + +**If all requirements are satisfied:** + +```text +... +Trace matrix report generated successfully. +``` + +Exit code: **0** (success) + +**If requirements are not satisfied:** + +```text +... +Trace matrix report generated successfully. +Error: Only 15 of 20 requirements are satisfied with tests. +``` + +Exit code: **1** (failure) + +The error message clearly indicates how many requirements are satisfied, making it easy to track progress toward +full coverage. + +### CI/CD Integration + +Requirements enforcement is designed for CI/CD pipelines. Here are examples for common platforms: + +**GitHub Actions:** + +```yaml +name: Validate Requirements + +on: [push, pull_request] + +jobs: + requirements: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Install ReqStream + run: dotnet tool install -g DemaConsulting.ReqStream + + - name: Run Tests + run: dotnet test --logger trx + + - name: Validate Requirements Coverage + run: | + reqstream \ + --requirements "docs/**/*.yaml" \ + --tests "**/*.trx" \ + --matrix trace-matrix.md \ + --enforce + + - name: Upload Trace Matrix + if: always() + uses: actions/upload-artifact@v4 + with: + name: trace-matrix + path: trace-matrix.md +``` + +**Azure Pipelines:** + +```yaml +steps: + - task: DotNetCoreCLI@2 + displayName: 'Install ReqStream' + inputs: + command: 'custom' + custom: 'tool' + arguments: 'install -g DemaConsulting.ReqStream' + + - task: DotNetCoreCLI@2 + displayName: 'Run Tests' + inputs: + command: 'test' + arguments: '--logger trx' + + - script: | + reqstream \ + --requirements "docs/**/*.yaml" \ + --tests "**/*.trx" \ + --matrix trace-matrix.md \ + --enforce + displayName: 'Validate Requirements Coverage' +``` + +**GitLab CI:** + +```yaml +validate-requirements: + stage: test + script: + - dotnet tool install -g DemaConsulting.ReqStream + - export PATH="$PATH:$HOME/.dotnet/tools" + - dotnet test --logger trx + - reqstream --requirements "docs/**/*.yaml" --tests "**/*.trx" --matrix trace-matrix.md --enforce + artifacts: + when: always + paths: + - trace-matrix.md +``` + +### Best Practices + +**Start without enforcement:** + +When first adopting ReqStream, start by generating trace matrices without enforcement to understand your current +coverage: + +```bash +reqstream --requirements "**/*.yaml" \ + --tests "**/*.trx" \ + --matrix trace-matrix.md +``` + +Review the trace matrix to identify gaps, then work toward full coverage before enabling enforcement. + +**Enable enforcement incrementally:** + +If you have a large requirements set with incomplete coverage, consider: + +1. Start with enforcement on critical requirements only +2. Gradually expand coverage +3. Enable enforcement for all requirements once baseline is achieved + +**Use in pull requests:** + +Enable enforcement in PR validation to prevent coverage from decreasing: + +```yaml +# GitHub Actions PR validation +on: [pull_request] + +jobs: + requirements-coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: dotnet test --logger trx + - run: dotnet reqstream --requirements "**/*.yaml" --tests "**/*.trx" --enforce +``` + +**Generate reports for failure analysis:** + +Always generate the trace matrix when using enforcement so you can review which requirements are not satisfied: + +```bash +reqstream --requirements "**/*.yaml" \ + --tests "**/*.trx" \ + --matrix trace-matrix.md \ + --enforce +``` + +The trace matrix will show which requirements lack tests or have failing tests. + +**Leverage transitive coverage:** + +Use parent-child relationships to organize requirements hierarchically. High-level requirements don't need direct +tests if they're satisfied through child requirements: + +```yaml +requirements: + - id: "HIGH-LEVEL-001" + title: "System shall be secure" + children: + - "SEC-001" + - "SEC-002" + - "SEC-003" + + # Children have direct tests + - id: "SEC-001" + title: "Authentication required" + tests: + - "Test_Auth_Required" +``` + +### Troubleshooting Enforcement + +#### Error: Cannot enforce requirements without test results + +This error occurs when `--enforce` is used without the `--tests` option. You must provide test result files to +validate coverage: + +```bash +# Wrong - no test results +reqstream --requirements "**/*.yaml" --enforce + +# Correct - with test results +reqstream --requirements "**/*.yaml" --tests "**/*.trx" --enforce +``` + +#### All requirements show as unsatisfied + +If all or most requirements are showing as unsatisfied, check: + +1. Test names in requirements YAML match test names in test result files exactly (case-sensitive) +2. Test result files are in TRX or JUnit format +3. Tests are actually being executed (check test result file contents) +4. Tests are passing (failing tests count as unsatisfied) + +#### Some tests don't match + +If specific tests aren't being recognized: + +1. Verify exact test name match (including namespaces if present) +2. Check for typos in requirements YAML +3. If using source-specific tests (`filepart@testname`), verify the file part matches the test result filename +4. Run without `--enforce` first and review the trace matrix to see which tests are found + +#### Requirements with no direct tests show as unsatisfied + +Ensure parent requirements reference their children via the `children` field: + +```yaml +requirements: + - id: "PARENT-001" + title: "Parent requirement" + children: + - "CHILD-001" # Add child references + + - id: "CHILD-001" + title: "Child requirement" + tests: + - "Test_Child" +``` + ## FAQ ### General Questions @@ -736,6 +1059,40 @@ results from all test result files. A: Yes, you can mix both styles in the same requirement. Plain test names will aggregate results from all test result files, while source-specific test names will only match their specified sources. +### Enforcement Questions + +**Q: What does the --enforce flag do?** + +A: The `--enforce` flag validates that all requirements have adequate test coverage. If any requirement lacks tests or +has failing tests, the tool will exit with a non-zero status code, failing the build. This is useful for CI/CD +pipelines to ensure requirements are properly verified. + +**Q: When should I use --enforce?** + +A: Use `--enforce` in CI/CD pipelines to prevent merging code that reduces requirements coverage. Start by reviewing +your coverage with trace matrices first, then enable enforcement once you have acceptable baseline coverage. + +**Q: How does transitive coverage work with --enforce?** + +A: A parent requirement can be satisfied through its child requirements. If a requirement references children via the +`children` field, it's considered satisfied if all its children are satisfied with tests. This allows high-level +requirements to be validated through their detailed child requirements. + +**Q: What happens if tests fail when --enforce is enabled?** + +A: Requirements with failing tests are considered not satisfied. The tool will report an error and exit with code 1. +Review the trace matrix (use `--matrix`) to see which tests failed. + +**Q: Can I use --enforce without generating reports?** + +A: Yes, but it's recommended to generate the trace matrix (`--matrix`) alongside enforcement. The matrix provides +detailed information about which requirements are not satisfied, making it easier to identify and fix coverage gaps. + +**Q: Why does --enforce require test results?** + +A: Enforcement validates that requirements have passing tests, which requires test result files. If you use `--enforce` +without `--tests`, the tool will report an error asking you to specify test result files. + ### Export Questions **Q: Can I customize the markdown format of reports?** diff --git a/requirements.yaml b/requirements.yaml index 27a585f..5f10bc7 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -7,91 +7,91 @@ sections: - id: "CLI-001" title: "The tool shall provide a command-line interface." tests: - - "Create_NoArguments_ReturnsDefaultContext" - - "Create_MultipleArguments_ParsesAllCorrectly" + - "Context_Create_NoArguments_ReturnsDefaultContext" + - "Context_Create_MultipleArguments_ParsesAllCorrectly" - id: "CLI-002" title: "The tool shall display version information when requested." tests: - - "Run_WithVersionFlag_PrintsVersion" - - "Create_VersionFlag_SetsVersionProperty" + - "Program_Run_WithVersionFlag_PrintsVersion" + - "Context_Create_VersionFlag_SetsVersionProperty" - id: "CLI-003" title: "The tool shall display help information when requested." tests: - - "Run_WithHelpFlag_PrintsHelp" - - "Create_HelpFlags_SetsHelpProperty" + - "Program_Run_WithHelpFlag_PrintsHelp" + - "Context_Create_HelpFlags_SetsHelpProperty" - title: "Requirements File Processing" requirements: - id: "REQ-001" title: "The tool shall process YAML requirements files." tests: - - "Read_SimpleRequirement_ParsesCorrectly" - - "Run_WithRequirementsFiles_ProcessesSuccessfully" - - "Read_ComplexStructure_ParsesCorrectly" + - "Requirements_Read_SimpleRequirement_ParsesCorrectly" + - "Program_Run_WithRequirementsFiles_ProcessesSuccessfully" + - "Requirements_Read_ComplexStructure_ParsesCorrectly" - id: "REQ-002" title: "The tool shall support glob patterns for requirements files." tests: - - "Create_WithRequirementsPattern_ExpandsGlobPattern" + - "Context_Create_WithRequirementsPattern_ExpandsGlobPattern" - id: "REQ-003" title: "The tool shall validate requirements file structure." tests: - - "Read_BlankSectionTitle_ThrowsExceptionWithFileLocation" - - "Read_BlankRequirementId_ThrowsExceptionWithFileLocation" - - "Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation" - - "Read_DuplicateRequirementId_ThrowsException" - - "Read_DuplicateRequirementId_ExceptionIncludesFileLocation" + - "Requirements_Read_BlankSectionTitle_ThrowsExceptionWithFileLocation" + - "Requirements_Read_BlankRequirementId_ThrowsExceptionWithFileLocation" + - "Requirements_Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation" + - "Requirements_Read_DuplicateRequirementId_ThrowsException" + - "Requirements_Read_DuplicateRequirementId_ExceptionIncludesFileLocation" - id: "REQ-004" title: "The tool shall support hierarchical sections and subsections." tests: - - "Read_NestedSections_ParsesHierarchyCorrectly" - - "Export_NestedSections_CreatesHierarchy" + - "Requirements_Read_NestedSections_ParsesHierarchyCorrectly" + - "Requirements_Export_NestedSections_CreatesHierarchy" - id: "REQ-005" title: "The tool shall support file includes in requirements files." tests: - - "Read_WithIncludes_MergesFilesCorrectly" - - "Read_MultipleFiles_MergesAllFiles" - - "Read_IncludeLoop_DoesNotCauseInfiniteLoop" + - "Requirements_Read_WithIncludes_MergesFilesCorrectly" + - "Requirements_Read_MultipleFiles_MergesAllFiles" + - "Requirements_Read_IncludeLoop_DoesNotCauseInfiniteLoop" - id: "REQ-006" title: "The tool shall merge sections with the same hierarchy path." tests: - - "Read_IdenticalSections_MergesCorrectly" - - "Read_MultipleFilesWithSameSections_MergesSections" + - "Requirements_Read_IdenticalSections_MergesCorrectly" + - "Requirements_Read_MultipleFilesWithSameSections_MergesSections" - title: "Requirements Definition" requirements: - id: "REQ-007" title: "The tool shall require each requirement to have a unique identifier." tests: - - "Read_DuplicateRequirementId_ThrowsException" - - "Read_BlankRequirementId_ThrowsExceptionWithFileLocation" - - "Read_MultipleFilesWithDuplicateIds_ThrowsException" + - "Requirements_Read_DuplicateRequirementId_ThrowsException" + - "Requirements_Read_BlankRequirementId_ThrowsExceptionWithFileLocation" + - "Requirements_Read_MultipleFilesWithDuplicateIds_ThrowsException" - id: "REQ-008" title: "The tool shall require each requirement to have a title." tests: - - "Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation" + - "Requirements_Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation" - id: "REQ-009" title: "The tool shall support parent-child relationships between requirements." tests: - - "Read_RequirementWithChildren_ParsesChildrenCorrectly" - - "Export_WithChildRequirements_ConsidersChildTests" + - "Requirements_Read_RequirementWithChildren_ParsesChildrenCorrectly" + - "TraceMatrix_Export_WithChildRequirements_ConsidersChildTests" - id: "REQ-010" title: "The tool shall support test mappings for requirements." tests: - - "Read_RequirementWithTests_ParsesTestsCorrectly" - - "Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation" - - "Read_TestMappings_AppliesMappingsCorrectly" - - "Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation" - - "Read_BlankMappingId_ThrowsExceptionWithFileLocation" + - "Requirements_Read_RequirementWithTests_ParsesTestsCorrectly" + - "Requirements_Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation" + - "Requirements_Read_TestMappings_AppliesMappingsCorrectly" + - "Requirements_Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation" + - "Requirements_Read_BlankMappingId_ThrowsExceptionWithFileLocation" - title: "Test Integration" requirements: @@ -117,7 +117,7 @@ sections: - id: "TEST-004" title: "The tool shall support glob patterns for test result files." tests: - - "Create_WithTestsPattern_ExpandsGlobPattern" + - "Context_Create_WithTestsPattern_ExpandsGlobPattern" - "TraceMatrix_WithMixedFormats_ProcessesBoth" - id: "TEST-005" @@ -151,48 +151,48 @@ sections: - id: "RPT-001" title: "The tool shall export requirements to markdown format." tests: - - "Run_WithRequirementsExport_GeneratesReport" - - "Export_SimpleRequirements_CreatesMarkdownFile" - - "Export_MultipleSections_ExportsAll" - - "Export_EmptyRequirements_CreatesEmptyFile" + - "Program_Run_WithRequirementsExport_GeneratesReport" + - "Requirements_Export_SimpleRequirements_CreatesMarkdownFile" + - "Requirements_Export_MultipleSections_ExportsAll" + - "Requirements_Export_EmptyRequirements_CreatesEmptyFile" - id: "RPT-002" title: "The tool shall support configurable markdown header depth for requirements reports." tests: - - "Create_ReportDepth_SetsReportDepthProperty" - - "Export_WithCustomDepth_UsesCorrectHeaderLevel" + - "Context_Create_ReportDepth_SetsReportDepthProperty" + - "Requirements_Export_WithCustomDepth_UsesCorrectHeaderLevel" - id: "RPT-003" title: "The tool shall export trace matrices to markdown format." tests: - - "Run_WithTraceMatrixExport_GeneratesMatrix" - - "Export_SimpleTraceMatrix_CreatesMarkdownFile" - - "Export_WithFailedTests_ShowsFailures" - - "Export_WithNoTests_ShowsNotSatisfied" - - "Export_WithNotExecutedTests_ShowsNotExecuted" + - "Program_Run_WithTraceMatrixExport_GeneratesMatrix" + - "TraceMatrix_Export_SimpleTraceMatrix_CreatesMarkdownFile" + - "TraceMatrix_Export_WithFailedTests_ShowsFailures" + - "TraceMatrix_Export_WithNoTests_ShowsNotSatisfied" + - "TraceMatrix_Export_WithNotExecutedTests_ShowsNotExecuted" - id: "RPT-004" title: "The tool shall support configurable markdown header depth for trace matrices." tests: - - "Create_MatrixDepth_SetsMatrixDepthProperty" - - "Export_WithCustomDepth_UsesCorrectHeaderLevel" + - "Context_Create_MatrixDepth_SetsMatrixDepthProperty" + - "Requirements_Export_WithCustomDepth_UsesCorrectHeaderLevel" - title: "Logging" requirements: - id: "LOG-001" title: "The tool shall support writing output to a log file." tests: - - "Create_WithLogFile_WritesToLogFile" - - "Create_WithLogFileAndSilent_WritesToLogOnly" - - "Dispose_WithLogFile_ClosesLogFile" + - "Context_Create_WithLogFile_WritesToLogFile" + - "Context_Create_WithLogFileAndSilent_WritesToLogOnly" + - "Context_Dispose_WithLogFile_ClosesLogFile" - title: "Validation" requirements: - id: "VAL-001" title: "The tool shall support self-validation mode." tests: - - "Run_WithValidateFlag_ShowsPlaceholder" - - "Create_ValidateFlag_SetsValidateProperty" + - "Program_Run_WithValidateFlag_ShowsPlaceholder" + - "Context_Create_ValidateFlag_SetsValidateProperty" - title: "Platform Support" requirements: diff --git a/src/DemaConsulting.ReqStream/Context.cs b/src/DemaConsulting.ReqStream/Context.cs index 77e2569..97a4ee3 100644 --- a/src/DemaConsulting.ReqStream/Context.cs +++ b/src/DemaConsulting.ReqStream/Context.cs @@ -58,6 +58,11 @@ public sealed class Context : IDisposable /// public bool Validate { get; private init; } + /// + /// Gets a value indicating whether the enforce flag was specified. + /// + public bool Enforce { get; private init; } + /// /// Gets the list of requirements files found from the --requirements glob pattern. /// @@ -113,6 +118,7 @@ public static Context Create(string[] args) var help = false; var silent = false; var validate = false; + var enforce = false; // Initialize collection variables var requirementsFiles = new List(); @@ -153,6 +159,10 @@ public static Context Create(string[] args) validate = true; break; + case "--enforce": + enforce = true; + break; + case "--log": // Ensure argument has a value if (i >= args.Length) @@ -245,6 +255,7 @@ public static Context Create(string[] args) Help = help, Silent = silent, Validate = validate, + Enforce = enforce, RequirementsFiles = requirementsFiles, TestFiles = testFiles, RequirementsReport = requirementsReport, diff --git a/src/DemaConsulting.ReqStream/Program.cs b/src/DemaConsulting.ReqStream/Program.cs index d5d64a4..d4e7b05 100644 --- a/src/DemaConsulting.ReqStream/Program.cs +++ b/src/DemaConsulting.ReqStream/Program.cs @@ -142,6 +142,7 @@ private static void PrintHelp() Console.WriteLine(" --tests Test result files glob pattern (TRX or JUnit)"); Console.WriteLine(" --matrix Export trace matrix to markdown file"); Console.WriteLine(" --matrix-depth Markdown header depth for trace matrix (default: 1)"); + Console.WriteLine(" --enforce Fail if requirements are not fully tested"); } /// @@ -171,10 +172,11 @@ private static void ProcessRequirements(Context context) } // Create trace matrix if test files are specified + TraceMatrix? traceMatrix = null; if (context.TestFiles.Count > 0) { context.WriteLine($"Processing {context.TestFiles.Count} test result file(s)..."); - var traceMatrix = new TraceMatrix(requirements, context.TestFiles.ToArray()); + traceMatrix = new TraceMatrix(requirements, context.TestFiles.ToArray()); context.WriteLine("Trace matrix created successfully."); // Export trace matrix if requested @@ -185,5 +187,22 @@ private static void ProcessRequirements(Context context) context.WriteLine("Trace matrix report generated successfully."); } } + + // Enforce requirements coverage if requested + if (context.Enforce) + { + if (traceMatrix != null) + { + var (satisfied, total) = traceMatrix.CalculateSatisfiedRequirements(); + if (satisfied < total) + { + context.WriteError($"Error: Only {satisfied} of {total} requirements are satisfied with tests."); + } + } + else + { + context.WriteError("Error: Cannot enforce requirements without test results. Use --tests to specify test result files."); + } + } } } diff --git a/src/DemaConsulting.ReqStream/TraceMatrix.cs b/src/DemaConsulting.ReqStream/TraceMatrix.cs index 4c02ea5..511a0ef 100644 --- a/src/DemaConsulting.ReqStream/TraceMatrix.cs +++ b/src/DemaConsulting.ReqStream/TraceMatrix.cs @@ -128,6 +128,16 @@ private void ExportSummary(TextWriter writer, int depth) writer.WriteLine(); } + /// + /// Calculates how many requirements are satisfied. + /// A requirement is satisfied if it has at least one test and all tests have passed. + /// + /// A tuple of (satisfied count, total count). + public (int satisfied, int total) CalculateSatisfiedRequirements() + { + return CalculateSatisfiedRequirements(_requirements); + } + /// /// Calculates how many requirements are satisfied. /// A requirement is satisfied if it has at least one test and all tests have passed. diff --git a/test/DemaConsulting.ReqStream.Tests/ContextTests.cs b/test/DemaConsulting.ReqStream.Tests/ContextTests.cs index 674b37d..139b99c 100644 --- a/test/DemaConsulting.ReqStream.Tests/ContextTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/ContextTests.cs @@ -54,7 +54,7 @@ public void TestCleanup() /// Test creating a context with no arguments. /// [TestMethod] - public void Create_NoArguments_ReturnsDefaultContext() + public void Context_Create_NoArguments_ReturnsDefaultContext() { using var context = Context.Create([]); @@ -75,7 +75,7 @@ public void Create_NoArguments_ReturnsDefaultContext() /// Test creating a context with version flag. /// [TestMethod] - public void Create_VersionFlag_SetsVersionProperty() + public void Context_Create_VersionFlag_SetsVersionProperty() { using var context1 = Context.Create(["-v"]); Assert.IsTrue(context1.Version); @@ -90,7 +90,7 @@ public void Create_VersionFlag_SetsVersionProperty() /// Test creating a context with help flags. /// [TestMethod] - public void Create_HelpFlags_SetsHelpProperty() + public void Context_Create_HelpFlags_SetsHelpProperty() { using var context1 = Context.Create(["-?"]); Assert.IsTrue(context1.Help); @@ -109,7 +109,7 @@ public void Create_HelpFlags_SetsHelpProperty() /// Test creating a context with silent flag. /// [TestMethod] - public void Create_SilentFlag_SetsSilentProperty() + public void Context_Create_SilentFlag_SetsSilentProperty() { using var context = Context.Create(["--silent"]); @@ -121,7 +121,7 @@ public void Create_SilentFlag_SetsSilentProperty() /// Test creating a context with validate flag. /// [TestMethod] - public void Create_ValidateFlag_SetsValidateProperty() + public void Context_Create_ValidateFlag_SetsValidateProperty() { using var context = Context.Create(["--validate"]); @@ -129,11 +129,23 @@ public void Create_ValidateFlag_SetsValidateProperty() Assert.AreEqual(0, context.ExitCode); } + /// + /// Test creating a context with enforce flag. + /// + [TestMethod] + public void Context_Create_EnforceFlag_SetsEnforceProperty() + { + using var context = Context.Create(["--enforce"]); + + Assert.IsTrue(context.Enforce); + Assert.AreEqual(0, context.ExitCode); + } + /// /// Test creating a context with report depth. /// [TestMethod] - public void Create_ReportDepth_SetsReportDepthProperty() + public void Context_Create_ReportDepth_SetsReportDepthProperty() { using var context = Context.Create(["--report-depth", "3"]); @@ -145,7 +157,7 @@ public void Create_ReportDepth_SetsReportDepthProperty() /// Test creating a context with matrix depth. /// [TestMethod] - public void Create_MatrixDepth_SetsMatrixDepthProperty() + public void Context_Create_MatrixDepth_SetsMatrixDepthProperty() { using var context = Context.Create(["--matrix-depth", "2"]); @@ -157,7 +169,7 @@ public void Create_MatrixDepth_SetsMatrixDepthProperty() /// Test creating a context with report file. /// [TestMethod] - public void Create_ReportFile_SetsReportProperty() + public void Context_Create_ReportFile_SetsReportProperty() { using var context = Context.Create(["--report", "report.md"]); @@ -169,7 +181,7 @@ public void Create_ReportFile_SetsReportProperty() /// Test creating a context with matrix file. /// [TestMethod] - public void Create_MatrixFile_SetsMatrixProperty() + public void Context_Create_MatrixFile_SetsMatrixProperty() { using var context = Context.Create(["--matrix", "matrix.md"]); @@ -181,7 +193,7 @@ public void Create_MatrixFile_SetsMatrixProperty() /// Test creating a context with unsupported argument. /// [TestMethod] - public void Create_UnsupportedArgument_ThrowsException() + public void Context_Create_UnsupportedArgument_ThrowsException() { try { @@ -198,7 +210,7 @@ public void Create_UnsupportedArgument_ThrowsException() /// Test creating a context with missing log filename. /// [TestMethod] - public void Create_MissingLogFilename_ThrowsException() + public void Context_Create_MissingLogFilename_ThrowsException() { try { @@ -215,7 +227,7 @@ public void Create_MissingLogFilename_ThrowsException() /// Test creating a context with missing report filename. /// [TestMethod] - public void Create_MissingReportFilename_ThrowsException() + public void Context_Create_MissingReportFilename_ThrowsException() { try { @@ -232,7 +244,7 @@ public void Create_MissingReportFilename_ThrowsException() /// Test creating a context with missing matrix filename. /// [TestMethod] - public void Create_MissingMatrixFilename_ThrowsException() + public void Context_Create_MissingMatrixFilename_ThrowsException() { try { @@ -249,7 +261,7 @@ public void Create_MissingMatrixFilename_ThrowsException() /// Test creating a context with missing report depth. /// [TestMethod] - public void Create_MissingReportDepth_ThrowsException() + public void Context_Create_MissingReportDepth_ThrowsException() { try { @@ -266,7 +278,7 @@ public void Create_MissingReportDepth_ThrowsException() /// Test creating a context with missing matrix depth. /// [TestMethod] - public void Create_MissingMatrixDepth_ThrowsException() + public void Context_Create_MissingMatrixDepth_ThrowsException() { try { @@ -283,7 +295,7 @@ public void Create_MissingMatrixDepth_ThrowsException() /// Test creating a context with invalid report depth. /// [TestMethod] - public void Create_InvalidReportDepth_ThrowsException() + public void Context_Create_InvalidReportDepth_ThrowsException() { try { @@ -320,7 +332,7 @@ public void Create_InvalidReportDepth_ThrowsException() /// Test creating a context with invalid matrix depth. /// [TestMethod] - public void Create_InvalidMatrixDepth_ThrowsException() + public void Context_Create_InvalidMatrixDepth_ThrowsException() { try { @@ -347,7 +359,7 @@ public void Create_InvalidMatrixDepth_ThrowsException() /// Test WriteLine writes to console. /// [TestMethod] - public void WriteLine_NormalMode_WritesToConsole() + public void Context_WriteLine_NormalMode_WritesToConsole() { var originalOut = Console.Out; var output = new StringWriter(); @@ -370,7 +382,7 @@ public void WriteLine_NormalMode_WritesToConsole() /// Test WriteLine in silent mode doesn't write to console. /// [TestMethod] - public void WriteLine_SilentMode_DoesNotWriteToConsole() + public void Context_WriteLine_SilentMode_DoesNotWriteToConsole() { var originalOut = Console.Out; var output = new StringWriter(); @@ -393,7 +405,7 @@ public void WriteLine_SilentMode_DoesNotWriteToConsole() /// Test WriteError writes to console. /// [TestMethod] - public void WriteError_NormalMode_WritesToConsole() + public void Context_WriteError_NormalMode_WritesToConsole() { var originalOut = Console.Out; var output = new StringWriter(); @@ -417,7 +429,7 @@ public void WriteError_NormalMode_WritesToConsole() /// Test WriteError in silent mode doesn't write to console. /// [TestMethod] - public void WriteError_SilentMode_DoesNotWriteToConsole() + public void Context_WriteError_SilentMode_DoesNotWriteToConsole() { var originalOut = Console.Out; var output = new StringWriter(); @@ -441,7 +453,7 @@ public void WriteError_SilentMode_DoesNotWriteToConsole() /// Test log file creation and writing. /// [TestMethod] - public void Create_WithLogFile_WritesToLogFile() + public void Context_Create_WithLogFile_WritesToLogFile() { var logPath = Path.Combine(_testDirectory, "test.log"); @@ -461,7 +473,7 @@ public void Create_WithLogFile_WritesToLogFile() /// Test log file with silent mode still writes to log. /// [TestMethod] - public void Create_WithLogFileAndSilent_WritesToLogOnly() + public void Context_Create_WithLogFileAndSilent_WritesToLogOnly() { var originalOut = Console.Out; var output = new StringWriter(); @@ -494,7 +506,7 @@ public void Create_WithLogFileAndSilent_WritesToLogOnly() /// Test requirements glob pattern expansion. /// [TestMethod] - public void Create_WithRequirementsPattern_ExpandsGlobPattern() + public void Context_Create_WithRequirementsPattern_ExpandsGlobPattern() { // Create test files var file1 = Path.Combine(_testDirectory, "req1.yaml"); @@ -525,7 +537,7 @@ public void Create_WithRequirementsPattern_ExpandsGlobPattern() /// Test tests glob pattern expansion. /// [TestMethod] - public void Create_WithTestsPattern_ExpandsGlobPattern() + public void Context_Create_WithTestsPattern_ExpandsGlobPattern() { // Create test files var file1 = Path.Combine(_testDirectory, "test1.trx"); @@ -556,7 +568,7 @@ public void Create_WithTestsPattern_ExpandsGlobPattern() /// Test missing requirements pattern argument. /// [TestMethod] - public void Create_MissingRequirementsPattern_ThrowsException() + public void Context_Create_MissingRequirementsPattern_ThrowsException() { try { @@ -573,7 +585,7 @@ public void Create_MissingRequirementsPattern_ThrowsException() /// Test missing tests pattern argument. /// [TestMethod] - public void Create_MissingTestsPattern_ThrowsException() + public void Context_Create_MissingTestsPattern_ThrowsException() { try { @@ -590,7 +602,7 @@ public void Create_MissingTestsPattern_ThrowsException() /// Test combining multiple arguments. /// [TestMethod] - public void Create_MultipleArguments_ParsesAllCorrectly() + public void Context_Create_MultipleArguments_ParsesAllCorrectly() { using var context = Context.Create( ["--version", "--help", "--silent", "--validate", "--report", "out.md", "--report-depth", "2"]); @@ -608,7 +620,7 @@ public void Create_MultipleArguments_ParsesAllCorrectly() /// Test dispose closes log file. /// [TestMethod] - public void Dispose_WithLogFile_ClosesLogFile() + public void Context_Dispose_WithLogFile_ClosesLogFile() { var logPath = Path.Combine(_testDirectory, "test.log"); @@ -625,7 +637,7 @@ public void Dispose_WithLogFile_ClosesLogFile() /// Test invalid log file path. /// [TestMethod] - public void Create_InvalidLogPath_ThrowsException() + public void Context_Create_InvalidLogPath_ThrowsException() { var invalidPath = Path.Combine(_testDirectory, "nonexistent", "test.log"); diff --git a/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs b/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs index 70671a8..f521867 100644 --- a/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/ProgramTests.cs @@ -54,7 +54,7 @@ public void TestCleanup() /// Test Run with version flag prints version information. /// [TestMethod] - public void Run_WithVersionFlag_PrintsVersion() + public void Program_Run_WithVersionFlag_PrintsVersion() { var originalOut = Console.Out; var output = new StringWriter(); @@ -81,7 +81,7 @@ public void Run_WithVersionFlag_PrintsVersion() /// Test Run with help flag prints help information. /// [TestMethod] - public void Run_WithHelpFlag_PrintsHelp() + public void Program_Run_WithHelpFlag_PrintsHelp() { var originalOut = Console.Out; var output = new StringWriter(); @@ -108,7 +108,7 @@ public void Run_WithHelpFlag_PrintsHelp() /// Test Run with validate flag shows placeholder message. /// [TestMethod] - public void Run_WithValidateFlag_ShowsPlaceholder() + public void Program_Run_WithValidateFlag_ShowsPlaceholder() { using var context = Context.Create(["--validate"]); Program.Run(context); @@ -121,7 +121,7 @@ public void Run_WithValidateFlag_ShowsPlaceholder() /// Test Run with no requirements files shows message. /// [TestMethod] - public void Run_WithNoRequirementsFiles_ShowsMessage() + public void Program_Run_WithNoRequirementsFiles_ShowsMessage() { using var context = Context.Create([]); Program.Run(context); @@ -134,7 +134,7 @@ public void Run_WithNoRequirementsFiles_ShowsMessage() /// Test Run with requirements files processes them successfully. /// [TestMethod] - public void Run_WithRequirementsFiles_ProcessesSuccessfully() + public void Program_Run_WithRequirementsFiles_ProcessesSuccessfully() { // Create a test requirements file var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); @@ -167,7 +167,7 @@ public void Run_WithRequirementsFiles_ProcessesSuccessfully() /// Test Run with requirements export generates report file. /// [TestMethod] - public void Run_WithRequirementsExport_GeneratesReport() + public void Program_Run_WithRequirementsExport_GeneratesReport() { // Create a test requirements file var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); @@ -207,7 +207,7 @@ public void Run_WithRequirementsExport_GeneratesReport() /// Test Run with trace matrix export generates matrix file. /// [TestMethod] - public void Run_WithTraceMatrixExport_GeneratesMatrix() + public void Program_Run_WithTraceMatrixExport_GeneratesMatrix() { // Create a test requirements file var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); @@ -262,7 +262,7 @@ public void Run_WithTraceMatrixExport_GeneratesMatrix() /// Test priority order: version takes precedence over help. /// [TestMethod] - public void Run_WithVersionAndHelp_ProcessesVersionFirst() + public void Program_Run_WithVersionAndHelp_ProcessesVersionFirst() { var originalOut = Console.Out; var output = new StringWriter(); @@ -289,7 +289,7 @@ public void Run_WithVersionAndHelp_ProcessesVersionFirst() /// Test priority order: help takes precedence over validate. /// [TestMethod] - public void Run_WithHelpAndValidate_ProcessesHelpFirst() + public void Program_Run_WithHelpAndValidate_ProcessesHelpFirst() { var originalOut = Console.Out; var output = new StringWriter(); @@ -309,4 +309,201 @@ public void Run_WithHelpAndValidate_ProcessesHelpFirst() Console.SetOut(originalOut); } } + + /// + /// Test enforcement with fully satisfied requirements succeeds. + /// + [TestMethod] + public void Program_Run_WithEnforcementAndFullySatisfiedRequirements_Succeeds() + { + // Create a test requirements file + var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); + File.WriteAllText(reqFile, @" +sections: + - title: Test Section + requirements: + - id: REQ-001 + title: Test Requirement + tests: + - TestMethod1 +"); + + // Create a test TRX file with passing test using TestResults library + var testResults = new DemaConsulting.TestResults.TestResults { Name = "TestRun" }; + testResults.Results.Add(new DemaConsulting.TestResults.TestResult + { + Name = "TestMethod1", + ClassName = "TestClass", + CodeBase = "Tests.dll", + Outcome = DemaConsulting.TestResults.TestOutcome.Passed, + Duration = TimeSpan.FromSeconds(1) + }); + + var trxFile = Path.Combine(_testDirectory, "tests.trx"); + File.WriteAllText(trxFile, DemaConsulting.TestResults.IO.TrxSerializer.Serialize(testResults)); + + // Save current directory and change to test directory + var originalDir = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(_testDirectory); + + using var context = Context.Create([ + "--requirements", "*.yaml", + "--tests", "*.trx", + "--enforce" + ]); + Program.Run(context); + + Assert.AreEqual(0, context.ExitCode); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + } + } + + /// + /// Test enforcement with unsatisfied requirements fails. + /// + [TestMethod] + public void Program_Run_WithEnforcementAndUnsatisfiedRequirements_Fails() + { + // Create a test requirements file with one tested and one untested requirement + var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); + File.WriteAllText(reqFile, @" +sections: + - title: Test Section + requirements: + - id: REQ-001 + title: Tested Requirement + tests: + - TestMethod1 + - id: REQ-002 + title: Untested Requirement +"); + + // Create a test TRX file with passing test using TestResults library + var testResults = new DemaConsulting.TestResults.TestResults { Name = "TestRun" }; + testResults.Results.Add(new DemaConsulting.TestResults.TestResult + { + Name = "TestMethod1", + ClassName = "TestClass", + CodeBase = "Tests.dll", + Outcome = DemaConsulting.TestResults.TestOutcome.Passed, + Duration = TimeSpan.FromSeconds(1) + }); + + var trxFile = Path.Combine(_testDirectory, "tests.trx"); + File.WriteAllText(trxFile, DemaConsulting.TestResults.IO.TrxSerializer.Serialize(testResults)); + + // Save current directory and change to test directory + var originalDir = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(_testDirectory); + + using var context = Context.Create([ + "--requirements", "*.yaml", + "--tests", "*.trx", + "--enforce" + ]); + Program.Run(context); + + Assert.AreEqual(1, context.ExitCode); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + } + } + + /// + /// Test enforcement without test files fails. + /// + [TestMethod] + public void Program_Run_WithEnforcementAndNoTests_Fails() + { + // Create a test requirements file + var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); + File.WriteAllText(reqFile, @" +sections: + - title: Test Section + requirements: + - id: REQ-001 + title: Test Requirement +"); + + // Save current directory and change to test directory + var originalDir = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(_testDirectory); + + using var context = Context.Create([ + "--requirements", "*.yaml", + "--enforce" + ]); + Program.Run(context); + + Assert.AreEqual(1, context.ExitCode); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + } + } + + /// + /// Test enforcement with failed tests fails. + /// + [TestMethod] + public void Program_Run_WithEnforcementAndFailedTests_Fails() + { + // Create a test requirements file + var reqFile = Path.Combine(_testDirectory, "requirements.yaml"); + File.WriteAllText(reqFile, @" +sections: + - title: Test Section + requirements: + - id: REQ-001 + title: Test Requirement + tests: + - TestMethod1 +"); + + // Create a test TRX file with failing test using TestResults library + var testResults = new DemaConsulting.TestResults.TestResults { Name = "TestRun" }; + testResults.Results.Add(new DemaConsulting.TestResults.TestResult + { + Name = "TestMethod1", + ClassName = "TestClass", + CodeBase = "Tests.dll", + Outcome = DemaConsulting.TestResults.TestOutcome.Failed, + Duration = TimeSpan.FromSeconds(1) + }); + + var trxFile = Path.Combine(_testDirectory, "tests.trx"); + File.WriteAllText(trxFile, DemaConsulting.TestResults.IO.TrxSerializer.Serialize(testResults)); + + // Save current directory and change to test directory + var originalDir = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(_testDirectory); + + using var context = Context.Create([ + "--requirements", "*.yaml", + "--tests", "*.trx", + "--enforce" + ]); + Program.Run(context); + + Assert.AreEqual(1, context.ExitCode); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + } + } } diff --git a/test/DemaConsulting.ReqStream.Tests/RequirementsExportTests.cs b/test/DemaConsulting.ReqStream.Tests/RequirementsExportTests.cs index afdbca2..32795a3 100644 --- a/test/DemaConsulting.ReqStream.Tests/RequirementsExportTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/RequirementsExportTests.cs @@ -54,7 +54,7 @@ public void TestCleanup() /// Test exporting a simple requirements document to Markdown. /// [TestMethod] - public void Export_SimpleRequirements_CreatesMarkdownFile() + public void Requirements_Export_SimpleRequirements_CreatesMarkdownFile() { var yamlContent = @"--- sections: @@ -84,7 +84,7 @@ public void Export_SimpleRequirements_CreatesMarkdownFile() /// Test exporting requirements with custom depth. /// [TestMethod] - public void Export_WithCustomDepth_UsesCorrectHeaderLevel() + public void Requirements_Export_WithCustomDepth_UsesCorrectHeaderLevel() { var yamlContent = @"--- sections: @@ -108,7 +108,7 @@ public void Export_WithCustomDepth_UsesCorrectHeaderLevel() /// Test exporting nested sections with proper hierarchy. /// [TestMethod] - public void Export_NestedSections_CreatesHierarchy() + public void Requirements_Export_NestedSections_CreatesHierarchy() { var yamlContent = @"--- sections: @@ -142,7 +142,7 @@ public void Export_NestedSections_CreatesHierarchy() /// Test exporting a section with no requirements (only subsections). /// [TestMethod] - public void Export_SectionWithNoRequirements_CreatesHeaderOnly() + public void Requirements_Export_SectionWithNoRequirements_CreatesHeaderOnly() { var yamlContent = @"--- sections: @@ -170,7 +170,7 @@ public void Export_SectionWithNoRequirements_CreatesHeaderOnly() /// Test that export throws exception when file path is null. /// [TestMethod] - public void Export_NullFilePath_ThrowsArgumentException() + public void Requirements_Export_NullFilePath_ThrowsArgumentException() { var yamlContent = @"--- sections: @@ -198,7 +198,7 @@ public void Export_NullFilePath_ThrowsArgumentException() /// Test that export throws exception when file path is empty. /// [TestMethod] - public void Export_EmptyFilePath_ThrowsArgumentException() + public void Requirements_Export_EmptyFilePath_ThrowsArgumentException() { var yamlContent = @"--- sections: @@ -226,7 +226,7 @@ public void Export_EmptyFilePath_ThrowsArgumentException() /// Test exporting multiple sections at the root level. /// [TestMethod] - public void Export_MultipleSections_ExportsAll() + public void Requirements_Export_MultipleSections_ExportsAll() { var yamlContent = @"--- sections: @@ -257,7 +257,7 @@ public void Export_MultipleSections_ExportsAll() /// Test exporting empty requirements document. /// [TestMethod] - public void Export_EmptyRequirements_CreatesEmptyFile() + public void Requirements_Export_EmptyRequirements_CreatesEmptyFile() { var yamlContent = @"--- "; diff --git a/test/DemaConsulting.ReqStream.Tests/RequirementsParsingTests.cs b/test/DemaConsulting.ReqStream.Tests/RequirementsParsingTests.cs index acf1d6e..935e40d 100644 --- a/test/DemaConsulting.ReqStream.Tests/RequirementsParsingTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/RequirementsParsingTests.cs @@ -54,7 +54,7 @@ public void TestCleanup() /// Test reading a simple YAML file with a single requirement. /// [TestMethod] - public void Read_SimpleRequirement_ParsesCorrectly() + public void Requirements_Read_SimpleRequirement_ParsesCorrectly() { var yamlContent = @"--- sections: @@ -80,7 +80,7 @@ public void Read_SimpleRequirement_ParsesCorrectly() /// Test reading a requirement with tests. /// [TestMethod] - public void Read_RequirementWithTests_ParsesTestsCorrectly() + public void Requirements_Read_RequirementWithTests_ParsesTestsCorrectly() { var yamlContent = @"--- sections: @@ -111,7 +111,7 @@ public void Read_RequirementWithTests_ParsesTestsCorrectly() /// Test reading a requirement with child requirements. /// [TestMethod] - public void Read_RequirementWithChildren_ParsesChildrenCorrectly() + public void Requirements_Read_RequirementWithChildren_ParsesChildrenCorrectly() { var yamlContent = @"--- sections: @@ -140,7 +140,7 @@ public void Read_RequirementWithChildren_ParsesChildrenCorrectly() /// Test reading nested sections. /// [TestMethod] - public void Read_NestedSections_ParsesHierarchyCorrectly() + public void Requirements_Read_NestedSections_ParsesHierarchyCorrectly() { var yamlContent = @"--- sections: @@ -174,7 +174,7 @@ public void Read_NestedSections_ParsesHierarchyCorrectly() /// Test reading test mappings that are separate from requirements. /// [TestMethod] - public void Read_TestMappings_AppliesMappingsCorrectly() + public void Requirements_Read_TestMappings_AppliesMappingsCorrectly() { var yamlContent = @"--- sections: @@ -206,7 +206,7 @@ public void Read_TestMappings_AppliesMappingsCorrectly() /// Test reading a file with includes. /// [TestMethod] - public void Read_WithIncludes_MergesFilesCorrectly() + public void Requirements_Read_WithIncludes_MergesFilesCorrectly() { var mainYaml = @"--- sections: @@ -244,7 +244,7 @@ public void Read_WithIncludes_MergesFilesCorrectly() /// Test that identical sections are merged. /// [TestMethod] - public void Read_IdenticalSections_MergesCorrectly() + public void Requirements_Read_IdenticalSections_MergesCorrectly() { var mainYaml = @"--- sections: @@ -282,7 +282,7 @@ public void Read_IdenticalSections_MergesCorrectly() /// Test that duplicate requirement IDs throw an exception. /// [TestMethod] - public void Read_DuplicateRequirementId_ThrowsException() + public void Requirements_Read_DuplicateRequirementId_ThrowsException() { var yamlContent = @"--- sections: @@ -312,7 +312,7 @@ public void Read_DuplicateRequirementId_ThrowsException() /// Test that include loops are prevented. /// [TestMethod] - public void Read_IncludeLoop_DoesNotCauseInfiniteLoop() + public void Requirements_Read_IncludeLoop_DoesNotCauseInfiniteLoop() { var fileA = @"--- sections: @@ -349,7 +349,7 @@ public void Read_IncludeLoop_DoesNotCauseInfiniteLoop() /// Test that file not found throws an exception. /// [TestMethod] - public void Read_FileNotFound_ThrowsException() + public void Requirements_Read_FileNotFound_ThrowsException() { var nonExistentPath = Path.Combine(_testDirectory, "nonexistent.yaml"); @@ -368,7 +368,7 @@ public void Read_FileNotFound_ThrowsException() /// Test reading an empty YAML file. /// [TestMethod] - public void Read_EmptyFile_ReturnsEmptyRequirements() + public void Requirements_Read_EmptyFile_ReturnsEmptyRequirements() { var yamlContent = @"--- "; @@ -386,7 +386,7 @@ public void Read_EmptyFile_ReturnsEmptyRequirements() /// Test reading a complex nested structure. /// [TestMethod] - public void Read_ComplexStructure_ParsesCorrectly() + public void Requirements_Read_ComplexStructure_ParsesCorrectly() { var yamlContent = @"--- sections: @@ -454,7 +454,7 @@ public void Read_ComplexStructure_ParsesCorrectly() /// Test that blank requirement ID throws an exception with file location. /// [TestMethod] - public void Read_BlankRequirementId_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankRequirementId_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -483,7 +483,7 @@ public void Read_BlankRequirementId_ThrowsExceptionWithFileLocation() /// Test that blank requirement title throws an exception with file location. /// [TestMethod] - public void Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -512,7 +512,7 @@ public void Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation() /// Test that blank section title throws an exception with file location. /// [TestMethod] - public void Read_BlankSectionTitle_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankSectionTitle_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -540,7 +540,7 @@ public void Read_BlankSectionTitle_ThrowsExceptionWithFileLocation() /// Test that blank test name in requirement throws an exception with file location. /// [TestMethod] - public void Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -573,7 +573,7 @@ public void Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation() /// Test that blank test name in mapping throws an exception with file location. /// [TestMethod] - public void Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -608,7 +608,7 @@ public void Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation() /// Test that blank mapping ID throws an exception with file location. /// [TestMethod] - public void Read_BlankMappingId_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankMappingId_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -641,7 +641,7 @@ public void Read_BlankMappingId_ThrowsExceptionWithFileLocation() /// Test that duplicate requirement ID message includes file location. /// [TestMethod] - public void Read_DuplicateRequirementId_ExceptionIncludesFileLocation() + public void Requirements_Read_DuplicateRequirementId_ExceptionIncludesFileLocation() { var yamlContent = @"--- sections: @@ -672,7 +672,7 @@ public void Read_DuplicateRequirementId_ExceptionIncludesFileLocation() /// Test reading multiple files with params array. /// [TestMethod] - public void Read_MultipleFiles_MergesAllFiles() + public void Requirements_Read_MultipleFiles_MergesAllFiles() { var file1Yaml = @"--- sections: @@ -718,7 +718,7 @@ public void Read_MultipleFiles_MergesAllFiles() /// Test reading multiple files that merge sections. /// [TestMethod] - public void Read_MultipleFilesWithSameSections_MergesSections() + public void Requirements_Read_MultipleFilesWithSameSections_MergesSections() { var file1Yaml = @"--- sections: @@ -753,7 +753,7 @@ public void Read_MultipleFilesWithSameSections_MergesSections() /// Test reading single file with params array (backwards compatibility). /// [TestMethod] - public void Read_SingleFileWithParamsArray_WorksCorrectly() + public void Requirements_Read_SingleFileWithParamsArray_WorksCorrectly() { var yamlContent = @"--- sections: @@ -778,7 +778,7 @@ public void Read_SingleFileWithParamsArray_WorksCorrectly() /// Test that calling Read with no arguments throws ArgumentException. /// [TestMethod] - public void Read_NoArguments_ThrowsArgumentException() + public void Requirements_Read_NoArguments_ThrowsArgumentException() { try { @@ -795,7 +795,7 @@ public void Read_NoArguments_ThrowsArgumentException() /// Test that calling Read with null throws ArgumentException. /// [TestMethod] - public void Read_NullArgument_ThrowsArgumentException() + public void Requirements_Read_NullArgument_ThrowsArgumentException() { try { @@ -812,7 +812,7 @@ public void Read_NullArgument_ThrowsArgumentException() /// Test that duplicate IDs across multiple files are detected. /// [TestMethod] - public void Read_MultipleFilesWithDuplicateIds_ThrowsException() + public void Requirements_Read_MultipleFilesWithDuplicateIds_ThrowsException() { var file1Yaml = @"--- sections: diff --git a/test/DemaConsulting.ReqStream.Tests/RequirementsReadTests.cs b/test/DemaConsulting.ReqStream.Tests/RequirementsReadTests.cs index 7735240..a2b2737 100644 --- a/test/DemaConsulting.ReqStream.Tests/RequirementsReadTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/RequirementsReadTests.cs @@ -54,7 +54,7 @@ public void TestCleanup() /// Test reading a simple YAML file with a single requirement. /// [TestMethod] - public void Read_SimpleRequirement_ParsesCorrectly() + public void Requirements_Read_SimpleRequirement_ParsesCorrectly() { var yamlContent = @"--- sections: @@ -80,7 +80,7 @@ public void Read_SimpleRequirement_ParsesCorrectly() /// Test reading a requirement with tests. /// [TestMethod] - public void Read_RequirementWithTests_ParsesTestsCorrectly() + public void Requirements_Read_RequirementWithTests_ParsesTestsCorrectly() { var yamlContent = @"--- sections: @@ -111,7 +111,7 @@ public void Read_RequirementWithTests_ParsesTestsCorrectly() /// Test reading a requirement with child requirements. /// [TestMethod] - public void Read_RequirementWithChildren_ParsesChildrenCorrectly() + public void Requirements_Read_RequirementWithChildren_ParsesChildrenCorrectly() { var yamlContent = @"--- sections: @@ -140,7 +140,7 @@ public void Read_RequirementWithChildren_ParsesChildrenCorrectly() /// Test reading nested sections. /// [TestMethod] - public void Read_NestedSections_ParsesHierarchyCorrectly() + public void Requirements_Read_NestedSections_ParsesHierarchyCorrectly() { var yamlContent = @"--- sections: @@ -174,7 +174,7 @@ public void Read_NestedSections_ParsesHierarchyCorrectly() /// Test reading test mappings that are separate from requirements. /// [TestMethod] - public void Read_TestMappings_AppliesMappingsCorrectly() + public void Requirements_Read_TestMappings_AppliesMappingsCorrectly() { var yamlContent = @"--- sections: @@ -206,7 +206,7 @@ public void Read_TestMappings_AppliesMappingsCorrectly() /// Test reading a file with includes. /// [TestMethod] - public void Read_WithIncludes_MergesFilesCorrectly() + public void Requirements_Read_WithIncludes_MergesFilesCorrectly() { var mainYaml = @"--- sections: @@ -244,7 +244,7 @@ public void Read_WithIncludes_MergesFilesCorrectly() /// Test that identical sections are merged. /// [TestMethod] - public void Read_IdenticalSections_MergesCorrectly() + public void Requirements_Read_IdenticalSections_MergesCorrectly() { var mainYaml = @"--- sections: @@ -282,7 +282,7 @@ public void Read_IdenticalSections_MergesCorrectly() /// Test that duplicate requirement IDs throw an exception. /// [TestMethod] - public void Read_DuplicateRequirementId_ThrowsException() + public void Requirements_Read_DuplicateRequirementId_ThrowsException() { var yamlContent = @"--- sections: @@ -312,7 +312,7 @@ public void Read_DuplicateRequirementId_ThrowsException() /// Test that include loops are prevented. /// [TestMethod] - public void Read_IncludeLoop_DoesNotCauseInfiniteLoop() + public void Requirements_Read_IncludeLoop_DoesNotCauseInfiniteLoop() { var fileA = @"--- sections: @@ -349,7 +349,7 @@ public void Read_IncludeLoop_DoesNotCauseInfiniteLoop() /// Test that file not found throws an exception. /// [TestMethod] - public void Read_FileNotFound_ThrowsException() + public void Requirements_Read_FileNotFound_ThrowsException() { var nonExistentPath = Path.Combine(_testDirectory, "nonexistent.yaml"); @@ -368,7 +368,7 @@ public void Read_FileNotFound_ThrowsException() /// Test reading an empty YAML file. /// [TestMethod] - public void Read_EmptyFile_ReturnsEmptyRequirements() + public void Requirements_Read_EmptyFile_ReturnsEmptyRequirements() { var yamlContent = @"--- "; @@ -386,7 +386,7 @@ public void Read_EmptyFile_ReturnsEmptyRequirements() /// Test reading a complex nested structure. /// [TestMethod] - public void Read_ComplexStructure_ParsesCorrectly() + public void Requirements_Read_ComplexStructure_ParsesCorrectly() { var yamlContent = @"--- sections: @@ -454,7 +454,7 @@ public void Read_ComplexStructure_ParsesCorrectly() /// Test that blank requirement ID throws an exception with file location. /// [TestMethod] - public void Read_BlankRequirementId_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankRequirementId_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -483,7 +483,7 @@ public void Read_BlankRequirementId_ThrowsExceptionWithFileLocation() /// Test that blank requirement title throws an exception with file location. /// [TestMethod] - public void Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -512,7 +512,7 @@ public void Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation() /// Test that blank section title throws an exception with file location. /// [TestMethod] - public void Read_BlankSectionTitle_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankSectionTitle_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -540,7 +540,7 @@ public void Read_BlankSectionTitle_ThrowsExceptionWithFileLocation() /// Test that blank test name in requirement throws an exception with file location. /// [TestMethod] - public void Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -573,7 +573,7 @@ public void Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation() /// Test that blank test name in mapping throws an exception with file location. /// [TestMethod] - public void Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -608,7 +608,7 @@ public void Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation() /// Test that blank mapping ID throws an exception with file location. /// [TestMethod] - public void Read_BlankMappingId_ThrowsExceptionWithFileLocation() + public void Requirements_Read_BlankMappingId_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -641,7 +641,7 @@ public void Read_BlankMappingId_ThrowsExceptionWithFileLocation() /// Test that duplicate requirement ID message includes file location. /// [TestMethod] - public void Read_DuplicateRequirementId_ExceptionIncludesFileLocation() + public void Requirements_Read_DuplicateRequirementId_ExceptionIncludesFileLocation() { var yamlContent = @"--- sections: @@ -672,7 +672,7 @@ public void Read_DuplicateRequirementId_ExceptionIncludesFileLocation() /// Test reading multiple files with params array. /// [TestMethod] - public void Read_MultipleFiles_MergesAllFiles() + public void Requirements_Read_MultipleFiles_MergesAllFiles() { var file1Yaml = @"--- sections: @@ -718,7 +718,7 @@ public void Read_MultipleFiles_MergesAllFiles() /// Test reading multiple files that merge sections. /// [TestMethod] - public void Read_MultipleFilesWithSameSections_MergesSections() + public void Requirements_Read_MultipleFilesWithSameSections_MergesSections() { var file1Yaml = @"--- sections: @@ -753,7 +753,7 @@ public void Read_MultipleFilesWithSameSections_MergesSections() /// Test reading single file with params array (backwards compatibility). /// [TestMethod] - public void Read_SingleFileWithParamsArray_WorksCorrectly() + public void Requirements_Read_SingleFileWithParamsArray_WorksCorrectly() { var yamlContent = @"--- sections: @@ -778,7 +778,7 @@ public void Read_SingleFileWithParamsArray_WorksCorrectly() /// Test that calling Read with no arguments throws ArgumentException. /// [TestMethod] - public void Read_NoArguments_ThrowsArgumentException() + public void Requirements_Read_NoArguments_ThrowsArgumentException() { try { @@ -795,7 +795,7 @@ public void Read_NoArguments_ThrowsArgumentException() /// Test that calling Read with null throws ArgumentException. /// [TestMethod] - public void Read_NullArgument_ThrowsArgumentException() + public void Requirements_Read_NullArgument_ThrowsArgumentException() { try { @@ -812,7 +812,7 @@ public void Read_NullArgument_ThrowsArgumentException() /// Test that duplicate IDs across multiple files are detected. /// [TestMethod] - public void Read_MultipleFilesWithDuplicateIds_ThrowsException() + public void Requirements_Read_MultipleFilesWithDuplicateIds_ThrowsException() { var file1Yaml = @"--- sections: