qa360 1.4.5 → 2.0.1
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/README.md +1 -1
- package/dist/commands/ai.d.ts +41 -0
- package/dist/commands/ai.js +499 -0
- package/dist/commands/ask.js +12 -12
- package/dist/commands/coverage.d.ts +8 -0
- package/dist/commands/coverage.js +252 -0
- package/dist/commands/explain.d.ts +27 -0
- package/dist/commands/explain.js +630 -0
- package/dist/commands/flakiness.d.ts +73 -0
- package/dist/commands/flakiness.js +435 -0
- package/dist/commands/generate.d.ts +66 -0
- package/dist/commands/generate.js +438 -0
- package/dist/commands/init.d.ts +56 -9
- package/dist/commands/init.js +217 -10
- package/dist/commands/monitor.d.ts +27 -0
- package/dist/commands/monitor.js +225 -0
- package/dist/commands/ollama.d.ts +40 -0
- package/dist/commands/ollama.js +301 -0
- package/dist/commands/pack.d.ts +37 -9
- package/dist/commands/pack.js +240 -141
- package/dist/commands/regression.d.ts +8 -0
- package/dist/commands/regression.js +340 -0
- package/dist/commands/repair.d.ts +26 -0
- package/dist/commands/repair.js +307 -0
- package/dist/commands/retry.d.ts +43 -0
- package/dist/commands/retry.js +275 -0
- package/dist/commands/run.d.ts +8 -3
- package/dist/commands/run.js +45 -31
- package/dist/commands/slo.d.ts +8 -0
- package/dist/commands/slo.js +327 -0
- package/dist/core/adapters/playwright-native-api.d.ts +183 -0
- package/dist/core/adapters/playwright-native-api.js +461 -0
- package/dist/core/adapters/playwright-ui.d.ts +7 -0
- package/dist/core/adapters/playwright-ui.js +29 -1
- package/dist/core/ai/anthropic-provider.d.ts +50 -0
- package/dist/core/ai/anthropic-provider.js +211 -0
- package/dist/core/ai/deepseek-provider.d.ts +81 -0
- package/dist/core/ai/deepseek-provider.js +254 -0
- package/dist/core/ai/index.d.ts +60 -0
- package/dist/core/ai/index.js +18 -0
- package/dist/core/ai/llm-client.d.ts +45 -0
- package/dist/core/ai/llm-client.js +7 -0
- package/dist/core/ai/mock-provider.d.ts +49 -0
- package/dist/core/ai/mock-provider.js +121 -0
- package/dist/core/ai/ollama-provider.d.ts +78 -0
- package/dist/core/ai/ollama-provider.js +192 -0
- package/dist/core/ai/openai-provider.d.ts +48 -0
- package/dist/core/ai/openai-provider.js +188 -0
- package/dist/core/ai/provider-factory.d.ts +160 -0
- package/dist/core/ai/provider-factory.js +269 -0
- package/dist/core/auth/api-key-provider.d.ts +16 -0
- package/dist/core/auth/api-key-provider.js +63 -0
- package/dist/core/auth/aws-iam-provider.d.ts +35 -0
- package/dist/core/auth/aws-iam-provider.js +177 -0
- package/dist/core/auth/azure-ad-provider.d.ts +15 -0
- package/dist/core/auth/azure-ad-provider.js +99 -0
- package/dist/core/auth/basic-auth-provider.d.ts +26 -0
- package/dist/core/auth/basic-auth-provider.js +111 -0
- package/dist/core/auth/gcp-adc-provider.d.ts +27 -0
- package/dist/core/auth/gcp-adc-provider.js +126 -0
- package/dist/core/auth/index.d.ts +238 -0
- package/dist/core/auth/index.js +82 -0
- package/dist/core/auth/jwt-provider.d.ts +19 -0
- package/dist/core/auth/jwt-provider.js +160 -0
- package/dist/core/auth/manager.d.ts +84 -0
- package/dist/core/auth/manager.js +230 -0
- package/dist/core/auth/oauth2-provider.d.ts +17 -0
- package/dist/core/auth/oauth2-provider.js +114 -0
- package/dist/core/auth/totp-provider.d.ts +31 -0
- package/dist/core/auth/totp-provider.js +134 -0
- package/dist/core/auth/ui-login-provider.d.ts +26 -0
- package/dist/core/auth/ui-login-provider.js +198 -0
- package/dist/core/cache/index.d.ts +7 -0
- package/dist/core/cache/index.js +6 -0
- package/dist/core/cache/lru-cache.d.ts +203 -0
- package/dist/core/cache/lru-cache.js +397 -0
- package/dist/core/coverage/analyzer.d.ts +101 -0
- package/dist/core/coverage/analyzer.js +415 -0
- package/dist/core/coverage/collector.d.ts +74 -0
- package/dist/core/coverage/collector.js +459 -0
- package/dist/core/coverage/config.d.ts +37 -0
- package/dist/core/coverage/config.js +156 -0
- package/dist/core/coverage/index.d.ts +11 -0
- package/dist/core/coverage/index.js +15 -0
- package/dist/core/coverage/types.d.ts +267 -0
- package/dist/core/coverage/types.js +6 -0
- package/dist/core/coverage/vault.d.ts +95 -0
- package/dist/core/coverage/vault.js +405 -0
- package/dist/core/dashboard/assets.d.ts +6 -0
- package/dist/core/dashboard/assets.js +690 -0
- package/dist/core/dashboard/index.d.ts +6 -0
- package/dist/core/dashboard/index.js +5 -0
- package/dist/core/dashboard/server.d.ts +72 -0
- package/dist/core/dashboard/server.js +354 -0
- package/dist/core/dashboard/types.d.ts +70 -0
- package/dist/core/dashboard/types.js +5 -0
- package/dist/core/discoverer/index.d.ts +115 -0
- package/dist/core/discoverer/index.js +250 -0
- package/dist/core/flakiness/index.d.ts +228 -0
- package/dist/core/flakiness/index.js +384 -0
- package/dist/core/generation/code-formatter.d.ts +111 -0
- package/dist/core/generation/code-formatter.js +307 -0
- package/dist/core/generation/code-generator.d.ts +144 -0
- package/dist/core/generation/code-generator.js +293 -0
- package/dist/core/generation/generator.d.ts +40 -0
- package/dist/core/generation/generator.js +76 -0
- package/dist/core/generation/index.d.ts +30 -0
- package/dist/core/generation/index.js +28 -0
- package/dist/core/generation/pack-generator.d.ts +107 -0
- package/dist/core/generation/pack-generator.js +416 -0
- package/dist/core/generation/prompt-builder.d.ts +132 -0
- package/dist/core/generation/prompt-builder.js +672 -0
- package/dist/core/generation/source-analyzer.d.ts +213 -0
- package/dist/core/generation/source-analyzer.js +657 -0
- package/dist/core/generation/test-optimizer.d.ts +117 -0
- package/dist/core/generation/test-optimizer.js +328 -0
- package/dist/core/generation/types.d.ts +214 -0
- package/dist/core/generation/types.js +4 -0
- package/dist/core/index.d.ts +23 -1
- package/dist/core/index.js +39 -0
- package/dist/core/pack/validator.js +31 -1
- package/dist/core/pack-v2/index.d.ts +9 -0
- package/dist/core/pack-v2/index.js +8 -0
- package/dist/core/pack-v2/loader.d.ts +62 -0
- package/dist/core/pack-v2/loader.js +231 -0
- package/dist/core/pack-v2/migrator.d.ts +56 -0
- package/dist/core/pack-v2/migrator.js +455 -0
- package/dist/core/pack-v2/validator.d.ts +61 -0
- package/dist/core/pack-v2/validator.js +577 -0
- package/dist/core/regression/detector.d.ts +107 -0
- package/dist/core/regression/detector.js +497 -0
- package/dist/core/regression/index.d.ts +9 -0
- package/dist/core/regression/index.js +11 -0
- package/dist/core/regression/trend-analyzer.d.ts +102 -0
- package/dist/core/regression/trend-analyzer.js +345 -0
- package/dist/core/regression/types.d.ts +222 -0
- package/dist/core/regression/types.js +7 -0
- package/dist/core/regression/vault.d.ts +87 -0
- package/dist/core/regression/vault.js +289 -0
- package/dist/core/repair/engine/fixer.d.ts +24 -0
- package/dist/core/repair/engine/fixer.js +226 -0
- package/dist/core/repair/engine/suggestion-engine.d.ts +18 -0
- package/dist/core/repair/engine/suggestion-engine.js +187 -0
- package/dist/core/repair/index.d.ts +10 -0
- package/dist/core/repair/index.js +13 -0
- package/dist/core/repair/repairer.d.ts +90 -0
- package/dist/core/repair/repairer.js +284 -0
- package/dist/core/repair/types.d.ts +91 -0
- package/dist/core/repair/types.js +6 -0
- package/dist/core/repair/utils/error-analyzer.d.ts +28 -0
- package/dist/core/repair/utils/error-analyzer.js +264 -0
- package/dist/core/retry/flakiness-integration.d.ts +60 -0
- package/dist/core/retry/flakiness-integration.js +228 -0
- package/dist/core/retry/index.d.ts +14 -0
- package/dist/core/retry/index.js +16 -0
- package/dist/core/retry/retry-engine.d.ts +80 -0
- package/dist/core/retry/retry-engine.js +296 -0
- package/dist/core/retry/types.d.ts +178 -0
- package/dist/core/retry/types.js +52 -0
- package/dist/core/retry/vault.d.ts +77 -0
- package/dist/core/retry/vault.js +304 -0
- package/dist/core/runner/e2e-helpers.d.ts +102 -0
- package/dist/core/runner/e2e-helpers.js +153 -0
- package/dist/core/runner/phase3-runner.d.ts +101 -2
- package/dist/core/runner/phase3-runner.js +559 -24
- package/dist/core/self-healing/assertion-healer.d.ts +97 -0
- package/dist/core/self-healing/assertion-healer.js +371 -0
- package/dist/core/self-healing/engine.d.ts +122 -0
- package/dist/core/self-healing/engine.js +538 -0
- package/dist/core/self-healing/index.d.ts +10 -0
- package/dist/core/self-healing/index.js +11 -0
- package/dist/core/self-healing/selector-healer.d.ts +103 -0
- package/dist/core/self-healing/selector-healer.js +372 -0
- package/dist/core/self-healing/types.d.ts +152 -0
- package/dist/core/self-healing/types.js +6 -0
- package/dist/core/slo/config.d.ts +107 -0
- package/dist/core/slo/config.js +360 -0
- package/dist/core/slo/index.d.ts +11 -0
- package/dist/core/slo/index.js +15 -0
- package/dist/core/slo/sli-calculator.d.ts +92 -0
- package/dist/core/slo/sli-calculator.js +364 -0
- package/dist/core/slo/slo-tracker.d.ts +148 -0
- package/dist/core/slo/slo-tracker.js +379 -0
- package/dist/core/slo/types.d.ts +281 -0
- package/dist/core/slo/types.js +7 -0
- package/dist/core/slo/vault.d.ts +102 -0
- package/dist/core/slo/vault.js +427 -0
- package/dist/core/tui/index.d.ts +7 -0
- package/dist/core/tui/index.js +6 -0
- package/dist/core/tui/monitor.d.ts +92 -0
- package/dist/core/tui/monitor.js +271 -0
- package/dist/core/tui/renderer.d.ts +33 -0
- package/dist/core/tui/renderer.js +218 -0
- package/dist/core/tui/types.d.ts +63 -0
- package/dist/core/tui/types.js +5 -0
- package/dist/core/types/pack-v2.d.ts +425 -0
- package/dist/core/types/pack-v2.js +8 -0
- package/dist/core/vault/index.d.ts +116 -0
- package/dist/core/vault/index.js +400 -5
- package/dist/core/watch/index.d.ts +7 -0
- package/dist/core/watch/index.js +6 -0
- package/dist/core/watch/watch-mode.d.ts +213 -0
- package/dist/core/watch/watch-mode.js +389 -0
- package/dist/index.js +68 -68
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +136 -0
- package/package.json +5 -1
- package/dist/core/adapters/playwright-api.d.ts +0 -82
- package/dist/core/adapters/playwright-api.js +0 -264
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Source Code Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes source code to extract test requirements and generate test specifications.
|
|
5
|
+
* Supports TypeScript, JavaScript, Python, and Go source code.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const analyzer = new SourceAnalyzer();
|
|
10
|
+
* const spec = await analyzer.analyzeFile('./src/userService.ts');
|
|
11
|
+
* const tests = await analyzer.generateTests(spec);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
15
|
+
import { join, extname, basename } from 'path';
|
|
16
|
+
/**
|
|
17
|
+
* Source Analyzer
|
|
18
|
+
*
|
|
19
|
+
* Analyzes source code to extract testable elements and suggest tests.
|
|
20
|
+
*/
|
|
21
|
+
export class SourceAnalyzer {
|
|
22
|
+
options;
|
|
23
|
+
defaultOptions = {
|
|
24
|
+
includePrivate: false,
|
|
25
|
+
maxComplexity: 10,
|
|
26
|
+
exclude: ['node_modules', 'dist', 'build', '.git', 'coverage'],
|
|
27
|
+
followImports: false
|
|
28
|
+
};
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.options = { ...this.defaultOptions, ...options };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Analyze a single source file
|
|
34
|
+
*/
|
|
35
|
+
analyzeFile(filePath) {
|
|
36
|
+
if (!existsSync(filePath)) {
|
|
37
|
+
throw new Error(`Source file not found: ${filePath}`);
|
|
38
|
+
}
|
|
39
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
40
|
+
const ext = extname(filePath);
|
|
41
|
+
const language = this.getLanguage(ext);
|
|
42
|
+
const analysis = {
|
|
43
|
+
filePath,
|
|
44
|
+
language,
|
|
45
|
+
exports: [],
|
|
46
|
+
functions: [],
|
|
47
|
+
classes: [],
|
|
48
|
+
imports: [],
|
|
49
|
+
hasTests: false,
|
|
50
|
+
complexity: 0,
|
|
51
|
+
linesOfCode: content.split('\n').length
|
|
52
|
+
};
|
|
53
|
+
switch (language) {
|
|
54
|
+
case 'typescript':
|
|
55
|
+
case 'javascript':
|
|
56
|
+
this.analyzeTypeScriptScript(content, analysis);
|
|
57
|
+
break;
|
|
58
|
+
case 'python':
|
|
59
|
+
this.analyzePython(content, analysis);
|
|
60
|
+
break;
|
|
61
|
+
case 'go':
|
|
62
|
+
this.analyzeGo(content, analysis);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
// Calculate overall complexity
|
|
66
|
+
analysis.complexity = this.calculateComplexity(analysis);
|
|
67
|
+
// Check if test file exists
|
|
68
|
+
analysis.testFilePath = this.findTestFile(filePath);
|
|
69
|
+
analysis.hasTests = analysis.testFilePath !== undefined;
|
|
70
|
+
return analysis;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Analyze a directory recursively
|
|
74
|
+
*/
|
|
75
|
+
analyzeDirectory(dirPath) {
|
|
76
|
+
const results = [];
|
|
77
|
+
if (!existsSync(dirPath)) {
|
|
78
|
+
throw new Error(`Directory not found: ${dirPath}`);
|
|
79
|
+
}
|
|
80
|
+
this.traverseDirectory(dirPath, (filePath) => {
|
|
81
|
+
try {
|
|
82
|
+
const ext = extname(filePath);
|
|
83
|
+
if (this.isSourceFile(ext)) {
|
|
84
|
+
results.push(this.analyzeFile(filePath));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
// Skip files that fail to analyze
|
|
89
|
+
console.warn(`Warning: Failed to analyze ${filePath}:`, error);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generate test specification from analysis
|
|
96
|
+
*/
|
|
97
|
+
generateSpec(analysis) {
|
|
98
|
+
const functionsToTest = analysis.exports.filter(e => {
|
|
99
|
+
const fn = analysis.functions.find(f => f.name === e);
|
|
100
|
+
return fn && fn.isPublic && fn.complexity <= this.options.maxComplexity;
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
type: 'unit',
|
|
104
|
+
name: this.generateTestName(analysis.filePath),
|
|
105
|
+
source: {
|
|
106
|
+
language: analysis.language,
|
|
107
|
+
filePath: analysis.filePath,
|
|
108
|
+
exports: functionsToTest
|
|
109
|
+
},
|
|
110
|
+
framework: this.recommendFramework(analysis.language)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Generate test suggestions
|
|
115
|
+
*/
|
|
116
|
+
generateSuggestions(analysis) {
|
|
117
|
+
const suggestions = [];
|
|
118
|
+
// Suggest unit tests for each public function
|
|
119
|
+
for (const fn of analysis.functions) {
|
|
120
|
+
if (!fn.isPublic)
|
|
121
|
+
continue;
|
|
122
|
+
const complexity = fn.complexity;
|
|
123
|
+
let priority = 'low';
|
|
124
|
+
if (complexity > this.options.maxComplexity) {
|
|
125
|
+
priority = 'high';
|
|
126
|
+
}
|
|
127
|
+
else if (complexity > 5) {
|
|
128
|
+
priority = 'medium';
|
|
129
|
+
}
|
|
130
|
+
suggestions.push({
|
|
131
|
+
type: 'unit',
|
|
132
|
+
priority,
|
|
133
|
+
description: `Test function ${fn.name}`,
|
|
134
|
+
functionName: fn.name,
|
|
135
|
+
assertions: this.generateAssertions(fn),
|
|
136
|
+
mocks: this.generateMocks(fn, analysis.imports)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// Suggest integration tests for classes with multiple methods
|
|
140
|
+
for (const cls of analysis.classes) {
|
|
141
|
+
if (cls.methods.length >= 3) {
|
|
142
|
+
suggestions.push({
|
|
143
|
+
type: 'integration',
|
|
144
|
+
priority: 'medium',
|
|
145
|
+
description: `Test class ${cls.name} interactions`,
|
|
146
|
+
assertions: ['class instance created', 'methods work together'],
|
|
147
|
+
mocks: this.generateMocksForClass(cls, analysis.imports)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Suggest E2E tests if external APIs are used
|
|
152
|
+
const hasExternalCalls = analysis.imports.some(i => i.isExternal && this.isHttpModule(i.module));
|
|
153
|
+
if (hasExternalCalls) {
|
|
154
|
+
suggestions.push({
|
|
155
|
+
type: 'e2e',
|
|
156
|
+
priority: 'low',
|
|
157
|
+
description: 'Test external API integrations',
|
|
158
|
+
assertions: ['API calls succeed', 'error handling works'],
|
|
159
|
+
mocks: []
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return suggestions.sort((a, b) => {
|
|
163
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
164
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Generate API spec from Express/HTTP handlers
|
|
169
|
+
*/
|
|
170
|
+
generateApiSpec(analysis) {
|
|
171
|
+
const endpoints = [];
|
|
172
|
+
const baseUrl = this.extractBaseUrl(analysis);
|
|
173
|
+
for (const fn of analysis.functions) {
|
|
174
|
+
const endpoint = this.extractEndpoint(fn, analysis);
|
|
175
|
+
if (endpoint) {
|
|
176
|
+
endpoints.push(endpoint);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const cls of analysis.classes) {
|
|
180
|
+
for (const method of cls.methods) {
|
|
181
|
+
const endpoint = this.extractEndpoint(method, analysis);
|
|
182
|
+
if (endpoint) {
|
|
183
|
+
endpoints.push(endpoint);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (endpoints.length === 0) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
type: 'api',
|
|
192
|
+
name: `${basename(analysis.filePath)} API`,
|
|
193
|
+
baseUrl,
|
|
194
|
+
endpoints
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Analyze TypeScript/JavaScript code
|
|
199
|
+
*/
|
|
200
|
+
analyzeTypeScriptScript(content, analysis) {
|
|
201
|
+
const lines = content.split('\n');
|
|
202
|
+
// Extract exports
|
|
203
|
+
const exportRegex = /export\s+(?:async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)\s*=>|function))/g;
|
|
204
|
+
for (const match of content.matchAll(exportRegex)) {
|
|
205
|
+
const name = match[1] || match[2];
|
|
206
|
+
if (name && !analysis.exports.includes(name)) {
|
|
207
|
+
analysis.exports.push(name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Extract class declarations
|
|
211
|
+
const classRegex = /export\s+class\s+(\w+)(?:\s+extends\s+(\w+))?\s*\{/g;
|
|
212
|
+
for (const match of content.matchAll(classRegex)) {
|
|
213
|
+
const className = match[1];
|
|
214
|
+
const extendsClass = match[2];
|
|
215
|
+
const classInfo = {
|
|
216
|
+
name: className,
|
|
217
|
+
methods: [],
|
|
218
|
+
properties: [],
|
|
219
|
+
extends: extendsClass,
|
|
220
|
+
lineNumber: this.findLineNumber(content, match.index)
|
|
221
|
+
};
|
|
222
|
+
// Extract class methods
|
|
223
|
+
const classContent = this.extractClassContent(content, match.index);
|
|
224
|
+
const methodRegex = /(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*(\w+))?/g;
|
|
225
|
+
for (const methodMatch of classContent.matchAll(methodRegex)) {
|
|
226
|
+
if (methodMatch[1] === 'constructor')
|
|
227
|
+
continue;
|
|
228
|
+
classInfo.methods.push({
|
|
229
|
+
name: methodMatch[1],
|
|
230
|
+
params: this.parseParams(methodMatch[2]),
|
|
231
|
+
returnType: methodMatch[3],
|
|
232
|
+
isAsync: classContent.includes('async') && classContent.indexOf('async') < classContent.indexOf(methodMatch[1]),
|
|
233
|
+
isPublic: !classContent.slice(0, methodMatch.index).includes('private'),
|
|
234
|
+
lineNumber: analysis.linesOfCode,
|
|
235
|
+
complexity: this.estimateFunctionComplexity(classContent)
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
analysis.classes.push(classInfo);
|
|
239
|
+
}
|
|
240
|
+
// Extract function declarations
|
|
241
|
+
const fnRegex = /((?:export\s+)?(?:async\s+)?function\s+\w+\s*\([^)]*\)(?:\s*:\s*\w+)?)/g;
|
|
242
|
+
for (const match of content.matchAll(fnRegex)) {
|
|
243
|
+
const fullMatch = match[1];
|
|
244
|
+
const fnNameMatch = /function\s+(\w+)/.exec(fullMatch);
|
|
245
|
+
const paramsMatch = /\(([^)]*)\)/.exec(fullMatch);
|
|
246
|
+
const returnMatch = /:\s*(\w+)/.exec(fullMatch);
|
|
247
|
+
if (fnNameMatch) {
|
|
248
|
+
// Get more context for complexity estimation (function body)
|
|
249
|
+
const fnStart = match.index;
|
|
250
|
+
const fnEnd = Math.min(fnStart + 1000, content.length); // Look ahead up to 1000 chars
|
|
251
|
+
const fnContext = content.slice(fnStart, fnEnd);
|
|
252
|
+
analysis.functions.push({
|
|
253
|
+
name: fnNameMatch[1],
|
|
254
|
+
params: paramsMatch ? this.parseParams(paramsMatch[1]) : [],
|
|
255
|
+
returnType: returnMatch ? returnMatch[1] : undefined,
|
|
256
|
+
isAsync: fullMatch.includes('async'),
|
|
257
|
+
isPublic: this.options.includePrivate || !fullMatch.includes('private'),
|
|
258
|
+
lineNumber: this.findLineNumber(content, match.index),
|
|
259
|
+
complexity: this.estimateFunctionComplexity(fnContext)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Extract imports
|
|
264
|
+
const importRegex = /import\s+(?:(\{[^}]+\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
265
|
+
for (const match of content.matchAll(importRegex)) {
|
|
266
|
+
const module = match[2];
|
|
267
|
+
const imports = match[1] ? match[1].replace(/[{}*\s]/g, '').split(',').filter(Boolean) : [module.split('/').pop() || module];
|
|
268
|
+
analysis.imports.push({
|
|
269
|
+
module,
|
|
270
|
+
imports,
|
|
271
|
+
isExternal: this.isExternalModule(module)
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
// Check for require statements
|
|
275
|
+
const requireRegex = /(?:const|let|var)\s+(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)/g;
|
|
276
|
+
for (const match of content.matchAll(requireRegex)) {
|
|
277
|
+
analysis.imports.push({
|
|
278
|
+
module: match[2],
|
|
279
|
+
imports: [match[1]],
|
|
280
|
+
isExternal: this.isExternalModule(match[2])
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Analyze Python code
|
|
286
|
+
*/
|
|
287
|
+
analyzePython(content, analysis) {
|
|
288
|
+
// Extract class definitions
|
|
289
|
+
const classRegex = /^class\s+(\w+)(?:\(([^)]+)\))?\s*:/gm;
|
|
290
|
+
for (const match of content.matchAll(classRegex)) {
|
|
291
|
+
const className = match[1];
|
|
292
|
+
const baseClass = match[2];
|
|
293
|
+
analysis.classes.push({
|
|
294
|
+
name: className,
|
|
295
|
+
methods: [],
|
|
296
|
+
properties: [],
|
|
297
|
+
extends: baseClass,
|
|
298
|
+
lineNumber: this.findLineNumber(content, match.index)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
// Extract function definitions
|
|
302
|
+
const fnRegex = /^def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?:/gm;
|
|
303
|
+
for (const match of content.matchAll(fnRegex)) {
|
|
304
|
+
const isPrivate = match[1].startsWith('_');
|
|
305
|
+
analysis.functions.push({
|
|
306
|
+
name: match[1],
|
|
307
|
+
params: this.parseParams(match[2]),
|
|
308
|
+
returnType: match[3],
|
|
309
|
+
isAsync: content.slice(0, match.index).trimEnd().endsWith('async'),
|
|
310
|
+
isPublic: this.options.includePrivate || !isPrivate,
|
|
311
|
+
lineNumber: this.findLineNumber(content, match.index),
|
|
312
|
+
complexity: this.estimateFunctionComplexity(content.slice(match.index, Math.min(match.index + 500, content.length)))
|
|
313
|
+
});
|
|
314
|
+
// Top-level functions are exports
|
|
315
|
+
if (!isPrivate && !content.slice(0, match.index).includes('class ')) {
|
|
316
|
+
analysis.exports.push(match[1]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Extract imports
|
|
320
|
+
const importRegex = /^(?:from\s+(\S+)\s+)?import\s+(.+)/gm;
|
|
321
|
+
for (const match of content.matchAll(importRegex)) {
|
|
322
|
+
const module = match[1] || match[2].split('.')[0];
|
|
323
|
+
const imports = match[1] ? match[2].split(',').map(s => s.trim()) : [module];
|
|
324
|
+
analysis.imports.push({
|
|
325
|
+
module,
|
|
326
|
+
imports,
|
|
327
|
+
isExternal: !module.startsWith('.')
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Analyze Go code
|
|
333
|
+
*/
|
|
334
|
+
analyzeGo(content, analysis) {
|
|
335
|
+
// Extract function definitions
|
|
336
|
+
const fnRegex = /func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*\(([^)]*)\))?/g;
|
|
337
|
+
for (const match of content.matchAll(fnRegex)) {
|
|
338
|
+
const isPrivate = match[1] && match[1][0] === match[1][0].toLowerCase();
|
|
339
|
+
analysis.exports.push(match[1]);
|
|
340
|
+
analysis.functions.push({
|
|
341
|
+
name: match[1],
|
|
342
|
+
params: this.parseParams(match[2]),
|
|
343
|
+
returnType: match[3],
|
|
344
|
+
isAsync: false,
|
|
345
|
+
isPublic: this.options.includePrivate || !isPrivate,
|
|
346
|
+
lineNumber: this.findLineNumber(content, match.index),
|
|
347
|
+
complexity: this.estimateFunctionComplexity(content.slice(match.index, match.index + 500))
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// Extract type definitions (similar to classes)
|
|
351
|
+
const typeRegex = /type\s+(\w+)\s+struct\s*\{([^}]*)\}/g;
|
|
352
|
+
for (const match of content.matchAll(typeRegex)) {
|
|
353
|
+
analysis.classes.push({
|
|
354
|
+
name: match[1],
|
|
355
|
+
methods: [],
|
|
356
|
+
properties: match[2].split('\n').map(l => l.trim()).filter(Boolean),
|
|
357
|
+
lineNumber: this.findLineNumber(content, match.index)
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
// Extract imports
|
|
361
|
+
const importRegex = /import\s+(?:\(\s*)?["']([^"']+)["']/g;
|
|
362
|
+
for (const match of content.matchAll(importRegex)) {
|
|
363
|
+
analysis.imports.push({
|
|
364
|
+
module: match[1],
|
|
365
|
+
imports: [],
|
|
366
|
+
isExternal: !match[1].startsWith('.')
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Get language from file extension
|
|
372
|
+
*/
|
|
373
|
+
getLanguage(ext) {
|
|
374
|
+
const langMap = {
|
|
375
|
+
'.ts': 'typescript',
|
|
376
|
+
'.tsx': 'typescript',
|
|
377
|
+
'.js': 'javascript',
|
|
378
|
+
'.jsx': 'javascript',
|
|
379
|
+
'.py': 'python',
|
|
380
|
+
'.go': 'go'
|
|
381
|
+
};
|
|
382
|
+
return langMap[ext] || 'typescript';
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Check if file is a source file
|
|
386
|
+
*/
|
|
387
|
+
isSourceFile(ext) {
|
|
388
|
+
return ['.ts', '.tsx', '.js', '.jsx', '.py', '.go'].includes(ext);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Traverse directory recursively
|
|
392
|
+
*/
|
|
393
|
+
traverseDirectory(dir, callback) {
|
|
394
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
const fullPath = join(dir, entry.name);
|
|
397
|
+
// Skip excluded directories
|
|
398
|
+
if (this.options.exclude.some(pattern => fullPath.includes(pattern))) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (entry.isDirectory()) {
|
|
402
|
+
this.traverseDirectory(fullPath, callback);
|
|
403
|
+
}
|
|
404
|
+
else if (entry.isFile()) {
|
|
405
|
+
callback(fullPath);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Find test file for a source file
|
|
411
|
+
*/
|
|
412
|
+
findTestFile(filePath) {
|
|
413
|
+
const dir = join(filePath, '..');
|
|
414
|
+
const base = basename(filePath, extname(filePath));
|
|
415
|
+
const ext = extname(filePath);
|
|
416
|
+
const testPatterns = [
|
|
417
|
+
join(dir, `${base}.test.ts`),
|
|
418
|
+
join(dir, `${base}.spec.ts`),
|
|
419
|
+
join(dir, `${base}.test.js`),
|
|
420
|
+
join(dir, `${base}.spec.js`),
|
|
421
|
+
join(dir, '__tests__', `${base}.test.ts`),
|
|
422
|
+
join(dir, '__tests__', `${base}.spec.ts`),
|
|
423
|
+
join(dir, 'tests', `${base}.test.ts`)
|
|
424
|
+
];
|
|
425
|
+
for (const pattern of testPatterns) {
|
|
426
|
+
if (existsSync(pattern)) {
|
|
427
|
+
return pattern;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Calculate overall complexity score
|
|
434
|
+
*/
|
|
435
|
+
calculateComplexity(analysis) {
|
|
436
|
+
let complexity = 0;
|
|
437
|
+
for (const fn of analysis.functions) {
|
|
438
|
+
complexity += fn.complexity;
|
|
439
|
+
}
|
|
440
|
+
for (const cls of analysis.classes) {
|
|
441
|
+
for (const method of cls.methods) {
|
|
442
|
+
complexity += method.complexity;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return complexity;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Estimate function complexity
|
|
449
|
+
*/
|
|
450
|
+
estimateFunctionComplexity(code) {
|
|
451
|
+
let complexity = 1; // Base complexity
|
|
452
|
+
// Count decision points
|
|
453
|
+
const decisionPatterns = [
|
|
454
|
+
/\bif\b/g,
|
|
455
|
+
/\belse\b/g,
|
|
456
|
+
/\bfor\b/g,
|
|
457
|
+
/\bwhile\b/g,
|
|
458
|
+
/\bswitch\b/g,
|
|
459
|
+
/\bcase\b/g,
|
|
460
|
+
/\bcatch\b/g,
|
|
461
|
+
/\?\s*:/g, // ternary
|
|
462
|
+
/\|\|/g, // logical OR
|
|
463
|
+
/&&/g // logical AND
|
|
464
|
+
];
|
|
465
|
+
for (const pattern of decisionPatterns) {
|
|
466
|
+
const matches = code.match(pattern);
|
|
467
|
+
if (matches) {
|
|
468
|
+
complexity += matches.length;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return complexity;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Parse function parameters
|
|
475
|
+
*/
|
|
476
|
+
parseParams(paramsStr) {
|
|
477
|
+
if (!paramsStr.trim())
|
|
478
|
+
return [];
|
|
479
|
+
return paramsStr.split(',').map(p => p.trim()).filter(Boolean);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Extract class content
|
|
483
|
+
*/
|
|
484
|
+
extractClassContent(content, startIndex) {
|
|
485
|
+
let depth = 0;
|
|
486
|
+
let inClass = false;
|
|
487
|
+
let result = '';
|
|
488
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
489
|
+
const char = content[i];
|
|
490
|
+
if (char === '{') {
|
|
491
|
+
depth++;
|
|
492
|
+
inClass = true;
|
|
493
|
+
}
|
|
494
|
+
else if (char === '}') {
|
|
495
|
+
depth--;
|
|
496
|
+
if (inClass && depth === 0) {
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (inClass) {
|
|
501
|
+
result += char;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Find line number for a position in content
|
|
508
|
+
*/
|
|
509
|
+
findLineNumber(content, index) {
|
|
510
|
+
return content.slice(0, index).split('\n').length;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Generate test name from file path
|
|
514
|
+
*/
|
|
515
|
+
generateTestName(filePath) {
|
|
516
|
+
const base = basename(filePath, extname(filePath));
|
|
517
|
+
return base.replace(/[^a-zA-Z0-9]/g, ' ') + ' Tests';
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Recommend testing framework based on language
|
|
521
|
+
*/
|
|
522
|
+
recommendFramework(language) {
|
|
523
|
+
const frameworkMap = {
|
|
524
|
+
typescript: 'vitest',
|
|
525
|
+
javascript: 'vitest',
|
|
526
|
+
python: 'pytest',
|
|
527
|
+
go: 'go test'
|
|
528
|
+
};
|
|
529
|
+
return frameworkMap[language] || 'vitest';
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Generate assertions for a function
|
|
533
|
+
*/
|
|
534
|
+
generateAssertions(fn) {
|
|
535
|
+
const assertions = [];
|
|
536
|
+
if (fn.isAsync) {
|
|
537
|
+
assertions.push('function resolves', 'function completes without timeout');
|
|
538
|
+
}
|
|
539
|
+
if (fn.returnType?.toLowerCase().includes('void') || fn.params.length === 0) {
|
|
540
|
+
assertions.push('function executes without error');
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
assertions.push('function returns expected value');
|
|
544
|
+
}
|
|
545
|
+
if (fn.params.length > 0) {
|
|
546
|
+
assertions.push('function handles invalid input');
|
|
547
|
+
}
|
|
548
|
+
return assertions;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Generate mocks for a function
|
|
552
|
+
*/
|
|
553
|
+
generateMocks(fn, imports) {
|
|
554
|
+
const mocks = [];
|
|
555
|
+
for (const imp of imports) {
|
|
556
|
+
if (imp.isExternal && this.isHttpModule(imp.module)) {
|
|
557
|
+
mocks.push(imp.module);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return mocks;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Generate mocks for a class
|
|
564
|
+
*/
|
|
565
|
+
generateMocksForClass(cls, imports) {
|
|
566
|
+
const mocks = [];
|
|
567
|
+
for (const imp of imports) {
|
|
568
|
+
if (imp.isExternal) {
|
|
569
|
+
mocks.push(imp.module);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return mocks;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Check if module is an HTTP module
|
|
576
|
+
*/
|
|
577
|
+
isHttpModule(module) {
|
|
578
|
+
const httpModules = ['http', 'https', 'axios', 'fetch', 'node-fetch', 'request', 'supertest'];
|
|
579
|
+
return httpModules.some(m => module.toLowerCase().includes(m));
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Check if module is external
|
|
583
|
+
*/
|
|
584
|
+
isExternalModule(module) {
|
|
585
|
+
return !module.startsWith('.') && !module.startsWith('/');
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Extract base URL from imports
|
|
589
|
+
*/
|
|
590
|
+
extractBaseUrl(analysis) {
|
|
591
|
+
// Try to find a base URL in constants or config
|
|
592
|
+
const urlConstants = analysis.imports.filter(i => i.module.toLowerCase().includes('config') ||
|
|
593
|
+
i.module.toLowerCase().includes('constants'));
|
|
594
|
+
if (urlConstants.length > 0) {
|
|
595
|
+
return 'https://api.example.com'; // Placeholder
|
|
596
|
+
}
|
|
597
|
+
return 'https://localhost:3000';
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Extract API endpoint from function
|
|
601
|
+
*/
|
|
602
|
+
extractEndpoint(fn, analysis) {
|
|
603
|
+
const name = fn.name.toLowerCase();
|
|
604
|
+
// Check for HTTP method prefixes
|
|
605
|
+
const httpMethods = ['get', 'post', 'put', 'patch', 'delete'];
|
|
606
|
+
let method;
|
|
607
|
+
let path = `/${fn.name}`;
|
|
608
|
+
for (const httpMethod of httpMethods) {
|
|
609
|
+
if (name.startsWith(httpMethod)) {
|
|
610
|
+
method = httpMethod.toUpperCase();
|
|
611
|
+
path = `/${name.slice(httpMethod.length)}`;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// Check for Express/Fastify patterns
|
|
616
|
+
if (fn.params.some(p => p.includes('req') || p.includes('request') || p.includes('res') || p.includes('response'))) {
|
|
617
|
+
return {
|
|
618
|
+
method: method || 'GET',
|
|
619
|
+
path,
|
|
620
|
+
description: fn.name,
|
|
621
|
+
expectations: [{ status: 200 }]
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Get analysis as GenerationSource
|
|
628
|
+
*/
|
|
629
|
+
toGenerationSource(analysis) {
|
|
630
|
+
return {
|
|
631
|
+
type: 'code',
|
|
632
|
+
code: readFileSync(analysis.filePath, 'utf-8'),
|
|
633
|
+
language: analysis.language
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Convenience function to analyze a source file
|
|
639
|
+
*/
|
|
640
|
+
export function analyzeSourceFile(filePath, options) {
|
|
641
|
+
const analyzer = new SourceAnalyzer(options);
|
|
642
|
+
return analyzer.analyzeFile(filePath);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Convenience function to analyze a directory
|
|
646
|
+
*/
|
|
647
|
+
export function analyzeSourceDirectory(dirPath, options) {
|
|
648
|
+
const analyzer = new SourceAnalyzer(options);
|
|
649
|
+
return analyzer.analyzeDirectory(dirPath);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Convenience function to generate test spec from analysis
|
|
653
|
+
*/
|
|
654
|
+
export function generateTestSpec(analysis) {
|
|
655
|
+
const analyzer = new SourceAnalyzer();
|
|
656
|
+
return analyzer.generateSpec(analysis);
|
|
657
|
+
}
|