Skip to content

feat: Phase 5.2 migrate embedded-resource APIs to InitializeEmbeddedResourcesFromAssembly#16

Merged
vbreuss merged 2 commits into
mainfrom
feat/phase-5.2-embedded-resources
May 15, 2026
Merged

feat: Phase 5.2 migrate embedded-resource APIs to InitializeEmbeddedResourcesFromAssembly#16
vbreuss merged 2 commits into
mainfrom
feat/phase-5.2-embedded-resources

Conversation

@vbreuss
Copy link
Copy Markdown
Member

@vbreuss vbreuss commented May 15, 2026

Two new pattern ids cover the TestableIO embedded-resource surface:

  • MockFileSystem.AddFileFromEmbeddedResource (single file by exact resource name) — manual review only. Testably exposes only a bulk InitializeEmbeddedResourcesFromAssembly with no single-file overload, and bulk replacement would lose the user's right to rename the destination.
  • MockFileSystem.AddFilesFromEmbeddedNamespace (bulk by literal StartsWith prefix) — auto-fixed when the assembly arg resolves statically and the third arg is a string literal that begins with that assembly's name. Both patterns also cover IMockFileDataAccessor receivers; when the receiver is the interface (not concrete MockFileSystem) the InitializeEmbeddedResourcesFromAssembly extension on IFileSystem cannot bind, so those receivers fall through to manual review.

Auto-fix conditions for AddFilesFromEmbeddedNamespace:

  • Receiver is concrete TestableIO MockFileSystem.
  • Assembly arg is typeof(SomeType).Assembly (resolved via semantic model) OR Assembly.GetExecutingAssembly() (resolved to Compilation.AssemblyName).
  • Third arg is a string literal that starts with the resolved assembly name.

Rewrite shape:
fs.AddFilesFromEmbeddedNamespace("/data", typeof(C).Assembly, "MyAsm.TestData")
→ fs.InitializeEmbeddedResourcesFromAssembly("/data", typeof(C).Assembly,
relativePath: "TestData");

Dots in the stripped remainder become forward slashes (Testably normalizes AltDirectorySeparatorChar). Literal == assembly name (with or without trailing dot) drops the relativePath: argument entirely so the call materializes every embedded resource (matching TestableIO's StartsWith(<asm-name>)).

The fix adds using Testably.Abstractions.Testing; ADDITIVELY without removing the existing TestingHelpers using — the receiver type stays bound to TestableIO so other call sites in the file keep compiling. New helper EnsureTestablyUsing for additive insertion (vs the existing destructive SwapToTestablyUsing).

Known semantic difference: TestableIO flattens nested-namespace resources under the target dir; Testably preserves nested structure as subdirectories. Common case (flat resource set) is equivalent; nested cases produce more correct but different output, surfaced through manual inspection of the diff.

Tests cover: analyzer firing on each pattern (3); manual-review codefix for AddFileFromEmbeddedResource; auto-fix happy path for typeof-Assembly, nested namespace path preservation, literal == assembly name (relativePath omitted), GetExecutingAssembly path; manual review for prefix mismatch, non-literal third arg, non-resolvable assembly arg, interface-typed receiver. Playground gains TestData/sample.txt as embedded resource and a parity fixture per pattern that exercises the TestableIO call shape end-to-end.

…esourcesFromAssembly

Three new pattern ids cover the TestableIO embedded-resource surface:
 - MockFileSystem.AddFileFromEmbeddedResource (single file by exact resource
   name) — manual review only. Testably exposes only a bulk
   InitializeEmbeddedResourcesFromAssembly with no single-file overload, and
   bulk replacement would lose the user's right to rename the destination.
 - MockFileSystem.AddFilesFromEmbeddedNamespace (bulk by literal StartsWith
   prefix) — auto-fixed when the assembly arg resolves statically and the
   third arg is a string literal that begins with that assembly's name.
 - The previous two cover the IMockFileDataAccessor surface as well: when
   the receiver is the interface (not concrete MockFileSystem) the
   InitializeEmbeddedResourcesFromAssembly extension on IFileSystem cannot
   bind, so those receivers fall through to manual review.

Auto-fix conditions for AddFilesFromEmbeddedNamespace:
 - Receiver is concrete TestableIO MockFileSystem.
 - Assembly arg is `typeof(SomeType).Assembly` (resolved via semantic model)
   OR `Assembly.GetExecutingAssembly()` (resolved to Compilation.AssemblyName).
 - Third arg is a string literal that starts with the resolved assembly name.

Rewrite shape:
  fs.AddFilesFromEmbeddedNamespace("/data", typeof(C).Assembly, "MyAsm.TestData")
  → fs.InitializeEmbeddedResourcesFromAssembly("/data", typeof(C).Assembly,
        relativePath: "TestData");

Dots in the stripped remainder become forward slashes (Testably normalizes
AltDirectorySeparatorChar). Literal == assembly name (with or without trailing
dot) drops the `relativePath:` argument entirely so the call materializes
every embedded resource (matching TestableIO's `StartsWith(<asm-name>)`).

The fix adds `using Testably.Abstractions.Testing;` ADDITIVELY without
removing the existing TestingHelpers using — the receiver type stays bound
to TestableIO so other call sites in the file keep compiling. New helper
`EnsureTestablyUsing` for additive insertion (vs the existing destructive
`SwapToTestablyUsing`).

Known semantic difference: TestableIO flattens nested-namespace resources
under the target dir; Testably preserves nested structure as subdirectories.
Common case (flat resource set) is equivalent; nested cases produce more
correct but different output, surfaced through manual inspection of the diff.

Tests cover: analyzer firing on each pattern (3); manual-review codefix for
AddFileFromEmbeddedResource; auto-fix happy path for typeof-Assembly,
nested namespace path preservation, literal == assembly name (relativePath
omitted), GetExecutingAssembly path; manual review for prefix mismatch,
non-literal third arg, non-resolvable assembly arg, interface-typed receiver.
Playground gains TestData/sample.txt as embedded resource and a parity
fixture per pattern that exercises the TestableIO call shape end-to-end.
@vbreuss vbreuss self-assigned this May 15, 2026
Copilot AI review requested due to automatic review settings May 15, 2026 08:48
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

Test Results

  6 files  ± 0    6 suites  ±0   1m 29s ⏱️ -31s
154 tests +13  154 ✅ +13  0 💤 ±0  0 ❌ ±0 
462 runs  +39  460 ✅ +39  2 💤 ±0  0 ❌ ±0 

Results for commit d55a692. ± Comparison against base commit 0e2b270.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the System.IO.Abstractions → Testably migration analyzer/code-fix to cover TestableIO’s embedded-resource APIs by flagging embedded-resource calls and (when safe) rewriting bulk namespace loads to InitializeEmbeddedResourcesFromAssembly.

Changes:

  • Added new pattern IDs + analyzer detection for AddFileFromEmbeddedResource and AddFilesFromEmbeddedNamespace.
  • Implemented a code fix for AddFilesFromEmbeddedNamespace that rewrites to InitializeEmbeddedResourcesFromAssembly when the assembly/prefix can be resolved statically, and adds using Testably.Abstractions.Testing; additively.
  • Added/expanded tests and a playground embedded resource fixture to validate analyzer + code-fix behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs Adds code-fix tests for embedded-resource patterns (auto-fix + manual-review cases).
Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs Adds analyzer coverage for embedded-resource method diagnostics.
Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/TestData/sample.txt Adds a sample embedded resource payload for end-to-end playground parity.
Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground.csproj Embeds the new sample resource in the playground project.
Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/ManualReviewTests.cs Adds playground tests exercising the TestableIO embedded-resource call shapes.
Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs Flags embedded-resource API usage with new pattern IDs.
Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs Introduces pattern ID constants and documents their migration expectations.
Source/Testably.Abstractions.Migration.Analyzers.CodeFixers/SystemIOAbstractionsCodeFixProvider.cs Registers/applies the embedded-namespace rewrite and adds EnsureTestablyUsing.
Comments suppressed due to low confidence (2)

Source/Testably.Abstractions.Migration.Analyzers.CodeFixers/SystemIOAbstractionsCodeFixProvider.cs:1717

  • EnsureTestablyUsing treats any using whose Name string matches Testably.Abstractions.Testing as sufficient. If the file already has an alias/static using (e.g. using T = Testably.Abstractions.Testing;), extension methods from that namespace are NOT imported, so the rewritten InitializeEmbeddedResourcesFromAssembly call would fail to bind. Consider only treating non-aliased, non-static using directives as satisfying this check (or always adding a normal using when the existing one is aliased/static).
		if (compilationUnit.Usings.Any(u => u.Name?.ToString() == TestablyTestingNamespace))
		{
			return compilationUnit;
		}

Source/Testably.Abstractions.Migration.Analyzers.CodeFixers/SystemIOAbstractionsCodeFixProvider.cs:1575

  • This rewrite assumes the three arguments are in source-order (Arguments[0] = path, [1] = assembly, [2] = embeddedResourcePath). If the caller uses named arguments out-of-order, the code fix can produce an incorrect (or even non-compiling) InitializeEmbeddedResourcesFromAssembly call. Consider either (a) mapping arguments to parameters via IInvocationOperation.Arguments / Argument.Parameter, or (b) conservatively declining the fix when any of the arguments uses NameColon/NameEquals.
		ArgumentSyntax pathArg = invocation.ArgumentList.Arguments[0];
		ArgumentSyntax assemblyArg = invocation.ArgumentList.Arguments[1];
		if (!TryComputeRelativePathFromAssemblyAndLiteral(

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs
@vbreuss vbreuss enabled auto-merge (squash) May 15, 2026 09:07
@sonarqubecloud
Copy link
Copy Markdown

@vbreuss vbreuss merged commit 570cabe into main May 15, 2026
9 checks passed
@vbreuss vbreuss deleted the feat/phase-5.2-embedded-resources branch May 15, 2026 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants