Skip to content

Commit

Permalink
Merge pull request #429 from vim-denops/stacktrace-on-error
Browse files Browse the repository at this point in the history
👍 Show stack-trace on plugin error
  • Loading branch information
lambdalisue authored Nov 28, 2024
2 parents 0c72261 + 95a8dad commit e3fd503
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 6 deletions.
4 changes: 3 additions & 1 deletion denops/@denops-private/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@ class Plugin {
try {
return await this.#denops.dispatcher[fn](...args);
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
const errMsg = err instanceof Error
? err.stack ?? err.message // Prefer 'stack' if available
: String(err);
throw new Error(
`Failed to call '${fn}' API in '${this.name}': ${errMsg}`,
);
Expand Down
62 changes: 59 additions & 3 deletions tests/denops/runtime/functions/denops/request_async_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
assertArrayIncludes,
assertEquals,
assertObjectMatch,
assertStringIncludes,
} from "jsr:@std/assert@^1.0.1";
import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts";
import { resolveTestDataPath } from "/denops-testdata/resolve.ts";
Expand Down Expand Up @@ -115,6 +116,53 @@ testHost({
);
});

await t.step("if the dispatcher method throws an error", async (t) => {
await t.step("returns immediately", async () => {
await host.call("execute", [
"let g:__test_denops_events = []",
"call denops#request_async('dummy', 'fail', ['foo'], 'TestDenopsRequestAsyncSuccess', 'TestDenopsRequestAsyncFailure')",
"let g:__test_denops_events_after_called = g:__test_denops_events->copy()",
], "");

assertEquals(
await host.call("eval", "g:__test_denops_events_after_called"),
[],
);
});

await t.step("calls failure callback", async () => {
await wait(() => host.call("eval", "len(g:__test_denops_events)"));
const result = await host.call(
"eval",
"g:__test_denops_events",
// deno-lint-ignore no-explicit-any
) as any[];
assertObjectMatch(
result,
{
0: [
"TestDenopsRequestAsyncFailure:Called",
[
{
name: "Error",
},
],
],
},
);
const message = result[0][1][0].message as string;
assertStringIncludes(
message,
"Failed to call 'fail' API in 'dummy': Error: Dummy failure",
);
assertStringIncludes(
message,
"dummy_dispatcher_plugin.ts:19:13",
"Error message should include the where the original error occurred",
);
});
});

await t.step("if the dispatcher method is not exist", async (t) => {
await t.step("returns immediately", async () => {
await host.call("execute", [
Expand All @@ -131,21 +179,29 @@ testHost({

await t.step("calls failure callback", async () => {
await wait(() => host.call("eval", "len(g:__test_denops_events)"));
const result = await host.call(
"eval",
"g:__test_denops_events",
// deno-lint-ignore no-explicit-any
) as any[];
assertObjectMatch(
await host.call("eval", "g:__test_denops_events") as unknown[],
result,
{
0: [
"TestDenopsRequestAsyncFailure:Called",
[
{
message:
"Failed to call 'not_exist_method' API in 'dummy': this[#denops].dispatcher[fn] is not a function",
name: "Error",
},
],
],
},
);
const message = result[0][1][0].message as string;
assertStringIncludes(
message,
"Failed to call 'not_exist_method' API in 'dummy': TypeError: this[#denops].dispatcher[fn] is not a function",
);
});
});
});
Expand Down
30 changes: 28 additions & 2 deletions tests/denops/runtime/functions/denops/request_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { assertEquals, assertRejects } from "jsr:@std/assert@^1.0.1";
import {
assertEquals,
assertInstanceOf,
assertRejects,
assertStringIncludes,
} from "jsr:@std/assert@^1.0.1";
import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts";
import { resolveTestDataPath } from "/denops-testdata/resolve.ts";
import { testHost } from "/denops-testutil/host.ts";
Expand Down Expand Up @@ -66,6 +71,27 @@ testHost({
assertEquals(result, { result: "OK", args: ["foo"] });
});

await t.step("if the dispatcher method throws an error", async (t) => {
await t.step("throws an error", async () => {
const result = await host.call(
"denops#request",
"dummy",
"fail",
["foo"],
).catch((e) => e);
assertInstanceOf(result, Error);
assertStringIncludes(
result.message,
"Failed to call 'fail' API in 'dummy': Error: Dummy failure",
);
assertStringIncludes(
result.message,
"dummy_dispatcher_plugin.ts:19:13",
"Error message should include the where the original error occurred",
);
});
});

await t.step("if the dispatcher method is not exist", async (t) => {
await t.step("throws an error", async () => {
await assertRejects(
Expand All @@ -77,7 +103,7 @@ testHost({
["foo"],
),
Error,
"Failed to call 'not_exist_method' API in 'dummy'",
"Failed to call 'not_exist_method' API in 'dummy': TypeError: this[#denops].dispatcher[fn] is not a function",
);
});
});
Expand Down
4 changes: 4 additions & 0 deletions tests/denops/testdata/dummy_dispatcher_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ export const main: Entrypoint = (denops) => {
);
return { result: "OK", args };
},
fail: async () => {
await delay(MIMIC_DISPATCHER_METHOD_DELAY);
throw new Error("Dummy failure");
},
};
};

0 comments on commit e3fd503

Please sign in to comment.