Add Worker service with isolated task management and integrate into app
This commit is contained in:
158
finlog_app/fluttery/test/system/worker/worker_impl_test.dart
Normal file
158
finlog_app/fluttery/test/system/worker/worker_impl_test.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
// test/system/worker/worker_impl_test.dart
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fluttery/fluttery.dart';
|
||||
import 'package:fluttery/preferences.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:fluttery/src/system/worker/worker_impl.dart';
|
||||
import 'package:fluttery/worker.dart';
|
||||
|
||||
Future<void> pumpMicro([int times = 10]) => pumpEventQueue(times: times);
|
||||
|
||||
Future<void> waitFor(
|
||||
bool Function() predicate, {
|
||||
Duration timeout = const Duration(seconds: 2),
|
||||
Duration step = const Duration(milliseconds: 20),
|
||||
}) async {
|
||||
final deadline = DateTime.now().add(timeout);
|
||||
while (DateTime.now().isBefore(deadline)) {
|
||||
if (predicate()) return;
|
||||
await Future<void>.delayed(step);
|
||||
}
|
||||
fail('Condition not met within $timeout');
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
expect(ServicesBinding.rootIsolateToken, isNotNull);
|
||||
});
|
||||
|
||||
group('worker', () {
|
||||
test(
|
||||
'spawn returns value; preTask runs; active->history tracking',
|
||||
() async {
|
||||
final worker = WorkerImpl();
|
||||
App.service<Preferences>().setBool("test", false);
|
||||
|
||||
var preCalled = false;
|
||||
|
||||
final fut = worker.spawn<int>(
|
||||
'ok',
|
||||
() async {
|
||||
await Future.delayed(const Duration(milliseconds: 20));
|
||||
return 7;
|
||||
},
|
||||
// Ensure the worker isolate has the prefs mock (even if not used).
|
||||
preTask: () {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
preCalled = true;
|
||||
},
|
||||
);
|
||||
|
||||
// Shortly after spawn there should be one active job.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
expect(worker.getActiveWorkers().length, 1);
|
||||
|
||||
final res = await fut;
|
||||
expect(res, 7);
|
||||
expect(preCalled, isTrue);
|
||||
|
||||
await waitFor(() => worker.getActiveWorkers().isEmpty);
|
||||
await waitFor(() => worker.getAllWorkers().isNotEmpty);
|
||||
final all = worker.getAllWorkers();
|
||||
expect(all.first.status, WorkerStatus.completed);
|
||||
expect(all.first.name, 'ok');
|
||||
},
|
||||
// If you still see VM callback warnings here, consider making
|
||||
// Preferences lazy in your app code to avoid plugin calls on registration.
|
||||
// skip: true,
|
||||
);
|
||||
|
||||
test('timeout marks job as timedOut and throws TimeoutException', () async {
|
||||
final worker = WorkerImpl(
|
||||
defaultTimeout: const Duration(milliseconds: 50),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
worker.spawn<void>(
|
||||
'timeout',
|
||||
// Long task so the wrapper .timeout triggers
|
||||
() async => Future.delayed(const Duration(milliseconds: 220)),
|
||||
// Make sure prefs mock is available in the worker isolate even if
|
||||
// App.registerDefaultServices touches SharedPreferences.
|
||||
preTask: () =>
|
||||
SharedPreferences.setMockInitialValues({}),
|
||||
),
|
||||
throwsA(isA<TimeoutException>()),
|
||||
);
|
||||
|
||||
// Wait until the worker updates history in its catchError path
|
||||
await waitFor(() => worker.getAllWorkers().isNotEmpty);
|
||||
final all = worker.getAllWorkers();
|
||||
expect(all.first.status, WorkerStatus.timedOut);
|
||||
expect(all.first.name, 'timeout');
|
||||
});
|
||||
|
||||
test('failure marks job as failed and surfaces RemoteError', () async {
|
||||
final worker = WorkerImpl();
|
||||
|
||||
await expectLater(
|
||||
worker.spawn<void>(
|
||||
'fail',
|
||||
() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
throw StateError('boom');
|
||||
},
|
||||
// Ensure plugin mocks exist if defaults touch plugins
|
||||
preTask: () =>
|
||||
SharedPreferences.setMockInitialValues({}),
|
||||
),
|
||||
// Isolate.run returns a RemoteError to the caller isolate
|
||||
throwsA(isA<RemoteError>()),
|
||||
);
|
||||
|
||||
await waitFor(() => worker.getAllWorkers().isNotEmpty);
|
||||
final all = worker.getAllWorkers();
|
||||
expect(all.first.status, WorkerStatus.failed);
|
||||
expect(all.first.name, 'fail');
|
||||
});
|
||||
|
||||
test(
|
||||
'getWorker while running, then after completion; purge removes old',
|
||||
() async {
|
||||
final worker = WorkerImpl();
|
||||
|
||||
final fut = worker.spawn<void>(
|
||||
'long',
|
||||
() async => Future.delayed(const Duration(milliseconds: 160)),
|
||||
preTask: () =>
|
||||
SharedPreferences.setMockInitialValues({}),
|
||||
);
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 25));
|
||||
final active = worker.getActiveWorkers();
|
||||
expect(active.length, 1);
|
||||
final id = active.first.id;
|
||||
expect(worker.getWorker(id)?.status, WorkerStatus.running);
|
||||
|
||||
await fut;
|
||||
await waitFor(
|
||||
() => worker.getWorker(id)?.status == WorkerStatus.completed,
|
||||
);
|
||||
|
||||
expect(worker.getAllWorkers().length, 1);
|
||||
|
||||
worker.purge(maxAge: Duration.zero);
|
||||
await pumpMicro();
|
||||
expect(worker.getAllWorkers(), isEmpty);
|
||||
},
|
||||
// skip: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user