From 161e2c07d55c2b8378c7a96af23010945ec22031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Fri, 15 May 2026 09:46:47 +0200 Subject: [PATCH] feat: Phase 5.3 step 1 - flag MockFileSystem.MockTime for manual review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestableIO's `fs.MockTime(Func)` is invoked on every timestamp request. Testably's equivalent (`o => o.UseTimeSystem(new MockTimeSystem(...))`) takes a fixed initial time + mutable advance API and lives only inside the construction-time options lambda. The two have no observably-equivalent automatic rewrite for arbitrary delegates. Ship the analyzer side now: pattern id `MockFileSystem.MockTime`, code-fix dispatcher silently falls through. The cross-statement fold for the narrow constant-DateTime lambda shape needs a custom FixAllProvider (the rewrite touches the construction expression that the parameterless / options ctor fixes also touch — BatchFixer races) and is deferred to a follow-up. --- .../Patterns.cs | 13 ++++++++ .../SystemIOAbstractionsAnalyzer.cs | 1 + .../ManualReviewTests.cs | 15 +++++++++ .../SystemIOAbstractionsAnalyzerTests.cs | 19 +++++++++++ ...odeFixProviderTests.AccessorMethodTests.cs | 33 +++++++++++++++++++ 5 files changed, 81 insertions(+) diff --git a/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs b/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs index 1db5a3e..49d16c8 100644 --- a/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs +++ b/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs @@ -57,6 +57,19 @@ public static class Patterns /// public const string MockFileSystemAddDrive = "MockFileSystem.AddDrive"; + /// + /// fs.MockTime(Func<DateTime>). Manual review (Phase 5.3): TestableIO + /// calls the supplied delegate on every timestamp request, while Testably installs + /// a fixed-then-mutable MockTimeSystem at construction. The two have no + /// observably-equivalent automatic rewrite for arbitrary delegates, and the + /// equivalent surface (o => o.UseTimeSystem(...)) lives in the + /// MockFileSystemOptions lambda — a cross-statement fold that conflicts + /// with the parameterless / options-ctor fixes when both touch the construction. + /// A future sub-phase may add an opt-in fix for the narrow constant-DateTime + /// lambda shape with a custom FixAllProvider. + /// + public const string MockFileSystemMockTime = "MockFileSystem.MockTime"; + // ── Enumeration properties (Phase 5.1) ──────────────────────────────── // These IMockFileDataAccessor properties enumerate the whole mocked file // system. Testably has no direct equivalent — the natural replacements diff --git a/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs b/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs index 63932a5..3b7c129 100644 --- a/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs +++ b/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs @@ -314,6 +314,7 @@ private static bool IsMockFileSystemOptions(IParameterSymbol parameter) "MoveDirectory" => Patterns.AccessorMoveDirectory, "FileExists" => Patterns.AccessorFileExists, "AddDrive" => Patterns.MockFileSystemAddDrive, + "MockTime" => Patterns.MockFileSystemMockTime, _ => null, }; diff --git a/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/ManualReviewTests.cs b/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/ManualReviewTests.cs index 2d268f3..a7f9479 100644 --- a/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/ManualReviewTests.cs +++ b/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/ManualReviewTests.cs @@ -84,6 +84,21 @@ public async Task MockFileData_CopyConstructor_ClonesTextContents() await That(clone.TextContents).IsEqualTo("hello"); } + [Fact] + public async Task MockFileSystem_MockTime_ReturnsSelfForFluentChaining() + { + // TestableIO calls the supplied delegate every time it needs a timestamp. + // Testably installs a fixed-then-mutable MockTimeSystem at construction with + // no equivalent post-construction fluent API, so this site is reported with + // pattern id `MockFileSystem.MockTime` and left for manual migration. The + // playground only needs to keep the call shape compiling; the timestamp + // semantics of MockTime are out of scope for the parity baseline. + MockFileSystem fs = new(); + MockFileSystem chained = fs.MockTime(() => DateTime.UnixEpoch); + + await That(chained).IsSameAs(fs); + } + private sealed class MyMockFs : MockFileSystem { } diff --git a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs index 64d965d..48dddb1 100644 --- a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs +++ b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs @@ -142,6 +142,25 @@ await Verifier.VerifyAnalyzerAsync( Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0)); } + [Fact] + public async Task MockTime_ShouldBeFlagged() + { + const string source = """ + using System; + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs) + => {|#0:fs.MockTime(() => DateTime.UnixEpoch)|}; + } + """; + + await Verifier.VerifyAnalyzerAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0)); + } + [Theory] [InlineData("AllPaths")] [InlineData("AllFiles")] diff --git a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs index c1d8540..23ec82c 100644 --- a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs +++ b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs @@ -705,5 +705,38 @@ await Verifier.VerifyCodeFixAsync( Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), source); } + + [Theory] + [InlineData("() => System.DateTime.UnixEpoch")] + [InlineData("() => System.DateTime.Now")] + [InlineData("() => System.DateTime.UtcNow")] + [InlineData("dateTimeProvider")] + public async Task MockTime_HasNoFix(string argument) + { + // Phase 5.3 ships MockTime as manual review only. TestableIO calls the + // supplied delegate every time it needs a timestamp; Testably installs a + // fixed-then-mutable MockTimeSystem at construction. The two have no + // observably-equivalent automatic rewrite for arbitrary delegates, and the + // equivalent surface (`o => o.UseTimeSystem(...)`) lives inside the + // MockFileSystemOptions lambda — a cross-statement fold that conflicts + // with the parameterless / options-ctor fixes when both touch the same + // construction. A future sub-phase may opt-in fix the narrow constant- + // DateTime lambda shape with a custom FixAllProvider. + string source = $$""" + using System; + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs, Func dateTimeProvider) + => {|#0:fs.MockTime({{argument}})|}; + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + source); + } } }