diff --git a/.gitignore b/.gitignore index 4a189ec..90793be 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ example/.metadata # VS Code which you may wish to be included in version control, so this line # is commented out by default. .vscode/ +.example/.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3326d18..b37dd27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -175,22 +175,28 @@ MaterialApp( - [NEW] Added `logStream` for realtime log subscriptions. - [NEW] Added explicit `dispose` cleanup for logger stream resources. -- [BREAKING] `LoggingInterface` no longer includes built-in singleton-style - initialization state. - [NEW] Added optional lifecycle capability via `LoggingInitializable` and `LoggingInitialization`. - [NEW] Added optional `feature` tag support via `@LoggingFeature(...)` annotation. -- [CHANGE] `initializeInterfaces()` now initializes only interfaces that - implement `LoggingInitializable`; other interfaces are skipped. - [NEW] Added a `skipAutodetection` parameter to `Arcane.log` (defaults to `false`) that, when enabled, skips detection of the `module`, `method`, and file/line number where logs originated from. - [NEW] Added the `LogInterceptor` class which can (optionally) be added to `ArcaneLogger` to pre-process log messages before they are sent to the registered `ArcaneLoggingInterface`(s). +- [NEW] Added collection-style interceptor APIs: + `Arcane.logger.interceptors.add(...)`, + `Arcane.logger.interceptors.remove(...)`, and + `Arcane.logger.interceptors.clear()` with an optional `matcher` for + explicit type-scoped matching strategies, including subtype-inclusive + matching. - [CHANGE] Updated `Arcane.log` metadata type from `Map?` to `Map?` to support structured metadata values. +- [CHANGE] `initializeInterfaces()` now initializes only interfaces that + implement `LoggingInitializable`; other interfaces are skipped. +- [BREAKING] `LoggingInterface` no longer includes built-in singleton-style + initialization state. #### Migration Steps (LoggingInterface) diff --git a/README.md b/README.md index 138b06d..02f0db2 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ authentication, feature flags, theming, and more. This framework is ideal for building scalable applications that require dynamic configuration and service management. +

+ Arcane logo +

+ - [Arcane Framework](#arcane-framework) - [Features](#features) - [Installation](#installation) @@ -531,14 +535,15 @@ class AnalyticsLogger extends LoggingInterface { } } -Arcane.logger.registerInterceptor( +Arcane.logger.interceptors.add( LogInterceptor((event, context) { - if (context.interface is AnalyticsLogger && event.level == Level.debug) { + if (event.level == Level.debug) { return null; } return event; }), + matcher: (interface) => interface is AnalyticsLogger, ); ``` @@ -565,8 +570,8 @@ await Arcane.logger.registerInterface(AuthLogger()); ``` Next, register your logging interface with the Arcane logger service. You can -attach interceptors when registering an interface, or add global interceptors -later at runtime. +attach interceptors when registering an interface, or manage logger +interceptors via `Arcane.logger.interceptors` later at runtime. ```dart final DebugConsole debugConsole = DebugConsole(); @@ -575,7 +580,7 @@ await Arcane.logger.registerInterface( debugConsole, interceptors: [ LogInterceptor((event, context) { - if (context.interface is DebugConsole && event.level == Level.debug) { + if (event.level == Level.debug) { return null; } @@ -584,7 +589,7 @@ await Arcane.logger.registerInterface( ], ); -Arcane.logger.registerInterceptor( +Arcane.logger.interceptors.add( LogInterceptor((event, context) { return event.copyWith( metadata: { @@ -601,10 +606,24 @@ await Arcane.logger.initializeInterfaces(); ``` Global interceptors are evaluated for each registered interface, and interface -interceptors run immediately after them for that same destination. Every +interceptors run immediately after them for that same destination. Use +`Arcane.logger.interceptors.add(...)` with no matcher for global behavior, or +provide a matcher for scoped behavior. Every interceptor receives a `LogInterceptorContext` whose `interface` value is the -current destination, which allows a single global interceptor to allow one -interface to receive an event while dropping it for another. +current destination. + +If you want subtype-inclusive matching (for example, a base interface plus all +derived interfaces), use `Arcane.logger.interceptors.add(...)` with +an explicit matcher. + +```dart +Arcane.logger.interceptors.add( + LogInterceptor((event, context) { + return event.copyWith(message: "[scoped] ${event.message}"); + }), + matcher: (interface) => interface is AnalyticsLogger, +); +``` Returning `null` from an interceptor drops the event for the current scope. Returning a modified `LogEvent` allows you to rewrite the message, metadata, @@ -664,10 +683,10 @@ void dispose() { } ``` -You can also add and remove global interceptors after startup. Because every -interceptor receives a `LogInterceptorContext`, a single global interceptor can -still make interface-specific decisions by checking `context.interface`. If you -prefer, you can also define your own interceptor class by implementing +You can also add, remove, and clear interceptors after startup. Because every +interceptor receives a `LogInterceptorContext`, global interceptors can still +make interface-specific decisions when needed by checking `context.interface`. +If you prefer, you can also define your own interceptor class by implementing `LogInterceptor` instead of using the callback constructor. ```dart @@ -686,8 +705,8 @@ final LogInterceptor redactSecrets = LogInterceptor(( ); }); -Arcane.logger.registerInterceptor(redactSecrets); -Arcane.logger.unregisterInterceptor(redactSecrets); +Arcane.logger.interceptors.add(redactSecrets); +Arcane.logger.interceptors.remove(redactSecrets); ``` If you prefer a reusable named type, you can also implement `LogInterceptor` @@ -716,8 +735,8 @@ class RedactingLogInterceptor implements LogInterceptor { final LogInterceptor redactSecrets = RedactingLogInterceptor(); -Arcane.logger.registerInterceptor(redactSecrets); -Arcane.logger.unregisterInterceptor(redactSecrets); +Arcane.logger.interceptors.add(redactSecrets); +Arcane.logger.interceptors.remove(redactSecrets); ``` Multiple logging interfaces and multiple interceptors can be registered diff --git a/lib/src/services/logging/logging_interceptors_service.dart b/lib/src/services/logging/logging_interceptors_service.dart new file mode 100644 index 0000000..e214541 --- /dev/null +++ b/lib/src/services/logging/logging_interceptors_service.dart @@ -0,0 +1,147 @@ +part of "logging_service.dart"; + +final class LoggingInterceptorsService { + LoggingInterceptorsService._internal(); + + static final LoggingInterceptorsService _instance = + LoggingInterceptorsService._internal(); + + static LoggingInterceptorsService get I => _instance; + + final List _globalInterceptors = []; + final List<_InterfaceScopedInterceptorRegistration> + _interfaceScopedInterceptorRegistrations = []; + final List<_TypeScopedInterceptorRegistration> + _typeScopedInterceptorRegistrations = []; + + /// Registers an interceptor. + /// + /// If [matcher] is omitted, the interceptor is registered globally. + void add( + LogInterceptor interceptor, { + bool Function(LoggingInterface interface)? matcher, + }) { + if (matcher == null) { + _globalInterceptors.add(interceptor); + return; + } + + _typeScopedInterceptorRegistrations.add( + _TypeScopedInterceptorRegistration( + interceptor: interceptor, + matcher: matcher, + ), + ); + } + + /// Registers multiple interceptors. + void addAll( + Iterable interceptors, { + bool Function(LoggingInterface interface)? matcher, + }) { + for (final LogInterceptor interceptor in interceptors) { + add(interceptor, matcher: matcher); + } + } + + /// Removes interceptor registrations. + /// + /// If [matcher] is omitted, global registrations are removed. + /// If [matcher] is provided, scoped registrations with the same matcher + /// identity are removed. + void remove( + LogInterceptor interceptor, { + bool Function(LoggingInterface interface)? matcher, + }) { + if (matcher == null) { + _globalInterceptors.removeWhere( + (LogInterceptor current) => identical(current, interceptor), + ); + return; + } + + _typeScopedInterceptorRegistrations.removeWhere( + (_TypeScopedInterceptorRegistration registration) => + identical(registration.interceptor, interceptor) && + identical(registration.matcher, matcher), + ); + } + + /// Clears all globally and scoped interceptor registrations. + void clear() { + _globalInterceptors.clear(); + _interfaceScopedInterceptorRegistrations.clear(); + _typeScopedInterceptorRegistrations.clear(); + } + + /// Clears global interceptor registrations. + void clearGlobal() => _globalInterceptors.clear(); + + void registerForInterface( + LoggingInterface interface, + Iterable interceptors, + ) { + for (final LogInterceptor interceptor in interceptors) { + _interfaceScopedInterceptorRegistrations.add( + _InterfaceScopedInterceptorRegistration( + interface: interface, + interceptor: interceptor, + ), + ); + } + } + + void unregisterInterface(LoggingInterface interface) { + _interfaceScopedInterceptorRegistrations.removeWhere( + (_InterfaceScopedInterceptorRegistration registration) => + identical(registration.interface, interface), + ); + } + + List resolveForInterface( + LoggingInterface interface, + ) { + return [ + ..._globalInterceptors, + for (final _InterfaceScopedInterceptorRegistration registration + in _interfaceScopedInterceptorRegistrations) + if (identical(registration.interface, interface)) + registration.interceptor, + for (final _TypeScopedInterceptorRegistration registration + in _typeScopedInterceptorRegistrations) + if (registration.matches(interface)) registration.interceptor, + ]; + } +} + +final class _LoggingInterfaceRegistration { + _LoggingInterfaceRegistration({ + required this.interface, + }); + + final LoggingInterface interface; +} + +final class _InterfaceScopedInterceptorRegistration { + const _InterfaceScopedInterceptorRegistration({ + required this.interface, + required this.interceptor, + }); + + final LoggingInterface interface; + final LogInterceptor interceptor; +} + +final class _TypeScopedInterceptorRegistration { + _TypeScopedInterceptorRegistration({ + required this.interceptor, + required bool Function(LoggingInterface interface) matcher, + }) : _matcher = matcher; + + final LogInterceptor interceptor; + final bool Function(LoggingInterface interface) _matcher; + + bool Function(LoggingInterface interface) get matcher => _matcher; + + bool matches(LoggingInterface interface) => _matcher(interface); +} diff --git a/lib/src/services/logging/logging_service.dart b/lib/src/services/logging/logging_service.dart index dbace2e..734950d 100644 --- a/lib/src/services/logging/logging_service.dart +++ b/lib/src/services/logging/logging_service.dart @@ -5,6 +5,7 @@ import "package:arcane_helper_utils/arcane_helper_utils.dart"; part "log_event.dart"; part "log_interceptor.dart"; part "logging_enums.dart"; +part "logging_interceptors_service.dart"; part "logging_interface.dart"; /// A singleton class that manages logging to one or more logging interfaces @@ -22,8 +23,8 @@ class ArcaneLogger { static ArcaneLogger get I => _instance; final List<_LoggingInterfaceRegistration> _interfaceRegistrations = []; - - final List _interceptors = []; + final LoggingInterceptorsService _interceptorService = + LoggingInterceptorsService.I; /// A list of registered logging interfaces. List get interfaces => [ @@ -32,10 +33,8 @@ class ArcaneLogger { registration.interface, ]; - /// A list of globally registered interceptors. - List get interceptors => [ - ...I._interceptors, - ]; + /// Interceptor service used to add, remove, and clear interceptors. + LoggingInterceptorsService get interceptors => I._interceptorService; final Map _additionalMetadata = {}; @@ -272,16 +271,16 @@ class ArcaneLogger { for (final _LoggingInterfaceRegistration registration in I._interfaceRegistrations) { if (initialized) { + final List interceptors = + I.interceptors.resolveForInterface(registration.interface); + final LogEvent? interfaceEvent = _runInterceptors( event.copyWith( metadata: event.metadata == null ? null : Map.from(event.metadata!), ), - interceptors: [ - ...I._interceptors, - ...registration.interceptors, - ], + interceptors: interceptors, context: LogInterceptorContext(interface: registration.interface), ); @@ -316,40 +315,20 @@ class ArcaneLogger { }) async { if (!initialized) await _init(); + I.interceptors.registerForInterface( + loggingInterface, + interceptors ?? const [], + ); + I._interfaceRegistrations.add( _LoggingInterfaceRegistration( interface: loggingInterface, - interceptors: interceptors, ), ); return I; } - /// Registers a global [LogInterceptor] to run before interface fan-out. - ArcaneLogger registerInterceptor(LogInterceptor interceptor) { - I._interceptors.add(interceptor); - return I; - } - - /// Registers a `List` of global [LogInterceptor]s. - ArcaneLogger registerInterceptors(List interceptors) { - I._interceptors.addAll(interceptors); - return I; - } - - /// Unregisters a previously registered global [LogInterceptor]. - ArcaneLogger unregisterInterceptor(LogInterceptor interceptor) { - I._interceptors.remove(interceptor); - return I; - } - - /// Removes all previously registered global interceptors. - ArcaneLogger clearInterceptors() { - I._interceptors.clear(); - return I; - } - /// Registers a `List` of [LoggingInterface] with the [ArcaneLogger]. /// /// Once registered, logs are eligible to be sent to these interfaces @@ -361,10 +340,14 @@ class ArcaneLogger { if (!initialized) await _init(); for (final LoggingInterface i in interfaces) { + I.interceptors.registerForInterface( + i, + interceptors?[i] ?? const [], + ); + I._interfaceRegistrations.add( _LoggingInterfaceRegistration( interface: i, - interceptors: interceptors?[i], ), ); } @@ -379,6 +362,8 @@ class ArcaneLogger { ) async { if (!initialized) await _init(); + I.interceptors.unregisterInterface(interface); + I._interfaceRegistrations.removeWhere( (_LoggingInterfaceRegistration registration) => identical(registration.interface, interface), @@ -395,6 +380,8 @@ class ArcaneLogger { if (!initialized) await _init(); for (final LoggingInterface i in interfaces) { + I.interceptors.unregisterInterface(i); + I._interfaceRegistrations.removeWhere( (_LoggingInterfaceRegistration registration) => identical(registration.interface, i), @@ -408,6 +395,12 @@ class ArcaneLogger { /// [ArcaneLogger], if any were previously registered. Future unregisterAllInterfaces() async { if (!initialized) await _init(); + + for (final _LoggingInterfaceRegistration registration + in I._interfaceRegistrations) { + I.interceptors.unregisterInterface(registration.interface); + } + I._interfaceRegistrations.clear(); return I; } @@ -482,7 +475,7 @@ class ArcaneLogger { void reset() { dispose(); I._interfaceRegistrations.clear(); - I._interceptors.clear(); + I.interceptors.clear(); I._initialized = false; I._additionalMetadata.clear(); } @@ -510,15 +503,3 @@ class ArcaneLogger { return currentEvent; } } - -final class _LoggingInterfaceRegistration { - _LoggingInterfaceRegistration({ - required this.interface, - List? interceptors, - }) : interceptors = [ - ...?interceptors, - ]; - - final LoggingInterface interface; - final List interceptors; -} diff --git a/test/services/logging/logging_service_test.dart b/test/services/logging/logging_service_test.dart index 28e1ddf..4998ad0 100644 --- a/test/services/logging/logging_service_test.dart +++ b/test/services/logging/logging_service_test.dart @@ -60,6 +60,36 @@ class TestPassiveLoggingInterface extends LoggingInterface { } } +class TestAlternativeLoggingInterface extends LoggingInterface { + TestAlternativeLoggingInterface(this.name); + + final String name; + final List events = []; + + @override + void log( + String message, { + Map? metadata, + Level? level, + StackTrace? stackTrace, + Object? extra, + }) { + events.add( + LogEvent( + message: message, + metadata: metadata == null ? null : Map.from(metadata), + level: level, + stackTrace: stackTrace, + extra: extra, + ), + ); + } +} + +class TestDerivedLoggingInterface extends TestLoggingInterface { + TestDerivedLoggingInterface(super.name); +} + class RedactingLogInterceptor implements LogInterceptor { const RedactingLogInterceptor(); @@ -198,7 +228,18 @@ void main() { await Arcane.logger.registerInterface(myInterface); Arcane.log("before"); - Arcane.logger.registerInterceptor(prefixInterceptor); + Arcane.logger.interceptors.add(prefixInterceptor); + Arcane.log("after"); + + expect(myInterface.events[0].message, "before"); + expect(myInterface.events[1].message, "[global] after"); + }); + + test("interceptor collection add defaults to global", () async { + await Arcane.logger.registerInterface(myInterface); + + Arcane.log("before"); + Arcane.logger.interceptors.add(prefixInterceptor); Arcane.log("after"); expect(myInterface.events[0].message, "before"); @@ -219,16 +260,90 @@ void main() { await Arcane.logger.registerInterface(myInterface); Arcane.log("before"); - Arcane.logger.registerInterceptor(dropForPrimary); + Arcane.logger.interceptors.add( + dropForPrimary, + matcher: (LoggingInterface interface) => + interface is TestLoggingInterface, + ); Arcane.log("blocked"); - Arcane.logger.unregisterInterceptor(dropForPrimary); - Arcane.log("after"); expect( myInterface.events.map((LogEvent event) => event.message), - ["before", "after"], + ["before"], ); }); + + test("type-scoped interceptors only apply to matching interfaces", + () async { + final TestAlternativeLoggingInterface alternativeInterface = + TestAlternativeLoggingInterface("alternative"); + + await Arcane.logger.registerInterfaces([ + myInterface, + alternativeInterface, + ]); + + Arcane.logger.interceptors.add( + LogInterceptor((event, context) { + return event.copyWith(message: "[typed] ${event.message}"); + }), + matcher: (LoggingInterface interface) => + interface is TestLoggingInterface, + ); + + Arcane.log("typed"); + + expect(myInterface.events.last.message, "[typed] typed"); + expect(alternativeInterface.events.last.message, "typed"); + }); + + test("matcher-based type-scoped interceptors can include subtypes", + () async { + final TestDerivedLoggingInterface derivedInterface = + TestDerivedLoggingInterface("derived"); + final TestAlternativeLoggingInterface alternativeInterface = + TestAlternativeLoggingInterface("alternative"); + + await Arcane.logger.registerInterfaces([ + myInterface, + derivedInterface, + alternativeInterface, + ]); + + Arcane.logger.interceptors.add( + LogInterceptor((event, context) { + return event.copyWith(message: "[family] ${event.message}"); + }), + matcher: (LoggingInterface interface) => + interface is TestLoggingInterface, + ); + + Arcane.log("typed"); + + expect(myInterface.events.last.message, "[family] typed"); + expect(derivedInterface.events.last.message, "[family] typed"); + expect(alternativeInterface.events.last.message, "typed"); + }); + + test( + "registering a type-scoped interceptor before matching interface exists stores it for future registrations", + () async { + final TestLoggingInterface secondary = + TestLoggingInterface("secondary"); + + Arcane.logger.interceptors.add( + LogInterceptor((event, context) { + return event.copyWith(message: "[future] ${event.message}"); + }), + matcher: (LoggingInterface interface) => + interface is TestLoggingInterface, + ); + + await Arcane.logger.registerInterface(secondary); + Arcane.log("hello"); + + expect(secondary.events.single.message, "[future] hello"); + }); }); group("persistent metadata", () { @@ -313,7 +428,7 @@ void main() { }); test("global interceptors run in registration order", () async { - Arcane.logger.registerInterceptors([ + Arcane.logger.interceptors.addAll([ LogInterceptor((event, context) { expect(context.interface, same(myInterface)); return event.copyWith(message: "${event.message}:first"); @@ -331,7 +446,7 @@ void main() { test("global interceptors can drop events for all interfaces", () async { await Arcane.logger.registerInterface(myInterface); - Arcane.logger.registerInterceptor( + Arcane.logger.interceptors.add( LogInterceptor((event, context) => null), ); @@ -340,8 +455,17 @@ void main() { expect(myInterface.events, isEmpty); }); + test("interceptor collection remove defaults to global", () async { + Arcane.logger.interceptors.add(prefixInterceptor); + Arcane.logger.interceptors.remove(prefixInterceptor); + + Arcane.log(logMessage); + + expect(myInterface.events.single.message, logMessage); + }); + test("custom interceptor classes can implement LogInterceptor", () async { - Arcane.logger.registerInterceptor(const RedactingLogInterceptor()); + Arcane.logger.interceptors.add(const RedactingLogInterceptor()); Arcane.log( logMessage, @@ -365,7 +489,11 @@ void main() { }, ); - Arcane.logger.registerInterceptor(allowPrimaryOnly); + Arcane.logger.interceptors.add( + allowPrimaryOnly, + matcher: (LoggingInterface interface) => + interface is TestLoggingInterface, + ); await Arcane.logger.registerInterface( secondaryInterface, ); @@ -376,11 +504,39 @@ void main() { expect(secondaryInterface.events, isEmpty); }); + test("interceptor collection remove with matcher removes scoped entries", + () async { + final TestLoggingInterface secondaryInterface = + TestLoggingInterface("secondary"); + final LogInterceptor scopedInterceptor = + LogInterceptor((event, context) { + return event.copyWith(message: "[scoped] ${event.message}"); + }); + bool scopedMatcher(LoggingInterface interface) => + interface is TestLoggingInterface; + + Arcane.logger.interceptors.add( + scopedInterceptor, + matcher: scopedMatcher, + ); + + await Arcane.logger.registerInterface(secondaryInterface); + Arcane.logger.interceptors.remove( + scopedInterceptor, + matcher: scopedMatcher, + ); + + Arcane.log(logMessage); + + expect(myInterface.events.single.message, logMessage); + expect(secondaryInterface.events.single.message, logMessage); + }); + test("interface interceptors receive the current interface", () async { final TestLoggingInterface secondaryInterface = TestLoggingInterface("secondary"); - Arcane.logger.registerInterceptor( + Arcane.logger.interceptors.add( LogInterceptor((event, context) { final TestLoggingInterface currentInterface = context.interface! as TestLoggingInterface; @@ -391,6 +547,8 @@ void main() { }, ); }), + matcher: (LoggingInterface interface) => + interface is TestLoggingInterface, ); await Arcane.logger.registerInterface( secondaryInterface, @@ -410,12 +568,14 @@ void main() { final TestLoggingInterface secondaryInterface = TestLoggingInterface("secondary"); - Arcane.logger.registerInterceptor( + Arcane.logger.interceptors.add( LogInterceptor((event, context) { event.metadata?["mutatedBy"] = (context.interface as TestLoggingInterface).name; return event; }), + matcher: (LoggingInterface interface) => + interface is TestLoggingInterface, ); await Arcane.logger.registerInterface( secondaryInterface, @@ -457,7 +617,7 @@ void main() { }); test("reset clears global interceptors", () async { - Arcane.logger.registerInterceptor(prefixInterceptor); + Arcane.logger.interceptors.add(prefixInterceptor); Arcane.logger.reset(); await Arcane.logger.registerInterface(myInterface); @@ -465,6 +625,56 @@ void main() { expect(myInterface.events.single.message, logMessage); }); + + test("global and type-scoped duplicate registrations are additive", + () async { + final LogInterceptor duplicateInterceptor = LogInterceptor( + (event, context) { + final int count = (event.metadata?["count"] as int?) ?? 0; + return event.copyWith( + metadata: { + ...?event.metadata, + "count": count + 1, + }, + ); + }, + ); + + Arcane.logger + ..interceptors.add(duplicateInterceptor) + ..interceptors.add( + duplicateInterceptor, + matcher: (LoggingInterface interface) => + interface is TestLoggingInterface, + ); + + Arcane.log( + logMessage, + metadata: { + "count": 0, + }, + ); + + expect(myInterface.events.single.metadata?["count"], 2); + }); + + test( + "interceptor collection clear removes global and scoped interceptors", + () async { + Arcane.logger.interceptors.add(prefixInterceptor); + Arcane.logger.interceptors.add( + LogInterceptor((event, context) { + return event.copyWith(message: "[scoped] ${event.message}"); + }), + matcher: (LoggingInterface interface) => + interface is TestLoggingInterface, + ); + + Arcane.logger.interceptors.clear(); + Arcane.log(logMessage); + + expect(myInterface.events.single.message, logMessage); + }); }); }); } diff --git a/test/services/theme/theme_service_test.dart b/test/services/theme/theme_service_test.dart index 89b9a62..14ddd63 100644 --- a/test/services/theme/theme_service_test.dart +++ b/test/services/theme/theme_service_test.dart @@ -36,9 +36,11 @@ void main() { test("reset restores defaults", () { ArcaneThemeService.I.setDarkTheme( - ThemeData(primaryColor: Colors.red, brightness: Brightness.dark),); + ThemeData(primaryColor: Colors.red, brightness: Brightness.dark), + ); ArcaneThemeService.I.setLightTheme( - ThemeData(primaryColor: Colors.blue, brightness: Brightness.light),); + ThemeData(primaryColor: Colors.blue, brightness: Brightness.light), + ); ArcaneThemeService.I.reset(); expect(ArcaneThemeService.I.dark, ThemeData.dark()); expect(ArcaneThemeService.I.light, ThemeData.light());