дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(run): run Ruby tests from the editor and Project view (-I roots, test-root folders) (#1545)
A Ruby file run from the context menu had no project load path, so require "foo" for lib/foo.rb failed: Sources roots are now passed as -I<root>. Folders under a test root get Run Tests in '<dir>': RSpec on the folder, rake test with TEST=<globs> (two comma-separated globs, since Rake splits TEST at commas), or a detected minitest configuration that loads every *_test.rb / test_*.rb under the folder when the project has neither. Co-authored-by: kjeldahl <[email protected]>
Цей коміт міститься в:
1 джерело
4959b430b1
коміт
f6a1823121
4 files changed
+144
-1
No files matched your search
@@ -1059,6 +1059,9 @@ fn ruby_config(
|
||||
c
|
||||
}
|
||||
|
||||
/// Loads the test files of the directories (or files) in `ARGV`, default `test`: the way `rake test` does.
|
||||
const MINITEST_RUNNER: &str = r#"dirs = ARGV.empty? ? ["test"] : ARGV.dup; ARGV.clear; dirs.flat_map { |d| File.file?(d) ? [d] : Dir[File.join(d, "**", "{*_test,test_*}.rb")] }.sort.each { |f| require File.expand_path(f) }"#;
|
||||
|
||||
fn uses_rspec(root: &Path) -> bool {
|
||||
root.join("spec").is_dir()
|
||||
|| root.join(".rspec").is_file()
|
||||
@@ -1101,6 +1104,17 @@ fn detect_ruby(root: &Path, out: &mut Vec<RunConfig>) {
|
||||
}
|
||||
if uses_rspec(root) {
|
||||
out.push(ruby_config("rspec".into(), "rspec".into(), "rspec", ruby_tool(root, "rspec"), &[]).test());
|
||||
} else if root.join("test").is_dir() && !out.iter().any(|c| c.id == "rake:test") {
|
||||
// Minitest / Test::Unit without a `test` Rake task: load every test file under `test/`.
|
||||
let mut args = Vec::new();
|
||||
for dir in ["lib", "test"] {
|
||||
if root.join(dir).is_dir() {
|
||||
args.push(format!("-I{dir}"));
|
||||
}
|
||||
}
|
||||
args.extend(["-e".to_owned(), MINITEST_RUNNER.to_owned(), "test".to_owned()]);
|
||||
let extra: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||
out.push(ruby_config("minitest".into(), "minitest".into(), "ruby", ruby_tool(root, "ruby"), &extra).test());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -820,6 +820,25 @@ task default: :build
|
||||
assert_eq!(find(&c, "rake:build").extras.folder.as_deref(), Some("Rake"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_minitest_projects_get_a_test_runner() {
|
||||
let dir = project(&[("Gemfile", "source 'https://rubygems.org'\n"), ("lib/a.rb", ""), ("test/a_test.rb", "")]);
|
||||
let c = detect(dir.path());
|
||||
let m = find(&c, "minitest");
|
||||
assert!(m.is_test);
|
||||
assert_eq!(m.kind, "ruby");
|
||||
assert_eq!(
|
||||
(m.command.as_str(), &m.args[..4]),
|
||||
("bundle", &["exec", "ruby", "-Ilib", "-Itest"].map(String::from)[..])
|
||||
);
|
||||
assert_eq!(m.args.last().map(String::as_str), Some("test"));
|
||||
// A `test` Rake task or RSpec already runs the tests.
|
||||
let dir = project(&[("Rakefile", "task :test\n"), ("test/a_test.rb", "")]);
|
||||
assert!(!ids(&detect(dir.path())).contains(&"minitest"));
|
||||
let dir = project(&[("Gemfile", "gem 'rspec'\n"), ("test/a_test.rb", "")]);
|
||||
assert!(!ids(&detect(dir.path())).contains(&"minitest"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_without_bundler_runs_bare_tools() {
|
||||
let dir = project(&[("Rakefile", "task :test\n")]);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/platform/projectRoots", () => ({
|
||||
currentRoots: () => [
|
||||
{ path: "lib", kind: "source", detectedFrom: null },
|
||||
{ path: "test", kind: "test", detectedFrom: null },
|
||||
{ path: "spec", kind: "test", detectedFrom: null },
|
||||
{ path: "", kind: "source", detectedFrom: null },
|
||||
],
|
||||
}));
|
||||
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { produceContext, produceDirectory, type ToolEnv } from "./producers";
|
||||
|
||||
const env: ToolEnv = {
|
||||
tool: (n) => (n === "ruby" ? { path: "/bin/ruby", version: null } : null),
|
||||
compileDir: "/tmp/openidea-run",
|
||||
};
|
||||
const input = (path: string) => ({ path, root: "/work/app", configs: [], content: undefined, line: undefined });
|
||||
|
||||
describe("Ruby file run", () => {
|
||||
it("puts Sources roots on the load path", () => {
|
||||
const p = produceContext(input("/work/app/test/integration/a_test.rb"), env);
|
||||
expect(p?.config?.args).toEqual(["-Ilib", "test/integration/a_test.rb"]);
|
||||
});
|
||||
|
||||
it("adds none for files outside the project", () => {
|
||||
const p = produceContext(input("/elsewhere/a.rb"), env);
|
||||
expect(p?.config?.args).toEqual(["a.rb"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ruby directory run", () => {
|
||||
const cfg = (over: Partial<RunConfig> & { id: string; kind: string }): RunConfig => ({
|
||||
name: over.id,
|
||||
command: "bundle",
|
||||
args: [],
|
||||
cwd: null,
|
||||
env: {},
|
||||
detected: true,
|
||||
isTest: true,
|
||||
debugAdapter: null,
|
||||
...over,
|
||||
});
|
||||
const dir = (path: string, configs: RunConfig[]) =>
|
||||
produceDirectory({ path: `/work/app/${path}`, root: "/work/app", configs });
|
||||
|
||||
it("RSpec on a nested folder", () => {
|
||||
const p = dir("spec/models/user", [cfg({ id: "rspec", kind: "rspec", args: ["exec", "rspec"] })]);
|
||||
expect(p?.config?.args).toEqual(["exec", "rspec", "spec/models/user"]);
|
||||
expect(p?.name).toBe("Tests in 'user'");
|
||||
});
|
||||
|
||||
it("rake test with a TEST glob", () => {
|
||||
const p = dir("test/unit", [cfg({ id: "rake:test", kind: "rake", args: ["exec", "rake", "test"] })]);
|
||||
expect(p?.config?.args).toEqual(["exec", "rake", "test", "TEST=test/unit/**/*_test.rb,test/unit/**/test_*.rb"]);
|
||||
});
|
||||
|
||||
it("the Minitest runner with Sources roots and the folder", () => {
|
||||
const base = cfg({ id: "minitest", kind: "ruby", args: ["exec", "ruby", "-Itest", "-e", "SCRIPT", "test"] });
|
||||
const p = dir("test/unit", [base]);
|
||||
expect(p?.config?.args).toEqual(["exec", "ruby", "-Itest", "-Ilib", "-e", "SCRIPT", "test/unit"]);
|
||||
});
|
||||
|
||||
it("the test root itself", () => {
|
||||
const p = dir("test", [cfg({ id: "rspec", kind: "rspec", args: ["exec", "rspec"] })]);
|
||||
expect(p?.config?.args).toEqual(["exec", "rspec", "test"]);
|
||||
});
|
||||
|
||||
it("nothing for non-test folders or projects without a Ruby runner", () => {
|
||||
expect(dir("lib/unit", [cfg({ id: "rspec", kind: "rspec" })])).toBeNull();
|
||||
expect(dir("other/unit", [cfg({ id: "rspec", kind: "rspec" })])).toBeNull();
|
||||
expect(dir("test/unit", [])).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -596,6 +596,10 @@ function simple(
|
||||
return { name: s.file, scope: "file", config: fileConfig(s, kind, t.command, args(t.command, t.name), debugAdapter) };
|
||||
}
|
||||
|
||||
/** `-I<dir>` for each Sources root of the project (the project root itself is already the cwd). */
|
||||
const rubyLoadPath = (roots: readonly { path: string; kind: string }[]): string[] =>
|
||||
roots.filter((r) => r.kind === "source" && r.path !== "").map((r) => `-I${r.path}`);
|
||||
|
||||
const hasGoMain = (content?: string) => content === undefined || /^\s*package\s+main\b/m.test(content);
|
||||
const hasRustMain = (content?: string) =>
|
||||
content === undefined || /^(?:pub\s+)?(?:async\s+)?fn\s+main\s*\(/m.test(content);
|
||||
@@ -798,6 +802,8 @@ export function produceFile(input: ContextInput, env: ToolEnv): Produced | null
|
||||
};
|
||||
}
|
||||
case "ruby":
|
||||
// Sources roots go on the load path (`-Ilib`), as `rake test` does; paths are project-relative.
|
||||
return simple(lang, s, env, lang, () => [...(s.inProject ? rubyLoadPath(currentRoots()) : []), s.arg]);
|
||||
case "php":
|
||||
case "perl":
|
||||
case "lua":
|
||||
@@ -1021,6 +1027,33 @@ function cmakeTargets(input: ContextInput): RunConfig[] {
|
||||
|
||||
const TEST_DIR = /(^|\/)(tests?|__tests__|specs?|test_?\w*)$/;
|
||||
|
||||
/**
|
||||
* The tests of a Ruby folder: RSpec on the folder, `rake test` with `TEST=<glob>`, or the
|
||||
* detected Minitest runner given the folder (Sources roots on its load path).
|
||||
*/
|
||||
function rubyDirectory(
|
||||
closest: (kinds: string[]) => RunConfig | undefined,
|
||||
relToCwd: (c: RunConfig) => string,
|
||||
make: (base: RunConfig, label: string, args: string[]) => Produced,
|
||||
label: string,
|
||||
): Produced | null {
|
||||
const rspec = closest(["rspec"]);
|
||||
if (rspec) return make(rspec, label, [...rspec.args, relToCwd(rspec)]);
|
||||
const rake = closest(["rake"]);
|
||||
if (rake && rake.args.at(-1) === "test") {
|
||||
// Rake splits `TEST` at commas into patterns, so no braces: two globs.
|
||||
const dir = relToCwd(rake);
|
||||
return make(rake, label, [...rake.args, `TEST=${dir}/**/*_test.rb,${dir}/**/test_*.rb`]);
|
||||
}
|
||||
const minitest = closest(["ruby"]);
|
||||
if (!minitest) return null;
|
||||
const at = minitest.args.indexOf("-e");
|
||||
const flags = rubyLoadPath(currentRoots()).filter((f) => !minitest.args.includes(f));
|
||||
const args = [...minitest.args.slice(0, -1)];
|
||||
args.splice(at < 0 ? args.length : at, 0, ...flags);
|
||||
return make(minitest, label, [...args, relToCwd(minitest)]);
|
||||
}
|
||||
|
||||
export function produceDirectory(input: ContextInput): Produced | null {
|
||||
const { root } = input;
|
||||
if (!root || !isProjectPath(input) || input.path === root) return null;
|
||||
@@ -1029,8 +1062,8 @@ export function produceDirectory(input: ContextInput): Produced | null {
|
||||
const jvmRoot = /^(?:(.*)\/)?src\/test(?:\/(java|kotlin|scala|groovy))?$/.exec(rel);
|
||||
const jvmPackage = /^(?:(.*)\/)?src\/test\/(?:java|kotlin|scala|groovy)\/(.+)$/.exec(rel);
|
||||
const marked = isTestPath(currentRoots(), rel);
|
||||
if (!TEST_DIR.test(rel) && !jvmRoot && !jvmPackage && !marked && !/(^|\/)src\/test(\/|$)/.test(rel)) return null;
|
||||
const tests = input.configs.filter((c) => c.isTest);
|
||||
if (!TEST_DIR.test(rel) && !jvmRoot && !jvmPackage && !marked && !/(^|\/)src\/test(\/|$)/.test(rel)) return null;
|
||||
const closest = (kinds: string[]) => {
|
||||
const cands = tests.filter((c) => kinds.includes(c.kind));
|
||||
const score = (c: RunConfig) => {
|
||||
@@ -1071,6 +1104,8 @@ export function produceDirectory(input: ContextInput): Produced | null {
|
||||
}
|
||||
}
|
||||
const js = closest(["npm", "bun", "deno"]);
|
||||
const ruby = marked && !js && rubyDirectory(closest, relToCwd, make, testsIn);
|
||||
if (ruby) return ruby;
|
||||
const py = closest(["pytest"]);
|
||||
const django = closest(["django-test"]);
|
||||
const go = closest(["go"]);
|
||||
|
||||
Reference in new issue
Заблокувати користувача