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