1
0

atomic.test.js 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import os from 'node:os'
  4. import path from 'node:path'
  5. import { promises as fs } from 'node:fs'
  6. import { writeAtomicBatch } from '../../src/storage/atomic.js'
  7. test('writeAtomicBatch:后续文件失败时,已替换文件恢复原样', async () => {
  8. const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-atomic-'))
  9. try {
  10. await fs.writeFile(path.join(root, 'a.txt'), 'old', 'utf8')
  11. await fs.mkdir(path.join(root, 'b.txt'))
  12. await assert.rejects(() =>
  13. writeAtomicBatch(root, [
  14. { path: 'a.txt', content: 'new' },
  15. { path: 'b.txt', content: 'bad' },
  16. ])
  17. )
  18. assert.equal(await fs.readFile(path.join(root, 'a.txt'), 'utf8'), 'old')
  19. const bStat = await fs.stat(path.join(root, 'b.txt'))
  20. assert.equal(bStat.isDirectory(), true, '既有目录不应被批量写删除')
  21. } finally {
  22. await fs.rm(root, { recursive: true, force: true })
  23. }
  24. })
  25. // R12 故障注入:atomic.js 与本测试共享 node:fs 的 promises 单例,patch 即生效。
  26. test('writeAtomicBatch:写 tmp 就失败的文件,其原文绝不能被回滚删除(R12)', async () => {
  27. const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-atomic-'))
  28. const origWriteFile = fs.writeFile
  29. try {
  30. await origWriteFile.call(fs, path.join(root, 'a.txt'), 'oldA', 'utf8')
  31. await origWriteFile.call(fs, path.join(root, 'b.txt'), 'oldB', 'utf8')
  32. fs.writeFile = async (p, ...rest) => {
  33. if (String(p).includes('b.txt') && String(p).includes('.wnwtmp')) {
  34. const err = new Error('注入:EPERM 模拟(文件被占用/路径超长)')
  35. err.code = 'EPERM'
  36. throw err
  37. }
  38. return origWriteFile.call(fs, p, ...rest)
  39. }
  40. await assert.rejects(() =>
  41. writeAtomicBatch(root, [
  42. { path: 'a.txt', content: 'newA' },
  43. { path: 'b.txt', content: 'newB' },
  44. ])
  45. )
  46. assert.equal(await fs.readFile(path.join(root, 'a.txt'), 'utf8'), 'oldA', 'A 应从备份恢复')
  47. assert.equal(await fs.readFile(path.join(root, 'b.txt'), 'utf8'), 'oldB', 'B 从未动过,原文必须还在')
  48. } finally {
  49. fs.writeFile = origWriteFile
  50. await fs.rm(root, { recursive: true, force: true })
  51. }
  52. })
  53. test('writeAtomicBatch:备份 rename 失败的文件,其原文绝不能被回滚删除(R12)', async () => {
  54. const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-atomic-'))
  55. const origRename = fs.rename
  56. try {
  57. await fs.writeFile(path.join(root, 'a.txt'), 'oldA', 'utf8')
  58. await fs.writeFile(path.join(root, 'b.txt'), 'oldB', 'utf8')
  59. fs.rename = async (from, to, ...rest) => {
  60. if (String(from).endsWith('b.txt') && String(to).includes('.wnwbackup')) {
  61. const err = new Error('注入:EPERM 模拟(原文件被编辑器占用)')
  62. err.code = 'EPERM'
  63. throw err
  64. }
  65. return origRename.call(fs, from, to, ...rest)
  66. }
  67. await assert.rejects(() =>
  68. writeAtomicBatch(root, [
  69. { path: 'a.txt', content: 'newA' },
  70. { path: 'b.txt', content: 'newB' },
  71. ])
  72. )
  73. assert.equal(await fs.readFile(path.join(root, 'a.txt'), 'utf8'), 'oldA', 'A 应从备份恢复')
  74. assert.equal(await fs.readFile(path.join(root, 'b.txt'), 'utf8'), 'oldB', 'B 原文必须还在')
  75. const leftovers = (await fs.readdir(root)).filter((f) => f.includes('.wnwtmp') || f.includes('.wnwbackup'))
  76. assert.deepEqual(leftovers, [], '不应残留 tmp/backup 文件')
  77. } finally {
  78. fs.rename = origRename
  79. await fs.rm(root, { recursive: true, force: true })
  80. }
  81. })