@mutineerjs/mutineer 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/core/__tests__/module.spec.js +66 -3
  2. package/dist/core/__tests__/sfc.spec.d.ts +1 -0
  3. package/dist/core/__tests__/sfc.spec.js +76 -0
  4. package/dist/core/__tests__/variant-utils.spec.d.ts +1 -0
  5. package/dist/core/__tests__/variant-utils.spec.js +93 -0
  6. package/dist/runner/__tests__/args.spec.d.ts +1 -0
  7. package/dist/runner/__tests__/args.spec.js +225 -0
  8. package/dist/runner/__tests__/cache.spec.d.ts +1 -0
  9. package/dist/runner/__tests__/cache.spec.js +180 -0
  10. package/dist/runner/__tests__/changed.spec.d.ts +1 -0
  11. package/dist/runner/__tests__/changed.spec.js +227 -0
  12. package/dist/runner/__tests__/cleanup.spec.d.ts +1 -0
  13. package/dist/runner/__tests__/cleanup.spec.js +41 -0
  14. package/dist/runner/__tests__/config.spec.d.ts +1 -0
  15. package/dist/runner/__tests__/config.spec.js +71 -0
  16. package/dist/runner/__tests__/coverage-resolver.spec.d.ts +1 -0
  17. package/dist/runner/__tests__/coverage-resolver.spec.js +171 -0
  18. package/dist/runner/__tests__/pool-executor.spec.d.ts +1 -0
  19. package/dist/runner/__tests__/pool-executor.spec.js +213 -0
  20. package/dist/runner/__tests__/tasks.spec.d.ts +1 -0
  21. package/dist/runner/__tests__/tasks.spec.js +95 -0
  22. package/dist/runner/__tests__/variants.spec.d.ts +1 -0
  23. package/dist/runner/__tests__/variants.spec.js +259 -0
  24. package/dist/runner/args.d.ts +5 -0
  25. package/dist/runner/args.js +7 -0
  26. package/dist/runner/config.js +2 -2
  27. package/dist/runner/coverage-resolver.d.ts +21 -0
  28. package/dist/runner/coverage-resolver.js +96 -0
  29. package/dist/runner/jest/__tests__/pool.spec.d.ts +1 -0
  30. package/dist/runner/jest/__tests__/pool.spec.js +212 -0
  31. package/dist/runner/jest/__tests__/worker-runtime.spec.d.ts +1 -0
  32. package/dist/runner/jest/__tests__/worker-runtime.spec.js +148 -0
  33. package/dist/runner/orchestrator.js +43 -295
  34. package/dist/runner/pool-executor.d.ts +17 -0
  35. package/dist/runner/pool-executor.js +143 -0
  36. package/dist/runner/shared/__tests__/mutant-paths.spec.d.ts +1 -0
  37. package/dist/runner/shared/__tests__/mutant-paths.spec.js +66 -0
  38. package/dist/runner/shared/__tests__/redirect-state.spec.d.ts +1 -0
  39. package/dist/runner/shared/__tests__/redirect-state.spec.js +56 -0
  40. package/dist/runner/tasks.d.ts +12 -0
  41. package/dist/runner/tasks.js +25 -0
  42. package/dist/runner/variants.d.ts +17 -2
  43. package/dist/runner/variants.js +33 -0
  44. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +4 -0
  45. package/dist/utils/__tests__/logger.spec.d.ts +1 -0
  46. package/dist/utils/__tests__/logger.spec.js +61 -0
  47. package/dist/utils/__tests__/normalizePath.spec.d.ts +1 -0
  48. package/dist/utils/__tests__/normalizePath.spec.js +22 -0
  49. package/package.json +1 -1
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { initializeRedirectState, setRedirect, getRedirect, clearRedirect, } from '../redirect-state.js';
3
+ describe('redirect-state', () => {
4
+ beforeEach(() => {
5
+ initializeRedirectState();
6
+ });
7
+ describe('initializeRedirectState', () => {
8
+ it('sets global redirect to null/null', () => {
9
+ expect(globalThis.__mutineer_redirect__).toEqual({
10
+ from: null,
11
+ to: null,
12
+ });
13
+ });
14
+ });
15
+ describe('setRedirect', () => {
16
+ it('sets the redirect config', () => {
17
+ setRedirect({ from: '/src/foo.ts', to: '/tmp/mutant.ts' });
18
+ expect(globalThis.__mutineer_redirect__).toEqual({
19
+ from: '/src/foo.ts',
20
+ to: '/tmp/mutant.ts',
21
+ });
22
+ });
23
+ });
24
+ describe('getRedirect', () => {
25
+ it('returns null when no redirect is set', () => {
26
+ expect(getRedirect()).toBeNull();
27
+ });
28
+ it('returns the redirect config when set', () => {
29
+ setRedirect({ from: '/src/foo.ts', to: '/tmp/mutant.ts' });
30
+ const redirect = getRedirect();
31
+ expect(redirect).toEqual({
32
+ from: '/src/foo.ts',
33
+ to: '/tmp/mutant.ts',
34
+ });
35
+ });
36
+ it('returns null when from is null', () => {
37
+ globalThis.__mutineer_redirect__ = { from: null, to: '/tmp/mutant.ts' };
38
+ expect(getRedirect()).toBeNull();
39
+ });
40
+ it('returns null when to is null', () => {
41
+ globalThis.__mutineer_redirect__ = { from: '/src/foo.ts', to: null };
42
+ expect(getRedirect()).toBeNull();
43
+ });
44
+ });
45
+ describe('clearRedirect', () => {
46
+ it('resets the redirect to null/null', () => {
47
+ setRedirect({ from: '/src/foo.ts', to: '/tmp/mutant.ts' });
48
+ clearRedirect();
49
+ expect(getRedirect()).toBeNull();
50
+ expect(globalThis.__mutineer_redirect__).toEqual({
51
+ from: null,
52
+ to: null,
53
+ });
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,12 @@
1
+ import type { Variant } from '../types/mutant.js';
2
+ import type { PerTestCoverageMap } from '../utils/coverage.js';
3
+ export interface MutantTask {
4
+ v: Variant;
5
+ tests: string[];
6
+ key: string;
7
+ }
8
+ /**
9
+ * Prepare mutant tasks from variants by pruning tests via per-test coverage,
10
+ * sorting tests deterministically, and computing cache keys.
11
+ */
12
+ export declare function prepareTasks(variants: readonly Variant[], perTestCoverage: PerTestCoverageMap | null): MutantTask[];
@@ -0,0 +1,25 @@
1
+ import { filterTestsByCoverage } from './variants.js';
2
+ import { hash, keyForTests } from './cache.js';
3
+ import { createLogger } from '../utils/logger.js';
4
+ const log = createLogger('tasks');
5
+ /**
6
+ * Prepare mutant tasks from variants by pruning tests via per-test coverage,
7
+ * sorting tests deterministically, and computing cache keys.
8
+ */
9
+ export function prepareTasks(variants, perTestCoverage) {
10
+ return variants.map((v) => {
11
+ let tests = Array.from(v.tests);
12
+ if (perTestCoverage && tests.length) {
13
+ const before = tests.length;
14
+ tests = filterTestsByCoverage(perTestCoverage, tests, v.file, v.line);
15
+ if (tests.length !== before) {
16
+ log.debug(`Pruned tests ${before} -> ${tests.length} for mutant ${v.name} via per-test coverage`);
17
+ }
18
+ }
19
+ tests.sort();
20
+ const testSig = hash(keyForTests(tests));
21
+ const codeSig = hash(v.code);
22
+ const key = `${testSig}:${codeSig}`;
23
+ return { v, tests, key };
24
+ });
25
+ }
@@ -4,8 +4,10 @@
4
4
  * Functions for enumerating mutation variants from source files.
5
5
  * Handles both regular modules and Vue SFC files.
6
6
  */
7
- import type { MutateTarget } from '../types/config.js';
8
- import type { MutantPayload } from '../types/mutant.js';
7
+ import type { MutateTarget, MutineerConfig } from '../types/config.js';
8
+ import type { MutantPayload, Variant } from '../types/mutant.js';
9
+ import { type CoverageData } from '../utils/coverage.js';
10
+ import type { TestMap } from './discover.js';
9
11
  /**
10
12
  * Get file path from target (handles both string and object forms).
11
13
  */
@@ -18,4 +20,17 @@ export declare function enumerateVariantsForTarget(root: string, t: MutateTarget
18
20
  * Filter tests to only those that cover a specific line in a file.
19
21
  */
20
22
  export declare function filterTestsByCoverage(perTest: Map<string, Map<string, Set<number>>>, tests: readonly string[], filePath: string, line: number): string[];
23
+ export interface EnumerateAllParams {
24
+ cwd: string;
25
+ targets: readonly MutateTarget[];
26
+ testMap: TestMap;
27
+ changedFiles: Set<string> | null;
28
+ coverageData: CoverageData | null;
29
+ config: MutineerConfig;
30
+ }
31
+ /**
32
+ * Enumerate variants for all targets, filtering by changed files and coverage.
33
+ * Links each variant to its relevant test files via the testMap.
34
+ */
35
+ export declare function enumerateAllVariants(params: EnumerateAllParams): Promise<Variant[]>;
21
36
  export type { Variant } from '../types/mutant.js';
@@ -8,6 +8,8 @@ import fs from 'node:fs/promises';
8
8
  import path from 'node:path';
9
9
  import { mutateVueSfcScriptSetup } from '../core/sfc.js';
10
10
  import { mutateModuleSource } from '../core/module.js';
11
+ import { normalizePath } from '../utils/normalizePath.js';
12
+ import { isLineCovered } from '../utils/coverage.js';
11
13
  import { createLogger } from '../utils/logger.js';
12
14
  const log = createLogger('variants');
13
15
  /**
@@ -64,3 +66,34 @@ export function filterTestsByCoverage(perTest, tests, filePath, line) {
64
66
  return lines.has(line);
65
67
  });
66
68
  }
69
+ /**
70
+ * Enumerate variants for all targets, filtering by changed files and coverage.
71
+ * Links each variant to its relevant test files via the testMap.
72
+ */
73
+ export async function enumerateAllVariants(params) {
74
+ const { cwd, targets, testMap, changedFiles, coverageData, config } = params;
75
+ const enumerated = await Promise.all(targets.map(async (target) => {
76
+ const file = getTargetFile(target);
77
+ const absFile = normalizePath(path.isAbsolute(file) ? file : path.join(cwd, file));
78
+ if (changedFiles && !changedFiles.has(absFile))
79
+ return [];
80
+ log.debug('Target file: ' + absFile);
81
+ const files = await enumerateVariantsForTarget(cwd, target, config.include, config.exclude, config.maxMutantsPerFile);
82
+ const testsAbs = testMap.get(normalizePath(absFile));
83
+ const tests = testsAbs ? Array.from(testsAbs) : [];
84
+ log.debug(` found ${files.length} variants, linked to ${tests.length} tests`);
85
+ // Filter by coverage if enabled
86
+ let filtered = files;
87
+ if (coverageData) {
88
+ filtered = files.filter((v) => isLineCovered(coverageData, absFile, v.line));
89
+ if (filtered.length !== files.length) {
90
+ log.debug(` filtered ${files.length} -> ${filtered.length} variants by coverage`);
91
+ }
92
+ }
93
+ return filtered.map((v) => ({ ...v, tests }));
94
+ }));
95
+ const variants = [];
96
+ for (const list of enumerated)
97
+ variants.push(...list);
98
+ return variants;
99
+ }
@@ -3,6 +3,10 @@ import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
5
  import { pathToFileURL } from 'node:url';
6
+ vi.mock('node:module', async (importOriginal) => {
7
+ const actual = await importOriginal();
8
+ return { ...actual, register: vi.fn() };
9
+ });
6
10
  import { resolve as poolResolve } from '../redirect-loader.js';
7
11
  describe('pool-redirect-loader resolve', () => {
8
12
  afterEach(() => {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ // We need to test with different DEBUG values, so we mock the module dynamically
3
+ describe('logger', () => {
4
+ let originalDebug;
5
+ beforeEach(() => {
6
+ originalDebug = process.env.MUTINEER_DEBUG;
7
+ vi.restoreAllMocks();
8
+ });
9
+ afterEach(() => {
10
+ if (originalDebug === undefined) {
11
+ delete process.env.MUTINEER_DEBUG;
12
+ }
13
+ else {
14
+ process.env.MUTINEER_DEBUG = originalDebug;
15
+ }
16
+ });
17
+ it('info logs to console.log', async () => {
18
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
19
+ const { createLogger } = await import('../logger.js');
20
+ const log = createLogger('test');
21
+ log.info('hello %s', 'world');
22
+ expect(spy).toHaveBeenCalledWith('hello %s', 'world');
23
+ });
24
+ it('warn logs to console.warn', async () => {
25
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => { });
26
+ const { createLogger } = await import('../logger.js');
27
+ const log = createLogger('test');
28
+ log.warn('warning message');
29
+ expect(spy).toHaveBeenCalledWith('warning message');
30
+ });
31
+ it('error logs to console.error', async () => {
32
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
33
+ const { createLogger } = await import('../logger.js');
34
+ const log = createLogger('test');
35
+ log.error('error message');
36
+ expect(spy).toHaveBeenCalledWith('error message');
37
+ });
38
+ it('debug logs to console.error when DEBUG is enabled', async () => {
39
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
40
+ // DEBUG is a module-level constant, so we check the tag prefix behavior
41
+ const { createLogger, DEBUG } = await import('../logger.js');
42
+ const log = createLogger('mytag');
43
+ log.debug('debug message');
44
+ if (DEBUG) {
45
+ expect(spy).toHaveBeenCalledWith('[mytag] debug message');
46
+ }
47
+ else {
48
+ // When DEBUG is false, debug should be a no-op
49
+ expect(spy).not.toHaveBeenCalled();
50
+ }
51
+ });
52
+ it('debug includes the tag prefix', async () => {
53
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => { });
54
+ const { createLogger, DEBUG } = await import('../logger.js');
55
+ const log = createLogger('custom-tag');
56
+ log.debug('test');
57
+ if (DEBUG) {
58
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining('[custom-tag]'));
59
+ }
60
+ });
61
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { normalizePath } from '../normalizePath.js';
3
+ describe('normalizePath', () => {
4
+ it('replaces backslashes with forward slashes', () => {
5
+ expect(normalizePath('src\\utils\\file.ts')).toBe('src/utils/file.ts');
6
+ });
7
+ it('handles multiple consecutive backslashes', () => {
8
+ expect(normalizePath('src\\\\file.ts')).toBe('src//file.ts');
9
+ });
10
+ it('leaves forward slashes unchanged', () => {
11
+ expect(normalizePath('src/utils/file.ts')).toBe('src/utils/file.ts');
12
+ });
13
+ it('handles empty string', () => {
14
+ expect(normalizePath('')).toBe('');
15
+ });
16
+ it('handles mixed slashes', () => {
17
+ expect(normalizePath('src\\utils/file.ts')).toBe('src/utils/file.ts');
18
+ });
19
+ it('handles Windows-style absolute paths', () => {
20
+ expect(normalizePath('C:\\Users\\dev\\project')).toBe('C:/Users/dev/project');
21
+ });
22
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutineerjs/mutineer",
3
- "version": "v0.2.3",
3
+ "version": "v0.2.4",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {