From 3175b7a633dbf2c2a71883359df04e27771dcb20 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 13 May 2026 06:41:14 +0200 Subject: [PATCH] test sweep: cover kernel/src/syntax (T-91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two test files covering the pure-Dart and fallback paths of the tree-sitter integration. The native-FFI parsing depth stays untested — DynamicLibrary.open('libtree-sitter.so') doesn't resolve under the flutter test runner because the bundled lib sits at native/linux-x64/ rather than on the linker search path. Real parsing coverage would need an integration-test harness that copies or symlinks the lib into the runner's working directory. - test/kernel/src/syntax/language_map_test.dart (6 tests): every branch of grammarForPath — common extensions, case-insensitive matching, special filenames (Makefile / Dockerfile / .gitignore / justfile), no-dot/no-special-match fallthrough, unknown extension. - test/kernel/src/syntax/tree_sitter_service_test.dart (9 tests): graceful-fallback paths of TreeSitterService when the library can't load — hasGrammar / languageFor / highlight all short- circuit cleanly, plus SyntaxResult.empty and SyntaxSpan field storage. Coverage: language_map.dart 0/7 -> 7/7; tree_sitter_service.dart 1/131 -> 22/131 (the rest is deep FFI work); tree_sitter_ffi.dart 0/15 -> 1/15 (init entry only). Total coverage 75.11% -> 75.42%. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/kernel/src/syntax/language_map_test.dart | 48 ++++++++++++++ .../src/syntax/tree_sitter_service_test.dart | 66 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 test/kernel/src/syntax/language_map_test.dart create mode 100644 test/kernel/src/syntax/tree_sitter_service_test.dart diff --git a/test/kernel/src/syntax/language_map_test.dart b/test/kernel/src/syntax/language_map_test.dart new file mode 100644 index 00000000..e88064d7 --- /dev/null +++ b/test/kernel/src/syntax/language_map_test.dart @@ -0,0 +1,48 @@ +/// Pure-Dart tests for `grammarForPath` in +/// `lib/kernel/src/syntax/language_map.dart`. +library; + +import 'package:clide/kernel/src/syntax/language_map.dart'; +import 'package:test/test.dart'; + +void main() { + group('grammarForPath', () { + test('maps common file extensions to grammar names', () { + expect(grammarForPath('foo.dart'), 'dart'); + expect(grammarForPath('a/b/c.go'), 'go'); + expect(grammarForPath('script.py'), 'python'); + expect(grammarForPath('view.tsx'), 'typescript'); + expect(grammarForPath('config.yaml'), 'yaml'); + expect(grammarForPath('config.yml'), 'yaml'); + expect(grammarForPath('readme.md'), 'markdown'); + }); + + test('extension match is case-insensitive', () { + expect(grammarForPath('a.R'), 'r'); + expect(grammarForPath('Q.PY'), 'python'); + }); + + test('special filenames bypass the extension lookup', () { + expect(grammarForPath('Makefile'), 'make'); + expect(grammarForPath('a/b/Dockerfile'), 'dockerfile'); + expect(grammarForPath('.gitignore'), 'gitignore'); + expect(grammarForPath('justfile'), 'just'); + }); + + test('special filename + recognised extension still picks the special name', () { + // Makefile has no '.' so the extension branch wouldn't fire anyway — + // this just locks in that the filename map runs first. + expect(grammarForPath('Makefile'), 'make'); + }); + + test('returns null when there is no dot and no special filename match', () { + expect(grammarForPath('README'), isNull); + expect(grammarForPath('a/b/UNKNOWNFILE'), isNull); + }); + + test('returns null for an unknown extension', () { + expect(grammarForPath('foo.unknownextension'), isNull); + expect(grammarForPath('a/b/c.xyz'), isNull); + }); + }); +} diff --git a/test/kernel/src/syntax/tree_sitter_service_test.dart b/test/kernel/src/syntax/tree_sitter_service_test.dart new file mode 100644 index 00000000..c917f864 --- /dev/null +++ b/test/kernel/src/syntax/tree_sitter_service_test.dart @@ -0,0 +1,66 @@ +/// Tests the graceful-fallback paths of `TreeSitterService` when the +/// native `libtree-sitter` library isn't dlopen-resolvable. In that +/// environment the service short-circuits every public method without +/// throwing — which is also how the running app behaves on platforms +/// where the library hasn't been bundled. +library; + +import 'package:clide/kernel/src/syntax/tree_sitter_service.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('TreeSitterService — fallback paths', () { + final svc = TreeSitterService.shared; + + test('hasGrammar(path) returns false for an unknown extension', () async { + expect(await svc.hasGrammar('foo.unknownextension'), isFalse); + }); + + test('hasGrammar(path) returns false when the library cannot load', () async { + // 'foo.dart' resolves to grammar "dart", which still requires the + // native library + asset to actually load. In the test runner the + // native lib isn't on the dlopen search path, so _init() fails and + // hasGrammar reports false. + expect(await svc.hasGrammar('foo.dart'), isFalse); + }); + + test('languageFor(path) returns null when the language is unknown', () async { + expect(await svc.languageFor('foo.unknownextension'), isNull); + }); + + test('languageFor(path) returns null when the library cannot load', () async { + expect(await svc.languageFor('foo.dart'), isNull); + }); + + test('highlight returns an empty result for unknown extensions', () async { + final r = await svc.highlight('foo.unknownextension', 'whatever'); + expect(r.spans, isEmpty); + }); + + test('highlight returns an empty result when the library cannot load', () async { + final r = await svc.highlight('foo.dart', 'void main() {}'); + expect(r.spans, isEmpty); + }); + + test('loadedLanguages getter returns a list (may be empty)', () { + // Just exercise the getter — the actual contents depend on the + // environment. + expect(svc.loadedLanguages, isA>()); + }); + }); + + group('SyntaxResult / SyntaxSpan', () { + test('SyntaxResult.empty has no spans', () { + expect(SyntaxResult.empty.spans, isEmpty); + }); + + test('SyntaxSpan stores start / end / role', () { + const s = SyntaxSpan(start: 3, end: 7, role: 'keyword'); + expect(s.start, 3); + expect(s.end, 7); + expect(s.role, 'keyword'); + }); + }); +}