From 314aaebe759139e256c4fcb54098aaa6ed7095df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Fri, 15 May 2026 12:33:43 +0200 Subject: [PATCH 1/2] feat: Phase 5.6 wire HelpLinkUri and expand migration docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set `helpLinkUri` on `Rules.SystemIOAbstractionsRule` to the package's docs URL so IDE "Show help" actions land on a useful page. The URL matches the `PackageProjectUrl` already declared in `Source/Directory.Build.props`. Expand the README to document the dev-only meta-package workflow (install → migrate → uninstall) and the explicit requirement to reference `Testably.Abstractions.Testing` directly so migrated tests keep compiling after the migration package is removed. Add tables covering the supported automatic rewrites and the manual-review patterns flagged by `TestablyM001`. The README is the docs source: the release pipeline copies it into the package and the `notify-docs-site` workflow rebuilds the docs site from it. Add `Docs/pages/00-index.md` so the Testably docs site has an entry page that templates the README, mirroring the convention used by `Mockolate.Migration`. Tests unchanged: 114 main + 40 playground + 2 example = 156. --- Docs/pages/00-index.md | 5 + README.md | 108 ++++++++++++++++-- .../Rules.cs | 6 +- 3 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 Docs/pages/00-index.md diff --git a/Docs/pages/00-index.md b/Docs/pages/00-index.md new file mode 100644 index 0000000..e849ea3 --- /dev/null +++ b/Docs/pages/00-index.md @@ -0,0 +1,5 @@ +# [Testably.Abstractions.Migration](https://github.com/Testably/Testably.Abstractions.Migration) [![Nuget](https://img.shields.io/nuget/v/Testably.Abstractions.Migration)](https://www.nuget.org/packages/Testably.Abstractions.Migration) + +A Roslyn analyzer and code-fix provider that migrates `System.IO.Abstractions` testing usage to `Testably.Abstractions.Testing`. + +{README} diff --git a/README.md b/README.md index 82007cb..cb4531e 100644 --- a/README.md +++ b/README.md @@ -14,22 +14,116 @@ construct it can migrate; the accompanying code fix rewrites the call site. ## Installation -Install the NuGet package into the project you want to migrate: +The migration package is a one-shot development tool — install it, migrate, then uninstall. It ships only +the analyzer and code fixer, not runtime code, and is marked as a `DevelopmentDependency` so it never +flows transitively to consumers of your test project. + +Because the package does not pull `Testably.Abstractions.Testing` transitively, **you must reference it +yourself** in the project being migrated. Otherwise the rewritten call sites would compile while the +migration package is installed but stop compiling the moment you remove it. ```shell +dotnet add package Testably.Abstractions.Testing dotnet add package Testably.Abstractions.Migration ``` -The package only needs to be referenced while you are migrating — it ships the analyzer and code fixer, -not runtime code. Once `System.IO.Abstractions.TestingHelpers` is gone from a project you can remove the -reference again. +## Recommended workflow + +1. **Reference the target library.** Add `Testably.Abstractions.Testing` to the project you want to + migrate (see above). Existing `System.IO.Abstractions.TestingHelpers` usage keeps compiling + side-by-side. +2. **Install the migration package.** Adds the analyzer. Every supported construct is reported as + warning `TestablyM001`. +3. **Apply the code fix.** Use your IDE (Visual Studio, Rider, VS Code with C# Dev Kit) to fix + diagnostics one by one, or run `dotnet format analyzers` to apply every available fix in bulk. +4. **Address manual-review diagnostics.** Some patterns have no safe automatic rewrite (see + [Manual review](#manual-review)). The analyzer reports them so they are discoverable; you migrate + each call site by hand. +5. **Remove `System.IO.Abstractions.TestingHelpers`.** Once the analyzer is quiet, drop the dependency. +6. **Uninstall the migration package.** It has served its purpose and only adds analyzer overhead + from here on. + +```shell +dotnet remove package System.IO.Abstractions.TestingHelpers +dotnet remove package Testably.Abstractions.Migration +``` ## How it works -After installing the package, every supported construct is reported as a warning. Apply the relevant -code fix from your IDE (Visual Studio, Rider, VS Code with C# Dev Kit) or via -`dotnet format analyzers` to rewrite the call site. +The analyzer emits a single diagnostic id, `TestablyM001`. Each call site carries a `pattern` property +in `Diagnostic.Properties` that tells the code-fix provider which rewrite to perform. Patterns without +an automatic rewrite still get a `TestablyM001` warning so you can locate them — the code fix just +declines to register an action. | Diagnostic | Source library | Code fix title | |----------------|------------------------|-------------------------------------------------------------| | `TestablyM001` | System.IO.Abstractions | *Migrate System.IO.Abstractions MockFileSystem to Testably* | + +## Supported migrations + +### `MockFileSystem` constructors + +| TestableIO | Testably | +|-----------------------------------------------------------|-------------------------------------------------------------------| +| `new MockFileSystem()` | `new MockFileSystem()` | +| `new MockFileSystem(IDictionary)` | `new MockFileSystem()` followed by per-entry `Initialize…` calls | +| `new MockFileSystem(MockFileSystemOptions)` | `new MockFileSystem(o => o…)` with mapped option setters | +| `new MockFileSystem(IDictionary<…>, MockFileSystemOptions)` | combined dict expansion + options lambda | + +### `IMockFileDataAccessor` methods on `MockFileSystem` + +| TestableIO | Testably | +|---------------------------------------------|---------------------------------------------------------| +| `fs.AddFile(path, mockFileData)` | `fs.Initialize().With…(…)` (chain mapped from contents) | +| `fs.AddEmptyFile(path)` | `fs.File.Create(path).Dispose()` | +| `fs.AddDirectory(path)` | `fs.Directory.CreateDirectory(path)` | +| `fs.RemoveFile(path)` | `fs.File.Delete(path)` | +| `fs.MoveDirectory(source, dest)` | `fs.Directory.Move(source, dest)` | +| `fs.FileExists(path)` | `fs.File.Exists(path)` | +| `fs.AddDrive(name, mockDriveData)` | `fs.WithDrive(name, d => d.Set…(…))` (mapped setters) | +| `fs.AddFilesFromEmbeddedNamespace(path, assembly, prefix)` | `fs.InitializeEmbeddedResourcesFromAssembly(path, assembly, relativePath: …)` (when the assembly arg resolves statically and the prefix starts with the assembly name) | + +### `MockFileData` property access + +Reads of `MockFileData` properties (e.g. `fs.GetFile(path).LastWriteTime`) are routed to the +matching Testably file-system call (e.g. `fs.File.GetLastWriteTime(path)`). Writes +(e.g. `fs.GetFile(path).LastWriteTime = value`) become `fs.File.SetLastWriteTime(path, value)`. The +fixer only handles the one-shot `fs.GetFile(path).Prop` shape; property access through a captured +reference is left for manual review (see below). + +## Manual review + +These call sites are flagged with `TestablyM001` but have no automatic rewrite, either because +`Testably.Abstractions` has no equivalent surface or because a safe rewrite would require flow +analysis the analyzer does not perform. Address each one by hand. + +| Pattern | Why manual | +|------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| `MockFileData.AccessControl` | Windows `FileSecurity` has no Testably equivalent. | +| `MockFileData.AllowedFileShare` | File-share locking has no Testably equivalent. | +| `MockFileData.UnixMode` | Unix file permissions have no Testably equivalent. | +| `new MockFileVersionInfo(...)` | File-version metadata has no Testably equivalent. | +| Subclassing `MockFileSystem` / `MockFileData` | Inheritance contract differs in Testably. | +| `new MockFileData(MockFileData template)` | Copy-clone semantics differ; no Testably equivalent. | +| Captured-reference `MockFileData` property access | `var data = fs.GetFile(path); data.Prop = …` cannot be rewritten without local flow analysis. | +| `fs.AllPaths` / `AllFiles` / `AllDirectories` / `AllDrives` | Testably has no enumeration properties; the natural replacements need a root or drive scope the analyzer cannot infer. | +| `fs.MockTime(Func)` | TestableIO calls the delegate per timestamp request; Testably installs a fixed-then-mutable `MockTimeSystem` at construction. No observably-equivalent automatic rewrite for arbitrary delegates. | +| `fs.AddFileFromEmbeddedResource(...)` | Testably exposes only a bulk `InitializeEmbeddedResourcesFromAssembly` with path-style matching; the single-file mapping is not safe to automate. | + +## Suppressing the diagnostic + +If you choose not to migrate a particular call site, suppress `TestablyM001` per usage with the +standard mechanisms: + +```csharp +#pragma warning disable TestablyM001 +var fs = new MockFileSystem(); +#pragma warning restore TestablyM001 +``` + +or via an `.editorconfig` entry scoped to the file/folder: + +```ini +[**/Legacy/**.cs] +dotnet_diagnostic.TestablyM001.severity = none +``` diff --git a/Source/Testably.Abstractions.Migration.Analyzers/Rules.cs b/Source/Testably.Abstractions.Migration.Analyzers/Rules.cs index 50e8915..fc89fc9 100644 --- a/Source/Testably.Abstractions.Migration.Analyzers/Rules.cs +++ b/Source/Testably.Abstractions.Migration.Analyzers/Rules.cs @@ -9,6 +9,9 @@ public static class Rules { private const string UsageCategory = "Usage"; + private const string DocsBaseUrl = + "https://docs.testably.org/Testably.Abstractions/Migration"; + /// /// Migration rule for System.IO.Abstractions.TestingHelpers usage. Flags any usage of /// new MockFileSystem(...), new MockFileData(...) or the IMockFileDataAccessor @@ -29,6 +32,7 @@ private static DiagnosticDescriptor CreateDescriptor(string diagnosticId, string severity, true, new LocalizableResourceString(diagnosticId + "Description", Resources.ResourceManager, - typeof(Resources)) + typeof(Resources)), + helpLinkUri: DocsBaseUrl ); } From b3dd5cb97c7cdecf4fbaad315861c63323740db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Fri, 15 May 2026 12:50:06 +0200 Subject: [PATCH 2/2] feat: align docs URL and bump repository-dispatch action Switch the `PackageProjectUrl` and `HelpLinkUri` from `docs.testably.org/Testably.Abstractions/Migration` to `docs.testably.org/Abstractions/Migration` to match how the docs site actually serves Testably.Abstractions content: every slice in `Testably.Site/Pipeline/Build.Pages.cs` lands under a target subdirectory without the `Testably.` prefix (`Abstractions`, not `Testably.Abstractions`). A companion change in Testably.Site adds the migration repo as a slice at `docs/Abstractions/Migration` with `InlineReadme: true`, which inlines the README into the `Docs/pages/00-index.md` placeholder added in Phase 5.6. Bump `peter-evans/repository-dispatch` in `notify-docs-site.yml` from v3 to v4 to align with sibling repos (Mockolate.Migration) so the docs rebuild dispatch uses the same action version everywhere. --- .github/workflows/notify-docs-site.yml | 2 +- Docs/pages/00-index.md | 7 ++++++- Source/Directory.Build.props | 2 +- Source/Testably.Abstractions.Migration.Analyzers/Rules.cs | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/notify-docs-site.yml b/.github/workflows/notify-docs-site.yml index 5980dda..d9b3758 100644 --- a/.github/workflows/notify-docs-site.yml +++ b/.github/workflows/notify-docs-site.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Dispatch extension-documentation-updated-event to Testably/Testably.Site - uses: peter-evans/repository-dispatch@v3 + uses: peter-evans/repository-dispatch@v4 with: token: ${{ secrets.SITE_DISPATCH_TOKEN }} repository: Testably/Testably.Site diff --git a/Docs/pages/00-index.md b/Docs/pages/00-index.md index e849ea3..6ac960e 100644 --- a/Docs/pages/00-index.md +++ b/Docs/pages/00-index.md @@ -1,4 +1,9 @@ -# [Testably.Abstractions.Migration](https://github.com/Testably/Testably.Abstractions.Migration) [![Nuget](https://img.shields.io/nuget/v/Testably.Abstractions.Migration)](https://www.nuget.org/packages/Testably.Abstractions.Migration) +--- +title: Migration +sidebar_position: 8 +--- + +[![Nuget](https://img.shields.io/nuget/v/Testably.Abstractions.Migration?label=Testably.Abstractions.Migration&logo=nuget)](https://www.nuget.org/packages/Testably.Abstractions.Migration) A Roslyn analyzer and code-fix provider that migrates `System.IO.Abstractions` testing usage to `Testably.Abstractions.Testing`. diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index 8139882..5db6dc7 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -9,7 +9,7 @@ Copyright (c) 2026 - $([System.DateTime]::Now.ToString('yyyy')) Valentin Breuß https://github.com/Testably/Testably.Abstractions.Migration.git git - https://docs.testably.org/Testably.Abstractions/Migration + https://docs.testably.org/Abstractions/Migration MIT Docs/logo.png Docs/README.md diff --git a/Source/Testably.Abstractions.Migration.Analyzers/Rules.cs b/Source/Testably.Abstractions.Migration.Analyzers/Rules.cs index fc89fc9..a65937a 100644 --- a/Source/Testably.Abstractions.Migration.Analyzers/Rules.cs +++ b/Source/Testably.Abstractions.Migration.Analyzers/Rules.cs @@ -10,7 +10,7 @@ public static class Rules private const string UsageCategory = "Usage"; private const string DocsBaseUrl = - "https://docs.testably.org/Testably.Abstractions/Migration"; + "https://docs.testably.org/Abstractions/Migration"; /// /// Migration rule for System.IO.Abstractions.TestingHelpers usage. Flags any usage of