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:
2026-05-17 20:21:05 +02:00
co-authored by Claude Opus 4.7
parent e430a87569
commit ab2e5e618b
7 changed files with 691 additions and 15 deletions
@@ -6,8 +6,11 @@
library;
import 'package:clide/kernel/src/syntax/tree_sitter_service.dart';
import 'package:clide/kernel/src/theme/tokens.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../helpers/kernel_fixture.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@@ -63,4 +66,99 @@ void main() {
expect(s.role, 'keyword');
});
});
group('TreeSitterService.colorForRole', () {
late SurfaceTokens tokens;
setUpAll(() async {
final f = await KernelFixture.create();
tokens = f.services.theme.current.surface;
await f.dispose();
});
test('every keyword-flavoured role maps to syntaxKeyword', () {
for (final role in ['keyword', 'repeat', 'conditional', 'include', 'exception', 'operator']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxKeyword, reason: role);
}
});
test('type-flavoured roles map to syntaxType', () {
for (final role in ['type', 'type.builtin', 'constructor']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxType, reason: role);
}
});
test('string-flavoured roles map to syntaxString', () {
for (final role in ['string', 'string.special']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxString, reason: role);
}
});
test('number-flavoured roles map to syntaxNumber', () {
for (final role in ['number', 'float', 'boolean']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxNumber, reason: role);
}
});
test('comment maps to syntaxComment', () {
expect(TreeSitterService.colorForRole('comment', tokens), tokens.syntaxComment);
});
test('function-flavoured roles map to syntaxMethod', () {
for (final role in ['function', 'function.builtin', 'function.method', 'method']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxMethod, reason: role);
}
});
test('punctuation roles map to syntaxPunct', () {
for (final role in ['punctuation.bracket', 'punctuation.delimiter', 'punctuation.special']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxPunct, reason: role);
}
});
test('variable roles fall back to globalForeground', () {
for (final role in ['variable', 'variable.builtin', 'variable.parameter']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.globalForeground, reason: role);
}
});
test('property / field share syntaxMethod with functions', () {
for (final role in ['property', 'field']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxMethod, reason: role);
}
});
test('constant roles share syntaxNumber with numbers', () {
for (final role in ['constant', 'constant.builtin']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxNumber, reason: role);
}
});
test('tag / attribute share syntaxKeyword', () {
for (final role in ['tag', 'attribute']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxKeyword, reason: role);
}
});
test('namespace / module share syntaxType', () {
for (final role in ['namespace', 'module']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxType, reason: role);
}
});
test('markdown text.* roles distribute across keyword / string / type tokens', () {
expect(TreeSitterService.colorForRole('text.title', tokens), tokens.syntaxKeyword);
for (final role in ['text.literal', 'text.reference', 'text.uri']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxString, reason: role);
}
for (final role in ['text.emphasis', 'text.strong']) {
expect(TreeSitterService.colorForRole(role, tokens), tokens.syntaxType, reason: role);
}
});
test('unknown roles fall through to globalForeground', () {
expect(TreeSitterService.colorForRole('definitely-not-a-real-role', tokens), tokens.globalForeground);
expect(TreeSitterService.colorForRole('', tokens), tokens.globalForeground);
});
});
}