Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ cognitive-complexity-threshold = 20

[[disallowed-methods]]
path = "tree_sitter::Node::named_child"
reason = "tree-sitter does not add automatically filter extras, replace with"
replacement = "python_nth_named_child::<NTH>"

[[disallowed-methods]]
path = "tree_sitter::Node::next_named_sibling"
replacement = "odoo_lsp::utils::python_next_named_sibling"
reason = "tree-sitter does not automatically filter extras, replace with"
replacement = "python_next_named_sibling"

[[await-holding-invalid-types]]
path = "dashmap::mapref::one::RefMut"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
id: cargo-test
if: matrix.coverage == 'no'
continue-on-error: true
run: cargo nextest run -p odoo-lsp -p odoo-lsp-tests --all-features --profile ci
run: cargo nextest run --workspace --no-fail-fast --all-features --profile ci

- name: Run unit tests (with coverage)
id: cargo-testcov
Expand All @@ -70,7 +70,7 @@ jobs:
run: |
set -euxo pipefail
cargo llvm-cov clean --workspace -v
cargo llvm-cov --codecov --output-path lcov.info nextest -p odoo-lsp -p odoo-lsp-tests --all-features --profile ci
cargo llvm-cov --codecov --output-path lcov.info nextest --workspace --no-fail-fast --all-features --profile ci

- name: Upload test results
if: always()
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ ts-macros.workspace = true
tracing-subscriber.workspace = true
mini-moka = "0.10.3"
boxcar = "0.2.14"
scopeguard = "1.2.0"

[dev-dependencies]
pretty_assertions.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion crates/ts-indent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Basic formatter for tree-sitter queries written in Scheme.

## Usage

Install using `cargo install -p ts-indent`. The program operates on standard input and output.
Install using `cargo install --path .`. The program operates on standard input and output.

Example usage in Helix:

Expand Down
28 changes: 20 additions & 8 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
set quiet

test *args="--no-fail-fast": (ensure_cargo "cargo-nextest")
cargo nextest run -p odoo-lsp -p odoo-lsp-tests {{args}}
test *args="--workspace --no-fail-fast": (_test args)

bench: (ensure_cargo "gungraun-runner")
gungraun-runner -V
cargo bench -p odoo-lsp-tests
gungraun-runner -V
cargo bench -p odoo-lsp-tests

install profile="dev":
cargo install --path . --profile={{profile}}

coverage clean="0": (ensure_cargo "cargo-llvm-cov")
test {{clean}} = "0" || cargo llvm-cov clean --workspace -v
cargo llvm-cov nextest -p odoo-lsp -p odoo-lsp-tests --html --open

[private]
ensure_cargo command:
command -v {{command}} || \
(command -v cargo-binstall && cargo binstall {{command}} --force -y) || \
(command -v cargo && cargo install cargo-binstall && cargo binstall {{command}} --force -y) || \
(echo 'cargo is not installed, exiting' && exit 1)
command -v {{command}} >/dev/null || \
(command -v cargo-binstall >/dev/null && cargo binstall {{command}} --force -y) || \
(command -v cargo >/dev/null && cargo install cargo-binstall && cargo binstall {{command}} --force -y) || \
(echo 'cargo is not installed, exiting' && exit 1)

[private]
report-md:
glow -p -w 0 || cat

[private]
_test *args: (ensure_cargo "cargo-nextest")
NEXTEST_EXPERIMENTAL_LIBTEST_JSON=1 cargo nextest run --message-format=libtest-json-plus {{args}} \
| ./scripts/libtest_to_md.mjs | just report-md
182 changes: 182 additions & 0 deletions scripts/libtest_to_md.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/usr/bin/env node
import { createReadStream } from "fs";
import { createInterface } from "readline";

const inputFile = process.argv[2];
const rl = createInterface({ input: inputFile ? createReadStream(inputFile) : process.stdin, crlfDelay: Infinity });

function suiteKeyFromNextest(nextest) {
const crate = nextest?.crate ?? "unknown";
const binary = nextest?.test_binary ?? "unknown";
return `${crate}::${binary}`;
}

function suiteKeyFromTestName(name) {
const separator = name.indexOf("$");
return separator === -1 ? null : name.slice(0, separator);
}

function getOrCreateSuite(suites, suiteMap, key, nextest = {}) {
let suite = suiteMap.get(key);
if (suite) {
if (suite.crate === "unknown" && nextest.crate) suite.crate = nextest.crate;
if (suite.binary === "unknown" && nextest.test_binary) suite.binary = nextest.test_binary;
if (suite.kind === "unknown" && nextest.kind) suite.kind = nextest.kind;
return suite;
}

suite = {
key,
crate: nextest.crate ?? "unknown",
binary: nextest.test_binary ?? "unknown",
kind: nextest.kind ?? "unknown",
testCount: 0,
tests: [],
summary: null,
};
suites.push(suite);
suiteMap.set(key, suite);
return suite;
}

function stripAnsi(text) {
return text.replaceAll(/\u001b\[[0-9;]*m/g, "");
}

function pushCodeBlock(lines, language, content) {
lines.push(`\`\`\`${language}`);
lines.push(content);
lines.push("```");
}

function pushFormattedStdout(lines, stdout) {
const cleaned = stripAnsi(stdout).trimEnd();
if (!cleaned) return;

const stdoutLines = cleaned.split("\n");
const diffHeader = "Diff < left / right > :";
const diffIndex = stdoutLines.findIndex((line) => line.trim() === diffHeader);

if (diffIndex === -1) {
pushCodeBlock(lines, "text", cleaned);
return;
}

const prelude = stdoutLines.slice(0, diffIndex).join("\n").trimEnd();
const diffBody = stdoutLines.slice(diffIndex + 1).join("\n").trimEnd();

if (prelude) {
pushCodeBlock(lines, "text", prelude);
}
if (diffBody) {
pushCodeBlock(lines, "diff", `--- left\n+++ right\n${diffBody}`);
}
}

const suites = [];
const suiteMap = new Map();
const testResults = new Map();

for await (const line of rl) {
if (!line.trim()) continue;
let obj;
try {
obj = JSON.parse(line);
} catch {
continue;
}

if (obj.type === "suite" && obj.event === "started") {
const key = suiteKeyFromNextest(obj.nextest);
const suite = getOrCreateSuite(suites, suiteMap, key, obj.nextest);
suite.testCount = obj.test_count ?? suite.testCount;
} else if (obj.type === "suite" && (obj.event === "ok" || obj.event === "failed")) {
const key = suiteKeyFromNextest(obj.nextest);
const suite = getOrCreateSuite(suites, suiteMap, key, obj.nextest);
suite.summary = {
result: obj.event,
passed: obj.passed ?? 0,
failed: obj.failed ?? 0,
ignored: obj.ignored ?? 0,
execTime: obj.exec_time ?? 0,
};
} else if (obj.type === "test" && obj.event === "started") {
const suiteKey = suiteKeyFromTestName(obj.name);
if (suiteKey) getOrCreateSuite(suites, suiteMap, suiteKey);
testResults.set(obj.name, {
name: obj.name,
status: "running",
suiteKey,
execTime: 0,
stdout: null,
});
} else if (obj.type === "test" && (obj.event === "ok" || obj.event === "failed")) {
const suiteKey = suiteKeyFromTestName(obj.name);
if (suiteKey) getOrCreateSuite(suites, suiteMap, suiteKey);
const entry = {
name: obj.name,
status: obj.event,
suiteKey,
execTime: obj.exec_time ?? 0,
stdout: obj.stdout ?? null,
};
testResults.set(obj.name, entry);
}
}

for (const suite of suites) {
suite.tests = [];
}
for (const [, entry] of testResults) {
if (entry.status === "running") continue;
const suite = entry.suiteKey ? suiteMap.get(entry.suiteKey) : null;
if (suite) suite.tests.push(entry);
}

// Build markdown
const lines = [];

const totalPassed = suites.reduce((s, x) => s + (x.summary?.passed ?? 0), 0);
const totalFailed = suites.reduce((s, x) => s + (x.summary?.failed ?? 0), 0);
const totalTests = totalPassed + totalFailed;
const overallResult = totalFailed === 0 ? "✅ PASSED" : "❌ FAILED";

lines.push(`# Test Report`);
lines.push(``);
lines.push(`**Result:** ${overallResult} `);
lines.push(`**Total:** ${totalTests} tests — ${totalPassed} passed, ${totalFailed} failed`);
lines.push(``);

for (const suite of suites) {
const s = suite.summary;
if (!s) continue;
const icon = s.result === "ok" ? "✅" : "❌";
lines.push(`## ${icon} ${suite.crate} (${suite.kind})`);
lines.push(``);
lines.push(`| | |`);
lines.push(`|---|---|`);
lines.push(`| Passed | ${s.passed} |`);
lines.push(`| Failed | ${s.failed} |`);
if (s.ignored) lines.push(`| Ignored | ${s.ignored} |`);
lines.push(`| Duration | ${s.execTime.toFixed(3)}s |`);
lines.push(``);

const failed = suite.tests.filter((t) => t.status === "failed");
if (failed.length > 0) {
lines.push(`### Failed Tests`);
lines.push(``);
for (const t of failed) {
// strip suite prefix for display
const prefix = `${suite.crate}::${suite.binary}$`;
const shortName = t.name.startsWith(prefix) ? t.name.slice(prefix.length) : t.name;
lines.push(`#### \`${shortName}\``);
lines.push(``);
if (t.stdout) {
pushFormattedStdout(lines, t.stdout);
}
lines.push(``);
}
}
}

process.stdout.write(lines.join("\n") + "\n");
Loading
Loading