unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
testing-strategyrefactoring

test_runner: match dotfiles in default coverage exclude

Project: nodejs/node | Language: JavaScript | Concepts: testing-strategy, refactoring | Difficulty: mid
Link: https://github.com/nodejs/node/pull/63401

The problem

When running node --experimental-test-coverage, test files with names starting with a dot (dotfiles, e.g. test/.foo.cjs) were incorrectly included in the coverage report, even though the default exclude patterns (such as test/**/*) were supposed to exclude all test files. Non-dotfile test files were excluded as intended.

Why it's non-trivial

A naive fix might simply add more patterns or tweak the glob, but the underlying cause is that the glob-matching library (minimatch) does not match dotfiles unless the dot: true option is set. Changing this globally could have unintended consequences, such as accidentally excluding files in dot-prefixed directories elsewhere in the filesystem (e.g., temp directories), so care must be taken to apply the option only where appropriate.

The approach

The PR changes how coverage exclusion globs are matched so that dotfiles are correctly excluded, but only in the intended context.

1. Add a constant with the correct minimatch option:

+// Match dotfiles (e.g. `test/.foo.js`) when applying coverage globs so the
+// default exclude patterns cover them.
+const kMatchGlobPatternOptions = { __proto__: null, dot: true };

This constant ensures that dotfiles are included in glob matches when desired.

2. Add a helper to create matchers with and without the dot option:

+function createCoverageMatcher(pattern) {
+  return {
+    __proto__: null,
+    relative: createMatcher(pattern, kMatchGlobPatternOptions),
+    absolute: createMatcher(pattern),
+  };
+}

This helper returns an object with two matchers:

  • relative uses dot: true so patterns like test/**/* match dotfiles in the project.
  • absolute omits dot: true to avoid matching dot-prefixed directories in absolute paths (like temp directories).

3. Update matcher usage in the coverage logic:

-    this.#excludeMatchers ??= ArrayPrototypeMap(
-      this.options.coverageExcludeGlobs ?? [], (pattern) => createMatcher(pattern));
-    this.#includeMatchers ??= ArrayPrototypeMap(
-      this.options.coverageIncludeGlobs ?? [], (pattern) => createMatcher(pattern));
+    this.#excludeMatchers ??= ArrayPrototypeMap(
+      this.options.coverageExcludeGlobs ?? [], createCoverageMatcher);
+    this.#includeMatchers ??= ArrayPrototypeMap(
+      this.options.coverageIncludeGlobs ?? [], createCoverageMatcher);

Now, exclude/include globs are compiled into the new matcher objects.

4. Use both matchers when checking for exclusions:

-      if (matcher.match(relativePath) || matcher.match(absolutePath)) return true;
+      if (matcher.relative.match(relativePath) ||
+          matcher.absolute.match(absolutePath)) return true;

Both the relative (with dot: true) and absolute (default) matchers are checked. This ensures that dotfiles are excluded when matched relative to the project, but not over-excluded due to dot directories in absolute paths.

5. Add a test for dotfile exclusion:

+  it('should exclude dotfile test files from coverage by default', async () => {
+    const args = [
+      '--no-experimental-strip-types',
+      '--test',
+      '--experimental-test-coverage',
+      '--test-reporter=tap',
+      'test/.dotfile.cjs',
+    ];
+    const result = spawnSync(process.execPath, args, {
+      env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path },
+      cwd: tmpdir.path
+    });
+
+    assert.strictEqual(result.stderr.toString(), '');
+    assertDefaultExclusions(result.stdout.toString());
+    assert.doesNotMatch(result.stdout.toString(), /#\s+\.dotfile\.cjs\s+\|/);
+    assert.strictEqual(result.status, 0);
+  });

This test verifies that a dotfile test is not included in the coverage report.

From the review thread

avivkeller: Can't we just set dot: true?

semimikoh: Good catch — reverted to the dot: true approach (which the PR body originally described). Pushed as a follow-up fixup. This also makes user-supplied --test-coverage-exclude globs match dotfiles, which the extra default pattern wouldn't have covered.


avivkeller: Can't we pass the same options and thus and use the same matcher for both or no?

semimikoh: Intentional — relative needs dot: true so the default exclude globs match project dotfiles like test/.foo.test.js (the point of this PR), but absolute deliberately leaves it off. Enabling dot: true there could make a glob unintentionally match a dot segment that shows up in the absolute path for unrelated reasons (e.g. an OS temp dir like test/.tmp.0), which isn't something the user's pattern was written to target.

Takeaways

  • Understand library defaults and edge cases: Always check how third-party libraries (like minimatch) handle special cases (e.g., dotfiles), and set options explicitly to match your intent.
  • Be careful with global option changes: Applying an option like dot: true everywhere can have unintended side effects, especially when matching both relative and absolute paths. Use context-aware logic to avoid over-matching.
  • Test the specific edge case: When fixing a subtle bug, add a targeted test to ensure the fix works as expected and to prevent regressions.