@archznn/xavva 3.0.0 → 3.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.
- package/README.md +79 -1
- package/package.json +1 -1
- package/src/commands/ConfigCommand.ts +48 -0
- package/src/commands/DbCommand.ts +126 -0
- package/src/commands/DockerCommand.ts +122 -0
- package/src/commands/HelpCommand.ts +32 -0
- package/src/commands/HttpCommand.ts +134 -0
- package/src/commands/InitCommand.ts +70 -0
- package/src/commands/TestCommand.ts +63 -0
- package/src/di/container.ts +12 -0
- package/src/index.ts +6 -1
- package/src/services/DbService.ts +357 -0
- package/src/services/DockerService.ts +361 -0
- package/src/services/HttpService.ts +259 -0
- package/src/services/TestService.ts +326 -0
- package/src/types/config.ts +38 -0
- package/src/utils/config.ts +38 -2
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serviço de execução de testes
|
|
3
|
+
* Suporta Maven e Gradle com modo watch
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Logger } from "../utils/ui";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { watch, type FSWatcher } from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
|
|
12
|
+
export interface TestOptions {
|
|
13
|
+
watch?: boolean;
|
|
14
|
+
coverage?: boolean;
|
|
15
|
+
filter?: string;
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
failFast?: boolean;
|
|
18
|
+
parallel?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TestResult {
|
|
22
|
+
success: boolean;
|
|
23
|
+
totalTests: number;
|
|
24
|
+
passed: number;
|
|
25
|
+
failed: number;
|
|
26
|
+
skipped: number;
|
|
27
|
+
duration: number;
|
|
28
|
+
failures: TestFailure[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TestFailure {
|
|
32
|
+
className: string;
|
|
33
|
+
methodName: string;
|
|
34
|
+
message: string;
|
|
35
|
+
stackTrace?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class TestService {
|
|
39
|
+
private buildTool: "maven" | "gradle";
|
|
40
|
+
private watcher: FSWatcher | null = null;
|
|
41
|
+
private isRunning = false;
|
|
42
|
+
|
|
43
|
+
constructor(buildTool: "maven" | "gradle") {
|
|
44
|
+
this.buildTool = buildTool;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async runTests(options: TestOptions = {}): Promise<TestResult> {
|
|
48
|
+
if (this.isRunning) {
|
|
49
|
+
Logger.warn("Test execution already in progress...");
|
|
50
|
+
return this.createEmptyResult();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.isRunning = true;
|
|
54
|
+
const startTime = Date.now();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
Logger.section("Running Tests");
|
|
58
|
+
|
|
59
|
+
if (options.filter) {
|
|
60
|
+
Logger.info("Filter", options.filter);
|
|
61
|
+
}
|
|
62
|
+
if (options.coverage) {
|
|
63
|
+
Logger.info("Coverage", "enabled");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const command = this.buildCommand(options);
|
|
67
|
+
const result = await this.executeTests(command, options.verbose);
|
|
68
|
+
|
|
69
|
+
result.duration = Date.now() - startTime;
|
|
70
|
+
this.printResults(result);
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
} finally {
|
|
74
|
+
this.isRunning = false;
|
|
75
|
+
Logger.endSection();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
startWatch(options: TestOptions = {}): void {
|
|
80
|
+
if (this.watcher) {
|
|
81
|
+
Logger.warn("Test watcher already running");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Logger.section("Test Watch Mode");
|
|
86
|
+
Logger.info("Watching", "src/test/**/*");
|
|
87
|
+
Logger.info("Press", "Ctrl+C to stop");
|
|
88
|
+
Logger.endSection();
|
|
89
|
+
|
|
90
|
+
// Run tests initially
|
|
91
|
+
this.runTests(options);
|
|
92
|
+
|
|
93
|
+
// Watch for changes
|
|
94
|
+
const testPath = path.join(process.cwd(), "src", "test");
|
|
95
|
+
const mainPath = path.join(process.cwd(), "src", "main");
|
|
96
|
+
|
|
97
|
+
this.watcher = watch(
|
|
98
|
+
[testPath, mainPath],
|
|
99
|
+
{ recursive: true },
|
|
100
|
+
(eventType, filename) => {
|
|
101
|
+
if (filename && this.isTestFile(filename)) {
|
|
102
|
+
Logger.watch(`Test file changed: ${filename}`);
|
|
103
|
+
this.debounceRun(options);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stopWatch(): void {
|
|
110
|
+
if (this.watcher) {
|
|
111
|
+
this.watcher.close();
|
|
112
|
+
this.watcher = null;
|
|
113
|
+
Logger.info("Test watcher stopped");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private debounceRun(options: TestOptions): void {
|
|
118
|
+
// Simple debounce
|
|
119
|
+
if (this.debounceTimer) {
|
|
120
|
+
clearTimeout(this.debounceTimer);
|
|
121
|
+
}
|
|
122
|
+
this.debounceTimer = setTimeout(() => {
|
|
123
|
+
this.runTests(options);
|
|
124
|
+
}, 500);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private debounceTimer: Timer | null = null;
|
|
128
|
+
|
|
129
|
+
private buildCommand(options: TestOptions): string[] {
|
|
130
|
+
if (this.buildTool === "maven") {
|
|
131
|
+
return this.buildMavenCommand(options);
|
|
132
|
+
} else {
|
|
133
|
+
return this.buildGradleCommand(options);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private buildMavenCommand(options: TestOptions): string[] {
|
|
138
|
+
const cmd = process.platform === "win32" ? "mvn.cmd" : "mvn";
|
|
139
|
+
const args: string[] = [cmd];
|
|
140
|
+
|
|
141
|
+
if (options.coverage) {
|
|
142
|
+
args.push("jacoco:prepare-agent");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
args.push("test");
|
|
146
|
+
|
|
147
|
+
if (options.coverage) {
|
|
148
|
+
args.push("jacoco:report");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (options.filter) {
|
|
152
|
+
// Maven surefire plugin syntax
|
|
153
|
+
args.push(`-Dtest=${options.filter}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (options.failFast) {
|
|
157
|
+
args.push("-Dsurefire.failIfNoSpecifiedTests=false");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (options.parallel) {
|
|
161
|
+
args.push("-Dsurefire.parallel=methods");
|
|
162
|
+
args.push("-Dsurefire.threadCount=4");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!options.verbose) {
|
|
166
|
+
args.push("-q");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return args;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private buildGradleCommand(options: TestOptions): string[] {
|
|
173
|
+
const cmd = process.platform === "win32" ? "gradle.bat" : "gradle";
|
|
174
|
+
const args: string[] = [cmd, "test"];
|
|
175
|
+
|
|
176
|
+
if (options.coverage) {
|
|
177
|
+
args.push("jacocoTestReport");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (options.filter) {
|
|
181
|
+
args.push(`--tests`, options.filter);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (options.failFast) {
|
|
185
|
+
args.push("--fail-fast");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (options.parallel) {
|
|
189
|
+
args.push("--parallel");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!options.verbose) {
|
|
193
|
+
args.push("-q");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return args;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private executeTests(command: string[], verbose: boolean = false): Promise<TestResult> {
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
const [cmd, ...args] = command;
|
|
202
|
+
const child = spawn(cmd, args, {
|
|
203
|
+
cwd: process.cwd(),
|
|
204
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
205
|
+
shell: process.platform === "win32"
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
let stdout = "";
|
|
209
|
+
let stderr = "";
|
|
210
|
+
|
|
211
|
+
if (!verbose) {
|
|
212
|
+
child.stdout?.on("data", (data) => {
|
|
213
|
+
stdout += data.toString();
|
|
214
|
+
});
|
|
215
|
+
child.stderr?.on("data", (data) => {
|
|
216
|
+
stderr += data.toString();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const spinner = verbose ? () => {} : Logger.spinner("Running tests");
|
|
221
|
+
|
|
222
|
+
child.on("close", (code) => {
|
|
223
|
+
spinner(code === 0);
|
|
224
|
+
const result = this.parseTestOutput(stdout + stderr, code === 0);
|
|
225
|
+
resolve(result);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
child.on("error", (error) => {
|
|
229
|
+
spinner(false);
|
|
230
|
+
Logger.error(`Failed to run tests: ${error.message}`);
|
|
231
|
+
resolve(this.createEmptyResult());
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private parseTestOutput(output: string, success: boolean): TestResult {
|
|
237
|
+
const result: TestResult = {
|
|
238
|
+
success,
|
|
239
|
+
totalTests: 0,
|
|
240
|
+
passed: 0,
|
|
241
|
+
failed: 0,
|
|
242
|
+
skipped: 0,
|
|
243
|
+
duration: 0,
|
|
244
|
+
failures: []
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (this.buildTool === "maven") {
|
|
248
|
+
// Parse Maven Surefire output
|
|
249
|
+
const testsRun = output.match(/Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)/);
|
|
250
|
+
if (testsRun) {
|
|
251
|
+
result.totalTests = parseInt(testsRun[1]);
|
|
252
|
+
result.failed = parseInt(testsRun[2]) + parseInt(testsRun[3]);
|
|
253
|
+
result.skipped = parseInt(testsRun[4]);
|
|
254
|
+
result.passed = result.totalTests - result.failed - result.skipped;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Parse failures
|
|
258
|
+
const failureMatches = output.matchAll(/\[ERROR\]\s+(\S+)\.(\S+)\s+\[(.+?)\]\s+(.+)/g);
|
|
259
|
+
for (const match of failureMatches) {
|
|
260
|
+
result.failures.push({
|
|
261
|
+
className: match[1],
|
|
262
|
+
methodName: match[2],
|
|
263
|
+
message: match[4]
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
// Parse Gradle output
|
|
268
|
+
const testsRun = output.match(/(\d+) tests completed,(\s*\d+ failed)?/);
|
|
269
|
+
if (testsRun) {
|
|
270
|
+
result.totalTests = parseInt(testsRun[1]);
|
|
271
|
+
const failed = testsRun[2] ? parseInt(testsRun[2].trim()) : 0;
|
|
272
|
+
result.failed = failed;
|
|
273
|
+
result.passed = result.totalTests - result.failed;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private printResults(result: TestResult): void {
|
|
281
|
+
Logger.divider();
|
|
282
|
+
|
|
283
|
+
if (result.success && result.failed === 0) {
|
|
284
|
+
Logger.success(`All tests passed! (${result.passed} tests)`);
|
|
285
|
+
} else if (result.failed > 0) {
|
|
286
|
+
Logger.error(`${result.failed} test(s) failed`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
Logger.info("Total", result.totalTests);
|
|
290
|
+
Logger.info("Passed", `${Logger.C.success}${result.passed}${Logger.C.reset}`);
|
|
291
|
+
Logger.info("Failed", result.failed > 0 ? `${Logger.C.error}${result.failed}${Logger.C.reset}` : "0");
|
|
292
|
+
Logger.info("Skipped", result.skipped);
|
|
293
|
+
Logger.info("Duration", `${(result.duration / 1000).toFixed(2)}s`);
|
|
294
|
+
|
|
295
|
+
if (result.failures.length > 0) {
|
|
296
|
+
Logger.divider();
|
|
297
|
+
Logger.section("Failures");
|
|
298
|
+
for (const failure of result.failures.slice(0, 5)) {
|
|
299
|
+
Logger.error(`${failure.className}.${failure.methodName}`);
|
|
300
|
+
Logger.dim(` ${failure.message}`);
|
|
301
|
+
}
|
|
302
|
+
if (result.failures.length > 5) {
|
|
303
|
+
Logger.dim(` ... and ${result.failures.length - 5} more`);
|
|
304
|
+
}
|
|
305
|
+
Logger.endSection();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private isTestFile(filename: string): boolean {
|
|
310
|
+
return filename.endsWith("Test.java") ||
|
|
311
|
+
filename.endsWith("IT.java") ||
|
|
312
|
+
filename.endsWith("Tests.java");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private createEmptyResult(): TestResult {
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
totalTests: 0,
|
|
319
|
+
passed: 0,
|
|
320
|
+
failed: 0,
|
|
321
|
+
skipped: 0,
|
|
322
|
+
duration: 0,
|
|
323
|
+
failures: []
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
export interface EnvironmentConfig {
|
|
2
|
+
port?: number;
|
|
3
|
+
profile?: string;
|
|
4
|
+
db?: {
|
|
5
|
+
url?: string;
|
|
6
|
+
username?: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
driver?: string;
|
|
9
|
+
};
|
|
10
|
+
tomcat?: Partial<TomcatConfig>;
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
export interface TomcatConfig {
|
|
2
15
|
path: string;
|
|
3
16
|
port: number;
|
|
@@ -24,6 +37,8 @@ export interface ProjectConfig {
|
|
|
24
37
|
encoding?: string;
|
|
25
38
|
war?: boolean;
|
|
26
39
|
cache?: boolean;
|
|
40
|
+
environment?: string;
|
|
41
|
+
environments?: Record<string, EnvironmentConfig>;
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
export interface AppConfig {
|
|
@@ -68,6 +83,29 @@ export interface CLIArguments {
|
|
|
68
83
|
"dry-run"?: boolean;
|
|
69
84
|
force?: boolean;
|
|
70
85
|
src?: string;
|
|
86
|
+
// Multi-environment
|
|
87
|
+
env?: string;
|
|
88
|
+
environment?: string;
|
|
89
|
+
// Test runner
|
|
90
|
+
coverage?: boolean;
|
|
91
|
+
"fail-fast"?: boolean;
|
|
92
|
+
parallel?: boolean;
|
|
93
|
+
// HTTP client
|
|
94
|
+
interactive?: boolean;
|
|
95
|
+
"base-url"?: string;
|
|
96
|
+
body?: string;
|
|
97
|
+
file?: string;
|
|
98
|
+
header?: string | string[];
|
|
99
|
+
"content-type"?: string;
|
|
100
|
+
accept?: string;
|
|
101
|
+
param?: string | string[];
|
|
102
|
+
timeout?: string;
|
|
103
|
+
// Docker
|
|
104
|
+
tag?: string;
|
|
105
|
+
"java-version"?: string;
|
|
106
|
+
detached?: boolean;
|
|
107
|
+
registry?: string;
|
|
108
|
+
namespace?: string;
|
|
71
109
|
}
|
|
72
110
|
|
|
73
111
|
export interface CommandContext {
|
package/src/utils/config.ts
CHANGED
|
@@ -44,6 +44,29 @@ export class ConfigManager {
|
|
|
44
44
|
"dry-run": { type: "boolean" },
|
|
45
45
|
force: { type: "boolean" },
|
|
46
46
|
src: { type: "string" },
|
|
47
|
+
// Multi-environment
|
|
48
|
+
env: { type: "string" },
|
|
49
|
+
environment: { type: "string" },
|
|
50
|
+
// Test runner
|
|
51
|
+
coverage: { type: "boolean" },
|
|
52
|
+
"fail-fast": { type: "boolean" },
|
|
53
|
+
parallel: { type: "boolean" },
|
|
54
|
+
// HTTP client
|
|
55
|
+
interactive: { type: "boolean", short: "i" },
|
|
56
|
+
"base-url": { type: "string" },
|
|
57
|
+
body: { type: "string" },
|
|
58
|
+
file: { type: "string" },
|
|
59
|
+
header: { type: "string", multiple: true },
|
|
60
|
+
"content-type": { type: "string" },
|
|
61
|
+
accept: { type: "string" },
|
|
62
|
+
param: { type: "string", multiple: true },
|
|
63
|
+
timeout: { type: "string" },
|
|
64
|
+
// Docker
|
|
65
|
+
tag: { type: "string" },
|
|
66
|
+
"java-version": { type: "string" },
|
|
67
|
+
detached: { type: "boolean", short: "d" },
|
|
68
|
+
registry: { type: "string" },
|
|
69
|
+
namespace: { type: "string" },
|
|
47
70
|
},
|
|
48
71
|
strict: false,
|
|
49
72
|
allowPositionals: true,
|
|
@@ -142,19 +165,30 @@ export class ConfigManager {
|
|
|
142
165
|
tomcatPath = embeddedService.getTomcatHome();
|
|
143
166
|
}
|
|
144
167
|
|
|
168
|
+
// Detectar environment
|
|
169
|
+
const environment = String(cliValues.env || cliValues.environment || xavvaJson.env || xavvaJson.environment || "");
|
|
170
|
+
const envConfig = environment && (xavvaJson as any).environments?.[environment];
|
|
171
|
+
|
|
172
|
+
// Merge environment config
|
|
173
|
+
const finalPort = envConfig?.port
|
|
174
|
+
? parseInt(String(envConfig.port))
|
|
175
|
+
: parseInt(String(cliValues.port || xavvaJson.port || String(DEFAULT_TOMCAT_PORT)));
|
|
176
|
+
const finalProfile = envConfig?.profile || String(cliValues.profile || xavvaJson.profile || "");
|
|
177
|
+
|
|
145
178
|
const config: AppConfig = {
|
|
146
179
|
tomcat: {
|
|
147
180
|
path: tomcatPath,
|
|
148
|
-
port:
|
|
181
|
+
port: finalPort,
|
|
149
182
|
webapps: "webapps",
|
|
150
183
|
grep: cliValues.grep || xavvaJson.grep ? String(cliValues.grep || xavvaJson.grep) : "",
|
|
151
184
|
embedded: useEmbedded,
|
|
152
185
|
version: embeddedVersion,
|
|
186
|
+
...(envConfig?.tomcat || {})
|
|
153
187
|
},
|
|
154
188
|
project: {
|
|
155
189
|
appName: cliValues.name || xavvaJson.name ? String(cliValues.name || xavvaJson.name) : "",
|
|
156
190
|
buildTool: (cliValues.tool as any) || (xavvaJson.tool as any) || detectedTool,
|
|
157
|
-
profile:
|
|
191
|
+
profile: finalProfile,
|
|
158
192
|
skipBuild: !!(cliValues["no-build"] ?? xavvaJson["no-build"]),
|
|
159
193
|
skipScan: cliValues.scan !== undefined ? !cliValues.scan : (xavvaJson.scan !== undefined ? !xavvaJson.scan : true),
|
|
160
194
|
clean: !!(cliValues.clean ?? xavvaJson.clean),
|
|
@@ -168,6 +202,8 @@ export class ConfigManager {
|
|
|
168
202
|
encoding: cliValues.encoding || xavvaJson.encoding || "",
|
|
169
203
|
war: !!(cliValues.war ?? xavvaJson.war),
|
|
170
204
|
cache: !!(cliValues.cache ?? xavvaJson.cache),
|
|
205
|
+
environment,
|
|
206
|
+
environments: (xavvaJson as any).environments
|
|
171
207
|
}
|
|
172
208
|
};
|
|
173
209
|
|