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>
81 lines
3.2 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|