diff --git a/docs/design/modeling/requirements.md b/docs/design/modeling/requirements.md index 4fcafc9..49f548e 100644 --- a/docs/design/modeling/requirements.md +++ b/docs/design/modeling/requirements.md @@ -2,10 +2,10 @@ ## Overview -The three classes `Requirements`, `Section`, and `Requirement` together form the domain model for -requirement data in ReqStream. They are responsible for reading YAML files, merging hierarchical -section trees, validating data integrity, preventing infinite include loops and circular child -references, applying test mappings, and exporting content to Markdown reports. +The classes `Requirements`, `Section`, `Requirement`, and `LoadResult` together form the domain +model for requirement data in ReqStream. They are responsible for loading YAML files, merging +hierarchical section trees, validating data integrity, preventing infinite include loops and circular +child references, applying test mappings, and exporting content to Markdown reports. ## Data Model @@ -34,14 +34,21 @@ references, applying test mappings, and exporting content to Markdown reports. ### `Requirements` -`Requirements` extends `Section` and acts as the root of the tree. In addition to the properties -inherited from `Section`, it maintains two private fields that span the lifetime of a load -operation: +`Requirements` extends `Section` and acts as the root of the tree. -| Field | Type | Purpose | -| ----- | ---- | ------- | -| `_includedFiles` | `HashSet` | Absolute paths of files already processed; prevents infinite include loops | -| `_allRequirements` | `Dictionary` | Maps requirement ID to `Requirement`; detects duplicates | +### `LoadResult` + +`LoadResult` encapsulates the outcome of a `Requirements.Load` call. + +| Member | Type | Notes | +| ------ | ---- | ----- | +| `Requirements` | `Requirements?` | Parsed tree; `null` when error-level issues are present | +| `Issues` | `IReadOnlyList` | All lint issues collected during loading | +| `HasErrors` | `bool` | `true` when any issue has `LintSeverity.Error` | +| `ReportIssues(context)` | `void` | Routes each issue to the context output | + +`ReportIssues` accepts a `Context` argument. Warning-level issues are sent to `context.WriteLine`; +error-level issues are sent to `context.WriteError`. ## YAML Intermediate Types @@ -55,34 +62,38 @@ YAML is deserialized into a set of intermediate types using `YamlDotNet` with th | `YamlRequirement` | `requirements[]` entries | Contains `id`, `title`, `justification`, `tests`, `children`, `tags` | | `YamlMapping` | `mappings[]` entries | Contains `id`, `tests` | -These intermediate types are discarded after `ReadFile` completes; the resulting `Requirement`, +These intermediate types are discarded after `LoadFile` completes; the resulting `Requirement`, `Section`, and `Requirements` objects are the only long-lived representations. ## Methods -### `Requirements.Read(paths)` +### `Requirements.Load(paths)` + +`Load` is the single static factory method on `Requirements`. It delegates to +`RequirementsLoader.Load` and returns a `LoadResult` containing: + +- The parsed `Requirements` tree (or `null` if any error-level issues were found), and +- The complete list of `LintIssue` objects collected during the walk. -`Read` is the static factory method that constructs and returns a fully loaded `Requirements` -instance. It calls `ReadFile` for each supplied path to merge content into the tree, then calls -`ValidateCycles()` to confirm the child-requirement graph is acyclic before returning. +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)`. -### `ReadFile(path)` +### `LoadFile(path)` -`ReadFile` loads a single YAML file and merges its content into the `Requirements` tree. Four +`LoadFile` loads a single YAML file and merges its content into the `Requirements` tree. Four design points govern its behavior: -- **Deduplication**: `path` is normalized to an absolute path and checked against `_includedFiles` +- **Deduplication**: `path` is normalized to an absolute path and checked against `visitedFiles` before any work is done. If already present, the method returns immediately. This prevents infinite loops when files include each other directly or transitively. - **YAML deserialization**: the file text is deserialized into a `YamlDocument` using `YamlDotNet` with `HyphenatedNamingConvention`. An empty or `null` document is silently accepted. - **Validation and merging**: each section is validated (title must not be blank) and each requirement is validated (ID and title must not be blank; ID must not duplicate an entry already - in `_allRequirements`). Validated sections are merged into the tree via `MergeSection`. Mapping - entries append additional test IDs to already-registered requirements. + seen). Validated sections are merged into the tree via `MergeSection`. Mapping entries append + additional test IDs to already-registered requirements. - **Recursive includes**: each path in the document's `includes` block is resolved relative to the - current file's directory and passed to `ReadFile` recursively, enabling modular file - organization. + current file's directory and passed to `LoadFile` recursively, enabling modular file organization. ### `MergeSection(parent, yamlSection)` @@ -111,10 +122,10 @@ references. It is called once after all files are loaded. **Algorithm** (per requirement): 1. If the ID is in `visited`, return immediately. -2. If the ID is in `visiting`, a cycle is detected; throw `InvalidOperationException` with the - cycle path formatted as `REQ-A -> REQ-B -> ... -> REQ-A`. +2. If the ID is in `visiting`, a cycle is detected; add an error `LintIssue` with the cycle path + formatted as `REQ-A -> REQ-B -> ... -> REQ-A`. 3. Add the ID to `visiting` and `path`. -4. Recurse into each child ID present in `_allRequirements`. +4. Recurse into each child ID present in `allRequirements`. 5. Remove the ID from `visiting` and `path`; add it to `visited`. Because `ValidateCycles` runs before any downstream analysis, `TraceMatrix.CollectAllTests` can @@ -141,16 +152,18 @@ matching tag are included in the output. | Test name | Blank entry in `tests` list | `Test name cannot be blank` | | Mapping ID | Blank | `Mapping requirement ID cannot be blank` | -All validation errors throw `InvalidOperationException` and include the source file path for -actionable debugging. +All validation errors are reported as `LintSeverity.Error` `LintIssue` objects and include the +source file path for actionable debugging. When any error-level issue is present, `LoadResult.Requirements` +is `null` and `LoadResult.HasErrors` returns `true`. ## Interactions with Other Units | Unit | Nature of interaction | | ---- | --------------------- | -| `Program` | Calls `Requirements.Read`; passes file paths from `Context.RequirementsFiles` | +| `Program` | Calls `Requirements.Load`; passes file paths from `Context.RequirementsFiles`; | +| | calls `result.ReportIssues(context)` | | `TraceMatrix` | Receives the populated `Requirements` root and iterates the tree | -| `Validation` | Exercises `Requirements.Read` with fixture YAML files in tests | +| `Validation` | Exercises `Requirements.Load` with fixture YAML files in tests | ## References diff --git a/docs/reqstream/modeling/requirements.yaml b/docs/reqstream/modeling/requirements.yaml index f70df1b..a5d8ae1 100644 --- a/docs/reqstream/modeling/requirements.yaml +++ b/docs/reqstream/modeling/requirements.yaml @@ -12,9 +12,9 @@ sections: tags: - requirements tests: - - Requirements_Read_SimpleRequirement_ParsesCorrectly + - Requirements_Load_SimpleRequirement_ParsesCorrectly - Program_Run_WithRequirementsFiles_ProcessesSuccessfully - - Requirements_Read_ComplexStructure_ParsesCorrectly + - Requirements_Load_ComplexStructure_ParsesCorrectly - id: ReqStream-Req-Validation title: The tool shall validate requirements file structure. @@ -24,11 +24,11 @@ sections: tags: - requirements tests: - - Requirements_Read_BlankSectionTitle_ThrowsExceptionWithFileLocation - - Requirements_Read_BlankRequirementId_ThrowsExceptionWithFileLocation - - Requirements_Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation - - Requirements_Read_DuplicateRequirementId_ThrowsException - - Requirements_Read_DuplicateRequirementId_ExceptionIncludesFileLocation + - Requirements_Load_BlankSectionTitle_ThrowsExceptionWithFileLocation + - Requirements_Load_BlankRequirementId_ThrowsExceptionWithFileLocation + - Requirements_Load_BlankRequirementTitle_ThrowsExceptionWithFileLocation + - Requirements_Load_DuplicateRequirementId_ThrowsException + - Requirements_Load_DuplicateRequirementId_ExceptionIncludesFileLocation - id: ReqStream-Req-YamlErrorReporting title: The tool shall report YAML deserialization errors with the filename and location. @@ -38,7 +38,7 @@ sections: tags: - requirements tests: - - Requirements_Read_InvalidYamlContent_ThrowsExceptionWithFileLocation + - Requirements_Load_InvalidYamlContent_ThrowsExceptionWithFileLocation - id: ReqStream-Req-Hierarchy title: The tool shall support hierarchical sections and subsections. @@ -48,7 +48,7 @@ sections: tags: - requirements tests: - - Requirements_Read_NestedSections_ParsesHierarchyCorrectly + - Requirements_Load_NestedSections_ParsesHierarchyCorrectly - Requirements_Export_NestedSections_CreatesHierarchy - id: ReqStream-Req-Includes @@ -59,9 +59,9 @@ sections: tags: - requirements tests: - - Requirements_Read_WithIncludes_MergesFilesCorrectly - - Requirements_Read_MultipleFiles_MergesAllFiles - - Requirements_Read_IncludeLoop_DoesNotCauseInfiniteLoop + - Requirements_Load_WithIncludes_MergesFilesCorrectly + - Requirements_Load_MultipleFiles_MergesAllFiles + - Requirements_Load_IncludeLoop_DoesNotCauseInfiniteLoop - id: ReqStream-Req-SectionMerging title: The tool shall merge sections with the same hierarchy path. @@ -71,8 +71,8 @@ sections: tags: - requirements tests: - - Requirements_Read_IdenticalSections_MergesCorrectly - - Requirements_Read_MultipleFilesWithSameSections_MergesSections + - Requirements_Load_IdenticalSections_MergesCorrectly + - Requirements_Load_MultipleFilesWithSameSections_MergesSections - title: Requirements Definition requirements: @@ -84,9 +84,9 @@ sections: tags: - requirements tests: - - Requirements_Read_DuplicateRequirementId_ThrowsException - - Requirements_Read_BlankRequirementId_ThrowsExceptionWithFileLocation - - Requirements_Read_MultipleFilesWithDuplicateIds_ThrowsException + - Requirements_Load_DuplicateRequirementId_ThrowsException + - Requirements_Load_BlankRequirementId_ThrowsExceptionWithFileLocation + - Requirements_Load_MultipleFilesWithDuplicateIds_ThrowsException - id: ReqStream-Req-RequiredTitle title: The tool shall require each requirement to have a title. @@ -96,7 +96,7 @@ sections: tags: - requirements tests: - - Requirements_Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation + - Requirements_Load_BlankRequirementTitle_ThrowsExceptionWithFileLocation - id: ReqStream-Req-ParentChild title: The tool shall support parent-child relationships between requirements. @@ -106,7 +106,7 @@ sections: tags: - requirements tests: - - Requirements_Read_RequirementWithChildren_ParsesChildrenCorrectly + - Requirements_Load_RequirementWithChildren_ParsesChildrenCorrectly - TraceMatrix_Export_WithChildRequirements_ConsidersChildTests - id: ReqStream-Req-TestMappings @@ -117,11 +117,11 @@ sections: tags: - requirements tests: - - Requirements_Read_RequirementWithTests_ParsesTestsCorrectly - - Requirements_Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation - - Requirements_Read_TestMappings_AppliesMappingsCorrectly - - Requirements_Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation - - Requirements_Read_BlankMappingId_ThrowsExceptionWithFileLocation + - Requirements_Load_RequirementWithTests_ParsesTestsCorrectly + - Requirements_Load_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation + - Requirements_Load_TestMappings_AppliesMappingsCorrectly + - Requirements_Load_BlankTestNameInMapping_ThrowsExceptionWithFileLocation + - Requirements_Load_BlankMappingId_ThrowsExceptionWithFileLocation - id: ReqStream-Req-UnifiedLoad title: >- diff --git a/docs/reqstream/ots/mstest.yaml b/docs/reqstream/ots/mstest.yaml index 3fafcf4..b13b734 100644 --- a/docs/reqstream/ots/mstest.yaml +++ b/docs/reqstream/ots/mstest.yaml @@ -18,6 +18,6 @@ sections: - Context_Create_HelpFlags_SetsHelpProperty - Context_Create_ValidateFlag_SetsValidateProperty - Context_Create_EnforceFlag_SetsEnforceProperty - - Requirements_Read_SimpleRequirement_ParsesCorrectly + - Requirements_Load_SimpleRequirement_ParsesCorrectly - Program_Run_WithVersionFlag_PrintsVersion - Program_Run_WithHelpFlag_PrintsHelp diff --git a/src/DemaConsulting.ReqStream/Modeling/LoadResult.cs b/src/DemaConsulting.ReqStream/Modeling/LoadResult.cs new file mode 100644 index 0000000..01c1851 --- /dev/null +++ b/src/DemaConsulting.ReqStream/Modeling/LoadResult.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2026 DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.ReqStream.Cli; + +namespace DemaConsulting.ReqStream.Modeling; + +/// +/// Encapsulates the result of loading one or more requirements YAML files, including the +/// parsed requirements tree and any lint issues found during loading. +/// +public sealed class LoadResult +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The parsed requirements tree, or null when error-level issues prevent successful loading. + /// + /// The read-only list of lint issues found during loading. + internal LoadResult(Requirements? requirements, IReadOnlyList issues) + { + Requirements = requirements; + Issues = issues; + } + + /// + /// Gets the parsed requirements tree, or null when error-level issues are present. + /// + public Requirements? Requirements { get; } + + /// + /// Gets the read-only list of lint issues found during loading. + /// + public IReadOnlyList Issues { get; } + + /// + /// Gets a value indicating whether any error-level lint issues were found during loading. + /// + public bool HasErrors => Issues.Any(i => i.Severity == LintSeverity.Error); + + /// + /// Reports all lint issues to the supplied context. + /// Warning-level issues are sent to ; + /// error-level issues are sent to . + /// + /// The context to report issues to. + public void ReportIssues(Context context) + { + foreach (var issue in Issues) + { + if (issue.Severity == LintSeverity.Error) + { + context.WriteError(issue.ToString()); + } + else + { + context.WriteLine(issue.ToString()); + } + } + } +} diff --git a/src/DemaConsulting.ReqStream/Modeling/Requirements.cs b/src/DemaConsulting.ReqStream/Modeling/Requirements.cs index 490b363..ab823a3 100644 --- a/src/DemaConsulting.ReqStream/Modeling/Requirements.cs +++ b/src/DemaConsulting.ReqStream/Modeling/Requirements.cs @@ -25,38 +25,17 @@ namespace DemaConsulting.ReqStream.Modeling; /// public class Requirements : Section { - /// - /// Reads one or more requirements YAML files and returns the parsed Requirements object. - /// Throws an exception if any error-level issues are found during loading. - /// - /// One or more paths to YAML files to read. - /// A Requirements object containing the parsed requirements from all files. - /// Thrown when no paths are provided. - /// Thrown when any error-level issue is found during loading. - public static Requirements Read(params string[] paths) - { - var (requirements, issues) = Load(paths); - if (requirements != null) - { - return requirements; - } - - // Throw an exception conveying the first error-level issue - var firstError = issues.First(i => i.Severity == LintSeverity.Error); - throw new InvalidOperationException(firstError.ToString()); - } - /// /// Loads one or more requirements YAML files using a single YAML DOM tree walk that /// simultaneously builds the requirements model and collects lint issues. /// /// One or more paths to YAML files to load. /// - /// A tuple of the parsed (or null when error-level issues - /// are present) and a read-only list of objects. + /// A containing the parsed (or null + /// when error-level issues are present) and all lint issues found during loading. /// /// Thrown when no paths are provided. - public static (Requirements? Requirements, IReadOnlyList Issues) Load(params string[] paths) + public static LoadResult Load(params string[] paths) { return RequirementsLoader.Load(paths); } diff --git a/src/DemaConsulting.ReqStream/Modeling/RequirementsLoader.cs b/src/DemaConsulting.ReqStream/Modeling/RequirementsLoader.cs index bea7fd7..5528198 100644 --- a/src/DemaConsulting.ReqStream/Modeling/RequirementsLoader.cs +++ b/src/DemaConsulting.ReqStream/Modeling/RequirementsLoader.cs @@ -59,11 +59,12 @@ internal static class RequirementsLoader /// /// One or more paths to YAML files to load. /// - /// A tuple of the parsed (or null when error-level issues - /// are present) and a read-only list of objects describing all issues found. + /// A containing the parsed (or null + /// when error-level issues are present) and a read-only list of objects + /// describing all issues found. /// /// Thrown when no paths are provided. - internal static (Requirements? Requirements, IReadOnlyList Issues) Load(string[] paths) + internal static LoadResult Load(string[] paths) { if (paths == null || paths.Length == 0) { @@ -95,9 +96,8 @@ internal static (Requirements? Requirements, IReadOnlyList Issues) Lo } // Return null requirements if any error-level issues were found - return issues.Any(i => i.Severity == LintSeverity.Error) - ? (null, issues) - : (requirements, issues); + var hasErrors = issues.Any(i => i.Severity == LintSeverity.Error); + return new LoadResult(hasErrors ? null : requirements, issues); } /// diff --git a/src/DemaConsulting.ReqStream/Program.cs b/src/DemaConsulting.ReqStream/Program.cs index fbdd8b2..696608c 100644 --- a/src/DemaConsulting.ReqStream/Program.cs +++ b/src/DemaConsulting.ReqStream/Program.cs @@ -122,13 +122,10 @@ public static void Run(Context context) return; } - var (_, lintIssues) = Requirements.Load(context.RequirementsFiles.ToArray()); - foreach (var issue in lintIssues) - { - context.WriteError(issue.ToString()); - } + var result = Requirements.Load(context.RequirementsFiles.ToArray()); + result.ReportIssues(context); - if (lintIssues.Count == 0) + if (result.Issues.Count == 0) { context.WriteLine("No issues found"); } @@ -195,20 +192,19 @@ private static void ProcessRequirements(Context context) // Read requirements from files context.WriteLine($"Reading {context.RequirementsFiles.Count} requirements file(s)..."); - var (requirements, loadIssues) = Requirements.Load(context.RequirementsFiles.ToArray()); + var result = Requirements.Load(context.RequirementsFiles.ToArray()); // Report any lint issues found during loading - foreach (var issue in loadIssues) - { - context.WriteError(issue.ToString()); - } + result.ReportIssues(context); // Abort if loading failed due to lint errors - if (requirements == null) + if (result.Requirements == null) { return; } + var requirements = result.Requirements; + context.WriteLine("Requirements loaded successfully."); // Export requirements report if requested diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsExportTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsExportTests.cs index 09e2313..3b4cd6f 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsExportTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsExportTests.cs @@ -69,7 +69,9 @@ public void Requirements_Export_SimpleRequirements_CreatesMarkdownFile() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath); @@ -97,7 +99,9 @@ public void Requirements_Export_WithCustomDepth_UsesCorrectHeaderLevel() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath, depth: 3); @@ -127,7 +131,9 @@ public void Requirements_Export_NestedSections_CreatesHierarchy() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath); @@ -157,7 +163,9 @@ public void Requirements_Export_SectionWithNoRequirements_CreatesHeaderOnly() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath); @@ -183,7 +191,9 @@ public void Requirements_Export_NullFilePath_ThrowsArgumentException() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var ex = Assert.ThrowsExactly(() => requirements.Export(null!)); Assert.Contains("File path cannot be null or empty", ex.Message); @@ -204,7 +214,9 @@ public void Requirements_Export_EmptyFilePath_ThrowsArgumentException() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var ex = Assert.ThrowsExactly(() => requirements.Export(string.Empty)); Assert.Contains("File path cannot be null or empty", ex.Message); @@ -229,7 +241,9 @@ public void Requirements_Export_MultipleSections_ExportsAll() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath); @@ -251,7 +265,9 @@ public void Requirements_Export_EmptyRequirements_CreatesEmptyFile() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath); @@ -284,7 +300,9 @@ brute force or dictionary attacks. "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "justifications.md"); requirements.ExportJustifications(mdPath); @@ -316,7 +334,9 @@ public void Requirements_ExportJustifications_WithCustomDepth_UsesCorrectHeaderL "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "justifications.md"); requirements.ExportJustifications(mdPath, depth: 2); @@ -342,7 +362,9 @@ public void Requirements_ExportJustifications_WithoutJustifications_CreatesHeade "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "justifications.md"); requirements.ExportJustifications(mdPath); @@ -393,7 +415,9 @@ public void Requirements_ExportJustifications_NestedSections_CreatesHierarchy() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "justifications.md"); requirements.ExportJustifications(mdPath); @@ -432,7 +456,9 @@ public void Requirements_Export_WithFilterTags_ExportsOnlyMatchingRequirements() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); var filterTags = new HashSet { "security" }; @@ -472,7 +498,9 @@ public void Requirements_Export_WithMultipleFilterTags_ExportsRequirementsMatchi "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); var filterTags = new HashSet { "security", "data-integrity" }; @@ -502,7 +530,9 @@ public void Requirements_Export_WithFilterMatchingNoRequirements_ExportsEmptyFil "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); var filterTags = new HashSet { "performance" }; @@ -535,7 +565,9 @@ public void Requirements_Export_WithoutFilter_ExportsAllRequirements() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); requirements.Export(mdPath); @@ -570,7 +602,9 @@ public void Requirements_ExportJustifications_WithFilterTags_ExportsOnlyMatching "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "justifications.md"); var filterTags = new HashSet { "security" }; @@ -607,7 +641,9 @@ public void Requirements_Export_WithFilterExcludesEmptySections_OnlyShowsSection "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var mdPath = Path.Combine(_testDirectory, "requirements.md"); var filterTags = new HashSet { "security" }; diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsReadTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs similarity index 74% rename from test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsReadTests.cs rename to test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs index a5fd3d1..cbf78aa 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsReadTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs @@ -23,10 +23,10 @@ namespace DemaConsulting.ReqStream.Tests.Modeling; /// -/// Unit tests for Requirements YAML reading functionality. +/// Unit tests for Requirements YAML loading and model parsing functionality. /// [TestClass] -public class RequirementsReadTests +public class RequirementsLoadParsingTests { private string _testDirectory = string.Empty; @@ -56,7 +56,7 @@ public void TestCleanup() /// Test reading a simple YAML file with a single requirement. /// [TestMethod] - public void Requirements_Read_SimpleRequirement_ParsesCorrectly() + public void Requirements_Load_SimpleRequirement_ParsesCorrectly() { var yamlContent = @"--- sections: @@ -68,7 +68,9 @@ public void Requirements_Read_SimpleRequirement_ParsesCorrectly() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.HasCount(1, requirements.Sections); @@ -82,7 +84,7 @@ public void Requirements_Read_SimpleRequirement_ParsesCorrectly() /// Test reading a requirement with tests. /// [TestMethod] - public void Requirements_Read_RequirementWithTests_ParsesTestsCorrectly() + public void Requirements_Load_RequirementWithTests_ParsesTestsCorrectly() { var yamlContent = @"--- sections: @@ -98,7 +100,9 @@ public void Requirements_Read_RequirementWithTests_ParsesTestsCorrectly() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); var req = requirements.Sections[0].Requirements[0]; @@ -113,7 +117,7 @@ public void Requirements_Read_RequirementWithTests_ParsesTestsCorrectly() /// Test reading a requirement with child requirements. /// [TestMethod] - public void Requirements_Read_RequirementWithChildren_ParsesChildrenCorrectly() + public void Requirements_Load_RequirementWithChildren_ParsesChildrenCorrectly() { var yamlContent = @"--- sections: @@ -128,7 +132,9 @@ public void Requirements_Read_RequirementWithChildren_ParsesChildrenCorrectly() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); var req = requirements.Sections[0].Requirements[0]; @@ -142,7 +148,7 @@ public void Requirements_Read_RequirementWithChildren_ParsesChildrenCorrectly() /// Test reading a requirement with justification. /// [TestMethod] - public void Requirements_Read_RequirementWithJustification_ParsesJustificationCorrectly() + public void Requirements_Load_RequirementWithJustification_ParsesJustificationCorrectly() { var yamlContent = @"--- sections: @@ -157,7 +163,9 @@ can access the system and to maintain data security and integrity. var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); var req = requirements.Sections[0].Requirements[0]; @@ -172,7 +180,7 @@ can access the system and to maintain data security and integrity. /// Test reading nested sections. /// [TestMethod] - public void Requirements_Read_NestedSections_ParsesHierarchyCorrectly() + public void Requirements_Load_NestedSections_ParsesHierarchyCorrectly() { var yamlContent = @"--- sections: @@ -190,7 +198,9 @@ public void Requirements_Read_NestedSections_ParsesHierarchyCorrectly() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.HasCount(1, requirements.Sections); @@ -206,7 +216,7 @@ public void Requirements_Read_NestedSections_ParsesHierarchyCorrectly() /// Test reading test mappings that are separate from requirements. /// [TestMethod] - public void Requirements_Read_TestMappings_AppliesMappingsCorrectly() + public void Requirements_Load_TestMappings_AppliesMappingsCorrectly() { var yamlContent = @"--- sections: @@ -224,7 +234,9 @@ public void Requirements_Read_TestMappings_AppliesMappingsCorrectly() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); var req = requirements.Sections[0].Requirements[0]; @@ -238,7 +250,7 @@ public void Requirements_Read_TestMappings_AppliesMappingsCorrectly() /// Test reading a file with includes. /// [TestMethod] - public void Requirements_Read_WithIncludes_MergesFilesCorrectly() + public void Requirements_Load_WithIncludes_MergesFilesCorrectly() { var mainYaml = @"--- sections: @@ -262,7 +274,9 @@ public void Requirements_Read_WithIncludes_MergesFilesCorrectly() File.WriteAllText(mainPath, mainYaml); File.WriteAllText(includedPath, includedYaml); - var requirements = Requirements.Read(mainPath); + var result = Requirements.Load(mainPath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.HasCount(2, requirements.Sections); @@ -276,7 +290,7 @@ public void Requirements_Read_WithIncludes_MergesFilesCorrectly() /// Test that identical sections are merged. /// [TestMethod] - public void Requirements_Read_IdenticalSections_MergesCorrectly() + public void Requirements_Load_IdenticalSections_MergesCorrectly() { var mainYaml = @"--- sections: @@ -300,7 +314,9 @@ public void Requirements_Read_IdenticalSections_MergesCorrectly() File.WriteAllText(mainPath, mainYaml); File.WriteAllText(includedPath, includedYaml); - var requirements = Requirements.Read(mainPath); + var result = Requirements.Load(mainPath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.HasCount(1, requirements.Sections); @@ -314,7 +330,7 @@ public void Requirements_Read_IdenticalSections_MergesCorrectly() /// Test that duplicate requirement IDs throw an exception. /// [TestMethod] - public void Requirements_Read_DuplicateRequirementId_ThrowsException() + public void Requirements_Load_DuplicateRequirementId_ThrowsException() { var yamlContent = @"--- sections: @@ -328,16 +344,18 @@ public void Requirements_Read_DuplicateRequirementId_ThrowsException() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("SYS-SEC-001", ex.Message); - Assert.Contains("Duplicate requirement ID", ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("SYS-SEC-001"))); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Duplicate requirement ID"))); } /// /// Test that include loops are prevented. /// [TestMethod] - public void Requirements_Read_IncludeLoop_DoesNotCauseInfiniteLoop() + public void Requirements_Load_IncludeLoop_DoesNotCauseInfiniteLoop() { var fileA = @"--- sections: @@ -364,7 +382,9 @@ public void Requirements_Read_IncludeLoop_DoesNotCauseInfiniteLoop() File.WriteAllText(pathA, fileA); File.WriteAllText(pathB, fileB); - var requirements = Requirements.Read(pathA); + var result = Requirements.Load(pathA); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.HasCount(2, requirements.Sections); @@ -374,20 +394,22 @@ public void Requirements_Read_IncludeLoop_DoesNotCauseInfiniteLoop() /// Test that file not found throws an exception. /// [TestMethod] - public void Requirements_Read_FileNotFound_ThrowsException() + public void Requirements_Load_FileNotFound_ThrowsException() { var nonExistentPath = Path.Combine(_testDirectory, "nonexistent.yaml"); - var ex = Assert.ThrowsExactly(() => Requirements.Read(nonExistentPath)); - Assert.Contains("File not found", ex.Message); - Assert.Contains(nonExistentPath, ex.Message); + var result = Requirements.Load(nonExistentPath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("File not found"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(nonExistentPath))); } /// /// Test that an invalid YAML content (schema error) throws an InvalidOperationException with the file location. /// [TestMethod] - public void Requirements_Read_InvalidYamlContent_ThrowsExceptionWithFileLocation() + public void Requirements_Load_InvalidYamlContent_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -399,24 +421,28 @@ public void Requirements_Read_InvalidYamlContent_ThrowsExceptionWithFileLocation var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Unknown field 'text' in requirement", ex.Message); - Assert.Contains(filePath, ex.Message); - Assert.Contains($"{filePath}(", ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Unknown field 'text' in requirement"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(filePath))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains($"{filePath}("))); } /// /// Test reading an empty YAML file. /// [TestMethod] - public void Requirements_Read_EmptyFile_ReturnsEmptyRequirements() + public void Requirements_Load_EmptyFile_ReturnsEmptyRequirements() { var yamlContent = @"--- "; var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.IsEmpty(requirements.Sections); @@ -427,7 +453,7 @@ public void Requirements_Read_EmptyFile_ReturnsEmptyRequirements() /// Test reading a complex nested structure. /// [TestMethod] - public void Requirements_Read_ComplexStructure_ParsesCorrectly() + public void Requirements_Load_ComplexStructure_ParsesCorrectly() { var yamlContent = @"--- sections: @@ -463,7 +489,9 @@ public void Requirements_Read_ComplexStructure_ParsesCorrectly() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.HasCount(2, requirements.Sections); @@ -495,7 +523,7 @@ public void Requirements_Read_ComplexStructure_ParsesCorrectly() /// Test that blank requirement ID throws an exception with file location. /// [TestMethod] - public void Requirements_Read_BlankRequirementId_ThrowsExceptionWithFileLocation() + public void Requirements_Load_BlankRequirementId_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -507,16 +535,18 @@ public void Requirements_Read_BlankRequirementId_ThrowsExceptionWithFileLocation var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Requirement 'id' cannot be blank", ex.Message); - Assert.Contains(filePath, ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Requirement 'id' cannot be blank"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(filePath))); } /// /// Test that blank requirement title throws an exception with file location. /// [TestMethod] - public void Requirements_Read_BlankRequirementTitle_ThrowsExceptionWithFileLocation() + public void Requirements_Load_BlankRequirementTitle_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -528,16 +558,18 @@ public void Requirements_Read_BlankRequirementTitle_ThrowsExceptionWithFileLocat var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Requirement 'title' cannot be blank", ex.Message); - Assert.Contains(filePath, ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Requirement 'title' cannot be blank"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(filePath))); } /// /// Test that blank section title throws an exception with file location. /// [TestMethod] - public void Requirements_Read_BlankSectionTitle_ThrowsExceptionWithFileLocation() + public void Requirements_Load_BlankSectionTitle_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -549,16 +581,18 @@ public void Requirements_Read_BlankSectionTitle_ThrowsExceptionWithFileLocation( var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Section 'title' cannot be blank", ex.Message); - Assert.Contains(filePath, ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Section 'title' cannot be blank"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(filePath))); } /// /// Test that blank test name in requirement throws an exception with file location. /// [TestMethod] - public void Requirements_Read_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation() + public void Requirements_Load_BlankTestNameInRequirement_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -574,16 +608,18 @@ public void Requirements_Read_BlankTestNameInRequirement_ThrowsExceptionWithFile var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Test name cannot be blank", ex.Message); - Assert.Contains(filePath, ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Test name cannot be blank"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(filePath))); } /// /// Test that blank test name in mapping throws an exception with file location. /// [TestMethod] - public void Requirements_Read_BlankTestNameInMapping_ThrowsExceptionWithFileLocation() + public void Requirements_Load_BlankTestNameInMapping_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -601,16 +637,18 @@ public void Requirements_Read_BlankTestNameInMapping_ThrowsExceptionWithFileLoca var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Test name cannot be blank", ex.Message); - Assert.Contains(filePath, ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Test name cannot be blank"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(filePath))); } /// /// Test that blank mapping ID throws an exception with file location. /// [TestMethod] - public void Requirements_Read_BlankMappingId_ThrowsExceptionWithFileLocation() + public void Requirements_Load_BlankMappingId_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -627,16 +665,18 @@ public void Requirements_Read_BlankMappingId_ThrowsExceptionWithFileLocation() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Mapping 'id' cannot be blank", ex.Message); - Assert.Contains(filePath, ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Mapping 'id' cannot be blank"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(filePath))); } /// /// Test that duplicate requirement ID message includes file location. /// [TestMethod] - public void Requirements_Read_DuplicateRequirementId_ExceptionIncludesFileLocation() + public void Requirements_Load_DuplicateRequirementId_ExceptionIncludesFileLocation() { var yamlContent = @"--- sections: @@ -650,17 +690,19 @@ public void Requirements_Read_DuplicateRequirementId_ExceptionIncludesFileLocati var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("SYS-SEC-001", ex.Message); - Assert.Contains("Duplicate requirement ID", ex.Message); - Assert.Contains(filePath, ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("SYS-SEC-001"))); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Duplicate requirement ID"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(filePath))); } /// /// Test reading multiple files with params array. /// [TestMethod] - public void Requirements_Read_MultipleFiles_MergesAllFiles() + public void Requirements_Load_MultipleFiles_MergesAllFiles() { var file1Yaml = @"--- sections: @@ -690,7 +732,9 @@ public void Requirements_Read_MultipleFiles_MergesAllFiles() File.WriteAllText(file2Path, file2Yaml); File.WriteAllText(file3Path, file3Yaml); - var requirements = Requirements.Read(file1Path, file2Path, file3Path); + var result = Requirements.Load(file1Path, file2Path, file3Path); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.HasCount(3, requirements.Sections); @@ -706,7 +750,7 @@ public void Requirements_Read_MultipleFiles_MergesAllFiles() /// Test reading multiple files that merge sections. /// [TestMethod] - public void Requirements_Read_MultipleFilesWithSameSections_MergesSections() + public void Requirements_Load_MultipleFilesWithSameSections_MergesSections() { var file1Yaml = @"--- sections: @@ -727,7 +771,9 @@ public void Requirements_Read_MultipleFilesWithSameSections_MergesSections() File.WriteAllText(file1Path, file1Yaml); File.WriteAllText(file2Path, file2Yaml); - var requirements = Requirements.Read(file1Path, file2Path); + var result = Requirements.Load(file1Path, file2Path); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.HasCount(1, requirements.Sections); @@ -741,7 +787,7 @@ public void Requirements_Read_MultipleFilesWithSameSections_MergesSections() /// Test reading single file with params array (backwards compatibility). /// [TestMethod] - public void Requirements_Read_SingleFileWithParamsArray_WorksCorrectly() + public void Requirements_Load_SingleFileWithParamsArray_WorksCorrectly() { var yamlContent = @"--- sections: @@ -753,7 +799,9 @@ public void Requirements_Read_SingleFileWithParamsArray_WorksCorrectly() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); Assert.HasCount(1, requirements.Sections); @@ -766,9 +814,9 @@ public void Requirements_Read_SingleFileWithParamsArray_WorksCorrectly() /// Test that calling Read with no arguments throws ArgumentException. /// [TestMethod] - public void Requirements_Read_NoArguments_ThrowsArgumentException() + public void Requirements_Load_NoArguments_ThrowsArgumentException() { - var ex = Assert.ThrowsExactly(() => Requirements.Read()); + var ex = Assert.ThrowsExactly(() => Requirements.Load()); Assert.Contains("At least one file path must be provided", ex.Message); } @@ -776,9 +824,9 @@ public void Requirements_Read_NoArguments_ThrowsArgumentException() /// Test that calling Read with null throws ArgumentException. /// [TestMethod] - public void Requirements_Read_NullArgument_ThrowsArgumentException() + public void Requirements_Load_NullArgument_ThrowsArgumentException() { - var ex = Assert.ThrowsExactly(() => Requirements.Read(null!)); + var ex = Assert.ThrowsExactly(() => Requirements.Load(null!)); Assert.Contains("At least one file path must be provided", ex.Message); } @@ -786,7 +834,7 @@ public void Requirements_Read_NullArgument_ThrowsArgumentException() /// Test that duplicate IDs across multiple files are detected. /// [TestMethod] - public void Requirements_Read_MultipleFilesWithDuplicateIds_ThrowsException() + public void Requirements_Load_MultipleFilesWithDuplicateIds_ThrowsException() { var file1Yaml = @"--- sections: @@ -807,17 +855,19 @@ public void Requirements_Read_MultipleFilesWithDuplicateIds_ThrowsException() File.WriteAllText(file1Path, file1Yaml); File.WriteAllText(file2Path, file2Yaml); - var ex = Assert.ThrowsExactly(() => Requirements.Read(file1Path, file2Path)); - Assert.Contains("SYS-SEC-001", ex.Message); - Assert.Contains("Duplicate requirement ID", ex.Message); - Assert.Contains(file2Path, ex.Message); + var result = Requirements.Load(file1Path, file2Path); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("SYS-SEC-001"))); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Duplicate requirement ID"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(file2Path))); } /// /// Test reading a requirement with tags. /// [TestMethod] - public void Requirements_Read_RequirementWithTags_ParsesTagsCorrectly() + public void Requirements_Load_RequirementWithTags_ParsesTagsCorrectly() { var yamlContent = @"--- sections: @@ -832,7 +882,9 @@ public void Requirements_Read_RequirementWithTags_ParsesTagsCorrectly() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var requirements = Requirements.Read(filePath); + var result = Requirements.Load(filePath); + Assert.IsFalse(result.HasErrors); + var requirements = result.Requirements; Assert.IsNotNull(requirements); var req = requirements.Sections[0].Requirements[0]; @@ -846,7 +898,7 @@ public void Requirements_Read_RequirementWithTags_ParsesTagsCorrectly() /// Test that blank tag name throws an exception with file location. /// [TestMethod] - public void Requirements_Read_BlankTagName_ThrowsExceptionWithFileLocation() + public void Requirements_Load_BlankTagName_ThrowsExceptionWithFileLocation() { var yamlContent = @"--- sections: @@ -862,16 +914,18 @@ public void Requirements_Read_BlankTagName_ThrowsExceptionWithFileLocation() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Tag name cannot be blank", ex.Message); - Assert.Contains(filePath, ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Tag name cannot be blank"))); + Assert.IsTrue(result.Issues.Any(i => i.Location.Contains(filePath))); } /// /// Test that circular requirements (A -> B -> A) throw an exception at read time. /// [TestMethod] - public void Requirements_Read_CircularRequirements_ThrowsInvalidOperationException() + public void Requirements_Load_CircularRequirements_ThrowsInvalidOperationException() { var yamlContent = @"--- sections: @@ -889,17 +943,19 @@ public void Requirements_Read_CircularRequirements_ThrowsInvalidOperationExcepti var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Circular requirement reference detected", ex.Message); - Assert.Contains("REQ-A", ex.Message); - Assert.Contains("REQ-B", ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Circular requirement reference detected"))); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("REQ-A"))); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("REQ-B"))); } /// /// Test that a self-referencing requirement (A -> A) throws an exception at read time. /// [TestMethod] - public void Requirements_Read_SelfReferencingRequirement_ThrowsInvalidOperationException() + public void Requirements_Load_SelfReferencingRequirement_ThrowsInvalidOperationException() { var yamlContent = @"--- sections: @@ -913,8 +969,10 @@ public void Requirements_Read_SelfReferencingRequirement_ThrowsInvalidOperationE var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var ex = Assert.ThrowsExactly(() => Requirements.Read(filePath)); - Assert.Contains("Circular requirement reference detected", ex.Message); - Assert.Contains("REQ-A", ex.Message); + var result = Requirements.Load(filePath); + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Circular requirement reference detected"))); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("REQ-A"))); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadTests.cs index e3634ca..aa8983c 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadTests.cs @@ -18,6 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using DemaConsulting.ReqStream.Cli; using DemaConsulting.ReqStream.Modeling; namespace DemaConsulting.ReqStream.Tests.Modeling; @@ -68,12 +69,12 @@ public void Requirements_Load_ValidFile_ReturnsRequirementsAndNoIssues() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var (requirements, issues) = Requirements.Load(filePath); + var result = Requirements.Load(filePath); - Assert.IsNotNull(requirements); - Assert.HasCount(0, issues); - Assert.HasCount(1, requirements.Sections); - Assert.AreEqual("REQ-001", requirements.Sections[0].Requirements[0].Id); + 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); } /// @@ -93,12 +94,12 @@ public void Requirements_Load_WithLintError_ReturnsNullAndIssues() var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); - var (requirements, issues) = Requirements.Load(filePath); + var result = Requirements.Load(filePath); - Assert.IsNull(requirements); - Assert.IsTrue(issues.Count > 0); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); - Assert.IsTrue(issues.Any(i => i.Description.Contains("Unknown field 'unknown_field'"))); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Count > 0); + Assert.IsTrue(result.Issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Unknown field 'unknown_field'"))); } /// @@ -107,12 +108,12 @@ public void Requirements_Load_WithLintError_ReturnsNullAndIssues() [TestMethod] public void Requirements_Load_MissingFile_ReturnsNullAndIssues() { - var (requirements, issues) = Requirements.Load("/nonexistent/path/missing.yaml"); + var result = Requirements.Load("/nonexistent/path/missing.yaml"); - Assert.IsNull(requirements); - Assert.IsTrue(issues.Count > 0); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); - Assert.IsTrue(issues.Any(i => i.Description.Contains("File not found"))); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Count > 0); + Assert.IsTrue(result.Issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("File not found"))); } /// @@ -129,12 +130,12 @@ invalid yaml here var filePath = Path.Combine(_testDirectory, "malformed.yaml"); File.WriteAllText(filePath, yamlContent); - var (requirements, issues) = Requirements.Load(filePath); + var result = Requirements.Load(filePath); - Assert.IsNull(requirements); - Assert.IsTrue(issues.Count > 0); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); - Assert.IsTrue(issues.Any(i => i.Description.Contains("Malformed YAML"))); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Count > 0); + Assert.IsTrue(result.Issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Malformed YAML"))); } /// @@ -148,10 +149,10 @@ public void Requirements_Load_WithLintError_IssueIncludesLocation() var filePath = Path.Combine(_testDirectory, "location-test.yaml"); File.WriteAllText(filePath, yamlContent); - var (_, issues) = Requirements.Load(filePath); + var result = Requirements.Load(filePath); - Assert.IsTrue(issues.Count > 0); - var issue = issues[0]; + Assert.IsTrue(result.Issues.Count > 0); + var issue = result.Issues[0]; StringAssert.Contains(issue.Location, filePath); StringAssert.Contains(issue.ToString(), "error:"); } @@ -189,14 +190,14 @@ public void Requirements_Load_WithMultipleLintErrors_ReportsAllIssues() var filePath = Path.Combine(_testDirectory, "multiple-issues.yaml"); File.WriteAllText(filePath, yamlContent); - var (requirements, issues) = Requirements.Load(filePath); + var result = Requirements.Load(filePath); - Assert.IsNull(requirements); - Assert.IsTrue(issues.Count >= 4); - Assert.IsTrue(issues.Any(i => i.Description.Contains("Unknown field 'unknown_section_field'"))); - Assert.IsTrue(issues.Any(i => i.Description.Contains("Requirement missing required field 'id'"))); - Assert.IsTrue(issues.Any(i => i.Description.Contains("Duplicate requirement ID 'REQ-001'"))); - Assert.IsTrue(issues.Any(i => i.Description.Contains("Unknown field 'unknown_root_field'"))); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Count >= 4); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Unknown field 'unknown_section_field'"))); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Requirement missing required field 'id'"))); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Duplicate requirement ID 'REQ-001'"))); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Unknown field 'unknown_root_field'"))); } /// @@ -233,10 +234,123 @@ public void Requirements_Load_WithIncludes_LintsIncludedFiles() title: Root requirement "); - var (requirements, issues) = Requirements.Load(rootFile); + var result = Requirements.Load(rootFile); - Assert.IsNull(requirements); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); - Assert.IsTrue(issues.Any(i => i.Description.Contains("Unknown field 'unknown_field'"))); + Assert.IsNull(result.Requirements); + Assert.IsTrue(result.Issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.IsTrue(result.Issues.Any(i => i.Description.Contains("Unknown field 'unknown_field'"))); + } + + /// + /// Test that ReportIssues routes error-level issues to the context error output. + /// + [TestMethod] + public void LoadResult_ReportIssues_ErrorIssue_SetsContextError() + { + var yamlContent = @"--- +sections: + - title: ""Test Section"" + requirements: + - id: ""REQ-001"" + title: ""A requirement."" + unknown_field: bad +"; + var filePath = Path.Combine(_testDirectory, "requirements.yaml"); + File.WriteAllText(filePath, yamlContent); + + var result = Requirements.Load(filePath); + + var logFile = Path.Combine(_testDirectory, "report-issues-error.log"); + int exitCode; + using (var context = Context.Create(["--silent", "--log", logFile])) + { + result.ReportIssues(context); + exitCode = context.ExitCode; + } + + Assert.AreEqual(1, exitCode); + var log = File.ReadAllText(logFile); + Assert.IsTrue(log.Contains("unknown_field")); + } + + /// + /// Test that ReportIssues routes warning-level issues to context normal output. + /// + [TestMethod] + public void LoadResult_ReportIssues_WarningIssue_DoesNotSetContextError() + { + var warningResult = new LoadResult( + new Requirements(), + [new LintIssue("file.yaml", LintSeverity.Warning, "A warning")]); + + var logFile = Path.Combine(_testDirectory, "report-issues-warning.log"); + int exitCode; + using (var context = Context.Create(["--silent", "--log", logFile])) + { + warningResult.ReportIssues(context); + exitCode = context.ExitCode; + } + + Assert.AreEqual(0, exitCode); + var log = File.ReadAllText(logFile); + Assert.IsTrue(log.Contains("A warning")); + } + + /// + /// Test that ReportIssues produces no output when there are no issues. + /// + [TestMethod] + public void LoadResult_ReportIssues_NoIssues_ProducesNoOutput() + { + var yamlContent = @"--- +sections: + - title: ""Test Section"" + requirements: + - id: ""REQ-001"" + title: ""A valid requirement."" +"; + var filePath = Path.Combine(_testDirectory, "requirements.yaml"); + File.WriteAllText(filePath, yamlContent); + + var result = Requirements.Load(filePath); + + var logFile = Path.Combine(_testDirectory, "report-issues-none.log"); + int exitCode; + using (var context = Context.Create(["--silent", "--log", logFile])) + { + result.ReportIssues(context); + exitCode = context.ExitCode; + } + + Assert.AreEqual(0, exitCode); + Assert.AreEqual(string.Empty, File.ReadAllText(logFile)); + } + + /// + /// Test that HasErrors is false when there are only warnings. + /// + [TestMethod] + public void LoadResult_HasErrors_WithOnlyWarnings_ReturnsFalse() + { + var result = new LoadResult( + new Requirements(), + [new LintIssue("file.yaml", LintSeverity.Warning, "A warning")]); + + Assert.IsFalse(result.HasErrors); + Assert.IsNotNull(result.Requirements); + } + + /// + /// Test that HasErrors is true when there are error-level issues. + /// + [TestMethod] + public void LoadResult_HasErrors_WithErrorIssue_ReturnsTrue() + { + var result = new LoadResult( + null, + [new LintIssue("file.yaml", LintSeverity.Error, "An error")]); + + Assert.IsTrue(result.HasErrors); + Assert.IsNull(result.Requirements); } } diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs index 78ff4a3..1429e34 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs @@ -59,9 +59,9 @@ public void TestCleanup() /// private static (int exitCode, string errors) RunLint(params string[] files) { - var (_, issues) = Requirements.Load(files); - var errors = string.Join(Environment.NewLine, issues.Select(i => i.ToString())); - var exitCode = issues.Any(i => i.Severity == LintSeverity.Error) ? 1 : 0; + var result = Requirements.Load(files); + var errors = string.Join(Environment.NewLine, result.Issues.Select(i => i.ToString())); + var exitCode = result.HasErrors ? 1 : 0; return (exitCode, errors); } @@ -71,9 +71,9 @@ private static (int exitCode, string errors) RunLint(params string[] files) /// private static (int exitCode, string output, string errors) RunLintWithOutput(params string[] files) { - var (_, issues) = Requirements.Load(files); - var errors = string.Join(Environment.NewLine, issues.Select(i => i.ToString())); - var exitCode = issues.Any(i => i.Severity == LintSeverity.Error) ? 1 : 0; + var result = Requirements.Load(files); + var errors = string.Join(Environment.NewLine, result.Issues.Select(i => i.ToString())); + var exitCode = result.HasErrors ? 1 : 0; var output = exitCode == 0 && files.Length > 0 ? $"{files[0]}: No issues found" : string.Empty; return (exitCode, output, errors); } diff --git a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixExportTests.cs b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixExportTests.cs index c242db3..49b88c7 100644 --- a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixExportTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixExportTests.cs @@ -77,7 +77,9 @@ public void TraceMatrix_Export_SimpleTraceMatrix_CreatesMarkdownFile() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX file var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -139,7 +141,9 @@ public void TraceMatrix_Export_WithCustomDepth_UsesCorrectHeaderLevel() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX file var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -187,7 +191,9 @@ public void TraceMatrix_Export_WithFailedTests_ShowsFailures() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX file with one failure var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -244,7 +250,9 @@ public void TraceMatrix_Export_WithNotExecutedTests_ShowsNotExecuted() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX file with only one test var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -298,7 +306,9 @@ public void TraceMatrix_Export_WithNestedSections_CreatesHierarchy() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX file var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -355,7 +365,9 @@ public void TraceMatrix_Export_NullFilePath_ThrowsArgumentException() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TraceMatrix var matrix = new TraceMatrix(requirements); @@ -382,7 +394,9 @@ public void TraceMatrix_Export_EmptyFilePath_ThrowsArgumentException() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TraceMatrix var matrix = new TraceMatrix(requirements); @@ -415,7 +429,9 @@ public void TraceMatrix_Export_WithChildRequirements_ConsidersChildTests() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX file var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -460,7 +476,9 @@ public void TraceMatrix_Export_WithNoTests_ShowsNotSatisfied() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TraceMatrix with no test results var matrix = new TraceMatrix(requirements); @@ -495,7 +513,9 @@ public void TraceMatrix_Export_TestMapsToMultipleRequirements_ShowsAllMappings() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX file var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -548,7 +568,9 @@ public void TraceMatrix_Export_WithFilterTags_ExportsOnlyMatchingRequirements() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create test results var testResults = new DemaConsulting.TestResults.TestResults { Name = "TestRun" }; @@ -618,7 +640,9 @@ public void TraceMatrix_CalculateSatisfiedRequirements_WithFilterTags_CountsOnly "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create test results var testResults = new DemaConsulting.TestResults.TestResults { Name = "TestRun" }; @@ -676,7 +700,9 @@ public void TraceMatrix_GetUnsatisfiedRequirements_WithFilterTags_ReturnsOnlyMat "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, yamlContent); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var matrix = new TraceMatrix(requirements); diff --git a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixReadTests.cs b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixReadTests.cs index aa846f4..4d08009 100644 --- a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixReadTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixReadTests.cs @@ -75,7 +75,9 @@ public void TraceMatrix_WithTrxFile_ParsesCorrectly() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX file using the TestResults library var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -132,7 +134,9 @@ public void TraceMatrix_WithMultipleFiles_AggregatesResults() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create first TRX file (Windows, passed) var testResults1 = new TestResults.TestResults { Name = "WindowsRun" }; @@ -202,7 +206,9 @@ public void TraceMatrix_WithExtraTests_IgnoresUnreferencedTests() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX with multiple tests var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -290,7 +296,9 @@ public void TraceMatrix_MissingFile_ThrowsFileNotFoundException() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; var nonExistentPath = Path.Combine(_testDirectory, "nonexistent.trx"); @@ -317,7 +325,9 @@ public void TraceMatrix_WithFailedTests_TracksFailures() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX with passed and failed tests var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -376,7 +386,9 @@ public void TraceMatrix_WithNoFiles_CreatesEmptyMatrix() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TraceMatrix with no files var matrix = new TraceMatrix(requirements); @@ -408,7 +420,9 @@ public void TraceMatrix_WithJUnitFile_ParsesCorrectly() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create JUnit file using the TestResults library var testResults = new TestResults.TestResults { Name = "DataTests" }; @@ -466,7 +480,9 @@ public void TraceMatrix_WithMixedFormats_ProcessesBoth() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create TRX file var trxResults = new TestResults.TestResults { Name = "TrxRun" }; @@ -529,7 +545,9 @@ public void TraceMatrix_WithJUnitFailedTests_TracksFailures() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create JUnit file with passed and failed tests var testResults = new TestResults.TestResults { Name = "JUnitTestRun" }; diff --git a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixTests.cs b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixTests.cs index c7fa1e9..1cd7e8c 100644 --- a/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Tracing/TraceMatrixTests.cs @@ -78,7 +78,9 @@ public void TraceMatrix_WithSourceSpecificTests_MatchesCorrectly() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create Windows test results var windowsResults = new TestResults.TestResults { Name = "WindowsRun" }; @@ -144,7 +146,9 @@ public void TraceMatrix_WithSourceSpecificTests_DoesNotMatchOtherSources() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create Linux test results (should not match) var linuxResults = new TestResults.TestResults { Name = "LinuxRun" }; @@ -185,7 +189,9 @@ public void TraceMatrix_WithPlainTestNames_MatchesAllSources() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create Windows test results var windowsResults = new TestResults.TestResults { Name = "WindowsRun" }; @@ -243,7 +249,9 @@ public void TraceMatrix_WithMixedTestNames_MatchesAppropriately() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create Windows test results var windowsResults = new TestResults.TestResults { Name = "WindowsRun" }; @@ -331,7 +339,9 @@ public void TraceMatrix_WithSourceSpecificTests_IsCaseInsensitive() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create test results with uppercase WINDOWS in filename var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -374,7 +384,9 @@ public void TraceMatrix_WithSourceSpecificTests_MatchesPartialFilename() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create test results with full filename containing ubuntu var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -422,7 +434,9 @@ public void TraceMatrix_WithMultipleSourceSpecifiers_MatchesAllRequirements() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create test results with filename containing both windows and dotnet8 var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -482,7 +496,9 @@ public void TraceMatrix_WithMixedFilterAndPlainReferences_MatchesBoth() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create a Windows test result file containing the shared test var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -537,7 +553,9 @@ public void TraceMatrix_WithNotExecutedTests_IgnoresNonExecutedTests() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create test results with one executed and one not-executed test var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -598,7 +616,9 @@ public void TraceMatrix_WithOnlyNotExecutedTests_TreatsAsNoTests() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create test results with only not-executed tests var testResults = new TestResults.TestResults { Name = "TestRun" }; @@ -657,7 +677,9 @@ public void TraceMatrix_WithMixedOutcomes_OnlyCountsExecutedTests() "; var reqPath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(reqPath, reqYaml); - var requirements = Requirements.Read(reqPath); + var loadResult = Requirements.Load(reqPath); + Assert.IsNotNull(loadResult.Requirements); + var requirements = loadResult.Requirements; // Create test results with passed, failed, and not-executed tests var testResults = new TestResults.TestResults { Name = "TestRun" };