Skip to content

Commit

Permalink
Merge pull request #6 from vim-denops/stub
Browse files Browse the repository at this point in the history
👍 Add `DenopsStub` class to stub denops for tests.
  • Loading branch information
lambdalisue committed Jun 7, 2023
2 parents 98d33ef + d25ae6a commit e35dd1b
Show file tree
Hide file tree
Showing 9 changed files with 564 additions and 29 deletions.
40 changes: 24 additions & 16 deletions denops_test/conf.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import * as path from "https://deno.land/std@0.187.0/path/mod.ts";
import * as path from "https://deno.land/std@0.190.0/path/mod.ts";
import { orElse } from "./or_else.ts";

function get(name: string): string | undefined {
return Deno.env.get(`DENOPS_TEST_${name}`);
}
let conf: Config | undefined;

/** Absolute local path of denops added to Vim runtimepath */
export const DENOPS_PATH = path.resolve(
orElse(get("DENOPS_PATH"), () => {
throw new Error(
"Environment variable 'DENOPS_TEST_DENOPS_PATH' is required",
);
}),
);
export type Config = {
denopsPath: string;
vimExecutable: string;
nvimExecutable: string;
};

/** Executable name of Vim */
export const VIM_EXECUTABLE = get("VIM_EXECUTABLE") ?? "vim";
export function getConfig(): Config {
if (!conf) {
conf = {
denopsPath: path.resolve(orElse(get("DENOPS_PATH"), () => {
throw new Error(
"Environment variable 'DENOPS_TEST_DENOPS_PATH' is required",
);
})),
vimExecutable: get("VIM_EXECUTABLE") ?? "vim",
nvimExecutable: get("NVIM_EXECUTABLE") ?? "nvim",
};
}
return conf;
}

/** Executable name of Neovim */
export const NVIM_EXECUTABLE = get("NVIM_EXECUTABLE") ?? "nvim";
function get(name: string): string | undefined {
return Deno.env.get(`DENOPS_TEST_${name}`);
}
20 changes: 20 additions & 0 deletions denops_test/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,29 @@
* },
* });
* ```
*
* Use `DenopsStub` class to create a stub instance of `Denops` interface
* if no real Vim/Neovim behavior is required for the tests.
*
* ```typescript
* import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
* import { DenopsStub } from "./mod.ts";
*
* Deno.test("denops.call", async () => {
* const denops = new DenopsStub({
* call: (fn, ...args) => {
* return Promise.resolve([fn, ...args]);
* },
* });
* assertEquals(await denops.call("foo", "bar"), ["foo", "bar"]);
* });
* ```
*
* @module
*/
export type { DenopsStubber } from "./stub.ts";
export type { TestDefinition } from "./tester.ts";
export type { WithDenopsOptions } from "./with.ts";
export { DenopsStub } from "./stub.ts";
export { test } from "./tester.ts";
export { withDenops } from "./with.ts";
7 changes: 4 additions & 3 deletions denops_test/runner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NVIM_EXECUTABLE, VIM_EXECUTABLE } from "./conf.ts";
import { getConfig } from "./conf.ts";

/** Runner mode */
export type RunMode = "vim" | "nvim";
Expand Down Expand Up @@ -46,15 +46,16 @@ export function isRunMode(mode: string): mode is RunMode {
}

function buildArgs(mode: RunMode): [string, string[]] {
const conf = getConfig();
switch (mode) {
case "vim":
return [
VIM_EXECUTABLE,
conf.vimExecutable,
["-u", "NONE", "-i", "NONE", "-n", "-N", "-X", "-e", "-s"],
];
case "nvim":
return [
NVIM_EXECUTABLE,
conf.nvimExecutable,
["--clean", "--embed", "--headless", "-n"],
];
}
Expand Down
181 changes: 181 additions & 0 deletions denops_test/stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
Context,
Denops,
Dispatcher,
Meta,
} from "https://deno.land/x/[email protected]/mod.ts";

/**
* Represents a `Denops` stubber object.
*/
export type DenopsStubber = {
/**
* Denops instance name whihc uses to communicate with vim.
*
* If not specified, the default value is `denops-test-stub`.
*/
name?: string;
/**
* Environment meta information.
*
* If not specified, the default value is:
* ```json
* {
* "mode": "release",
* "host": "vim",
* "version": "0.0.0",
* "platform": "linux"
* }
* ```
*/
meta?: Meta;
/**
* A stub function of `redraw` method of the `Denops`.
*
* If not specified, it returns a promise resolves to undefined.
*/
redraw?: (force?: boolean) => Promise<void>;
/**
* A stub function of `call` method of the `Denops`.
*
* If not specified, it returns a promise resolves to undefined.
*/
call?(fn: string, ...args: unknown[]): Promise<unknown>;
/**
* A stub function of `batch` method of the `Denops`.
*
* If not specified, it returns a promise resolves to an empty list.
*/
batch?(...calls: [string, ...unknown[]][]): Promise<unknown[]>;
/**
* A stub function of `cmd` method of the `Denops`.
*
* If not specified, it returns a promise resolves to undefined.
*/
cmd?(cmd: string, ctx: Context): Promise<void>;
/**
* A stub function of `eval` method of the `Denops`.
*
* If not specified, it returns a promise resolves to undefined.
*/
eval?(expr: string, ctx: Context): Promise<unknown>;
/**
* A stub function of `dispatch` method of the `Denops`.
*
* If not specified, it returns a promise resolves to undefined.
*/
dispatch?(name: string, fn: string, ...args: unknown[]): Promise<unknown>;
/**
* Indicates whether to use `call` function in `batch` method.
*/
useCallInBatch?: boolean;
/**
* Indicates whether to use `call` function in `cmd` method.
*/
useCallInCmd?: boolean;
/**
* Indicates whether to use `call` function in `eval` method.
*/
useCallInEval?: boolean;
};

/**
* Represents a `Denops` stub object.
*/
export class DenopsStub implements Denops {
readonly context: Record<string | number | symbol, unknown> = {};
dispatcher: Dispatcher = {};

#stubber: DenopsStubber;

/**
* Creates a new instance of DenopsStub.
* @param stubber - The Denops stubber object.
*/
constructor(stubber: DenopsStubber = {}) {
this.#stubber = stubber;
}

get name(): string {
return this.#stubber.name ?? "denops-test-stub";
}

get meta(): Meta {
return this.#stubber.meta ?? {
mode: "release",
host: "vim",
version: "0.0.0",
platform: "linux",
};
}

redraw(force?: boolean): Promise<void> {
if (this.#stubber.redraw) {
return this.#stubber.redraw(force);
}
return Promise.resolve();
}

call(fn: string, ...args: unknown[]): Promise<unknown> {
if (this.#stubber.call) {
args = normArgs(args);
return this.#stubber.call(fn, ...args);
}
return Promise.resolve();
}

batch(...calls: [string, ...unknown[]][]): Promise<unknown[]> {
if (this.#stubber.batch) {
calls = calls.map(([fn, ...args]) => [fn, ...normArgs(args)]);
return this.#stubber.batch(...calls);
}
if (this.#stubber.call && this.#stubber.useCallInBatch) {
return Promise.all(
calls.map(([fn, ...args]) => this.call(fn, ...args)),
);
}
return Promise.resolve(calls.map(() => undefined));
}

cmd(cmd: string, ctx: Context = {}): Promise<void> {
if (this.#stubber.cmd) {
return this.#stubber.cmd(cmd, ctx);
}
if (this.#stubber.call && this.#stubber.useCallInCmd) {
return this.call("denops#api#cmd", cmd, ctx).then(() => {});
}
return Promise.resolve();
}

eval(expr: string, ctx: Context = {}): Promise<unknown> {
if (this.#stubber.eval) {
return this.#stubber.eval(expr, ctx);
}
if (this.#stubber.call && this.#stubber.useCallInEval) {
return this.call("denops#api#eval", expr, ctx);
}
return Promise.resolve();
}

dispatch(
name: string,
fn: string,
...args: unknown[]
): Promise<unknown> {
if (this.#stubber.dispatch) {
return this.#stubber.dispatch(name, fn, ...args);
}
return Promise.resolve();
}
}

function normArgs(args: unknown[]): unknown[] {
const normArgs = [];
for (const arg of args) {
if (arg === undefined) {
break;
}
normArgs.push(arg);
}
return normArgs;
}
Loading

0 comments on commit e35dd1b

Please sign in to comment.