@archznn/xavva 3.0.0 → 3.1.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.
@@ -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
+ }
@@ -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 {
@@ -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: parseInt(String(cliValues.port || xavvaJson.port || String(DEFAULT_TOMCAT_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: String(cliValues.profile || xavvaJson.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