From 98ba3ef12d0d6c36a355308ef8dc5b40177cb39d Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 26 Nov 2024 17:30:12 +0900 Subject: [PATCH 1/3] :herb: Improve test for non exist method on denops#request --- tests/denops/runtime/functions/denops/request_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/denops/runtime/functions/denops/request_test.ts b/tests/denops/runtime/functions/denops/request_test.ts index c87f5534..ce535dd9 100644 --- a/tests/denops/runtime/functions/denops/request_test.ts +++ b/tests/denops/runtime/functions/denops/request_test.ts @@ -77,7 +77,7 @@ testHost({ ["foo"], ), Error, - "Failed to call 'not_exist_method' API in 'dummy'", + "Failed to call 'not_exist_method' API in 'dummy': this[#denops].dispatcher[fn] is not a function", ); }); }); From 74715fa469e9680023ae1be5d6e2c7d3644715ee Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 26 Nov 2024 17:30:44 +0900 Subject: [PATCH 2/3] :herb: Add test for calling method that fails internally --- .../functions/denops/request_async_test.ts | 34 +++++++++++++++++++ .../runtime/functions/denops/request_test.ts | 16 +++++++++ .../testdata/dummy_dispatcher_plugin.ts | 4 +++ 3 files changed, 54 insertions(+) diff --git a/tests/denops/runtime/functions/denops/request_async_test.ts b/tests/denops/runtime/functions/denops/request_async_test.ts index 6353cf39..15c5919f 100644 --- a/tests/denops/runtime/functions/denops/request_async_test.ts +++ b/tests/denops/runtime/functions/denops/request_async_test.ts @@ -115,6 +115,40 @@ 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)")); + assertObjectMatch( + await host.call("eval", "g:__test_denops_events") as unknown[], + { + 0: [ + "TestDenopsRequestAsyncFailure:Called", + [ + { + message: + "Failed to call 'fail' API in 'dummy': Dummy failure", + name: "Error", + }, + ], + ], + }, + ); + }); + }); + await t.step("if the dispatcher method is not exist", async (t) => { await t.step("returns immediately", async () => { await host.call("execute", [ diff --git a/tests/denops/runtime/functions/denops/request_test.ts b/tests/denops/runtime/functions/denops/request_test.ts index ce535dd9..0023949d 100644 --- a/tests/denops/runtime/functions/denops/request_test.ts +++ b/tests/denops/runtime/functions/denops/request_test.ts @@ -66,6 +66,22 @@ 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 () => { + await assertRejects( + () => + host.call( + "denops#request", + "dummy", + "fail", + ["foo"], + ), + Error, + "Failed to call 'fail' API in 'dummy': Dummy failure", + ); + }); + }); + await t.step("if the dispatcher method is not exist", async (t) => { await t.step("throws an error", async () => { await assertRejects( diff --git a/tests/denops/testdata/dummy_dispatcher_plugin.ts b/tests/denops/testdata/dummy_dispatcher_plugin.ts index 63442343..2ce1fffd 100644 --- a/tests/denops/testdata/dummy_dispatcher_plugin.ts +++ b/tests/denops/testdata/dummy_dispatcher_plugin.ts @@ -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"); + }, }; }; From 95a8dad19d86c1ddc5b0ba2ec90250c498bef885 Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 26 Nov 2024 17:58:47 +0900 Subject: [PATCH 3/3] :+1: Show stack-trace on plugin error --- denops/@denops-private/service.ts | 4 ++- .../functions/denops/request_async_test.ts | 34 +++++++++++++++---- .../runtime/functions/denops/request_test.ts | 34 ++++++++++++------- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 49efb243..38091be9 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -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}`, ); diff --git a/tests/denops/runtime/functions/denops/request_async_test.ts b/tests/denops/runtime/functions/denops/request_async_test.ts index 15c5919f..4ad4b314 100644 --- a/tests/denops/runtime/functions/denops/request_async_test.ts +++ b/tests/denops/runtime/functions/denops/request_async_test.ts @@ -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"; @@ -131,21 +132,34 @@ 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 'fail' API in 'dummy': Dummy failure", 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", + ); }); }); @@ -165,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", + ); }); }); }); diff --git a/tests/denops/runtime/functions/denops/request_test.ts b/tests/denops/runtime/functions/denops/request_test.ts index 0023949d..a5fa17ce 100644 --- a/tests/denops/runtime/functions/denops/request_test.ts +++ b/tests/denops/runtime/functions/denops/request_test.ts @@ -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"; @@ -68,16 +73,21 @@ testHost({ await t.step("if the dispatcher method throws an error", async (t) => { await t.step("throws an error", async () => { - await assertRejects( - () => - host.call( - "denops#request", - "dummy", - "fail", - ["foo"], - ), - Error, - "Failed to call 'fail' API in 'dummy': Dummy failure", + 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", ); }); }); @@ -93,7 +103,7 @@ testHost({ ["foo"], ), Error, - "Failed to call 'not_exist_method' API in 'dummy': this[#denops].dispatcher[fn] is not a function", + "Failed to call 'not_exist_method' API in 'dummy': TypeError: this[#denops].dispatcher[fn] is not a function", ); }); });