jest-test-lineage-reporter 2.0.2 → 2.1.0

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.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * CLI Command Router
3
+ * Main CLI logic with commander.js
4
+ */
5
+
6
+ const { Command } = require('commander');
7
+ const testCommand = require('./commands/test');
8
+ const mutateCommand = require('./commands/mutate');
9
+ const reportCommand = require('./commands/report');
10
+ const queryCommand = require('./commands/query');
11
+ const analyzeCommand = require('./commands/analyze');
12
+ const pkg = require('../../package.json');
13
+
14
+ async function run(argv) {
15
+ const program = new Command();
16
+
17
+ program
18
+ .name('jest-lineage')
19
+ .description('Comprehensive test analytics with lineage tracking and mutation testing')
20
+ .version(pkg.version, '-v, --version', 'Display version number');
21
+
22
+ // Test command - Run Jest with lineage tracking
23
+ program
24
+ .command('test [jest-args...]')
25
+ .description('Run Jest tests with lineage tracking enabled')
26
+ .option('--no-lineage', 'Disable lineage tracking')
27
+ .option('--no-performance', 'Disable performance tracking')
28
+ .option('--no-quality', 'Disable quality analysis')
29
+ .option('--config <path>', 'Path to Jest config file')
30
+ .option('--quiet, -q', 'Suppress console output')
31
+ .action(testCommand);
32
+
33
+ // Mutate command - Run mutation testing standalone
34
+ program
35
+ .command('mutate')
36
+ .description('Run mutation testing on existing lineage data')
37
+ .option('--data <path>', 'Path to lineage data file', '.jest-lineage-data.json')
38
+ .option('--threshold <number>', 'Mutation score threshold (%)', '80')
39
+ .option('--timeout <ms>', 'Timeout per mutation (ms)', '5000')
40
+ .option('--debug', 'Create debug mutation files instead of running tests')
41
+ .option('--debug-dir <path>', 'Directory for debug files', './mutations-debug')
42
+ .option('--operators <list>', 'Comma-separated mutation operators to enable')
43
+ .option('--verbose', 'Enable debug logging')
44
+ .action(mutateCommand);
45
+
46
+ // Report command - Generate HTML report
47
+ program
48
+ .command('report')
49
+ .description('Generate HTML report from existing lineage data')
50
+ .option('--data <path>', 'Path to lineage data file', '.jest-lineage-data.json')
51
+ .option('--output <path>', 'Output HTML file path', 'test-lineage-report.html')
52
+ .option('--open', 'Open report in browser after generation')
53
+ .option('--format <type>', 'Report format (html, json)', 'html')
54
+ .action(reportCommand);
55
+
56
+ // Query command - Query test coverage
57
+ program
58
+ .command('query <file> [line]')
59
+ .description('Query which tests cover specific files or lines')
60
+ .option('--data <path>', 'Path to lineage data file', '.jest-lineage-data.json')
61
+ .option('--json', 'Output as JSON')
62
+ .option('--format <type>', 'Output format (table, list, json)', 'table')
63
+ .action(queryCommand);
64
+
65
+ // Analyze command - Full workflow
66
+ program
67
+ .command('analyze')
68
+ .description('Full workflow: test + mutation + report')
69
+ .option('--config <path>', 'Path to Jest config file')
70
+ .option('--threshold <number>', 'Mutation score threshold (%)', '80')
71
+ .option('--output <path>', 'Output HTML file path', 'test-lineage-report.html')
72
+ .option('--open', 'Open report in browser')
73
+ .option('--skip-tests', 'Skip running tests (use existing data)')
74
+ .option('--skip-mutation', 'Skip mutation testing')
75
+ .option('--no-lineage', 'Disable lineage tracking')
76
+ .option('--no-performance', 'Disable performance tracking')
77
+ .option('--no-quality', 'Disable quality analysis')
78
+ .action(analyzeCommand);
79
+
80
+ // Show help if no command provided
81
+ if (argv.length <= 2) {
82
+ program.help();
83
+ }
84
+
85
+ // Parse and execute
86
+ await program.parseAsync(argv);
87
+ }
88
+
89
+ module.exports = { run };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Configuration Loader
3
+ * Load and merge configuration from multiple sources
4
+ * Priority: CLI args > env vars > config file > package.json > defaults
5
+ */
6
+
7
+ const { loadConfig } = require('../../config');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ /**
12
+ * Load full configuration with proper priority
13
+ * @param {object} cliOptions - Options from CLI arguments
14
+ * @returns {object} Merged configuration
15
+ */
16
+ function loadFullConfig(cliOptions = {}) {
17
+ // Start with defaults from config.js (includes env var processing)
18
+ let config = loadConfig();
19
+
20
+ // Load from package.json if available
21
+ const pkgConfig = loadPackageJsonConfig();
22
+ if (pkgConfig) {
23
+ config = { ...config, ...pkgConfig };
24
+ }
25
+
26
+ // Override with CLI options (highest priority)
27
+ const cliConfig = mapCliOptionsToConfig(cliOptions);
28
+ config = { ...config, ...cliConfig };
29
+
30
+ return config;
31
+ }
32
+
33
+ /**
34
+ * Map CLI options to config object
35
+ * @param {object} cliOptions - CLI options
36
+ * @returns {object} Config object
37
+ */
38
+ function mapCliOptionsToConfig(cliOptions) {
39
+ const config = {};
40
+
41
+ // Feature toggles
42
+ if (cliOptions.lineage === false) config.enableLineageTracking = false;
43
+ if (cliOptions.performance === false) config.enablePerformanceTracking = false;
44
+ if (cliOptions.quality === false) config.enableQualityAnalysis = false;
45
+
46
+ // Mutation testing settings
47
+ if (cliOptions.threshold !== undefined) {
48
+ config.mutationThreshold = parseInt(cliOptions.threshold);
49
+ }
50
+ if (cliOptions.timeout !== undefined) {
51
+ config.mutationTimeout = parseInt(cliOptions.timeout);
52
+ }
53
+ if (cliOptions.debug === true) {
54
+ config.debugMutations = true;
55
+ }
56
+ if (cliOptions.debugDir !== undefined) {
57
+ config.debugMutationDir = cliOptions.debugDir;
58
+ }
59
+ if (cliOptions.operators !== undefined) {
60
+ // Parse comma-separated operators
61
+ const operators = cliOptions.operators.split(',').map(o => o.trim());
62
+ config.mutationOperators = {
63
+ arithmetic: operators.includes('arithmetic'),
64
+ comparison: operators.includes('comparison'),
65
+ logical: operators.includes('logical'),
66
+ conditional: operators.includes('conditional'),
67
+ assignment: operators.includes('assignment'),
68
+ literals: operators.includes('literals'),
69
+ returns: operators.includes('returns'),
70
+ increments: operators.includes('increments')
71
+ };
72
+ }
73
+
74
+ // Output settings
75
+ if (cliOptions.output !== undefined) {
76
+ config.outputFile = cliOptions.output;
77
+ }
78
+ if (cliOptions.verbose === true) {
79
+ config.enableDebugLogging = true;
80
+ }
81
+ if (cliOptions.quiet === true) {
82
+ config.enableConsoleOutput = false;
83
+ }
84
+
85
+ // Skip options
86
+ if (cliOptions.skipMutation === true) {
87
+ config.enableMutationTesting = false;
88
+ }
89
+
90
+ return config;
91
+ }
92
+
93
+ /**
94
+ * Load config from package.json "jest-lineage" field
95
+ * @returns {object|null} Config from package.json or null
96
+ */
97
+ function loadPackageJsonConfig() {
98
+ try {
99
+ const pkgPath = path.join(process.cwd(), 'package.json');
100
+ if (fs.existsSync(pkgPath)) {
101
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
102
+ return pkg['jest-lineage'] || null;
103
+ }
104
+ } catch (error) {
105
+ // Silently ignore errors
106
+ }
107
+ return null;
108
+ }
109
+
110
+ module.exports = {
111
+ loadFullConfig,
112
+ mapCliOptionsToConfig,
113
+ loadPackageJsonConfig
114
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Lineage Data Loader
3
+ * Load and validate .jest-lineage-data.json
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const chalk = require('chalk');
9
+
10
+ /**
11
+ * Load lineage data from file
12
+ * @param {string} dataPath - Path to lineage data file
13
+ * @returns {object} Parsed lineage data
14
+ */
15
+ function loadLineageData(dataPath = '.jest-lineage-data.json') {
16
+ const resolvedPath = path.resolve(process.cwd(), dataPath);
17
+
18
+ if (!fs.existsSync(resolvedPath)) {
19
+ throw new Error(
20
+ `Lineage data file not found: ${chalk.yellow(resolvedPath)}\n\n` +
21
+ `${chalk.cyan('Hint:')} Run ${chalk.green('jest-lineage test')} first to generate lineage data.`
22
+ );
23
+ }
24
+
25
+ try {
26
+ const content = fs.readFileSync(resolvedPath, 'utf8');
27
+ const data = JSON.parse(content);
28
+
29
+ // Validate structure
30
+ if (!data.timestamp || !Array.isArray(data.tests)) {
31
+ throw new Error(
32
+ `Invalid lineage data format in ${chalk.yellow(resolvedPath)}\n\n` +
33
+ `Expected format: { timestamp: number, tests: array }`
34
+ );
35
+ }
36
+
37
+ if (data.tests.length === 0) {
38
+ throw new Error(
39
+ `No test data found in ${chalk.yellow(resolvedPath)}\n\n` +
40
+ `The file exists but contains no test results. Run ${chalk.green('jest-lineage test')} to generate data.`
41
+ );
42
+ }
43
+
44
+ return data;
45
+ } catch (error) {
46
+ if (error.name === 'SyntaxError') {
47
+ throw new Error(
48
+ `Failed to parse lineage data file: ${chalk.yellow(resolvedPath)}\n\n` +
49
+ `The file contains invalid JSON. Error: ${error.message}`
50
+ );
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Process raw lineage data into format needed by MutationTester
58
+ * @param {object} rawData - Raw data from .jest-lineage-data.json
59
+ * @returns {object} Processed lineage data { filePath: { lineNumber: [testInfo, ...] } }
60
+ */
61
+ function processLineageDataForMutation(rawData) {
62
+ const processed = {};
63
+
64
+ if (!rawData.tests) {
65
+ return processed;
66
+ }
67
+
68
+ rawData.tests.forEach((test) => {
69
+ if (!test.coverage) {
70
+ return;
71
+ }
72
+
73
+ const coverageKeys = Object.keys(test.coverage);
74
+
75
+ coverageKeys.forEach((lineKey) => {
76
+ // Parse line key: "file.ts:lineNumber"
77
+ const [filePath, lineNumber, ...suffixes] = lineKey.split(':');
78
+
79
+ // Skip metadata entries (depth, performance, meta) - only process basic line coverage
80
+ if (!lineNumber || suffixes.length > 0) {
81
+ return;
82
+ }
83
+
84
+ if (!processed[filePath]) {
85
+ processed[filePath] = {};
86
+ }
87
+
88
+ if (!processed[filePath][lineNumber]) {
89
+ processed[filePath][lineNumber] = [];
90
+ }
91
+
92
+ processed[filePath][lineNumber].push({
93
+ testName: test.name,
94
+ testType: test.type || 'it',
95
+ testFile: test.testFile || 'unknown',
96
+ executionCount: test.coverage[lineKey] || 1
97
+ });
98
+ });
99
+ });
100
+
101
+ return processed;
102
+ }
103
+
104
+ /**
105
+ * Check if lineage data file exists
106
+ * @param {string} dataPath - Path to check
107
+ * @returns {boolean} True if file exists
108
+ */
109
+ function lineageDataExists(dataPath = '.jest-lineage-data.json') {
110
+ const resolvedPath = path.resolve(process.cwd(), dataPath);
111
+ return fs.existsSync(resolvedPath);
112
+ }
113
+
114
+ module.exports = {
115
+ loadLineageData,
116
+ processLineageDataForMutation,
117
+ lineageDataExists
118
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Jest Runner
3
+ * Orchestrate Jest execution with proper environment variables
4
+ */
5
+
6
+ const { spawn } = require('child_process');
7
+ const path = require('path');
8
+ const chalk = require('chalk');
9
+
10
+ /**
11
+ * Run Jest with lineage tracking enabled
12
+ * @param {object} options - Jest run options
13
+ * @returns {Promise<object>} Result object with success status and exit code
14
+ */
15
+ async function runJest(options = {}) {
16
+ const {
17
+ args = [], // Jest arguments
18
+ config = null, // Path to Jest config
19
+ enableLineage = true, // Enable lineage tracking
20
+ enablePerformance = true, // Enable performance tracking
21
+ enableQuality = true, // Enable quality analysis
22
+ enableMutation = false, // Enable mutation mode
23
+ cwd = process.cwd(), // Working directory
24
+ stdio = 'inherit', // Stdio handling
25
+ quiet = false // Suppress output
26
+ } = options;
27
+
28
+ // Build Jest command
29
+ const jestPath = 'jest'; // Use npx/global jest
30
+ const jestArgs = [...args];
31
+
32
+ // Add config if specified
33
+ if (config) {
34
+ jestArgs.push('--config', config);
35
+ }
36
+
37
+ // Ensure coverage is collected (required for lineage tracking)
38
+ if (!jestArgs.includes('--coverage') && !jestArgs.includes('--no-coverage')) {
39
+ jestArgs.push('--coverage');
40
+ }
41
+
42
+ // Set environment variables for lineage tracking
43
+ const env = {
44
+ ...process.env,
45
+ JEST_LINEAGE_ENABLED: enableLineage ? 'true' : 'false',
46
+ JEST_LINEAGE_TRACKING: enableLineage ? 'true' : 'false',
47
+ JEST_LINEAGE_PERFORMANCE: enablePerformance ? 'true' : 'false',
48
+ JEST_LINEAGE_QUALITY: enableQuality ? 'true' : 'false',
49
+ JEST_LINEAGE_MUTATION: enableMutation ? 'true' : 'false',
50
+ JEST_LINEAGE_MUTATION_TESTING: 'false', // Not in mutation testing mode
51
+ };
52
+
53
+ if (!quiet) {
54
+ console.log(chalk.cyan('\n🧪 Running Jest with lineage tracking...\n'));
55
+ console.log(chalk.gray(`Command: ${jestPath} ${jestArgs.join(' ')}\n`));
56
+ }
57
+
58
+ return new Promise((resolve, reject) => {
59
+ const jest = spawn(jestPath, jestArgs, {
60
+ cwd,
61
+ env,
62
+ stdio,
63
+ shell: true
64
+ });
65
+
66
+ jest.on('close', (code) => {
67
+ if (code === 0) {
68
+ if (!quiet) {
69
+ console.log(chalk.green('\nāœ… Tests completed successfully'));
70
+ }
71
+ resolve({ success: true, exitCode: code });
72
+ } else {
73
+ if (!quiet) {
74
+ console.log(chalk.red(`\nāŒ Tests failed with exit code ${code}`));
75
+ }
76
+ resolve({ success: false, exitCode: code });
77
+ }
78
+ });
79
+
80
+ jest.on('error', (error) => {
81
+ console.error(chalk.red('\nāŒ Failed to run Jest:'), error.message);
82
+ console.error(chalk.yellow('\nMake sure Jest is installed:'));
83
+ console.error(chalk.gray(' npm install --save-dev jest\n'));
84
+ reject(error);
85
+ });
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Validate that Jest is available
91
+ * @returns {boolean} True if Jest is available
92
+ */
93
+ function isJestAvailable() {
94
+ try {
95
+ require.resolve('jest');
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ module.exports = {
103
+ runJest,
104
+ isJestAvailable
105
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Output Formatter
3
+ * Format console output with colors and formatting
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+
9
+ /**
10
+ * Print success message
11
+ * @param {string} message - Success message
12
+ */
13
+ function success(message) {
14
+ console.log(chalk.green(`āœ… ${message}`));
15
+ }
16
+
17
+ /**
18
+ * Print error message
19
+ * @param {string} message - Error message
20
+ */
21
+ function error(message) {
22
+ console.error(chalk.red(`āŒ ${message}`));
23
+ }
24
+
25
+ /**
26
+ * Print warning message
27
+ * @param {string} message - Warning message
28
+ */
29
+ function warning(message) {
30
+ console.log(chalk.yellow(`āš ļø ${message}`));
31
+ }
32
+
33
+ /**
34
+ * Print info message
35
+ * @param {string} message - Info message
36
+ */
37
+ function info(message) {
38
+ console.log(chalk.cyan(`ā„¹ļø ${message}`));
39
+ }
40
+
41
+ /**
42
+ * Print section header
43
+ * @param {string} title - Section title
44
+ */
45
+ function section(title) {
46
+ console.log(chalk.bold.cyan(`\n${title}`));
47
+ console.log(chalk.gray('═'.repeat(title.length + 2)));
48
+ }
49
+
50
+ /**
51
+ * Create a spinner for long operations
52
+ * @param {string} text - Spinner text
53
+ * @returns {object} Ora spinner instance
54
+ */
55
+ function spinner(text) {
56
+ return ora({
57
+ text,
58
+ color: 'cyan',
59
+ spinner: 'dots'
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Print mutation results summary
65
+ * @param {object} results - Mutation test results
66
+ */
67
+ function printMutationSummary(results) {
68
+ section('🧬 Mutation Testing Results');
69
+
70
+ console.log(`šŸ“Š ${chalk.bold('Total Mutations:')} ${results.totalMutations}`);
71
+ console.log(`${chalk.green('āœ… Killed:')} ${results.killedMutations}`);
72
+ console.log(`${chalk.red('šŸ”“ Survived:')} ${results.survivedMutations}`);
73
+ console.log(`${chalk.yellow('ā° Timeout:')} ${results.timeoutMutations || 0}`);
74
+ console.log(`${chalk.gray('āŒ Error:')} ${results.errorMutations || 0}`);
75
+ console.log(`${chalk.bold.cyan('šŸŽÆ Mutation Score:')} ${chalk.bold(results.mutationScore.toFixed(1))}%`);
76
+
77
+ if (results.mutationScore >= 80) {
78
+ console.log(chalk.green('\nāœ… Excellent mutation score!'));
79
+ } else if (results.mutationScore >= 60) {
80
+ console.log(chalk.yellow('\nāš ļø Good mutation score, but room for improvement'));
81
+ } else {
82
+ console.log(chalk.red('\nāŒ Low mutation score - consider improving test quality'));
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Print lineage data summary
88
+ * @param {object} data - Lineage data
89
+ */
90
+ function printLineageDataSummary(data) {
91
+ const testCount = data.tests.length;
92
+ const failedTests = data.tests.filter(t => t.failed).length;
93
+ const passedTests = testCount - failedTests;
94
+
95
+ console.log(chalk.cyan(`\nšŸ“Š Lineage data loaded:`));
96
+ console.log(` ${chalk.green('āœ“')} ${passedTests} tests passed`);
97
+ if (failedTests > 0) {
98
+ console.log(` ${chalk.red('āœ—')} ${failedTests} tests failed`);
99
+ }
100
+ console.log(` ${chalk.gray('šŸ“…')} Generated: ${new Date(data.timestamp).toLocaleString()}`);
101
+ }
102
+
103
+ /**
104
+ * Format file path for display
105
+ * @param {string} filePath - File path
106
+ * @returns {string} Formatted path
107
+ */
108
+ function formatPath(filePath) {
109
+ const cwd = process.cwd();
110
+ if (filePath.startsWith(cwd)) {
111
+ return chalk.gray(filePath.replace(cwd, '.'));
112
+ }
113
+ return chalk.gray(filePath);
114
+ }
115
+
116
+ module.exports = {
117
+ success,
118
+ error,
119
+ warning,
120
+ info,
121
+ section,
122
+ spinner,
123
+ printMutationSummary,
124
+ printLineageDataSummary,
125
+ formatPath
126
+ };
@@ -0,0 +1,66 @@
1
+ // Example file to demonstrate call depth tracking
2
+
3
+ export function directFunction(x: number): number {
4
+ return x * 2; // This will be depth 1 when called directly from tests
5
+ }
6
+ export function oneLevel(x: number): number {
7
+ return directFunction(x) + 1; // directFunction will be depth 2 here
8
+ }
9
+ export function twoLevels(x: number): number {
10
+ return oneLevel(x) + 1; // directFunction will be depth 3 here
11
+ }
12
+ export function threeLevels(x: number): number {
13
+ return twoLevels(x) + 1; // directFunction will be depth 4 here
14
+ }
15
+
16
+ // Complex function that calls multiple other functions
17
+ export function complexFunction(x: number): number {
18
+ const a = directFunction(x); // depth 2
19
+ const b = oneLevel(x); // directFunction will be depth 3 here
20
+ const c = twoLevels(x); // directFunction will be depth 4 here
21
+ return a + b + c;
22
+ }
23
+
24
+ // Recursive function to test deep call stacks
25
+ export function recursiveFunction(n: number, depth: number = 0): number {
26
+ if (depth >= 3) {
27
+ return directFunction(n); // This will be very deep
28
+ }
29
+ return recursiveFunction(n, depth + 1) + 1;
30
+ }
31
+
32
+ // Global array to hold leaked memory - this will cause actual memory leaks
33
+ const memoryLeakStorage: any[] = [];
34
+ export function memoryLeakFunction(size: number): number {
35
+ // Create large objects and store them globally (this leaks memory!)
36
+ const largeObject = {
37
+ id: Date.now(),
38
+ data: new Array(size).fill(0).map((_, i) => ({
39
+ index: i,
40
+ value: Math.random(),
41
+ timestamp: new Date(),
42
+ largeString: 'x'.repeat(1000),
43
+ // 1KB string per item
44
+ metadata: {
45
+ created: Date.now(),
46
+ processed: false,
47
+ tags: ['memory', 'leak', 'test', 'large'],
48
+ history: new Array(100).fill(0).map(() => Math.random())
49
+ }
50
+ }))
51
+ };
52
+
53
+ // Store in global array - this prevents garbage collection (memory leak!)
54
+ memoryLeakStorage.push(largeObject);
55
+
56
+ // Also call our tracked function
57
+ return directFunction(size);
58
+ }
59
+ export function clearMemoryLeaks(): number {
60
+ const count = memoryLeakStorage.length;
61
+ memoryLeakStorage.length = 0; // Clear the array
62
+ return count;
63
+ }
64
+ export function getMemoryLeakCount(): number {
65
+ return memoryLeakStorage.length;
66
+ }