Skip to content

Commit 95d2b12

Browse files
committed
Rewrite ExpandedFileAttributesAreUpdated for .NET 10 hydration behavior
.NET 10's FileInfo property setters (CreationTime, LastAccessTime, LastWriteTime, Attributes) no longer open write handles that trigger ProjFS hydration. Only actual file content I/O (read+write) causes hydration. The original ExpandedFileAttributesAreUpdated test relied on CreationTime's setter triggering hydration as a side effect. Replace it with two tests: 1. PlaceholderMetadataSurvivesHydration: Sets all metadata (timestamps + Hidden) on a ProjFS placeholder, verifies the file remains a placeholder, asserts properties took effect, then hydrates via FileStream read+write and verifies CreationTime and Hidden survived the conversion. (LastAccessTime/LastWriteTime are inherently updated by the hydration I/O and cannot be asserted post-hydration.) 2. HydratedFileTimestampsAndAttributesAreUpdated: Hydrates a file via read+write first, asserts it is hydrated, then sets all timestamps and Hidden, and verifies everything sticks. Additional .NET 10 behavioral changes documented: - File.WriteAllText uses FileMode.Create which fails on Hidden files - FileStream with FileMode.Open works on Hidden files Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent 6646b2b commit 95d2b12

1 file changed

Lines changed: 111 additions & 28 deletions

File tree

GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/BasicFileSystemTests.cs

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -133,49 +133,132 @@ public void NewFolderAttributesAreUpdated(string parentFolder)
133133
Directory.Delete(virtualFolder);
134134
}
135135

136+
// On .NET 10, no FileInfo property setter (CreationTime, LastAccessTime, LastWriteTime,
137+
// Attributes) triggers ProjFS hydration. Only actual file content I/O (read+write) does.
138+
// These tests replace the original ExpandedFileAttributesAreUpdated test, which relied on
139+
// .NET Framework 4.7.1's CreationTime setter triggering hydration as a side effect.
140+
141+
/// <summary>
142+
/// Hydrates a ProjFS placeholder by reading and writing its content, then waits for
143+
/// ProjFS to clear the RecallOnDataAccess flag (which happens asynchronously).
144+
/// Uses FileStream with FileMode.Open since File.WriteAllText fails on Hidden files.
145+
/// </summary>
146+
private static void HydrateFile(string virtualFile)
147+
{
148+
using (FileStream fs = new FileStream(virtualFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
149+
{
150+
byte[] buf = new byte[fs.Length];
151+
fs.Read(buf, 0, buf.Length);
152+
fs.Position = 0;
153+
fs.Write(buf, 0, buf.Length);
154+
}
155+
156+
// ProjFS clears RecallOnDataAccess asynchronously after hydration.
157+
// Wait for it to complete — CI machines can be slow.
158+
int retryCount = 0;
159+
while (retryCount < 10)
160+
{
161+
FileAttributes attrs = File.GetAttributes(virtualFile);
162+
if (((int)attrs & FileAttributeRecallOnDataAccess) == 0)
163+
{
164+
return;
165+
}
166+
167+
++retryCount;
168+
Thread.Sleep(500);
169+
}
170+
171+
File.GetAttributes(virtualFile).ShouldNotEqual(
172+
(FileAttributes)FileAttributeRecallOnDataAccess,
173+
"File should be hydrated (no RecallOnDataAccess) after content write and retry");
174+
}
175+
136176
[TestCase]
137-
public void ExpandedFileAttributesAreUpdated()
177+
public void PlaceholderMetadataSurvivesHydration()
138178
{
179+
// Set all metadata properties on a ProjFS placeholder, verify they took effect
180+
// while the file is still a placeholder, then hydrate via content I/O and verify
181+
// the values survived the placeholder-to-full-file conversion.
139182
FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
140183

141184
string filename = Path.Combine("GVFS", "GVFS", "GVFS.csproj");
142185
string virtualFile = this.Enlistment.GetVirtualPathTo(filename);
143-
144-
// Update defaults. FileInfo is not batched, so each of these will create a separate Open-Update-Close set.
145-
FileInfo before = new FileInfo(virtualFile);
146186
DateTime testValue = DateTime.Now + TimeSpan.FromDays(1);
147187

148-
// Setting the CreationTime results in a write handle being open to the file and the file being expanded
149-
before.CreationTime = testValue;
150-
before.LastAccessTime = testValue;
151-
before.LastWriteTime = testValue;
152-
before.Attributes = FileAttributes.Hidden;
153-
154-
// FileInfo caches information. We can refresh, but just to be absolutely sure...
155-
FileInfo info = virtualFile.ShouldBeAFile(fileSystem).WithInfo(testValue, testValue, testValue);
156-
157-
// Ignore the archive bit as it can be re-added to the file as part of its expansion to full
158-
FileAttributes attributes = info.Attributes & ~FileAttributes.Archive;
159-
188+
// Set all properties while file is still a placeholder
189+
FileInfo fi = new FileInfo(virtualFile);
190+
fi.CreationTime = testValue;
191+
fi.LastAccessTime = testValue;
192+
fi.LastWriteTime = testValue;
193+
fi.Attributes = FileAttributes.Hidden;
194+
195+
// Verify file is still a placeholder (no property setter triggers hydration on .NET 10)
196+
fi.Refresh();
197+
((int)fi.Attributes & FileAttributeRecallOnDataAccess).ShouldNotEqual(
198+
0,
199+
"File should still be a placeholder after setting metadata properties");
200+
201+
// Verify the properties took effect on the placeholder
202+
fi.CreationTime.ShouldEqual(testValue, "CreationTime should be set on placeholder");
203+
fi.LastAccessTime.ShouldEqual(testValue, "LastAccessTime should be set on placeholder");
204+
fi.LastWriteTime.ShouldEqual(testValue, "LastWriteTime should be set on placeholder");
205+
FileAttributes placeholderAttrs = fi.Attributes & ~FileAttributes.Archive & (FileAttributes)~(FileAttributeSparseFile | FileAttributeReparsePoint | FileAttributeRecallOnDataAccess);
206+
placeholderAttrs.ShouldEqual(FileAttributes.Hidden, $"Hidden should be set on placeholder, got: {placeholderAttrs}");
207+
208+
// Hydrate and wait for ProjFS to finish clearing placeholder flags
209+
HydrateFile(virtualFile);
210+
211+
// Verify metadata survived hydration.
212+
// CreationTime should survive — it's not affected by read or write operations.
213+
fi.Refresh();
214+
fi.CreationTime.ShouldEqual(testValue, "CreationTime should survive hydration");
215+
216+
// LastAccessTime and LastWriteTime are inherently updated by the read+write
217+
// hydration step, so we cannot assert the pre-hydration values survived.
218+
219+
// Hidden attribute should survive hydration (with async ProjFS flag cleanup)
160220
int retryCount = 0;
161-
int maxRetries = 10;
162-
while (attributes != FileAttributes.Hidden && retryCount < maxRetries)
221+
FileAttributes attributes = fi.Attributes & ~FileAttributes.Archive;
222+
while (attributes != FileAttributes.Hidden && retryCount < 10)
163223
{
164-
// ProjFS attributes are remoted asynchronously when files are converted to full
165-
FileAttributes attributesLessProjFS = attributes & (FileAttributes)~(FileAttributeSparseFile | FileAttributeReparsePoint | FileAttributeRecallOnDataAccess);
166-
167-
attributesLessProjFS.ShouldEqual(
224+
FileAttributes withoutProjFS = attributes & (FileAttributes)~(FileAttributeSparseFile | FileAttributeReparsePoint | FileAttributeRecallOnDataAccess);
225+
withoutProjFS.ShouldEqual(
168226
FileAttributes.Hidden,
169-
$"Attributes (ignoring ProjFS attributes) do not match, expected: {FileAttributes.Hidden} actual: {attributesLessProjFS}");
170-
227+
$"Attributes (ignoring ProjFS) should be Hidden, got: {withoutProjFS}");
171228
++retryCount;
172229
Thread.Sleep(500);
173-
174-
info.Refresh();
175-
attributes = info.Attributes & ~FileAttributes.Archive;
230+
fi.Refresh();
231+
attributes = fi.Attributes & ~FileAttributes.Archive;
176232
}
177233

178-
attributes.ShouldEqual(FileAttributes.Hidden, $"Attributes do not match, expected: {FileAttributes.Hidden} actual: {attributes}");
234+
attributes.ShouldEqual(FileAttributes.Hidden, $"Hidden should survive hydration, got: {attributes}");
235+
}
236+
237+
[TestCase]
238+
public void HydratedFileTimestampsAndAttributesAreUpdated()
239+
{
240+
// Verify that all timestamps and attributes can be set on an already-hydrated
241+
// (dirty full) file in a GVFS enlistment.
242+
FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
243+
244+
string filename = Path.Combine("GVFS", "GVFS.Common", "GVFSConstants.cs");
245+
string virtualFile = this.Enlistment.GetVirtualPathTo(filename);
246+
DateTime testValue = DateTime.Now + TimeSpan.FromDays(1);
247+
248+
// Hydrate and wait for ProjFS to finish clearing placeholder flags
249+
HydrateFile(virtualFile);
250+
251+
// Set all properties on the now-hydrated file
252+
FileInfo fi = new FileInfo(virtualFile);
253+
fi.CreationTime = testValue;
254+
fi.LastAccessTime = testValue;
255+
fi.LastWriteTime = testValue;
256+
fi.Attributes = FileAttributes.Hidden;
257+
258+
// Verify all properties stuck
259+
FileInfo verify = virtualFile.ShouldBeAFile(fileSystem).WithInfo(testValue, testValue, testValue);
260+
FileAttributes attributes = verify.Attributes & ~FileAttributes.Archive;
261+
attributes.ShouldEqual(FileAttributes.Hidden, $"Attributes should be Hidden, got: {attributes}");
179262
}
180263

181264
[TestCase]

0 commit comments

Comments
 (0)