From 9cf23997edb7fbbcce024119b12c952650e79fba Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Mon, 9 Nov 2026 06:19:50 -0500 Subject: [PATCH] Close the #1837 LiteRT-LM test gaps Refs #1837 #1845 --- .../litert_lm_v1_env_dir_embedding.dart | 26 ++++++++ .../litert_lm_embedding_native_test.dart | 5 +- .../litert_lm/litert_lm_gpu_adapter_test.dart | 28 ++++++-- .../litert_lm/litert_lm_service_test.dart | 1 + .../litert_lm_v1_embedding_test.dart | 64 +++++++++++++++++++ 5 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 test/fixtures/litert_lm_v1_env_dir_embedding.dart diff --git a/test/fixtures/litert_lm_v1_env_dir_embedding.dart b/test/fixtures/litert_lm_v1_env_dir_embedding.dart new file mode 100644 index 000000000..825d35c42 --- /dev/null +++ b/test/fixtures/litert_lm_v1_env_dir_embedding.dart @@ -0,0 +1,26 @@ +// Creates a LiteRT-LM embedding session through the default (environment +// directory) runtime route; args: . +import 'dart:ffi'; +import 'dart:io'; + +import 'package:llamadart/src/backends/litert_lm/litert_lm_runtime.dart'; + +Future main(List args) async { + debugLiteRtLmAbiOverride = Abi.values.singleWhere((abi) => '$abi' == args[0]); + final client = LiteRtLmRuntimeClient(); + try { + final session = await client.createEmbeddingSession( + modelPath: args[1], + backend: 'cpu', + cacheDir: args[2], + maxInputLength: 1024, + numberOfThreads: 2, + ); + session.dispose(); + stdout.writeln('ENV_DIR_EMBEDDING created'); + } on Object catch (error) { + stdout.writeln('ENV_DIR_EMBEDDING ${error.runtimeType}: $error'); + } finally { + client.dispose(); + } +} diff --git a/test/unit/backends/litert_lm/litert_lm_embedding_native_test.dart b/test/unit/backends/litert_lm/litert_lm_embedding_native_test.dart index f56e08dc2..83f48f33f 100644 --- a/test/unit/backends/litert_lm/litert_lm_embedding_native_test.dart +++ b/test/unit/backends/litert_lm/litert_lm_embedding_native_test.dart @@ -114,8 +114,11 @@ void main() { test( 'embedding deadline throws a typed error and cancels native work before reuse', () async { + // The deadline also bounds the reuse call, which must not race it on a + // loaded host. + const deadline = Duration(seconds: 1); final (backend, _, context) = await backendForTest( - embeddingTimeout: const Duration(milliseconds: 100), + embeddingTimeout: deadline, ); mode(11); final timedOut = expectLater( diff --git a/test/unit/backends/litert_lm/litert_lm_gpu_adapter_test.dart b/test/unit/backends/litert_lm/litert_lm_gpu_adapter_test.dart index b03e23843..f7813f58c 100644 --- a/test/unit/backends/litert_lm/litert_lm_gpu_adapter_test.dart +++ b/test/unit/backends/litert_lm/litert_lm_gpu_adapter_test.dart @@ -88,6 +88,20 @@ void main() { expect(vulkan.destroyedInstances, 1); }); + test('reads the count the second call rewrites', () { + vulkan.devices = [('llvmpipe', 4), ('NVIDIA L4', 2), ('gone', 2)]; + vulkan.writtenDevices = 2; + + final adapters = vulkan.loader.listAdapters(); + + expect(adapters.unavailableReason, isNull); + expect( + [for (final device in adapters.devices!) device.name], + ['llvmpipe', 'NVIDIA L4'], + ); + expect(vulkan.destroyedInstances, 1); + }); + test('lists no device when the loader reports none', () { final adapters = vulkan.loader.listAdapters(); @@ -156,6 +170,10 @@ final class _FakeVulkan { /// The count the first call reports when it is below [devices], as when a /// device appears between the two calls. int? countedDevices; + + /// The devices the second call writes and reports with `VK_SUCCESS` when + /// fewer than the first count, as when a device disappears between calls. + int? writtenDevices; int? createInfoStructureType; int destroyedInstances = 0; @@ -172,18 +190,20 @@ final class _FakeVulkan { count.value = countedDevices ?? devices.length; return 0; } - final written = count.value < devices.length - ? count.value - : devices.length; + final available = writtenDevices ?? devices.length; + final written = count.value < available ? count.value : available; for (var index = 0; index < written; index++) { final handle = calloc()..value = index; _handles.add(handle); handles[index] = handle.cast(); } count.value = written; - return written < devices.length ? 5 : 0; + return written < available ? 5 : 0; }, getPhysicalDeviceProperties: (device, properties) { + if (device == nullptr) { + throw StateError('Read an unwritten device handle.'); + } final (name, type) = devices[device.cast().value]; final bytes = properties.cast(); (bytes + 16).cast().value = type; diff --git a/test/unit/backends/litert_lm/litert_lm_service_test.dart b/test/unit/backends/litert_lm/litert_lm_service_test.dart index bf673bfe6..0bf6c0ef4 100644 --- a/test/unit/backends/litert_lm/litert_lm_service_test.dart +++ b/test/unit/backends/litert_lm/litert_lm_service_test.dart @@ -3595,6 +3595,7 @@ void main() { (error) => error.message, 'message', allOf( + contains('LiteRT-LM GPU on ${liteRtLmGateAbi()} needs'), contains('needs a hardware Vulkan device'), contains('"llvmpipe (LLVM 20.1.2, 256 bits)"'), contains('VK_PHYSICAL_DEVICE_TYPE_CPU'), diff --git a/test/unit/backends/litert_lm/litert_lm_v1_embedding_test.dart b/test/unit/backends/litert_lm/litert_lm_v1_embedding_test.dart index b2ae3bac3..74f76e0bf 100644 --- a/test/unit/backends/litert_lm/litert_lm_v1_embedding_test.dart +++ b/test/unit/backends/litert_lm/litert_lm_v1_embedding_test.dart @@ -14,6 +14,7 @@ import 'package:llamadart/src/backends/litert_lm/litert_lm_service.dart'; import 'package:llamadart/src/backends/litert_lm/litert_lm_v1_runtime.dart'; import 'package:test/test.dart'; +import '../../../support/dart_script_snapshot.dart'; import '../../../support/suite_system_temp.dart'; import 'litert_lm_embedding_test.dart' as fixtures; @@ -380,6 +381,69 @@ void main() { expect(count('call_count'), before); }); } + group('LLAMADART_LITERT_LM_LIB_DIR route', () { + late String script; + setUpAll( + () async => script = await compileDartScript( + 'test/fixtures/litert_lm_v1_env_dir_embedding.dart', + ), + ); + + Future createFromEnvironmentDirectory( + String runtimePath, + Abi abi, + ) async { + final runtime = Directory( + '${suite().path}/env-dir-${runtimePath.hashCode}', + )..createSync(); + final primary = liteRtLmPrimaryLibraryForAbi(Abi.current())!; + File(runtimePath).copySync('${runtime.path}/$primary'); + final stub = File('${runtime.path}/stub.c') + ..writeAsStringSync('int fixture_companion_stub(void) { return 1; }\n'); + final stubLibrary = '${runtime.path}/stub.bin'; + final built = await Process.run('cc', [ + '-shared', + '-fPIC', + stub.path, + '-o', + stubLibrary, + ]); + expect(built.exitCode, 0, reason: '${built.stderr}'); + for (final name in liteRtLmRequiredLibrariesForAbi( + Abi.current(), + ).where((name) => name != primary)) { + File(stubLibrary).copySync('${runtime.path}/$name'); + } + final result = await Process.run( + Platform.resolvedExecutable, + [script, '$abi', model, '${runtime.path}/cache'], + environment: { + 'LLAMADART_LITERT_LM_LIB_DIR': runtime.path, + 'TMPDIR': suite().path, + }, + ); + expect(result.exitCode, 0, reason: '${result.stdout}\n${result.stderr}'); + return '${result.stdout}'; + } + + for (final abi in [Abi.linuxArm64, Abi.linuxX64, Abi.windowsX64]) { + test('$abi refuses an unmarked runtime selected by the environment ' + 'directory', () async { + final output = await createFromEnvironmentDirectory(unmarkedPath, abi); + expect(output, contains('LlamaUnsupportedException')); + expect(output, contains('native embeddings on $abi')); + expect(output, contains('does not export')); + }); + } + + test('linux_x64 admits a marked runtime selected by the environment ' + 'directory', () async { + expect( + await createFromEnvironmentDirectory(libraryPath, Abi.linuxX64), + contains('ENV_DIR_EMBEDDING created'), + ); + }); + }); for (final abi in [Abi.macosArm64, Abi.iosArm64, Abi.androidArm64]) { test( '$abi creates embeddings without the in-process LiteRT marker',