node icon indicating copy to clipboard operation
node copied to clipboard

Test runner: recursive or globbed loading of non-JS test files

Open meyfa opened this issue 1 year ago • 51 comments

What is the problem this feature will solve?

As per https://nodejs.org/dist/latest-v18.x/docs/api/test.html#test-runner-execution-model, the test runner can be started as node --test dirname/ to recursively run all test files (ending in .js, .cjs or .mjs) contained in the directory dirname. This unfortunately means that non-JS test files, specifically test files written in TypeScript in my case, will not be found since they end in .ts instead of .js. The docs say to provide each test file separately, e.g. node --loader=ts-node/esm --test dirname/foo.test.ts dirname/bar.test.ts but this gets messy very quickly.

What is the feature you are proposing to solve the problem?

I see a few options (in order of my preference, starting with most preferred):

  • Support globbing (e.g., node --loader ts-node/esm --test "dirname/**/*.ts"). Note that this does work on Linux, to some extent, since the shell will expand asterisks. This doesn't work on Windows, though, and doesn't work when the string is quoted. Tools like mocha will perform globbing independently of the operating system or shell.
  • Provide a CLI arg for additional test file extensions (e.g., node --loader ts-node/esm --test --test-extensions=.ts,.cts,.mts dirname/).
  • Have some sort of configuration file for the test runner where this can be configured.
  • Let loaders tell Node about additional file extensions (e.g., node --loader ts-node/esm --test dirname/ and ts-node could hook into the directory traversal).

What alternatives have you considered?

  • Listing test files manually (hard to maintain)
  • Having a script do the globbing and construct the command line with all files explicitly listed
  • Running node --test pointed to an index file that performs the globbing and dynamically imports all actual test files (very ugly; loses process separation between test files)

I also opened an issue at ts-node (https://github.com/TypeStrong/ts-node/issues/1853) specifically for TypeScript, to see if they are interested in better test runner support.

meyfa avatar Jul 28 '22 12:07 meyfa

@nodejs/test_runner

benjamingr avatar Jul 28 '22 13:07 benjamingr

Honestly, I would just add .ts support explicitly (and probably .tsx/.jsx) given how common it is compared to the alternative with a helpful message in case people didn't pass a loader/require hook.

benjamingr avatar Jul 28 '22 13:07 benjamingr

probably for starters, as @benjamingr support /.tsx?$/ as well, eventually allow configuration of custom file filtering

MoLow avatar Jul 28 '22 16:07 MoLow

I don’t think supporting ts (or jsx) by default makes any sense if TS/JSX content isn’t auto loaded.

ljharb avatar Jul 29 '22 00:07 ljharb

Perhaps we should implement a solution for https://github.com/nodejs/node/issues/43895 (that will allow passing a custom function to filter files)

I am willing to implement some --test-config flag if there is consensus on it. CC @nodejs/test_runner

MoLow avatar Jul 29 '22 04:07 MoLow

Maybe a better solution would be to have an option for the loader to have an option to tell which file patten it supports for tests? /cc @nodejs/loaders

aduh95 avatar Jul 29 '22 08:07 aduh95

Maybe a better solution would be to have an option for the loader to have an option to tell which file patten it supports for tests?

why will it be better? I think a configuration file will cover more use-cases (real usecases from here), for example:

  • within a file, filter which tests should run
  • support glob (without relation to ts support)

loaders should process test files regardless of this discussion, there is a seperate issue about that

MoLow avatar Jul 29 '22 08:07 MoLow

Running the test runner on a .ts file only makes sense if there's a loader able to understand it, so it's convenient if the loader itself can provide this info. If the info comes from somewhere else, it's more likely to de-sync at some point. Anyway that was just a thought.

aduh95 avatar Jul 29 '22 08:07 aduh95

I completely agree that that’s the only way to handle it. Nothing should be implicitly included unless node understands it;cams adding a loader should also implicitly include anything the loader handles. Users should only have to configure file extensions if they’re deviating from their loaders’ defaults.

ljharb avatar Jul 29 '22 09:07 ljharb

I might not fully understand how loaders work, so correct me in case I am wrong - a loader does not expose what file extensions it supports, So there is no way for --test to know what to extensions to filter If we are talking about a new flag/option declaring what extensions a loader supports in the context of running tests - I don't really see a reason that should be part of loaders and not just part of the test runner config?

MoLow avatar Jul 29 '22 10:07 MoLow

Personally, I see a huge advantage in letting users specify the file pattern precisely: I often find myself putting test fixtures, shared code etc. in the test/ directory (e.g. test/fixtures.ts), along with test files (such as test/foo.test.ts, test/bar.test.ts). In these cases, I would want to use a glob like test/**/*.test.ts instead of basing it off of the .ts extension.

meyfa avatar Jul 29 '22 10:07 meyfa

I see mts and cts file extensions were not mentioned above. This is evidence in favor of allowing users and customization hooks to specify.

cspotcode avatar Jul 29 '22 20:07 cspotcode

I think probably just allowing an RE or glob of test files then as an argument for --test ?

benjamingr avatar Jul 30 '22 14:07 benjamingr

I think probably just allowing an RE or glob of test files then as an argument for --test ?

core doesn't currently support glob and regex can be hard to distinguish from paths

MoLow avatar Jul 30 '22 19:07 MoLow

I think probably just allowing an RE or glob of test files then as an argument for --test ?

core doesn't currently support glob and regex can be hard to distinguish from paths

Currently --test doesn't take any value right? So we could introduce a semver-minor change saying that now --test can take an optional string value that will be interpreted as a regex I think.

aduh95 avatar Jul 30 '22 20:07 aduh95

Currently --test doesn't take any value right? So we could introduce a semver-minor change saying that now --test can take an optional string value that will be interpreted as a regex I think.

it currently accepts paths

MoLow avatar Jul 30 '22 20:07 MoLow

Currently --test doesn't take any value right? So we could introduce a semver-minor change saying that now --test can take an optional string value that will be interpreted as a regex I think.

it currently accepts paths

That's incorrect, --test is a boolean flag: https://github.com/nodejs/node/blob/d29e78a7803759c0b4e80723a989f66db6289a0c/src/node_options.h#L153 https://github.com/nodejs/node/blob/d29e78a7803759c0b4e80723a989f66db6289a0c/src/node_options.cc#L529-L531

We could potentially change it to also accept a string. (i.e. node --test='/.\.test\.ts$/' path/to/test/dir)

aduh95 avatar Jul 30 '22 22:07 aduh95

@aduh95 see https://github.com/nodejs/node/blob/2fe4e9473fb50630326754886dee1d301a1a661e/test/parallel/test-runner-cli.js#L11 https://github.com/nodejs/node/blob/2fe4e9473fb50630326754886dee1d301a1a661e/test/parallel/test-runner-cli.js#L23 https://github.com/nodejs/node/blob/2fe4e9473fb50630326754886dee1d301a1a661e/test/parallel/test-runner-cli.js#L55 https://github.com/nodejs/node/blob/2fe4e9473fb50630326754886dee1d301a1a661e/test/parallel/test-runner-exit-code.js#L47 etc

MoLow avatar Jul 30 '22 22:07 MoLow

I think a glob would an ideal solution:

  • It is very common in test runners (I can't think of any test runner that does not support it), so users will already be familiar
  • It addresses several other issues, such as using spec instead of test (eg foo.spec.js)

If node does not understand the file it was explicitly pointed to, user-error. I can't imagine a user suddenly complaining that node didn't support typescript before and still doesn't when fed a glob on typescript. I think this will be even less of an issue when we add the more detailed error message to ESMLoader::resolve() when an unknown file is encountered.

JakobJingleheimer avatar Jul 31 '22 08:07 JakobJingleheimer

assuming that adding glob to core has a bigger scope than a small fix to the test runner, what will be the process of adding this to core? which teams need to be CC'd and consulted before working on it? and should some collaborator/tsc member contact user-land glob pacakge mainaners as a lesson learned from https://github.com/nodejs/node/issues/43382?

MoLow avatar Jul 31 '22 09:07 MoLow

Perhaps node:fs?

import { glob } from 'node:fs/promises';

const [
  file1,
  file2,
  ...files
] = await glob('./src/**/*.spec.mjs');

As for the team, I imagine it depends on where it lives.

Yes, probably a good idea to reach out for lessons learnt 🙂

JakobJingleheimer avatar Jul 31 '22 10:07 JakobJingleheimer

I see that this was already raised here https://github.com/nodejs/node/issues/40731#issuecomment-1054519184 and https://github.com/nodejs/node/issues/40954#issuecomment-1037973165 with no real objections
@isaacs can you update where this stands? there is now a real use case for using minimach internally, I am willing to help with porting/implementing if help is needed and ok with you

MoLow avatar Jul 31 '22 10:07 MoLow

Something else to consider: will node support a single flavor of globbing, or will it support multiple behaviors via options, similar to bash's various shopts: dotglob, extglob, etc? I'm guessing node --test <glob> is interpreted in one and only one way, but the {glob} from 'fs' API will support options?

cspotcode avatar Aug 01 '22 16:08 cspotcode

I’m growing concerned by the number of dependencies that are becoming part of core as part of shipping our own test runner. Like are we going to vendor https://github.com/isaacs/node-glob or https://github.com/isaacs/minimatch now too, or create our own slimmed-down versions or alternatives? This feels a lot like the path we went down with chalk in https://github.com/nodejs/node/issues/43382.

GeoffreyBooth avatar Aug 01 '22 19:08 GeoffreyBooth

I’m growing concerned by the number of dependencies that are becoming part of core as part of shipping our own test runner.

What dependencies has core already taken on because of the test runner?

Like are we going to vendor https://github.com/isaacs/node-glob or https://github.com/isaacs/minimatch now too, or create our own slimmed-down versions or alternatives?

It looks like that may already be in the works independent of the test runner. If they do make it into core, there is no reason for the test runner not to use them.

cjihrig avatar Aug 01 '22 19:08 cjihrig

I’m growing concerned by the number of dependencies that are becoming part of core as part of shipping our own test runner.

I realize this might be a concern, and that is why I initially suggested introducing a configuration file for the test runner that will allow just importing glob or any other library to filter tests. I still think will come in handy regardless of this specific issue

Like are we going to vendor isaacs/node-glob or isaacs/minimatch now too, or create our own slimmed-down versions or alternatives? This feels a lot like the path we went down with chalk in https://github.com/nodejs/node/issues/43382.

IIUC, adding glob has been requested and raised a couple of times before the test runner existed, and the situation is very different than chalk since the author of glob & minimatch is the one that has proposed adding it to core

MoLow avatar Aug 01 '22 19:08 MoLow

introducing a configuration file for the test runner that will allow just importing glob or any other library to filter tests.

This is already possible without work from core, right?

require('node:child_process').spawn(
  process.execPath,
  ['--test', ...require('glob').sync('./src/**/*.spec.mjs')],
  { stdio: 'inherit' }
).on('exit', process.exit);

It seems also quite easy to achieve that from an npm package, I'm not sure it's worth adding more to core when it's easy enough to implement in user-land.

aduh95 avatar Aug 01 '22 19:08 aduh95

This is already possible without work from core, right?

Possible yes. not great DX if you ask me :/

MoLow avatar Aug 01 '22 19:08 MoLow

It looks like that may already be in the works independent of the test runner. If they do make it into core, there is no reason for the test runner not to use them.

Sorry, I wasn’t aware of this. This is great news; @isaacs maintaining their libraries as part of core seems like the ideal outcome.

GeoffreyBooth avatar Aug 01 '22 20:08 GeoffreyBooth

These particular ones should definitely be in core regardless; but test runners are a quagmire of complexity, and choosing to ship one guarantees node will need a lot more new deps in the future - that’s just the tradeoff that was chosen.

ljharb avatar Aug 02 '22 10:08 ljharb

Or we could keep the test runner minimalistic, and let user land build on top of it to add more complex missing features.

aduh95 avatar Aug 02 '22 10:08 aduh95

That's a very interesting idea, but I suspect it would need a fundamental redesign/rearchitecting to be able to shoot for that, and a number of features that have landed and that are open PRs would need to be removed/rejected.

ljharb avatar Aug 03 '22 18:08 ljharb

Or we could keep the test runner minimalistic, and let user land build on top of it to add more complex missing features.

I think this is theoretically a great idea. The "how" might be medicine worse than the disease though. E.G. a hook specifically to provide the glob results seems overkill: everyone is gonna want basically the same thing, so why have everyone do the same thing themselves.

I think it couldn't be simple json config (so couldn't go in package.json with the others planned to go in there); it would need to be a script file that returns config (the file path to which could go in package.json). Many test runners support similar script-to-config files, but then that begs the question: how is ours better. As a user, Mocha already has everything node:test has as well as globbing yet isn't bloated like Jest, so if I have to do that crazy process.spawn etc, I'd just use Mocha.

JakobJingleheimer avatar Aug 04 '22 07:08 JakobJingleheimer

if I have to do that crazy process.spawn etc, I'd just use Mocha.

Or you could use an npm package that reads a Mocha config and run it with Node.js built-in test runner. I agree that doing it in every projects would not make sense, but I think leaving it to the npm ecosystem is best for now, if there's a solution that indeeds seems to satisfy a great majority it can then be vendored in. (just my opinion, to be clear I'm not blocking the initiative)

aduh95 avatar Aug 04 '22 07:08 aduh95

Or you could use an npm package

are you saying the test runner --test flag is not useful and only the node:test module is?

MoLow avatar Aug 04 '22 11:08 MoLow

My example (https://github.com/nodejs/node/issues/44023#issuecomment-1201628483) is using the --test flag, demonstrating its usefulness.

aduh95 avatar Aug 04 '22 11:08 aduh95

@MoLow what Antoine is saying is "we don't necessarily need config since we can parse whatever with a "regular" entry file and then pass it to child_process which would let people glob for example.

I (personally) think that glob is common enough that users expect it to be part of core like regular expressions or dates like it is in Python/C#/other runtimes.

I also think that test config is common enough to be a file (or "known globals") though I wonder if a solution can't be either:

Running node:test in "test mode" programmatically, essentially Antoine's suggestion with a (possibly) nicer API:

require('node:test').run({
  concurrency: true,
  timeout: 10000, // whatever
  files: "**/*.test.ts"
});

Or alternative expose configuration through --require/--import

> node --import test-config.mjs --test 
// test-config.mjs
import { config, beforeEach } from 'node:test';
config({
  concurrency: true // or whatever
});
// add global setup/teardown code
beforeEach(() => assertNoLeaksOfDbHandles());
// configure stuff like test-double

benjamingr avatar Aug 04 '22 16:08 benjamingr

It might be a good idea to set up a meeting where we each share testing experience and we write down the use cases we care about (like the one mentioned here of using it with a TypeScript project) and make sure our visions align.

benjamingr avatar Aug 04 '22 16:08 benjamingr

@benjamingr that would be great - tbh that's what i'd have expected to happen before the runner landed in the first place :-)

ljharb avatar Aug 04 '22 16:08 ljharb

Great, setting up a Doodle with some close dates (for early next week, some on the weekend, some in US friendly times and some in Europe friendly times).

it is intentionally close times since I believe these things work better when the meeting is close.

@nodejs/test_runner @cjihrig @ljharb @MoLow @aduh95

https://doodle.com/meeting/participate/id/dwjGRJmb

benjamingr avatar Aug 04 '22 16:08 benjamingr

Also, @aduh95 can you please add me (and @cjihrig probably since he wrote the thing?) to the @nodejs/test_runner team?

(I think I can add myself since I'm an org-admin but I don't want to do that since I can only do that because I'm a moderator so I'd rather someone who is an org-owner because of the TSC do it)

benjamingr avatar Aug 04 '22 16:08 benjamingr

Also maybe @isaacs since glob (minimatch) core inclusion will/may be discussed ?

benjamingr avatar Aug 04 '22 16:08 benjamingr

We should also consider if an equivalent to this pseudo-code makes sense: require('node:test').runDashDashTest(['pathA', 'pathB'])

Equivalent to the behavior of the --test flag. Understanding that yeah, --test spawns child processes already. , but it reduces the total number by one.

cspotcode avatar Aug 04 '22 17:08 cspotcode

@cspotcode yes that's a suggestion in https://github.com/nodejs/node/issues/44023#issuecomment-1205476021 :)

Also making sure you don't miss the meeting doodle at https://github.com/nodejs/node/issues/44023#issuecomment-1205500132

benjamingr avatar Aug 04 '22 17:08 benjamingr

Also, @aduh95 can you please add me (and @cjihrig probably since he wrote the thing?) to the @nodejs/test_runner team?

That's done!

I (personally) think that glob is common enough that users expect it to be part of core like regular expressions or dates like it is in Python/C#/other runtimes.

There have been discussions to include it in fs (see https://github.com/nodejs/node/issues/40731#issuecomment-1054519184 and https://github.com/nodejs/node/issues/40731#issuecomment-1054519184), so IMO a better API would be:

require('node:test').run({
  concurrency: true,
  timeout: 10_000, // whatever
  files: require('node:fs').globSync("**/*.test.ts"),
});

IMO there should be a way to a file which name would be*.test.ts in a directory named ** without the whole string being interpreted as a glob – of course that's a silly example, but I really think there's virtue in keeping such transforms explicit, same as we require file: URLs to be wrapped in new URL().

Other than that, I think that's a very neat idea, I like both options.

aduh95 avatar Aug 04 '22 23:08 aduh95

I am +1 on import { runTests } from 'node:test'

However, import { config } from 'node:test' doesn't sound correct: What happens if two different configurations are set in two different files? What happens if it is configured once using --import and then overridden inside some of the test files?

MoLow avatar Aug 05 '22 06:08 MoLow

What happens if two different configurations are set in two different files? What happens if it is configured once using --import and then overridden inside some of the test files?

I assume config would only apply when --test is passed, so it would only have an effect if it's pre-imported (using --require or --import).

aduh95 avatar Aug 05 '22 08:08 aduh95

I assume config would only apply when --test is passed, so it would only have an effect if it's pre-imported (using --require or --import).

that is pretty much what I suggested, but instead of using --test-config it will use --import?

MoLow avatar Aug 05 '22 08:08 MoLow

One more ping to @nodejs/test_runner @cjihrig @aduh95 for the meeting doodle to make sure you saw it https://doodle.com/meeting/participate/id/dwjGRJmb

benjamingr avatar Aug 05 '22 09:08 benjamingr

However, import { config } from 'node:test' doesn't sound correct: What happens if two different configurations are set in two different files?

I feel like we should solve the overall question of how to configure Node from a config file, before solving specific use cases like node:test and loaders. See also #43973.

GeoffreyBooth avatar Aug 05 '22 20:08 GeoffreyBooth

@benjamingr is there a verdict for the meeting time?

MoLow avatar Aug 09 '22 17:08 MoLow

Hey, due to a combination of bereavement leave and food poisoning / tummy flu I've totally dropped the ball on this and won't be able to engage in the next 3-4 days (at least until the Shiv'ah is after us).

I'll send another doodle next week (hopefully) or if someone else wants to pick it up - go tor it.

benjamingr avatar Aug 10 '22 09:08 benjamingr