From d98b400ccf496ce436181d839201ad7c1aa6e323 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 1 Sep 2026 16:26:10 -0400 Subject: [PATCH] Add root-tags orphan detection with ReqStream 1.11.0 Upgrade demaconsulting.reqstream 1.10.0 -> 1.11.0 and adopt the root-tags orphan-detection feature to guarantee every requirement traces to either a product-facing system requirement or a documented quality/process outcome. - Tag all 29 FileAssert-System-* requirements with [system]. - Add docs/reqstream/quality.yaml (8 categories: build integrity, traceable versions, static analysis, peer review, documentation generation, test infrastructure, requirements traceability, architecture traceability) tagged [quality], covering process/tooling OTS and the Shared-FileAssert-* self-dogfooding requirements. - Set root-tags: [system, quality] in requirements.yaml. - Close ~20 traced orphan gaps across cli, program, configuration, modeling, selftest, and utilities requirement trees by wiring subsystem/unit-level requirements up to their real System-level parents (all links verified against actual call sites in src/, not fabricated). - Link genuine product-library OTS dependencies (YamlDotNet, FileSystemGlobbing, HtmlAgilityPack, PdfPig) to the specific feature/unit that consumes them, since these are directly used by production code rather than being process/tooling dependencies. Dead-code discovery and removal (found via orphan tracing): - FileAssert-Cli-ScopedContext (IContext.WithPrefix / Context.ScopedContext) could not be honestly linked to any System requirement. Git history (PR #54) confirmed WithPrefix was removed from FileAssertZipAssert.Run's production path in the same commit that introduced it, to fix a duplicated-breadcrumb bug, and was never reinstated. Removed the now-superseded WithPrefix/ ScopedContext code, its dedicated test file, and rewrote the FileAssert-IContext-OutputContract requirement and companion design/ verification docs to describe the current (correct) breadcrumb mechanism, which is ZipFileContainer.GetDisplayPath string interpolation. Result: dotnet reqstream --requirements requirements.yaml reports 0 orphans. Full build, all 987 tests (net8.0/net9.0/net10.0), lint.ps1, and reviewmark --lint all pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- docs/design/file-assert/cli.md | 1 - docs/design/file-assert/cli/context.md | 31 +--- docs/design/file-assert/cli/i-context.md | 63 +------ docs/design/file-assert/modeling.md | 6 +- .../modeling/file-assert-zip-assert.md | 10 +- docs/reqstream/file-assert.yaml | 45 +++++ docs/reqstream/file-assert/cli.yaml | 16 -- docs/reqstream/file-assert/cli/i-context.yaml | 22 +-- .../configuration/file-assert-config.yaml | 2 + docs/reqstream/file-assert/modeling.yaml | 2 + .../modeling/file-assert-file.yaml | 4 + .../modeling/file-assert-test.yaml | 2 + docs/reqstream/file-assert/program.yaml | 2 + docs/reqstream/file-assert/selftest.yaml | 1 + docs/reqstream/file-assert/utilities.yaml | 3 + docs/reqstream/quality.yaml | 160 ++++++++++++++++++ .../model/file-assert/cli/i-context.sysml | 4 +- docs/verification/file-assert/cli.md | 23 +-- .../verification/file-assert/cli/i-context.md | 57 ++----- requirements.yaml | 2 + src/DemaConsulting.FileAssert/Cli/Context.cs | 97 ----------- src/DemaConsulting.FileAssert/Cli/IContext.cs | 16 +- .../Cli/ScopedContextTests.cs | 135 --------------- .../Modeling/FileAssertHtmlAssertTests.cs | 3 - .../Modeling/FileAssertJsonAssertTests.cs | 3 - .../Modeling/FileAssertYamlAssertTests.cs | 3 - .../Modeling/FileAssertZipAssertTests.cs | 27 --- 28 files changed, 271 insertions(+), 471 deletions(-) create mode 100644 docs/reqstream/quality.yaml delete mode 100644 test/DemaConsulting.FileAssert.Tests/Cli/ScopedContextTests.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b687954..e320391 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -33,7 +33,7 @@ ] }, "demaconsulting.reqstream": { - "version": "1.10.0", + "version": "1.11.0", "commands": [ "reqstream" ] diff --git a/docs/design/file-assert/cli.md b/docs/design/file-assert/cli.md index 6cf1946..d5ec620 100644 --- a/docs/design/file-assert/cli.md +++ b/docs/design/file-assert/cli.md @@ -34,7 +34,6 @@ The `IContext` interface exposes the following public members: | :----------- | :------------------------------------ | :----------------------------------------------------------------------- | | `WriteLine` | `void WriteLine(string message)` | Writes an informational message. | | `WriteError` | `void WriteError(string message)` | Writes an error message and marks the context as having errors. | -| `WithPrefix` | `IContext WithPrefix(string prefix)` | Returns a scoped context prepending `"{prefix} > "` to error messages. | The `Context` unit exposes the following public interface: diff --git a/docs/design/file-assert/cli/context.md b/docs/design/file-assert/cli/context.md index 36f180d..3d6f227 100644 --- a/docs/design/file-assert/cli/context.md +++ b/docs/design/file-assert/cli/context.md @@ -40,29 +40,11 @@ Delegates argument parsing to the private `ArgumentParser` nested class. Opens a ```csharp public void WriteLine(string message) public void WriteError(string message) -public IContext WithPrefix(string prefix) ``` `WriteLine` writes to stdout and the log file (unless `--silent` suppresses console output). `WriteError` sets the internal error flag, writes to stderr in red (unless silent), and writes to the log file. -`WithPrefix` creates and returns a new `ScopedContext` that prepends `"{prefix} > "` to every -`WriteError` message before delegating to this context. The prefix must not be null. - -##### ScopedContext Nested Class - -`ScopedContext` is a private sealed class nested inside `Context`. It implements `IContext` and -holds a reference to its parent `IContext` and a prefix string: - -- `WriteLine` — delegated unchanged to the parent context. -- `WriteError` — the message is rewritten as `"{_prefix} > {message}"` before passing to the - parent, so all errors bubble up through the chain and reach the root context's `_hasErrors` - and `_errorCount` fields. -- `WithPrefix` — creates a further-nested `ScopedContext(this, prefix)`, enabling arbitrarily - deep breadcrumb chains (e.g., `"outer.zip > inner.zip > error message"`). - -`ScopedContext` is used by `FileAssertZipAssert.Run` to scope every error message with the -archive entry's display path before passing the context to nested `FileAssertFile` assertions. ##### Argument Parsing @@ -77,10 +59,9 @@ The private nested class `ArgumentParser` processes each argument in order: #### Design Decisions -- **Implements `IContext`**: `Context` implements the `IContext` interface, which is also - implemented by `ScopedContext`. This allows all asserters to accept `IContext` rather than the - concrete `Context`, enabling `FileAssertZipAssert` to supply a scoped context without those - asserters requiring any knowledge of the scoping mechanism. +- **Implements `IContext`**: `Context` implements the `IContext` interface. This allows all + asserters to accept `IContext` rather than the concrete `Context`, decoupling their reporting + logic from the concrete implementation. - **Sealed with IDisposable**: The class is sealed to prevent inheritance of internal state, and implements `IDisposable` to ensure the log file stream is always closed. - **Factory method**: The `Create` factory method is `public` so tests and the self-validation @@ -124,7 +105,6 @@ to the console. | `Context.Create(string[])` | Factory: parses args, opens log file, returns initialized instance. | | `WriteLine(string)` | Writes to stdout (unless silent) and log file. | | `WriteError(string)` | Sets error flag and counter; writes to stderr/log (unless silent). | -| `WithPrefix(string) → IContext` | Returns a new `ScopedContext` prepending the given prefix to errors.| | `Dispose()` | Closes and disposes the log-file stream writer. | | `ArgumentParser.ParseArguments(string[])` | Inner class: translates argument array into named parser state. | @@ -137,13 +117,12 @@ to the console. | Value-requiring flag with no value | `ArgumentException` propagated to the caller of `Create`. | | `--depth` value not in 1–6 range | `ArgumentException` propagated to the caller of `Create`. | | Log file cannot be opened | `InvalidOperationException` wrapping the underlying I/O. | -| Null `prefix` passed to `WithPrefix` | `ArgumentNullException` thrown before `ScopedContext` created.| | Assertion or rule failure at runtime | Handled by `WriteError`; no throw — errors in `_errorCount`. | #### Dependencies - **Internal dependency**: `ArgumentParser` (private nested class) is used exclusively by - `Context.Create`. `ScopedContext` (private nested class) is returned by `WithPrefix`. + `Context.Create`. #### Callers @@ -154,5 +133,3 @@ to the console. assert unit (`FileAssertTextAssert`, `FileAssertPdfAssert`, `FileAssertXmlAssert`, `FileAssertHtmlAssert`, `FileAssertYamlAssert`, `FileAssertJsonAssert`, `FileAssertZipAssert`) receive an `IContext` reference and call `WriteLine` / `WriteError` to report results. - `FileAssertZipAssert` additionally calls `context.WithPrefix(displayPath)` to derive a scoped - context for nested zip entry assertions. diff --git a/docs/design/file-assert/cli/i-context.md b/docs/design/file-assert/cli/i-context.md index 9ef4dea..1e775be 100644 --- a/docs/design/file-assert/cli/i-context.md +++ b/docs/design/file-assert/cli/i-context.md @@ -5,21 +5,15 @@ #### Overview `IContext` is the output contract interface for reporting assertion results and errors within -FileAssert. It is implemented by `Context` (the root context) and by `Context.ScopedContext` (a -scoped wrapper that prepends a path prefix to every error message). - -Accepting `IContext` in asserter `Run` methods and in `FileAssertFile.Run` allows -`FileAssertZipAssert` to pass a scoped context to nested asserters when processing zip archive -entries, without those asserters requiring any knowledge of the scoping mechanism. +FileAssert. It is implemented by `Context` and accepted by all asserters so that their reporting +logic is decoupled from the concrete `Context` implementation. #### Purpose `IContext` exists to decouple the asserters from the concrete output and error-tracking implementation. By depending only on this interface, each asserter can write informational -output and errors, and request a scoped (breadcrumb-prefixed) child context, without knowing -whether it holds the root `Context` or a nested `ScopedContext`. This enables consistent, -self-describing error reporting across plain and nested (zip) assertion scenarios while keeping -the scoping mechanism entirely transparent to callers. +output and errors without knowing the concrete implementation it holds (console, log file, or a +future alternative). #### Interface Members @@ -28,7 +22,6 @@ internal interface IContext { void WriteLine(string message); void WriteError(string message); - IContext WithPrefix(string prefix); } ``` @@ -36,59 +29,19 @@ internal interface IContext | :----------------------------- | :-------------------------------------------------------------------------------- | | `WriteLine(string message)` | Writes an informational output line. Does not affect the error state. | | `WriteError(string message)` | Writes an error message and marks the context as having errors. | -| `WithPrefix(string prefix)` | Returns a new scoped context that prepends `"{prefix} > "` to all error messages. | - -#### ScopedContext - -`ScopedContext` is a private sealed nested class inside `Context`. It implements `IContext` and holds -a reference to its parent `IContext`. All calls are delegated: - -- `WriteLine` — delegated unchanged to the parent. -- `WriteError` — the message is prefixed with `"{_prefix} > "` before being passed to the parent. -- `WithPrefix` — creates a further-nested `ScopedContext` wrapping `this`, enabling arbitrary breadcrumb depth. - -This chain ensures that all scoped errors ultimately reach the root `Context` and increment its -`ErrorCount` and `ExitCode`. #### Design Rationale - **Interface not abstract class**: Using an interface rather than a base class avoids inheritance - hierarchies and allows `ScopedContext` to be a lightweight private nested class with no - instance state beyond a prefix string and a parent reference. -- **Prefix as breadcrumb**: Scoped error messages produced while asserting a zip entry appear as - `"archive.zip > entry.xml > error message"`, giving users immediate context about which archive - and entry caused the failure without requiring any special formatting in the asserter itself. -- **`WithPrefix` on `IContext`**: Declaring `WithPrefix` on the interface rather than only on - `Context` means that asserters can scope further without a cast, supporting arbitrarily deep - nesting (e.g., zip-in-zip-in-zip). + hierarchies and keeps the contract minimal. #### Data Model -`IContext` carries no instance data. `ScopedContext` holds: - -| Field | Type | Description | -| :-------- | :--------- | :---------------------------------------------------- | -| `_parent` | `IContext` | The parent context to delegate all calls to. | -| `_prefix` | `string` | The prefix prepended to every `WriteError` message. | - -#### Key Methods - -| Method | Description | -| :------------------------------ | :------------------------------------------------ | -| `WriteLine(string)` | Informational output, no error state change. | -| `WriteError(string)` | Error message with prefix, delegates to parent. | -| `WithPrefix(string) → IContext` | Returns a new nested `ScopedContext`. | - -#### Error Handling - -| Scenario | Handling | -| :--------------------------------- | :-------------------------------------------------------------------- | -| Null prefix passed to `WithPrefix` | `ArgumentNullException` thrown before construction of scoped context. | +`IContext` carries no instance data. #### Dependencies -- No external dependencies. `IContext` and `ScopedContext` are self-contained within the `Cli` - namespace. +- No external dependencies. `IContext` is self-contained within the `Cli` namespace. #### Callers @@ -96,6 +49,4 @@ This chain ensures that all scoped errors ultimately reach the root `Context` an - All 7 asserters (`FileAssertTextAssert`, `FileAssertXmlAssert`, `FileAssertHtmlAssert`, `FileAssertYamlAssert`, `FileAssertJsonAssert`, `FileAssertPdfAssert`, `FileAssertZipAssert`) — each `Run` method accepts `IContext`. -- `FileAssertZipAssert.Run` — calls `context.WithPrefix(displayPath)` before passing the - scoped context to nested file assertions inside a zip archive. - `FileAssertTest.Run` — accepts `IContext` and passes it down the assertion chain. diff --git a/docs/design/file-assert/modeling.md b/docs/design/file-assert/modeling.md index aa253cc..de1cf6f 100644 --- a/docs/design/file-assert/modeling.md +++ b/docs/design/file-assert/modeling.md @@ -89,9 +89,9 @@ Domain objects are constructed and executed in the following layers: 3. During execution, `FileAssertConfig.Run` calls `FileAssertTest.Run` → `FileAssertFile.Run` → assert unit `Run` methods, threading `IContext` through every layer so all failures are reported via a single path. `FileAssertTest.Run` wraps the base path in a - `DirectoryFileContainer` before passing it down. `FileAssertZipAssert.Run` calls - `context.WithPrefix(displayPath)` to create a scoped `IContext` that prepends the archive - path as a breadcrumb to every nested error message. + `DirectoryFileContainer` before passing it down. `FileAssertZipAssert.Run` derives a + breadcrumb-prefixed display path via `ZipFileContainer.GetDisplayPath` so every nested error + message identifies the archive and entry that produced it. ### Dependencies diff --git a/docs/design/file-assert/modeling/file-assert-zip-assert.md b/docs/design/file-assert/modeling/file-assert-zip-assert.md index ee011d3..57ae617 100644 --- a/docs/design/file-assert/modeling/file-assert-zip-assert.md +++ b/docs/design/file-assert/modeling/file-assert-zip-assert.md @@ -57,8 +57,7 @@ Execution proceeds in the following steps: 4. If `InvalidDataException`, `IOException`, or `UnauthorizedAccessException` is thrown constructing the `ZipFileContainer`, writes the parse error and returns immediately. The stream is disposed in a nested `try` block even when the `ZipFileContainer` constructor throws. -5. Creates a scoped context via `context.WithPrefix(displayPath)`. -6. Runs each `FileAssertFile` in `Files` against the `ZipFileContainer` and scoped context. +5. Runs each `FileAssertFile` in `Files` against the `ZipFileContainer` and context. ###### Run Error Messages @@ -107,10 +106,9 @@ pdf, nested zip) against the archive contents. stream is disposed even when the `ZipFileContainer` constructor throws `InvalidDataException`. Without this guard, the stream from `container.OpenEntry` would remain open, locking the underlying file or archive entry. -- **Scoped context for breadcrumbs**: `context.WithPrefix(displayPath)` creates a scoped - `IContext` that prepends the archive's display path to every error message, giving users - unambiguous context (`"outer.zip > entry.xml > error"`) without requiring any formatting - logic in the individual asserters. +- **Breadcrumb display path**: `container.GetDisplayPath(entryPath)` computes a breadcrumb path + (e.g., `"outer.zip > entry.xml"`) that `ZipFileContainer` embeds in error messages, giving users + unambiguous context without requiring any formatting logic in the individual asserters. - **Forward-slash normalization handled by `ZipFileContainer`**: Entry path normalization is the responsibility of `ZipFileContainer.GetEntries`, not `FileAssertZipAssert`. This keeps the asserter free of container-specific logic. diff --git a/docs/reqstream/file-assert.yaml b/docs/reqstream/file-assert.yaml index 63ef586..2fe16dc 100644 --- a/docs/reqstream/file-assert.yaml +++ b/docs/reqstream/file-assert.yaml @@ -16,8 +16,10 @@ sections: The primary purpose of the tool is to assert properties of files on the file system using YAML-defined test suites. Verifying this core behavior at the system level confirms that all subsystems integrate correctly to produce the expected outcome. + tags: [system] children: - FileAssert-Configuration-LoadAndBuild + - FileAssert-Modeling-ExecutionChain tests: - IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero @@ -29,8 +31,11 @@ sections: CI/CD pipelines and scripts depend on the exit code to detect assertion failures. A non-zero exit code when an assertion fails is the primary mechanism for the tool to signal that files do not meet the declared constraints. + tags: [system] children: - FileAssert-Context-ExitCode + - FileAssert-Cli-ErrorReporting + - FileAssert-Modeling-FailureReporting tests: - IntegrationTest_ValidConfig_FailingAssertions_ReturnsNonZero @@ -40,8 +45,10 @@ sections: Automated pipelines that parse or redirect tool output must be able to run the tool without cluttering their own output. The --silent flag provides this capability while still allowing exit code and log file output to carry result information. + tags: [system] children: - FileAssert-Context-Silent + - FileAssert-Cli-OutputPipeline tests: - IntegrationTest_SilentFlag_SuppressesOutput @@ -51,8 +58,10 @@ sections: Persistent log files allow post-hoc inspection of tool output in CI/CD environments where console output may be discarded. The log file must capture all messages that would otherwise appear on the console. + tags: [system] children: - FileAssert-Context-Output + - FileAssert-Cli-OutputPipeline tests: - IntegrationTest_LogFlag_WritesOutputToFile @@ -64,9 +73,13 @@ sections: Supporting a custom configuration file path lets users maintain multiple test suites and select the appropriate one per invocation. The default path ensures the common single-suite case requires no additional arguments. + tags: [system] children: - FileAssert-Context-ConfigFile - FileAssert-FileAssertConfig-ReadFromFile + - FileAssert-Cli-ArgumentExposure + - FileAssert-Program-MissingDefaultConfig + - FileAssert-Program-MissingExplicitConfig tests: - IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero - IntegrationTest_ValidConfig_FailingAssertions_ReturnsNonZero @@ -81,10 +94,12 @@ sections: Pipelines; JUnit XML is supported by Jenkins, GitLab CI, and most other CI tools. Results are written both from the self-validation path (--validate --results) and from the normal file assertion path (--results without --validate). + tags: [system] children: - FileAssert-FileAssertConfig-ResultsTrx - FileAssert-FileAssertConfig-ResultsJUnit - FileAssert-Validation-Results + - FileAssert-Configuration-ResultsFiles tests: - IntegrationTest_ValidateWithResults_GeneratesTrxFile - IntegrationTest_ValidateWithResults_GeneratesJUnitFile @@ -99,9 +114,11 @@ sections: Selective test execution is essential for phased CI/CD pipelines where only a subset of tests (for example, smoke tests) should run in a given stage. Filtering by name or tag keeps the command line concise and consistent with common CLI testing tools. + tags: [system] children: - FileAssert-Context-Filters - FileAssert-Configuration-FilterExecution + - FileAssert-Cli-ArgumentExposure tests: - IntegrationTest_TestFiltering_OnlyRunsMatchingTests @@ -113,6 +130,7 @@ sections: Enforcing a minimum file count guards against missing build outputs or required artifacts that should always be present. Users declare the lower bound they expect; the tool reports a violation when fewer files are found. + tags: [system] children: - FileAssert-FileAssertFile-MinCountConstraint tests: @@ -126,6 +144,7 @@ sections: Enforcing a maximum file count prevents unexpected duplicate build outputs or configuration files from silently passing validation. Users declare the upper bound they expect; the tool reports a violation when more files are found. + tags: [system] children: - FileAssert-FileAssertFile-MaxCountConstraint tests: @@ -140,6 +159,7 @@ sections: formats and date patterns that cannot be expressed as a simple substring check. Verifying both the passing and failing paths at the system level confirms end-to-end integration from the YAML rule definition through to the exit code. + tags: [system] children: - FileAssert-FileAssertRule-MatchesRule tests: @@ -155,6 +175,7 @@ sections: must assert that precisely one artifact, configuration file, or report was produced. Any deviation from the expected count is a configuration drift that should be caught immediately. + tags: [system] children: - FileAssert-FileAssertFile-ExactCount tests: @@ -170,6 +191,7 @@ sections: (max-size). Enforcing these constraints at the system level confirms that the YAML configuration, deserialization, and file system inspection all integrate correctly. + tags: [system] children: - FileAssert-FileAssertFile-MinSize - FileAssert-FileAssertFile-MaxSize @@ -186,6 +208,7 @@ sections: assert that sensitive strings such as hard-coded passwords or debug flags are absent from committed files. System-level verification confirms the rule integrates correctly from YAML configuration through to the exit code. + tags: [system] children: - FileAssert-FileAssertRule-DoesNotContainRule tests: @@ -201,6 +224,7 @@ sections: as asserting that log files contain no FATAL or ERROR entries. System-level verification confirms the rule integrates correctly from YAML configuration through to the exit code. + tags: [system] children: - FileAssert-FileAssertRule-DoesNotContainRegexRule tests: @@ -211,6 +235,7 @@ sections: justification: | Displaying the tool version on request is a standard CLI convention and allows users and CI/CD pipelines to verify which version of the tool is installed. + tags: [system] children: - FileAssert-Program-Version tests: @@ -221,6 +246,7 @@ sections: justification: | A help flag is a standard CLI convention that allows users to discover available options and their usage without consulting external documentation. + tags: [system] children: - FileAssert-Program-Help tests: @@ -232,6 +258,7 @@ sections: The self-validation mode lets users verify that the installed tool is functioning correctly in their environment. This is especially valuable in regulated environments where tool qualification evidence is required. + tags: [system] children: - FileAssert-Program-Validate tests: @@ -243,8 +270,10 @@ sections: Rejecting unknown arguments prevents silent misconfiguration where a typo in a flag name causes the tool to run without the intended option. A non-zero exit code ensures CI/CD pipelines detect the erroneous invocation. + tags: [system] children: - FileAssert-Context-InvalidArgs + - FileAssert-Cli-ArgumentParsing tests: - IntegrationTest_UnknownArgument_ReturnsError @@ -258,6 +287,7 @@ sections: assertions are the primary mechanism for content validation in CI/CD pipelines. The supported assertion types are: `contains`, `does-not-contain`, `matches` (regex), and `does-not-contain-regex` (regex). + tags: [system] children: - FileAssert-Modeling-FileTypeParsing tests: @@ -275,9 +305,11 @@ sections: text content allows users to verify that generated PDF outputs meet documented requirements. An immediate failure when the file is not a valid PDF prevents misleading partial results. + tags: [system] children: - FileAssert-Modeling-FileTypeParsing - FileAssert-Modeling-FileTypeParseError + - FileAssert-OTS-PdfPig tests: - IntegrationTest_PdfAssert_InvalidFile_ReturnsNonZero - IntegrationTest_PdfAssert_FailingAssertion_ReturnsNonZero @@ -292,6 +324,7 @@ sections: in enterprise and regulated environments. XPath-based node count assertions allow users to verify document structure and element presence without writing custom parsers. An immediate failure on invalid XML prevents misleading partial results. + tags: [system] children: - FileAssert-Modeling-FileTypeParsing - FileAssert-Modeling-FileTypeParseError @@ -311,9 +344,11 @@ sections: structure such as title presence and link counts. Because the parser is lenient and tolerates syntactically imperfect markup, only IO failures (not parse failures) are treated as errors. + tags: [system] children: - FileAssert-Modeling-FileTypeParsing - FileAssert-Modeling-QueryAssertions + - FileAssert-OTS-HtmlAgilityPack tests: - IntegrationTest_HtmlAssert_PassingQuery_ReturnsZero - IntegrationTest_HtmlAssert_ZeroMatchingElements_ReturnsNonZero @@ -328,6 +363,7 @@ sections: and infrastructure-as-code files. Dot-notation path assertions allow users to verify that required configuration keys are present and have the expected cardinality. An immediate failure on invalid YAML prevents misleading partial results. + tags: [system] children: - FileAssert-Modeling-FileTypeParsing - FileAssert-Modeling-FileTypeParseError @@ -346,6 +382,7 @@ sections: outputs. Dot-notation paths allow users to verify that required keys are present and have the expected cardinality without writing custom parsers. An immediate failure on invalid JSON prevents misleading partial results. + tags: [system] children: - FileAssert-Modeling-FileTypeParsing - FileAssert-Modeling-FileTypeParseError @@ -367,10 +404,13 @@ sections: individual entries allow users to validate the content of packaged files directly from the archive. Recursive nesting lets users validate archives embedded inside other archives. An immediate failure on an invalid zip archive prevents misleading partial results. + tags: [system] children: - FileAssert-Modeling-FileTypeParsing - FileAssert-Modeling-FileTypeParseError - FileAssert-FileAssertZipAssert-EntryMatching + - FileAssert-Modeling-ZipEntryContentAssertions + - FileAssert-Modeling-ZipEntryBreadcrumbReporting tests: - IntegrationTest_ZipAssert_PassingQuery_ReturnsZero - IntegrationTest_ZipAssert_InvalidFile_ReturnsNonZero @@ -388,6 +428,7 @@ sections: Running the default-named configuration from the current directory with no arguments provides a zero-configuration entry point for users and CI/CD pipelines, allowing a project to be validated simply by invoking the tool from its root. + tags: [system] children: - FileAssert-Program-DefaultBehavior tests: @@ -401,8 +442,10 @@ sections: The --depth flag allows users to embed self-validation output at an appropriate heading level within existing Markdown documents. A default of 1 ensures top-level output without explicit configuration. + tags: [system] children: - FileAssert-Context-Depth + - FileAssert-Cli-ArgumentExposure tests: - IntegrationTest_DepthFlag_ProducesHeadingsAtSpecifiedDepth - Validation_Run_WithDepth_UsesSpecifiedHeadingDepth @@ -413,6 +456,7 @@ sections: justification: | CI/CD pipelines operate across heterogeneous environments. Supporting all three major platforms ensures the tool can be used consistently regardless of the build agent OS. + tags: [system] children: - FileAssert-Platform-Windows - FileAssert-Platform-Linux @@ -428,6 +472,7 @@ sections: justification: | Supporting multiple .NET runtimes allows projects targeting different LTS and current versions to adopt FileAssert without being forced to upgrade their runtime. + tags: [system] children: - FileAssert-Platform-Net8 - FileAssert-Platform-Net9 diff --git a/docs/reqstream/file-assert/cli.yaml b/docs/reqstream/file-assert/cli.yaml index c42a09e..447cbf7 100644 --- a/docs/reqstream/file-assert/cli.yaml +++ b/docs/reqstream/file-assert/cli.yaml @@ -66,19 +66,3 @@ sections: tests: - Cli_OutputPipeline_WithLogPathAndSilentFlag_WritesMessagesToLogFile - Cli_OutputPipeline_WithoutSilentFlag_WritesMessagesToConsole - - - id: FileAssert-Cli-ScopedContext - title: The Cli subsystem shall provide breadcrumb-style context scoping so that error messages produced inside a nested - assertion identify the enclosing container path. - justification: | - When asserting files inside a zip archive, error messages must identify both the - archive and the entry that failed. Breadcrumb-style context scoping prepends the - enclosing container path to error messages, giving users immediate context without - requiring each asserter to format breadcrumb paths explicitly. Nested scopes support - zip-in-zip scenarios. - children: - - FileAssert-IContext-OutputContract - tests: - - Context_WithPrefix_ReturnsNonNullScopedContext - - ScopedContext_WriteError_PropagatesExitCodeToRoot - - ScopedContext_Nested_WriteError_PropagatesExitCodeToRoot diff --git a/docs/reqstream/file-assert/cli/i-context.yaml b/docs/reqstream/file-assert/cli/i-context.yaml index c27f868..0b5292c 100644 --- a/docs/reqstream/file-assert/cli/i-context.yaml +++ b/docs/reqstream/file-assert/cli/i-context.yaml @@ -2,9 +2,8 @@ # Software Unit Requirements for the IContext Interface # # IContext is the output contract interface for reporting assertion results and -# errors. It is implemented by Context (the root context) and by -# Context.ScopedContext (a scoped wrapper that prepends a path prefix to every -# error message), enabling breadcrumb-style error reporting in nested zip assertions. +# errors. It is implemented by Context and accepted by all asserters so that +# reporting logic is decoupled from the concrete Context implementation. sections: - title: IContext Unit Requirements @@ -12,16 +11,9 @@ sections: - id: FileAssert-IContext-OutputContract title: The IContext interface shall define an output contract for reporting informational messages and errors. justification: | - All asserters and FileAssertFile must be decoupled from the concrete Context - class so that a scoped wrapper can be passed in place of the root context. - Defining an interface rather than accepting Context directly allows - FileAssertZipAssert to supply a prefix-bearing ScopedContext to nested - asserters without those asserters requiring any knowledge of the scoping - mechanism. + All asserters and FileAssertFile accept IContext rather than the concrete Context + class, decoupling their reporting logic from the specific output implementation + (console, log file, or a future alternative). tests: - - Context_WithPrefix_ReturnsNonNullScopedContext - - Context_WithPrefix_NullPrefix_ThrowsArgumentNullException - - ScopedContext_WriteError_PropagatesExitCodeToRoot - - ScopedContext_WriteLine_DoesNotSetError - - ScopedContext_Nested_WriteError_PropagatesExitCodeToRoot - - ScopedContext_MultipleErrors_AllAccumulateOnRoot + - Context_WriteLine_NotSilent_WritesToConsole + - Context_WriteError_SetsErrorExitCode diff --git a/docs/reqstream/file-assert/configuration/file-assert-config.yaml b/docs/reqstream/file-assert/configuration/file-assert-config.yaml index fd8d18f..55e76f4 100644 --- a/docs/reqstream/file-assert/configuration/file-assert-config.yaml +++ b/docs/reqstream/file-assert/configuration/file-assert-config.yaml @@ -15,6 +15,8 @@ sections: caller and allows the class to validate its inputs before returning a usable instance. Failing early with a descriptive exception when the file is missing avoids silent failures that would be difficult to diagnose. + children: + - FileAssert-OTS-YamlDotNet tests: - FileAssertConfig_ReadFromFile_ValidFile_ReturnsConfig - FileAssertConfig_ReadFromFile_FileNotFound_ThrowsFileNotFoundException diff --git a/docs/reqstream/file-assert/modeling.yaml b/docs/reqstream/file-assert/modeling.yaml index 1781b1f..1f35c98 100644 --- a/docs/reqstream/file-assert/modeling.yaml +++ b/docs/reqstream/file-assert/modeling.yaml @@ -60,6 +60,7 @@ sections: children: - FileAssert-FileAssertTextAssert-Creation - FileAssert-FileAssertTextAssert-RuleApplication + - FileAssert-FileAssertTextAssert-IOError tests: - Modeling_ExecuteChain_PassesWhenAllConstraintsMet @@ -192,6 +193,7 @@ sections: - FileAssert-FileAssertYamlAssert-QueryMinCount - FileAssert-FileAssertYamlAssert-QueryMaxCount - FileAssert-FileAssertYamlAssert-QueryExactCount + - FileAssert-FileAssertYamlAssert-MalformedQueryRejected tests: - Modeling_QueryAssertions_YamlQueryMeetsCount_NoError diff --git a/docs/reqstream/file-assert/modeling/file-assert-file.yaml b/docs/reqstream/file-assert/modeling/file-assert-file.yaml index fe5e92d..2f5e362 100644 --- a/docs/reqstream/file-assert/modeling/file-assert-file.yaml +++ b/docs/reqstream/file-assert/modeling/file-assert-file.yaml @@ -26,6 +26,8 @@ sections: justification: | The minimum-count constraint catches missing required artifacts (such as a build output that should always be produced). + children: + - FileAssert-OTS-FileSystemGlobbing tests: - FileAssertFile_Run_TooFewFiles_WritesError - FileAssertFile_Run_NoMatchingFiles_NoConstraints_NoError @@ -83,6 +85,8 @@ sections: Delegating file-type assertions to dedicated units keeps FileAssertFile free of format-specific parsing and assertion logic, making each unit independently testable and the overall design extensible to future formats. + children: + - FileAssert-IContext-OutputContract tests: - FileAssertFile_Create_ValidData_CreatesFile - FileAssertFile_Run_WithContentRule_ContentContainsValue_NoError diff --git a/docs/reqstream/file-assert/modeling/file-assert-test.yaml b/docs/reqstream/file-assert/modeling/file-assert-test.yaml index cc3a273..ef53554 100644 --- a/docs/reqstream/file-assert/modeling/file-assert-test.yaml +++ b/docs/reqstream/file-assert/modeling/file-assert-test.yaml @@ -39,6 +39,8 @@ sections: justification: | Running all file assertions in the test ensures complete coverage; skipping any file assertion entry would leave gaps in validation that may go undetected. + children: + - FileAssert-Utilities-FileContainerAbstraction tests: - FileAssertTest_Run_RunsAllFiles diff --git a/docs/reqstream/file-assert/program.yaml b/docs/reqstream/file-assert/program.yaml index a2180da..2dea85a 100644 --- a/docs/reqstream/file-assert/program.yaml +++ b/docs/reqstream/file-assert/program.yaml @@ -30,6 +30,8 @@ sections: justification: | Provides a built-in mechanism to verify the tool is functioning correctly in the deployment environment. + children: + - FileAssert-SelfTest-ValidationPipeline tests: - Program_Run_WithValidateFlag_RunsValidation diff --git a/docs/reqstream/file-assert/selftest.yaml b/docs/reqstream/file-assert/selftest.yaml index df5b67e..f8ff3c7 100644 --- a/docs/reqstream/file-assert/selftest.yaml +++ b/docs/reqstream/file-assert/selftest.yaml @@ -29,6 +29,7 @@ sections: - FileAssert-Validation-ZipTest - FileAssert-Validation-NullContext - FileAssert-Validation-Depth + - FileAssert-Utilities-TemporaryDirectory tests: - SelfTest_Run_ExecutesBuiltInTestsAndProducesSummary - SelfTest_Run_WhenInvoked_PrintsSystemInfoHeader diff --git a/docs/reqstream/file-assert/utilities.yaml b/docs/reqstream/file-assert/utilities.yaml index 5433c4f..3f581e7 100644 --- a/docs/reqstream/file-assert/utilities.yaml +++ b/docs/reqstream/file-assert/utilities.yaml @@ -17,6 +17,7 @@ sections: utility functions are accessible to and correctly used by dependent subsystems. children: - FileAssert-PathHelpers-SafeCombine + - FileAssert-PathHelpers-RejectRootedPaths - FileAssert-PathHelpers-NullValidation tests: - Utilities_SafePathCombine_PreventsPathTraversalToFileSystem @@ -31,6 +32,7 @@ sections: children: - FileAssert-TemporaryDirectory-Lifecycle - FileAssert-TemporaryDirectory-SafePath + - FileAssert-Utilities-SafePathOperations tests: - Utilities_TemporaryDirectory_IsolatesAndCleansUpScratchSpace @@ -47,6 +49,7 @@ sections: - FileAssert-DirectoryFileContainer-FileSystemAccess - FileAssert-ZipFileContainer-EntryEnumeration - FileAssert-ZipFileContainer-BreadcrumbDisplayPath + - FileAssert-ZipFileContainer-OpenEntry - FileAssert-ZipFileContainer-MissingEntryException - FileAssert-ZipFileContainer-EntrySize tests: diff --git a/docs/reqstream/quality.yaml b/docs/reqstream/quality.yaml new file mode 100644 index 0000000..dd005be --- /dev/null +++ b/docs/reqstream/quality.yaml @@ -0,0 +1,160 @@ +--- +# Repository quality requirements +# +# These requirements express repository/process-level quality outcomes that are +# satisfied through the CI pipeline's use of off-the-shelf compliance and +# documentation tools. They are distinct from the FileAssert product requirements +# in file-assert.yaml: each requirement here states WHAT quality outcome the +# repository needs (e.g. "changes are peer reviewed"), never HOW that outcome is +# currently achieved. The OTS tool that currently satisfies each outcome is +# recorded only as a `children` link, never in the requirement title or +# justification, so that satisfying the outcome by different or additional means +# in the future would not require rewriting the requirement itself. +sections: + - title: Repository Quality Requirements + requirements: + - id: FileAssert-Quality-BuildIntegrity + title: >- + Every release shall carry a documented record of the CI build that + produced it, captured automatically from pipeline metadata. + justification: | + Regulated and quality-conscious consumers of FileAssert require assurance that + each release artifact was produced by a specific, auditable build rather than + an ad-hoc or undocumented process. Capturing build provenance (workflow run + identity, commit, timestamp) automatically as part of the CI pipeline, rather + than relying on manual record-keeping, ensures the record is always present, + accurate, and resistant to after-the-fact tampering. + This requirement is satisfied by its children: it is a non-requirement grouping + node, and verification evidence is carried by the atomic child requirement's tests. + tags: [quality] + children: + - FileAssert-OTS-BuildMark + + - id: FileAssert-Quality-TraceableVersions + title: >- + Every release shall have version information traceable to its source + commit and build. + justification: | + Consumers and auditors need to know exactly which tool and dependency + versions produced a given release artifact, to reproduce builds, diagnose + defects, and confirm license and security compliance. Capturing version + information automatically as part of the release pipeline prevents version + drift and the manual record-keeping errors that would otherwise undermine + traceability. + This requirement is satisfied by its children: it is a non-requirement grouping + node, and verification evidence is carried by the atomic child requirement's tests. + tags: [quality] + children: + - FileAssert-OTS-VersionMark + + - id: FileAssert-Quality-StaticAnalysis + title: >- + Code shall be automatically scanned for defects, vulnerabilities, and + quality regressions before merge. + justification: | + Manual code review alone cannot reliably catch every security vulnerability, + code smell, or quality regression introduced by a change. Automated static + analysis on every pull request, with results captured as auditable reports, + provides continuous, objective quality and security assurance that scales + with the size of the codebase and does not depend solely on reviewer + attentiveness. + This requirement is satisfied by its children: it is a non-requirement grouping + node, and verification evidence is carried by each atomic child requirement's tests. + tags: [quality] + children: + - FileAssert-OTS-SarifMark + - FileAssert-OTS-SonarMark + + - id: FileAssert-Quality-PeerReview + title: >- + Every change shall receive documented, enforced peer review evidence + before merge. + justification: | + Peer review is a core defense against defects and unintended behavior + reaching production, but review activity that is not tracked cannot be + audited or enforced. Generating a review plan identifying the reviews + required for a change, and a review report documenting the reviews actually + completed, provides objective, auditable evidence that the review process + was followed, supporting compliance and quality assurance activities. + This requirement is satisfied by its children: it is a non-requirement grouping + node, and verification evidence is carried by each atomic child requirement's tests. + tags: [quality] + children: + - FileAssert-OTS-ReviewMark + + - id: FileAssert-Quality-DocumentationGeneration + title: >- + Repository documentation shall be generated in reviewable and + distributable formats and validated before release. + justification: | + Design, verification, review, and user-guide documentation authored in + Markdown must be converted into distributable formats (HTML for review, + PDF for archival and distribution) as part of the release pipeline, and + each generated document must be validated to confirm it exists, is + well-formed, and contains the expected content, so that a broken + conversion step cannot silently ship incomplete or corrupt compliance + documentation. Because this repository builds FileAssert itself, an + earlier released version of the tool is used in CI to validate the + generated documentation (a project cannot treat its own package as OTS). + This requirement is satisfied by its children: it is a non-requirement grouping + node, and verification evidence is carried by each atomic child requirement's tests. + tags: [quality] + children: + - FileAssert-OTS-Pandoc + - FileAssert-OTS-WeasyPrint + - FileAssert-Shared-FileAssert-Results + - FileAssert-Shared-FileAssert-File + - FileAssert-Shared-FileAssert-Text + - FileAssert-Shared-FileAssert-Html + - FileAssert-Shared-FileAssert-Pdf + + - id: FileAssert-Quality-TestInfrastructure + title: >- + The project's own developer test suite shall execute automatically + and report results in every CI run. + justification: | + Requirements traceability depends on evidence that tests actually executed + and passed; that evidence has no value unless the test framework reliably + discovers, runs, and reports every test method on every CI run, across all + supported platforms and .NET versions, without manual intervention. + This requirement is satisfied by its children: it is a non-requirement grouping + node, and verification evidence is carried by each atomic child requirement's tests. + tags: [quality] + children: + - FileAssert-OTS-xUnit-Discover + - FileAssert-OTS-xUnit-Execute + - FileAssert-OTS-xUnit-Report + + - id: FileAssert-Quality-RequirementsTraceability + title: >- + Every requirement shall be linked to passing tests and enforced as part + of the release pipeline. + justification: | + Requirements that are not linked to verifying tests, or whose tests are not + checked as part of the pipeline, provide no real assurance that the software + behaves as documented. Automatically enforcing requirements-to-test + traceability on every CI run ensures untested or orphaned requirements are + caught before release rather than discovered later. + This requirement is satisfied by its children: it is a non-requirement grouping + node, and verification evidence is carried by the atomic child requirement's tests. + tags: [quality] + children: + - FileAssert-OTS-ReqStream + + - id: FileAssert-Quality-ArchitectureTraceability + title: >- + The project's software structure shall be modeled, validated, and + rendered independent of source code, so its architecture can be + queried and reviewed without reading implementation. + justification: | + Reviewers, auditors, and automated agents need to understand system + structure and responsibilities without reading through source code line by + line. Maintaining an explicit architecture model that is automatically + validated for structural correctness and rendered as diagrams embedded in + design documentation ensures the documented architecture remains + internally consistent and stays usable as a standalone reference. + This requirement is satisfied by its children: it is a non-requirement grouping + node, and verification evidence is carried by each atomic child requirement's tests. + tags: [quality] + children: + - FileAssert-OTS-SysML2Tools diff --git a/docs/sysml2/model/file-assert/cli/i-context.sysml b/docs/sysml2/model/file-assert/cli/i-context.sysml index c561493..7b7b8de 100644 --- a/docs/sysml2/model/file-assert/cli/i-context.sysml +++ b/docs/sysml2/model/file-assert/cli/i-context.sysml @@ -1,9 +1,9 @@ package FileAssert { part def IContext { - doc /* Output contract interface for reporting assertion results and scoping. */ + doc /* Output contract interface for reporting assertion results and errors. */ comment sourceRef /* Source: src/DemaConsulting.FileAssert/Cli/IContext.cs */ - comment testRef /* Test: test/DemaConsulting.FileAssert.Tests/Cli/ScopedContextTests.cs */ + comment testRef /* Test: test/DemaConsulting.FileAssert.Tests/Cli/ContextTests.cs */ comment designRef /* Design: docs/design/file-assert/cli/i-context.md */ comment verificationRef /* Verification: docs/verification/file-assert/cli/i-context.md */ comment reqRef /* Requirements: docs/reqstream/file-assert/cli/i-context.yaml */ diff --git a/docs/verification/file-assert/cli.md b/docs/verification/file-assert/cli.md index cc3c62b..deef362 100644 --- a/docs/verification/file-assert/cli.md +++ b/docs/verification/file-assert/cli.md @@ -8,7 +8,7 @@ that together verify the `Cli` subsystem requirements. The `Cli` subsystem boundary is verified by integration tests defined in `CliTests.cs`. Each test exercises the `Cli` subsystem's public surface — primarily `Context.Create` and the -`Context` instance methods (`WriteLine`, `WriteError`, `WithPrefix`) — rather than +`Context` instance methods (`WriteLine`, `WriteError`) — rather than `Program.Run`. Tests pass controlled argument arrays and assert on captured console output, file system side-effects, and exit codes. @@ -90,24 +90,3 @@ through `Context.Create`. called with a message. **Expected**: The message appears on standard output; exit code is 0. - -### ScopedContext Verification - -The `ScopedContext` implementation (returned by `Context.WithPrefix`) is verified by unit tests -defined in `ScopedContextTests.cs`. Each test exercises prefix creation, error propagation, and -multi-level nesting. - -#### ScopedContext Test Scenarios - -- **Context_WithPrefix_ReturnsNonNullScopedContext** – confirms that `WithPrefix` returns a - non-null `IContext` instance. -- **Context_WithPrefix_NullPrefix_ThrowsArgumentNullException** – confirms `ArgumentNullException` - for a null prefix. -- **ScopedContext_WriteError_PropagatesExitCodeToRoot** – confirms that an error written via a - scoped context increments the root context's `ExitCode` and `ErrorCount`. -- **ScopedContext_WriteLine_DoesNotSetError** – confirms that informational output via a scoped - context does not set any error state on the root context. -- **ScopedContext_Nested_WriteError_PropagatesExitCodeToRoot** – confirms that errors propagate - through two levels of `WithPrefix` nesting to the root context. -- **ScopedContext_MultipleErrors_AllAccumulateOnRoot** – confirms that errors from two separate - scoped contexts and a direct root `WriteError` call all accumulate on the root `ErrorCount`. diff --git a/docs/verification/file-assert/cli/i-context.md b/docs/verification/file-assert/cli/i-context.md index 67af67d..b471934 100644 --- a/docs/verification/file-assert/cli/i-context.md +++ b/docs/verification/file-assert/cli/i-context.md @@ -1,15 +1,16 @@ ### IContext Verification -This document describes the unit-level verification design for the `IContext` interface and its -`ScopedContext` implementation. It defines the test scenarios, dependency usage, and requirement -coverage for `Cli/IContext.cs` and the nested `ScopedContext` class inside `Cli/Context.cs`. +This document describes the unit-level verification design for the `IContext` interface. It +defines the test scenarios, dependency usage, and requirement coverage for `Cli/IContext.cs`. #### Verification Approach -`IContext` and `ScopedContext` are verified with unit tests defined in `ScopedContextTests.cs`. -Tests exercise `Context.WithPrefix`, error propagation from scoped contexts to the root context, -and multi-level nesting. No mocking or test doubles are needed because the tests operate directly -on a `Context` instance created with `["--silent"]` to suppress console output. +`IContext` has no implementation of its own; it is verified indirectly through `Context`, its +sole implementer. Tests defined in `ContextTests.cs` exercise the `WriteLine` and `WriteError` +contract members via the concrete `Context` instance, confirming that output/error reporting +and exit-code state behave as `IContext` consumers (the asserters) expect. No mocking or test +doubles are needed at this level because the tests operate directly on a `Context` instance +created with the standard `Context.Create` factory. #### Test Environment @@ -26,45 +27,19 @@ meets the project minimum threshold. #### Dependencies -`ScopedContext` depends on `Context` for error accumulation. No external dependencies -require mocking at this level. +No external dependencies require mocking at this level. #### Test Scenarios -##### Context_WithPrefix_ReturnsNonNullScopedContext +##### Context_WriteLine_NotSilent_WritesToConsole -**Scenario**: `context.WithPrefix("archive.zip")` is called on a valid root context. +**Scenario**: `context.WriteLine("Test message")` is called on a context created without +`--silent`. -**Expected**: The returned `IContext` instance is not null. +**Expected**: The message appears on console standard output. -##### Context_WithPrefix_NullPrefix_ThrowsArgumentNullException +##### Context_WriteError_SetsErrorExitCode -**Scenario**: `context.WithPrefix(null!)` is called on a valid root context. +**Scenario**: `context.WriteError("Test error message")` is called on a valid context. -**Expected**: An `ArgumentNullException` is thrown. - -**Boundary / error path**: Null argument guard. - -##### ScopedContext_WriteError_PropagatesExitCodeToRoot - -**Scenario**: An error is written via a scoped context derived from a root context. - -**Expected**: `context.ExitCode` is `1` and `context.ErrorCount` is `1`. - -##### ScopedContext_WriteLine_DoesNotSetError - -**Scenario**: An informational message is written via a scoped context. - -**Expected**: `context.ExitCode` is `0` and `context.ErrorCount` is `0`. - -##### ScopedContext_Nested_WriteError_PropagatesExitCodeToRoot - -**Scenario**: Two levels of `WithPrefix` are applied; an error is written via the deepest scope. - -**Expected**: `context.ExitCode` is `1` and `context.ErrorCount` is `1`. - -##### ScopedContext_MultipleErrors_AllAccumulateOnRoot - -**Scenario**: Two separate scoped contexts and the root context each write one error. - -**Expected**: `context.ErrorCount` is `3` and `context.ExitCode` is `1`. +**Expected**: `context.ExitCode` is `1`. diff --git a/requirements.yaml b/requirements.yaml index 5d792e7..529f715 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -1,7 +1,9 @@ --- # Root requirements file - includes all subsystem, platform, and OTS requirements +root-tags: [system, quality] includes: - docs/reqstream/file-assert.yaml + - docs/reqstream/quality.yaml - docs/reqstream/file-assert/program.yaml - docs/reqstream/file-assert/cli.yaml - docs/reqstream/file-assert/cli/context.yaml diff --git a/src/DemaConsulting.FileAssert/Cli/Context.cs b/src/DemaConsulting.FileAssert/Cli/Context.cs index b21ac9c..9c909d6 100644 --- a/src/DemaConsulting.FileAssert/Cli/Context.cs +++ b/src/DemaConsulting.FileAssert/Cli/Context.cs @@ -359,26 +359,6 @@ public void WriteError(string message) _logWriter?.WriteLine(message); } - /// - /// Returns a new scoped context that prepends "{prefix} > " to every - /// message. - /// - /// - /// ScopedContext is used by FileAssertZipAssert to route nested asserter errors - /// through a breadcrumb prefix that identifies the zip archive entry being tested. - /// The scoped context delegates state (error flag and counter) to the root Context - /// so that error accumulation remains consistent across nested contexts. - /// - /// The prefix to prepend to error messages. Must not be null. - /// A new that prepends to errors. - /// Thrown when is null. - public IContext WithPrefix(string prefix) - { - // Validate the prefix before constructing the scoped context - ArgumentNullException.ThrowIfNull(prefix); - return new ScopedContext(this, prefix); - } - /// /// Disposes resources used by the Context. /// @@ -388,81 +368,4 @@ public void Dispose() _logWriter?.Dispose(); _logWriter = null; } - - /// - /// A scoped context wrapper that prepends a path prefix to every - /// message. - /// - /// - /// ScopedContext delegates all output and state to the parent IContext, ensuring - /// that error counters and exit-code logic remain in the root Context. It is used - /// by FileAssertZipAssert to inject breadcrumb context into error messages without - /// requiring the calling asserter to know about the scoping mechanism. - /// - private sealed class ScopedContext : IContext - { - /// - /// The parent context that owns the error flag and counter. - /// - private readonly IContext _parent; - - /// - /// The prefix prepended to every error message. - /// - private readonly string _prefix; - - /// - /// Initializes a new instance of the class. - /// - /// The parent context. Must not be null. - /// The prefix to prepend to error messages. Must not be null. - /// - /// Thrown when or is null. - /// - internal ScopedContext(IContext parent, string prefix) - { - // Validate required dependencies before storing them - ArgumentNullException.ThrowIfNull(parent); - ArgumentNullException.ThrowIfNull(prefix); - - _parent = parent; - _prefix = prefix; - } - - /// - /// Writes a line of informational output by delegating to the parent context. - /// - /// The message to write. - public void WriteLine(string message) - { - // Delegate informational output unchanged — prefix applies only to errors - _parent.WriteLine(message); - } - - /// - /// Writes an error message to the parent context, prepending the path prefix. - /// - /// The error message to write. - public void WriteError(string message) - { - // Prepend the path prefix to give the error a navigation breadcrumb - _parent.WriteError($"{_prefix} > {message}"); - } - - /// - /// Returns a new scoped context that nests this context's prefix with - /// an additional level. - /// - /// The additional prefix segment. Must not be null. - /// A new with the combined prefix. - /// Thrown when is null. - public IContext WithPrefix(string prefix) - { - // Validate before chaining a new level - ArgumentNullException.ThrowIfNull(prefix); - - // Chain a new ScopedContext that builds on this context's already-prefixed output - return new ScopedContext(this, prefix); - } - } } diff --git a/src/DemaConsulting.FileAssert/Cli/IContext.cs b/src/DemaConsulting.FileAssert/Cli/IContext.cs index 47695e6..abe375b 100644 --- a/src/DemaConsulting.FileAssert/Cli/IContext.cs +++ b/src/DemaConsulting.FileAssert/Cli/IContext.cs @@ -24,10 +24,9 @@ namespace DemaConsulting.FileAssert.Cli; /// Defines the output contract for reporting assertion results and errors. /// /// -/// IContext is implemented by Context (the root context) and Context.ScopedContext (a -/// scoped wrapper that prepends a path prefix to all error messages). Accepting IContext -/// in Run methods allows FileAssertZipAssert to pass a scoped context to nested asserters -/// without requiring those asserters to know about the scoping mechanism. +/// IContext is implemented by Context. Accepting IContext in Run methods allows +/// asserters to report output and errors without depending on the concrete Context +/// implementation. /// internal interface IContext { @@ -42,13 +41,4 @@ internal interface IContext /// /// The error message to write. void WriteError(string message); - - /// - /// Returns a new scoped context that prepends "{prefix} > " to every - /// message. - /// - /// The prefix to prepend to all error messages. Must not be null. - /// A new scoped context delegating state to this context. - /// Thrown when is null. - IContext WithPrefix(string prefix); } diff --git a/test/DemaConsulting.FileAssert.Tests/Cli/ScopedContextTests.cs b/test/DemaConsulting.FileAssert.Tests/Cli/ScopedContextTests.cs deleted file mode 100644 index 7e878c2..0000000 --- a/test/DemaConsulting.FileAssert.Tests/Cli/ScopedContextTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) DEMA Consulting -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -using DemaConsulting.FileAssert.Cli; - -namespace DemaConsulting.FileAssert.Tests.Cli; - -/// -/// Unit tests for and the nested ScopedContext class. -/// -[Collection("Sequential")] -public sealed class ScopedContextTests -{ - /// - /// Verifies that WithPrefix returns a non-null scoped context. - /// - [Fact] - public void Context_WithPrefix_ReturnsNonNullScopedContext() - { - // Arrange - using var context = Context.Create(["--silent"]); - - // Act - var scoped = context.WithPrefix("archive.zip"); - - // Assert - Assert.NotNull(scoped); - } - - /// - /// Verifies that WithPrefix throws when prefix is null. - /// - [Fact] - public void Context_WithPrefix_NullPrefix_ThrowsArgumentNullException() - { - // Arrange - using var context = Context.Create(["--silent"]); - - // Act & Assert - Assert.Throws(() => context.WithPrefix(null!)); - } - - /// - /// Verifies that errors written via a scoped context propagate to the root context exit code. - /// - [Fact] - public void ScopedContext_WriteError_PropagatesExitCodeToRoot() - { - // Arrange - create a root context; derive a scoped context from it - using var context = Context.Create(["--silent"]); - var scoped = context.WithPrefix("archive.zip"); - - // Act - write an error via the scoped context - scoped.WriteError("some error"); - - // Assert - the root context must reflect the error - Assert.Equal(1, context.ExitCode); - Assert.Equal(1, context.ErrorCount); - } - - /// - /// Verifies that WriteLine written via a scoped context does not set an error on the root. - /// - [Fact] - public void ScopedContext_WriteLine_DoesNotSetError() - { - // Arrange - using var context = Context.Create(["--silent"]); - var scoped = context.WithPrefix("archive.zip"); - - // Act - scoped.WriteLine("informational message"); - - // Assert - no error should be recorded - Assert.Equal(0, context.ExitCode); - Assert.Equal(0, context.ErrorCount); - } - - /// - /// Verifies that nested scoped contexts chain prefixes correctly and still propagate errors. - /// - [Fact] - public void ScopedContext_Nested_WriteError_PropagatesExitCodeToRoot() - { - // Arrange - two levels of scoping - using var context = Context.Create(["--silent"]); - var level1 = context.WithPrefix("outer.zip"); - var level2 = level1.WithPrefix("inner.zip"); - - // Act - level2.WriteError("nested error"); - - // Assert - root reflects the error - Assert.Equal(1, context.ExitCode); - Assert.Equal(1, context.ErrorCount); - } - - /// - /// Verifies that multiple errors from different scoped contexts all accumulate on the root. - /// - [Fact] - public void ScopedContext_MultipleErrors_AllAccumulateOnRoot() - { - // Arrange - using var context = Context.Create(["--silent"]); - var scoped1 = context.WithPrefix("zip1.zip"); - var scoped2 = context.WithPrefix("zip2.zip"); - - // Act - scoped1.WriteError("error in zip1"); - scoped2.WriteError("error in zip2"); - context.WriteError("direct error"); - - // Assert - all three errors must be counted - Assert.Equal(3, context.ErrorCount); - Assert.Equal(1, context.ExitCode); - } -} diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertHtmlAssertTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertHtmlAssertTests.cs index 0d06859..acf1436 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertHtmlAssertTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertHtmlAssertTests.cs @@ -479,9 +479,6 @@ public void WriteLine(string message) { } /// public void WriteError(string message) => _errors.Add(message); - - /// - public IContext WithPrefix(string prefix) => this; } /// diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertJsonAssertTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertJsonAssertTests.cs index f088e47..d79870d 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertJsonAssertTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertJsonAssertTests.cs @@ -408,9 +408,6 @@ public void WriteLine(string message) { } /// public void WriteError(string message) => _errors.Add(message); - - /// - public IContext WithPrefix(string prefix) => this; } /// diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertYamlAssertTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertYamlAssertTests.cs index 93c1d91..e556e87 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertYamlAssertTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertYamlAssertTests.cs @@ -462,8 +462,5 @@ public void WriteLine(string message) { } /// public void WriteError(string message) => _errors.Add(message); - - /// - public IContext WithPrefix(string prefix) => this; } } diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertZipAssertTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertZipAssertTests.cs index 8838afe..e89ad67 100644 --- a/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertZipAssertTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertZipAssertTests.cs @@ -178,8 +178,6 @@ private static void CreateZipFileWithBinaryEntry(string path, string entryName, /// /// A test-only implementation that captures all error messages /// written via for inspection in breadcrumb tests. - /// chains a scoped wrapper that mirrors the behavior of - /// Context.ScopedContext so that the full breadcrumb path is accumulated. /// private sealed class CapturingContext : IContext { @@ -193,31 +191,6 @@ public void WriteLine(string message) { } /// public void WriteError(string message) => _errors.Add(message); - - /// - public IContext WithPrefix(string prefix) => new PrefixedContext(this, prefix); - - /// - /// Scoped wrapper that prepends a prefix to each error before delegating to the - /// parent context. Mirrors the behavior of Context.ScopedContext. - /// - private sealed class PrefixedContext : IContext - { - private readonly IContext _parent; - private readonly string _prefix; - - internal PrefixedContext(IContext parent, string prefix) - { - _parent = parent; - _prefix = prefix; - } - - public void WriteLine(string message) => _parent.WriteLine(message); - - public void WriteError(string message) => _parent.WriteError($"{_prefix} > {message}"); - - public IContext WithPrefix(string prefix) => new PrefixedContext(this, prefix); - } } ///