Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test, fs: introduce helper function to test fs.* methods as fsPromises.* methods #20439

Closed
wants to merge 10 commits into from

Conversation

Trott
Copy link
Member

@Trott Trott commented Apr 30, 2018

Introduce common.fsTest() helper function which (hopefully) makes it easy to repurpose all test-fs-* tests to also test equivalent functionality in fsPromises.* methods. As a proof-of-concept, use the helper function in test-fs-access and test-fs-link. If people think this is a reasonable approach, the remaining 131 test-fs-* files can probably be done relatively quickly.

@ChALkeR @jasnell @nodejs/testing @nodejs/fs

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • documentation is changed or added
  • commit message follows commit guidelines

@Trott Trott added fs Issues and PRs related to the fs subsystem / file system. test Issues and PRs related to the tests. labels Apr 30, 2018
@nodejs-github-bot nodejs-github-bot added the test Issues and PRs related to the tests. label Apr 30, 2018
@Trott
Copy link
Member Author

Trott commented Apr 30, 2018

@TimothyGu
Copy link
Member

+1 on the approach.

@ChALkeR
Copy link
Member

ChALkeR commented May 1, 2018

Great! 👍

Note that fs/promises has two alternative methods to be used - a method on the object and a "global" one accepting the object as the first argument.

So that should be three checks in total?

@Trott
Copy link
Member Author

Trott commented May 1, 2018

So that should be three checks in total?

Are you talking about, for example: fs.appendFile(), fsPromises.appendFile() and filehandle.appendFile()? A problem is that only some functions are on filehandle. For example, it lacks access() and link(). Maybe whether or not to try on filehandle can be an option specified in the options object supplied to fsTest().

@ChALkeR ChALkeR mentioned this pull request May 1, 2018
4 tasks
* return [<boolean>]

Checks if `pathname` exists

### fsTest(method, args, setup)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last argument should be options right?

### fsTest(method, args, setup)
* `method` [<string>]
* `args` [<Array>]
* `options` [<Function>]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object?

function callback(err) {
assert.ifError(err);
const dstContent = fs.readFileSync(dstPath, 'utf8');
assert.strictEqual('hello world', dstContent);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: swap args.

@@ -120,11 +120,28 @@ an expected warning does not have a code then `common.noWarnCode` can be used
to indicate this.

### fileExists(pathname)
* pathname [<string>]
* `pathname` [<string>]
* return [<boolean>]

Checks if `pathname` exists
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should here be a period? (and in some other places as well.)

Copy link
Member Author

@Trott Trott May 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If so, that would be a large diff that I would prefer to save for another PR. Personally, I'd prefer no period at the end of lines like this, but as always, consistency would trump my preference. :-D

into one that takes a callback. The last value in `args` must be a callback that
is expected to run once for each of the two invocations.

The options object may contain a `setup` property that is a function that is run
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options object -> `options` object?


The options object may contain a `setup` property that is a function that is run
before each test. This function might refresh the temporary directory if both
tests need to use it, for example. The options object may also contain a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options object -> `options` object?

@ChALkeR
Copy link
Member

ChALkeR commented May 1, 2018

@Trott Looks like this function tests only methods that don't expect fd as the first argument in the callback version. And probably correctly tests only non-modifying methods, e.g. testing unlink could be tricker.

Everything with the fd is different for fs/promises, as they don't operate with fd integer but rather with a FileHandle object. Probably those tests need to be promises-first and specify the FileHandle object as input — then it would make sense to test all three variants (fd could be extracted as .fd property of the FileHandle object). The test function wouldn't need an extra flag then — just when the first argument is a FileHandle — test all three, in other cases — test two.

I guess it could be reversed the other way around — if the first argument is integer — construct FileHandle from it and test all three.

About modifying (e.g. unlink) — it would make sense to add a type parameter to test only one variant instead of all possible (two or three).
The code for testing unlink could then look like this:

for (const type of ['callback', 'promise', 'class']) {
  …bootstrap…
  fsTest[type]('unlink', ...args);
  …cleanup…
}

Perhaps even the whole fs tests file could be just wrapped in that — then we mostly wouldn't need to use a function that tests all three variants at the same time, mostly wouldn't care if the first argument (previously returned by the previous fsTest[type].open) is an int or a FileHandle and wouldn't have to worry about sharing the bootsrap logic.

Thoughts?

@Trott Trott force-pushed the fs-callbackify branch 2 times, most recently from 4921ce2 to d4cd54e Compare May 3, 2018 02:30
@Trott
Copy link
Member Author

Trott commented May 3, 2018

@ChALkeR Modifying functions can be made to test properly using the options.setup function. So far, I haven't found a test that can't be made to work that way. We'll see as I keep working through stuff.

The fd vs. FileHandle thing is definitely a problem. I like your solution idea. Will try to make that work. Thanks!

@Trott Trott force-pushed the fs-callbackify branch 2 times, most recently from f5faa2f to 9bcc7f4 Compare May 4, 2018 05:14
@Trott
Copy link
Member Author

Trott commented May 4, 2018

@ChALkeR Here's my first pass at solving the problems you flagged. https://github.com/Trott/io.js/blob/9bcc7f474f006266919eabec89c70fa6791ce149/test/parallel/test-fs-append-file.js#L114-L150

I introduced an option called differentFiles that is an array of filenames/paths that causes common.fsTest to use one file for the callback API and another file for the promise API. The easiest thing to do (for now, at least) is to put the switching-based-on-typeof fd stuff directly into the callback supplied by the test, so that's what I did in those lines. I think that should be OK, although I'm certainly not opposed to someone coming up with a more elegant solution now or at some point in the future.

@Trott
Copy link
Member Author

Trott commented May 4, 2018

Making a checklist of tests that need to be looked at and (if necessary) updated to test Promise-based APIs too.

  • test/parallel/test-fs-access.js
  • test/parallel/test-fs-append-file-sync.js (only uses synchronous functions, no need to update)
  • test/parallel/test-fs-append-file.js
  • test/parallel/test-fs-assert-encoding-error.js
  • test/parallel/test-fs-buffer.js
  • test/parallel/test-fs-buffertype-writesync.js (only uses synchronous functions, no need to update)
  • test/parallel/test-fs-chdir-errormessage.js (misnamed file, does not test fs, will rename in another PR)
  • test/parallel/test-fs-chmod.js
  • test/parallel/test-fs-chown-type-check.js
  • test/parallel/test-fs-close-errors.js
  • test/parallel/test-fs-copyfile.js
  • test/parallel/test-fs-empty-readStream.js
  • test/parallel/test-fs-error-messages.js
  • test/parallel/test-fs-exists.js
  • test/parallel/test-fs-existssync-false.js
  • test/parallel/test-fs-fchmod.js
  • test/parallel/test-fs-fchown.js
  • test/parallel/test-fs-filehandle.js
  • test/parallel/test-fs-fsync.js
  • test/parallel/test-fs-link.js
  • test/parallel/test-fs-long-path.js
  • test/parallel/test-fs-make-callback.js
  • test/parallel/test-fs-makeStatsCallback.js
  • test/parallel/test-fs-mkdir-rmdir.js
  • test/parallel/test-fs-mkdir.js
  • test/parallel/test-fs-mkdtemp-prefix-check.js
  • test/parallel/test-fs-mkdtemp.js
  • test/parallel/test-fs-non-number-arguments-throw.js
  • test/parallel/test-fs-null-bytes.js
  • test/parallel/test-fs-open-flags.js
  • test/parallel/test-fs-open-numeric-flags.js
  • test/parallel/test-fs-open.js
  • test/parallel/test-fs-options-immutable.js
  • test/parallel/test-fs-promises-file-handle-append-file.js
  • test/parallel/test-fs-promises-file-handle-chmod.js
  • test/parallel/test-fs-promises-file-handle-read.js
  • test/parallel/test-fs-promises-file-handle-readFile.js
  • test/parallel/test-fs-promises-file-handle-write.js
  • test/parallel/test-fs-promises-file-handle-writeFile.js
  • test/parallel/test-fs-promises-readfile.js
  • test/parallel/test-fs-promises-writefile.js
  • test/parallel/test-fs-promises.js
  • test/parallel/test-fs-promisified.js
  • test/parallel/test-fs-read-file-assert-encoding.js
  • test/parallel/test-fs-read-file-sync-hostname.js
  • test/parallel/test-fs-read-file-sync.js
  • test/parallel/test-fs-read-stream-double-close.js
  • test/parallel/test-fs-read-stream-encoding.js
  • test/parallel/test-fs-read-stream-err.js
  • test/parallel/test-fs-read-stream-fd-leak.js
  • test/parallel/test-fs-read-stream-fd.js
  • test/parallel/test-fs-read-stream-inherit.js
  • test/parallel/test-fs-read-stream-resume.js
  • test/parallel/test-fs-read-stream-throw-type-error.js
  • test/parallel/test-fs-read-stream.js
  • test/parallel/test-fs-read-type.js
  • test/parallel/test-fs-read-zero-length.js
  • test/parallel/test-fs-read.js
  • test/parallel/test-fs-readdir-stack-overflow.js
  • test/parallel/test-fs-readdir-ucs2.js
  • test/parallel/test-fs-readdir.js
  • test/parallel/test-fs-readfile-empty.js
  • test/parallel/test-fs-readfile-error.js
  • test/parallel/test-fs-readfile-fd.js
  • test/parallel/test-fs-readfile-pipe-large.js
  • test/parallel/test-fs-readfile-pipe.js
  • test/parallel/test-fs-readfile-unlink.js
  • test/parallel/test-fs-readfile-zero-byte-liar.js
  • test/parallel/test-fs-readfile.js
  • test/parallel/test-fs-readfilesync-enoent.js
  • test/parallel/test-fs-readfilesync-pipe-large.js
  • test/parallel/test-fs-readlink-type-check.js
  • test/parallel/test-fs-ready-event-stream.js
  • test/parallel/test-fs-realpath-buffer-encoding.js
  • test/parallel/test-fs-realpath-native.js
  • test/parallel/test-fs-realpath-on-substed-drive.js
  • test/parallel/test-fs-realpath-pipe.js
  • test/parallel/test-fs-realpath.js
  • test/parallel/test-fs-rename-type-check.js
  • test/parallel/test-fs-rmdir-type-check.js
  • test/parallel/test-fs-sir-writes-alot.js
  • test/parallel/test-fs-stat.js
  • test/parallel/test-fs-stream-destroy-emit-error.js
  • test/parallel/test-fs-stream-double-close.js
  • test/parallel/test-fs-symlink-dir-junction-relative.js
  • test/parallel/test-fs-symlink-dir-junction.js
  • test/parallel/test-fs-symlink.js
  • test/parallel/test-fs-sync-fd-leak.js
  • test/parallel/test-fs-syncwritestream.js
  • test/parallel/test-fs-timestamp-parsing-error.js
  • test/parallel/test-fs-truncate-clear-file-zero.js
  • test/parallel/test-fs-truncate-fd.js
  • test/parallel/test-fs-truncate-sync.js
  • test/parallel/test-fs-truncate.js
  • test/parallel/test-fs-unlink-type-check.js
  • test/parallel/test-fs-utimes.js
  • test/parallel/test-fs-watch-encoding.js
  • test/parallel/test-fs-watch-enoent.js
  • test/parallel/test-fs-watch-recursive.js
  • test/parallel/test-fs-watch-stop-async.js
  • test/parallel/test-fs-watch-stop-sync.js
  • test/parallel/test-fs-watch.js
  • test/parallel/test-fs-watchfile.js
  • test/parallel/test-fs-whatwg-url.js
  • test/parallel/test-fs-write-buffer.js
  • test/parallel/test-fs-write-file-buffer.js
  • test/parallel/test-fs-write-file-invalid-path.js
  • test/parallel/test-fs-write-file-sync.js
  • test/parallel/test-fs-write-file-uint8array.js
  • test/parallel/test-fs-write-file.js
  • test/parallel/test-fs-write-no-fd.js
  • test/parallel/test-fs-write-stream-autoclose-option.js
  • test/parallel/test-fs-write-stream-change-open.js
  • test/parallel/test-fs-write-stream-close-without-callback.js
  • test/parallel/test-fs-write-stream-double-close.js
  • test/parallel/test-fs-write-stream-encoding.js
  • test/parallel/test-fs-write-stream-end.js
  • test/parallel/test-fs-write-stream-err.js
  • test/parallel/test-fs-write-stream-throw-type-error.js
  • test/parallel/test-fs-write-stream.js
  • test/parallel/test-fs-write-string-coerce.js
  • test/parallel/test-fs-write-sync.js
  • test/parallel/test-fs-write.js
  • test/pummel/test-fs-largefile.js (only uses sync functions and close())
  • test/pummel/test-fs-watch-file-slow.js (there is no promises-based watch API)
  • test/pummel/test-fs-watch-file.js (there is no promises-based watch API)
  • test/pummel/test-fs-watch-non-recursive.js (there is no promises-based watch API)
  • test/sequential/test-fs-readfile-tostring-fail.js
  • test/sequential/test-fs-stat-sync-overflow.js (only uses sync function)
  • test/sequential/test-fs-watch-file-enoent-after-deletion.js (there is no promises-based watch API)
  • test/sequential/test-fs-watch.js (there is no promises-based watch API)

@ChALkeR
Copy link
Member

ChALkeR commented May 5, 2018

@Trott I'm thinking — how hard would it be to mock callback-based fs behaviour instead in two variants and wrap all fs tests into that?

That should minimize the number of changes inside the tests themselves.

@BridgeAR
Copy link
Member

BridgeAR commented May 5, 2018

I think it is great that we have this PR as we likely end up more often in this situation if we plan on adding native promise support to other APIs as well.

I am thinking about a maybe more controversial approach that would not need any changes to the existing test cases and tests future test cases as well: if we monkey patch all callback fs functions when calling require('../common') that do not have any side effects on the touched files, we can call the function callback based and promise based (maybe even the sync version?) and compare the output (we clearly have to wrap it into a try / catch). If it is the same output: perfect, we just return that output and the test should work as before. If not, fail.

That approach should also be possible for functions where we have slightly different inputs in case we are able to unify inputs before passing them through. We could also provide helpers to e.g. add a file after deleting it so we can check for that as well.

I am not sure if I miss something but I doubt that this would have negative effects, since we control everything involved. Other opinions?

@Trott
Copy link
Member Author

Trott commented May 5, 2018

how hard would it be to mock callback-based fs behaviour instead in two variants and wrap all fs tests into that?

@ChALkeR I'm not sure I understand the suggestion. Can you explain it a little more?

@Trott
Copy link
Member Author

Trott commented May 5, 2018

@BridgeAR Not necessarily a deal-breaker for it, but one thing about the approach you suggest is that it probably breaks down for tests with nested callbacks. For example, if fs.close() is called in an innermost callback, the promisified version will need to know to call filehandle.close() and not fs.close() or (non-existent) fsPromises.close(). There's likely a bunch of things like this that will only come out during implementation.

In the approach in this PR, that same issue comes up. The solution I've used so far will work (and is hopefully sufficient for initial landing) but I'm open to the idea that it can be improved for sure.

@ChALkeR
Copy link
Member

ChALkeR commented May 5, 2018

@Trott I tried to provide a PoC, but stumbled upon #20548. Will do that a bit later ;-).

@ChALkeR
Copy link
Member

ChALkeR commented May 5, 2018

@Trott quick and dirty PoC:

const fs = require('fs');
const fsPromises = require('fs/promises')
const assert = require('assert');
const util = require('util');

function createFsMock(classes = false) {
  const methods = Object.keys(fs);
  const fsMock = {};
  for (const key of methods) {
    if (
      key.endsWith('Sync') || key.endsWith('Stream') || /^[A-Z_]/.test(key) ||
      key.includes('watch') || key === 'exists' ||
      typeof fs[key] !== 'function'
    ) {
      assert.strictEqual(fsPromises[key], undefined);
      fsMock[key] = fs[key];
      continue;
    }
    const head = fs[key].toString().replace(/\n[\s\S]*/, '').replace(/\s/g, '');
    const fd = head.includes('(fd,');
    if (classes && fd || key === 'close' && !fsPromises.close) {
      fsMock[key] = async (h, ...args) => h[key.replace(/^f/, '')](...args);
    } else {
      fsMock[key] = fsPromises[key];
    }
    assert.ok(fsMock[key], key);
    fsMock[key] = util.callbackify(fsMock[key]);
  }
  return fsMock;
}

const fsImpls = [fs, createFsMock(false), createFsMock(true)];

module.exports = fsImpls;

Example usage:

for (const fs of fsImpls) {
  const name = `tmp.fstest.${Math.random().toString(36).slice(2)}`;
  fs.open(name, 'w', (err, res) => {
    assert.ok(!err && res);
    const fd = res;
    fs.write(fd, 'boo', (err, res) => {
      assert.ok(!err && res);
      fs.write(fd, 'foo', (err, res) => {
        assert.ok(!err && res);
        fs.close(fd, err => {
          assert.ok(!err);
          fs.readFile(name, 'utf-8', (err, res) => {
            assert.ok(!err);
            assert.strictEqual(res, 'boofoo');
            fs.unlink(name, err => {
              assert.ok(!err);
              fs.access(name, (err, res) => {
                assert.ok(err && !res);
              });
            });
          });
        });
      });
    });
  });
}

Trott added 2 commits May 7, 2018 20:54
Add helper function to the test `common` module that simplifies
converting tests for fs.* methods to be for both fs.* and
fsPromises.*.
Make test-fs-link test both fs.link() and fsPromises.link().
Trott added 8 commits May 7, 2018 20:54
Make test-fs-access test both fs.access() and fsPromises.access().
Make test-fs-unlink-type-check test both fs.access() and
fsPromises.access().
Make test-fs-append-file.js test both fs.appendFile() and
fsPromises.appendFile().
Where relevant, apply tests in test-fs-assert-encoding-error.js to
equivalent functions in fsPromises.
Make test-fs-readfile-tostring-fail test both fs.readfile() and
fsPromises.readfile().
Make test-fs-append-file test both fs.* methods and fsPromises.*
methods.
Make test-fs-chmod test both fs and fsPromises methods.
Make test-fs-chown-type-check test both fs.chown() and
fsPromises.chown().
@ChALkeR ChALkeR added experimental Issues and PRs related to experimental features. promises Issues and PRs related to ECMAScript promises. labels May 8, 2018
@liron-navon
Copy link

Ill be doing this one: test/parallel/test-fs-write-stream-end.js

@Trott
Copy link
Member Author

Trott commented May 15, 2018

@liron-navon I'm not sure modeling on this is the way to go. There seems to be some question as to whether it is the right approach and I've started picking things off independently more like in #20739 and #20667.

@jasnell
Copy link
Member

jasnell commented May 23, 2018

Definitely needs a rebase.

@Trott Trott added the wip Issues and PRs that are still a work in progress. label Jun 2, 2018
@Trott Trott closed this Jun 10, 2018
@Trott Trott deleted the fs-callbackify branch January 13, 2022 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
experimental Issues and PRs related to experimental features. fs Issues and PRs related to the fs subsystem / file system. promises Issues and PRs related to ECMAScript promises. test Issues and PRs related to the tests. wip Issues and PRs that are still a work in progress.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants