@@ -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