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>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
/// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user