Merge branch 'dev' into 'production'
Basic framework See merge request rhein-software/finlog/finlog!5
This commit was merged in pull request #8.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,3 +5,5 @@ backend/**/target
|
||||
|
||||
finlog_app/**/.idea
|
||||
finlog_app/**/.dart_tool
|
||||
|
||||
finlog_app/**/pubspec.lock
|
||||
@@ -7,5 +7,7 @@ build_backend:
|
||||
artifacts:
|
||||
paths:
|
||||
- backend/common/target/
|
||||
- backend/server/target
|
||||
- backend/discovery/target/
|
||||
- backend/gateway/target/
|
||||
- backend/server/target/
|
||||
expire_in: 1 hour
|
||||
|
||||
13
backend/discovery/pom.xml
Normal file
13
backend/discovery/pom.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>dev.rheinsw.finlog.backend</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>discovery</artifactId>
|
||||
</project>
|
||||
30
backend/gateway/pom.xml
Normal file
30
backend/gateway/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>dev.rheinsw.finlog.backend</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>gateway</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- common -->
|
||||
<dependency>
|
||||
<groupId>dev.rheinsw.finlog.backend</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
<version>${revision}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -12,6 +12,8 @@
|
||||
<modules>
|
||||
<module>common</module>
|
||||
<module>server</module>
|
||||
<module>gateway</module>
|
||||
<module>discovery</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttery/fluttery.dart';
|
||||
import 'package:fluttery/logger.dart';
|
||||
import 'package:fluttery/worker.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
// Ensures that the Flutter engine and widget binding
|
||||
// are initialized before using async services or plugins
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// any services
|
||||
App.registerDefaultServices();
|
||||
|
||||
final logger = App.service<Logger>();
|
||||
logger.debug("[MAIN] Registered all default services");
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
@@ -10,24 +23,11 @@ class MyApp extends StatelessWidget {
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
App.service<Logger>().info("test");
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
@@ -38,15 +38,6 @@ class MyApp extends StatelessWidget {
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
@@ -57,51 +48,27 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
App.service<Worker>().spawn("worker-$_counter", () async {
|
||||
App.service<Logger>().info("test");
|
||||
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
|
||||
App.service<Logger>().info("end worker");
|
||||
});
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text('You have pushed the button this many times:'),
|
||||
@@ -109,6 +76,11 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
},
|
||||
child: Text("Print workers"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -31,6 +31,8 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
fluttery:
|
||||
path: ../fluttery
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
||||
@@ -1,30 +1,7 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:app/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
test('description', () {
|
||||
expect(1, 1);
|
||||
});
|
||||
}
|
||||
|
||||
27
finlog_app/fluttery/lib/environment.dart
Normal file
27
finlog_app/fluttery/lib/environment.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:fluttery/fluttery.dart';
|
||||
|
||||
/// Abstract Environment contract
|
||||
abstract class Environment extends Service {
|
||||
/// Platform checks
|
||||
bool get isAndroid;
|
||||
|
||||
bool get isIOS;
|
||||
|
||||
/// Build mode
|
||||
bool get isDebug;
|
||||
|
||||
bool get isRelease;
|
||||
|
||||
bool get isProfile;
|
||||
|
||||
/// App info
|
||||
Future<void> loadPackageInfo();
|
||||
|
||||
String get appName;
|
||||
|
||||
String get packageName;
|
||||
|
||||
String get version;
|
||||
|
||||
String get buildNumber;
|
||||
}
|
||||
@@ -1,5 +1,73 @@
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
library;
|
||||
|
||||
import 'package:fluttery/environment.dart';
|
||||
import 'package:fluttery/logger.dart';
|
||||
import 'package:fluttery/preferences.dart';
|
||||
import 'package:fluttery/secure_storage.dart';
|
||||
import 'package:fluttery/src/logger/logger_impl.dart';
|
||||
import 'package:fluttery/src/preferences/preferences_impl.dart';
|
||||
import 'package:fluttery/src/storage/secure/secure_storage_impl.dart';
|
||||
import 'package:fluttery/src/system/environment/environment_impl.dart';
|
||||
import 'package:fluttery/src/system/worker/worker_impl.dart';
|
||||
import 'package:fluttery/worker.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
|
||||
/// A class to manage services.
|
||||
class App {
|
||||
static final _AppService _appService = _AppService();
|
||||
|
||||
/// Registers a service with a factory function to instantiate the implementation.
|
||||
///
|
||||
/// This ensures that the implementation is created when the service is requested.
|
||||
///
|
||||
/// `implFactory` - A factory method to create the service implementation.
|
||||
static void registerService<T extends Service>(T Function() implFactory) {
|
||||
_appService.registerSingleton<T>(implFactory);
|
||||
}
|
||||
|
||||
/// Retrieves the registered service.
|
||||
///
|
||||
/// Returns an instance of the registered service.
|
||||
static T service<T extends Service>() {
|
||||
return _appService.resolve<T>();
|
||||
}
|
||||
|
||||
/// Registers the default services required by the application.
|
||||
static void registerDefaultServices() {
|
||||
registerService<Logger>(() => LoggerImpl());
|
||||
registerService<Preferences>(() => PreferencesImpl());
|
||||
registerService<Environment>(() => EnvironmentImpl());
|
||||
registerService<SecureStorage>(() => SecureStorageImpl());
|
||||
registerService<Worker>(() => WorkerImpl());
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract class to represent a service.
|
||||
abstract class Service {}
|
||||
|
||||
/// Internal class to manage the registration and resolution of services.
|
||||
class _AppService {
|
||||
static _AppService? _singleton;
|
||||
|
||||
static final KiwiContainer _kiwi = KiwiContainer();
|
||||
|
||||
/// Factory constructor to ensure singleton instance of _AppService.
|
||||
factory _AppService() => _singleton ??= _AppService._();
|
||||
|
||||
/// Private constructor.
|
||||
_AppService._();
|
||||
|
||||
/// Registers a singleton service with a factory function to create the instance.
|
||||
///
|
||||
/// `serviceFactory` - A factory method to create the service implementation.
|
||||
void registerSingleton<T extends Service>(T Function() serviceFactory) {
|
||||
_kiwi.registerSingleton<T>((c) => serviceFactory());
|
||||
}
|
||||
|
||||
/// Resolves and retrieves the registered service.
|
||||
///
|
||||
/// Returns an instance of the registered service.
|
||||
T resolve<T extends Service>() {
|
||||
return _kiwi.resolve<T>();
|
||||
}
|
||||
}
|
||||
|
||||
33
finlog_app/fluttery/lib/logger.dart
Normal file
33
finlog_app/fluttery/lib/logger.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:fluttery/fluttery.dart';
|
||||
import 'package:logging/logging.dart' as lib;
|
||||
|
||||
/// Abstract class for logging service.
|
||||
/// Provides methods for different log levels and configuration.
|
||||
abstract class Logger extends Service {
|
||||
/// Logs an informational message.
|
||||
///
|
||||
/// [message] is the information to log.
|
||||
void info(String message);
|
||||
|
||||
/// Logs a warning message.
|
||||
///
|
||||
/// [message] is the warning to log.
|
||||
void warning(String message);
|
||||
|
||||
/// Logs an error message with optional error and stack trace.
|
||||
///
|
||||
/// [message] is the error message to log.
|
||||
/// [error] is the optional error object associated with this log entry.
|
||||
/// [stackTrace] is the optional stack trace associated with this log entry.
|
||||
void error(String message, [Object? error, StackTrace? stackTrace]);
|
||||
|
||||
/// Logs a debug message.
|
||||
///
|
||||
/// [message] is the debug message to log.
|
||||
void debug(String message);
|
||||
|
||||
/// Sets the log level for the logger.
|
||||
///
|
||||
/// [level] is the new log level to set.
|
||||
void setLogLevel(lib.Level level);
|
||||
}
|
||||
40
finlog_app/fluttery/lib/preferences.dart
Normal file
40
finlog_app/fluttery/lib/preferences.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:fluttery/fluttery.dart';
|
||||
|
||||
/// providing methods for managing persistent storage of key-value pairs.
|
||||
abstract class Preferences implements Service {
|
||||
/// Stores a string value with the given [key].
|
||||
Future<void> setString(String key, String value);
|
||||
|
||||
/// Retrieves the string value associated with the given [key].
|
||||
Future<String?> getString(String key);
|
||||
|
||||
/// Stores an integer value with the given [key].
|
||||
Future<void> setInt(String key, int value);
|
||||
|
||||
/// Retrieves the integer value associated with the given [key].
|
||||
Future<int?> getInt(String key);
|
||||
|
||||
/// Stores a boolean value with the given [key].
|
||||
Future<void> setBool(String key, bool value);
|
||||
|
||||
/// Retrieves the boolean value associated with the given [key].
|
||||
Future<bool?> getBool(String key);
|
||||
|
||||
/// Stores a double value with the given [key].
|
||||
Future<void> setDouble(String key, double value);
|
||||
|
||||
/// Retrieves the double value associated with the given [key].
|
||||
Future<double?> getDouble(String key);
|
||||
|
||||
/// Stores a list of strings with the given [key].
|
||||
Future<void> setStringList(String key, List<String> value);
|
||||
|
||||
/// Retrieves the list of strings associated with the given [key].
|
||||
Future<List<String>?> getStringList(String key);
|
||||
|
||||
/// Removes the key-value pair associated with the given [key].
|
||||
Future<void> remove(String key);
|
||||
|
||||
/// Clears all key-value pairs in the preferences.
|
||||
Future<void> clear();
|
||||
}
|
||||
75
finlog_app/fluttery/lib/secure_storage.dart
Normal file
75
finlog_app/fluttery/lib/secure_storage.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:fluttery/fluttery.dart';
|
||||
|
||||
/// Interface for secure storage operations.
|
||||
///
|
||||
/// Provides methods for securely storing and retrieving sensitive data
|
||||
/// like passwords, tokens, API keys, etc. Data stored through this interface
|
||||
/// is encrypted and stored in the device's secure storage (Keychain on iOS,
|
||||
/// Keystore on Android).
|
||||
abstract class SecureStorage implements Service {
|
||||
/// Stores a string value securely with the given [key].
|
||||
///
|
||||
/// Returns a Future that completes when the value is successfully stored.
|
||||
/// Throws an exception if the storage operation fails.
|
||||
Future<void> write(String key, String value);
|
||||
|
||||
/// Retrieves the securely stored string value for the given [key].
|
||||
///
|
||||
/// Returns the stored value if found, null otherwise.
|
||||
/// Throws an exception if the retrieval operation fails.
|
||||
Future<String?> read(String key);
|
||||
|
||||
/// Stores an integer value securely with the given [key].
|
||||
///
|
||||
/// The integer is converted to a string for storage.
|
||||
Future<void> writeInt(String key, int value);
|
||||
|
||||
/// Retrieves the securely stored integer value for the given [key].
|
||||
///
|
||||
/// Returns the stored integer if found and valid, null otherwise.
|
||||
Future<int?> readInt(String key);
|
||||
|
||||
/// Stores a boolean value securely with the given [key].
|
||||
///
|
||||
/// The boolean is converted to a string for storage.
|
||||
Future<void> writeBool(String key, bool value);
|
||||
|
||||
/// Retrieves the securely stored boolean value for the given [key].
|
||||
///
|
||||
/// Returns the stored boolean if found and valid, null otherwise.
|
||||
Future<bool?> readBool(String key);
|
||||
|
||||
/// Stores a double value securely with the given [key].
|
||||
///
|
||||
/// The double is converted to a string for storage.
|
||||
Future<void> writeDouble(String key, double value);
|
||||
|
||||
/// Retrieves the securely stored double value for the given [key].
|
||||
///
|
||||
/// Returns the stored double if found and valid, null otherwise.
|
||||
Future<double?> readDouble(String key);
|
||||
|
||||
/// Removes the securely stored value for the given [key].
|
||||
///
|
||||
/// Returns a Future that completes when the value is successfully removed.
|
||||
Future<void> delete(String key);
|
||||
|
||||
/// Removes all securely stored values.
|
||||
///
|
||||
/// Returns a Future that completes when all values are successfully removed.
|
||||
/// Use with caution as this operation cannot be undone.
|
||||
Future<void> deleteAll();
|
||||
|
||||
/// Returns all keys incl. values in secure storage
|
||||
Future<Set<String>> readAll();
|
||||
|
||||
/// Returns all keys currently stored in secure storage.
|
||||
///
|
||||
/// Useful for debugging or migration purposes.
|
||||
Future<Set<String>> readAllKeys();
|
||||
|
||||
/// Checks if a value exists for the given [key].
|
||||
///
|
||||
/// Returns true if a value exists, false otherwise.
|
||||
Future<bool> containsKey(String key);
|
||||
}
|
||||
57
finlog_app/fluttery/lib/src/logger/logger_impl.dart
Normal file
57
finlog_app/fluttery/lib/src/logger/logger_impl.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttery/logger.dart';
|
||||
import 'package:logging/logging.dart' as lib;
|
||||
|
||||
// ignore_for_file: avoid_print
|
||||
class LoggerImpl implements Logger {
|
||||
final lib.Logger _logger;
|
||||
|
||||
// coverage:ignore-start
|
||||
/// Constructor
|
||||
LoggerImpl() : _logger = lib.Logger("Logger") {
|
||||
_logger.onRecord.listen((lib.LogRecord record) {
|
||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
||||
if (record.error != null) {
|
||||
print('Error: ${record.error}');
|
||||
}
|
||||
if (record.stackTrace != null) {
|
||||
print('Stack Trace: ${record.stackTrace}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// coverage:ignore-end
|
||||
@visibleForTesting
|
||||
factory LoggerImpl.forTest(lib.Logger logger) {
|
||||
final instance = LoggerImpl._internal(logger);
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Private internal constructor
|
||||
LoggerImpl._internal(this._logger);
|
||||
|
||||
@override
|
||||
void info(String message) {
|
||||
_logger.info(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void warning(String message) {
|
||||
_logger.warning(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void error(String message, [Object? error, StackTrace? stackTrace]) {
|
||||
_logger.severe(message, error, stackTrace);
|
||||
}
|
||||
|
||||
@override
|
||||
void debug(String message) {
|
||||
_logger.fine(message);
|
||||
}
|
||||
|
||||
@override
|
||||
void setLogLevel(lib.Level level) {
|
||||
_logger.level = level;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:fluttery/preferences.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class PreferencesImpl implements Preferences {
|
||||
late final SharedPreferences _prefs;
|
||||
bool _initialized = false;
|
||||
|
||||
Future<void> _ensureInitialized() async {
|
||||
if (!_initialized) {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setString(String key, String value) async {
|
||||
await _ensureInitialized();
|
||||
await _prefs.setString(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getString(String key) async {
|
||||
await _ensureInitialized();
|
||||
return _prefs.getString(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setInt(String key, int value) async {
|
||||
await _ensureInitialized();
|
||||
await _prefs.setInt(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> getInt(String key) async {
|
||||
await _ensureInitialized();
|
||||
return _prefs.getInt(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBool(String key, bool value) async {
|
||||
await _ensureInitialized();
|
||||
await _prefs.setBool(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool?> getBool(String key) async {
|
||||
await _ensureInitialized();
|
||||
return _prefs.getBool(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setDouble(String key, double value) async {
|
||||
await _ensureInitialized();
|
||||
await _prefs.setDouble(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double?> getDouble(String key) async {
|
||||
await _ensureInitialized();
|
||||
return _prefs.getDouble(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setStringList(String key, List<String> value) async {
|
||||
await _ensureInitialized();
|
||||
await _prefs.setStringList(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>?> getStringList(String key) async {
|
||||
await _ensureInitialized();
|
||||
return _prefs.getStringList(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) async {
|
||||
await _ensureInitialized();
|
||||
await _prefs.remove(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
await _ensureInitialized();
|
||||
await _prefs.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:fluttery/secure_storage.dart';
|
||||
|
||||
class SecureStorageImpl implements SecureStorage {
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
|
||||
/// Constructor - creates a single instance with default FlutterSecureStorage
|
||||
SecureStorageImpl() : _secureStorage = const FlutterSecureStorage();
|
||||
|
||||
/// Testing constructor
|
||||
@visibleForTesting
|
||||
SecureStorageImpl.forTesting({required FlutterSecureStorage instance})
|
||||
: _secureStorage = instance;
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value) async {
|
||||
await _secureStorage.write(key: key, value: value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> read(String key) async {
|
||||
return await _secureStorage.read(key: key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> containsKey(String key) async {
|
||||
return await _secureStorage.containsKey(key: key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String key) async {
|
||||
await _secureStorage.delete(key: key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAll() async {
|
||||
await _secureStorage.deleteAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<String>> readAll() async {
|
||||
final allData = await _secureStorage.readAll();
|
||||
return allData.keys.toSet();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<String>> readAllKeys() async {
|
||||
final allData = await _secureStorage.readAll();
|
||||
return allData.keys.toSet();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeInt(String key, int value) async {
|
||||
await _secureStorage.write(key: key, value: value.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> readInt(String key) async {
|
||||
final value = await _secureStorage.read(key: key);
|
||||
if (value == null) return null;
|
||||
return int.tryParse(value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeBool(String key, bool value) async {
|
||||
await _secureStorage.write(key: key, value: value.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool?> readBool(String key) async {
|
||||
final value = await _secureStorage.read(key: key);
|
||||
if (value == null) return null;
|
||||
if (value.toLowerCase() == 'true') return true;
|
||||
if (value.toLowerCase() == 'false') return false;
|
||||
return null; // Invalid boolean value
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeDouble(String key, double value) async {
|
||||
await _secureStorage.write(key: key, value: value.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double?> readDouble(String key) async {
|
||||
final value = await _secureStorage.read(key: key);
|
||||
if (value == null) return null;
|
||||
return double.tryParse(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter/foundation.dart'
|
||||
show kDebugMode, kReleaseMode, kProfileMode;
|
||||
|
||||
import 'package:fluttery/environment.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class EnvironmentImpl implements Environment {
|
||||
PackageInfo? _packageInfo;
|
||||
|
||||
@override
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
@override
|
||||
bool get isIOS => Platform.isIOS;
|
||||
|
||||
@override
|
||||
Future<void> loadPackageInfo() async {
|
||||
_packageInfo = await PackageInfo.fromPlatform();
|
||||
}
|
||||
|
||||
@override
|
||||
String get appName => _packageInfo?.appName ?? 'Unknown';
|
||||
|
||||
@override
|
||||
String get packageName => _packageInfo?.packageName ?? 'Unknown';
|
||||
|
||||
@override
|
||||
String get version => _packageInfo?.version ?? '0.0.0';
|
||||
|
||||
@override
|
||||
String get buildNumber => _packageInfo?.buildNumber ?? '0';
|
||||
|
||||
@override
|
||||
bool get isDebug => kDebugMode;
|
||||
|
||||
@override
|
||||
bool get isRelease => kReleaseMode;
|
||||
|
||||
@override
|
||||
bool get isProfile => kProfileMode;
|
||||
}
|
||||
189
finlog_app/fluttery/lib/src/system/worker/worker_impl.dart
Normal file
189
finlog_app/fluttery/lib/src/system/worker/worker_impl.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'package:flutter/services.dart'
|
||||
show ServicesBinding, RootIsolateToken, BackgroundIsolateBinaryMessenger;
|
||||
import 'package:fluttery/fluttery.dart';
|
||||
import 'package:fluttery/logger.dart';
|
||||
import 'package:fluttery/worker.dart';
|
||||
|
||||
class WorkerImpl implements Worker {
|
||||
final Logger _logger;
|
||||
|
||||
WorkerImpl({
|
||||
this.defaultTimeout,
|
||||
this.maxHistory = 100,
|
||||
RootIsolateToken? rootToken,
|
||||
}) : _rootToken = rootToken ?? ServicesBinding.rootIsolateToken,
|
||||
_logger = App.service<Logger>();
|
||||
|
||||
final Duration? defaultTimeout;
|
||||
final int maxHistory;
|
||||
|
||||
// Captured from the root isolate (may be null in some test envs)
|
||||
final RootIsolateToken? _rootToken;
|
||||
|
||||
final Map<String, WorkerInfo> _active = {};
|
||||
final List<WorkerInfo> _history = [];
|
||||
|
||||
@override
|
||||
Future<T> spawn<T>(
|
||||
String debugName,
|
||||
FutureOr<T> Function() task, {
|
||||
void Function()? preTask,
|
||||
Duration? timeout,
|
||||
}) {
|
||||
final id = _generateWorkerId();
|
||||
final started = DateTime.now();
|
||||
|
||||
_logger.debug('Spawning worker "$debugName" ($id)');
|
||||
_registerActiveWorker(id, debugName, started);
|
||||
|
||||
final future = _executeWithTimeout(
|
||||
id,
|
||||
debugName,
|
||||
task,
|
||||
preTask,
|
||||
timeout ?? defaultTimeout,
|
||||
);
|
||||
|
||||
_attachCompletionHandlers(id, debugName, future);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
String _generateWorkerId() {
|
||||
return 'iso-${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
|
||||
void _registerActiveWorker(String id, String debugName, DateTime started) {
|
||||
_active[id] = WorkerInfo(
|
||||
id: id,
|
||||
name: debugName,
|
||||
startedAt: started,
|
||||
status: WorkerStatus.running,
|
||||
);
|
||||
_logger.debug('Registered worker "$debugName" ($id)');
|
||||
}
|
||||
|
||||
Future<T> _executeWithTimeout<T>(
|
||||
String id,
|
||||
String debugName,
|
||||
FutureOr<T> Function() task,
|
||||
void Function()? preTask,
|
||||
Duration? timeout,
|
||||
) {
|
||||
_logger.debug(
|
||||
'Executing worker "$debugName" ($id) with timeout: ${timeout?.inSeconds ?? "none"} seconds',
|
||||
);
|
||||
final future = _executeInIsolate(debugName, task, preTask);
|
||||
|
||||
return timeout == null ? future : future.timeout(timeout);
|
||||
}
|
||||
|
||||
Future<T> _executeInIsolate<T>(
|
||||
String debugName,
|
||||
FutureOr<T> Function() task,
|
||||
void Function()? preTask,
|
||||
) {
|
||||
final token = _rootToken; // captured into closure
|
||||
_logger.debug('Starting isolate for worker "$debugName"');
|
||||
|
||||
return Isolate.run<T>(() async {
|
||||
// Initialize platform channels for this background isolate.
|
||||
if (token != null) {
|
||||
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||
}
|
||||
// Now it's safe to touch plugins (e.g., SharedPreferences).
|
||||
App.registerDefaultServices();
|
||||
if (preTask != null) {
|
||||
_logger.debug('Executing pre-task for worker "$debugName"');
|
||||
preTask();
|
||||
}
|
||||
return await Future.sync(task);
|
||||
}, debugName: debugName);
|
||||
}
|
||||
|
||||
void _attachCompletionHandlers<T>(
|
||||
String id,
|
||||
String debugName,
|
||||
Future<T> future,
|
||||
) {
|
||||
future
|
||||
.then((_) {
|
||||
_logger.debug('Worker "$debugName" ($id) completed successfully');
|
||||
_finish(id, status: WorkerStatus.completed);
|
||||
})
|
||||
.catchError((e, st) {
|
||||
final status = e is TimeoutException
|
||||
? WorkerStatus.timedOut
|
||||
: WorkerStatus.failed;
|
||||
|
||||
_finish(id, status: status, error: e, stack: st);
|
||||
_logWorkerError(debugName, id, e, st);
|
||||
});
|
||||
}
|
||||
|
||||
void _logWorkerError(
|
||||
String debugName,
|
||||
String id,
|
||||
Object error,
|
||||
StackTrace stackTrace,
|
||||
) {
|
||||
// Best-effort logging
|
||||
try {
|
||||
App.service<Logger>().error(
|
||||
'Worker job "$debugName" ($id) failed: $error',
|
||||
stackTrace,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _finish(
|
||||
String id, {
|
||||
required WorkerStatus status,
|
||||
Object? error,
|
||||
StackTrace? stack,
|
||||
}) {
|
||||
final prev = _active.remove(id);
|
||||
final endedAt = DateTime.now();
|
||||
final info = WorkerInfo(
|
||||
id: prev?.id ?? id,
|
||||
name: prev?.name ?? 'unknown',
|
||||
startedAt: prev?.startedAt ?? endedAt,
|
||||
status: status,
|
||||
endedAt: endedAt,
|
||||
error: error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
_history.insert(0, info);
|
||||
if (_history.length > maxHistory) {
|
||||
_history.removeRange(maxHistory, _history.length);
|
||||
}
|
||||
_logger.debug('Worker "${prev?.name}" ($id) finished with status: $status');
|
||||
}
|
||||
|
||||
@override
|
||||
List<WorkerInfo> getActiveWorkers() =>
|
||||
_active.values.toList()
|
||||
..sort((a, b) => a.startedAt.compareTo(b.startedAt));
|
||||
|
||||
@override
|
||||
List<WorkerInfo> getAllWorkers() => [...getActiveWorkers(), ..._history];
|
||||
|
||||
@override
|
||||
WorkerInfo? getWorker(String id) {
|
||||
final active = _active[id];
|
||||
if (active != null) return active;
|
||||
for (final w in _history) {
|
||||
if (w.id == id) return w;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void purge({Duration maxAge = const Duration(minutes: 30)}) {
|
||||
final cutoff = DateTime.now().subtract(maxAge);
|
||||
_logger.debug('Purging workers older than $maxAge');
|
||||
_history.removeWhere((w) => (w.endedAt ?? w.startedAt).isBefore(cutoff));
|
||||
}
|
||||
}
|
||||
47
finlog_app/fluttery/lib/worker.dart
Normal file
47
finlog_app/fluttery/lib/worker.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'dart:async';
|
||||
import 'package:fluttery/fluttery.dart';
|
||||
|
||||
abstract class Worker extends Service {
|
||||
Future<T> spawn<T>(
|
||||
String debugName,
|
||||
FutureOr<T> Function() task, {
|
||||
void Function()? preTask,
|
||||
Duration? timeout, // per-job override
|
||||
});
|
||||
|
||||
/// Currently running jobs.
|
||||
List<WorkerInfo> getActiveWorkers();
|
||||
|
||||
/// All known jobs (active + completed + failed), up to a capped history.
|
||||
List<WorkerInfo> getAllWorkers();
|
||||
|
||||
/// Optional: get a single worker by id.
|
||||
WorkerInfo? getWorker(String id);
|
||||
|
||||
/// Remove completed/failed jobs older than [maxAge] from history.
|
||||
void purge({Duration maxAge = const Duration(minutes: 30)});
|
||||
}
|
||||
|
||||
enum WorkerStatus { running, completed, failed, timedOut }
|
||||
|
||||
class WorkerInfo {
|
||||
WorkerInfo({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.startedAt,
|
||||
required this.status,
|
||||
this.endedAt,
|
||||
this.error,
|
||||
this.stackTrace,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final DateTime startedAt;
|
||||
final WorkerStatus status;
|
||||
final DateTime? endedAt;
|
||||
final Object? error;
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
Duration get duration => ((endedAt ?? DateTime.now()).difference(startedAt));
|
||||
}
|
||||
@@ -11,6 +11,12 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_secure_storage: ^9.2.4
|
||||
kiwi: ^5.0.1
|
||||
logging: ^1.3.0
|
||||
mocktail: ^1.0.4
|
||||
package_info_plus: ^9.0.0
|
||||
shared_preferences: ^2.5.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:fluttery/fluttery.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fluttery/logger.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
|
||||
import 'mocks/mocks.dart';
|
||||
|
||||
void main() {
|
||||
test('adds one to input values', () {
|
||||
final calculator = Calculator();
|
||||
expect(calculator.addOne(2), 3);
|
||||
expect(calculator.addOne(-7), -6);
|
||||
expect(calculator.addOne(0), 1);
|
||||
group('App Service Tests', () {
|
||||
// Clear the singleton state before each test to ensure isolation
|
||||
setUp(() {
|
||||
// KiwiContainer provides a clear method to remove all registered services
|
||||
KiwiContainer().clear();
|
||||
});
|
||||
|
||||
test('should register and resolve a custom service', () {
|
||||
// Register a mock logger service
|
||||
final mockLogger = MockLogger();
|
||||
App.registerService<Logger>(() => mockLogger);
|
||||
|
||||
// Resolve the service and check if it's the same instance
|
||||
final resolvedLogger = App.service<Logger>();
|
||||
expect(resolvedLogger, isA<MockLogger>());
|
||||
expect(resolvedLogger, same(mockLogger));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
69
finlog_app/fluttery/test/logger/logger_test.dart
Normal file
69
finlog_app/fluttery/test/logger/logger_test.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:logging/logging.dart' as lib;
|
||||
import 'package:fluttery/src/logger/logger_impl.dart';
|
||||
|
||||
// Mock class for the lib.Logger
|
||||
class MockLibLogger extends Mock implements lib.Logger {}
|
||||
|
||||
void main() {
|
||||
final mockLibLogger = MockLibLogger();
|
||||
|
||||
group('LoggerImpl', () {
|
||||
late LoggerImpl loggerImpl;
|
||||
|
||||
setUp(() {
|
||||
loggerImpl = LoggerImpl.forTest(mockLibLogger);
|
||||
});
|
||||
|
||||
test('info method logs an info message', () {
|
||||
loggerImpl.info('Info message');
|
||||
|
||||
// Verify that the info method was called on the mock logger with the correct message
|
||||
verify(() => mockLibLogger.info('Info message')).called(1);
|
||||
});
|
||||
|
||||
test('warning method logs a warning message', () {
|
||||
loggerImpl.warning('Warning message');
|
||||
|
||||
// Verify that the warning method was called on the mock logger with the correct message
|
||||
verify(() => mockLibLogger.warning('Warning message')).called(1);
|
||||
});
|
||||
|
||||
test('error method logs an error message with optional parameters', () {
|
||||
final exception = Exception('Test exception');
|
||||
final stackTrace = StackTrace.current;
|
||||
|
||||
loggerImpl.error('Error message', exception, stackTrace);
|
||||
|
||||
// Verify that the severe method was called on the mock logger with the correct parameters
|
||||
verify(
|
||||
() => mockLibLogger.severe('Error message', exception, stackTrace),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('debug method logs a debug message', () {
|
||||
loggerImpl.debug('Debug message');
|
||||
|
||||
// Verify that the fine method was called on the mock logger with the correct message
|
||||
verify(() => mockLibLogger.fine('Debug message')).called(1);
|
||||
});
|
||||
|
||||
test('setLogLevel method sets the logger level', () {
|
||||
// This is to capture the change in the logger level
|
||||
var capturedLevel = lib.Level.INFO;
|
||||
|
||||
// Capture the setter call
|
||||
when(() => mockLibLogger.level = any()).thenAnswer((invocation) {
|
||||
capturedLevel = invocation.positionalArguments.first as lib.Level;
|
||||
return capturedLevel;
|
||||
});
|
||||
|
||||
// Set the log level
|
||||
loggerImpl.setLogLevel(lib.Level.WARNING);
|
||||
|
||||
// Verify that the logger level is set to the expected level
|
||||
expect(capturedLevel, lib.Level.WARNING);
|
||||
});
|
||||
});
|
||||
}
|
||||
25
finlog_app/fluttery/test/mocks/mocks.dart
Normal file
25
finlog_app/fluttery/test/mocks/mocks.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:fluttery/logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockLogger extends Mock implements Logger {}
|
||||
|
||||
class MockUtils {
|
||||
static Logger mockLogger() {
|
||||
final logger = MockLogger();
|
||||
|
||||
when(() => logger.debug(any())).thenAnswer((a) {
|
||||
print("[DEBUG] ${a.positionalArguments[0]}");
|
||||
});
|
||||
when(() => logger.info(any())).thenAnswer((a) {
|
||||
print("[INFO] ${a.positionalArguments[0]}");
|
||||
});
|
||||
when(() => logger.warning(any())).thenAnswer((a) {
|
||||
print("[WARN] ${a.positionalArguments[0]}");
|
||||
});
|
||||
when(() => logger.error(any(), any(), any())).thenAnswer((a) {
|
||||
print("[ERROR] ${a.positionalArguments[0]}\n${a.positionalArguments[2]}");
|
||||
});
|
||||
|
||||
return logger;
|
||||
}
|
||||
}
|
||||
176
finlog_app/fluttery/test/preferences/preferences_test.dart
Normal file
176
finlog_app/fluttery/test/preferences/preferences_test.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fluttery/src/preferences/preferences_impl.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
late PreferencesImpl preferences;
|
||||
|
||||
setUp(() async {
|
||||
// Clear any existing data and set up a fresh in-memory instance
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
// Create preferences instance that will use the real SharedPreferences
|
||||
// but with in-memory storage for testing
|
||||
preferences = PreferencesImpl();
|
||||
|
||||
// Give time for initialization
|
||||
await Future.delayed(Duration.zero);
|
||||
});
|
||||
|
||||
group('PreferencesImpl Tests', () {
|
||||
test('setString and getString work with real implementation', () async {
|
||||
const key = 'testKey';
|
||||
const value = 'testValue';
|
||||
|
||||
await preferences.setString(key, value);
|
||||
final result = await preferences.getString(key);
|
||||
|
||||
expect(result, value);
|
||||
});
|
||||
|
||||
test('setInt and getInt work with real implementation', () async {
|
||||
const key = 'testKey';
|
||||
const value = 42;
|
||||
|
||||
await preferences.setInt(key, value);
|
||||
final result = await preferences.getInt(key);
|
||||
|
||||
expect(result, value);
|
||||
});
|
||||
|
||||
test('setBool and getBool work with real implementation', () async {
|
||||
const key = 'testKey';
|
||||
const value = true;
|
||||
|
||||
await preferences.setBool(key, value);
|
||||
final result = await preferences.getBool(key);
|
||||
|
||||
expect(result, value);
|
||||
});
|
||||
|
||||
test('setDouble and getDouble work with real implementation', () async {
|
||||
const key = 'testKey';
|
||||
const value = 3.14;
|
||||
|
||||
await preferences.setDouble(key, value);
|
||||
final result = await preferences.getDouble(key);
|
||||
|
||||
expect(result, value);
|
||||
});
|
||||
|
||||
test(
|
||||
'setStringList and getStringList work with real implementation',
|
||||
() async {
|
||||
const key = 'testKey';
|
||||
const value = ['one', 'two', 'three'];
|
||||
|
||||
await preferences.setStringList(key, value);
|
||||
final result = await preferences.getStringList(key);
|
||||
|
||||
expect(result, value);
|
||||
},
|
||||
);
|
||||
|
||||
test('remove deletes key-value pair', () async {
|
||||
const key = 'testKey';
|
||||
const value = 'testValue';
|
||||
|
||||
// Set a value first
|
||||
await preferences.setString(key, value);
|
||||
expect(await preferences.getString(key), value);
|
||||
|
||||
// Remove it
|
||||
await preferences.remove(key);
|
||||
final result = await preferences.getString(key);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('clear removes all data', () async {
|
||||
// Set multiple values
|
||||
await preferences.setString('key1', 'value1');
|
||||
await preferences.setInt('key2', 42);
|
||||
await preferences.setBool('key3', true);
|
||||
|
||||
// Verify they exist
|
||||
expect(await preferences.getString('key1'), 'value1');
|
||||
expect(await preferences.getInt('key2'), 42);
|
||||
expect(await preferences.getBool('key3'), true);
|
||||
|
||||
// Clear all
|
||||
await preferences.clear();
|
||||
|
||||
// Verify they're gone
|
||||
expect(await preferences.getString('key1'), isNull);
|
||||
expect(await preferences.getInt('key2'), isNull);
|
||||
expect(await preferences.getBool('key3'), isNull);
|
||||
});
|
||||
|
||||
test('getting non-existent keys returns null', () async {
|
||||
expect(await preferences.getString('nonExistent'), isNull);
|
||||
expect(await preferences.getInt('nonExistent'), isNull);
|
||||
expect(await preferences.getBool('nonExistent'), isNull);
|
||||
expect(await preferences.getDouble('nonExistent'), isNull);
|
||||
expect(await preferences.getStringList('nonExistent'), isNull);
|
||||
});
|
||||
|
||||
test('can overwrite existing values of the same type', () async {
|
||||
const key = 'testKey';
|
||||
const initialValue = 'initialValue';
|
||||
const newValue = 'newValue';
|
||||
|
||||
await preferences.setString(key, initialValue);
|
||||
expect(await preferences.getString(key), initialValue);
|
||||
|
||||
await preferences.setString(key, newValue);
|
||||
expect(await preferences.getString(key), newValue);
|
||||
});
|
||||
|
||||
test(
|
||||
'different keys can store different data types simultaneously',
|
||||
() async {
|
||||
await preferences.setString('stringKey', 'value');
|
||||
await preferences.setInt('intKey', 42);
|
||||
await preferences.setBool('boolKey', true);
|
||||
await preferences.setDouble('doubleKey', 3.14);
|
||||
await preferences.setStringList('listKey', ['a', 'b', 'c']);
|
||||
|
||||
expect(await preferences.getString('stringKey'), 'value');
|
||||
expect(await preferences.getInt('intKey'), 42);
|
||||
expect(await preferences.getBool('boolKey'), true);
|
||||
expect(await preferences.getDouble('doubleKey'), 3.14);
|
||||
expect(await preferences.getStringList('listKey'), ['a', 'b', 'c']);
|
||||
},
|
||||
);
|
||||
|
||||
test('values can be overwritten with different data types', () async {
|
||||
const key = 'testKey';
|
||||
|
||||
// Store a string value
|
||||
await preferences.setString(key, 'stringValue');
|
||||
expect(await preferences.getString(key), 'stringValue');
|
||||
|
||||
// Overwrite with an int - this replaces the string value
|
||||
await preferences.setInt(key, 42);
|
||||
expect(await preferences.getInt(key), 42);
|
||||
|
||||
// Overwrite with a bool - this replaces the int value
|
||||
await preferences.setBool(key, true);
|
||||
expect(await preferences.getBool(key), true);
|
||||
});
|
||||
|
||||
test('persistence works across multiple operations', () async {
|
||||
// Test that values persist through multiple set/get operations
|
||||
await preferences.setString('key1', 'value1');
|
||||
await preferences.setInt('key2', 100);
|
||||
|
||||
expect(await preferences.getString('key1'), 'value1');
|
||||
expect(await preferences.getInt('key2'), 100);
|
||||
|
||||
// Modify one value and ensure the other remains
|
||||
await preferences.setString('key1', 'newValue1');
|
||||
expect(await preferences.getString('key1'), 'newValue1');
|
||||
expect(await preferences.getInt('key2'), 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
352
finlog_app/fluttery/test/storage/secure/secure_storage_test.dart
Normal file
352
finlog_app/fluttery/test/storage/secure/secure_storage_test.dart
Normal file
@@ -0,0 +1,352 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:fluttery/src/storage/secure/secure_storage_impl.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockFlutterSecureStorage extends Mock implements FlutterSecureStorage {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late SecureStorageImpl secureStorage;
|
||||
late MockFlutterSecureStorage mockStorage;
|
||||
|
||||
// Single instance of SecureStorageImpl used throughout all tests
|
||||
setUpAll(() {
|
||||
mockStorage = MockFlutterSecureStorage();
|
||||
secureStorage = SecureStorageImpl.forTesting(instance: mockStorage);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
// Reset mock between tests
|
||||
reset(mockStorage);
|
||||
});
|
||||
|
||||
group('SecureStorageImpl String Tests', () {
|
||||
test('write and read string work', () async {
|
||||
const key = 'testStringKey';
|
||||
const value = 'testStringValue';
|
||||
|
||||
when(
|
||||
() => mockStorage.write(key: key, value: value),
|
||||
).thenAnswer((_) async {});
|
||||
when(() => mockStorage.read(key: key)).thenAnswer((_) async => value);
|
||||
|
||||
await secureStorage.write(key, value);
|
||||
final result = await secureStorage.read(key);
|
||||
|
||||
expect(result, value);
|
||||
verify(() => mockStorage.write(key: key, value: value)).called(1);
|
||||
verify(() => mockStorage.read(key: key)).called(1);
|
||||
});
|
||||
|
||||
test('read returns null for non-existent string key', () async {
|
||||
const key = 'nonExistentKey';
|
||||
|
||||
when(() => mockStorage.read(key: key)).thenAnswer((_) async => null);
|
||||
|
||||
final result = await secureStorage.read(key);
|
||||
|
||||
expect(result, isNull);
|
||||
verify(() => mockStorage.read(key: key)).called(1);
|
||||
});
|
||||
|
||||
test('can overwrite existing string values', () async {
|
||||
const key = 'overwriteKey';
|
||||
const initialValue = 'initialValue';
|
||||
const newValue = 'newValue';
|
||||
|
||||
when(
|
||||
() => mockStorage.write(key: key, value: initialValue),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.write(key: key, value: newValue),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.read(key: key),
|
||||
).thenAnswer((_) async => initialValue);
|
||||
|
||||
await secureStorage.write(key, initialValue);
|
||||
final firstResult = await secureStorage.read(key);
|
||||
expect(firstResult, initialValue);
|
||||
|
||||
when(() => mockStorage.read(key: key)).thenAnswer((_) async => newValue);
|
||||
|
||||
await secureStorage.write(key, newValue);
|
||||
final secondResult = await secureStorage.read(key);
|
||||
expect(secondResult, newValue);
|
||||
});
|
||||
});
|
||||
|
||||
group('SecureStorageImpl Integer Tests', () {
|
||||
test('writeInt and readInt work', () async {
|
||||
const key = 'testIntKey';
|
||||
const value = 42;
|
||||
const stringValue = '42';
|
||||
|
||||
when(
|
||||
() => mockStorage.write(key: key, value: stringValue),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.read(key: key),
|
||||
).thenAnswer((_) async => stringValue);
|
||||
|
||||
await secureStorage.writeInt(key, value);
|
||||
final result = await secureStorage.readInt(key);
|
||||
|
||||
expect(result, value);
|
||||
verify(() => mockStorage.write(key: key, value: stringValue)).called(1);
|
||||
verify(() => mockStorage.read(key: key)).called(1);
|
||||
});
|
||||
|
||||
test('readInt returns null for non-existent key', () async {
|
||||
const key = 'nonExistentIntKey';
|
||||
|
||||
when(() => mockStorage.read(key: key)).thenAnswer((_) async => null);
|
||||
|
||||
final result = await secureStorage.readInt(key);
|
||||
|
||||
expect(result, isNull);
|
||||
verify(() => mockStorage.read(key: key)).called(1);
|
||||
});
|
||||
|
||||
test('readInt handles negative numbers', () async {
|
||||
const key = 'negativeIntKey';
|
||||
const value = -123;
|
||||
const stringValue = '-123';
|
||||
|
||||
when(
|
||||
() => mockStorage.write(key: key, value: stringValue),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.read(key: key),
|
||||
).thenAnswer((_) async => stringValue);
|
||||
|
||||
await secureStorage.writeInt(key, value);
|
||||
final result = await secureStorage.readInt(key);
|
||||
|
||||
expect(result, value);
|
||||
});
|
||||
|
||||
test('readInt returns null for invalid integer string', () async {
|
||||
const key = 'invalidIntKey';
|
||||
const invalidValue = 'not_a_number';
|
||||
|
||||
when(
|
||||
() => mockStorage.read(key: key),
|
||||
).thenAnswer((_) async => invalidValue);
|
||||
|
||||
final result = await secureStorage.readInt(key);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('SecureStorageImpl Boolean Tests', () {
|
||||
test('writeBool and readBool work with true', () async {
|
||||
const key = 'testBoolTrueKey';
|
||||
const value = true;
|
||||
const stringValue = 'true';
|
||||
|
||||
when(
|
||||
() => mockStorage.write(key: key, value: stringValue),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.read(key: key),
|
||||
).thenAnswer((_) async => stringValue);
|
||||
|
||||
await secureStorage.writeBool(key, value);
|
||||
final result = await secureStorage.readBool(key);
|
||||
|
||||
expect(result, value);
|
||||
});
|
||||
|
||||
test('writeBool and readBool work with false', () async {
|
||||
const key = 'testBoolFalseKey';
|
||||
const value = false;
|
||||
const stringValue = 'false';
|
||||
|
||||
when(
|
||||
() => mockStorage.write(key: key, value: stringValue),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.read(key: key),
|
||||
).thenAnswer((_) async => stringValue);
|
||||
|
||||
await secureStorage.writeBool(key, value);
|
||||
final result = await secureStorage.readBool(key);
|
||||
|
||||
expect(result, value);
|
||||
});
|
||||
|
||||
test('readBool handles case insensitive true/false', () async {
|
||||
const key = 'caseInsensitiveKey';
|
||||
|
||||
// Test "TRUE"
|
||||
when(() => mockStorage.read(key: key)).thenAnswer((_) async => 'TRUE');
|
||||
expect(await secureStorage.readBool(key), true);
|
||||
|
||||
// Test "False"
|
||||
when(() => mockStorage.read(key: key)).thenAnswer((_) async => 'False');
|
||||
expect(await secureStorage.readBool(key), false);
|
||||
});
|
||||
|
||||
test('readBool returns null for invalid boolean string', () async {
|
||||
const key = 'invalidBoolKey';
|
||||
const invalidValue = 'maybe';
|
||||
|
||||
when(
|
||||
() => mockStorage.read(key: key),
|
||||
).thenAnswer((_) async => invalidValue);
|
||||
|
||||
final result = await secureStorage.readBool(key);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('SecureStorageImpl Double Tests', () {
|
||||
test('writeDouble and readDouble work', () async {
|
||||
const key = 'testDoubleKey';
|
||||
const value = 3.14159;
|
||||
const stringValue = '3.14159';
|
||||
|
||||
when(
|
||||
() => mockStorage.write(key: key, value: stringValue),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.read(key: key),
|
||||
).thenAnswer((_) async => stringValue);
|
||||
|
||||
await secureStorage.writeDouble(key, value);
|
||||
final result = await secureStorage.readDouble(key);
|
||||
|
||||
expect(result, value);
|
||||
});
|
||||
|
||||
test('readDouble returns null for invalid double string', () async {
|
||||
const key = 'invalidDoubleKey';
|
||||
const invalidValue = 'not_a_double';
|
||||
|
||||
when(
|
||||
() => mockStorage.read(key: key),
|
||||
).thenAnswer((_) async => invalidValue);
|
||||
|
||||
final result = await secureStorage.readDouble(key);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('SecureStorageImpl Utility Tests', () {
|
||||
test('containsKey returns true for existing key', () async {
|
||||
const key = 'existingKey';
|
||||
|
||||
when(
|
||||
() => mockStorage.containsKey(key: key),
|
||||
).thenAnswer((_) async => true);
|
||||
|
||||
final exists = await secureStorage.containsKey(key);
|
||||
|
||||
expect(exists, isTrue);
|
||||
verify(() => mockStorage.containsKey(key: key)).called(1);
|
||||
});
|
||||
|
||||
test('containsKey returns false for non-existent key', () async {
|
||||
const key = 'nonExistentKey';
|
||||
|
||||
when(
|
||||
() => mockStorage.containsKey(key: key),
|
||||
).thenAnswer((_) async => false);
|
||||
|
||||
final exists = await secureStorage.containsKey(key);
|
||||
|
||||
expect(exists, isFalse);
|
||||
});
|
||||
|
||||
test('delete removes key-value pair', () async {
|
||||
const key = 'deleteTestKey';
|
||||
|
||||
when(() => mockStorage.delete(key: key)).thenAnswer((_) async {});
|
||||
|
||||
await secureStorage.delete(key);
|
||||
|
||||
verify(() => mockStorage.delete(key: key)).called(1);
|
||||
});
|
||||
|
||||
test('readAll and readAllKeys return all stored keys', () async {
|
||||
final testData = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'};
|
||||
|
||||
when(() => mockStorage.readAll()).thenAnswer((_) async => testData);
|
||||
|
||||
final allKeys = await secureStorage.readAll();
|
||||
final allKeysAlt = await secureStorage.readAllKeys();
|
||||
|
||||
expect(allKeys, containsAll(testData.keys));
|
||||
expect(allKeys.length, testData.length);
|
||||
expect(allKeysAlt, equals(allKeys));
|
||||
});
|
||||
|
||||
test('deleteAll removes all key-value pairs', () async {
|
||||
when(() => mockStorage.deleteAll()).thenAnswer((_) async {});
|
||||
|
||||
await secureStorage.deleteAll();
|
||||
|
||||
verify(() => mockStorage.deleteAll()).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('SecureStorageImpl Mixed Data Type Tests', () {
|
||||
test('different data types work correctly', () async {
|
||||
const stringKey = 'stringKey';
|
||||
const intKey = 'intKey';
|
||||
const boolKey = 'boolKey';
|
||||
const doubleKey = 'doubleKey';
|
||||
|
||||
const stringValue = 'hello world';
|
||||
const intValue = 123;
|
||||
const boolValue = true;
|
||||
const doubleValue = 45.67;
|
||||
|
||||
// Mock all write operations
|
||||
when(
|
||||
() => mockStorage.write(key: stringKey, value: stringValue),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.write(key: intKey, value: intValue.toString()),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.write(key: boolKey, value: boolValue.toString()),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => mockStorage.write(key: doubleKey, value: doubleValue.toString()),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
// Mock all read operations
|
||||
when(
|
||||
() => mockStorage.read(key: stringKey),
|
||||
).thenAnswer((_) async => stringValue);
|
||||
when(
|
||||
() => mockStorage.read(key: intKey),
|
||||
).thenAnswer((_) async => intValue.toString());
|
||||
when(
|
||||
() => mockStorage.read(key: boolKey),
|
||||
).thenAnswer((_) async => boolValue.toString());
|
||||
when(
|
||||
() => mockStorage.read(key: doubleKey),
|
||||
).thenAnswer((_) async => doubleValue.toString());
|
||||
|
||||
// Store different types
|
||||
await secureStorage.write(stringKey, stringValue);
|
||||
await secureStorage.writeInt(intKey, intValue);
|
||||
await secureStorage.writeBool(boolKey, boolValue);
|
||||
await secureStorage.writeDouble(doubleKey, doubleValue);
|
||||
|
||||
// Retrieve and verify all types
|
||||
expect(await secureStorage.read(stringKey), stringValue);
|
||||
expect(await secureStorage.readInt(intKey), intValue);
|
||||
expect(await secureStorage.readBool(boolKey), boolValue);
|
||||
expect(await secureStorage.readDouble(doubleKey), doubleValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fluttery/src/system/environment/environment_impl.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
PackageInfo.setMockInitialValues(
|
||||
appName: 'Test App',
|
||||
packageName: 'com.example.testapp',
|
||||
version: '1.2.3',
|
||||
buildNumber: '42',
|
||||
buildSignature: 'mock-signature',
|
||||
installerStore: 'mock-store',
|
||||
);
|
||||
});
|
||||
|
||||
group('EnvironmentImpl', () {
|
||||
test('defaults before loadPackageInfo()', () {
|
||||
final env = EnvironmentImpl();
|
||||
|
||||
expect(env.appName, equals('Unknown'));
|
||||
expect(env.packageName, equals('Unknown'));
|
||||
expect(env.version, equals('0.0.0'));
|
||||
expect(env.buildNumber, equals('0'));
|
||||
});
|
||||
|
||||
test('loadPackageInfo() populates fields from PackageInfo', () async {
|
||||
final env = EnvironmentImpl();
|
||||
await env.loadPackageInfo();
|
||||
|
||||
expect(env.appName, equals('Test App'));
|
||||
expect(env.packageName, equals('com.example.testapp'));
|
||||
expect(env.version, equals('1.2.3'));
|
||||
expect(env.buildNumber, equals('42'));
|
||||
});
|
||||
|
||||
test('platform flags mirror dart:io Platform', () {
|
||||
final env = EnvironmentImpl();
|
||||
|
||||
expect(env.isAndroid, equals(Platform.isAndroid));
|
||||
expect(env.isIOS, equals(Platform.isIOS));
|
||||
});
|
||||
|
||||
test('build mode flags reflect Flutter constants', () {
|
||||
final env = EnvironmentImpl();
|
||||
|
||||
// In unit tests, this should typically be: debug=true, release/profile=false.
|
||||
expect(env.isDebug, isTrue);
|
||||
expect(env.isRelease, isFalse);
|
||||
expect(env.isProfile, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
413
finlog_app/fluttery/test/system/worker/worker_impl_test.dart
Normal file
413
finlog_app/fluttery/test/system/worker/worker_impl_test.dart
Normal file
@@ -0,0 +1,413 @@
|
||||
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/logger.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:fluttery/src/system/worker/worker_impl.dart';
|
||||
import 'package:fluttery/worker.dart';
|
||||
|
||||
import '../../mocks/mocks.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);
|
||||
|
||||
App.registerService<Logger>(() => MockUtils.mockLogger());
|
||||
});
|
||||
|
||||
group('WorkerImpl', () {
|
||||
late WorkerImpl worker;
|
||||
|
||||
setUp(() {
|
||||
worker = WorkerImpl();
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('spawn returns value; preTask runs; active->history tracking', () async {
|
||||
// We'll verify preTask runs by checking the task itself can access
|
||||
// what the preTask sets up (SharedPreferences mock)
|
||||
final future = worker.spawn<int>(
|
||||
'successful_task',
|
||||
() async {
|
||||
await Future.delayed(const Duration(milliseconds: 20));
|
||||
|
||||
// This would fail if preTask didn't run to set up SharedPreferences mock
|
||||
SharedPreferences.setMockInitialValues({'test': 'verified'});
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('preTaskRan', 'true');
|
||||
|
||||
return 42;
|
||||
},
|
||||
preTask: () {
|
||||
// Set up the SharedPreferences mock so the task can use it
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
},
|
||||
);
|
||||
|
||||
// Verify worker is registered as active shortly after spawn
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
expect(worker.getActiveWorkers().length, 1);
|
||||
|
||||
final activeWorkers = worker.getActiveWorkers();
|
||||
expect(activeWorkers.first.name, 'successful_task');
|
||||
expect(activeWorkers.first.status, WorkerStatus.running);
|
||||
|
||||
// Wait for completion
|
||||
final result = await future;
|
||||
expect(result, 42);
|
||||
|
||||
// The fact that the task completed successfully without throwing an exception
|
||||
// when trying to use SharedPreferences proves that preTask ran
|
||||
|
||||
// Wait for the completion handlers to run and move worker to history
|
||||
await waitFor(() => worker.getActiveWorkers().isEmpty);
|
||||
|
||||
// Verify the worker was moved to history with completed status
|
||||
final historyWorkers = worker.getAllWorkers();
|
||||
expect(historyWorkers.length, 1);
|
||||
expect(historyWorkers.first.status, WorkerStatus.completed);
|
||||
expect(historyWorkers.first.name, 'successful_task');
|
||||
expect(historyWorkers.first.endedAt, isNotNull);
|
||||
});
|
||||
test('timeout marks job as timedOut and throws TimeoutException', () async {
|
||||
final timedWorker = WorkerImpl(
|
||||
defaultTimeout: const Duration(milliseconds: 50),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
timedWorker.spawn<void>(
|
||||
'timeout_task',
|
||||
() async => Future.delayed(const Duration(milliseconds: 200)),
|
||||
preTask: () => SharedPreferences.setMockInitialValues({}),
|
||||
),
|
||||
throwsA(isA<TimeoutException>()),
|
||||
);
|
||||
|
||||
// Wait for worker to update history
|
||||
await waitFor(() => timedWorker.getAllWorkers().isNotEmpty);
|
||||
|
||||
final allWorkers = timedWorker.getAllWorkers();
|
||||
expect(allWorkers.first.status, WorkerStatus.timedOut);
|
||||
expect(allWorkers.first.name, 'timeout_task');
|
||||
});
|
||||
|
||||
test('custom timeout overrides default timeout', () async {
|
||||
final timedWorker = WorkerImpl(
|
||||
defaultTimeout: const Duration(milliseconds: 200), // Long default
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
timedWorker.spawn<void>(
|
||||
'custom_timeout_task',
|
||||
() async => Future.delayed(const Duration(milliseconds: 100)),
|
||||
timeout: const Duration(milliseconds: 50), // Short custom timeout
|
||||
preTask: () => SharedPreferences.setMockInitialValues({}),
|
||||
),
|
||||
throwsA(isA<TimeoutException>()),
|
||||
);
|
||||
|
||||
await waitFor(() => timedWorker.getAllWorkers().isNotEmpty);
|
||||
expect(timedWorker.getAllWorkers().first.status, WorkerStatus.timedOut);
|
||||
});
|
||||
|
||||
test('failure marks job as failed and surfaces exception', () async {
|
||||
// Create a variable to capture the actual exception
|
||||
Object? caughtException;
|
||||
|
||||
try {
|
||||
await worker.spawn<void>('failing_task', () async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
throw StateError('intentional failure');
|
||||
}, preTask: () => SharedPreferences.setMockInitialValues({}));
|
||||
fail('Expected an exception to be thrown');
|
||||
} catch (e) {
|
||||
caughtException = e;
|
||||
}
|
||||
|
||||
// Verify that an exception was thrown (could be RemoteError or the original StateError)
|
||||
expect(caughtException, isNotNull);
|
||||
expect(
|
||||
caughtException is RemoteError || caughtException is StateError,
|
||||
isTrue,
|
||||
reason:
|
||||
'Should throw either RemoteError or StateError, got: ${caughtException.runtimeType}',
|
||||
);
|
||||
|
||||
// Wait for worker to update history
|
||||
await waitFor(() => worker.getAllWorkers().isNotEmpty);
|
||||
|
||||
final allWorkers = worker.getAllWorkers();
|
||||
expect(allWorkers.first.status, WorkerStatus.failed);
|
||||
expect(allWorkers.first.name, 'failing_task');
|
||||
expect(allWorkers.first.error, isNotNull);
|
||||
});
|
||||
|
||||
test('getWorker finds active and completed workers by ID', () async {
|
||||
final future = worker.spawn<int>('trackable_task', () async {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
return 123;
|
||||
}, preTask: () => SharedPreferences.setMockInitialValues({}));
|
||||
|
||||
// Find worker while active
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
final activeWorkers = worker.getActiveWorkers();
|
||||
expect(activeWorkers.length, 1);
|
||||
|
||||
final workerId = activeWorkers.first.id;
|
||||
final activeWorker = worker.getWorker(workerId);
|
||||
expect(activeWorker, isNotNull);
|
||||
expect(activeWorker!.status, WorkerStatus.running);
|
||||
|
||||
// Wait for completion
|
||||
await future;
|
||||
await waitFor(() => worker.getActiveWorkers().isEmpty);
|
||||
|
||||
// Find worker in history
|
||||
final completedWorker = worker.getWorker(workerId);
|
||||
expect(completedWorker, isNotNull);
|
||||
expect(completedWorker!.status, WorkerStatus.completed);
|
||||
});
|
||||
|
||||
test('getWorker returns null for non-existent ID', () {
|
||||
expect(worker.getWorker('non-existent'), isNull);
|
||||
expect(worker.getWorker('999999'), isNull);
|
||||
});
|
||||
|
||||
test('worker ID generation uses timestamp format', () async {
|
||||
final futures = <Future>[];
|
||||
final Set<String> generatedIds = <String>{};
|
||||
|
||||
// Spawn multiple workers with sufficient delay to ensure unique timestamps
|
||||
for (int i = 0; i < 3; i++) {
|
||||
futures.add(
|
||||
worker.spawn<void>(
|
||||
'task_$i',
|
||||
() async => Future.delayed(const Duration(milliseconds: 10)),
|
||||
preTask: () => SharedPreferences.setMockInitialValues({}),
|
||||
),
|
||||
);
|
||||
// Ensure sufficient delay for different timestamps
|
||||
await Future<void>.delayed(const Duration(milliseconds: 1));
|
||||
}
|
||||
|
||||
// Wait for all workers to be registered as active
|
||||
await waitFor(() => worker.getActiveWorkers().length == 3);
|
||||
|
||||
final activeWorkers = worker.getActiveWorkers();
|
||||
expect(activeWorkers.length, 3);
|
||||
|
||||
// Verify each ID follows the timestamp format and is unique
|
||||
for (final workerInfo in activeWorkers) {
|
||||
expect(workerInfo.id, startsWith('iso-'));
|
||||
expect(
|
||||
generatedIds.contains(workerInfo.id),
|
||||
isFalse,
|
||||
reason: 'Worker ID should be unique: ${workerInfo.id}',
|
||||
);
|
||||
generatedIds.add(workerInfo.id);
|
||||
|
||||
final timestampPart = workerInfo.id.substring(
|
||||
4,
|
||||
); // Remove 'iso-' prefix
|
||||
final timestamp = int.tryParse(timestampPart);
|
||||
expect(
|
||||
timestamp,
|
||||
isNotNull,
|
||||
reason: 'Timestamp part should be a valid integer: $timestampPart',
|
||||
);
|
||||
expect(
|
||||
timestamp,
|
||||
greaterThan(0),
|
||||
reason: 'Timestamp should be positive: $timestamp',
|
||||
);
|
||||
|
||||
// Verify timestamp is reasonable (not too old, not in future)
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
expect(
|
||||
timestamp,
|
||||
lessThanOrEqualTo(now),
|
||||
reason: 'Timestamp should not be in the future',
|
||||
);
|
||||
expect(
|
||||
timestamp,
|
||||
greaterThan(now - 10000),
|
||||
reason: 'Timestamp should be recent (within 10 seconds)',
|
||||
);
|
||||
}
|
||||
|
||||
// Verify we generated 3 unique IDs
|
||||
expect(generatedIds.length, 3);
|
||||
|
||||
await Future.wait(futures);
|
||||
});
|
||||
|
||||
test('getAllWorkers combines active and history workers', () async {
|
||||
// Spawn and complete a short task first
|
||||
await worker.spawn<void>(
|
||||
'short_task',
|
||||
() async => Future.delayed(const Duration(milliseconds: 10)),
|
||||
preTask: () => SharedPreferences.setMockInitialValues({}),
|
||||
);
|
||||
|
||||
// Wait for short task to complete and move to history
|
||||
await waitFor(() => worker.getAllWorkers().length == 1);
|
||||
await waitFor(() => worker.getActiveWorkers().isEmpty);
|
||||
|
||||
// Verify we have one completed worker in history
|
||||
expect(worker.getAllWorkers().length, 1);
|
||||
expect(worker.getAllWorkers().first.status, WorkerStatus.completed);
|
||||
expect(worker.getAllWorkers().first.name, 'short_task');
|
||||
|
||||
// Now spawn a long-running task
|
||||
final longTask = worker.spawn<void>(
|
||||
'long_task',
|
||||
() async => Future.delayed(const Duration(milliseconds: 100)),
|
||||
preTask: () => SharedPreferences.setMockInitialValues({}),
|
||||
);
|
||||
|
||||
// Wait briefly for long task to be registered as active
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
|
||||
// Now we should have 2 workers: 1 active (long_task) and 1 in history (short_task)
|
||||
final allWorkers = worker.getAllWorkers();
|
||||
expect(allWorkers.length, 2);
|
||||
|
||||
final activeCount = allWorkers
|
||||
.where((w) => w.status == WorkerStatus.running)
|
||||
.length;
|
||||
final completedCount = allWorkers
|
||||
.where((w) => w.status == WorkerStatus.completed)
|
||||
.length;
|
||||
|
||||
expect(activeCount, 1);
|
||||
expect(completedCount, 1);
|
||||
|
||||
// Verify the active worker is the long task
|
||||
final activeWorkers = worker.getActiveWorkers();
|
||||
expect(activeWorkers.length, 1);
|
||||
expect(activeWorkers.first.name, 'long_task');
|
||||
|
||||
await longTask;
|
||||
});
|
||||
|
||||
test('purge removes old workers from history', () async {
|
||||
// Complete a task
|
||||
await worker.spawn<void>(
|
||||
'old_task',
|
||||
() async => Future.delayed(const Duration(milliseconds: 10)),
|
||||
preTask: () => SharedPreferences.setMockInitialValues({}),
|
||||
);
|
||||
|
||||
await waitFor(() => worker.getAllWorkers().isNotEmpty);
|
||||
expect(worker.getAllWorkers().length, 1);
|
||||
|
||||
// Purge with zero max age (removes everything)
|
||||
worker.purge(maxAge: Duration.zero);
|
||||
await pumpMicro();
|
||||
|
||||
expect(worker.getAllWorkers(), isEmpty);
|
||||
});
|
||||
|
||||
test('maxHistory limit is enforced', () async {
|
||||
final limitedWorker = WorkerImpl(maxHistory: 2);
|
||||
|
||||
// Complete 3 tasks
|
||||
for (int i = 0; i < 3; i++) {
|
||||
await limitedWorker.spawn<void>(
|
||||
'task_$i',
|
||||
() async => Future.delayed(const Duration(milliseconds: 5)),
|
||||
preTask: () => SharedPreferences.setMockInitialValues({}),
|
||||
);
|
||||
await waitFor(() => limitedWorker.getActiveWorkers().isEmpty);
|
||||
}
|
||||
|
||||
// Should only keep the last 2 in history
|
||||
final allWorkers = limitedWorker.getAllWorkers();
|
||||
expect(allWorkers.length, 2);
|
||||
|
||||
// Should be the most recent tasks (task_1 and task_2)
|
||||
final names = allWorkers.map((w) => w.name).toSet();
|
||||
expect(names.contains('task_1'), isTrue);
|
||||
expect(names.contains('task_2'), isTrue);
|
||||
expect(names.contains('task_0'), isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'no timeout when defaultTimeout is null and timeout is null',
|
||||
() async {
|
||||
final noTimeoutWorker = WorkerImpl(defaultTimeout: null);
|
||||
|
||||
final result = await noTimeoutWorker.spawn<int>(
|
||||
'no_timeout_task',
|
||||
() async {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
return 999;
|
||||
},
|
||||
preTask: () => SharedPreferences.setMockInitialValues({}),
|
||||
);
|
||||
|
||||
expect(result, 999);
|
||||
|
||||
await waitFor(() => noTimeoutWorker.getAllWorkers().isNotEmpty);
|
||||
expect(
|
||||
noTimeoutWorker.getAllWorkers().first.status,
|
||||
WorkerStatus.completed,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('active workers are sorted by start time', () async {
|
||||
final futures = <Future>[];
|
||||
|
||||
// Spawn workers with small delays between them
|
||||
for (int i = 0; i < 3; i++) {
|
||||
futures.add(
|
||||
worker.spawn<void>(
|
||||
'timed_task_$i',
|
||||
() async => Future.delayed(const Duration(milliseconds: 100)),
|
||||
preTask: () => SharedPreferences.setMockInitialValues({}),
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
|
||||
final activeWorkers = worker.getActiveWorkers();
|
||||
expect(activeWorkers.length, 3);
|
||||
|
||||
// Verify they are sorted by start time (earliest first)
|
||||
for (int i = 1; i < activeWorkers.length; i++) {
|
||||
expect(
|
||||
activeWorkers[i - 1].startedAt.isBefore(activeWorkers[i].startedAt) ||
|
||||
activeWorkers[i - 1].startedAt.isAtSameMomentAs(
|
||||
activeWorkers[i].startedAt,
|
||||
),
|
||||
isTrue,
|
||||
reason: 'Workers should be sorted by start time',
|
||||
);
|
||||
}
|
||||
|
||||
await Future.wait(futures);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user