@@ -1012,6 +1012,133 @@ onlyOnNode20('FsaNodeFs', () => {
10121012 expect ( error . message ) . toBe ( '"start" must be >= zero' ) ;
10131013 }
10141014 } ) ;
1015+
1016+ test ( 'emits "error" and destroys the stream when the path is a directory' , async ( ) => {
1017+ const { fs } = setup ( { folder : { file : 'test' } } ) ;
1018+ const stream = fs . createWriteStream ( '/folder' ) ;
1019+ const error = await new Promise < any > ( resolve => stream . once ( 'error' , resolve ) ) ;
1020+ expect ( error . code ) . toBe ( 'EISDIR' ) ;
1021+ expect ( stream . destroyed ) . toBe ( true ) ;
1022+ } ) ;
1023+
1024+ test ( 'a failed open does not leak an unhandled rejection' , async ( ) => {
1025+ const { fs } = setup ( { folder : { file : 'test' } } ) ;
1026+ const unhandled : unknown [ ] = [ ] ;
1027+ const onUnhandled = ( error : unknown ) => unhandled . push ( error ) ;
1028+ process . on ( 'unhandledRejection' , onUnhandled ) ;
1029+ try {
1030+ const stream = fs . createWriteStream ( '/folder' ) ;
1031+ stream . on ( 'error' , ( ) => { } ) ;
1032+ await new Promise ( resolve => stream . once ( 'close' , resolve ) ) ;
1033+ await tick ( 10 ) ;
1034+ expect ( unhandled ) . toStrictEqual ( [ ] ) ;
1035+ } finally {
1036+ process . off ( 'unhandledRejection' , onUnhandled ) ;
1037+ }
1038+ } ) ;
1039+
1040+ test ( 'a failed open surfaces through the write callback' , async ( ) => {
1041+ const { fs } = setup ( { folder : { file : 'test' } } ) ;
1042+ const stream = fs . createWriteStream ( '/folder' ) ;
1043+ stream . on ( 'error' , ( ) => { } ) ;
1044+ const error = await new Promise < any > ( resolve => stream . write ( Buffer . from ( 'A' ) , resolve ) ) ;
1045+ expect ( error . code ) . toBe ( 'EISDIR' ) ;
1046+ } ) ;
1047+
1048+ test ( 'a failed open is reported once, even if the stream is closed afterwards' , async ( ) => {
1049+ const { fs } = setup ( { folder : { file : 'test' } } ) ;
1050+ const stream = fs . createWriteStream ( '/folder' ) ;
1051+ let errors = 0 ;
1052+ let closes = 0 ;
1053+ stream . on ( 'error' , ( ) => errors ++ ) ;
1054+ stream . on ( 'close' , ( ) => closes ++ ) ;
1055+ await new Promise ( resolve => stream . once ( 'close' , resolve ) ) ;
1056+ await new Promise < void > ( resolve => stream . close ( ( ) => resolve ( ) ) ) ;
1057+ await tick ( 10 ) ;
1058+ expect ( errors ) . toBe ( 1 ) ;
1059+ expect ( closes ) . toBe ( 1 ) ;
1060+ } ) ;
1061+
1062+ test ( 'autoClose closes the file descriptor even when "emitClose" is false' , async ( ) => {
1063+ const { fs } = setup ( { folder : { file : 'test' } } ) ;
1064+ const handle = await fs . promises . open ( '/folder/file' , 'a' ) ;
1065+ const stream = fs . createWriteStream ( '' , { fd : handle . fd , start : 1 , flags : 'a' , emitClose : false } ) ;
1066+ stream . write ( Buffer . from ( 'BC' ) ) ;
1067+ const stat = async ( ) =>
1068+ await new Promise ( ( resolve , reject ) =>
1069+ fs . fstat ( handle . fd , ( err , stats ) => {
1070+ if ( err ) reject ( err ) ;
1071+ else resolve ( stats ) ;
1072+ } ) ,
1073+ ) ;
1074+ await stat ( ) ;
1075+ stream . end ( ) ;
1076+ await until ( async ( ) => {
1077+ const [ , error ] = await of ( stat ( ) ) ;
1078+ return ! ! error ;
1079+ } ) ;
1080+ const [ , error ] = await of ( stat ( ) ) ;
1081+ expect ( ( error as any ) . code ) . toBe ( 'EBADF' ) ;
1082+ } ) ;
1083+
1084+ test ( 'autoClose closes the file descriptor exactly once' , async ( ) => {
1085+ const { fs } = setup ( { folder : { file : 'test' } } ) ;
1086+ const handle = await fs . promises . open ( '/folder/file' , 'a' ) ;
1087+ const closed : number [ ] = [ ] ;
1088+ const close = fs . close ;
1089+ ( fs as any ) . close = ( fd : number , cb : any ) => {
1090+ closed . push ( fd ) ;
1091+ return close ( fd , cb ) ;
1092+ } ;
1093+ const stream = fs . createWriteStream ( '' , { fd : handle . fd , start : 1 , flags : 'a' } ) ;
1094+ stream . write ( Buffer . from ( 'BC' ) ) ;
1095+ stream . end ( ) ;
1096+ await new Promise ( resolve => stream . once ( 'close' , resolve ) ) ;
1097+ await tick ( 20 ) ;
1098+ expect ( closed ) . toStrictEqual ( [ handle . fd ] ) ;
1099+ } ) ;
1100+
1101+ test ( 'close() after a destroy() reports the close failure, and only once it has happened' , async ( ) => {
1102+ const { fs, mfs } = setup ( { dir : { keep : 'x' } } ) ;
1103+ const stream = fs . createWriteStream ( '/dir/file.txt' ) ;
1104+ const order : string [ ] = [ ] ;
1105+ stream . on ( 'error' , ( error : any ) => order . push ( 'error(' + error . code + ')' ) ) ;
1106+ await new Promise ( resolve => stream . once ( 'open' , resolve ) ) ;
1107+ stream . write ( 'important' ) ;
1108+ await tick ( 10 ) ;
1109+ mfs . rmSync ( '/mountpoint/dir' , { recursive : true , force : true } ) ;
1110+ stream . destroy ( ) ;
1111+ await new Promise < void > ( resolve =>
1112+ stream . close ( ( error : any ) => {
1113+ order . push ( 'cb(' + ( error ? error . code : 'no error' ) + ')' ) ;
1114+ resolve ( ) ;
1115+ } ) ,
1116+ ) ;
1117+ expect ( order ) . toStrictEqual ( [ 'error(ENOENT)' , 'cb(ENOENT)' ] ) ;
1118+ } ) ;
1119+
1120+ test ( 'a genuine close() failure is still reported after a plain destroy()' , async ( ) => {
1121+ const { fs, mfs } = setup ( { dir : { keep : 'x' } } ) ;
1122+ const stream = fs . createWriteStream ( '/dir/file.txt' ) ;
1123+ const errors : string [ ] = [ ] ;
1124+ stream . on ( 'error' , ( error : any ) => errors . push ( error . code ) ) ;
1125+ await new Promise ( resolve => stream . once ( 'open' , resolve ) ) ;
1126+ stream . write ( 'important' ) ;
1127+ await tick ( 10 ) ;
1128+ mfs . rmSync ( '/mountpoint/dir' , { recursive : true , force : true } ) ;
1129+ stream . destroy ( ) ;
1130+ stream . close ( ( ) => { } ) ;
1131+ await tick ( 20 ) ;
1132+ expect ( errors ) . toStrictEqual ( [ 'ENOENT' ] ) ;
1133+ } ) ;
1134+
1135+ test ( 'an unhandled failed open is not masked by the autoClose listener' , async ( ) => {
1136+ const { fs } = setup ( { folder : { file : 'test' } } ) ;
1137+ const stream = fs . createWriteStream ( '/folder' ) ;
1138+ expect ( stream . listenerCount ( 'error' ) ) . toBe ( 0 ) ;
1139+ stream . on ( 'error' , ( ) => { } ) ;
1140+ await tick ( 10 ) ;
1141+ } ) ;
10151142 } ) ;
10161143
10171144 describe ( '.createReadStream()' , ( ) => {
@@ -1093,6 +1220,28 @@ onlyOnNode20('FsaNodeFs', () => {
10931220 } ) ;
10941221 } ) ;
10951222
1223+ test ( 'emits "error" and destroys the stream when the file is missing' , async ( ) => {
1224+ const { fs } = setup ( { folder : { file : 'test' } } ) ;
1225+ const stream = fs . createReadStream ( '/nope.txt' ) ;
1226+ const error = await new Promise < any > ( resolve => stream . once ( 'error' , resolve ) ) ;
1227+ expect ( error . code ) . toBe ( 'ENOENT' ) ;
1228+ expect ( stream . destroyed ) . toBe ( true ) ;
1229+ } ) ;
1230+
1231+ test ( 'a failed open does not leak an unhandled rejection' , async ( ) => {
1232+ const { fs } = setup ( { folder : { file : 'test' } } ) ;
1233+ const unhandled : unknown [ ] = [ ] ;
1234+ const onUnhandled = ( error : unknown ) => unhandled . push ( error ) ;
1235+ process . on ( 'unhandledRejection' , onUnhandled ) ;
1236+ try {
1237+ fs . createReadStream ( '/nope.txt' ) . on ( 'error' , ( ) => { } ) ;
1238+ await tick ( 10 ) ;
1239+ expect ( unhandled ) . toStrictEqual ( [ ] ) ;
1240+ } finally {
1241+ process . off ( 'unhandledRejection' , onUnhandled ) ;
1242+ }
1243+ } ) ;
1244+
10961245 test ( 'can read with start position at file size' , async ( ) => {
10971246 const { fs, vol } = setup ( { folder : { file : 'test' } , 'empty-folder' : null , 'f.html' : 'test' } ) ;
10981247 const readStream = fs . createReadStream ( '/folder/file' , { start : 4 } ) ;
0 commit comments