Skip to content

Commit e3fd503

Browse files
authored
Merge pull request #429 from vim-denops/stacktrace-on-error
👍 Show stack-trace on plugin error
2 parents 0c72261 + 95a8dad commit e3fd503

File tree

4 files changed

+94
-6
lines changed

4 files changed

+94
-6
lines changed

denops/@denops-private/service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,9 @@ class Plugin {
275275
try {
276276
return await this.#denops.dispatcher[fn](...args);
277277
} catch (err) {
278-
const errMsg = err instanceof Error ? err.message : String(err);
278+
const errMsg = err instanceof Error
279+
? err.stack ?? err.message // Prefer 'stack' if available
280+
: String(err);
279281
throw new Error(
280282
`Failed to call '${fn}' API in '${this.name}': ${errMsg}`,
281283
);

tests/denops/runtime/functions/denops/request_async_test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
assertArrayIncludes,
33
assertEquals,
44
assertObjectMatch,
5+
assertStringIncludes,
56
} from "jsr:@std/assert@^1.0.1";
67
import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts";
78
import { resolveTestDataPath } from "/denops-testdata/resolve.ts";
@@ -115,6 +116,53 @@ testHost({
115116
);
116117
});
117118

119+
await t.step("if the dispatcher method throws an error", async (t) => {
120+
await t.step("returns immediately", async () => {
121+
await host.call("execute", [
122+
"let g:__test_denops_events = []",
123+
"call denops#request_async('dummy', 'fail', ['foo'], 'TestDenopsRequestAsyncSuccess', 'TestDenopsRequestAsyncFailure')",
124+
"let g:__test_denops_events_after_called = g:__test_denops_events->copy()",
125+
], "");
126+
127+
assertEquals(
128+
await host.call("eval", "g:__test_denops_events_after_called"),
129+
[],
130+
);
131+
});
132+
133+
await t.step("calls failure callback", async () => {
134+
await wait(() => host.call("eval", "len(g:__test_denops_events)"));
135+
const result = await host.call(
136+
"eval",
137+
"g:__test_denops_events",
138+
// deno-lint-ignore no-explicit-any
139+
) as any[];
140+
assertObjectMatch(
141+
result,
142+
{
143+
0: [
144+
"TestDenopsRequestAsyncFailure:Called",
145+
[
146+
{
147+
name: "Error",
148+
},
149+
],
150+
],
151+
},
152+
);
153+
const message = result[0][1][0].message as string;
154+
assertStringIncludes(
155+
message,
156+
"Failed to call 'fail' API in 'dummy': Error: Dummy failure",
157+
);
158+
assertStringIncludes(
159+
message,
160+
"dummy_dispatcher_plugin.ts:19:13",
161+
"Error message should include the where the original error occurred",
162+
);
163+
});
164+
});
165+
118166
await t.step("if the dispatcher method is not exist", async (t) => {
119167
await t.step("returns immediately", async () => {
120168
await host.call("execute", [
@@ -131,21 +179,29 @@ testHost({
131179

132180
await t.step("calls failure callback", async () => {
133181
await wait(() => host.call("eval", "len(g:__test_denops_events)"));
182+
const result = await host.call(
183+
"eval",
184+
"g:__test_denops_events",
185+
// deno-lint-ignore no-explicit-any
186+
) as any[];
134187
assertObjectMatch(
135-
await host.call("eval", "g:__test_denops_events") as unknown[],
188+
result,
136189
{
137190
0: [
138191
"TestDenopsRequestAsyncFailure:Called",
139192
[
140193
{
141-
message:
142-
"Failed to call 'not_exist_method' API in 'dummy': this[#denops].dispatcher[fn] is not a function",
143194
name: "Error",
144195
},
145196
],
146197
],
147198
},
148199
);
200+
const message = result[0][1][0].message as string;
201+
assertStringIncludes(
202+
message,
203+
"Failed to call 'not_exist_method' API in 'dummy': TypeError: this[#denops].dispatcher[fn] is not a function",
204+
);
149205
});
150206
});
151207
});

tests/denops/runtime/functions/denops/request_test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { assertEquals, assertRejects } from "jsr:@std/assert@^1.0.1";
1+
import {
2+
assertEquals,
3+
assertInstanceOf,
4+
assertRejects,
5+
assertStringIncludes,
6+
} from "jsr:@std/assert@^1.0.1";
27
import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts";
38
import { resolveTestDataPath } from "/denops-testdata/resolve.ts";
49
import { testHost } from "/denops-testutil/host.ts";
@@ -66,6 +71,27 @@ testHost({
6671
assertEquals(result, { result: "OK", args: ["foo"] });
6772
});
6873

74+
await t.step("if the dispatcher method throws an error", async (t) => {
75+
await t.step("throws an error", async () => {
76+
const result = await host.call(
77+
"denops#request",
78+
"dummy",
79+
"fail",
80+
["foo"],
81+
).catch((e) => e);
82+
assertInstanceOf(result, Error);
83+
assertStringIncludes(
84+
result.message,
85+
"Failed to call 'fail' API in 'dummy': Error: Dummy failure",
86+
);
87+
assertStringIncludes(
88+
result.message,
89+
"dummy_dispatcher_plugin.ts:19:13",
90+
"Error message should include the where the original error occurred",
91+
);
92+
});
93+
});
94+
6995
await t.step("if the dispatcher method is not exist", async (t) => {
7096
await t.step("throws an error", async () => {
7197
await assertRejects(
@@ -77,7 +103,7 @@ testHost({
77103
["foo"],
78104
),
79105
Error,
80-
"Failed to call 'not_exist_method' API in 'dummy'",
106+
"Failed to call 'not_exist_method' API in 'dummy': TypeError: this[#denops].dispatcher[fn] is not a function",
81107
);
82108
});
83109
});

tests/denops/testdata/dummy_dispatcher_plugin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,9 @@ export const main: Entrypoint = (denops) => {
1414
);
1515
return { result: "OK", args };
1616
},
17+
fail: async () => {
18+
await delay(MIMIC_DISPATCHER_METHOD_DELAY);
19+
throw new Error("Dummy failure");
20+
},
1721
};
1822
};

0 commit comments

Comments
 (0)