Skip to content

Commit 0b0d956

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. This was confirmed by testing each operation against ProjFS placeholders. The original ExpandedFileAttributesAreUpdated test relied on CreationTime's setter triggering hydration as a side effect. Replace it with two focused tests: 1. PlaceholderMetadataSurvivesHydration(property) [parameterized]: Sets a single metadata property on a ProjFS placeholder, verifies the file remains a placeholder, then hydrates via FileStream read+write, and asserts the property survived the conversion. Tested for CreationTime, LastWriteTime, and Attributes (Hidden). Each test case uses a separate file since hydration is irreversible in the shared enlistment. 2. HydratedFileTimestampsAndAttributesAreUpdated: Hydrates a file first, then sets all timestamps and Hidden attribute, and verifies everything sticks. This covers the post-hydration scenario. Additional .NET 10 findings documented in test comments: - File.WriteAllText uses FileMode.Create which fails on Hidden files - FileStream with FileMode.Open works on Hidden files - LastAccessTime cannot survive read-based hydration (read updates it) - LastWriteTime cannot survive write-based hydration (write updates it) Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent 6646b2b commit 0b0d956

1 file changed

Lines changed: 118 additions & 29 deletions

File tree

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

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

136-
[TestCase]
137-
public void ExpandedFileAttributesAreUpdated()
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+
[TestCase("CreationTime")]
142+
[TestCase("LastWriteTime")]
143+
[TestCase("Attributes")]
144+
public void PlaceholderMetadataSurvivesHydration(string property)
138145
{
146+
// Set a metadata property on a ProjFS placeholder, then hydrate via content I/O,
147+
// and verify the property value survived the placeholder-to-full-file conversion.
139148
FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
140149

141-
string filename = Path.Combine("GVFS", "GVFS", "GVFS.csproj");
142-
string virtualFile = this.Enlistment.GetVirtualPathTo(filename);
150+
// Each test case needs its own file since hydration is irreversible in a shared enlistment
151+
string filename;
152+
switch (property)
153+
{
154+
case "CreationTime":
155+
filename = Path.Combine("GVFS", "GVFS", "GVFS.csproj");
156+
break;
157+
case "LastWriteTime":
158+
filename = Path.Combine("GVFS", "GVFS.Common", "GVFSContext.cs");
159+
break;
160+
case "Attributes":
161+
filename = Path.Combine("GVFS", "GVFS.Common", "GVFSEnlistment.cs");
162+
break;
163+
default:
164+
throw new ArgumentException($"Unknown property: {property}");
165+
}
143166

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);
167+
string virtualFile = this.Enlistment.GetVirtualPathTo(filename);
146168
DateTime testValue = DateTime.Now + TimeSpan.FromDays(1);
147169

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);
170+
// Set the property while file is still a placeholder
171+
FileInfo fi = new FileInfo(virtualFile);
172+
switch (property)
173+
{
174+
case "CreationTime":
175+
fi.CreationTime = testValue;
176+
break;
177+
case "LastWriteTime":
178+
fi.LastWriteTime = testValue;
179+
break;
180+
case "Attributes":
181+
fi.Attributes = FileAttributes.Hidden;
182+
break;
183+
}
156184

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;
185+
// Verify file is still a placeholder (no property setter triggers hydration on .NET 10)
186+
fi.Refresh();
187+
((int)fi.Attributes & FileAttributeRecallOnDataAccess).ShouldNotEqual(
188+
0,
189+
$"File should still be a placeholder after setting {property}");
159190

160-
int retryCount = 0;
161-
int maxRetries = 10;
162-
while (attributes != FileAttributes.Hidden && retryCount < maxRetries)
191+
// Hydrate via content read+write (converts placeholder to dirty full file).
192+
// Use FileStream with FileMode.Open since File.WriteAllText fails on Hidden files.
193+
using (FileStream fs = new FileStream(virtualFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
163194
{
164-
// ProjFS attributes are remoted asynchronously when files are converted to full
165-
FileAttributes attributesLessProjFS = attributes & (FileAttributes)~(FileAttributeSparseFile | FileAttributeReparsePoint | FileAttributeRecallOnDataAccess);
195+
byte[] buf = new byte[fs.Length];
196+
fs.Read(buf, 0, buf.Length);
197+
fs.Position = 0;
198+
fs.Write(buf, 0, buf.Length);
199+
}
166200

167-
attributesLessProjFS.ShouldEqual(
168-
FileAttributes.Hidden,
169-
$"Attributes (ignoring ProjFS attributes) do not match, expected: {FileAttributes.Hidden} actual: {attributesLessProjFS}");
201+
// Verify file is now hydrated
202+
fi.Refresh();
203+
((int)fi.Attributes & FileAttributeRecallOnDataAccess).ShouldEqual(
204+
0,
205+
"File should be hydrated (no RecallOnDataAccess) after content write");
170206

171-
++retryCount;
172-
Thread.Sleep(500);
207+
// Verify the property value survived hydration
208+
switch (property)
209+
{
210+
case "CreationTime":
211+
fi.CreationTime.ShouldEqual(testValue, "CreationTime should survive hydration");
212+
break;
213+
case "LastWriteTime":
214+
// LastWriteTime is updated by the write operation itself, so we cannot
215+
// assert the pre-hydration value. Just verify it's recent (not default).
216+
fi.LastWriteTime.ShouldNotEqual(
217+
default(DateTime),
218+
"LastWriteTime should be set after hydration");
219+
break;
220+
case "Attributes":
221+
int retryCount = 0;
222+
FileAttributes attributes = fi.Attributes & ~FileAttributes.Archive;
223+
while (attributes != FileAttributes.Hidden && retryCount < 10)
224+
{
225+
// ProjFS attributes are remoted asynchronously during conversion
226+
FileAttributes withoutProjFS = attributes & (FileAttributes)~(FileAttributeSparseFile | FileAttributeReparsePoint | FileAttributeRecallOnDataAccess);
227+
withoutProjFS.ShouldEqual(
228+
FileAttributes.Hidden,
229+
$"Attributes (ignoring ProjFS) should be Hidden, got: {withoutProjFS}");
230+
++retryCount;
231+
Thread.Sleep(500);
232+
fi.Refresh();
233+
attributes = fi.Attributes & ~FileAttributes.Archive;
234+
}
173235

174-
info.Refresh();
175-
attributes = info.Attributes & ~FileAttributes.Archive;
236+
attributes.ShouldEqual(
237+
FileAttributes.Hidden,
238+
$"Hidden attribute should survive hydration, got: {attributes}");
239+
break;
176240
}
241+
}
242+
243+
[TestCase]
244+
public void HydratedFileTimestampsAndAttributesAreUpdated()
245+
{
246+
// Verify that all timestamps and attributes can be set on an already-hydrated
247+
// (dirty full) file in a GVFS enlistment.
248+
FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner;
249+
250+
string filename = Path.Combine("GVFS", "GVFS.Common", "GVFSConstants.cs");
251+
string virtualFile = this.Enlistment.GetVirtualPathTo(filename);
252+
DateTime testValue = DateTime.Now + TimeSpan.FromDays(1);
253+
254+
// Hydrate first by reading content (converts placeholder to full file)
255+
File.ReadAllBytes(virtualFile);
256+
257+
// Set all properties on the now-hydrated file
258+
FileInfo fi = new FileInfo(virtualFile);
259+
fi.CreationTime = testValue;
260+
fi.LastAccessTime = testValue;
261+
fi.LastWriteTime = testValue;
262+
fi.Attributes = FileAttributes.Hidden;
177263

178-
attributes.ShouldEqual(FileAttributes.Hidden, $"Attributes do not match, expected: {FileAttributes.Hidden} actual: {attributes}");
264+
// Verify all properties stuck
265+
FileInfo verify = virtualFile.ShouldBeAFile(fileSystem).WithInfo(testValue, testValue, testValue);
266+
FileAttributes attributes = verify.Attributes & ~FileAttributes.Archive;
267+
attributes.ShouldEqual(FileAttributes.Hidden, $"Attributes should be Hidden, got: {attributes}");
179268
}
180269

181270
[TestCase]

0 commit comments

Comments
 (0)