@theihtisham/ai-testgen 1.0.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.
- package/LICENSE +21 -0
- package/README.md +383 -0
- package/dist/analyzers/analyzer.d.ts +10 -0
- package/dist/analyzers/analyzer.d.ts.map +1 -0
- package/dist/analyzers/analyzer.js +131 -0
- package/dist/analyzers/analyzer.js.map +1 -0
- package/dist/analyzers/go-analyzer.d.ts +3 -0
- package/dist/analyzers/go-analyzer.d.ts.map +1 -0
- package/dist/analyzers/go-analyzer.js +244 -0
- package/dist/analyzers/go-analyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +5 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +15 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/js-ts-analyzer.d.ts +3 -0
- package/dist/analyzers/js-ts-analyzer.d.ts.map +1 -0
- package/dist/analyzers/js-ts-analyzer.js +299 -0
- package/dist/analyzers/js-ts-analyzer.js.map +1 -0
- package/dist/analyzers/python-analyzer.d.ts +3 -0
- package/dist/analyzers/python-analyzer.d.ts.map +1 -0
- package/dist/analyzers/python-analyzer.js +306 -0
- package/dist/analyzers/python-analyzer.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +381 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +80 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +14 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +6 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +126 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/coverage.d.ts +4 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +108 -0
- package/dist/coverage.js.map +1 -0
- package/dist/generators/ai-generator.d.ts +4 -0
- package/dist/generators/ai-generator.d.ts.map +1 -0
- package/dist/generators/ai-generator.js +175 -0
- package/dist/generators/ai-generator.js.map +1 -0
- package/dist/generators/generator.d.ts +4 -0
- package/dist/generators/generator.d.ts.map +1 -0
- package/dist/generators/generator.js +121 -0
- package/dist/generators/generator.js.map +1 -0
- package/dist/generators/go-generator.d.ts +3 -0
- package/dist/generators/go-generator.d.ts.map +1 -0
- package/dist/generators/go-generator.js +175 -0
- package/dist/generators/go-generator.js.map +1 -0
- package/dist/generators/index.d.ts +6 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +16 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/js-ts-generator.d.ts +3 -0
- package/dist/generators/js-ts-generator.d.ts.map +1 -0
- package/dist/generators/js-ts-generator.js +331 -0
- package/dist/generators/js-ts-generator.js.map +1 -0
- package/dist/generators/python-generator.d.ts +3 -0
- package/dist/generators/python-generator.d.ts.map +1 -0
- package/dist/generators/python-generator.js +180 -0
- package/dist/generators/python-generator.js.map +1 -0
- package/dist/incremental.d.ts +16 -0
- package/dist/incremental.d.ts.map +1 -0
- package/dist/incremental.js +146 -0
- package/dist/incremental.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/mutation/index.d.ts +2 -0
- package/dist/mutation/index.d.ts.map +1 -0
- package/dist/mutation/index.js +9 -0
- package/dist/mutation/index.js.map +1 -0
- package/dist/mutation/mutator.d.ts +6 -0
- package/dist/mutation/mutator.d.ts.map +1 -0
- package/dist/mutation/mutator.js +237 -0
- package/dist/mutation/mutator.js.map +1 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/file.d.ts +10 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +108 -0
- package/dist/utils/file.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +24 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/language.d.ts +8 -0
- package/dist/utils/language.d.ts.map +1 -0
- package/dist/utils/language.js +137 -0
- package/dist/utils/language.js.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +57 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/watcher/index.d.ts +2 -0
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/index.js +6 -0
- package/dist/watcher/index.js.map +1 -0
- package/dist/watcher/watcher.d.ts +19 -0
- package/dist/watcher/watcher.d.ts.map +1 -0
- package/dist/watcher/watcher.js +122 -0
- package/dist/watcher/watcher.js.map +1 -0
- package/package.json +63 -0
- package/src/analyzers/analyzer.ts +180 -0
- package/src/analyzers/go-analyzer.ts +235 -0
- package/src/analyzers/index.ts +4 -0
- package/src/analyzers/js-ts-analyzer.ts +324 -0
- package/src/analyzers/python-analyzer.ts +306 -0
- package/src/cli.ts +416 -0
- package/src/config/defaults.ts +81 -0
- package/src/config/index.ts +2 -0
- package/src/config/loader.ts +114 -0
- package/src/coverage.ts +128 -0
- package/src/generators/ai-generator.ts +170 -0
- package/src/generators/generator.ts +117 -0
- package/src/generators/go-generator.ts +183 -0
- package/src/generators/index.ts +5 -0
- package/src/generators/js-ts-generator.ts +379 -0
- package/src/generators/python-generator.ts +201 -0
- package/src/incremental.ts +131 -0
- package/src/index.ts +8 -0
- package/src/mutation/index.ts +1 -0
- package/src/mutation/mutator.ts +314 -0
- package/src/types.ts +240 -0
- package/src/utils/file.ts +73 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/language.ts +114 -0
- package/src/utils/logger.ts +61 -0
- package/src/watcher/index.ts +1 -0
- package/src/watcher/watcher.ts +103 -0
- package/tests/analyzer.test.ts +429 -0
- package/tests/config.test.ts +171 -0
- package/tests/coverage.test.ts +197 -0
- package/tests/file-utils.test.ts +121 -0
- package/tests/generators.test.ts +383 -0
- package/tests/incremental.test.ts +108 -0
- package/tests/language.test.ts +90 -0
- package/tests/mutation.test.ts +286 -0
- package/tests/watcher.test.ts +35 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +25 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { loadConfig, resolveConfig, findConfig, createSampleConfig } from '../src/config/loader.js';
|
|
6
|
+
import { DEFAULT_CONFIG, LANGUAGE_EXTENSIONS, LANGUAGE_FRAMEWORKS } from '../src/config/defaults.js';
|
|
7
|
+
|
|
8
|
+
describe('Configuration', () => {
|
|
9
|
+
describe('DEFAULT_CONFIG', () => {
|
|
10
|
+
it('has all required top-level fields', () => {
|
|
11
|
+
expect(DEFAULT_CONFIG.version).toBe('1.0.0');
|
|
12
|
+
expect(DEFAULT_CONFIG.language).toBe('auto');
|
|
13
|
+
expect(DEFAULT_CONFIG.framework).toBe('auto');
|
|
14
|
+
expect(DEFAULT_CONFIG.outputDir).toBe('__tests__');
|
|
15
|
+
expect(DEFAULT_CONFIG.coverage.target).toBe(90);
|
|
16
|
+
expect(DEFAULT_CONFIG.ai.enabled).toBe(false);
|
|
17
|
+
expect(DEFAULT_CONFIG.ai.privacyMode).toBe(true);
|
|
18
|
+
expect(DEFAULT_CONFIG.generation.unitTests).toBe(true);
|
|
19
|
+
expect(DEFAULT_CONFIG.generation.integrationTests).toBe(true);
|
|
20
|
+
expect(DEFAULT_CONFIG.generation.edgeCaseTests).toBe(true);
|
|
21
|
+
expect(DEFAULT_CONFIG.generation.mockGeneration).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('has sensible defaults for all sections', () => {
|
|
25
|
+
expect(DEFAULT_CONFIG.exclude).toContain('node_modules/**');
|
|
26
|
+
expect(DEFAULT_CONFIG.watch.debounceMs).toBeGreaterThan(0);
|
|
27
|
+
expect(DEFAULT_CONFIG.generation.maxTestsPerFunction).toBeGreaterThan(0);
|
|
28
|
+
expect(DEFAULT_CONFIG.ai.temperature).toBeGreaterThanOrEqual(0);
|
|
29
|
+
expect(DEFAULT_CONFIG.ai.temperature).toBeLessThanOrEqual(1);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('LANGUAGE_EXTENSIONS', () => {
|
|
34
|
+
it('maps all supported extensions', () => {
|
|
35
|
+
expect(LANGUAGE_EXTENSIONS['.ts']).toBe('typescript');
|
|
36
|
+
expect(LANGUAGE_EXTENSIONS['.tsx']).toBe('typescript');
|
|
37
|
+
expect(LANGUAGE_EXTENSIONS['.js']).toBe('javascript');
|
|
38
|
+
expect(LANGUAGE_EXTENSIONS['.jsx']).toBe('javascript');
|
|
39
|
+
expect(LANGUAGE_EXTENSIONS['.py']).toBe('python');
|
|
40
|
+
expect(LANGUAGE_EXTENSIONS['.go']).toBe('go');
|
|
41
|
+
expect(LANGUAGE_EXTENSIONS['.rs']).toBe('rust');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('covers all 5 supported languages', () => {
|
|
45
|
+
const languages = new Set(Object.values(LANGUAGE_EXTENSIONS));
|
|
46
|
+
expect(languages.has('typescript')).toBe(true);
|
|
47
|
+
expect(languages.has('javascript')).toBe(true);
|
|
48
|
+
expect(languages.has('python')).toBe(true);
|
|
49
|
+
expect(languages.has('go')).toBe(true);
|
|
50
|
+
expect(languages.has('rust')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('LANGUAGE_FRAMEWORKS', () => {
|
|
55
|
+
it('maps correct frameworks for each language', () => {
|
|
56
|
+
expect(LANGUAGE_FRAMEWORKS.typescript).toContain('jest');
|
|
57
|
+
expect(LANGUAGE_FRAMEWORKS.typescript).toContain('vitest');
|
|
58
|
+
expect(LANGUAGE_FRAMEWORKS.javascript).toContain('jest');
|
|
59
|
+
expect(LANGUAGE_FRAMEWORKS.python).toContain('pytest');
|
|
60
|
+
expect(LANGUAGE_FRAMEWORKS.go).toContain('go-test');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('loadConfig', () => {
|
|
65
|
+
let tempDir: string;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testgen-config-'));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('loads YAML config files', () => {
|
|
76
|
+
const configPath = path.join(tempDir, '.aitestgen.yml');
|
|
77
|
+
fs.writeFileSync(configPath, `
|
|
78
|
+
language: typescript
|
|
79
|
+
framework: vitest
|
|
80
|
+
outputDir: tests
|
|
81
|
+
coverage:
|
|
82
|
+
target: 95
|
|
83
|
+
ai:
|
|
84
|
+
enabled: true
|
|
85
|
+
provider: openai
|
|
86
|
+
`);
|
|
87
|
+
const config = loadConfig(configPath);
|
|
88
|
+
expect(config.language).toBe('typescript');
|
|
89
|
+
expect(config.framework).toBe('vitest');
|
|
90
|
+
expect(config.outputDir).toBe('tests');
|
|
91
|
+
expect(config.coverage.target).toBe(95);
|
|
92
|
+
expect(config.ai.enabled).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('loads JSON config files', () => {
|
|
96
|
+
const configPath = path.join(tempDir, '.aitestgen.json');
|
|
97
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
98
|
+
language: 'python',
|
|
99
|
+
framework: 'pytest',
|
|
100
|
+
outputDir: 'tests',
|
|
101
|
+
}));
|
|
102
|
+
const config = loadConfig(configPath);
|
|
103
|
+
expect(config.language).toBe('python');
|
|
104
|
+
expect(config.framework).toBe('pytest');
|
|
105
|
+
expect(config.outputDir).toBe('tests');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('merges with defaults for missing fields', () => {
|
|
109
|
+
const configPath = path.join(tempDir, '.aitestgen.yml');
|
|
110
|
+
fs.writeFileSync(configPath, 'language: go');
|
|
111
|
+
const config = loadConfig(configPath);
|
|
112
|
+
expect(config.language).toBe('go');
|
|
113
|
+
expect(config.version).toBe('1.0.0'); // from defaults
|
|
114
|
+
expect(config.coverage.target).toBe(90); // from defaults
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('findConfig', () => {
|
|
119
|
+
let tempDir: string;
|
|
120
|
+
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testgen-find-'));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterEach(() => {
|
|
126
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('finds config in the current directory', () => {
|
|
130
|
+
const configPath = path.join(tempDir, '.aitestgen.yml');
|
|
131
|
+
fs.writeFileSync(configPath, 'language: typescript');
|
|
132
|
+
const found = findConfig(tempDir);
|
|
133
|
+
expect(found).toBe(configPath);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('finds config in parent directories', () => {
|
|
137
|
+
const childDir = path.join(tempDir, 'src', 'lib');
|
|
138
|
+
fs.mkdirSync(childDir, { recursive: true });
|
|
139
|
+
const configPath = path.join(tempDir, '.aitestgen.yml');
|
|
140
|
+
fs.writeFileSync(configPath, 'language: typescript');
|
|
141
|
+
const found = findConfig(childDir);
|
|
142
|
+
expect(found).toBe(configPath);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns null when no config is found', () => {
|
|
146
|
+
const found = findConfig(os.tmpdir());
|
|
147
|
+
expect(found).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('createSampleConfig', () => {
|
|
152
|
+
let tempDir: string;
|
|
153
|
+
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testgen-sample-'));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
afterEach(() => {
|
|
159
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('creates a valid YAML config file', () => {
|
|
163
|
+
const outputPath = path.join(tempDir, '.aitestgen.yml');
|
|
164
|
+
createSampleConfig(outputPath);
|
|
165
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
166
|
+
const config = loadConfig(outputPath);
|
|
167
|
+
expect(config.ai.enabled).toBe(true);
|
|
168
|
+
expect(config.generation.unitTests).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { predictCoverage, formatCoverageReport } from '../src/coverage.js';
|
|
3
|
+
import { SourceAnalysis, TestSuite, CoveragePrediction } from '../src/types.js';
|
|
4
|
+
|
|
5
|
+
function createMockAnalysis(): SourceAnalysis {
|
|
6
|
+
return {
|
|
7
|
+
filePath: '/src/utils.ts',
|
|
8
|
+
language: 'typescript',
|
|
9
|
+
functions: [
|
|
10
|
+
{
|
|
11
|
+
name: 'add',
|
|
12
|
+
isAsync: false,
|
|
13
|
+
isExported: true,
|
|
14
|
+
params: [
|
|
15
|
+
{ name: 'a', type: 'number', optional: false, defaultValue: null },
|
|
16
|
+
{ name: 'b', type: 'number', optional: false, defaultValue: null },
|
|
17
|
+
],
|
|
18
|
+
returnType: 'number',
|
|
19
|
+
throws: [],
|
|
20
|
+
complexity: 1,
|
|
21
|
+
hasSideEffects: false,
|
|
22
|
+
startLine: 1,
|
|
23
|
+
endLine: 3,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'divide',
|
|
27
|
+
isAsync: false,
|
|
28
|
+
isExported: true,
|
|
29
|
+
params: [
|
|
30
|
+
{ name: 'a', type: 'number', optional: false, defaultValue: null },
|
|
31
|
+
{ name: 'b', type: 'number', optional: false, defaultValue: null },
|
|
32
|
+
],
|
|
33
|
+
returnType: 'number',
|
|
34
|
+
throws: ['Error'],
|
|
35
|
+
complexity: 3,
|
|
36
|
+
hasSideEffects: false,
|
|
37
|
+
startLine: 5,
|
|
38
|
+
endLine: 10,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'formatName',
|
|
42
|
+
isAsync: false,
|
|
43
|
+
isExported: true,
|
|
44
|
+
params: [
|
|
45
|
+
{ name: 'first', type: 'string', optional: false, defaultValue: null },
|
|
46
|
+
{ name: 'last', type: 'string', optional: true, defaultValue: "''" },
|
|
47
|
+
],
|
|
48
|
+
returnType: 'string',
|
|
49
|
+
throws: [],
|
|
50
|
+
complexity: 2,
|
|
51
|
+
hasSideEffects: false,
|
|
52
|
+
startLine: 12,
|
|
53
|
+
endLine: 16,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
classes: [
|
|
57
|
+
{
|
|
58
|
+
name: 'Calculator',
|
|
59
|
+
isExported: true,
|
|
60
|
+
constructorParams: [],
|
|
61
|
+
methods: [
|
|
62
|
+
{
|
|
63
|
+
name: 'round',
|
|
64
|
+
isAsync: false,
|
|
65
|
+
isExported: false,
|
|
66
|
+
params: [
|
|
67
|
+
{ name: 'value', type: 'number', optional: false, defaultValue: null },
|
|
68
|
+
],
|
|
69
|
+
returnType: 'number',
|
|
70
|
+
throws: [],
|
|
71
|
+
complexity: 1,
|
|
72
|
+
hasSideEffects: false,
|
|
73
|
+
startLine: 20,
|
|
74
|
+
endLine: 22,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
properties: [],
|
|
78
|
+
implements: [],
|
|
79
|
+
extends: null,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
interfaces: [],
|
|
83
|
+
exports: [
|
|
84
|
+
{ name: 'add', type: 'function', filePath: '/src/utils.ts' },
|
|
85
|
+
{ name: 'divide', type: 'function', filePath: '/src/utils.ts' },
|
|
86
|
+
{ name: 'formatName', type: 'function', filePath: '/src/utils.ts' },
|
|
87
|
+
{ name: 'Calculator', type: 'class', filePath: '/src/utils.ts' },
|
|
88
|
+
],
|
|
89
|
+
imports: [],
|
|
90
|
+
dependencies: [],
|
|
91
|
+
linesOfCode: 30,
|
|
92
|
+
cyclomaticComplexity: 7,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createMockSuite(testedNames: string[]): TestSuite {
|
|
97
|
+
return {
|
|
98
|
+
filePath: '/tests/utils.test.ts',
|
|
99
|
+
sourceFilePath: '/src/utils.ts',
|
|
100
|
+
language: 'typescript',
|
|
101
|
+
framework: 'vitest',
|
|
102
|
+
testCases: testedNames.map((name) => ({
|
|
103
|
+
name: `${name} test`,
|
|
104
|
+
type: 'unit' as const,
|
|
105
|
+
description: `Test ${name}`,
|
|
106
|
+
code: '',
|
|
107
|
+
expectedBehavior: 'Works',
|
|
108
|
+
inputDescription: 'Valid input',
|
|
109
|
+
tags: ['unit'],
|
|
110
|
+
})),
|
|
111
|
+
mocks: [],
|
|
112
|
+
imports: [],
|
|
113
|
+
setupCode: '',
|
|
114
|
+
teardownCode: '',
|
|
115
|
+
coverageEstimate: 80,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
describe('Coverage Prediction', () => {
|
|
120
|
+
it('predicts coverage for fully tested code', () => {
|
|
121
|
+
const analysis = createMockAnalysis();
|
|
122
|
+
const suite = createMockSuite(['add', 'divide', 'formatName', 'round']);
|
|
123
|
+
const prediction = predictCoverage(analysis, suite);
|
|
124
|
+
|
|
125
|
+
expect(prediction.estimatedFunctionCoverage).toBeGreaterThan(50);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('reports uncovered paths', () => {
|
|
129
|
+
const analysis = createMockAnalysis();
|
|
130
|
+
const suite = createMockSuite(['add']);
|
|
131
|
+
const prediction = predictCoverage(analysis, suite);
|
|
132
|
+
|
|
133
|
+
expect(prediction.uncoveredPaths.length).toBeGreaterThan(0);
|
|
134
|
+
expect(prediction.uncoveredPaths.some((p) => p.includes('divide'))).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('suggests additional tests for uncovered code', () => {
|
|
138
|
+
const analysis = createMockAnalysis();
|
|
139
|
+
const suite = createMockSuite(['add']);
|
|
140
|
+
const prediction = predictCoverage(analysis, suite);
|
|
141
|
+
|
|
142
|
+
expect(prediction.suggestions.length).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('identifies untested error paths', () => {
|
|
146
|
+
const analysis = createMockAnalysis();
|
|
147
|
+
const suite = createMockSuite(['add', 'divide', 'formatName']);
|
|
148
|
+
const prediction = predictCoverage(analysis, suite);
|
|
149
|
+
|
|
150
|
+
// divide throws but we have no error test
|
|
151
|
+
expect(prediction.suggestions.some((s) => s.includes('error path') || s.includes('error'))).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('identifies untested optional parameters', () => {
|
|
155
|
+
const analysis = createMockAnalysis();
|
|
156
|
+
const suite = createMockSuite(['add', 'divide']);
|
|
157
|
+
const prediction = predictCoverage(analysis, suite);
|
|
158
|
+
|
|
159
|
+
// formatName has optional 'last' param
|
|
160
|
+
expect(
|
|
161
|
+
prediction.suggestions.some((s) => s.includes('formatName') && s.includes('optional')),
|
|
162
|
+
).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('provides coverage percentages between 0 and 100', () => {
|
|
166
|
+
const analysis = createMockAnalysis();
|
|
167
|
+
const suite = createMockSuite(['add']);
|
|
168
|
+
const prediction = predictCoverage(analysis, suite);
|
|
169
|
+
|
|
170
|
+
expect(prediction.estimatedLineCoverage).toBeGreaterThanOrEqual(0);
|
|
171
|
+
expect(prediction.estimatedLineCoverage).toBeLessThanOrEqual(100);
|
|
172
|
+
expect(prediction.estimatedBranchCoverage).toBeGreaterThanOrEqual(0);
|
|
173
|
+
expect(prediction.estimatedBranchCoverage).toBeLessThanOrEqual(100);
|
|
174
|
+
expect(prediction.estimatedFunctionCoverage).toBeGreaterThanOrEqual(0);
|
|
175
|
+
expect(prediction.estimatedFunctionCoverage).toBeLessThanOrEqual(100);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('formatCoverageReport', () => {
|
|
179
|
+
it('formats a readable coverage report', () => {
|
|
180
|
+
const prediction: CoveragePrediction = {
|
|
181
|
+
filePath: '/src/utils.ts',
|
|
182
|
+
estimatedLineCoverage: 85,
|
|
183
|
+
estimatedBranchCoverage: 72,
|
|
184
|
+
estimatedFunctionCoverage: 90,
|
|
185
|
+
uncoveredPaths: ['divide error path'],
|
|
186
|
+
suggestions: ['Add error path test for divide'],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const report = formatCoverageReport(prediction);
|
|
190
|
+
expect(report).toContain('85%');
|
|
191
|
+
expect(report).toContain('72%');
|
|
192
|
+
expect(report).toContain('90%');
|
|
193
|
+
expect(report).toContain('divide error path');
|
|
194
|
+
expect(report).toContain('Add error path test');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import {
|
|
6
|
+
readFile,
|
|
7
|
+
writeFile,
|
|
8
|
+
fileExists,
|
|
9
|
+
getRelativePath,
|
|
10
|
+
ensureDir,
|
|
11
|
+
getFileHash,
|
|
12
|
+
hashString,
|
|
13
|
+
} from '../src/utils/file.js';
|
|
14
|
+
|
|
15
|
+
describe('File Utilities', () => {
|
|
16
|
+
let tempDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testgen-file-'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('readFile', () => {
|
|
27
|
+
it('reads file contents', () => {
|
|
28
|
+
const filePath = path.join(tempDir, 'test.txt');
|
|
29
|
+
fs.writeFileSync(filePath, 'hello world');
|
|
30
|
+
expect(readFile(filePath)).toBe('hello world');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('throws for non-existent files', () => {
|
|
34
|
+
expect(() => readFile(path.join(tempDir, 'missing.txt'))).toThrow('File not found');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('writeFile', () => {
|
|
39
|
+
it('writes file contents', () => {
|
|
40
|
+
const filePath = path.join(tempDir, 'output.txt');
|
|
41
|
+
writeFile(filePath, 'test content');
|
|
42
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('test content');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('creates directories as needed', () => {
|
|
46
|
+
const filePath = path.join(tempDir, 'nested', 'deep', 'output.txt');
|
|
47
|
+
writeFile(filePath, 'nested content');
|
|
48
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('nested content');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('fileExists', () => {
|
|
53
|
+
it('returns true for existing files', () => {
|
|
54
|
+
const filePath = path.join(tempDir, 'exists.txt');
|
|
55
|
+
fs.writeFileSync(filePath, 'content');
|
|
56
|
+
expect(fileExists(filePath)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns false for non-existent files', () => {
|
|
60
|
+
expect(fileExists(path.join(tempDir, 'missing.txt'))).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('getRelativePath', () => {
|
|
65
|
+
it('computes relative paths between files', () => {
|
|
66
|
+
const from = '/src/components/App.tsx';
|
|
67
|
+
const to = '/src/utils/helpers.ts';
|
|
68
|
+
const rel = getRelativePath(from, to);
|
|
69
|
+
expect(rel).toBe('../utils/helpers.ts');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('prefixes with ./ for same-directory files', () => {
|
|
73
|
+
const from = '/src/App.tsx';
|
|
74
|
+
const to = '/src/utils.ts';
|
|
75
|
+
const rel = getRelativePath(from, to);
|
|
76
|
+
expect(rel).toBe('./utils.ts');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('ensureDir', () => {
|
|
81
|
+
it('creates directories recursively', () => {
|
|
82
|
+
const dirPath = path.join(tempDir, 'a', 'b', 'c');
|
|
83
|
+
ensureDir(dirPath);
|
|
84
|
+
expect(fs.existsSync(dirPath)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not throw for existing directories', () => {
|
|
88
|
+
expect(() => ensureDir(tempDir)).not.toThrow();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('getFileHash', () => {
|
|
93
|
+
it('returns consistent hashes for same content', () => {
|
|
94
|
+
const filePath1 = path.join(tempDir, 'file1.txt');
|
|
95
|
+
const filePath2 = path.join(tempDir, 'file2.txt');
|
|
96
|
+
fs.writeFileSync(filePath1, 'same content');
|
|
97
|
+
fs.writeFileSync(filePath2, 'same content');
|
|
98
|
+
|
|
99
|
+
expect(getFileHash(filePath1)).toBe(getFileHash(filePath2));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns different hashes for different content', () => {
|
|
103
|
+
const filePath1 = path.join(tempDir, 'file1.txt');
|
|
104
|
+
const filePath2 = path.join(tempDir, 'file2.txt');
|
|
105
|
+
fs.writeFileSync(filePath1, 'content A');
|
|
106
|
+
fs.writeFileSync(filePath2, 'content B');
|
|
107
|
+
|
|
108
|
+
expect(getFileHash(filePath1)).not.toBe(getFileHash(filePath2));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('hashString', () => {
|
|
113
|
+
it('returns consistent hash for same input', () => {
|
|
114
|
+
expect(hashString('test')).toBe(hashString('test'));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns different hash for different input', () => {
|
|
118
|
+
expect(hashString('test1')).not.toBe(hashString('test2'));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|