Files
clide/test/kernel/src/syntax/tree_sitter_smoke_test.dart
T
jpmschweitzerandClaude Opus 4.7 ab2e5e618b tree_sitter test coverage + DI seam, ratchet floor to 95
Add `colorForRole` switch-arm tests (every role → token mapping plus
the unknown-role fallback). Introduce a DI seam in `TreeSitterService`
and `TreeSitterLib` so tests can substitute the FFI surface and asset
loaders without dlopen'ing `libtree-sitter.so` —
`TreeSitterLib.testing(...)` takes named per-function overrides with
safe no-op defaults, and `TreeSitterLib.fromDynamicLibrary(...)` lets
the smoke test load the vendored library explicitly. Production
paths (`TreeSitterService.shared`, `TreeSitterLib.instance`) are
unchanged.

Fake-FFI tests walk every branch of `_init`, `_loadGrammar`,
`highlight`, and `dispose`. The smoke test catches FFI-signature
regressions the fakes can't, by exercising the real native library
end-to-end on Linux. Together this takes `tree_sitter_service.dart`
from 17% to 96% and crosses the global 95% target — closing out the
D-66 line-coverage epic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:21:05 +02:00

81 lines
3.2 KiB
Dart

/// Real-library smoke test for `TreeSitterService`. Dlopen's the vendored
/// `native/linux-x64/libtree-sitter.so`, loads the bundled `dart` grammar
/// from `assets/grammars/dart.wasm` + `assets/queries/dart.scm` straight off
/// the filesystem (no `rootBundle`), and verifies highlight produces sane
/// spans over a tiny Dart program. Catches FFI-signature regressions that
/// the fake-driven branch tests cannot.
///
/// Skips cleanly on non-Linux hosts and on Linux hosts where the vendored
/// library hasn't been built yet.
library;
import 'dart:ffi';
import 'dart:io';
import 'package:clide/kernel/src/syntax/tree_sitter_ffi.dart';
import 'package:clide/kernel/src/syntax/tree_sitter_service.dart';
import 'package:flutter_test/flutter_test.dart';
const _libPath = 'native/linux-x64/libtree-sitter.so';
void main() {
if (!Platform.isLinux || !File(_libPath).existsSync()) return;
group('TreeSitterService — native smoke (real libtree-sitter.so + dart grammar)', () {
late TreeSitterService svc;
setUpAll(() {
final dylib = DynamicLibrary.open(_libPath);
svc = TreeSitterService(
lib: TreeSitterLib.fromDynamicLibrary(dylib),
grammarBytes: (lang) async => File('assets/grammars/$lang.wasm').readAsBytes(),
grammarQuery: (lang) async {
final f = File('assets/queries/$lang.scm');
return await f.exists() ? f.readAsString() : null;
},
);
});
// Intentionally do NOT call svc.dispose() at teardown — wasmtime's
// store-delete path collides with the Flutter test runner's process
// finalization (libc `double free` on exit). The OS reclaims everything
// when the runner process exits.
test('loads the dart grammar end-to-end', () async {
expect(await svc.hasGrammar('main.dart'), isTrue);
expect(await svc.languageFor('main.dart'), 'dart');
expect(svc.loadedLanguages, contains('dart'));
});
test('highlight returns at least one span for a tiny dart program', () async {
const source = 'void main() {\n print("hi");\n}\n';
final r = await svc.highlight('main.dart', source);
expect(r.spans, isNotEmpty);
// Every span must be in-range and well-ordered.
for (final s in r.spans) {
expect(s.start, lessThanOrEqualTo(s.end));
expect(s.end, lessThanOrEqualTo(source.length));
expect(s.role, isNotEmpty);
}
// The captures the upstream dart.scm emits cover at least one of these
// semantic roles for a `void main()` program — assert intersection so
// the test survives minor query reshufflings.
final roles = r.spans.map((s) => s.role).toSet();
expect(
roles.intersection({'keyword', 'type', 'function', 'string', 'punctuation.bracket', 'punctuation.delimiter'}),
isNotEmpty,
reason: 'expected at least one familiar dart role, got: $roles',
);
});
test('a second highlight reuses the cached grammar (no re-load)', () async {
// Re-issue a highlight; the grammar cache hit is internal but if the
// service is healthy this just completes without throwing and returns
// a sane result.
final r = await svc.highlight('main.dart', 'class X {}');
expect(r.spans, isNotEmpty);
});
});
}