Skip to content

fix: share one cached state across FileSystemInfo properties like the real file system - #1113

Merged
vbreuss merged 1 commit into
Testably:mainfrom
Mpdreamz:fix/filesysteminfo-cache
Sep 30, 2026
Merged

vbreuss merged 1 commit into
Testably:mainfrom
Mpdreamz:fix/filesysteminfo-cache

Conversation

@Mpdreamz

Copy link
Copy Markdown
Contributor

FileSystemInfo takes one snapshot of a file's state on the first read of any cached property: Exists, Length, Attributes, the times and UnixFileMode. It serves that snapshot until Refresh(), or until an operation on the same instance invalidates it. Instances returned by an enumeration carry the state found during the enumeration. The mock cached Exists the same way, but read every other property live. So a test could observe values the real file system never returns:

var fileInfo = fileSystem.FileInfo.New(path);   // file has 3 bytes
_ = fileInfo.Exists;                             // snapshot taken
fileSystem.File.WriteAllText(path, "abcdefghij");
fileInfo.Length;                                  // real: 3, mock: 10

The same happens with Attributes and the times after they change on disk. A GetFiles() result also reported Exists == false after its file was deleted, where the real file system still reports true.

Change

FileSystemInfoMock captures all cached properties together, in one state:

  • When the state is captured: lazily on first access, and straight away on Refresh() and for instances returned by enumeration.
  • What resets it: the property setters, Delete, Create, MoveTo, CreateAsSymbolicLink, and CreateText/AppendText on .NET 8+. These are the operations the runtime invalidates on. Open, CopyTo, Replace, Encrypt and Decrypt don't reset it, as in the runtime. AppendText didn't reset it before.
  • LinkTarget is cached separately and read lazily, as in the runtime.
  • Setters write to the container that is currently at the path. Before, they could write to a stale container reference.

There is no public API change.

Tests

The tests are added to Testably.Abstractions.Tests, so each one also runs against the real file system:

  • New FileInfo/RefreshTests (14 tests) and DirectoryInfo/RefreshTests (5 tests).
  • One test each in DeleteTests, MoveToTests, OpenTests, CreateAsSymbolicLinkTests, AppendTextTests, FileSystemInfo/Tests, and the six DirectoryInfo Get/Enumerate test files.

Against the current mock, 79 mock runs of these tests fail, and all the real-file-system runs pass.

Encrypt_Decrypt_ShouldChangeEncryptedFileAttribute now calls Refresh() after Decrypt(). FileInfo.Decrypt only calls File.Decrypt and does not invalidate the cached attributes. That test only runs on the simulated Windows file system, so this follows the runtime's code rather than a run on a real file system.

Validation

  • Scenario harness: 78 scenarios through a real file system and MockFileSystem, on net8.0, net9.0 and net10.0. Before, 31 differed from the real file system; with this change, none do.

  • Test suites (Release, macOS):

    net10.0 net9.0 net8.0
    Testably.Abstractions.Tests 13464 13434 12955
    Testably.Abstractions.Testing.Tests 1424 1424 1408

    All pass, as do the API, Core API and parity tests.

Worth knowing

Tests that change a file through File.* and then read Length, Attributes or a time from a FileInfo they had already read will now see the old value until they call Refresh(). That matches the real file system, but it is a behaviour change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TymoqrVYgZwdNSper5ZZB1

… real file system

`FileSystemInfo` takes a single snapshot of a file's state on the first
read of any cached property (Exists, Length, Attributes, the times and
UnixFileMode) and serves it until `Refresh()` or an operation on the same
instance that invalidates it. Instances returned by an enumeration carry
the state found during the enumeration. The mock only cached `Exists` and
read every other property live, so reading `Length` after the file grew,
or `Exists` on a `GetFiles()` result after the file was deleted, returned
values the real file system does not.

The mock now captures all cached properties together:
- lazily on first access, eagerly on `Refresh()` and for enumerated
  instances;
- reset by the property setters, `Delete`, `Create`, `MoveTo`,
  `CreateAsSymbolicLink`, and `CreateText`/`AppendText` (.NET 8+), which
  are the operations the runtime invalidates on; `Open`, `CopyTo`,
  `Replace`, `Encrypt` and `Decrypt` do not;
- `LinkTarget` is cached separately and lazily, as in the runtime.

`Encrypt_Decrypt_ShouldChangeEncryptedFileAttribute` now calls `Refresh()`
after `Decrypt()`, as `FileInfo.Decrypt` does not invalidate the cached
attributes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TymoqrVYgZwdNSper5ZZB1
Copilot AI balanced review requested due to automatic review settings September 30, 2026 16:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The implementation consistently matches the documented runtime behavior and is covered by comprehensive parity tests.

Review effort: Balanced
Findings: None

What changed in this PR

Aligns mock FileSystemInfo metadata caching with the real file system.

Changes:

  • Introduces unified cached metadata state with appropriate invalidation.
  • Initializes enumeration results with captured state.
  • Adds parity tests across files, directories, links, and operations.
File Description
Tests/​.../​FileSystemInfo/​Tests.cs Tests independent link-target caching.
Tests/​.../​FileInfo/​RefreshTests.cs Tests file metadata snapshots and refresh behavior.
Tests/​.../​FileInfo/​OpenTests.cs Verifies opening does not invalidate state.
Tests/​.../​FileInfo/​MoveToTests.cs Verifies moving invalidates state.
Tests/​.../​FileInfo/​EncryptDecryptTests.cs Refreshes cached attributes after decryption.
Tests/​.../​FileInfo/​DeleteTests.cs Tests deletion invalidation across frameworks.
Tests/​.../​FileInfo/​CreateAsSymbolicLinkTests.cs Tests link creation invalidation.
Tests/​.../​FileInfo/​AppendTextTests.cs Tests framework-specific append invalidation.
Tests/​.../​DirectoryInfo/​RefreshTests.cs Tests directory metadata caching.
Tests/​.../​DirectoryInfo/​GetFileSystemInfosTests.cs Tests captured enumeration state.
Tests/​.../​DirectoryInfo/​GetFilesTests.cs Tests cached file results.
Tests/​.../​DirectoryInfo/​GetDirectoriesTests.cs Tests cached directory results.
Tests/​.../​DirectoryInfo/​EnumerateFileSystemInfosTests.cs Tests lazy mixed enumeration state.
Tests/​.../​DirectoryInfo/​EnumerateFilesTests.cs Tests lazy file enumeration state.
Tests/​.../​DirectoryInfo/​EnumerateDirectoriesTests.cs Tests lazy directory enumeration state.
Source/​.../​FileSystemInfoMock.cs Implements shared metadata and link-target caches.
Source/​.../​FileInfoMock.cs Applies cache semantics to file operations.
Source/​.../​DirectoryInfoMock.cs Initializes enumeration state and invalidates moves.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@mergify

mergify Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again.

@vbreuss
vbreuss merged commit 8b0fe65 into Testably:main Sep 30, 2026
13 checks passed
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

This is addressed in release v7.1.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants