tschaub / mock-fs

Configurable mock for the fs module
https://npmjs.org/package/mock-fs
Other
906 stars 86 forks source link

Unexpected ENOENT when using createReadStream #338

Open lostfictions opened 2 years ago

lostfictions commented 2 years ago

Hi there, I'm trying mock-fs out. I might be misunderstanding, but it sounds like it creates an in-memory filesystem that can be written to as well as read from, right? Unfortunately, I seem to be getting an error when I write a file to the os tmpdir and then try to read it back via createReadStream. This works fine on the real filesystem.

Here's a minimal repro, using mock-fs 5.1.0 and jest 27.2.1.

const mockFs = require("mock-fs");
const { createReadStream } = require("fs");
const { writeFile, unlink } = require("fs/promises");
const { join } = require("path");
const { tmpdir } = require("os");

beforeEach(() => {
  mockFs();
});

afterEach(() => {
  mockFs.restore();
});

describe("repro", () => {
  it("gives an ENOENT when it shouldn't", async () => {
    const path = join(tmpdir(), `tmpfile-${Math.random()}.png`);
    await writeFile(path, Buffer.from("whatever"));

    const rs = createReadStream(path);
    rs.close();

    await unlink(path);

    expect(true);
  });
});

Here's the output I get:

$ repro/node_modules/.bin/jest
 FAIL  ./repro.test.js
  repro
    ✕ gives an ENOENT when it shouldn't (8 ms)

  ● repro › gives an ENOENT when it shouldn't

    Unhandled error. (Error {
      message: "ENOENT, no such file or directory '/tmp/tmpfile-0.567831857611433.png'",
      code: 'ENOENT',
      errno: -2,
      path: '/tmp/tmpfile-0.567831857611433.png'
    })

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.281 s, estimated 1 s
Ran all test suites.

Commenting out const rs = createReadStream(path); rs.close(); causes the test to pass.

3cp commented 2 years ago

I can reproduce the error in Nodejs v16, but not v14. Don't need to use jest to reproduce the bug. @Rugvip it looks there is edge case for fs.createReadStream, it tries to access the real file system.

const mockFs = require("mock-fs");
const fs = require("fs");
const { writeFile, unlink } = require("fs/promises");
const { join } = require("path");
const { tmpdir } = require("os");

mockFs();
// mockFs.restore();

async function test() {
  const path = join(tmpdir(), `tmpfile-${Math.random()}.png`);
  await writeFile(path, Buffer.from("whatever"));

  const rs = fs.createReadStream(path);
  // rs.close();
  await unlink(path);
}

test().then(() => console.log("DONE"));
Error: ENOENT, no such file or directory '/var/folders/v_/y1wwsg8d7fvcr981nq9l89x00000gn/T/tmpfile-0.870533120229688.png'
3cp commented 2 years ago

Update: I found there is a very strange timing issue. My log shows the logic inside fs.createReadStream seems being delayed after the await unlink(path), that's why it cannot find the file (it's already unlinked).

This can be approved with 2 ways:

  1. remove await unlink(path);.
  2. or add a delay before unlink.
async function delay() {
  return new Promise(resolve => {
    setTimeout(resolve, 500);
  });
}

// ...
await delay();
await unlink(path);

Since nodejs v14 has no such issue, there might be some bug or edge case in nodejs v16.

3cp commented 2 years ago

I can reproduce the same issue WITHOUT using mock-fs in nodejs v16.13.0, so this is probably an unexpected bug in nodejs v16+ itself.

const fs = require("fs");
const { writeFile, unlink } = require("fs/promises");
async function test() {
  const path = `tmpfile-${Math.random()}.png`;
  await writeFile(path, Buffer.from("whatever"));
  const rs = fs.createReadStream(path);
  // it doesn't matter whether we call close()
  rs.close();
  await unlink(path);
}

test().then(() => console.log("DONE"));
> node test.js
DONE
node:events:368
      throw er; // Unhandled 'error' event
      ^

Error: ENOENT: no such file or directory, open 'tmpfile-0.16589223939625652.png'
Emitted 'error' event on ReadStream instance at:
    at emitErrorNT (node:internal/streams/destroy:157:8)
    at emitErrorCloseNT (node:internal/streams/destroy:122:3)
    at processTicksAndRejections (node:internal/process/task_queues:83:21) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'tmpfile-0.16589223939625652.png'
}

Note the error is printed after "DONE" (means test() is resolved successfully...)

KenEucker commented 2 years ago

This also appears to be an issue impacting readFileSync. I tried to create a stream from a file loaded by readFileSync but I get the same ENOENT error.