@@ -11,6 +11,7 @@ import { getLogger } from './logger'
1111import * as pathutils from './utilities/pathUtils'
1212import globals from '../shared/extensionGlobals'
1313import fs from '../shared/fs/fs'
14+ import { ToolkitError } from './errors'
1415
1516export const tempDirPath = path . join (
1617 // https://github.com/aws/aws-toolkit-vscode/issues/240
@@ -246,3 +247,117 @@ export async function setDefaultDownloadPath(downloadPath: string) {
246247 getLogger ( ) . error ( 'Error while setting "aws.downloadPath"' , err as Error )
247248 }
248249}
250+
251+ const FileSystemStabilityExceptionId = 'FileSystemStabilityException'
252+ const FileSystemStabilityException = ToolkitError . named ( FileSystemStabilityExceptionId )
253+
254+ /**
255+ * Run this function to validate common file system calls/flows, ensuring they work
256+ * as expected.
257+ *
258+ * The intent of this function is to catch potential fs issues early,
259+ * testing common cases of fs usage. We need this since each machine can behave
260+ * differently depending on things like OS, permissions, disk speed, ...
261+ *
262+ * @throws a {@link FileSystemStabilityException} which wraps the underlying failed fs operation error.
263+ */
264+ export async function throwOnUnstableFileSystem ( tmpRoot : string ) {
265+ const tmpFolder = path . join ( tmpRoot , `validateStableFS-${ crypto . randomBytes ( 4 ) . toString ( 'hex' ) } ` )
266+ const tmpFile = path . join ( tmpFolder , 'file.txt' )
267+
268+ try {
269+ // test basic folder operations
270+ await withFailCtx ( 'mkdirInitial' , ( ) => fs . mkdir ( tmpFolder ) )
271+ // Verifies that we do not throw if the dir exists and we try to make it again
272+ await withFailCtx ( 'mkdirButAlreadyExists' , ( ) => fs . mkdir ( tmpFolder ) )
273+ await withFailCtx ( 'mkdirSubfolder' , ( ) => fs . mkdir ( path . join ( tmpFolder , 'subfolder' ) ) )
274+ await withFailCtx ( 'rmdirInitial' , ( ) => fs . delete ( tmpFolder , { recursive : true } ) )
275+
276+ // test basic file operations
277+ await withFailCtx ( 'mkdirForFileOpsTest' , ( ) => fs . mkdir ( tmpFolder ) )
278+ await withFailCtx ( 'writeFile1' , ( ) => fs . writeFile ( tmpFile , 'test1' ) )
279+ await withFailCtx ( 'readFile1' , async ( ) => {
280+ const text = await fs . readFileText ( tmpFile )
281+ if ( text !== 'test1' ) {
282+ throw new Error ( `Unexpected file contents: "${ text } "` )
283+ }
284+ } )
285+ // overwrite the file content multiple times
286+ await withFailCtx ( 'writeFile2' , ( ) => fs . writeFile ( tmpFile , 'test2' ) )
287+ await withFailCtx ( 'writeFile3' , ( ) => fs . writeFile ( tmpFile , 'test3' ) )
288+ await withFailCtx ( 'writeFile4' , ( ) => fs . writeFile ( tmpFile , 'test4' ) )
289+ await withFailCtx ( 'writeFile5' , ( ) => fs . writeFile ( tmpFile , 'test5' ) )
290+ await withFailCtx ( 'readFile5' , async ( ) => {
291+ const text = await fs . readFileText ( tmpFile )
292+ if ( text !== 'test5' ) {
293+ throw new Error ( `Unexpected file contents after multiple writes: "${ text } "` )
294+ }
295+ } )
296+ // test concurrent reads on a file
297+ await withFailCtx ( 'writeFileConcurrencyTest' , ( ) => fs . writeFile ( tmpFile , 'concurrencyTest' ) )
298+ const result = await Promise . all ( [
299+ withFailCtx ( 'readFileConcurrent1' , ( ) => fs . readFileText ( tmpFile ) ) ,
300+ withFailCtx ( 'readFileConcurrent2' , ( ) => fs . readFileText ( tmpFile ) ) ,
301+ withFailCtx ( 'readFileConcurrent3' , ( ) => fs . readFileText ( tmpFile ) ) ,
302+ withFailCtx ( 'readFileConcurrent4' , ( ) => fs . readFileText ( tmpFile ) ) ,
303+ withFailCtx ( 'readFileConcurrent5' , ( ) => fs . readFileText ( tmpFile ) ) ,
304+ ] )
305+ if ( result . some ( ( text ) => text !== 'concurrencyTest' ) ) {
306+ throw new Error ( `Unexpected concurrent file reads: ${ result } ` )
307+ }
308+ // test deleting a file
309+ await withFailCtx ( 'deleteFileInitial' , ( ) => fs . delete ( tmpFile ) )
310+ await withFailCtx ( 'writeFileAfterDelete' , ( ) => fs . writeFile ( tmpFile , 'afterDelete' ) )
311+ await withFailCtx ( 'readNewFileAfterDelete' , async ( ) => {
312+ const text = await fs . readFileText ( tmpFile )
313+ if ( text !== 'afterDelete' ) {
314+ throw new Error ( `Unexpected file content after writing to deleted file: "${ text } "` )
315+ }
316+ } )
317+ await withFailCtx ( 'deleteFileFully' , ( ) => fs . delete ( tmpFile ) )
318+ await withFailCtx ( 'notExistsFileAfterDelete' , async ( ) => {
319+ const res = await fs . exists ( tmpFile )
320+ if ( res ) {
321+ throw new Error ( `Expected file to NOT exist: "${ tmpFile } "` )
322+ }
323+ } )
324+
325+ // test rename
326+ await withFailCtx ( 'writeFileForRename' , ( ) => fs . writeFile ( tmpFile , 'TestingRename' ) )
327+ const tmpFileRenamed = tmpFile + '.renamed'
328+ await withFailCtx ( 'renameFile' , ( ) => fs . rename ( tmpFile , tmpFileRenamed ) )
329+ await withFailCtx ( 'existsRenamedFile' , async ( ) => {
330+ const res = await fs . exists ( tmpFileRenamed )
331+ if ( ! res ) {
332+ throw new Error ( `Expected RENAMED file to exist: "${ tmpFileRenamed } "` )
333+ }
334+ } )
335+ await withFailCtx ( 'writeToRenamedFile' , async ( ) => fs . writeFile ( tmpFileRenamed , 'hello' ) )
336+ await withFailCtx ( 'readFromRenamedFile' , async ( ) => {
337+ const res = await fs . readFileText ( tmpFileRenamed )
338+ if ( res !== 'hello' ) {
339+ throw new Error ( `Expected RENAMED file to be writable: "${ tmpFileRenamed } "` )
340+ }
341+ } )
342+ await withFailCtx ( 'renameFileReset' , ( ) => fs . rename ( tmpFileRenamed , tmpFile ) )
343+ await withFailCtx ( 'renameFileResetExists' , async ( ) => {
344+ const res = await fs . exists ( tmpFile )
345+ if ( ! res ) {
346+ throw new Error ( `Expected reverted renamed file to exist: "${ tmpFile } "` )
347+ }
348+ } )
349+ } finally {
350+ await fs . delete ( tmpFolder , { recursive : true , force : true } )
351+ }
352+
353+ async function withFailCtx < T > ( ctx : string , fn : ( ) => Promise < T > ) : Promise < T > {
354+ try {
355+ return await fn ( )
356+ } catch ( e ) {
357+ if ( ! ( e instanceof Error ) ) {
358+ throw e
359+ }
360+ throw FileSystemStabilityException . chain ( e , `context: "${ ctx } "` , { code : FileSystemStabilityExceptionId } )
361+ }
362+ }
363+ }
0 commit comments