1// Flags: --expose-internals 2'use strict'; 3const common = require('../common'); 4const tmpdir = require('../common/tmpdir'); 5const assert = require('assert'); 6const fs = require('fs'); 7const path = require('path'); 8const { validateRmdirOptions } = require('internal/fs/utils'); 9 10tmpdir.refresh(); 11 12let count = 0; 13const nextDirPath = (name = 'rmdir-recursive') => 14 path.join(tmpdir.path, `${name}-${count++}`); 15 16function makeNonEmptyDirectory(depth, files, folders, dirname, createSymLinks) { 17 fs.mkdirSync(dirname, { recursive: true }); 18 fs.writeFileSync(path.join(dirname, 'text.txt'), 'hello', 'utf8'); 19 20 const options = { flag: 'wx' }; 21 22 for (let f = files; f > 0; f--) { 23 fs.writeFileSync(path.join(dirname, `f-${depth}-${f}`), '', options); 24 } 25 26 if (createSymLinks) { 27 // Valid symlink 28 fs.symlinkSync( 29 `f-${depth}-1`, 30 path.join(dirname, `link-${depth}-good`), 31 'file' 32 ); 33 34 // Invalid symlink 35 fs.symlinkSync( 36 'does-not-exist', 37 path.join(dirname, `link-${depth}-bad`), 38 'file' 39 ); 40 } 41 42 // File with a name that looks like a glob 43 fs.writeFileSync(path.join(dirname, '[a-z0-9].txt'), '', options); 44 45 depth--; 46 if (depth <= 0) { 47 return; 48 } 49 50 for (let f = folders; f > 0; f--) { 51 fs.mkdirSync( 52 path.join(dirname, `folder-${depth}-${f}`), 53 { recursive: true } 54 ); 55 makeNonEmptyDirectory( 56 depth, 57 files, 58 folders, 59 path.join(dirname, `d-${depth}-${f}`), 60 createSymLinks 61 ); 62 } 63} 64 65function removeAsync(dir) { 66 // Removal should fail without the recursive option. 67 fs.rmdir(dir, common.mustCall((err) => { 68 assert.strictEqual(err.syscall, 'rmdir'); 69 70 // Removal should fail without the recursive option set to true. 71 fs.rmdir(dir, { recursive: false }, common.mustCall((err) => { 72 assert.strictEqual(err.syscall, 'rmdir'); 73 74 // Recursive removal should succeed. 75 fs.rmdir(dir, { recursive: true }, common.mustCall((err) => { 76 assert.ifError(err); 77 78 // No error should occur if recursive and the directory does not exist. 79 fs.rmdir(dir, { recursive: true }, common.mustCall((err) => { 80 assert.ifError(err); 81 82 // Attempted removal should fail now because the directory is gone. 83 fs.rmdir(dir, common.mustCall((err) => { 84 assert.strictEqual(err.syscall, 'rmdir'); 85 })); 86 })); 87 })); 88 })); 89 })); 90} 91 92// Test the asynchronous version 93{ 94 // Create a 4-level folder hierarchy including symlinks 95 let dir = nextDirPath(); 96 makeNonEmptyDirectory(4, 10, 2, dir, true); 97 removeAsync(dir); 98 99 // Create a 2-level folder hierarchy without symlinks 100 dir = nextDirPath(); 101 makeNonEmptyDirectory(2, 10, 2, dir, false); 102 removeAsync(dir); 103 104 // Create a flat folder including symlinks 105 dir = nextDirPath(); 106 makeNonEmptyDirectory(1, 10, 2, dir, true); 107 removeAsync(dir); 108} 109 110// Test the synchronous version. 111{ 112 const dir = nextDirPath(); 113 makeNonEmptyDirectory(4, 10, 2, dir, true); 114 115 // Removal should fail without the recursive option set to true. 116 assert.throws(() => { 117 fs.rmdirSync(dir); 118 }, { syscall: 'rmdir' }); 119 assert.throws(() => { 120 fs.rmdirSync(dir, { recursive: false }); 121 }, { syscall: 'rmdir' }); 122 123 // Recursive removal should succeed. 124 fs.rmdirSync(dir, { recursive: true }); 125 126 // No error should occur if recursive and the directory does not exist. 127 fs.rmdirSync(dir, { recursive: true }); 128 129 // Attempted removal should fail now because the directory is gone. 130 assert.throws(() => fs.rmdirSync(dir), { syscall: 'rmdir' }); 131} 132 133// Test the Promises based version. 134(async () => { 135 const dir = nextDirPath(); 136 makeNonEmptyDirectory(4, 10, 2, dir, true); 137 138 // Removal should fail without the recursive option set to true. 139 assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' }); 140 assert.rejects(fs.promises.rmdir(dir, { recursive: false }), { 141 syscall: 'rmdir' 142 }); 143 144 // Recursive removal should succeed. 145 await fs.promises.rmdir(dir, { recursive: true }); 146 147 // No error should occur if recursive and the directory does not exist. 148 await fs.promises.rmdir(dir, { recursive: true }); 149 150 // Attempted removal should fail now because the directory is gone. 151 assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' }); 152})(); 153 154// Test input validation. 155{ 156 const defaults = { 157 retryDelay: 100, 158 maxRetries: 0, 159 recursive: false 160 }; 161 const modified = { 162 retryDelay: 953, 163 maxRetries: 5, 164 recursive: true 165 }; 166 167 assert.deepStrictEqual(validateRmdirOptions(), defaults); 168 assert.deepStrictEqual(validateRmdirOptions({}), defaults); 169 assert.deepStrictEqual(validateRmdirOptions(modified), modified); 170 assert.deepStrictEqual(validateRmdirOptions({ 171 maxRetries: 99 172 }), { 173 retryDelay: 100, 174 maxRetries: 99, 175 recursive: false 176 }); 177 178 [null, 'foo', 5, NaN].forEach((bad) => { 179 assert.throws(() => { 180 validateRmdirOptions(bad); 181 }, { 182 code: 'ERR_INVALID_ARG_TYPE', 183 name: 'TypeError', 184 message: /^The "options" argument must be of type object\./ 185 }); 186 }); 187 188 [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => { 189 assert.throws(() => { 190 validateRmdirOptions({ recursive: bad }); 191 }, { 192 code: 'ERR_INVALID_ARG_TYPE', 193 name: 'TypeError', 194 message: /^The "recursive" argument must be of type boolean\./ 195 }); 196 }); 197 198 assert.throws(() => { 199 validateRmdirOptions({ retryDelay: -1 }); 200 }, { 201 code: 'ERR_OUT_OF_RANGE', 202 name: 'RangeError', 203 message: /^The value of "retryDelay" is out of range\./ 204 }); 205 206 assert.throws(() => { 207 validateRmdirOptions({ maxRetries: -1 }); 208 }, { 209 code: 'ERR_OUT_OF_RANGE', 210 name: 'RangeError', 211 message: /^The value of "maxRetries" is out of range\./ 212 }); 213} 214