basic.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. 'use strict'
  2. process.setMaxListeners(1000000);
  3. const _ = require('lodash')
  4. const fs = require('fs')
  5. const os = require('os')
  6. const path = require('path')
  7. const {test} = require('tap')
  8. const requireInject = require('require-inject')
  9. let expectClose = 0
  10. let closeCalled = 0
  11. let expectCloseSync = 0
  12. let closeSyncCalled = 0
  13. const createErr = code => Object.assign(new Error(code), { code })
  14. let unlinked = []
  15. const fsMock = Object.assign ( {}, fs, {
  16. /* ASYNC */
  17. mkdir (filename, opts, cb) {
  18. return cb(null);
  19. },
  20. realpath (filename, cb) {
  21. return cb(null, filename)
  22. },
  23. open (tmpfile, options, mode, cb) {
  24. if (/noopen/.test(tmpfile)) return cb(createErr('ENOOPEN'))
  25. expectClose++
  26. cb(null, tmpfile)
  27. },
  28. write (fd) {
  29. const cb = arguments[arguments.length - 1]
  30. if (/nowrite/.test(fd)) return cb(createErr('ENOWRITE'))
  31. cb()
  32. },
  33. fsync (fd, cb) {
  34. if (/nofsync/.test(fd)) return cb(createErr('ENOFSYNC'))
  35. cb()
  36. },
  37. close (fd, cb) {
  38. closeCalled++
  39. cb()
  40. },
  41. chown (tmpfile, uid, gid, cb) {
  42. if (/nochown/.test(tmpfile)) return cb(createErr('ENOCHOWN'))
  43. if (/enosys/.test(tmpfile)) return cb(createErr('ENOSYS'))
  44. if (/einval/.test(tmpfile)) return cb(createErr('EINVAL'))
  45. if (/eperm/.test(tmpfile)) return cb(createErr('EPERM'))
  46. cb()
  47. },
  48. chmod (tmpfile, mode, cb) {
  49. if (/nochmod/.test(tmpfile)) return cb(createErr('ENOCHMOD'))
  50. if (/enosys/.test(tmpfile)) return cb(createErr('ENOSYS'))
  51. if (/eperm/.test(tmpfile)) return cb(createErr('EPERM'))
  52. if (/einval/.test(tmpfile)) return cb(createErr('EINVAL'))
  53. cb()
  54. },
  55. rename (tmpfile, filename, cb) {
  56. if (/norename/.test(tmpfile)) return cb(createErr('ENORENAME'))
  57. cb()
  58. },
  59. unlink (tmpfile, cb) {
  60. if (/nounlink/.test(tmpfile)) return cb(createErr('ENOUNLINK'))
  61. cb()
  62. },
  63. stat (tmpfile, cb) {
  64. if (/nostat/.test(tmpfile)) return cb(createErr('ENOSTAT'))
  65. if (/statful/.test(tmpfile)) return cb(null, fs.statSync('/'));
  66. cb()
  67. },
  68. /* SYNC */
  69. mkdirSync (filename) {},
  70. realpathSync (filename, cb) {
  71. return filename
  72. },
  73. openSync (tmpfile, options) {
  74. if (/noopen/.test(tmpfile)) throw createErr('ENOOPEN')
  75. expectCloseSync++
  76. return tmpfile
  77. },
  78. writeSync (fd) {
  79. if (/nowrite/.test(fd)) throw createErr('ENOWRITE')
  80. },
  81. fsyncSync (fd) {
  82. if (/nofsync/.test(fd)) throw createErr('ENOFSYNC')
  83. },
  84. closeSync (fd) {
  85. closeSyncCalled++
  86. },
  87. chownSync (tmpfile, uid, gid) {
  88. if (/nochown/.test(tmpfile)) throw createErr('ENOCHOWN')
  89. if (/enosys/.test(tmpfile)) throw createErr('ENOSYS')
  90. if (/einval/.test(tmpfile)) throw createErr('EINVAL')
  91. if (/eperm/.test(tmpfile)) throw createErr('EPERM')
  92. },
  93. chmodSync (tmpfile, mode) {
  94. if (/nochmod/.test(tmpfile)) throw createErr('ENOCHMOD')
  95. if (/enosys/.test(tmpfile)) throw createErr('ENOSYS')
  96. if (/einval/.test(tmpfile)) throw createErr('EINVAL')
  97. if (/eperm/.test(tmpfile)) throw createErr('EPERM')
  98. },
  99. renameSync (tmpfile, filename) {
  100. if (/norename/.test(tmpfile)) throw createErr('ENORENAME')
  101. },
  102. unlinkSync (tmpfile) {
  103. if (/nounlink/.test(tmpfile)) throw createErr('ENOUNLINK')
  104. unlinked.push(tmpfile)
  105. },
  106. statSync (tmpfile) {
  107. if (/nostat/.test(tmpfile)) throw createErr('ENOSTAT')
  108. if (/statful/.test(tmpfile)) return fs.statSync('/');
  109. }
  110. });
  111. const makeUnstableAsyncFn = function () {
  112. return function () {
  113. if ( Math.random () <= .75 ) {
  114. const code = _.shuffle ([ 'EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY', 'EACCESS', 'EPERM' ])[0];
  115. throw createErr ( code );
  116. }
  117. return arguments[arguments.length -1](null, arguments[0]);
  118. };
  119. };
  120. const makeUnstableSyncFn = function ( fn ) {
  121. return function () {
  122. if ( Math.random () <= .75 ) {
  123. const code = _.shuffle ([ 'EMFILE', 'ENFILE', 'EAGAIN', 'EBUSY', 'EACCESS', 'EPERM' ])[0];
  124. throw createErr ( code );
  125. }
  126. return fn.apply(undefined, arguments)
  127. };
  128. };
  129. const fsMockUnstable = Object.assign ( {}, fsMock, {
  130. open: makeUnstableAsyncFn (),
  131. write: makeUnstableAsyncFn (),
  132. fsync: makeUnstableAsyncFn (),
  133. close: makeUnstableAsyncFn (),
  134. rename: makeUnstableAsyncFn (),
  135. openSync: makeUnstableSyncFn ( _.identity ),
  136. writeSync: makeUnstableSyncFn ( _.noop ),
  137. fsyncSync: makeUnstableSyncFn ( _.noop ),
  138. closeSync: makeUnstableSyncFn ( _.noop ),
  139. renameSync: makeUnstableSyncFn ( _.noop )
  140. });
  141. const {writeFile: writeFileAtomic, writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMock });
  142. test('async tests', t => {
  143. t.plan(2)
  144. expectClose = 0
  145. closeCalled = 0
  146. t.teardown(() => {
  147. t.parent.equal(closeCalled, expectClose, 'async tests closed all files')
  148. expectClose = 0
  149. closeCalled = 0
  150. })
  151. t.test('non-root tests', t => {
  152. t.plan(28)
  153. writeFileAtomic('good', 'test', { mode: '0777' }, err => {
  154. t.notOk(err, 'No errors occur when passing in options')
  155. })
  156. writeFileAtomic('good', 'test', 'utf8', err => {
  157. t.notOk(err, 'No errors occur when passing in options as string')
  158. })
  159. writeFileAtomic('good', 'test', undefined, err => {
  160. t.notOk(err, 'No errors occur when NOT passing in options')
  161. })
  162. writeFileAtomic('good', 'test', err => {
  163. t.notOk(err)
  164. })
  165. writeFileAtomic('noopen', 'test', err => {
  166. t.is(err && err.message, 'ENOOPEN', 'fs.open failures propagate')
  167. })
  168. writeFileAtomic('nowrite', 'test', err => {
  169. t.is(err && err.message, 'ENOWRITE', 'fs.writewrite failures propagate')
  170. })
  171. writeFileAtomic('nowrite', Buffer.from('test', 'utf8'), err => {
  172. t.is(err && err.message, 'ENOWRITE', 'fs.writewrite failures propagate for buffers')
  173. })
  174. writeFileAtomic('nochown', 'test', { chown: { uid: 100, gid: 100 } }, err => {
  175. t.is(err && err.message, 'ENOCHOWN', 'Chown failures propagate')
  176. })
  177. writeFileAtomic('nochown', 'test', err => {
  178. t.notOk(err, 'No attempt to chown when no uid/gid passed in')
  179. })
  180. writeFileAtomic('nochmod', 'test', { mode: parseInt('741', 8) }, err => {
  181. t.is(err && err.message, 'ENOCHMOD', 'Chmod failures propagate')
  182. })
  183. writeFileAtomic('nofsyncopt', 'test', { fsync: false }, err => {
  184. t.notOk(err, 'fsync skipped if options.fsync is false')
  185. })
  186. writeFileAtomic('norename', 'test', err => {
  187. t.is(err && err.message, 'ENORENAME', 'Rename errors propagate')
  188. })
  189. writeFileAtomic('norename nounlink', 'test', err => {
  190. t.is(err && err.message, 'ENORENAME', 'Failure to unlink the temp file does not clobber the original error')
  191. })
  192. writeFileAtomic('nofsync', 'test', err => {
  193. t.is(err && err.message, 'ENOFSYNC', 'Fsync failures propagate')
  194. })
  195. writeFileAtomic('enosys', 'test', err => {
  196. t.notOk(err, 'No errors on ENOSYS')
  197. })
  198. writeFileAtomic('einval', 'test', { mode: 0o741 }, err => {
  199. t.notOk(err, 'No errors on EINVAL for non root')
  200. })
  201. writeFileAtomic('eperm', 'test', { mode: 0o741 }, err => {
  202. t.notOk(err, 'No errors on EPERM for non root')
  203. })
  204. writeFileAtomic('einval', 'test', { chown: { uid: 100, gid: 100 } }, err => {
  205. t.notOk(err, 'No errors on EINVAL for non root')
  206. })
  207. writeFileAtomic('eperm', 'test', { chown: { uid: 100, gid: 100 } }, err => {
  208. t.notOk(err, 'No errors on EPERM for non root')
  209. })
  210. const optionsImmutable = {};
  211. writeFileAtomic('statful', 'test', optionsImmutable, err => {
  212. t.notOk(err);
  213. t.deepEquals(optionsImmutable, {});
  214. });
  215. const schedule = filePath => {
  216. t.is(filePath, 'good');
  217. return new Promise ( resolve => {
  218. resolve ( () => {
  219. t.is(true,true);
  220. });
  221. });
  222. };
  223. writeFileAtomic('good','test', {schedule}, err => {
  224. t.notOk(err);
  225. });
  226. const tmpCreate = filePath => `.${filePath}.custom`;
  227. const tmpCreated = filePath => t.is ( filePath, '.good.custom' );
  228. writeFileAtomic('good','test', {tmpCreate, tmpCreated}, err => {
  229. t.notOk(err)
  230. })
  231. const longPath = path.join(os.tmpdir(),'.012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789.txt');
  232. const {writeFile: writeFileAtomicNative} = requireInject('../dist', { fs });
  233. writeFileAtomicNative(longPath,'test', err => {
  234. t.notOk(err)
  235. })
  236. const pathMissingFolders = path.join(os.tmpdir(),String(Math.random()),String(Math.random()),String(Math.random()),'foo.txt');
  237. writeFileAtomicNative(pathMissingFolders,'test', err => {
  238. t.notOk(err)
  239. })
  240. })
  241. t.test('errors for root', t => {
  242. const { getuid } = process
  243. process.getuid = () => 0
  244. t.teardown(() => {
  245. process.getuid = getuid
  246. })
  247. const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMock });
  248. t.plan(2)
  249. writeFileAtomic('einval', 'test', { chown: { uid: 100, gid: 100 } }, err => {
  250. t.match(err, { code: 'EINVAL' })
  251. })
  252. writeFileAtomic('einval', 'test', { mode: 0o741 }, err => {
  253. t.match(err, { code: 'EINVAL' })
  254. })
  255. })
  256. })
  257. test('unstable async tests', t => {
  258. t.plan(2);
  259. const {writeFile: writeFileAtomic} = requireInject('../dist', { fs: fsMockUnstable });
  260. writeFileAtomic('good', 'test', err => {
  261. t.notOk(err, 'No errors occur when retryable errors are thrown')
  262. })
  263. writeFileAtomic('good', 'test', { timeout: 0 }, err => {
  264. t.is(!!err.code, true, 'Retrying can be disabled')
  265. })
  266. });
  267. test('sync tests', t => {
  268. t.plan(2)
  269. closeSyncCalled = 0
  270. expectCloseSync = 0
  271. t.teardown(() => {
  272. t.parent.equal(closeSyncCalled, expectCloseSync, 'sync closed all files')
  273. expectCloseSync = 0
  274. closeSyncCalled = 0
  275. })
  276. const throws = function (t, shouldthrow, msg, todo) {
  277. let err
  278. try { todo() } catch (e) { err = e }
  279. t.is(shouldthrow, err && err.message, msg)
  280. }
  281. const noexception = function (t, msg, todo) {
  282. let err
  283. try { todo() } catch (e) { err = e }
  284. t.ifError(err, msg)
  285. }
  286. let tmpfile
  287. t.test('non-root', t => {
  288. t.plan(38)
  289. noexception(t, 'No errors occur when passing in options', () => {
  290. writeFileAtomicSync('good', 'test', { mode: '0777' })
  291. })
  292. noexception(t, 'No errors occur when passing in options as string', () => {
  293. writeFileAtomicSync('good', 'test', 'utf8')
  294. })
  295. noexception(t, 'No errors occur when NOT passing in options', () => {
  296. writeFileAtomicSync('good', 'test')
  297. })
  298. noexception(t, 'fsync never called if options.fsync is falsy', () => {
  299. writeFileAtomicSync('good', 'test', { fsync: false })
  300. })
  301. noexception(t, 'tmpCreated is called on success', () => {
  302. writeFileAtomicSync('good', 'test', {
  303. tmpCreated (gottmpfile) {
  304. tmpfile = gottmpfile
  305. }
  306. })
  307. t.match(tmpfile, /^good\.tmp-\w+$/, 'tmpCreated called for success')
  308. t.match(tmpfile, /^good\.tmp-\d{10}[a-f0-9]{6}$/, 'tmpCreated format')
  309. })
  310. tmpfile = undefined
  311. throws(t, 'ENOOPEN', 'fs.openSync failures propagate', () => {
  312. writeFileAtomicSync('noopen', 'test', {
  313. tmpCreated (gottmpfile) {
  314. tmpfile = gottmpfile
  315. }
  316. })
  317. })
  318. t.is(tmpfile, undefined, 'tmpCreated not called for open failure')
  319. throws(t, 'ENOWRITE', 'fs.writeSync failures propagate', () => {
  320. writeFileAtomicSync('nowrite', 'test', {
  321. tmpCreated (gottmpfile) {
  322. tmpfile = gottmpfile
  323. }
  324. })
  325. })
  326. t.match(tmpfile, /^nowrite\.tmp-\w+$/, 'tmpCreated called for failure after open')
  327. throws(t, 'ENOCHOWN', 'Chown failures propagate', () => {
  328. writeFileAtomicSync('nochown', 'test', { chown: { uid: 100, gid: 100 } })
  329. })
  330. noexception(t, 'No attempt to chown when false passed in', () => {
  331. writeFileAtomicSync('nochown', 'test', { chown: false })
  332. })
  333. noexception(t, 'No errors occured when chown is undefined and original file owner used', () => {
  334. writeFileAtomicSync('chowncopy', 'test', { chown: undefined })
  335. })
  336. throws(t, 'ENORENAME', 'Rename errors propagate', () => {
  337. writeFileAtomicSync('norename', 'test')
  338. })
  339. throws(t, 'ENORENAME', 'Failure to unlink the temp file does not clobber the original error', () => {
  340. writeFileAtomicSync('norename nounlink', 'test')
  341. })
  342. throws(t, 'ENOFSYNC', 'Fsync errors propagate', () => {
  343. writeFileAtomicSync('nofsync', 'test')
  344. })
  345. noexception(t, 'No errors on ENOSYS', () => {
  346. writeFileAtomicSync('enosys', 'test', { chown: { uid: 100, gid: 100 } })
  347. })
  348. noexception(t, 'No errors on EINVAL for non root', () => {
  349. writeFileAtomicSync('einval', 'test', { chown: { uid: 100, gid: 100 } })
  350. })
  351. noexception(t, 'No errors on EPERM for non root', () => {
  352. writeFileAtomicSync('eperm', 'test', { chown: { uid: 100, gid: 100 } })
  353. })
  354. throws(t, 'ENOCHMOD', 'Chmod failures propagate', () => {
  355. writeFileAtomicSync('nochmod', 'test', { mode: 0o741 })
  356. })
  357. noexception(t, 'No errors on EPERM for non root', () => {
  358. writeFileAtomicSync('eperm', 'test', { mode: 0o741 })
  359. })
  360. noexception(t, 'No attempt to chmod when no mode provided', () => {
  361. writeFileAtomicSync('nochmod', 'test', { mode: false })
  362. })
  363. const optionsImmutable = {};
  364. noexception(t, 'options are immutable', () => {
  365. writeFileAtomicSync('statful', 'test', optionsImmutable)
  366. })
  367. t.deepEquals(optionsImmutable, {});
  368. const tmpCreate = filePath => `.${filePath}.custom`;
  369. const tmpCreated = filePath => t.is ( filePath, '.good.custom' );
  370. noexception(t, 'custom temp creator', () => {
  371. writeFileAtomicSync('good', 'test', {tmpCreate, tmpCreated})
  372. })
  373. const path0 = path.join(os.tmpdir(),'atomically-test-0');
  374. const tmpPath0 = path0 + '.temp';
  375. noexception(t, 'temp files are purged on success', () => {
  376. const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs });
  377. writeFileAtomicSync(path0, 'test', {tmpCreate: () => tmpPath0})
  378. })
  379. t.is(true,fs.existsSync(path0));
  380. t.is(false,fs.existsSync(tmpPath0));
  381. const path1 = path.join(os.tmpdir(),'atomically-test-norename-1');
  382. const tmpPath1 = path1 + '.temp';
  383. throws(t, 'ENORENAME', 'temp files are purged on error', () => {
  384. const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: Object.assign ( {}, fs, { renameSync: fsMock.renameSync })});
  385. writeFileAtomicSync(path1, 'test', {tmpCreate: () => tmpPath1})
  386. })
  387. t.is(false,fs.existsSync(path1));
  388. t.is(false,fs.existsSync(tmpPath1));
  389. const path2 = path.join(os.tmpdir(),'atomically-test-norename-2');
  390. const tmpPath2 = path2 + '.temp';
  391. throws(t, 'ENORENAME', 'temp files can also not be purged on error', () => {
  392. const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: Object.assign ( {}, fs, { renameSync: fsMock.renameSync })});
  393. writeFileAtomicSync(path2, 'test', {tmpCreate: () => tmpPath2,tmpPurge: false})
  394. })
  395. t.is(false,fs.existsSync(path2));
  396. t.is(true,fs.existsSync(tmpPath2));
  397. const longPath = path.join(os.tmpdir(),'.012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789.txt');
  398. noexception(t, 'temp files are truncated', () => {
  399. const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs });
  400. writeFileAtomicSync(longPath, 'test')
  401. })
  402. const pathMissingFolders = path.join(os.tmpdir(),String(Math.random()),String(Math.random()),String(Math.random()),'foo.txt');
  403. noexception(t, 'parent folders are created', () => {
  404. const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs });
  405. writeFileAtomicSync(pathMissingFolders, 'test')
  406. })
  407. })
  408. t.test('errors for root', t => {
  409. const { getuid } = process
  410. process.getuid = () => 0
  411. t.teardown(() => {
  412. process.getuid = getuid
  413. })
  414. const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMock });
  415. t.plan(2)
  416. throws(t, 'EINVAL', 'Chown error as root user', () => {
  417. writeFileAtomicSync('einval', 'test', { chown: { uid: 100, gid: 100 } })
  418. })
  419. throws(t, 'EINVAL', 'Chmod error as root user', () => {
  420. writeFileAtomicSync('einval', 'test', { mode: 0o741 })
  421. })
  422. })
  423. })
  424. test('unstable sync tests', t => {
  425. t.plan(2);
  426. const throws = function (t, msg, todo) {
  427. let err
  428. try { todo() } catch (e) { err = e }
  429. t.is(!!err.code, true, msg)
  430. }
  431. const noexception = function (t, msg, todo) {
  432. let err
  433. try { todo() } catch (e) { err = e }
  434. t.ifError(err, msg)
  435. }
  436. noexception(t, 'No errors occur when retryable errors are thrown', () => {
  437. const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMockUnstable });
  438. writeFileAtomicSync('good', 'test')
  439. })
  440. throws(t, 'retrying can be disabled', () => {
  441. const {writeFileSync: writeFileAtomicSync} = requireInject('../dist', { fs: fsMockUnstable });
  442. writeFileAtomicSync('good', 'test', { timeout: 0 })
  443. })
  444. });
  445. test('promises', async t => {
  446. let tmpfile
  447. closeCalled = 0
  448. expectClose = 0
  449. t.teardown(() => {
  450. t.parent.equal(closeCalled, expectClose, 'promises closed all files')
  451. closeCalled = 0
  452. expectClose = 0
  453. })
  454. await writeFileAtomic('good', 'test', {
  455. tmpCreated (gottmpfile) {
  456. tmpfile = gottmpfile
  457. }
  458. })
  459. t.match(tmpfile, /^good\.tmp-\w+$/, 'tmpCreated is called for success')
  460. await writeFileAtomic('good', 'test', {
  461. tmpCreated (gottmpfile) {
  462. return Promise.resolve()
  463. }
  464. })
  465. tmpfile = undefined
  466. await t.rejects(writeFileAtomic('noopen', 'test', {
  467. tmpCreated (gottmpfile) {
  468. tmpfile = gottmpfile
  469. }
  470. }))
  471. t.is(tmpfile, undefined, 'tmpCreated is not called on open failure')
  472. await t.rejects(writeFileAtomic('nowrite', 'test', {
  473. tmpCreated (gottmpfile) {
  474. tmpfile = gottmpfile
  475. }
  476. }))
  477. t.match(tmpfile, /^nowrite\.tmp-\w+$/, 'tmpCreated is called if failure is after open')
  478. })