Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
]
},
"demaconsulting.reqstream": {
"version": "1.10.0",
"version": "1.11.0",
"commands": [
"reqstream"
]
Expand Down
1 change: 0 additions & 1 deletion docs/design/file-assert/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
31 changes: 4 additions & 27 deletions docs/design/file-assert/cli/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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. |

Expand All @@ -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

Expand All @@ -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.
63 changes: 7 additions & 56 deletions docs/design/file-assert/cli/i-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -28,74 +22,31 @@ internal interface IContext
{
void WriteLine(string message);
void WriteError(string message);
IContext WithPrefix(string prefix);
}
```

| Member | Description |
| :----------------------------- | :-------------------------------------------------------------------------------- |
| `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

- `FileAssertFile.Run` — accepts `IContext` instead of `Context`.
- 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.
6 changes: 3 additions & 3 deletions docs/design/file-assert/modeling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 4 additions & 6 deletions docs/design/file-assert/modeling/file-assert-zip-assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
Loading