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 new file mode 100644 index 0000000..6ac960e --- /dev/null +++ b/Docs/pages/00-index.md @@ -0,0 +1,10 @@ +--- +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`. + +{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/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 50e8915..a65937a 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/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 ); }