From b63030bf41ea42df4f9ac1310f704db65f745d09 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 12 May 2024 21:18:04 +0900 Subject: [PATCH 01/99] :boom: Upgrade supported versions - Deno: 1.38.x -> 1.45.x - Vim: 9.0.2189 -> 9.1.0448 - Neovim: 0.9.4 -> 0.10.0 --- .github/workflows/test.yml | 6 +++--- README.md | 6 +++--- autoload/health/denops.vim | 6 +++--- plugin/denops.vim | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 67ebaf46..d7b21c25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,11 +48,11 @@ jobs: - macos-latest - ubuntu-latest version: - - "1.38.x" + - "1.45.x" - "1.x" host_version: - - vim: "v9.0.2189" - nvim: "v0.9.4" + - vim: "v9.1.0448" + nvim: "v0.10.0" runs-on: ${{ matrix.runner }} steps: - run: git config --global core.autocrlf false diff --git a/README.md b/README.md index e6bd7e5e..f7a50f8f 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ Denops
An ecosystem for Vim/Neovim enabling developers to write plugins in Deno. -[![Deno 1.38.5 or above](https://img.shields.io/badge/Deno-Support%201.38.5-yellowgreen.svg?logo=deno)](https://github.com/denoland/deno/tree/v1.38.5) -[![Vim 9.0.2189 or above](https://img.shields.io/badge/Vim-Support%209.0.2189-yellowgreen.svg?logo=vim)](https://github.com/vim/vim/tree/v9.0.2189) -[![Neovim 0.9.4 or above](https://img.shields.io/badge/Neovim-Support%200.9.4-yellowgreen.svg?logo=neovim&logoColor=white)](https://github.com/neovim/neovim/tree/v0.9.4) +[![Deno 1.45.0 or above](https://img.shields.io/badge/Deno-Support%201.45.0-yellowgreen.svg?logo=deno)](https://github.com/denoland/deno/tree/v1.45.0) +[![Vim 9.1.0448 or above](https://img.shields.io/badge/Vim-Support%209.1.0448-yellowgreen.svg?logo=vim)](https://github.com/vim/vim/tree/v9.1.0448) +[![Neovim 0.10.0 or above](https://img.shields.io/badge/Neovim-Support%200.10.0-yellowgreen.svg?logo=neovim&logoColor=white)](https://github.com/neovim/neovim/tree/v0.10.0) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![deno land](http://img.shields.io/badge/available%20on-deno.land/x/denops__core-lightgrey.svg?logo=deno)](https://deno.land/x/denops_core) diff --git a/autoload/health/denops.vim b/autoload/health/denops.vim index b1913906..18f82e40 100644 --- a/autoload/health/denops.vim +++ b/autoload/health/denops.vim @@ -1,6 +1,6 @@ -const s:DENO_VERSION = '1.38.5' -const s:VIM_VERSION = '9.0.2189' -const s:NEOVIM_VERSION = '0.9.4' +const s:DENO_VERSION = '1.45.0' +const s:VIM_VERSION = '9.1.0448' +const s:NEOVIM_VERSION = '0.10.0' function! s:compare_version(v1, v2) abort let l:v1 = map(split(a:v1, '\.'), { _, v -> v + 0 }) diff --git a/plugin/denops.vim b/plugin/denops.vim index a7b58274..06a9c248 100644 --- a/plugin/denops.vim +++ b/plugin/denops.vim @@ -3,9 +3,9 @@ if exists('g:loaded_denops') endif let g:loaded_denops = 1 -if !get(g:, 'denops_disable_version_check') && !has('nvim-0.9.4') && !has('patch-9.0.2189') +if !get(g:, 'denops_disable_version_check') && !has('nvim-0.10.0') && !has('patch-9.1.0448') echohl WarningMsg - echomsg '[denops] Denops requires Vim 9.0.2189 or Neovim 0.9.4. See ":h g:denops_disable_version_check" to disable this check.' + echomsg '[denops] Denops requires Vim 9.1.0448 or Neovim 0.10.0. See ":h g:denops_disable_version_check" to disable this check.' echohl None finish endif From d56dd0929ed22f05a4db6181c8852cbd22b14731 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 13 May 2024 02:45:28 +0900 Subject: [PATCH 02/99] :coffee: Refine deno tasks and CIs --- .github/workflows/test.yml | 29 +++++++++++++++-------------- .github/workflows/update.yml | 2 +- deno.jsonc | 7 +++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d7b21c25..eab23ad7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,46 +53,47 @@ jobs: host_version: - vim: "v9.1.0448" nvim: "v0.10.0" + runs-on: ${{ matrix.runner }} + steps: - run: git config --global core.autocrlf false if: runner.os == 'Windows' + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v1.1.4 with: deno-version: "${{ matrix.version }}" + - uses: rhysd/action-setup-vim@v1 id: vim with: version: "${{ matrix.host_version.vim }}" - - name: Check Vim - run: | - echo ${DENOPS_TEST_VIM} - ${DENOPS_TEST_VIM} --version - env: - DENOPS_TEST_VIM: ${{ steps.vim.outputs.executable }} + - uses: rhysd/action-setup-vim@v1 id: nvim with: neovim: true version: "${{ matrix.host_version.nvim }}" - - name: Check Neovim + + - name: Export executables run: | - echo ${DENOPS_TEST_NVIM} - ${DENOPS_TEST_NVIM} --version - env: - DENOPS_TEST_NVIM: ${{ steps.nvim.outputs.executable }} + echo "DENOPS_TEST_VIM_EXECUTABLE=${{ steps.vim.outputs.executable }}" >> "$GITHUB_ENV" + echo "DENOPS_TEST_NVIM_EXECUTABLE=${{ steps.nvim.outputs.executable }}" >> "$GITHUB_ENV" + - name: Perform pre-cache run: deno cache ./denops/@denops-private/mod.ts + - name: Test run: deno task test:coverage env: DENOPS_TEST_DENOPS_PATH: "./" - DENOPS_TEST_VIM_EXECUTABLE: ${{ steps.vim.outputs.executable }} - DENOPS_TEST_NVIM_EXECUTABLE: ${{ steps.nvim.outputs.executable }} - timeout-minutes: 5 + timeout-minutes: 10 + - run: | deno task coverage --lcov > coverage.lcov + - uses: codecov/codecov-action@v4 with: os: ${{ runner.os }} diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index c130c366..ba304678 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -18,7 +18,7 @@ jobs: git config user.name github-actions[bot] git config user.email github-actions[bot]@users.noreply.github.com - name: Update dependencies and commit changes - run: deno task -q upgrade:commit --summary ../title.txt --report ../body.md + run: deno task -q update:commit --summary ../title.txt --report ../body.md - name: Check result id: result uses: andstor/file-existence-action@v2 diff --git a/deno.jsonc b/deno.jsonc index d5f1fc7a..4eabb98e 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,11 +1,10 @@ { - "lock": false, "tasks": { + "check": "deno check **/*.ts", "test": "deno test -A --parallel --shuffle --doc", "test:coverage": "deno task test --coverage=.coverage", - "check": "deno check ./**/*.ts", "coverage": "deno coverage .coverage --exclude=cli.ts --exclude=worker.ts --exclude=testdata/", - "upgrade": "deno run -q -A https://deno.land/x/molt@0.14.2/cli.ts ./**/*.ts", - "upgrade:commit": "deno task -q upgrade --commit --prefix :package: --pre-commit=fmt" + "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", + "update:commit": "deno task -q update --commit --pre-commit=fmt,lint" } } From 59921456821a13a7de68fb2b293e0c355ffd67bb Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 13 May 2024 02:45:40 +0900 Subject: [PATCH 03/99] :boom: Use JSR imports instead --- denops/@denops-private/cli.ts | 6 +++--- denops/@denops-private/denops.ts | 11 +++-------- denops/@denops-private/denops_test.ts | 15 ++++++--------- denops/@denops-private/error.ts | 4 ++-- denops/@denops-private/error_test.ts | 4 ++-- denops/@denops-private/host.ts | 2 +- denops/@denops-private/host/nvim.ts | 9 +++------ denops/@denops-private/host/nvim_test.ts | 13 +++++-------- denops/@denops-private/host/vim.ts | 8 ++++---- denops/@denops-private/host/vim_test.ts | 15 ++++++--------- denops/@denops-private/host_test.ts | 10 +++++----- denops/@denops-private/service.ts | 11 ++++------- denops/@denops-private/service_test.ts | 10 +++++----- denops/@denops-private/testutil/conf.ts | 8 +++----- denops/@denops-private/testutil/conf_test.ts | 2 +- denops/@denops-private/util.ts | 4 ++-- denops/@denops-private/version.ts | 9 ++++----- denops/@denops-private/worker.ts | 6 +++--- 18 files changed, 62 insertions(+), 85 deletions(-) diff --git a/denops/@denops-private/cli.ts b/denops/@denops-private/cli.ts index 00832196..8a31b872 100644 --- a/denops/@denops-private/cli.ts +++ b/denops/@denops-private/cli.ts @@ -1,10 +1,10 @@ import { readableStreamFromWorker, writableStreamFromWorker, -} from "https://deno.land/x/workerio@v3.1.0/mod.ts"; -import { parseArgs } from "https://deno.land/std@0.217.0/cli/parse_args.ts"; +} from "jsr:@lambdalisue/workerio@4.0.0"; +import { parseArgs } from "jsr:@std/cli/parse-args"; -const script = new URL("./worker.ts", import.meta.url); +const script = import.meta.resolve("./worker.ts"); async function handleConn( conn: Deno.Conn, diff --git a/denops/@denops-private/denops.ts b/denops/@denops-private/denops.ts index a659346d..ea1cce72 100644 --- a/denops/@denops-private/denops.ts +++ b/denops/@denops-private/denops.ts @@ -1,11 +1,6 @@ -import type { - Context, - Denops, - Dispatcher, - Meta, -} from "https://deno.land/x/denops_core@v6.0.5/mod.ts"; -import { BatchError } from "https://deno.land/x/denops_core@v6.0.5/mod.ts"; -import { ensure, is } from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; +import type { Context, Denops, Dispatcher, Meta } from "jsr:@denops/core@6.0.6"; +import { BatchError } from "jsr:@denops/core@6.0.6"; +import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; import type { Host as HostOrigin } from "./host.ts"; import type { Service as ServiceOrigin } from "./service.ts"; diff --git a/denops/@denops-private/denops_test.ts b/denops/@denops-private/denops_test.ts index 0068a82a..717a9a01 100644 --- a/denops/@denops-private/denops_test.ts +++ b/denops/@denops-private/denops_test.ts @@ -1,12 +1,9 @@ -import type { Meta } from "https://deno.land/x/denops_core@v6.0.5/mod.ts"; -import { assertEquals } from "https://deno.land/std@0.217.0/assert/mod.ts"; -import { - assertSpyCall, - stub, -} from "https://deno.land/std@0.217.0/testing/mock.ts"; -import { DenopsImpl, Host, Service } from "./denops.ts"; -import { promiseState } from "https://deno.land/x/async@v2.1.0/mod.ts"; -import { unimplemented } from "https://deno.land/x/errorutil@v0.1.1/mod.ts"; +import type { Meta } from "jsr:@denops/core@6.0.6"; +import { assertEquals } from "jsr:@std/assert@0.225.1"; +import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; +import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; +import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { DenopsImpl, type Host, type Service } from "./denops.ts"; Deno.test("DenopsImpl", async (t) => { const meta: Meta = { diff --git a/denops/@denops-private/error.ts b/denops/@denops-private/error.ts index 002a7081..23d5d59f 100644 --- a/denops/@denops-private/error.ts +++ b/denops/@denops-private/error.ts @@ -1,10 +1,10 @@ -import { is } from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; +import { is } from "jsr:@core/unknownutil@3.18.0"; import { fromErrorObject, isErrorObject, toErrorObject, tryOr, -} from "https://deno.land/x/errorutil@v0.1.1/mod.ts"; +} from "jsr:@lambdalisue/errorutil@1.0.0"; export function errorSerializer(err: unknown): unknown { if (err instanceof Error) { diff --git a/denops/@denops-private/error_test.ts b/denops/@denops-private/error_test.ts index 86ff832b..371d77de 100644 --- a/denops/@denops-private/error_test.ts +++ b/denops/@denops-private/error_test.ts @@ -2,8 +2,8 @@ import { assert, assertEquals, assertInstanceOf, -} from "https://deno.land/std@0.217.0/assert/mod.ts"; -import { is } from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; +} from "jsr:@std/assert@0.225.1"; +import { is } from "jsr:@core/unknownutil@3.18.0"; import { errorDeserializer, errorSerializer } from "./error.ts"; Deno.test("errorSerializer", async (t) => { diff --git a/denops/@denops-private/host.ts b/denops/@denops-private/host.ts index cf5f32dc..d9ae557f 100644 --- a/denops/@denops-private/host.ts +++ b/denops/@denops-private/host.ts @@ -1,4 +1,4 @@ -import { ensure, is } from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; +import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; /** * Host (Vim/Neovim) which is visible from Service diff --git a/denops/@denops-private/host/nvim.ts b/denops/@denops-private/host/nvim.ts index 88c69c69..7b24de9a 100644 --- a/denops/@denops-private/host/nvim.ts +++ b/denops/@denops-private/host/nvim.ts @@ -1,11 +1,8 @@ -import { ensure, is } from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; -import { - Client, - Session, -} from "https://deno.land/x/messagepack_rpc@v2.0.3/mod.ts"; +import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; +import { Client, Session } from "jsr:@lambdalisue/messagepack-rpc@2.1.1"; import { errorDeserializer, errorSerializer } from "../error.ts"; import { getVersionOr } from "../version.ts"; -import { Host, invoke, Service } from "../host.ts"; +import { type Host, invoke, type Service } from "../host.ts"; export class Neovim implements Host { #session: Session; diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index 88857c38..fd0ee067 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -2,16 +2,13 @@ import { assertEquals, assertMatch, assertRejects, -} from "https://deno.land/std@0.217.0/assert/mod.ts"; -import { - assertSpyCall, - stub, -} from "https://deno.land/std@0.217.0/testing/mock.ts"; -import { promiseState } from "https://deno.land/x/async@v2.1.0/mod.ts"; +} from "jsr:@std/assert@0.225.1"; +import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; +import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; +import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import { withNeovim } from "../testutil/with.ts"; -import { Service } from "../host.ts"; +import type { Service } from "../host.ts"; import { Neovim } from "./nvim.ts"; -import { unimplemented } from "https://deno.land/x/errorutil@v0.1.1/mod.ts"; Deno.test("Neovim", async (t) => { let waitClosed: Promise | undefined; diff --git a/denops/@denops-private/host/vim.ts b/denops/@denops-private/host/vim.ts index 35863211..17debe58 100644 --- a/denops/@denops-private/host/vim.ts +++ b/denops/@denops-private/host/vim.ts @@ -1,10 +1,10 @@ -import { ensure, is } from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; +import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; import { Client, - Message, + type Message, Session, -} from "https://deno.land/x/vim_channel_command@v3.0.0/mod.ts"; -import { Host, invoke, Service } from "../host.ts"; +} from "jsr:@denops/vim-channel-command@4.0.0"; +import { type Host, invoke, type Service } from "../host.ts"; export class Vim implements Host { #session: Session; diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index 43aaff41..7b1fc3da 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -2,17 +2,14 @@ import { assertEquals, assertMatch, assertRejects, -} from "https://deno.land/std@0.217.0/assert/mod.ts"; -import { - assertSpyCall, - stub, -} from "https://deno.land/std@0.217.0/testing/mock.ts"; -import { delay } from "https://deno.land/std@0.217.0/async/mod.ts"; -import { promiseState } from "https://deno.land/x/async@v2.1.0/mod.ts"; +} from "jsr:@std/assert@0.225.1"; +import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; +import { delay } from "jsr:@std/async@0.224.0/delay"; +import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; +import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import { withVim } from "../testutil/with.ts"; -import { Service } from "../host.ts"; +import type { Service } from "../host.ts"; import { Vim } from "./vim.ts"; -import { unimplemented } from "https://deno.land/x/errorutil@v0.1.1/mod.ts"; Deno.test("Vim", async (t) => { let waitClosed: Promise | undefined; diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index 34c71231..cb564438 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -1,12 +1,12 @@ -import { assertThrows } from "https://deno.land/std@0.217.0/assert/mod.ts"; +import { assertThrows } from "jsr:@std/assert@0.225.1"; import { assertSpyCall, assertSpyCalls, stub, -} from "https://deno.land/std@0.217.0/testing/mock.ts"; -import { AssertError } from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; -import { invoke, Service } from "./host.ts"; -import { unimplemented } from "https://deno.land/x/errorutil@v0.1.1/mod.ts"; +} from "jsr:@std/testing@0.224.0/mock"; +import { AssertError } from "jsr:@core/unknownutil@3.18.0"; +import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { invoke, type Service } from "./host.ts"; Deno.test("invoke", async (t) => { const service: Omit = { diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index b49e1427..aaf71b7f 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -1,10 +1,7 @@ -import type { - Denops, - Meta, -} from "https://deno.land/x/denops_core@v6.0.5/mod.ts"; -import { toFileUrl } from "https://deno.land/std@0.217.0/path/mod.ts"; -import { toErrorObject } from "https://deno.land/x/errorutil@v0.1.1/mod.ts"; -import { DenopsImpl, Host } from "./denops.ts"; +import type { Denops, Meta } from "jsr:@denops/core@6.0.6"; +import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; +import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { DenopsImpl, type Host } from "./denops.ts"; // We can use `PromiseWithResolvers` but Deno 1.38 doesn't have `PromiseWithResolvers` type Waiter = { diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 511a28d2..995d8130 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -3,15 +3,15 @@ import { assertEquals, assertMatch, assertRejects, -} from "https://deno.land/std@0.217.0/assert/mod.ts"; +} from "jsr:@std/assert@0.225.1"; import { assertSpyCall, assertSpyCalls, stub, -} from "https://deno.land/std@0.217.0/testing/mock.ts"; -import type { Meta } from "https://deno.land/x/denops_core@v6.0.5/mod.ts"; -import { promiseState } from "https://deno.land/x/async@v2.1.0/mod.ts"; -import { unimplemented } from "https://deno.land/x/errorutil@v0.1.1/mod.ts"; +} from "jsr:@std/testing@0.224.0/mock"; +import type { Meta } from "jsr:@denops/core@6.0.6"; +import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; +import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import type { Host } from "./denops.ts"; import { Service } from "./service.ts"; diff --git a/denops/@denops-private/testutil/conf.ts b/denops/@denops-private/testutil/conf.ts index 61b66fda..9eff621d 100644 --- a/denops/@denops-private/testutil/conf.ts +++ b/denops/@denops-private/testutil/conf.ts @@ -1,8 +1,6 @@ -import { - fromFileUrl, - resolve, - SEPARATOR as SEP, -} from "https://deno.land/std@0.217.0/path/mod.ts"; +import { fromFileUrl } from "jsr:@std/path@0.225.0/from-file-url"; +import { resolve } from "jsr:@std/path@0.225.0/resolve"; +import { SEPARATOR as SEP } from "jsr:@std/path@0.225.0/constants"; let conf: Config | undefined; diff --git a/denops/@denops-private/testutil/conf_test.ts b/denops/@denops-private/testutil/conf_test.ts index 3f1bfe15..433b7474 100644 --- a/denops/@denops-private/testutil/conf_test.ts +++ b/denops/@denops-private/testutil/conf_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "https://deno.land/std@0.217.0/assert/mod.ts"; +import { assertEquals } from "jsr:@std/assert@0.225.1"; import { _internal } from "./conf.ts"; Deno.test({ diff --git a/denops/@denops-private/util.ts b/denops/@denops-private/util.ts index a828549f..cd7ab15c 100644 --- a/denops/@denops-private/util.ts +++ b/denops/@denops-private/util.ts @@ -1,5 +1,5 @@ -import type { Meta } from "https://deno.land/x/denops_core@v6.0.5/mod.ts"; -import { is, Predicate } from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; +import type { Meta } from "jsr:@denops/core@6.0.6"; +import { is, type Predicate } from "jsr:@core/unknownutil@3.18.0"; export const isMeta: Predicate = is.ObjectOf({ mode: is.LiteralOneOf(["release", "debug", "test"] as const), diff --git a/denops/@denops-private/version.ts b/denops/@denops-private/version.ts index e5b727f3..55224e93 100644 --- a/denops/@denops-private/version.ts +++ b/denops/@denops-private/version.ts @@ -1,8 +1,7 @@ -import { - dirname, - fromFileUrl, -} from "https://deno.land/std@0.217.0/path/mod.ts"; -import { parse, SemVer } from "https://deno.land/std@0.217.0/semver/mod.ts"; +import { dirname } from "jsr:@std/path@0.225.0/dirname"; +import { fromFileUrl } from "jsr:@std/path@0.225.0/from-file-url"; +import type { SemVer } from "jsr:@std/semver@0.224.0/types"; +import { parse } from "jsr:@std/semver@0.224.0/parse"; const decoder = new TextDecoder(); diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index 3a7bae85..1074c4c2 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -1,9 +1,9 @@ import { readableStreamFromWorker, writableStreamFromWorker, -} from "https://deno.land/x/workerio@v3.1.0/mod.ts"; -import { ensure } from "https://deno.land/x/unknownutil@v3.16.3/mod.ts"; -import { pop } from "https://deno.land/x/streamtools@v0.5.0/mod.ts"; +} from "jsr:@lambdalisue/workerio@4.0.0"; +import { ensure } from "jsr:@core/unknownutil@3.18.0"; +import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import type { HostConstructor } from "./host.ts"; import { Vim } from "./host/vim.ts"; import { Neovim } from "./host/nvim.ts"; From 7c4c3422b664151abf42cc77cbf3527c3f0ef1e4 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 13 May 2024 02:47:05 +0900 Subject: [PATCH 04/99] :memo: Remove deno.land badges No JavaScript/TypeScript documentation is required for denops itself. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index f7a50f8f..593d7c3a 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,10 @@ [![Neovim 0.10.0 or above](https://img.shields.io/badge/Neovim-Support%200.10.0-yellowgreen.svg?logo=neovim&logoColor=white)](https://github.com/neovim/neovim/tree/v0.10.0) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![deno land](http://img.shields.io/badge/available%20on-deno.land/x/denops__core-lightgrey.svg?logo=deno)](https://deno.land/x/denops_core) [![test](https://github.com/vim-denops/denops.vim/actions/workflows/test.yml/badge.svg)](https://github.com/vim-denops/denops.vim/actions/workflows/test.yml) [![codecov](https://codecov.io/github/vim-denops/denops.vim/branch/main/graph/badge.svg?token=k50SaoYUp0)](https://codecov.io/github/vim-denops/denops.vim) [![vim help](https://img.shields.io/badge/vim-%3Ah%20denops-orange.svg)](doc/denops.txt) -[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/denops_core/mod.ts) [![Documentation](https://img.shields.io/badge/denops-Documentation-yellow.svg)](https://vim-denops.github.io/denops-documentation/) From 19fdc9842b3776288488b5cde8ebe2a3f2abb8ab Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 17 May 2024 06:54:53 +0900 Subject: [PATCH 05/99] :herb: Add test utilities --- denops/@denops-private/testutil/host.ts | 65 +++++++++++++++++++++++++ denops/@denops-private/testutil/wait.ts | 40 +++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 denops/@denops-private/testutil/host.ts create mode 100644 denops/@denops-private/testutil/wait.ts diff --git a/denops/@denops-private/testutil/host.ts b/denops/@denops-private/testutil/host.ts new file mode 100644 index 00000000..e8aacf65 --- /dev/null +++ b/denops/@denops-private/testutil/host.ts @@ -0,0 +1,65 @@ +import { Host } from "../host.ts"; +import { Neovim } from "../host/nvim.ts"; +import { Vim } from "../host/vim.ts"; +import { withNeovim, WithOptions, withVim } from "./with.ts"; + +export type HostFn = (host: Host) => Promise; + +export type WithHostOptions = Omit, "fn"> & { + mode: "vim" | "nvim"; + fn: HostFn; +}; + +export function withHost( + options: WithHostOptions, +): Promise { + const { mode, fn, ...withOptions } = options; + if (mode === "vim") { + return withVim({ + fn: async (reader, writer) => { + await using host = new Vim(reader, writer); + return await fn(host); + }, + ...withOptions, + }); + } + if (mode === "nvim") { + return withNeovim({ + fn: async (reader, writer) => { + await using host = new Neovim(reader, writer); + return await fn(host); + }, + ...withOptions, + }); + } + return Promise.reject(new TypeError(`Invalid mode: ${mode}`)); +} + +export type TestFn = (host: Host, t: Deno.TestContext) => Promise; + +export type TestHostOptions = Omit, "mode" | "fn"> & { + mode?: "vim" | "nvim" | "all"; + name?: string; + fn: TestFn; +}; + +export function testHost( + options: TestHostOptions, +): void { + const { mode = "all", fn, name, ...hostOptions } = options; + if (mode === "all") { + testHost({ ...options, mode: "vim" }); + testHost({ ...options, mode: "nvim" }); + } else if (mode === "vim" || mode === "nvim") { + const prefix = name ? `${name} ` : ""; + Deno.test(`${prefix}(${mode})`, async (t) => { + await withHost({ + mode, + fn: (host) => fn(host, t), + ...hostOptions, + }); + }); + } else { + throw new TypeError(`Invalid mode: ${mode}`); + } +} diff --git a/denops/@denops-private/testutil/wait.ts b/denops/@denops-private/testutil/wait.ts new file mode 100644 index 00000000..9786fb6f --- /dev/null +++ b/denops/@denops-private/testutil/wait.ts @@ -0,0 +1,40 @@ +export type WaitOptions = { + /** + * Timeout period to an exception is thrown. + * @default {10_000} + */ + timeout?: number; + /** + * Polling interval. + * @default {50} + */ + interval?: number; +}; + +/** + * Calls `fn` periodically and returns the result if it is TRUE. + * An exception is thrown when the timeout expires. + */ +export function wait( + fn: () => unknown | Promise, + options?: WaitOptions, +): Promise { + const { timeout = 10_000, interval = 50 } = options ?? {}; + return new Promise((resolve, reject) => { + let i: number | undefined; + const t = setTimeout(() => { + clearTimeout(i); + reject(new Error(`Timeout waitTrue in ${timeout} millisec`)); + }, timeout); + const next = async () => { + const res = await fn(); + if (res) { + clearTimeout(t); + resolve(res); + } else { + i = setTimeout(next, interval); + } + }; + next(); + }); +} From 29baee94c2cbf1cb1a082c13ffaaed8de1d0bd58 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 16 May 2024 19:11:26 +0900 Subject: [PATCH 06/99] :bug: `denops#server#close()` should fires DenopsClosed event --- autoload/denops/_internal/rpc/nvim.vim | 2 +- autoload/denops/_internal/rpc/vim.vim | 17 +++++++++++------ autoload/denops/_internal/test.vim | 4 ++-- tests/denops/server_test.ts | 21 +++++++++++++++++++++ 4 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 tests/denops/server_test.ts diff --git a/autoload/denops/_internal/rpc/nvim.vim b/autoload/denops/_internal/rpc/nvim.vim index 1b4cac8a..5b2d7a2d 100644 --- a/autoload/denops/_internal/rpc/nvim.vim +++ b/autoload/denops/_internal/rpc/nvim.vim @@ -24,7 +24,7 @@ endfunction function! denops#_internal#rpc#nvim#close(chan) abort call timer_stop(a:chan._healthcheck_timer) call chanclose(a:chan._id) - call a:chan._on_close(a:chan) + call timer_start(0, { -> a:chan._on_close(a:chan) }) endfunction function! denops#_internal#rpc#nvim#notify(chan, method, params) abort diff --git a/autoload/denops/_internal/rpc/vim.vim b/autoload/denops/_internal/rpc/vim.vim index 89bf8a02..a40a37bd 100644 --- a/autoload/denops/_internal/rpc/vim.vim +++ b/autoload/denops/_internal/rpc/vim.vim @@ -3,29 +3,34 @@ function! denops#_internal#rpc#vim#connect(addr, ...) abort \ 'on_close': { -> 0 }, \}, a:0 ? a:1 : {}, \) - let l:chan = ch_open(a:addr, { + let l:chan = { + \ '_on_close': l:options.on_close, + \} + let l:chan._handle = ch_open(a:addr, { \ 'mode': 'json', \ 'drop': 'auto', \ 'noblock': 1, \ 'timeout': g:denops#_internal#rpc#vim#timeout, - \ 'close_cb': l:options.on_close, + \ 'close_cb': { -> l:chan._on_close(l:chan) }, \}) - if ch_status(l:chan) !=# 'open' + if ch_status(l:chan._handle) !=# 'open' throw printf('Failed to connect `%s`', a:addr) endif return l:chan endfunction function! denops#_internal#rpc#vim#close(chan) abort - return ch_close(a:chan) + " NOTE: 'close_cb' specified on `ch_open` is not invoked when `ch_close` called. + call ch_close(a:chan._handle) + call timer_start(0, { -> a:chan._on_close(a:chan) }) endfunction function! denops#_internal#rpc#vim#notify(chan, method, params) abort - return ch_sendraw(a:chan, json_encode([0, [a:method] + a:params]) . "\n") + return ch_sendraw(a:chan._handle, json_encode([0, [a:method] + a:params]) . "\n") endfunction function! denops#_internal#rpc#vim#request(chan, method, params) abort - let [l:ok, l:err] = ch_evalexpr(a:chan, [a:method] + a:params) + let [l:ok, l:err] = ch_evalexpr(a:chan._handle, [a:method] + a:params) if l:err isnot# v:null throw l:err endif diff --git a/autoload/denops/_internal/test.vim b/autoload/denops/_internal/test.vim index 76825d86..72cf0efa 100644 --- a/autoload/denops/_internal/test.vim +++ b/autoload/denops/_internal/test.vim @@ -8,10 +8,10 @@ if has('nvim') endfunction else function! denops#_internal#test#notify(method, params) abort - return denops#_internal#rpc#vim#notify(g:denops_test_channel, a:method, a:params) + return denops#_internal#rpc#vim#notify(#{ _handle: g:denops_test_channel }, a:method, a:params) endfunction function! denops#_internal#test#request(method, params) abort - return denops#_internal#rpc#vim#request(g:denops_test_channel, a:method, a:params) + return denops#_internal#rpc#vim#request(#{ _handle: g:denops_test_channel }, a:method, a:params) endfunction endif diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts new file mode 100644 index 00000000..00a9a650 --- /dev/null +++ b/tests/denops/server_test.ts @@ -0,0 +1,21 @@ +import { assertEquals } from "jsr:@std/assert@0.225.2"; +import { testHost } from "../../denops/@denops-private/testutil/host.ts"; +import { wait } from "../../denops/@denops-private/testutil/wait.ts"; + +testHost({ + name: "denops#server#close() should fires DenopsClosed", + fn: async (host) => { + await host.call("execute", [ + "source plugin/denops.vim", + "autocmd User DenopsReady let g:denops_ready_called = 1", + "autocmd User DenopsClosed let g:denops_closed_called = 1", + ], ""); + await wait(() => host.call("exists", "g:denops_ready_called")); + assertEquals(await host.call("exists", "g:denops_closed_called"), 0); + await host.call("denops#server#close"); + assertEquals( + await wait(() => host.call("exists", "g:denops_closed_called")), + 1, + ); + }, +}); From d1545817634df5c6a8f31ed333d618d41f42a30e Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 25 May 2024 10:19:43 +0900 Subject: [PATCH 07/99] :herb: run test task with `LANG=C` environment Test failed because error message not matched. --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 4eabb98e..01a25c40 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,7 +1,7 @@ { "tasks": { "check": "deno check **/*.ts", - "test": "deno test -A --parallel --shuffle --doc", + "test": "LANG=C deno test -A --parallel --shuffle --doc", "test:coverage": "deno task test --coverage=.coverage", "coverage": "deno coverage .coverage --exclude=cli.ts --exclude=worker.ts --exclude=testdata/", "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", From 0a22d0beb2858e67a1fdaf63254017c1b7bb4e40 Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 25 May 2024 10:35:04 +0900 Subject: [PATCH 08/99] :herb: improve verbose messages on error --- denops/@denops-private/testutil/host.ts | 43 ++++++----- denops/@denops-private/testutil/wait.ts | 63 ++++++++++------ denops/@denops-private/testutil/wait_test.ts | 75 ++++++++++++++++++++ denops/@denops-private/testutil/with.ts | 46 +++++++----- 4 files changed, 173 insertions(+), 54 deletions(-) create mode 100644 denops/@denops-private/testutil/wait_test.ts diff --git a/denops/@denops-private/testutil/host.ts b/denops/@denops-private/testutil/host.ts index e8aacf65..3ca8b98d 100644 --- a/denops/@denops-private/testutil/host.ts +++ b/denops/@denops-private/testutil/host.ts @@ -3,16 +3,17 @@ import { Neovim } from "../host/nvim.ts"; import { Vim } from "../host/vim.ts"; import { withNeovim, WithOptions, withVim } from "./with.ts"; -export type HostFn = (host: Host) => Promise; +export type HostFn = (host: Host) => T; -export type WithHostOptions = Omit, "fn"> & { - mode: "vim" | "nvim"; +export interface WithHostOptions extends Omit, "fn"> { fn: HostFn; -}; + /** Run mode. */ + mode: "vim" | "nvim"; +} export function withHost( options: WithHostOptions, -): Promise { +): Promise> { const { mode, fn, ...withOptions } = options; if (mode === "vim") { return withVim({ @@ -35,29 +36,39 @@ export function withHost( return Promise.reject(new TypeError(`Invalid mode: ${mode}`)); } -export type TestFn = (host: Host, t: Deno.TestContext) => Promise; +export type TestFn = (host: Host, t: Deno.TestContext) => void | Promise; -export type TestHostOptions = Omit, "mode" | "fn"> & { +export interface TestHostOptions + extends + Omit>, "fn" | "mode">, + Pick { + fn: TestFn; + /** Test mode. */ mode?: "vim" | "nvim" | "all"; + /** Test name. */ name?: string; - fn: TestFn; -}; +} export function testHost( options: TestHostOptions, ): void { - const { mode = "all", fn, name, ...hostOptions } = options; + const { mode = "all", fn, name, ignore, only, ...hostOptions } = options; if (mode === "all") { testHost({ ...options, mode: "vim" }); testHost({ ...options, mode: "nvim" }); } else if (mode === "vim" || mode === "nvim") { const prefix = name ? `${name} ` : ""; - Deno.test(`${prefix}(${mode})`, async (t) => { - await withHost({ - mode, - fn: (host) => fn(host, t), - ...hostOptions, - }); + Deno.test({ + ignore, + only, + name: `${prefix}(${mode})`, + fn: async (t) => { + await withHost>({ + mode, + fn: (host) => fn(host, t), + ...hostOptions, + }); + }, }); } else { throw new TypeError(`Invalid mode: ${mode}`); diff --git a/denops/@denops-private/testutil/wait.ts b/denops/@denops-private/testutil/wait.ts index 9786fb6f..cadf4068 100644 --- a/denops/@denops-private/testutil/wait.ts +++ b/denops/@denops-private/testutil/wait.ts @@ -1,3 +1,5 @@ +import { AssertionError } from "jsr:@std/assert@^0.225.1/assertion-error"; + export type WaitOptions = { /** * Timeout period to an exception is thrown. @@ -9,32 +11,53 @@ export type WaitOptions = { * @default {50} */ interval?: number; + /** Message for timeout error. */ + message?: string; }; /** * Calls `fn` periodically and returns the result if it is TRUE. * An exception is thrown when the timeout expires. */ -export function wait( - fn: () => unknown | Promise, +export async function wait( + fn: () => T | Promise, options?: WaitOptions, -): Promise { - const { timeout = 10_000, interval = 50 } = options ?? {}; - return new Promise((resolve, reject) => { - let i: number | undefined; - const t = setTimeout(() => { - clearTimeout(i); - reject(new Error(`Timeout waitTrue in ${timeout} millisec`)); - }, timeout); - const next = async () => { - const res = await fn(); - if (res) { - clearTimeout(t); - resolve(res); - } else { - i = setTimeout(next, interval); - } - }; - next(); +): Promise { + const { timeout = 10_000, interval = 50, message } = options ?? {}; + const TIMEOUT = {}; + + let timeoutId: number | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(TIMEOUT), timeout); }); + + let intervalId: number | undefined; + const delay = () => + new Promise((resolve) => { + intervalId = setTimeout(resolve, interval); + }); + + try { + return await Promise.race([ + (async () => { + for (;;) { + const res = await fn(); + if (res) { + return res; + } + await delay(); + } + })(), + timeoutPromise, + ]); + } catch (e) { + if (e === TIMEOUT) { + const suffix = message ? `: ${message}` : "."; + throw new AssertionError(`Timeout in ${timeout} millisec${suffix}`); + } + throw e; + } finally { + clearTimeout(timeoutId); + clearTimeout(intervalId); + } } diff --git a/denops/@denops-private/testutil/wait_test.ts b/denops/@denops-private/testutil/wait_test.ts new file mode 100644 index 00000000..8646932e --- /dev/null +++ b/denops/@denops-private/testutil/wait_test.ts @@ -0,0 +1,75 @@ +import { assertEquals, assertRejects } from "jsr:@std/assert@0.225.1"; +import { + assertSpyCalls, + resolvesNext, + returnsNext, + spy, +} from "jsr:@std/testing@0.224.0/mock"; +import { FakeTime } from "jsr:@std/testing@0.224.0/time"; +import { wait } from "./wait.ts"; + +Deno.test("wait()", async (t) => { + await t.step("calls `fn` periodically", async () => { + using time = new FakeTime(); + const fn = spy(returnsNext([0, 0, 1])); + const p = wait(fn); + assertSpyCalls(fn, 1); + await time.tickAsync(50); + assertSpyCalls(fn, 2); + await time.tickAsync(50); + assertSpyCalls(fn, 3); + assertEquals(await p, 1); + }); + + await t.step("calls `fn` specified `interval`", async () => { + using time = new FakeTime(); + const fn = spy(returnsNext([0, 0, 1])); + const p = wait(fn, { interval: 100 }); + assertSpyCalls(fn, 1); + await time.tickAsync(100); + assertSpyCalls(fn, 2); + await time.tickAsync(100); + assertSpyCalls(fn, 3); + assertEquals(await p, 1); + }); + + await t.step("resolves with `fn` return value", async () => { + const fn = spy(returnsNext([42])); + const actual = await wait(fn); + assertEquals(actual, 42); + }); + + await t.step("resolves with only truthy `fn` return value", async () => { + const fn = spy(returnsNext([0, false, null, undefined, "", "foo"])); + const actualPromise = wait(fn, { interval: 0 }); + assertEquals(await actualPromise, "foo"); + }); + + await t.step("rejects when `fn` throws", async () => { + const fn = spy(returnsNext([new Error("fn-throws")])); + await assertRejects(() => wait(fn), Error, "fn-throws"); + }); + + await t.step("rejects when `fn` rejects", async () => { + const fn = spy(resolvesNext([new Error("fn-throws")])); + await assertRejects(() => wait(fn), Error, "fn-throws"); + }); + + await t.step("rejects when `timeout`", async () => { + using time = new FakeTime(); + const fn = spy(returnsNext([0, 0])); + const p = wait(fn, { timeout: 150, interval: 100 }); + await time.tickAsync(150); + await assertRejects(() => p, Error, "Timeout in 150 millisec."); + assertSpyCalls(fn, 2); + }); + + await t.step("rejects with `message`", async () => { + using time = new FakeTime(); + const fn = spy(returnsNext([0, 0])); + const p = wait(fn, { message: "foo bar", timeout: 150, interval: 100 }); + await time.tickAsync(150); + await assertRejects(() => p, Error, "Timeout in 150 millisec: foo bar"); + assertSpyCalls(fn, 2); + }); +}); diff --git a/denops/@denops-private/testutil/with.ts b/denops/@denops-private/testutil/with.ts index dc9c5e3e..e04ce5e3 100644 --- a/denops/@denops-private/testutil/with.ts +++ b/denops/@denops-private/testutil/with.ts @@ -6,24 +6,32 @@ const script = new URL("./cli.ts", import.meta.url); export type Fn = ( reader: ReadableStream, writer: WritableStream, -) => Promise; +) => T; -export type WithOptions = { +export interface WithOptions { fn: Fn; + /** Print Vim messages (echomsg). */ + verbose?: boolean; + /** Vim commands to be executed before the startup. */ prelude?: string[]; + /** Vim commands to be executed after the startup. */ postlude?: string[]; + /** Environment variables. */ env?: Record; -}; +} export function withVim( options: WithOptions, -): Promise { +): Promise> { const conf = getConfig(); const exec = Deno.execPath(); const commands = [ ...(options.prelude ?? []), `set runtimepath^=${conf.denopsPath}`, - `let g:denops_test_channel = job_start(['${exec}', 'run', '--allow-all', '${script}'], {'mode': 'json', 'err_mode': 'nl'})`, + "let g:denops_test_channel = job_start(" + + ` ['${exec}', 'run', '--allow-all', '${script}'],` + + ` {'mode': 'json', 'err_mode': 'nl'}` + + ")", ...(options.postlude ?? []), ]; const cmd = conf.vimExecutable; @@ -36,47 +44,49 @@ export function withVim( "-N", // Disable compatible mode "-X", // Disable xterm "-e", // Start Vim in Ex mode - "-s", // Silent or batch mode + "-s", // Silent or batch mode ("-e" is required before) + "-V1", // Verbose level 1 (Echo messages to stderr) + "-c", + "visual", // Go to Normal mode ...commands.flatMap((c) => ["-c", c]), ]; - return withProcess(cmd, args, conf.verbose, options); + return withProcess(cmd, args, { verbose: conf.verbose, ...options }); } export function withNeovim( options: WithOptions, -): Promise { +): Promise> { const conf = getConfig(); const exec = Deno.execPath(); const commands = [ ...(options.prelude ?? []), `set runtimepath^=${conf.denopsPath}`, - `let g:denops_test_channel = jobstart(['${exec}', 'run', '--allow-all', '${script}'], {'rpc': v:true})`, + "let g:denops_test_channel = jobstart(" + + ` ['${exec}', 'run', '--allow-all', '${script}'],` + + ` {'rpc': v:true}` + + ")", ...(options.postlude ?? []), ]; const cmd = conf.nvimExecutable; const args = [ "--clean", - "--embed", "--headless", - "-n", + "-n", // Disable swap file + "-V1", // Verbose level 1 (Echo messages to stderr) ...commands.flatMap((c) => ["-c", c]), ]; - return withProcess(cmd, args, conf.verbose, options); + return withProcess(cmd, args, { verbose: conf.verbose, ...options }); } async function withProcess( cmd: string, args: string[], - verbose: boolean, - { fn, env }: WithOptions, -): Promise { + { fn, env, verbose }: WithOptions, +): Promise> { const listener = Deno.listen({ hostname: "127.0.0.1", port: 0, // Automatically select free port }); - if (verbose) { - args.unshift("--cmd", "redir >> /dev/stdout"); - } const command = new Deno.Command(cmd, { args, stdin: "piped", From e8b9630315e3aa4e1eb2ed3e662e8624f052b68c Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 25 May 2024 15:22:20 +0900 Subject: [PATCH 09/99] :herb: add server tests --- tests/denops/server_test.ts | 110 ++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 10 deletions(-) diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts index 00a9a650..a6469af6 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/server_test.ts @@ -3,19 +3,109 @@ import { testHost } from "../../denops/@denops-private/testutil/host.ts"; import { wait } from "../../denops/@denops-private/testutil/wait.ts"; testHost({ - name: "denops#server#close() should fires DenopsClosed", - fn: async (host) => { + name: "denops#server#status()", + mode: "all", + fn: async (host, t) => { await host.call("execute", [ + "autocmd User DenopsReady let g:denops_ready_fired = 1", + "autocmd User DenopsClosed let g:denops_closed_fired = 1", + ], ""); + + await t.step( + "returns 'stopped' when no server running", + async () => { + const actual = await host.call("denops#server#status"); + assertEquals(actual, "stopped"); + }, + ); + + await t.step( + "returns 'starting' when denops#server#start() is called", + async () => { + await host.call("denops#server#start"); + const actual = await host.call("denops#server#status"); + assertEquals(actual, "starting"); + }, + ); + + await t.step( + "returns 'preparing' before DenopsReady is fired", + async () => { + const actual = await wait(() => + host.call( + "eval", + "exists('g:denops_ready_fired') ? 'DenopsReady is fired'" + + ": denops#server#status() !=# 'starting' ? denops#server#status() : 0", + ) + ); + assertEquals(actual, "preparing"); + }, + ); + + await t.step( + "returns 'running' after DenopsReady is fired", + async () => { + await wait(() => host.call("exists", "g:denops_ready_fired")); + const actual = await host.call("denops#server#status"); + assertEquals(actual, "running"); + }, + ); + + await t.step( + "returns 'stopped' after denops#server#stop() is called", + async () => { + await host.call("denops#server#stop"); + await wait(() => + host.call("eval", "denops#server#status() ==# 'stopped'") + ); + }, + ); + }, +}); + +testHost({ + name: "Denops server", + mode: "all", + verbose: true, + fn: async (host, t) => { + await host.call("execute", [ + "autocmd User DenopsReady let g:denops_ready_fired = 1", + "autocmd User DenopsClosed let g:denops_closed_fired = 1", "source plugin/denops.vim", - "autocmd User DenopsReady let g:denops_ready_called = 1", - "autocmd User DenopsClosed let g:denops_closed_called = 1", ], ""); - await wait(() => host.call("exists", "g:denops_ready_called")); - assertEquals(await host.call("exists", "g:denops_closed_called"), 0); - await host.call("denops#server#close"); - assertEquals( - await wait(() => host.call("exists", "g:denops_closed_called")), - 1, + + await t.step( + "'plugin/denops.vim' changes status to 'starting' when sourced", + async () => { + const actual = await host.call("denops#server#status"); + assertEquals(actual, "starting"); + }, + ); + + await t.step( + "denops#server#status() returns 'running' after DenopsReady is fired", + async () => { + await wait(() => host.call("exists", "g:denops_ready_fired")); + const actual = await host.call("denops#server#status"); + assertEquals(actual, "running"); + }, + ); + + await t.step( + "denops#server#close() closes the connection then DenopsClosed is fired", + async () => { + await host.call("denops#server#close"); + await wait(() => host.call("exists", "g:denops_closed_fired")); + }, + ); + + await t.step( + "denops#server#stop() stops the server process asynchronously", + async () => { + await host.call("denops#server#stop"); + const actual = await host.call("denops#server#status"); + assertEquals(actual, "stopped"); + }, ); }, }); From 34fd84f3f2672cef3cd18c2d9cb6cc6432051edd Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 25 May 2024 15:22:38 +0900 Subject: [PATCH 10/99] :herb: add `useSharedServer` test helper --- .../@denops-private/testutil/shared_server.ts | 83 +++++++++++++++++++ .../testutil/shared_server_test.ts | 46 ++++++++++ 2 files changed, 129 insertions(+) create mode 100644 denops/@denops-private/testutil/shared_server.ts create mode 100644 denops/@denops-private/testutil/shared_server_test.ts diff --git a/denops/@denops-private/testutil/shared_server.ts b/denops/@denops-private/testutil/shared_server.ts new file mode 100644 index 00000000..1de6d6fd --- /dev/null +++ b/denops/@denops-private/testutil/shared_server.ts @@ -0,0 +1,83 @@ +import { assert } from "jsr:@std/assert@0.225.2"; +import { deadline } from "jsr:@std/async@0.224.0/deadline"; +import { resolve } from "jsr:@std/path@0.224.0/resolve"; +import { TextLineStream } from "jsr:@std/streams@0.224.0/text-line-stream"; +import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; +import { getConfig } from "./conf.ts"; + +const DEFAULT_TIMEOUT = 5000; + +export interface UseSharedServerOptions { + /** Print shared-server messages. */ + verbose?: boolean; + /** Environment variables. */ + env?: Record; + /** Timeout for shared server initialization. */ + timeout?: number; +} + +export interface UseSharedServerResult extends AsyncDisposable { + /** Address to connect to the shared server. */ + addr: string; + /** Shared server standard output. */ + stdout: ReadableStream; +} + +/** + * Start a shared server and return an address for testing. + */ +export async function useSharedServer( + options?: UseSharedServerOptions, +): Promise { + const { denopsPath, verbose, env, timeout = DEFAULT_TIMEOUT } = { + ...getConfig(), + ...options, + }; + const aborter = new AbortController(); + const { signal } = aborter; + const cmd = Deno.execPath(); + const script = resolve(denopsPath, "denops/@denops-private/cli.ts"); + const args = [ + "run", + "-A", + "--no-lock", + script, + "--identity", + "--port", + "0", + ]; + const proc = new Deno.Command(cmd, { + args, + stdout: "piped", + stderr: verbose ? "inherit" : "null", + env, + signal, + }).spawn(); + try { + const [stdout, verboseStdout] = proc.stdout + .pipeThrough(new TextDecoderStream(), { signal }) + .pipeThrough(new TextLineStream()) + .tee(); + if (verbose) { + verboseStdout.pipeTo( + new WritableStream({ + write: (out) => console.log(out), + }), + ).catch(() => {}); + } + const addr = await deadline(pop(stdout), timeout); + assert(typeof addr === "string"); + return { + addr, + stdout, + async [Symbol.asyncDispose]() { + aborter.abort("useSharedServer disposed"); + await proc.status; + }, + }; + } catch (e) { + aborter.abort(e); + await proc.status; + throw e; + } +} diff --git a/denops/@denops-private/testutil/shared_server_test.ts b/denops/@denops-private/testutil/shared_server_test.ts new file mode 100644 index 00000000..e82ac996 --- /dev/null +++ b/denops/@denops-private/testutil/shared_server_test.ts @@ -0,0 +1,46 @@ +import { + assertInstanceOf, + assertMatch, + assertRejects, +} from "jsr:@std/assert@0.225.2"; +import { delay } from "jsr:@std/async@^0.224.0/delay"; +import { stub } from "jsr:@std/testing@0.224.0/mock"; +import { useSharedServer } from "./shared_server.ts"; + +Deno.test("useSharedServer()", async (t) => { + await t.step("returns `result.addr`", async () => { + await using server = await useSharedServer(); + assertMatch(server.addr, /^127\.0\.0\.1:\d+$/); + }); + + await t.step("returns `result.stdout`", async () => { + await using server = await useSharedServer(); + assertInstanceOf(server.stdout, ReadableStream); + const outputs: string[] = []; + server.stdout.pipeTo( + new WritableStream({ + write: (line) => void outputs.push(line), + }), + ).catch(() => {}); + await delay(100); + assertMatch(outputs.join("\n"), /Listen denops clients on/); + }); + + await t.step("calls console.log() if `verbose` is true", async () => { + using console_log = stub(console, "log"); + await using _server = await useSharedServer({ verbose: true }); + await delay(100); + const outputs = console_log.calls.flatMap((call) => call.args).join("\n"); + assertMatch(outputs, /Listen denops clients on/); + }); + + await t.step("closes child process when rejectes", async () => { + await assertRejects( + async () => { + await useSharedServer({ timeout: 0 }); + }, + Error, + "Deadline", + ); + }); +}); From f175d8902563af57d7964620370756658e494cd7 Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 25 May 2024 15:22:59 +0900 Subject: [PATCH 11/99] :herb: add plugin/denops.vim tests --- tests/denops/plugin_test.ts | 53 +++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/denops/plugin_test.ts diff --git a/tests/denops/plugin_test.ts b/tests/denops/plugin_test.ts new file mode 100644 index 00000000..1b58b613 --- /dev/null +++ b/tests/denops/plugin_test.ts @@ -0,0 +1,53 @@ +import { assertMatch } from "jsr:@std/assert@0.225.2"; +import { + testHost, + withHost, +} from "../../denops/@denops-private/testutil/host.ts"; +import { useSharedServer } from "../../denops/@denops-private/testutil/shared_server.ts"; +import { wait } from "../../denops/@denops-private/testutil/wait.ts"; + +testHost({ + name: + "'plugin/denops.vim' starts a local server when sourced before VimEnter", + postlude: [ + "runtime plugin/denops.vim", + ], + fn: async (host) => { + await wait(() => host.call("eval", "!has('vim_starting')")); + const actual = await host.call("denops#server#status") as string; + assertMatch(actual, /^(starting|preparing|running)$/); + }, +}); + +testHost({ + name: "'plugin/denops.vim' starts a local server when sourced after VimEnter", + fn: async (host) => { + await wait(() => host.call("eval", "!has('vim_starting')")); + await host.call("execute", [ + "runtime plugin/denops.vim", + ], ""); + const actual = await host.call("denops#server#status") as string; + assertMatch(actual, /^(starting|preparing|running)$/); + }, +}); + +for (const mode of ["vim", "nvim"] as const) { + Deno.test( + `'plugin/denops.vim' connects to the shared server when sourced before VimEnter (${mode})`, + async () => { + await using server = await useSharedServer(); + await withHost({ + mode, + postlude: [ + `let g:denops_server_addr = '${server.addr}'`, + "runtime plugin/denops.vim", + ], + fn: async (host) => { + await wait(() => host.call("eval", "!has('vim_starting')")); + const actual = await host.call("denops#server#status") as string; + assertMatch(actual, /^(preparing|running)$/); + }, + }); + }, + ); +} From 4e1aabca032b31b8bd1414d00af1e36f944e3420 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 26 May 2024 01:41:49 +0900 Subject: [PATCH 12/99] :herb: exclude `.coverage/` `deno fmt` formats json files in `.coverage/`. --- deno.jsonc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 01a25c40..2ebd6c14 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -6,5 +6,8 @@ "coverage": "deno coverage .coverage --exclude=cli.ts --exclude=worker.ts --exclude=testdata/", "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", "update:commit": "deno task -q update --commit --pre-commit=fmt,lint" - } + }, + "exclude": [ + ".coverage/" + ] } From c5e44c620effa2420f2e559f9b1934a43edd2255 Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 27 May 2024 05:41:47 +0900 Subject: [PATCH 13/99] :herb: use `using s = stub(...)` Instead `const s = stub(...); try { ... } finally { s.restore(); }`. --- denops/@denops-private/denops_test.ts | 122 ++--- denops/@denops-private/host/nvim_test.ts | 46 +- denops/@denops-private/host/vim_test.ts | 28 +- denops/@denops-private/host_test.ts | 104 ++-- denops/@denops-private/service_test.ts | 633 +++++++++++------------ 5 files changed, 414 insertions(+), 519 deletions(-) diff --git a/denops/@denops-private/denops_test.ts b/denops/@denops-private/denops_test.ts index 717a9a01..692770c3 100644 --- a/denops/@denops-private/denops_test.ts +++ b/denops/@denops-private/denops_test.ts @@ -24,110 +24,80 @@ Deno.test("DenopsImpl", async (t) => { const denops = new DenopsImpl("dummy", meta, host, service); await t.step("redraw() calls host.redraw()", async () => { - const s = stub(host, "redraw"); - try { - await denops.redraw(); - assertSpyCall(s, 0, { args: [undefined] }); + using s = stub(host, "redraw"); + await denops.redraw(); + assertSpyCall(s, 0, { args: [undefined] }); - await denops.redraw(false); - assertSpyCall(s, 1, { args: [false] }); + await denops.redraw(false); + assertSpyCall(s, 1, { args: [false] }); - await denops.redraw(true); - assertSpyCall(s, 2, { args: [true] }); - } finally { - s.restore(); - } + await denops.redraw(true); + assertSpyCall(s, 2, { args: [true] }); }); await t.step("call() calls host.call()", async () => { - const s = stub(host, "call"); - try { - await denops.call("abs", -4); - assertSpyCall(s, 0, { args: ["abs", -4] }); + using s = stub(host, "call"); + await denops.call("abs", -4); + assertSpyCall(s, 0, { args: ["abs", -4] }); - await denops.call("abs", 10); - assertSpyCall(s, 1, { args: ["abs", 10] }); - } finally { - s.restore(); - } + await denops.call("abs", 10); + assertSpyCall(s, 1, { args: ["abs", 10] }); }); await t.step("batch() calls host.batch()", async () => { - const s = stub( + using s = stub( host, "batch", () => Promise.resolve([[], ""] as [unknown[], string]), ); - try { - await denops.batch(["abs", -4], ["abs", 10], ["abs", -9]); - assertSpyCall(s, 0, { - args: [["abs", -4], ["abs", 10], ["abs", -9]], - }); - } finally { - s.restore(); - } + await denops.batch(["abs", -4], ["abs", 10], ["abs", -9]); + assertSpyCall(s, 0, { + args: [["abs", -4], ["abs", 10], ["abs", -9]], + }); }); await t.step("cmd() calls host.call()", async () => { - const s = stub(host, "call"); - try { - await denops.cmd("echo 'foo'"); - assertSpyCall(s, 0, { - args: ["denops#api#cmd", "echo 'foo'", {}], - }); - } finally { - s.restore(); - } + using s = stub(host, "call"); + await denops.cmd("echo 'foo'"); + assertSpyCall(s, 0, { + args: ["denops#api#cmd", "echo 'foo'", {}], + }); }); await t.step("eval() calls host.call()", async () => { - const s = stub(host, "call"); - try { - await denops.eval("v:version"); - assertSpyCall(s, 0, { - args: ["denops#api#eval", "v:version", {}], - }); - } finally { - s.restore(); - } + using s = stub(host, "call"); + await denops.eval("v:version"); + assertSpyCall(s, 0, { + args: ["denops#api#eval", "v:version", {}], + }); }); await t.step("dispatch() calls service.dispatch()", async () => { - const s1 = stub(service, "waitLoaded", () => Promise.resolve()); - const s2 = stub(service, "dispatch", () => Promise.resolve()); - try { - await denops.dispatch("dummy", "fn", "args"); - assertSpyCall(s1, 0, { - args: ["dummy"], - }); - assertSpyCall(s2, 0, { - args: ["dummy", "fn", ["args"]], - }); - } finally { - s1.restore(); - s2.restore(); - } + using s1 = stub(service, "waitLoaded", () => Promise.resolve()); + using s2 = stub(service, "dispatch", () => Promise.resolve()); + await denops.dispatch("dummy", "fn", "args"); + assertSpyCall(s1, 0, { + args: ["dummy"], + }); + assertSpyCall(s2, 0, { + args: ["dummy", "fn", ["args"]], + }); }); await t.step( "dispatch() internally waits 'service.waitLoaded()' before 'service.dispatch()'", async () => { const { promise, resolve } = Promise.withResolvers(); - const s1 = stub(service, "waitLoaded", () => promise); - const s2 = stub(service, "dispatch", () => Promise.resolve()); - try { - const p = denops.dispatch("dummy", "fn", "args"); - assertEquals(await promiseState(p), "pending"); - assertEquals(s1.calls.length, 1); - assertEquals(s2.calls.length, 0); - resolve(); - assertEquals(await promiseState(p), "fulfilled"); - assertEquals(s1.calls.length, 1); - assertEquals(s2.calls.length, 1); - } finally { - s1.restore(); - s2.restore(); - } + using s1 = stub(service, "waitLoaded", () => promise); + using s2 = stub(service, "dispatch", () => Promise.resolve()); + const p = denops.dispatch("dummy", "fn", "args"); + assertEquals(await promiseState(p), "pending"); + assertEquals(s1.calls.length, 1); + assertEquals(s2.calls.length, 0); + resolve(); + assertEquals(await promiseState(p), "fulfilled"); + assertEquals(s1.calls.length, 1); + assertEquals(s2.calls.length, 1); }, ); }); diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index fd0ee067..6cc55b31 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -40,13 +40,9 @@ Deno.test("Neovim", async (t) => { ); await t.step("init() calls Service.bind()", async () => { - const s = stub(service, "bind"); - try { - await host.init(service); - assertSpyCall(s, 0, { args: [host] }); - } finally { - s.restore(); - } + using s = stub(service, "bind"); + await host.init(service); + assertSpyCall(s, 0, { args: [host] }); }); await t.step("redraw() does nothing", async () => { @@ -114,34 +110,26 @@ Deno.test("Neovim", async (t) => { await t.step( "'invoke' message calls Service method", async () => { - const s = stub(service, "reload"); - try { - await host.call( - "denops#_internal#test#request", - "invoke", - ["reload", ["dummy"]], - ); - assertSpyCall(s, 0, { args: ["dummy"] }); - } finally { - s.restore(); - } + using s = stub(service, "reload"); + await host.call( + "denops#_internal#test#request", + "invoke", + ["reload", ["dummy"]], + ); + assertSpyCall(s, 0, { args: ["dummy"] }); }, ); await t.step( "'nvim_error_event' message shows error message", async () => { - const s = stub(console, "error"); - try { - await host.call( - "denops#_internal#test#request", - "nvim_error_event", - [0, "message"], - ); - assertSpyCall(s, 0, { args: ["nvim_error_event(0)", "message"] }); - } finally { - s.restore(); - } + using s = stub(console, "error"); + await host.call( + "denops#_internal#test#request", + "nvim_error_event", + [0, "message"], + ); + assertSpyCall(s, 0, { args: ["nvim_error_event(0)", "message"] }); }, ); diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index 7b1fc3da..bf356fff 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -41,13 +41,9 @@ Deno.test("Vim", async (t) => { ); await t.step("init() calls Service.bind()", async () => { - const s = stub(service, "bind"); - try { - await host.init(service); - assertSpyCall(s, 0, { args: [host] }); - } finally { - s.restore(); - } + using s = stub(service, "bind"); + await host.init(service); + assertSpyCall(s, 0, { args: [host] }); }); await t.step("redraw() does nothing", async () => { @@ -117,17 +113,13 @@ Deno.test("Vim", async (t) => { await t.step( "'invoke' message calls Service method", async () => { - const s = stub(service, "reload"); - try { - await host.call( - "denops#_internal#test#request", - "invoke", - ["reload", ["dummy"]], - ); - assertSpyCall(s, 0, { args: ["dummy"] }); - } finally { - s.restore(); - } + using s = stub(service, "reload"); + await host.call( + "denops#_internal#test#request", + "invoke", + ["reload", ["dummy"]], + ); + assertSpyCall(s, 0, { args: ["dummy"] }); }, ); diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index cb564438..847d649a 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -18,101 +18,69 @@ Deno.test("invoke", async (t) => { await t.step("calls 'load'", async (t) => { await t.step("ok", async () => { - const s = stub(service, "load"); - try { - await invoke(service, "load", ["dummy", "dummy.ts"]); - assertSpyCalls(s, 1); - assertSpyCall(s, 0, { args: ["dummy", "dummy.ts"] }); - } finally { - s.restore(); - } + using s = stub(service, "load"); + await invoke(service, "load", ["dummy", "dummy.ts"]); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { args: ["dummy", "dummy.ts"] }); }); await t.step("invalid args", () => { - const s = stub(service, "load"); - try { - assertThrows(() => invoke(service, "load", []), AssertError); - assertSpyCalls(s, 0); - } finally { - s.restore(); - } + using s = stub(service, "load"); + assertThrows(() => invoke(service, "load", []), AssertError); + assertSpyCalls(s, 0); }); }); await t.step("calls 'reload'", async (t) => { await t.step("ok", async () => { - const s = stub(service, "reload"); - try { - await invoke(service, "reload", ["dummy"]); - assertSpyCalls(s, 1); - assertSpyCall(s, 0, { args: ["dummy"] }); - } finally { - s.restore(); - } + using s = stub(service, "reload"); + await invoke(service, "reload", ["dummy"]); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { args: ["dummy"] }); }); await t.step("invalid args", () => { - const s = stub(service, "reload"); - try { - assertThrows(() => invoke(service, "reload", []), AssertError); - assertSpyCalls(s, 0); - } finally { - s.restore(); - } + using s = stub(service, "reload"); + assertThrows(() => invoke(service, "reload", []), AssertError); + assertSpyCalls(s, 0); }); }); await t.step("calls 'dispatch'", async (t) => { await t.step("ok", async () => { - const s = stub(service, "dispatch"); - try { - await invoke(service, "dispatch", ["dummy", "fn", ["args"]]); - assertSpyCalls(s, 1); - assertSpyCall(s, 0, { args: ["dummy", "fn", ["args"]] }); - } finally { - s.restore(); - } + using s = stub(service, "dispatch"); + await invoke(service, "dispatch", ["dummy", "fn", ["args"]]); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { args: ["dummy", "fn", ["args"]] }); }); await t.step("invalid args", () => { - const s = stub(service, "dispatch"); - try { - assertThrows(() => invoke(service, "dispatch", []), AssertError); - assertSpyCalls(s, 0); - } finally { - s.restore(); - } + using s = stub(service, "dispatch"); + assertThrows(() => invoke(service, "dispatch", []), AssertError); + assertSpyCalls(s, 0); }); }); await t.step("calls 'dispatchAsync'", async (t) => { await t.step("ok", async () => { - const s = stub(service, "dispatchAsync"); - try { - await invoke(service, "dispatchAsync", [ - "dummy", - "fn", - ["args"], - "success", - "failure", - ]); - assertSpyCalls(s, 1); - assertSpyCall(s, 0, { - args: ["dummy", "fn", ["args"], "success", "failure"], - }); - } finally { - s.restore(); - } + using s = stub(service, "dispatchAsync"); + await invoke(service, "dispatchAsync", [ + "dummy", + "fn", + ["args"], + "success", + "failure", + ]); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { + args: ["dummy", "fn", ["args"], "success", "failure"], + }); }); await t.step("invalid args", () => { - const s = stub(service, "dispatchAsync"); - try { - assertThrows(() => invoke(service, "dispatchAsync", []), AssertError); - assertSpyCalls(s, 0); - } finally { - s.restore(); - } + using s = stub(service, "dispatchAsync"); + assertThrows(() => invoke(service, "dispatchAsync", []), AssertError); + assertSpyCalls(s, 0); }); }); diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 995d8130..8202b6fa 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -78,250 +78,249 @@ Deno.test("Service", async (t) => { ); await t.step("load() loads plugin and emits autocmd events", async () => { - const s = stub(host, "call"); - try { - await service.load("dummy", scriptValid); - assertSpyCalls(s, 3); - assertSpyCall(s, 0, { + using s = stub(host, "call"); + await service.load("dummy", scriptValid); + assertSpyCalls(s, 3); + assertSpyCall(s, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + }); + assertSpyCall(s, 1, { + args: [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + }); + assertSpyCall(s, 2, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + }); + }); + + await t.step( + "the result promise of waitLoaded() become 'fulfilled' when the plugin is loaded", + async () => { + assertEquals(await promiseState(waitLoaded), "fulfilled"); + }, + ); + + await t.step( + "load() loads plugin and emits autocmd events (failure)", + async () => { + using c = stub(console, "error"); + using s = stub(host, "call"); + await service.load("dummyFail", scriptInvalid); + assertSpyCalls(c, 1); + assertSpyCall(c, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "Failed to load plugin 'dummyFail': Error: This is dummy error", ], }); - assertSpyCall(s, 1, { + assertSpyCalls(s, 2); + assertSpyCall(s, 0, { args: [ "denops#api#cmd", - "echo 'Hello, Denops!'", + "doautocmd User DenopsSystemPluginPre:dummyFail", {}, ], }); - assertSpyCall(s, 2, { + assertSpyCall(s, 1, { args: [ "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", + "doautocmd User DenopsSystemPluginFail:dummyFail", {}, ], }); - } finally { - s.restore(); - } - }); - - await t.step( - "the result promise of waitLoaded() become 'fulfilled' when the plugin is loaded", - async () => { - assertEquals(await promiseState(waitLoaded), "fulfilled"); - }, - ); - - await t.step( - "load() loads plugin and emits autocmd events (failure)", - async () => { - const c = stub(console, "error"); - const s = stub(host, "call"); - try { - await service.load("dummyFail", scriptInvalid); - assertSpyCalls(c, 1); - assertSpyCall(c, 0, { - args: [ - "Failed to load plugin 'dummyFail': Error: This is dummy error", - ], - }); - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummyFail", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummyFail", - {}, - ], - }); - } finally { - s.restore(); - c.restore(); - } }, ); await t.step( "load() loads plugin and emits autocmd events (could not find constraint)", async () => { - const c = stub(console, "warn"); - const s = stub(host, "call"); - try { - await service.load("dummyFailConstraint", scriptInvalidConstraint); - const expects = [ - "********************************************************************************", - "Deno module cache issue is detected.", - "Execute 'call denops#cache#update(#{reload: v:true})' and restart Vim/Neovim.", - "See https://github.com/vim-denops/denops.vim/issues/358 for more detail.", - "********************************************************************************", - ]; - assertSpyCalls(c, expects.length); - for (let i = 0; i < expects.length; i++) { - assertSpyCall(c, i, { - args: [expects[i]], - }); - } - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummyFailConstraint", - {}, - ], + using c = stub(console, "warn"); + using s = stub(host, "call"); + await service.load("dummyFailConstraint", scriptInvalidConstraint); + const expects = [ + "********************************************************************************", + "Deno module cache issue is detected.", + "Execute 'call denops#cache#update(#{reload: v:true})' and restart Vim/Neovim.", + "See https://github.com/vim-denops/denops.vim/issues/358 for more detail.", + "********************************************************************************", + ]; + assertSpyCalls(c, expects.length); + for (let i = 0; i < expects.length; i++) { + assertSpyCall(c, i, { + args: [expects[i]], }); - assertSpyCall(s, 1, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummyFailConstraint", - {}, - ], - }); - } finally { - s.restore(); - c.restore(); } + assertSpyCalls(s, 2); + assertSpyCall(s, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummyFailConstraint", + {}, + ], + }); + assertSpyCall(s, 1, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginFail:dummyFailConstraint", + {}, + ], + }); }, ); await t.step( "load() loads plugin and emits autocmd events (could not find version)", async () => { - const c = stub(console, "warn"); - const s = stub(host, "call"); - try { - await service.load("dummyFailConstraint2", scriptInvalidConstraint2); - const expects = [ - "********************************************************************************", - "Deno module cache issue is detected.", - "Execute 'call denops#cache#update(#{reload: v:true})' and restart Vim/Neovim.", - "See https://github.com/vim-denops/denops.vim/issues/358 for more detail.", - "********************************************************************************", - ]; - assertSpyCalls(c, expects.length); - for (let i = 0; i < expects.length; i++) { - assertSpyCall(c, i, { - args: [expects[i]], - }); - } - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummyFailConstraint2", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummyFailConstraint2", - {}, - ], - }); - } finally { - s.restore(); - c.restore(); - } - }, - ); - - await t.step( - "load() does nothing when the plugin is already loaded", - async () => { - const s1 = stub(host, "call"); - const s2 = stub(console, "log"); - try { - await service.load("dummy", scriptValid); - assertSpyCalls(s1, 0); - assertSpyCalls(s2, 1); - assertSpyCall(s2, 0, { - args: [ - "A denops plugin 'dummy' is already loaded. Skip", - ], + using c = stub(console, "warn"); + using s = stub(host, "call"); + await service.load("dummyFailConstraint2", scriptInvalidConstraint2); + const expects = [ + "********************************************************************************", + "Deno module cache issue is detected.", + "Execute 'call denops#cache#update(#{reload: v:true})' and restart Vim/Neovim.", + "See https://github.com/vim-denops/denops.vim/issues/358 for more detail.", + "********************************************************************************", + ]; + assertSpyCalls(c, expects.length); + for (let i = 0; i < expects.length; i++) { + assertSpyCall(c, i, { + args: [expects[i]], }); - } finally { - s1.restore(); - s2.restore(); } - }, - ); - - await t.step("reload() reloads plugin and emits autocmd events", async () => { - const s = stub(host, "call"); - try { - await service.reload("dummy"); - assertSpyCalls(s, 3); + assertSpyCalls(s, 2); assertSpyCall(s, 0, { args: [ "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", + "doautocmd User DenopsSystemPluginPre:dummyFailConstraint2", {}, ], }); assertSpyCall(s, 1, { args: [ "denops#api#cmd", - "echo 'Hello, Denops!'", + "doautocmd User DenopsSystemPluginFail:dummyFailConstraint2", {}, ], }); - assertSpyCall(s, 2, { + }, + ); + + await t.step( + "load() does nothing when the plugin is already loaded", + async () => { + using s1 = stub(host, "call"); + using s2 = stub(console, "log"); + await service.load("dummy", scriptValid); + assertSpyCalls(s1, 0); + assertSpyCalls(s2, 1); + assertSpyCall(s2, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "A denops plugin 'dummy' is already loaded. Skip", ], }); - } finally { - s.restore(); - } + }, + ); + + await t.step("reload() reloads plugin and emits autocmd events", async () => { + using s = stub(host, "call"); + await service.reload("dummy"); + assertSpyCalls(s, 3); + assertSpyCall(s, 1, { + args: [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + }); + assertSpyCall(s, 2, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + }); }); await t.step( - "reload() does nothing when the plugin is not loaded yet", + "load() does nothing when the plugin is already loaded", async () => { - const s1 = stub(host, "call"); - const s2 = stub(console, "log"); - try { - await service.reload("pluginthatisnotloaded"); - assertSpyCalls(s1, 0); - assertSpyCalls(s2, 1); - assertSpyCall(s2, 0, { - args: [ - "A denops plugin 'pluginthatisnotloaded' is not loaded yet. Skip", - ], - }); - } finally { - s1.restore(); - s2.restore(); - } + using s1 = stub(host, "call"); + using s2 = stub(console, "log"); + await service.load("dummy", scriptValid); + assertSpyCalls(s1, 0); + assertSpyCalls(s2, 1); + assertSpyCall(s2, 0, { + args: [ + "A denops plugin 'dummy' is already loaded. Skip", + ], + }); }, ); - await t.step("dispatch() call API of a plugin", async () => { - const s = stub(host, "call"); - try { - await service.dispatch("dummy", "test", ["foo"]); - assertSpyCalls(s, 1); - assertSpyCall(s, 0, { + await t.step("reload() reloads plugin and emits autocmd events", async () => { + using s = stub(host, "call"); + await service.reload("dummy"); + assertSpyCalls(s, 3); + assertSpyCall(s, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + }); + assertSpyCall(s, 1, { + args: [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + }); + assertSpyCall(s, 2, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + }); + }); + + await t.step( + "reload() does nothing when the plugin is not loaded yet", + async () => { + using s1 = stub(host, "call"); + using s2 = stub(console, "log"); + await service.reload("pluginthatisnotloaded"); + assertSpyCalls(s1, 0); + assertSpyCalls(s2, 1); + assertSpyCall(s2, 0, { args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, + "A denops plugin 'pluginthatisnotloaded' is not loaded yet. Skip", ], }); - } finally { - s.restore(); - } + }, + ); + + await t.step("dispatch() call API of a plugin", async () => { + using s = stub(host, "call"); + await service.dispatch("dummy", "test", ["foo"]); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { + args: [ + "denops#api#cmd", + "echo 'This is test call: [\"foo\"]'", + {}, + ], + }); }); await t.step( @@ -338,68 +337,60 @@ Deno.test("Service", async (t) => { await t.step( "dispatch() rejects when failed to call plugin API", async () => { - const s = stub( + using s = stub( host, "call", () => Promise.reject(new Error("invalid call")), ); - try { - const err = await assertRejects( - () => service.dispatch("dummy", "test", ["foo"]), - ); - assertSpyCalls(s, 1); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assert(typeof err === "string"); - assertMatch(err, /Failed to call 'test' API in 'dummy': invalid call/); - } finally { - s.restore(); - } + const err = await assertRejects( + () => service.dispatch("dummy", "test", ["foo"]), + ); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { + args: [ + "denops#api#cmd", + "echo 'This is test call: [\"foo\"]'", + {}, + ], + }); + assert(typeof err === "string"); + assertMatch(err, /invalid call/); }, ); await t.step( "dispatchAsync() call success callback when API call is succeeded", async () => { - const s = stub(host, "call"); - try { - await service.dispatchAsync( - "dummy", - "test", - ["foo"], + using s = stub(host, "call"); + await service.dispatchAsync( + "dummy", + "test", + ["foo"], + "success", + "failure", + ); + assertSpyCalls(s, 2); + assertSpyCall(s, 0, { + args: [ + "denops#api#cmd", + "echo 'This is test call: [\"foo\"]'", + {}, + ], + }); + assertSpyCall(s, 1, { + args: [ + "denops#callback#call", "success", - "failure", - ); - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#callback#call", - "success", - undefined, - ], - }); - } finally { - s.restore(); - } + undefined, + ], + }); }, ); await t.step( "dispatchAsync() call failure callback when API call is failed", async () => { - const s = stub( + using s = stub( host, "call", (method) => @@ -407,39 +398,35 @@ Deno.test("Service", async (t) => { ? Promise.reject(new Error("invalid call")) : Promise.resolve(), ); - try { - await service.dispatchAsync( - "dummy", - "test", - ["foo"], - "success", + await service.dispatchAsync( + "dummy", + "test", + ["foo"], + "success", + "failure", + ); + assertSpyCalls(s, 2); + assertSpyCall(s, 0, { + args: [ + "denops#api#cmd", + "echo 'This is test call: [\"foo\"]'", + {}, + ], + }); + assertSpyCall(s, 1, { + args: [ + "denops#callback#call", "failure", - ); - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#callback#call", - "failure", - s.calls[1].args[2], - ], - }); - } finally { - s.restore(); - } + s.calls[1].args[2], + ], + }); }, ); await t.step( "dispatchAsync() call success callback when API call is succeeded (but fail)", async () => { - const s1 = stub( + using s1 = stub( host, "call", (method) => @@ -447,85 +434,75 @@ Deno.test("Service", async (t) => { ? Promise.reject(new Error("invalid call")) : Promise.resolve(), ); - const s2 = stub(console, "error"); - try { - await service.dispatchAsync( - "dummy", - "test", - ["foo"], + using s2 = stub(console, "error"); + await service.dispatchAsync( + "dummy", + "test", + ["foo"], + "success", + "failure", + ); + assertSpyCalls(s1, 2); + assertSpyCall(s1, 0, { + args: [ + "denops#api#cmd", + "echo 'This is test call: [\"foo\"]'", + {}, + ], + }); + assertSpyCall(s1, 1, { + args: [ + "denops#callback#call", "success", - "failure", - ); - assertSpyCalls(s1, 2); - assertSpyCall(s1, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assertSpyCall(s1, 1, { - args: [ - "denops#callback#call", - "success", - undefined, - ], - }); - assertSpyCalls(s2, 1); - assertSpyCall(s2, 0, { - args: [ - "Failed to call success callback 'success': Error: invalid call", - ], - }); - } finally { - s1.restore(); - s2.restore(); - } + undefined, + ], + }); + assertSpyCalls(s2, 1); + assertSpyCall(s2, 0, { + args: [ + "Failed to call success callback 'success': Error: invalid call", + ], + }); }, ); await t.step( "dispatchAsync() call failure callback when API call is failed (but fail)", async () => { - const s1 = stub( + using s1 = stub( host, "call", () => Promise.reject(new Error("invalid call")), ); - const s2 = stub(console, "error"); - try { - await service.dispatchAsync( - "dummy", - "test", - ["foo"], - "success", + using s2 = stub(console, "error"); + await service.dispatchAsync( + "dummy", + "test", + ["foo"], + "success", + "failure", + ); + assertSpyCalls(s1, 2); + assertSpyCall(s1, 0, { + args: [ + "denops#api#cmd", + "echo 'This is test call: [\"foo\"]'", + {}, + ], + }); + assertSpyCall(s1, 1, { + args: [ + "denops#callback#call", "failure", - ); - assertSpyCalls(s1, 2); - assertSpyCall(s1, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assertSpyCall(s1, 1, { - args: [ - "denops#callback#call", - "failure", - s1.calls[1].args[2], - ], - }); - assertSpyCalls(s2, 1); - assertSpyCall(s2, 0, { - args: [ - "Failed to call failure callback 'failure': Error: invalid call", - ], - }); - } finally { - s1.restore(); - s2.restore(); - } + s1.calls[1].args[2], + ], + }); + assertSpyCalls(s2, 1); + assertSpyCall(s2, 0, { + args: [ + "Failed to call failure callback 'failure': Error: invalid call", + ], + }); }, ); }); From 1df9d5e137eae8917fa9e2f17811057fab820ae4 Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 27 May 2024 05:30:44 +0900 Subject: [PATCH 14/99] :herb: split test steps of `Host.notify()` - Separate `notify() does not throws ...`. - Add `console.error` stub testing for `Neovim.notify()`. --- denops/@denops-private/host/nvim_test.ts | 23 +++++++++++++++++++++-- denops/@denops-private/host/vim_test.ts | 8 +++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index 6cc55b31..008bef60 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -2,8 +2,14 @@ import { assertEquals, assertMatch, assertRejects, + assertStringIncludes, } from "jsr:@std/assert@0.225.1"; -import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; +import { + assertSpyCall, + assertSpyCalls, + stub, +} from "jsr:@std/testing@0.224.0/mock"; +import { delay } from "jsr:@std/async@0.224.0/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import { withNeovim } from "../testutil/with.ts"; @@ -93,9 +99,22 @@ Deno.test("Neovim", async (t) => { await t.step("notify() calls the function", () => { host.notify("abs", -4); - host.notify("@@@@@", -4); // should not throw }); + await t.step( + "notify() does not throws if calls a non-existent function", + async () => { + using console_error = stub(console, "error"); + host.notify("@@@@@", -4); // should not throw + await delay(100); // maybe flaky + assertSpyCalls(console_error, 1); + assertStringIncludes( + console_error.calls.flatMap((c) => c.args).join(" "), + "nvim_error_event(0) Vim:E117: Unknown function: @@@@@", + ); + }, + ); + await t.step( "'void' message does nothing", async () => { diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index bf356fff..c840390f 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -96,9 +96,15 @@ Deno.test("Vim", async (t) => { await t.step("notify() calls the function", () => { host.notify("abs", -4); - host.notify("@@@@@", -4); // should not throw }); + await t.step( + "notify() does not throws if calls a non-existent function", + () => { + host.notify("@@@@@", -4); // should not throw + }, + ); + await t.step( "'void' message does nothing", async () => { From 98e4a98c8f79c1a923dbc2b859e83b78f21886b5 Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 27 May 2024 05:43:39 +0900 Subject: [PATCH 15/99] :herb: increased default test timeout to 30 seconds Some flaky failures are avoided. --- denops/@denops-private/testutil/shared_server.ts | 2 +- denops/@denops-private/testutil/wait.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/denops/@denops-private/testutil/shared_server.ts b/denops/@denops-private/testutil/shared_server.ts index 1de6d6fd..10b52983 100644 --- a/denops/@denops-private/testutil/shared_server.ts +++ b/denops/@denops-private/testutil/shared_server.ts @@ -5,7 +5,7 @@ import { TextLineStream } from "jsr:@std/streams@0.224.0/text-line-stream"; import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import { getConfig } from "./conf.ts"; -const DEFAULT_TIMEOUT = 5000; +const DEFAULT_TIMEOUT = 30_000; export interface UseSharedServerOptions { /** Print shared-server messages. */ diff --git a/denops/@denops-private/testutil/wait.ts b/denops/@denops-private/testutil/wait.ts index cadf4068..b990b660 100644 --- a/denops/@denops-private/testutil/wait.ts +++ b/denops/@denops-private/testutil/wait.ts @@ -1,5 +1,8 @@ import { AssertionError } from "jsr:@std/assert@^0.225.1/assertion-error"; +const DEFAULT_TIMEOUT = 30_000; +const DEFAULT_INTERVAL = 50; + export type WaitOptions = { /** * Timeout period to an exception is thrown. @@ -23,7 +26,11 @@ export async function wait( fn: () => T | Promise, options?: WaitOptions, ): Promise { - const { timeout = 10_000, interval = 50, message } = options ?? {}; + const { + timeout = DEFAULT_TIMEOUT, + interval = DEFAULT_INTERVAL, + message, + } = options ?? {}; const TIMEOUT = {}; let timeoutId: number | undefined; From 9c07ac3239d70bbaf7d46b62e083dce25458eaf9 Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 27 May 2024 05:49:11 +0900 Subject: [PATCH 16/99] :package: remove unnecessary module version caret `^` --- denops/@denops-private/testutil/shared_server_test.ts | 2 +- denops/@denops-private/testutil/wait.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/denops/@denops-private/testutil/shared_server_test.ts b/denops/@denops-private/testutil/shared_server_test.ts index e82ac996..b46ed338 100644 --- a/denops/@denops-private/testutil/shared_server_test.ts +++ b/denops/@denops-private/testutil/shared_server_test.ts @@ -3,7 +3,7 @@ import { assertMatch, assertRejects, } from "jsr:@std/assert@0.225.2"; -import { delay } from "jsr:@std/async@^0.224.0/delay"; +import { delay } from "jsr:@std/async@0.224.0/delay"; import { stub } from "jsr:@std/testing@0.224.0/mock"; import { useSharedServer } from "./shared_server.ts"; diff --git a/denops/@denops-private/testutil/wait.ts b/denops/@denops-private/testutil/wait.ts index b990b660..d7c815f5 100644 --- a/denops/@denops-private/testutil/wait.ts +++ b/denops/@denops-private/testutil/wait.ts @@ -1,4 +1,4 @@ -import { AssertionError } from "jsr:@std/assert@^0.225.1/assertion-error"; +import { AssertionError } from "jsr:@std/assert@0.225.1/assertion-error"; const DEFAULT_TIMEOUT = 30_000; const DEFAULT_INTERVAL = 50; From 61afa342b8b595954b6bc37bd6c02782d73dc2d6 Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 27 May 2024 06:22:11 +0900 Subject: [PATCH 17/99] :herb: fix flaky timing tests refs #354 --- tests/denops/server_test.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts index a6469af6..ef14b023 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/server_test.ts @@ -22,8 +22,16 @@ testHost({ await t.step( "returns 'starting' when denops#server#start() is called", async () => { - await host.call("denops#server#start"); - const actual = await host.call("denops#server#status"); + // NOTE: The status may transition to `preparing`, so get it within execute. + // SEE: https://github.com/vim-denops/denops.vim/issues/354 + await host.call("execute", [ + "source plugin/denops.vim", + "let g:__test_denops_server_status_when_start_called = denops#server#status()", + ]); + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_start_called", + ); assertEquals(actual, "starting"); }, ); @@ -68,16 +76,22 @@ testHost({ mode: "all", verbose: true, fn: async (host, t) => { + // NOTE: The status may transition to `preparing`, so get it within execute. + // SEE: https://github.com/vim-denops/denops.vim/issues/354 await host.call("execute", [ "autocmd User DenopsReady let g:denops_ready_fired = 1", "autocmd User DenopsClosed let g:denops_closed_fired = 1", "source plugin/denops.vim", + "let g:__test_denops_server_status_on_sourced = denops#server#status()", ], ""); await t.step( "'plugin/denops.vim' changes status to 'starting' when sourced", async () => { - const actual = await host.call("denops#server#status"); + const actual = await host.call( + "eval", + "g:__test_denops_server_status_on_sourced", + ); assertEquals(actual, "starting"); }, ); From 0b970ba8bf6748f3e230828f2fde22bf724b065a Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 27 May 2024 06:41:06 +0900 Subject: [PATCH 18/99] :herb: add `DENOPS_TEST_TIMEOUT` environment We can extend the timeout in slow environments. --- denops/@denops-private/testutil/conf.ts | 5 +++++ denops/@denops-private/testutil/wait.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/denops/@denops-private/testutil/conf.ts b/denops/@denops-private/testutil/conf.ts index 9eff621d..be817788 100644 --- a/denops/@denops-private/testutil/conf.ts +++ b/denops/@denops-private/testutil/conf.ts @@ -9,6 +9,7 @@ export interface Config { vimExecutable: string; nvimExecutable: string; verbose: boolean; + timeout?: number; } export function getConfig(): Config { @@ -18,11 +19,15 @@ export function getConfig(): Config { const denopsPath = Deno.env.get("DENOPS_TEST_DENOPS_PATH") ?? fromFileUrl(new URL("../../..", import.meta.url)); const verbose = Deno.env.get("DENOPS_TEST_VERBOSE"); + const timeout = Number.parseFloat( + Deno.env.get("DENOPS_TEST_TIMEOUT") ?? "NaN", + ); conf = { denopsPath: removeTrailingSep(resolve(denopsPath)), vimExecutable: Deno.env.get("DENOPS_TEST_VIM_EXECUTABLE") ?? "vim", nvimExecutable: Deno.env.get("DENOPS_TEST_NVIM_EXECUTABLE") ?? "nvim", verbose: verbose === "1" || verbose === "true", + timeout: timeout >= 0 ? timeout : undefined, }; return conf; } diff --git a/denops/@denops-private/testutil/wait.ts b/denops/@denops-private/testutil/wait.ts index d7c815f5..95f62c87 100644 --- a/denops/@denops-private/testutil/wait.ts +++ b/denops/@denops-private/testutil/wait.ts @@ -1,4 +1,5 @@ import { AssertionError } from "jsr:@std/assert@0.225.1/assertion-error"; +import { getConfig } from "./conf.ts"; const DEFAULT_TIMEOUT = 30_000; const DEFAULT_INTERVAL = 50; @@ -30,7 +31,7 @@ export async function wait( timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL, message, - } = options ?? {}; + } = { ...getConfig(), ...options }; const TIMEOUT = {}; let timeoutId: number | undefined; From 8bf17448ba8603ea4c55c9de51ef59923bfd1065 Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 27 May 2024 04:24:23 +0900 Subject: [PATCH 19/99] :herb: add cli tests --- deno.jsonc | 2 +- denops/@denops-private/cli.ts | 16 +- denops/@denops-private/cli_test.ts | 455 +++++++++++++++++++ denops/@denops-private/testutil/mock.ts | 103 +++++ denops/@denops-private/testutil/mock_test.ts | 337 ++++++++++++++ 5 files changed, 904 insertions(+), 9 deletions(-) create mode 100644 denops/@denops-private/cli_test.ts create mode 100644 denops/@denops-private/testutil/mock.ts create mode 100644 denops/@denops-private/testutil/mock_test.ts diff --git a/deno.jsonc b/deno.jsonc index 2ebd6c14..666c9106 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -3,7 +3,7 @@ "check": "deno check **/*.ts", "test": "LANG=C deno test -A --parallel --shuffle --doc", "test:coverage": "deno task test --coverage=.coverage", - "coverage": "deno coverage .coverage --exclude=cli.ts --exclude=worker.ts --exclude=testdata/", + "coverage": "deno coverage .coverage", "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", "update:commit": "deno task -q update --commit --pre-commit=fmt,lint" }, diff --git a/denops/@denops-private/cli.ts b/denops/@denops-private/cli.ts index 8a31b872..d41d73be 100644 --- a/denops/@denops-private/cli.ts +++ b/denops/@denops-private/cli.ts @@ -4,19 +4,19 @@ import { } from "jsr:@lambdalisue/workerio@4.0.0"; import { parseArgs } from "jsr:@std/cli/parse-args"; -const script = import.meta.resolve("./worker.ts"); +const WORKER_SCRIPT = import.meta.resolve("./worker.ts"); async function handleConn( - conn: Deno.Conn, + conn: Deno.TcpConn, { quiet }: { quiet?: boolean }, ): Promise { - const remoteAddr = conn.remoteAddr as Deno.NetAddr; + const remoteAddr = conn.remoteAddr; const name = `${remoteAddr.hostname}:${remoteAddr.port}`; if (!quiet) { console.info(`${name} is connected`); } - const worker = new Worker(script, { + const worker = new Worker(WORKER_SCRIPT, { name, type: "module", }); @@ -34,8 +34,8 @@ async function handleConn( } } -async function main(): Promise { - const { hostname, port, quiet, identity } = parseArgs(Deno.args, { +export async function main(args: string[]): Promise { + const { hostname, port, quiet, identity } = parseArgs(args, { string: ["hostname", "port"], boolean: ["quiet", "identity"], }); @@ -44,7 +44,7 @@ async function main(): Promise { hostname: hostname ?? "127.0.0.1", port: Number(port ?? "32123"), }); - const localAddr = listener.addr as Deno.NetAddr; + const localAddr = listener.addr; if (identity) { // WARNING: @@ -68,5 +68,5 @@ async function main(): Promise { } if (import.meta.main) { - await main(); + await main(Deno.args); } diff --git a/denops/@denops-private/cli_test.ts b/denops/@denops-private/cli_test.ts new file mode 100644 index 00000000..5c9e8431 --- /dev/null +++ b/denops/@denops-private/cli_test.ts @@ -0,0 +1,455 @@ +// NOTE: Use sinon to stub the getter property. +// @deno-types="npm:@types/sinon@17.0.3" +import sinon from "npm:sinon@17.0.1"; + +import { + assertEquals, + assertMatch, + assertNotMatch, + assertStringIncludes, +} from "jsr:@std/assert@0.225.2"; +import { + assertSpyCallArgs, + assertSpyCalls, + resolvesNext, + returnsNext, + type Stub, + stub, +} from "jsr:@std/testing@0.224.0/mock"; +import { delay } from "jsr:@std/async@0.224.0/delay"; +import { + createFakeTcpConn, + createFakeTcpListener, + createFakeWorker, + pendingPromise, +} from "./testutil/mock.ts"; +import { main } from "./cli.ts"; + +const stubDenoListen = ( + fn: ( + options: Deno.TcpListenOptions & { transpost?: "tcp" }, + ) => Deno.TcpListener, +) => { + return stub( + Deno, + "listen", + fn as unknown as typeof Deno["listen"], + ) as unknown as Stub< + typeof Deno, + [options: Deno.TcpListenOptions], + Deno.TcpListener + >; +}; + +Deno.test("main()", async (t) => { + await t.step("listens", async (t) => { + await t.step("127.0.0.1:32123", async () => { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + using deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using _console_info = stub(console, "info"); + + const p = main([]); + + assertSpyCalls(deno_listen, 1); + assertSpyCallArgs(deno_listen, 0, [{ + hostname: "127.0.0.1", + port: 32123, + }]); + + fakeTcpListener.close(); + await p; + }); + + await t.step("`--hostname`:32123", async () => { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + using deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using _console_info = stub(console, "info"); + + const p = main(["--hostname", "foobar.example.net"]); + + assertSpyCalls(deno_listen, 1); + assertSpyCallArgs(deno_listen, 0, [{ + hostname: "foobar.example.net", + port: 32123, + }]); + + fakeTcpListener.close(); + await p; + }); + + await t.step("127.0.0.1:`--port`", async () => { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + using deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using _console_info = stub(console, "info"); + + const p = main(["--port", "39393"]); + + assertSpyCalls(deno_listen, 1); + assertSpyCallArgs(deno_listen, 0, [{ + hostname: "127.0.0.1", + port: 39393, + }]); + + fakeTcpListener.close(); + await p; + }); + }); + + await t.step("outputs info logs", async () => { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using console_info = stub(console, "info"); + + const p = main([]); + + assertStringIncludes( + console_info.calls.flatMap((c) => c.args).join(" "), + "Listen denops clients on stub.example.net:99999", + ); + + fakeTcpListener.close(); + await p; + }); + + await t.step("if `--identity`", async (t) => { + await t.step("outputs the listen addr FIRST", async () => { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + const outputs: unknown[][] = []; + const appendOutput = (...args: unknown[]) => { + outputs.push(args); + }; + using _console_info = stub(console, "info", appendOutput); + using _console_log = stub(console, "log", appendOutput); + + const p = main(["--identity"]); + + assertEquals(outputs[0], ["stub.example.net:99999"]); + + fakeTcpListener.close(); + await p; + }); + + await t.step("and `--quiet` outputs the listen addr FIRST", async () => { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + const outputs: unknown[][] = []; + const appendOutput = (...args: unknown[]) => { + outputs.push(args); + }; + using _console_info = stub(console, "info", appendOutput); + using _console_log = stub(console, "log", appendOutput); + + const p = main(["--identity", "--quiet"]); + + assertEquals(outputs[0], ["stub.example.net:99999"]); + + fakeTcpListener.close(); + await p; + }); + }); + + await t.step("when the connection is accepted", async (t) => { + { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + + const fakeTcpConn = createFakeTcpConn(); + const connStreamCloseWaiter = Promise.withResolvers(); + sinon.stub(fakeTcpConn, "remoteAddr").get(() => ({ + hostname: "stub-remote.example.net", + port: 98765, + })); + sinon.stub(fakeTcpConn, "readable").get(() => + new ReadableStream({ + start(con) { + connStreamCloseWaiter.promise.then(() => con.close()); + }, + }) + ); + sinon.stub(fakeTcpConn, "writable").get(() => + new WritableStream({ + start(con) { + connStreamCloseWaiter.promise.then(() => + con.error("fake-tcpconn-writable-closed") + ); + }, + }) + ); + + const fakeWorker = createFakeWorker(); + using globalThis_Worker = stub( + globalThis, + "Worker", + returnsNext([fakeWorker]), + ); + using worker_terminate = stub(fakeWorker, "terminate"); + using _worker_postMessage = stub(fakeWorker, "postMessage"); + + using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using _listener_accept = stub( + fakeTcpListener, + "accept", + resolvesNext([fakeTcpConn, pendingPromise()]), + ); + using console_info = stub(console, "info"); + using console_error = stub(console, "error"); + + const p = main([]); + + await t.step("creates a Worker", async () => { + assertSpyCalls(globalThis_Worker, 0); + + // Resolves microtasks. + await delay(0); + + assertSpyCalls(globalThis_Worker, 1); + assertMatch( + globalThis_Worker.calls[0].args[0] as string, + /.*\/denops\/@denops-private\/worker\.ts$/, + "Worker.specifier should be `*/denops/@denops-private/worker.ts`", + ); + assertEquals(globalThis_Worker.calls[0].args[1], { + name: "stub-remote.example.net:98765", + type: "module", + }, "Worker.name should be `remote-hostname:remote-port`"); + }); + + await t.step("outputs info logs", () => { + assertStringIncludes( + console_info.calls.flatMap((c) => c.args).join(" "), + "stub-remote.example.net:98765 is connected", + ); + }); + + await t.step("when the listener is closed", async (t) => { + fakeTcpListener.close(); + await p; + + await t.step("does not calls Worker.terminate()", () => { + assertSpyCalls(worker_terminate, 0); + }); + }); + + await t.step("when the connection is closed", async (t) => { + await t.step("calls Worker.terminate()", async () => { + connStreamCloseWaiter.resolve(); + + // Resolves microtasks. + await delay(0); + + assertSpyCalls(worker_terminate, 1); + }); + + await t.step("outputs error logs", () => { + assertStringIncludes( + console_error.calls.flatMap((c) => c.args).join(" "), + "Internal error occurred and Host/Denops connection is dropped fake-tcpconn-writable-closed", + ); + }); + }); + } + { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + + const fakeTcpConn = createFakeTcpConn(); + const connStreamCloseWaiter = Promise.withResolvers(); + sinon.stub(fakeTcpConn, "remoteAddr").get(() => ({ + hostname: "stub-remote.example.net", + port: 98765, + })); + sinon.stub(fakeTcpConn, "readable").get(() => + new ReadableStream({ + start(con) { + connStreamCloseWaiter.promise.then(() => con.close()); + }, + }) + ); + sinon.stub(fakeTcpConn, "writable").get(() => + new WritableStream({ + start(con) { + connStreamCloseWaiter.promise.then(() => + con.error("fake-tcpconn-writable-closed") + ); + }, + }) + ); + + const fakeWorker = createFakeWorker(); + using globalThis_Worker = stub( + globalThis, + "Worker", + returnsNext([fakeWorker]), + ); + using worker_terminate = stub(fakeWorker, "terminate"); + using _worker_postMessage = stub(fakeWorker, "postMessage"); + + using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using _listener_accept = stub( + fakeTcpListener, + "accept", + resolvesNext([fakeTcpConn, pendingPromise()]), + ); + using _console_info = stub(console, "info"); + using console_error = stub(console, "error"); + + const p = main([]); + + // Resolves microtasks. + await delay(0); + + assertSpyCalls(globalThis_Worker, 1); + + fakeTcpListener.close(); + await p; + + await t.step("when the worker stream is closed", async (t) => { + await t.step("calls Worker.terminate()", async () => { + assertSpyCalls(worker_terminate, 0); + + fakeWorker.onmessage(new MessageEvent("message", { data: null })); + + // Resolves microtasks. + await delay(0); + + assertSpyCalls(worker_terminate, 1); + }); + + await t.step("does not outputs error logs", () => { + assertNotMatch( + console_error.calls.flatMap((c) => c.args).join(" "), + /Internal error occurred/, + ); + }); + }); + } + }); + + await t.step("if `--quiet`", async (t) => { + await t.step("does not outputs info logs", async () => { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using console_info = stub(console, "info"); + + const p = main(["--quiet"]); + + assertSpyCalls(console_info, 0); + + fakeTcpListener.close(); + await p; + }); + + await t.step("when the connection is accepted", async (t) => { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + + const fakeTcpConn = createFakeTcpConn(); + const connStreamCloseWaiter = Promise.withResolvers(); + sinon.stub(fakeTcpConn, "remoteAddr").get(() => ({ + hostname: "stub-remote.example.net", + port: 98765, + })); + sinon.stub(fakeTcpConn, "readable").get(() => + new ReadableStream({ + start(con) { + connStreamCloseWaiter.promise.then(() => con.close()); + }, + }) + ); + sinon.stub(fakeTcpConn, "writable").get(() => + new WritableStream({ + start(con) { + connStreamCloseWaiter.promise.then(() => + con.error("fake-tcpconn-writable-closed") + ); + }, + }) + ); + + const fakeWorker = createFakeWorker(); + using _globalThis_Worker = stub( + globalThis, + "Worker", + returnsNext([fakeWorker]), + ); + using _worker_terminate = stub(fakeWorker, "terminate"); + using _worker_postMessage = stub(fakeWorker, "postMessage"); + + using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using _listener_accept = stub( + fakeTcpListener, + "accept", + resolvesNext([fakeTcpConn, pendingPromise()]), + ); + using console_info = stub(console, "info"); + using console_error = stub(console, "error"); + + const p = main(["--quiet"]); + + // Resolves microtasks. + await delay(0); + + await t.step("does not outputs info logs", () => { + assertNotMatch( + console_info.calls.flatMap((c) => c.args).join(" "), + /is connected/, + ); + }); + + fakeTcpListener.close(); + await p; + + await t.step("when the connection is closed", async (t) => { + connStreamCloseWaiter.resolve(); + + // Resolves microtasks. + await delay(0); + + await t.step("outputs error logs", () => { + assertStringIncludes( + console_error.calls.flatMap((c) => c.args).join(" "), + "Internal error occurred and Host/Denops connection is dropped fake-tcpconn-writable-closed", + ); + }); + }); + }); + }); +}); diff --git a/denops/@denops-private/testutil/mock.ts b/denops/@denops-private/testutil/mock.ts new file mode 100644 index 00000000..1830bc2e --- /dev/null +++ b/denops/@denops-private/testutil/mock.ts @@ -0,0 +1,103 @@ +import { AssertionError, unimplemented } from "jsr:@std/assert@0.225.2"; + +/** Returns a Promise that is never resolves or rejects. */ +export function pendingPromise(): Promise { + return new Promise(() => {}); +} + +/** Returns a fake `TcpListener` instance. */ +export function createFakeTcpListener(): Deno.TcpListener { + let closeWaiter: PromiseWithResolvers | undefined = Promise + .withResolvers(); + closeWaiter.promise.catch(() => {}); + return { + get addr(): Deno.NetAddr { + return unimplemented(); + }, + get rid() { + return unimplemented(); + }, + ref: () => unimplemented(), + unref: () => unimplemented(), + accept: () => pendingPromise(), + close() { + // NOTE: Listener.close() throws if already closed. + if (closeWaiter == null) { + throw new AssertionError("fake-tcp-listner-already-closed"); + } + closeWaiter.reject("listener-closed"); + closeWaiter = undefined; + }, + async *[Symbol.asyncIterator]() { + // NOTE: Listener[@@asyncIterator]() returns immediately if already closed. + if (closeWaiter == null) { + return; + } + try { + for (;;) { + yield Promise.race([this.accept(), closeWaiter.promise]); + } + } catch (e) { + if (e !== "listener-closed") { + throw e; + } + } + }, + [Symbol.dispose]() { + // NOTE: Listener[@@dispose]() does not calls .close() if already closed. + if (closeWaiter != null) { + this.close(); + } + }, + }; +} + +/** Returns a fake `TcpConn` instance. */ +export function createFakeTcpConn(): Deno.TcpConn { + return { + get localAddr() { + return unimplemented(); + }, + get remoteAddr() { + return unimplemented(); + }, + get rid() { + return unimplemented(); + }, + get readable() { + return unimplemented(); + }, + get writable() { + return unimplemented(); + }, + ref: () => unimplemented(), + unref: () => unimplemented(), + setNoDelay: () => unimplemented(), + setKeepAlive: () => unimplemented(), + read: () => unimplemented(), + write: () => unimplemented(), + close: () => unimplemented(), + closeWrite: () => unimplemented(), + [Symbol.dispose]() { + try { + this.close(); + } catch { + // NOTE: TcpConn[@@dispose]() does not throws if already closed. + } + }, + }; +} + +/** Returns a fake `Worker` instance. */ +export function createFakeWorker(): Worker { + return { + onerror: () => unimplemented(), + onmessage: () => unimplemented(), + onmessageerror: () => unimplemented(), + postMessage: () => unimplemented(), + addEventListener: () => unimplemented(), + removeEventListener: () => unimplemented(), + dispatchEvent: () => unimplemented(), + terminate: () => unimplemented(), + }; +} diff --git a/denops/@denops-private/testutil/mock_test.ts b/denops/@denops-private/testutil/mock_test.ts new file mode 100644 index 00000000..01d92bf1 --- /dev/null +++ b/denops/@denops-private/testutil/mock_test.ts @@ -0,0 +1,337 @@ +import { + assert, + assertEquals, + assertInstanceOf, + assertRejects, +} from "jsr:@std/assert@^0.225.2"; +import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; +import { + createFakeTcpConn, + createFakeTcpListener, + createFakeWorker, + pendingPromise, +} from "./mock.ts"; +import { assertThrows } from "jsr:@std/assert@^0.225.1/assert-throws"; +import { + assertSpyCalls, + resolvesNext, + spy, + stub, +} from "jsr:@std/testing@0.224.0/mock"; + +// deno-lint-ignore no-explicit-any +type AnyFn = (...args: any[]) => unknown; + +// deno-lint-ignore no-explicit-any +type AnyRecord = Record; + +type MethodKeyOf = ({ + [K in keyof T]: T[K] extends AnyFn ? K : never; +})[keyof T]; + +type GetterKeyOf = ({ + [K in keyof T]: T[K] extends AnyFn ? never : K; +})[keyof T]; + +Deno.test("pendingPromise()", async (t) => { + await t.step("returns a pending Promise", async () => { + const actual = pendingPromise(); + + assertInstanceOf(actual, Promise); + assertEquals(await promiseState(actual), "pending"); + }); +}); + +Deno.test("createFakeTcpListener()", async (t) => { + await t.step("returns a TcpListener like object", async (t) => { + const listener = createFakeTcpListener(); + + await t.step("has own key", async (t) => { + const keys = Reflect.ownKeys( + { + addr: 0, + rid: 0, + ref: 0, + unref: 0, + accept: 0, + close: 0, + [Symbol.asyncIterator]: 0, + [Symbol.dispose]: 0, + } as const satisfies Record, + ); + for (const key of keys) { + await t.step(key.toString(), () => { + assert(key in listener); + }); + } + }); + + await t.step("unimplements", async (t) => { + const unimplementedProps = [ + "addr", + "rid", + ] as const satisfies readonly GetterKeyOf[]; + for (const key of unimplementedProps) { + await t.step(`.${key}`, () => { + assertThrows(() => listener[key], Error, "Unimplemented"); + }); + } + + const unimplementedMethods = [ + "ref", + "unref", + ] as const satisfies readonly MethodKeyOf[]; + for (const key of unimplementedMethods) { + await t.step(`.${key}()`, () => { + assertThrows(() => listener[key](), Error, "Unimplemented"); + }); + } + }); + + await t.step(".accept()", async (t) => { + await t.step("returns a pending Promise", async () => { + const promise = listener.accept(); + + assertInstanceOf(promise, Promise); + assertEquals(await promiseState(promise), "pending"); + }); + }); + + await t.step(".close()", async (t) => { + const iterator = listener[Symbol.asyncIterator](); + const resultPromise = iterator.next(); + + await t.step("closes the conn iterator", async () => { + assertEquals(await promiseState(resultPromise), "pending"); + + listener.close(); + + assertEquals(await promiseState(resultPromise), "fulfilled"); + assertEquals(await resultPromise, { + done: true, + value: undefined, + }); + }); + + await t.step("throws if already closed", () => { + assertThrows(() => listener.close(), Error, "closed"); + }); + }); + + await t.step("[Symbol.asyncIterator]()", async (t) => { + // deno-lint-ignore no-explicit-any + const firstAcceptWaiter = Promise.withResolvers(); + const listener = createFakeTcpListener(); + using listener_accept = stub( + listener, + "accept", + resolvesNext([ + firstAcceptWaiter.promise, + pendingPromise(), + ]), + ); + + let iterator: AsyncIterator; + await t.step("returns the conn iterator", () => { + iterator = listener[Symbol.asyncIterator](); + + assertInstanceOf(iterator.next, Function); + }); + + await t.step("yield a TcpConn when .accept() resolves", async () => { + const resultPromise = iterator.next(); + + assertSpyCalls(listener_accept, 1); + assertEquals(await promiseState(resultPromise), "pending"); + + firstAcceptWaiter.resolve("fake-tcp-conn"); + + assertEquals(await promiseState(resultPromise), "fulfilled"); + assertEquals(await resultPromise, { + done: false, + // deno-lint-ignore no-explicit-any + value: "fake-tcp-conn" as any, + }); + }); + + await t.step("rejects when .accept() rejcets", async () => { + const listener = createFakeTcpListener(); + using listener_accept = stub( + listener, + "accept", + resolvesNext([ + Promise.reject("fake-tcp-listener-accept-error"), + ]), + ); + + const iterator = listener[Symbol.asyncIterator](); + const error = await assertRejects(() => iterator.next()); + + assertEquals(error, "fake-tcp-listener-accept-error"); + assertSpyCalls(listener_accept, 1); + }); + + await t.step( + "returns the closed iterator after .close() calls", + async () => { + listener.close(); + + const iterator = listener[Symbol.asyncIterator](); + const result = await iterator.next(); + + assertEquals(result, { done: true, value: undefined }); + }, + ); + }); + + await t.step("[Symbol.dispose]()", async (t) => { + const listener = createFakeTcpListener(); + const listener_close = spy(listener, "close"); + + await t.step("calls .close()", () => { + assertSpyCalls(listener_close, 0); + + listener[Symbol.dispose](); + + assertSpyCalls(listener_close, 1); + }); + + await t.step("does not calls .close() if already closed", () => { + assertSpyCalls(listener_close, 1); + + listener[Symbol.dispose](); + + assertSpyCalls(listener_close, 1); + }); + }); + }); +}); + +Deno.test("createFakeTcpConn()", async (t) => { + await t.step("returns a TcpConn like object", async (t) => { + const conn = createFakeTcpConn(); + + await t.step("has own key", async (t) => { + const keys = Reflect.ownKeys( + { + localAddr: 0, + remoteAddr: 0, + readable: 0, + writable: 0, + rid: 0, + ref: 0, + unref: 0, + setNoDelay: 0, + setKeepAlive: 0, + read: 0, + write: 0, + close: 0, + closeWrite: 0, + [Symbol.dispose]: 0, + } as const satisfies Record, + ); + for (const key of keys) { + await t.step(key.toString(), () => { + assert(key in conn); + }); + } + }); + + await t.step("unimplements", async (t) => { + const unimplementedProps = [ + "localAddr", + "remoteAddr", + "rid", + "readable", + "writable", + ] as const satisfies readonly GetterKeyOf[]; + for (const key of unimplementedProps) { + await t.step(`.${key}`, () => { + assertThrows(() => conn[key], Error, "Unimplemented"); + }); + } + + const unimplementedMethods = [ + "ref", + "unref", + "setNoDelay", + "setKeepAlive", + "read", + "write", + "close", + "closeWrite", + ] as const satisfies readonly MethodKeyOf[]; + for (const key of unimplementedMethods) { + await t.step(`.${key}()`, () => { + assertThrows(() => (conn[key] as AnyFn)(), Error, "Unimplemented"); + }); + } + }); + + await t.step("[Symbol.dispose]()", async (t) => { + await t.step("calls .close()", () => { + using listener_close = stub(conn, "close"); + assertSpyCalls(listener_close, 0); + + conn[Symbol.dispose](); + + assertSpyCalls(listener_close, 1); + }); + + await t.step("does not throws if .close() throws", () => { + using listener_close = stub(conn, "close", () => { + throw "fake-close-throws-a-error"; + }); + assertSpyCalls(listener_close, 0); + + conn[Symbol.dispose](); + + assertSpyCalls(listener_close, 1); + }); + }); + }); +}); + +Deno.test("createFakeWorker()", async (t) => { + await t.step("returns a Worker like object", async (t) => { + const worker = createFakeWorker(); + + await t.step("has own key", async (t) => { + const keys = Reflect.ownKeys( + { + onerror: 0, + onmessage: 0, + onmessageerror: 0, + postMessage: 0, + addEventListener: 0, + removeEventListener: 0, + dispatchEvent: 0, + terminate: 0, + } as const satisfies Record, + ); + for (const key of keys) { + await t.step(key.toString(), () => { + assert(key in worker); + }); + } + }); + + await t.step("unimplements", async (t) => { + const unimplementedMethods = [ + "onerror", + "onmessage", + "onmessageerror", + "postMessage", + "addEventListener", + "removeEventListener", + "dispatchEvent", + "terminate", + ] as const satisfies readonly MethodKeyOf[]; + for (const key of unimplementedMethods) { + await t.step(`.${key}()`, () => { + assertThrows(() => (worker[key] as AnyFn)(), Error, "Unimplemented"); + }); + } + }); + }); +}); From f2bdf725a9ea53c5b79e72c00bcad8a6192f9113 Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 28 May 2024 09:26:04 +0900 Subject: [PATCH 20/99] :+1: specify version to `@std/cli` --- denops/@denops-private/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/denops/@denops-private/cli.ts b/denops/@denops-private/cli.ts index d41d73be..9c655527 100644 --- a/denops/@denops-private/cli.ts +++ b/denops/@denops-private/cli.ts @@ -2,7 +2,7 @@ import { readableStreamFromWorker, writableStreamFromWorker, } from "jsr:@lambdalisue/workerio@4.0.0"; -import { parseArgs } from "jsr:@std/cli/parse-args"; +import { parseArgs } from "jsr:@std/cli@0.224.3/parse-args"; const WORKER_SCRIPT = import.meta.resolve("./worker.ts"); From a90a3dd5662c3a42be0d250480a66c9ff0819de3 Mon Sep 17 00:00:00 2001 From: Alisue Date: Wed, 15 May 2024 03:14:29 +0900 Subject: [PATCH 21/99] :+1: Add `denops#interrupt()` function --- autoload/denops.vim | 8 ++++++++ denops/@denops-private/denops.ts | 9 ++++++++- denops/@denops-private/denops_test.ts | 7 ++++++- denops/@denops-private/host.ts | 6 ++++++ denops/@denops-private/host/nvim_test.ts | 1 + denops/@denops-private/host/vim_test.ts | 1 + denops/@denops-private/host_test.ts | 23 +++++++++++++++++++++++ denops/@denops-private/service.ts | 14 ++++++++++++-- denops/@denops-private/service_test.ts | 21 +++++++++++++++++++++ doc/denops.txt | 20 ++++++++++++++++++++ 10 files changed, 106 insertions(+), 4 deletions(-) diff --git a/autoload/denops.vim b/autoload/denops.vim index 2518646e..9c0262f2 100644 --- a/autoload/denops.vim +++ b/autoload/denops.vim @@ -25,6 +25,14 @@ function! denops#request_async(name, method, params, success, failure) abort \) endfunction +function! denops#interrupt(...) abort + let l:args = a:0 ? [a:1] : [] + call denops#server#wait_async({ -> denops#_internal#server#chan#notify( + \ 'invoke', + \ ['interrupt', l:args], + \)}) +endfunction + " Configuration call denops#_internal#conf#define('denops#disabled', 0) call denops#_internal#conf#define('denops#deno', 'deno') diff --git a/denops/@denops-private/denops.ts b/denops/@denops-private/denops.ts index ea1cce72..bb3e5f54 100644 --- a/denops/@denops-private/denops.ts +++ b/denops/@denops-private/denops.ts @@ -8,7 +8,10 @@ const isBatchReturn = is.TupleOf([is.Array, is.String] as const); export type Host = Pick; -export type Service = Pick; +export type Service = Pick< + ServiceOrigin, + "dispatch" | "waitLoaded" | "interrupted" +>; export class DenopsImpl implements Denops { readonly name: string; @@ -32,6 +35,10 @@ export class DenopsImpl implements Denops { this.#service = service; } + get interrupted(): AbortSignal { + return this.#service.interrupted; + } + redraw(force?: boolean): Promise { return this.#host.redraw(force); } diff --git a/denops/@denops-private/denops_test.ts b/denops/@denops-private/denops_test.ts index 692770c3..9277a2ed 100644 --- a/denops/@denops-private/denops_test.ts +++ b/denops/@denops-private/denops_test.ts @@ -1,5 +1,5 @@ import type { Meta } from "jsr:@denops/core@6.0.6"; -import { assertEquals } from "jsr:@std/assert@0.225.1"; +import { assertEquals, assertInstanceOf } from "jsr:@std/assert@0.225.1"; import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; @@ -20,9 +20,14 @@ Deno.test("DenopsImpl", async (t) => { const service: Service = { dispatch: () => unimplemented(), waitLoaded: () => unimplemented(), + interrupted: new AbortController().signal, }; const denops = new DenopsImpl("dummy", meta, host, service); + await t.step("interrupted returns AbortSignal instance", () => { + assertInstanceOf(denops.interrupted, AbortSignal); + }); + await t.step("redraw() calls host.redraw()", async () => { using s = stub(host, "redraw"); await denops.redraw(); diff --git a/denops/@denops-private/host.ts b/denops/@denops-private/host.ts index d9ae557f..88f9c3de 100644 --- a/denops/@denops-private/host.ts +++ b/denops/@denops-private/host.ts @@ -50,6 +50,7 @@ export type Service = { bind(host: Host): void; load(name: string, script: string): Promise; reload(name: string): Promise; + interrupt(reason?: unknown): void; dispatch(name: string, fn: string, args: unknown[]): Promise; dispatchAsync( name: string, @@ -74,6 +75,11 @@ export function invoke( return service.reload( ...ensure(args, is.TupleOf([is.String] as const)), ); + case "interrupt": + service.interrupt( + ...ensure(args, is.ParametersOf([is.OptionalOf(is.Unknown)] as const)), + ); + return Promise.resolve(); case "dispatch": return service.dispatch( ...ensure(args, is.TupleOf([is.String, is.String, is.Array] as const)), diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index 008bef60..ce473c61 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -24,6 +24,7 @@ Deno.test("Neovim", async (t) => { bind: () => unimplemented(), load: () => unimplemented(), reload: () => unimplemented(), + interrupt: () => unimplemented(), dispatch: () => unimplemented(), dispatchAsync: () => unimplemented(), }; diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index c840390f..bac29fda 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -19,6 +19,7 @@ Deno.test("Vim", async (t) => { bind: () => unimplemented(), load: () => unimplemented(), reload: () => unimplemented(), + interrupt: () => unimplemented(), dispatch: () => unimplemented(), dispatchAsync: () => unimplemented(), }; diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index 847d649a..d1201440 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -12,6 +12,7 @@ Deno.test("invoke", async (t) => { const service: Omit = { load: () => unimplemented(), reload: () => unimplemented(), + interrupt: () => unimplemented(), dispatch: () => unimplemented(), dispatchAsync: () => unimplemented(), }; @@ -46,6 +47,28 @@ Deno.test("invoke", async (t) => { }); }); + await t.step("calls 'interrupt'", async (t) => { + await t.step("ok", async () => { + using s = stub(service, "interrupt"); + await invoke(service, "interrupt", []); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { args: [] }); + }); + + await t.step("ok (with reason)", async () => { + using s = stub(service, "interrupt"); + await invoke(service, "interrupt", ["reason"]); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { args: ["reason"] }); + }); + + await t.step("invalid args", () => { + using s = stub(service, "interrupt"); + assertThrows(() => invoke(service, "interrupt", ["a", "b"]), AssertError); + assertSpyCalls(s, 0); + }); + }); + await t.step("calls 'dispatch'", async (t) => { await t.step("ok", async () => { using s = stub(service, "dispatch"); diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index aaf71b7f..d3db77aa 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -13,8 +13,9 @@ type Waiter = { * Service manage plugins and is visible from the host (Vim/Neovim) through `invoke()` function. */ export class Service implements Disposable { - #plugins: Map = new Map(); - #waiters: Map = new Map(); + #interruptController = new AbortController(); + #plugins = new Map(); + #waiters = new Map(); #meta: Meta; #host?: Host; @@ -29,6 +30,10 @@ export class Service implements Disposable { return this.#waiters.get(name)!; } + get interrupted(): AbortSignal { + return this.#interruptController.signal; + } + bind(host: Host): void { this.#host = host; } @@ -77,6 +82,11 @@ export class Service implements Disposable { return this.#getWaiter(name).promise; } + interrupt(reason?: unknown): void { + this.#interruptController.abort(reason); + this.#interruptController = new AbortController(); + } + async #dispatch(name: string, fn: string, args: unknown[]): Promise { const plugin = this.#plugins.get(name); if (!plugin) { diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 8202b6fa..98d54098 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -3,6 +3,7 @@ import { assertEquals, assertMatch, assertRejects, + assertThrows, } from "jsr:@std/assert@0.225.1"; import { assertSpyCall, @@ -505,4 +506,24 @@ Deno.test("Service", async (t) => { }); }, ); + + await t.step( + "interrupt() sends interrupt signal to `interrupted` attribute", + () => { + const signal = service.interrupted; + signal.throwIfAborted(); // Should not throw + service.interrupt(); + assertThrows(() => signal.throwIfAborted()); + }, + ); + + await t.step( + "interrupt() sends interrupt signal to `interrupted` attribute with reason", + () => { + const signal = service.interrupted; + signal.throwIfAborted(); // Should not throw + service.interrupt("test"); + assertThrows(() => signal.throwIfAborted(), "test"); + }, + ); }); diff --git a/doc/denops.txt b/doc/denops.txt index 1f0e063d..90da5956 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -199,6 +199,26 @@ denops#request_async({name}, {method}, {params}, {success}, {failure}) \ { v -> s:success(v) }, \ { e -> s:failure(e) }, \) +< + *denops#interrupt()* +denops#interrupt([{reason}]) + Interrupts the process of plugins. It is assumed to be used to + interrupt the process of plugins when the user presses a key or + issues a command. + {reason} is an optional string to describe the reason for the + interruption. + Note that the interruption is not guaranteed to be immediate. + + Use the following mapping to interrupt the process of plugins: +> + " For normal/visual/select mode + noremap call denops#interrupt() + + " For insert mode + inoremap call denops#interrupt() + + " For command mode + cnoremap call denops#interrupt() < *denops#cache#update()* denops#cache#update([{options}]) From eece2a1ea321c29517fb1f1567262c88c3c5c497 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 1 Jun 2024 15:47:34 +0900 Subject: [PATCH 22/99] :memo: Add `RECOMMENDED` section to help --- doc/denops.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/denops.txt b/doc/denops.txt index 90da5956..ae09a900 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -28,6 +28,23 @@ API Reference: https://deno.land/x/denops_std/mod.ts ============================================================================= USAGE *denops-usage* +----------------------------------------------------------------------------- +RECOMMENDED *denops-recommended* + +Add the following recommended settings to your |vimrc| or |init.vim|: +> + " Interrupt the process of plugins via + noremap call denops#interrupt() + inoremap call denops#interrupt() + cnoremap call denops#interrupt() + + " Restart Denops server + command! DenopsRestart call denops#server#restart() + + " Fix Deno module cache issue + command! DenopsFixCache call denops#cache#update(#{reload: v:true}) +< + ----------------------------------------------------------------------------- SHARED SERVER *denops-shared-server* From eecadec77d31bea97b3873497d50855793290b6e Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 11 Jun 2024 21:35:23 +0900 Subject: [PATCH 23/99] :+1: use workerio@4.0.1 --- denops/@denops-private/worker.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index 1074c4c2..7ebb45f0 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -1,7 +1,10 @@ +/// +/// + import { readableStreamFromWorker, writableStreamFromWorker, -} from "jsr:@lambdalisue/workerio@4.0.0"; +} from "jsr:@lambdalisue/workerio@4.0.1"; import { ensure } from "jsr:@core/unknownutil@3.18.0"; import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import type { HostConstructor } from "./host.ts"; @@ -35,9 +38,8 @@ function formatArgs(args: unknown[]): string[] { } async function main(): Promise { - const worker = self as unknown as Worker; - const writer = writableStreamFromWorker(worker); - const [reader, detector] = readableStreamFromWorker(worker).tee(); + const writer = writableStreamFromWorker(self); + const [reader, detector] = readableStreamFromWorker(self).tee(); // Detect host from payload const hostCtor = await detectHost(detector); From 5b5ee965874d6563f078627ed352365bdc25b51e Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 13 Jun 2024 15:47:23 +0900 Subject: [PATCH 24/99] :+1: only import for cache There is no need to exports. --- denops/@denops-private/mod.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/denops/@denops-private/mod.ts b/denops/@denops-private/mod.ts index 97c2f02e..47a24b3e 100644 --- a/denops/@denops-private/mod.ts +++ b/denops/@denops-private/mod.ts @@ -7,5 +7,5 @@ * * @module */ -export * from "./cli.ts"; -export * from "./worker.ts"; +import "./cli.ts"; +import "./worker.ts"; From ed3a0e2b3ca43172ed1c50c78d4ddd39e4a72f9b Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 13 Jun 2024 11:08:23 +0900 Subject: [PATCH 25/99] :muscle: wrap logics into main() function --- denops/@denops-private/worker.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index 7ebb45f0..c7817b82 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -37,7 +37,7 @@ function formatArgs(args: unknown[]): string[] { }); } -async function main(): Promise { +async function connectHost(): Promise { const writer = writableStreamFromWorker(self); const [reader, detector] = readableStreamFromWorker(self).tee(); @@ -85,17 +85,23 @@ async function main(): Promise { await host.waitClosed(); } -if (import.meta.main) { +async function main(): Promise { // Avoid denops server crash via UnhandledRejection globalThis.addEventListener("unhandledrejection", (event) => { event.preventDefault(); console.error(`Unhandled rejection:`, event.reason); }); - await main().catch((err) => { + try { + await connectHost(); + } catch (err) { console.error( `Internal error occurred in Worker`, err, ); - }); + } +} + +if (import.meta.main) { + await main(); } From 460fc3e2192427eed795b975fbded701c1cd3daf Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 13 Jun 2024 11:13:18 +0900 Subject: [PATCH 26/99] :herb: add tests for worker --- denops/@denops-private/testutil/mock.ts | 11 + denops/@denops-private/worker.ts | 2 +- denops/@denops-private/worker_test.ts | 342 ++++++++++++++++++++++++ 3 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 denops/@denops-private/worker_test.ts diff --git a/denops/@denops-private/testutil/mock.ts b/denops/@denops-private/testutil/mock.ts index 1830bc2e..fa8f890b 100644 --- a/denops/@denops-private/testutil/mock.ts +++ b/denops/@denops-private/testutil/mock.ts @@ -1,4 +1,5 @@ import { AssertionError, unimplemented } from "jsr:@std/assert@0.225.2"; +import type { Meta } from "jsr:@denops/core@6.0.6"; /** Returns a Promise that is never resolves or rejects. */ export function pendingPromise(): Promise { @@ -101,3 +102,13 @@ export function createFakeWorker(): Worker { terminate: () => unimplemented(), }; } + +/** Returns as fake `Meta` object. */ +export function createFakeMeta(): Meta { + return { + mode: "test", + host: "vim", + version: "9.1.457", + platform: "linux", + }; +} diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index c7817b82..53ab321b 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -85,7 +85,7 @@ async function connectHost(): Promise { await host.waitClosed(); } -async function main(): Promise { +export async function main(): Promise { // Avoid denops server crash via UnhandledRejection globalThis.addEventListener("unhandledrejection", (event) => { event.preventDefault(); diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts new file mode 100644 index 00000000..877d8e9a --- /dev/null +++ b/denops/@denops-private/worker_test.ts @@ -0,0 +1,342 @@ +// @deno-types="npm:@types/sinon@17.0.3" +import sinon from "npm:sinon@17.0.1"; +import { assertEquals, assertInstanceOf } from "jsr:@std/assert@0.225.2"; +import { delay } from "jsr:@std/async@0.224.0/delay"; +import { DisposableStack } from "jsr:@nick/dispose@1.1.0/disposable-stack"; +import * as nvimCodec from "jsr:@lambdalisue/messagepack@^1.0.1"; +import { createFakeMeta } from "./testutil/mock.ts"; +import { main } from "./worker.ts"; + +const CONSOLE_PATCH_METHODS = [ + "log", + "info", + "debug", + "warn", + "error", +] as const satisfies (keyof typeof console)[]; + +function stubConsole() { + const target = globalThis.console; + const sandbox = sinon.createSandbox(); + const entries = CONSOLE_PATCH_METHODS.map((name) => { + type Fn = typeof target[typeof name]; + const fn = sandbox.fake((() => {}) as Fn); + const bindedFn = fn.bind(target); + const get = sandbox.fake(() => bindedFn); + const set = sandbox.fake<[fn: Fn], void>(); + sandbox.stub(target, name).get(get).set(set); + return [name, Object.assign(fn, { get, set })] as const; + }); + const methods = Object.fromEntries(entries) as Record< + typeof entries[number][0], + typeof entries[number][1] + >; + return Object.assign(methods, { + [Symbol.dispose]() { + sandbox.restore(); + }, + }); +} + +function stubMessage() { + const workerGlobal = globalThis as DedicatedWorkerGlobalScope; + const sandbox = sinon.createSandbox(); + sandbox.define(workerGlobal, "postMessage", () => {}); + const postMessage = sandbox.stub(workerGlobal, "postMessage"); + sandbox.define(workerGlobal, "onmessage", null); + const onmessage = sandbox.stub(workerGlobal, "onmessage"); + const fakeHostMessage = (data: unknown) => { + workerGlobal.onmessage?.(new MessageEvent("message", { data })); + }; + return { + postMessage, + onmessage, + /** Send fake message to worker global. */ + fakeHostMessage, + [Symbol.dispose]() { + sandbox.restore(); + }, + }; +} + +function spyAddEventListener() { + const stack = new DisposableStack(); + const stub = stack.adopt( + sinon.stub(globalThis, "addEventListener") + .callsFake((...args) => { + stub.wrappedMethod.call(globalThis, ...args); + stack.defer(() => { + globalThis.removeEventListener(...args); + }); + }), + (stub) => stub.restore(), + ); + return Object.assign(stub, { + [Symbol.dispose]() { + stack.dispose(); + }, + }); +} + +function stubDenoCommand() { + const sandbox = sinon.createSandbox(); + sandbox.stub(Deno.Command.prototype, "output").resolves({ + success: true, + stdout: _encoder.encode("v11.22.33-abcdef01"), + stderr: new Uint8Array(), + code: 0, + signal: null, + }); + return { + [Symbol.dispose]() { + sandbox.restore(); + }, + }; +} + +const _encoder = new TextEncoder(); +const _decoder = new TextDecoder(); +const vimCodec = { + encode: (value: unknown) => _encoder.encode(`${JSON.stringify(value)}\n`), + decode: (bytes: Uint8Array) => JSON.parse(_decoder.decode(bytes)), +}; + +const HOSTS = ["vim", "nvim"] as const; +const MODES = ["test", "debug"] as const; +const matrix = HOSTS.flatMap((host) => MODES.map((mode) => ({ host, mode }))); + +for (const { host, mode } of matrix) { + Deno.test(`(host: ${host}, mode: ${mode})`, async (t) => { + await t.step("main()", async (t) => { + using messageStub = stubMessage(); + using consoleStub = stubConsole(); + using _addEventListenerSpy = spyAddEventListener(); + using _denoCommandStub = stubDenoCommand(); + const usePostMessageHistory = () => ({ + [Symbol.dispose]: () => messageStub.postMessage.resetHistory(), + }); + const fakeMeta = { ...createFakeMeta(), host, mode }; + + const mainPromise = main(); + + await t.step("catches unhandledrejection", async () => { + const error = new Error("error"); + + new Promise(() => { + throw error; + }); + + await delay(0); + assertEquals(consoleStub.error.firstCall.args, [ + "Unhandled rejection:", + error, + ]); + }); + + // Initial message from the host. + if (host === "vim") { + messageStub.fakeHostMessage(vimCodec.encode([0, ["void"]])); + } else { + messageStub.fakeHostMessage(nvimCodec.encode([2, "void", []])); + } + + await t.step("requests Meta data", async () => { + using _ = usePostMessageHistory(); + await delay(0); + assertEquals(messageStub.postMessage.callCount, 1); + if (host === "vim") { + assertEquals( + vimCodec.decode(messageStub.postMessage.getCall(0).args[0]), + [ + "call", + "denops#api#vim#call", + ["denops#_internal#meta#get", []], + -1, + ], + ); + messageStub.fakeHostMessage(vimCodec.encode([-1, [fakeMeta, ""]])); + } else { + assertEquals( + nvimCodec.decode(messageStub.postMessage.getCall(0).args[0]), + [ + 0, + 0, + "nvim_call_function", + ["denops#_internal#meta#get", []], + ], + ); + messageStub.fakeHostMessage(nvimCodec.encode([1, 0, null, fakeMeta])); + } + }); + + if (host === "nvim") { + await t.step({ + name: "sets client info", + fn: async () => { + using _ = usePostMessageHistory(); + await delay(0); + assertEquals(messageStub.postMessage.callCount, 1); + const request = nvimCodec.decode( + messageStub.postMessage.getCall(0).args[0], + ) as unknown[]; + assertEquals(request.slice(0, 3), [0, 1, "nvim_set_client_info"]); + messageStub.fakeHostMessage(nvimCodec.encode([1, 1, null, 0])); + }, + }); + } + + await t.step("doautocmd `User DenopsReady`", async () => { + using _ = usePostMessageHistory(); + await delay(0); + assertEquals(messageStub.postMessage.callCount, 1); + if (host === "vim") { + assertEquals( + vimCodec.decode(messageStub.postMessage.getCall(0).args[0]), + [ + "call", + "denops#api#vim#call", + ["execute", ["doautocmd User DenopsReady", ""]], + -2, + ], + ); + messageStub.fakeHostMessage(vimCodec.encode([-2, ["", ""]])); + } else { + assertEquals( + nvimCodec.decode(messageStub.postMessage.getCall(0).args[0]), + [ + 0, + 2, + "nvim_call_function", + ["execute", ["doautocmd User DenopsReady", ""]], + ], + ); + messageStub.fakeHostMessage(nvimCodec.encode([1, 2, null, ""])); + } + }); + + await t.step("patches `console`", async (t) => { + for (const name of CONSOLE_PATCH_METHODS) { + await t.step(`.${name}()`, async (t) => { + await t.step("is patched", () => { + assertEquals(consoleStub[name].set.callCount, 1); + assertInstanceOf( + consoleStub[name].set.getCall(0).args[0], + Function, + ); + }); + + if (name === "debug" && fakeMeta.mode !== "debug") { + await t.step({ + name: "does nothing", + fn: async () => { + using _ = usePostMessageHistory(); + const fn = consoleStub[name].set.getCall(0).args[0]; + + fn.apply(globalThis.console, ["foo", 123, false]); + + await delay(0); + assertEquals(messageStub.postMessage.callCount, 0); + }, + }); + } else { + await t.step({ + name: `notifies \`denops#_internal#echo#${name}\``, + fn: async () => { + using _ = usePostMessageHistory(); + const fn = consoleStub[name].set.getCall(0).args[0]; + const errorWithStack = Object.assign( + new Error("fake-error-with-stack"), + { + stack: + "Error: fake-error-with-stack\n at foo (bar.ts:10:20)", + }, + ); + const errorWithoutStack = Object.assign( + new Error("fake-error-without-stack"), + { stack: undefined }, + ); + + fn.apply(globalThis.console, [ + "foo", + 123, + false, + errorWithStack, + errorWithoutStack, + ]); + + await delay(0); + assertEquals(messageStub.postMessage.callCount, 1); + if (host === "vim") { + assertEquals( + vimCodec.decode( + messageStub.postMessage.getCall(0).args[0], + ), + [ + "call", + `denops#_internal#echo#${name}`, + [ + "foo", + "123", + "false", + "Error: fake-error-with-stack\n at foo (bar.ts:10:20)", + "Error: fake-error-without-stack", + ], + ], + ); + } else { + assertEquals( + nvimCodec.decode( + messageStub.postMessage.getCall(0).args[0], + ), + [ + 2, + "nvim_call_function", + [ + `denops#_internal#echo#${name}`, + [ + "foo", + "123", + "false", + "Error: fake-error-with-stack\n at foo (bar.ts:10:20)", + "Error: fake-error-without-stack", + ], + ], + ], + ); + } + }, + }); + } + }); + } + }); + + await t.step("resolves when stream is closed", async () => { + // NOTE: Send `null` to close workerio stream. + messageStub.fakeHostMessage(null); + await mainPromise; + }); + }); + await t.step( + "main() outputs an error log when an internal error occurs", + async () => { + using messageStub = stubMessage(); + using consoleStub = stubConsole(); + using _addEventListenerSpy = spyAddEventListener(); + + const error = new Error("fake-error"); + messageStub.onmessage.set(() => { + throw error; + }); + + await main(); + + assertEquals(consoleStub.error.callCount, 1); + assertEquals(consoleStub.error.getCall(0).args, [ + "Internal error occurred in Worker", + error, + ]); + }, + ); + }); +} From 6a2506c5b9af776c1e7068899b686cc5f2b73851 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 26 May 2024 14:14:31 +0900 Subject: [PATCH 27/99] :herb: remove unnecessary `verbose` --- tests/denops/server_test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts index ef14b023..ffdd81f8 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/server_test.ts @@ -74,7 +74,6 @@ testHost({ testHost({ name: "Denops server", mode: "all", - verbose: true, fn: async (host, t) => { // NOTE: The status may transition to `preparing`, so get it within execute. // SEE: https://github.com/vim-denops/denops.vim/issues/354 From b43cba984f7c84e504b72fe3e8961dfe16432a26 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 26 May 2024 14:13:48 +0900 Subject: [PATCH 28/99] :+1: terminate worker gracefully --- denops/@denops-private/cli.ts | 64 ++++++-- denops/@denops-private/cli_test.ts | 210 ++++++++++++++++++++++---- denops/@denops-private/worker.ts | 8 +- denops/@denops-private/worker_test.ts | 148 +++++++++++++----- 4 files changed, 350 insertions(+), 80 deletions(-) diff --git a/denops/@denops-private/cli.ts b/denops/@denops-private/cli.ts index 9c655527..58fa8114 100644 --- a/denops/@denops-private/cli.ts +++ b/denops/@denops-private/cli.ts @@ -2,20 +2,14 @@ import { readableStreamFromWorker, writableStreamFromWorker, } from "jsr:@lambdalisue/workerio@4.0.0"; +import { deadline } from "jsr:@std/async@0.224.0/deadline"; import { parseArgs } from "jsr:@std/cli@0.224.3/parse-args"; +import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; const WORKER_SCRIPT = import.meta.resolve("./worker.ts"); +const WORKER_CLOSE_TIMEOUT_MS = 5000; -async function handleConn( - conn: Deno.TcpConn, - { quiet }: { quiet?: boolean }, -): Promise { - const remoteAddr = conn.remoteAddr; - const name = `${remoteAddr.hostname}:${remoteAddr.port}`; - if (!quiet) { - console.info(`${name} is connected`); - } - +async function processWorker(name: string, conn: Deno.Conn): Promise { const worker = new Worker(WORKER_SCRIPT, { name, type: "module", @@ -25,10 +19,16 @@ async function handleConn( try { await Promise.race([ - reader.pipeTo(conn.writable), + reader.pipeTo(conn.writable, { preventCancel: true }), conn.readable.pipeTo(writer), ]); } finally { + try { + const closeWaiter = reader.pipeTo(new WritableStream()); + await deadline(closeWaiter, WORKER_CLOSE_TIMEOUT_MS); + } catch { + // `reader` already closed or deadline has passed, do nothing + } // Terminate worker to avoid leak worker.terminate(); } @@ -57,14 +57,48 @@ export async function main(args: string[]): Promise { ); } - for await (const conn of listener) { - handleConn(conn, { quiet }).catch((err) => + const handleConn = async (conn: Deno.Conn): Promise => { + const { remoteAddr } = conn; + const name = `${remoteAddr.hostname}:${remoteAddr.port}`; + if (!quiet) { + console.info(`${name} is connected`); + } + try { + await processWorker(name, conn); + } catch (err) { console.error( "Internal error occurred and Host/Denops connection is dropped", err, - ) - ); + ); + } finally { + try { + conn.close(); + } catch { + // Already closed, do nothing + } + if (!quiet) { + console.info(`${name} is closed`); + } + } + }; + + const connections = new Set>(); + + { + using sigintTrap = asyncSignal("SIGINT"); + sigintTrap.catch(() => { + listener.close(); + }); + + for await (const conn of listener) { + const handler = handleConn(conn) + .finally(() => connections.delete(handler)); + connections.add(handler); + } } + + // The listener is closed and waits for existing connections to terminate. + await Promise.allSettled([...connections]); } if (import.meta.main) { diff --git a/denops/@denops-private/cli_test.ts b/denops/@denops-private/cli_test.ts index 5c9e8431..6fed7e0e 100644 --- a/denops/@denops-private/cli_test.ts +++ b/denops/@denops-private/cli_test.ts @@ -13,10 +13,13 @@ import { assertSpyCalls, resolvesNext, returnsNext, + spy, type Stub, stub, } from "jsr:@std/testing@0.224.0/mock"; +import { FakeTime } from "jsr:@std/testing@0.224.0/time"; import { delay } from "jsr:@std/async@0.224.0/delay"; +import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { createFakeTcpConn, createFakeTcpListener, @@ -42,6 +45,8 @@ const stubDenoListen = ( }; Deno.test("main()", async (t) => { + using deno_addSignalListener = stub(Deno, "addSignalListener"); + await t.step("listens", async (t) => { await t.step("127.0.0.1:32123", async () => { const fakeTcpListener = createFakeTcpListener(); @@ -211,7 +216,7 @@ Deno.test("main()", async (t) => { returnsNext([fakeWorker]), ); using worker_terminate = stub(fakeWorker, "terminate"); - using _worker_postMessage = stub(fakeWorker, "postMessage"); + using worker_postMessage = stub(fakeWorker, "postMessage"); using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); using _listener_accept = stub( @@ -251,21 +256,48 @@ Deno.test("main()", async (t) => { await t.step("when the listener is closed", async (t) => { fakeTcpListener.close(); - await p; + await delay(0); await t.step("does not calls Worker.terminate()", () => { assertSpyCalls(worker_terminate, 0); }); + + await t.step("pendings main() Promise", async () => { + assertEquals(await promiseState(p), "pending"); + }); }); await t.step("when the connection is closed", async (t) => { - await t.step("calls Worker.terminate()", async () => { - connStreamCloseWaiter.resolve(); + connStreamCloseWaiter.resolve(); + await delay(0); + + await t.step("does not calls Worker.terminate()", () => { + assertSpyCalls(worker_terminate, 0); + }); + + await t.step("pendings main() Promise", async () => { + assertEquals(await promiseState(p), "pending"); + }); + + await t.step( + "post a `null` message to tell the worker to close", + () => { + assertSpyCalls(worker_postMessage, 1); + assertSpyCallArgs(worker_postMessage, 0, [null]); + }, + ); - // Resolves microtasks. + await t.step("and the worker stream is closed", async (t) => { + fakeWorker.onmessage(new MessageEvent("message", { data: null })); await delay(0); - assertSpyCalls(worker_terminate, 1); + await t.step("calls Worker.terminate()", () => { + assertSpyCalls(worker_terminate, 1); + }); + + await t.step("resolves main() Promise", async () => { + assertEquals(await promiseState(p), "fulfilled"); + }); }); await t.step("outputs error logs", () => { @@ -276,6 +308,7 @@ Deno.test("main()", async (t) => { }); }); } + { const fakeTcpListener = createFakeTcpListener(); sinon.stub(fakeTcpListener, "addr").get(() => ({ @@ -325,27 +358,111 @@ Deno.test("main()", async (t) => { using console_error = stub(console, "error"); const p = main([]); - - // Resolves microtasks. await delay(0); - assertSpyCalls(globalThis_Worker, 1); + await t.step("when the connection is closed", async (t) => { + assertSpyCalls(globalThis_Worker, 1); - fakeTcpListener.close(); - await p; + await t.step("when the worker close times out", async (t) => { + using _time = new FakeTime(); - await t.step("when the worker stream is closed", async (t) => { - await t.step("calls Worker.terminate()", async () => { - assertSpyCalls(worker_terminate, 0); + connStreamCloseWaiter.resolve(); + const WORKER_CLOSE_TIMEOUT_MS = 5000; + await _time.tickAsync(WORKER_CLOSE_TIMEOUT_MS); + await _time.nextAsync(); - fakeWorker.onmessage(new MessageEvent("message", { data: null })); + await t.step("calls Worker.terminate()", () => { + assertSpyCalls(worker_terminate, 1); + }); + }); - // Resolves microtasks. - await delay(0); + await t.step("outputs error logs", () => { + assertMatch( + console_error.calls.flatMap((c) => c.args).join(" "), + /Internal error occurred/, + ); + }); + + await t.step("pendings main() Promise", async () => { + assertEquals(await promiseState(p), "pending"); + }); + + await t.step("and the listner is closed", async (t) => { + fakeTcpListener.close(); + + await t.step("resolves main() Promise", async () => { + assertEquals(await promiseState(p), "fulfilled"); + }); + }); + }); + } + + { + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + + const fakeTcpConn = createFakeTcpConn(); + const connStreamCloseWaiter = Promise.withResolvers(); + sinon.stub(fakeTcpConn, "remoteAddr").get(() => ({ + hostname: "stub-remote.example.net", + port: 98765, + })); + sinon.stub(fakeTcpConn, "readable").get(() => + new ReadableStream({ + start(con) { + connStreamCloseWaiter.promise.then(() => con.close()); + }, + }) + ); + sinon.stub(fakeTcpConn, "writable").get(() => + new WritableStream({ + start(con) { + connStreamCloseWaiter.promise.then(() => + con.error("fake-tcpconn-writable-closed") + ); + }, + }) + ); + + const fakeWorker = createFakeWorker(); + using globalThis_Worker = stub( + globalThis, + "Worker", + returnsNext([fakeWorker]), + ); + using worker_terminate = stub(fakeWorker, "terminate"); + using _worker_postMessage = stub(fakeWorker, "postMessage"); + + using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using _listener_accept = stub( + fakeTcpListener, + "accept", + resolvesNext([fakeTcpConn, pendingPromise()]), + ); + using _console_info = stub(console, "info"); + using console_error = stub(console, "error"); + const p = main([]); + await delay(0); + + await t.step("when the worker stream is closed", async (t) => { + assertSpyCalls(globalThis_Worker, 1); + fakeTcpListener.close(); + + fakeWorker.onmessage(new MessageEvent("message", { data: null })); + await delay(0); + + await t.step("calls Worker.terminate()", () => { assertSpyCalls(worker_terminate, 1); }); + await t.step("resolves main() Promise", async () => { + assertEquals(await promiseState(p), "fulfilled"); + }); + await t.step("does not outputs error logs", () => { assertNotMatch( console_error.calls.flatMap((c) => c.args).join(" "), @@ -423,8 +540,6 @@ Deno.test("main()", async (t) => { using console_error = stub(console, "error"); const p = main(["--quiet"]); - - // Resolves microtasks. await delay(0); await t.step("does not outputs info logs", () => { @@ -435,21 +550,62 @@ Deno.test("main()", async (t) => { }); fakeTcpListener.close(); - await p; await t.step("when the connection is closed", async (t) => { connStreamCloseWaiter.resolve(); - - // Resolves microtasks. await delay(0); - await t.step("outputs error logs", () => { - assertStringIncludes( - console_error.calls.flatMap((c) => c.args).join(" "), - "Internal error occurred and Host/Denops connection is dropped fake-tcpconn-writable-closed", - ); + await t.step("and the worker stream is closed", async (t) => { + fakeWorker.onmessage(new MessageEvent("message", { data: null })); + await delay(0); + + await t.step("outputs error logs", () => { + assertStringIncludes( + console_error.calls.flatMap((c) => c.args).join(" "), + "Internal error occurred and Host/Denops connection is dropped fake-tcpconn-writable-closed", + ); + }); }); }); + + await p; + }); + }); + + await t.step("listens SIGINT", async (t) => { + const prevSignalListenerCalls = deno_addSignalListener.calls.length; + const fakeTcpListener = createFakeTcpListener(); + sinon.stub(fakeTcpListener, "addr").get(() => ({ + hostname: "stub.example.net", + port: 99999, + })); + using listener_close = spy(fakeTcpListener, "close"); + using _deno_listen = stubDenoListen(returnsNext([fakeTcpListener])); + using _console_info = stub(console, "info"); + + const mainPromise = main([]); + await delay(0); + + assertSpyCalls(deno_addSignalListener, prevSignalListenerCalls + 1); + const [ + signal, + signalHandler, + ] = deno_addSignalListener.calls[prevSignalListenerCalls].args; + assertEquals(signal, "SIGINT"); + + await t.step("when SINGINT is trapped", async (t) => { + assertSpyCalls(listener_close, 0); + + signalHandler(); + await delay(0); + + await t.step("closes the listener", () => { + assertSpyCalls(listener_close, 1); + }); + + await t.step("resolves main() Promise", async () => { + assertEquals(await promiseState(mainPromise), "fulfilled"); + }); }); }); }); diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index 53ab321b..08b4abdf 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -7,6 +7,7 @@ import { } from "jsr:@lambdalisue/workerio@4.0.1"; import { ensure } from "jsr:@core/unknownutil@3.18.0"; import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; +import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; import type { HostConstructor } from "./host.ts"; import { Vim } from "./host/vim.ts"; import { Neovim } from "./host/nvim.ts"; @@ -79,10 +80,14 @@ async function connectHost(): Promise { }; // Start service + using sigintTrap = asyncSignal("SIGINT"); using service = new Service(meta); await host.init(service); await host.call("execute", "doautocmd User DenopsReady", ""); - await host.waitClosed(); + await Promise.race([ + host.waitClosed(), + sigintTrap, + ]); } export async function main(): Promise { @@ -100,6 +105,7 @@ export async function main(): Promise { err, ); } + self.close(); } if (import.meta.main) { diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index 877d8e9a..aaaa60f5 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -1,6 +1,11 @@ // @deno-types="npm:@types/sinon@17.0.3" import sinon from "npm:sinon@17.0.1"; -import { assertEquals, assertInstanceOf } from "jsr:@std/assert@0.225.2"; +import { + assertEquals, + assertInstanceOf, + assertMatch, +} from "jsr:@std/assert@0.225.2"; +import { assertSpyCalls, stub } from "jsr:@std/testing@0.224.0/mock"; import { delay } from "jsr:@std/async@0.224.0/delay"; import { DisposableStack } from "jsr:@nick/dispose@1.1.0/disposable-stack"; import * as nvimCodec from "jsr:@lambdalisue/messagepack@^1.0.1"; @@ -112,6 +117,8 @@ for (const { host, mode } of matrix) { using consoleStub = stubConsole(); using _addEventListenerSpy = spyAddEventListener(); using _denoCommandStub = stubDenoCommand(); + using deno_addSignalListener = stub(Deno, "addSignalListener"); + using self_close = stub(globalThis, "close"); const usePostMessageHistory = () => ({ [Symbol.dispose]: () => messageStub.postMessage.resetHistory(), }); @@ -170,18 +177,15 @@ for (const { host, mode } of matrix) { }); if (host === "nvim") { - await t.step({ - name: "sets client info", - fn: async () => { - using _ = usePostMessageHistory(); - await delay(0); - assertEquals(messageStub.postMessage.callCount, 1); - const request = nvimCodec.decode( - messageStub.postMessage.getCall(0).args[0], - ) as unknown[]; - assertEquals(request.slice(0, 3), [0, 1, "nvim_set_client_info"]); - messageStub.fakeHostMessage(nvimCodec.encode([1, 1, null, 0])); - }, + await t.step("sets client info", async () => { + using _ = usePostMessageHistory(); + await delay(0); + assertEquals(messageStub.postMessage.callCount, 1); + const request = nvimCodec.decode( + messageStub.postMessage.getCall(0).args[0], + ) as unknown[]; + assertEquals(request.slice(0, 3), [0, 1, "nvim_set_client_info"]); + messageStub.fakeHostMessage(nvimCodec.encode([1, 1, null, 0])); }); } @@ -226,17 +230,14 @@ for (const { host, mode } of matrix) { }); if (name === "debug" && fakeMeta.mode !== "debug") { - await t.step({ - name: "does nothing", - fn: async () => { - using _ = usePostMessageHistory(); - const fn = consoleStub[name].set.getCall(0).args[0]; + await t.step("does nothing", async () => { + using _ = usePostMessageHistory(); + const fn = consoleStub[name].set.getCall(0).args[0]; - fn.apply(globalThis.console, ["foo", 123, false]); + fn.apply(globalThis.console, ["foo", 123, false]); - await delay(0); - assertEquals(messageStub.postMessage.callCount, 0); - }, + await delay(0); + assertEquals(messageStub.postMessage.callCount, 0); }); } else { await t.step({ @@ -311,32 +312,105 @@ for (const { host, mode } of matrix) { } }); - await t.step("resolves when stream is closed", async () => { + await t.step("listens SIGINT", () => { + assertSpyCalls(deno_addSignalListener, 1); + const [signal, signalHandler] = deno_addSignalListener.calls[0].args; + assertEquals(signal, "SIGINT"); + assertInstanceOf(signalHandler, Function); + }); + + await t.step("closes worker when stream is closed", async () => { + assertSpyCalls(self_close, 0); + // NOTE: Send `null` to close workerio stream. messageStub.fakeHostMessage(null); + + await delay(0); + assertSpyCalls(self_close, 1); await mainPromise; }); }); - await t.step( - "main() outputs an error log when an internal error occurs", - async () => { - using messageStub = stubMessage(); - using consoleStub = stubConsole(); - using _addEventListenerSpy = spyAddEventListener(); - - const error = new Error("fake-error"); - messageStub.onmessage.set(() => { - throw error; - }); - await main(); + await t.step("main() if it raises an internal error", async (t) => { + using messageStub = stubMessage(); + using consoleStub = stubConsole(); + using _addEventListenerSpy = spyAddEventListener(); + using self_close = stub(globalThis, "close"); + + const error = new Error("fake-error"); + messageStub.onmessage.set(() => { + throw error; + }); + await main(); + + await t.step("outputs an error log", () => { assertEquals(consoleStub.error.callCount, 1); assertEquals(consoleStub.error.getCall(0).args, [ "Internal error occurred in Worker", error, ]); - }, - ); + }); + + await t.step("closes worker", () => { + assertSpyCalls(self_close, 1); + }); + }); + + await t.step("main() if SIGINT is trapped", async (t) => { + using messageStub = stubMessage(); + using consoleStub = stubConsole(); + using _addEventListenerSpy = spyAddEventListener(); + using _denoCommandStub = stubDenoCommand(); + using deno_addSignalListener = stub(Deno, "addSignalListener"); + using self_close = stub(globalThis, "close"); + const fakeMeta = { ...createFakeMeta(), host, mode }; + + const mainPromise = main(); + + if (host === "vim") { + // Initial message from the host. + messageStub.fakeHostMessage(vimCodec.encode([0, ["void"]])); + await delay(0); + // requests Meta data + messageStub.fakeHostMessage(vimCodec.encode([-1, [fakeMeta, ""]])); + await delay(0); + // doautocmd `User DenopsReady` + messageStub.fakeHostMessage(vimCodec.encode([-2, ["", ""]])); + await delay(0); + } else { + // Initial message from the host. + messageStub.fakeHostMessage(nvimCodec.encode([2, "void", []])); + await delay(0); + // requests Meta data + messageStub.fakeHostMessage(nvimCodec.encode([1, 0, null, fakeMeta])); + await delay(0); + // sets client info + messageStub.fakeHostMessage(nvimCodec.encode([1, 1, null, 0])); + await delay(0); + // doautocmd `User DenopsReady` + messageStub.fakeHostMessage(nvimCodec.encode([1, 2, null, ""])); + await delay(0); + } + + const [_signal, signalHandler] = deno_addSignalListener.calls[0].args; + consoleStub.error.resetHistory(); + + signalHandler(); + await delay(0); + + await t.step("outputs an error log", () => { + assertEquals(consoleStub.error.callCount, 1); + assertMatch( + `${consoleStub.error.getCall(0).args[1]}`, + /^SignalError: SIGINT is trapped/, + ); + }); + + await t.step("closes worker", async () => { + assertSpyCalls(self_close, 1); + await mainPromise; + }); + }); }); } From 48682fc33bb9f1823739fc71a5a6d6b0d95a8551 Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 15 Jun 2024 14:43:07 +0900 Subject: [PATCH 29/99] :herb: prevent error in test helper - Prevent error when aborting the process. - Wait for the stdout cancelled. --- .../@denops-private/testutil/shared_server.ts | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/denops/@denops-private/testutil/shared_server.ts b/denops/@denops-private/testutil/shared_server.ts index 10b52983..a4a5accb 100644 --- a/denops/@denops-private/testutil/shared_server.ts +++ b/denops/@denops-private/testutil/shared_server.ts @@ -35,6 +35,7 @@ export async function useSharedServer( }; const aborter = new AbortController(); const { signal } = aborter; + const cmd = Deno.execPath(); const script = resolve(denopsPath, "denops/@denops-private/cli.ts"); const args = [ @@ -53,31 +54,47 @@ export async function useSharedServer( env, signal, }).spawn(); - try { - const [stdout, verboseStdout] = proc.stdout - .pipeThrough(new TextDecoderStream(), { signal }) - .pipeThrough(new TextLineStream()) - .tee(); - if (verbose) { - verboseStdout.pipeTo( - new WritableStream({ - write: (out) => console.log(out), - }), - ).catch(() => {}); + + let stdout = proc.stdout + .pipeThrough(new TextDecoderStream(), { signal }) + .pipeThrough(new TextLineStream()); + if (verbose) { + const [splitStdout, verboseStdout] = stdout.tee(); + stdout = splitStdout; + verboseStdout.pipeTo( + new WritableStream({ + write: (out) => console.log(out), + }), + ).catch(() => {}); + } + + const abort = async (reason: unknown) => { + try { + aborter.abort(reason); + } catch { + // Already exited, do nothing. } + await Promise.all([ + stdout.locked ? undefined : stdout.cancel(reason), + proc.status, + ]); + await proc.stdout.cancel(reason); + }; + + try { const addr = await deadline(pop(stdout), timeout); assert(typeof addr === "string"); return { addr, stdout, async [Symbol.asyncDispose]() { - aborter.abort("useSharedServer disposed"); - await proc.status; + if (!signal.aborted) { + await abort("useSharedServer disposed"); + } }, }; } catch (e) { - aborter.abort(e); - await proc.status; + await abort(e); throw e; } } From 658ecdaeb08ad88956e9c3fb432cc8a769d6aab9 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 16 Jun 2024 16:17:25 +0900 Subject: [PATCH 30/99] :+1: satisfies Service as HostService --- denops/@denops-private/host.ts | 2 +- denops/@denops-private/service.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/denops/@denops-private/host.ts b/denops/@denops-private/host.ts index 88f9c3de..b641048a 100644 --- a/denops/@denops-private/host.ts +++ b/denops/@denops-private/host.ts @@ -45,7 +45,7 @@ export type HostConstructor = { }; // Minimum interface of Service that Host is relies on -type CallbackId = string; +export type CallbackId = string; export type Service = { bind(host: Host): void; load(name: string, script: string): Promise; diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index d3db77aa..f11e58e2 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -2,6 +2,7 @@ import type { Denops, Meta } from "jsr:@denops/core@6.0.6"; import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.0.0"; import { DenopsImpl, type Host } from "./denops.ts"; +import type { CallbackId, Service as HostService } from "./host.ts"; // We can use `PromiseWithResolvers` but Deno 1.38 doesn't have `PromiseWithResolvers` type Waiter = { @@ -12,7 +13,7 @@ type Waiter = { /** * Service manage plugins and is visible from the host (Vim/Neovim) through `invoke()` function. */ -export class Service implements Disposable { +export class Service implements HostService, Disposable { #interruptController = new AbortController(); #plugins = new Map(); #waiters = new Map(); @@ -107,8 +108,8 @@ export class Service implements Disposable { name: string, fn: string, args: unknown[], - success: string, // Callback ID - failure: string, // Callback ID + success: CallbackId, + failure: CallbackId, ): Promise { if (!this.#host) { throw new Error("No host is bound to the service"); From 4a4002a80ca105c9088d6d2bf173adc3be8fb821 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 16 May 2024 22:15:27 +0900 Subject: [PATCH 31/99] :muscle: define argument parsers in module scope - Improve performance. - Type satisfies. --- denops/@denops-private/host.ts | 41 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/denops/@denops-private/host.ts b/denops/@denops-private/host.ts index b641048a..bef0c24a 100644 --- a/denops/@denops-private/host.ts +++ b/denops/@denops-private/host.ts @@ -1,4 +1,4 @@ -import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; +import { ensure, is, type Predicate } from "jsr:@core/unknownutil@3.18.0"; /** * Host (Vim/Neovim) which is visible from Service @@ -61,39 +61,40 @@ export type Service = { ): Promise; }; +type ServiceForInvoke = Omit; + export function invoke( - service: Omit, + service: ServiceForInvoke, name: string, args: unknown[], ): Promise { switch (name) { case "load": - return service.load( - ...ensure(args, is.TupleOf([is.String, is.String] as const)), - ); + return service.load(...ensure(args, serviceMethodArgs.load)); case "reload": - return service.reload( - ...ensure(args, is.TupleOf([is.String] as const)), - ); + return service.reload(...ensure(args, serviceMethodArgs.reload)); case "interrupt": - service.interrupt( - ...ensure(args, is.ParametersOf([is.OptionalOf(is.Unknown)] as const)), - ); + service.interrupt(...ensure(args, serviceMethodArgs.interrupt)); return Promise.resolve(); case "dispatch": - return service.dispatch( - ...ensure(args, is.TupleOf([is.String, is.String, is.Array] as const)), - ); + return service.dispatch(...ensure(args, serviceMethodArgs.dispatch)); case "dispatchAsync": return service.dispatchAsync( - ...ensure( - args, - is.TupleOf( - [is.String, is.String, is.Array, is.String, is.String] as const, - ), - ), + ...ensure(args, serviceMethodArgs.dispatchAsync), ); default: throw new Error(`Service does not have a method '${name}'`); } } + +const serviceMethodArgs = { + load: is.ParametersOf([is.String, is.String] as const), + reload: is.ParametersOf([is.String] as const), + interrupt: is.ParametersOf([is.OptionalOf(is.Unknown)] as const), + dispatch: is.ParametersOf([is.String, is.String, is.Array] as const), + dispatchAsync: is.ParametersOf( + [is.String, is.String, is.Array, is.String, is.String] as const, + ), +} as const satisfies { + [K in keyof ServiceForInvoke]: Predicate>; +}; From 90825611e19d47772985070b27cf28b4f75c9637 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 16 Jun 2024 17:38:01 +0900 Subject: [PATCH 32/99] :+1: change `Host#notify()` to async `Host#notify()` should return a Promise as it may cause an unhandled-rejection. --- denops/@denops-private/host.ts | 4 ++-- denops/@denops-private/host/nvim.ts | 6 +++--- denops/@denops-private/host/vim.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/denops/@denops-private/host.ts b/denops/@denops-private/host.ts index bef0c24a..e8b2c131 100644 --- a/denops/@denops-private/host.ts +++ b/denops/@denops-private/host.ts @@ -22,9 +22,9 @@ export interface Host extends AsyncDisposable { ): Promise<[unknown[], string]>; /** - * Call host function and do nothing + * Call host function and does not check results */ - notify(fn: string, ...args: unknown[]): void; + notify(fn: string, ...args: unknown[]): Promise; /** * Initialize host diff --git a/denops/@denops-private/host/nvim.ts b/denops/@denops-private/host/nvim.ts index 7b24de9a..81f2615c 100644 --- a/denops/@denops-private/host/nvim.ts +++ b/denops/@denops-private/host/nvim.ts @@ -1,5 +1,5 @@ import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; -import { Client, Session } from "jsr:@lambdalisue/messagepack-rpc@2.1.1"; +import { Client, Session } from "jsr:@lambdalisue/messagepack-rpc@2.4.0"; import { errorDeserializer, errorSerializer } from "../error.ts"; import { getVersionOr } from "../version.ts"; import { type Host, invoke, type Service } from "../host.ts"; @@ -86,8 +86,8 @@ export class Neovim implements Host { return [ret, ""]; } - notify(fn: string, ...args: unknown[]): void { - this.#client.notify("nvim_call_function", fn, args); + async notify(fn: string, ...args: unknown[]): Promise { + await this.#client.notify("nvim_call_function", fn, args); } async init(service: Service): Promise { diff --git a/denops/@denops-private/host/vim.ts b/denops/@denops-private/host/vim.ts index 17debe58..e92b3592 100644 --- a/denops/@denops-private/host/vim.ts +++ b/denops/@denops-private/host/vim.ts @@ -81,8 +81,8 @@ export class Vim implements Host { return [ret, ""]; } - notify(fn: string, ...args: unknown[]): void { - this.#client.callNoReply(fn, ...args); + async notify(fn: string, ...args: unknown[]): Promise { + await this.#client.callNoReply(fn, ...args); } init(service: Service): Promise { From 9ff52f3d7fb17ec8ef28ae251167c5fed6eb8705 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 16 May 2024 18:29:48 +0900 Subject: [PATCH 33/99] :+1: `console.*` patch fallback to true console output Patches in `console.*` fallback to true console output. It does not throw an exception if the call to Host fails. --- denops/@denops-private/worker.ts | 60 +++++++++++++-------------- denops/@denops-private/worker_test.ts | 36 +++++++++++++++- 2 files changed, 63 insertions(+), 33 deletions(-) diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index 08b4abdf..66524796 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -8,12 +8,21 @@ import { import { ensure } from "jsr:@core/unknownutil@3.18.0"; import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; -import type { HostConstructor } from "./host.ts"; +import type { Meta } from "jsr:@denops/core@6.0.6"; +import type { Host, HostConstructor } from "./host.ts"; import { Vim } from "./host/vim.ts"; import { Neovim } from "./host/nvim.ts"; import { Service } from "./service.ts"; import { isMeta } from "./util.ts"; +const CONSOLE_PATCH_METHODS = [ + "log", + "info", + "debug", + "warn", + "error", +] as const satisfies (keyof typeof console)[]; + const marks = new TextEncoder().encode('[{tf"0123456789'); async function detectHost( @@ -38,6 +47,22 @@ function formatArgs(args: unknown[]): string[] { }); } +function patchConsole(host: Host, meta: Meta): void { + for (const name of CONSOLE_PATCH_METHODS) { + if (name === "debug" && meta.mode !== "debug") { + console[name] = () => {}; + continue; + } + const orig = console[name].bind(console); + const fn = `denops#_internal#echo#${name}`; + console[name] = (...args: unknown[]): void => { + host + .notify(fn, ...formatArgs(args)) + .catch(() => orig(...args)); + }; + } +} + async function connectHost(): Promise { const writer = writableStreamFromWorker(self); const [reader, detector] = readableStreamFromWorker(self).tee(); @@ -47,37 +72,8 @@ async function connectHost(): Promise { await using host = new hostCtor(reader, writer); const meta = ensure(await host.call("denops#_internal#meta#get"), isMeta); - // Patch console - console.log = (...args: unknown[]) => { - host.notify( - "denops#_internal#echo#log", - ...formatArgs(args), - ); - }; - console.info = (...args: unknown[]) => { - host.notify( - "denops#_internal#echo#info", - ...formatArgs(args), - ); - }; - console.debug = meta.mode !== "debug" ? () => {} : (...args: unknown[]) => { - host.notify( - "denops#_internal#echo#debug", - ...formatArgs(args), - ); - }; - console.warn = (...args: unknown[]) => { - host.notify( - "denops#_internal#echo#warn", - ...formatArgs(args), - ); - }; - console.error = (...args: unknown[]) => { - host.notify( - "denops#_internal#echo#error", - ...formatArgs(args), - ); - }; + + patchConsole(host, meta); // Start service using sigintTrap = asyncSignal("SIGINT"); diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index aaaa60f5..80a182ff 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -4,12 +4,19 @@ import { assertEquals, assertInstanceOf, assertMatch, + assertObjectMatch, } from "jsr:@std/assert@0.225.2"; -import { assertSpyCalls, stub } from "jsr:@std/testing@0.224.0/mock"; +import { + assertSpyCalls, + resolvesNext, + stub, +} from "jsr:@std/testing@0.224.0/mock"; import { delay } from "jsr:@std/async@0.224.0/delay"; import { DisposableStack } from "jsr:@nick/dispose@1.1.0/disposable-stack"; import * as nvimCodec from "jsr:@lambdalisue/messagepack@^1.0.1"; import { createFakeMeta } from "./testutil/mock.ts"; +import { Neovim } from "./host/nvim.ts"; +import { Vim } from "./host/vim.ts"; import { main } from "./worker.ts"; const CONSOLE_PATCH_METHODS = [ @@ -307,6 +314,33 @@ for (const { host, mode } of matrix) { } }, }); + + await t.step(`calls native \`console.${name}\``, async () => { + const notifyError = new Error("fake-post-error"); + using _host_notify = stub( + (host === "vim" ? Vim : Neovim).prototype, + "notify", + resolvesNext([notifyError]), + ); + const nativeMethod = consoleStub[name]; + nativeMethod.resetHistory(); + const fn = consoleStub[name].set.getCall(0).args[0]; + const error = new Error("fake-error"); + + fn.apply(globalThis.console, ["foo", 123, false, error]); + + await delay(0); + assertEquals(nativeMethod.callCount, 1); + assertObjectMatch(nativeMethod.getCall(0), { + thisValue: globalThis.console, + args: [ + "foo", + 123, + false, + error, + ], + }); + }); } }); } From b32f784007538814a939675e00723bbe9f17ce3c Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 17 Jun 2024 22:58:43 +0900 Subject: [PATCH 34/99] :herb: remove duplicate test step --- denops/@denops-private/service_test.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 98d54098..36d09d93 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -232,26 +232,6 @@ Deno.test("Service", async (t) => { }, ); - await t.step("reload() reloads plugin and emits autocmd events", async () => { - using s = stub(host, "call"); - await service.reload("dummy"); - assertSpyCalls(s, 3); - assertSpyCall(s, 1, { - args: [ - "denops#api#cmd", - "echo 'Hello, Denops!'", - {}, - ], - }); - assertSpyCall(s, 2, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, - ], - }); - }); - await t.step( "load() does nothing when the plugin is already loaded", async () => { From 0079a63cd34d9793b92afe02d7d89bbf44cd060b Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 17 Jun 2024 23:09:52 +0900 Subject: [PATCH 35/99] :muscle: use `PromiseWithResolvers<...>` Because minimum version of Deno is changed to 1.44.x. --- denops/@denops-private/service.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index f11e58e2..2f43d8c7 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -4,19 +4,13 @@ import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.0.0"; import { DenopsImpl, type Host } from "./denops.ts"; import type { CallbackId, Service as HostService } from "./host.ts"; -// We can use `PromiseWithResolvers` but Deno 1.38 doesn't have `PromiseWithResolvers` -type Waiter = { - promise: Promise; - resolve: () => void; -}; - /** * Service manage plugins and is visible from the host (Vim/Neovim) through `invoke()` function. */ export class Service implements HostService, Disposable { #interruptController = new AbortController(); #plugins = new Map(); - #waiters = new Map(); + #waiters = new Map>(); #meta: Meta; #host?: Host; @@ -24,7 +18,7 @@ export class Service implements HostService, Disposable { this.#meta = meta; } - #getWaiter(name: string): Waiter { + #getWaiter(name: string): PromiseWithResolvers { if (!this.#waiters.has(name)) { this.#waiters.set(name, Promise.withResolvers()); } From 49b9cebbd35527d89dd36a76a946eafa6e4ecd0a Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 17 Jun 2024 23:28:33 +0900 Subject: [PATCH 36/99] :muscle: refactor map reference --- denops/@denops-private/service.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 2f43d8c7..4e9a9516 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -19,10 +19,12 @@ export class Service implements HostService, Disposable { } #getWaiter(name: string): PromiseWithResolvers { - if (!this.#waiters.has(name)) { - this.#waiters.set(name, Promise.withResolvers()); + let waiter = this.#waiters.get(name); + if (!waiter) { + waiter = Promise.withResolvers(); + this.#waiters.set(name, waiter); } - return this.#waiters.get(name)!; + return waiter; } get interrupted(): AbortSignal { @@ -41,15 +43,14 @@ export class Service implements HostService, Disposable { if (!this.#host) { throw new Error("No host is bound to the service"); } - let plugin = this.#plugins.get(name); - if (plugin) { + if (this.#plugins.has(name)) { if (this.#meta.mode === "debug") { console.log(`A denops plugin '${name}' is already loaded. Skip`); } return; } const denops = new DenopsImpl(name, this.#meta, this.#host, this); - plugin = new Plugin(denops, name, script); + const plugin = new Plugin(denops, name, script); this.#plugins.set(name, plugin); await plugin.load(suffix); this.#getWaiter(name).resolve(); From 1c088c0edb95aa10001679def5e6f422b0374ed1 Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 18 Jun 2024 09:55:53 +0900 Subject: [PATCH 37/99] :+1: gracefully close service - The Service is closed when The worker is terminated. - The worker is teminated when The Service is closed. - `denops.dispatch()` (but `service.dispatch()`) rejects with a service closed error if the service is closed before the plugin loaded. --- denops/@denops-private/service.ts | 34 ++++++- denops/@denops-private/service_test.ts | 75 ++++++++++++++ denops/@denops-private/worker.ts | 3 +- denops/@denops-private/worker_test.ts | 130 +++++++++++++++++++++++-- 4 files changed, 231 insertions(+), 11 deletions(-) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 4e9a9516..25a04e95 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -7,12 +7,14 @@ import type { CallbackId, Service as HostService } from "./host.ts"; /** * Service manage plugins and is visible from the host (Vim/Neovim) through `invoke()` function. */ -export class Service implements HostService, Disposable { +export class Service implements HostService, AsyncDisposable { #interruptController = new AbortController(); #plugins = new Map(); #waiters = new Map>(); #meta: Meta; #host?: Host; + #closed = false; + #closedWaiter = Promise.withResolvers(); constructor(meta: Meta) { this.#meta = meta; @@ -22,6 +24,7 @@ export class Service implements HostService, Disposable { let waiter = this.#waiters.get(name); if (!waiter) { waiter = Promise.withResolvers(); + waiter.promise.catch(() => {}); this.#waiters.set(name, waiter); } return waiter; @@ -40,6 +43,9 @@ export class Service implements HostService, Disposable { script: string, suffix = "", ): Promise { + if (this.#closed) { + throw new Error("Service closed"); + } if (!this.#host) { throw new Error("No host is bound to the service"); } @@ -75,6 +81,9 @@ export class Service implements HostService, Disposable { } waitLoaded(name: string): Promise { + if (this.#closed) { + return Promise.reject(new Error("Service closed")); + } return this.#getWaiter(name).promise; } @@ -128,8 +137,27 @@ export class Service implements HostService, Disposable { } } - [Symbol.dispose](): void { - this.#plugins.clear(); + close(): Promise { + if (!this.#closed) { + this.#closed = true; + const error = new Error("Service closed"); + for (const { reject } of this.#waiters.values()) { + reject(error); + } + this.#waiters.clear(); + this.#plugins.clear(); + this.#host = void 0; + this.#closedWaiter.resolve(); + } + return this.waitClosed(); + } + + waitClosed(): Promise { + return this.#closedWaiter.promise; + } + + [Symbol.asyncDispose](): Promise { + return this.close(); } } diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 36d09d93..3f38e492 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -506,4 +506,79 @@ Deno.test("Service", async (t) => { assertThrows(() => signal.throwIfAborted(), "test"); }, ); + + const waitClosed = service.waitClosed(); + + const waitLoadedCalledBeforeClose = service.waitLoaded( + "whenclosedtestplugin", + ); + + await t.step( + "the result promise of waitClosed() become 'pending' when the service is not closed", + async () => { + assert(await promiseState(waitClosed), "pending"); + }, + ); + + await t.step( + "close() closes the service", + async () => { + await service.close(); + }, + ); + + await t.step( + "the result promise of waitClosed() become 'fulfilled' when the service is closed", + async () => { + assert(await promiseState(waitClosed), "fulfilled"); + }, + ); + + await t.step( + "the result promise of waitLoaded() become 'rejected' when the service is closed", + async () => { + assertEquals(await promiseState(waitLoadedCalledBeforeClose), "rejected"); + await assertRejects( + () => waitLoadedCalledBeforeClose, + Error, + "Service closed", + ); + }, + ); + + await t.step( + "waitClosed() returns 'fulfilled' promise if the service is already closed", + async () => { + const actual = service.waitClosed(); + assert(await promiseState(actual), "fulfilled"); + }, + ); + + await t.step( + "waitLoaded() returns 'rejected' promise if the service is already closed", + async () => { + await assertRejects( + () => service.waitLoaded("after-closed-test-plugin"), + Error, + "Service closed", + ); + }, + ); + + await t.step( + "load() rejects an error when the service is already closed", + async () => { + await assertRejects( + () => service.load("dummyValid", scriptValid), + Error, + "Service closed", + ); + }, + ); + + await t.step("[@@asyncDispose]() calls close()", async () => { + using service_close = stub(service, "close"); + await service[Symbol.asyncDispose](); + assertSpyCalls(service_close, 1); + }); }); diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index 66524796..aa0538b1 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -77,10 +77,11 @@ async function connectHost(): Promise { // Start service using sigintTrap = asyncSignal("SIGINT"); - using service = new Service(meta); + await using service = new Service(meta); await host.init(service); await host.call("execute", "doautocmd User DenopsReady", ""); await Promise.race([ + service.waitClosed(), host.waitClosed(), sigintTrap, ]); diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index 80a182ff..9289c2a6 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -9,6 +9,7 @@ import { import { assertSpyCalls, resolvesNext, + spy, stub, } from "jsr:@std/testing@0.224.0/mock"; import { delay } from "jsr:@std/async@0.224.0/delay"; @@ -17,6 +18,7 @@ import * as nvimCodec from "jsr:@lambdalisue/messagepack@^1.0.1"; import { createFakeMeta } from "./testutil/mock.ts"; import { Neovim } from "./host/nvim.ts"; import { Vim } from "./host/vim.ts"; +import { Service } from "./service.ts"; import { main } from "./worker.ts"; const CONSOLE_PATCH_METHODS = [ @@ -125,6 +127,14 @@ for (const { host, mode } of matrix) { using _addEventListenerSpy = spyAddEventListener(); using _denoCommandStub = stubDenoCommand(); using deno_addSignalListener = stub(Deno, "addSignalListener"); + using host_asyncDispose = spy( + (host === "vim" ? Vim : Neovim).prototype, + Symbol.asyncDispose, + ); + using service_asyncDispose = spy( + Service.prototype, + Symbol.asyncDispose, + ); using self_close = stub(globalThis, "close"); const usePostMessageHistory = () => ({ [Symbol.dispose]: () => messageStub.postMessage.resetHistory(), @@ -353,15 +363,37 @@ for (const { host, mode } of matrix) { assertInstanceOf(signalHandler, Function); }); - await t.step("closes worker when stream is closed", async () => { - assertSpyCalls(self_close, 0); + await t.step("before stream is closed", async (t) => { + await t.step("does not dispose service", () => { + assertSpyCalls(service_asyncDispose, 0); + }); - // NOTE: Send `null` to close workerio stream. - messageStub.fakeHostMessage(null); + await t.step("does not dispose host", () => { + assertSpyCalls(host_asyncDispose, 0); + }); - await delay(0); - assertSpyCalls(self_close, 1); - await mainPromise; + await t.step("does not close worker", () => { + assertSpyCalls(self_close, 0); + }); + }); + + // NOTE: Send `null` to close workerio stream. + messageStub.fakeHostMessage(null); + await delay(0); + + await t.step("after stream is closed", async (t) => { + await t.step("disposes service", () => { + assertSpyCalls(service_asyncDispose, 1); + }); + + await t.step("disposes host", () => { + assertSpyCalls(host_asyncDispose, 1); + }); + + await t.step("closes worker", async () => { + assertSpyCalls(self_close, 1); + await mainPromise; + }); }); }); @@ -397,6 +429,14 @@ for (const { host, mode } of matrix) { using _addEventListenerSpy = spyAddEventListener(); using _denoCommandStub = stubDenoCommand(); using deno_addSignalListener = stub(Deno, "addSignalListener"); + using host_asyncDispose = spy( + (host === "vim" ? Vim : Neovim).prototype, + Symbol.asyncDispose, + ); + using service_asyncDispose = spy( + Service.prototype, + Symbol.asyncDispose, + ); using self_close = stub(globalThis, "close"); const fakeMeta = { ...createFakeMeta(), host, mode }; @@ -441,6 +481,82 @@ for (const { host, mode } of matrix) { ); }); + await t.step("disposes service", () => { + assertSpyCalls(service_asyncDispose, 1); + }); + + await t.step("disposes host", () => { + assertSpyCalls(host_asyncDispose, 1); + }); + + await t.step("closes worker", async () => { + assertSpyCalls(self_close, 1); + await mainPromise; + }); + }); + + await t.step("main() if service is closed", async (t) => { + using messageStub = stubMessage(); + using _consoleStub = stubConsole(); + using _addEventListenerSpy = spyAddEventListener(); + using _denoCommandStub = stubDenoCommand(); + using _deno_addSignalListener = stub(Deno, "addSignalListener"); + using host_asyncDispose = spy( + (host === "vim" ? Vim : Neovim).prototype, + Symbol.asyncDispose, + ); + using service_asyncDispose = spy( + Service.prototype, + Symbol.asyncDispose, + ); + const service_waitClosed_waiter = Promise.withResolvers(); + using service_waitClosed = stub( + Service.prototype, + "waitClosed", + () => service_waitClosed_waiter.promise, + ); + using self_close = stub(globalThis, "close"); + const fakeMeta = { ...createFakeMeta(), host, mode }; + + const mainPromise = main(); + + if (host === "vim") { + // Initial message from the host. + messageStub.fakeHostMessage(vimCodec.encode([0, ["void"]])); + await delay(0); + // requests Meta data + messageStub.fakeHostMessage(vimCodec.encode([-1, [fakeMeta, ""]])); + await delay(0); + // doautocmd `User DenopsReady` + messageStub.fakeHostMessage(vimCodec.encode([-2, ["", ""]])); + await delay(0); + } else { + // Initial message from the host. + messageStub.fakeHostMessage(nvimCodec.encode([2, "void", []])); + await delay(0); + // requests Meta data + messageStub.fakeHostMessage(nvimCodec.encode([1, 0, null, fakeMeta])); + await delay(0); + // sets client info + messageStub.fakeHostMessage(nvimCodec.encode([1, 1, null, 0])); + await delay(0); + // doautocmd `User DenopsReady` + messageStub.fakeHostMessage(nvimCodec.encode([1, 2, null, ""])); + await delay(0); + } + + assertSpyCalls(service_waitClosed, 1); + service_waitClosed_waiter.resolve(); + await delay(0); + + await t.step("disposes service", () => { + assertSpyCalls(service_asyncDispose, 1); + }); + + await t.step("disposes host", () => { + assertSpyCalls(host_asyncDispose, 1); + }); + await t.step("closes worker", async () => { assertSpyCalls(self_close, 1); await mainPromise; From 88cf74399f68cf20b82a492a742d0e1e74f2c510 Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 18 Jun 2024 14:55:46 +0900 Subject: [PATCH 38/99] `undefined` instead `void 0` --- denops/@denops-private/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 25a04e95..fc380917 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -146,7 +146,7 @@ export class Service implements HostService, AsyncDisposable { } this.#waiters.clear(); this.#plugins.clear(); - this.#host = void 0; + this.#host = undefined; this.#closedWaiter.resolve(); } return this.waitClosed(); From f6a5d74127244ac8f54af133070695e2423a4e1f Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 18 Jun 2024 18:00:40 +0900 Subject: [PATCH 39/99] :herb: fix flaky tests --- tests/denops/server_test.ts | 52 +++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts index ffdd81f8..a194e664 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/server_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "jsr:@std/assert@0.225.2"; +import { assertEquals, assertFalse } from "jsr:@std/assert@0.225.2"; import { testHost } from "../../denops/@denops-private/testutil/host.ts"; import { wait } from "../../denops/@denops-private/testutil/wait.ts"; @@ -6,11 +6,6 @@ testHost({ name: "denops#server#status()", mode: "all", fn: async (host, t) => { - await host.call("execute", [ - "autocmd User DenopsReady let g:denops_ready_fired = 1", - "autocmd User DenopsClosed let g:denops_closed_fired = 1", - ], ""); - await t.step( "returns 'stopped' when no server running", async () => { @@ -19,15 +14,30 @@ testHost({ }, ); + // NOTE: The `plugin/denops.vim` must be sourced to initialize the environment. + await host.call("execute", [ + "source plugin/denops.vim", + "autocmd User DenopsReady let g:denops_ready_fired = 1", + "autocmd User DenopsClosed let g:denops_closed_fired = 1", + ], ""); + + // Wait to stopped the server that starts by `plugin/denops.vim` + await wait(() => host.call("exists", "g:denops_ready_fired")); + await host.call("denops#server#stop"); + await wait(() => host.call("eval", "denops#server#status() ==# 'stopped'")); + + // NOTE: The status may transition to `preparing`, so get it within execute. + // SEE: https://github.com/vim-denops/denops.vim/issues/354 + await host.call("execute", [ + "unlet g:denops_ready_fired", + "unlet g:denops_closed_fired", + "call denops#server#start()", + "let g:__test_denops_server_status_when_start_called = denops#server#status()", + ], ""); + await t.step( "returns 'starting' when denops#server#start() is called", async () => { - // NOTE: The status may transition to `preparing`, so get it within execute. - // SEE: https://github.com/vim-denops/denops.vim/issues/354 - await host.call("execute", [ - "source plugin/denops.vim", - "let g:__test_denops_server_status_when_start_called = denops#server#status()", - ]); const actual = await host.call( "eval", "g:__test_denops_server_status_when_start_called", @@ -37,12 +47,12 @@ testHost({ ); await t.step( - "returns 'preparing' before DenopsReady is fired", + "returns 'preparing' before DenopsReady is fired (flaky)", async () => { const actual = await wait(() => host.call( "eval", - "exists('g:denops_ready_fired') ? 'DenopsReady is fired'" + + "exists('g:denops_ready_fired') ? 'DenopsReady is fired (flaky result)'" + ": denops#server#status() !=# 'starting' ? denops#server#status() : 0", ) ); @@ -104,10 +114,20 @@ testHost({ }, ); + await host.call("execute", [ + "call denops#server#close()", + "let g:__test_denops_server_denops_closed_exists_after_close = exists('g:denops_closed_fired')", + ], ""); + await t.step( - "denops#server#close() closes the connection then DenopsClosed is fired", + "denops#server#close() closes the connection asynchronously then DenopsClosed is fired", async () => { - await host.call("denops#server#close"); + assertFalse( + await host.call( + "eval", + "g:__test_denops_server_denops_closed_exists_after_close", + ), + ); await wait(() => host.call("exists", "g:denops_closed_fired")); }, ); From 80eb0bfe76d5afd7369ee1c4658a4dd935669178 Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 22 Jun 2024 19:29:12 +0900 Subject: [PATCH 40/99] :herb: improve `useSharedServer` helper --- .../@denops-private/testutil/shared_server.ts | 48 +++++----- .../testutil/shared_server_test.ts | 87 ++++++++++++++----- .../testutil/shared_server_test_no_verbose.ts | 7 ++ .../shared_server_test_verbose_true.ts | 7 ++ 4 files changed, 104 insertions(+), 45 deletions(-) create mode 100644 denops/@denops-private/testutil/shared_server_test_no_verbose.ts create mode 100644 denops/@denops-private/testutil/shared_server_test_verbose_true.ts diff --git a/denops/@denops-private/testutil/shared_server.ts b/denops/@denops-private/testutil/shared_server.ts index a4a5accb..016bd90e 100644 --- a/denops/@denops-private/testutil/shared_server.ts +++ b/denops/@denops-private/testutil/shared_server.ts @@ -1,11 +1,14 @@ import { assert } from "jsr:@std/assert@0.225.2"; import { deadline } from "jsr:@std/async@0.224.0/deadline"; import { resolve } from "jsr:@std/path@0.224.0/resolve"; -import { TextLineStream } from "jsr:@std/streams@0.224.0/text-line-stream"; -import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; +import { channel, pop } from "jsr:@lambdalisue/streamtools@1.0.0"; +import { tap } from "jsr:@milly/streams@^1.0.0/transform/tap"; import { getConfig } from "./conf.ts"; const DEFAULT_TIMEOUT = 30_000; +const origLog = console.log.bind(console); +const origError = console.error.bind(console); +const noop = () => {}; export interface UseSharedServerOptions { /** Print shared-server messages. */ @@ -21,6 +24,8 @@ export interface UseSharedServerResult extends AsyncDisposable { addr: string; /** Shared server standard output. */ stdout: ReadableStream; + /** Shared server error output. */ + stderr: ReadableStream; } /** @@ -50,23 +55,21 @@ export async function useSharedServer( const proc = new Deno.Command(cmd, { args, stdout: "piped", - stderr: verbose ? "inherit" : "null", + stderr: "piped", env, signal, }).spawn(); - let stdout = proc.stdout - .pipeThrough(new TextDecoderStream(), { signal }) - .pipeThrough(new TextLineStream()); + let stdout = proc.stdout.pipeThrough(new TextDecoderStream(), { signal }); + let stderr = proc.stderr.pipeThrough(new TextDecoderStream(), { signal }); if (verbose) { - const [splitStdout, verboseStdout] = stdout.tee(); - stdout = splitStdout; - verboseStdout.pipeTo( - new WritableStream({ - write: (out) => console.log(out), - }), - ).catch(() => {}); + stdout = stdout.pipeThrough(tap((s) => origLog(s))); + stderr = stderr.pipeThrough(tap((s) => origError(s))); } + const { writer: stdoutWriter, reader: stdoutReader } = channel(); + stdout.pipeTo(stdoutWriter).catch(noop); + const { writer: stderrWriter, reader: stderrReader } = channel(); + stderr.pipeTo(stderrWriter).catch(noop); const abort = async (reason: unknown) => { try { @@ -74,23 +77,22 @@ export async function useSharedServer( } catch { // Already exited, do nothing. } - await Promise.all([ - stdout.locked ? undefined : stdout.cancel(reason), - proc.status, + await proc.status; + await Promise.allSettled([ + proc.stdout.cancel(reason), + proc.stderr.cancel(reason), ]); - await proc.stdout.cancel(reason); }; try { - const addr = await deadline(pop(stdout), timeout); + const addr = await deadline(pop(stdoutReader), timeout); assert(typeof addr === "string"); return { - addr, - stdout, + addr: addr.replace(/[\r\n].*/, ""), + stdout: stdoutReader, + stderr: stderrReader, async [Symbol.asyncDispose]() { - if (!signal.aborted) { - await abort("useSharedServer disposed"); - } + await abort("useSharedServer disposed"); }, }; } catch (e) { diff --git a/denops/@denops-private/testutil/shared_server_test.ts b/denops/@denops-private/testutil/shared_server_test.ts index b46ed338..f3c32bd0 100644 --- a/denops/@denops-private/testutil/shared_server_test.ts +++ b/denops/@denops-private/testutil/shared_server_test.ts @@ -1,37 +1,80 @@ import { assertInstanceOf, assertMatch, + assertNotMatch, assertRejects, } from "jsr:@std/assert@0.225.2"; import { delay } from "jsr:@std/async@0.224.0/delay"; -import { stub } from "jsr:@std/testing@0.224.0/mock"; +import { join } from "jsr:@std/path@0.225.0/join"; import { useSharedServer } from "./shared_server.ts"; Deno.test("useSharedServer()", async (t) => { - await t.step("returns `result.addr`", async () => { - await using server = await useSharedServer(); - assertMatch(server.addr, /^127\.0\.0\.1:\d+$/); - }); + await t.step("if `verbose` is not specified", async (t) => { + await t.step("returns `result.addr`", async () => { + await using server = await useSharedServer(); + assertMatch(server.addr, /^127\.0\.0\.1:\d+$/); + }); + + await t.step("returns `result.stdout`", async () => { + await using server = await useSharedServer(); + assertInstanceOf(server.stdout, ReadableStream); + const outputs: string[] = []; + server.stdout.pipeTo( + new WritableStream({ write: (line) => void outputs.push(line) }), + ).catch(() => {}); + await delay(100); + assertMatch(outputs.join("\n"), /Listen denops clients on/); + }); - await t.step("returns `result.stdout`", async () => { - await using server = await useSharedServer(); - assertInstanceOf(server.stdout, ReadableStream); - const outputs: string[] = []; - server.stdout.pipeTo( - new WritableStream({ - write: (line) => void outputs.push(line), - }), - ).catch(() => {}); - await delay(100); - assertMatch(outputs.join("\n"), /Listen denops clients on/); + await t.step("does not output stdout", async () => { + const proc = new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--allow-env", + "--allow-read", + "--allow-run", + join(import.meta.dirname!, "shared_server_test_no_verbose.ts"), + ], + stdout: "piped", + }).spawn(); + const output = await proc.output(); + const stdout = new TextDecoder().decode(output.stdout); + assertNotMatch(stdout, /Listen denops clients on/); + }); }); - await t.step("calls console.log() if `verbose` is true", async () => { - using console_log = stub(console, "log"); - await using _server = await useSharedServer({ verbose: true }); - await delay(100); - const outputs = console_log.calls.flatMap((call) => call.args).join("\n"); - assertMatch(outputs, /Listen denops clients on/); + await t.step("if `verbose` is true", async (t) => { + await t.step("returns `result.addr`", async () => { + await using server = await useSharedServer({ verbose: true }); + assertMatch(server.addr, /^127\.0\.0\.1:\d+$/); + }); + + await t.step("returns `result.stdout`", async () => { + await using server = await useSharedServer({ verbose: true }); + assertInstanceOf(server.stdout, ReadableStream); + const outputs: string[] = []; + server.stdout.pipeTo( + new WritableStream({ write: (line) => void outputs.push(line) }), + ).catch(() => {}); + await delay(100); + assertMatch(outputs.join("\n"), /Listen denops clients on/); + }); + + await t.step("outputs stdout", async () => { + const proc = new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--allow-env", + "--allow-read", + "--allow-run", + join(import.meta.dirname!, "shared_server_test_verbose_true.ts"), + ], + stdout: "piped", + }).spawn(); + const output = await proc.output(); + const stdout = new TextDecoder().decode(output.stdout); + assertMatch(stdout, /Listen denops clients on/); + }); }); await t.step("closes child process when rejectes", async () => { diff --git a/denops/@denops-private/testutil/shared_server_test_no_verbose.ts b/denops/@denops-private/testutil/shared_server_test_no_verbose.ts new file mode 100644 index 00000000..5583e7ea --- /dev/null +++ b/denops/@denops-private/testutil/shared_server_test_no_verbose.ts @@ -0,0 +1,7 @@ +import { delay } from "jsr:@std/async@0.224.0/delay"; +import { useSharedServer } from "./shared_server.ts"; + +{ + await using _server = await useSharedServer(); + await delay(100); +} diff --git a/denops/@denops-private/testutil/shared_server_test_verbose_true.ts b/denops/@denops-private/testutil/shared_server_test_verbose_true.ts new file mode 100644 index 00000000..6cbd73e2 --- /dev/null +++ b/denops/@denops-private/testutil/shared_server_test_verbose_true.ts @@ -0,0 +1,7 @@ +import { delay } from "jsr:@std/async@0.224.0/delay"; +import { useSharedServer } from "./shared_server.ts"; + +{ + await using _server = await useSharedServer({ verbose: true }); + await delay(100); +} From 3243b1c53411e10ca19000fe9cebf0e7a1af6a6f Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 22 Jun 2024 22:23:19 +0900 Subject: [PATCH 41/99] :herb: improve `with*` helpers - Change helper argument to record. - Add `stdout` and `stderr` helper properties. --- denops/@denops-private/host/nvim_test.ts | 2 +- denops/@denops-private/host/vim_test.ts | 2 +- denops/@denops-private/testutil/host.ts | 23 ++++++--- denops/@denops-private/testutil/with.ts | 60 ++++++++++++++++++++---- tests/denops/plugin_test.ts | 6 +-- tests/denops/server_test.ts | 4 +- 6 files changed, 73 insertions(+), 24 deletions(-) diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index ce473c61..d1e56125 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -19,7 +19,7 @@ import { Neovim } from "./nvim.ts"; Deno.test("Neovim", async (t) => { let waitClosed: Promise | undefined; await withNeovim({ - fn: async (reader, writer) => { + fn: async ({ reader, writer }) => { const service: Service = { bind: () => unimplemented(), load: () => unimplemented(), diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index bac29fda..65b16f1a 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -14,7 +14,7 @@ import { Vim } from "./vim.ts"; Deno.test("Vim", async (t) => { let waitClosed: Promise | undefined; await withVim({ - fn: async (reader, writer) => { + fn: async ({ reader, writer }) => { const service: Service = { bind: () => unimplemented(), load: () => unimplemented(), diff --git a/denops/@denops-private/testutil/host.ts b/denops/@denops-private/testutil/host.ts index 3ca8b98d..a24cf465 100644 --- a/denops/@denops-private/testutil/host.ts +++ b/denops/@denops-private/testutil/host.ts @@ -3,7 +3,11 @@ import { Neovim } from "../host/nvim.ts"; import { Vim } from "../host/vim.ts"; import { withNeovim, WithOptions, withVim } from "./with.ts"; -export type HostFn = (host: Host) => T; +export type HostFn = (helper: { + host: Host; + stdout: ReadableStream; + stderr: ReadableStream; +}) => T; export interface WithHostOptions extends Omit, "fn"> { fn: HostFn; @@ -17,18 +21,18 @@ export function withHost( const { mode, fn, ...withOptions } = options; if (mode === "vim") { return withVim({ - fn: async (reader, writer) => { + fn: async ({ reader, writer, stdout, stderr }) => { await using host = new Vim(reader, writer); - return await fn(host); + return await fn({ host, stdout, stderr }); }, ...withOptions, }); } if (mode === "nvim") { return withNeovim({ - fn: async (reader, writer) => { + fn: async ({ reader, writer, stdout, stderr }) => { await using host = new Neovim(reader, writer); - return await fn(host); + return await fn({ host, stdout, stderr }); }, ...withOptions, }); @@ -36,7 +40,12 @@ export function withHost( return Promise.reject(new TypeError(`Invalid mode: ${mode}`)); } -export type TestFn = (host: Host, t: Deno.TestContext) => void | Promise; +export type TestFn = (helper: { + host: Host; + t: Deno.TestContext; + stdout: ReadableStream; + stderr: ReadableStream; +}) => void | Promise; export interface TestHostOptions extends @@ -65,7 +74,7 @@ export function testHost( fn: async (t) => { await withHost>({ mode, - fn: (host) => fn(host, t), + fn: ({ host, stdout, stderr }) => fn({ host, t, stdout, stderr }), ...hostOptions, }); }, diff --git a/denops/@denops-private/testutil/with.ts b/denops/@denops-private/testutil/with.ts index e04ce5e3..421eba7e 100644 --- a/denops/@denops-private/testutil/with.ts +++ b/denops/@denops-private/testutil/with.ts @@ -1,12 +1,19 @@ +import { channel } from "jsr:@lambdalisue/streamtools@1.0.0"; +import { tap } from "jsr:@milly/streams@^1.0.0/transform/tap"; import { ADDR_ENV_NAME } from "./cli.ts"; import { getConfig } from "./conf.ts"; const script = new URL("./cli.ts", import.meta.url); +const origLog = console.log.bind(console); +const origError = console.error.bind(console); +const noop = () => {}; -export type Fn = ( - reader: ReadableStream, - writer: WritableStream, -) => T; +export type Fn = (helper: { + reader: ReadableStream; + writer: WritableStream; + stdout: ReadableStream; + stderr: ReadableStream; +}) => T; export interface WithOptions { fn: Fn; @@ -48,6 +55,8 @@ export function withVim( "-V1", // Verbose level 1 (Echo messages to stderr) "-c", "visual", // Go to Normal mode + "-c", + "set columns=9999", // Avoid unwilling output newline ...commands.flatMap((c) => ["-c", c]), ]; return withProcess(cmd, args, { verbose: conf.verbose, ...options }); @@ -73,6 +82,8 @@ export function withNeovim( "--headless", "-n", // Disable swap file "-V1", // Verbose level 1 (Echo messages to stderr) + "-c", + "set columns=9999", // Avoid unwilling output newline ...commands.flatMap((c) => ["-c", c]), ]; return withProcess(cmd, args, { verbose: conf.verbose, ...options }); @@ -83,30 +94,59 @@ async function withProcess( args: string[], { fn, env, verbose }: WithOptions, ): Promise> { + const aborter = new AbortController(); + const { signal } = aborter; const listener = Deno.listen({ hostname: "127.0.0.1", port: 0, // Automatically select free port }); + const command = new Deno.Command(cmd, { args, stdin: "piped", - stdout: verbose ? "inherit" : "null", - stderr: verbose ? "inherit" : "null", + stdout: "piped", + stderr: "piped", env: { ...env, [ADDR_ENV_NAME]: JSON.stringify(listener.addr), }, + signal, }); const proc = command.spawn(); + + let stdout = proc.stdout.pipeThrough(new TextDecoderStream(), { signal }); + let stderr = proc.stderr.pipeThrough(new TextDecoderStream(), { signal }); + if (verbose) { + stdout = stdout.pipeThrough(tap((s) => origLog(s))); + stderr = stderr.pipeThrough(tap((s) => origError(s))); + } + const { writer: stdoutWriter, reader: stdoutReader } = channel(); + stdout.pipeTo(stdoutWriter).catch(noop); + const { writer: stderrWriter, reader: stderrReader } = channel(); + stderr.pipeTo(stderrWriter).catch(noop); + const conn = await listener.accept(); try { - return await fn(conn.readable, conn.writable); + return await fn({ + reader: conn.readable, + writer: conn.writable, + stdout: stdoutReader, + stderr: stderrReader, + }); } finally { listener.close(); - proc.kill(); + try { + aborter.abort("withProcess disposed"); + } catch { + // Already exited, do nothing. + } + await Promise.all([ + proc.stdin.close(), + proc.status, + ]); await Promise.all([ - proc.stdin?.close(), - proc.output(), + proc.stdout.cancel(), + proc.stderr.cancel(), ]); } } diff --git a/tests/denops/plugin_test.ts b/tests/denops/plugin_test.ts index 1b58b613..9b88228d 100644 --- a/tests/denops/plugin_test.ts +++ b/tests/denops/plugin_test.ts @@ -12,7 +12,7 @@ testHost({ postlude: [ "runtime plugin/denops.vim", ], - fn: async (host) => { + fn: async ({ host }) => { await wait(() => host.call("eval", "!has('vim_starting')")); const actual = await host.call("denops#server#status") as string; assertMatch(actual, /^(starting|preparing|running)$/); @@ -21,7 +21,7 @@ testHost({ testHost({ name: "'plugin/denops.vim' starts a local server when sourced after VimEnter", - fn: async (host) => { + fn: async ({ host }) => { await wait(() => host.call("eval", "!has('vim_starting')")); await host.call("execute", [ "runtime plugin/denops.vim", @@ -42,7 +42,7 @@ for (const mode of ["vim", "nvim"] as const) { `let g:denops_server_addr = '${server.addr}'`, "runtime plugin/denops.vim", ], - fn: async (host) => { + fn: async ({ host }) => { await wait(() => host.call("eval", "!has('vim_starting')")); const actual = await host.call("denops#server#status") as string; assertMatch(actual, /^(preparing|running)$/); diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts index a194e664..007c0014 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/server_test.ts @@ -5,7 +5,7 @@ import { wait } from "../../denops/@denops-private/testutil/wait.ts"; testHost({ name: "denops#server#status()", mode: "all", - fn: async (host, t) => { + fn: async ({ host, t }) => { await t.step( "returns 'stopped' when no server running", async () => { @@ -84,7 +84,7 @@ testHost({ testHost({ name: "Denops server", mode: "all", - fn: async (host, t) => { + fn: async ({ host, t }) => { // NOTE: The status may transition to `preparing`, so get it within execute. // SEE: https://github.com/vim-denops/denops.vim/issues/354 await host.call("execute", [ From d3da45ef58b0a7fbc807cfd7b5be82cb8dbe9389 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 16 May 2024 23:39:02 +0900 Subject: [PATCH 42/99] :+1: change `denops#server#start` and `...#stop` to asynchronous - Add new `denops-variable`: - `g:denops#server#close_timeout` - Add new state for `denops#server#status()`: - `closing` - `closed` - Change behavior `denops-function`: - `denops#server#start()` can now be called even when status is `closing`. In that case, the status will become `stopped` and then restart asynchronously. - `denops#server#stop()` is changed to asynchronously. It waits for the server to close gracefully, and force terminate if timeouted. - `denops#server#restart()` is changed to asynchronously. Perform the stop and start steps above. --- autoload/denops/_internal/job.vim | 31 +- autoload/denops/_internal/server/chan.vim | 90 +- autoload/denops/_internal/server/proc.vim | 34 +- autoload/denops/server.vim | 161 +++- denops/@denops-private/host.ts | 4 + denops/@denops-private/host/nvim_test.ts | 1 + denops/@denops-private/host/vim_test.ts | 1 + denops/@denops-private/host_test.ts | 16 + .../@denops-private/testutil/shared_server.ts | 10 +- doc/denops.txt | 15 +- tests/denops/server_test.ts | 904 ++++++++++++++++-- 11 files changed, 1109 insertions(+), 158 deletions(-) diff --git a/autoload/denops/_internal/job.vim b/autoload/denops/_internal/job.vim index 7d1f32e6..f147eb9c 100644 --- a/autoload/denops/_internal/job.vim +++ b/autoload/denops/_internal/job.vim @@ -24,13 +24,17 @@ if has('nvim') \ 'on_stderr': funcref('s:on_recv', [a:options.on_stderr]), \ 'on_exit': funcref('s:on_exit', [a:options.on_exit]), \} - return jobstart(a:args, l:options) + try + return jobstart(a:args, l:options) + catch + " NOTE: Call `on_exit` when cmd (args[0]) is not executable. + call timer_start(0, { -> l:options.on_exit(-1, -1, 'exit') }) + endtry endfunction function! s:stop(job) abort try call jobstop(a:job) - call jobwait([a:job]) catch /^Vim\%((\a\+)\)\=:E900/ " NOTE: " Vim does not raise exception even the job has already closed so fail @@ -58,24 +62,27 @@ else \ 'err_cb': funcref('s:out_cb', [a:options.on_stderr, 'stderr']), \ 'exit_cb': funcref('s:exit_cb', [a:options.on_exit, 'exit']), \} - return job_start(a:args, l:options) + let l:job = job_start(a:args, l:options) + if l:job->job_status() ==# "fail" + " NOTE: + " On Windows call `on_exit` when cmd (args[0]) is not executable. + " On Unix a non-existing command results in "dead" instead of "fail", + " and `on_exit` is called by `job_start()`. + call timer_start(0, { -> l:options.exit_cb(-1, -1) }) + endif + return l:job endfunction function! s:stop(job) abort call job_stop(a:job) call timer_start(s:KILL_TIMEOUT_MS, { -> job_stop(a:job, 'kill') }) - " Wait until the job is actually closed - while job_status(a:job) ==# 'run' - sleep 10m - endwhile - redraw endfunction - function! s:out_cb(callback, event, ch, msg) abort - call a:callback(a:ch, a:msg, a:event) + function! s:out_cb(callback, event, job, msg) abort + call a:callback(a:job, a:msg, a:event) endfunction - function! s:exit_cb(callback, event, ch, status) abort - call a:callback(a:ch, a:status, a:event) + function! s:exit_cb(callback, event, job, status) abort + call a:callback(a:job, a:status, a:event) endfunction endif diff --git a/autoload/denops/_internal/server/chan.vim b/autoload/denops/_internal/server/chan.vim index 00c97921..6b54b9b0 100644 --- a/autoload/denops/_internal/server/chan.vim +++ b/autoload/denops/_internal/server/chan.vim @@ -28,13 +28,13 @@ function! denops#_internal#server#chan#connect(addr, options) abort endif let l:retry_threshold = a:options.retry_threshold let l:retry_interval = a:options.retry_interval - let l:previous_exception = '' - for l:i in range(l:retry_threshold) + let l:i = 1 + while v:true call denops#_internal#echo#debug(printf( \ 'Connecting to channel `%s` [%d/%d]', \ a:addr, - \ l:i + 1, - \ l:retry_threshold + 1, + \ l:i, + \ l:retry_threshold, \)) try call s:connect(a:addr, a:options) @@ -43,28 +43,46 @@ function! denops#_internal#server#chan#connect(addr, options) abort call denops#_internal#echo#debug(printf( \ 'Failed to connect channel `%s` [%d/%d]: %s', \ a:addr, - \ l:i + 1, - \ l:retry_threshold + 1, + \ l:i, + \ l:retry_threshold, \ v:exception, \)) - let l:previous_exception = v:exception + if l:i >= l:retry_threshold + call denops#_internal#echo#error(printf( + \ 'Failed to connect channel `%s`: %s', + \ a:addr, + \ v:exception, + \)) + return + endif endtry execute printf('sleep %dm', l:retry_interval) - endfor - call denops#_internal#echo#error(printf( - \ 'Failed to connect channel `%s`: %s', - \ a:addr, - \ l:previous_exception, - \)) + let l:i += 1 + endwhile endfunction -function! denops#_internal#server#chan#close() abort +" Args: +" options: { +" timeout: number (default: 0) +" } +function! denops#_internal#server#chan#close(options) abort if s:chan is# v:null throw '[denops] Channel does not exist yet' endif + let l:options = extend({ + \ 'timeout': 0, + \}, a:options) let s:closed_on_purpose = 1 - call s:rpcclose(s:chan) - let s:chan = v:null + if l:options.timeout ==# 0 + return s:force_close(l:options) + endif + if l:options.timeout > 0 + let s:force_close_delayer = timer_start( + \ l:options.timeout, + \ { -> s:force_close(l:options) }, + \) + endif + call denops#_internal#server#chan#notify('invoke', ['close', []]) endfunction function! denops#_internal#server#chan#is_connected() abort @@ -91,36 +109,56 @@ endfunction function! s:connect(addr, options) abort let s:closed_on_purpose = 0 - let s:chan = s:rpcconnect(a:addr, { - \ 'on_close': { -> s:on_close(a:options) }, - \}) let s:addr = a:addr let s:options = a:options + let s:chan = s:rpcconnect(a:addr, { + \ 'on_close': { -> s:on_close() }, + \}) call denops#_internal#echo#debug(printf('Channel connected (%s)', a:addr)) call s:rpcnotify(s:chan, 'void', []) endfunction -function! s:on_close(options) abort +function! s:force_close(options) abort + let l:chan = s:chan + let s:chan = v:null + call s:clear_force_close_delayer() + call denops#_internal#echo#warn(printf( + \ 'Channel cannot close gracefully within %d millisec, force close (%s)', + \ a:options.timeout, + \ s:addr, + \)) + call s:rpcclose(l:chan) +endfunction + +function! s:clear_force_close_delayer() abort + if exists('s:force_close_delayer') + call timer_stop(s:force_close_delayer) + unlet s:force_close_delayer + endif +endfunction + +function! s:on_close() abort let s:chan = v:null + call s:clear_force_close_delayer() call denops#_internal#echo#debug(printf('Channel closed (%s)', s:addr)) doautocmd User DenopsClosed - if !a:options.reconnect_on_close || s:closed_on_purpose || s:exiting + if s:chan isnot# v:null || !s:options.reconnect_on_close || s:closed_on_purpose || s:exiting return endif " Reconnect - if s:reconnect_guard(a:options) + if s:reconnect_guard() return endif call denops#_internal#echo#warn('Channel closed. Reconnecting...') call timer_start( - \ a:options.reconnect_delay, + \ s:options.reconnect_delay, \ { -> denops#_internal#server#chan#connect(s:addr, s:options) }, \) endfunction -function! s:reconnect_guard(options) abort - let l:reconnect_threshold = a:options.reconnect_threshold - let l:reconnect_interval = a:options.reconnect_interval +function! s:reconnect_guard() abort + let l:reconnect_threshold = s:options.reconnect_threshold + let l:reconnect_interval = s:options.reconnect_interval let s:reconnect_count = get(s:, 'reconnect_count', 0) + 1 if s:reconnect_count >= l:reconnect_threshold call denops#_internal#echo#warn(printf( diff --git a/autoload/denops/_internal/server/proc.vim b/autoload/denops/_internal/server/proc.vim index 6b6c868f..e25ded1e 100644 --- a/autoload/denops/_internal/server/proc.vim +++ b/autoload/denops/_internal/server/proc.vim @@ -7,8 +7,6 @@ let s:exiting = 0 " Args: " options: { -" retry_interval: number -" retry_threshold: number " restart_on_exit: boolean " restart_delay: number " restart_interval: number @@ -20,33 +18,9 @@ function! denops#_internal#server#proc#start(options) abort if s:job isnot# v:null throw '[denops] Server already exists' endif - let l:retry_interval = a:options.retry_interval - let l:retry_threshold = a:options.retry_threshold - let l:previous_exception = '' - for l:i in range(l:retry_threshold) - call denops#_internal#echo#debug(printf( - \ 'Spawn server [%d/%d]', - \ l:i + 1, - \ l:retry_threshold + 1, - \)) - try - call s:start(a:options) - return v:true - catch - call denops#_internal#echo#debug(printf( - \ 'Failed to spawn server [%d/%d]: %s', - \ l:i + 1, - \ l:retry_threshold + 1, - \ v:exception, - \)) - let l:previous_exception = v:exception - endtry - execute printf('sleep %dm', l:retry_interval) - endfor - call denops#_internal#echo#error(printf( - \ 'Failed to spawn server: %s', - \ l:previous_exception, - \)) + call denops#_internal#echo#debug('Spawn server') + call s:start(a:options) + return v:true endfunction function! denops#_internal#server#proc#stop() abort @@ -116,7 +90,7 @@ function! s:on_exit(options, status) abort let s:job = v:null call denops#_internal#echo#debug(printf('Server stopped: %s', a:status)) execute printf('doautocmd User DenopsProcessStopped:%s', a:status) - if !a:options.restart_on_exit || s:stopped_on_purpose || s:exiting + if s:job isnot# v:null || !a:options.restart_on_exit || s:stopped_on_purpose || s:exiting return endif " Restart diff --git a/autoload/denops/server.vim b/autoload/denops/server.vim index 6eccde43..0c47b98e 100644 --- a/autoload/denops/server.vim +++ b/autoload/denops/server.vim @@ -2,18 +2,41 @@ const s:STATUS_STOPPED = 'stopped' const s:STATUS_STARTING = 'starting' const s:STATUS_PREPARING = 'preparing' const s:STATUS_RUNNING = 'running' +const s:STATUS_CLOSING = 'closing' +const s:STATUS_CLOSED = 'closed' let s:is_ready = v:false let s:ready_callbacks = [] +let s:stopping = v:false +let s:restart_once = v:false +let s:local_addr = "" + +let s:is_closed = v:false +let s:closing = v:false +let s:reconnect_once = v:false +let s:addr = "" + " Local server function! denops#server#start() abort - if g:denops#disabled || denops#_internal#server#proc#is_started() + if g:denops#disabled + return + endif + if s:stopping + let s:restart_once = v:true + return + endif + if denops#_internal#server#proc#is_started() + return + endif + if denops#_internal#server#chan#is_connected() + call denops#_internal#echo#warn(printf( + \ 'Not starting local server, already connected to (%s).', + \ s:addr, + \)) return endif return denops#_internal#server#proc#start({ - \ 'retry_interval': g:denops#server#retry_interval, - \ 'retry_threshold': g:denops#server#retry_threshold, \ 'restart_on_exit': v:true, \ 'restart_delay': g:denops#server#restart_delay, \ 'restart_interval': g:denops#server#restart_interval, @@ -22,10 +45,18 @@ function! denops#server#start() abort endfunction function! denops#server#stop() abort - if !denops#_internal#server#proc#is_started() + let s:restart_once = v:false + if s:stopping || !denops#_internal#server#proc#is_started() return endif - call denops#_internal#server#proc#stop() + let s:stopping = v:true + if s:is_connected_to_local_server() + if !s:closing + call s:disconnect() + endif + return + endif + call s:force_stop() endfunction function! denops#server#restart() abort @@ -33,33 +64,42 @@ function! denops#server#restart() abort call denops#server#start() endfunction +function! s:force_stop() abort + call denops#_internal#server#proc#stop() +endfunction + +function! s:is_connected_to_local_server() abort + return denops#_internal#server#chan#is_connected() && s:addr ==# s:local_addr +endfunction + " Shared server function! denops#server#connect() abort - if g:denops#disabled || denops#_internal#server#chan#is_connected() + if g:denops#disabled return endif - let l:addr = get(g:, 'denops_server_addr') - if empty(l:addr) - call denops#_internal#echo#error( - \ 'denops shared server address (g:denops_server_addr) is not given', - \) + if s:closing + let s:addr = s:get_server_addr() + if !empty(s:addr) + let s:reconnect_once = v:true + endif return endif - return denops#_internal#server#chan#connect(l:addr, { - \ 'retry_interval': g:denops#server#retry_interval, - \ 'retry_threshold': g:denops#server#retry_threshold, - \ 'reconnect_on_close': v:true, - \ 'reconnect_delay': g:denops#server#reconnect_delay, - \ 'reconnect_interval': g:denops#server#reconnect_interval, - \ 'reconnect_threshold': g:denops#server#reconnect_threshold, - \}) + if denops#_internal#server#chan#is_connected() + return + endif + let s:addr = s:get_server_addr() + if empty(s:addr) + return + endif + return s:connect(s:addr, { 'reconnect_on_close': v:true }) endfunction function! denops#server#close() abort - if !denops#_internal#server#chan#is_connected() + let s:reconnect_once = v:false + if s:closing || !denops#_internal#server#chan#is_connected() return endif - call denops#_internal#server#chan#close() + call s:disconnect() endfunction function! denops#server#reconnect() abort @@ -67,12 +107,26 @@ function! denops#server#reconnect() abort call denops#server#connect() endfunction +function! s:get_server_addr() abort + let l:addr = get(g:, 'denops_server_addr') + if empty(l:addr) + call denops#_internal#echo#error( + \ 'denops shared server address (g:denops_server_addr) is not given', + \) + endif + return l:addr +endfunction + " Common function! denops#server#status() abort - if denops#_internal#server#chan#is_connected() + if s:closing + return s:STATUS_CLOSING + elseif denops#_internal#server#chan#is_connected() return s:is_ready ? s:STATUS_RUNNING : s:STATUS_PREPARING elseif denops#_internal#server#proc#is_started() - return s:STATUS_STARTING + return s:is_closed ? s:STATUS_CLOSED : s:STATUS_STARTING + elseif s:stopping + return s:STATUS_CLOSED endif return s:STATUS_STOPPED endfunction @@ -119,16 +173,30 @@ function! denops#server#wait_async(callback) abort call add(s:ready_callbacks, a:callback) endfunction -function! s:DenopsProcessListen(expr) abort - let l:addr = matchstr(a:expr, '\')) autocmd User DenopsReady ++nested call s:DenopsReady() - autocmd User DenopsClosed let s:is_ready = v:false + autocmd User DenopsClosed call s:DenopsClosed() + autocmd User DenopsProcessStopped:* call s:DenopsProcessStopped() augroup END call denops#_internal#conf#define('denops#server#deno', g:denops#deno) @@ -165,5 +266,7 @@ call denops#_internal#conf#define('denops#server#reconnect_delay', 100) call denops#_internal#conf#define('denops#server#reconnect_interval', 1000) call denops#_internal#conf#define('denops#server#reconnect_threshold', 3) +call denops#_internal#conf#define('denops#server#close_timeout', 5000) + call denops#_internal#conf#define('denops#server#wait_interval', 200) call denops#_internal#conf#define('denops#server#wait_timeout', 30000) diff --git a/denops/@denops-private/host.ts b/denops/@denops-private/host.ts index e8b2c131..d0f57b49 100644 --- a/denops/@denops-private/host.ts +++ b/denops/@denops-private/host.ts @@ -59,6 +59,7 @@ export type Service = { success: CallbackId, failure: CallbackId, ): Promise; + close(): Promise; }; type ServiceForInvoke = Omit; @@ -82,6 +83,8 @@ export function invoke( return service.dispatchAsync( ...ensure(args, serviceMethodArgs.dispatchAsync), ); + case "close": + return service.close(...ensure(args, serviceMethodArgs.close)); default: throw new Error(`Service does not have a method '${name}'`); } @@ -95,6 +98,7 @@ const serviceMethodArgs = { dispatchAsync: is.ParametersOf( [is.String, is.String, is.Array, is.String, is.String] as const, ), + close: is.ParametersOf([] as const), } as const satisfies { [K in keyof ServiceForInvoke]: Predicate>; }; diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index d1e56125..20cdb1ae 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -27,6 +27,7 @@ Deno.test("Neovim", async (t) => { interrupt: () => unimplemented(), dispatch: () => unimplemented(), dispatchAsync: () => unimplemented(), + close: () => unimplemented(), }; await using host = new Neovim(reader, writer); diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index 65b16f1a..37aaefd7 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -22,6 +22,7 @@ Deno.test("Vim", async (t) => { interrupt: () => unimplemented(), dispatch: () => unimplemented(), dispatchAsync: () => unimplemented(), + close: () => unimplemented(), }; await using host = new Vim(reader, writer); diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index d1201440..b4d87f76 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -15,6 +15,7 @@ Deno.test("invoke", async (t) => { interrupt: () => unimplemented(), dispatch: () => unimplemented(), dispatchAsync: () => unimplemented(), + close: () => unimplemented(), }; await t.step("calls 'load'", async (t) => { @@ -107,6 +108,21 @@ Deno.test("invoke", async (t) => { }); }); + await t.step("calls 'close'", async (t) => { + await t.step("ok", async () => { + using s = stub(service, "close"); + await invoke(service, "close", []); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { args: [] }); + }); + + await t.step("invalid args", () => { + using s = stub(service, "close"); + assertThrows(() => invoke(service, "close", ["foo"]), AssertError); + assertSpyCalls(s, 0); + }); + }); + await t.step("calls unknown method", () => { assertThrows( () => invoke(service, "unknown-method", []), diff --git a/denops/@denops-private/testutil/shared_server.ts b/denops/@denops-private/testutil/shared_server.ts index 016bd90e..7aa79a02 100644 --- a/denops/@denops-private/testutil/shared_server.ts +++ b/denops/@denops-private/testutil/shared_server.ts @@ -71,6 +71,10 @@ export async function useSharedServer( const { writer: stderrWriter, reader: stderrReader } = channel(); stderr.pipeTo(stderrWriter).catch(noop); + const [addrReader, stdoutReader2] = stdoutReader.tee(); + const addrPromise = pop(addrReader); + addrPromise.finally(() => addrReader.cancel()); + const abort = async (reason: unknown) => { try { aborter.abort(reason); @@ -85,11 +89,11 @@ export async function useSharedServer( }; try { - const addr = await deadline(pop(stdoutReader), timeout); + const addr = await deadline(addrPromise, timeout); assert(typeof addr === "string"); return { - addr: addr.replace(/[\r\n].*/, ""), - stdout: stdoutReader, + addr: addr.replace(/\n.*/s, ""), + stdout: stdoutReader2, stderr: stderrReader, async [Symbol.asyncDispose]() { await abort("useSharedServer disposed"); diff --git a/doc/denops.txt b/doc/denops.txt index ae09a900..a01140f3 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -143,8 +143,7 @@ VARIABLE *denops-variable* Default: 10000 *g:denops#server#restart_threshold* - The number of restart counts within - |g:denops#server#restart_interval|. + The number of restart counts within |g:denops#server#restart_interval|. Default: 3 *g:denops#server#reconnect_interval* @@ -155,6 +154,11 @@ VARIABLE *denops-variable* The number of reconnect counts on connection failure. Default: 3 +*g:denops#server#close_timeout* + Timeout in milliseconds to wait for the channel to close gracefully. + If the timeout expires, the channel will be forcibly closed. + Default: 5000 + *g:denops#server#wait_interval* Interval in milliseconds for |denops#server#wait()|. Default: 10 @@ -250,7 +254,8 @@ denops#cache#update([{options}]) *denops#server#start()* denops#server#start() Starts a denops server process and connects to the channel. It does - nothing when the server is already started. + nothing when the server is already started or the channel is already + established. It is automatically called 1) on |VimEnter| autocmd when denops is in 'runtimepath' during Vim startup, 2) immediately when denops is @@ -288,7 +293,7 @@ denops#server#close() *denops#server#reconnect()* denops#server#reconnect() - Reconnects to a denops shared server. + Closes the channel and reconnects to a |denops-shared-server|. *denops#server#status()* denops#server#status() @@ -299,6 +304,8 @@ denops#server#status() "starting" Server is starting. "preparing" Server is preparing (initializing). "running" Server is running (ready). + "closing" Server is closing (disconnecting). + "closed" Server is closed (disconnected but server is running). Note that "starting" is never returned when a |denops-shared-server| is used. diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts index 007c0014..9403827b 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/server_test.ts @@ -1,10 +1,27 @@ -import { assertEquals, assertFalse } from "jsr:@std/assert@0.225.2"; +import { + assert, + assertEquals, + assertFalse, + assertMatch, + assertRejects, +} from "jsr:@std/assert@0.225.2"; +import { delay } from "jsr:@std/async@^0.224.0/delay"; +import { AsyncDisposableStack } from "jsr:@nick/dispose@1.1.0/async-disposable-stack"; import { testHost } from "../../denops/@denops-private/testutil/host.ts"; +import { useSharedServer } from "../../denops/@denops-private/testutil/shared_server.ts"; import { wait } from "../../denops/@denops-private/testutil/wait.ts"; +const MESSAGE_DELAY = 200; + testHost({ name: "denops#server#status()", mode: "all", + postlude: [ + // NOTE: The `plugin/denops.vim` must be sourced to initialize the environment. + "runtime plugin/denops.vim", + // NOTE: Disable startup on VimEnter. + "autocmd! denops_plugin_internal_startup VimEnter", + ], fn: async ({ host, t }) => { await t.step( "returns 'stopped' when no server running", @@ -14,23 +31,14 @@ testHost({ }, ); - // NOTE: The `plugin/denops.vim` must be sourced to initialize the environment. await host.call("execute", [ - "source plugin/denops.vim", - "autocmd User DenopsReady let g:denops_ready_fired = 1", - "autocmd User DenopsClosed let g:denops_closed_fired = 1", + "autocmd User DenopsReady let g:__test_denops_ready_fired = 1", + "autocmd User DenopsClosed let g:__test_denops_closed_fired = 1", ], ""); - // Wait to stopped the server that starts by `plugin/denops.vim` - await wait(() => host.call("exists", "g:denops_ready_fired")); - await host.call("denops#server#stop"); - await wait(() => host.call("eval", "denops#server#status() ==# 'stopped'")); - // NOTE: The status may transition to `preparing`, so get it within execute. // SEE: https://github.com/vim-denops/denops.vim/issues/354 await host.call("execute", [ - "unlet g:denops_ready_fired", - "unlet g:denops_closed_fired", "call denops#server#start()", "let g:__test_denops_server_status_when_start_called = denops#server#status()", ], ""); @@ -52,8 +60,10 @@ testHost({ const actual = await wait(() => host.call( "eval", - "exists('g:denops_ready_fired') ? 'DenopsReady is fired (flaky result)'" + - ": denops#server#status() !=# 'starting' ? denops#server#status() : 0", + "exists('g:__test_denops_ready_fired')" + + "? 'DenopsReady is fired (flaky result)'" + + ": denops#server#status() !=# 'starting'" + + " ? denops#server#status() : 0", ) ); assertEquals(actual, "preparing"); @@ -63,18 +73,42 @@ testHost({ await t.step( "returns 'running' after DenopsReady is fired", async () => { - await wait(() => host.call("exists", "g:denops_ready_fired")); + await wait(() => host.call("exists", "g:__test_denops_ready_fired")); const actual = await host.call("denops#server#status"); assertEquals(actual, "running"); }, ); + await t.step( + "returns 'closing' when denops#server#close() is called", + async () => { + await host.call("execute", [ + "call denops#server#close()", + "let g:__test_denops_server_status_when_close_called = denops#server#status()", + ], ""); + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_close_called", + ); + assertEquals(actual, "closing"); + }, + ); + + await t.step( + "returns 'closed' after DenopsClosed is fired", + async () => { + await wait(() => host.call("exists", "g:__test_denops_closed_fired")); + const actual = await host.call("denops#server#status"); + assertEquals(actual, "closed"); + }, + ); + await t.step( "returns 'stopped' after denops#server#stop() is called", async () => { await host.call("denops#server#stop"); - await wait(() => - host.call("eval", "denops#server#status() ==# 'stopped'") + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), ); }, ); @@ -82,63 +116,825 @@ testHost({ }); testHost({ - name: "Denops server", + name: "denops#server#start()", mode: "all", - fn: async ({ host, t }) => { - // NOTE: The status may transition to `preparing`, so get it within execute. - // SEE: https://github.com/vim-denops/denops.vim/issues/354 + postlude: [ + // NOTE: The `plugin/denops.vim` must be sourced to initialize the environment. + "runtime plugin/denops.vim", + // NOTE: Disable startup on VimEnter. + "autocmd! denops_plugin_internal_startup VimEnter", + ], + fn: async ({ host, t, stderr }) => { + let outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); + await host.call("execute", [ - "autocmd User DenopsReady let g:denops_ready_fired = 1", - "autocmd User DenopsClosed let g:denops_closed_fired = 1", - "source plugin/denops.vim", - "let g:__test_denops_server_status_on_sourced = denops#server#status()", + "autocmd User DenopsReady let g:__test_denops_ready_fired = 1", + "autocmd User DenopsProcessStopped:* let g:__test_denops_process_stopped_fired = expand('')", ], ""); - await t.step( - "'plugin/denops.vim' changes status to 'starting' when sourced", - async () => { + await t.step("if not yet started", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + + await host.call("execute", [ + "silent! unlet g:__test_denops_ready_fired", + "let g:__test_denops_server_start_result = denops#server#start()", + "let g:__test_denops_server_status_when_start_called = denops#server#status()", + ], ""); + + await t.step("returns truthy", async () => { + assert(await host.call("eval", "g:__test_denops_server_start_result")); + }); + + await t.step("changes status to 'starting' immediately", async () => { const actual = await host.call( "eval", - "g:__test_denops_server_status_on_sourced", + "g:__test_denops_server_status_when_start_called", ); assertEquals(actual, "starting"); - }, - ); + }); - await t.step( - "denops#server#status() returns 'running' after DenopsReady is fired", - async () => { - await wait(() => host.call("exists", "g:denops_ready_fired")); + await t.step("fires DenopsReady", async () => { + await wait(() => host.call("exists", "g:__test_denops_ready_fired")); + }); + + await t.step("changes status to 'running' asynchronously", async () => { const actual = await host.call("denops#server#status"); assertEquals(actual, "running"); - }, - ); + }); + }); + + await t.step("if already starting", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { + // FIXME: Without this block, Neovim cannot stop the server. + if (await host.call("denops#server#status") !== "stopped") { + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + } + + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + + await host.call("execute", [ + // First call, changes state to 'starting'. + "call denops#server#start()", + // 2nd call, do nothing. + "let g:__test_denops_server_start_result_2nd = denops#server#start()", + "let g:__test_denops_server_status_2nd = denops#server#status()", + ], ""); + await host.call("execute", [ + // 3rd call with asynchronously, do nothing. + "let g:__test_denops_server_start_result_3rd = denops#server#start()", + "let g:__test_denops_server_status_3rd = denops#server#status()", + ], ""); + + await t.step("returns falsy", async () => { + assertFalse( + await host.call("eval", "g:__test_denops_server_start_result_2nd"), + ); + assertFalse( + await host.call("eval", "g:__test_denops_server_start_result_3rd"), + ); + }); + + await t.step("does not change status from 'running'", async () => { + const actual = await host.call( + "eval", + "[g:__test_denops_server_status_2nd, g:__test_denops_server_status_3rd]", + ); + assertEquals(actual, ["starting", "starting"]); + }); + }); + + await t.step("if already connected to shared-server", async (t) => { + outputs = []; + await using stack = new AsyncDisposableStack(); + const server = stack.use(await useSharedServer()); + stack.defer(async () => { + await host.call("denops#server#close"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + await host.call("execute", [ + `let g:denops_server_addr = '${server.addr}'`, + "call denops#server#connect()", + ], ""); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await host.call("execute", [ + "let g:__test_denops_server_start_result = denops#server#start()", + "let g:__test_denops_server_status_when_start_called = denops#server#status()", + ], ""); + + await t.step("returns falsy", async () => { + assertFalse( + await host.call("eval", "g:__test_denops_server_start_result"), + ); + }); + + await t.step("does not change status from 'running'", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_start_called", + ); + assertEquals(actual, "running"); + }); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Not starting local server, already connected to /, + ); + }); + }); + + await t.step("if `deno` command is not exists", async (t) => { + await using stack = new AsyncDisposableStack(); + const saved_deno_path = await host.call("eval", "g:denops#server#deno"); + stack.defer(async () => { + await host.call("execute", [ + `let g:denops#server#deno = '${saved_deno_path}'`, + ]); + }); + + await host.call("execute", [ + "silent! unlet g:__test_denops_process_stopped_fired", + "let g:denops#server#deno = '__test_no_exists_deno_executable_path'", + "let g:denops#server#restart_threshold = 0", + "let g:__test_denops_server_start_result = denops#server#start()", + "let g:__test_denops_server_status_when_start_called = denops#server#status()", + ], ""); + + await t.step("returns truthy", async () => { + assert(await host.call("eval", "g:__test_denops_server_start_result")); + }); + + await t.step("changes status to 'starting' immediately", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_start_called", + ); + assertEquals(actual, "starting"); + }); + + await t.step("fires DenopsProcessStopped:*", async () => { + const actual = await wait(() => + host.call("eval", "get(g:, '__test_denops_process_stopped_fired')") + ); + assertMatch(actual as string, /^DenopsProcessStopped:-?[0-9]+/); + }); + + await t.step("changes status to 'stopped' asynchronously", async () => { + const actual = await host.call("denops#server#status"); + assertEquals(actual, "stopped"); + }); + }); + }, +}); + +testHost({ + name: "denops#server#stop()", + mode: "all", + postlude: [ + // NOTE: The `plugin/denops.vim` must be sourced to initialize the environment. + "runtime plugin/denops.vim", + // NOTE: Disable startup on VimEnter. + "autocmd! denops_plugin_internal_startup VimEnter", + ], + fn: async ({ host, t }) => { await host.call("execute", [ - "call denops#server#close()", - "let g:__test_denops_server_denops_closed_exists_after_close = exists('g:denops_closed_fired')", + "autocmd User DenopsClosed let g:__test_denops_closed_fired = 1", + "autocmd User DenopsProcessStopped:* let g:__test_denops_process_stopped_fired = expand('')", ], ""); - await t.step( - "denops#server#close() closes the connection asynchronously then DenopsClosed is fired", - async () => { + await t.step("if not yet started", async (t) => { + await host.call("execute", [ + "call denops#server#stop()", + "let g:__test_denops_server_status_when_stop_called = denops#server#status()", + ], ""); + + await t.step("does not change status from 'stopped'", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_stop_called", + ); + assertEquals(actual, "stopped"); + }); + }); + + await t.step("if already connected to local-server", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + await host.call("denops#server#start"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await host.call("execute", [ + "call denops#server#stop()", + "let g:__test_denops_server_status_when_stop_called = denops#server#status()", + "silent! unlet g:__test_denops_closed_fired", + "silent! unlet g:__test_denops_process_stopped_fired", + ], ""); + + await t.step("changes status to 'closing' immediately", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_stop_called", + ); + assertEquals(actual, "closing"); + }); + + await t.step("fires DenopsClosed", async () => { + await wait(() => host.call("exists", "g:__test_denops_closed_fired")); + }); + + await t.step("fires DenopsProcessStopped:*", async () => { + const actual = await wait(() => + host.call("eval", "get(g:, '__test_denops_process_stopped_fired')") + ); + assertMatch(actual as string, /^DenopsProcessStopped:-?[0-9]+/); + }); + + await t.step("changes status to 'stopped' asynchronously", async () => { + const actual = await host.call("denops#server#status"); + assertEquals(actual, "stopped"); + }); + }); + + await t.step("if already connected to shared-server", async (t) => { + await using stack = new AsyncDisposableStack(); + const server = stack.use(await useSharedServer()); + stack.defer(async () => { + await host.call("denops#server#close"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + + await host.call("execute", [ + `let g:denops_server_addr = '${server.addr}'`, + "call denops#server#connect()", + ], ""); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await host.call("execute", [ + "silent! unlet g:__test_denops_closed_fired", + "silent! unlet g:__test_denops_process_stopped_fired", + "call denops#server#stop()", + "let g:__test_denops_server_status_when_stop_called = denops#server#status()", + ], ""); + + await t.step("does not change status from 'running'", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_stop_called", + ); + assertEquals(actual, "running"); + }); + + await t.step("does not fire DenopsClosed", async () => { + await assertRejects( + () => + wait( + () => host.call("exists", "g:__test_denops_closed_fired"), + { timeout: 1000, interval: 100 }, + ), + Error, + "Timeout", + ); + }); + + await t.step("does not fire DenopsProcessStopped:*", async () => { + // NOTE: The timeout test is performed in `does not fire DenopsClosed`. assertFalse( - await host.call( + await host.call("exists", "g:__test_denops_process_stopped_fired"), + ); + }); + }); + }, +}); + +testHost({ + name: "denops#server#connect()", + mode: "all", + postlude: [ + // NOTE: The `plugin/denops.vim` must be sourced to initialize the environment. + "runtime plugin/denops.vim", + // NOTE: Disable startup on VimEnter. + "autocmd! denops_plugin_internal_startup VimEnter", + ], + fn: async ({ host, t, stderr }) => { + let outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); + + await host.call("execute", [ + "autocmd User DenopsReady let g:__test_denops_ready_fired = 1", + ], ""); + + await t.step("if not yet connected", async (t) => { + await t.step("if `g:denops_server_addr` is empty", async (t) => { + outputs = []; + + await host.call("execute", [ + `let g:denops_server_addr = ''`, + "silent! unlet g:__test_denops_ready_fired", + "let g:__test_denops_server_connect_result = denops#server#connect()", + "let g:__test_denops_server_status_when_connect_called = denops#server#status()", + ], ""); + + await t.step("returns falsy", async () => { + assertFalse( + await host.call("eval", "g:__test_denops_server_connect_result"), + ); + }); + + await t.step("does not change status from 'stopped'", async () => { + const actual = await host.call( "eval", - "g:__test_denops_server_denops_closed_exists_after_close", - ), + "g:__test_denops_server_status_when_connect_called", + ); + assertEquals(actual, "stopped"); + }); + + await t.step("outputs error message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /denops shared server address \(g:denops_server_addr\) is not given/, + ); + }); + + await t.step("does not fire DenopsReady", async () => { + await assertRejects( + () => + wait( + () => host.call("exists", "g:__test_denops_ready_fired"), + { timeout: 1000, interval: 100 }, + ), + Error, + "Timeout", + ); + }); + }); + + await t.step("if `g:denops_server_addr` is invalid", async (t) => { + outputs = []; + + // NOTE: Get a non-existent address. + const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 }); + const not_exists_address = `127.0.0.1:${listener.addr.port}`; + listener.close(); + + await host.call("execute", [ + `let g:denops_server_addr = '${not_exists_address}'`, + "let g:denops#server#retry_interval = 10", + "let g:denops#server#retry_threshold = 1", + "silent! unlet g:__test_denops_ready_fired", + "let g:__test_denops_server_connect_result = denops#server#connect()", + "let g:__test_denops_server_status_when_connect_called = denops#server#status()", + ], ""); + + await t.step("returns falsy", async () => { + assertFalse( + await host.call("eval", "g:__test_denops_server_connect_result"), + ); + }); + + await t.step("does not change status from 'stopped'", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_connect_called", + ); + assertEquals(actual, "stopped"); + }); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to connect channel `127\.0\.0\.1:[0-9]+`:/, + ); + }); + + await t.step("does not fire DenopsReady", async () => { + await assertRejects( + () => + wait( + () => host.call("exists", "g:__test_denops_ready_fired"), + { timeout: 1000, interval: 100 }, + ), + Error, + "Timeout", + ); + }); + }); + + await t.step("if `g:denops_server_addr` is valid", async (t) => { + await using stack = new AsyncDisposableStack(); + const server = stack.use(await useSharedServer()); + stack.defer(async () => { + await host.call("denops#server#close"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + + await host.call("execute", [ + `let g:denops_server_addr = '${server.addr}'`, + "silent! unlet g:__test_denops_ready_fired", + "let g:__test_denops_server_connect_result = denops#server#connect()", + "let g:__test_denops_server_status_when_connect_called = denops#server#status()", + ], ""); + + await t.step("returns truthy", async () => { + assert( + await host.call("eval", "g:__test_denops_server_connect_result"), + ); + }); + + await t.step("change status to 'preparing' immediately", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_connect_called", + ); + assertEquals(actual, "preparing"); + }); + + await t.step("fires DenopsReady", async () => { + await wait(() => host.call("exists", "g:__test_denops_ready_fired")); + }); + + await t.step("changes status to 'running' asynchronously", async () => { + assertEquals(await host.call("denops#server#status"), "running"); + }); + }); + }); + + await t.step("if already connected to local-server", async (t) => { + await using stack = new AsyncDisposableStack(); + const server = stack.use(await useSharedServer()); + stack.defer(async () => { + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), ); - await wait(() => host.call("exists", "g:denops_closed_fired")); - }, - ); + }); + await host.call("denops#server#start"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); - await t.step( - "denops#server#stop() stops the server process asynchronously", - async () => { + await host.call("execute", [ + `let g:denops_server_addr = '${server.addr}'`, + "call denops#server#connect()", + ], ""); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await host.call("execute", [ + "silent! unlet g:__test_denops_ready_fired", + "let g:__test_denops_server_connect_result = denops#server#connect()", + "let g:__test_denops_server_status_when_connect_called = denops#server#status()", + ], ""); + + await t.step("returns falsy", async () => { + assertFalse( + await host.call("eval", "g:__test_denops_server_connect_result"), + ); + }); + + await t.step("does not change status from 'running'", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_connect_called", + ); + assertEquals(actual, "running"); + }); + + await t.step("does not fire DenopsReady", async () => { + await assertRejects( + () => + wait( + () => host.call("exists", "g:__test_denops_ready_fired"), + { timeout: 1000, interval: 100 }, + ), + Error, + "Timeout", + ); + }); + }); + + await t.step("if already connected to shared-server", async (t) => { + await using stack = new AsyncDisposableStack(); + const server = stack.use(await useSharedServer()); + stack.defer(async () => { + await host.call("denops#server#close"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + + await host.call("execute", [ + `let g:denops_server_addr = '${server.addr}'`, + "call denops#server#connect()", + ], ""); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await host.call("execute", [ + "silent! unlet g:__test_denops_ready_fired", + "let g:__test_denops_server_connect_result = denops#server#connect()", + "let g:__test_denops_server_status_when_connect_called = denops#server#status()", + ], ""); + + await t.step("returns falsy", async () => { + assertFalse( + await host.call("eval", "g:__test_denops_server_connect_result"), + ); + }); + + await t.step("does not change status from 'running'", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_connect_called", + ); + assertEquals(actual, "running"); + }); + + await t.step("does not fire DenopsReady", async () => { + await assertRejects( + () => + wait( + () => host.call("exists", "g:__test_denops_ready_fired"), + { timeout: 1000, interval: 100 }, + ), + Error, + "Timeout", + ); + }); + }); + }, +}); + +testHost({ + name: "denops#server#close()", + mode: "all", + postlude: [ + // NOTE: The `plugin/denops.vim` must be sourced to initialize the environment. + "runtime plugin/denops.vim", + // NOTE: Disable startup on VimEnter. + "autocmd! denops_plugin_internal_startup VimEnter", + ], + fn: async ({ host, t, stderr }) => { + let outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); + + await host.call("execute", [ + "autocmd User DenopsClosed let g:__test_denops_closed_fired = 1", + ], ""); + + await t.step("if not yet connected", async (t) => { + await host.call("execute", [ + "silent! unlet g:__test_denops_closed_fired", + "call denops#server#close()", + "let g:__test_denops_server_status_when_close_called = denops#server#status()", + ], ""); + + await t.step("does not change status from 'stopped'", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_close_called", + ); + assertEquals(actual, "stopped"); + }); + + await t.step("does not fire DenopsClosed", async () => { + await assertRejects( + () => + wait( + () => host.call("exists", "g:__test_denops_closed_fired"), + { timeout: 1000, interval: 100 }, + ), + Error, + "Timeout", + ); + }); + }); + + await t.step("if already connected to local-server", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + await host.call("denops#server#start"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await host.call("execute", [ + "call denops#server#close()", + "let g:__test_denops_server_status_when_close_called = denops#server#status()", + "silent! unlet g:__test_denops_closed_fired", + ], ""); + + await t.step("changes status to 'closing' immediately", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_close_called", + ); + assertEquals(actual, "closing"); + }); + + await t.step("fires DenopsClosed", async () => { + await wait(() => host.call("exists", "g:__test_denops_closed_fired")); + }); + + await t.step("changes status to 'closed' asynchronously", async () => { const actual = await host.call("denops#server#status"); - assertEquals(actual, "stopped"); - }, - ); + assertEquals(actual, "closed"); + }); + + await t.step("does not stop the local-server", async () => { + await assertRejects( + () => + wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + { timeout: 1000, interval: 100 }, + ), + Error, + "Timeout", + ); + }); + + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + await host.call("denops#server#start"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("if timeouted", async (t) => { + outputs = []; + + await host.call("execute", [ + "silent! unlet g:__test_denops_closed_fired", + "let g:denops#server#close_timeout = 0", + "call denops#server#close()", + ], ""); + + await host.call("execute", [ + "let g:__test_denops_server_status_after_close_called = denops#server#status()", + ], ""); + + await t.step("closes the channel forcibly", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_after_close_called", + ); + assertEquals(actual, "closed"); + }); + + await t.step("fires DenopsClosed", async () => { + assert(await host.call("exists", "g:__test_denops_closed_fired")); + }); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Channel cannot close gracefully within 0 millisec, force close/, + ); + }); + + await t.step("does not stop the local-server", async () => { + await assertRejects( + () => + wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + { timeout: 1000, interval: 100 }, + ), + Error, + "Timeout", + ); + }); + }); + }); + + await t.step("if already connected to shared-server", async (t) => { + await using stack = new AsyncDisposableStack(); + const server = stack.use(await useSharedServer()); + stack.defer(async () => { + await host.call("denops#server#close"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + + await host.call("execute", [ + `let g:denops_server_addr = '${server.addr}'`, + "call denops#server#connect()", + ], ""); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await host.call("execute", [ + "call denops#server#close()", + "let g:__test_denops_server_status_when_close_called = denops#server#status()", + "silent! unlet g:__test_denops_closed_fired", + ], ""); + + await t.step("changes status to 'closing' immediately", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_close_called", + ); + assertEquals(actual, "closing"); + }); + + await t.step("fires DenopsClosed", async () => { + await wait(() => host.call("exists", "g:__test_denops_closed_fired")); + }); + + await t.step("changes status to 'stopped' asynchronously", async () => { + assertEquals(await host.call("denops#server#status"), "stopped"); + }); + + await host.call("denops#server#connect"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("if timeouted", async (t) => { + outputs = []; + + await host.call("execute", [ + "silent! unlet g:__test_denops_closed_fired", + "let g:denops#server#close_timeout = 0", + "call denops#server#close()", + ], ""); + + await host.call("execute", [ + "let g:__test_denops_server_status_after_close_called = denops#server#status()", + ], ""); + + await t.step("closes the channel forcibly", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_after_close_called", + ); + assertEquals(actual, "stopped"); + }); + + await t.step("fires DenopsClosed", async () => { + assert(await host.call("exists", "g:__test_denops_closed_fired")); + }); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Channel cannot close gracefully within 0 millisec, force close/, + ); + }); + }); + }); }, }); From a732c4b35262c08e011cb1c168bfed7faec4df7c Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 30 Jun 2024 13:31:43 +0900 Subject: [PATCH 43/99] :muscle: use internal `DenopsSystem*` events --- autoload/denops/_internal/server/chan.vim | 6 +- autoload/denops/_internal/server/proc.vim | 12 ++-- autoload/denops/server.vim | 85 ++++++++++++++--------- denops/@denops-private/worker.ts | 6 +- denops/@denops-private/worker_test.ts | 20 ++++-- 5 files changed, 81 insertions(+), 48 deletions(-) diff --git a/autoload/denops/_internal/server/chan.vim b/autoload/denops/_internal/server/chan.vim index 6b54b9b0..6ebce573 100644 --- a/autoload/denops/_internal/server/chan.vim +++ b/autoload/denops/_internal/server/chan.vim @@ -141,7 +141,7 @@ function! s:on_close() abort let s:chan = v:null call s:clear_force_close_delayer() call denops#_internal#echo#debug(printf('Channel closed (%s)', s:addr)) - doautocmd User DenopsClosed + doautocmd User DenopsSystemClosed if s:chan isnot# v:null || !s:options.reconnect_on_close || s:closed_on_purpose || s:exiting return endif @@ -181,6 +181,6 @@ endfunction augroup denops_internal_server_chan_internal autocmd! autocmd VimLeave * let s:exiting = 1 - autocmd User DenopsReady : - autocmd User DenopsClosed : + autocmd User DenopsSystemReady : + autocmd User DenopsSystemClosed : augroup END diff --git a/autoload/denops/_internal/server/proc.vim b/autoload/denops/_internal/server/proc.vim index e25ded1e..936eedf8 100644 --- a/autoload/denops/_internal/server/proc.vim +++ b/autoload/denops/_internal/server/proc.vim @@ -62,7 +62,7 @@ function! s:start(options) abort \}) let s:options = a:options call denops#_internal#echo#debug(printf('Server started: %s', l:args)) - doautocmd User DenopsProcessStarted + doautocmd User DenopsSystemProcessStarted endfunction function! s:on_stdout(store, data) abort @@ -75,7 +75,7 @@ function! s:on_stdout(store, data) abort let a:store.prepared = 1 let l:addr = substitute(a:data, '\r\?\n$', '', 'g') call denops#_internal#echo#debug(printf('Server listen: %s', l:addr)) - execute printf('doautocmd User DenopsProcessListen:%s', l:addr) + execute printf('doautocmd User DenopsSystemProcessListen:%s', l:addr) endfunction function! s:on_stderr(data) abort @@ -89,7 +89,7 @@ endfunction function! s:on_exit(options, status) abort let s:job = v:null call denops#_internal#echo#debug(printf('Server stopped: %s', a:status)) - execute printf('doautocmd User DenopsProcessStopped:%s', a:status) + execute printf('doautocmd User DenopsSystemProcessStopped:%s', a:status) if s:job isnot# v:null || !a:options.restart_on_exit || s:stopped_on_purpose || s:exiting return endif @@ -132,7 +132,7 @@ endfunction augroup denops_internal_server_proc_internal autocmd! autocmd VimLeave * let s:exiting = 1 - autocmd User DenopsProcessStarted : - autocmd User DenopsProcessListen:* : - autocmd User DenopsProcessStopped:* : + autocmd User DenopsSystemProcessStarted : + autocmd User DenopsSystemProcessListen:* : + autocmd User DenopsSystemProcessStopped:* : augroup END diff --git a/autoload/denops/server.vim b/autoload/denops/server.vim index 0c47b98e..03d98fd3 100644 --- a/autoload/denops/server.vim +++ b/autoload/denops/server.vim @@ -193,59 +193,82 @@ function! s:disconnect(...) abort call denops#_internal#server#chan#close(l:options) endfunction -function! s:DenopsProcessListen(expr) abort - let s:addr = matchstr(a:expr, '\ User DenopsProcessStarted +endfunction + +function! s:DenopsSystemProcessListen(expr) abort + let s:addr = matchstr(a:expr, '\ User DenopsReady + endtry endfunction -function! s:DenopsClosed() abort -echom "DenopsClosed" +function! s:DenopsSystemClosed() abort let s:closing = v:false let s:is_ready = v:false if denops#_internal#server#proc#is_started() let s:is_closed = v:true endif - " Shared server - if s:reconnect_once - let s:reconnect_once = v:false - call s:connect(s:addr, { 'reconnect_on_close': v:true }) - return - endif - let s:addr = "" - " Local server - if s:stopping && denops#_internal#server#proc#is_started() - call s:force_stop() - endif - let s:local_addr = "" + try + " Shared server + if s:reconnect_once + let s:reconnect_once = v:false + call s:connect(s:addr, { 'reconnect_on_close': v:true }) + return + endif + let s:addr = "" + " Local server + if s:stopping && denops#_internal#server#proc#is_started() + call s:force_stop() + endif + let s:local_addr = "" + finally + doautocmd User DenopsClosed + endtry endfunction -function! s:DenopsProcessStopped() abort -echom "DenopsProcessStopped" +function! s:DenopsSystemProcessStopped(expr) abort + let l:status = matchstr(a:expr, '\ User DenopsProcessStopped:%s', + \ l:status, + \) + endtry endfunction augroup denops_server_internal autocmd! - autocmd User DenopsProcessListen:* call s:DenopsProcessListen(expand('')) - autocmd User DenopsReady ++nested call s:DenopsReady() - autocmd User DenopsClosed call s:DenopsClosed() - autocmd User DenopsProcessStopped:* call s:DenopsProcessStopped() + autocmd User DenopsReady : + autocmd User DenopsClosed : + autocmd User DenopsProcessStarted : + autocmd User DenopsProcessStopped:* : + autocmd User DenopsSystemProcessStarted ++nested call s:DenopsSystemProcessStarted() + autocmd User DenopsSystemProcessListen:* ++nested call s:DenopsSystemProcessListen(expand('')) + autocmd User DenopsSystemReady ++nested call s:DenopsSystemReady() + autocmd User DenopsSystemClosed ++nested call s:DenopsSystemClosed() + autocmd User DenopsSystemProcessStopped:* ++nested call s:DenopsSystemProcessStopped(expand('')) augroup END call denops#_internal#conf#define('denops#server#deno', g:denops#deno) diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index aa0538b1..06fcb7b3 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -79,7 +79,11 @@ async function connectHost(): Promise { using sigintTrap = asyncSignal("SIGINT"); await using service = new Service(meta); await host.init(service); - await host.call("execute", "doautocmd User DenopsReady", ""); + await host.call( + "execute", + "doautocmd User DenopsSystemReady", + "", + ); await Promise.race([ service.waitClosed(), host.waitClosed(), diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index 9289c2a6..38873784 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -206,7 +206,7 @@ for (const { host, mode } of matrix) { }); } - await t.step("doautocmd `User DenopsReady`", async () => { + await t.step("doautocmd `User DenopsSystemReady`", async () => { using _ = usePostMessageHistory(); await delay(0); assertEquals(messageStub.postMessage.callCount, 1); @@ -216,7 +216,10 @@ for (const { host, mode } of matrix) { [ "call", "denops#api#vim#call", - ["execute", ["doautocmd User DenopsReady", ""]], + ["execute", [ + "doautocmd User DenopsSystemReady", + "", + ]], -2, ], ); @@ -228,7 +231,10 @@ for (const { host, mode } of matrix) { 0, 2, "nvim_call_function", - ["execute", ["doautocmd User DenopsReady", ""]], + ["execute", [ + "doautocmd User DenopsSystemReady", + "", + ]], ], ); messageStub.fakeHostMessage(nvimCodec.encode([1, 2, null, ""])); @@ -449,7 +455,7 @@ for (const { host, mode } of matrix) { // requests Meta data messageStub.fakeHostMessage(vimCodec.encode([-1, [fakeMeta, ""]])); await delay(0); - // doautocmd `User DenopsReady` + // doautocmd `User DenopsSystemReady` messageStub.fakeHostMessage(vimCodec.encode([-2, ["", ""]])); await delay(0); } else { @@ -462,7 +468,7 @@ for (const { host, mode } of matrix) { // sets client info messageStub.fakeHostMessage(nvimCodec.encode([1, 1, null, 0])); await delay(0); - // doautocmd `User DenopsReady` + // doautocmd `User DenopsSystemReady` messageStub.fakeHostMessage(nvimCodec.encode([1, 2, null, ""])); await delay(0); } @@ -527,7 +533,7 @@ for (const { host, mode } of matrix) { // requests Meta data messageStub.fakeHostMessage(vimCodec.encode([-1, [fakeMeta, ""]])); await delay(0); - // doautocmd `User DenopsReady` + // doautocmd `User DenopsSystemReady` messageStub.fakeHostMessage(vimCodec.encode([-2, ["", ""]])); await delay(0); } else { @@ -540,7 +546,7 @@ for (const { host, mode } of matrix) { // sets client info messageStub.fakeHostMessage(nvimCodec.encode([1, 1, null, 0])); await delay(0); - // doautocmd `User DenopsReady` + // doautocmd `User DenopsSystemReady` messageStub.fakeHostMessage(nvimCodec.encode([1, 2, null, ""])); await delay(0); } From 5a6f45f3378a3ce9add1afc2c01b7d9c18e61c7f Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 29 Jun 2024 12:22:21 +0900 Subject: [PATCH 44/99] :herb: `useSharedServer` helper returns `addr.host` and `addr.port` --- .../@denops-private/testutil/shared_server.ts | 19 +++++++++--- .../testutil/shared_server_test.ts | 31 ++++++++++++++++--- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/denops/@denops-private/testutil/shared_server.ts b/denops/@denops-private/testutil/shared_server.ts index 7aa79a02..9c422af5 100644 --- a/denops/@denops-private/testutil/shared_server.ts +++ b/denops/@denops-private/testutil/shared_server.ts @@ -11,6 +11,8 @@ const origError = console.error.bind(console); const noop = () => {}; export interface UseSharedServerOptions { + /** The port number of the shared server. */ + port?: number; /** Print shared-server messages. */ verbose?: boolean; /** Environment variables. */ @@ -21,7 +23,11 @@ export interface UseSharedServerOptions { export interface UseSharedServerResult extends AsyncDisposable { /** Address to connect to the shared server. */ - addr: string; + addr: { + host: string; + port: number; + toString(): string; + }; /** Shared server standard output. */ stdout: ReadableStream; /** Shared server error output. */ @@ -34,7 +40,7 @@ export interface UseSharedServerResult extends AsyncDisposable { export async function useSharedServer( options?: UseSharedServerOptions, ): Promise { - const { denopsPath, verbose, env, timeout = DEFAULT_TIMEOUT } = { + const { denopsPath, port = 0, verbose, env, timeout = DEFAULT_TIMEOUT } = { ...getConfig(), ...options, }; @@ -50,7 +56,7 @@ export async function useSharedServer( script, "--identity", "--port", - "0", + `${port}`, ]; const proc = new Deno.Command(cmd, { args, @@ -91,8 +97,13 @@ export async function useSharedServer( try { const addr = await deadline(addrPromise, timeout); assert(typeof addr === "string"); + const [_, host, port] = addr.match(/^([^:]*):(\d+)(?:\n|$)/) ?? []; return { - addr: addr.replace(/\n.*/s, ""), + addr: { + host, + port: Number.parseInt(port), + toString: () => `${host}:${port}`, + }, stdout: stdoutReader2, stderr: stderrReader, async [Symbol.asyncDispose]() { diff --git a/denops/@denops-private/testutil/shared_server_test.ts b/denops/@denops-private/testutil/shared_server_test.ts index f3c32bd0..b8d52dd3 100644 --- a/denops/@denops-private/testutil/shared_server_test.ts +++ b/denops/@denops-private/testutil/shared_server_test.ts @@ -1,4 +1,5 @@ import { + assertEquals, assertInstanceOf, assertMatch, assertNotMatch, @@ -10,9 +11,19 @@ import { useSharedServer } from "./shared_server.ts"; Deno.test("useSharedServer()", async (t) => { await t.step("if `verbose` is not specified", async (t) => { - await t.step("returns `result.addr`", async () => { - await using server = await useSharedServer(); - assertMatch(server.addr, /^127\.0\.0\.1:\d+$/); + await t.step("returns `result.addr`", async (t) => { + await using server = await useSharedServer({ verbose: true }); + const { addr } = server; + + await t.step("`addr.host` is string", () => { + assertEquals(addr.host, "127.0.0.1"); + }); + await t.step("`addr.port` is number", () => { + assertEquals(typeof addr.port, "number"); + }); + await t.step("`addr.toString()` returns the address", () => { + assertMatch(addr.toString(), /^127\.0\.0\.1:\d+$/); + }); }); await t.step("returns `result.stdout`", async () => { @@ -44,9 +55,19 @@ Deno.test("useSharedServer()", async (t) => { }); await t.step("if `verbose` is true", async (t) => { - await t.step("returns `result.addr`", async () => { + await t.step("returns `result.addr`", async (t) => { await using server = await useSharedServer({ verbose: true }); - assertMatch(server.addr, /^127\.0\.0\.1:\d+$/); + const { addr } = server; + + await t.step("`addr.host` is string", () => { + assertEquals(addr.host, "127.0.0.1"); + }); + await t.step("`addr.port` is number", () => { + assertEquals(typeof addr.port, "number"); + }); + await t.step("`addr.toString()` returns the address", () => { + assertMatch(addr.toString(), /^127\.0\.0\.1:\d+$/); + }); }); await t.step("returns `result.stdout`", async () => { From c419ca7bd3bd7358d529b77ce9f1fdf9d349b56e Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 2 Jul 2024 10:05:16 +0900 Subject: [PATCH 45/99] :herb: `withHost` helper provides current `mode` --- denops/@denops-private/testutil/host.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/denops/@denops-private/testutil/host.ts b/denops/@denops-private/testutil/host.ts index a24cf465..440cd0fe 100644 --- a/denops/@denops-private/testutil/host.ts +++ b/denops/@denops-private/testutil/host.ts @@ -4,6 +4,7 @@ import { Vim } from "../host/vim.ts"; import { withNeovim, WithOptions, withVim } from "./with.ts"; export type HostFn = (helper: { + mode: "vim" | "nvim"; host: Host; stdout: ReadableStream; stderr: ReadableStream; @@ -23,7 +24,7 @@ export function withHost( return withVim({ fn: async ({ reader, writer, stdout, stderr }) => { await using host = new Vim(reader, writer); - return await fn({ host, stdout, stderr }); + return await fn({ mode, host, stdout, stderr }); }, ...withOptions, }); @@ -32,7 +33,7 @@ export function withHost( return withNeovim({ fn: async ({ reader, writer, stdout, stderr }) => { await using host = new Neovim(reader, writer); - return await fn({ host, stdout, stderr }); + return await fn({ mode, host, stdout, stderr }); }, ...withOptions, }); @@ -41,6 +42,7 @@ export function withHost( } export type TestFn = (helper: { + mode: "vim" | "nvim"; host: Host; t: Deno.TestContext; stdout: ReadableStream; @@ -74,7 +76,7 @@ export function testHost( fn: async (t) => { await withHost>({ mode, - fn: ({ host, stdout, stderr }) => fn({ host, t, stdout, stderr }), + fn: (helper) => fn({ ...helper, t }), ...hostOptions, }); }, From 9c5978b631d126a95886240a7880819a01cfbef4 Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 2 Jul 2024 12:44:15 +0900 Subject: [PATCH 46/99] :muscle: use `a:options` instead of script variable --- autoload/denops/_internal/server/chan.vim | 20 +++++++++----------- autoload/denops/_internal/server/proc.vim | 4 +--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/autoload/denops/_internal/server/chan.vim b/autoload/denops/_internal/server/chan.vim index 6ebce573..2a307106 100644 --- a/autoload/denops/_internal/server/chan.vim +++ b/autoload/denops/_internal/server/chan.vim @@ -6,7 +6,6 @@ const s:rpcrequest = function(printf('denops#_internal#rpc#%s#request', s:HOST)) let s:chan = v:null let s:addr = v:null -let s:options = v:null let s:closed_on_purpose = 0 let s:exiting = 0 @@ -110,9 +109,8 @@ endfunction function! s:connect(addr, options) abort let s:closed_on_purpose = 0 let s:addr = a:addr - let s:options = a:options let s:chan = s:rpcconnect(a:addr, { - \ 'on_close': { -> s:on_close() }, + \ 'on_close': { -> s:on_close(a:options) }, \}) call denops#_internal#echo#debug(printf('Channel connected (%s)', a:addr)) call s:rpcnotify(s:chan, 'void', []) @@ -137,28 +135,28 @@ function! s:clear_force_close_delayer() abort endif endfunction -function! s:on_close() abort +function! s:on_close(options) abort let s:chan = v:null call s:clear_force_close_delayer() call denops#_internal#echo#debug(printf('Channel closed (%s)', s:addr)) doautocmd User DenopsSystemClosed - if s:chan isnot# v:null || !s:options.reconnect_on_close || s:closed_on_purpose || s:exiting + if s:chan isnot# v:null || !a:options.reconnect_on_close || s:closed_on_purpose || s:exiting return endif " Reconnect - if s:reconnect_guard() + if s:reconnect_guard(a:options) return endif call denops#_internal#echo#warn('Channel closed. Reconnecting...') call timer_start( - \ s:options.reconnect_delay, - \ { -> denops#_internal#server#chan#connect(s:addr, s:options) }, + \ a:options.reconnect_delay, + \ { -> denops#_internal#server#chan#connect(s:addr, a:options) }, \) endfunction -function! s:reconnect_guard() abort - let l:reconnect_threshold = s:options.reconnect_threshold - let l:reconnect_interval = s:options.reconnect_interval +function! s:reconnect_guard(options) abort + let l:reconnect_threshold = a:options.reconnect_threshold + let l:reconnect_interval = a:options.reconnect_interval let s:reconnect_count = get(s:, 'reconnect_count', 0) + 1 if s:reconnect_count >= l:reconnect_threshold call denops#_internal#echo#warn(printf( diff --git a/autoload/denops/_internal/server/proc.vim b/autoload/denops/_internal/server/proc.vim index 936eedf8..293ed9d7 100644 --- a/autoload/denops/_internal/server/proc.vim +++ b/autoload/denops/_internal/server/proc.vim @@ -1,7 +1,6 @@ const s:SCRIPT = denops#_internal#path#script(['@denops-private', 'cli.ts']) let s:job = v:null -let s:options = v:null let s:stopped_on_purpose = 0 let s:exiting = 0 @@ -60,7 +59,6 @@ function! s:start(options) abort \ 'on_stderr': { _job, data, _event -> s:on_stderr(data) }, \ 'on_exit': { _job, status, _event -> s:on_exit(a:options, status) }, \}) - let s:options = a:options call denops#_internal#echo#debug(printf('Server started: %s', l:args)) doautocmd User DenopsSystemProcessStarted endfunction @@ -103,7 +101,7 @@ function! s:on_exit(options, status) abort \)) call timer_start( \ a:options.restart_delay, - \ { -> denops#_internal#server#proc#start(s:options) }, + \ { -> denops#_internal#server#proc#start(a:options) }, \) endfunction From 3ee8a2e82033e73c126e40c4728f7383d52bc64d Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 2 Jul 2024 14:42:57 +0900 Subject: [PATCH 47/99] :+1: fix restart behavior --- autoload/denops/_internal/server/proc.vim | 34 +++- autoload/denops/server.vim | 2 +- doc/denops.txt | 7 +- tests/denops/server_test.ts | 185 +++++++++++++++++++++- 4 files changed, 217 insertions(+), 11 deletions(-) diff --git a/autoload/denops/_internal/server/proc.vim b/autoload/denops/_internal/server/proc.vim index 293ed9d7..e2916f17 100644 --- a/autoload/denops/_internal/server/proc.vim +++ b/autoload/denops/_internal/server/proc.vim @@ -12,17 +12,25 @@ let s:exiting = 0 " restart_threshold: number " } " Return: -" boolean +" v:true function! denops#_internal#server#proc#start(options) abort + call s:clear_restart_delayer() if s:job isnot# v:null throw '[denops] Server already exists' endif - call denops#_internal#echo#debug('Spawn server') + call denops#_internal#echo#debug(printf( + \ 'Spawn server [%d/%d]', + \ get(s:, 'restart_count', 0), + \ a:options.restart_threshold, + \)) call s:start(a:options) return v:true endfunction function! denops#_internal#server#proc#stop() abort + if s:clear_restart_delayer() + return + endif if s:job is# v:null throw '[denops] Server does not exist yet' endif @@ -99,7 +107,7 @@ function! s:on_exit(options, status) abort \ 'Server stopped (%d). Restarting...', \ a:status, \)) - call timer_start( + let s:restart_delayer = timer_start( \ a:options.restart_delay, \ { -> denops#_internal#server#proc#start(a:options) }, \) @@ -109,12 +117,13 @@ function! s:restart_guard(options) abort let l:restart_threshold = a:options.restart_threshold let l:restart_interval = a:options.restart_interval let s:restart_count = get(s:, 'restart_count', 0) + 1 - if s:restart_count >= l:restart_threshold + if s:restart_count > l:restart_threshold call denops#_internal#echo#warn(printf( \ 'Server stopped %d times within %d millisec. Denops is disabled to avoid infinity restart loop.', - \ l:restart_threshold, + \ s:restart_count, \ l:restart_interval, \)) + let s:restart_count = 0 let g:denops#disabled = 1 return 1 endif @@ -127,6 +136,14 @@ function! s:restart_guard(options) abort \) endfunction +function! s:clear_restart_delayer() abort + if exists('s:restart_delayer') + call timer_stop(s:restart_delayer) + unlet s:restart_delayer + return v:true + endif +endfunction + augroup denops_internal_server_proc_internal autocmd! autocmd VimLeave * let s:exiting = 1 @@ -134,3 +151,10 @@ augroup denops_internal_server_proc_internal autocmd User DenopsSystemProcessListen:* : autocmd User DenopsSystemProcessStopped:* : augroup END + +function! denops#_internal#server#proc#_get_job_for_test() abort + call denops#_internal#echo#warn( + \ '_get_job_for_test() should only be used for testing.' + \) + return s:job +endfunction diff --git a/autoload/denops/server.vim b/autoload/denops/server.vim index 03d98fd3..23c09a7c 100644 --- a/autoload/denops/server.vim +++ b/autoload/denops/server.vim @@ -138,7 +138,7 @@ function! denops#server#wait(...) abort \ 'silent': 0, \}, a:0 ? a:1 : {}, \) - if denops#server#status() ==# 'stopped' + if denops#server#status() ==# s:STATUS_STOPPED if !l:options.silent call denops#_internal#echo#error( \ 'Failed to wait `DenopsReady` autocmd. Denops server itself is not started.', diff --git a/doc/denops.txt b/doc/denops.txt index a01140f3..6052d1c6 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -132,8 +132,8 @@ VARIABLE *denops-variable* Default: ['-q', '--no-lock', '-A'] *g:denops#server#restart_delay* - Restart delay in milliseconds to avoid #136. - https://github.com/vim-denops/denops.vim/issues/136 + The delay in milliseconds before restarting the server. + This avoid #136. https://github.com/vim-denops/denops.vim/issues/136 Default: 100 *g:denops#server#restart_interval* @@ -143,7 +143,8 @@ VARIABLE *denops-variable* Default: 10000 *g:denops#server#restart_threshold* - The number of restart counts within |g:denops#server#restart_interval|. + The number of restart counts on unexpected process terminattion within + |g:denops#server#restart_interval|. Default: 3 *g:denops#server#reconnect_interval* diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts index 9403827b..45de6007 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/server_test.ts @@ -124,17 +124,58 @@ testHost({ // NOTE: Disable startup on VimEnter. "autocmd! denops_plugin_internal_startup VimEnter", ], - fn: async ({ host, t, stderr }) => { + fn: async ({ mode, host, t, stderr }) => { let outputs: string[] = []; stderr.pipeTo( new WritableStream({ write: (s) => void outputs.push(s) }), ).catch(() => {}); + async function forceShutdownServer() { + const serverPid = await host.call( + "eval", + mode === "vim" + ? "job_info(denops#_internal#server#proc#_get_job_for_test()).process" + : "jobpid(denops#_internal#server#proc#_get_job_for_test())", + ) as number; + Deno.kill(serverPid, "SIGKILL"); + } + await host.call("execute", [ "autocmd User DenopsReady let g:__test_denops_ready_fired = 1", + "autocmd User DenopsClosed let g:__test_denops_closed_fired = 1", + "autocmd User DenopsProcessStarted let g:__test_denops_process_started_fired = 1", "autocmd User DenopsProcessStopped:* let g:__test_denops_process_stopped_fired = expand('')", ], ""); + await t.step("if denops disabled", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { + await host.call("execute", [ + "let g:denops#disabled = 0", + ], ""); + }); + + await host.call("execute", [ + "let g:denops#disabled = 1", + "let g:__test_denops_server_start_result = denops#server#start()", + "let g:__test_denops_server_status_when_start_called = denops#server#status()", + ], ""); + + await t.step("returns falsy", async () => { + assertFalse( + await host.call("eval", "g:__test_denops_server_start_result"), + ); + }); + + await t.step("does not change status from 'stopped'", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_start_called", + ); + assertEquals(actual, "stopped"); + }); + }); + await t.step("if not yet started", async (t) => { await using stack = new AsyncDisposableStack(); stack.defer(async () => { @@ -146,6 +187,7 @@ testHost({ await host.call("execute", [ "silent! unlet g:__test_denops_ready_fired", + "silent! unlet g:__test_denops_process_started_fired", "let g:__test_denops_server_start_result = denops#server#start()", "let g:__test_denops_server_status_when_start_called = denops#server#status()", ], ""); @@ -162,6 +204,12 @@ testHost({ assertEquals(actual, "starting"); }); + await t.step("fires DenopsProcessStarted", async () => { + await wait(() => + host.call("exists", "g:__test_denops_process_started_fired") + ); + }); + await t.step("fires DenopsReady", async () => { await wait(() => host.call("exists", "g:__test_denops_ready_fired")); }); @@ -270,9 +318,14 @@ testHost({ await using stack = new AsyncDisposableStack(); const saved_deno_path = await host.call("eval", "g:denops#server#deno"); stack.defer(async () => { + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); await host.call("execute", [ `let g:denops#server#deno = '${saved_deno_path}'`, - ]); + `let g:denops#disabled = 0`, + ], ""); }); await host.call("execute", [ @@ -306,6 +359,134 @@ testHost({ const actual = await host.call("denops#server#status"); assertEquals(actual, "stopped"); }); + + await t.step("outputs warning message", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Server stopped 1 times .* Denops is disabled/, + ); + }); + + await t.step("changes `g:denops#disabled` to truthy", async () => { + assert(await host.call("eval", "g:denops#disabled")); + }); + }); + + await t.step("if the server is stopped unexpectedly", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + + await host.call("execute", [ + "let g:denops#server#restart_delay = 1000", + "let g:denops#server#restart_interval = 1000", + "let g:denops#server#restart_threshold = 1", + "call denops#server#start()", + ], ""); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + await host.call("execute", [ + "silent! unlet g:__test_denops_ready_fired", + "silent! unlet g:__test_denops_closed_fired", + "silent! unlet g:__test_denops_process_started_fired", + "silent! unlet g:__test_denops_process_stopped_fired", + ], ""); + outputs = []; + + await forceShutdownServer(); + + await t.step("fires DenopsClosed", async () => { + await wait(() => host.call("exists", "g:__test_denops_closed_fired")); + }); + + await t.step("fires DenopsProcessStopped", async () => { + await wait(() => + host.call("exists", "g:__test_denops_process_stopped_fired") + ); + }); + + await t.step("changes status to 'stopped' asynchronously", async () => { + assertEquals(await host.call("denops#server#status"), "stopped"); + }); + + await t.step("restart the server", async (t) => { + await t.step("fires DenopsProcessStarted", async () => { + await wait(() => + host.call("exists", "g:__test_denops_process_started_fired") + ); + }); + + await t.step("fires DenopsReady", async () => { + await wait(() => host.call("exists", "g:__test_denops_ready_fired")); + }); + + await t.step("changes status to 'running' asynchronously", async () => { + assertEquals(await host.call("denops#server#status"), "running"); + }); + + await t.step("outputs warning message", () => { + assertMatch( + outputs.join(""), + /Server stopped \(-?[0-9]+\)\. Restarting\.\.\./, + ); + }); + }); + }); + + await t.step("if restart count exceed", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + await host.call("execute", [ + `let g:denops#disabled = 0`, + ], ""); + }); + + await host.call("execute", [ + "silent! unlet g:__test_denops_process_started_fired", + "let g:denops#server#restart_delay = 10", + "let g:denops#server#restart_interval = 30000", + "let g:denops#server#restart_threshold = 3", + "call denops#server#start()", + ], ""); + outputs = []; + + for (let i = 0; i < 4; i++) { + await wait(() => + host.call("exists", "g:__test_denops_process_started_fired") + ); + await host.call("execute", [ + "unlet g:__test_denops_process_started_fired", + ], ""); + await forceShutdownServer(); + } + + await t.step("changes `g:denops#disabled` to truthy", async () => { + await wait(() => host.call("eval", "g:denops#disabled")); + }); + + await t.step("changes status to 'stopped'", async () => { + await wait(() => + host.call("eval", "denops#server#status() ==# 'stopped'") + ); + }); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Server stopped 4 times within 30000 millisec/, + ); + }); }); }, }); From 3c4a84f4061eb679c1932a5d8ec94992aaa0443e Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 29 Jun 2024 13:14:28 +0900 Subject: [PATCH 48/99] :+1: fix reconnect behavior --- autoload/denops/_internal/server/chan.vim | 115 ++++++++++------ autoload/denops/server.vim | 24 +++- doc/denops.txt | 13 +- plugin/denops.vim | 14 +- tests/denops/server_test.ts | 159 +++++++++++++++++++++- 5 files changed, 261 insertions(+), 64 deletions(-) diff --git a/autoload/denops/_internal/server/chan.vim b/autoload/denops/_internal/server/chan.vim index 2a307106..36d8c55e 100644 --- a/autoload/denops/_internal/server/chan.vim +++ b/autoload/denops/_internal/server/chan.vim @@ -12,52 +12,47 @@ let s:exiting = 0 " Args: " addr: string " options: { -" retry_interval: number -" retry_threshold: number " reconnect_on_close: boolean " reconnect_delay: number " reconnect_interval: number " reconnect_threshold: number +" on_connect_failure: funcref " } " Return: -" boolean +" v:true - If the connection is successful immediately. +" 0 - Otherwise, if it fails or waits for reconnection. function! denops#_internal#server#chan#connect(addr, options) abort + call s:clear_reconnect_delayer() if s:chan isnot# v:null throw '[denops] Channel already exists' endif - let l:retry_threshold = a:options.retry_threshold - let l:retry_interval = a:options.retry_interval - let l:i = 1 - while v:true - call denops#_internal#echo#debug(printf( - \ 'Connecting to channel `%s` [%d/%d]', - \ a:addr, - \ l:i, - \ l:retry_threshold, - \)) - try - call s:connect(a:addr, a:options) - return v:true - catch - call denops#_internal#echo#debug(printf( - \ 'Failed to connect channel `%s` [%d/%d]: %s', + try + call s:connect(a:addr, a:options) + return v:true + catch + if s:reconnect_guard(a:options) + call denops#_internal#echo#error(printf( + \ 'Failed to connect channel `%s`: %s', \ a:addr, - \ l:i, - \ l:retry_threshold, \ v:exception, \)) - if l:i >= l:retry_threshold - call denops#_internal#echo#error(printf( - \ 'Failed to connect channel `%s`: %s', - \ a:addr, - \ v:exception, - \)) - return + if a:options->has_key('on_connect_failure') + call a:options.on_connect_failure(a:options) endif - endtry - execute printf('sleep %dm', l:retry_interval) - let l:i += 1 - endwhile + return + endif + call denops#_internal#echo#debug(printf( + \ 'Failed to connect channel `%s` [%d/%d]: %s', + \ a:addr, + \ s:reconnect_count, + \ a:options.reconnect_threshold, + \ v:exception, + \)) + let s:reconnect_delayer = timer_start( + \ a:options.reconnect_delay, + \ { -> denops#_internal#server#chan#connect(a:addr, a:options) }, + \) + endtry endfunction " Args: @@ -65,6 +60,9 @@ endfunction " timeout: number (default: 0) " } function! denops#_internal#server#chan#close(options) abort + if s:clear_reconnect_delayer() + return + endif if s:chan is# v:null throw '[denops] Channel does not exist yet' endif @@ -143,28 +141,61 @@ function! s:on_close(options) abort if s:chan isnot# v:null || !a:options.reconnect_on_close || s:closed_on_purpose || s:exiting return endif - " Reconnect + call s:schedule_reconnect(a:options) +endfunction + +function! s:schedule_reconnect(options) if s:reconnect_guard(a:options) + call denops#_internal#echo#warn(printf( + \ 'Channel closed %d times within %d millisec. Denops is disabled to avoid infinity reconnect loop.', + \ a:options.reconnect_threshold + 1, + \ a:options.reconnect_interval + \)) + let g:denops#disabled = 1 return endif call denops#_internal#echo#warn('Channel closed. Reconnecting...') - call timer_start( + let s:reconnect_delayer = timer_start( \ a:options.reconnect_delay, - \ { -> denops#_internal#server#chan#connect(s:addr, a:options) }, + \ { -> s:reconnect(a:options) }, \) endfunction +function! s:reconnect(options) abort + call denops#_internal#echo#debug(printf( + \ 'Reconnect channel `%s` [%d/%d]', + \ s:addr, + \ s:reconnect_count, + \ a:options.reconnect_threshold, + \)) + try + call s:connect(s:addr, a:options) + catch + call denops#_internal#echo#debug(printf( + \ 'Failed to reconnect channel `%s` [%d/%d]: %s', + \ s:addr, + \ s:reconnect_count, + \ a:options.reconnect_threshold, + \ v:exception, + \)) + call s:schedule_reconnect(a:options) + endtry +endfunction + +function! s:clear_reconnect_delayer() abort + if exists('s:reconnect_delayer') + call timer_stop(s:reconnect_delayer) + unlet s:reconnect_delayer + return v:true + endif +endfunction + function! s:reconnect_guard(options) abort let l:reconnect_threshold = a:options.reconnect_threshold let l:reconnect_interval = a:options.reconnect_interval let s:reconnect_count = get(s:, 'reconnect_count', 0) + 1 - if s:reconnect_count >= l:reconnect_threshold - call denops#_internal#echo#warn(printf( - \ 'Channel closed %d times within %d millisec. Denops is disabled to avoid infinity reconnect loop.', - \ l:reconnect_threshold, - \ l:reconnect_interval, - \)) - let g:denops#disabled = 1 + if s:reconnect_count > l:reconnect_threshold + let s:reconnect_count = 0 return 1 endif if exists('s:reset_reconnect_count_delayer') diff --git a/autoload/denops/server.vim b/autoload/denops/server.vim index 23c09a7c..9483cb3d 100644 --- a/autoload/denops/server.vim +++ b/autoload/denops/server.vim @@ -5,13 +5,16 @@ const s:STATUS_RUNNING = 'running' const s:STATUS_CLOSING = 'closing' const s:STATUS_CLOSED = 'closed' +" Common variables let s:is_ready = v:false let s:ready_callbacks = [] +" Local server variables let s:stopping = v:false let s:restart_once = v:false let s:local_addr = "" +" Shared server variables let s:is_closed = v:false let s:closing = v:false let s:reconnect_once = v:false @@ -118,6 +121,22 @@ function! s:get_server_addr() abort endfunction " Common +function! denops#server#connect_or_start() abort + if g:denops#disabled || denops#server#status() !=# s:STATUS_STOPPED + return + endif + let s:addr = s:get_server_addr() + if !empty(s:addr) + " Connect to a shared server or fallback to a local server. + call s:connect(s:addr, { + \ 'reconnect_on_close': v:true, + \ 'on_connect_failure': { -> denops#server#start() }, + \}) + else + call denops#server#start() + endif +endfunction + function! denops#server#status() abort if s:closing return s:STATUS_CLOSING @@ -176,8 +195,6 @@ endfunction function! s:connect(addr, ...) abort let s:is_closed = v:false let l:options = extend({ - \ 'retry_interval': g:denops#server#retry_interval, - \ 'retry_threshold': g:denops#server#retry_threshold, \ 'reconnect_delay': g:denops#server#reconnect_delay, \ 'reconnect_interval': g:denops#server#reconnect_interval, \ 'reconnect_threshold': g:denops#server#reconnect_threshold, @@ -278,9 +295,6 @@ call denops#_internal#conf#define('denops#server#deno_args', [ \ '-A', \]) -call denops#_internal#conf#define('denops#server#retry_interval', 500) -call denops#_internal#conf#define('denops#server#retry_threshold', 3) - call denops#_internal#conf#define('denops#server#restart_delay', 100) call denops#_internal#conf#define('denops#server#restart_interval', 10000) call denops#_internal#conf#define('denops#server#restart_threshold', 3) diff --git a/doc/denops.txt b/doc/denops.txt index 6052d1c6..877a8360 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -147,12 +147,19 @@ VARIABLE *denops-variable* |g:denops#server#restart_interval|. Default: 3 -*g:denops#server#reconnect_interval* - Interval in milliseconds before retrying connection to the server. +*g:denops#server#reconnect_delay* + The delay in milliseconds before reconnecting to the server. Default: 100 +*g:denops#server#reconnect_interval* + Interval in milliseconds to avoid infinite errors. Denops will reset + internal counter when the channel keeps connecting more than this + interval. + Default: 1000 + *g:denops#server#reconnect_threshold* - The number of reconnect counts on connection failure. + The number of reconnect counts on connection failure within + |g:denops#server#reconnect_interval|. Default: 3 *g:denops#server#close_timeout* diff --git a/plugin/denops.vim b/plugin/denops.vim index 06a9c248..66d10a1b 100644 --- a/plugin/denops.vim +++ b/plugin/denops.vim @@ -25,21 +25,11 @@ augroup denops_plugin_internal autocmd User DenopsReady call denops#plugin#discover() augroup END -function! s:init() abort - if !empty(get(g:, 'denops_server_addr')) - if denops#server#connect() - return - endif - " Fallback to a local denops server - endif - call denops#server#start() -endfunction - if has('vim_starting') augroup denops_plugin_internal_startup autocmd! - autocmd VimEnter * call s:init() + autocmd VimEnter * call denops#server#connect_or_start() augroup END else - call s:init() + call denops#server#connect_or_start() endif diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts index 45de6007..07a75066 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/server_test.ts @@ -638,8 +638,38 @@ testHost({ await host.call("execute", [ "autocmd User DenopsReady let g:__test_denops_ready_fired = 1", + "autocmd User DenopsClosed let g:__test_denops_closed_fired = 1", ], ""); + await t.step("if denops disabled", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { + await host.call("execute", [ + "let g:denops#disabled = 0", + ], ""); + }); + + await host.call("execute", [ + "let g:denops#disabled = 1", + "let g:__test_denops_server_connect_result = denops#server#connect()", + "let g:__test_denops_server_status_when_connect_called = denops#server#status()", + ], ""); + + await t.step("returns falsy", async () => { + assertFalse( + await host.call("eval", "g:__test_denops_server_connect_result"), + ); + }); + + await t.step("does not change status from 'stopped'", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_when_connect_called", + ); + assertEquals(actual, "stopped"); + }); + }); + await t.step("if not yet connected", async (t) => { await t.step("if `g:denops_server_addr` is empty", async (t) => { outputs = []; @@ -684,6 +714,13 @@ testHost({ "Timeout", ); }); + + await t.step( + "does not change `g:denops#disabled` from falsy", + async () => { + assertFalse(await host.call("eval", "g:denops#disabled")); + }, + ); }); await t.step("if `g:denops_server_addr` is invalid", async (t) => { @@ -696,8 +733,9 @@ testHost({ await host.call("execute", [ `let g:denops_server_addr = '${not_exists_address}'`, - "let g:denops#server#retry_interval = 10", - "let g:denops#server#retry_threshold = 1", + "let g:denops#server#reconnect_delay = 10", + "let g:denops#server#reconnect_interval = 30000", + "let g:denops#server#reconnect_threshold = 3", "silent! unlet g:__test_denops_ready_fired", "let g:__test_denops_server_connect_result = denops#server#connect()", "let g:__test_denops_server_status_when_connect_called = denops#server#status()", @@ -736,6 +774,13 @@ testHost({ "Timeout", ); }); + + await t.step( + "does not change `g:denops#disabled` from falsy", + async () => { + assertFalse(await host.call("eval", "g:denops#disabled")); + }, + ); }); await t.step("if `g:denops_server_addr` is valid", async (t) => { @@ -884,6 +929,116 @@ testHost({ ); }); }); + + await t.step("if the channel is closed unexpectedly", async (t) => { + await using stack = new AsyncDisposableStack(); + const server = stack.use(await useSharedServer()); + stack.defer(async () => { + await host.call("denops#server#close"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + }); + + await host.call("execute", [ + `let g:denops_server_addr = '${server.addr}'`, + "let g:denops#server#reconnect_delay = 1000", + "let g:denops#server#reconnect_interval = 1000", + "let g:denops#server#reconnect_threshold = 1", + "call denops#server#connect()", + ], ""); + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + await host.call("execute", [ + "silent! unlet g:__test_denops_closed_fired", + ], ""); + outputs = []; + + // Close the channel by stop the shared-server. + await server[Symbol.asyncDispose](); + + await t.step("fires DenopsClosed", async () => { + await wait(() => host.call("exists", "g:__test_denops_closed_fired")); + }); + + await t.step("changes status to 'stopped' asynchronously", async () => { + assertEquals(await host.call("denops#server#status"), "stopped"); + }); + + await t.step("reconnect to the server", async (t) => { + await host.call("execute", [ + "silent! unlet g:__test_denops_ready_fired", + ], ""); + + // Start the shared-server with the same port number. + stack.use(await useSharedServer({ port: server.addr.port })); + + await t.step("fires DenopsReady", async () => { + await wait(() => host.call("exists", "g:__test_denops_ready_fired")); + }); + + await t.step("changes status to 'running' asynchronously", async () => { + assertEquals(await host.call("denops#server#status"), "running"); + }); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Channel closed\. Reconnecting\.\.\./, + ); + }); + }); + }); + + await t.step("if reconnect count exceed", async (t) => { + await using stack = new AsyncDisposableStack(); + const listener = stack.use( + Deno.listen({ hostname: "127.0.0.1", port: 0 }), + ); + stack.defer(async () => { + await host.call("denops#server#close"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + await host.call("execute", [ + "let g:denops#disabled = 0", + ], ""); + }); + + await host.call("execute", [ + `let g:denops_server_addr = '127.0.0.1:${listener.addr.port}'`, + "let g:denops#server#reconnect_delay = 10", + "let g:denops#server#reconnect_interval = 30000", + "let g:denops#server#reconnect_threshold = 3", + "call denops#server#connect()", + ], ""); + outputs = []; + + // Close the channel from server side. + (async () => { + for await (const conn of listener) { + conn.close(); + } + })(); + + await t.step("changes `g:denops#disabled` to truthy", async () => { + await wait(() => host.call("eval", "g:denops#disabled")); + }); + + await t.step("changes status to 'stopped'", async () => { + assertEquals(await host.call("denops#server#status"), "stopped"); + }); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Channel closed 4 times within 30000 millisec/, + ); + }); + }); }, }); From e5e697b11e6464607a92cc14ed4322bc12202c97 Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 2 Jul 2024 19:08:17 +0900 Subject: [PATCH 49/99] :herb: fix flaky tests a bit --- tests/denops/server_test.ts | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/tests/denops/server_test.ts b/tests/denops/server_test.ts index 07a75066..dd47db28 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/server_test.ts @@ -360,7 +360,7 @@ testHost({ assertEquals(actual, "stopped"); }); - await t.step("outputs warning message", async () => { + await t.step("outputs warning message after delay", async () => { await delay(MESSAGE_DELAY); assertMatch( outputs.join(""), @@ -430,7 +430,8 @@ testHost({ assertEquals(await host.call("denops#server#status"), "running"); }); - await t.step("outputs warning message", () => { + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); assertMatch( outputs.join(""), /Server stopped \(-?[0-9]+\)\. Restarting\.\.\./, @@ -695,14 +696,6 @@ testHost({ assertEquals(actual, "stopped"); }); - await t.step("outputs error message after delayed", async () => { - await delay(MESSAGE_DELAY); - assertMatch( - outputs.join(""), - /denops shared server address \(g:denops_server_addr\) is not given/, - ); - }); - await t.step("does not fire DenopsReady", async () => { await assertRejects( () => @@ -721,6 +714,14 @@ testHost({ assertFalse(await host.call("eval", "g:denops#disabled")); }, ); + + await t.step("outputs error message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /denops shared server address \(g:denops_server_addr\) is not given/, + ); + }); }); await t.step("if `g:denops_server_addr` is invalid", async (t) => { @@ -755,14 +756,6 @@ testHost({ assertEquals(actual, "stopped"); }); - await t.step("outputs warning message after delayed", async () => { - await delay(MESSAGE_DELAY); - assertMatch( - outputs.join(""), - /Failed to connect channel `127\.0\.0\.1:[0-9]+`:/, - ); - }); - await t.step("does not fire DenopsReady", async () => { await assertRejects( () => @@ -781,6 +774,14 @@ testHost({ assertFalse(await host.call("eval", "g:denops#disabled")); }, ); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to connect channel `127\.0\.0\.1:[0-9]+`:/, + ); + }); }); await t.step("if `g:denops_server_addr` is valid", async (t) => { From a876bb8c81453ae1949800d133ed5ce067acc4d7 Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 3 Jul 2024 10:40:58 +0900 Subject: [PATCH 50/99] :herb: improve plugin/denops.vim tests --- tests/denops/plugin_test.ts | 350 +++++++++++++++++++++++++++++++----- 1 file changed, 301 insertions(+), 49 deletions(-) diff --git a/tests/denops/plugin_test.ts b/tests/denops/plugin_test.ts index 9b88228d..730cea58 100644 --- a/tests/denops/plugin_test.ts +++ b/tests/denops/plugin_test.ts @@ -1,53 +1,305 @@ -import { assertMatch } from "jsr:@std/assert@0.225.2"; -import { - testHost, - withHost, -} from "../../denops/@denops-private/testutil/host.ts"; +import { assertEquals, assertMatch } from "jsr:@std/assert@0.225.2"; +import { delay } from "jsr:@std/async@^0.224.0/delay"; +import { withHost } from "../../denops/@denops-private/testutil/host.ts"; import { useSharedServer } from "../../denops/@denops-private/testutil/shared_server.ts"; import { wait } from "../../denops/@denops-private/testutil/wait.ts"; -testHost({ - name: - "'plugin/denops.vim' starts a local server when sourced before VimEnter", - postlude: [ - "runtime plugin/denops.vim", - ], - fn: async ({ host }) => { - await wait(() => host.call("eval", "!has('vim_starting')")); - const actual = await host.call("denops#server#status") as string; - assertMatch(actual, /^(starting|preparing|running)$/); - }, -}); - -testHost({ - name: "'plugin/denops.vim' starts a local server when sourced after VimEnter", - fn: async ({ host }) => { - await wait(() => host.call("eval", "!has('vim_starting')")); - await host.call("execute", [ - "runtime plugin/denops.vim", - ], ""); - const actual = await host.call("denops#server#status") as string; - assertMatch(actual, /^(starting|preparing|running)$/); - }, -}); - -for (const mode of ["vim", "nvim"] as const) { - Deno.test( - `'plugin/denops.vim' connects to the shared server when sourced before VimEnter (${mode})`, - async () => { - await using server = await useSharedServer(); - await withHost({ - mode, - postlude: [ - `let g:denops_server_addr = '${server.addr}'`, - "runtime plugin/denops.vim", - ], - fn: async ({ host }) => { - await wait(() => host.call("eval", "!has('vim_starting')")); - const actual = await host.call("denops#server#status") as string; - assertMatch(actual, /^(preparing|running)$/); - }, - }); - }, - ); +const MESSAGE_DELAY = 200; +const MODES = ["vim", "nvim"] as const; + +for (const mode of MODES) { + Deno.test(`plugin/denops.vim (${mode})`, async (t) => { + await t.step("if sourced before VimEnter", async (t) => { + await t.step("if `g:denops_server_addr` is not specified", async (t) => { + await withHost({ + mode, + postlude: [ + "let g:__test_denops_events = []", + "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", + "autocmd User DenopsReady call add(g:__test_denops_events, 'DenopsReady')", + // Test target + "runtime plugin/denops.vim", + "let g:__test_denops_server_status_before_vimenter = denops#server#status()", + ], + fn: async ({ host }) => { + await t.step( + "does not start a local server before VimEnter", + async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_before_vimenter", + ); + assertEquals(actual, "stopped"); + }, + ); + + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("starts a local server", async () => { + assertEquals( + await host.call("eval", "g:__test_denops_events"), + ["DenopsProcessStarted", "DenopsReady"], + ); + }); + }, + }); + }); + + await t.step("if `g:denops_server_addr` is empty", async (t) => { + await withHost({ + mode, + postlude: [ + "let g:__test_denops_events = []", + "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", + "autocmd User DenopsReady call add(g:__test_denops_events, 'DenopsReady')", + // Test target + "let g:denops_server_addr = ''", + "runtime plugin/denops.vim", + "let g:__test_denops_server_status_before_vimenter = denops#server#status()", + ], + fn: async ({ host }) => { + await t.step( + "does not start a local server before VimEnter", + async () => { + const actual = await host.call( + "eval", + "g:__test_denops_server_status_before_vimenter", + ); + assertEquals(actual, "stopped"); + }, + ); + + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("starts a local server", async () => { + assertEquals( + await host.call("eval", "g:__test_denops_events"), + ["DenopsProcessStarted", "DenopsReady"], + ); + }); + }, + }); + }); + + await t.step("if `g:denops_server_addr` is valid", async (t) => { + await using server = await useSharedServer(); + + await withHost({ + mode, + postlude: [ + "let g:__test_denops_events = []", + "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", + "autocmd User DenopsReady call add(g:__test_denops_events, 'DenopsReady')", + // Test target + `let g:denops_server_addr = '${server.addr}'`, + "runtime plugin/denops.vim", + ], + fn: async ({ host }) => { + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("connects to the shared server", async () => { + assertEquals( + await host.call("eval", "g:__test_denops_events"), + ["DenopsReady"], + ); + }); + }, + }); + }); + + await t.step("if `g:denops_server_addr` is invalid", async (t) => { + // NOTE: Get a non-existent address. + const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 }); + const not_exists_address = `127.0.0.1:${listener.addr.port}`; + listener.close(); + + await withHost({ + mode, + postlude: [ + "let g:__test_denops_events = []", + "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", + "autocmd User DenopsReady call add(g:__test_denops_events, 'DenopsReady')", + // Test target + `let g:denops_server_addr = '${not_exists_address}'`, + "runtime plugin/denops.vim", + ], + fn: async ({ host, stderr }) => { + const outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); + + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("starts a local server", async () => { + assertEquals( + await host.call("eval", "g:__test_denops_events"), + ["DenopsProcessStarted", "DenopsReady"], + ); + }); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to connect channel `127\.0\.0\.1:[0-9]+`:/, + ); + }); + }, + }); + }); + }); + + await t.step("if sourced after VimEnter", async (t) => { + await t.step("if `g:denops_server_addr` is not specified", async (t) => { + await withHost({ + mode, + fn: async ({ host }) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", + "autocmd User DenopsReady call add(g:__test_denops_events, 'DenopsReady')", + ], ""); + await wait(() => host.call("eval", "!has('vim_starting')")); + + // Test target + await host.call("execute", [ + "runtime plugin/denops.vim", + ], ""); + + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("starts a local server", async () => { + assertEquals( + await host.call("eval", "g:__test_denops_events"), + ["DenopsProcessStarted", "DenopsReady"], + ); + }); + }, + }); + }); + + await t.step("if `g:denops_server_addr` is empty", async (t) => { + await withHost({ + mode, + fn: async ({ host }) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", + "autocmd User DenopsReady call add(g:__test_denops_events, 'DenopsReady')", + ], ""); + await wait(() => host.call("eval", "!has('vim_starting')")); + + // Test target + await host.call("execute", [ + "let g:denops_server_addr = ''", + "runtime plugin/denops.vim", + ], ""); + + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("starts a local server", async () => { + assertEquals( + await host.call("eval", "g:__test_denops_events"), + ["DenopsProcessStarted", "DenopsReady"], + ); + }); + }, + }); + }); + + await t.step("if `g:denops_server_addr` is valid", async (t) => { + await using server = await useSharedServer(); + + await withHost({ + mode, + fn: async ({ host }) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", + "autocmd User DenopsReady call add(g:__test_denops_events, 'DenopsReady')", + ], ""); + await wait(() => host.call("eval", "!has('vim_starting')")); + + // Test target + await host.call("execute", [ + `let g:denops_server_addr = '${server.addr}'`, + "runtime plugin/denops.vim", + ], ""); + + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("connects to the shared server", async () => { + assertEquals( + await host.call("eval", "g:__test_denops_events"), + ["DenopsReady"], + ); + }); + }, + }); + }); + + await t.step("if `g:denops_server_addr` is invalid", async (t) => { + // NOTE: Get a non-existent address. + const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 }); + const not_exists_address = `127.0.0.1:${listener.addr.port}`; + listener.close(); + + await withHost({ + mode, + fn: async ({ host, stderr }) => { + const outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); + await host.call("execute", [ + "let g:__test_denops_events = []", + "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", + "autocmd User DenopsReady call add(g:__test_denops_events, 'DenopsReady')", + ], ""); + await wait(() => host.call("eval", "!has('vim_starting')")); + + // Test target + await host.call("execute", [ + `let g:denops_server_addr = '${not_exists_address}'`, + "runtime plugin/denops.vim", + ], ""); + + await wait( + () => host.call("eval", "denops#server#status() ==# 'running'"), + ); + + await t.step("starts a local server", async () => { + assertEquals( + await host.call("eval", "g:__test_denops_events"), + ["DenopsProcessStarted", "DenopsReady"], + ); + }); + + await t.step("outputs warning message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to connect channel `127\.0\.0\.1:[0-9]+`:/, + ); + }); + }, + }); + }); + }); + }); } From f4bec25308049889526e1cab0e8315961d10c494 Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 3 Jul 2024 10:49:35 +0900 Subject: [PATCH 51/99] :bug: no error about `g:denops_server_addr` missing on startup --- autoload/denops/server.vim | 2 +- tests/denops/plugin_test.ts | 68 +++++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/autoload/denops/server.vim b/autoload/denops/server.vim index 9483cb3d..7c768e26 100644 --- a/autoload/denops/server.vim +++ b/autoload/denops/server.vim @@ -125,7 +125,7 @@ function! denops#server#connect_or_start() abort if g:denops#disabled || denops#server#status() !=# s:STATUS_STOPPED return endif - let s:addr = s:get_server_addr() + let s:addr = get(g:, 'denops_server_addr') if !empty(s:addr) " Connect to a shared server or fallback to a local server. call s:connect(s:addr, { diff --git a/tests/denops/plugin_test.ts b/tests/denops/plugin_test.ts index 730cea58..c7467e26 100644 --- a/tests/denops/plugin_test.ts +++ b/tests/denops/plugin_test.ts @@ -5,7 +5,7 @@ import { useSharedServer } from "../../denops/@denops-private/testutil/shared_se import { wait } from "../../denops/@denops-private/testutil/wait.ts"; const MESSAGE_DELAY = 200; -const MODES = ["vim", "nvim"] as const; +const MODES = ["vim"] as const; for (const mode of MODES) { Deno.test(`plugin/denops.vim (${mode})`, async (t) => { @@ -21,7 +21,11 @@ for (const mode of MODES) { "runtime plugin/denops.vim", "let g:__test_denops_server_status_before_vimenter = denops#server#status()", ], - fn: async ({ host }) => { + fn: async ({ host, stderr }) => { + const outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); await t.step( "does not start a local server before VimEnter", async () => { @@ -43,6 +47,11 @@ for (const mode of MODES) { ["DenopsProcessStarted", "DenopsReady"], ); }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); }, }); }); @@ -59,7 +68,11 @@ for (const mode of MODES) { "runtime plugin/denops.vim", "let g:__test_denops_server_status_before_vimenter = denops#server#status()", ], - fn: async ({ host }) => { + fn: async ({ host, stderr }) => { + const outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); await t.step( "does not start a local server before VimEnter", async () => { @@ -81,6 +94,11 @@ for (const mode of MODES) { ["DenopsProcessStarted", "DenopsReady"], ); }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); }, }); }); @@ -98,7 +116,11 @@ for (const mode of MODES) { `let g:denops_server_addr = '${server.addr}'`, "runtime plugin/denops.vim", ], - fn: async ({ host }) => { + fn: async ({ host, stderr }) => { + const outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); await wait( () => host.call("eval", "denops#server#status() ==# 'running'"), ); @@ -109,6 +131,11 @@ for (const mode of MODES) { ["DenopsReady"], ); }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); }, }); }); @@ -162,7 +189,11 @@ for (const mode of MODES) { await t.step("if `g:denops_server_addr` is not specified", async (t) => { await withHost({ mode, - fn: async ({ host }) => { + fn: async ({ host, stderr }) => { + const outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); await host.call("execute", [ "let g:__test_denops_events = []", "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", @@ -185,6 +216,11 @@ for (const mode of MODES) { ["DenopsProcessStarted", "DenopsReady"], ); }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); }, }); }); @@ -192,7 +228,11 @@ for (const mode of MODES) { await t.step("if `g:denops_server_addr` is empty", async (t) => { await withHost({ mode, - fn: async ({ host }) => { + fn: async ({ host, stderr }) => { + const outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); await host.call("execute", [ "let g:__test_denops_events = []", "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", @@ -216,6 +256,11 @@ for (const mode of MODES) { ["DenopsProcessStarted", "DenopsReady"], ); }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); }, }); }); @@ -225,7 +270,11 @@ for (const mode of MODES) { await withHost({ mode, - fn: async ({ host }) => { + fn: async ({ host, stderr }) => { + const outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); await host.call("execute", [ "let g:__test_denops_events = []", "autocmd User DenopsProcessStarted call add(g:__test_denops_events, 'DenopsProcessStarted')", @@ -249,6 +298,11 @@ for (const mode of MODES) { ["DenopsReady"], ); }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); }, }); }); From 6a7f4c851b1234813f3b66aa5be021e0ca0db0be Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 3 Jul 2024 11:52:35 +0900 Subject: [PATCH 52/99] :herb: add nvim to test target --- tests/denops/plugin_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/denops/plugin_test.ts b/tests/denops/plugin_test.ts index c7467e26..63217a65 100644 --- a/tests/denops/plugin_test.ts +++ b/tests/denops/plugin_test.ts @@ -5,7 +5,7 @@ import { useSharedServer } from "../../denops/@denops-private/testutil/shared_se import { wait } from "../../denops/@denops-private/testutil/wait.ts"; const MESSAGE_DELAY = 200; -const MODES = ["vim"] as const; +const MODES = ["vim", "nvim"] as const; for (const mode of MODES) { Deno.test(`plugin/denops.vim (${mode})`, async (t) => { From 7822e5d6f2b91f79fe61bf5433c74b7ea7ebfab0 Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 3 Jul 2024 15:21:41 +0900 Subject: [PATCH 53/99] :herb: improve `Service` tests --- denops/@denops-private/service_test.ts | 1298 +++++++++++++++--------- 1 file changed, 802 insertions(+), 496 deletions(-) diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 3f38e492..b32382f0 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -1,13 +1,18 @@ import { assert, assertEquals, + assertFalse, + assertInstanceOf, assertMatch, + assertNotStrictEquals, assertRejects, + assertStrictEquals, assertThrows, } from "jsr:@std/assert@0.225.1"; import { assertSpyCall, assertSpyCalls, + resolvesNext, stub, } from "jsr:@std/testing@0.224.0/mock"; import type { Meta } from "jsr:@denops/core@6.0.6"; @@ -16,16 +21,16 @@ import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import type { Host } from "./denops.ts"; import { Service } from "./service.ts"; -const scriptValid = - new URL("./testdata/dummy_valid_plugin.ts", import.meta.url).href; -const scriptInvalid = - new URL("./testdata/dummy_invalid_plugin.ts", import.meta.url).href; -const scriptInvalidConstraint = - new URL("./testdata/dummy_invalid_constraint_plugin.ts", import.meta.url) - .href; -const scriptInvalidConstraint2 = - new URL("./testdata/dummy_invalid_constraint_plugin2.ts", import.meta.url) - .href; +const NOOP = () => {}; + +const scriptValid = resolve("./testdata/dummy_valid_plugin.ts"); +const scriptInvalid = resolve("./testdata/dummy_invalid_plugin.ts"); +const scriptInvalidConstraint = resolve( + "./testdata/dummy_invalid_constraint_plugin.ts", +); +const scriptInvalidConstraint2 = resolve( + "./testdata/dummy_invalid_constraint_plugin2.ts", +); Deno.test("Service", async (t) => { const meta: Meta = { @@ -39,546 +44,847 @@ Deno.test("Service", async (t) => { call: () => unimplemented(), batch: () => unimplemented(), }; - const service = new Service(meta); - await t.step("load() rejects an error when no host is bound", async () => { - await assertRejects( - () => service.load("dummy", scriptValid), - Error, - "No host is bound to the service", - ); + await t.step("new Service()", async (t) => { + await t.step("creates an instance", () => { + const actual = new Service(meta); + + assertInstanceOf(actual, Service); + }); }); - await t.step( - "dispatchAsync() rejects when no host is bound", - async () => { - await assertRejects( - () => - service.dispatchAsync( - "dummy", - "test", - ["foo"], - "success", - "failure", - ), - Error, - "No host is bound to the service", - ); - }, - ); - - service.bind(host); - - const waitLoaded = service.waitLoaded("dummy"); - - await t.step( - "the result promise of waitLoaded() is 'pending' when the plugin is not loaded yet", - async () => { - assertEquals(await promiseState(waitLoaded), "pending"); - }, - ); - - await t.step("load() loads plugin and emits autocmd events", async () => { - using s = stub(host, "call"); - await service.load("dummy", scriptValid); - assertSpyCalls(s, 3); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#api#cmd", - "echo 'Hello, Denops!'", - {}, - ], - }); - assertSpyCall(s, 2, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, - ], + await t.step(".bind()", async (t) => { + await t.step("binds the host", () => { + const service = new Service(meta); + + service.bind(host); }); }); - await t.step( - "the result promise of waitLoaded() become 'fulfilled' when the plugin is loaded", - async () => { - assertEquals(await promiseState(waitLoaded), "fulfilled"); - }, - ); - - await t.step( - "load() loads plugin and emits autocmd events (failure)", - async () => { - using c = stub(console, "error"); - using s = stub(host, "call"); - await service.load("dummyFail", scriptInvalid); - assertSpyCalls(c, 1); - assertSpyCall(c, 0, { - args: [ - "Failed to load plugin 'dummyFail': Error: This is dummy error", - ], - }); - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummyFail", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummyFail", - {}, - ], - }); - }, - ); - - await t.step( - "load() loads plugin and emits autocmd events (could not find constraint)", - async () => { - using c = stub(console, "warn"); - using s = stub(host, "call"); - await service.load("dummyFailConstraint", scriptInvalidConstraint); - const expects = [ - "********************************************************************************", - "Deno module cache issue is detected.", - "Execute 'call denops#cache#update(#{reload: v:true})' and restart Vim/Neovim.", - "See https://github.com/vim-denops/denops.vim/issues/358 for more detail.", - "********************************************************************************", - ]; - assertSpyCalls(c, expects.length); - for (let i = 0; i < expects.length; i++) { - assertSpyCall(c, i, { - args: [expects[i]], + await t.step(".load()", async (t) => { + await t.step("if no host is bound", async (t) => { + const service = new Service(meta); + + await t.step("rejects", async () => { + await assertRejects( + () => service.load("dummy", scriptValid), + Error, + "No host is bound to the service", + ); + }); + }); + + await t.step("if the service is already closed", async (t) => { + const service = new Service(meta); + service.bind(host); + await service.close(); + using host_call = stub(host, "call"); + + await t.step("rejects", async () => { + await assertRejects( + () => service.load("dummy", scriptValid), + Error, + "Service closed", + ); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); + + await t.step("if the plugin is valid", async (t) => { + const service = new Service(meta); + service.bind(host); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.load("dummy", scriptValid); + }); + + await t.step("emits DenopsSystemPluginPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + }); + }); + + await t.step("calls the plugin entrypoint", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginPost", () => { + assertSpyCall(host_call, 2, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], }); + }); + }); + + await t.step("if the plugin entrypoint throws", async (t) => { + const service = new Service(meta); + service.bind(host); + using console_error = stub(console, "error"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.load("dummy", scriptInvalid); + }); + + await t.step("outputs an error message", () => { + assertSpyCall(console_error, 0, { + args: [ + "Failed to load plugin 'dummy': Error: This is dummy error", + ], + }); + }); + + await t.step("emits DenopsSystemPluginPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginFail", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginFail:dummy", + {}, + ], + }); + }); + }); + + await t.step("if the plugin constraints could not find", async (t) => { + const service = new Service(meta); + service.bind(host); + using console_error = stub(console, "error"); + using console_warn = stub(console, "warn"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.load("dummy", scriptInvalidConstraint); + }); + + await t.step("outputs an error message", () => { + assertMatch( + console_error.calls[0].args[0], + /^Failed to load plugin 'dummy': TypeError: Could not find constraint in the list of versions:/, + ); + }); + + await t.step("outputs warning messages", () => { + assertEquals( + console_warn.calls.flatMap((c) => c.args), + [ + "********************************************************************************", + "Deno module cache issue is detected.", + "Execute 'call denops#cache#update(#{reload: v:true})' and restart Vim/Neovim.", + "See https://github.com/vim-denops/denops.vim/issues/358 for more detail.", + "********************************************************************************", + ], + ); + }); + + await t.step("emits DenopsSystemPluginPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginFail", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginFail:dummy", + {}, + ], + }); + }); + }); + + await t.step( + "if the plugin constraint versions could not find", + async (t) => { + const service = new Service(meta); + service.bind(host); + using console_error = stub(console, "error"); + using console_warn = stub(console, "warn"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.load("dummy", scriptInvalidConstraint2); + }); + + await t.step("outputs an error message", () => { + assertMatch( + console_error.calls[0].args[0], + /^Failed to load plugin 'dummy': TypeError: Could not find version of /, + ); + }); + + await t.step("outputs warning messages", () => { + assertEquals( + console_warn.calls.flatMap((c) => c.args), + [ + "********************************************************************************", + "Deno module cache issue is detected.", + "Execute 'call denops#cache#update(#{reload: v:true})' and restart Vim/Neovim.", + "See https://github.com/vim-denops/denops.vim/issues/358 for more detail.", + "********************************************************************************", + ], + ); + }); + + await t.step("emits DenopsSystemPluginPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginFail", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginFail:dummy", + {}, + ], + }); + }); + }, + ); + + await t.step("if the plugin is already loaded", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); } - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummyFailConstraint", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummyFailConstraint", - {}, - ], - }); - }, - ); - - await t.step( - "load() loads plugin and emits autocmd events (could not find version)", - async () => { - using c = stub(console, "warn"); - using s = stub(host, "call"); - await service.load("dummyFailConstraint2", scriptInvalidConstraint2); - const expects = [ - "********************************************************************************", - "Deno module cache issue is detected.", - "Execute 'call denops#cache#update(#{reload: v:true})' and restart Vim/Neovim.", - "See https://github.com/vim-denops/denops.vim/issues/358 for more detail.", - "********************************************************************************", - ]; - assertSpyCalls(c, expects.length); - for (let i = 0; i < expects.length; i++) { - assertSpyCall(c, i, { - args: [expects[i]], + using console_log = stub(console, "log"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.load("dummy", scriptValid); + }); + + await t.step("outputs a log message", () => { + assertSpyCall(console_log, 0, { + args: [ + "A denops plugin 'dummy' is already loaded. Skip", + ], }); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); + }); + + await t.step(".reload()", async (t) => { + await t.step("if the plugin is already loaded", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); } - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummyFailConstraint2", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummyFailConstraint2", - {}, - ], - }); - }, - ); - - await t.step( - "load() does nothing when the plugin is already loaded", - async () => { - using s1 = stub(host, "call"); - using s2 = stub(console, "log"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.reload("dummy"); + }); + + await t.step("emits DenopsSystemPluginPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + }); + }); + + await t.step("calls the plugin entrypoint", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginPost", () => { + assertSpyCall(host_call, 2, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + }); + }); + }); + + await t.step("if the plugin is not yet loaded", async (t) => { + const service = new Service(meta); + service.bind(host); + using console_log = stub(console, "log"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.reload("dummy"); + }); + + await t.step("outputs a log message", () => { + assertSpyCall(console_log, 0, { + args: [ + "A denops plugin 'dummy' is not loaded yet. Skip", + ], + }); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); + }); + + await t.step(".waitLoaded()", async (t) => { + await t.step("pendings if the plugin is not yet loaded", async () => { + const service = new Service(meta); + service.bind(host); + using _host_call = stub(host, "call"); + + const actual = service.waitLoaded("dummy"); + + assertEquals(await promiseState(actual), "pending"); + }); + + await t.step("resolves if the plugin is already loaded", async () => { + const service = new Service(meta); + service.bind(host); + using _host_call = stub(host, "call"); await service.load("dummy", scriptValid); - assertSpyCalls(s1, 0); - assertSpyCalls(s2, 1); - assertSpyCall(s2, 0, { - args: [ - "A denops plugin 'dummy' is already loaded. Skip", - ], - }); - }, - ); - - await t.step( - "load() does nothing when the plugin is already loaded", - async () => { - using s1 = stub(host, "call"); - using s2 = stub(console, "log"); + + const actual = service.waitLoaded("dummy"); + + assertEquals(await promiseState(actual), "fulfilled"); + }); + + await t.step("resolves when the plugin is loaded", async () => { + const service = new Service(meta); + service.bind(host); + using _host_call = stub(host, "call"); + + const actual = service.waitLoaded("dummy"); await service.load("dummy", scriptValid); - assertSpyCalls(s1, 0); - assertSpyCalls(s2, 1); - assertSpyCall(s2, 0, { - args: [ - "A denops plugin 'dummy' is already loaded. Skip", - ], - }); - }, - ); - - await t.step("reload() reloads plugin and emits autocmd events", async () => { - using s = stub(host, "call"); - await service.reload("dummy"); - assertSpyCalls(s, 3); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#api#cmd", - "echo 'Hello, Denops!'", - {}, - ], - }); - assertSpyCall(s, 2, { - args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, - ], + + assertEquals(await promiseState(actual), "fulfilled"); + }); + + await t.step("rejects if the service is already closed", async () => { + const service = new Service(meta); + service.bind(host); + using _host_call = stub(host, "call"); + await service.close(); + + const actual = service.waitLoaded("dummy"); + actual.catch(NOOP); + + assertEquals(await promiseState(actual), "rejected"); + await assertRejects( + () => actual, + Error, + "Service closed", + ); + }); + + await t.step("rejects when the service is closed", async () => { + const service = new Service(meta); + service.bind(host); + using _host_call = stub(host, "call"); + + const actual = service.waitLoaded("dummy"); + await service.close(); + + assertEquals(await promiseState(actual), "rejected"); + await assertRejects( + () => actual, + Error, + "Service closed", + ); }); }); - await t.step( - "reload() does nothing when the plugin is not loaded yet", - async () => { - using s1 = stub(host, "call"); - using s2 = stub(console, "log"); - await service.reload("pluginthatisnotloaded"); - assertSpyCalls(s1, 0); - assertSpyCalls(s2, 1); - assertSpyCall(s2, 0, { - args: [ - "A denops plugin 'pluginthatisnotloaded' is not loaded yet. Skip", - ], - }); - }, - ); - - await t.step("dispatch() call API of a plugin", async () => { - using s = stub(host, "call"); - await service.dispatch("dummy", "test", ["foo"]); - assertSpyCalls(s, 1); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], + await t.step(".interrupt()", async (t) => { + await t.step("sends signal to `interrupted` attribute", () => { + const service = new Service(meta); + const signal = service.interrupted; + + service.interrupt(); + + assertThrows(() => signal.throwIfAborted()); + }); + + await t.step("sends signal to `interrupted` attribute with reason", () => { + const service = new Service(meta); + const signal = service.interrupted; + + service.interrupt("test"); + + assertThrows(() => signal.throwIfAborted(), "test"); }); }); - await t.step( - "dispatch() rejects when the plugin is not loaded yet", - async () => { - const err = await assertRejects( - () => service.dispatch("pluginthatisnotloaded", "test", ["foo"]), - ); - assert(typeof err === "string"); - assertMatch(err, /No plugin 'pluginthatisnotloaded' is loaded/); - }, - ); - - await t.step( - "dispatch() rejects when failed to call plugin API", - async () => { - using s = stub( + await t.step(".interrupted property", async (t) => { + await t.step("does not aborted before .interrupt() is called", () => { + const service = new Service(meta); + + assertFalse(service.interrupted.aborted); + }); + + await t.step("does not aborted after .interrupt() is called", () => { + const service = new Service(meta); + service.interrupt(); + + assertFalse(service.interrupted.aborted); + }); + + await t.step("aborts when .interrupt() is called", () => { + const service = new Service(meta); + const signal = service.interrupted; + + service.interrupt(); + + assert(signal.aborted); + }); + + await t.step("returns same instance if .interrupt() is not called", () => { + const service = new Service(meta); + + const a = service.interrupted; + const b = service.interrupted; + + assertStrictEquals(a, b); + }); + + await t.step("returns new instance after .interrupt() is called", () => { + const service = new Service(meta); + + const a = service.interrupted; + service.interrupt(); + const b = service.interrupted; + + assertNotStrictEquals(a, b); + }); + }); + + await t.step(".dispatch()", async (t) => { + await t.step("if the plugin is already loaded", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.dispatch("dummy", "test", ["foo"]); + }); + + await t.step("calls the plugin API", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + `echo 'This is test call: ["foo"]'`, + {}, + ], + }); + }); + }); + + await t.step("if the plugin is not yet loaded", async (t) => { + const service = new Service(meta); + service.bind(host); + using host_call = stub(host, "call"); + + await t.step("rejects with string", async () => { + const err = await assertRejects( + () => service.dispatch("dummy", "test", ["foo"]), + ); + assert(typeof err === "string"); + assertMatch(err, /No plugin 'dummy' is loaded/); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); + + await t.step("if the plugin API call fails", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using host_call = stub( host, "call", - () => Promise.reject(new Error("invalid call")), - ); - const err = await assertRejects( - () => service.dispatch("dummy", "test", ["foo"]), + resolvesNext([ + // The plugin API call + new Error("invalid call"), + ]), ); - assertSpyCalls(s, 1); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assert(typeof err === "string"); - assertMatch(err, /invalid call/); - }, - ); - - await t.step( - "dispatchAsync() call success callback when API call is succeeded", - async () => { - using s = stub(host, "call"); - await service.dispatchAsync( - "dummy", - "test", - ["foo"], - "success", - "failure", - ); - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#callback#call", + + await t.step("rejects with string", async () => { + const err = await assertRejects( + () => service.dispatch("dummy", "test", ["foo"]), + ); + assert(typeof err === "string"); + assertMatch(err, /invalid call/); + }); + + await t.step("calls the plugin API", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + `echo 'This is test call: ["foo"]'`, + {}, + ], + }); + }); + }); + }); + + await t.step(".dispatchAsync()", async (t) => { + await t.step("if no host is bound", async (t) => { + const service = new Service(meta); + + await t.step("rejects", async () => { + await assertRejects( + () => + service.dispatchAsync( + "dummy", + "test", + ["foo"], + "success", + "failure", + ), + Error, + "No host is bound to the service", + ); + }); + }); + + await t.step("if the plugin API calls succeeded", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.dispatchAsync( + "dummy", + "test", + ["foo"], "success", - undefined, - ], + "failure", + ); + }); + + await t.step("calls the plugin API", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + `echo 'This is test call: ["foo"]'`, + {}, + ], + }); }); - }, - ); - await t.step( - "dispatchAsync() call failure callback when API call is failed", - async () => { - using s = stub( + await t.step("calls 'success' callback", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#callback#call", + "success", + undefined, + ], + }); + }); + }); + + await t.step("if the plugin API calls failed", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using host_call = stub( host, "call", - (method) => - method === "denops#api#cmd" - ? Promise.reject(new Error("invalid call")) - : Promise.resolve(), - ); - await service.dispatchAsync( - "dummy", - "test", - ["foo"], - "success", - "failure", + resolvesNext([ + // The plugin API call + new Error("invalid call"), + // 'success' callback call + undefined, + ]), ); - assertSpyCalls(s, 2); - assertSpyCall(s, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assertSpyCall(s, 1, { - args: [ - "denops#callback#call", + + await t.step("resolves", async () => { + await service.dispatchAsync( + "dummy", + "test", + ["foo"], + "success", "failure", - s.calls[1].args[2], - ], + ); + }); + + await t.step("calls the plugin API", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + `echo 'This is test call: ["foo"]'`, + {}, + ], + }); + }); + + await t.step("calls 'failure' callback", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#callback#call", + "failure", + host_call.calls[1].args[2], + ], + }); }); - }, - ); + }); - await t.step( - "dispatchAsync() call success callback when API call is succeeded (but fail)", - async () => { - using s1 = stub( + await t.step("if 'success' callback calls failed", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using console_error = stub(console, "error"); + using host_call = stub( host, "call", - (method) => - method !== "denops#api#cmd" - ? Promise.reject(new Error("invalid call")) - : Promise.resolve(), - ); - using s2 = stub(console, "error"); - await service.dispatchAsync( - "dummy", - "test", - ["foo"], - "success", - "failure", + resolvesNext([ + // The plugin API call + undefined, + // 'success' callback call + new Error("invalid call"), + ]), ); - assertSpyCalls(s1, 2); - assertSpyCall(s1, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assertSpyCall(s1, 1, { - args: [ - "denops#callback#call", + + await t.step("resolves", async () => { + await service.dispatchAsync( + "dummy", + "test", + ["foo"], "success", - undefined, - ], + "failure", + ); + }); + + await t.step("calls the plugin API", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + `echo 'This is test call: ["foo"]'`, + {}, + ], + }); }); - assertSpyCalls(s2, 1); - assertSpyCall(s2, 0, { - args: [ - "Failed to call success callback 'success': Error: invalid call", - ], + + await t.step("calls 'success' callback", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#callback#call", + "success", + undefined, + ], + }); }); - }, - ); - await t.step( - "dispatchAsync() call failure callback when API call is failed (but fail)", - async () => { - using s1 = stub( + await t.step("outputs an error message", () => { + assertSpyCall(console_error, 0, { + args: [ + "Failed to call success callback 'success': Error: invalid call", + ], + }); + }); + }); + + await t.step("if 'failure' callback calls failed", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using console_error = stub(console, "error"); + using host_call = stub( host, "call", - () => Promise.reject(new Error("invalid call")), + resolvesNext([ + // The plugin API call + new Error("invalid call"), + // 'failure' callback call + new Error("invalid call"), + ]), ); - using s2 = stub(console, "error"); - await service.dispatchAsync( - "dummy", - "test", - ["foo"], - "success", - "failure", - ); - assertSpyCalls(s1, 2); - assertSpyCall(s1, 0, { - args: [ - "denops#api#cmd", - "echo 'This is test call: [\"foo\"]'", - {}, - ], - }); - assertSpyCall(s1, 1, { - args: [ - "denops#callback#call", + + await t.step("resolves", async () => { + await service.dispatchAsync( + "dummy", + "test", + ["foo"], + "success", "failure", - s1.calls[1].args[2], - ], + ); }); - assertSpyCalls(s2, 1); - assertSpyCall(s2, 0, { - args: [ - "Failed to call failure callback 'failure': Error: invalid call", - ], - }); - }, - ); - await t.step( - "interrupt() sends interrupt signal to `interrupted` attribute", - () => { - const signal = service.interrupted; - signal.throwIfAborted(); // Should not throw - service.interrupt(); - assertThrows(() => signal.throwIfAborted()); - }, - ); + await t.step("calls the plugin API", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + `echo 'This is test call: ["foo"]'`, + {}, + ], + }); + }); - await t.step( - "interrupt() sends interrupt signal to `interrupted` attribute with reason", - () => { - const signal = service.interrupted; - signal.throwIfAborted(); // Should not throw - service.interrupt("test"); - assertThrows(() => signal.throwIfAborted(), "test"); - }, - ); + await t.step("calls 'failure' callback", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#callback#call", + "failure", + host_call.calls[1].args[2], + ], + }); + }); - const waitClosed = service.waitClosed(); + await t.step("outputs an error message", () => { + assertSpyCall(console_error, 0, { + args: [ + "Failed to call failure callback 'failure': Error: invalid call", + ], + }); + }); + }); + }); - const waitLoadedCalledBeforeClose = service.waitLoaded( - "whenclosedtestplugin", - ); + await t.step(".close()", async (t) => { + await t.step("if the service is not yet closed", async (t) => { + const service = new Service(meta); + using _host_call = stub(host, "call"); + service.bind(host); - await t.step( - "the result promise of waitClosed() become 'pending' when the service is not closed", - async () => { - assert(await promiseState(waitClosed), "pending"); - }, - ); + await t.step("resolves", async () => { + await service.close(); + }); + }); - await t.step( - "close() closes the service", - async () => { + await t.step("if the service is already closed", async (t) => { + const service = new Service(meta); + using _host_call = stub(host, "call"); + service.bind(host); await service.close(); - }, - ); - - await t.step( - "the result promise of waitClosed() become 'fulfilled' when the service is closed", - async () => { - assert(await promiseState(waitClosed), "fulfilled"); - }, - ); - - await t.step( - "the result promise of waitLoaded() become 'rejected' when the service is closed", - async () => { - assertEquals(await promiseState(waitLoadedCalledBeforeClose), "rejected"); - await assertRejects( - () => waitLoadedCalledBeforeClose, - Error, - "Service closed", - ); - }, - ); - await t.step( - "waitClosed() returns 'fulfilled' promise if the service is already closed", - async () => { + await t.step("resolves", async () => { + await service.close(); + }); + }); + }); + + await t.step(".waitClosed()", async (t) => { + await t.step("pendings if the service is not yet closed", async () => { + using _host_call = stub(host, "call"); + const service = new Service(meta); + service.bind(host); + const actual = service.waitClosed(); - assert(await promiseState(actual), "fulfilled"); - }, - ); - await t.step( - "waitLoaded() returns 'rejected' promise if the service is already closed", - async () => { - await assertRejects( - () => service.waitLoaded("after-closed-test-plugin"), - Error, - "Service closed", - ); - }, - ); + assertEquals(await promiseState(actual), "pending"); + }); - await t.step( - "load() rejects an error when the service is already closed", - async () => { - await assertRejects( - () => service.load("dummyValid", scriptValid), - Error, - "Service closed", - ); - }, - ); + await t.step("resolves if the service is already closed", async () => { + using _host_call = stub(host, "call"); + const service = new Service(meta); + service.bind(host); + service.close(); + + const actual = service.waitClosed(); + + assertEquals(await promiseState(actual), "fulfilled"); + }); + + await t.step("resolves when the service is closed", async () => { + using _host_call = stub(host, "call"); + const service = new Service(meta); + service.bind(host); + + const actual = service.waitClosed(); + service.close(); - await t.step("[@@asyncDispose]() calls close()", async () => { + assertEquals(await promiseState(actual), "fulfilled"); + }); + }); + + await t.step("[@@asyncDispose]()", async (t) => { + const service = new Service(meta); + using _host_call = stub(host, "call"); + service.bind(host); using service_close = stub(service, "close"); - await service[Symbol.asyncDispose](); - assertSpyCalls(service_close, 1); + + await t.step("resolves", async () => { + await service[Symbol.asyncDispose](); + }); + + await t.step("calls .close()", () => { + assertSpyCalls(service_close, 1); + }); }); }); + +/** Resolve related script URL. */ +function resolve(path: string): string { + return new URL(path, import.meta.url).href; +} From 05a71b5a9db3eadb1c1185366451b0cdc7013ab5 Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 3 Jul 2024 22:08:08 +0900 Subject: [PATCH 54/99] :herb: move test directories --- deno.jsonc | 7 ++++++- denops/@denops-private/cli_test.ts | 2 +- denops/@denops-private/host/nvim_test.ts | 2 +- denops/@denops-private/host/vim_test.ts | 2 +- denops/@denops-private/service_test.ts | 16 ++++++---------- denops/@denops-private/worker_test.ts | 2 +- .../{ => runtime/functions}/server_test.ts | 6 +++--- tests/denops/{ => runtime}/plugin_test.ts | 6 +++--- .../testdata/dummy_invalid_constraint_plugin.ts | 0 .../testdata/dummy_invalid_constraint_plugin2.ts | 0 .../denops}/testdata/dummy_invalid_plugin.ts | 0 .../denops}/testdata/dummy_valid_plugin.ts | 0 .../testdata}/shared_server_test_no_verbose.ts | 2 +- .../testdata}/shared_server_test_verbose_true.ts | 2 +- .../denops}/testutil/cli.ts | 0 .../denops}/testutil/conf.ts | 0 .../denops}/testutil/conf_test.ts | 0 .../denops}/testutil/host.ts | 6 +++--- .../denops}/testutil/mock.ts | 0 .../denops}/testutil/mock_test.ts | 0 .../denops}/testutil/shared_server.ts | 0 .../denops}/testutil/shared_server_test.ts | 9 +++++++-- .../denops}/testutil/wait.ts | 0 .../denops}/testutil/wait_test.ts | 0 .../denops}/testutil/with.ts | 0 25 files changed, 34 insertions(+), 28 deletions(-) rename tests/denops/{ => runtime/functions}/server_test.ts (99%) rename tests/denops/{ => runtime}/plugin_test.ts (98%) rename {denops/@denops-private => tests/denops}/testdata/dummy_invalid_constraint_plugin.ts (100%) rename {denops/@denops-private => tests/denops}/testdata/dummy_invalid_constraint_plugin2.ts (100%) rename {denops/@denops-private => tests/denops}/testdata/dummy_invalid_plugin.ts (100%) rename {denops/@denops-private => tests/denops}/testdata/dummy_valid_plugin.ts (100%) rename {denops/@denops-private/testutil => tests/denops/testdata}/shared_server_test_no_verbose.ts (64%) rename {denops/@denops-private/testutil => tests/denops/testdata}/shared_server_test_verbose_true.ts (67%) rename {denops/@denops-private => tests/denops}/testutil/cli.ts (100%) rename {denops/@denops-private => tests/denops}/testutil/conf.ts (100%) rename {denops/@denops-private => tests/denops}/testutil/conf_test.ts (100%) rename {denops/@denops-private => tests/denops}/testutil/host.ts (93%) rename {denops/@denops-private => tests/denops}/testutil/mock.ts (100%) rename {denops/@denops-private => tests/denops}/testutil/mock_test.ts (100%) rename {denops/@denops-private => tests/denops}/testutil/shared_server.ts (100%) rename {denops/@denops-private => tests/denops}/testutil/shared_server_test.ts (93%) rename {denops/@denops-private => tests/denops}/testutil/wait.ts (100%) rename {denops/@denops-private => tests/denops}/testutil/wait_test.ts (100%) rename {denops/@denops-private => tests/denops}/testutil/with.ts (100%) diff --git a/deno.jsonc b/deno.jsonc index 666c9106..a084a759 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -9,5 +9,10 @@ }, "exclude": [ ".coverage/" - ] + ], + // NOTE: Import maps should only be used from test modules. + "imports": { + "/denops-private/": "./denops/@denops-private/", + "/denops-testutil/": "./tests/denops/testutil/" + } } diff --git a/denops/@denops-private/cli_test.ts b/denops/@denops-private/cli_test.ts index 6fed7e0e..b968ad95 100644 --- a/denops/@denops-private/cli_test.ts +++ b/denops/@denops-private/cli_test.ts @@ -25,7 +25,7 @@ import { createFakeTcpListener, createFakeWorker, pendingPromise, -} from "./testutil/mock.ts"; +} from "/denops-testutil/mock.ts"; import { main } from "./cli.ts"; const stubDenoListen = ( diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index 20cdb1ae..6ee52f1a 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -12,7 +12,7 @@ import { import { delay } from "jsr:@std/async@0.224.0/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; -import { withNeovim } from "../testutil/with.ts"; +import { withNeovim } from "/denops-testutil/with.ts"; import type { Service } from "../host.ts"; import { Neovim } from "./nvim.ts"; diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index 37aaefd7..74b2eb9f 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -7,7 +7,7 @@ import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; import { delay } from "jsr:@std/async@0.224.0/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; -import { withVim } from "../testutil/with.ts"; +import { withVim } from "/denops-testutil/with.ts"; import type { Service } from "../host.ts"; import { Vim } from "./vim.ts"; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index b32382f0..0e2b4c24 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -23,14 +23,10 @@ import { Service } from "./service.ts"; const NOOP = () => {}; -const scriptValid = resolve("./testdata/dummy_valid_plugin.ts"); -const scriptInvalid = resolve("./testdata/dummy_invalid_plugin.ts"); -const scriptInvalidConstraint = resolve( - "./testdata/dummy_invalid_constraint_plugin.ts", -); -const scriptInvalidConstraint2 = resolve( - "./testdata/dummy_invalid_constraint_plugin2.ts", -); +const scriptValid = resolve("dummy_valid_plugin.ts"); +const scriptInvalid = resolve("dummy_invalid_plugin.ts"); +const scriptInvalidConstraint = resolve("dummy_invalid_constraint_plugin.ts"); +const scriptInvalidConstraint2 = resolve("dummy_invalid_constraint_plugin2.ts"); Deno.test("Service", async (t) => { const meta: Meta = { @@ -884,7 +880,7 @@ Deno.test("Service", async (t) => { }); }); -/** Resolve related script URL. */ +/** Resolve testdata script URL. */ function resolve(path: string): string { - return new URL(path, import.meta.url).href; + return new URL(`../../tests/denops/testdata/${path}`, import.meta.url).href; } diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index 38873784..8be61b76 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -15,7 +15,7 @@ import { import { delay } from "jsr:@std/async@0.224.0/delay"; import { DisposableStack } from "jsr:@nick/dispose@1.1.0/disposable-stack"; import * as nvimCodec from "jsr:@lambdalisue/messagepack@^1.0.1"; -import { createFakeMeta } from "./testutil/mock.ts"; +import { createFakeMeta } from "/denops-testutil/mock.ts"; import { Neovim } from "./host/nvim.ts"; import { Vim } from "./host/vim.ts"; import { Service } from "./service.ts"; diff --git a/tests/denops/server_test.ts b/tests/denops/runtime/functions/server_test.ts similarity index 99% rename from tests/denops/server_test.ts rename to tests/denops/runtime/functions/server_test.ts index dd47db28..b16adcb4 100644 --- a/tests/denops/server_test.ts +++ b/tests/denops/runtime/functions/server_test.ts @@ -7,9 +7,9 @@ import { } from "jsr:@std/assert@0.225.2"; import { delay } from "jsr:@std/async@^0.224.0/delay"; import { AsyncDisposableStack } from "jsr:@nick/dispose@1.1.0/async-disposable-stack"; -import { testHost } from "../../denops/@denops-private/testutil/host.ts"; -import { useSharedServer } from "../../denops/@denops-private/testutil/shared_server.ts"; -import { wait } from "../../denops/@denops-private/testutil/wait.ts"; +import { testHost } from "/denops-testutil/host.ts"; +import { useSharedServer } from "/denops-testutil/shared_server.ts"; +import { wait } from "/denops-testutil/wait.ts"; const MESSAGE_DELAY = 200; diff --git a/tests/denops/plugin_test.ts b/tests/denops/runtime/plugin_test.ts similarity index 98% rename from tests/denops/plugin_test.ts rename to tests/denops/runtime/plugin_test.ts index 63217a65..690221a6 100644 --- a/tests/denops/plugin_test.ts +++ b/tests/denops/runtime/plugin_test.ts @@ -1,8 +1,8 @@ import { assertEquals, assertMatch } from "jsr:@std/assert@0.225.2"; import { delay } from "jsr:@std/async@^0.224.0/delay"; -import { withHost } from "../../denops/@denops-private/testutil/host.ts"; -import { useSharedServer } from "../../denops/@denops-private/testutil/shared_server.ts"; -import { wait } from "../../denops/@denops-private/testutil/wait.ts"; +import { withHost } from "/denops-testutil/host.ts"; +import { useSharedServer } from "/denops-testutil/shared_server.ts"; +import { wait } from "/denops-testutil/wait.ts"; const MESSAGE_DELAY = 200; const MODES = ["vim", "nvim"] as const; diff --git a/denops/@denops-private/testdata/dummy_invalid_constraint_plugin.ts b/tests/denops/testdata/dummy_invalid_constraint_plugin.ts similarity index 100% rename from denops/@denops-private/testdata/dummy_invalid_constraint_plugin.ts rename to tests/denops/testdata/dummy_invalid_constraint_plugin.ts diff --git a/denops/@denops-private/testdata/dummy_invalid_constraint_plugin2.ts b/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts similarity index 100% rename from denops/@denops-private/testdata/dummy_invalid_constraint_plugin2.ts rename to tests/denops/testdata/dummy_invalid_constraint_plugin2.ts diff --git a/denops/@denops-private/testdata/dummy_invalid_plugin.ts b/tests/denops/testdata/dummy_invalid_plugin.ts similarity index 100% rename from denops/@denops-private/testdata/dummy_invalid_plugin.ts rename to tests/denops/testdata/dummy_invalid_plugin.ts diff --git a/denops/@denops-private/testdata/dummy_valid_plugin.ts b/tests/denops/testdata/dummy_valid_plugin.ts similarity index 100% rename from denops/@denops-private/testdata/dummy_valid_plugin.ts rename to tests/denops/testdata/dummy_valid_plugin.ts diff --git a/denops/@denops-private/testutil/shared_server_test_no_verbose.ts b/tests/denops/testdata/shared_server_test_no_verbose.ts similarity index 64% rename from denops/@denops-private/testutil/shared_server_test_no_verbose.ts rename to tests/denops/testdata/shared_server_test_no_verbose.ts index 5583e7ea..87347653 100644 --- a/denops/@denops-private/testutil/shared_server_test_no_verbose.ts +++ b/tests/denops/testdata/shared_server_test_no_verbose.ts @@ -1,5 +1,5 @@ import { delay } from "jsr:@std/async@0.224.0/delay"; -import { useSharedServer } from "./shared_server.ts"; +import { useSharedServer } from "/denops-testutil/shared_server.ts"; { await using _server = await useSharedServer(); diff --git a/denops/@denops-private/testutil/shared_server_test_verbose_true.ts b/tests/denops/testdata/shared_server_test_verbose_true.ts similarity index 67% rename from denops/@denops-private/testutil/shared_server_test_verbose_true.ts rename to tests/denops/testdata/shared_server_test_verbose_true.ts index 6cbd73e2..d43f4759 100644 --- a/denops/@denops-private/testutil/shared_server_test_verbose_true.ts +++ b/tests/denops/testdata/shared_server_test_verbose_true.ts @@ -1,5 +1,5 @@ import { delay } from "jsr:@std/async@0.224.0/delay"; -import { useSharedServer } from "./shared_server.ts"; +import { useSharedServer } from "/denops-testutil/shared_server.ts"; { await using _server = await useSharedServer({ verbose: true }); diff --git a/denops/@denops-private/testutil/cli.ts b/tests/denops/testutil/cli.ts similarity index 100% rename from denops/@denops-private/testutil/cli.ts rename to tests/denops/testutil/cli.ts diff --git a/denops/@denops-private/testutil/conf.ts b/tests/denops/testutil/conf.ts similarity index 100% rename from denops/@denops-private/testutil/conf.ts rename to tests/denops/testutil/conf.ts diff --git a/denops/@denops-private/testutil/conf_test.ts b/tests/denops/testutil/conf_test.ts similarity index 100% rename from denops/@denops-private/testutil/conf_test.ts rename to tests/denops/testutil/conf_test.ts diff --git a/denops/@denops-private/testutil/host.ts b/tests/denops/testutil/host.ts similarity index 93% rename from denops/@denops-private/testutil/host.ts rename to tests/denops/testutil/host.ts index 440cd0fe..a95aebda 100644 --- a/denops/@denops-private/testutil/host.ts +++ b/tests/denops/testutil/host.ts @@ -1,6 +1,6 @@ -import { Host } from "../host.ts"; -import { Neovim } from "../host/nvim.ts"; -import { Vim } from "../host/vim.ts"; +import { Host } from "/denops-private/host.ts"; +import { Neovim } from "/denops-private/host/nvim.ts"; +import { Vim } from "/denops-private/host/vim.ts"; import { withNeovim, WithOptions, withVim } from "./with.ts"; export type HostFn = (helper: { diff --git a/denops/@denops-private/testutil/mock.ts b/tests/denops/testutil/mock.ts similarity index 100% rename from denops/@denops-private/testutil/mock.ts rename to tests/denops/testutil/mock.ts diff --git a/denops/@denops-private/testutil/mock_test.ts b/tests/denops/testutil/mock_test.ts similarity index 100% rename from denops/@denops-private/testutil/mock_test.ts rename to tests/denops/testutil/mock_test.ts diff --git a/denops/@denops-private/testutil/shared_server.ts b/tests/denops/testutil/shared_server.ts similarity index 100% rename from denops/@denops-private/testutil/shared_server.ts rename to tests/denops/testutil/shared_server.ts diff --git a/denops/@denops-private/testutil/shared_server_test.ts b/tests/denops/testutil/shared_server_test.ts similarity index 93% rename from denops/@denops-private/testutil/shared_server_test.ts rename to tests/denops/testutil/shared_server_test.ts index b8d52dd3..fdc7225d 100644 --- a/denops/@denops-private/testutil/shared_server_test.ts +++ b/tests/denops/testutil/shared_server_test.ts @@ -44,7 +44,7 @@ Deno.test("useSharedServer()", async (t) => { "--allow-env", "--allow-read", "--allow-run", - join(import.meta.dirname!, "shared_server_test_no_verbose.ts"), + resolve("shared_server_test_no_verbose.ts"), ], stdout: "piped", }).spawn(); @@ -88,7 +88,7 @@ Deno.test("useSharedServer()", async (t) => { "--allow-env", "--allow-read", "--allow-run", - join(import.meta.dirname!, "shared_server_test_verbose_true.ts"), + resolve("shared_server_test_verbose_true.ts"), ], stdout: "piped", }).spawn(); @@ -108,3 +108,8 @@ Deno.test("useSharedServer()", async (t) => { ); }); }); + +/** Resolve testdata script path. */ +function resolve(path: string): string { + return join(import.meta.dirname!, `../testdata/${path}`); +} diff --git a/denops/@denops-private/testutil/wait.ts b/tests/denops/testutil/wait.ts similarity index 100% rename from denops/@denops-private/testutil/wait.ts rename to tests/denops/testutil/wait.ts diff --git a/denops/@denops-private/testutil/wait_test.ts b/tests/denops/testutil/wait_test.ts similarity index 100% rename from denops/@denops-private/testutil/wait_test.ts rename to tests/denops/testutil/wait_test.ts diff --git a/denops/@denops-private/testutil/with.ts b/tests/denops/testutil/with.ts similarity index 100% rename from denops/@denops-private/testutil/with.ts rename to tests/denops/testutil/with.ts From 7c1d4195941fa4dd10d2c22274b6a6cd7393f23d Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 5 Jul 2024 02:26:44 +0900 Subject: [PATCH 55/99] :herb: add tests for `denops#plugin#*` --- tests/denops/runtime/functions/plugin_test.ts | 566 ++++++++++++++++++ .../denops/@dummy_namespace/main.ts | 6 + .../denops/dummy_invalid/main.ts | 5 + .../dummy_plugins/denops/dummy_valid/main.ts | 5 + 4 files changed, 582 insertions(+) create mode 100644 tests/denops/runtime/functions/plugin_test.ts create mode 100644 tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts create mode 100644 tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts create mode 100644 tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts new file mode 100644 index 00000000..f11e8cfe --- /dev/null +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -0,0 +1,566 @@ +import { + assertArrayIncludes, + assertEquals, + assertMatch, + assertRejects, +} from "jsr:@std/assert@0.225.2"; +import { delay } from "jsr:@std/async@^0.224.0/delay"; +import { join } from "jsr:@std/path@0.225.0/join"; +import { testHost } from "/denops-testutil/host.ts"; +import { wait } from "/denops-testutil/wait.ts"; +import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; + +const MESSAGE_DELAY = 200; + +const scriptValid = resolve("dummy_valid_plugin.ts"); +const scriptInvalid = resolve("dummy_invalid_plugin.ts"); +const runtimepathPlugin = resolve("dummy_plugins"); + +testHost({ + mode: "all", + postlude: [ + "runtime plugin/denops.vim", + ], + fn: async ({ host, t, stderr }) => { + let outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); + await wait(() => host.call("eval", "denops#server#status() ==# 'running'")); + await host.call("execute", [ + "let g:__test_denops_events = []", + "autocmd User DenopsPlugin* call add(g:__test_denops_events, expand(''))", + ], ""); + + await t.step("denops#plugin#load()", async (t) => { + await t.step("if the plugin is valid", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummy', '${scriptValid}')`, + ], ""); + + await t.step("loads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummy") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummy", + "DenopsPluginPost:dummy", + ]); + }); + + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); + }); + }); + + await t.step("if the plugin entrypoint throws", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyInvalid', '${scriptInvalid}')`, + ], ""); + + await t.step("fails loading a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginFail:dummyInvalid") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummyInvalid", + "DenopsPluginFail:dummyInvalid", + ]); + }); + + await t.step("outputs an error message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to load plugin 'dummyInvalid': Error: This is dummy error/, + ); + }); + }); + + // NOTE: Depends on 'dummy' which was already loaded in the test above. + await t.step("if the plugin is already loaded", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummy', '${scriptValid}')`, + ], ""); + + await t.step("does not load a denops plugin", async () => { + const actual = wait( + async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummy"), + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("does not fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), []); + }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + + await t.step( + "if the plugin is the same script with a different name", + async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyOther', '${scriptValid}')`, + ], ""); + + await t.step("loads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyOther") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummyOther", + "DenopsPluginPost:dummyOther", + ]); + }); + + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); + }); + }, + ); + }); + + await t.step("denops#plugin#reload()", async (t) => { + // NOTE: Depends on 'dummy' which was already loaded in the test above. + await t.step("if the plugin is already loaded", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#reload('dummy')", + ], ""); + + await t.step("reloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummy") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummy", + "DenopsPluginPost:dummy", + ]); + }); + + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); + }); + }); + + await t.step("if the plugin is not yet loaded", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#reload('notexistsplugin')", + ], ""); + + await t.step("does not reload a denops plugin", async () => { + const actual = wait( + async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummy"), + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("does not fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), []); + }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + }); + + await t.step("denops#plugin#is_loaded()", async (t) => { + // NOTE: Depends on 'dummy' which was already loaded in the test above. + await t.step("returns 1 if the plugin `name` is loaded", async () => { + const actual = await host.call("denops#plugin#is_loaded", "dummy"); + assertEquals(actual, 1); + }); + + await t.step("returns 0 if the plugin `name` is not exists", async () => { + const actual = await host.call( + "denops#plugin#is_loaded", + "notexistsplugin", + ); + assertEquals(actual, 0); + }); + }); + + await t.step("denops#plugin#discover()", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `set runtimepath+=${await host.call("fnameescape", runtimepathPlugin)}`, + `call denops#plugin#discover()`, + ], ""); + + await t.step("loads denops plugins", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => /^DenopsPlugin(?:Post|Fail):/.test(ev)).length >= 2 + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + [ + "DenopsPluginPre:dummy_valid", + "DenopsPluginPost:dummy_valid", + "DenopsPluginPre:dummy_invalid", + "DenopsPluginFail:dummy_invalid", + ], + ); + }); + + await t.step("does not load plugins name start with '@'", async () => { + const events = + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.includes("@dummy_namespace")); + assertEquals(events, []); + }); + + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); + }); + + await t.step("outputs an error message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to load plugin 'dummy_invalid': Error: This is dummy error/, + ); + }); + }); + + // NOTE: Depends on 'dummy' which was already loaded in the test above. + await t.step("denops#plugin#check_type()", async (t) => { + /* + await t.step("if no arguments is specified", async (t) => { + outputs = []; + await host.call("execute", [ + // NOTE: + // Call `denops#plugin#is_loaded()` and add an entry to the internal list. + // This will result in a plugin entry whose script is empty. + "call denops#plugin#is_loaded('notexistsplugin')", + ], ""); + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#check_type()`, + ], ""); + + await t.step("outputs an info message after delayed", async () => { + await wait(() => outputs.join("").includes("Type check")); + assertMatch(outputs.join(""), /Type check succeeded/); + }); + }); + */ + + await t.step("if the script name is specified", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#check_type('dummy')`, + ], ""); + + await t.step("outputs an info message after delayed", async () => { + await wait(() => outputs.join("").includes("Type check")); + assertMatch(outputs.join(""), /Type check succeeded/); + }); + }); + + await t.step("if a non-existent script name is specified", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#check_type('notexistsplugin')`, + ], ""); + + await t.step("outputs an error message after delayed", async () => { + await wait(() => outputs.join("").includes("Type check")); + assertMatch(outputs.join(""), /Type check failed:/); + }); + }); + }); + + await t.step("denops#plugin#wait_async()", async (t) => { + await t.step("if the plugin is valid", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call timer_start(1000, { -> denops#plugin#load('dummyWaitAsync', '${scriptValid}') })`, + ], ""); + + const resultPromise = host.call("execute", [ + "call denops#plugin#wait_async('dummyWaitAsync', { -> add(g:__test_denops_events, 'wait_async callback called: dummyWaitAsync') })", + ], ""); + + await t.step("returns immediately", async () => { + await delay(100); // host.call delay + assertEquals(await promiseState(resultPromise), "fulfilled"); + await resultPromise; + }); + + await t.step("does not call the callback immediately", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), []); + }); + + await t.step( + "calls the callback when the plugin is loaded", + async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitAsync") + ); + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + ["wait_async callback called: dummyWaitAsync"], + ); + }, + ); + }); + + // NOTE: Depends on 'dummyWaitAsync' which was already loaded in the test above. + await t.step("if the plugin is already loaded", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + ], ""); + + const resultPromise = host.call("execute", [ + "call denops#plugin#wait_async('dummyWaitAsync', { -> add(g:__test_denops_events, 'wait_async callback called: dummyWaitAsync') })", + ], ""); + + await t.step("returns immediately", async () => { + await delay(100); // host.call delay + assertEquals(await promiseState(resultPromise), "fulfilled"); + await resultPromise; + }); + + await t.step("calls the callback immediately", async () => { + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + ["wait_async callback called: dummyWaitAsync"], + ); + }); + }); + + await t.step("if the plugin entrypoint throws", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call timer_start(1000, { -> denops#plugin#load('dummyWaitAsyncInvalid', '${scriptInvalid}') })`, + ], ""); + + const resultPromise = host.call("execute", [ + "call denops#plugin#wait_async('dummyWaitAsyncInvalid', { -> add(g:__test_denops_events, 'wait_async callback called: dummyWaitAsync') })", + ], ""); + + await t.step("returns immediately", async () => { + await delay(100); // host.call delay + assertEquals(await promiseState(resultPromise), "fulfilled"); + await resultPromise; + }); + + await t.step( + "does not call the callback when the plugin is failed", + async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginFail:dummyWaitAsyncInvalid") + ); + const events = + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => !/^DenopsPlugin/.test(ev)); + assertEquals(events, []); + }, + ); + }); + }); + + // NOTE: This test stops the denops server. + await t.step("denops#plugin#wait()", async (t) => { + await t.step("if the plugin is valid", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWait', '${scriptValid}')`, + ], ""); + + const resultPromise = host.call("denops#plugin#wait", "dummyWait"); + + await t.step("waits the plugin is loaded", async () => { + assertEquals(await promiseState(resultPromise), "pending"); + }); + + await t.step("returns 0", async () => { + assertEquals(await resultPromise, 0); + }); + + await t.step("the plugin is already loaded after returns", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummyWait", + "DenopsPluginPost:dummyWait", + ]); + }); + }); + + // NOTE: Depends on 'dummyWait' which was already loaded in the test above. + await t.step("if the plugin is already loaded", async (t) => { + const resultPromise = host.call("denops#plugin#wait", "dummyWait"); + + await t.step("returns immediately", async () => { + await delay(100); // host.call delay + assertEquals(await promiseState(resultPromise), "fulfilled"); + }); + + await t.step("returns 0", async () => { + assertEquals(await resultPromise, 0); + }); + }); + + await t.step("if the plugin entrypoint throws", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitInvalid', '${scriptInvalid}')`, + ], ""); + + const resultPromise = host.call( + "denops#plugin#wait", + "dummyWaitInvalid", + ); + + await t.step("waits the plugin is failed", async () => { + assertEquals(await promiseState(resultPromise), "pending"); + }); + + await t.step("returns -3", async () => { + assertEquals(await resultPromise, -3); + }); + + await t.step("the plugin is already failed after returns", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummyWaitInvalid", + "DenopsPluginFail:dummyWaitInvalid", + ]); + }); + }); + + await t.step("if it times out", async (t) => { + await t.step("if no `silent` is specified", async (t) => { + outputs = []; + + await t.step("returns -1", async () => { + const actual = await host.call( + "denops#plugin#wait", + "notexistsplugin", + { timeout: 100 }, + ); + assertEquals(actual, -1); + }); + + await t.step("outputs an error message", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to wait for "notexistsplugin" to start\. It took more than 100 milliseconds and timed out\./, + ); + }); + }); + + await t.step("if `silent=1`", async (t) => { + outputs = []; + + await t.step("returns -1", async () => { + const actual = await host.call( + "denops#plugin#wait", + "notexistsplugin", + { timeout: 100, silent: 1 }, + ); + assertEquals(actual, -1); + }); + + await t.step("does not output error messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + }); + + // NOTE: This test stops the denops server. + await t.step("if the denops server is stopped", async (t) => { + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + + await t.step("if no `silent` is specified", async (t) => { + outputs = []; + + await t.step("returns -2", async () => { + const actual = await host.call("denops#plugin#wait", "dummy"); + assertEquals(actual, -2); + }); + + await t.step("outputs an error message", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to wait for "dummy" to start\. Denops server itself is not started\./, + ); + }); + }); + + await t.step("if `silent=1`", async (t) => { + outputs = []; + + await t.step("returns -2", async () => { + const actual = await host.call("denops#plugin#wait", "dummy", { + silent: 1, + }); + assertEquals(actual, -2); + }); + + await t.step("does not output error messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + }); + }); + }, +}); + +/** Resolve testdata script path. */ +function resolve(path: string): string { + return join(import.meta.dirname!, `../../testdata/${path}`); +} diff --git a/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts b/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts new file mode 100644 index 00000000..9eadbb3b --- /dev/null +++ b/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts @@ -0,0 +1,6 @@ +import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; + +// NOTE: This should not be called, a directory starting with '@' is not a denops plugin. +export const main: Entrypoint = async (denops) => { + await denops.cmd("echo 'Hello, Denops!'"); +}; diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts new file mode 100644 index 00000000..5cb57ab6 --- /dev/null +++ b/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts @@ -0,0 +1,5 @@ +import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; + +export const main: Entrypoint = (_denops) => { + throw new Error("This is dummy error"); +}; diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts new file mode 100644 index 00000000..9d9b73f5 --- /dev/null +++ b/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts @@ -0,0 +1,5 @@ +import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; + +export const main: Entrypoint = async (denops) => { + await denops.cmd("echo 'Hello, Denops!'"); +}; From 81f267e764cdc3419d184bda3c0e49707e89ede5 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 5 Jul 2024 02:29:47 +0900 Subject: [PATCH 56/99] :bug: prevent error on `denops#plugin#check_type()` If `denops#plugin#is_loaded()` is called for a plugin that is not loaded, specifying an empty path will cause the `deno check` to fail. --- autoload/denops/plugin.vim | 12 +++++++----- tests/denops/runtime/functions/plugin_test.ts | 2 -- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/autoload/denops/plugin.vim b/autoload/denops/plugin.vim index 89f80e9a..27b1998e 100644 --- a/autoload/denops/plugin.vim +++ b/autoload/denops/plugin.vim @@ -72,11 +72,13 @@ function! denops#plugin#discover() abort endfunction function! denops#plugin#check_type(...) abort - let l:plugins = a:0 - \ ? [denops#_internal#plugin#get(a:1)] - \ : denops#_internal#plugin#list() - let l:args = [g:denops#deno, 'check'] - let l:args = extend(l:args, map(l:plugins, { _, v -> v.script })) + if a:0 + let l:scripts = [denops#_internal#plugin#get(a:1).script] + else + let l:scripts = denops#_internal#plugin#list() + \->copy()->map({ _, v -> v.script })->filter({ _, v -> v !=# '' }) + endif + let l:args = [g:denops#deno, 'check'] + l:scripts let l:job = denops#_internal#job#start(l:args, { \ 'env': { \ 'NO_COLOR': 1, diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index f11e8cfe..f1705d9a 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -268,7 +268,6 @@ testHost({ // NOTE: Depends on 'dummy' which was already loaded in the test above. await t.step("denops#plugin#check_type()", async (t) => { - /* await t.step("if no arguments is specified", async (t) => { outputs = []; await host.call("execute", [ @@ -287,7 +286,6 @@ testHost({ assertMatch(outputs.join(""), /Type check succeeded/); }); }); - */ await t.step("if the script name is specified", async (t) => { outputs = []; From 87e4cdc478e723c00ec470bfa80f6becf32efbd5 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 5 Jul 2024 03:20:11 +0900 Subject: [PATCH 57/99] :herb: ignore `denops#plugin#wait()` test on Mac --- tests/denops/runtime/functions/plugin_test.ts | 235 +++++++++--------- 1 file changed, 123 insertions(+), 112 deletions(-) diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index f1705d9a..b680fb36 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -407,153 +407,164 @@ testHost({ }); // NOTE: This test stops the denops server. - await t.step("denops#plugin#wait()", async (t) => { - await t.step("if the plugin is valid", async (t) => { - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyWait', '${scriptValid}')`, - ], ""); + // FIXME: This test will run infinitely on Mac. + await t.step({ + name: "denops#plugin#wait()", + ignore: Deno.build.os === "darwin", + fn: async (t) => { + await t.step("if the plugin is valid", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWait', '${scriptValid}')`, + ], ""); - const resultPromise = host.call("denops#plugin#wait", "dummyWait"); + const resultPromise = host.call("denops#plugin#wait", "dummyWait"); - await t.step("waits the plugin is loaded", async () => { - assertEquals(await promiseState(resultPromise), "pending"); - }); + await t.step("waits the plugin is loaded", async () => { + assertEquals(await promiseState(resultPromise), "pending"); + }); - await t.step("returns 0", async () => { - assertEquals(await resultPromise, 0); - }); + await t.step("returns 0", async () => { + assertEquals(await resultPromise, 0); + }); - await t.step("the plugin is already loaded after returns", async () => { - assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginPre:dummyWait", - "DenopsPluginPost:dummyWait", - ]); + await t.step( + "the plugin is already loaded after returns", + async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummyWait", + "DenopsPluginPost:dummyWait", + ]); + }, + ); }); - }); - // NOTE: Depends on 'dummyWait' which was already loaded in the test above. - await t.step("if the plugin is already loaded", async (t) => { - const resultPromise = host.call("denops#plugin#wait", "dummyWait"); + // NOTE: Depends on 'dummyWait' which was already loaded in the test above. + await t.step("if the plugin is already loaded", async (t) => { + const resultPromise = host.call("denops#plugin#wait", "dummyWait"); - await t.step("returns immediately", async () => { - await delay(100); // host.call delay - assertEquals(await promiseState(resultPromise), "fulfilled"); - }); + await t.step("returns immediately", async () => { + await delay(100); // host.call delay + assertEquals(await promiseState(resultPromise), "fulfilled"); + }); - await t.step("returns 0", async () => { - assertEquals(await resultPromise, 0); + await t.step("returns 0", async () => { + assertEquals(await resultPromise, 0); + }); }); - }); - await t.step("if the plugin entrypoint throws", async (t) => { - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyWaitInvalid', '${scriptInvalid}')`, - ], ""); + await t.step("if the plugin entrypoint throws", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitInvalid', '${scriptInvalid}')`, + ], ""); - const resultPromise = host.call( - "denops#plugin#wait", - "dummyWaitInvalid", - ); + const resultPromise = host.call( + "denops#plugin#wait", + "dummyWaitInvalid", + ); - await t.step("waits the plugin is failed", async () => { - assertEquals(await promiseState(resultPromise), "pending"); - }); + await t.step("waits the plugin is failed", async () => { + assertEquals(await promiseState(resultPromise), "pending"); + }); - await t.step("returns -3", async () => { - assertEquals(await resultPromise, -3); - }); + await t.step("returns -3", async () => { + assertEquals(await resultPromise, -3); + }); - await t.step("the plugin is already failed after returns", async () => { - assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginPre:dummyWaitInvalid", - "DenopsPluginFail:dummyWaitInvalid", - ]); + await t.step( + "the plugin is already failed after returns", + async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummyWaitInvalid", + "DenopsPluginFail:dummyWaitInvalid", + ]); + }, + ); }); - }); - await t.step("if it times out", async (t) => { - await t.step("if no `silent` is specified", async (t) => { - outputs = []; + await t.step("if it times out", async (t) => { + await t.step("if no `silent` is specified", async (t) => { + outputs = []; - await t.step("returns -1", async () => { - const actual = await host.call( - "denops#plugin#wait", - "notexistsplugin", - { timeout: 100 }, - ); - assertEquals(actual, -1); - }); + await t.step("returns -1", async () => { + const actual = await host.call( + "denops#plugin#wait", + "notexistsplugin", + { timeout: 100 }, + ); + assertEquals(actual, -1); + }); - await t.step("outputs an error message", async () => { - await delay(MESSAGE_DELAY); - assertMatch( - outputs.join(""), - /Failed to wait for "notexistsplugin" to start\. It took more than 100 milliseconds and timed out\./, - ); + await t.step("outputs an error message", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to wait for "notexistsplugin" to start\. It took more than 100 milliseconds and timed out\./, + ); + }); }); - }); - await t.step("if `silent=1`", async (t) => { - outputs = []; + await t.step("if `silent=1`", async (t) => { + outputs = []; - await t.step("returns -1", async () => { - const actual = await host.call( - "denops#plugin#wait", - "notexistsplugin", - { timeout: 100, silent: 1 }, - ); - assertEquals(actual, -1); - }); + await t.step("returns -1", async () => { + const actual = await host.call( + "denops#plugin#wait", + "notexistsplugin", + { timeout: 100, silent: 1 }, + ); + assertEquals(actual, -1); + }); - await t.step("does not output error messages", async () => { - await delay(MESSAGE_DELAY); - assertEquals(outputs, []); + await t.step("does not output error messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); }); }); - }); - // NOTE: This test stops the denops server. - await t.step("if the denops server is stopped", async (t) => { - await host.call("denops#server#stop"); - await wait( - () => host.call("eval", "denops#server#status() ==# 'stopped'"), - ); + // NOTE: This test stops the denops server. + await t.step("if the denops server is stopped", async (t) => { + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); - await t.step("if no `silent` is specified", async (t) => { - outputs = []; + await t.step("if no `silent` is specified", async (t) => { + outputs = []; - await t.step("returns -2", async () => { - const actual = await host.call("denops#plugin#wait", "dummy"); - assertEquals(actual, -2); - }); + await t.step("returns -2", async () => { + const actual = await host.call("denops#plugin#wait", "dummy"); + assertEquals(actual, -2); + }); - await t.step("outputs an error message", async () => { - await delay(MESSAGE_DELAY); - assertMatch( - outputs.join(""), - /Failed to wait for "dummy" to start\. Denops server itself is not started\./, - ); + await t.step("outputs an error message", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to wait for "dummy" to start\. Denops server itself is not started\./, + ); + }); }); - }); - await t.step("if `silent=1`", async (t) => { - outputs = []; + await t.step("if `silent=1`", async (t) => { + outputs = []; - await t.step("returns -2", async () => { - const actual = await host.call("denops#plugin#wait", "dummy", { - silent: 1, + await t.step("returns -2", async () => { + const actual = await host.call("denops#plugin#wait", "dummy", { + silent: 1, + }); + assertEquals(actual, -2); }); - assertEquals(actual, -2); - }); - await t.step("does not output error messages", async () => { - await delay(MESSAGE_DELAY); - assertEquals(outputs, []); + await t.step("does not output error messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); }); }); - }); + }, }); }, }); From d3d5eb964224e65bd4bbc8328e7a07b836d1e4eb Mon Sep 17 00:00:00 2001 From: Milly Date: Tue, 30 Apr 2024 03:31:47 +0900 Subject: [PATCH 58/99] :+1: Add plugin unload features Plugin API: - `Entrypoint` can now return `AsyncDisposable`. - It is called by `denops#plugin#unload()`. - It is called when the server closing. - When `denops#server#close()` is called. - When `denops#server#stop()` is called. - When the channel is closed. - When the server process trapped `SIGINT`. Vim API: - Add new `denops-function`: - `denops#plugin#unload({plugin})` - Add new events: - `DenopsPluginUnloadPre:{plugin}` - `DenopsPluginUnloadPost:{plugin}` - `DenopsPluginUnloadFail:{plugin}` --- autoload/denops/_internal/plugin.vim | 35 ++ autoload/denops/plugin.vim | 4 + deno.jsonc | 6 + denops/@denops-private/host.ts | 4 + denops/@denops-private/host/nvim_test.ts | 1 + denops/@denops-private/host/vim_test.ts | 1 + denops/@denops-private/host_test.ts | 1 + denops/@denops-private/plugin.ts | 35 ++ denops/@denops-private/service.ts | 95 ++++-- denops/@denops-private/service_test.ts | 316 +++++++++++++++++- doc/denops.txt | 53 ++- plugin/denops.vim | 3 + plugin/denops/debug.vim | 4 +- tests/denops/runtime/functions/plugin_test.ts | 206 +++++++++++- .../testdata/dummy_invalid_dispose_plugin.ts | 11 + .../testdata/dummy_valid_dispose_plugin.ts | 11 + 16 files changed, 748 insertions(+), 38 deletions(-) create mode 100644 denops/@denops-private/plugin.ts create mode 100644 tests/denops/testdata/dummy_invalid_dispose_plugin.ts create mode 100644 tests/denops/testdata/dummy_valid_dispose_plugin.ts diff --git a/autoload/denops/_internal/plugin.vim b/autoload/denops/_internal/plugin.vim index 73d96ba5..8bde492b 100644 --- a/autoload/denops/_internal/plugin.vim +++ b/autoload/denops/_internal/plugin.vim @@ -1,6 +1,7 @@ const s:STATE_RESERVED = 'reserved' const s:STATE_LOADING = 'loading' const s:STATE_LOADED = 'loaded' +const s:STATE_UNLOADING = 'unloading' const s:STATE_FAILED = 'failed' let s:plugins = {} @@ -27,6 +28,14 @@ function! denops#_internal#plugin#load(name, script) abort call denops#_internal#server#chan#notify('invoke', ['load', l:args]) endfunction +function! denops#_internal#plugin#unload(name) abort + let l:args = [a:name] + let l:plugin = denops#_internal#plugin#get(a:name) + let l:plugin.state = s:STATE_UNLOADING + call denops#_internal#echo#debug(printf('unload plugin: %s', l:args)) + call denops#_internal#server#chan#notify('invoke', ['unload', l:args]) +endfunction + function! denops#_internal#plugin#reload(name) abort const l:args = [a:name] let l:plugin = denops#_internal#plugin#get(a:name) @@ -60,10 +69,36 @@ function! s:DenopsSystemPluginFail() abort execute printf('doautocmd User DenopsPluginFail:%s', l:name) endfunction +function! s:DenopsSystemPluginUnloadPre() abort + const l:name = matchstr(expand(''), 'DenopsSystemPluginUnloadPre:\zs.*') + let l:plugin = denops#_internal#plugin#get(l:name) + let l:plugin.state = s:STATE_UNLOADING + execute printf('doautocmd User DenopsPluginUnloadPre:%s', l:name) +endfunction + +function! s:DenopsSystemPluginUnloadPost() abort + const l:name = matchstr(expand(''), 'DenopsSystemPluginUnloadPost:\zs.*') + let l:plugin = denops#_internal#plugin#get(l:name) + let l:plugin.state = s:STATE_RESERVED + let l:plugin.callbacks = [] + execute printf('doautocmd User DenopsPluginUnloadPost:%s', l:name) +endfunction + +function! s:DenopsSystemPluginUnloadFail() abort + const l:name = matchstr(expand(''), 'DenopsSystemPluginUnloadFail:\zs.*') + let l:plugin = denops#_internal#plugin#get(l:name) + let l:plugin.state = s:STATE_FAILED + let l:plugin.callbacks = [] + execute printf('doautocmd User DenopsPluginUnloadFail:%s', l:name) +endfunction + augroup denops_autoload_plugin_internal autocmd! autocmd User DenopsSystemPluginPre:* ++nested call s:DenopsSystemPluginPre() autocmd User DenopsSystemPluginPost:* ++nested call s:DenopsSystemPluginPost() autocmd User DenopsSystemPluginFail:* ++nested call s:DenopsSystemPluginFail() + autocmd User DenopsSystemPluginUnloadPre:* ++nested call s:DenopsSystemPluginUnloadPre() + autocmd User DenopsSystemPluginUnloadPost:* ++nested call s:DenopsSystemPluginUnloadPost() + autocmd User DenopsSystemPluginUnloadFail:* ++nested call s:DenopsSystemPluginUnloadFail() autocmd User DenopsClosed let s:plugins = {} augroup END diff --git a/autoload/denops/plugin.vim b/autoload/denops/plugin.vim index 27b1998e..84ca02c9 100644 --- a/autoload/denops/plugin.vim +++ b/autoload/denops/plugin.vim @@ -53,6 +53,10 @@ function! denops#plugin#load(name, script) abort call denops#_internal#plugin#load(a:name, a:script) endfunction +function! denops#plugin#unload(name) abort + call denops#_internal#plugin#unload(a:name) +endfunction + function! denops#plugin#reload(name) abort call denops#_internal#plugin#reload(a:name) endfunction diff --git a/deno.jsonc b/deno.jsonc index a084a759..1e196b23 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -7,6 +7,12 @@ "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", "update:commit": "deno task -q update --commit --pre-commit=fmt,lint" }, + "test": { + "exclude": [ + // TODO: #349 Update `Entrypoint` in denops-core, and remove this entry. + "denops/@denops-private/plugin.ts" + ] + }, "exclude": [ ".coverage/" ], diff --git a/denops/@denops-private/host.ts b/denops/@denops-private/host.ts index d0f57b49..93887158 100644 --- a/denops/@denops-private/host.ts +++ b/denops/@denops-private/host.ts @@ -49,6 +49,7 @@ export type CallbackId = string; export type Service = { bind(host: Host): void; load(name: string, script: string): Promise; + unload(name: string): Promise; reload(name: string): Promise; interrupt(reason?: unknown): void; dispatch(name: string, fn: string, args: unknown[]): Promise; @@ -72,6 +73,8 @@ export function invoke( switch (name) { case "load": return service.load(...ensure(args, serviceMethodArgs.load)); + case "unload": + return service.unload(...ensure(args, serviceMethodArgs.unload)); case "reload": return service.reload(...ensure(args, serviceMethodArgs.reload)); case "interrupt": @@ -92,6 +95,7 @@ export function invoke( const serviceMethodArgs = { load: is.ParametersOf([is.String, is.String] as const), + unload: is.ParametersOf([is.String] as const), reload: is.ParametersOf([is.String] as const), interrupt: is.ParametersOf([is.OptionalOf(is.Unknown)] as const), dispatch: is.ParametersOf([is.String, is.String, is.Array] as const), diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index 6ee52f1a..859e5de7 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -23,6 +23,7 @@ Deno.test("Neovim", async (t) => { const service: Service = { bind: () => unimplemented(), load: () => unimplemented(), + unload: () => unimplemented(), reload: () => unimplemented(), interrupt: () => unimplemented(), dispatch: () => unimplemented(), diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index 74b2eb9f..3c31176f 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -18,6 +18,7 @@ Deno.test("Vim", async (t) => { const service: Service = { bind: () => unimplemented(), load: () => unimplemented(), + unload: () => unimplemented(), reload: () => unimplemented(), interrupt: () => unimplemented(), dispatch: () => unimplemented(), diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index b4d87f76..a7989435 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -11,6 +11,7 @@ import { invoke, type Service } from "./host.ts"; Deno.test("invoke", async (t) => { const service: Omit = { load: () => unimplemented(), + unload: () => unimplemented(), reload: () => unimplemented(), interrupt: () => unimplemented(), dispatch: () => unimplemented(), diff --git a/denops/@denops-private/plugin.ts b/denops/@denops-private/plugin.ts new file mode 100644 index 00000000..00c9441b --- /dev/null +++ b/denops/@denops-private/plugin.ts @@ -0,0 +1,35 @@ +// TODO: #349 Update `Entrypoint` in denops-core, remove this module from `$.test.exclude` in `deno.jsonc`, and remove this module. +import type { Denops } from "jsr:@denops/core@6.1.0"; + +/** + * Denops's entrypoint definition. + * + * Use this type to ensure the `main` function is properly implemented like: + * + * ```ts + * import type { Entrypoint } from "jsr:@denops/core"; + * + * export const main: Entrypoint = (denops) => { + * // ... + * } + * ``` + * + * If an `AsyncDisposable` object is returned, resources can be disposed of + * asynchronously when the plugin is unloaded, like: + * + * ```ts + * import type { Entrypoint } from "jsr:@denops/core"; + * + * export const main: Entrypoint = (denops) => { + * // ... + * return { + * [Symbol.asyncDispose]: () => { + * // Dispose resources... + * } + * } + * } + * ``` + */ +export type Entrypoint = ( + denops: Denops, +) => void | AsyncDisposable | Promise; diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index fc380917..4552a5cd 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -1,3 +1,6 @@ +// TODO: #349 Import `Entrypoint` from denops-core. +// import type { Entrypoint } from "jsr:@denops/core@7.0.0"; +import type { Entrypoint } from "./plugin.ts"; import type { Denops, Meta } from "jsr:@denops/core@6.0.6"; import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.0.0"; @@ -38,11 +41,7 @@ export class Service implements HostService, AsyncDisposable { this.#host = host; } - async load( - name: string, - script: string, - suffix = "", - ): Promise { + async load(name: string, script: string): Promise { if (this.#closed) { throw new Error("Service closed"); } @@ -58,26 +57,36 @@ export class Service implements HostService, AsyncDisposable { const denops = new DenopsImpl(name, this.#meta, this.#host, this); const plugin = new Plugin(denops, name, script); this.#plugins.set(name, plugin); - await plugin.load(suffix); - this.#getWaiter(name).resolve(); + try { + await plugin.waitLoaded(); + this.#getWaiter(name).resolve(); + } catch { + this.#plugins.delete(name); + } } - reload( - name: string, - ): Promise { + async #unload(name: string): Promise { const plugin = this.#plugins.get(name); if (!plugin) { if (this.#meta.mode === "debug") { console.log(`A denops plugin '${name}' is not loaded yet. Skip`); } - return Promise.resolve(); + return; } this.#plugins.delete(name); - this.#waiters.delete(name); - // Import module with fragment so that reload works properly - // https://github.com/vim-denops/denops.vim/issues/227 - const suffix = `#${performance.now()}`; - return this.load(name, plugin.script, suffix); + await plugin.unload(); + return plugin; + } + + async unload(name: string): Promise { + await this.#unload(name); + } + + async reload(name: string): Promise { + const plugin = await this.#unload(name); + if (plugin) { + await this.load(name, plugin.script); + } } waitLoaded(name: string): Promise { @@ -137,7 +146,7 @@ export class Service implements HostService, AsyncDisposable { } } - close(): Promise { + async close(): Promise { if (!this.#closed) { this.#closed = true; const error = new Error("Service closed"); @@ -145,6 +154,9 @@ export class Service implements HostService, AsyncDisposable { reject(error); } this.#waiters.clear(); + await Promise.all( + [...this.#plugins.values()].map((plugin) => plugin.unload()), + ); this.#plugins.clear(); this.#host = undefined; this.#closedWaiter.resolve(); @@ -161,8 +173,14 @@ export class Service implements HostService, AsyncDisposable { } } +type PluginModule = { + main: Entrypoint; +}; + class Plugin { #denops: Denops; + #loadedWaiter: Promise; + #disposable: Partial = {}; readonly name: string; readonly script: string; @@ -171,13 +189,19 @@ class Plugin { this.#denops = denops; this.name = name; this.script = resolveScriptUrl(script); + this.#loadedWaiter = this.#load(); } - async load(suffix = ""): Promise { + waitLoaded(): Promise { + return this.#loadedWaiter; + } + + async #load(): Promise { + const suffix = createScriptSuffix(this.script); try { await emit(this.#denops, `DenopsSystemPluginPre:${this.name}`); - const mod = await import(`${this.script}${suffix}`); - await mod.main(this.#denops); + const mod: PluginModule = await import(`${this.script}${suffix}`); + this.#disposable = await mod.main(this.#denops) ?? {}; await emit(this.#denops, `DenopsSystemPluginPost:${this.name}`); } catch (e) { // Show a warning message when Deno module cache issue is detected @@ -195,6 +219,27 @@ class Plugin { } console.error(`Failed to load plugin '${this.name}': ${e}`); await emit(this.#denops, `DenopsSystemPluginFail:${this.name}`); + throw e; + } + } + + async unload(): Promise { + try { + // Wait for the load to complete to make the events atomically. + await this.#loadedWaiter; + } catch { + // Load failed, do nothing + return; + } + try { + await emit(this.#denops, `DenopsSystemPluginUnloadPre:${this.name}`); + await this.#disposable[Symbol.asyncDispose]?.(); + await emit(this.#denops, `DenopsSystemPluginUnloadPost:${this.name}`); + } catch (e) { + console.error(`Failed to unload plugin '${this.name}': ${e}`); + await emit(this.#denops, `DenopsSystemPluginUnloadFail:${this.name}`); + } finally { + this.#disposable = {}; } } @@ -210,6 +255,16 @@ class Plugin { } } +const loadedScripts = new Set(); + +function createScriptSuffix(script: string): string { + // Import module with fragment so that reload works properly + // https://github.com/vim-denops/denops.vim/issues/227 + const suffix = loadedScripts.has(script) ? `#${performance.now()}` : ""; + loadedScripts.add(script); + return suffix; +} + async function emit(denops: Denops, name: string): Promise { try { await denops.cmd(`doautocmd User ${name}`); diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 0e2b4c24..5a778463 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -1,5 +1,6 @@ import { assert, + assertArrayIncludes, assertEquals, assertFalse, assertInstanceOf, @@ -13,6 +14,7 @@ import { assertSpyCall, assertSpyCalls, resolvesNext, + spy, stub, } from "jsr:@std/testing@0.224.0/mock"; import type { Meta } from "jsr:@denops/core@6.0.6"; @@ -25,6 +27,8 @@ const NOOP = () => {}; const scriptValid = resolve("dummy_valid_plugin.ts"); const scriptInvalid = resolve("dummy_invalid_plugin.ts"); +const scriptValidDispose = resolve("dummy_valid_dispose_plugin.ts"); +const scriptInvalidDispose = resolve("dummy_invalid_dispose_plugin.ts"); const scriptInvalidConstraint = resolve("dummy_invalid_constraint_plugin.ts"); const scriptInvalidConstraint2 = resolve("dummy_invalid_constraint_plugin2.ts"); @@ -301,6 +305,225 @@ Deno.test("Service", async (t) => { assertSpyCalls(host_call, 0); }); }); + + await t.step("if the plugin is already unloaded", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + await service.unload("dummy"); + } + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.load("dummy", scriptValid); + }); + + await t.step("emits DenopsSystemPluginPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + }); + }); + + await t.step("calls the plugin entrypoint", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginPost", () => { + assertSpyCall(host_call, 2, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + }); + }); + }); + }); + + await t.step(".unload()", async (t) => { + await t.step("if the plugin returns void", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.unload("dummy"); + }); + + await t.step("emits DenopsSystemPluginUnloadPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginUnloadPost", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + }); + }); + }); + + await t.step("if the plugin returns AsyncDisposable", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValidDispose); + } + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.unload("dummy"); + }); + + await t.step("emits DenopsSystemPluginUnloadPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + }); + }); + + await t.step("calls the plugin dispose method", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "echo 'Goodbye, Denops!'", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginUnloadPost", () => { + assertSpyCall(host_call, 2, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + }); + }); + }); + + await t.step("if the plugin dispose method throws", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptInvalidDispose); + } + using console_error = stub(console, "error"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.unload("dummy"); + }); + + await t.step("outputs an error message", () => { + assertSpyCall(console_error, 0, { + args: [ + "Failed to unload plugin 'dummy': Error: This is dummy error in async dispose", + ], + }); + }); + + await t.step("emits DenopsSystemPluginUnloadPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginUnloadFail", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadFail:dummy", + {}, + ], + }); + }); + }); + + await t.step("if the plugin is not yet loaded", async (t) => { + const service = new Service(meta); + service.bind(host); + using console_log = stub(console, "log"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.unload("dummy"); + }); + + await t.step("outputs a log message", () => { + assertSpyCall(console_log, 0, { + args: [ + "A denops plugin 'dummy' is not loaded yet. Skip", + ], + }); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); + + await t.step("if the plugin is already unloaded", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + await service.unload("dummy"); + } + using console_log = stub(console, "log"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.unload("dummy"); + }); + + await t.step("outputs a log message", () => { + assertSpyCall(console_log, 0, { + args: [ + "A denops plugin 'dummy' is not loaded yet. Skip", + ], + }); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); }); await t.step(".reload()", async (t) => { @@ -317,8 +540,28 @@ Deno.test("Service", async (t) => { await service.reload("dummy"); }); - await t.step("emits DenopsSystemPluginPre", () => { + await t.step("emits DenopsSystemPluginUnloadPre", () => { assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginUnloadPost", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginPre", () => { + assertSpyCall(host_call, 2, { args: [ "denops#api#cmd", "doautocmd User DenopsSystemPluginPre:dummy", @@ -328,7 +571,7 @@ Deno.test("Service", async (t) => { }); await t.step("calls the plugin entrypoint", () => { - assertSpyCall(host_call, 1, { + assertSpyCall(host_call, 3, { args: [ "denops#api#cmd", "echo 'Hello, Denops!'", @@ -338,7 +581,7 @@ Deno.test("Service", async (t) => { }); await t.step("emits DenopsSystemPluginPost", () => { - assertSpyCall(host_call, 2, { + assertSpyCall(host_call, 4, { args: [ "denops#api#cmd", "doautocmd User DenopsSystemPluginPost:dummy", @@ -370,6 +613,34 @@ Deno.test("Service", async (t) => { assertSpyCalls(host_call, 0); }); }); + + await t.step("if the plugin is already unloaded", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + await service.unload("dummy"); + } + using console_log = stub(console, "log"); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.reload("dummy"); + }); + + await t.step("outputs a log message", () => { + assertSpyCall(console_log, 0, { + args: [ + "A denops plugin 'dummy' is not loaded yet. Skip", + ], + }); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); }); await t.step(".waitLoaded()", async (t) => { @@ -810,12 +1081,47 @@ Deno.test("Service", async (t) => { await t.step(".close()", async (t) => { await t.step("if the service is not yet closed", async (t) => { const service = new Service(meta); - using _host_call = stub(host, "call"); service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + await service.load("dummyDispose", scriptValidDispose); + } + using host_call = stub(host, "call"); await t.step("resolves", async () => { await service.close(); }); + + await t.step("unloads loaded plugins", () => { + assertArrayIncludes(host_call.calls.map((c) => c.args), [ + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummyDispose", + {}, + ], + [ + "denops#api#cmd", + "echo 'Goodbye, Denops!'", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummyDispose", + {}, + ], + ]); + }); }); await t.step("if the service is already closed", async (t) => { @@ -868,7 +1174,7 @@ Deno.test("Service", async (t) => { const service = new Service(meta); using _host_call = stub(host, "call"); service.bind(host); - using service_close = stub(service, "close"); + using service_close = spy(service, "close"); await t.step("resolves", async () => { await service[Symbol.asyncDispose](); diff --git a/doc/denops.txt b/doc/denops.txt index 877a8360..b661646a 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -409,14 +409,37 @@ denops#plugin#discover() denops#plugin#load({name}, {script}) Loads a denops plugin. Use this function to load denops plugins that are not discovered by |denops#plugin#discover()|. - It invokes |User| |DenopsPluginPre|:{plugin} just before denops - executes a "main" function of the plugin and |User| - |DenopsPluginPost|:{plugin} just after denops executes a "main" - function of the plugin. + + Loading a plugin involves the following event steps: + + - |User| |DenopsPluginPre|:{plugin} is fired. + - The plugin is loaded and the "main" function is executed. + - If it succeeds, |User| |DenopsPluginPost|:{plugin} is fired. + - If it fails, |User| |DenopsPluginFail|:{plugin} is fired. + + *denops#plugin#unload()* +denops#plugin#unload({name}) + Unloads a denops plugin. It does nothing when the plugin is not loaded + yet. + + Unloading a plugin involves the following event steps: + + - |User| |DenopsPluginUnloadPre|:{plugin} is fired. + - The plugin's dispose callback is executed, if it exists. + - If it succeeds, |User| |DenopsPluginUnloadPost|:{plugin} is fired. + - If it fails, |User| |DenopsPluginUnloadFail|:{plugin} is fired. + + The above events may not be fired if the connection is forcibly + closed or the denops server is forcibly terminated due to timeout + or other reasons. Plugins should also use the |DenopsClosed| event. *denops#plugin#reload()* -denops#plugin#reload({name}) - Reloads a denops plugin. +denops#plugin#reload({plugin}[, {options}]) + Reloads a denops plugin. It does nothing when the plugin is not loaded + yet. + + It invokes |User| autocommand events. See |denops#plugin#load()| and + |denops#plugin#unload()| for details. *denops#plugin#check_type()* denops#plugin#check_type([{name}]) @@ -483,6 +506,24 @@ DenopsPluginPre:{plugin} *DenopsPluginPre* DenopsPluginPost:{plugin} *DenopsPluginPost* Fired after the "main" function of each plugin is called. {plugin} is the name of the target plugin. + Note that if the "main" function throws an error, it will not be fired. + +DenopsPluginFail:{plugin} *DenopsPluginFail* + Fired when the "main" function of each plugin throws an error. + {plugin} is the name of the target plugin. + +DenopsPluginUnloadPre:{plugin} *DenopsPluginUnloadPre* + Fired before each plugin is unloaded. + {plugin} is the name of the target plugin. + +DenopsPluginUnloadPost:{plugin} *DenopsPluginUnloadPost* + Fired after each plugin is unloaded. + {plugin} is the name of the target plugin. + Note that if an error is thrown when unloading, it will not be fired. + +DenopsPluginUnloadFail:{plugin} *DenopsPluginUnloadFail* + Fired if an error is thrown when unloading each plugin. + {plugin} is the name of the target plugin. DenopsProcessStarted *DenopsProcessStarted* Fires when the Denops local server process is started. diff --git a/plugin/denops.vim b/plugin/denops.vim index 66d10a1b..96e40ed0 100644 --- a/plugin/denops.vim +++ b/plugin/denops.vim @@ -22,6 +22,9 @@ augroup denops_plugin_internal autocmd User DenopsPluginPre:* : autocmd User DenopsPluginPost:* : autocmd User DenopsPluginFail:* : + autocmd User DenopsPluginUnloadPre:* : + autocmd User DenopsPluginUnloadPost:* : + autocmd User DenopsPluginUnloadFail:* : autocmd User DenopsReady call denops#plugin#discover() augroup END diff --git a/plugin/denops/debug.vim b/plugin/denops/debug.vim index 6b09bb57..aa306b57 100644 --- a/plugin/denops/debug.vim +++ b/plugin/denops/debug.vim @@ -9,6 +9,8 @@ augroup denops_debug_plugin_internal \ call denops#_internal#echo#debug(expand(':t')) autocmd User DenopsStarted,DenopsListen:*,DenopsStopped:* \ call denops#_internal#echo#debug(expand(':t')) - autocmd User DenopsPluginPre:*,DenopsPluginPost:* + autocmd User DenopsPluginPre:*,DenopsPluginPost:*,DenopsPluginFail:* + \ call denops#_internal#echo#debug(expand(':t')) + autocmd User DenopsPluginUnloadPre:*,DenopsPluginUnloadPost:*,DenopsPluginUnloadFail:* \ call denops#_internal#echo#debug(expand(':t')) augroup END diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index b680fb36..76514549 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -14,6 +14,8 @@ const MESSAGE_DELAY = 200; const scriptValid = resolve("dummy_valid_plugin.ts"); const scriptInvalid = resolve("dummy_invalid_plugin.ts"); +const scriptValidDispose = resolve("dummy_valid_dispose_plugin.ts"); +const scriptInvalidDispose = resolve("dummy_invalid_dispose_plugin.ts"); const runtimepathPlugin = resolve("dummy_plugins"); testHost({ @@ -99,9 +101,7 @@ testHost({ await t.step("does not load a denops plugin", async () => { const actual = wait( - async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummy"), + () => host.call("eval", "len(g:__test_denops_events)"), { timeout: 1000, interval: 100 }, ); await assertRejects(() => actual, Error, "Timeout"); @@ -147,6 +147,133 @@ testHost({ ); }); + await t.step("denops#plugin#unload()", async (t) => { + await t.step("if the plugin is already loaded", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyUnload', '${scriptValidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyUnload") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#unload('dummyUnload')", + ], ""); + + await t.step("unloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyUnload") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyUnload", + "DenopsPluginUnloadPost:dummyUnload", + ]); + }); + + await t.step("calls the plugin dispose method", () => { + assertMatch(outputs.join(""), /Goodbye, Denops!/); + }); + }); + + await t.step("if the plugin dispose method throws", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyUnloadInvalid', '${scriptInvalidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyUnloadInvalid") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#unload('dummyUnloadInvalid')", + ], ""); + + await t.step("unloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadFail:dummyUnloadInvalid") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyUnloadInvalid", + "DenopsPluginUnloadFail:dummyUnloadInvalid", + ]); + }); + + await t.step("outputs an error message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to unload plugin 'dummyUnloadInvalid': Error: This is dummy error in async dispose/, + ); + }); + }); + + await t.step("if the plugin is not yet loaded", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#unload('notexistsplugin')", + ], ""); + + await t.step("does not unload a denops plugin", async () => { + const actual = wait( + () => host.call("eval", "len(g:__test_denops_events)"), + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("does not fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), []); + }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + + // NOTE: Depends on 'dummyUnload' which was already unloaded in the test above. + await t.step("if the plugin is already unloaded", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#unload('dummyUnload')", + ], ""); + + await t.step("does not unload a denops plugin", async () => { + const actual = wait( + () => host.call("eval", "len(g:__test_denops_events)"), + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("does not fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), []); + }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + }); + await t.step("denops#plugin#reload()", async (t) => { // NOTE: Depends on 'dummy' which was already loaded in the test above. await t.step("if the plugin is already loaded", async (t) => { @@ -165,6 +292,8 @@ testHost({ await t.step("fires DenopsPlugin* events", async () => { assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummy", + "DenopsPluginUnloadPost:dummy", "DenopsPluginPre:dummy", "DenopsPluginPost:dummy", ]); @@ -175,6 +304,47 @@ testHost({ }); }); + await t.step("if the plugin dispose method throws", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyReloadInvalid', '${scriptInvalidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadInvalid") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#reload('dummyReloadInvalid')", + ], ""); + + await t.step("reloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadInvalid") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyReloadInvalid", + "DenopsPluginUnloadFail:dummyReloadInvalid", + "DenopsPluginPre:dummyReloadInvalid", + "DenopsPluginPost:dummyReloadInvalid", + ]); + }); + + await t.step("outputs an error message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to unload plugin 'dummyReloadInvalid': Error: This is dummy error in async dispose/, + ); + }); + }); + await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; await host.call("execute", [ @@ -184,9 +354,33 @@ testHost({ await t.step("does not reload a denops plugin", async () => { const actual = wait( - async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummy"), + () => host.call("eval", "len(g:__test_denops_events)"), + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("does not fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), []); + }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + + // NOTE: Depends on 'dummyUnload' which was already unloaded in the test above. + await t.step("if the plugin is already unloaded", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#reload('dummyUnload')", + ], ""); + + await t.step("does not reload a denops plugin", async () => { + const actual = wait( + () => host.call("eval", "len(g:__test_denops_events)"), { timeout: 1000, interval: 100 }, ); await assertRejects(() => actual, Error, "Timeout"); diff --git a/tests/denops/testdata/dummy_invalid_dispose_plugin.ts b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts new file mode 100644 index 00000000..eafad403 --- /dev/null +++ b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts @@ -0,0 +1,11 @@ +// TODO: #349 Import `Entrypoint` from denops-core. +// import type { Entrypoint } from "jsr:@denops/core@7.0.0"; +import type { Entrypoint } from "/denops-private/plugin.ts"; + +export const main: Entrypoint = (_denops) => { + return { + [Symbol.asyncDispose]: () => { + throw new Error("This is dummy error in async dispose"); + }, + }; +}; diff --git a/tests/denops/testdata/dummy_valid_dispose_plugin.ts b/tests/denops/testdata/dummy_valid_dispose_plugin.ts new file mode 100644 index 00000000..65d7aebe --- /dev/null +++ b/tests/denops/testdata/dummy_valid_dispose_plugin.ts @@ -0,0 +1,11 @@ +// TODO: #349 Import `Entrypoint` from denops-core. +// import type { Entrypoint } from "jsr:@denops/core@7.0.0"; +import type { Entrypoint } from "/denops-private/plugin.ts"; + +export const main: Entrypoint = (denops) => { + return { + [Symbol.asyncDispose]: async () => { + await denops.cmd("echo 'Goodbye, Denops!'"); + }, + }; +}; From 7c79693b2422d05630cd408469c02d6360645077 Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 3 Jul 2024 21:03:25 +0900 Subject: [PATCH 59/99] :herb: add script rewrite reload test This breaks `--watch` option. --- deno.jsonc | 2 +- denops/@denops-private/service_test.ts | 92 ++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 1e196b23..fe5572d5 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -3,7 +3,7 @@ "check": "deno check **/*.ts", "test": "LANG=C deno test -A --parallel --shuffle --doc", "test:coverage": "deno task test --coverage=.coverage", - "coverage": "deno coverage .coverage", + "coverage": "deno coverage --exclude=\"test[.]ts(#.*)?$\" .coverage", "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", "update:commit": "deno task -q update --commit --pre-commit=fmt,lint" }, diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 5a778463..f29bc5e0 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -22,6 +22,7 @@ import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import type { Host } from "./denops.ts"; import { Service } from "./service.ts"; +import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; const NOOP = () => {}; @@ -641,6 +642,87 @@ Deno.test("Service", async (t) => { assertSpyCalls(host_call, 0); }); }); + + await t.step("if the plugin file is changed", async (t) => { + // Generate source script file. + await using tempFile = await useTempFile({ + // NOTE: Temporary script files should be ignored from coverage. + prefix: "test-denops-service-", + suffix: "_test.ts", + }); + const scriptRewrite = toFileUrl(tempFile.path).href; + const sourceOriginal = await Deno.readTextFile(new URL(scriptValid)); + await Deno.writeTextFile(new URL(scriptRewrite), sourceOriginal); + + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptRewrite); + } + using host_call = stub(host, "call"); + + // Change source script file. + const sourceRewrited = sourceOriginal.replaceAll( + "Hello, Denops!", + "Source Changed!", + ); + await Deno.writeTextFile(new URL(scriptRewrite), sourceRewrited); + + await t.step("resolves", async () => { + await service.reload("dummy"); + }); + + await t.step("emits DenopsSystemPluginUnloadPre", () => { + assertSpyCall(host_call, 0, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginUnloadPost", () => { + assertSpyCall(host_call, 1, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginPre", () => { + assertSpyCall(host_call, 2, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + }); + }); + + await t.step("calls the plugin entrypoint", () => { + assertSpyCall(host_call, 3, { + args: [ + "denops#api#cmd", + "echo 'Source Changed!'", + {}, + ], + }); + }); + + await t.step("emits DenopsSystemPluginPost", () => { + assertSpyCall(host_call, 4, { + args: [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + }); + }); + }); }); await t.step(".waitLoaded()", async (t) => { @@ -1190,3 +1272,13 @@ Deno.test("Service", async (t) => { function resolve(path: string): string { return new URL(`../../tests/denops/testdata/${path}`, import.meta.url).href; } + +async function useTempFile(options?: Deno.MakeTempOptions) { + const path = await Deno.makeTempFile(options); + return { + path, + async [Symbol.asyncDispose]() { + await Deno.remove(path, { recursive: true }); + }, + }; +} From 93a4669824a52e729dbca6c721ae5791fc798147 Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 6 Jul 2024 20:35:09 +0900 Subject: [PATCH 60/99] :herb: improve test coverage --- denops/@denops-private/service_test.ts | 110 +++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index f29bc5e0..367bdd1e 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -525,6 +525,116 @@ Deno.test("Service", async (t) => { assertSpyCalls(host_call, 0); }); }); + + await t.step("if called before `load()` resolves", async (t) => { + const service = new Service(meta); + service.bind(host); + using host_call = stub(host, "call"); + + const loadPromise = service.load("dummy", scriptValid); + const unloadPromise = service.unload("dummy"); + + await t.step("resolves", async () => { + await unloadPromise; + }); + + await t.step("`load()` was resolved", async () => { + assertEquals(await promiseState(loadPromise), "fulfilled"); + }); + + await t.step("emits events in the correct order", () => { + const events = host_call.calls + .map((c) => c.args) + .filter((args) => (args[1] as string)?.startsWith("doautocmd")); + assertEquals(events, [ + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + ]); + }); + }); + + await t.step("if called before `load()` resolves with error", async (t) => { + const service = new Service(meta); + service.bind(host); + using host_call = stub(host, "call"); + + const loadPromise = service.load("dummy", scriptInvalid); + const unloadPromise = service.unload("dummy"); + + await t.step("resolves", async () => { + await unloadPromise; + }); + + await t.step("`load()` was resolved", async () => { + assertEquals(await promiseState(loadPromise), "fulfilled"); + }); + + await t.step("emits events in the correct order", () => { + const events = host_call.calls + .map((c) => c.args) + .filter((args) => (args[1] as string)?.startsWith("doautocmd")); + assertEquals(events, [ + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginFail:dummy", + {}, + ], + ]); + }); + }); + + await t.step("if `host.call()` rejects (channel closed)", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using console_error = stub(console, "error"); + using _host_call = stub( + host, + "call", + () => Promise.reject(new Error("channel closed")), + ); + + await t.step("resolves", async () => { + await service.unload("dummy"); + }); + + await t.step("outputs error messages", () => { + assertEquals(console_error.calls.map((c) => c.args), [ + [ + "Failed to emit DenopsSystemPluginUnloadPre:dummy: Error: channel closed", + ], + [ + "Failed to emit DenopsSystemPluginUnloadPost:dummy: Error: channel closed", + ], + ]); + }); + }); }); await t.step(".reload()", async (t) => { From 6fdb1cf98fdeadb207569304bcb8de1ccfc7869f Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 6 Jul 2024 20:37:01 +0900 Subject: [PATCH 61/99] :+1: fix `Service.waitLoaded()` never resolved It is never resolved if it is called between `load()` and `unload()`. --- denops/@denops-private/service.ts | 3 +++ denops/@denops-private/service_test.ts | 28 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 4552a5cd..c992bbbd 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -73,6 +73,9 @@ export class Service implements HostService, AsyncDisposable { } return; } + this.#waiters.get(name)?.promise.finally(() => { + this.#waiters.delete(name); + }); this.#plugins.delete(name); await plugin.unload(); return plugin; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 367bdd1e..30e5e478 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -846,6 +846,18 @@ Deno.test("Service", async (t) => { assertEquals(await promiseState(actual), "pending"); }); + await t.step("pendings if the plugin is already unloaded", async () => { + const service = new Service(meta); + service.bind(host); + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + await service.unload("dummy"); + + const actual = service.waitLoaded("dummy"); + + assertEquals(await promiseState(actual), "pending"); + }); + await t.step("resolves if the plugin is already loaded", async () => { const service = new Service(meta); service.bind(host); @@ -868,6 +880,22 @@ Deno.test("Service", async (t) => { assertEquals(await promiseState(actual), "fulfilled"); }); + await t.step( + "resolves if it is called between `load()` and `unload()`", + async () => { + const service = new Service(meta); + service.bind(host); + using _host_call = stub(host, "call"); + + const loadPromise = service.load("dummy", scriptValid); + const actual = service.waitLoaded("dummy"); + const unloadPromise = service.unload("dummy"); + await Promise.all([loadPromise, unloadPromise]); + + assertEquals(await promiseState(actual), "fulfilled"); + }, + ); + await t.step("rejects if the service is already closed", async () => { const service = new Service(meta); service.bind(host); From 8070bd5a0401742706974869586c433cea9a1141 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 7 Jul 2024 15:32:20 +0900 Subject: [PATCH 62/99] :+1: do not use `Partial` --- denops/@denops-private/service.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index c992bbbd..cdd949d7 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -183,7 +183,7 @@ type PluginModule = { class Plugin { #denops: Denops; #loadedWaiter: Promise; - #disposable: Partial = {}; + #disposable: AsyncDisposable = voidAsyncDisposable; readonly name: string; readonly script: string; @@ -204,7 +204,7 @@ class Plugin { try { await emit(this.#denops, `DenopsSystemPluginPre:${this.name}`); const mod: PluginModule = await import(`${this.script}${suffix}`); - this.#disposable = await mod.main(this.#denops) ?? {}; + this.#disposable = await mod.main(this.#denops) ?? voidAsyncDisposable; await emit(this.#denops, `DenopsSystemPluginPost:${this.name}`); } catch (e) { // Show a warning message when Deno module cache issue is detected @@ -236,13 +236,13 @@ class Plugin { } try { await emit(this.#denops, `DenopsSystemPluginUnloadPre:${this.name}`); - await this.#disposable[Symbol.asyncDispose]?.(); + await this.#disposable[Symbol.asyncDispose](); await emit(this.#denops, `DenopsSystemPluginUnloadPost:${this.name}`); } catch (e) { console.error(`Failed to unload plugin '${this.name}': ${e}`); await emit(this.#denops, `DenopsSystemPluginUnloadFail:${this.name}`); } finally { - this.#disposable = {}; + this.#disposable = voidAsyncDisposable; } } @@ -258,6 +258,10 @@ class Plugin { } } +const voidAsyncDisposable = { + [Symbol.asyncDispose]: () => Promise.resolve(), +} as const satisfies AsyncDisposable; + const loadedScripts = new Set(); function createScriptSuffix(script: string): string { From 00b0b02e94f6b34fe3c40a38f5da9da586339c78 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 7 Jul 2024 16:07:03 +0900 Subject: [PATCH 63/99] :package: Use @denops/core@7.0.0-pre0 instead --- denops/@denops-private/denops.ts | 9 +++++++-- denops/@denops-private/denops_test.ts | 2 +- denops/@denops-private/plugin.ts | 2 +- denops/@denops-private/service.ts | 6 ++---- denops/@denops-private/service_test.ts | 2 +- denops/@denops-private/util.ts | 2 +- denops/@denops-private/worker.ts | 2 +- tests/denops/testdata/dummy_invalid_dispose_plugin.ts | 4 +--- tests/denops/testdata/dummy_valid_dispose_plugin.ts | 4 +--- tests/denops/testutil/mock.ts | 2 +- 10 files changed, 17 insertions(+), 18 deletions(-) diff --git a/denops/@denops-private/denops.ts b/denops/@denops-private/denops.ts index bb3e5f54..42cb9379 100644 --- a/denops/@denops-private/denops.ts +++ b/denops/@denops-private/denops.ts @@ -1,5 +1,10 @@ -import type { Context, Denops, Dispatcher, Meta } from "jsr:@denops/core@6.0.6"; -import { BatchError } from "jsr:@denops/core@6.0.6"; +import type { + Context, + Denops, + Dispatcher, + Meta, +} from "jsr:@denops/core@7.0.0-pre0"; +import { BatchError } from "jsr:@denops/core@7.0.0-pre0"; import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; import type { Host as HostOrigin } from "./host.ts"; import type { Service as ServiceOrigin } from "./service.ts"; diff --git a/denops/@denops-private/denops_test.ts b/denops/@denops-private/denops_test.ts index 9277a2ed..2e09e865 100644 --- a/denops/@denops-private/denops_test.ts +++ b/denops/@denops-private/denops_test.ts @@ -1,4 +1,4 @@ -import type { Meta } from "jsr:@denops/core@6.0.6"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; import { assertEquals, assertInstanceOf } from "jsr:@std/assert@0.225.1"; import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; diff --git a/denops/@denops-private/plugin.ts b/denops/@denops-private/plugin.ts index 00c9441b..eabb8b5a 100644 --- a/denops/@denops-private/plugin.ts +++ b/denops/@denops-private/plugin.ts @@ -1,5 +1,5 @@ // TODO: #349 Update `Entrypoint` in denops-core, remove this module from `$.test.exclude` in `deno.jsonc`, and remove this module. -import type { Denops } from "jsr:@denops/core@6.1.0"; +import type { Denops } from "jsr:@denops/core@7.0.0-pre0"; /** * Denops's entrypoint definition. diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index cdd949d7..52c14ce3 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -1,7 +1,5 @@ -// TODO: #349 Import `Entrypoint` from denops-core. -// import type { Entrypoint } from "jsr:@denops/core@7.0.0"; -import type { Entrypoint } from "./plugin.ts"; -import type { Denops, Meta } from "jsr:@denops/core@6.0.6"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre0"; +import type { Denops, Meta } from "jsr:@denops/core@7.0.0-pre0"; import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.0.0"; import { DenopsImpl, type Host } from "./denops.ts"; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 30e5e478..a6571890 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -17,7 +17,7 @@ import { spy, stub, } from "jsr:@std/testing@0.224.0/mock"; -import type { Meta } from "jsr:@denops/core@6.0.6"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import type { Host } from "./denops.ts"; diff --git a/denops/@denops-private/util.ts b/denops/@denops-private/util.ts index cd7ab15c..1e4b59ce 100644 --- a/denops/@denops-private/util.ts +++ b/denops/@denops-private/util.ts @@ -1,4 +1,4 @@ -import type { Meta } from "jsr:@denops/core@6.0.6"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; import { is, type Predicate } from "jsr:@core/unknownutil@3.18.0"; export const isMeta: Predicate = is.ObjectOf({ diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index 06fcb7b3..c64f6538 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -8,7 +8,7 @@ import { import { ensure } from "jsr:@core/unknownutil@3.18.0"; import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; -import type { Meta } from "jsr:@denops/core@6.0.6"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; import type { Host, HostConstructor } from "./host.ts"; import { Vim } from "./host/vim.ts"; import { Neovim } from "./host/nvim.ts"; diff --git a/tests/denops/testdata/dummy_invalid_dispose_plugin.ts b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts index eafad403..da86dfac 100644 --- a/tests/denops/testdata/dummy_invalid_dispose_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts @@ -1,6 +1,4 @@ -// TODO: #349 Import `Entrypoint` from denops-core. -// import type { Entrypoint } from "jsr:@denops/core@7.0.0"; -import type { Entrypoint } from "/denops-private/plugin.ts"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre0"; export const main: Entrypoint = (_denops) => { return { diff --git a/tests/denops/testdata/dummy_valid_dispose_plugin.ts b/tests/denops/testdata/dummy_valid_dispose_plugin.ts index 65d7aebe..a71bf2bb 100644 --- a/tests/denops/testdata/dummy_valid_dispose_plugin.ts +++ b/tests/denops/testdata/dummy_valid_dispose_plugin.ts @@ -1,6 +1,4 @@ -// TODO: #349 Import `Entrypoint` from denops-core. -// import type { Entrypoint } from "jsr:@denops/core@7.0.0"; -import type { Entrypoint } from "/denops-private/plugin.ts"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre0"; export const main: Entrypoint = (denops) => { return { diff --git a/tests/denops/testutil/mock.ts b/tests/denops/testutil/mock.ts index fa8f890b..18e5d9b1 100644 --- a/tests/denops/testutil/mock.ts +++ b/tests/denops/testutil/mock.ts @@ -1,5 +1,5 @@ import { AssertionError, unimplemented } from "jsr:@std/assert@0.225.2"; -import type { Meta } from "jsr:@denops/core@6.0.6"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; /** Returns a Promise that is never resolves or rejects. */ export function pendingPromise(): Promise { From b83586b98b596d824988bf33ca3de988d9f16d3c Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 7 Jul 2024 16:11:47 +0900 Subject: [PATCH 64/99] :memo: Use JSR instead of deno.land --- doc/denops.txt | 2 +- tests/denops/testdata/dummy_invalid_constraint_plugin.ts | 2 +- tests/denops/testdata/dummy_invalid_constraint_plugin2.ts | 2 +- tests/denops/testdata/dummy_invalid_plugin.ts | 2 +- .../testdata/dummy_plugins/denops/@dummy_namespace/main.ts | 2 +- .../denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts | 2 +- tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts | 2 +- tests/denops/testdata/dummy_valid_plugin.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/denops.txt b/doc/denops.txt index b661646a..1a25304e 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -22,7 +22,7 @@ Visit the Denops Document or Wiki for guidance on creating denops plugins. Denops Document: https://vim-denops.github.io/denops-documentation/ Wiki: https://github.com/vim-denops/denops.vim/wiki -API Reference: https://deno.land/x/denops_std/mod.ts +API Reference: https://jsr.io/@denops/std ============================================================================= diff --git a/tests/denops/testdata/dummy_invalid_constraint_plugin.ts b/tests/denops/testdata/dummy_invalid_constraint_plugin.ts index 7d490768..34f9b453 100644 --- a/tests/denops/testdata/dummy_invalid_constraint_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_constraint_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; +import type { Entrypoint } from "jsr:@denops/core"; export const main: Entrypoint = (_denops) => { // Mimic the situation diff --git a/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts b/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts index d0d8c59c..ffa4e46b 100644 --- a/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts +++ b/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; +import type { Entrypoint } from "jsr:@denops/core"; export const main: Entrypoint = (_denops) => { // Mimic the situation diff --git a/tests/denops/testdata/dummy_invalid_plugin.ts b/tests/denops/testdata/dummy_invalid_plugin.ts index 5cb57ab6..da68b918 100644 --- a/tests/denops/testdata/dummy_invalid_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; +import type { Entrypoint } from "jsr:@denops/core"; export const main: Entrypoint = (_denops) => { throw new Error("This is dummy error"); diff --git a/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts b/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts index 9eadbb3b..f51df3b1 100644 --- a/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; +import type { Entrypoint } from "jsr:@denops/core"; // NOTE: This should not be called, a directory starting with '@' is not a denops plugin. export const main: Entrypoint = async (denops) => { diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts index 5cb57ab6..da68b918 100644 --- a/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; +import type { Entrypoint } from "jsr:@denops/core"; export const main: Entrypoint = (_denops) => { throw new Error("This is dummy error"); diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts index 9d9b73f5..fff30d63 100644 --- a/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; +import type { Entrypoint } from "jsr:@denops/core"; export const main: Entrypoint = async (denops) => { await denops.cmd("echo 'Hello, Denops!'"); diff --git a/tests/denops/testdata/dummy_valid_plugin.ts b/tests/denops/testdata/dummy_valid_plugin.ts index bfda6538..5c8dd2a8 100644 --- a/tests/denops/testdata/dummy_valid_plugin.ts +++ b/tests/denops/testdata/dummy_valid_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "https://deno.land/x/denops_core@v6.1.0/mod.ts"; +import type { Entrypoint } from "jsr:@denops/core"; export const main: Entrypoint = async (denops) => { denops.dispatcher = { From d9c16e8ce9b30c963dd2ae519640e3f2ec27e252 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 7 Jul 2024 17:19:25 +0900 Subject: [PATCH 65/99] :+1: remove unused module Refs #349 --- deno.jsonc | 6 ------ denops/@denops-private/plugin.ts | 35 ------------------------------- denops/@denops-private/service.ts | 3 +-- 3 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 denops/@denops-private/plugin.ts diff --git a/deno.jsonc b/deno.jsonc index fe5572d5..eb713672 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -7,12 +7,6 @@ "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", "update:commit": "deno task -q update --commit --pre-commit=fmt,lint" }, - "test": { - "exclude": [ - // TODO: #349 Update `Entrypoint` in denops-core, and remove this entry. - "denops/@denops-private/plugin.ts" - ] - }, "exclude": [ ".coverage/" ], diff --git a/denops/@denops-private/plugin.ts b/denops/@denops-private/plugin.ts deleted file mode 100644 index eabb8b5a..00000000 --- a/denops/@denops-private/plugin.ts +++ /dev/null @@ -1,35 +0,0 @@ -// TODO: #349 Update `Entrypoint` in denops-core, remove this module from `$.test.exclude` in `deno.jsonc`, and remove this module. -import type { Denops } from "jsr:@denops/core@7.0.0-pre0"; - -/** - * Denops's entrypoint definition. - * - * Use this type to ensure the `main` function is properly implemented like: - * - * ```ts - * import type { Entrypoint } from "jsr:@denops/core"; - * - * export const main: Entrypoint = (denops) => { - * // ... - * } - * ``` - * - * If an `AsyncDisposable` object is returned, resources can be disposed of - * asynchronously when the plugin is unloaded, like: - * - * ```ts - * import type { Entrypoint } from "jsr:@denops/core"; - * - * export const main: Entrypoint = (denops) => { - * // ... - * return { - * [Symbol.asyncDispose]: () => { - * // Dispose resources... - * } - * } - * } - * ``` - */ -export type Entrypoint = ( - denops: Denops, -) => void | AsyncDisposable | Promise; diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 52c14ce3..58e8bcfb 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -1,5 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre0"; -import type { Denops, Meta } from "jsr:@denops/core@7.0.0-pre0"; +import type { Denops, Entrypoint, Meta } from "jsr:@denops/core@7.0.0-pre0"; import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.0.0"; import { DenopsImpl, type Host } from "./denops.ts"; From 35c5758dedc0486faafccb5b3dfd2345be217a8f Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 8 Jul 2024 10:09:46 +0900 Subject: [PATCH 66/99] :package: Update `@denops/core` --- denops/@denops-private/denops.ts | 4 ++-- denops/@denops-private/denops_test.ts | 2 +- denops/@denops-private/service.ts | 2 +- denops/@denops-private/service_test.ts | 2 +- denops/@denops-private/util.ts | 2 +- denops/@denops-private/worker.ts | 2 +- tests/denops/testdata/dummy_invalid_dispose_plugin.ts | 2 +- tests/denops/testdata/dummy_valid_dispose_plugin.ts | 2 +- tests/denops/testutil/mock.ts | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/denops/@denops-private/denops.ts b/denops/@denops-private/denops.ts index 42cb9379..373a99b7 100644 --- a/denops/@denops-private/denops.ts +++ b/denops/@denops-private/denops.ts @@ -3,8 +3,8 @@ import type { Denops, Dispatcher, Meta, -} from "jsr:@denops/core@7.0.0-pre0"; -import { BatchError } from "jsr:@denops/core@7.0.0-pre0"; +} from "jsr:@denops/core@7.0.0-pre1"; +import { BatchError } from "jsr:@denops/core@7.0.0-pre1"; import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; import type { Host as HostOrigin } from "./host.ts"; import type { Service as ServiceOrigin } from "./service.ts"; diff --git a/denops/@denops-private/denops_test.ts b/denops/@denops-private/denops_test.ts index 2e09e865..3d4ef5e6 100644 --- a/denops/@denops-private/denops_test.ts +++ b/denops/@denops-private/denops_test.ts @@ -1,4 +1,4 @@ -import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; import { assertEquals, assertInstanceOf } from "jsr:@std/assert@0.225.1"; import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 58e8bcfb..4b9016df 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -1,4 +1,4 @@ -import type { Denops, Entrypoint, Meta } from "jsr:@denops/core@7.0.0-pre0"; +import type { Denops, Entrypoint, Meta } from "jsr:@denops/core@7.0.0-pre1"; import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.0.0"; import { DenopsImpl, type Host } from "./denops.ts"; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index a6571890..b388cbc6 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -17,7 +17,7 @@ import { spy, stub, } from "jsr:@std/testing@0.224.0/mock"; -import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import type { Host } from "./denops.ts"; diff --git a/denops/@denops-private/util.ts b/denops/@denops-private/util.ts index 1e4b59ce..3f906fa5 100644 --- a/denops/@denops-private/util.ts +++ b/denops/@denops-private/util.ts @@ -1,4 +1,4 @@ -import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; import { is, type Predicate } from "jsr:@core/unknownutil@3.18.0"; export const isMeta: Predicate = is.ObjectOf({ diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index c64f6538..c6e16b20 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -8,7 +8,7 @@ import { import { ensure } from "jsr:@core/unknownutil@3.18.0"; import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; -import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; import type { Host, HostConstructor } from "./host.ts"; import { Vim } from "./host/vim.ts"; import { Neovim } from "./host/nvim.ts"; diff --git a/tests/denops/testdata/dummy_invalid_dispose_plugin.ts b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts index da86dfac..2d4302d7 100644 --- a/tests/denops/testdata/dummy_invalid_dispose_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre0"; +import type { Entrypoint } from "jsr:@denops/core"; export const main: Entrypoint = (_denops) => { return { diff --git a/tests/denops/testdata/dummy_valid_dispose_plugin.ts b/tests/denops/testdata/dummy_valid_dispose_plugin.ts index a71bf2bb..176a461c 100644 --- a/tests/denops/testdata/dummy_valid_dispose_plugin.ts +++ b/tests/denops/testdata/dummy_valid_dispose_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre0"; +import type { Entrypoint } from "jsr:@denops/core"; export const main: Entrypoint = (denops) => { return { diff --git a/tests/denops/testutil/mock.ts b/tests/denops/testutil/mock.ts index 18e5d9b1..f0895fc7 100644 --- a/tests/denops/testutil/mock.ts +++ b/tests/denops/testutil/mock.ts @@ -1,5 +1,5 @@ import { AssertionError, unimplemented } from "jsr:@std/assert@0.225.2"; -import type { Meta } from "jsr:@denops/core@7.0.0-pre0"; +import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; /** Returns a Promise that is never resolves or rejects. */ export function pendingPromise(): Promise { From 416255df62fb16c8490a48f6d49b46a29c8583f0 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 8 Jul 2024 10:54:16 +0900 Subject: [PATCH 67/99] :package: Specify versions on testdata --- tests/denops/testdata/dummy_invalid_constraint_plugin.ts | 2 +- tests/denops/testdata/dummy_invalid_constraint_plugin2.ts | 2 +- tests/denops/testdata/dummy_invalid_dispose_plugin.ts | 2 +- tests/denops/testdata/dummy_invalid_plugin.ts | 2 +- .../testdata/dummy_plugins/denops/@dummy_namespace/main.ts | 2 +- .../denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts | 2 +- tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts | 2 +- tests/denops/testdata/dummy_valid_dispose_plugin.ts | 2 +- tests/denops/testdata/dummy_valid_plugin.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/denops/testdata/dummy_invalid_constraint_plugin.ts b/tests/denops/testdata/dummy_invalid_constraint_plugin.ts index 34f9b453..2c5356e8 100644 --- a/tests/denops/testdata/dummy_invalid_constraint_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_constraint_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; export const main: Entrypoint = (_denops) => { // Mimic the situation diff --git a/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts b/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts index ffa4e46b..7e903ac4 100644 --- a/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts +++ b/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; export const main: Entrypoint = (_denops) => { // Mimic the situation diff --git a/tests/denops/testdata/dummy_invalid_dispose_plugin.ts b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts index 2d4302d7..314abf8d 100644 --- a/tests/denops/testdata/dummy_invalid_dispose_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; export const main: Entrypoint = (_denops) => { return { diff --git a/tests/denops/testdata/dummy_invalid_plugin.ts b/tests/denops/testdata/dummy_invalid_plugin.ts index da68b918..c98c9db1 100644 --- a/tests/denops/testdata/dummy_invalid_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; export const main: Entrypoint = (_denops) => { throw new Error("This is dummy error"); diff --git a/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts b/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts index f51df3b1..db34c981 100644 --- a/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; // NOTE: This should not be called, a directory starting with '@' is not a denops plugin. export const main: Entrypoint = async (denops) => { diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts index da68b918..c98c9db1 100644 --- a/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; export const main: Entrypoint = (_denops) => { throw new Error("This is dummy error"); diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts index fff30d63..6e21fc82 100644 --- a/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; export const main: Entrypoint = async (denops) => { await denops.cmd("echo 'Hello, Denops!'"); diff --git a/tests/denops/testdata/dummy_valid_dispose_plugin.ts b/tests/denops/testdata/dummy_valid_dispose_plugin.ts index 176a461c..85f159da 100644 --- a/tests/denops/testdata/dummy_valid_dispose_plugin.ts +++ b/tests/denops/testdata/dummy_valid_dispose_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; export const main: Entrypoint = (denops) => { return { diff --git a/tests/denops/testdata/dummy_valid_plugin.ts b/tests/denops/testdata/dummy_valid_plugin.ts index 5c8dd2a8..7482eb4e 100644 --- a/tests/denops/testdata/dummy_valid_plugin.ts +++ b/tests/denops/testdata/dummy_valid_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; export const main: Entrypoint = async (denops) => { denops.dispatcher = { From e19b574ba3b715adaffe864405a1ffcf1384d7e6 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 7 Jul 2024 16:47:04 +0900 Subject: [PATCH 68/99] :herb: add host `invoke()` calls 'unload' tests --- denops/@denops-private/host_test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index a7989435..0d93d3d4 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -34,6 +34,21 @@ Deno.test("invoke", async (t) => { }); }); + await t.step("calls 'unload'", async (t) => { + await t.step("ok", async () => { + using s = stub(service, "unload"); + await invoke(service, "unload", ["dummy"]); + assertSpyCalls(s, 1); + assertSpyCall(s, 0, { args: ["dummy"] }); + }); + + await t.step("invalid args", () => { + using s = stub(service, "unload"); + assertThrows(() => invoke(service, "unload", []), AssertError); + assertSpyCalls(s, 0); + }); + }); + await t.step("calls 'reload'", async (t) => { await t.step("ok", async () => { using s = stub(service, "reload"); From 583a7bdeb80aee336492927dc34265dc06175c76 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 7 Jul 2024 19:12:58 +0900 Subject: [PATCH 69/99] :herb: improve `DenopsImpl` tests --- denops/@denops-private/denops_test.ts | 410 ++++++++++++++++++++++---- 1 file changed, 349 insertions(+), 61 deletions(-) diff --git a/denops/@denops-private/denops_test.ts b/denops/@denops-private/denops_test.ts index 3d4ef5e6..f845db46 100644 --- a/denops/@denops-private/denops_test.ts +++ b/denops/@denops-private/denops_test.ts @@ -1,10 +1,22 @@ -import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; -import { assertEquals, assertInstanceOf } from "jsr:@std/assert@0.225.1"; -import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; +import { BatchError, type Meta } from "jsr:@denops/core@7.0.0-pre1"; +import { + assertEquals, + assertInstanceOf, + assertRejects, + assertStrictEquals, + unimplemented, +} from "jsr:@std/assert@0.225.1"; +import { + assertSpyCallArgs, + assertSpyCalls, + resolvesNext, + stub, +} from "jsr:@std/testing@0.224.0/mock"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; -import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import { DenopsImpl, type Host, type Service } from "./denops.ts"; +type BatchReturn = [results: unknown[], errmsg: string]; + Deno.test("DenopsImpl", async (t) => { const meta: Meta = { mode: "release", @@ -28,81 +40,357 @@ Deno.test("DenopsImpl", async (t) => { assertInstanceOf(denops.interrupted, AbortSignal); }); - await t.step("redraw() calls host.redraw()", async () => { - using s = stub(host, "redraw"); - await denops.redraw(); - assertSpyCall(s, 0, { args: [undefined] }); + await t.step(".redraw()", async (t) => { + await t.step("calls host.redraw() without `force`", async () => { + using host_redraw = stub(host, "redraw", resolvesNext([undefined])); - await denops.redraw(false); - assertSpyCall(s, 1, { args: [false] }); + await denops.redraw(); - await denops.redraw(true); - assertSpyCall(s, 2, { args: [true] }); - }); + assertSpyCallArgs(host_redraw, 0, [undefined]); + }); - await t.step("call() calls host.call()", async () => { - using s = stub(host, "call"); - await denops.call("abs", -4); - assertSpyCall(s, 0, { args: ["abs", -4] }); + await t.step("calls host.redraw() with `force=false`", async () => { + using host_redraw = stub(host, "redraw", resolvesNext([undefined])); + + await denops.redraw(false); + + assertSpyCallArgs(host_redraw, 0, [false]); + }); - await denops.call("abs", 10); - assertSpyCall(s, 1, { args: ["abs", 10] }); + await t.step("calls host.redraw() with `force=true`", async () => { + using host_redraw = stub(host, "redraw", resolvesNext([undefined])); + + await denops.redraw(true); + + assertSpyCallArgs(host_redraw, 0, [true]); + }); }); - await t.step("batch() calls host.batch()", async () => { - using s = stub( - host, - "batch", - () => Promise.resolve([[], ""] as [unknown[], string]), + await t.step(".call()", async (t) => { + await t.step("calls host.call() without `args`", async () => { + using host_call = stub(host, "call", resolvesNext([1])); + + await denops.call("bufnr"); + + assertSpyCallArgs(host_call, 0, ["bufnr"]); + }); + + await t.step("calls host.call() with one `args`", async () => { + using host_call = stub(host, "call", resolvesNext([4])); + + await denops.call("abs", -4); + + assertSpyCallArgs(host_call, 0, ["abs", -4]); + }); + + await t.step("calls host.call() with multiple `args`", async () => { + using host_call = stub(host, "call", resolvesNext([-1])); + + await denops.call("byteidx", "foobar", 42, false); + + assertSpyCallArgs(host_call, 0, ["byteidx", "foobar", 42, false]); + }); + + await t.step( + "calls host.call() with `args` omitting after `undefined`", + async () => { + using host_call = stub(host, "call", resolvesNext([3])); + + await denops.call("charidx", "foobar", 3, undefined, false); + + assertSpyCallArgs(host_call, 0, ["charidx", "foobar", 3]); + }, ); - await denops.batch(["abs", -4], ["abs", 10], ["abs", -9]); - assertSpyCall(s, 0, { - args: [["abs", -4], ["abs", 10], ["abs", -9]], + + await t.step("resolves a result of host.call()", async () => { + using _host_call = stub(host, "call", resolvesNext([42])); + + const actual = await denops.call("bufnr"); + + assertEquals(actual, 42); + }); + + await t.step("if host.call() rejects with an error", async (t) => { + await t.step("rejects with an error", async () => { + const error = new Error("test error in host.call"); + using _host_call = stub(host, "call", resolvesNext([error])); + + const actual = await assertRejects( + () => denops.call("foo", true), + ); + assertStrictEquals(actual, error); + }); }); }); - await t.step("cmd() calls host.call()", async () => { - using s = stub(host, "call"); - await denops.cmd("echo 'foo'"); - assertSpyCall(s, 0, { - args: ["denops#api#cmd", "echo 'foo'", {}], + await t.step(".batch()", async (t) => { + await t.step("calls host.batch() with `calls`", async () => { + using host_batch = stub( + host, + "batch", + resolvesNext([[[4, 10, 9], ""]]), + ); + + await denops.batch(["abs", -4], ["abs", 10], ["abs", -9]); + + assertSpyCallArgs(host_batch, 0, [["abs", -4], ["abs", 10], ["abs", -9]]); + }); + + await t.step( + "calls host.batch() with `calls` omitting arguments after `undefined`", + async () => { + using host_batch = stub( + host, + "batch", + resolvesNext([[["", ""], ""]]), + ); + + await denops.batch( + ["findfile", "foo", undefined, 42], + ["getreg", undefined, 1, true], + ); + + assertSpyCallArgs(host_batch, 0, [["findfile", "foo"], ["getreg"]]); + }, + ); + + await t.step("resolves results of host.batch()", async () => { + using _host_batch = stub( + host, + "batch", + resolvesNext([[[4, 10, 9], ""]]), + ); + + const actual = await denops.batch(["abs", -4], ["abs", 10], ["abs", -9]); + + assertEquals(actual, [4, 10, 9]); + }); + + await t.step( + "if host.batch() resolves with an error message", + async (t) => { + await t.step("rejects with BatchError", async () => { + using _host_batch = stub( + host, + "batch", + resolvesNext([[[], "test error in host.batch"]]), + ); + + await assertRejects( + () => denops.batch(["foo", true]), + BatchError, + "test error in host.batch", + ); + }); + }, + ); + + await t.step("if host.batch() rejects with an error", async (t) => { + await t.step("rejects with an error", async () => { + const error = new Error("test error in host.batch"); + using _host_batch = stub( + host, + "batch", + resolvesNext([error]), + ); + + const actual = await assertRejects( + () => denops.batch(["foo", true]), + ); + assertStrictEquals(actual, error); + }); }); }); - await t.step("eval() calls host.call()", async () => { - using s = stub(host, "call"); - await denops.eval("v:version"); - assertSpyCall(s, 0, { - args: ["denops#api#eval", "v:version", {}], + await t.step(".cmd()", async (t) => { + await t.step("calls host.call() without `ctx`", async () => { + using host_call = stub(host, "call", resolvesNext([undefined])); + + await denops.cmd("echo 'foo'"); + + assertSpyCallArgs(host_call, 0, ["denops#api#cmd", "echo 'foo'", {}]); + }); + + await t.step("calls host.call() with `ctx`", async () => { + using host_call = stub(host, "call", resolvesNext([undefined])); + + await denops.cmd("echo 'foo'", { bar: 1, qux: true }); + + assertSpyCallArgs(host_call, 0, [ + "denops#api#cmd", + "echo 'foo'", + { bar: 1, qux: true }, + ]); + }); + + await t.step("if host.call() rejects with an error", async (t) => { + await t.step("rejects with an error", async () => { + const error = new Error("test error in host.call"); + using _host_call = stub(host, "call", resolvesNext([error])); + + const actual = await assertRejects( + () => denops.cmd("echo 'foo'"), + ); + assertStrictEquals(actual, error); + }); }); }); - await t.step("dispatch() calls service.dispatch()", async () => { - using s1 = stub(service, "waitLoaded", () => Promise.resolve()); - using s2 = stub(service, "dispatch", () => Promise.resolve()); - await denops.dispatch("dummy", "fn", "args"); - assertSpyCall(s1, 0, { - args: ["dummy"], + await t.step(".eval()", async (t) => { + await t.step("calls host.call() without `ctx`", async () => { + using host_call = stub(host, "call", resolvesNext([undefined])); + + await denops.eval("v:version"); + + assertSpyCallArgs(host_call, 0, ["denops#api#eval", "v:version", {}]); + }); + + await t.step("calls host.call() with `ctx`", async () => { + using host_call = stub(host, "call", resolvesNext([undefined])); + + await denops.eval("v:version", { foo: 1, bar: true }); + + assertSpyCallArgs(host_call, 0, [ + "denops#api#eval", + "v:version", + { foo: 1, bar: true }, + ]); }); - assertSpyCall(s2, 0, { - args: ["dummy", "fn", ["args"]], + + await t.step("resolves a result of host.call()", async () => { + using _host_call = stub(host, "call", resolvesNext([901])); + + const actual = await denops.eval("v:version"); + + assertEquals(actual, 901); + }); + + await t.step("if host.call() rejects with an error", async (t) => { + await t.step("rejects with an error", async () => { + const error = new Error("test error in host.call"); + using _host_call = stub(host, "call", resolvesNext([error])); + + const actual = await assertRejects( + () => denops.eval("v:version"), + ); + assertStrictEquals(actual, error); + }); }); }); - await t.step( - "dispatch() internally waits 'service.waitLoaded()' before 'service.dispatch()'", - async () => { - const { promise, resolve } = Promise.withResolvers(); - using s1 = stub(service, "waitLoaded", () => promise); - using s2 = stub(service, "dispatch", () => Promise.resolve()); - const p = denops.dispatch("dummy", "fn", "args"); - assertEquals(await promiseState(p), "pending"); - assertEquals(s1.calls.length, 1); - assertEquals(s2.calls.length, 0); - resolve(); - assertEquals(await promiseState(p), "fulfilled"); - assertEquals(s1.calls.length, 1); - assertEquals(s2.calls.length, 1); - }, - ); + await t.step(".dispatch()", async (t) => { + await t.step("if the plugin is already loaded", async (t) => { + await t.step("calls service.dispatch() without `args`", async () => { + using _service_waitLoaded = stub( + service, + "waitLoaded", + resolvesNext([undefined]), + ); + using service_dispatch = stub( + service, + "dispatch", + resolvesNext([undefined]), + ); + + await denops.dispatch("dummy", "fn"); + + assertSpyCallArgs(service_dispatch, 0, ["dummy", "fn", []]); + }); + + await t.step("calls service.dispatch() with `args`", async () => { + using _service_waitLoaded = stub( + service, + "waitLoaded", + resolvesNext([undefined]), + ); + using service_dispatch = stub( + service, + "dispatch", + resolvesNext([undefined]), + ); + + await denops.dispatch( + "dummy", + "fn", + "args0", + undefined, + null, + true, + 42, + ); + + assertSpyCallArgs(service_dispatch, 0, [ + "dummy", + "fn", + ["args0", undefined, null, true, 42], + ]); + }); + + await t.step("resolves result of service.dispatch()", async () => { + using _service_waitLoaded = stub( + service, + "waitLoaded", + resolvesNext([undefined]), + ); + using _service_dispatch = stub( + service, + "dispatch", + resolvesNext([{ foo: "a", bar: 42, qux: true }]), + ); + + const actual = await denops.dispatch("dummy", "fn"); + + assertEquals(actual, { foo: "a", bar: 42, qux: true }); + }); + }); + + await t.step("if the plugin is not yet loaded", async (t) => { + await t.step("waits service.waitLoaded() resolves", async () => { + const waiter = Promise.withResolvers(); + using service_waitLoaded = stub( + service, + "waitLoaded", + resolvesNext([waiter.promise]), + ); + using service_dispatch = stub( + service, + "dispatch", + resolvesNext([undefined]), + ); + + const dispatchPromise = denops.dispatch("dummy", "fn", "args"); + + assertEquals(await promiseState(dispatchPromise), "pending"); + assertSpyCalls(service_waitLoaded, 1); + assertSpyCalls(service_dispatch, 0); + waiter.resolve(); + assertEquals(await promiseState(dispatchPromise), "fulfilled"); + assertSpyCalls(service_waitLoaded, 1); + assertSpyCalls(service_dispatch, 1); + }); + }); + + await t.step("if the service is closed", async (t) => { + await t.step("rejects and service.dispatch() never calls", async () => { + using service_waitLoaded = stub( + service, + "waitLoaded", + resolvesNext([new Error("Service closed")]), + ); + using service_dispatch = stub( + service, + "dispatch", + resolvesNext([undefined]), + ); + + await assertRejects( + () => denops.dispatch("dummy", "fn"), + Error, + "Service closed", + ); + assertSpyCallArgs(service_waitLoaded, 0, ["dummy"]); + assertSpyCalls(service_dispatch, 0); + }); + }); + }); }); From a211913fee7014102f6789358a1f541e039438a1 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 7 Jul 2024 20:20:10 +0900 Subject: [PATCH 70/99] :herb: add `getVersionOr()` tests --- denops/@denops-private/version_test.ts | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 denops/@denops-private/version_test.ts diff --git a/denops/@denops-private/version_test.ts b/denops/@denops-private/version_test.ts new file mode 100644 index 00000000..e49624ae --- /dev/null +++ b/denops/@denops-private/version_test.ts @@ -0,0 +1,90 @@ +import { assert, assertEquals } from "jsr:@std/assert@0.225.1"; +import { resolvesNext, stub } from "jsr:@std/testing@0.224.0/mock"; +import type { SemVer } from "jsr:@std/semver@0.224.0/types"; +import { is, type Predicate } from "jsr:@core/unknownutil@3.18.0"; +import { getVersionOr } from "./version.ts"; + +Deno.test("getVersionOr()", async (t) => { + const HAS_TAG = (await isInsideWorkTree()) && (await isWorkTreeHasTags()); + + await t.step({ + name: "resolves a SemVer from git tags", + ignore: !HAS_TAG, + fn: async () => { + const actual = await getVersionOr({}); + assert(isSemVer(actual)); + }, + }); + + await t.step("if git command fails", async (t) => { + await t.step("resolves with `fallback`", async () => { + using _deno_command_output = stub( + Deno.Command.prototype, + "output", + resolvesNext([{ + success: false, + code: 1, + signal: null, + stdout: new Uint8Array(), + stderr: new Uint8Array(), + }]), + ); + const actual = await getVersionOr({ foo: "fallback value" }); + assertEquals(actual, { foo: "fallback value" }); + }); + }); + + await t.step("if git command outputs invalid value", async (t) => { + await t.step("resolves with `fallback`", async () => { + using _deno_command_output = stub( + Deno.Command.prototype, + "output", + resolvesNext([{ + success: true, + code: 0, + signal: null, + stdout: new TextEncoder().encode("invalid value"), + stderr: new Uint8Array(), + }]), + ); + const actual = await getVersionOr({ foo: "fallback value" }); + assertEquals(actual, { foo: "fallback value" }); + }); + }); +}); + +const isSemVer = is.ObjectOf({ + major: is.Number, + minor: is.Number, + patch: is.Number, + prerelease: is.ArrayOf(is.UnionOf([is.String, is.Number])), + build: is.ArrayOf(is.String), +}) satisfies Predicate; + +async function isInsideWorkTree(): Promise { + const cmd = new Deno.Command("git", { + args: ["rev-parse", "--is-inside-work-tree"], + stdout: "piped", + }); + try { + const { stdout } = await cmd.output(); + return /^true/.test(new TextDecoder().decode(stdout)); + } catch (e) { + console.error(e); + return false; + } +} + +async function isWorkTreeHasTags(): Promise { + const cmd = new Deno.Command("git", { + args: ["describe", "--tags", "--always"], + stdout: "piped", + }); + try { + const { stdout } = await cmd.output(); + return /-/.test(new TextDecoder().decode(stdout)); + } catch (e) { + console.error(e); + return false; + } +} From 56e71b58957055dc65a1ac36511a794163ab0ed7 Mon Sep 17 00:00:00 2001 From: Milly Date: Mon, 8 Jul 2024 09:29:41 +0900 Subject: [PATCH 71/99] :herb: improve `Vim` and `Neovim` tests --- denops/@denops-private/host/nvim_test.ts | 482 +++++++++++++++++------ denops/@denops-private/host/vim_test.ts | 447 ++++++++++++++++----- 2 files changed, 707 insertions(+), 222 deletions(-) diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index 859e5de7..6afd6ef2 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -5,19 +5,22 @@ import { assertStringIncludes, } from "jsr:@std/assert@0.225.1"; import { - assertSpyCall, + assertSpyCallArgs, assertSpyCalls, + resolvesNext, stub, } from "jsr:@std/testing@0.224.0/mock"; import { delay } from "jsr:@std/async@0.224.0/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { Client } from "jsr:@lambdalisue/messagepack-rpc@2.4.0"; import { withNeovim } from "/denops-testutil/with.ts"; import type { Service } from "../host.ts"; import { Neovim } from "./nvim.ts"; +const NOTIFY_DELAY = 100; + Deno.test("Neovim", async (t) => { - let waitClosed: Promise | undefined; await withNeovim({ fn: async ({ reader, writer }) => { const service: Service = { @@ -32,142 +35,381 @@ Deno.test("Neovim", async (t) => { }; await using host = new Neovim(reader, writer); - await t.step( - "'invoke' message before init throws error", - async () => { - await assertRejects( - () => - host.call( - "denops#_internal#test#request", - "invoke", - ["reload", ["dummy"]], - ), - Error, - "Failed to call", - ); - }, - ); - await t.step("init() calls Service.bind()", async () => { - using s = stub(service, "bind"); - await host.init(service); - assertSpyCall(s, 0, { args: [host] }); + await t.step("before .init() calls", async (t) => { + await t.step("when handle message", async (t) => { + await t.step("'invoke' rejects", async () => { + await assertRejects( + () => + host.call( + "denops#_internal#test#request", + "invoke", + ["reload", ["dummy"]], + ), + Error, + "No service is registered in the host", + ); + }); + }); }); - await t.step("redraw() does nothing", async () => { - await host.redraw(); + await t.step(".init()", async (t) => { + await t.step("calls service.bind()", async () => { + using service_bind = stub(service, "bind"); + + await host.init(service); + + assertSpyCallArgs(service_bind, 0, [host]); + }); }); - await t.step("call() returns a result of the function", async () => { - const result = await host.call("abs", -4); - assertEquals(result, 4); + await t.step(".redraw()", async (t) => { + await t.step("does nothing", async () => { + await host.redraw(); + }); }); - await t.step( - "call() throws an error when failed to call the function", - async () => { - await assertRejects( - () => host.call("@@@@@", -4), - Error, - "Failed to call '@@@@@' in Neovim: Vim:E117: Unknown function: @@@@@ (code: 0)", - ); - }, - ); - - await t.step("batch() returns results of the functions", async () => { - const [ret, err] = await host.batch( - ["abs", -4], - ["abs", 10], - ["abs", -9], - ); - assertEquals(ret, [4, 10, 9]); - assertEquals(err, ""); + await t.step(".call()", async (t) => { + await t.step("resolves a result of `fn`", async () => { + const result = await host.call("abs", -4); + + assertEquals(result, 4); + }); + + await t.step("if `fn` does not exist", async (t) => { + await t.step("rejects with an error", async () => { + await assertRejects( + () => host.call("@@@@@", -4), + Error, + "Failed to call '@@@@@' in Neovim: Vim:E117: Unknown function: @@@@@ (code: 0)", + ); + }); + }); + + await t.step("if Client error occurs", async (t) => { + await t.step("rejects with an error", async () => { + using _client_call = stub( + Client.prototype, + "call", + resolvesNext([new Error("Client call error")]), + ); + + await assertRejects( + () => host.call("abs", -4), + Error, + "Client call error", + ); + }); + }); }); - await t.step( - "batch() returns resutls with an error when failed to call the function", - async () => { + await t.step(".batch()", async (t) => { + await t.step("resolves results of `calls`", async () => { const [ret, err] = await host.batch( ["abs", -4], ["abs", 10], - ["@@@@@", -9], + ["abs", -9], ); - assertEquals(ret, [4, 10]); - assertMatch( - err, - /Failed to call '@@@@@' in Neovim: Vim:E117: Unknown function: @@@@@/, - ); - }, - ); - await t.step("notify() calls the function", () => { - host.notify("abs", -4); + assertEquals(ret, [4, 10, 9]); + assertEquals(err, ""); + }); + + await t.step("if some function does not exist", async (t) => { + await t.step("resolves resutls and an error", async () => { + const [ret, err] = await host.batch( + ["abs", -4], + ["abs", 10], + ["@@@@@", -9], + ["abs", 10], + ); + + assertEquals(ret, [4, 10]); + assertMatch( + err, + /^Failed to call '@@@@@' in Neovim: Vim:E117: Unknown function: @@@@@/, + ); + }); + + await t.step("does not call functions after failure", async () => { + await host.call("execute", [ + "let g:__test_host_batch_fn_calls = []", + "function! TestHostBatchFn(...) abort", + " call add(g:__test_host_batch_fn_calls, a:000)", + "endfunction", + ], ""); + + await host.batch( + ["TestHostBatchFn", -4], + ["TestHostBatchFn", 10], + ["@@@@@", 10], + ["TestHostBatchFn", -9], + ["TestHostBatchFn", -4], + ); + + const actual = await host.call( + "eval", + "g:__test_host_batch_fn_calls", + ); + assertEquals(actual, [[-4], [10]]); + }); + }); + + await t.step("if Client error occurs", async (t) => { + await t.step("rejects with an error", async () => { + using _client_call = stub( + Client.prototype, + "call", + resolvesNext([new Error("Client call error")]), + ); + + await assertRejects( + () => host.batch(["abs", -4]), + Error, + "Client call error", + ); + }); + }); }); - await t.step( - "notify() does not throws if calls a non-existent function", - async () => { - using console_error = stub(console, "error"); - host.notify("@@@@@", -4); // should not throw - await delay(100); // maybe flaky - assertSpyCalls(console_error, 1); - assertStringIncludes( - console_error.calls.flatMap((c) => c.args).join(" "), - "nvim_error_event(0) Vim:E117: Unknown function: @@@@@", - ); - }, - ); - - await t.step( - "'void' message does nothing", - async () => { - await host.call( - "denops#_internal#test#request", - "void", - [], - ); - }, - ); - - await t.step( - "'invoke' message calls Service method", - async () => { - using s = stub(service, "reload"); - await host.call( - "denops#_internal#test#request", - "invoke", - ["reload", ["dummy"]], + await t.step(".notify()", async (t) => { + await t.step("calls `fn`", async () => { + await host.call("execute", [ + "let g:__test_host_notify_fn_calls = []", + "function! TestHostNotifyFn(...) abort", + " call add(g:__test_host_notify_fn_calls, a:000)", + "endfunction", + ], ""); + + await host.notify( + "TestHostNotifyFn", + "foo", + 4, + undefined, + null, + false, ); - assertSpyCall(s, 0, { args: ["dummy"] }); - }, - ); - - await t.step( - "'nvim_error_event' message shows error message", - async () => { - using s = stub(console, "error"); - await host.call( - "denops#_internal#test#request", - "nvim_error_event", - [0, "message"], + + await delay(NOTIFY_DELAY); // maybe flaky + const actual = await host.call( + "eval", + "g:__test_host_notify_fn_calls", ); - assertSpyCall(s, 0, { args: ["nvim_error_event(0)", "message"] }); - }, - ); - - await t.step( - "waitClosed() returns a promise that is pending when the session is not closed", - async () => { - waitClosed = host.waitClosed(); - assertEquals(await promiseState(waitClosed), "pending"); - }, - ); + assertEquals(actual, [["foo", 4, 0, null, false]]); + }); + + await t.step("if `fn` does not exist", async (t) => { + using console_error = stub(console, "error"); + + await t.step("does not reject", async () => { + await host.notify("@@@@@", -4); + }); + + await t.step("outputs an error message", async () => { + await delay(NOTIFY_DELAY); // maybe flaky + assertSpyCalls(console_error, 1); + assertStringIncludes( + console_error.calls.flatMap((c) => c.args).join(" "), + "nvim_error_event(0) Vim:E117: Unknown function: @@@@@", + ); + }); + }); + }); + + await t.step("when handle request message", async (t) => { + await t.step("'void'", async (t) => { + await t.step("does nothing", async () => { + await host.call( + "denops#_internal#test#request", + "void", + [], + ); + }); + }); + + await t.step("'invoke'", async (t) => { + await t.step("calls Service method", async () => { + using service_reload = stub(service, "reload"); + + await host.call( + "denops#_internal#test#request", + "invoke", + ["reload", ["dummy"]], + ); + + assertSpyCallArgs(service_reload, 0, ["dummy"]); + }); + + await t.step("resolves a result of Service method", async () => { + using _service_dispatch = stub( + service, + "dispatch", + resolvesNext([{ foo: "dummy result" }]), + ); + + const actual = await host.call( + "denops#_internal#test#request", + "invoke", + ["dispatch", ["dummy", "fn", ["arg0"]]], + ); + + assertEquals(actual, { foo: "dummy result" }); + }); + + await t.step("if Service method rejects", async (t) => { + await t.step("rejects with an error", async () => { + using _service_dispatch = stub( + service, + "dispatch", + () => Promise.reject("Error: stringified error message"), + ); + + await assertRejects( + () => + host.call( + "denops#_internal#test#request", + "invoke", + ["dispatch", ["dummy", "fn", ["arg0"]]], + ), + Error, + "Error: stringified error message", + ); + }); + }); + }); + + await t.step("'nvim_error_event'", async (t) => { + await t.step("outputs an error message", async () => { + using console_error = stub(console, "error"); + + await host.call( + "denops#_internal#test#request", + "nvim_error_event", + [0, "message"], + ); + + assertSpyCallArgs(console_error, 0, [ + "nvim_error_event(0)", + "message", + ]); + }); + }); + + await t.step("unknown message", async (t) => { + await t.step("rejects with an error", async () => { + await assertRejects( + () => + host.call( + "denops#_internal#test#request", + "unknown_message", + [0, "message"], + ), + Error, + "NoMethodFoundError: No MessagePack-RPC method 'unknown_message' exists", + ); + }); + }); + }); + + await t.step("when handle notify message", async (t) => { + await t.step("'void'", async (t) => { + await t.step("does nothing", async () => { + await host.call( + "denops#_internal#test#notify", + "void", + [], + ); + }); + }); + + await t.step("'invoke'", async (t) => { + await t.step("calls Service method", async () => { + using service_reload = stub(service, "reload"); + + await host.call( + "denops#_internal#test#notify", + "invoke", + ["reload", ["dummy"]], + ); + + assertSpyCallArgs(service_reload, 0, ["dummy"]); + }); + + await t.step("if Service method rejects", async (t) => { + await t.step("outputs an error message", async () => { + using console_error = stub(console, "error"); + using _service_dispatch = stub( + service, + "dispatch", + () => Promise.reject("Error: stringified error message"), + ); + + await host.call( + "denops#_internal#test#notify", + "invoke", + ["dispatch", ["dummy", "fn", ["arg0"]]], + ); + + assertSpyCalls(console_error, 1); + assertMatch( + console_error.calls[0].args.join(" "), + /^Failed to handle message [0-9]+,invoke,dispatch,dummy,fn,arg0/, + ); + }); + }); + }); + + await t.step("'nvim_error_event'", async (t) => { + await t.step("outputs an error message", async () => { + using console_error = stub(console, "error"); + + await host.call( + "denops#_internal#test#notify", + "nvim_error_event", + [0, "message"], + ); + + assertSpyCallArgs(console_error, 0, [ + "nvim_error_event(0)", + "message", + ]); + }); + }); + + await t.step("unknown message", async (t) => { + await t.step("outputs an error message", async () => { + using console_error = stub(console, "error"); + + await host.call( + "denops#_internal#test#notify", + "unknown_message", + [0, "message"], + ); + + assertSpyCalls(console_error, 1); + assertMatch( + console_error.calls[0].args.join(" "), + /^Failed to handle message [0-9]+,unknown_message,0,message/, + ); + }); + }); + }); + + // NOTE: This test closes the session of the host. + await t.step(".waitClosed()", async (t) => { + const waitClosedPromise = host.waitClosed(); + + await t.step("pendings before the session closes", async () => { + assertEquals(await promiseState(waitClosedPromise), "pending"); + }); + + // NOTE: Close the session of the host. + await host[Symbol.asyncDispose](); + + await t.step("fulfilled when the session closes", async () => { + assertEquals(await promiseState(waitClosedPromise), "fulfilled"); + }); + }); }, }); - await t.step( - "waitClosed promise is fulfilled when the session is closed", - async () => { - assertEquals(await promiseState(waitClosed!), "fulfilled"); - }, - ); }); diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index 3c31176f..fa269d31 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -3,16 +3,23 @@ import { assertMatch, assertRejects, } from "jsr:@std/assert@0.225.1"; -import { assertSpyCall, stub } from "jsr:@std/testing@0.224.0/mock"; +import { + assertSpyCallArgs, + assertSpyCalls, + resolvesNext, + stub, +} from "jsr:@std/testing@0.224.0/mock"; import { delay } from "jsr:@std/async@0.224.0/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { Client, Session } from "jsr:@denops/vim-channel-command@4.0.0"; import { withVim } from "/denops-testutil/with.ts"; import type { Service } from "../host.ts"; import { Vim } from "./vim.ts"; +const NOTIFY_DELAY = 100; + Deno.test("Vim", async (t) => { - let waitClosed: Promise | undefined; await withVim({ fn: async ({ reader, writer }) => { const service: Service = { @@ -27,124 +34,360 @@ Deno.test("Vim", async (t) => { }; await using host = new Vim(reader, writer); - await t.step( - "'invoke' message before init throws error", - async () => { - await assertRejects( - () => - host.call( - "denops#_internal#test#request", - "invoke", - ["reload", ["dummy"]], - ), - Error, - "Failed to call", - ); - }, - ); - await t.step("init() calls Service.bind()", async () => { - using s = stub(service, "bind"); - await host.init(service); - assertSpyCall(s, 0, { args: [host] }); + await t.step("before .init() calls", async (t) => { + await t.step("when handle message", async (t) => { + await t.step("'invoke' rejects", async () => { + await assertRejects( + () => + host.call( + "denops#_internal#test#request", + "invoke", + ["reload", ["dummy"]], + ), + Error, + // TODO: Fix it by stringifying the error object. + // "No service is registered in the host", + "Failed to call", + ); + }); + }); }); - await t.step("redraw() does nothing", async () => { - await host.redraw(); - // To avoid 'async operation to op_write_all was started before this test, ...' error. - await delay(10); - }); + await t.step(".init()", async (t) => { + await t.step("calls service.bind()", async () => { + using service_bind = stub(service, "bind"); + + await host.init(service); - await t.step("call() returns a result of the function", async () => { - const result = await host.call("abs", -4); - assertEquals(result, 4); + assertSpyCallArgs(service_bind, 0, [host]); + }); }); - await t.step( - "call() throws an error when failed to call the function", - async () => { - await assertRejects( - () => host.call("@@@@@", -4), - Error, - "Failed to call '@@@@@' in Vim: Vim(let):E117: Unknown function: @@@@@", + await t.step(".redraw()", async (t) => { + await t.step("sends a redraw command", async () => { + using session_send = stub( + Session.prototype, + "send", + resolvesNext([undefined]), ); - }, - ); - - await t.step("batch() returns results of the functions", async () => { - const [ret, err] = await host.batch( - ["abs", -4], - ["abs", 10], - ["abs", -9], - ); - assertEquals(ret, [4, 10, 9]); - assertEquals(err, ""); + + await host.redraw(); + + assertSpyCallArgs(session_send, 0, [["redraw", ""]]); + }); + + await t.step("sends a redraw command with `force`", async () => { + using session_send = stub( + Session.prototype, + "send", + resolvesNext([undefined]), + ); + + await host.redraw(true); + + assertSpyCallArgs(session_send, 0, [["redraw", "force"]]); + }); + }); + + await t.step(".call()", async (t) => { + await t.step("resolves a result of `fn`", async () => { + const result = await host.call("abs", -4); + + assertEquals(result, 4); + }); + + await t.step("if `fn` does not exist", async (t) => { + await t.step("rejects with an error", async () => { + await assertRejects( + () => host.call("@@@@@", -4), + Error, + "Failed to call '@@@@@' in Vim: Vim(let):E117: Unknown function: @@@@@", + ); + }); + }); + + await t.step("if Vim returns 'ERROR'", async (t) => { + await t.step("rejects with an error", async () => { + using _client_call = stub( + Client.prototype, + "call", + resolvesNext(["ERROR"]), + ); + + await assertRejects( + () => host.call("abs", -4), + Error, + 'Vim just returns "ERROR"', + ); + }); + }); }); - await t.step( - "batch() returns resutls with an error when failed to call the function", - async () => { + await t.step(".batch()", async (t) => { + await t.step("resolves results of `calls`", async () => { const [ret, err] = await host.batch( ["abs", -4], ["abs", 10], - ["@@@@@", -9], - ); - assertEquals(ret, [4, 10]); - assertMatch( - err, - /Failed to call '@@@@@' in Vim: Vim\(.*\):E117: Unknown function: @@@@@/, + ["abs", -9], ); - }, - ); - await t.step("notify() calls the function", () => { - host.notify("abs", -4); + assertEquals(ret, [4, 10, 9]); + assertEquals(err, ""); + }); + + await t.step("if some function does not exist", async (t) => { + await t.step("resolves resutls and an error", async () => { + const [ret, err] = await host.batch( + ["abs", -4], + ["abs", 10], + ["@@@@@", -9], + ["abs", 10], + ); + + assertEquals(ret, [4, 10]); + assertMatch( + err, + /^Failed to call '@@@@@' in Vim: Vim\(.*\):E117: Unknown function: @@@@@/, + ); + }); + + await t.step("does not call functions after failure", async () => { + await host.call("execute", [ + "let g:__test_host_batch_fn_calls = []", + "function! TestHostBatchFn(...) abort", + " call add(g:__test_host_batch_fn_calls, a:000)", + "endfunction", + ], ""); + + await host.batch( + ["TestHostBatchFn", -4], + ["TestHostBatchFn", 10], + ["@@@@@", 10], + ["TestHostBatchFn", -9], + ["TestHostBatchFn", -4], + ); + + const actual = await host.call( + "eval", + "g:__test_host_batch_fn_calls", + ); + assertEquals(actual, [[-4], [10]]); + }); + }); + + await t.step("if Vim returns 'ERROR'", async (t) => { + await t.step("rejects with an error", async () => { + using _client_call = stub( + Client.prototype, + "call", + resolvesNext(["ERROR"]), + ); + + await assertRejects( + () => host.batch(["abs", -4]), + Error, + 'Vim just returns "ERROR"', + ); + }); + }); }); - await t.step( - "notify() does not throws if calls a non-existent function", - () => { - host.notify("@@@@@", -4); // should not throw - }, - ); - - await t.step( - "'void' message does nothing", - async () => { - await host.call( - "denops#_internal#test#request", - "void", - [], + await t.step(".notify()", async (t) => { + await t.step("calls `fn`", async () => { + await host.call("execute", [ + "let g:__test_host_notify_fn_calls = []", + "function! TestHostNotifyFn(...) abort", + " call add(g:__test_host_notify_fn_calls, a:000)", + "endfunction", + ], ""); + + await host.notify( + "TestHostNotifyFn", + "foo", + 4, + undefined, + null, + false, ); - }, - ); - - await t.step( - "'invoke' message calls Service method", - async () => { - using s = stub(service, "reload"); - await host.call( - "denops#_internal#test#request", - "invoke", - ["reload", ["dummy"]], + + await delay(NOTIFY_DELAY); // maybe flaky + const actual = await host.call( + "eval", + "g:__test_host_notify_fn_calls", ); - assertSpyCall(s, 0, { args: ["dummy"] }); - }, - ); - - await t.step( - "waitClosed() returns a promise that is pending when the session is not closed", - async () => { - waitClosed = host.waitClosed(); - assertEquals(await promiseState(waitClosed), "pending"); - }, - ); + assertEquals(actual, [["foo", 4, null, null, false]]); + }); + + await t.step("if `fn` does not exist", async (t) => { + await t.step("does not reject", async () => { + await host.notify("@@@@@", -4); + }); + }); + }); + + await t.step("when handle request message", async (t) => { + await t.step("'void'", async (t) => { + await t.step("does nothing", async () => { + await host.call( + "denops#_internal#test#request", + "void", + [], + ); + }); + }); + + await t.step("'invoke'", async (t) => { + await t.step("calls Service method", async () => { + using service_reload = stub(service, "reload"); + + await host.call( + "denops#_internal#test#request", + "invoke", + ["reload", ["dummy"]], + ); + + assertSpyCallArgs(service_reload, 0, ["dummy"]); + }); + + await t.step("resolves a result of Service method", async () => { + using _service_dispatch = stub( + service, + "dispatch", + resolvesNext([{ foo: "dummy result" }]), + ); + + const actual = await host.call( + "denops#_internal#test#request", + "invoke", + ["dispatch", ["dummy", "fn", ["arg0"]]], + ); + + assertEquals(actual, { foo: "dummy result" }); + }); + + await t.step("if Service method rejects", async (t) => { + await t.step("rejects with an error", async () => { + using _service_dispatch = stub( + service, + "dispatch", + () => Promise.reject("Error: stringified error message"), + ); + + await assertRejects( + () => + host.call( + "denops#_internal#test#request", + "invoke", + ["dispatch", ["dummy", "fn", ["arg0"]]], + ), + Error, + "Error: stringified error message", + ); + }); + }); + }); + + await t.step("unknown message", async (t) => { + await t.step("rejects with an error", async () => { + await assertRejects( + () => + host.call( + "denops#_internal#test#request", + "unknown_message", + [0, "message"], + ), + Error, + // TODO: Fix it by stringifying the error object. + // 'Unexpected JSON channel message is received: ["unknown_message",0,"message"]', + "Failed to call", + ); + }); + }); + }); + + await t.step("when handle notify message", async (t) => { + await t.step("'void'", async (t) => { + await t.step("does nothing", async () => { + await host.call( + "denops#_internal#test#notify", + "void", + [], + ); + }); + }); + + await t.step("'invoke'", async (t) => { + await t.step("calls Service method", async () => { + using service_reload = stub(service, "reload"); + + await host.call( + "denops#_internal#test#notify", + "invoke", + ["reload", ["dummy"]], + ); + + assertSpyCallArgs(service_reload, 0, ["dummy"]); + }); + + await t.step("if Service method rejects", async (t) => { + await t.step("outputs an error message", async () => { + using console_error = stub(console, "error"); + using _service_dispatch = stub( + service, + "dispatch", + () => Promise.reject("Error: stringified error message"), + ); + + await host.call( + "denops#_internal#test#notify", + "invoke", + ["dispatch", ["dummy", "fn", ["arg0"]]], + ); + + assertSpyCallArgs( + console_error, + 0, + ["Error: stringified error message"], + ); + }); + }); + }); + + await t.step("unknown message", async (t) => { + await t.step("outputs an error message", async () => { + using console_error = stub(console, "error"); + + await host.call( + "denops#_internal#test#notify", + "unknown_message", + [0, "message"], + ); + + assertSpyCalls(console_error, 1); + assertEquals( + console_error.calls[0].args.join(" "), + 'Error: Unexpected JSON channel message is received: ["unknown_message",0,"message"]', + ); + }); + }); + }); + + // NOTE: This test closes the session of the host. + await t.step(".waitClosed()", async (t) => { + const waitClosedPromise = host.waitClosed(); + + await t.step("pendings before the session closes", async () => { + assertEquals(await promiseState(waitClosedPromise), "pending"); + }); + + // NOTE: Close the session of the host. + await host[Symbol.asyncDispose](); + + await t.step("fulfilled when the session closes", async () => { + assertEquals(await promiseState(waitClosedPromise), "fulfilled"); + }); + }); }, }); - await t.step( - "waitClosed promise is fulfilled when the session is closed", - async () => { - assertEquals(await promiseState(waitClosed!), "fulfilled"); - }, - ); }); From 8e1b292ab8657e5d9d5bc4b3179be4fd0fce509c Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 10 Jul 2024 16:27:22 +0900 Subject: [PATCH 72/99] :muscle: clarify error handling blocks --- denops/@denops-private/service.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 4b9016df..b3e88e96 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -198,11 +198,10 @@ class Plugin { async #load(): Promise { const suffix = createScriptSuffix(this.script); + await emit(this.#denops, `DenopsSystemPluginPre:${this.name}`); try { - await emit(this.#denops, `DenopsSystemPluginPre:${this.name}`); const mod: PluginModule = await import(`${this.script}${suffix}`); this.#disposable = await mod.main(this.#denops) ?? voidAsyncDisposable; - await emit(this.#denops, `DenopsSystemPluginPost:${this.name}`); } catch (e) { // Show a warning message when Deno module cache issue is detected // https://github.com/vim-denops/denops.vim/issues/358 @@ -221,6 +220,7 @@ class Plugin { await emit(this.#denops, `DenopsSystemPluginFail:${this.name}`); throw e; } + await emit(this.#denops, `DenopsSystemPluginPost:${this.name}`); } async unload(): Promise { @@ -231,16 +231,17 @@ class Plugin { // Load failed, do nothing return; } + const disposable = this.#disposable; + this.#disposable = voidAsyncDisposable; + await emit(this.#denops, `DenopsSystemPluginUnloadPre:${this.name}`); try { - await emit(this.#denops, `DenopsSystemPluginUnloadPre:${this.name}`); - await this.#disposable[Symbol.asyncDispose](); - await emit(this.#denops, `DenopsSystemPluginUnloadPost:${this.name}`); + await disposable[Symbol.asyncDispose](); } catch (e) { console.error(`Failed to unload plugin '${this.name}': ${e}`); await emit(this.#denops, `DenopsSystemPluginUnloadFail:${this.name}`); - } finally { - this.#disposable = voidAsyncDisposable; + return; } + await emit(this.#denops, `DenopsSystemPluginUnloadPost:${this.name}`); } async call(fn: string, ...args: unknown[]): Promise { @@ -269,6 +270,7 @@ function createScriptSuffix(script: string): string { return suffix; } +/** NOTE: `emit()` is never throws or rejects. */ async function emit(denops: Denops, name: string): Promise { try { await denops.cmd(`doautocmd User ${name}`); From 4880b90058157f744e7bf3aca5c9be5ba9e86e91 Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 10 Jul 2024 16:28:56 +0900 Subject: [PATCH 73/99] :+1: improved multiple calls to `unload()` --- denops/@denops-private/service.ts | 12 +- denops/@denops-private/service_test.ts | 297 +++++++++++++++++++++++-- 2 files changed, 291 insertions(+), 18 deletions(-) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index b3e88e96..0ce63b78 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -73,8 +73,8 @@ export class Service implements HostService, AsyncDisposable { this.#waiters.get(name)?.promise.finally(() => { this.#waiters.delete(name); }); - this.#plugins.delete(name); await plugin.unload(); + this.#plugins.delete(name); return plugin; } @@ -180,6 +180,7 @@ type PluginModule = { class Plugin { #denops: Denops; #loadedWaiter: Promise; + #unloadedWaiter?: Promise; #disposable: AsyncDisposable = voidAsyncDisposable; readonly name: string; @@ -223,7 +224,14 @@ class Plugin { await emit(this.#denops, `DenopsSystemPluginPost:${this.name}`); } - async unload(): Promise { + unload(): Promise { + if (!this.#unloadedWaiter) { + this.#unloadedWaiter = this.#unload(); + } + return this.#unloadedWaiter; + } + + async #unload(): Promise { try { // Wait for the load to complete to make the events atomically. await this.#loadedWaiter; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index b388cbc6..d5e28268 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -351,6 +351,101 @@ Deno.test("Service", async (t) => { }); }); }); + + await t.step("if the plugin is loading", async (t) => { + const service = new Service(meta); + service.bind(host); + using console_log = stub(console, "log"); + using host_call = stub(host, "call"); + + const prevLoadPromise = service.load("dummy", scriptValid); + const loadPromise = service.load("dummy", scriptInvalid); + + await t.step("resolves", async () => { + await loadPromise; + }); + + await t.step("outputs a log message", () => { + assertSpyCall(console_log, 0, { + args: [ + "A denops plugin 'dummy' is already loaded. Skip", + ], + }); + }); + + await t.step("previous `load()` resolves", async () => { + await prevLoadPromise; + }); + + await t.step("emits `load()` events", () => { + const events = host_call.calls.map((c) => c.args); + assertEquals(events, [ + // Previous `load()` events. + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + ]); + }); + }); + + await t.step("if the plugin is unloading", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using console_log = stub(console, "log"); + using host_call = stub(host, "call"); + + const prevUnloadPromise = service.unload("dummy"); + const loadPromise = service.load("dummy", scriptInvalid); + + await t.step("resolves", async () => { + await loadPromise; + }); + + await t.step("outputs a log message", () => { + assertSpyCall(console_log, 0, { + args: [ + "A denops plugin 'dummy' is already loaded. Skip", + ], + }); + }); + + await t.step("previous `unload()` resolves", async () => { + await prevUnloadPromise; + }); + + await t.step("emits `unload()` events", () => { + const events = host_call.calls.map((c) => c.args); + assertEquals(events, [ + // Previous `unload()` events. + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + ]); + }); + }); }); await t.step(".unload()", async (t) => { @@ -526,37 +621,42 @@ Deno.test("Service", async (t) => { }); }); - await t.step("if called before `load()` resolves", async (t) => { + await t.step("if the plugin is loading", async (t) => { const service = new Service(meta); service.bind(host); using host_call = stub(host, "call"); - const loadPromise = service.load("dummy", scriptValid); + const prevLoadPromise = service.load("dummy", scriptValid); const unloadPromise = service.unload("dummy"); await t.step("resolves", async () => { await unloadPromise; }); - await t.step("`load()` was resolved", async () => { - assertEquals(await promiseState(loadPromise), "fulfilled"); + await t.step("previous `load()` was resolved", async () => { + assertEquals(await promiseState(prevLoadPromise), "fulfilled"); }); - await t.step("emits events in the correct order", () => { - const events = host_call.calls - .map((c) => c.args) - .filter((args) => (args[1] as string)?.startsWith("doautocmd")); + await t.step("emits `load()` and `unload()` events", () => { + const events = host_call.calls.map((c) => c.args); assertEquals(events, [ + // Previous `load()` events. [ "denops#api#cmd", "doautocmd User DenopsSystemPluginPre:dummy", {}, ], + [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], [ "denops#api#cmd", "doautocmd User DenopsSystemPluginPost:dummy", {}, ], + // This `unload()` events. [ "denops#api#cmd", "doautocmd User DenopsSystemPluginUnloadPre:dummy", @@ -571,27 +671,35 @@ Deno.test("Service", async (t) => { }); }); - await t.step("if called before `load()` resolves with error", async (t) => { + await t.step("if the plugin is loading and failur", async (t) => { const service = new Service(meta); service.bind(host); + using console_error = stub(console, "error"); using host_call = stub(host, "call"); - const loadPromise = service.load("dummy", scriptInvalid); + const prevLoadPromise = service.load("dummy", scriptInvalid); const unloadPromise = service.unload("dummy"); await t.step("resolves", async () => { await unloadPromise; }); - await t.step("`load()` was resolved", async () => { - assertEquals(await promiseState(loadPromise), "fulfilled"); + await t.step("previous `load()` was resolved", async () => { + assertEquals(await promiseState(prevLoadPromise), "fulfilled"); + }); + + await t.step("outputs an error message", () => { + assertSpyCall(console_error, 0, { + args: [ + "Failed to load plugin 'dummy': Error: This is dummy error", + ], + }); }); - await t.step("emits events in the correct order", () => { - const events = host_call.calls - .map((c) => c.args) - .filter((args) => (args[1] as string)?.startsWith("doautocmd")); + await t.step("emits `load()` events", () => { + const events = host_call.calls.map((c) => c.args); assertEquals(events, [ + // Previous `load()` events. [ "denops#api#cmd", "doautocmd User DenopsSystemPluginPre:dummy", @@ -606,6 +714,44 @@ Deno.test("Service", async (t) => { }); }); + await t.step("if the plugin is unloading", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using host_call = stub(host, "call"); + + const prevUnloadPromise = service.unload("dummy"); + const unloadPromise = service.unload("dummy"); + + await t.step("resolves", async () => { + await unloadPromise; + }); + + await t.step("previous `unload()` was resolved", async () => { + assertEquals(await promiseState(prevUnloadPromise), "fulfilled"); + }); + + await t.step("emits `unload()` events", () => { + const events = host_call.calls.map((c) => c.args); + assertEquals(events, [ + // Previous `unload()` events. + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + ]); + }); + }); + await t.step("if `host.call()` rejects (channel closed)", async (t) => { const service = new Service(meta); service.bind(host); @@ -753,6 +899,125 @@ Deno.test("Service", async (t) => { }); }); + await t.step("if the plugin is loading", async (t) => { + const service = new Service(meta); + service.bind(host); + using host_call = stub(host, "call"); + + const prevLoadPromise = service.load("dummy", scriptValid); + const reloadPromise = service.reload("dummy"); + + await t.step("resolves", async () => { + await reloadPromise; + }); + + await t.step("previous `load()` was resolved", async () => { + assertEquals(await promiseState(prevLoadPromise), "fulfilled"); + }); + + await t.step("emits `load()` and `reload()` events", () => { + const events = host_call.calls.map((c) => c.args); + assertEquals(events, [ + // Previous `load()` events. + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + // This `reload()` events. + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + ]); + }); + }); + + await t.step("if the plugin is unloading", async (t) => { + const service = new Service(meta); + service.bind(host); + { + using _host_call = stub(host, "call"); + await service.load("dummy", scriptValid); + } + using host_call = stub(host, "call"); + + const prevUnloadPromise = service.unload("dummy"); + const reloadPromise = service.reload("dummy"); + + await t.step("resolves", async () => { + await reloadPromise; + }); + + await t.step("previous `unload()` was resolved", async () => { + assertEquals(await promiseState(prevUnloadPromise), "fulfilled"); + }); + + await t.step("emits `reload()` events", () => { + const events = host_call.calls.map((c) => c.args); + assertEquals(events, [ + // Previous `unload()` events. + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginUnloadPost:dummy", + {}, + ], + // This `reload()` events. + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPre:dummy", + {}, + ], + [ + "denops#api#cmd", + "echo 'Hello, Denops!'", + {}, + ], + [ + "denops#api#cmd", + "doautocmd User DenopsSystemPluginPost:dummy", + {}, + ], + ]); + }); + }); + await t.step("if the plugin file is changed", async (t) => { // Generate source script file. await using tempFile = await useTempFile({ From 9b27654f957dfbbad0e9db9d48fedf9e04679b01 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 11 Jul 2024 02:26:58 +0900 Subject: [PATCH 74/99] :memo: improve `denops#plugin#*` docs --- doc/denops.txt | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/doc/denops.txt b/doc/denops.txt index 1a25304e..8d4fc4b0 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -345,17 +345,23 @@ denops#server#wait_async({callback}) *denops#plugin#is_loaded()* denops#plugin#is_loaded({name}) - Returns 1 if a {name} plugin is already loaded. Otherwise, returns - 0. + Returns 1 if a {name} plugin is already loaded or failed to load. + Returns 0 otherwise, even if |denops#plugin#unload()| or + |denops#plugin#reload()| are called. *denops#plugin#wait()* denops#plugin#wait({name}[, {options}]) - Waits synchronously until a {name} plugin is loaded. It returns - immediately when the {name} plugin is already loaded. - It returns -1 if it times out, -2 if the server is not yet ready or - interrupted, or -3 if the plugin initialization failed. - Developers need to consider this return value to decide whether to - continue with the subsequent process. + Waits synchronously until a {name} plugin is loaded or failed to load. + It returns immediately when the {name} plugin is already loaded. + + Developers need to consider the following return value to decide + whether to continue with the subsequent process: + + 0 The {name} plugin is loaded successfully. + -1 "timeout" expired. + -2 The server is not yet ready or interrupted. + -3 The {name} plugin failed to load. + The following attributes are available on {options}. "interval" Interval in milliseconds for |:sleep| in internal @@ -408,26 +414,28 @@ denops#plugin#discover() *denops#plugin#load()* denops#plugin#load({name}, {script}) Loads a denops plugin. Use this function to load denops plugins that - are not discovered by |denops#plugin#discover()|. + are not discovered by |denops#plugin#discover()|. If the {name} plugin + is already loaded, it does nothing. Loading a plugin involves the following event steps: - - |User| |DenopsPluginPre|:{plugin} is fired. + - |User| |DenopsPluginPre|:{name} is fired. - The plugin is loaded and the "main" function is executed. - - If it succeeds, |User| |DenopsPluginPost|:{plugin} is fired. - - If it fails, |User| |DenopsPluginFail|:{plugin} is fired. + - If it succeeds, |User| |DenopsPluginPost|:{name} is fired. + - If it fails, |User| |DenopsPluginFail|:{name} is fired. *denops#plugin#unload()* denops#plugin#unload({name}) - Unloads a denops plugin. It does nothing when the plugin is not loaded - yet. + Unloads a denops plugin. If the {name} plugin is currently loading, it + will be unloaded after it has been successfully loaded. If the {name} + plugin does not exist or fails to load, it does nothing. Unloading a plugin involves the following event steps: - - |User| |DenopsPluginUnloadPre|:{plugin} is fired. + - |User| |DenopsPluginUnloadPre|:{name} is fired. - The plugin's dispose callback is executed, if it exists. - - If it succeeds, |User| |DenopsPluginUnloadPost|:{plugin} is fired. - - If it fails, |User| |DenopsPluginUnloadFail|:{plugin} is fired. + - If it succeeds, |User| |DenopsPluginUnloadPost|:{name} is fired. + - If it fails, |User| |DenopsPluginUnloadFail|:{name} is fired. The above events may not be fired if the connection is forcibly closed or the denops server is forcibly terminated due to timeout @@ -435,8 +443,8 @@ denops#plugin#unload({name}) *denops#plugin#reload()* denops#plugin#reload({plugin}[, {options}]) - Reloads a denops plugin. It does nothing when the plugin is not loaded - yet. + Reloads a denops plugin. If the {name} plugin does not exist or fails + to load, it does nothing. It invokes |User| autocommand events. See |denops#plugin#load()| and |denops#plugin#unload()| for details. From 7c49bb84f7ca5d61bd3714cbe60f5f771a606f29 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 11 Jul 2024 08:10:54 +0900 Subject: [PATCH 75/99] :herb: ignore test if `DENOPS_TEST_VERBOSE` env exist --- tests/denops/testutil/shared_server_test.ts | 82 +++++++++++---------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/tests/denops/testutil/shared_server_test.ts b/tests/denops/testutil/shared_server_test.ts index fdc7225d..c63ca98a 100644 --- a/tests/denops/testutil/shared_server_test.ts +++ b/tests/denops/testutil/shared_server_test.ts @@ -10,48 +10,54 @@ import { join } from "jsr:@std/path@0.225.0/join"; import { useSharedServer } from "./shared_server.ts"; Deno.test("useSharedServer()", async (t) => { - await t.step("if `verbose` is not specified", async (t) => { - await t.step("returns `result.addr`", async (t) => { - await using server = await useSharedServer({ verbose: true }); - const { addr } = server; + const HAS_DENOPS_TEST_VERBOSE = Deno.env.has("DENOPS_TEST_VERBOSE"); - await t.step("`addr.host` is string", () => { - assertEquals(addr.host, "127.0.0.1"); - }); - await t.step("`addr.port` is number", () => { - assertEquals(typeof addr.port, "number"); - }); - await t.step("`addr.toString()` returns the address", () => { - assertMatch(addr.toString(), /^127\.0\.0\.1:\d+$/); + await t.step({ + name: "if `verbose` is not specified", + ignore: HAS_DENOPS_TEST_VERBOSE, + fn: async (t) => { + await t.step("returns `result.addr`", async (t) => { + await using server = await useSharedServer(); + const { addr } = server; + + await t.step("`addr.host` is string", () => { + assertEquals(addr.host, "127.0.0.1"); + }); + await t.step("`addr.port` is number", () => { + assertEquals(typeof addr.port, "number"); + }); + await t.step("`addr.toString()` returns the address", () => { + assertMatch(addr.toString(), /^127\.0\.0\.1:\d+$/); + }); }); - }); - await t.step("returns `result.stdout`", async () => { - await using server = await useSharedServer(); - assertInstanceOf(server.stdout, ReadableStream); - const outputs: string[] = []; - server.stdout.pipeTo( - new WritableStream({ write: (line) => void outputs.push(line) }), - ).catch(() => {}); - await delay(100); - assertMatch(outputs.join("\n"), /Listen denops clients on/); - }); + await t.step("returns `result.stdout`", async () => { + await using server = await useSharedServer(); + assertInstanceOf(server.stdout, ReadableStream); + const outputs: string[] = []; + server.stdout.pipeTo( + new WritableStream({ write: (line) => void outputs.push(line) }), + ).catch(() => {}); + await delay(100); + assertMatch(outputs.join("\n"), /Listen denops clients on/); + }); - await t.step("does not output stdout", async () => { - const proc = new Deno.Command(Deno.execPath(), { - args: [ - "run", - "--allow-env", - "--allow-read", - "--allow-run", - resolve("shared_server_test_no_verbose.ts"), - ], - stdout: "piped", - }).spawn(); - const output = await proc.output(); - const stdout = new TextDecoder().decode(output.stdout); - assertNotMatch(stdout, /Listen denops clients on/); - }); + await t.step("does not output stdout", async () => { + const proc = new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--allow-env", + "--allow-read", + "--allow-run", + resolve("shared_server_test_no_verbose.ts"), + ], + stdout: "piped", + }).spawn(); + const output = await proc.output(); + const stdout = new TextDecoder().decode(output.stdout); + assertNotMatch(stdout, /Listen denops clients on/); + }); + }, }); await t.step("if `verbose` is true", async (t) => { From 21c56d2fed2eb443370648031146dd535ab2704c Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 11 Jul 2024 08:11:58 +0900 Subject: [PATCH 76/99] :+1: no `:message` if delayed message is one line or during test --- autoload/denops/_internal/echo.vim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/autoload/denops/_internal/echo.vim b/autoload/denops/_internal/echo.vim index a5b6d56e..9c7f0c0f 100644 --- a/autoload/denops/_internal/echo.vim +++ b/autoload/denops/_internal/echo.vim @@ -56,5 +56,7 @@ function! s:echomsg_batch() abort let s:delayed_timer = 0 let s:delayed_messages = [] " Forcibly show the messages to the user - call feedkeys(printf("\%dmessages\", l:counter), 'n') + if l:counter > 1 && !g:denops#_test + call feedkeys(printf("\%dmessages\", l:counter), 'n') + endif endfunction From 43f4fabe3e849f037499179dce9468fdaa5bcec9 Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 10 Jul 2024 16:52:35 +0900 Subject: [PATCH 77/99] :herb: add state-specific tests for `denops#plugin#*` --- tests/denops/runtime/functions/plugin_test.ts | 2106 ++++++++++++++--- .../testdata/dummy_invalid_wait_plugin.ts | 7 + .../testdata/dummy_valid_wait_plugin.ts | 6 + 3 files changed, 1729 insertions(+), 390 deletions(-) create mode 100644 tests/denops/testdata/dummy_invalid_wait_plugin.ts create mode 100644 tests/denops/testdata/dummy_valid_wait_plugin.ts diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index 76514549..7a91748c 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -1,21 +1,26 @@ import { assertArrayIncludes, assertEquals, + assertGreater, + assertLess, assertMatch, assertRejects, + assertStringIncludes, } from "jsr:@std/assert@0.225.2"; -import { delay } from "jsr:@std/async@^0.224.0/delay"; +import { delay } from "jsr:@std/async@^0.224.0"; import { join } from "jsr:@std/path@0.225.0/join"; +import { AsyncDisposableStack } from "jsr:@nick/dispose@1.1.0/async-disposable-stack"; import { testHost } from "/denops-testutil/host.ts"; import { wait } from "/denops-testutil/wait.ts"; -import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; -const MESSAGE_DELAY = 200; +const MESSAGE_DELAY = 200; // msc const scriptValid = resolve("dummy_valid_plugin.ts"); const scriptInvalid = resolve("dummy_invalid_plugin.ts"); const scriptValidDispose = resolve("dummy_valid_dispose_plugin.ts"); const scriptInvalidDispose = resolve("dummy_invalid_dispose_plugin.ts"); +const scriptValidWait = resolve("dummy_valid_wait_plugin.ts"); +const scriptInvalidWait = resolve("dummy_invalid_wait_plugin.ts"); const runtimepathPlugin = resolve("dummy_plugins"); testHost({ @@ -35,24 +40,24 @@ testHost({ ], ""); await t.step("denops#plugin#load()", async (t) => { - await t.step("if the plugin is valid", async (t) => { + await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#load('dummy', '${scriptValid}')`, + `call denops#plugin#load('dummyLoadNotLoaded', '${scriptValid}')`, ], ""); await t.step("loads a denops plugin", async () => { await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummy") + .includes("DenopsPluginPost:dummyLoadNotLoaded") ); }); await t.step("fires DenopsPlugin* events", async () => { assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginPre:dummy", - "DenopsPluginPost:dummy", + "DenopsPluginPre:dummyLoadNotLoaded", + "DenopsPluginPost:dummyLoadNotLoaded", ]); }); @@ -65,20 +70,20 @@ testHost({ outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#load('dummyInvalid', '${scriptInvalid}')`, + `call denops#plugin#load('dummyLoadInvalid', '${scriptInvalid}')`, ], ""); await t.step("fails loading a denops plugin", async () => { await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginFail:dummyInvalid") + .includes("DenopsPluginFail:dummyLoadInvalid") ); }); await t.step("fires DenopsPlugin* events", async () => { assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginPre:dummyInvalid", - "DenopsPluginFail:dummyInvalid", + "DenopsPluginPre:dummyLoadInvalid", + "DenopsPluginFail:dummyLoadInvalid", ]); }); @@ -86,34 +91,8 @@ testHost({ await delay(MESSAGE_DELAY); assertMatch( outputs.join(""), - /Failed to load plugin 'dummyInvalid': Error: This is dummy error/, - ); - }); - }); - - // NOTE: Depends on 'dummy' which was already loaded in the test above. - await t.step("if the plugin is already loaded", async (t) => { - outputs = []; - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummy', '${scriptValid}')`, - ], ""); - - await t.step("does not load a denops plugin", async () => { - const actual = wait( - () => host.call("eval", "len(g:__test_denops_events)"), - { timeout: 1000, interval: 100 }, + /Failed to load plugin 'dummyLoadInvalid': Error: This is dummy error/, ); - await assertRejects(() => actual, Error, "Timeout"); - }); - - await t.step("does not fires DenopsPlugin* events", async () => { - assertEquals(await host.call("eval", "g:__test_denops_events"), []); - }); - - await t.step("does not output messages", async () => { - await delay(MESSAGE_DELAY); - assertEquals(outputs, []); }); }); @@ -123,20 +102,20 @@ testHost({ outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#load('dummyOther', '${scriptValid}')`, + `call denops#plugin#load('dummyLoadOther', '${scriptValid}')`, ], ""); await t.step("loads a denops plugin", async () => { await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyOther") + .includes("DenopsPluginPost:dummyLoadOther") ); }); await t.step("fires DenopsPlugin* events", async () => { assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginPre:dummyOther", - "DenopsPluginPost:dummyOther", + "DenopsPluginPre:dummyLoadOther", + "DenopsPluginPost:dummyLoadOther", ]); }); @@ -145,91 +124,63 @@ testHost({ }); }, ); - }); - - await t.step("denops#plugin#unload()", async (t) => { - await t.step("if the plugin is already loaded", async (t) => { - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyUnload', '${scriptValidDispose}')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyUnload") - ); + await t.step("if the plugin is loading", async (t) => { outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - "call denops#plugin#unload('dummyUnload')", + `call denops#plugin#load('dummyLoadLoading', '${scriptValid}')`, + `call denops#plugin#load('dummyLoadLoading', '${scriptValid}')`, ], ""); - await t.step("unloads a denops plugin", async () => { + await t.step("loads a denops plugin", async () => { await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginUnloadPost:dummyUnload") + .includes("DenopsPluginPost:dummyLoadLoading") + ); + }); + + await t.step("does not load a denops plugin twice", async () => { + const actual = wait( + async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("DenopsPluginPost:")) + .length >= 2, + { timeout: 1000, interval: 100 }, ); + await assertRejects(() => actual, Error, "Timeout"); }); await t.step("fires DenopsPlugin* events", async () => { assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginUnloadPre:dummyUnload", - "DenopsPluginUnloadPost:dummyUnload", + "DenopsPluginPre:dummyLoadLoading", + "DenopsPluginPost:dummyLoadLoading", ]); }); - await t.step("calls the plugin dispose method", () => { - assertMatch(outputs.join(""), /Goodbye, Denops!/); + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); }); }); - await t.step("if the plugin dispose method throws", async (t) => { + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#load('dummyUnloadInvalid', '${scriptInvalidDispose}')`, + `call denops#plugin#load('dummyLoadLoaded', '${scriptValid}')`, ], ""); await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyUnloadInvalid") + .includes("DenopsPluginPost:dummyLoadLoaded") ); outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - "call denops#plugin#unload('dummyUnloadInvalid')", - ], ""); - - await t.step("unloads a denops plugin", async () => { - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginUnloadFail:dummyUnloadInvalid") - ); - }); - - await t.step("fires DenopsPlugin* events", async () => { - assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginUnloadPre:dummyUnloadInvalid", - "DenopsPluginUnloadFail:dummyUnloadInvalid", - ]); - }); - - await t.step("outputs an error message after delayed", async () => { - await delay(MESSAGE_DELAY); - assertMatch( - outputs.join(""), - /Failed to unload plugin 'dummyUnloadInvalid': Error: This is dummy error in async dispose/, - ); - }); - }); - - await t.step("if the plugin is not yet loaded", async (t) => { - outputs = []; - await host.call("execute", [ - "let g:__test_denops_events = []", - "call denops#plugin#unload('notexistsplugin')", + `call denops#plugin#load('dummyLoadLoaded', '${scriptValid}')`, ], ""); - await t.step("does not unload a denops plugin", async () => { + await t.step("does not load a denops plugin", async () => { const actual = wait( () => host.call("eval", "len(g:__test_denops_events)"), { timeout: 1000, interval: 100 }, @@ -247,55 +198,90 @@ testHost({ }); }); - // NOTE: Depends on 'dummyUnload' which was already unloaded in the test above. - await t.step("if the plugin is already unloaded", async (t) => { + await t.step("if the plugin is unloading", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyLoadUnloading', '${scriptValidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyLoadUnloading") + ); + outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - "call denops#plugin#unload('dummyUnload')", + `call denops#plugin#unload('dummyLoadUnloading')`, + `call denops#plugin#load('dummyLoadUnloading', '${scriptValid}')`, ], ""); - await t.step("does not unload a denops plugin", async () => { + await t.step("unloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyLoadUnloading") + ); + }); + + await t.step("does not load a denops plugin", async () => { const actual = wait( - () => host.call("eval", "len(g:__test_denops_events)"), + async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyLoadUnloading"), { timeout: 1000, interval: 100 }, ); await assertRejects(() => actual, Error, "Timeout"); }); - await t.step("does not fires DenopsPlugin* events", async () => { - assertEquals(await host.call("eval", "g:__test_denops_events"), []); + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyLoadUnloading", + "DenopsPluginUnloadPost:dummyLoadUnloading", + ]); }); - await t.step("does not output messages", async () => { - await delay(MESSAGE_DELAY); - assertEquals(outputs, []); + await t.step("calls the plugin dispose method", () => { + assertMatch(outputs.join(""), /Goodbye, Denops!/); }); }); - }); - await t.step("denops#plugin#reload()", async (t) => { - // NOTE: Depends on 'dummy' which was already loaded in the test above. - await t.step("if the plugin is already loaded", async (t) => { + await t.step("if the plugin is unloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyLoadUnloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyLoadUnloaded") + ); + // Unload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyLoadUnloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyLoadUnloaded") + ); + outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - "call denops#plugin#reload('dummy')", + `call denops#plugin#load('dummyLoadUnloaded', '${scriptValid}')`, ], ""); - await t.step("reloads a denops plugin", async () => { + await t.step("loads a denops plugin", async () => { await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummy") + .includes("DenopsPluginPost:dummyLoadUnloaded") ); }); await t.step("fires DenopsPlugin* events", async () => { assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginUnloadPre:dummy", - "DenopsPluginUnloadPost:dummy", - "DenopsPluginPre:dummy", - "DenopsPluginPost:dummy", + "DenopsPluginPre:dummyLoadUnloaded", + "DenopsPluginPost:dummyLoadUnloaded", ]); }); @@ -304,55 +290,83 @@ testHost({ }); }); - await t.step("if the plugin dispose method throws", async (t) => { + await t.step("if the plugin is reloading", async (t) => { + // Load plugin and wait. await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#load('dummyReloadInvalid', '${scriptInvalidDispose}')`, + `call denops#plugin#load('dummyLoadReloading', '${scriptValid}')`, ], ""); await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyReloadInvalid") + .includes("DenopsPluginPost:dummyLoadReloading") ); outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - "call denops#plugin#reload('dummyReloadInvalid')", + `call denops#plugin#reload('dummyLoadReloading')`, + `call denops#plugin#load('dummyLoadReloading', '${scriptValid}')`, ], ""); await t.step("reloads a denops plugin", async () => { await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyReloadInvalid") + .includes("DenopsPluginPost:dummyLoadReloading") + ); + }); + + await t.step("does not load a denops plugin twice", async () => { + const actual = wait( + async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("DenopsPluginPost:")) + .length >= 2, + { timeout: 1000, interval: 100 }, ); + await assertRejects(() => actual, Error, "Timeout"); }); await t.step("fires DenopsPlugin* events", async () => { assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginUnloadPre:dummyReloadInvalid", - "DenopsPluginUnloadFail:dummyReloadInvalid", - "DenopsPluginPre:dummyReloadInvalid", - "DenopsPluginPost:dummyReloadInvalid", + "DenopsPluginUnloadPre:dummyLoadReloading", + "DenopsPluginUnloadPost:dummyLoadReloading", + "DenopsPluginPre:dummyLoadReloading", + "DenopsPluginPost:dummyLoadReloading", ]); }); - await t.step("outputs an error message after delayed", async () => { - await delay(MESSAGE_DELAY); - assertMatch( - outputs.join(""), - /Failed to unload plugin 'dummyReloadInvalid': Error: This is dummy error in async dispose/, - ); + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); }); }); - await t.step("if the plugin is not yet loaded", async (t) => { + await t.step("if the plugin is reloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyLoadReloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyLoadReloaded") + ); + // Reload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyLoadReloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyLoadReloaded") + ); + outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - "call denops#plugin#reload('notexistsplugin')", + `call denops#plugin#load('dummyLoadReloaded', '${scriptValid}')`, ], ""); - await t.step("does not reload a denops plugin", async () => { + await t.step("does not load a denops plugin", async () => { const actual = wait( () => host.call("eval", "len(g:__test_denops_events)"), { timeout: 1000, interval: 100 }, @@ -369,16 +383,17 @@ testHost({ assertEquals(outputs, []); }); }); + }); - // NOTE: Depends on 'dummyUnload' which was already unloaded in the test above. - await t.step("if the plugin is already unloaded", async (t) => { + await t.step("denops#plugin#unload()", async (t) => { + await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - "call denops#plugin#reload('dummyUnload')", + "call denops#plugin#unload('notexistsplugin')", ], ""); - await t.step("does not reload a denops plugin", async () => { + await t.step("does not unload a denops plugin", async () => { const actual = wait( () => host.call("eval", "len(g:__test_denops_events)"), { timeout: 1000, interval: 100 }, @@ -395,84 +410,1038 @@ testHost({ assertEquals(outputs, []); }); }); - }); - - await t.step("denops#plugin#is_loaded()", async (t) => { - // NOTE: Depends on 'dummy' which was already loaded in the test above. - await t.step("returns 1 if the plugin `name` is loaded", async () => { - const actual = await host.call("denops#plugin#is_loaded", "dummy"); - assertEquals(actual, 1); - }); - - await t.step("returns 0 if the plugin `name` is not exists", async () => { - const actual = await host.call( - "denops#plugin#is_loaded", - "notexistsplugin", - ); - assertEquals(actual, 0); - }); - }); - - await t.step("denops#plugin#discover()", async (t) => { - outputs = []; - await host.call("execute", [ - "let g:__test_denops_events = []", - `set runtimepath+=${await host.call("fnameescape", runtimepathPlugin)}`, - `call denops#plugin#discover()`, - ], ""); - await t.step("loads denops plugins", async () => { + await t.step("if the plugin dispose method throws", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyUnloadInvalid', '${scriptInvalidDispose}')`, + ], ""); await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .filter((ev) => /^DenopsPlugin(?:Post|Fail):/.test(ev)).length >= 2 + .includes("DenopsPluginPost:dummyUnloadInvalid") ); - }); - await t.step("fires DenopsPlugin* events", async () => { - assertArrayIncludes( - await host.call("eval", "g:__test_denops_events") as string[], - [ - "DenopsPluginPre:dummy_valid", - "DenopsPluginPost:dummy_valid", - "DenopsPluginPre:dummy_invalid", - "DenopsPluginFail:dummy_invalid", - ], - ); - }); + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#unload('dummyUnloadInvalid')", + ], ""); - await t.step("does not load plugins name start with '@'", async () => { - const events = - (await host.call("eval", "g:__test_denops_events") as string[]) - .filter((ev) => ev.includes("@dummy_namespace")); - assertEquals(events, []); - }); + await t.step("unloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadFail:dummyUnloadInvalid") + ); + }); - await t.step("calls the plugin entrypoint", () => { - assertMatch(outputs.join(""), /Hello, Denops!/); - }); + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyUnloadInvalid", + "DenopsPluginUnloadFail:dummyUnloadInvalid", + ]); + }); - await t.step("outputs an error message after delayed", async () => { - await delay(MESSAGE_DELAY); - assertMatch( - outputs.join(""), - /Failed to load plugin 'dummy_invalid': Error: This is dummy error/, - ); + await t.step("outputs an error message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to unload plugin 'dummyUnloadInvalid': Error: This is dummy error in async dispose/, + ); + }); }); - }); - // NOTE: Depends on 'dummy' which was already loaded in the test above. - await t.step("denops#plugin#check_type()", async (t) => { - await t.step("if no arguments is specified", async (t) => { + await t.step("if the plugin is loading", async (t) => { outputs = []; - await host.call("execute", [ - // NOTE: - // Call `denops#plugin#is_loaded()` and add an entry to the internal list. - // This will result in a plugin entry whose script is empty. - "call denops#plugin#is_loaded('notexistsplugin')", - ], ""); await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#check_type()`, + `call denops#plugin#load('dummyUnloadLoading', '${scriptValidDispose}')`, + "call denops#plugin#unload('dummyUnloadLoading')", + ], ""); + + await t.step("unloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyUnloadLoading") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummyUnloadLoading", + "DenopsPluginPost:dummyUnloadLoading", + "DenopsPluginUnloadPre:dummyUnloadLoading", + "DenopsPluginUnloadPost:dummyUnloadLoading", + ]); + }); + + await t.step("calls the plugin dispose method", () => { + assertMatch(outputs.join(""), /Goodbye, Denops!/); + }); + }); + + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyUnloadLoaded', '${scriptValidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyUnloadLoaded") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#unload('dummyUnloadLoaded')", + ], ""); + + await t.step("unloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyUnloadLoaded") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyUnloadLoaded", + "DenopsPluginUnloadPost:dummyUnloadLoaded", + ]); + }); + + await t.step("calls the plugin dispose method", () => { + assertMatch(outputs.join(""), /Goodbye, Denops!/); + }); + }); + + await t.step("if the plugin is unloading", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyUnloadUnloading', '${scriptValidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyUnloadUnloading") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyUnloadUnloading')`, + "call denops#plugin#unload('dummyUnloadUnloading')", + ], ""); + + await t.step("unloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyUnloadUnloading") + ); + }); + + await t.step("does not unload a denops plugin twice", async () => { + const actual = wait( + async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("DenopsPluginUnloadPost:")) + .length >= 2, + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyUnloadUnloading", + "DenopsPluginUnloadPost:dummyUnloadUnloading", + ]); + }); + + await t.step("calls the plugin dispose method", () => { + assertMatch(outputs.join(""), /Goodbye, Denops!/); + }); + }); + + await t.step("if the plugin is unloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyUnloadUnloaded', '${scriptValidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyUnloadUnloaded") + ); + // Unload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyUnloadUnloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyUnloadUnloaded") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#unload('dummyUnloadUnloaded')", + ], ""); + + await t.step("does not unload a denops plugin", async () => { + const actual = wait( + () => host.call("eval", "len(g:__test_denops_events)"), + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("does not fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), []); + }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + + await t.step("if the plugin is reloading", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyUnloadReloading', '${scriptValidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyUnloadReloading") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyUnloadReloading')`, + "call denops#plugin#unload('dummyUnloadReloading')", + ], ""); + + await t.step("reloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyUnloadReloading") + ); + }); + + await t.step("does not unload a denops plugin twice", async () => { + const actual = wait( + async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("DenopsPluginUnloadPost:")) + .length >= 2, + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyUnloadReloading", + "DenopsPluginUnloadPost:dummyUnloadReloading", + "DenopsPluginPre:dummyUnloadReloading", + "DenopsPluginPost:dummyUnloadReloading", + ]); + }); + + await t.step("calls the plugin dispose method", () => { + assertMatch(outputs.join(""), /Goodbye, Denops!/); + }); + }); + + await t.step("if the plugin is reloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyUnloadReloaded', '${scriptValidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyUnloadReloaded") + ); + // Reload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyUnloadReloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyUnloadReloaded") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#unload('dummyUnloadReloaded')", + ], ""); + + await t.step("unloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyUnloadReloaded") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyUnloadReloaded", + "DenopsPluginUnloadPost:dummyUnloadReloaded", + ]); + }); + + await t.step("calls the plugin dispose method", () => { + assertMatch(outputs.join(""), /Goodbye, Denops!/); + }); + }); + }); + + await t.step("denops#plugin#reload()", async (t) => { + await t.step("if the plugin is not yet loaded", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#reload('notexistsplugin')", + ], ""); + + await t.step("does not reload a denops plugin", async () => { + const actual = wait( + () => host.call("eval", "len(g:__test_denops_events)"), + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("does not fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), []); + }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + + await t.step("if the plugin dispose method throws", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyReloadInvalid', '${scriptInvalidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadInvalid") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#reload('dummyReloadInvalid')", + ], ""); + + await t.step("reloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadInvalid") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyReloadInvalid", + "DenopsPluginUnloadFail:dummyReloadInvalid", + "DenopsPluginPre:dummyReloadInvalid", + "DenopsPluginPost:dummyReloadInvalid", + ]); + }); + + await t.step("outputs an error message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to unload plugin 'dummyReloadInvalid': Error: This is dummy error in async dispose/, + ); + }); + }); + + await t.step("if the plugin is loading", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyReloadLoading', '${scriptValid}')`, + "call denops#plugin#reload('dummyReloadLoading')", + ], ""); + + await t.step("reloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("DenopsPluginPost:")) + .length >= 2 + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginPre:dummyReloadLoading", + "DenopsPluginPost:dummyReloadLoading", + "DenopsPluginUnloadPre:dummyReloadLoading", + "DenopsPluginUnloadPost:dummyReloadLoading", + "DenopsPluginPre:dummyReloadLoading", + "DenopsPluginPost:dummyReloadLoading", + ]); + }); + + await t.step("calls the plugin entrypoint twice", () => { + assertMatch(outputs.join(""), /Hello, Denops!.*Hello, Denops!/s); + }); + }); + + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyReloadLoaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadLoaded") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#reload('dummyReloadLoaded')", + ], ""); + + await t.step("reloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadLoaded") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyReloadLoaded", + "DenopsPluginUnloadPost:dummyReloadLoaded", + "DenopsPluginPre:dummyReloadLoaded", + "DenopsPluginPost:dummyReloadLoaded", + ]); + }); + + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); + }); + }); + + await t.step("if the plugin is unloading", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyReloadUnloading', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadUnloading") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyReloadUnloading')`, + "call denops#plugin#reload('dummyReloadUnload')", + ], ""); + + await t.step("does not reload a denops plugin", async () => { + const actual = wait( + async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadUnloading"), + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyReloadUnloading", + "DenopsPluginUnloadPost:dummyReloadUnloading", + ]); + }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + + await t.step("if the plugin is unloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyReloadUnloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadUnloaded") + ); + // Unload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyReloadUnloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyReloadUnloaded") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#reload('dummyReloadUnload')", + ], ""); + + await t.step("does not reload a denops plugin", async () => { + const actual = wait( + () => host.call("eval", "len(g:__test_denops_events)"), + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("does not fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), []); + }); + + await t.step("does not output messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); + }); + }); + + await t.step("if the plugin is reloading", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyReloadReloading', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadReloading") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyReloadReloading')`, + "call denops#plugin#reload('dummyReloadReloading')", + ], ""); + + await t.step("reloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadReloading") + ); + }); + + await t.step("does not reload a denops plugin twice", async () => { + const actual = wait( + async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("DenopsPluginPost:")) + .length >= 2, + { timeout: 1000, interval: 100 }, + ); + await assertRejects(() => actual, Error, "Timeout"); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyReloadReloading", + "DenopsPluginUnloadPost:dummyReloadReloading", + "DenopsPluginPre:dummyReloadReloading", + "DenopsPluginPost:dummyReloadReloading", + ]); + }); + + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); + }); + }); + + await t.step("if the plugin is reloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyReloadReloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadReloaded") + ); + // Reload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyReloadReloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadReloaded") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + "call denops#plugin#reload('dummyReloadReloaded')", + ], ""); + + await t.step("reloads a denops plugin", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyReloadReloaded") + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertEquals(await host.call("eval", "g:__test_denops_events"), [ + "DenopsPluginUnloadPre:dummyReloadReloaded", + "DenopsPluginUnloadPost:dummyReloadReloaded", + "DenopsPluginPre:dummyReloadReloaded", + "DenopsPluginPost:dummyReloadReloaded", + ]); + }); + + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); + }); + }); + }); + + await t.step("denops#plugin#is_loaded()", async (t) => { + await t.step("if the plugin is not yet loaded", async (t) => { + await t.step("returns 0", async () => { + const actual = await host.call( + "denops#plugin#is_loaded", + "notexistsplugin", + ); + assertEquals(actual, 0); + }); + }); + + await t.step("if the plugin entrypoint throws", async (t) => { + // Load plugin and wait failure. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedInvalid', '${scriptInvalid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginFail:dummyIsLoadedInvalid") + ); + + await t.step("returns 1", async () => { + const actual = await host.call( + "denops#plugin#is_loaded", + "dummyIsLoadedInvalid", + ); + assertEquals(actual, 1); + }); + + await delay(MESSAGE_DELAY); // Wait outputs of denops#plugin#load() + }); + + // FIXME: Implement "if the plugin dispose method throws" + + await t.step("if the plugin is loading", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedLoading', '${scriptValid}')`, + "let g:__test_denops_plugin_is_loaded = denops#plugin#is_loaded('dummyIsLoadedLoading')", + ], ""); + + await t.step("returns 1", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_plugin_is_loaded", + ); + assertEquals(actual, 0); + }); + }); + + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedLoaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedLoaded") + ); + + await t.step("returns 1", async () => { + const actual = await host.call( + "denops#plugin#is_loaded", + "dummyIsLoadedLoaded", + ); + assertEquals(actual, 1); + }); + }); + + await t.step("if the plugin is unloading", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedUnloading', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedUnloading") + ); + + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyIsLoadedUnloading')`, + "let g:__test_denops_plugin_is_loaded = denops#plugin#is_loaded('dummyIsLoadedUnloading')", + ], ""); + + await t.step("returns 0", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_plugin_is_loaded", + ); + assertEquals(actual, 0); + }); + }); + + await t.step("if the plugin is unloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedUnloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedUnloaded") + ); + // Unload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyIsLoadedUnloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyIsLoadedUnloaded") + ); + + await t.step("returns 0", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_plugin_is_loaded", + ); + assertEquals(actual, 0); + }); + }); + + await t.step("if the plugin is reloading", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedReloading', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedReloading") + ); + + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyIsLoadedReloading')`, + "let g:__test_denops_plugin_is_loaded = denops#plugin#is_loaded('dummyIsLoadedReloading')", + ], ""); + + await t.step("returns 0", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_plugin_is_loaded", + ); + assertEquals(actual, 0); + }); + }); + + await t.step("if the plugin is reloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedReloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedReloaded") + ); + // Reload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyIsLoadedReloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedReloaded") + ); + + await t.step("returns 1", async () => { + const actual = await host.call( + "denops#plugin#is_loaded", + "dummyIsLoadedLoaded", + ); + assertEquals(actual, 1); + }); + }); + + await t.step("called in autocmd event", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { + await host.call("execute", [ + "augroup __test_denops_is_loaded", + " autocmd!", + "augroup END", + ], ""); + }); + await host.call("execute", [ + "let g:__test_denops_is_loaded = {}", + "augroup __test_denops_is_loaded", + " autocmd!", + " autocmd User DenopsPlugin* let g:__test_denops_is_loaded[expand('')] = denops#plugin#is_loaded(expand('')->matchstr(':\\zs.*'))", + "augroup END", + ], ""); + + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedEventValid', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedEventValid") + ); + // Unload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyIsLoadedEventValid')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyIsLoadedEventValid") + ); + + // Load plugin and wait failure. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedEventInvalid', '${scriptInvalid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginFail:dummyIsLoadedEventInvalid") + ); + + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedEventInvalidDispose', '${scriptInvalidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedEventInvalidDispose") + ); + // Unload plugin and wait failure. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyIsLoadedEventInvalidDispose')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadFail:dummyIsLoadedEventInvalidDispose") + ); + + await delay(MESSAGE_DELAY); // Wait outputs of denops#plugin#unload() + + await t.step("returns 0 when DenopsPluginPre", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginPre:dummyIsLoadedEventValid']", + ); + assertEquals(actual, 0); + }); + + await t.step("returns 1 when DenopsPluginPost", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginPost:dummyIsLoadedEventValid']", + ); + assertEquals(actual, 1); + }); + + await t.step("returns 1 when DenopsPluginFail", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginFail:dummyIsLoadedEventInvalid']", + ); + assertEquals(actual, 1); + }); + + await t.step("returns 0 when DenopsPluginUnloadPre", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginUnloadPre:dummyIsLoadedEventValid']", + ); + assertEquals(actual, 0); + }); + + await t.step("returns 0 when DenopsPluginUnloadPost", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginUnloadPost:dummyIsLoadedEventValid']", + ); + assertEquals(actual, 0); + }); + + // FIXME: Implement "returns 0 when DenopsPluginUnloadFail" + }); + }); + + await t.step("denops#plugin#discover()", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `set runtimepath+=${await host.call("fnameescape", runtimepathPlugin)}`, + `call denops#plugin#discover()`, + ], ""); + + await t.step("loads denops plugins", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => /^DenopsPlugin(?:Post|Fail):/.test(ev)).length >= 2 + ); + }); + + await t.step("fires DenopsPlugin* events", async () => { + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + [ + "DenopsPluginPre:dummy_valid", + "DenopsPluginPost:dummy_valid", + "DenopsPluginPre:dummy_invalid", + "DenopsPluginFail:dummy_invalid", + ], + ); + }); + + await t.step("does not load plugins name start with '@'", async () => { + const actual = + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.includes("@dummy_namespace")); + assertEquals(actual, []); + }); + + await t.step("calls the plugin entrypoint", () => { + assertMatch(outputs.join(""), /Hello, Denops!/); + }); + + await t.step("outputs an error message after delayed", async () => { + await delay(MESSAGE_DELAY); + assertMatch( + outputs.join(""), + /Failed to load plugin 'dummy_invalid': Error: This is dummy error/, + ); + }); + }); + + await t.step("denops#plugin#check_type()", async (t) => { + await t.step("if no arguments is specified", async (t) => { + outputs = []; + await host.call("execute", [ + // NOTE: + // Call `denops#plugin#is_loaded()` and add an entry to the internal list. + // This will result in a plugin entry whose script is empty. + "call denops#plugin#is_loaded('notexistsplugin')", + ], ""); + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#check_type()`, + ], ""); + + await t.step("outputs an info message after delayed", async () => { + await wait(() => outputs.join("").includes("Type check")); + assertMatch(outputs.join(""), /Type check succeeded/); + }); + }); + + await t.step("if the plugin is not yet loaded", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#check_type('notexistsplugin')`, + ], ""); + + await t.step("outputs an error message after delayed", async () => { + await wait(() => outputs.join("").includes("Type check")); + assertMatch(outputs.join(""), /Type check failed:/); + }); + }); + + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyCheckTypeLoaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyCheckTypeLoaded") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#check_type('dummyCheckTypeLoaded')`, + ], ""); + + await t.step("outputs an info message after delayed", async () => { + await wait(() => outputs.join("").includes("Type check")); + assertMatch(outputs.join(""), /Type check succeeded/); + }); + }); + + await t.step("if the plugin is unloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyCheckTypeUnloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyCheckTypeUnloaded") + ); + // Unload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyCheckTypeUnloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyCheckTypeUnloaded") + ); + + outputs = []; + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#check_type('dummyCheckTypeUnloaded')`, ], ""); await t.step("outputs an info message after delayed", async () => { @@ -481,11 +1450,30 @@ testHost({ }); }); - await t.step("if the script name is specified", async (t) => { + await t.step("if the plugin is reloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyCheckTypeReloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyCheckTypeReloaded") + ); + // Reload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyCheckTypeReloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyCheckTypeReloaded") + ); + outputs = []; await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#check_type('dummy')`, + `call denops#plugin#check_type('dummyCheckTypeReloaded')`, ], ""); await t.step("outputs an info message after delayed", async () => { @@ -493,272 +1481,610 @@ testHost({ assertMatch(outputs.join(""), /Type check succeeded/); }); }); + }); - await t.step("if a non-existent script name is specified", async (t) => { - outputs = []; + await t.step("denops#plugin#wait_async()", async (t) => { + await t.step("if the plugin is load asynchronously", async (t) => { + // Load plugin asynchronously. await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#check_type('notexistsplugin')`, + `call timer_start(1000, { -> denops#plugin#load('dummyWaitAsyncLoadAsync', '${scriptValid}') })`, ], ""); - await t.step("outputs an error message after delayed", async () => { - await wait(() => outputs.join("").includes("Type check")); - assertMatch(outputs.join(""), /Type check failed:/); + await host.call("execute", [ + "let g:__test_denops_wait_start = reltime()", + "call denops#plugin#wait_async('dummyWaitAsyncLoadAsync', { -> add(g:__test_denops_events, 'WaitAsyncCallbackCalled:dummyWaitAsyncLoadAsync') })", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); + + await t.step("returns immediately", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); + + await t.step("does not call `callback` immediately", async () => { + const actual = + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("WaitAsyncCallbackCalled:")); + assertEquals(actual, []); + }); + + await t.step("calls `callback` when the plugin is loaded", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitAsyncLoadAsync") + ); + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + ["WaitAsyncCallbackCalled:dummyWaitAsyncLoadAsync"], + ); }); }); - }); - await t.step("denops#plugin#wait_async()", async (t) => { - await t.step("if the plugin is valid", async (t) => { + await t.step("if the plugin is loading", async (t) => { await host.call("execute", [ "let g:__test_denops_events = []", - `call timer_start(1000, { -> denops#plugin#load('dummyWaitAsync', '${scriptValid}') })`, + `call denops#plugin#load('dummyWaitAsyncLoading', '${scriptValidWait}')`, + "let g:__test_denops_wait_start = reltime()", + "call denops#plugin#wait_async('dummyWaitAsyncLoading', { -> add(g:__test_denops_events, 'WaitAsyncCallbackCalled:dummyWaitAsyncLoading') })", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); + + await t.step("returns immediately", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); + + await t.step("does not call `callback` immediately", async () => { + const actual = + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("WaitAsyncCallbackCalled:")); + assertEquals(actual, []); + }); + + await t.step("calls `callback` when the plugin is loaded", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitAsyncLoading") + ); + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + ["WaitAsyncCallbackCalled:dummyWaitAsyncLoading"], + ); + }); + }); + + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + `call denops#plugin#load('dummyWaitAsyncLoaded', '${scriptValid}')`, ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitAsyncLoaded") + ); - const resultPromise = host.call("execute", [ - "call denops#plugin#wait_async('dummyWaitAsync', { -> add(g:__test_denops_events, 'wait_async callback called: dummyWaitAsync') })", + await host.call("execute", [ + "let g:__test_denops_events = []", + "let g:__test_denops_wait_start = reltime()", + "call denops#plugin#wait_async('dummyWaitAsyncLoaded', { -> add(g:__test_denops_events, 'WaitAsyncCallbackCalled:dummyWaitAsyncLoaded') })", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", ], ""); await t.step("returns immediately", async () => { - await delay(100); // host.call delay - assertEquals(await promiseState(resultPromise), "fulfilled"); - await resultPromise; + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); }); - await t.step("does not call the callback immediately", async () => { - assertEquals(await host.call("eval", "g:__test_denops_events"), []); + await t.step("calls `callback` immediately", async () => { + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + ["WaitAsyncCallbackCalled:dummyWaitAsyncLoaded"], + ); }); + }); - await t.step( - "calls the callback when the plugin is loaded", - async () => { - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyWaitAsync") - ); - assertArrayIncludes( - await host.call("eval", "g:__test_denops_events") as string[], - ["wait_async callback called: dummyWaitAsync"], - ); - }, + // FIXME: "if the plugin is reloading" + + await t.step("if the plugin is reloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitAsyncReloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitAsyncReloaded") ); + // Reload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyWaitAsyncReloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitAsyncReloaded") + ); + + await host.call("execute", [ + "let g:__test_denops_events = []", + "let g:__test_denops_wait_start = reltime()", + "call denops#plugin#wait_async('dummyWaitAsyncReloaded', { -> add(g:__test_denops_events, 'WaitAsyncCallbackCalled:dummyWaitAsyncReloaded') })", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); + + await t.step("returns immediately", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); + + await t.step("calls `callback` immediately", async () => { + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + ["WaitAsyncCallbackCalled:dummyWaitAsyncReloaded"], + ); + }); + }); + + await t.step("if the plugin is loading and fails", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitAsyncLoadingAndFails', '${scriptInvalidWait}')`, + "let g:__test_denops_wait_start = reltime()", + "call denops#plugin#wait_async('dummyWaitAsyncLoadingAndFails', { -> add(g:__test_denops_events, 'WaitAsyncCallbackCalled:dummyWaitAsyncLoadingAndFails') })", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); + + await t.step("returns immediately", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); + + await t.step("does not call `callback`", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginFail:dummyWaitAsyncLoadingAndFails") + ); + const actual = + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("WaitAsyncCallbackCalled:")); + assertEquals(actual, []); + }); + + await delay(MESSAGE_DELAY); // Wait outputs of denops#plugin#load() }); - // NOTE: Depends on 'dummyWaitAsync' which was already loaded in the test above. - await t.step("if the plugin is already loaded", async (t) => { + await t.step("if the plugin is failed to load", async (t) => { + // Load plugin and wait failure. await host.call("execute", [ "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitFailed', '${scriptInvalid}')`, ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginFail:dummyWaitFailed") + ); - const resultPromise = host.call("execute", [ - "call denops#plugin#wait_async('dummyWaitAsync', { -> add(g:__test_denops_events, 'wait_async callback called: dummyWaitAsync') })", + await host.call("execute", [ + "let g:__test_denops_wait_start = reltime()", + "call denops#plugin#wait_async('dummyWaitAsyncFailed', { -> add(g:__test_denops_events, 'WaitAsyncCallbackCalled:dummyWaitAsyncFailed') })", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", ], ""); await t.step("returns immediately", async () => { - await delay(100); // host.call delay - assertEquals(await promiseState(resultPromise), "fulfilled"); - await resultPromise; + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); + + await t.step("does not call `callback`", async () => { + const actual = + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("WaitAsyncCallbackCalled:")); + assertEquals(actual, []); }); - await t.step("calls the callback immediately", async () => { + await delay(MESSAGE_DELAY); // Wait outputs of denops#plugin#load() + }); + }); + + // NOTE: This test stops the denops server. + await t.step("denops#plugin#wait()", async (t) => { + await t.step("if the plugin is loading", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitLoading', '${scriptValidWait}')`, + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('dummyWaitLoading', {'timeout': 5000})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); + + await t.step("waits the plugin is loaded", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertGreater(elapsed_sec, 1.0); + assertLess(elapsed_sec, 5.0); + }); + + await t.step("returns 0", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, 0); + }); + + await t.step("the plugin is loaded after returns", async () => { assertArrayIncludes( await host.call("eval", "g:__test_denops_events") as string[], - ["wait_async callback called: dummyWaitAsync"], + ["DenopsPluginPost:dummyWaitLoading"], ); }); }); - await t.step("if the plugin entrypoint throws", async (t) => { + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. await host.call("execute", [ "let g:__test_denops_events = []", - `call timer_start(1000, { -> denops#plugin#load('dummyWaitAsyncInvalid', '${scriptInvalid}') })`, + `call denops#plugin#load('dummyWaitLoaded', '${scriptValid}')`, ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitLoaded") + ); - const resultPromise = host.call("execute", [ - "call denops#plugin#wait_async('dummyWaitAsyncInvalid', { -> add(g:__test_denops_events, 'wait_async callback called: dummyWaitAsync') })", + await host.call("execute", [ + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('dummyWaitLoaded', {'timeout': 5000})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", ], ""); await t.step("returns immediately", async () => { - await delay(100); // host.call delay - assertEquals(await promiseState(resultPromise), "fulfilled"); - await resultPromise; + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); }); - await t.step( - "does not call the callback when the plugin is failed", - async () => { - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginFail:dummyWaitAsyncInvalid") - ); - const events = - (await host.call("eval", "g:__test_denops_events") as string[]) - .filter((ev) => !/^DenopsPlugin/.test(ev)); - assertEquals(events, []); - }, + await t.step("returns 0", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, 0); + }); + }); + + await t.step("if the plugin is reloading", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitReloading', '${scriptValidWait}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitReloading") ); + + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyWaitReloading')`, + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('dummyWaitReloading', {'timeout': 5000})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); + + await t.step("waits the plugin is loaded", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertGreater(elapsed_sec, 1.0); + assertLess(elapsed_sec, 5.0); + }); + + await t.step("returns 0", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, 0); + }); + + await t.step("the plugin is loaded after returns", async () => { + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + ["DenopsPluginPost:dummyWaitReloading"], + ); + }); }); - }); - // NOTE: This test stops the denops server. - // FIXME: This test will run infinitely on Mac. - await t.step({ - name: "denops#plugin#wait()", - ignore: Deno.build.os === "darwin", - fn: async (t) => { - await t.step("if the plugin is valid", async (t) => { - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyWait', '${scriptValid}')`, - ], ""); + await t.step("if the plugin is reloaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitReloaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitReloaded") + ); + // Reload plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyWaitReloaded')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitReloaded") + ); + + await host.call("execute", [ + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('dummyWaitReloaded', {'timeout': 5000})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); - const resultPromise = host.call("denops#plugin#wait", "dummyWait"); + await t.step("returns immediately", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); - await t.step("waits the plugin is loaded", async () => { - assertEquals(await promiseState(resultPromise), "pending"); - }); + await t.step("returns 0", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, 0); + }); + }); - await t.step("returns 0", async () => { - assertEquals(await resultPromise, 0); - }); + await t.step("if the plugin is loading and fails", async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitLoadingAndFails', '${scriptInvalidWait}')`, + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('dummyWaitLoadingAndFails', {'timeout': 5000})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); + + await t.step("waits the plugin is failed", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertGreater(elapsed_sec, 1.0); + assertLess(elapsed_sec, 5.0); + }); + + await t.step("returns -3", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, -3); + }); - await t.step( - "the plugin is already loaded after returns", - async () => { - assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginPre:dummyWait", - "DenopsPluginPost:dummyWait", - ]); - }, + await t.step("the plugin is failed after returns", async () => { + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + ["DenopsPluginFail:dummyWaitLoadingAndFails"], ); }); + }); - // NOTE: Depends on 'dummyWait' which was already loaded in the test above. - await t.step("if the plugin is already loaded", async (t) => { - const resultPromise = host.call("denops#plugin#wait", "dummyWait"); + await t.step("if the plugin is failed to load", async (t) => { + // Load plugin and wait failure. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitFailed', '${scriptInvalid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginFail:dummyWaitFailed") + ); - await t.step("returns immediately", async () => { - await delay(100); // host.call delay - assertEquals(await promiseState(resultPromise), "fulfilled"); - }); + await host.call("execute", [ + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('dummyWaitFailed', {'timeout': 5000})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); - await t.step("returns 0", async () => { - assertEquals(await resultPromise, 0); - }); + await t.step("returns immediately", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); + + await t.step("returns -3", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, -3); }); - await t.step("if the plugin entrypoint throws", async (t) => { + await delay(MESSAGE_DELAY); // Wait outputs of denops#plugin#load() + }); + + await t.step("if it times out", async (t) => { + await t.step("if no `silent` is specified", async (t) => { + outputs = []; await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyWaitInvalid', '${scriptInvalid}')`, + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('notexistsplugin', {'timeout': 1000})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", ], ""); - const resultPromise = host.call( - "denops#plugin#wait", - "dummyWaitInvalid", - ); + await t.step("waits `timeout` expired", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertGreater(elapsed_sec, 1.0); + assertLess(elapsed_sec, 5.0); + }); - await t.step("waits the plugin is failed", async () => { - assertEquals(await promiseState(resultPromise), "pending"); + await t.step("returns -1", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, -1); + }); + + await t.step("outputs an error message", async () => { + await delay(MESSAGE_DELAY); + assertStringIncludes( + outputs.join(""), + 'Failed to wait for "notexistsplugin" to start. It took more than 1000 milliseconds and timed out.', + ); }); + }); + + await t.step("if `silent=1`", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('notexistsplugin', {'timeout': 1000, 'silent': 1})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); - await t.step("returns -3", async () => { - assertEquals(await resultPromise, -3); + await t.step("waits `timeout` expired", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertGreater(elapsed_sec, 1.0); + assertLess(elapsed_sec, 5.0); }); - await t.step( - "the plugin is already failed after returns", - async () => { - assertEquals(await host.call("eval", "g:__test_denops_events"), [ - "DenopsPluginPre:dummyWaitInvalid", - "DenopsPluginFail:dummyWaitInvalid", - ]); - }, - ); - }); - - await t.step("if it times out", async (t) => { - await t.step("if no `silent` is specified", async (t) => { - outputs = []; - - await t.step("returns -1", async () => { - const actual = await host.call( - "denops#plugin#wait", - "notexistsplugin", - { timeout: 100 }, - ); - assertEquals(actual, -1); - }); - - await t.step("outputs an error message", async () => { - await delay(MESSAGE_DELAY); - assertMatch( - outputs.join(""), - /Failed to wait for "notexistsplugin" to start\. It took more than 100 milliseconds and timed out\./, - ); - }); + await t.step("returns -1", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, -1); }); - await t.step("if `silent=1`", async (t) => { - outputs = []; - - await t.step("returns -1", async () => { - const actual = await host.call( - "denops#plugin#wait", - "notexistsplugin", - { timeout: 100, silent: 1 }, - ); - assertEquals(actual, -1); - }); - - await t.step("does not output error messages", async () => { - await delay(MESSAGE_DELAY); - assertEquals(outputs, []); - }); + await t.step("does not output error messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); }); }); + }); + + await t.step("if `denops#_internal#wait#timeout` expires", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:denops#_internal#wait#timeout = 500", + "call denops#plugin#wait('notexistsplugin', {'timeout': 1000})", + ], ""); - // NOTE: This test stops the denops server. - await t.step("if the denops server is stopped", async (t) => { - await host.call("denops#server#stop"); - await wait( - () => host.call("eval", "denops#server#status() ==# 'stopped'"), + await t.step("outputs an warning message", async () => { + await delay(MESSAGE_DELAY); + assertStringIncludes( + outputs.join(""), + "It tooks more than 500 ms. Use Ctrl-C to cancel.", ); + }); + }); + + // NOTE: This test stops the denops server. + await t.step("if the denops server is stopped", async (t) => { + await host.call("denops#server#stop"); + await wait( + () => host.call("eval", "denops#server#status() ==# 'stopped'"), + ); + + await t.step("if no `silent` is specified", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('dummy', {'timeout': 1000})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); - await t.step("if no `silent` is specified", async (t) => { - outputs = []; + await t.step("returns immediately", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); - await t.step("returns -2", async () => { - const actual = await host.call("denops#plugin#wait", "dummy"); - assertEquals(actual, -2); - }); + await t.step("returns -2", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, -2); + }); - await t.step("outputs an error message", async () => { - await delay(MESSAGE_DELAY); - assertMatch( - outputs.join(""), - /Failed to wait for "dummy" to start\. Denops server itself is not started\./, - ); - }); + await t.step("outputs an error message", async () => { + await delay(MESSAGE_DELAY); + assertStringIncludes( + outputs.join(""), + 'Failed to wait for "dummy" to start. Denops server itself is not started.', + ); }); + }); + + await t.step("if `silent=1`", async (t) => { + outputs = []; + await host.call("execute", [ + "let g:__test_denops_wait_start = reltime()", + "let g:__test_denops_wait_result = denops#plugin#wait('dummy', {'timeout': 1000, 'silent': 1})", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); - await t.step("if `silent=1`", async (t) => { - outputs = []; + await t.step("returns immediately", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); - await t.step("returns -2", async () => { - const actual = await host.call("denops#plugin#wait", "dummy", { - silent: 1, - }); - assertEquals(actual, -2); - }); + await t.step("returns -2", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_wait_result", + ); + assertEquals(actual, -2); + }); - await t.step("does not output error messages", async () => { - await delay(MESSAGE_DELAY); - assertEquals(outputs, []); - }); + await t.step("does not output error messages", async () => { + await delay(MESSAGE_DELAY); + assertEquals(outputs, []); }); }); - }, + }); }); }, }); diff --git a/tests/denops/testdata/dummy_invalid_wait_plugin.ts b/tests/denops/testdata/dummy_invalid_wait_plugin.ts new file mode 100644 index 00000000..e0ac4ff6 --- /dev/null +++ b/tests/denops/testdata/dummy_invalid_wait_plugin.ts @@ -0,0 +1,7 @@ +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import { delay } from "jsr:@std/async@0.224.0/delay"; + +export const main: Entrypoint = async (_denops) => { + await delay(1000); + throw new Error("This is dummy error"); +}; diff --git a/tests/denops/testdata/dummy_valid_wait_plugin.ts b/tests/denops/testdata/dummy_valid_wait_plugin.ts new file mode 100644 index 00000000..87cce101 --- /dev/null +++ b/tests/denops/testdata/dummy_valid_wait_plugin.ts @@ -0,0 +1,6 @@ +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import { delay } from "jsr:@std/async@0.224.0/delay"; + +export const main: Entrypoint = async (_denops) => { + await delay(1000); +}; From 655faf9b96fe38fdcbec41e3d1990370fc7ad1a8 Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 10 Jul 2024 16:58:00 +0900 Subject: [PATCH 78/99] :bug: fix plugin state Fixes #387 --- autoload/denops/_internal/plugin.vim | 19 ++++++--- tests/denops/runtime/functions/plugin_test.ts | 39 ++++++++++++++++++- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/autoload/denops/_internal/plugin.vim b/autoload/denops/_internal/plugin.vim index 8bde492b..81abf89b 100644 --- a/autoload/denops/_internal/plugin.vim +++ b/autoload/denops/_internal/plugin.vim @@ -21,17 +21,22 @@ function! denops#_internal#plugin#load(name, script) abort const l:script = denops#_internal#path#norm(a:script) const l:args = [a:name, l:script] let l:plugin = denops#_internal#plugin#get(a:name) + if l:plugin.state !=# s:STATE_RESERVED && l:plugin.state !=# s:STATE_FAILED + call denops#_internal#echo#debug(printf('already loaded. skip: %s', l:args)) + return + endif let l:plugin.state = s:STATE_LOADING let l:plugin.script = l:script - let s:plugins[a:name] = l:plugin call denops#_internal#echo#debug(printf('load plugin: %s', l:args)) call denops#_internal#server#chan#notify('invoke', ['load', l:args]) endfunction function! denops#_internal#plugin#unload(name) abort - let l:args = [a:name] + const l:args = [a:name] let l:plugin = denops#_internal#plugin#get(a:name) - let l:plugin.state = s:STATE_UNLOADING + if l:plugin.state ==# s:STATE_LOADED + let l:plugin.state = s:STATE_UNLOADING + endif call denops#_internal#echo#debug(printf('unload plugin: %s', l:args)) call denops#_internal#server#chan#notify('invoke', ['unload', l:args]) endfunction @@ -39,13 +44,17 @@ endfunction function! denops#_internal#plugin#reload(name) abort const l:args = [a:name] let l:plugin = denops#_internal#plugin#get(a:name) - let l:plugin.state = s:STATE_LOADING + if l:plugin.state ==# s:STATE_LOADED + let l:plugin.state = s:STATE_UNLOADING + endif call denops#_internal#echo#debug(printf('reload plugin: %s', l:args)) call denops#_internal#server#chan#notify('invoke', ['reload', l:args]) endfunction function! s:DenopsSystemPluginPre() abort const l:name = matchstr(expand(''), 'DenopsSystemPluginPre:\zs.*') + let l:plugin = denops#_internal#plugin#get(l:name) + let l:plugin.state = s:STATE_LOADING execute printf('doautocmd User DenopsPluginPre:%s', l:name) endfunction @@ -87,7 +96,7 @@ endfunction function! s:DenopsSystemPluginUnloadFail() abort const l:name = matchstr(expand(''), 'DenopsSystemPluginUnloadFail:\zs.*') let l:plugin = denops#_internal#plugin#get(l:name) - let l:plugin.state = s:STATE_FAILED + let l:plugin.state = s:STATE_RESERVED let l:plugin.callbacks = [] execute printf('doautocmd User DenopsPluginUnloadFail:%s', l:name) endfunction diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index 7a91748c..68bbfd76 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -1056,7 +1056,36 @@ testHost({ await delay(MESSAGE_DELAY); // Wait outputs of denops#plugin#load() }); - // FIXME: Implement "if the plugin dispose method throws" + await t.step("if the plugin dispose method throws", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyIsLoadedInvalidDispose', '${scriptInvalidDispose}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedInvalidDispose") + ); + // Unload plugin and wait failure. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#unload('dummyIsLoadedInvalidDispose')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadFail:dummyIsLoadedInvalidDispose") + ); + + await t.step("returns 0", async () => { + const actual = await host.call( + "denops#plugin#is_loaded", + "dummyIsLoadedInvalidDispose", + ); + assertEquals(actual, 0); + }); + + await delay(MESSAGE_DELAY); // Wait outputs of denops#plugin#unload() + }); await t.step("if the plugin is loading", async (t) => { await host.call("execute", [ @@ -1311,7 +1340,13 @@ testHost({ assertEquals(actual, 0); }); - // FIXME: Implement "returns 0 when DenopsPluginUnloadFail" + await t.step("returns 0 when DenopsPluginUnloadFail", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginUnloadFail:dummyIsLoadedEventInvalidDispose']", + ); + assertEquals(actual, 0); + }); }); }); From 2bd32a7566e66d1b8ca617bed96da475172efe1c Mon Sep 17 00:00:00 2001 From: Milly Date: Wed, 10 Jul 2024 17:01:07 +0900 Subject: [PATCH 79/99] :bug: fix `denops#plugin#wait_async()` calls the callback after reloaded --- autoload/denops/_internal/plugin.vim | 1 - tests/denops/runtime/functions/plugin_test.ts | 46 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/autoload/denops/_internal/plugin.vim b/autoload/denops/_internal/plugin.vim index 81abf89b..2a13246e 100644 --- a/autoload/denops/_internal/plugin.vim +++ b/autoload/denops/_internal/plugin.vim @@ -89,7 +89,6 @@ function! s:DenopsSystemPluginUnloadPost() abort const l:name = matchstr(expand(''), 'DenopsSystemPluginUnloadPost:\zs.*') let l:plugin = denops#_internal#plugin#get(l:name) let l:plugin.state = s:STATE_RESERVED - let l:plugin.callbacks = [] execute printf('doautocmd User DenopsPluginUnloadPost:%s', l:name) endfunction diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index 68bbfd76..37c81d1f 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -1628,7 +1628,51 @@ testHost({ }); }); - // FIXME: "if the plugin is reloading" + await t.step("if the plugin is reloading", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyWaitAsyncReloading', '${scriptValidWait}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitAsyncReloading") + ); + + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#reload('dummyWaitAsyncReloading')`, + "let g:__test_denops_wait_start = reltime()", + "call denops#plugin#wait_async('dummyWaitAsyncReloading', { -> add(g:__test_denops_events, 'WaitAsyncCallbackCalled:dummyWaitAsyncReloading') })", + "let g:__test_denops_wait_elapsed = g:__test_denops_wait_start->reltime()->reltimefloat()", + ], ""); + + await t.step("returns immediately", async () => { + const elapsed_sec = await host.call( + "eval", + "g:__test_denops_wait_elapsed", + ) as number; + assertLess(elapsed_sec, 0.1); + }); + + await t.step("does not call `callback` immediately", async () => { + const actual = + (await host.call("eval", "g:__test_denops_events") as string[]) + .filter((ev) => ev.startsWith("WaitAsyncCallbackCalled:")); + assertEquals(actual, []); + }); + + await t.step("calls `callback` when the plugin is loaded", async () => { + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyWaitAsyncReloading") + ); + assertArrayIncludes( + await host.call("eval", "g:__test_denops_events") as string[], + ["WaitAsyncCallbackCalled:dummyWaitAsyncReloading"], + ); + }); + }); await t.step("if the plugin is reloaded", async (t) => { // Load plugin and wait. From ae5b0025b9748b9d8bd1505b7f0bc7d652c9c558 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 11 Jul 2024 17:10:14 +0900 Subject: [PATCH 80/99] :coffee: disable strategy.fail-fast and tests needs check --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eab23ad7..e42b02ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,10 @@ jobs: run: deno task check test: + needs: check + strategy: + fail-fast: false matrix: runner: - windows-latest From 56da553c7657fcc0e4838efe5113a5d78367a435 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 11 Jul 2024 22:44:45 +0900 Subject: [PATCH 81/99] :herb: improve tests for `denops#plugin#is_loaded()` --- tests/denops/runtime/functions/plugin_test.ts | 278 ++++++++---------- 1 file changed, 119 insertions(+), 159 deletions(-) diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index 37c81d1f..e3e2e678 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -1024,6 +1024,22 @@ testHost({ }); await t.step("denops#plugin#is_loaded()", async (t) => { + await using stack = new AsyncDisposableStack(); + stack.defer(async () => { + await host.call("execute", [ + "augroup __test_denops_is_loaded", + " autocmd!", + "augroup END", + ], ""); + }); + await host.call("execute", [ + "let g:__test_denops_is_loaded = {}", + "augroup __test_denops_is_loaded", + " autocmd!", + " autocmd User DenopsPlugin* let g:__test_denops_is_loaded[expand('')] = denops#plugin#is_loaded(expand('')->matchstr(':\\zs.*'))", + "augroup END", + ], ""); + await t.step("if the plugin is not yet loaded", async (t) => { await t.step("returns 0", async () => { const actual = await host.call( @@ -1038,6 +1054,7 @@ testHost({ // Load plugin and wait failure. await host.call("execute", [ "let g:__test_denops_events = []", + "let g:__test_denops_is_loaded = {}", `call denops#plugin#load('dummyIsLoadedInvalid', '${scriptInvalid}')`, ], ""); await wait(async () => @@ -1045,7 +1062,23 @@ testHost({ .includes("DenopsPluginFail:dummyIsLoadedInvalid") ); - await t.step("returns 1", async () => { + await t.step("returns 0 when DenopsPluginPre", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginPre:dummyIsLoadedInvalid']", + ); + assertEquals(actual, 0); + }); + + await t.step("returns 1 when DenopsPluginFail", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginFail:dummyIsLoadedInvalid']", + ); + assertEquals(actual, 1); + }); + + await t.step("returns 1 after DenopsPluginFail", async () => { const actual = await host.call( "denops#plugin#is_loaded", "dummyIsLoadedInvalid", @@ -1069,6 +1102,7 @@ testHost({ // Unload plugin and wait failure. await host.call("execute", [ "let g:__test_denops_events = []", + "let g:__test_denops_is_loaded = {}", `call denops#plugin#unload('dummyIsLoadedInvalidDispose')`, ], ""); await wait(async () => @@ -1076,7 +1110,23 @@ testHost({ .includes("DenopsPluginUnloadFail:dummyIsLoadedInvalidDispose") ); - await t.step("returns 0", async () => { + await t.step("returns 0 when DenopsPluginUnloadPre", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginUnloadPre:dummyIsLoadedInvalidDispose']", + ); + assertEquals(actual, 0); + }); + + await t.step("returns 0 when DenopsPluginUnloadFail", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginUnloadFail:dummyIsLoadedInvalidDispose']", + ); + assertEquals(actual, 0); + }); + + await t.step("returns 0 after DenopsPluginUnloadFail", async () => { const actual = await host.call( "denops#plugin#is_loaded", "dummyIsLoadedInvalidDispose", @@ -1088,36 +1138,46 @@ testHost({ }); await t.step("if the plugin is loading", async (t) => { + // Load plugin and wait. await host.call("execute", [ "let g:__test_denops_events = []", + "let g:__test_denops_is_loaded = {}", `call denops#plugin#load('dummyIsLoadedLoading', '${scriptValid}')`, - "let g:__test_denops_plugin_is_loaded = denops#plugin#is_loaded('dummyIsLoadedLoading')", + "let g:__test_denops_plugin_is_loaded_after_load = denops#plugin#is_loaded('dummyIsLoadedLoading')", ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyIsLoadedLoading") + ); - await t.step("returns 1", async () => { + await t.step("returns 0 immediately after `load()`", async () => { const actual = await host.call( "eval", - "g:__test_denops_plugin_is_loaded", + "g:__test_denops_plugin_is_loaded_after_load", ); assertEquals(actual, 0); }); - }); - await t.step("if the plugin is loaded", async (t) => { - // Load plugin and wait. - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyIsLoadedLoaded', '${scriptValid}')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyIsLoadedLoaded") - ); + await t.step("returns 0 when DenopsPluginPre", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginPre:dummyIsLoadedLoading']", + ); + assertEquals(actual, 0); + }); + + await t.step("returns 1 when DenopsPluginPost", async () => { + const actual = await host.call( + "eval", + "g:__test_denops_is_loaded['DenopsPluginPost:dummyIsLoadedLoading']", + ); + assertEquals(actual, 1); + }); - await t.step("returns 1", async () => { + await t.step("returns 1 after DenopsPluginPost", async () => { const actual = await host.call( "denops#plugin#is_loaded", - "dummyIsLoadedLoaded", + "dummyIsLoadedLoading", ); assertEquals(actual, 1); }); @@ -1133,219 +1193,119 @@ testHost({ (await host.call("eval", "g:__test_denops_events") as string[]) .includes("DenopsPluginPost:dummyIsLoadedUnloading") ); - + // Unload plugin and wait. await host.call("execute", [ "let g:__test_denops_events = []", + "let g:__test_denops_is_loaded = {}", `call denops#plugin#unload('dummyIsLoadedUnloading')`, - "let g:__test_denops_plugin_is_loaded = denops#plugin#is_loaded('dummyIsLoadedUnloading')", + "let g:__test_denops_plugin_is_loaded_after_unload = denops#plugin#is_loaded('dummyIsLoadedUnloading')", ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginUnloadPost:dummyIsLoadedUnloading") + ); - await t.step("returns 0", async () => { + await t.step("returns 0 immediately after `unload()`", async () => { const actual = await host.call( "eval", - "g:__test_denops_plugin_is_loaded", + "g:__test_denops_plugin_is_loaded_after_unload", ); assertEquals(actual, 0); }); - }); - - await t.step("if the plugin is unloaded", async (t) => { - // Load plugin and wait. - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyIsLoadedUnloaded', '${scriptValid}')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyIsLoadedUnloaded") - ); - // Unload plugin and wait. - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#unload('dummyIsLoadedUnloaded')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginUnloadPost:dummyIsLoadedUnloaded") - ); - await t.step("returns 0", async () => { + await t.step("returns 0 when DenopsPluginUnloadPre", async () => { const actual = await host.call( "eval", - "g:__test_denops_plugin_is_loaded", + "g:__test_denops_is_loaded['DenopsPluginUnloadPre:dummyIsLoadedUnloading']", ); assertEquals(actual, 0); }); - }); - - await t.step("if the plugin is reloading", async (t) => { - // Load plugin and wait. - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyIsLoadedReloading', '${scriptValid}')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyIsLoadedReloading") - ); - - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#reload('dummyIsLoadedReloading')`, - "let g:__test_denops_plugin_is_loaded = denops#plugin#is_loaded('dummyIsLoadedReloading')", - ], ""); - await t.step("returns 0", async () => { + await t.step("returns 0 when DenopsPluginUnloadPost", async () => { const actual = await host.call( "eval", - "g:__test_denops_plugin_is_loaded", + "g:__test_denops_is_loaded['DenopsPluginUnloadPost:dummyIsLoadedUnloading']", ); assertEquals(actual, 0); }); - }); - await t.step("if the plugin is reloaded", async (t) => { - // Load plugin and wait. - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyIsLoadedReloaded', '${scriptValid}')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyIsLoadedReloaded") - ); - // Reload plugin and wait. - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#reload('dummyIsLoadedReloaded')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyIsLoadedReloaded") - ); - - await t.step("returns 1", async () => { + await t.step("returns 0 after DenopsPluginUnloadPost", async () => { const actual = await host.call( "denops#plugin#is_loaded", - "dummyIsLoadedLoaded", + "dummyIsLoadedUnloading", ); - assertEquals(actual, 1); + assertEquals(actual, 0); }); }); - await t.step("called in autocmd event", async (t) => { - await using stack = new AsyncDisposableStack(); - stack.defer(async () => { - await host.call("execute", [ - "augroup __test_denops_is_loaded", - " autocmd!", - "augroup END", - ], ""); - }); - await host.call("execute", [ - "let g:__test_denops_is_loaded = {}", - "augroup __test_denops_is_loaded", - " autocmd!", - " autocmd User DenopsPlugin* let g:__test_denops_is_loaded[expand('')] = denops#plugin#is_loaded(expand('')->matchstr(':\\zs.*'))", - "augroup END", - ], ""); - - // Load plugin and wait. - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyIsLoadedEventValid', '${scriptValid}')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyIsLoadedEventValid") - ); - // Unload plugin and wait. - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#unload('dummyIsLoadedEventValid')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginUnloadPost:dummyIsLoadedEventValid") - ); - - // Load plugin and wait failure. - await host.call("execute", [ - "let g:__test_denops_events = []", - `call denops#plugin#load('dummyIsLoadedEventInvalid', '${scriptInvalid}')`, - ], ""); - await wait(async () => - (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginFail:dummyIsLoadedEventInvalid") - ); - + await t.step("if the plugin is reloading", async (t) => { // Load plugin and wait. await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#load('dummyIsLoadedEventInvalidDispose', '${scriptInvalidDispose}')`, + `call denops#plugin#load('dummyIsLoadedReloading', '${scriptValid}')`, ], ""); await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginPost:dummyIsLoadedEventInvalidDispose") + .includes("DenopsPluginPost:dummyIsLoadedReloading") ); - // Unload plugin and wait failure. + // Reload plugin and wait. await host.call("execute", [ "let g:__test_denops_events = []", - `call denops#plugin#unload('dummyIsLoadedEventInvalidDispose')`, + "let g:__test_denops_is_loaded = {}", + `call denops#plugin#reload('dummyIsLoadedReloading')`, + "let g:__test_denops_plugin_is_loaded_after_reload = denops#plugin#is_loaded('dummyIsLoadedReloading')", ], ""); await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .includes("DenopsPluginUnloadFail:dummyIsLoadedEventInvalidDispose") + .includes("DenopsPluginPost:dummyIsLoadedReloading") ); - await delay(MESSAGE_DELAY); // Wait outputs of denops#plugin#unload() - - await t.step("returns 0 when DenopsPluginPre", async () => { + await t.step("returns 0 immediately after `reaload()`", async () => { const actual = await host.call( "eval", - "g:__test_denops_is_loaded['DenopsPluginPre:dummyIsLoadedEventValid']", + "g:__test_denops_plugin_is_loaded_after_reload", ); assertEquals(actual, 0); }); - await t.step("returns 1 when DenopsPluginPost", async () => { + await t.step("returns 0 when DenopsPluginUnloadPre", async () => { const actual = await host.call( "eval", - "g:__test_denops_is_loaded['DenopsPluginPost:dummyIsLoadedEventValid']", + "g:__test_denops_is_loaded['DenopsPluginUnloadPre:dummyIsLoadedReloading']", ); - assertEquals(actual, 1); + assertEquals(actual, 0); }); - await t.step("returns 1 when DenopsPluginFail", async () => { + await t.step("returns 0 when DenopsPluginUnloadPost", async () => { const actual = await host.call( "eval", - "g:__test_denops_is_loaded['DenopsPluginFail:dummyIsLoadedEventInvalid']", + "g:__test_denops_is_loaded['DenopsPluginUnloadPost:dummyIsLoadedReloading']", ); - assertEquals(actual, 1); + assertEquals(actual, 0); }); - await t.step("returns 0 when DenopsPluginUnloadPre", async () => { + await t.step("returns 0 when DenopsPluginPre", async () => { const actual = await host.call( "eval", - "g:__test_denops_is_loaded['DenopsPluginUnloadPre:dummyIsLoadedEventValid']", + "g:__test_denops_is_loaded['DenopsPluginPre:dummyIsLoadedReloading']", ); assertEquals(actual, 0); }); - await t.step("returns 0 when DenopsPluginUnloadPost", async () => { + await t.step("returns 1 when DenopsPluginPost", async () => { const actual = await host.call( "eval", - "g:__test_denops_is_loaded['DenopsPluginUnloadPost:dummyIsLoadedEventValid']", + "g:__test_denops_is_loaded['DenopsPluginPost:dummyIsLoadedReloading']", ); - assertEquals(actual, 0); + assertEquals(actual, 1); }); - await t.step("returns 0 when DenopsPluginUnloadFail", async () => { + await t.step("returns 1 after DenopsPluginPost", async () => { const actual = await host.call( - "eval", - "g:__test_denops_is_loaded['DenopsPluginUnloadFail:dummyIsLoadedEventInvalidDispose']", + "denops#plugin#is_loaded", + "dummyIsLoadedReloading", ); - assertEquals(actual, 0); + assertEquals(actual, 1); }); }); }); From 7d2ef71f4dc99f5c0ae9b9539d0096480180b47b Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 11 Jul 2024 23:21:03 +0900 Subject: [PATCH 82/99] :boom: restrict plugin {name} in `denops#plugin#*` methods --- autoload/denops/_internal/plugin.vim | 9 +++ doc/denops.txt | 24 ++++-- tests/denops/runtime/functions/plugin_test.ts | 80 +++++++++++++++++++ 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/autoload/denops/_internal/plugin.vim b/autoload/denops/_internal/plugin.vim index 2a13246e..f1ffe38a 100644 --- a/autoload/denops/_internal/plugin.vim +++ b/autoload/denops/_internal/plugin.vim @@ -4,10 +4,19 @@ const s:STATE_LOADED = 'loaded' const s:STATE_UNLOADING = 'unloading' const s:STATE_FAILED = 'failed' +const s:VALID_NAME_PATTERN = '^[-_0-9a-zA-Z]\+$' + let s:plugins = {} +function! denops#_internal#plugin#is_valid_name(name) abort + return a:name =~# s:VALID_NAME_PATTERN +endfunction + function! denops#_internal#plugin#get(name) abort if !has_key(s:plugins, a:name) + if !denops#_internal#plugin#is_valid_name(a:name) + throw printf('[denops] Invalid plugin name: %s', a:name) + endif let s:plugins[a:name] = #{name: a:name, script: '', state: s:STATE_RESERVED, callbacks: []} endif return s:plugins[a:name] diff --git a/doc/denops.txt b/doc/denops.txt index 8d4fc4b0..7d23b3bf 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -347,12 +347,17 @@ denops#server#wait_async({callback}) denops#plugin#is_loaded({name}) Returns 1 if a {name} plugin is already loaded or failed to load. Returns 0 otherwise, even if |denops#plugin#unload()| or - |denops#plugin#reload()| are called. + |denops#plugin#reload()| are called. It throws an error when {name} does + not match the |denops-plugin-name|. + + The *denops-plugin-name* only allows ASCII characters '0'-'9', 'a'-'z', + 'A'-'Z', '_' and '-', and may not be empty. *denops#plugin#wait()* denops#plugin#wait({name}[, {options}]) Waits synchronously until a {name} plugin is loaded or failed to load. - It returns immediately when the {name} plugin is already loaded. + It returns immediately when the {name} plugin is already loaded. It + throws an error when {name} does not match the |denops-plugin-name|. Developers need to consider the following return value to decide whether to continue with the subsequent process: @@ -378,7 +383,8 @@ denops#plugin#wait_async({name}, {callback}) {callback}. It invokes the {callback} immediately when the {name} plugin is already loaded. If this function is called multiple times for the same {name} plugin, callbacks registered for the plugin are - called in order of registration. + called in order of registration. It throws an error when {name} does + not match the |denops-plugin-name|. *denops#plugin#register()* denops#plugin#register({name}[, {script}[, {options}]]) @@ -415,7 +421,8 @@ denops#plugin#discover() denops#plugin#load({name}, {script}) Loads a denops plugin. Use this function to load denops plugins that are not discovered by |denops#plugin#discover()|. If the {name} plugin - is already loaded, it does nothing. + is already loaded, it does nothing. It throws an error when {name} + does not match the |denops-plugin-name|. Loading a plugin involves the following event steps: @@ -428,7 +435,8 @@ denops#plugin#load({name}, {script}) denops#plugin#unload({name}) Unloads a denops plugin. If the {name} plugin is currently loading, it will be unloaded after it has been successfully loaded. If the {name} - plugin does not exist or fails to load, it does nothing. + plugin does not exist or fails to load, it does nothing. It throws an + error when {name} does not match the |denops-plugin-name|. Unloading a plugin involves the following event steps: @@ -444,7 +452,8 @@ denops#plugin#unload({name}) *denops#plugin#reload()* denops#plugin#reload({plugin}[, {options}]) Reloads a denops plugin. If the {name} plugin does not exist or fails - to load, it does nothing. + to load, it does nothing. It throws an error when {name} does not + match the |denops-plugin-name|. It invokes |User| autocommand events. See |denops#plugin#load()| and |denops#plugin#unload()| for details. @@ -452,7 +461,8 @@ denops#plugin#reload({plugin}[, {options}]) *denops#plugin#check_type()* denops#plugin#check_type([{name}]) Runs Deno's type check feature for {name} or all registered plugins. - The result of the check will be displayed in |message-history|. + The result of the check will be displayed in |message-history|. It + throws an error when {name} does not match the |denops-plugin-name|. *denops#callback#register()* denops#callback#register({callback}[, {options}]) diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index e3e2e678..2a9aba86 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -40,6 +40,17 @@ testHost({ ], ""); await t.step("denops#plugin#load()", async (t) => { + await t.step("if the plugin name is invalid", async (t) => { + await t.step("throws an error", async () => { + // NOTE: '.' is not allowed in plugin name. + await assertRejects( + () => host.call("denops#plugin#load", "dummy.invalid", scriptValid), + Error, + "Invalid plugin name: dummy.invalid", + ); + }); + }); + await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; await host.call("execute", [ @@ -386,6 +397,17 @@ testHost({ }); await t.step("denops#plugin#unload()", async (t) => { + await t.step("if the plugin name is invalid", async (t) => { + await t.step("throws an error", async () => { + // NOTE: '.' is not allowed in plugin name. + await assertRejects( + () => host.call("denops#plugin#unload", "dummy.invalid"), + Error, + "Invalid plugin name: dummy.invalid", + ); + }); + }); + await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; await host.call("execute", [ @@ -704,6 +726,17 @@ testHost({ }); await t.step("denops#plugin#reload()", async (t) => { + await t.step("if the plugin name is invalid", async (t) => { + await t.step("throws an error", async () => { + // NOTE: '.' is not allowed in plugin name. + await assertRejects( + () => host.call("denops#plugin#reload", "dummy.invalid"), + Error, + "Invalid plugin name: dummy.invalid", + ); + }); + }); + await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; await host.call("execute", [ @@ -1040,6 +1073,17 @@ testHost({ "augroup END", ], ""); + await t.step("if the plugin name is invalid", async (t) => { + await t.step("throws an error", async () => { + // NOTE: '.' is not allowed in plugin name. + await assertRejects( + () => host.call("denops#plugin#is_loaded", "dummy.invalid"), + Error, + "Invalid plugin name: dummy.invalid", + ); + }); + }); + await t.step("if the plugin is not yet loaded", async (t) => { await t.step("returns 0", async () => { const actual = await host.call( @@ -1377,6 +1421,17 @@ testHost({ }); }); + await t.step("if the plugin name is invalid", async (t) => { + await t.step("throws an error", async () => { + // NOTE: '.' is not allowed in plugin name. + await assertRejects( + () => host.call("denops#plugin#check_type", "dummy.invalid"), + Error, + "Invalid plugin name: dummy.invalid", + ); + }); + }); + await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; await host.call("execute", [ @@ -1479,6 +1534,20 @@ testHost({ }); await t.step("denops#plugin#wait_async()", async (t) => { + await t.step("if the plugin name is invalid", async (t) => { + await t.step("throws an error", async () => { + // NOTE: '.' is not allowed in plugin name. + await assertRejects( + () => + host.call("execute", [ + "call denops#plugin#wait_async('dummy.invalid', { -> 0 })", + ], ""), + Error, + "Invalid plugin name: dummy.invalid", + ); + }); + }); + await t.step("if the plugin is load asynchronously", async (t) => { // Load plugin asynchronously. await host.call("execute", [ @@ -1746,6 +1815,17 @@ testHost({ // NOTE: This test stops the denops server. await t.step("denops#plugin#wait()", async (t) => { + await t.step("if the plugin name is invalid", async (t) => { + await t.step("throws an error", async () => { + // NOTE: '.' is not allowed in plugin name. + await assertRejects( + () => host.call("denops#plugin#wait", "dummy.invalid"), + Error, + "Invalid plugin name: dummy.invalid", + ); + }); + }); + await t.step("if the plugin is loading", async (t) => { await host.call("execute", [ "let g:__test_denops_events = []", From 0db258f1d0c3ddcfa9690da46579fa22ef4f7d10 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 12 Jul 2024 00:54:05 +0900 Subject: [PATCH 83/99] :boom: discover only valid name plugins --- autoload/denops/plugin.vim | 2 +- doc/denops.txt | 4 ++-- tests/denops/runtime/functions/plugin_test.ts | 15 ++++++++++++--- .../denops/dummy.invalid_name/main.ts | 6 ++++++ 4 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 tests/denops/testdata/dummy_plugins/denops/dummy.invalid_name/main.ts diff --git a/autoload/denops/plugin.vim b/autoload/denops/plugin.vim index 84ca02c9..29bd945a 100644 --- a/autoload/denops/plugin.vim +++ b/autoload/denops/plugin.vim @@ -66,7 +66,7 @@ function! denops#plugin#discover() abort let l:counter = 0 for l:script in globpath(&runtimepath, l:pattern, 1, 1, 1) let l:name = fnamemodify(l:script, ':h:t') - if l:name[:0] ==# '@' || !filereadable(l:script) + if !denops#_internal#plugin#is_valid_name(l:name) || !filereadable(l:script) continue endif call denops#plugin#load(l:name, l:script) diff --git a/doc/denops.txt b/doc/denops.txt index 7d23b3bf..dc2941be 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -412,8 +412,8 @@ denops#plugin#register({name}[, {script}[, {options}]]) *denops#plugin#discover()* denops#plugin#discover() Discovers denops plugins from 'runtimepath' and loads them. - It gathers "main.ts" under "denops/*" directories under 'runtimepath' - if the middle directory does not start with "@". + It gathers "main.ts" in directories that match the |denops-plugin-name| + from "denops/*" under 'runtimepath'. This is automatically called on |User| |DenopsReady| autocmd invoked by |denops#server#start()|. diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index 2a9aba86..f9718bcb 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -1363,9 +1363,14 @@ testHost({ ], ""); await t.step("loads denops plugins", async () => { + const loaded_events = [ + "DenopsPluginPost:", + "DenopsPluginFail:", + ]; await wait(async () => (await host.call("eval", "g:__test_denops_events") as string[]) - .filter((ev) => /^DenopsPlugin(?:Post|Fail):/.test(ev)).length >= 2 + .filter((ev) => loaded_events.some((name) => ev.startsWith(name))) + .length >= 2 ); }); @@ -1381,10 +1386,14 @@ testHost({ ); }); - await t.step("does not load plugins name start with '@'", async () => { + await t.step("does not load invaid name plugins", async () => { + const valid_names = [ + ":dummy_valid", + ":dummy_invalid", + ] as const; const actual = (await host.call("eval", "g:__test_denops_events") as string[]) - .filter((ev) => ev.includes("@dummy_namespace")); + .filter((ev) => !valid_names.some((name) => ev.endsWith(name))); assertEquals(actual, []); }); diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy.invalid_name/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy.invalid_name/main.ts new file mode 100644 index 00000000..8184e982 --- /dev/null +++ b/tests/denops/testdata/dummy_plugins/denops/dummy.invalid_name/main.ts @@ -0,0 +1,6 @@ +import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; + +// NOTE: This should not be called, a directory contains '.' is not a valid denops plugin. +export const main: Entrypoint = async (denops) => { + await denops.cmd("echo 'Hello, Denops!'"); +}; From 5f436285db9d8a6628eea54150b1a23187788591 Mon Sep 17 00:00:00 2001 From: Milly Date: Fri, 12 Jul 2024 01:06:16 +0900 Subject: [PATCH 84/99] :boom: delete deprecated `denops#plugin#register()` --- autoload/denops/plugin.vim | 28 ---------------------------- doc/denops.txt | 23 ----------------------- 2 files changed, 51 deletions(-) diff --git a/autoload/denops/plugin.vim b/autoload/denops/plugin.vim index 29bd945a..a5ce303b 100644 --- a/autoload/denops/plugin.vim +++ b/autoload/denops/plugin.vim @@ -96,33 +96,5 @@ function! denops#plugin#check_type(...) abort \ }) endfunction -" DEPRECATED -" Some plugins (e.g. dein.vim) use this function with options thus we cannot -" change the interface of this function. -" That's why we introduce 'load' function that replaces this function. -function! denops#plugin#register(name, ...) abort - call denops#_internal#echo#deprecate( - \ 'denops#plugin#register() is deprecated. Use denops#plugin#load() instead.', - \) - if a:0 is# 0 || type(a:1) is# v:t_dict - let l:script = s:find_script(a:name) - else - let l:script = a:1 - endif - return denops#plugin#load(a:name, l:script) -endfunction - -function! s:find_script(name) abort - const l:pattern = denops#_internal#path#join(['denops', a:name, 'main.ts']) - for l:script in globpath(&runtimepath, l:pattern, 1, 1, 1) - let l:name = fnamemodify(l:script, ':h:t') - if l:name[:0] ==# '@' || !filereadable(l:script) - continue - endif - return l:script - endfor - throw printf('Denops plugin "%s" does not exist in the runtimepath', a:name) -endfunction - call denops#_internal#conf#define('denops#plugin#wait_interval', 200) call denops#_internal#conf#define('denops#plugin#wait_timeout', 30000) diff --git a/doc/denops.txt b/doc/denops.txt index dc2941be..c99717c9 100644 --- a/doc/denops.txt +++ b/doc/denops.txt @@ -386,29 +386,6 @@ denops#plugin#wait_async({name}, {callback}) called in order of registration. It throws an error when {name} does not match the |denops-plugin-name|. - *denops#plugin#register()* -denops#plugin#register({name}[, {script}[, {options}]]) - DEPRECATED: Use |denops#plugin#load()| instead. - - Developers who would like to support previous denops versions should - fallback to this function like -> - try - call denops#plugin#load( - \ "denops-hello", - \ "/path/to/denops-hello/main.ts", - \) - catch /^Vim\%((\a\+)\)\=:E117:/ - " Fallback to `register` for backward compatibility - call denops#plugin#register( - \ "denops-hello", - \ "/path/to/denops-hello/main.ts", - \ {"mode": "skip"}, - \) - endtry -< - Note that the {options} are ignored and always treated as skip mode. - *denops#plugin#discover()* denops#plugin#discover() Discovers denops plugins from 'runtimepath' and loads them. From 488d33027587125d7ad3de95eb5002ba4cfabba2 Mon Sep 17 00:00:00 2001 From: Milly Date: Thu, 11 Jul 2024 16:48:33 +0900 Subject: [PATCH 85/99] :muscle: use `denops#_internal#event#emit()` instead `doautocmd` --- autoload/denops/_internal/event.vim | 3 + autoload/denops/_internal/plugin.vim | 12 +- autoload/denops/_internal/server/chan.vim | 2 +- autoload/denops/_internal/server/proc.vim | 6 +- autoload/denops/server.vim | 11 +- denops/@denops-private/service.ts | 2 +- denops/@denops-private/service_test.ts | 250 +++++++++------------- denops/@denops-private/worker.ts | 6 +- denops/@denops-private/worker_test.ts | 20 +- 9 files changed, 126 insertions(+), 186 deletions(-) create mode 100644 autoload/denops/_internal/event.vim diff --git a/autoload/denops/_internal/event.vim b/autoload/denops/_internal/event.vim new file mode 100644 index 00000000..694bac16 --- /dev/null +++ b/autoload/denops/_internal/event.vim @@ -0,0 +1,3 @@ +function denops#_internal#event#emit(name) abort + execute 'doautocmd User' a:name +endfunction diff --git a/autoload/denops/_internal/plugin.vim b/autoload/denops/_internal/plugin.vim index f1ffe38a..a14a8f9a 100644 --- a/autoload/denops/_internal/plugin.vim +++ b/autoload/denops/_internal/plugin.vim @@ -64,7 +64,7 @@ function! s:DenopsSystemPluginPre() abort const l:name = matchstr(expand(''), 'DenopsSystemPluginPre:\zs.*') let l:plugin = denops#_internal#plugin#get(l:name) let l:plugin.state = s:STATE_LOADING - execute printf('doautocmd User DenopsPluginPre:%s', l:name) + call denops#_internal#event#emit(printf('DenopsPluginPre:%s', l:name)) endfunction function! s:DenopsSystemPluginPost() abort @@ -76,7 +76,7 @@ function! s:DenopsSystemPluginPost() abort for l:Callback in l:callbacks call l:Callback() endfor - execute printf('doautocmd User DenopsPluginPost:%s', l:name) + call denops#_internal#event#emit(printf('DenopsPluginPost:%s', l:name)) endfunction function! s:DenopsSystemPluginFail() abort @@ -84,21 +84,21 @@ function! s:DenopsSystemPluginFail() abort let l:plugin = denops#_internal#plugin#get(l:name) let l:plugin.state = s:STATE_FAILED let l:plugin.callbacks = [] - execute printf('doautocmd User DenopsPluginFail:%s', l:name) + call denops#_internal#event#emit(printf('DenopsPluginFail:%s', l:name)) endfunction function! s:DenopsSystemPluginUnloadPre() abort const l:name = matchstr(expand(''), 'DenopsSystemPluginUnloadPre:\zs.*') let l:plugin = denops#_internal#plugin#get(l:name) let l:plugin.state = s:STATE_UNLOADING - execute printf('doautocmd User DenopsPluginUnloadPre:%s', l:name) + call denops#_internal#event#emit(printf('DenopsPluginUnloadPre:%s', l:name)) endfunction function! s:DenopsSystemPluginUnloadPost() abort const l:name = matchstr(expand(''), 'DenopsSystemPluginUnloadPost:\zs.*') let l:plugin = denops#_internal#plugin#get(l:name) let l:plugin.state = s:STATE_RESERVED - execute printf('doautocmd User DenopsPluginUnloadPost:%s', l:name) + call denops#_internal#event#emit(printf('DenopsPluginUnloadPost:%s', l:name)) endfunction function! s:DenopsSystemPluginUnloadFail() abort @@ -106,7 +106,7 @@ function! s:DenopsSystemPluginUnloadFail() abort let l:plugin = denops#_internal#plugin#get(l:name) let l:plugin.state = s:STATE_RESERVED let l:plugin.callbacks = [] - execute printf('doautocmd User DenopsPluginUnloadFail:%s', l:name) + call denops#_internal#event#emit(printf('DenopsPluginUnloadFail:%s', l:name)) endfunction augroup denops_autoload_plugin_internal diff --git a/autoload/denops/_internal/server/chan.vim b/autoload/denops/_internal/server/chan.vim index 36d8c55e..3272b0e5 100644 --- a/autoload/denops/_internal/server/chan.vim +++ b/autoload/denops/_internal/server/chan.vim @@ -137,7 +137,7 @@ function! s:on_close(options) abort let s:chan = v:null call s:clear_force_close_delayer() call denops#_internal#echo#debug(printf('Channel closed (%s)', s:addr)) - doautocmd User DenopsSystemClosed + call denops#_internal#event#emit('DenopsSystemClosed') if s:chan isnot# v:null || !a:options.reconnect_on_close || s:closed_on_purpose || s:exiting return endif diff --git a/autoload/denops/_internal/server/proc.vim b/autoload/denops/_internal/server/proc.vim index e2916f17..d3cb41cd 100644 --- a/autoload/denops/_internal/server/proc.vim +++ b/autoload/denops/_internal/server/proc.vim @@ -68,7 +68,7 @@ function! s:start(options) abort \ 'on_exit': { _job, status, _event -> s:on_exit(a:options, status) }, \}) call denops#_internal#echo#debug(printf('Server started: %s', l:args)) - doautocmd User DenopsSystemProcessStarted + call denops#_internal#event#emit('DenopsSystemProcessStarted') endfunction function! s:on_stdout(store, data) abort @@ -81,7 +81,7 @@ function! s:on_stdout(store, data) abort let a:store.prepared = 1 let l:addr = substitute(a:data, '\r\?\n$', '', 'g') call denops#_internal#echo#debug(printf('Server listen: %s', l:addr)) - execute printf('doautocmd User DenopsSystemProcessListen:%s', l:addr) + call denops#_internal#event#emit(printf('DenopsSystemProcessListen:%s', l:addr)) endfunction function! s:on_stderr(data) abort @@ -95,7 +95,7 @@ endfunction function! s:on_exit(options, status) abort let s:job = v:null call denops#_internal#echo#debug(printf('Server stopped: %s', a:status)) - execute printf('doautocmd User DenopsSystemProcessStopped:%s', a:status) + call denops#_internal#event#emit(printf('DenopsSystemProcessStopped:%s', a:status)) if s:job isnot# v:null || !a:options.restart_on_exit || s:stopped_on_purpose || s:exiting return endif diff --git a/autoload/denops/server.vim b/autoload/denops/server.vim index 7c768e26..269d2d9e 100644 --- a/autoload/denops/server.vim +++ b/autoload/denops/server.vim @@ -211,7 +211,7 @@ function! s:disconnect(...) abort endfunction function! s:DenopsSystemProcessStarted() abort - doautocmd User DenopsProcessStarted + call denops#_internal#event#emit('DenopsProcessStarted') endfunction function! s:DenopsSystemProcessListen(expr) abort @@ -229,7 +229,7 @@ function! s:DenopsSystemReady() abort call l:Callback() endfor finally - doautocmd User DenopsReady + call denops#_internal#event#emit('DenopsReady') endtry endfunction @@ -253,7 +253,7 @@ function! s:DenopsSystemClosed() abort endif let s:local_addr = "" finally - doautocmd User DenopsClosed + call denops#_internal#event#emit('DenopsClosed') endtry endfunction @@ -268,10 +268,7 @@ function! s:DenopsSystemProcessStopped(expr) abort call denops#server#start() endif finally - execute printf( - \ 'doautocmd User DenopsProcessStopped:%s', - \ l:status, - \) + call denops#_internal#event#emit(printf('DenopsProcessStopped:%s', l:status)) endtry endfunction diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 0ce63b78..b2666766 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -281,7 +281,7 @@ function createScriptSuffix(script: string): string { /** NOTE: `emit()` is never throws or rejects. */ async function emit(denops: Denops, name: string): Promise { try { - await denops.cmd(`doautocmd User ${name}`); + await denops.call("denops#_internal#event#emit", name); } catch (e) { console.error(`Failed to emit ${name}: ${e}`); } diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index d5e28268..98da350e 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -106,9 +106,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], }); }); @@ -126,9 +125,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPost", () => { assertSpyCall(host_call, 2, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPost:dummy", ], }); }); @@ -155,9 +153,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], }); }); @@ -165,9 +162,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginFail", () => { assertSpyCall(host_call, 1, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginFail:dummy", ], }); }); @@ -207,9 +203,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], }); }); @@ -217,9 +212,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginFail", () => { assertSpyCall(host_call, 1, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginFail:dummy", ], }); }); @@ -261,9 +255,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], }); }); @@ -271,9 +264,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginFail", () => { assertSpyCall(host_call, 1, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginFail:dummy", ], }); }); @@ -324,9 +316,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], }); }); @@ -344,9 +335,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPost", () => { assertSpyCall(host_call, 2, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPost:dummy", ], }); }); @@ -382,9 +372,8 @@ Deno.test("Service", async (t) => { assertEquals(events, [ // Previous `load()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], [ "denops#api#cmd", @@ -392,9 +381,8 @@ Deno.test("Service", async (t) => { {}, ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPost:dummy", ], ]); }); @@ -434,14 +422,12 @@ Deno.test("Service", async (t) => { assertEquals(events, [ // Previous `unload()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], ]); }); @@ -465,9 +451,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], }); }); @@ -475,9 +460,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadPost", () => { assertSpyCall(host_call, 1, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], }); }); @@ -499,9 +483,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], }); }); @@ -519,9 +502,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadPost", () => { assertSpyCall(host_call, 2, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], }); }); @@ -552,9 +534,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], }); }); @@ -562,9 +543,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadFail", () => { assertSpyCall(host_call, 1, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadFail:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadFail:dummy", ], }); }); @@ -642,9 +622,8 @@ Deno.test("Service", async (t) => { assertEquals(events, [ // Previous `load()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], [ "denops#api#cmd", @@ -652,20 +631,17 @@ Deno.test("Service", async (t) => { {}, ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPost:dummy", ], // This `unload()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], ]); }); @@ -701,14 +677,12 @@ Deno.test("Service", async (t) => { assertEquals(events, [ // Previous `load()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginFail:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginFail:dummy", ], ]); }); @@ -739,14 +713,12 @@ Deno.test("Service", async (t) => { assertEquals(events, [ // Previous `unload()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], ]); }); @@ -800,9 +772,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], }); }); @@ -810,9 +781,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadPost", () => { assertSpyCall(host_call, 1, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], }); }); @@ -820,9 +790,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPre", () => { assertSpyCall(host_call, 2, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], }); }); @@ -840,9 +809,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPost", () => { assertSpyCall(host_call, 4, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPost:dummy", ], }); }); @@ -920,9 +888,8 @@ Deno.test("Service", async (t) => { assertEquals(events, [ // Previous `load()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], [ "denops#api#cmd", @@ -930,25 +897,21 @@ Deno.test("Service", async (t) => { {}, ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPost:dummy", ], // This `reload()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], [ "denops#api#cmd", @@ -956,9 +919,8 @@ Deno.test("Service", async (t) => { {}, ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPost:dummy", ], ]); }); @@ -989,20 +951,17 @@ Deno.test("Service", async (t) => { assertEquals(events, [ // Previous `unload()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], // This `reload()` events. [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], [ "denops#api#cmd", @@ -1010,9 +969,8 @@ Deno.test("Service", async (t) => { {}, ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPost:dummy", ], ]); }); @@ -1051,9 +1009,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadPre", () => { assertSpyCall(host_call, 0, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], }); }); @@ -1061,9 +1018,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginUnloadPost", () => { assertSpyCall(host_call, 1, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], }); }); @@ -1071,9 +1027,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPre", () => { assertSpyCall(host_call, 2, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPre:dummy", ], }); }); @@ -1091,9 +1046,8 @@ Deno.test("Service", async (t) => { await t.step("emits DenopsSystemPluginPost", () => { assertSpyCall(host_call, 4, { args: [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginPost:dummy", ], }); }); @@ -1581,14 +1535,12 @@ Deno.test("Service", async (t) => { await t.step("unloads loaded plugins", () => { assertArrayIncludes(host_call.calls.map((c) => c.args), [ [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummy", ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPre:dummyDispose", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPre:dummyDispose", ], [ "denops#api#cmd", @@ -1596,14 +1548,12 @@ Deno.test("Service", async (t) => { {}, ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummy", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummy", ], [ - "denops#api#cmd", - "doautocmd User DenopsSystemPluginUnloadPost:dummyDispose", - {}, + "denops#_internal#event#emit", + "DenopsSystemPluginUnloadPost:dummyDispose", ], ]); }); diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index c6e16b20..13fda33d 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -79,11 +79,7 @@ async function connectHost(): Promise { using sigintTrap = asyncSignal("SIGINT"); await using service = new Service(meta); await host.init(service); - await host.call( - "execute", - "doautocmd User DenopsSystemReady", - "", - ); + await host.call("denops#_internal#event#emit", "DenopsSystemReady"); await Promise.race([ service.waitClosed(), host.waitClosed(), diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index 8be61b76..b344933f 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -206,7 +206,7 @@ for (const { host, mode } of matrix) { }); } - await t.step("doautocmd `User DenopsSystemReady`", async () => { + await t.step("emits `DenopsSystemReady`", async () => { using _ = usePostMessageHistory(); await delay(0); assertEquals(messageStub.postMessage.callCount, 1); @@ -216,10 +216,7 @@ for (const { host, mode } of matrix) { [ "call", "denops#api#vim#call", - ["execute", [ - "doautocmd User DenopsSystemReady", - "", - ]], + ["denops#_internal#event#emit", ["DenopsSystemReady"]], -2, ], ); @@ -231,10 +228,7 @@ for (const { host, mode } of matrix) { 0, 2, "nvim_call_function", - ["execute", [ - "doautocmd User DenopsSystemReady", - "", - ]], + ["denops#_internal#event#emit", ["DenopsSystemReady"]], ], ); messageStub.fakeHostMessage(nvimCodec.encode([1, 2, null, ""])); @@ -455,7 +449,7 @@ for (const { host, mode } of matrix) { // requests Meta data messageStub.fakeHostMessage(vimCodec.encode([-1, [fakeMeta, ""]])); await delay(0); - // doautocmd `User DenopsSystemReady` + // emits `DenopsSystemReady` messageStub.fakeHostMessage(vimCodec.encode([-2, ["", ""]])); await delay(0); } else { @@ -468,7 +462,7 @@ for (const { host, mode } of matrix) { // sets client info messageStub.fakeHostMessage(nvimCodec.encode([1, 1, null, 0])); await delay(0); - // doautocmd `User DenopsSystemReady` + // emits `DenopsSystemReady` messageStub.fakeHostMessage(nvimCodec.encode([1, 2, null, ""])); await delay(0); } @@ -533,7 +527,7 @@ for (const { host, mode } of matrix) { // requests Meta data messageStub.fakeHostMessage(vimCodec.encode([-1, [fakeMeta, ""]])); await delay(0); - // doautocmd `User DenopsSystemReady` + // emits `DenopsSystemReady` messageStub.fakeHostMessage(vimCodec.encode([-2, ["", ""]])); await delay(0); } else { @@ -546,7 +540,7 @@ for (const { host, mode } of matrix) { // sets client info messageStub.fakeHostMessage(nvimCodec.encode([1, 1, null, 0])); await delay(0); - // doautocmd `User DenopsSystemReady` + // emits `DenopsSystemReady` messageStub.fakeHostMessage(nvimCodec.encode([1, 2, null, ""])); await delay(0); } From 30505f12919dd1d013526dcf7c97c45a87e5e590 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:10:42 +0900 Subject: [PATCH 86/99] :package: Use `@denops/core@7.0.0` --- denops/@denops-private/denops.ts | 9 ++------- denops/@denops-private/denops_test.ts | 2 +- denops/@denops-private/service.ts | 2 +- denops/@denops-private/service_test.ts | 2 +- denops/@denops-private/util.ts | 2 +- denops/@denops-private/worker.ts | 2 +- tests/denops/testdata/dummy_invalid_constraint_plugin.ts | 2 +- .../denops/testdata/dummy_invalid_constraint_plugin2.ts | 2 +- tests/denops/testdata/dummy_invalid_dispose_plugin.ts | 2 +- tests/denops/testdata/dummy_invalid_plugin.ts | 2 +- tests/denops/testdata/dummy_invalid_wait_plugin.ts | 2 +- .../dummy_plugins/denops/@dummy_namespace/main.ts | 2 +- .../dummy_plugins/denops/dummy.invalid_name/main.ts | 2 +- .../testdata/dummy_plugins/denops/dummy_invalid/main.ts | 2 +- .../testdata/dummy_plugins/denops/dummy_valid/main.ts | 2 +- tests/denops/testdata/dummy_valid_dispose_plugin.ts | 2 +- tests/denops/testdata/dummy_valid_plugin.ts | 2 +- tests/denops/testdata/dummy_valid_wait_plugin.ts | 2 +- tests/denops/testutil/mock.ts | 2 +- 19 files changed, 20 insertions(+), 25 deletions(-) diff --git a/denops/@denops-private/denops.ts b/denops/@denops-private/denops.ts index 373a99b7..3cb17e35 100644 --- a/denops/@denops-private/denops.ts +++ b/denops/@denops-private/denops.ts @@ -1,10 +1,5 @@ -import type { - Context, - Denops, - Dispatcher, - Meta, -} from "jsr:@denops/core@7.0.0-pre1"; -import { BatchError } from "jsr:@denops/core@7.0.0-pre1"; +import type { Context, Denops, Dispatcher, Meta } from "jsr:@denops/core@7.0.0"; +import { BatchError } from "jsr:@denops/core@7.0.0"; import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; import type { Host as HostOrigin } from "./host.ts"; import type { Service as ServiceOrigin } from "./service.ts"; diff --git a/denops/@denops-private/denops_test.ts b/denops/@denops-private/denops_test.ts index f845db46..3809e66c 100644 --- a/denops/@denops-private/denops_test.ts +++ b/denops/@denops-private/denops_test.ts @@ -1,4 +1,4 @@ -import { BatchError, type Meta } from "jsr:@denops/core@7.0.0-pre1"; +import { BatchError, type Meta } from "jsr:@denops/core@7.0.0"; import { assertEquals, assertInstanceOf, diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index b2666766..578a383a 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -1,4 +1,4 @@ -import type { Denops, Entrypoint, Meta } from "jsr:@denops/core@7.0.0-pre1"; +import type { Denops, Entrypoint, Meta } from "jsr:@denops/core@7.0.0"; import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.0.0"; import { DenopsImpl, type Host } from "./denops.ts"; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 98da350e..118d06dc 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -17,7 +17,7 @@ import { spy, stub, } from "jsr:@std/testing@0.224.0/mock"; -import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; +import type { Meta } from "jsr:@denops/core@7.0.0"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import type { Host } from "./denops.ts"; diff --git a/denops/@denops-private/util.ts b/denops/@denops-private/util.ts index 3f906fa5..46ff7110 100644 --- a/denops/@denops-private/util.ts +++ b/denops/@denops-private/util.ts @@ -1,4 +1,4 @@ -import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; +import type { Meta } from "jsr:@denops/core@7.0.0"; import { is, type Predicate } from "jsr:@core/unknownutil@3.18.0"; export const isMeta: Predicate = is.ObjectOf({ diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index 13fda33d..4a65c004 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -8,7 +8,7 @@ import { import { ensure } from "jsr:@core/unknownutil@3.18.0"; import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; -import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; +import type { Meta } from "jsr:@denops/core@7.0.0"; import type { Host, HostConstructor } from "./host.ts"; import { Vim } from "./host/vim.ts"; import { Neovim } from "./host/nvim.ts"; diff --git a/tests/denops/testdata/dummy_invalid_constraint_plugin.ts b/tests/denops/testdata/dummy_invalid_constraint_plugin.ts index 2c5356e8..66f512a9 100644 --- a/tests/denops/testdata/dummy_invalid_constraint_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_constraint_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; export const main: Entrypoint = (_denops) => { // Mimic the situation diff --git a/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts b/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts index 7e903ac4..e38a3fe9 100644 --- a/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts +++ b/tests/denops/testdata/dummy_invalid_constraint_plugin2.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; export const main: Entrypoint = (_denops) => { // Mimic the situation diff --git a/tests/denops/testdata/dummy_invalid_dispose_plugin.ts b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts index 314abf8d..7d2cc862 100644 --- a/tests/denops/testdata/dummy_invalid_dispose_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_dispose_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; export const main: Entrypoint = (_denops) => { return { diff --git a/tests/denops/testdata/dummy_invalid_plugin.ts b/tests/denops/testdata/dummy_invalid_plugin.ts index c98c9db1..c2551a5d 100644 --- a/tests/denops/testdata/dummy_invalid_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; export const main: Entrypoint = (_denops) => { throw new Error("This is dummy error"); diff --git a/tests/denops/testdata/dummy_invalid_wait_plugin.ts b/tests/denops/testdata/dummy_invalid_wait_plugin.ts index e0ac4ff6..01db1ce6 100644 --- a/tests/denops/testdata/dummy_invalid_wait_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_wait_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; import { delay } from "jsr:@std/async@0.224.0/delay"; export const main: Entrypoint = async (_denops) => { diff --git a/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts b/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts index db34c981..7501ca3a 100644 --- a/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/@dummy_namespace/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; // NOTE: This should not be called, a directory starting with '@' is not a denops plugin. export const main: Entrypoint = async (denops) => { diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy.invalid_name/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy.invalid_name/main.ts index 8184e982..93cff944 100644 --- a/tests/denops/testdata/dummy_plugins/denops/dummy.invalid_name/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/dummy.invalid_name/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; // NOTE: This should not be called, a directory contains '.' is not a valid denops plugin. export const main: Entrypoint = async (denops) => { diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts index c98c9db1..c2551a5d 100644 --- a/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/dummy_invalid/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; export const main: Entrypoint = (_denops) => { throw new Error("This is dummy error"); diff --git a/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts b/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts index 6e21fc82..342d9196 100644 --- a/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts +++ b/tests/denops/testdata/dummy_plugins/denops/dummy_valid/main.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; export const main: Entrypoint = async (denops) => { await denops.cmd("echo 'Hello, Denops!'"); diff --git a/tests/denops/testdata/dummy_valid_dispose_plugin.ts b/tests/denops/testdata/dummy_valid_dispose_plugin.ts index 85f159da..9ab42ba0 100644 --- a/tests/denops/testdata/dummy_valid_dispose_plugin.ts +++ b/tests/denops/testdata/dummy_valid_dispose_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; export const main: Entrypoint = (denops) => { return { diff --git a/tests/denops/testdata/dummy_valid_plugin.ts b/tests/denops/testdata/dummy_valid_plugin.ts index 7482eb4e..b19c72e0 100644 --- a/tests/denops/testdata/dummy_valid_plugin.ts +++ b/tests/denops/testdata/dummy_valid_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; export const main: Entrypoint = async (denops) => { denops.dispatcher = { diff --git a/tests/denops/testdata/dummy_valid_wait_plugin.ts b/tests/denops/testdata/dummy_valid_wait_plugin.ts index 87cce101..11d01019 100644 --- a/tests/denops/testdata/dummy_valid_wait_plugin.ts +++ b/tests/denops/testdata/dummy_valid_wait_plugin.ts @@ -1,4 +1,4 @@ -import type { Entrypoint } from "jsr:@denops/core@7.0.0-pre1"; +import type { Entrypoint } from "jsr:@denops/core@7.0.0"; import { delay } from "jsr:@std/async@0.224.0/delay"; export const main: Entrypoint = async (_denops) => { diff --git a/tests/denops/testutil/mock.ts b/tests/denops/testutil/mock.ts index f0895fc7..1647addb 100644 --- a/tests/denops/testutil/mock.ts +++ b/tests/denops/testutil/mock.ts @@ -1,5 +1,5 @@ import { AssertionError, unimplemented } from "jsr:@std/assert@0.225.2"; -import type { Meta } from "jsr:@denops/core@7.0.0-pre1"; +import type { Meta } from "jsr:@denops/core@7.0.0"; /** Returns a Promise that is never resolves or rejects. */ export function pendingPromise(): Promise { From 40550fccd7b36e2305322ebf4c5e71f879722797 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:44:38 +0900 Subject: [PATCH 87/99] :coffee: Refine `update` task --- deno.jsonc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index eb713672..6b0eeeb9 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -4,8 +4,9 @@ "test": "LANG=C deno test -A --parallel --shuffle --doc", "test:coverage": "deno task test --coverage=.coverage", "coverage": "deno coverage --exclude=\"test[.]ts(#.*)?$\" .coverage", - "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", - "update:commit": "deno task -q update --commit --pre-commit=fmt,lint" + "update": "deno run --allow-env --allow-read --allow-write --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", + "update:write": "deno task -q update --write", + "update:commit": "deno task -q update --commit --prefix :package: --pre-commit=fmt,lint" }, "exclude": [ ".coverage/" From 04f546d2e23e81f45d5158b1cb9833a2b1211664 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:14 +0900 Subject: [PATCH 88/99] :package: bump @core/unknownutil from 3.18.0 to 3.18.1 --- denops/@denops-private/denops.ts | 2 +- denops/@denops-private/error.ts | 2 +- denops/@denops-private/error_test.ts | 2 +- denops/@denops-private/host.ts | 2 +- denops/@denops-private/host/nvim.ts | 2 +- denops/@denops-private/host/vim.ts | 2 +- denops/@denops-private/host_test.ts | 2 +- denops/@denops-private/util.ts | 2 +- denops/@denops-private/version_test.ts | 2 +- denops/@denops-private/worker.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/denops/@denops-private/denops.ts b/denops/@denops-private/denops.ts index 3cb17e35..996c9cae 100644 --- a/denops/@denops-private/denops.ts +++ b/denops/@denops-private/denops.ts @@ -1,6 +1,6 @@ import type { Context, Denops, Dispatcher, Meta } from "jsr:@denops/core@7.0.0"; import { BatchError } from "jsr:@denops/core@7.0.0"; -import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; +import { ensure, is } from "jsr:@core/unknownutil@3.18.1"; import type { Host as HostOrigin } from "./host.ts"; import type { Service as ServiceOrigin } from "./service.ts"; diff --git a/denops/@denops-private/error.ts b/denops/@denops-private/error.ts index 23d5d59f..2c095d11 100644 --- a/denops/@denops-private/error.ts +++ b/denops/@denops-private/error.ts @@ -1,4 +1,4 @@ -import { is } from "jsr:@core/unknownutil@3.18.0"; +import { is } from "jsr:@core/unknownutil@3.18.1"; import { fromErrorObject, isErrorObject, diff --git a/denops/@denops-private/error_test.ts b/denops/@denops-private/error_test.ts index 371d77de..1d341410 100644 --- a/denops/@denops-private/error_test.ts +++ b/denops/@denops-private/error_test.ts @@ -3,7 +3,7 @@ import { assertEquals, assertInstanceOf, } from "jsr:@std/assert@0.225.1"; -import { is } from "jsr:@core/unknownutil@3.18.0"; +import { is } from "jsr:@core/unknownutil@3.18.1"; import { errorDeserializer, errorSerializer } from "./error.ts"; Deno.test("errorSerializer", async (t) => { diff --git a/denops/@denops-private/host.ts b/denops/@denops-private/host.ts index 93887158..f135dfdd 100644 --- a/denops/@denops-private/host.ts +++ b/denops/@denops-private/host.ts @@ -1,4 +1,4 @@ -import { ensure, is, type Predicate } from "jsr:@core/unknownutil@3.18.0"; +import { ensure, is, type Predicate } from "jsr:@core/unknownutil@3.18.1"; /** * Host (Vim/Neovim) which is visible from Service diff --git a/denops/@denops-private/host/nvim.ts b/denops/@denops-private/host/nvim.ts index 81f2615c..82cf6363 100644 --- a/denops/@denops-private/host/nvim.ts +++ b/denops/@denops-private/host/nvim.ts @@ -1,4 +1,4 @@ -import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; +import { ensure, is } from "jsr:@core/unknownutil@3.18.1"; import { Client, Session } from "jsr:@lambdalisue/messagepack-rpc@2.4.0"; import { errorDeserializer, errorSerializer } from "../error.ts"; import { getVersionOr } from "../version.ts"; diff --git a/denops/@denops-private/host/vim.ts b/denops/@denops-private/host/vim.ts index e92b3592..dd0f2393 100644 --- a/denops/@denops-private/host/vim.ts +++ b/denops/@denops-private/host/vim.ts @@ -1,4 +1,4 @@ -import { ensure, is } from "jsr:@core/unknownutil@3.18.0"; +import { ensure, is } from "jsr:@core/unknownutil@3.18.1"; import { Client, type Message, diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index 0d93d3d4..c9ea81cb 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -4,7 +4,7 @@ import { assertSpyCalls, stub, } from "jsr:@std/testing@0.224.0/mock"; -import { AssertError } from "jsr:@core/unknownutil@3.18.0"; +import { AssertError } from "jsr:@core/unknownutil@3.18.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; import { invoke, type Service } from "./host.ts"; diff --git a/denops/@denops-private/util.ts b/denops/@denops-private/util.ts index 46ff7110..ecbaa6d1 100644 --- a/denops/@denops-private/util.ts +++ b/denops/@denops-private/util.ts @@ -1,5 +1,5 @@ import type { Meta } from "jsr:@denops/core@7.0.0"; -import { is, type Predicate } from "jsr:@core/unknownutil@3.18.0"; +import { is, type Predicate } from "jsr:@core/unknownutil@3.18.1"; export const isMeta: Predicate = is.ObjectOf({ mode: is.LiteralOneOf(["release", "debug", "test"] as const), diff --git a/denops/@denops-private/version_test.ts b/denops/@denops-private/version_test.ts index e49624ae..a2525a2a 100644 --- a/denops/@denops-private/version_test.ts +++ b/denops/@denops-private/version_test.ts @@ -1,7 +1,7 @@ import { assert, assertEquals } from "jsr:@std/assert@0.225.1"; import { resolvesNext, stub } from "jsr:@std/testing@0.224.0/mock"; import type { SemVer } from "jsr:@std/semver@0.224.0/types"; -import { is, type Predicate } from "jsr:@core/unknownutil@3.18.0"; +import { is, type Predicate } from "jsr:@core/unknownutil@3.18.1"; import { getVersionOr } from "./version.ts"; Deno.test("getVersionOr()", async (t) => { diff --git a/denops/@denops-private/worker.ts b/denops/@denops-private/worker.ts index 4a65c004..3da75287 100644 --- a/denops/@denops-private/worker.ts +++ b/denops/@denops-private/worker.ts @@ -5,7 +5,7 @@ import { readableStreamFromWorker, writableStreamFromWorker, } from "jsr:@lambdalisue/workerio@4.0.1"; -import { ensure } from "jsr:@core/unknownutil@3.18.0"; +import { ensure } from "jsr:@core/unknownutil@3.18.1"; import { pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; import type { Meta } from "jsr:@denops/core@7.0.0"; From 25e01b311fe5a54b46bbd71352c500909f7e6331 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:15 +0900 Subject: [PATCH 89/99] :package: bump @denops/vim-channel-command from 4.0.0 to 4.0.2 --- denops/@denops-private/host/vim.ts | 2 +- denops/@denops-private/host/vim_test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/denops/@denops-private/host/vim.ts b/denops/@denops-private/host/vim.ts index dd0f2393..2a61a27b 100644 --- a/denops/@denops-private/host/vim.ts +++ b/denops/@denops-private/host/vim.ts @@ -3,7 +3,7 @@ import { Client, type Message, Session, -} from "jsr:@denops/vim-channel-command@4.0.0"; +} from "jsr:@denops/vim-channel-command@4.0.2"; import { type Host, invoke, type Service } from "../host.ts"; export class Vim implements Host { diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index fa269d31..9209738f 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -12,7 +12,7 @@ import { import { delay } from "jsr:@std/async@0.224.0/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; -import { Client, Session } from "jsr:@denops/vim-channel-command@4.0.0"; +import { Client, Session } from "jsr:@denops/vim-channel-command@4.0.2"; import { withVim } from "/denops-testutil/with.ts"; import type { Service } from "../host.ts"; import { Vim } from "./vim.ts"; From 2f9f523a09b6fa9cb5b871ba99131305150fcc4b Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:15 +0900 Subject: [PATCH 90/99] :package: bump @lambdalisue/errorutil from 1.0.0 to 1.1.0 --- denops/@denops-private/error.ts | 2 +- denops/@denops-private/host/nvim_test.ts | 2 +- denops/@denops-private/host/vim_test.ts | 2 +- denops/@denops-private/host_test.ts | 2 +- denops/@denops-private/service.ts | 2 +- denops/@denops-private/service_test.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/denops/@denops-private/error.ts b/denops/@denops-private/error.ts index 2c095d11..4c698328 100644 --- a/denops/@denops-private/error.ts +++ b/denops/@denops-private/error.ts @@ -4,7 +4,7 @@ import { isErrorObject, toErrorObject, tryOr, -} from "jsr:@lambdalisue/errorutil@1.0.0"; +} from "jsr:@lambdalisue/errorutil@1.1.0"; export function errorSerializer(err: unknown): unknown { if (err instanceof Error) { diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index 6afd6ef2..46847400 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -12,7 +12,7 @@ import { } from "jsr:@std/testing@0.224.0/mock"; import { delay } from "jsr:@std/async@0.224.0/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; -import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; import { Client } from "jsr:@lambdalisue/messagepack-rpc@2.4.0"; import { withNeovim } from "/denops-testutil/with.ts"; import type { Service } from "../host.ts"; diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index 9209738f..f3373d60 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -11,7 +11,7 @@ import { } from "jsr:@std/testing@0.224.0/mock"; import { delay } from "jsr:@std/async@0.224.0/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; -import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; import { Client, Session } from "jsr:@denops/vim-channel-command@4.0.2"; import { withVim } from "/denops-testutil/with.ts"; import type { Service } from "../host.ts"; diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index c9ea81cb..5a685870 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -5,7 +5,7 @@ import { stub, } from "jsr:@std/testing@0.224.0/mock"; import { AssertError } from "jsr:@core/unknownutil@3.18.1"; -import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; import { invoke, type Service } from "./host.ts"; Deno.test("invoke", async (t) => { diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 578a383a..6e461842 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -1,6 +1,6 @@ import type { Denops, Entrypoint, Meta } from "jsr:@denops/core@7.0.0"; import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; -import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.1.0"; import { DenopsImpl, type Host } from "./denops.ts"; import type { CallbackId, Service as HostService } from "./host.ts"; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 118d06dc..58c79028 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -19,7 +19,7 @@ import { } from "jsr:@std/testing@0.224.0/mock"; import type { Meta } from "jsr:@denops/core@7.0.0"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; -import { unimplemented } from "jsr:@lambdalisue/errorutil@1.0.0"; +import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; import type { Host } from "./denops.ts"; import { Service } from "./service.ts"; import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; From 180d8cd99632a8d8a56eebc3bc1e847e05800536 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:15 +0900 Subject: [PATCH 91/99] :package: bump @lambdalisue/workerio from 4.0.0 to 4.0.1 --- denops/@denops-private/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/denops/@denops-private/cli.ts b/denops/@denops-private/cli.ts index 58fa8114..db936a54 100644 --- a/denops/@denops-private/cli.ts +++ b/denops/@denops-private/cli.ts @@ -1,7 +1,7 @@ import { readableStreamFromWorker, writableStreamFromWorker, -} from "jsr:@lambdalisue/workerio@4.0.0"; +} from "jsr:@lambdalisue/workerio@4.0.1"; import { deadline } from "jsr:@std/async@0.224.0/deadline"; import { parseArgs } from "jsr:@std/cli@0.224.3/parse-args"; import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; From 74f8650a1c3f4c37c76892c3237aabf4cb6874da Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:15 +0900 Subject: [PATCH 92/99] :package: bump @std/assert to 1.0.1 --- denops/@denops-private/cli_test.ts | 2 +- denops/@denops-private/denops_test.ts | 2 +- denops/@denops-private/error_test.ts | 6 +----- denops/@denops-private/host/nvim_test.ts | 2 +- denops/@denops-private/host/vim_test.ts | 2 +- denops/@denops-private/host_test.ts | 2 +- denops/@denops-private/service_test.ts | 2 +- denops/@denops-private/version_test.ts | 2 +- denops/@denops-private/worker_test.ts | 2 +- tests/denops/runtime/functions/plugin_test.ts | 2 +- tests/denops/runtime/functions/server_test.ts | 2 +- tests/denops/runtime/plugin_test.ts | 2 +- tests/denops/testutil/conf_test.ts | 2 +- tests/denops/testutil/mock.ts | 2 +- tests/denops/testutil/shared_server.ts | 2 +- tests/denops/testutil/shared_server_test.ts | 2 +- tests/denops/testutil/wait.ts | 2 +- tests/denops/testutil/wait_test.ts | 2 +- 18 files changed, 18 insertions(+), 22 deletions(-) diff --git a/denops/@denops-private/cli_test.ts b/denops/@denops-private/cli_test.ts index b968ad95..f027cdd7 100644 --- a/denops/@denops-private/cli_test.ts +++ b/denops/@denops-private/cli_test.ts @@ -7,7 +7,7 @@ import { assertMatch, assertNotMatch, assertStringIncludes, -} from "jsr:@std/assert@0.225.2"; +} from "jsr:@std/assert@1.0.1"; import { assertSpyCallArgs, assertSpyCalls, diff --git a/denops/@denops-private/denops_test.ts b/denops/@denops-private/denops_test.ts index 3809e66c..629ec97e 100644 --- a/denops/@denops-private/denops_test.ts +++ b/denops/@denops-private/denops_test.ts @@ -5,7 +5,7 @@ import { assertRejects, assertStrictEquals, unimplemented, -} from "jsr:@std/assert@0.225.1"; +} from "jsr:@std/assert@1.0.1"; import { assertSpyCallArgs, assertSpyCalls, diff --git a/denops/@denops-private/error_test.ts b/denops/@denops-private/error_test.ts index 1d341410..7f87a543 100644 --- a/denops/@denops-private/error_test.ts +++ b/denops/@denops-private/error_test.ts @@ -1,8 +1,4 @@ -import { - assert, - assertEquals, - assertInstanceOf, -} from "jsr:@std/assert@0.225.1"; +import { assert, assertEquals, assertInstanceOf } from "jsr:@std/assert@1.0.1"; import { is } from "jsr:@core/unknownutil@3.18.1"; import { errorDeserializer, errorSerializer } from "./error.ts"; diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index 46847400..f05b3cab 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -3,7 +3,7 @@ import { assertMatch, assertRejects, assertStringIncludes, -} from "jsr:@std/assert@0.225.1"; +} from "jsr:@std/assert@1.0.1"; import { assertSpyCallArgs, assertSpyCalls, diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index f3373d60..d6521819 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -2,7 +2,7 @@ import { assertEquals, assertMatch, assertRejects, -} from "jsr:@std/assert@0.225.1"; +} from "jsr:@std/assert@1.0.1"; import { assertSpyCallArgs, assertSpyCalls, diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index 5a685870..82f7efbe 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -1,4 +1,4 @@ -import { assertThrows } from "jsr:@std/assert@0.225.1"; +import { assertThrows } from "jsr:@std/assert@1.0.1"; import { assertSpyCall, assertSpyCalls, diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 58c79028..f38eb95b 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -9,7 +9,7 @@ import { assertRejects, assertStrictEquals, assertThrows, -} from "jsr:@std/assert@0.225.1"; +} from "jsr:@std/assert@1.0.1"; import { assertSpyCall, assertSpyCalls, diff --git a/denops/@denops-private/version_test.ts b/denops/@denops-private/version_test.ts index a2525a2a..b2be96d2 100644 --- a/denops/@denops-private/version_test.ts +++ b/denops/@denops-private/version_test.ts @@ -1,4 +1,4 @@ -import { assert, assertEquals } from "jsr:@std/assert@0.225.1"; +import { assert, assertEquals } from "jsr:@std/assert@1.0.1"; import { resolvesNext, stub } from "jsr:@std/testing@0.224.0/mock"; import type { SemVer } from "jsr:@std/semver@0.224.0/types"; import { is, type Predicate } from "jsr:@core/unknownutil@3.18.1"; diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index b344933f..b3c24e7b 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -5,7 +5,7 @@ import { assertInstanceOf, assertMatch, assertObjectMatch, -} from "jsr:@std/assert@0.225.2"; +} from "jsr:@std/assert@1.0.1"; import { assertSpyCalls, resolvesNext, diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index f9718bcb..b91bfe89 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -6,7 +6,7 @@ import { assertMatch, assertRejects, assertStringIncludes, -} from "jsr:@std/assert@0.225.2"; +} from "jsr:@std/assert@1.0.1"; import { delay } from "jsr:@std/async@^0.224.0"; import { join } from "jsr:@std/path@0.225.0/join"; import { AsyncDisposableStack } from "jsr:@nick/dispose@1.1.0/async-disposable-stack"; diff --git a/tests/denops/runtime/functions/server_test.ts b/tests/denops/runtime/functions/server_test.ts index b16adcb4..19efc79b 100644 --- a/tests/denops/runtime/functions/server_test.ts +++ b/tests/denops/runtime/functions/server_test.ts @@ -4,7 +4,7 @@ import { assertFalse, assertMatch, assertRejects, -} from "jsr:@std/assert@0.225.2"; +} from "jsr:@std/assert@1.0.1"; import { delay } from "jsr:@std/async@^0.224.0/delay"; import { AsyncDisposableStack } from "jsr:@nick/dispose@1.1.0/async-disposable-stack"; import { testHost } from "/denops-testutil/host.ts"; diff --git a/tests/denops/runtime/plugin_test.ts b/tests/denops/runtime/plugin_test.ts index 690221a6..2239d8a3 100644 --- a/tests/denops/runtime/plugin_test.ts +++ b/tests/denops/runtime/plugin_test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertMatch } from "jsr:@std/assert@0.225.2"; +import { assertEquals, assertMatch } from "jsr:@std/assert@1.0.1"; import { delay } from "jsr:@std/async@^0.224.0/delay"; import { withHost } from "/denops-testutil/host.ts"; import { useSharedServer } from "/denops-testutil/shared_server.ts"; diff --git a/tests/denops/testutil/conf_test.ts b/tests/denops/testutil/conf_test.ts index 433b7474..05417ad2 100644 --- a/tests/denops/testutil/conf_test.ts +++ b/tests/denops/testutil/conf_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "jsr:@std/assert@0.225.1"; +import { assertEquals } from "jsr:@std/assert@1.0.1"; import { _internal } from "./conf.ts"; Deno.test({ diff --git a/tests/denops/testutil/mock.ts b/tests/denops/testutil/mock.ts index 1647addb..fc32b318 100644 --- a/tests/denops/testutil/mock.ts +++ b/tests/denops/testutil/mock.ts @@ -1,4 +1,4 @@ -import { AssertionError, unimplemented } from "jsr:@std/assert@0.225.2"; +import { AssertionError, unimplemented } from "jsr:@std/assert@1.0.1"; import type { Meta } from "jsr:@denops/core@7.0.0"; /** Returns a Promise that is never resolves or rejects. */ diff --git a/tests/denops/testutil/shared_server.ts b/tests/denops/testutil/shared_server.ts index 9c422af5..4970a599 100644 --- a/tests/denops/testutil/shared_server.ts +++ b/tests/denops/testutil/shared_server.ts @@ -1,4 +1,4 @@ -import { assert } from "jsr:@std/assert@0.225.2"; +import { assert } from "jsr:@std/assert@1.0.1"; import { deadline } from "jsr:@std/async@0.224.0/deadline"; import { resolve } from "jsr:@std/path@0.224.0/resolve"; import { channel, pop } from "jsr:@lambdalisue/streamtools@1.0.0"; diff --git a/tests/denops/testutil/shared_server_test.ts b/tests/denops/testutil/shared_server_test.ts index c63ca98a..833a0097 100644 --- a/tests/denops/testutil/shared_server_test.ts +++ b/tests/denops/testutil/shared_server_test.ts @@ -4,7 +4,7 @@ import { assertMatch, assertNotMatch, assertRejects, -} from "jsr:@std/assert@0.225.2"; +} from "jsr:@std/assert@1.0.1"; import { delay } from "jsr:@std/async@0.224.0/delay"; import { join } from "jsr:@std/path@0.225.0/join"; import { useSharedServer } from "./shared_server.ts"; diff --git a/tests/denops/testutil/wait.ts b/tests/denops/testutil/wait.ts index 95f62c87..96a93141 100644 --- a/tests/denops/testutil/wait.ts +++ b/tests/denops/testutil/wait.ts @@ -1,4 +1,4 @@ -import { AssertionError } from "jsr:@std/assert@0.225.1/assertion-error"; +import { AssertionError } from "jsr:@std/assert@1.0.1/assertion-error"; import { getConfig } from "./conf.ts"; const DEFAULT_TIMEOUT = 30_000; diff --git a/tests/denops/testutil/wait_test.ts b/tests/denops/testutil/wait_test.ts index 8646932e..e79589cd 100644 --- a/tests/denops/testutil/wait_test.ts +++ b/tests/denops/testutil/wait_test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertRejects } from "jsr:@std/assert@0.225.1"; +import { assertEquals, assertRejects } from "jsr:@std/assert@1.0.1"; import { assertSpyCalls, resolvesNext, From ddc8956aae514c951ad98be933bb63bd7658779f Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:15 +0900 Subject: [PATCH 93/99] :package: bump @std/async from 0.224.0 to 1.0.1 --- denops/@denops-private/cli.ts | 2 +- denops/@denops-private/cli_test.ts | 2 +- denops/@denops-private/host/nvim_test.ts | 2 +- denops/@denops-private/host/vim_test.ts | 2 +- denops/@denops-private/worker_test.ts | 2 +- tests/denops/testdata/dummy_invalid_wait_plugin.ts | 2 +- tests/denops/testdata/dummy_valid_wait_plugin.ts | 2 +- tests/denops/testdata/shared_server_test_no_verbose.ts | 2 +- tests/denops/testdata/shared_server_test_verbose_true.ts | 2 +- tests/denops/testutil/shared_server.ts | 2 +- tests/denops/testutil/shared_server_test.ts | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/denops/@denops-private/cli.ts b/denops/@denops-private/cli.ts index db936a54..0ada81d2 100644 --- a/denops/@denops-private/cli.ts +++ b/denops/@denops-private/cli.ts @@ -2,7 +2,7 @@ import { readableStreamFromWorker, writableStreamFromWorker, } from "jsr:@lambdalisue/workerio@4.0.1"; -import { deadline } from "jsr:@std/async@0.224.0/deadline"; +import { deadline } from "jsr:@std/async@1.0.1/deadline"; import { parseArgs } from "jsr:@std/cli@0.224.3/parse-args"; import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; diff --git a/denops/@denops-private/cli_test.ts b/denops/@denops-private/cli_test.ts index f027cdd7..651aed91 100644 --- a/denops/@denops-private/cli_test.ts +++ b/denops/@denops-private/cli_test.ts @@ -18,7 +18,7 @@ import { stub, } from "jsr:@std/testing@0.224.0/mock"; import { FakeTime } from "jsr:@std/testing@0.224.0/time"; -import { delay } from "jsr:@std/async@0.224.0/delay"; +import { delay } from "jsr:@std/async@1.0.1/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { createFakeTcpConn, diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index f05b3cab..7339c342 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -10,7 +10,7 @@ import { resolvesNext, stub, } from "jsr:@std/testing@0.224.0/mock"; -import { delay } from "jsr:@std/async@0.224.0/delay"; +import { delay } from "jsr:@std/async@1.0.1/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; import { Client } from "jsr:@lambdalisue/messagepack-rpc@2.4.0"; diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index d6521819..89700aa9 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -9,7 +9,7 @@ import { resolvesNext, stub, } from "jsr:@std/testing@0.224.0/mock"; -import { delay } from "jsr:@std/async@0.224.0/delay"; +import { delay } from "jsr:@std/async@1.0.1/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; import { Client, Session } from "jsr:@denops/vim-channel-command@4.0.2"; diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index b3c24e7b..c0b9a69d 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -12,7 +12,7 @@ import { spy, stub, } from "jsr:@std/testing@0.224.0/mock"; -import { delay } from "jsr:@std/async@0.224.0/delay"; +import { delay } from "jsr:@std/async@1.0.1/delay"; import { DisposableStack } from "jsr:@nick/dispose@1.1.0/disposable-stack"; import * as nvimCodec from "jsr:@lambdalisue/messagepack@^1.0.1"; import { createFakeMeta } from "/denops-testutil/mock.ts"; diff --git a/tests/denops/testdata/dummy_invalid_wait_plugin.ts b/tests/denops/testdata/dummy_invalid_wait_plugin.ts index 01db1ce6..f66bd16f 100644 --- a/tests/denops/testdata/dummy_invalid_wait_plugin.ts +++ b/tests/denops/testdata/dummy_invalid_wait_plugin.ts @@ -1,5 +1,5 @@ import type { Entrypoint } from "jsr:@denops/core@7.0.0"; -import { delay } from "jsr:@std/async@0.224.0/delay"; +import { delay } from "jsr:@std/async@1.0.1/delay"; export const main: Entrypoint = async (_denops) => { await delay(1000); diff --git a/tests/denops/testdata/dummy_valid_wait_plugin.ts b/tests/denops/testdata/dummy_valid_wait_plugin.ts index 11d01019..bcd80b04 100644 --- a/tests/denops/testdata/dummy_valid_wait_plugin.ts +++ b/tests/denops/testdata/dummy_valid_wait_plugin.ts @@ -1,5 +1,5 @@ import type { Entrypoint } from "jsr:@denops/core@7.0.0"; -import { delay } from "jsr:@std/async@0.224.0/delay"; +import { delay } from "jsr:@std/async@1.0.1/delay"; export const main: Entrypoint = async (_denops) => { await delay(1000); diff --git a/tests/denops/testdata/shared_server_test_no_verbose.ts b/tests/denops/testdata/shared_server_test_no_verbose.ts index 87347653..ef15f41c 100644 --- a/tests/denops/testdata/shared_server_test_no_verbose.ts +++ b/tests/denops/testdata/shared_server_test_no_verbose.ts @@ -1,4 +1,4 @@ -import { delay } from "jsr:@std/async@0.224.0/delay"; +import { delay } from "jsr:@std/async@1.0.1/delay"; import { useSharedServer } from "/denops-testutil/shared_server.ts"; { diff --git a/tests/denops/testdata/shared_server_test_verbose_true.ts b/tests/denops/testdata/shared_server_test_verbose_true.ts index d43f4759..d55c7b31 100644 --- a/tests/denops/testdata/shared_server_test_verbose_true.ts +++ b/tests/denops/testdata/shared_server_test_verbose_true.ts @@ -1,4 +1,4 @@ -import { delay } from "jsr:@std/async@0.224.0/delay"; +import { delay } from "jsr:@std/async@1.0.1/delay"; import { useSharedServer } from "/denops-testutil/shared_server.ts"; { diff --git a/tests/denops/testutil/shared_server.ts b/tests/denops/testutil/shared_server.ts index 4970a599..22ee6787 100644 --- a/tests/denops/testutil/shared_server.ts +++ b/tests/denops/testutil/shared_server.ts @@ -1,5 +1,5 @@ import { assert } from "jsr:@std/assert@1.0.1"; -import { deadline } from "jsr:@std/async@0.224.0/deadline"; +import { deadline } from "jsr:@std/async@1.0.1/deadline"; import { resolve } from "jsr:@std/path@0.224.0/resolve"; import { channel, pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import { tap } from "jsr:@milly/streams@^1.0.0/transform/tap"; diff --git a/tests/denops/testutil/shared_server_test.ts b/tests/denops/testutil/shared_server_test.ts index 833a0097..14b2ab78 100644 --- a/tests/denops/testutil/shared_server_test.ts +++ b/tests/denops/testutil/shared_server_test.ts @@ -5,7 +5,7 @@ import { assertNotMatch, assertRejects, } from "jsr:@std/assert@1.0.1"; -import { delay } from "jsr:@std/async@0.224.0/delay"; +import { delay } from "jsr:@std/async@1.0.1/delay"; import { join } from "jsr:@std/path@0.225.0/join"; import { useSharedServer } from "./shared_server.ts"; From c5b38647b6d49d8e057300c88946e5d7ef1daa1b Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:16 +0900 Subject: [PATCH 94/99] :package: bump @std/cli from 0.224.3 to 1.0.1 --- denops/@denops-private/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/denops/@denops-private/cli.ts b/denops/@denops-private/cli.ts index 0ada81d2..e0044e90 100644 --- a/denops/@denops-private/cli.ts +++ b/denops/@denops-private/cli.ts @@ -3,7 +3,7 @@ import { writableStreamFromWorker, } from "jsr:@lambdalisue/workerio@4.0.1"; import { deadline } from "jsr:@std/async@1.0.1/deadline"; -import { parseArgs } from "jsr:@std/cli@0.224.3/parse-args"; +import { parseArgs } from "jsr:@std/cli@1.0.1/parse-args"; import { asyncSignal } from "jsr:@milly/async-signal@^1.0.0"; const WORKER_SCRIPT = import.meta.resolve("./worker.ts"); From 21dcf242041896cfe21523b462d6f6c4707aa367 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:16 +0900 Subject: [PATCH 95/99] :package: bump @std/path from 0.225.0 to 1.0.2 --- denops/@denops-private/service.ts | 2 +- denops/@denops-private/service_test.ts | 2 +- denops/@denops-private/version.ts | 4 ++-- tests/denops/runtime/functions/plugin_test.ts | 2 +- tests/denops/testutil/conf.ts | 6 +++--- tests/denops/testutil/shared_server.ts | 2 +- tests/denops/testutil/shared_server_test.ts | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index 6e461842..74299f42 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -1,5 +1,5 @@ import type { Denops, Entrypoint, Meta } from "jsr:@denops/core@7.0.0"; -import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; +import { toFileUrl } from "jsr:@std/path@1.0.2/to-file-url"; import { toErrorObject } from "jsr:@lambdalisue/errorutil@1.1.0"; import { DenopsImpl, type Host } from "./denops.ts"; import type { CallbackId, Service as HostService } from "./host.ts"; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index f38eb95b..0c96d160 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -22,7 +22,7 @@ import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; import type { Host } from "./denops.ts"; import { Service } from "./service.ts"; -import { toFileUrl } from "jsr:@std/path@0.225.0/to-file-url"; +import { toFileUrl } from "jsr:@std/path@1.0.2/to-file-url"; const NOOP = () => {}; diff --git a/denops/@denops-private/version.ts b/denops/@denops-private/version.ts index 55224e93..a7e84cd3 100644 --- a/denops/@denops-private/version.ts +++ b/denops/@denops-private/version.ts @@ -1,5 +1,5 @@ -import { dirname } from "jsr:@std/path@0.225.0/dirname"; -import { fromFileUrl } from "jsr:@std/path@0.225.0/from-file-url"; +import { dirname } from "jsr:@std/path@1.0.2/dirname"; +import { fromFileUrl } from "jsr:@std/path@1.0.2/from-file-url"; import type { SemVer } from "jsr:@std/semver@0.224.0/types"; import { parse } from "jsr:@std/semver@0.224.0/parse"; diff --git a/tests/denops/runtime/functions/plugin_test.ts b/tests/denops/runtime/functions/plugin_test.ts index b91bfe89..9df841a8 100644 --- a/tests/denops/runtime/functions/plugin_test.ts +++ b/tests/denops/runtime/functions/plugin_test.ts @@ -8,7 +8,7 @@ import { assertStringIncludes, } from "jsr:@std/assert@1.0.1"; import { delay } from "jsr:@std/async@^0.224.0"; -import { join } from "jsr:@std/path@0.225.0/join"; +import { join } from "jsr:@std/path@1.0.2/join"; import { AsyncDisposableStack } from "jsr:@nick/dispose@1.1.0/async-disposable-stack"; import { testHost } from "/denops-testutil/host.ts"; import { wait } from "/denops-testutil/wait.ts"; diff --git a/tests/denops/testutil/conf.ts b/tests/denops/testutil/conf.ts index be817788..743f5581 100644 --- a/tests/denops/testutil/conf.ts +++ b/tests/denops/testutil/conf.ts @@ -1,6 +1,6 @@ -import { fromFileUrl } from "jsr:@std/path@0.225.0/from-file-url"; -import { resolve } from "jsr:@std/path@0.225.0/resolve"; -import { SEPARATOR as SEP } from "jsr:@std/path@0.225.0/constants"; +import { fromFileUrl } from "jsr:@std/path@1.0.2/from-file-url"; +import { resolve } from "jsr:@std/path@1.0.2/resolve"; +import { SEPARATOR as SEP } from "jsr:@std/path@1.0.2/constants"; let conf: Config | undefined; diff --git a/tests/denops/testutil/shared_server.ts b/tests/denops/testutil/shared_server.ts index 22ee6787..8756663e 100644 --- a/tests/denops/testutil/shared_server.ts +++ b/tests/denops/testutil/shared_server.ts @@ -1,6 +1,6 @@ import { assert } from "jsr:@std/assert@1.0.1"; import { deadline } from "jsr:@std/async@1.0.1/deadline"; -import { resolve } from "jsr:@std/path@0.224.0/resolve"; +import { resolve } from "jsr:@std/path@1.0.2/resolve"; import { channel, pop } from "jsr:@lambdalisue/streamtools@1.0.0"; import { tap } from "jsr:@milly/streams@^1.0.0/transform/tap"; import { getConfig } from "./conf.ts"; diff --git a/tests/denops/testutil/shared_server_test.ts b/tests/denops/testutil/shared_server_test.ts index 14b2ab78..cc61d708 100644 --- a/tests/denops/testutil/shared_server_test.ts +++ b/tests/denops/testutil/shared_server_test.ts @@ -6,7 +6,7 @@ import { assertRejects, } from "jsr:@std/assert@1.0.1"; import { delay } from "jsr:@std/async@1.0.1/delay"; -import { join } from "jsr:@std/path@0.225.0/join"; +import { join } from "jsr:@std/path@1.0.2/join"; import { useSharedServer } from "./shared_server.ts"; Deno.test("useSharedServer()", async (t) => { From 211ed2dac0563161f4587b3141935e17d094192f Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:16 +0900 Subject: [PATCH 96/99] :package: bump @std/semver from 0.224.0 to 0.224.3 --- denops/@denops-private/version.ts | 4 ++-- denops/@denops-private/version_test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/denops/@denops-private/version.ts b/denops/@denops-private/version.ts index a7e84cd3..046ff221 100644 --- a/denops/@denops-private/version.ts +++ b/denops/@denops-private/version.ts @@ -1,7 +1,7 @@ import { dirname } from "jsr:@std/path@1.0.2/dirname"; import { fromFileUrl } from "jsr:@std/path@1.0.2/from-file-url"; -import type { SemVer } from "jsr:@std/semver@0.224.0/types"; -import { parse } from "jsr:@std/semver@0.224.0/parse"; +import type { SemVer } from "jsr:@std/semver@0.224.3/types"; +import { parse } from "jsr:@std/semver@0.224.3/parse"; const decoder = new TextDecoder(); diff --git a/denops/@denops-private/version_test.ts b/denops/@denops-private/version_test.ts index b2be96d2..80f95481 100644 --- a/denops/@denops-private/version_test.ts +++ b/denops/@denops-private/version_test.ts @@ -1,6 +1,6 @@ import { assert, assertEquals } from "jsr:@std/assert@1.0.1"; import { resolvesNext, stub } from "jsr:@std/testing@0.224.0/mock"; -import type { SemVer } from "jsr:@std/semver@0.224.0/types"; +import type { SemVer } from "jsr:@std/semver@0.224.3/types"; import { is, type Predicate } from "jsr:@core/unknownutil@3.18.1"; import { getVersionOr } from "./version.ts"; From 41e3d1253235155e00b1756de53bfed921d0f020 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:16 +0900 Subject: [PATCH 97/99] :package: bump @std/testing from 0.224.0 to 1.0.0-rc.5 --- denops/@denops-private/cli_test.ts | 4 ++-- denops/@denops-private/denops_test.ts | 2 +- denops/@denops-private/host/nvim_test.ts | 2 +- denops/@denops-private/host/vim_test.ts | 2 +- denops/@denops-private/host_test.ts | 2 +- denops/@denops-private/service_test.ts | 2 +- denops/@denops-private/version_test.ts | 2 +- denops/@denops-private/worker_test.ts | 2 +- tests/denops/testutil/mock_test.ts | 2 +- tests/denops/testutil/wait_test.ts | 4 ++-- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/denops/@denops-private/cli_test.ts b/denops/@denops-private/cli_test.ts index 651aed91..65ae9c28 100644 --- a/denops/@denops-private/cli_test.ts +++ b/denops/@denops-private/cli_test.ts @@ -16,8 +16,8 @@ import { spy, type Stub, stub, -} from "jsr:@std/testing@0.224.0/mock"; -import { FakeTime } from "jsr:@std/testing@0.224.0/time"; +} from "jsr:@std/testing@1.0.0-rc.5/mock"; +import { FakeTime } from "jsr:@std/testing@1.0.0-rc.5/time"; import { delay } from "jsr:@std/async@1.0.1/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { diff --git a/denops/@denops-private/denops_test.ts b/denops/@denops-private/denops_test.ts index 629ec97e..4a25b701 100644 --- a/denops/@denops-private/denops_test.ts +++ b/denops/@denops-private/denops_test.ts @@ -11,7 +11,7 @@ import { assertSpyCalls, resolvesNext, stub, -} from "jsr:@std/testing@0.224.0/mock"; +} from "jsr:@std/testing@1.0.0-rc.5/mock"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { DenopsImpl, type Host, type Service } from "./denops.ts"; diff --git a/denops/@denops-private/host/nvim_test.ts b/denops/@denops-private/host/nvim_test.ts index 7339c342..c5c5291c 100644 --- a/denops/@denops-private/host/nvim_test.ts +++ b/denops/@denops-private/host/nvim_test.ts @@ -9,7 +9,7 @@ import { assertSpyCalls, resolvesNext, stub, -} from "jsr:@std/testing@0.224.0/mock"; +} from "jsr:@std/testing@1.0.0-rc.5/mock"; import { delay } from "jsr:@std/async@1.0.1/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; diff --git a/denops/@denops-private/host/vim_test.ts b/denops/@denops-private/host/vim_test.ts index 89700aa9..17fc35cd 100644 --- a/denops/@denops-private/host/vim_test.ts +++ b/denops/@denops-private/host/vim_test.ts @@ -8,7 +8,7 @@ import { assertSpyCalls, resolvesNext, stub, -} from "jsr:@std/testing@0.224.0/mock"; +} from "jsr:@std/testing@1.0.0-rc.5/mock"; import { delay } from "jsr:@std/async@1.0.1/delay"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; diff --git a/denops/@denops-private/host_test.ts b/denops/@denops-private/host_test.ts index 82f7efbe..2147e477 100644 --- a/denops/@denops-private/host_test.ts +++ b/denops/@denops-private/host_test.ts @@ -3,7 +3,7 @@ import { assertSpyCall, assertSpyCalls, stub, -} from "jsr:@std/testing@0.224.0/mock"; +} from "jsr:@std/testing@1.0.0-rc.5/mock"; import { AssertError } from "jsr:@core/unknownutil@3.18.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; import { invoke, type Service } from "./host.ts"; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 0c96d160..b6b2940f 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -16,7 +16,7 @@ import { resolvesNext, spy, stub, -} from "jsr:@std/testing@0.224.0/mock"; +} from "jsr:@std/testing@1.0.0-rc.5/mock"; import type { Meta } from "jsr:@denops/core@7.0.0"; import { promiseState } from "jsr:@lambdalisue/async@2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@1.1.0"; diff --git a/denops/@denops-private/version_test.ts b/denops/@denops-private/version_test.ts index 80f95481..3fef50bb 100644 --- a/denops/@denops-private/version_test.ts +++ b/denops/@denops-private/version_test.ts @@ -1,5 +1,5 @@ import { assert, assertEquals } from "jsr:@std/assert@1.0.1"; -import { resolvesNext, stub } from "jsr:@std/testing@0.224.0/mock"; +import { resolvesNext, stub } from "jsr:@std/testing@1.0.0-rc.5/mock"; import type { SemVer } from "jsr:@std/semver@0.224.3/types"; import { is, type Predicate } from "jsr:@core/unknownutil@3.18.1"; import { getVersionOr } from "./version.ts"; diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index c0b9a69d..787a8b28 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -11,7 +11,7 @@ import { resolvesNext, spy, stub, -} from "jsr:@std/testing@0.224.0/mock"; +} from "jsr:@std/testing@1.0.0-rc.5/mock"; import { delay } from "jsr:@std/async@1.0.1/delay"; import { DisposableStack } from "jsr:@nick/dispose@1.1.0/disposable-stack"; import * as nvimCodec from "jsr:@lambdalisue/messagepack@^1.0.1"; diff --git a/tests/denops/testutil/mock_test.ts b/tests/denops/testutil/mock_test.ts index 01d92bf1..58ed763c 100644 --- a/tests/denops/testutil/mock_test.ts +++ b/tests/denops/testutil/mock_test.ts @@ -17,7 +17,7 @@ import { resolvesNext, spy, stub, -} from "jsr:@std/testing@0.224.0/mock"; +} from "jsr:@std/testing@1.0.0-rc.5/mock"; // deno-lint-ignore no-explicit-any type AnyFn = (...args: any[]) => unknown; diff --git a/tests/denops/testutil/wait_test.ts b/tests/denops/testutil/wait_test.ts index e79589cd..d9f2f1ed 100644 --- a/tests/denops/testutil/wait_test.ts +++ b/tests/denops/testutil/wait_test.ts @@ -4,8 +4,8 @@ import { resolvesNext, returnsNext, spy, -} from "jsr:@std/testing@0.224.0/mock"; -import { FakeTime } from "jsr:@std/testing@0.224.0/time"; +} from "jsr:@std/testing@1.0.0-rc.5/mock"; +import { FakeTime } from "jsr:@std/testing@1.0.0-rc.5/time"; import { wait } from "./wait.ts"; Deno.test("wait()", async (t) => { From 0a78e35d62c7aebc0b86c4ecebc1d4b5d231927e Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:45:17 +0900 Subject: [PATCH 98/99] :package: bump sinon from 17.0.1 to 18.0.0 --- denops/@denops-private/cli_test.ts | 2 +- denops/@denops-private/worker_test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/denops/@denops-private/cli_test.ts b/denops/@denops-private/cli_test.ts index 65ae9c28..80840eda 100644 --- a/denops/@denops-private/cli_test.ts +++ b/denops/@denops-private/cli_test.ts @@ -1,6 +1,6 @@ // NOTE: Use sinon to stub the getter property. // @deno-types="npm:@types/sinon@17.0.3" -import sinon from "npm:sinon@17.0.1"; +import sinon from "npm:sinon@18.0.0"; import { assertEquals, diff --git a/denops/@denops-private/worker_test.ts b/denops/@denops-private/worker_test.ts index 787a8b28..c0537f68 100644 --- a/denops/@denops-private/worker_test.ts +++ b/denops/@denops-private/worker_test.ts @@ -1,5 +1,5 @@ // @deno-types="npm:@types/sinon@17.0.3" -import sinon from "npm:sinon@17.0.1"; +import sinon from "npm:sinon@18.0.0"; import { assertEquals, assertInstanceOf, From 6eb990e76cb09293bf97c2ccfaa69f07dc14e61a Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 20 Jul 2024 14:37:30 +0900 Subject: [PATCH 99/99] :herb: Fix error message in test @std/async/deadline changes it's internal implementation and the error has changed from `DeadlineError` to `DOMException` with `Signal timed out` error originated from `AbortSignal.timeout()`. --- tests/denops/testutil/shared_server_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/denops/testutil/shared_server_test.ts b/tests/denops/testutil/shared_server_test.ts index cc61d708..8370de3e 100644 --- a/tests/denops/testutil/shared_server_test.ts +++ b/tests/denops/testutil/shared_server_test.ts @@ -110,7 +110,7 @@ Deno.test("useSharedServer()", async (t) => { await useSharedServer({ timeout: 0 }); }, Error, - "Deadline", + "Signal timed out", ); }); });