test: run the suite from a few shared entrypoints
This commit is contained in:
@@ -66,7 +66,7 @@ jobs:
|
|||||||
run: dart run tool/changelog.dart verify
|
run: dart run tool/changelog.dart verify
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: flutter test --reporter expanded --coverage
|
run: dart run tool/test.dart -- --reporter expanded --coverage
|
||||||
|
|
||||||
- name: Check coverage
|
- name: Check coverage
|
||||||
run: dart run tool/check_coverage.dart coverage/lcov.info 75
|
run: dart run tool/check_coverage.dart coverage/lcov.info 75
|
||||||
|
|||||||
+19
-1
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/ui/ui.dart';
|
||||||
|
|
||||||
extension FutureExt<T> on Future<T> {
|
extension FutureExt<T> on Future<T> {
|
||||||
Future<T> withTimeout({
|
Future<T> withTimeout({
|
||||||
@@ -37,6 +37,24 @@ extension FutureExt<T> on Future<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves once, then hands out the value rather than the future that
|
||||||
|
/// resolved it: a completed future still runs its listeners in the zone it was
|
||||||
|
/// created in, which under test may be a fake-async zone nothing drives.
|
||||||
|
class ResolveOnce<T> {
|
||||||
|
final Future<T> Function() _resolve;
|
||||||
|
Future<T>? _pending;
|
||||||
|
({T value})? _resolved;
|
||||||
|
|
||||||
|
ResolveOnce(this._resolve);
|
||||||
|
|
||||||
|
Future<T> get value async {
|
||||||
|
if (_resolved case (:final value)) return value;
|
||||||
|
final value = await (_pending ??= _resolve());
|
||||||
|
_resolved = (value: value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension CompleterExt<T> on Completer<T> {
|
extension CompleterExt<T> on Completer<T> {
|
||||||
void safeCompleter(T value) {
|
void safeCompleter(T value) {
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
|
|||||||
+24
-20
@@ -8,11 +8,10 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
|
|
||||||
class AppPath {
|
class AppPath {
|
||||||
static AppPath? _instance;
|
static AppPath? _instance;
|
||||||
Completer<Directory> dataDir = Completer();
|
late ResolveOnce<Directory> _dataDir;
|
||||||
late final Future<Directory?> _downloadDir = downloadDirectory();
|
late ResolveOnce<Directory?> _downloadDir;
|
||||||
Completer<Directory> tempDir = Completer();
|
late ResolveOnce<Directory> _tempDir;
|
||||||
Completer<Directory> cacheDir = Completer();
|
Completer<Directory> cacheDir = Completer();
|
||||||
late String appDirPath;
|
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
static Future<Directory> Function() supportDirectory =
|
static Future<Directory> Function() supportDirectory =
|
||||||
@@ -31,16 +30,21 @@ class AppPath {
|
|||||||
getDownloadsDirectory;
|
getDownloadsDirectory;
|
||||||
|
|
||||||
AppPath._internal() {
|
AppPath._internal() {
|
||||||
appDirPath = join(dirname(Platform.resolvedExecutable));
|
_bind();
|
||||||
supportDirectory().then((value) {
|
}
|
||||||
dataDir.complete(value);
|
|
||||||
});
|
@visibleForTesting
|
||||||
temporaryDirectory().then((value) {
|
Future<void> rebind() async {
|
||||||
tempDir.complete(value);
|
cacheDir = Completer();
|
||||||
});
|
_bind();
|
||||||
cacheDirectory().then((value) {
|
await Future.wait([_dataDir.value, _downloadDir.value, _tempDir.value]);
|
||||||
cacheDir.complete(value);
|
}
|
||||||
});
|
|
||||||
|
void _bind() {
|
||||||
|
_dataDir = ResolveOnce(supportDirectory);
|
||||||
|
_downloadDir = ResolveOnce(downloadDirectory);
|
||||||
|
_tempDir = ResolveOnce(temporaryDirectory);
|
||||||
|
cacheDirectory().then(cacheDir.complete);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AppPath() {
|
factory AppPath() {
|
||||||
@@ -66,12 +70,12 @@ class AppPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> get downloadDirPath async {
|
Future<String> get downloadDirPath async {
|
||||||
final directory = await _downloadDir;
|
final directory = await _downloadDir.value;
|
||||||
return directory?.path ?? await homeDirPath;
|
return directory?.path ?? await homeDirPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> get homeDirPath async {
|
Future<String> get homeDirPath async {
|
||||||
final directory = await dataDir.future;
|
final directory = await _dataDir.value;
|
||||||
return directory.path;
|
return directory.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +85,7 @@ class AppPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> get tempFilePath async {
|
Future<String> get tempFilePath async {
|
||||||
final mTempDir = await tempDir.future;
|
final mTempDir = await _tempDir.value;
|
||||||
return join(mTempDir.path, 'temp$uniqueId');
|
return join(mTempDir.path, 'temp$uniqueId');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +100,12 @@ class AppPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> get sharedPreferencesPath async {
|
Future<String> get sharedPreferencesPath async {
|
||||||
final directory = await dataDir.future;
|
final directory = await _dataDir.value;
|
||||||
return join(directory.path, 'shared_preferences.json');
|
return join(directory.path, 'shared_preferences.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> get profilesPath async {
|
Future<String> get profilesPath async {
|
||||||
final directory = await dataDir.future;
|
final directory = await _dataDir.value;
|
||||||
return join(directory.path, profilesDirectoryName);
|
return join(directory.path, profilesDirectoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +160,7 @@ class AppPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> get tempPath async {
|
Future<String> get tempPath async {
|
||||||
final directory = await tempDir.future;
|
final directory = await _tempDir.value;
|
||||||
return directory.path;
|
return directory.path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-20
@@ -6,19 +6,29 @@ import 'package:fl_clash/common/common.dart';
|
|||||||
import 'package:fl_clash/common/system_dns.dart';
|
import 'package:fl_clash/common/system_dns.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class Preferences {
|
class Preferences {
|
||||||
static Preferences? _instance;
|
static Preferences? _instance;
|
||||||
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
|
var _store = ResolveOnce(_open);
|
||||||
|
|
||||||
Future<bool> get isInit async =>
|
Future<bool> get isInit async => await _store.value != null;
|
||||||
await sharedPreferencesCompleter.future != null;
|
|
||||||
|
|
||||||
Preferences._internal() {
|
Preferences._internal();
|
||||||
SharedPreferences.getInstance()
|
|
||||||
.then((value) => sharedPreferencesCompleter.complete(value))
|
static Future<SharedPreferences?> _open() async {
|
||||||
.onError((_, _) => sharedPreferencesCompleter.complete(null));
|
try {
|
||||||
|
return await SharedPreferences.getInstance();
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
Future<void> rebind() async {
|
||||||
|
_store = ResolveOnce(_open);
|
||||||
|
await _store.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Preferences() {
|
factory Preferences() {
|
||||||
@@ -27,23 +37,23 @@ class Preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getVersion() async {
|
Future<int> getVersion() async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await _store.value;
|
||||||
return preferences?.getInt('version') ?? 0;
|
return preferences?.getInt('version') ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setVersion(int version) async {
|
Future<void> setVersion(int version) async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await _store.value;
|
||||||
await preferences?.setInt('version', version);
|
await preferences?.setInt('version', version);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveShareState(SharedState shareState) async {
|
Future<void> saveShareState(SharedState shareState) async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await _store.value;
|
||||||
await preferences?.setString('sharedState', json.encode(shareState));
|
await preferences?.setString('sharedState', json.encode(shareState));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, Object?>?> getConfigMap() async {
|
Future<Map<String, Object?>?> getConfigMap() async {
|
||||||
try {
|
try {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await _store.value;
|
||||||
final configString = preferences?.getString(configKey);
|
final configString = preferences?.getString(configKey);
|
||||||
if (configString == null) return null;
|
if (configString == null) return null;
|
||||||
final Map<String, Object?>? configMap = json.decode(configString);
|
final Map<String, Object?>? configMap = json.decode(configString);
|
||||||
@@ -59,7 +69,7 @@ class Preferences {
|
|||||||
|
|
||||||
Future<Map<String, Object?>?> getClashConfigMap() async {
|
Future<Map<String, Object?>?> getClashConfigMap() async {
|
||||||
try {
|
try {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await _store.value;
|
||||||
final clashConfigString = preferences?.getString(clashConfigKey);
|
final clashConfigString = preferences?.getString(clashConfigKey);
|
||||||
if (clashConfigString == null) return null;
|
if (clashConfigString == null) return null;
|
||||||
return json.decode(clashConfigString);
|
return json.decode(clashConfigString);
|
||||||
@@ -74,7 +84,7 @@ class Preferences {
|
|||||||
|
|
||||||
Future<void> clearClashConfig() async {
|
Future<void> clearClashConfig() async {
|
||||||
try {
|
try {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await _store.value;
|
||||||
await preferences?.remove(clashConfigKey);
|
await preferences?.remove(clashConfigKey);
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -95,13 +105,13 @@ class Preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> saveConfig(Config config) async {
|
Future<bool> saveConfig(Config config) async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await _store.value;
|
||||||
return preferences?.setString(configKey, json.encode(config)) ?? false;
|
return preferences?.setString(configKey, json.encode(config)) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SystemDnsRecord?> getSystemDnsRecord() async {
|
Future<SystemDnsRecord?> getSystemDnsRecord() async {
|
||||||
try {
|
try {
|
||||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
final sharedPreferencesIns = await _store.value;
|
||||||
final raw = sharedPreferencesIns?.getString(systemDnsRecordKey);
|
final raw = sharedPreferencesIns?.getString(systemDnsRecordKey);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -117,7 +127,7 @@ class Preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveSystemDnsRecord(SystemDnsRecord record) async {
|
Future<void> saveSystemDnsRecord(SystemDnsRecord record) async {
|
||||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
final sharedPreferencesIns = await _store.value;
|
||||||
await sharedPreferencesIns?.setString(
|
await sharedPreferencesIns?.setString(
|
||||||
systemDnsRecordKey,
|
systemDnsRecordKey,
|
||||||
json.encode(record),
|
json.encode(record),
|
||||||
@@ -125,13 +135,13 @@ class Preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearSystemDnsRecord() async {
|
Future<void> clearSystemDnsRecord() async {
|
||||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
final sharedPreferencesIns = await _store.value;
|
||||||
await sharedPreferencesIns?.remove(systemDnsRecordKey);
|
await sharedPreferencesIns?.remove(systemDnsRecordKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<BootRecord?> getBootRecord() async {
|
Future<BootRecord?> getBootRecord() async {
|
||||||
try {
|
try {
|
||||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
final sharedPreferencesIns = await _store.value;
|
||||||
final raw = sharedPreferencesIns?.getString(bootRecordKey);
|
final raw = sharedPreferencesIns?.getString(bootRecordKey);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -147,12 +157,12 @@ class Preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveBootRecord(BootRecord record) async {
|
Future<void> saveBootRecord(BootRecord record) async {
|
||||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
final sharedPreferencesIns = await _store.value;
|
||||||
await sharedPreferencesIns?.setString(bootRecordKey, json.encode(record));
|
await sharedPreferencesIns?.setString(bootRecordKey, json.encode(record));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearPreferences() async {
|
Future<void> clearPreferences() async {
|
||||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
final sharedPreferencesIns = await _store.value;
|
||||||
await sharedPreferencesIns?.clear();
|
await sharedPreferencesIns?.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1014,7 +1014,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.2.2"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: "direct dev"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ dev_dependencies:
|
|||||||
sqlite3: ^3.7.0
|
sqlite3: ^3.7.0
|
||||||
cross_file: ^0.3.5+5
|
cross_file: ^0.3.5+5
|
||||||
mocktail: ^1.0.5
|
mocktail: ^1.0.5
|
||||||
path_provider_platform_interface: ^2.1.3
|
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
@@ -6,19 +6,18 @@ import 'package:fl_clash/common/common.dart';
|
|||||||
import 'package:fl_clash/database/database.dart' as db;
|
import 'package:fl_clash/database/database.dart' as db;
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
|
import '../helpers/preferences.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
late Directory home;
|
late Directory home;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() async {
|
||||||
home = Directory.systemTemp.createTempSync('flclash-app-data-');
|
home = Directory.systemTemp.createTempSync('flclash-app-data-');
|
||||||
AppPath.supportDirectory = () async => home;
|
await useAppPaths(home);
|
||||||
AppPath.temporaryDirectory = () async => home;
|
|
||||||
AppPath.cacheDirectory = () async => home;
|
|
||||||
AppPath.downloadDirectory = () async => home;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
@@ -26,7 +25,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('clears what a startup failure left behind without the Core', () async {
|
test('clears what a startup failure left behind without the Core', () async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
await mockPreferences();
|
||||||
await preferences.setVersion(7);
|
await preferences.setVersion(7);
|
||||||
final databaseFile = File(await appPath.databasePath)
|
final databaseFile = File(await appPath.databasePath)
|
||||||
..writeAsStringSync('not a database');
|
..writeAsStringSync('not a database');
|
||||||
@@ -35,7 +34,9 @@ void main() {
|
|||||||
final broken = db.Database(
|
final broken = db.Database(
|
||||||
LazyDatabase(() async => throw const FileSystemException('corrupt')),
|
LazyDatabase(() async => throw const FileSystemException('corrupt')),
|
||||||
);
|
);
|
||||||
|
final previous = db.database;
|
||||||
db.database = broken;
|
db.database = broken;
|
||||||
|
addTearDown(() => db.database = previous);
|
||||||
await expectLater(broken.profilesDao.query().get(), throwsA(anything));
|
await expectLater(broken.profilesDao.query().get(), throwsA(anything));
|
||||||
|
|
||||||
await clearAppData();
|
await clearAppData();
|
||||||
|
|||||||
@@ -10,22 +10,8 @@ import 'package:fl_clash/enum/enum.dart';
|
|||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
||||||
|
|
||||||
class _FakePathProvider extends PathProviderPlatform {
|
import '../helpers/app_paths.dart';
|
||||||
final String root;
|
|
||||||
|
|
||||||
_FakePathProvider(this.root);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getTemporaryPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationSupportPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationCachePath() async => root;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The public entry points hand their work to a background isolate, which
|
/// The public entry points hand their work to a background isolate, which
|
||||||
/// cannot reach the mocked platform channels, so the tests drive the worker
|
/// cannot reach the mocked platform channels, so the tests drive the worker
|
||||||
@@ -40,7 +26,7 @@ void main() {
|
|||||||
// already-existing temp and cache dirs the platform hands out.
|
// already-existing temp and cache dirs the platform hands out.
|
||||||
Directory(join(root.path, 'tmp')).createSync(recursive: true);
|
Directory(join(root.path, 'tmp')).createSync(recursive: true);
|
||||||
Directory(join(root.path, 'archive')).createSync(recursive: true);
|
Directory(join(root.path, 'archive')).createSync(recursive: true);
|
||||||
PathProviderPlatform.instance = _FakePathProvider(root.path);
|
await useAppPaths(root);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import 'package:fl_clash/common/dav_client.dart';
|
|||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
|
|
||||||
class _FakeDAVClient extends DAVClient {
|
class _FakeDAVClient extends DAVClient {
|
||||||
_FakeDAVClient(super.dav, this.result, [this.onPing]);
|
_FakeDAVClient(super.dav, this.result, [this.onPing]);
|
||||||
@@ -22,21 +23,6 @@ class _FakeDAVClient extends DAVClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FakePathProvider extends PathProviderPlatform {
|
|
||||||
final String root;
|
|
||||||
|
|
||||||
_FakePathProvider(this.root);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getTemporaryPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationSupportPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationCachePath() async => root;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RecordedRequest {
|
class _RecordedRequest {
|
||||||
_RecordedRequest(this.method, this.path, this.authorization, this.body);
|
_RecordedRequest(this.method, this.path, this.authorization, this.body);
|
||||||
|
|
||||||
@@ -213,11 +199,9 @@ void main() {
|
|||||||
late Directory root;
|
late Directory root;
|
||||||
HttpOverrides? testOverrides;
|
HttpOverrides? testOverrides;
|
||||||
|
|
||||||
// appPath resolves the data directory once per process, so every test in
|
setUpAll(() async {
|
||||||
// this group has to share one root.
|
|
||||||
setUpAll(() {
|
|
||||||
root = Directory.systemTemp.createTempSync('dav_client_test');
|
root = Directory.systemTemp.createTempSync('dav_client_test');
|
||||||
PathProviderPlatform.instance = _FakePathProvider(root.path);
|
await useAppPaths(root);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -4,32 +4,18 @@ import 'package:fl_clash/common/common.dart';
|
|||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
||||||
import 'package:yaml/yaml.dart';
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
class _FakePathProvider extends PathProviderPlatform {
|
import '../helpers/app_paths.dart';
|
||||||
_FakePathProvider(this.root);
|
|
||||||
|
|
||||||
final String root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getTemporaryPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationSupportPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationCachePath() async => root;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
late Directory root;
|
late Directory root;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() async {
|
||||||
root = Directory.systemTemp.createTempSync('path_test');
|
root = Directory.systemTemp.createTempSync('path_test');
|
||||||
PathProviderPlatform.instance = _FakePathProvider(root.path);
|
await useAppPaths(root);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'package:fl_clash/models/models.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../helpers/preferences.dart';
|
||||||
|
|
||||||
const _sharedState = SharedState(
|
const _sharedState = SharedState(
|
||||||
stopTip: 'stop',
|
stopTip: 'stop',
|
||||||
startTip: 'start',
|
startTip: 'start',
|
||||||
@@ -19,13 +21,12 @@ const _sharedState = SharedState(
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
SharedPreferences.setMockInitialValues({});
|
|
||||||
|
|
||||||
late SharedPreferences store;
|
late SharedPreferences store;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
|
await mockPreferences();
|
||||||
store = await SharedPreferences.getInstance();
|
store = await SharedPreferences.getInstance();
|
||||||
await store.clear();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('version', () {
|
group('version', () {
|
||||||
|
|||||||
@@ -3,22 +3,8 @@ import 'dart:io';
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
||||||
|
|
||||||
class _FakePathProvider extends PathProviderPlatform {
|
import '../helpers/app_paths.dart';
|
||||||
_FakePathProvider(this.root);
|
|
||||||
|
|
||||||
final String root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getTemporaryPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationSupportPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationCachePath() async => root;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RecordedRun {
|
class _RecordedRun {
|
||||||
const _RecordedRun(this.executable, this.arguments);
|
const _RecordedRun(this.executable, this.arguments);
|
||||||
@@ -115,9 +101,9 @@ void main() {
|
|||||||
late _FakeProcesses processes;
|
late _FakeProcesses processes;
|
||||||
String? password;
|
String? password;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() async {
|
||||||
root = Directory.systemTemp.createTempSync('system_test');
|
root = Directory.systemTemp.createTempSync('system_test');
|
||||||
PathProviderPlatform.instance = _FakePathProvider(root.path);
|
await useAppPaths(root);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ import 'package:material_ui/material_ui.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:tray/tray.dart';
|
import 'package:tray/tray.dart';
|
||||||
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
|
|
||||||
const _channel = MethodChannel('tray');
|
const _channel = MethodChannel('tray');
|
||||||
|
|
||||||
const _proxyGroup = Group(
|
const _proxyGroup = Group(
|
||||||
@@ -41,21 +42,6 @@ HotKeyAction _bind(HotAction action, PhysicalKeyboardKey key) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FakePathProvider extends PathProviderPlatform {
|
|
||||||
_FakePathProvider(this.root);
|
|
||||||
|
|
||||||
final String root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getTemporaryPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationSupportPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationCachePath() async => root;
|
|
||||||
}
|
|
||||||
|
|
||||||
TrayState _trayState({
|
TrayState _trayState({
|
||||||
bool isStart = false,
|
bool isStart = false,
|
||||||
bool tunEnable = false,
|
bool tunEnable = false,
|
||||||
@@ -106,7 +92,7 @@ void main() {
|
|||||||
|
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
root = Directory.systemTemp.createTempSync('tray_menu_test');
|
root = Directory.systemTemp.createTempSync('tray_menu_test');
|
||||||
PathProviderPlatform.instance = _FakePathProvider(root.path);
|
await useAppPaths(root);
|
||||||
await AppLocalizations.load(const Locale('en'));
|
await AppLocalizations.load(const Locale('en'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_clash/common/path.dart';
|
||||||
|
|
||||||
|
/// Resolves every directory [appPath] hands out to [root].
|
||||||
|
Future<void> useAppPaths(Directory root) {
|
||||||
|
Future<Directory> resolve() async => root;
|
||||||
|
AppPath.supportDirectory = resolve;
|
||||||
|
AppPath.temporaryDirectory = resolve;
|
||||||
|
AppPath.cacheDirectory = resolve;
|
||||||
|
AppPath.downloadDirectory = resolve;
|
||||||
|
return appPath.rebind();
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:fl_clash/common/preferences.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// Backs [preferences] with a fresh in-memory store holding [values].
|
||||||
|
Future<void> mockPreferences([Map<String, Object> values = const {}]) {
|
||||||
|
SharedPreferences.setMockInitialValues(values);
|
||||||
|
return preferences.rebind();
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../helpers/preferences.dart';
|
||||||
import '../helpers/test_app.dart';
|
import '../helpers/test_app.dart';
|
||||||
import '../helpers/test_profiles.dart';
|
import '../helpers/test_profiles.dart';
|
||||||
|
|
||||||
@@ -28,14 +29,13 @@ class _RecordingSetupAction extends SetupAction {
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
SharedPreferences.setMockInitialValues({});
|
|
||||||
|
|
||||||
late ProviderContainer container;
|
late ProviderContainer container;
|
||||||
late SharedPreferences store;
|
late SharedPreferences store;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
|
await mockPreferences();
|
||||||
store = await SharedPreferences.getInstance();
|
store = await SharedPreferences.getInstance();
|
||||||
await store.clear();
|
|
||||||
_RecordingSetupAction.requests.clear();
|
_RecordingSetupAction.requests.clear();
|
||||||
container = ProviderContainer(
|
container = ProviderContainer(
|
||||||
overrides: [
|
overrides: [
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/common/theme.dart';
|
|
||||||
import 'package:fl_clash/core/core.dart';
|
import 'package:fl_clash/core/core.dart';
|
||||||
import 'package:fl_clash/core/interface.dart';
|
import 'package:fl_clash/core/interface.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
@@ -17,32 +15,18 @@ import 'package:fl_clash/providers/database.dart';
|
|||||||
import 'package:fl_clash/providers/route_state.dart';
|
import 'package:fl_clash/providers/route_state.dart';
|
||||||
import 'package:fl_clash/providers/state.dart';
|
import 'package:fl_clash/providers/state.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/ui/ui.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
import '../helpers/test_profiles.dart';
|
import '../helpers/test_profiles.dart';
|
||||||
|
|
||||||
class _MockCoreHandlerInterface extends Mock implements CoreHandlerInterface {}
|
class _MockCoreHandlerInterface extends Mock implements CoreHandlerInterface {}
|
||||||
|
|
||||||
class _FakePathProvider extends PathProviderPlatform {
|
|
||||||
final String root;
|
|
||||||
|
|
||||||
_FakePathProvider(this.root);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getTemporaryPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationSupportPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationCachePath() async => root;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _nullProfileSetupState = SetupState(
|
const _nullProfileSetupState = SetupState(
|
||||||
profileId: null,
|
profileId: null,
|
||||||
profileLastUpdateDate: null,
|
profileLastUpdateDate: null,
|
||||||
@@ -118,7 +102,6 @@ Future<ProviderContainer> _pumpCoreManager(
|
|||||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
globalState.measure = Measure.of(context, 1);
|
globalState.measure = Measure.of(context, 1);
|
||||||
globalState.theme = CommonTheme.of(context, 1);
|
|
||||||
return StatusManager(child: child!);
|
return StatusManager(child: child!);
|
||||||
},
|
},
|
||||||
home: const CoreManager(child: SizedBox()),
|
home: const CoreManager(child: SizedBox()),
|
||||||
@@ -161,9 +144,7 @@ void main() {
|
|||||||
profileSwitchTempDir = Directory.systemTemp.createTempSync(
|
profileSwitchTempDir = Directory.systemTemp.createTempSync(
|
||||||
'core_manager_test',
|
'core_manager_test',
|
||||||
);
|
);
|
||||||
PathProviderPlatform.instance = _FakePathProvider(
|
await useAppPaths(profileSwitchTempDir);
|
||||||
profileSwitchTempDir.path,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -5,31 +5,17 @@ import 'dart:typed_data';
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
||||||
|
|
||||||
class _FakePathProvider extends PathProviderPlatform {
|
import '../helpers/app_paths.dart';
|
||||||
final String root;
|
|
||||||
|
|
||||||
_FakePathProvider(this.root);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getTemporaryPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationSupportPath() async => root;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getApplicationCachePath() async => root;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
late Directory tempDir;
|
late Directory tempDir;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() async {
|
||||||
tempDir = Directory.systemTemp.createTempSync('profile_save_test');
|
tempDir = Directory.systemTemp.createTempSync('profile_save_test');
|
||||||
PathProviderPlatform.instance = _FakePathProvider(tempDir.path);
|
await useAppPaths(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import 'package:flutter/widgets.dart' show Locale;
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
|
import '../helpers/preferences.dart';
|
||||||
|
|
||||||
Profile _profile(int id, String label) => Profile(
|
Profile _profile(int id, String label) => Profile(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -29,13 +31,10 @@ void main() {
|
|||||||
late Directory home;
|
late Directory home;
|
||||||
|
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
SharedPreferences.setMockInitialValues({'version': 1});
|
await mockPreferences({'version': 1});
|
||||||
await AppLocalizations.load(const Locale('en'));
|
await AppLocalizations.load(const Locale('en'));
|
||||||
home = Directory.systemTemp.createTempSync('flclash-backup-action-');
|
home = Directory.systemTemp.createTempSync('flclash-backup-action-');
|
||||||
AppPath.supportDirectory = () async => home;
|
await useAppPaths(home);
|
||||||
AppPath.temporaryDirectory = () async => home;
|
|
||||||
AppPath.cacheDirectory = () async => home;
|
|
||||||
AppPath.downloadDirectory = () async => home;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
import '../helpers/test_profiles.dart';
|
import '../helpers/test_profiles.dart';
|
||||||
|
|
||||||
class _MockCoreHandlerInterface extends Mock implements CoreHandlerInterface {}
|
class _MockCoreHandlerInterface extends Mock implements CoreHandlerInterface {}
|
||||||
@@ -56,12 +57,9 @@ void main() {
|
|||||||
late Completer<void>? compileGate;
|
late Completer<void>? compileGate;
|
||||||
late List<({String name, String url})> compiles;
|
late List<({String name, String url})> compiles;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() async {
|
||||||
home = Directory.systemTemp.createTempSync('flclash-providers-action-');
|
home = Directory.systemTemp.createTempSync('flclash-providers-action-');
|
||||||
AppPath.supportDirectory = () async => home;
|
await useAppPaths(home);
|
||||||
AppPath.temporaryDirectory = () async => home;
|
|
||||||
AppPath.cacheDirectory = () async => home;
|
|
||||||
AppPath.downloadDirectory = () async => home;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import 'package:drift/native.dart';
|
|||||||
// notifier, so the schema side is imported behind a prefix.
|
// notifier, so the schema side is imported behind a prefix.
|
||||||
import 'package:fl_clash/database/database.dart' as db;
|
import 'package:fl_clash/database/database.dart' as db;
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/l10n/l10n.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/providers/database.dart';
|
import 'package:fl_clash/providers/database.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_riverpod/misc.dart' show ProviderListenable;
|
import 'package:flutter_riverpod/misc.dart' show ProviderListenable;
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -17,6 +19,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
/// fails. These tests drive the real drift schema on an in-memory executor so
|
/// fails. These tests drive the real drift schema on an in-memory executor so
|
||||||
/// both halves — the optimistic value and the rollback — are observable.
|
/// both halves — the optimistic value and the rollback — are observable.
|
||||||
void main() {
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
const profileId = 1;
|
const profileId = 1;
|
||||||
|
|
||||||
late db.Database testDatabase;
|
late db.Database testDatabase;
|
||||||
@@ -29,6 +32,8 @@ void main() {
|
|||||||
order: order,
|
order: order,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setUpAll(() => AppLocalizations.load(const Locale('en')));
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
testDatabase = db.Database(NativeDatabase.memory());
|
testDatabase = db.Database(NativeDatabase.memory());
|
||||||
db.database = testDatabase;
|
db.database = testDatabase;
|
||||||
@@ -48,14 +53,19 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Most call sites wrap the persist step in `unawaited(...)`, so the failure
|
/// Most call sites wrap the persist step in `unawaited(...)`, so the failure
|
||||||
/// never reaches the caller — it lands in the zone handler, and only after
|
/// never reaches the caller: the notifier logs it, and only after the
|
||||||
/// the rollback has already restored the snapshot.
|
/// rollback has already restored the snapshot.
|
||||||
Future<Object> captureWriteFailure(FutureOr<void> Function() body) {
|
Future<void> captureWriteFailure(void Function() body) {
|
||||||
final completer = Completer<Object>();
|
final reported = Completer<void>();
|
||||||
runZonedGuarded(() async => body(), (error, _) {
|
final print = debugPrint;
|
||||||
if (!completer.isCompleted) completer.complete(error);
|
debugPrint = (message, {wrapWidth}) {
|
||||||
});
|
if (message?.contains('Optimistic database write failed') ?? false) {
|
||||||
return completer.future.timeout(const Duration(seconds: 5));
|
if (!reported.isCompleted) reported.complete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addTearDown(() => debugPrint = print);
|
||||||
|
body();
|
||||||
|
return reported.future.timeout(const Duration(seconds: 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `customStatement` does not raise a drift table-update notification, so the
|
/// `customStatement` does not raise a drift table-update notification, so the
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ String _iconSetJson(String name, List<String> icons) => json.encode({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Real downloads over loopback: the test binding would answer every request
|
/// Real downloads over loopback: the test binding answers every request with a
|
||||||
/// with a 400, so this suite runs without it.
|
/// 400, so its override is lifted while a test runs.
|
||||||
void main() {
|
void main() {
|
||||||
late HttpServer server;
|
late HttpServer server;
|
||||||
late String body;
|
late String body;
|
||||||
@@ -56,6 +56,9 @@ void main() {
|
|||||||
tearDownAll(() => server.close(force: true));
|
tearDownAll(() => server.close(force: true));
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
final bindingOverrides = HttpOverrides.current;
|
||||||
|
HttpOverrides.global = null;
|
||||||
|
addTearDown(() => HttpOverrides.global = bindingOverrides);
|
||||||
held = null;
|
held = null;
|
||||||
testDatabase = db.Database(NativeDatabase.memory());
|
testDatabase = db.Database(NativeDatabase.memory());
|
||||||
db.database = testDatabase;
|
db.database = testDatabase;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import 'package:mocktail/mocktail.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:yaml/yaml.dart';
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
import '../helpers/test_app.dart';
|
import '../helpers/test_app.dart';
|
||||||
import '../helpers/test_profiles.dart';
|
import '../helpers/test_profiles.dart';
|
||||||
|
|
||||||
@@ -32,10 +33,7 @@ void main() {
|
|||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
await AppLocalizations.load(const Locale('en'));
|
await AppLocalizations.load(const Locale('en'));
|
||||||
home = Directory.systemTemp.createTempSync('flclash-profiles-action-');
|
home = Directory.systemTemp.createTempSync('flclash-profiles-action-');
|
||||||
AppPath.supportDirectory = () async => home;
|
await useAppPaths(home);
|
||||||
AppPath.temporaryDirectory = () async => home;
|
|
||||||
AppPath.cacheDirectory = () async => home;
|
|
||||||
AppPath.downloadDirectory = () async => home;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
|
import '../helpers/preferences.dart';
|
||||||
|
|
||||||
class _MockCoreHandlerInterface extends Mock implements CoreHandlerInterface {}
|
class _MockCoreHandlerInterface extends Mock implements CoreHandlerInterface {}
|
||||||
|
|
||||||
@@ -44,13 +46,10 @@ void main() {
|
|||||||
late _MockCoreHandlerInterface core;
|
late _MockCoreHandlerInterface core;
|
||||||
late db.Database testDatabase;
|
late db.Database testDatabase;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() async {
|
||||||
registerFallbackValue(0);
|
registerFallbackValue(0);
|
||||||
home = Directory.systemTemp.createTempSync('flclash-store-');
|
home = Directory.systemTemp.createTempSync('flclash-store-');
|
||||||
AppPath.supportDirectory = () async => home;
|
await useAppPaths(home);
|
||||||
AppPath.temporaryDirectory = () async => home;
|
|
||||||
AppPath.cacheDirectory = () async => home;
|
|
||||||
AppPath.downloadDirectory = () async => home;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
@@ -58,7 +57,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
await mockPreferences();
|
||||||
await preferences.setVersion(7);
|
await preferences.setVersion(7);
|
||||||
core = _MockCoreHandlerInterface();
|
core = _MockCoreHandlerInterface();
|
||||||
when(() => core.clearEffect(any())).thenAnswer((_) async => '');
|
when(() => core.clearEffect(any())).thenAnswer((_) async => '');
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import 'package:fl_clash/providers/config.dart';
|
|||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
import '../helpers/preferences.dart';
|
||||||
|
|
||||||
class TestSystemAction extends SystemAction {
|
class TestSystemAction extends SystemAction {
|
||||||
final List<String> calls = [];
|
final List<String> calls = [];
|
||||||
@@ -255,7 +256,7 @@ void main() {
|
|||||||
test(
|
test(
|
||||||
'saving preferences captures the latest normal window geometry',
|
'saving preferences captures the latest normal window geometry',
|
||||||
() async {
|
() async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
await mockPreferences();
|
||||||
const geometry = WindowProps(width: 1180, height: 760, left: 72, top: 48);
|
const geometry = WindowProps(width: 1180, height: 760, left: 72, top: 48);
|
||||||
windowPort = _GeometryWindowPort(geometry);
|
windowPort = _GeometryWindowPort(geometry);
|
||||||
final action = _PersistenceSystemAction();
|
final action = _PersistenceSystemAction();
|
||||||
@@ -279,7 +280,7 @@ void main() {
|
|||||||
test(
|
test(
|
||||||
'a geometry failure does not prevent saving the current config',
|
'a geometry failure does not prevent saving the current config',
|
||||||
() async {
|
() async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
await mockPreferences();
|
||||||
const current = WindowProps(width: 900, height: 640, left: 24, top: 16);
|
const current = WindowProps(width: 900, height: 640, left: 24, top: 16);
|
||||||
windowPort = _GeometryWindowPort(
|
windowPort = _GeometryWindowPort(
|
||||||
const WindowProps(),
|
const WindowProps(),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
import '../helpers/context_menu.dart';
|
import '../helpers/context_menu.dart';
|
||||||
import '../helpers/test_app.dart';
|
import '../helpers/test_app.dart';
|
||||||
import '../helpers/test_profiles.dart';
|
import '../helpers/test_profiles.dart';
|
||||||
@@ -89,11 +90,9 @@ ProviderContainer _containerFor(
|
|||||||
void main() {
|
void main() {
|
||||||
late Directory home;
|
late Directory home;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() async {
|
||||||
home = Directory.systemTemp.createTempSync('flclash-rule-sets-view-');
|
home = Directory.systemTemp.createTempSync('flclash-rule-sets-view-');
|
||||||
AppPath.supportDirectory = () async => home;
|
await useAppPaths(home);
|
||||||
AppPath.temporaryDirectory = () async => home;
|
|
||||||
AppPath.cacheDirectory = () async => home;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/icons/icons.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/providers/app.dart';
|
import 'package:fl_clash/providers/app.dart';
|
||||||
import 'package:fl_clash/providers/config.dart';
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/providers/database.dart';
|
import 'package:fl_clash/providers/database.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/ui/icons/icons.dart';
|
||||||
import 'package:fl_clash/views/resources.dart';
|
import 'package:fl_clash/views/resources.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -16,6 +16,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path/path.dart' hide context;
|
import 'package:path/path.dart' hide context;
|
||||||
|
|
||||||
|
import '../helpers/app_paths.dart';
|
||||||
import '../helpers/context_menu.dart';
|
import '../helpers/context_menu.dart';
|
||||||
import '../helpers/glyph_finders.dart';
|
import '../helpers/glyph_finders.dart';
|
||||||
import '../helpers/test_app.dart';
|
import '../helpers/test_app.dart';
|
||||||
@@ -25,22 +26,13 @@ void main() {
|
|||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
late Directory home;
|
late Directory home;
|
||||||
final supportDirectory = AppPath.supportDirectory;
|
|
||||||
final temporaryDirectory = AppPath.temporaryDirectory;
|
|
||||||
final cacheDirectory = AppPath.cacheDirectory;
|
|
||||||
|
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
home = Directory.systemTemp.createTempSync('flclash-resources-');
|
home = Directory.systemTemp.createTempSync('flclash-resources-');
|
||||||
AppPath.supportDirectory = () async => home;
|
await useAppPaths(home);
|
||||||
AppPath.temporaryDirectory = () async => home;
|
|
||||||
AppPath.cacheDirectory = () async => home;
|
|
||||||
await appPath.homeDirPath;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDownAll(() {
|
tearDownAll(() {
|
||||||
AppPath.supportDirectory = supportDirectory;
|
|
||||||
AppPath.temporaryDirectory = temporaryDirectory;
|
|
||||||
AppPath.cacheDirectory = cacheDirectory;
|
|
||||||
if (home.existsSync()) home.deleteSync(recursive: true);
|
if (home.existsSync()) home.deleteSync(recursive: true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+280
@@ -0,0 +1,280 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
const _usage =
|
||||||
|
'''
|
||||||
|
Runs the root package tests from a few generated entrypoints.
|
||||||
|
|
||||||
|
usage: dart run tool/test.dart [path ...] [-- flutter test arguments]
|
||||||
|
|
||||||
|
Every *_test.dart under the given paths (default: test) is imported into one of
|
||||||
|
$_bundleCount entrypoints under $_bundleDir, so `flutter test` compiles and
|
||||||
|
starts $_bundleCount programs rather than one per file. A file carrying a
|
||||||
|
library-level annotation such as @TestOn runs on its own.
|
||||||
|
|
||||||
|
Files in one entrypoint share an isolate. A file that fails there is run again
|
||||||
|
on its own, and that run decides the result; the files that pass alone are
|
||||||
|
listed, since they depend on or are broken by state another file left. A
|
||||||
|
failure in a file that already ran on its own, or one the report ties to no
|
||||||
|
file, fails the run whatever the rerun finds.''';
|
||||||
|
|
||||||
|
// Fixed, so a commit groups its files the same way on every machine.
|
||||||
|
const _bundleCount = 32;
|
||||||
|
const _bundleDir = '.dart_tool/test_bundles';
|
||||||
|
const _reportPath = '$_bundleDir/report.json';
|
||||||
|
|
||||||
|
final _annotation = RegExp(r'^@(TestOn|Tags|Timeout|Skip|OnPlatform)\b');
|
||||||
|
|
||||||
|
// Options of `flutter test` the rerun drops: it only decides pass or fail, and
|
||||||
|
// would otherwise overwrite the coverage of the whole run.
|
||||||
|
// Sharding its few files anew would skip the test that failed.
|
||||||
|
const _rerunDropsWithValue = {
|
||||||
|
'--coverage-path',
|
||||||
|
'--coverage-package',
|
||||||
|
'--file-reporter',
|
||||||
|
'--total-shards',
|
||||||
|
'--shard-index',
|
||||||
|
};
|
||||||
|
const _rerunDropsFlag = {'--coverage', '--merge-coverage', '--branch-coverage'};
|
||||||
|
|
||||||
|
class _Bundle {
|
||||||
|
final files = <String>[];
|
||||||
|
var size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> main(List<String> arguments) async {
|
||||||
|
final split = arguments.indexOf('--');
|
||||||
|
final paths = split < 0 ? arguments : arguments.sublist(0, split);
|
||||||
|
final flutterArguments = split < 0
|
||||||
|
? const <String>[]
|
||||||
|
: arguments.sublist(split + 1);
|
||||||
|
if (paths.contains('-h') || paths.contains('--help')) {
|
||||||
|
stdout.writeln(_usage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final files = _collect(paths.isEmpty ? const ['test'] : paths);
|
||||||
|
final standalone = files.where(_hasAnnotation).toList();
|
||||||
|
final bundles = _writeBundles(
|
||||||
|
files.where((file) => !standalone.contains(file)).toList(),
|
||||||
|
);
|
||||||
|
final setsConcurrency = flutterArguments.any(
|
||||||
|
(argument) =>
|
||||||
|
argument.startsWith('-j') || argument.startsWith('--concurrency'),
|
||||||
|
);
|
||||||
|
final reports = !flutterArguments.any(
|
||||||
|
(argument) => argument.startsWith('--file-reporter'),
|
||||||
|
);
|
||||||
|
|
||||||
|
final code = await _flutterTest([
|
||||||
|
if (!setsConcurrency)
|
||||||
|
'--concurrency=${min(_bundleCount, Platform.numberOfProcessors)}',
|
||||||
|
if (reports) '--file-reporter=json:$_reportPath',
|
||||||
|
...flutterArguments,
|
||||||
|
for (final bundle in bundles) bundle.path,
|
||||||
|
...standalone,
|
||||||
|
]);
|
||||||
|
if (code == 0 || !reports) exit(code);
|
||||||
|
|
||||||
|
final failures = _failures(bundles);
|
||||||
|
final failed = failures.files;
|
||||||
|
if (failed.isEmpty) exit(code);
|
||||||
|
stdout.writeln(
|
||||||
|
'\n${failed.length} file(s) failed in a shared isolate; '
|
||||||
|
'running them on their own:\n${failed.map((f) => ' $f').join('\n')}\n',
|
||||||
|
);
|
||||||
|
final rerunCode = await _flutterTest([
|
||||||
|
..._rerunArguments(flutterArguments),
|
||||||
|
...failed,
|
||||||
|
]);
|
||||||
|
if (rerunCode == 0) {
|
||||||
|
stdout.writeln(
|
||||||
|
'\nEvery file above passes on its own: each depends on, or is broken by, '
|
||||||
|
'state another file in its entrypoint left behind.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (failures.failsOutsideBundles) {
|
||||||
|
stdout.writeln(
|
||||||
|
'\nThe run still fails: other failures came from files that ran on '
|
||||||
|
'their own, or from no file the report names.',
|
||||||
|
);
|
||||||
|
exit(code);
|
||||||
|
}
|
||||||
|
exit(rerunCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> _flutterTest(List<String> arguments) async {
|
||||||
|
final process = await Process.start(
|
||||||
|
'flutter',
|
||||||
|
['test', ...arguments],
|
||||||
|
mode: ProcessStartMode.inheritStdio,
|
||||||
|
runInShell: Platform.isWindows,
|
||||||
|
);
|
||||||
|
return process.exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _rerunArguments(List<String> arguments) {
|
||||||
|
final kept = <String>[];
|
||||||
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
|
final argument = arguments[i];
|
||||||
|
final name = argument.split('=').first;
|
||||||
|
if (_rerunDropsFlag.contains(name)) continue;
|
||||||
|
if (_rerunDropsWithValue.contains(name)) {
|
||||||
|
if (!argument.contains('=')) i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
kept.add(argument);
|
||||||
|
}
|
||||||
|
return kept;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _collect(List<String> paths) {
|
||||||
|
final files = <String>{};
|
||||||
|
for (final path in paths) {
|
||||||
|
switch (FileSystemEntity.typeSync(path)) {
|
||||||
|
case FileSystemEntityType.directory:
|
||||||
|
files.addAll(
|
||||||
|
Directory(path)
|
||||||
|
.listSync(recursive: true)
|
||||||
|
.whereType<File>()
|
||||||
|
.map((file) => p.normalize(file.path))
|
||||||
|
.where((file) => file.endsWith('_test.dart')),
|
||||||
|
);
|
||||||
|
case FileSystemEntityType.file:
|
||||||
|
files.add(p.normalize(path));
|
||||||
|
default:
|
||||||
|
stderr.writeln('No such test path: $path');
|
||||||
|
exit(64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (files.isEmpty) {
|
||||||
|
stderr.writeln('No *_test.dart files under ${paths.join(' ')}');
|
||||||
|
exit(64);
|
||||||
|
}
|
||||||
|
return files.toList()..sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasAnnotation(String file) {
|
||||||
|
for (final line in File(file).readAsLinesSync()) {
|
||||||
|
if (_annotation.hasMatch(line)) return true;
|
||||||
|
if (line.startsWith('import ') || line.startsWith('void main')) break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hands each file, largest first, to the bundle with the least code so far;
|
||||||
|
/// size stands in for run time.
|
||||||
|
List<({String path, List<String> files})> _writeBundles(List<String> files) {
|
||||||
|
final directory = Directory(_bundleDir);
|
||||||
|
if (directory.existsSync()) directory.deleteSync(recursive: true);
|
||||||
|
directory.createSync(recursive: true);
|
||||||
|
|
||||||
|
final sizes = {for (final file in files) file: File(file).lengthSync()};
|
||||||
|
final bundles = List.generate(
|
||||||
|
min(_bundleCount, files.length),
|
||||||
|
(_) => _Bundle(),
|
||||||
|
);
|
||||||
|
final bySize = [...files]
|
||||||
|
..sort((a, b) {
|
||||||
|
final order = sizes[b]!.compareTo(sizes[a]!);
|
||||||
|
return order != 0 ? order : a.compareTo(b);
|
||||||
|
});
|
||||||
|
for (final file in bySize) {
|
||||||
|
final bundle = bundles.reduce((a, b) => b.size < a.size ? b : a);
|
||||||
|
bundle.files.add(file);
|
||||||
|
bundle.size += sizes[file]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
for (final (index, bundle) in bundles.indexed)
|
||||||
|
(
|
||||||
|
path: _writeBundle(directory, index, bundle.files..sort()),
|
||||||
|
files: bundle.files,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
String _writeBundle(Directory directory, int index, List<String> files) {
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln('// Generated by tool/test.dart.')
|
||||||
|
..writeln("import 'package:flutter_test/flutter_test.dart';");
|
||||||
|
for (final (i, file) in files.indexed) {
|
||||||
|
buffer.writeln(
|
||||||
|
"import '${_posix(p.relative(file, from: directory.path))}' as t$i;",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
buffer
|
||||||
|
..writeln()
|
||||||
|
..writeln('void main() {');
|
||||||
|
for (final (i, file) in files.indexed) {
|
||||||
|
buffer.writeln(" group('${_posix(file)}', t$i.main);");
|
||||||
|
}
|
||||||
|
buffer.writeln('}');
|
||||||
|
|
||||||
|
final name = 'bundle_${index.toString().padLeft(2, '0')}_test.dart';
|
||||||
|
final path = p.join(directory.path, name);
|
||||||
|
File(path).writeAsStringSync(buffer.toString());
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _posix(String path) => p.posix.joinAll(p.split(path));
|
||||||
|
|
||||||
|
/// The files behind every failed test in the JSON report; a bundle that failed
|
||||||
|
/// to load stands for all of its files.
|
||||||
|
({List<String> files, bool failsOutsideBundles}) _failures(
|
||||||
|
List<({String path, List<String> files})> bundles,
|
||||||
|
) {
|
||||||
|
final report = File(_reportPath);
|
||||||
|
if (!report.existsSync()) {
|
||||||
|
return (files: const [], failsOutsideBundles: false);
|
||||||
|
}
|
||||||
|
final byPath = {
|
||||||
|
for (final bundle in bundles) p.canonicalize(bundle.path): bundle.files,
|
||||||
|
};
|
||||||
|
final suites = <int, String>{};
|
||||||
|
final tests = <int, ({int suite, String name})>{};
|
||||||
|
final passed = <int>{};
|
||||||
|
final failed = <String>{};
|
||||||
|
var failsOutsideBundles = false;
|
||||||
|
void fail(int id) {
|
||||||
|
final test = tests[id];
|
||||||
|
final suite = test == null ? null : suites[test.suite];
|
||||||
|
final files = suite == null ? null : byPath[p.canonicalize(suite)];
|
||||||
|
final owners = test == null || files == null
|
||||||
|
? const <String>[]
|
||||||
|
: test.name.startsWith('loading ')
|
||||||
|
? files
|
||||||
|
: files.where((file) => test.name.startsWith('${_posix(file)} '));
|
||||||
|
if (owners.isEmpty) failsOutsideBundles = true;
|
||||||
|
failed.addAll(owners);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final line in report.readAsLinesSync()) {
|
||||||
|
if (!line.startsWith('{')) continue;
|
||||||
|
final event = jsonDecode(line) as Map<String, dynamic>;
|
||||||
|
switch (event['type']) {
|
||||||
|
case 'suite':
|
||||||
|
final suite = event['suite'] as Map<String, dynamic>;
|
||||||
|
suites[suite['id'] as int] = suite['path'] as String;
|
||||||
|
case 'testStart':
|
||||||
|
final test = event['test'] as Map<String, dynamic>;
|
||||||
|
tests[test['id'] as int] = (
|
||||||
|
suite: test['suiteID'] as int,
|
||||||
|
name: test['name'] as String,
|
||||||
|
);
|
||||||
|
case 'error' when passed.contains(event['testID']):
|
||||||
|
fail(event['testID'] as int);
|
||||||
|
case 'testDone' when event['result'] == 'success':
|
||||||
|
passed.add(event['testID'] as int);
|
||||||
|
case 'testDone':
|
||||||
|
fail(event['testID'] as int);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
files: failed.toList()..sort(),
|
||||||
|
failsOutsideBundles: failsOutsideBundles,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user