@kaluchi/jdtbridge 1.4.0 → 1.6.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 +4 -2
- package/package.json +1 -1
- package/src/args.mjs +13 -1
- package/src/claude-setup.mjs +83 -0
- package/src/cli.mjs +40 -5
- package/src/client.mjs +49 -0
- package/src/commands/editor.mjs +3 -3
- package/src/commands/hierarchy.mjs +12 -23
- package/src/commands/implementors.mjs +1 -1
- package/src/commands/refactoring.mjs +3 -3
- package/src/commands/references.mjs +4 -4
- package/src/commands/setup.mjs +16 -0
- package/src/commands/source.mjs +292 -32
- package/src/commands/subtypes.mjs +1 -1
- package/src/commands/test-run.mjs +100 -0
- package/src/commands/test-sessions.mjs +46 -0
- package/src/commands/test-status.mjs +59 -0
- package/src/commands/type-info.mjs +1 -1
- package/src/format/hierarchy.mjs +47 -0
- package/src/format/test-results.mjs +1 -1
- package/src/format/test-status.mjs +221 -0
- package/src/commands/test.mjs +0 -48
package/README.md
CHANGED
|
@@ -45,8 +45,10 @@ jdt source <FQMN> # (alias: src) source cod
|
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
jdt build [--project <name>] [--clean] # (alias: b) build project
|
|
48
|
-
jdt test <
|
|
49
|
-
jdt test --project <name> [
|
|
48
|
+
jdt test run <FQN> [-f] [-q] # launch tests (non-blocking)
|
|
49
|
+
jdt test run --project <name> [-f] # run tests in project
|
|
50
|
+
jdt test status <session> [-f] [--all] [--ignored] # show test progress/results
|
|
51
|
+
jdt test sessions # list test sessions
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
All commands auto-refresh from disk. `build` is the only command that triggers explicit builds.
|
package/package.json
CHANGED
package/src/args.mjs
CHANGED
|
@@ -103,5 +103,17 @@ function splitParams(str) {
|
|
|
103
103
|
}
|
|
104
104
|
const last = str.substring(start).trim();
|
|
105
105
|
if (last) params.push(last);
|
|
106
|
-
return params;
|
|
106
|
+
return params.map(eraseGenerics);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Strip generics: Map<String,Integer> → Map, List<String>[] → List[] */
|
|
110
|
+
function eraseGenerics(type) {
|
|
111
|
+
let result = "";
|
|
112
|
+
let depth = 0;
|
|
113
|
+
for (const ch of type) {
|
|
114
|
+
if (ch === "<") depth++;
|
|
115
|
+
else if (ch === ">") depth--;
|
|
116
|
+
else if (depth === 0) result += ch;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
107
119
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Configure Claude Code for JDT Bridge projects.
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const JDT_HOOK_COMMAND = `node -e "d=JSON.parse(require('fs').readFileSync(0,'utf8'));d.tool_input?.command?.startsWith('jdt ')&&process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'allow'}}))"`;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Find the project root (git root or cwd).
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function findProjectRoot() {
|
|
14
|
+
try {
|
|
15
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
18
|
+
}).trim();
|
|
19
|
+
} catch {
|
|
20
|
+
return process.cwd();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Merge JDT Bridge settings into a Claude Code settings object.
|
|
26
|
+
* Idempotent — safe to call multiple times.
|
|
27
|
+
* @param {object} settings existing settings (mutated in place)
|
|
28
|
+
* @returns {object} the mutated settings
|
|
29
|
+
*/
|
|
30
|
+
export function mergeJdtSettings(settings) {
|
|
31
|
+
// Permissions
|
|
32
|
+
if (!settings.permissions) settings.permissions = {};
|
|
33
|
+
const allow = settings.permissions.allow || [];
|
|
34
|
+
if (!allow.includes("Bash(jdt *)")) allow.push("Bash(jdt *)");
|
|
35
|
+
settings.permissions.allow = allow;
|
|
36
|
+
|
|
37
|
+
// Hook
|
|
38
|
+
if (!settings.hooks) settings.hooks = {};
|
|
39
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
40
|
+
const existing = settings.hooks.PreToolUse.find(
|
|
41
|
+
(h) =>
|
|
42
|
+
h.matcher === "Bash" &&
|
|
43
|
+
h.hooks?.some((hk) => hk.command === JDT_HOOK_COMMAND),
|
|
44
|
+
);
|
|
45
|
+
if (!existing) {
|
|
46
|
+
settings.hooks.PreToolUse.push({
|
|
47
|
+
matcher: "Bash",
|
|
48
|
+
hooks: [{ type: "command", command: JDT_HOOK_COMMAND }],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return settings;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Write Claude Code settings for JDT Bridge to a project.
|
|
57
|
+
* Creates .claude/settings.json if it doesn't exist,
|
|
58
|
+
* merges into existing if it does.
|
|
59
|
+
* @param {string} [root] project root (default: git root or cwd)
|
|
60
|
+
* @returns {{ file: string, settings: object }}
|
|
61
|
+
*/
|
|
62
|
+
export function installClaudeSettings(root) {
|
|
63
|
+
if (!root) root = findProjectRoot();
|
|
64
|
+
|
|
65
|
+
const dir = join(root, ".claude");
|
|
66
|
+
const file = join(dir, "settings.json");
|
|
67
|
+
|
|
68
|
+
let settings = {};
|
|
69
|
+
if (existsSync(file)) {
|
|
70
|
+
try {
|
|
71
|
+
settings = JSON.parse(readFileSync(file, "utf8"));
|
|
72
|
+
} catch {
|
|
73
|
+
// corrupt — overwrite
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
mergeJdtSettings(settings);
|
|
78
|
+
|
|
79
|
+
mkdirSync(dir, { recursive: true });
|
|
80
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + "\n");
|
|
81
|
+
|
|
82
|
+
return { file, settings };
|
|
83
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -11,7 +11,9 @@ import { implementors, help as implementorsHelp } from "./commands/implementors.
|
|
|
11
11
|
import { typeInfo, help as typeInfoHelp } from "./commands/type-info.mjs";
|
|
12
12
|
import { source, help as sourceHelp } from "./commands/source.mjs";
|
|
13
13
|
import { build, help as buildHelp } from "./commands/build.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import { testRun, help as testRunHelp } from "./commands/test-run.mjs";
|
|
15
|
+
import { testStatus, help as testStatusHelp } from "./commands/test-status.mjs";
|
|
16
|
+
import { testSessions, help as testSessionsHelp } from "./commands/test-sessions.mjs";
|
|
15
17
|
import { errors, help as errorsHelp } from "./commands/errors.mjs";
|
|
16
18
|
import {
|
|
17
19
|
organizeImports,
|
|
@@ -93,6 +95,36 @@ async function launchDispatch(args) {
|
|
|
93
95
|
await cmd.fn(rest);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
const testSubcommands = {
|
|
99
|
+
run: { fn: testRun, help: testRunHelp },
|
|
100
|
+
status: { fn: testStatus, help: testStatusHelp },
|
|
101
|
+
sessions: { fn: testSessions, help: testSessionsHelp },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const testHelp = `Run and monitor JUnit tests via Eclipse's built-in runner.
|
|
105
|
+
|
|
106
|
+
Subcommands:
|
|
107
|
+
jdt test run <FQN> [-f] [-q] launch tests (non-blocking)
|
|
108
|
+
jdt test status <session> [-f] [--all] show test progress/results
|
|
109
|
+
jdt test sessions list test sessions
|
|
110
|
+
|
|
111
|
+
Use "jdt help test <subcommand>" for details.`;
|
|
112
|
+
|
|
113
|
+
async function testDispatch(args) {
|
|
114
|
+
const [sub, ...rest] = args;
|
|
115
|
+
if (!sub || sub === "--help") {
|
|
116
|
+
console.log(testHelp);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const cmd = testSubcommands[sub];
|
|
120
|
+
if (!cmd) {
|
|
121
|
+
console.error(`Unknown test subcommand: ${sub}`);
|
|
122
|
+
console.log(testHelp);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
await cmd.fn(rest);
|
|
126
|
+
}
|
|
127
|
+
|
|
96
128
|
const commands = {
|
|
97
129
|
projects: { fn: projects, help: projectsHelp },
|
|
98
130
|
"project-info": { fn: projectInfo, help: projectInfoHelp },
|
|
@@ -104,7 +136,7 @@ const commands = {
|
|
|
104
136
|
"type-info": { fn: typeInfo, help: typeInfoHelp },
|
|
105
137
|
source: { fn: source, help: sourceHelp },
|
|
106
138
|
build: { fn: build, help: buildHelp },
|
|
107
|
-
test: { fn:
|
|
139
|
+
test: { fn: testDispatch, help: testHelp },
|
|
108
140
|
errors: { fn: errors, help: errorsHelp },
|
|
109
141
|
"organize-imports": { fn: organizeImports, help: organizeImportsHelp },
|
|
110
142
|
format: { fn: format, help: formatHelp },
|
|
@@ -162,12 +194,13 @@ Search & navigation:
|
|
|
162
194
|
hierarchy${fmtAliases("hierarchy")} <FQN> full hierarchy (supers + interfaces + subtypes)
|
|
163
195
|
implementors${fmtAliases("implementors")} <FQMN> implementations of interface method
|
|
164
196
|
type-info${fmtAliases("type-info")} <FQN> class overview (fields, methods, line numbers)
|
|
165
|
-
source${fmtAliases("source")} <FQMN>
|
|
197
|
+
source${fmtAliases("source")} <FQMN> source + resolved references (navigation)
|
|
166
198
|
|
|
167
199
|
Testing & building:
|
|
168
200
|
build${fmtAliases("build")} [--project <name>] [--clean] build project (incremental or clean)
|
|
169
|
-
test <
|
|
170
|
-
test
|
|
201
|
+
test run <FQN> [-f] [-q] launch tests (non-blocking)
|
|
202
|
+
test status <session> [-f] [--all] show test progress/results
|
|
203
|
+
test sessions list test sessions
|
|
171
204
|
|
|
172
205
|
Diagnostics:
|
|
173
206
|
errors${fmtAliases("errors")} [--file <path>] [--project <name>] compilation errors
|
|
@@ -215,6 +248,8 @@ export async function run(argv) {
|
|
|
215
248
|
const resolved = topic ? resolve(topic) : null;
|
|
216
249
|
if (resolved === "launch" && rest[1] && launchSubcommands[rest[1]]) {
|
|
217
250
|
console.log(launchSubcommands[rest[1]].help);
|
|
251
|
+
} else if (resolved === "test" && rest[1] && testSubcommands[rest[1]]) {
|
|
252
|
+
console.log(testSubcommands[rest[1]].help);
|
|
218
253
|
} else if (resolved) {
|
|
219
254
|
console.log(commands[resolved].help);
|
|
220
255
|
} else if (topic) {
|
package/src/client.mjs
CHANGED
|
@@ -196,6 +196,55 @@ export function getStream(path, dest) {
|
|
|
196
196
|
});
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* HTTP GET streaming with line-by-line callback. Designed for
|
|
201
|
+
* JSONL (newline-delimited JSON) endpoints. Calls onLine for
|
|
202
|
+
* each complete line received.
|
|
203
|
+
* Resolves when the server closes the connection.
|
|
204
|
+
* @param {string} path
|
|
205
|
+
* @param {(line: string) => void} onLine
|
|
206
|
+
* @returns {Promise<void>}
|
|
207
|
+
*/
|
|
208
|
+
export function getStreamLines(path, onLine) {
|
|
209
|
+
const inst = connect();
|
|
210
|
+
return new Promise((resolve, reject) => {
|
|
211
|
+
const req = request(
|
|
212
|
+
{
|
|
213
|
+
hostname: "127.0.0.1",
|
|
214
|
+
port: inst.port,
|
|
215
|
+
path,
|
|
216
|
+
method: "GET",
|
|
217
|
+
timeout: 0,
|
|
218
|
+
headers: authHeaders(),
|
|
219
|
+
},
|
|
220
|
+
(res) => {
|
|
221
|
+
if (res.statusCode !== 200) {
|
|
222
|
+
let data = "";
|
|
223
|
+
res.on("data", (chunk) => (data += chunk));
|
|
224
|
+
res.on("end", () => reject(new Error(data.trim())));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
let buffer = "";
|
|
228
|
+
res.on("data", (chunk) => {
|
|
229
|
+
buffer += chunk;
|
|
230
|
+
const lines = buffer.split("\n");
|
|
231
|
+
buffer = lines.pop();
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
if (line.trim()) onLine(line);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
res.on("end", () => {
|
|
237
|
+
if (buffer.trim()) onLine(buffer);
|
|
238
|
+
resolve();
|
|
239
|
+
});
|
|
240
|
+
res.on("error", reject);
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
req.on("error", reject);
|
|
244
|
+
req.end();
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
199
248
|
/**
|
|
200
249
|
* Check if error is a connection refused error.
|
|
201
250
|
* @param {Error} e
|
package/src/commands/editor.mjs
CHANGED
|
@@ -50,6 +50,6 @@ export const openHelp = `Open a type or method in the Eclipse editor.
|
|
|
50
50
|
Usage: jdt open <FQN>[#method[(param types)]]
|
|
51
51
|
|
|
52
52
|
Examples:
|
|
53
|
-
jdt open
|
|
54
|
-
jdt open
|
|
55
|
-
jdt open "
|
|
53
|
+
jdt open com.example.dao.UserDaoImpl
|
|
54
|
+
jdt open com.example.dao.UserDaoImpl#getStaff
|
|
55
|
+
jdt open "com.example.dao.UserDaoImpl#save(Order)"`;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { get } from "../client.mjs";
|
|
2
2
|
import { extractPositional } from "../args.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { bold } from "../color.mjs";
|
|
3
|
+
import { formatHierarchy } from "../format/hierarchy.mjs";
|
|
5
4
|
|
|
6
5
|
export async function hierarchy(args) {
|
|
7
6
|
const pos = extractPositional(args);
|
|
@@ -18,26 +17,16 @@ export async function hierarchy(args) {
|
|
|
18
17
|
console.error(result.error);
|
|
19
18
|
process.exit(1);
|
|
20
19
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const loc = s.binary ? "(binary)" : stripProject(s.file);
|
|
32
|
-
console.log(` ${s.fqn} ${loc}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (result.subtypes.length > 0) {
|
|
36
|
-
console.log(bold("Subtypes:"));
|
|
37
|
-
for (const s of result.subtypes) {
|
|
38
|
-
const loc = s.binary ? "(binary)" : stripProject(s.file);
|
|
39
|
-
console.log(` ${s.fqn} ${loc}`);
|
|
40
|
-
}
|
|
20
|
+
|
|
21
|
+
const lines = [];
|
|
22
|
+
lines.push(`#### ${result.fqn || fqn}`);
|
|
23
|
+
lines.push("");
|
|
24
|
+
lines.push(...formatHierarchy(result));
|
|
25
|
+
|
|
26
|
+
if (lines.length > 2) {
|
|
27
|
+
console.log(lines.join("\n"));
|
|
28
|
+
} else {
|
|
29
|
+
console.log("No hierarchy found.");
|
|
41
30
|
}
|
|
42
31
|
}
|
|
43
32
|
|
|
@@ -45,4 +34,4 @@ export const help = `Show full type hierarchy: superclasses, interfaces, and sub
|
|
|
45
34
|
|
|
46
35
|
Usage: jdt hierarchy <FQN>
|
|
47
36
|
|
|
48
|
-
Example: jdt hierarchy
|
|
37
|
+
Example: jdt hierarchy com.example.client.AppEntryPoint`;
|
|
@@ -109,11 +109,11 @@ Usage: jdt rename <FQN>[#method[(param types)]] <newName>
|
|
|
109
109
|
jdt rename <FQN> <newName> [--field <old>]
|
|
110
110
|
|
|
111
111
|
Examples:
|
|
112
|
-
jdt rename
|
|
113
|
-
jdt rename
|
|
112
|
+
jdt rename com.example.dto.Foo Bar
|
|
113
|
+
jdt rename com.example.dto.Foo#getFoo getBar`;
|
|
114
114
|
|
|
115
115
|
export const moveHelp = `Move a type to another package (updates all references).
|
|
116
116
|
|
|
117
117
|
Usage: jdt move <FQN> <target.package>
|
|
118
118
|
|
|
119
|
-
Example: jdt move
|
|
119
|
+
Example: jdt move com.example.dto.Foo com.example.dto.shared`;
|
|
@@ -48,7 +48,7 @@ Flags:
|
|
|
48
48
|
--field <name> find references to a field
|
|
49
49
|
|
|
50
50
|
Examples:
|
|
51
|
-
jdt references
|
|
52
|
-
jdt references
|
|
53
|
-
jdt references "
|
|
54
|
-
jdt references
|
|
51
|
+
jdt references com.example.dto.BaseEntity
|
|
52
|
+
jdt references com.example.dao.UserDaoImpl#getStaff
|
|
53
|
+
jdt references "com.example.dao.UserDaoImpl#save(Order)"
|
|
54
|
+
jdt references com.example.dao.UserDaoImpl --field staffCache`;
|
package/src/commands/setup.mjs
CHANGED
|
@@ -357,11 +357,26 @@ export async function setup(args) {
|
|
|
357
357
|
await runCheck(config);
|
|
358
358
|
} else if (args.includes("--remove")) {
|
|
359
359
|
await runRemove(config);
|
|
360
|
+
} else if (args.includes("--claude")) {
|
|
361
|
+
await setupClaude();
|
|
360
362
|
} else {
|
|
361
363
|
await runInstall(config, flags);
|
|
362
364
|
}
|
|
363
365
|
}
|
|
364
366
|
|
|
367
|
+
async function setupClaude() {
|
|
368
|
+
const { installClaudeSettings } = await import("../claude-setup.mjs");
|
|
369
|
+
const { file } = installClaudeSettings();
|
|
370
|
+
|
|
371
|
+
console.log(`${green("✓")} Claude Code settings written to ${file}`);
|
|
372
|
+
console.log();
|
|
373
|
+
console.log(" Installed:");
|
|
374
|
+
console.log(" - Bash(jdt *) permission rule");
|
|
375
|
+
console.log(" - PreToolUse hook for # workaround (issue #34061)");
|
|
376
|
+
console.log();
|
|
377
|
+
console.log(" Restart Claude Code to apply.");
|
|
378
|
+
}
|
|
379
|
+
|
|
365
380
|
export const help = `Set up the Eclipse JDT Bridge plugin.
|
|
366
381
|
|
|
367
382
|
Usage: jdt setup [options]
|
|
@@ -370,6 +385,7 @@ Modes:
|
|
|
370
385
|
(default) build plugin from source, install into Eclipse
|
|
371
386
|
--check show status of all components (diagnostic only)
|
|
372
387
|
--remove uninstall the plugin from Eclipse
|
|
388
|
+
--claude configure Claude Code for this project (permissions + hooks)
|
|
373
389
|
|
|
374
390
|
Options:
|
|
375
391
|
--eclipse <path> Eclipse installation directory
|
package/src/commands/source.mjs
CHANGED
|
@@ -1,46 +1,306 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { extractPositional,
|
|
3
|
-
import {
|
|
1
|
+
import { get } from "../client.mjs";
|
|
2
|
+
import { extractPositional, parseFqmn } from "../args.mjs";
|
|
3
|
+
import { formatHierEntry } from "../format/hierarchy.mjs";
|
|
4
4
|
|
|
5
5
|
export async function source(args) {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!fqn) {
|
|
11
|
-
console.error("Usage: source <FQN>[#method[(param types)]]");
|
|
6
|
+
const jsonFlag = args.includes("--json");
|
|
7
|
+
const pos = extractPositional(args).filter((a) => a !== "--json");
|
|
8
|
+
if (pos.length === 0) {
|
|
9
|
+
console.error("Usage: source <FQMN> [<FQMN> ...] [--json]");
|
|
12
10
|
process.exit(1);
|
|
13
11
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
|
|
13
|
+
const results = await Promise.all(pos.map((arg) => fetchOne(arg)));
|
|
14
|
+
|
|
15
|
+
if (jsonFlag) {
|
|
16
|
+
console.log(JSON.stringify(results.length === 1 ? results[0] : results, null, 2));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const blocks = [];
|
|
21
|
+
for (const r of results) {
|
|
22
|
+
if (r.error) {
|
|
23
|
+
console.error(r.error);
|
|
24
|
+
} else if (Array.isArray(r)) {
|
|
25
|
+
blocks.push(...r.map(formatMarkdown));
|
|
26
|
+
} else {
|
|
27
|
+
blocks.push(formatMarkdown(r));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (blocks.length === 0) process.exit(1);
|
|
31
|
+
console.log(blocks.join("\n\n---\n\n"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function fetchOne(fqmn) {
|
|
35
|
+
const parsed = parseFqmn(fqmn);
|
|
36
|
+
if (!parsed.className) return { error: `Invalid FQMN: ${fqmn}` };
|
|
37
|
+
let url = `/source?class=${encodeURIComponent(parsed.className)}`;
|
|
38
|
+
if (parsed.method) url += `&method=${encodeURIComponent(parsed.method)}`;
|
|
17
39
|
if (parsed.paramTypes) {
|
|
18
40
|
url += `¶mTypes=${encodeURIComponent(parsed.paramTypes.join(","))}`;
|
|
19
41
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
return get(url, 30_000);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- Badge helpers ----
|
|
46
|
+
|
|
47
|
+
const KIND_BADGE = {
|
|
48
|
+
method: "[M]",
|
|
49
|
+
field: "[F]",
|
|
50
|
+
constant: "[K]",
|
|
51
|
+
type: "[C]",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const TYPE_KIND_BADGE = {
|
|
55
|
+
class: "[C]",
|
|
56
|
+
interface: "[I]",
|
|
57
|
+
enum: "[E]",
|
|
58
|
+
annotation: "[A]",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function badge(ref) {
|
|
62
|
+
if (ref.kind === "type") return TYPE_KIND_BADGE[ref.typeKind] || "[C]";
|
|
63
|
+
return KIND_BADGE[ref.kind] || "[?]";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function returnTypeBadge(ref) {
|
|
67
|
+
if (ref.returnTypeKind) return TYPE_KIND_BADGE[ref.returnTypeKind] || "";
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- Ref formatting ----
|
|
72
|
+
|
|
73
|
+
function formatMemberRef(ref) {
|
|
74
|
+
let line = `${badge(ref)} \`${ref.fqmn}\``;
|
|
75
|
+
|
|
76
|
+
// Return / field type
|
|
77
|
+
if (ref.type) {
|
|
78
|
+
const rtBadge = returnTypeBadge(ref);
|
|
79
|
+
const typeName = ref.returnTypeFqn || ref.type;
|
|
80
|
+
if (ref.isTypeVariable && ref.typeBound) {
|
|
81
|
+
line += ` → \`${ref.typeBound}\` (bound)`;
|
|
82
|
+
} else if (rtBadge) {
|
|
83
|
+
line += ` → ${rtBadge} \`${typeName}\``;
|
|
84
|
+
} else {
|
|
85
|
+
line += ` → \`${typeName}\``;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Annotations
|
|
90
|
+
const annotations = [];
|
|
91
|
+
if (ref.static) annotations.push("static");
|
|
92
|
+
if (ref.inherited) annotations.push("inherited");
|
|
93
|
+
if (annotations.length > 0) line += ` (${annotations.join(", ")})`;
|
|
94
|
+
|
|
95
|
+
// Line number: server sends it but we don't render for incoming —
|
|
96
|
+
// callers are navigable by FQMN, line numbers just add noise
|
|
97
|
+
|
|
98
|
+
// Javadoc inline after —
|
|
99
|
+
if (ref.doc) line += ` — ${ref.doc}`;
|
|
100
|
+
|
|
101
|
+
return line;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatTypeHeader(ref) {
|
|
105
|
+
let line = `${badge(ref)} \`${ref.fqmn}\``;
|
|
106
|
+
if (ref.doc) line += ` — ${ref.doc}`;
|
|
107
|
+
return line;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- Grouping ----
|
|
111
|
+
|
|
112
|
+
function groupByDeclaringType(refs) {
|
|
113
|
+
const groups = [];
|
|
114
|
+
const groupMap = {};
|
|
115
|
+
for (const ref of refs) {
|
|
116
|
+
const typeFqn = ref.fqmn.split("#")[0];
|
|
117
|
+
if (!groupMap[typeFqn]) {
|
|
118
|
+
groupMap[typeFqn] = { typeRef: null, members: [] };
|
|
119
|
+
groups.push({ typeFqn, group: groupMap[typeFqn] });
|
|
120
|
+
}
|
|
121
|
+
if (!ref.fqmn.includes("#")) {
|
|
122
|
+
groupMap[typeFqn].typeRef = ref;
|
|
123
|
+
} else {
|
|
124
|
+
groupMap[typeFqn].members.push(ref);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return groups;
|
|
128
|
+
}
|
|
24
129
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
130
|
+
function formatRefGroup({ typeFqn, group }, implIndex, viewScope) {
|
|
131
|
+
const lines = [];
|
|
132
|
+
// Type header: only show for standalone type refs (no members).
|
|
133
|
+
// When members exist, the type is already visible in their FQMNs.
|
|
134
|
+
const standalone = group.members.length === 0;
|
|
135
|
+
if (standalone && group.typeRef) {
|
|
136
|
+
lines.push(formatTypeHeader(group.typeRef));
|
|
137
|
+
// Show type implementations (domain-scoped)
|
|
138
|
+
const impls = implIndex[group.typeRef.fqmn];
|
|
139
|
+
if (impls && !(viewScope === "project" && group.typeRef.scope === "dependency")) {
|
|
140
|
+
for (const impl of impls) {
|
|
141
|
+
let implLine = ` ${badge(impl)} \`${impl.fqmn}\``;
|
|
142
|
+
if (impl.anonymous && impl.enclosingFqmn) {
|
|
143
|
+
implLine += ` — in \`${impl.enclosingFqmn}\``;
|
|
144
|
+
}
|
|
145
|
+
lines.push(implLine);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
36
148
|
}
|
|
149
|
+
for (const ref of group.members) {
|
|
150
|
+
lines.push(formatMemberRef(ref));
|
|
151
|
+
// Show implementations — skip dependency interface impls
|
|
152
|
+
// when viewing project source (domain scoping)
|
|
153
|
+
const impls = implIndex[ref.fqmn];
|
|
154
|
+
if (impls && !(viewScope === "project" && ref.scope === "dependency")) {
|
|
155
|
+
for (const impl of impls) {
|
|
156
|
+
lines.push(` → ${badge(impl)} \`${impl.fqmn}\``);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return lines.join("\n");
|
|
37
161
|
}
|
|
38
162
|
|
|
39
|
-
|
|
163
|
+
/** Build index: interfaceFqmn → [impl refs] */
|
|
164
|
+
function buildImplIndex(refs) {
|
|
165
|
+
const index = {};
|
|
166
|
+
for (const ref of refs) {
|
|
167
|
+
if (ref.implementationOf) {
|
|
168
|
+
if (!index[ref.implementationOf]) index[ref.implementationOf] = [];
|
|
169
|
+
index[ref.implementationOf].push(ref);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return index;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
// ---- Hierarchy (type-level) ----
|
|
177
|
+
|
|
178
|
+
function formatHierarchySection(lines, result) {
|
|
179
|
+
const supers = result.supertypes || [];
|
|
180
|
+
const subs = result.subtypes || [];
|
|
181
|
+
if (supers.length > 0 || subs.length > 0) {
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push("#### Hierarchy:");
|
|
184
|
+
for (const s of supers) {
|
|
185
|
+
lines.push(...formatHierEntry("↑", s));
|
|
186
|
+
}
|
|
187
|
+
for (const s of subs) {
|
|
188
|
+
lines.push(...formatHierEntry("", s));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (result.enclosingType) {
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push("#### Enclosing Type:");
|
|
194
|
+
const et = result.enclosingType;
|
|
195
|
+
const fqn = typeof et === "string" ? et : et.fqn;
|
|
196
|
+
const kind = typeof et === "string" ? "class" : (et.kind || "class");
|
|
197
|
+
lines.push(...formatHierEntry("", { fqn, kind, ...et }));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- Markdown output ----
|
|
202
|
+
|
|
203
|
+
function formatMarkdown(result) {
|
|
204
|
+
const lines = [];
|
|
205
|
+
|
|
206
|
+
// Header
|
|
207
|
+
const headerBadge = result.fqmn.includes("#") ? "[M]" : "[C]";
|
|
208
|
+
lines.push(`#### ${headerBadge} ${result.fqmn}`);
|
|
209
|
+
if (result.overrideTarget) {
|
|
210
|
+
const ot = result.overrideTarget;
|
|
211
|
+
// Support both object {fqmn, kind} and legacy string format
|
|
212
|
+
const fqmn = typeof ot === "string"
|
|
213
|
+
? (ot.includes(" ") ? ot.split(" ", 2)[1] : ot)
|
|
214
|
+
: ot.fqmn;
|
|
215
|
+
lines.push(`overrides [M] \`${fqmn}\``);
|
|
216
|
+
}
|
|
217
|
+
lines.push(`\`${result.file}:${result.startLine}-${result.endLine}\``);
|
|
218
|
+
lines.push("");
|
|
219
|
+
|
|
220
|
+
// Source
|
|
221
|
+
lines.push("```java");
|
|
222
|
+
lines.push((result.source || "").trimEnd());
|
|
223
|
+
lines.push("```");
|
|
224
|
+
|
|
225
|
+
// Type-level: hierarchy instead of refs
|
|
226
|
+
if (result.supertypes || result.subtypes) {
|
|
227
|
+
formatHierarchySection(lines, result);
|
|
228
|
+
return lines.join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!result.refs || result.refs.length === 0) {
|
|
232
|
+
return lines.join("\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Self-reference: the viewed member's declaring type
|
|
236
|
+
const selfFqn = result.fqmn.includes("#")
|
|
237
|
+
? result.fqmn.split("#")[0] : null;
|
|
238
|
+
|
|
239
|
+
// Split by direction, filter self-reference type refs
|
|
240
|
+
const outgoing = result.refs.filter((r) =>
|
|
241
|
+
r.direction !== "incoming"
|
|
242
|
+
&& !(r.kind === "type" && r.fqmn === selfFqn));
|
|
243
|
+
const incoming = result.refs.filter((r) => r.direction === "incoming");
|
|
244
|
+
|
|
245
|
+
const viewScope = result.viewScope;
|
|
246
|
+
|
|
247
|
+
// Implementations section (interface/abstract methods)
|
|
248
|
+
const impls = result.implementations || [];
|
|
249
|
+
if (impls.length > 0) {
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push("#### Implementations:");
|
|
252
|
+
for (const impl of impls) {
|
|
253
|
+
let line = `[M] \`${impl.fqmn}\``;
|
|
254
|
+
if (impl.anonymous && impl.enclosingFqmn) {
|
|
255
|
+
line += ` — in \`${impl.enclosingFqmn}\``;
|
|
256
|
+
}
|
|
257
|
+
lines.push(line);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (outgoing.length > 0) {
|
|
262
|
+
const implIndex = buildImplIndex(outgoing);
|
|
263
|
+
const mainRefs = outgoing.filter((r) => !r.implementationOf);
|
|
264
|
+
lines.push("");
|
|
265
|
+
lines.push("#### Outgoing Calls:");
|
|
266
|
+
const groups = groupByDeclaringType(mainRefs);
|
|
267
|
+
for (const g of groups) {
|
|
268
|
+
lines.push(formatRefGroup(g, implIndex, viewScope));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (incoming.length > 0) {
|
|
273
|
+
const implIndex = buildImplIndex(incoming);
|
|
274
|
+
const mainRefs = incoming.filter((r) => !r.implementationOf);
|
|
275
|
+
lines.push("");
|
|
276
|
+
lines.push("#### Incoming Calls:");
|
|
277
|
+
const groups = groupByDeclaringType(mainRefs);
|
|
278
|
+
for (const g of groups) {
|
|
279
|
+
lines.push(formatRefGroup(g, implIndex, viewScope));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const help = `Print source code of a type or method with resolved references.
|
|
287
|
+
|
|
288
|
+
Returns markdown with source in a code block and references split into:
|
|
289
|
+
- Outgoing Calls — what this method/type calls or references
|
|
290
|
+
- Incoming Calls — who calls this method/type (when available)
|
|
291
|
+
|
|
292
|
+
References are grouped by declaring type. Each has a badge
|
|
293
|
+
([M] method, [C] class, [I] interface, [E] enum, [K] constant,
|
|
294
|
+
[F] field, [A] annotation) and metadata (static, inherited,
|
|
295
|
+
return type with kind).
|
|
296
|
+
|
|
297
|
+
Usage: jdt source <FQMN> [<FQMN> ...] [--json]
|
|
40
298
|
|
|
41
|
-
|
|
299
|
+
Flags:
|
|
300
|
+
--json output raw JSON from the server (for debugging)
|
|
42
301
|
|
|
43
302
|
Examples:
|
|
44
|
-
jdt source
|
|
45
|
-
jdt source
|
|
46
|
-
jdt source "
|
|
303
|
+
jdt source com.example.dao.UserDaoImpl
|
|
304
|
+
jdt source com.example.dao.UserDaoImpl#getStaff
|
|
305
|
+
jdt source "com.example.dao.UserDaoImpl#save(Order)"
|
|
306
|
+
jdt source com.example.Foo#bar --json`;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { get } from "../client.mjs";
|
|
2
|
+
import { extractPositional, parseFlags, parseFqmn } from "../args.mjs";
|
|
3
|
+
import {
|
|
4
|
+
formatTestRunHeader,
|
|
5
|
+
testRunGuide,
|
|
6
|
+
followTestStream,
|
|
7
|
+
} from "../format/test-status.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Launch tests non-blocking. Analogous to `jdt launch run`.
|
|
11
|
+
* Without -f: prints header + onboarding guide.
|
|
12
|
+
* With -f: prints header + streams test progress until done.
|
|
13
|
+
*/
|
|
14
|
+
export async function testRun(args) {
|
|
15
|
+
// Filter out single-char flags (-f, -q) before extracting positionals
|
|
16
|
+
const filtered = args.filter((a) => a !== "-f" && a !== "-q");
|
|
17
|
+
const pos = extractPositional(filtered);
|
|
18
|
+
const flags = parseFlags(args);
|
|
19
|
+
|
|
20
|
+
let url = "/test/run?";
|
|
21
|
+
const parsed = parseFqmn(pos[0]);
|
|
22
|
+
const fqn = parsed.className;
|
|
23
|
+
|
|
24
|
+
if (fqn) {
|
|
25
|
+
url += `class=${encodeURIComponent(fqn)}`;
|
|
26
|
+
const method = parsed.method;
|
|
27
|
+
if (method) url += `&method=${encodeURIComponent(method)}`;
|
|
28
|
+
} else if (flags.project) {
|
|
29
|
+
url += `project=${encodeURIComponent(flags.project)}`;
|
|
30
|
+
if (flags.package)
|
|
31
|
+
url += `&package=${encodeURIComponent(flags.package)}`;
|
|
32
|
+
} else {
|
|
33
|
+
console.error(
|
|
34
|
+
"Usage: test run <FQN>[#method] | test run --project <name> [--package <pkg>]",
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (args.includes("--no-refresh")) url += "&no-refresh";
|
|
40
|
+
|
|
41
|
+
const result = await get(url, 30_000);
|
|
42
|
+
if (result.error) {
|
|
43
|
+
console.error(result.error);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Wait briefly for session to register total count
|
|
48
|
+
const session = result.session;
|
|
49
|
+
await sleep(500);
|
|
50
|
+
try {
|
|
51
|
+
const status = await get(
|
|
52
|
+
`/test/status?session=${encodeURIComponent(session)}`,
|
|
53
|
+
5_000,
|
|
54
|
+
);
|
|
55
|
+
if (status && !status.error && status.total > 0) {
|
|
56
|
+
result.total = status.total;
|
|
57
|
+
if (status.label) result.label = status.label;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore — total just won't be shown
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(formatTestRunHeader(result));
|
|
64
|
+
|
|
65
|
+
const follow = args.includes("-f") || args.includes("--follow");
|
|
66
|
+
if (follow) {
|
|
67
|
+
console.log();
|
|
68
|
+
const exitCode = await followTestStream(session, args);
|
|
69
|
+
process.exit(exitCode);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const quiet = args.includes("-q") || args.includes("--quiet");
|
|
73
|
+
if (!quiet) {
|
|
74
|
+
console.log(testRunGuide(session));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sleep(ms) {
|
|
79
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const help = `Launch tests non-blocking with real-time progress.
|
|
83
|
+
|
|
84
|
+
Usage: jdt test run <FQN>[#method] [-f] [-q]
|
|
85
|
+
jdt test run --project <name> [--package <pkg>] [-f] [-q]
|
|
86
|
+
|
|
87
|
+
Without -f, launches and prints a guide with available commands.
|
|
88
|
+
With -f, launches and streams test progress until completion.
|
|
89
|
+
|
|
90
|
+
Flags:
|
|
91
|
+
-f, --follow stream test status (only failures by default)
|
|
92
|
+
-q, --quiet suppress onboarding guide
|
|
93
|
+
--all include passed tests in output (with -f)
|
|
94
|
+
--ignored show only ignored tests (with -f)
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
jdt test run com.example.MyTest run + show guide
|
|
98
|
+
jdt test run com.example.MyTest -f run + stream failures
|
|
99
|
+
jdt test run com.example.MyTest -f --all run + stream all tests
|
|
100
|
+
jdt test run --project my-project -f run project tests + stream`;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { get } from "../client.mjs";
|
|
2
|
+
import { green, red, yellow, dim } from "../color.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* List active and completed test sessions.
|
|
6
|
+
*/
|
|
7
|
+
export async function testSessions() {
|
|
8
|
+
const results = await get("/test/sessions");
|
|
9
|
+
if (results.error) {
|
|
10
|
+
console.error(results.error);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
if (results.length === 0) {
|
|
14
|
+
console.log("(no test sessions)");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const s of results) {
|
|
18
|
+
const label = s.label || s.session;
|
|
19
|
+
const counts = formatCounts(s);
|
|
20
|
+
const time =
|
|
21
|
+
Number.isFinite(s.time) && s.time > 0
|
|
22
|
+
? `${s.time.toFixed(1)}s`
|
|
23
|
+
: "";
|
|
24
|
+
const state = s.state === "running"
|
|
25
|
+
? `running (${s.completed}/${s.total})`
|
|
26
|
+
: s.state;
|
|
27
|
+
console.log(
|
|
28
|
+
`${s.session} ${label} ${s.total} tests ${counts} ${time} ${state}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatCounts(s) {
|
|
34
|
+
const parts = [];
|
|
35
|
+
if (s.passed > 0) parts.push(green(`${s.passed} passed`));
|
|
36
|
+
if (s.failed > 0) parts.push(red(`${s.failed} failed`));
|
|
37
|
+
if (s.errors > 0) parts.push(red(`${s.errors} errors`));
|
|
38
|
+
if (s.ignored > 0) parts.push(yellow(`${s.ignored} ign`));
|
|
39
|
+
return parts.join(", ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const help = `List active and completed test sessions.
|
|
43
|
+
|
|
44
|
+
Usage: jdt test sessions
|
|
45
|
+
|
|
46
|
+
Output: session ID, label, test counts, state — one session per line.`;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { get } from "../client.mjs";
|
|
2
|
+
import { extractPositional } from "../args.mjs";
|
|
3
|
+
import {
|
|
4
|
+
formatTestStatus,
|
|
5
|
+
followTestStream,
|
|
6
|
+
} from "../format/test-status.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Show test session status (snapshot or stream).
|
|
10
|
+
* Analogous to `jdt launch logs`.
|
|
11
|
+
*/
|
|
12
|
+
export async function testStatus(args) {
|
|
13
|
+
const pos = extractPositional(args);
|
|
14
|
+
const session = pos[0];
|
|
15
|
+
|
|
16
|
+
if (!session) {
|
|
17
|
+
console.error("Usage: test status <session> [-f] [--all] [--ignored]");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const follow = args.includes("-f") || args.includes("--follow");
|
|
22
|
+
if (follow) {
|
|
23
|
+
const exitCode = await followTestStream(session, args);
|
|
24
|
+
process.exit(exitCode);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Snapshot mode
|
|
28
|
+
let filter = "failures";
|
|
29
|
+
if (args.includes("--all")) filter = "all";
|
|
30
|
+
else if (args.includes("--ignored")) filter = "ignored";
|
|
31
|
+
|
|
32
|
+
let url = `/test/status?session=${encodeURIComponent(session)}`;
|
|
33
|
+
if (filter) url += `&filter=${filter}`;
|
|
34
|
+
|
|
35
|
+
const result = await get(url, 30_000);
|
|
36
|
+
if (result.error) {
|
|
37
|
+
console.error(result.error);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
formatTestStatus(result);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const help = `Show test session status (snapshot or live stream).
|
|
45
|
+
|
|
46
|
+
Usage: jdt test status <session> [-f] [--all] [--ignored]
|
|
47
|
+
|
|
48
|
+
Without -f, returns a snapshot of the current state.
|
|
49
|
+
With -f, streams test events until the session completes.
|
|
50
|
+
|
|
51
|
+
Flags:
|
|
52
|
+
-f, --follow stream events live until completion
|
|
53
|
+
--all show all tests (default: failures only)
|
|
54
|
+
--ignored show only ignored/skipped tests
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
jdt test status jdtbridge-test-1234567890
|
|
58
|
+
jdt test status jdtbridge-test-1234567890 -f
|
|
59
|
+
jdt test status jdtbridge-test-1234567890 --ignored`;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const TYPE_KIND_BADGE = {
|
|
2
|
+
class: "[C]",
|
|
3
|
+
interface: "[I]",
|
|
4
|
+
enum: "[E]",
|
|
5
|
+
annotation: "[A]",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function formatHierEntry(arrow, s) {
|
|
9
|
+
const depth = s.depth || 0;
|
|
10
|
+
const indent = " ".repeat(depth);
|
|
11
|
+
const b = TYPE_KIND_BADGE[s.kind] || "[C]";
|
|
12
|
+
const prefix = arrow ? `${arrow} ` : "";
|
|
13
|
+
let line = `${indent}- ${prefix}${b} \`${s.fqn}\``;
|
|
14
|
+
if (s.anonymous && s.enclosingFqmn) {
|
|
15
|
+
line += ` — in \`${s.enclosingFqmn}\``;
|
|
16
|
+
}
|
|
17
|
+
const lines = [line];
|
|
18
|
+
if (s.file) {
|
|
19
|
+
let loc = s.file;
|
|
20
|
+
if (s.line) {
|
|
21
|
+
loc += `:${s.line}`;
|
|
22
|
+
if (s.endLine && s.endLine !== s.line) loc += `-${s.endLine}`;
|
|
23
|
+
}
|
|
24
|
+
lines.push(`${indent} \`${loc}\``);
|
|
25
|
+
}
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatHierarchy(result) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
const supers = result.supertypes || [];
|
|
32
|
+
const subs = result.subtypes || [];
|
|
33
|
+
if (supers.length > 0) {
|
|
34
|
+
lines.push("#### Supertypes:");
|
|
35
|
+
for (const s of supers) {
|
|
36
|
+
lines.push(...formatHierEntry("↑", s));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (subs.length > 0) {
|
|
40
|
+
if (lines.length > 0) lines.push("");
|
|
41
|
+
lines.push("#### Subtypes:");
|
|
42
|
+
for (const s of subs) {
|
|
43
|
+
lines.push(...formatHierEntry("", s));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
@@ -20,7 +20,7 @@ export function formatTestResults(result) {
|
|
|
20
20
|
for (const f of result.failures) {
|
|
21
21
|
const status =
|
|
22
22
|
f.status === "FAILURE" ? red(f.status) : bold(red(f.status));
|
|
23
|
-
console.log(`${status}
|
|
23
|
+
console.log(`${status} [M] \`${f.class}#${f.method}\``);
|
|
24
24
|
if (f.trace) {
|
|
25
25
|
const traceLines = f.trace.split("\n").slice(0, 10);
|
|
26
26
|
for (const line of traceLines) console.log(` ${line}`);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Test status formatter.
|
|
2
|
+
// Formats test session snapshots and streaming JSONL events.
|
|
3
|
+
// Uses FQMN with # separator and [M] badges for navigation.
|
|
4
|
+
|
|
5
|
+
import { red, green, yellow, bold, dim } from "../color.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format a test status snapshot (JSON from /test/status).
|
|
9
|
+
* Shows summary line + failure/ignored details based on filter.
|
|
10
|
+
*/
|
|
11
|
+
export function formatTestStatus(result) {
|
|
12
|
+
const state = result.state === "running"
|
|
13
|
+
? " (running)"
|
|
14
|
+
: result.state === "stopped"
|
|
15
|
+
? " (stopped)"
|
|
16
|
+
: "";
|
|
17
|
+
|
|
18
|
+
const label = result.label || result.session;
|
|
19
|
+
const progress = result.state === "running"
|
|
20
|
+
? `${result.completed}/${result.total}`
|
|
21
|
+
: `${result.total}/${result.total}`;
|
|
22
|
+
|
|
23
|
+
console.log(
|
|
24
|
+
`#### ${label} — ${progress}${state}, ${formatCounts(result)}`,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const entries = result.entries || result.failures || [];
|
|
28
|
+
if (entries.length > 0) {
|
|
29
|
+
console.log();
|
|
30
|
+
for (const f of entries) {
|
|
31
|
+
formatEntry(f);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format a single JSONL event line from the stream.
|
|
38
|
+
* Returns true if something was printed.
|
|
39
|
+
*/
|
|
40
|
+
export function formatTestEvent(jsonLine) {
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(jsonLine);
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (parsed.event === "started") {
|
|
49
|
+
// Header already printed by test-run, skip in stream
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (parsed.event === "finished") {
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(formatCounts(parsed));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (parsed.event === "case") {
|
|
60
|
+
const status = parsed.status;
|
|
61
|
+
const fqmn = parsed.fqmn;
|
|
62
|
+
const time = formatTime(parsed.time);
|
|
63
|
+
|
|
64
|
+
if (status === "PASS") {
|
|
65
|
+
console.log(`${green("PASS")} [M] \`${fqmn}\` ${dim(time)}`);
|
|
66
|
+
} else if (status === "FAIL") {
|
|
67
|
+
console.log(`${red("FAIL")} [M] \`${fqmn}\` ${dim(time)}`);
|
|
68
|
+
printTrace(parsed);
|
|
69
|
+
} else if (status === "ERROR") {
|
|
70
|
+
console.log(
|
|
71
|
+
`${bold(red("ERROR"))} [M] \`${fqmn}\` ${dim(time)}`,
|
|
72
|
+
);
|
|
73
|
+
printTrace(parsed);
|
|
74
|
+
} else if (status === "IGNORED") {
|
|
75
|
+
console.log(
|
|
76
|
+
`${yellow("IGNORED")} [M] \`${fqmn}\``,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format a single entry from status snapshot (PASS, FAIL, ERROR, IGNORED).
|
|
87
|
+
*/
|
|
88
|
+
function formatEntry(f) {
|
|
89
|
+
const status = f.status;
|
|
90
|
+
const fqmn = f.fqmn;
|
|
91
|
+
const time = formatTime(f.time);
|
|
92
|
+
|
|
93
|
+
if (status === "PASS") {
|
|
94
|
+
console.log(`${green("PASS")} [M] \`${fqmn}\` ${dim(time)}`);
|
|
95
|
+
} else if (status === "FAIL") {
|
|
96
|
+
console.log(`${red("FAIL")} [M] \`${fqmn}\` ${dim(time)}`);
|
|
97
|
+
printTrace(f);
|
|
98
|
+
} else if (status === "ERROR") {
|
|
99
|
+
console.log(
|
|
100
|
+
`${bold(red("ERROR"))} [M] \`${fqmn}\` ${dim(time)}`,
|
|
101
|
+
);
|
|
102
|
+
printTrace(f);
|
|
103
|
+
} else if (status === "IGNORED") {
|
|
104
|
+
console.log(`${yellow("IGNORED")} [M] \`${fqmn}\``);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function printTrace(parsed) {
|
|
109
|
+
if (parsed.expected != null && parsed.actual != null) {
|
|
110
|
+
console.log(` Expected: ${parsed.expected}`);
|
|
111
|
+
console.log(` Actual: ${parsed.actual}`);
|
|
112
|
+
}
|
|
113
|
+
if (parsed.trace) {
|
|
114
|
+
const lines = parsed.trace.split("\n").slice(0, 10);
|
|
115
|
+
for (const line of lines) console.log(` ${line}`);
|
|
116
|
+
if (parsed.trace.split("\n").length > 10) console.log(" ...");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function formatCounts(r) {
|
|
121
|
+
const parts = [`${r.total} tests`];
|
|
122
|
+
if (r.passed > 0) parts.push(green(`${r.passed} passed`));
|
|
123
|
+
if (r.failed > 0) parts.push(red(`${r.failed} failed`));
|
|
124
|
+
if (r.errors > 0) parts.push(red(`${r.errors} errors`));
|
|
125
|
+
if (r.ignored > 0) parts.push(yellow(`${r.ignored} ignored`));
|
|
126
|
+
const time = Number.isFinite(r.time) ? r.time : 0;
|
|
127
|
+
parts.push(`${time.toFixed(1)}s`);
|
|
128
|
+
return parts.join(", ");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatTime(t) {
|
|
132
|
+
if (t == null || !Number.isFinite(t)) return "";
|
|
133
|
+
return `(${t.toFixed(3)}s)`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Format the header printed at test run start.
|
|
138
|
+
* Markdown-friendly: heading + backtick-wrapped values.
|
|
139
|
+
*/
|
|
140
|
+
export function formatTestRunHeader(result) {
|
|
141
|
+
const label = result.label || result.session;
|
|
142
|
+
const total = result.total ? ` (${result.total} tests)` : "";
|
|
143
|
+
const parts = [`#### Test: ${label}${total}`];
|
|
144
|
+
parts.push(`Launch: \`${result.session}\``);
|
|
145
|
+
if (result.project) parts.push(`Project: \`${result.project}\``);
|
|
146
|
+
if (result.runner) parts.push(`Runner: ${result.runner}`);
|
|
147
|
+
return parts.join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Onboarding guide printed after non-blocking launch.
|
|
152
|
+
* Markdown-friendly: bold sections, backtick commands.
|
|
153
|
+
*/
|
|
154
|
+
export function testRunGuide(session) {
|
|
155
|
+
return `
|
|
156
|
+
**Status** — snapshot or live stream:
|
|
157
|
+
\`jdt test status ${session}\` failures only (default)
|
|
158
|
+
\`jdt test status ${session} -f\` stream live until done
|
|
159
|
+
\`jdt test status ${session} --all\` all tests including passed
|
|
160
|
+
\`jdt test status ${session} --ignored\` only skipped/disabled tests
|
|
161
|
+
|
|
162
|
+
**Console** — raw stdout/stderr of the test JVM:
|
|
163
|
+
\`jdt launch logs ${session}\`
|
|
164
|
+
|
|
165
|
+
**Manage:**
|
|
166
|
+
\`jdt test sessions\` list active/completed sessions
|
|
167
|
+
\`jdt launch stop ${session}\` abort
|
|
168
|
+
\`jdt launch clear ${session}\` remove
|
|
169
|
+
|
|
170
|
+
**Navigate** — FQMNs from status output are copy-pasteable:
|
|
171
|
+
\`jdt source <FQMN>\` view test source
|
|
172
|
+
\`jdt test run <FQMN> -f\` re-run single test
|
|
173
|
+
|
|
174
|
+
Add \`-q\` to suppress this guide.`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Stream test status via JSONL and format output.
|
|
179
|
+
* Shared by test-run -f and test-status -f.
|
|
180
|
+
* Returns exit code: 0 if all pass, 1 if any failures.
|
|
181
|
+
*/
|
|
182
|
+
export async function followTestStream(session, args) {
|
|
183
|
+
const { getStreamLines } = await import("../client.mjs");
|
|
184
|
+
|
|
185
|
+
let filter = "failures";
|
|
186
|
+
if (args.includes("--all")) filter = "all";
|
|
187
|
+
else if (args.includes("--ignored")) filter = "ignored";
|
|
188
|
+
|
|
189
|
+
const url = `/test/status/stream?session=${encodeURIComponent(session)}&filter=${filter}`;
|
|
190
|
+
|
|
191
|
+
let detached = false;
|
|
192
|
+
const onSigint = () => {
|
|
193
|
+
detached = true;
|
|
194
|
+
process.stdout.write("\n");
|
|
195
|
+
process.exit(0);
|
|
196
|
+
};
|
|
197
|
+
process.on("SIGINT", onSigint);
|
|
198
|
+
|
|
199
|
+
let hasFailed = false;
|
|
200
|
+
try {
|
|
201
|
+
await getStreamLines(url, (line) => {
|
|
202
|
+
formatTestEvent(line);
|
|
203
|
+
try {
|
|
204
|
+
const ev = JSON.parse(line);
|
|
205
|
+
if (ev.event === "finished"
|
|
206
|
+
&& (ev.failed > 0 || ev.errors > 0)) {
|
|
207
|
+
hasFailed = true;
|
|
208
|
+
}
|
|
209
|
+
} catch { /* ignore parse errors */ }
|
|
210
|
+
});
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if (!detached) {
|
|
213
|
+
console.error(e.message);
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
return 0;
|
|
217
|
+
} finally {
|
|
218
|
+
process.removeListener("SIGINT", onSigint);
|
|
219
|
+
}
|
|
220
|
+
return hasFailed ? 1 : 0;
|
|
221
|
+
}
|
package/src/commands/test.mjs
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { get } from "../client.mjs";
|
|
2
|
-
import { extractPositional, parseFlags, parseFqmn } from "../args.mjs";
|
|
3
|
-
import { formatTestResults } from "../format/test-results.mjs";
|
|
4
|
-
|
|
5
|
-
export async function test(args) {
|
|
6
|
-
const pos = extractPositional(args);
|
|
7
|
-
const flags = parseFlags(args);
|
|
8
|
-
let url = "/test?";
|
|
9
|
-
const parsed = parseFqmn(pos[0]);
|
|
10
|
-
const fqn = parsed.className;
|
|
11
|
-
if (fqn) {
|
|
12
|
-
url += `class=${encodeURIComponent(fqn)}`;
|
|
13
|
-
const method = parsed.method || pos[1];
|
|
14
|
-
if (method) url += `&method=${encodeURIComponent(method)}`;
|
|
15
|
-
} else if (flags.project) {
|
|
16
|
-
url += `project=${encodeURIComponent(flags.project)}`;
|
|
17
|
-
if (flags.package)
|
|
18
|
-
url += `&package=${encodeURIComponent(flags.package)}`;
|
|
19
|
-
} else {
|
|
20
|
-
console.error(
|
|
21
|
-
"Usage: test <FQN> [method] | test --project <name> [--package <pkg>]",
|
|
22
|
-
);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
if (flags.timeout) url += `&timeout=${flags.timeout}`;
|
|
26
|
-
if (args.includes("--no-refresh")) url += "&no-refresh";
|
|
27
|
-
const result = await get(url, 300_000);
|
|
28
|
-
if (result.error) {
|
|
29
|
-
console.error(result.error);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
formatTestResults(result);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const help = `Run JUnit tests via Eclipse's built-in test runner.
|
|
36
|
-
|
|
37
|
-
Usage: jdt test <FQN>[#method]
|
|
38
|
-
jdt test --project <name> [--package <pkg>]
|
|
39
|
-
|
|
40
|
-
Flags:
|
|
41
|
-
--project <name> run all tests in a project
|
|
42
|
-
--package <pkg> narrow to a specific package (with --project)
|
|
43
|
-
--timeout <sec> test run timeout in seconds (default: 120)
|
|
44
|
-
|
|
45
|
-
Examples:
|
|
46
|
-
jdt test app.m8ws.utils.ObjectMapperTest
|
|
47
|
-
jdt test app.m8ws.utils.ObjectMapperTest#testSerialize
|
|
48
|
-
jdt test --project m8-server`;
|