From d9aa10bfd034311442b598d828b5ba6fbaccc7b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 20 May 2026 17:45:05 +0200 Subject: [PATCH 1/2] docs: document missing features --- README.md | 329 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 207 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index bc6aacd..89b2284 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,17 @@ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Testably_aweXpect.Testably&metric=coverage)](https://sonarcloud.io/summary/overall?id=Testably_aweXpect.Testably) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FTestably%2FaweXpect.Testably%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/Testably/aweXpect.Testably/main) -Expectations for the file and time system -from [Testably.Abstractions](https://github.com/Testably/Testably.Abstractions). +Drop-in [aweXpect](https://github.com/aweXpect/aweXpect) expectations for the +file-system and time-system mocks from +[Testably.Abstractions](https://github.com/Testably/Testably.Abstractions): +`MockFileSystem`, `IFileInfo`, `IDirectoryInfo`, `IDriveInfo`, +`IFileVersionInfo`, `IFileSystemWatcher`, `IFileSystemStatistics` and +`ITimerMock`. -## File system +## File system (`IFileSystem`) -You can verify that a specific file or directory exists in the file system: +Verify that a file, directory or drive is present in the file system. Every +positive assertion has a `DoesNot…` counterpart: ```csharp IFileSystem fileSystem = new MockFileSystem(); @@ -20,11 +25,15 @@ fileSystem.File.WriteAllText("my-file.txt", "some content"); await That(fileSystem).HasDirectory("my/path"); await That(fileSystem).HasFile("my-file.txt"); + +await That(fileSystem).DoesNotHaveDirectory("not/here"); +await That(fileSystem).DoesNotHaveFile("missing.txt"); ``` -### File +### File chain -For files, you can verify the file content: +`HasFile(path)` returns a result that lets you chain assertions about the +file's content and timestamps without re-resolving it: ```csharp IFileSystem fileSystem = new MockFileSystem(); @@ -32,13 +41,12 @@ fileSystem.File.WriteAllText("my-file.txt", "some content"); await That(fileSystem).HasFile("my-file.txt").WithContent("some content").IgnoringCase(); await That(fileSystem).HasFile("my-file.txt").WithContent().NotEqualTo("some unexpected content"); +await That(fileSystem).HasFile("my-file.txt").WithContent(new byte[] { 0x73, 0x6F, 0x6D, 0x65 }); ``` -You can also verify the file content with regard to another file: +You can compare against another file on the same file system: ```csharp -IFileSystem fileSystem = new MockFileSystem(); -fileSystem.File.WriteAllText("my-file.txt", "some content"); fileSystem.File.WriteAllText("my-other-file.txt", "SOME CONTENT"); fileSystem.File.WriteAllText("my-third-file.txt", "some other content"); @@ -46,108 +54,141 @@ await That(fileSystem).HasFile("my-file.txt").WithContent().SameAs("my-other-fil await That(fileSystem).HasFile("my-file.txt").WithContent().NotSameAs("my-third-file.txt"); ``` -For files, you can verify the creation time, last access time and last write time: +…and against the file's timestamps. `.Within(tolerance)` widens the comparison +to a window: ```csharp -IFileSystem fileSystem = new MockFileSystem(); -fileSystem.File.WriteAllText("my-file.txt", "some content"); - -await That(sut).HasFile(path).WithCreationTime(DateTime.Now).Within(1.Second()); -await That(sut).HasFile(path).WithLastAccessTime(DateTime.Now).Within(1.Second()); -await That(sut).HasFile(path).LastWriteTime(DateTime.Now).Within(1.Second()); +await That(fileSystem).HasFile("my-file.txt").WithCreationTime(DateTime.Now).Within(1.Second()); +await That(fileSystem).HasFile("my-file.txt").WithLastAccessTime(DateTime.Now).Within(1.Second()); +await That(fileSystem).HasFile("my-file.txt").WithLastWriteTime(DateTime.Now).Within(1.Second()); ``` -### Directory +### Directory chain -For directories, you can verify that they contain subdirectories: +`HasDirectory(path)` exposes sub-collections: ```csharp IFileSystem fileSystem = new MockFileSystem(); fileSystem.Directory.CreateDirectory("foo/bar1"); fileSystem.Directory.CreateDirectory("foo/bar2/baz"); - -await That(fileSystem).HasDirectory("foo").WithDirectories(f => f.HasCount().EqualTo(2)); -``` - -For directories, you can verify that they contain files: - -```csharp -IFileSystem fileSystem = new MockFileSystem(); -fileSystem.Directory.CreateDirectory("foo/bar"); fileSystem.File.WriteAllText("foo/bar/my-file.txt", "some content"); -await That(fileSystem).HasDirectory("foo/bar").WithFiles(f => f.All().ComplyWith(x => x.HasContent("SOME CONTENT").IgnoringCase())); +await That(fileSystem).HasDirectory("foo").WithDirectories(d => d.HasCount().EqualTo(2)); +await That(fileSystem).HasDirectory("foo/bar").WithFiles(f => f + .All().ComplyWith(x => x.HasContent("SOME CONTENT").IgnoringCase())); ``` -### Drives +### Bridging to `IFileInfo` / `IDirectoryInfo` / `IDriveInfo` via `.Which` -You can verify that a drive is registered on the file system. Drives are -matched by name (case-insensitive) against `IFileSystem.DriveInfo.GetDrives()`; -UNC drives (which do not appear in `GetDrives()`) are not supported by -`HasDrive`: +`HasFile`, `HasDirectory` and `HasDrive` each expose a `.Which` property that +returns the resolved `IFileInfo` / `IDirectoryInfo` / `IDriveInfo` so the +subject-level assertions below light up directly in the chain: ```csharp -MockFileSystem fileSystem = new(o => o.SimulatingOperatingSystem(SimulationMode.Windows)); -fileSystem.WithDrive("D:", d => d.SetTotalSize(2048)); - -await That(fileSystem).HasDrive("D:\\"); -await That(fileSystem).DoesNotHaveDrive("Z:\\"); +await That(fileSystem).HasFile("my-file.txt").Which.HasLength(12).And.HasContent("some content"); +await That(fileSystem).HasDirectory("logs").Which.IsEmpty(); +await That(fileSystem).HasDrive("D:\\").Which.IsReady().And.HasDriveFormat("NTFS"); ``` -`HasDrive` exposes a `.Which` property returning `IThat`, so all -`IDriveInfo` assertions can be chained directly: +## File (`IFileInfo`) ```csharp -await That(fileSystem).HasDrive("D:\\") - .Which.HasTotalSize(2048).And.IsReady(); -``` +IFileInfo fileInfo = fileSystem.FileInfo.New("my-file.txt"); -You can also assert directly on `IDriveInfo` instances: +await That(fileInfo).Exists(); +await That(fileInfo).DoesNotExist(); -```csharp -IDriveInfo driveInfo = fileSystem.DriveInfo.New("D:"); +await That(fileInfo).HasName("my-file.txt"); +await That(fileInfo).HasExtension(".txt"); +await That(fileInfo).HasLength(12); +await That(fileInfo).HasContent("some content"); +await That(fileInfo).HasContent(new byte[] { 0x73, 0x6F, 0x6D, 0x65 }); -await That(driveInfo).HasAvailableFreeSpace(2048); -await That(driveInfo).HasTotalSize(2048).And.HasTotalFreeSpace(2048); -await That(driveInfo).HasDriveFormat("NTFS"); -await That(driveInfo).HasDriveType(System.IO.DriveType.Fixed); -await That(driveInfo).HasName(driveInfo.Name).And.HasVolumeLabel(driveInfo.VolumeLabel); -await That(driveInfo).IsReady(); +await That(fileInfo).IsReadOnly(); +await That(fileInfo).IsNotReadOnly(); + +await That(fileInfo).HasAttribute(FileAttributes.ReadOnly); +await That(fileInfo).DoesNotHaveAttribute(FileAttributes.Hidden); + +await That(fileInfo).HasCreationTime(DateTime.Now).Within(1.Second()); +await That(fileInfo).HasLastAccessTime(DateTime.Now).Within(1.Second()); +await That(fileInfo).HasLastWriteTime(DateTime.Now).Within(1.Second()); ``` -### IFileInfo / IDirectoryInfo as subjects +`HasAttribute` / `DoesNotHaveAttribute` use flag containment, so +`FileAttributes.ReadOnly | FileAttributes.Hidden` satisfies +`HasAttribute(FileAttributes.ReadOnly)`. The empty / `default` value is +rejected with an `ArgumentException` to avoid silent passes. -You can also assert directly on `IFileInfo` and `IDirectoryInfo` instances: +On .NET 10 or later, `WhoseParent` switches the subject to the containing +directory so the directory-level assertions can be reused: ```csharp -IFileInfo fileInfo = fileSystem.FileInfo.New("my-file.txt"); -await That(fileInfo).HasLength(12).And.HasContent("some content"); -await That(fileInfo).HasName("my-file.txt").And.HasExtension(".txt"); -await That(fileInfo).Exists(); +await That(fileInfo).WhoseParent.HasName("docs").And.IsNotEmpty(); +``` + +## Directory (`IDirectoryInfo`) +```csharp IDirectoryInfo dirInfo = fileSystem.DirectoryInfo.New("foo"); + +await That(dirInfo).Exists(); +await That(dirInfo).DoesNotExist(); + +await That(dirInfo).HasName("foo"); + +await That(dirInfo).IsEmpty(); await That(dirInfo).IsNotEmpty(); + await That(dirInfo).HasFile("bar/my-file.txt"); +await That(dirInfo).DoesNotHaveFile("bar/missing.txt"); await That(dirInfo).HasDirectory("bar").Which.HasFile("my-file.txt"); +await That(dirInfo).DoesNotHaveDirectory("not-here"); + +await That(dirInfo).HasAttribute(FileAttributes.Directory); +await That(dirInfo).DoesNotHaveAttribute(FileAttributes.Hidden); + +await That(dirInfo).HasCreationTime(DateTime.Now).Within(1.Second()); +await That(dirInfo).HasLastAccessTime(DateTime.Now).Within(1.Second()); +await That(dirInfo).HasLastWriteTime(DateTime.Now).Within(1.Second()); ``` -### Bridging from the file-system chain via `.Which` +`HasFile` / `HasDirectory` on `IDirectoryInfo` return the same chain +results as the file-system-level versions, so `.WithContent(...)`, +`.WithLastWriteTime(...)`, `.Which`, etc. all work as well — the path is +resolved relative to the directory. -`HasFile`, `HasDirectory` and `HasDrive` expose a `.Which` property that returns -the `IThat` / `IThat` / `IThat` for the -resolved entry, so the same assertions light up in both places: +On .NET 10 or later, `WhoseParent` switches to the parent directory: ```csharp -await That(fileSystem).HasFile("my-file.txt").Which.HasLength(12).And.HasContent("some content"); -await That(fileSystem).HasDirectory("logs").Which.IsEmpty(); -await That(fileSystem).HasDrive("D:\\").Which.IsReady().And.HasDriveFormat("NTFS"); +await That(dirInfo).WhoseParent.HasName("…"); ``` -### IFileVersionInfo +## Drive (`IDriveInfo`) + +```csharp +MockFileSystem fileSystem = new(o => o.SimulatingOperatingSystem(SimulationMode.Windows)); +fileSystem.WithDrive("D:", d => d.SetTotalSize(2048)); -`IFileVersionInfo` instances obtained via `MockFileSystem.FileVersionInfo.GetVersionInfo` -can be asserted directly. The configured values come from -`MockFileSystem.WithFileVersionInfo(glob, builder)`: +IDriveInfo driveInfo = fileSystem.DriveInfo.New("D:"); + +await That(driveInfo).HasAvailableFreeSpace(2048); +await That(driveInfo).HasTotalSize(2048).And.HasTotalFreeSpace(2048); +await That(driveInfo).HasDriveFormat("NTFS"); +await That(driveInfo).HasDriveType(DriveType.Fixed); +await That(driveInfo).HasName(driveInfo.Name).And.HasVolumeLabel(driveInfo.VolumeLabel); +await That(driveInfo).IsReady(); +``` + +> Drives are matched by name (case-insensitive) against +> `IFileSystem.DriveInfo.GetDrives()`. UNC drives, which do not appear in +> `GetDrives()`, are not supported by `HasDrive`. + +## File version info (`IFileVersionInfo`) + +`IFileVersionInfo` instances obtained via +`MockFileSystem.FileVersionInfo.GetVersionInfo` can be asserted directly. The +values come from `MockFileSystem.WithFileVersionInfo(glob, builder)`: ```csharp MockFileSystem fileSystem = new(); @@ -165,15 +206,27 @@ await That(info).HasFileVersion("1.2.3.4"); await That(info).IsDebug().And.IsNotPreRelease(); ``` -The following common fields have dedicated assertions: -`HasCompanyName`, `HasProductName`, `HasFileDescription`, `HasFileVersion`, -`HasProductVersion`, `HasOriginalFilename`, `HasLanguage`, plus the bool pairs -`IsDebug` / `IsNotDebug`, `IsPreRelease` / `IsNotPreRelease`, -`IsPatched` / `IsNotPatched`. For the remaining properties (e.g. `Comments`, -`LegalCopyright`, `FileMajorPart`), assert on them directly via -`await That(info.LegalCopyright).IsEqualTo("…")`. - -### Notifications +Dedicated assertions exist for the common fields — `HasCompanyName`, +`HasProductName`, `HasFileDescription`, `HasFileVersion`, `HasProductVersion`, +`HasOriginalFilename`, `HasLanguage` — plus the boolean pairs +`IsDebug` / `IsNotDebug`, `IsPreRelease` / `IsNotPreRelease` and +`IsPatched` / `IsNotPatched`. + +| Property | Assertion | +|-----------------------|--------------------------------------------| +| `CompanyName` | `HasCompanyName(string)` | +| `ProductName` | `HasProductName(string)` | +| `FileDescription` | `HasFileDescription(string)` | +| `FileVersion` | `HasFileVersion(string)` | +| `ProductVersion` | `HasProductVersion(string)` | +| `OriginalFilename` | `HasOriginalFilename(string)` | +| `Language` | `HasLanguage(string)` | +| `IsDebug` | `IsDebug()` / `IsNotDebug()` | +| `IsPreRelease` | `IsPreRelease()` / `IsNotPreRelease()` | +| `IsPatched` | `IsPatched()` / `IsNotPatched()` | +| _other_ (e.g. `Comments`, `LegalCopyright`, `FileMajorPart`) | `await That(info.X).Is…` | + +## File-system notifications A `MockFileSystem` raises notifications when files or directories change. Run the code under test, then assert against the notifications it produced: @@ -204,8 +257,10 @@ await That(fileSystem).DidNotTriggerNotification().Within(100.Milliseconds()); await That(fileSystem).DidNotTriggerNotification(c => c.Name == "secret.txt"); ``` -`TriggeredNotification` accepts a `Quantifier` (`AtLeast`, `AtMost`, `Exactly`, -`Between`, `Never`, `Once`) so you can assert how often the notification fires: +Both accept a `Quantifier` (`AtLeast`, `AtMost`, `Exactly`, `Between`, +`Never`, `Once`) so you can assert how often the notification fires, and a +`.Which(c => …)` callback that composes the per-notification expectations +from [`ChangeDescription`](#changedescription): ```csharp fileSystem.File.WriteAllText("a.txt", "x"); @@ -213,14 +268,6 @@ fileSystem.File.WriteAllText("b.txt", "y"); await That(fileSystem).TriggeredNotification(c => c.ChangeType == WatcherChangeTypes.Created) .Exactly(2.Times()); -``` - -Both `TriggeredNotification` and `DidNotTriggerNotification` expose -`.Which(c => …)`, which applies inner expectations from `ChangeDescriptionExtensions` -as an additional per-notification filter — only changes that satisfy them count: - -```csharp -fileSystem.File.WriteAllText("a.txt", "x"); await That(fileSystem) .TriggeredNotification() @@ -233,11 +280,12 @@ await That(fileSystem) > `new MockFileSystem(o => o.WithoutNotificationHistory())` only if you don't > use these assertions — they throw against a history-disabled file system. -### Watcher events +## Watcher events (`IFileSystemWatcher`) An individual `IFileSystemWatcher` can also be the subject. The watcher must come from a `MockFileSystem`, and `EnableRaisingEvents` must be `true` for any -event to be observed: +event to be observed. Only events fired on this specific watcher count — +events fired on other watchers of the same `MockFileSystem` are ignored. ```csharp MockFileSystem fileSystem = new(); @@ -247,30 +295,17 @@ fileSystem.File.WriteAllText("my-file.txt", "some content"); await That(watcher).Triggered(); await That(watcher).Triggered(c => c.Name == "my-file.txt"); -``` - -Only events that originate from this specific watcher count — events fired on -other watchers of the same `MockFileSystem` are ignored. - -`.Within(timeout)` (default 30 s) lets the assertion wait for asynchronous -events: - -```csharp -_ = Task.Run(() => fileSystem.File.WriteAllText("foo.txt", "x")); -await That(watcher).Triggered().Within(100.Milliseconds()); -``` - -`DidNotTrigger` mirrors the same shapes and short-circuits as soon as a -matching event is observed: -```csharp await That(watcher).DidNotTrigger().Within(100.Milliseconds()); await That(watcher).DidNotTrigger(c => c.Name == "secret.txt"); ``` -Both `Triggered` and `DidNotTrigger` accept either a synchronous -`Func` predicate or a `.Which(c => …)` callback -that composes inner expectations from `ChangeDescriptionExtensions`: +`Triggered` and `DidNotTrigger` share the same shape as the +[notification](#file-system-notifications) assertions — a `Quantifier` +(`AtLeast`, `AtMost`, `Exactly`, `Between`, `Never`, `Once`), a +`.Within(timeout)` (default 30 s), and a `.Which(c => …)` callback that +composes the per-change expectations from +[`ChangeDescription`](#changedescription): ```csharp await That(watcher) @@ -279,13 +314,9 @@ await That(watcher) .Exactly(1.Times()); ``` -### `ChangeDescription` as a subject +## `ChangeDescription` -Individual `ChangeDescription` instances can be asserted directly. The -`HasChangeType`, `HasFileSystemType` and `HasNotifyFilters` assertions use flag -containment (so a `LastWrite | FileName` change satisfies -`HasNotifyFilters(NotifyFilters.LastWrite)`); the empty / `default` value is -rejected with an `ArgumentException` to avoid silent passes. +Individual `ChangeDescription` instances can be asserted directly: ```csharp await That(change).HasChangeType(WatcherChangeTypes.Created); @@ -298,12 +329,66 @@ await That(change).HasName("my-file.txt").And.HasPath("/abs/my-file.txt"); await That(renamedChange).HasOldName("old.txt").And.HasOldPath("/abs/old.txt"); ``` -## Time system +`HasChangeType`, `HasFileSystemType` and `HasNotifyFilters` use flag +containment (so a `LastWrite | FileName` change satisfies +`HasNotifyFilters(NotifyFilters.LastWrite)`); the empty / `default` value is +rejected with an `ArgumentException` to avoid silent passes. + +## Recorded calls (`IFileSystemStatistics`) + +`MockFileSystem.Statistics` records every method call and property access on +the mock. `.Recorded()` exposes a fluent mirror over these recordings, so you +can assert what the system under test actually called: + +```csharp +MockFileSystem fileSystem = new(); +fileSystem.File.WriteAllText("foo.txt", "x"); + +await That(fileSystem.Statistics).Recorded().File.WriteAllText().Once(); +await That(fileSystem.Statistics).Recorded().File.WriteAllText(path: p => p == "foo.txt").Once(); +``` + +The mirror has one entry per `IFileSystem` member — `.File`, `.Directory`, +`.FileInfo[path]`, `.DirectoryInfo[path]`, `.DriveInfo`, `.FileStream`, +`.FileSystemWatcher`, `.FileVersionInfo`, `.Path` — with one method per +underlying API and an indexer (`[path]`) for per-instance buckets. Every +result inherits the count vocabulary (`Once`, `Twice`, `Never`, `Exactly`, +`AtLeast`, `AtMost`, `Between`, …). + +Property reads and writes are recorded with `.Get()` / `.Set()`: + +```csharp +fileSystem.FileInfo.New("foo.txt").IsReadOnly = true; + +await That(fileSystem.Statistics).Recorded().FileInfo["foo.txt"].IsReadOnly.Set().Once(); +await That(fileSystem.Statistics).Recorded().DirectoryInfo["foo"].Exists.Get().AtLeast().Once(); +``` + +Each parameter on a mirror method is an optional `Func` predicate +matched **positionally** against the recorded argument: + +- Supplying no predicate (or `null`) skips that position and matches every + overload — `.File.Open()` counts _all_ `Open` invocations regardless of arity. +- A predicate whose position exceeds an overload's arity excludes that + overload — filtering `recursive` on `Directory.Delete` only matches the + two-argument overload. +- A predicate whose type differs from the recorded type at that position + silently excludes that overload — filtering `searchOption` on + `Directory.EnumerateDirectories` never matches the `EnumerationOptions` + overload. + +A handful of methods can't be filtered fully through this positional model +because two overloads place different types at the same recording position +(`File.Open` / `FileInfo.Open` with `FileStreamOptions`, +`FileSystemWatcher.WaitForChanged` with `TimeSpan`); the affected mirror +methods document the limitation in their own xmldoc. + +## Time system (`ITimeSystem`) -### Timers +### Timer (`ITimerMock`) -A `MockTimeSystem` exposes timers as `ITimerMock`. You can assert how often the -timer callback was executed without blocking the test thread: +A `MockTimeSystem` exposes timers as `ITimerMock`. You can assert how often +the timer callback was executed without blocking the test thread: ```csharp MockTimeSystem timeSystem = new(); @@ -314,9 +399,9 @@ await That(timer).Executed(3.Times()).Within(5.Seconds()); ``` `Executed()` accepts a `Quantifier` (`AtLeast`, `AtMost`, `Exactly`, -`Between`, …) and exposes `.Within(timeout)` for asynchronous execution. The -assertion polls `ITimerMock.ExecutionCount` until the quantifier is satisfied -or the timeout expires — 30 seconds by default. +`Between`, `Never`, `Once`) and exposes `.Within(timeout)` for asynchronous +execution. The assertion polls `ITimerMock.ExecutionCount` until the +quantifier is satisfied or the timeout expires — 30 seconds by default. ```csharp await That(timer).Executed().AtLeast(2.Times()).Within(100.Milliseconds()); From 9ec2fbf5b1506c09c3953b2ab74fbbf534456bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 20 May 2026 17:58:23 +0200 Subject: [PATCH 2/2] Fix review issues --- README.md | 35 +++++++++---------- ...eSystemExtensions.TriggeredNotification.cs | 8 ++--- .../FileSystemWatcherExtensions.Triggered.cs | 24 ++++++------- .../DidNotTriggerNotificationResult.cs | 4 +-- .../Results/DidNotTriggerWatcherResult.cs | 2 +- .../Results/TriggeredNotificationResult.cs | 4 +-- .../Results/TriggeredWatcherResult.cs | 4 +-- Source/aweXpect.Testably/TimerExtensions.cs | 2 +- 8 files changed, 40 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 89b2284..e367af4 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ await That(dirInfo).HasLastWriteTime(DateTime.Now).Within(1.Second()); `HasFile` / `HasDirectory` on `IDirectoryInfo` return the same chain results as the file-system-level versions, so `.WithContent(...)`, -`.WithLastWriteTime(...)`, `.Which`, etc. all work as well — the path is +`.WithLastWriteTime(...)`, `.Which`, etc. all work as well: the path is resolved relative to the directory. On .NET 10 or later, `WhoseParent` switches to the parent directory: @@ -206,9 +206,9 @@ await That(info).HasFileVersion("1.2.3.4"); await That(info).IsDebug().And.IsNotPreRelease(); ``` -Dedicated assertions exist for the common fields — `HasCompanyName`, +Dedicated assertions exist for the common fields (`HasCompanyName`, `HasProductName`, `HasFileDescription`, `HasFileVersion`, `HasProductVersion`, -`HasOriginalFilename`, `HasLanguage` — plus the boolean pairs +`HasOriginalFilename`, `HasLanguage`), plus the boolean pairs `IsDebug` / `IsNotDebug`, `IsPreRelease` / `IsNotPreRelease` and `IsPatched` / `IsNotPatched`. @@ -240,8 +240,8 @@ await That(fileSystem).TriggeredNotification(c => c.Name == "my-file.txt"); ``` `.Within(timeout)` (default 30 s) lets the assertion wait for asynchronous -notifications — if a matching notification already fired the assertion -completes synchronously, otherwise it waits up to the timeout for a late +notifications. If a matching notification already fired, the assertion +completes synchronously; otherwise it waits up to the timeout for a late arrival: ```csharp @@ -278,13 +278,13 @@ await That(fileSystem) > Replay of historical notifications relies on the `MockFileSystem` notification > history. Disable it via > `new MockFileSystem(o => o.WithoutNotificationHistory())` only if you don't -> use these assertions — they throw against a history-disabled file system. +> use these assertions: they throw against a history-disabled file system. ## Watcher events (`IFileSystemWatcher`) An individual `IFileSystemWatcher` can also be the subject. The watcher must come from a `MockFileSystem`, and `EnableRaisingEvents` must be `true` for any -event to be observed. Only events fired on this specific watcher count — +event to be observed. Only events fired on this specific watcher count; events fired on other watchers of the same `MockFileSystem` are ignored. ```csharp @@ -301,7 +301,7 @@ await That(watcher).DidNotTrigger(c => c.Name == "secret.txt"); ``` `Triggered` and `DidNotTrigger` share the same shape as the -[notification](#file-system-notifications) assertions — a `Quantifier` +[notification](#file-system-notifications) assertions: a `Quantifier` (`AtLeast`, `AtMost`, `Exactly`, `Between`, `Never`, `Once`), a `.Within(timeout)` (default 30 s), and a `.Which(c => …)` callback that composes the per-change expectations from @@ -348,9 +348,9 @@ await That(fileSystem.Statistics).Recorded().File.WriteAllText().Once(); await That(fileSystem.Statistics).Recorded().File.WriteAllText(path: p => p == "foo.txt").Once(); ``` -The mirror has one entry per `IFileSystem` member — `.File`, `.Directory`, +The mirror has one entry per `IFileSystem` member (`.File`, `.Directory`, `.FileInfo[path]`, `.DirectoryInfo[path]`, `.DriveInfo`, `.FileStream`, -`.FileSystemWatcher`, `.FileVersionInfo`, `.Path` — with one method per +`.FileSystemWatcher`, `.FileVersionInfo`, `.Path`), with one method per underlying API and an indexer (`[path]`) for per-instance buckets. Every result inherits the count vocabulary (`Once`, `Twice`, `Never`, `Exactly`, `AtLeast`, `AtMost`, `Between`, …). @@ -368,24 +368,21 @@ Each parameter on a mirror method is an optional `Func` predicate matched **positionally** against the recorded argument: - Supplying no predicate (or `null`) skips that position and matches every - overload — `.File.Open()` counts _all_ `Open` invocations regardless of arity. + overload, so `.File.Open()` counts _all_ `Open` invocations regardless of arity. - A predicate whose position exceeds an overload's arity excludes that - overload — filtering `recursive` on `Directory.Delete` only matches the + overload, so filtering `recursive` on `Directory.Delete` only matches the two-argument overload. - A predicate whose type differs from the recorded type at that position - silently excludes that overload — filtering `searchOption` on + silently excludes that overload, so filtering `searchOption` on `Directory.EnumerateDirectories` never matches the `EnumerationOptions` overload. A handful of methods can't be filtered fully through this positional model because two overloads place different types at the same recording position (`File.Open` / `FileInfo.Open` with `FileStreamOptions`, -`FileSystemWatcher.WaitForChanged` with `TimeSpan`); the affected mirror -methods document the limitation in their own xmldoc. +`FileSystemWatcher.WaitForChanged` with `TimeSpan`). -## Time system (`ITimeSystem`) - -### Timer (`ITimerMock`) +## Timer (`ITimerMock`) A `MockTimeSystem` exposes timers as `ITimerMock`. You can assert how often the timer callback was executed without blocking the test thread: @@ -401,7 +398,7 @@ await That(timer).Executed(3.Times()).Within(5.Seconds()); `Executed()` accepts a `Quantifier` (`AtLeast`, `AtMost`, `Exactly`, `Between`, `Never`, `Once`) and exposes `.Within(timeout)` for asynchronous execution. The assertion polls `ITimerMock.ExecutionCount` until the -quantifier is satisfied or the timeout expires — 30 seconds by default. +quantifier is satisfied or the timeout expires (30 seconds by default). ```csharp await That(timer).Executed().AtLeast(2.Times()).Within(100.Milliseconds()); diff --git a/Source/aweXpect.Testably/FileSystemExtensions.TriggeredNotification.cs b/Source/aweXpect.Testably/FileSystemExtensions.TriggeredNotification.cs index fc319a8..4223497 100644 --- a/Source/aweXpect.Testably/FileSystemExtensions.TriggeredNotification.cs +++ b/Source/aweXpect.Testably/FileSystemExtensions.TriggeredNotification.cs @@ -20,7 +20,7 @@ public static partial class FileSystemExtensions /// Subscribes via so notifications /// that already fired on this count toward the quantifier. /// The assertion always waits up to a timeout for late-arriving (asynchronous) - /// notifications — 30 seconds by default; use .Within(timeout) to override. + /// notifications (30 seconds by default; use .Within(timeout) to override). /// public static TriggeredNotificationResult TriggeredNotification( this IThat subject) @@ -34,7 +34,7 @@ public static TriggeredNotificationResult TriggeredNotification( /// Subscribes via so notifications /// that already fired on this count toward the quantifier. /// The assertion always waits up to a timeout for late-arriving (asynchronous) - /// notifications — 30 seconds by default; use .Within(timeout) to override. + /// notifications (30 seconds by default; use .Within(timeout) to override). /// public static TriggeredNotificationResult TriggeredNotification( this IThat subject, @@ -56,8 +56,8 @@ public static TriggeredNotificationResult TriggeredNotification( /// /// Subscribes via so any notification /// that already fired on this fails the assertion. The - /// assertion also waits up to a timeout for late-arriving notifications — 30 seconds by - /// default; use .Within(timeout) to lower it when you do not need to wait. The + /// assertion also waits up to a timeout for late-arriving notifications (30 seconds by + /// default; use .Within(timeout) to lower it when you do not need to wait). The /// assertion short-circuits as soon as a matching notification is observed. /// public static DidNotTriggerNotificationResult DidNotTriggerNotification( diff --git a/Source/aweXpect.Testably/FileSystemWatcherExtensions.Triggered.cs b/Source/aweXpect.Testably/FileSystemWatcherExtensions.Triggered.cs index c830873..b72811c 100644 --- a/Source/aweXpect.Testably/FileSystemWatcherExtensions.Triggered.cs +++ b/Source/aweXpect.Testably/FileSystemWatcherExtensions.Triggered.cs @@ -23,10 +23,10 @@ public static class FileSystemWatcherExtensions /// /// Subscribes via , so events /// that already fired on this watcher count toward the quantifier. The assertion always - /// waits up to a timeout for late-arriving (asynchronous) events — 30 seconds by default; - /// use .Within(timeout) to override. - /// The subject must be created from a — calling this on a - /// real-file-system watcher throws . The watcher's + /// waits up to a timeout for late-arriving (asynchronous) events (30 seconds by default; + /// use .Within(timeout) to override). + /// The subject must be created from a (calling this on a + /// real-file-system watcher throws ). The watcher's /// must be for /// any event to be observed. /// @@ -41,10 +41,10 @@ public static TriggeredWatcherResult Triggered( /// /// Subscribes via , so events /// that already fired on this watcher count toward the quantifier. The assertion always - /// waits up to a timeout for late-arriving (asynchronous) events — 30 seconds by default; - /// use .Within(timeout) to override. - /// The subject must be created from a — calling this on a - /// real-file-system watcher throws . The watcher's + /// waits up to a timeout for late-arriving (asynchronous) events (30 seconds by default; + /// use .Within(timeout) to override). + /// The subject must be created from a (calling this on a + /// real-file-system watcher throws ). The watcher's /// must be for /// any event to be observed. /// @@ -68,11 +68,11 @@ public static TriggeredWatcherResult Triggered( /// /// Subscribes via so any event /// that already fired on this watcher fails the assertion. The assertion also waits up to a - /// timeout for late-arriving events — 30 seconds by default; use .Within(timeout) - /// to lower it when you do not need to wait. The assertion short-circuits as soon as a + /// timeout for late-arriving events (30 seconds by default; use .Within(timeout) + /// to lower it when you do not need to wait). The assertion short-circuits as soon as a /// matching event is observed. - /// The subject must be created from a — calling this on a - /// real-file-system watcher throws . The watcher's + /// The subject must be created from a (calling this on a + /// real-file-system watcher throws ). The watcher's /// must be for /// any event to be observed. /// diff --git a/Source/aweXpect.Testably/Results/DidNotTriggerNotificationResult.cs b/Source/aweXpect.Testably/Results/DidNotTriggerNotificationResult.cs index 1c48382..32da45a 100644 --- a/Source/aweXpect.Testably/Results/DidNotTriggerNotificationResult.cs +++ b/Source/aweXpect.Testably/Results/DidNotTriggerNotificationResult.cs @@ -34,8 +34,8 @@ internal DidNotTriggerNotificationResult( /// /// The is applied as an additional per-change filter, so any /// assertions from (e.g. .HasName(...), - /// .HasChangeType(...)) compose naturally — the assertion fails if any notification - /// satisfies all of them. The expectation text is taken from the inner expectation builder, + /// .HasChangeType(...)) compose naturally (the assertion fails if any notification + /// satisfies all of them). The expectation text is taken from the inner expectation builder, /// so it reads like matching has name equal to "foo.txt" rather than the raw lambda /// source. /// diff --git a/Source/aweXpect.Testably/Results/DidNotTriggerWatcherResult.cs b/Source/aweXpect.Testably/Results/DidNotTriggerWatcherResult.cs index 5a3fcad..e932cc6 100644 --- a/Source/aweXpect.Testably/Results/DidNotTriggerWatcherResult.cs +++ b/Source/aweXpect.Testably/Results/DidNotTriggerWatcherResult.cs @@ -34,7 +34,7 @@ internal DidNotTriggerWatcherResult( /// /// The is applied as an additional per-event filter, so any /// assertions from (e.g. .HasName(...), - /// .HasChangeType(...)) compose naturally — the assertion fails if any event + /// .HasChangeType(...)) compose naturally. The assertion fails if any event /// satisfies all of them. The expectation text is taken from the inner expectation builder, /// so it reads like which has name equal to "foo.txt" rather than the raw lambda /// source. diff --git a/Source/aweXpect.Testably/Results/TriggeredNotificationResult.cs b/Source/aweXpect.Testably/Results/TriggeredNotificationResult.cs index ce98445..3cd9fd3 100644 --- a/Source/aweXpect.Testably/Results/TriggeredNotificationResult.cs +++ b/Source/aweXpect.Testably/Results/TriggeredNotificationResult.cs @@ -36,8 +36,8 @@ internal TriggeredNotificationResult( /// /// The is applied as an additional per-change filter, so any /// assertions from (e.g. .HasName(...), - /// .HasChangeType(...)) compose naturally — only notifications that satisfy all of - /// them count toward the quantifier. The expectation text is taken from the inner + /// .HasChangeType(...)) compose naturally (only notifications that satisfy all of + /// them count toward the quantifier). The expectation text is taken from the inner /// expectation builder, so it reads like matching has name equal to "foo.txt" rather /// than the raw lambda source. /// diff --git a/Source/aweXpect.Testably/Results/TriggeredWatcherResult.cs b/Source/aweXpect.Testably/Results/TriggeredWatcherResult.cs index 134686a..dc57826 100644 --- a/Source/aweXpect.Testably/Results/TriggeredWatcherResult.cs +++ b/Source/aweXpect.Testably/Results/TriggeredWatcherResult.cs @@ -36,8 +36,8 @@ internal TriggeredWatcherResult( /// /// The is applied as an additional per-event filter, so any /// assertions from (e.g. .HasName(...), - /// .HasChangeType(...)) compose naturally — only events that satisfy all of them - /// count toward the quantifier. The expectation text is taken from the inner expectation + /// .HasChangeType(...)) compose naturally (only events that satisfy all of them + /// count toward the quantifier). The expectation text is taken from the inner expectation /// builder, so it reads like which has name equal to "foo.txt" rather than the raw /// lambda source. /// diff --git a/Source/aweXpect.Testably/TimerExtensions.cs b/Source/aweXpect.Testably/TimerExtensions.cs index c03560b..d7827b2 100644 --- a/Source/aweXpect.Testably/TimerExtensions.cs +++ b/Source/aweXpect.Testably/TimerExtensions.cs @@ -16,7 +16,7 @@ public static class TimerExtensions /// /// /// Polls until either the quantifier is satisfied - /// or the timeout expires — 30 seconds by default; use .Within(timeout) to override. + /// or the timeout expires (30 seconds by default; use .Within(timeout) to override). /// public static TimerExecutedResult Executed( this IThat subject)