- Update usb-install.md milestone documentation - Improve ADB service functionality and error handling - Update app shell and settings page for usb install feature - Add test coverage for adb service and widget - Update dependencies in pubspec.yaml - Archive completed task artifacts
240 lines
7.7 KiB
Dart
240 lines
7.7 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:appsok/src/services/adb_service.dart';
|
|
|
|
void main() {
|
|
group('AdbRuntimeConfig', () {
|
|
test('prefers bundled adb when it exists', () {
|
|
final tempDir = Directory.systemTemp.createTempSync(
|
|
'appsok-adb-runtime-',
|
|
);
|
|
addTearDown(() => tempDir.deleteSync(recursive: true));
|
|
|
|
final bundledAdb = File('${tempDir.path}/adb')..writeAsStringSync('');
|
|
final runtime = AdbRuntimeConfig(
|
|
bundledAdbPath: bundledAdb.path,
|
|
).resolve();
|
|
|
|
expect(runtime.executablePath, bundledAdb.path);
|
|
expect(runtime.source, AdbExecutableSource.bundled);
|
|
expect(runtime.bundledExists, isTrue);
|
|
expect(runtime.fallbackUsed, isFalse);
|
|
expect(runtime.serverPort, AdbRuntimeConfig.defaultAppSokServerPort);
|
|
});
|
|
|
|
test('falls back to system adb when bundled adb is missing', () {
|
|
final runtime = const AdbRuntimeConfig(
|
|
bundledAdbPath: '/tmp/appsok-missing-adb',
|
|
).resolve();
|
|
|
|
expect(runtime.executablePath, AdbRuntimeConfig.systemAdbPath);
|
|
expect(runtime.source, AdbExecutableSource.system);
|
|
expect(runtime.bundledExists, isFalse);
|
|
expect(runtime.fallbackUsed, isTrue);
|
|
});
|
|
|
|
test('uses custom adb as an explicit override', () {
|
|
final tempDir = Directory.systemTemp.createTempSync(
|
|
'appsok-adb-runtime-',
|
|
);
|
|
addTearDown(() => tempDir.deleteSync(recursive: true));
|
|
|
|
final bundledAdb = File('${tempDir.path}/adb')..writeAsStringSync('');
|
|
final runtime = AdbRuntimeConfig(
|
|
bundledAdbPath: bundledAdb.path,
|
|
customAdbPath: '/custom/platform-tools/adb',
|
|
).resolve();
|
|
|
|
expect(runtime.executablePath, '/custom/platform-tools/adb');
|
|
expect(runtime.source, AdbExecutableSource.custom);
|
|
expect(runtime.fallbackUsed, isFalse);
|
|
});
|
|
|
|
test('uses shared ADB server port when requested', () {
|
|
final runtime = const AdbRuntimeConfig(
|
|
serverMode: AdbServerMode.shared,
|
|
).resolve();
|
|
|
|
expect(runtime.serverPort, AdbRuntimeConfig.sharedServerPort);
|
|
expect(runtime.environment, {'ADB_SERVER_PORT': '5037'});
|
|
});
|
|
});
|
|
|
|
group('AdbService.collectDiagnostics', () {
|
|
test('captures adb version on success', () async {
|
|
const versionOutput =
|
|
'Android Debug Bridge version 1.0.41\n'
|
|
'Version 35.0.2-12147458\n';
|
|
final service = AdbService(
|
|
runtimeConfig: const AdbRuntimeConfig(
|
|
customAdbPath: '/opt/appsok/adb',
|
|
),
|
|
processRunner: (executable, args, {environment}) async {
|
|
expect(args, ['version']);
|
|
return ProcessResult(42, 0, versionOutput, '');
|
|
},
|
|
);
|
|
|
|
final diag = await service.collectDiagnostics();
|
|
|
|
expect(diag.version, versionOutput.trim());
|
|
expect(diag.versionError, isNull);
|
|
expect(diag.runtime.executablePath, '/opt/appsok/adb');
|
|
});
|
|
|
|
test('records versionError on non-zero exit', () async {
|
|
final service = AdbService(
|
|
runtimeConfig: const AdbRuntimeConfig(
|
|
customAdbPath: '/opt/appsok/adb',
|
|
),
|
|
processRunner: (_, args, {environment}) async {
|
|
return ProcessResult(42, 1, '', 'cannot run adb');
|
|
},
|
|
);
|
|
|
|
final diag = await service.collectDiagnostics();
|
|
|
|
expect(diag.version, isNull);
|
|
expect(diag.versionError, 'cannot run adb');
|
|
});
|
|
|
|
test('records versionError with exit code when stderr is empty', () async {
|
|
final service = AdbService(
|
|
runtimeConfig: const AdbRuntimeConfig(
|
|
customAdbPath: '/opt/appsok/adb',
|
|
),
|
|
processRunner: (_, args, {environment}) async {
|
|
return ProcessResult(42, 127, '', '');
|
|
},
|
|
);
|
|
|
|
final diag = await service.collectDiagnostics();
|
|
|
|
expect(diag.version, isNull);
|
|
expect(diag.versionError, 'exit code 127');
|
|
});
|
|
|
|
test('records versionError when process throws', () async {
|
|
final service = AdbService(
|
|
runtimeConfig: const AdbRuntimeConfig(
|
|
customAdbPath: '/not/found/adb',
|
|
),
|
|
processRunner: (_, args, {environment}) async {
|
|
throw const OSError('No such file or directory', 2);
|
|
},
|
|
);
|
|
|
|
final diag = await service.collectDiagnostics();
|
|
|
|
expect(diag.version, isNull);
|
|
expect(diag.versionError, isNotNull);
|
|
});
|
|
|
|
test('computes stable sha256 for a temp executable', () async {
|
|
final tempDir = Directory.systemTemp.createTempSync('appsok-diag-');
|
|
addTearDown(() => tempDir.deleteSync(recursive: true));
|
|
|
|
final fakeAdb = File('${tempDir.path}/adb')
|
|
..writeAsBytesSync([0x7f, 0x45, 0x4c, 0x46]);
|
|
|
|
final service = AdbService(
|
|
runtimeConfig: AdbRuntimeConfig(bundledAdbPath: fakeAdb.path),
|
|
processRunner: (_, args, {environment}) async =>
|
|
ProcessResult(0, 0, 'Android Debug Bridge version 1.0.0', ''),
|
|
);
|
|
|
|
final diag1 = await service.collectDiagnostics();
|
|
final diag2 = await service.collectDiagnostics();
|
|
|
|
expect(diag1.sha256, isNotNull);
|
|
expect(diag1.sha256, diag2.sha256);
|
|
expect(diag1.sha256, hasLength(64));
|
|
});
|
|
|
|
test('sha256 is null when executable does not exist', () async {
|
|
final service = AdbService(
|
|
runtimeConfig: const AdbRuntimeConfig(
|
|
customAdbPath: '/no/such/adb',
|
|
),
|
|
processRunner: (_, args, {environment}) async =>
|
|
ProcessResult(0, 0, '', ''),
|
|
);
|
|
|
|
final diag = await service.collectDiagnostics();
|
|
|
|
expect(diag.sha256, isNull);
|
|
});
|
|
});
|
|
|
|
group('AdbService', () {
|
|
test(
|
|
'runs adb devices with the resolved executable and server port',
|
|
() async {
|
|
final calls =
|
|
<({String executable, List<String> args, String? port})>[];
|
|
final service = AdbService(
|
|
runtimeConfig: const AdbRuntimeConfig(
|
|
customAdbPath: '/opt/appsok/adb',
|
|
serverPort: 15037,
|
|
),
|
|
processRunner: (executable, args, {environment}) async {
|
|
calls.add((
|
|
executable: executable,
|
|
args: args,
|
|
port: environment?['ADB_SERVER_PORT'],
|
|
));
|
|
return ProcessResult(
|
|
42,
|
|
0,
|
|
'List of devices attached\n'
|
|
'emulator-5554 device product:emu64a '
|
|
'model:sdk_gphone64_arm64 device:emu64a\n',
|
|
'',
|
|
);
|
|
},
|
|
);
|
|
|
|
final devices = await service.listDevices();
|
|
|
|
expect(calls, hasLength(1));
|
|
expect(calls.single.executable, '/opt/appsok/adb');
|
|
expect(calls.single.args, ['devices', '-l']);
|
|
expect(calls.single.port, '15037');
|
|
expect(devices.single.serial, 'emulator-5554');
|
|
expect(devices.single.state, 'device');
|
|
expect(devices.single.model, 'sdk_gphone64_arm64');
|
|
},
|
|
);
|
|
|
|
test('runs adb install with the shared server port', () async {
|
|
final calls = <({List<String> args, String? port})>[];
|
|
final service = AdbService(
|
|
runtimeConfig: const AdbRuntimeConfig(
|
|
customAdbPath: '/opt/appsok/adb',
|
|
serverMode: AdbServerMode.shared,
|
|
),
|
|
processRunner: (_, args, {environment}) async {
|
|
calls.add((args: args, port: environment?['ADB_SERVER_PORT']));
|
|
return ProcessResult(42, 0, 'Success', '');
|
|
},
|
|
);
|
|
|
|
final result = await service.installApk(
|
|
serial: 'R5CT90A1B2C',
|
|
apkPath: '/tmp/app.apk',
|
|
);
|
|
|
|
expect(result.succeeded, isTrue);
|
|
expect(calls.single.args, [
|
|
'-s',
|
|
'R5CT90A1B2C',
|
|
'install',
|
|
'-r',
|
|
'/tmp/app.apk',
|
|
]);
|
|
expect(calls.single.port, '5037');
|
|
});
|
|
});
|
|
}
|