mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-23 13:03:11 +00:00
refactor(core,logger)migrate to CherryPickObserver API and drop CherryPickLogger
BREAKING CHANGE: - Removed the deprecated CherryPickLogger interface from cherrypick - Logger/adapters (e.g., talker_cherrypick_logger) must now implement CherryPickObserver - talker_cherrypick_logger: replace TalkerCherryPickLogger with TalkerCherryPickObserver - All usages, docs, tests migrated to observer API - Improved test mocks and integration tests for observer pattern - Removed obsolete files: cherrypick/src/logger.dart, talker_cherrypick_logger/src/talker_cherrypick_logger.dart - Updated README and example usages for new CherryPickObserver model This refactor introduces a unified observer pattern (CherryPickObserver) to handle all DI lifecycle events, replacing the limited info/warn/error logger pattern. All external logging adapters and integrations must migrate to use CherryPickObserver.
This commit is contained in:
@@ -15,7 +15,7 @@ class AppModule extends Module {
|
||||
|
||||
void main() {
|
||||
// Set a global logger for the DI system
|
||||
CherryPick.setGlobalLogger(PrintLogger());
|
||||
CherryPick.setGlobalObserver(PrintCherryPickObserver());
|
||||
|
||||
// Open the root scope
|
||||
final rootScope = CherryPick.openRootScope();
|
||||
@@ -32,6 +32,6 @@ void main() {
|
||||
subScope.closeSubScope('feature.profile');
|
||||
|
||||
// Demonstrate disabling and re-enabling logging
|
||||
CherryPick.setGlobalLogger(const SilentLogger());
|
||||
CherryPick.setGlobalObserver(SilentCherryPickObserver());
|
||||
rootScope.resolve<UserRepository>(); // now without logs
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ export 'package:cherrypick/src/global_cycle_detector.dart';
|
||||
export 'package:cherrypick/src/helper.dart';
|
||||
export 'package:cherrypick/src/module.dart';
|
||||
export 'package:cherrypick/src/scope.dart';
|
||||
export 'package:cherrypick/src/logger.dart';
|
||||
export 'package:cherrypick/src/disposable.dart';
|
||||
export 'package:cherrypick/src/observer.dart';
|
||||
|
||||
@@ -16,8 +16,7 @@ import 'package:cherrypick/src/binding_resolver.dart';
|
||||
/// RU: Класс Binding<T> настраивает параметры экземпляра.
|
||||
/// ENG: The Binding<T> class configures the settings for the instance.
|
||||
///
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
|
||||
class Binding<T> {
|
||||
late Type _key;
|
||||
@@ -25,50 +24,54 @@ class Binding<T> {
|
||||
|
||||
BindingResolver<T>? _resolver;
|
||||
|
||||
CherryPickLogger? logger;
|
||||
CherryPickObserver? observer;
|
||||
|
||||
// Deferred logging flags
|
||||
bool _createdLogged = false;
|
||||
bool _namedLogged = false;
|
||||
bool _singletonLogged = false;
|
||||
|
||||
Binding({this.logger}) {
|
||||
Binding({this.observer}) {
|
||||
_key = T;
|
||||
// Не логируем здесь! Делаем deferred лог после назначения logger
|
||||
// Deferred уведомения observer, не логировать здесь напрямую
|
||||
}
|
||||
|
||||
void markCreated() {
|
||||
if (!_createdLogged) {
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: _name != null ? {'name': _name} : null,
|
||||
description: 'created',
|
||||
));
|
||||
observer?.onBindingRegistered(
|
||||
runtimeType.toString(),
|
||||
T,
|
||||
);
|
||||
_createdLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void markNamed() {
|
||||
if (isNamed && !_namedLogged) {
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {'name': _name},
|
||||
description: 'named',
|
||||
));
|
||||
observer?.onDiagnostic(
|
||||
'Binding named: ${T.toString()} name: $_name',
|
||||
details: {
|
||||
'type': 'Binding',
|
||||
'name': T.toString(),
|
||||
'nameParam': _name,
|
||||
'description': 'named',
|
||||
},
|
||||
);
|
||||
_namedLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void markSingleton() {
|
||||
if (isSingleton && !_singletonLogged) {
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: _name != null ? {'name': _name} : null,
|
||||
description: 'singleton mode enabled',
|
||||
));
|
||||
observer?.onDiagnostic(
|
||||
'Binding singleton: ${T.toString()}${_name != null ? ' name: $_name' : ''}',
|
||||
details: {
|
||||
'type': 'Binding',
|
||||
'name': T.toString(),
|
||||
if (_name != null) 'name': _name,
|
||||
'description': 'singleton mode enabled',
|
||||
},
|
||||
);
|
||||
_singletonLogged = true;
|
||||
}
|
||||
}
|
||||
@@ -170,25 +173,23 @@ class Binding<T> {
|
||||
T? resolveSync([dynamic params]) {
|
||||
final res = resolver?.resolveSync(params);
|
||||
if (res != null) {
|
||||
logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
observer?.onDiagnostic(
|
||||
'Binding resolved instance: ${T.toString()}',
|
||||
details: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveSync',
|
||||
'description': 'object created/resolved',
|
||||
},
|
||||
description: 'object created/resolved',
|
||||
));
|
||||
);
|
||||
} else {
|
||||
logger?.warn(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
observer?.onWarning(
|
||||
'resolveSync returned null: ${T.toString()}',
|
||||
details: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveSync',
|
||||
'description': 'resolveSync returned null',
|
||||
},
|
||||
description: 'resolveSync returned null',
|
||||
));
|
||||
);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -197,38 +198,28 @@ class Binding<T> {
|
||||
final future = resolver?.resolveAsync(params);
|
||||
if (future != null) {
|
||||
future
|
||||
.then((res) => logger?.info(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
.then((res) => observer?.onDiagnostic(
|
||||
'Future resolved for: ${T.toString()}',
|
||||
details: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveAsync',
|
||||
'description': 'Future resolved',
|
||||
},
|
||||
description: 'Future resolved',
|
||||
)))
|
||||
.catchError((e, s) => logger?.error(
|
||||
formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveAsync',
|
||||
},
|
||||
description: 'resolveAsync error',
|
||||
),
|
||||
))
|
||||
.catchError((e, s) => observer?.onError(
|
||||
'resolveAsync error: ${T.toString()}',
|
||||
e,
|
||||
s,
|
||||
));
|
||||
} else {
|
||||
logger?.warn(formatLogMessage(
|
||||
type: 'Binding',
|
||||
name: T.toString(),
|
||||
params: {
|
||||
observer?.onWarning(
|
||||
'resolveAsync returned null: ${T.toString()}',
|
||||
details: {
|
||||
if (_name != null) 'name': _name,
|
||||
'method': 'resolveAsync',
|
||||
'description': 'resolveAsync returned null',
|
||||
},
|
||||
description: 'resolveAsync returned null',
|
||||
));
|
||||
);
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
//
|
||||
|
||||
import 'dart:collection';
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
|
||||
/// RU: Исключение, выбрасываемое при обнаружении циклической зависимости.
|
||||
/// ENG: Exception thrown when a circular dependency is detected.
|
||||
@@ -36,11 +35,11 @@ class CircularDependencyException implements Exception {
|
||||
/// RU: Детектор циклических зависимостей для CherryPick DI контейнера.
|
||||
/// ENG: Circular dependency detector for CherryPick DI container.
|
||||
class CycleDetector {
|
||||
final CherryPickLogger _logger;
|
||||
final CherryPickObserver _observer;
|
||||
final Set<String> _resolutionStack = HashSet<String>();
|
||||
final List<String> _resolutionHistory = [];
|
||||
|
||||
CycleDetector({required CherryPickLogger logger}): _logger = logger;
|
||||
CycleDetector({required CherryPickObserver observer}) : _observer = observer;
|
||||
|
||||
/// RU: Начинает отслеживание разрешения зависимости.
|
||||
/// ENG: Starts tracking dependency resolution.
|
||||
@@ -48,25 +47,24 @@ class CycleDetector {
|
||||
/// Throws [CircularDependencyException] if circular dependency is detected.
|
||||
void startResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
print('[DEBUG] CycleDetector logger type=${_logger.runtimeType} hash=${_logger.hashCode}');
|
||||
_logger.info(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'event': 'startResolving', 'stackSize': _resolutionStack.length},
|
||||
description: 'start resolving',
|
||||
));
|
||||
_observer.onDiagnostic(
|
||||
'CycleDetector startResolving: $dependencyKey',
|
||||
details: {
|
||||
'event': 'startResolving',
|
||||
'stackSize': _resolutionStack.length,
|
||||
},
|
||||
);
|
||||
if (_resolutionStack.contains(dependencyKey)) {
|
||||
// Найдена циклическая зависимость
|
||||
final cycleStartIndex = _resolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _resolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
||||
// print removed (trace)
|
||||
final msg = formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
_observer.onCycleDetected(
|
||||
cycle,
|
||||
);
|
||||
_observer.onError(
|
||||
'Cycle detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
_logger.error(msg);
|
||||
throw CircularDependencyException(
|
||||
'Circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -81,12 +79,10 @@ class CycleDetector {
|
||||
/// ENG: Finishes tracking dependency resolution.
|
||||
void finishResolving<T>({String? named}) {
|
||||
final dependencyKey = _createDependencyKey<T>(named);
|
||||
_logger.info(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'event': 'finishResolving'},
|
||||
description: 'finish resolving',
|
||||
));
|
||||
_observer.onDiagnostic(
|
||||
'CycleDetector finishResolving: $dependencyKey',
|
||||
details: {'event': 'finishResolving'},
|
||||
);
|
||||
_resolutionStack.remove(dependencyKey);
|
||||
// Удаляем из истории только если это последний элемент
|
||||
if (_resolutionHistory.isNotEmpty &&
|
||||
@@ -98,11 +94,13 @@ class CycleDetector {
|
||||
/// RU: Очищает все состояние детектора.
|
||||
/// ENG: Clears all detector state.
|
||||
void clear() {
|
||||
_logger.info(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
params: {'event': 'clear'},
|
||||
description: 'resolution stack cleared',
|
||||
));
|
||||
_observer.onDiagnostic(
|
||||
'CycleDetector clear',
|
||||
details: {
|
||||
'event': 'clear',
|
||||
'description': 'resolution stack cleared',
|
||||
},
|
||||
);
|
||||
_resolutionStack.clear();
|
||||
_resolutionHistory.clear();
|
||||
}
|
||||
@@ -130,28 +128,32 @@ class CycleDetector {
|
||||
/// ENG: Mixin for adding circular dependency detection support.
|
||||
mixin CycleDetectionMixin {
|
||||
CycleDetector? _cycleDetector;
|
||||
CherryPickLogger get logger;
|
||||
CherryPickObserver get observer;
|
||||
|
||||
/// RU: Включает обнаружение циклических зависимостей.
|
||||
/// ENG: Enables circular dependency detection.
|
||||
void enableCycleDetection() {
|
||||
_cycleDetector = CycleDetector(logger: logger);
|
||||
logger.info(formatLogMessage(
|
||||
type: 'CycleDetection',
|
||||
params: {'event': 'enable'},
|
||||
description: 'cycle detection enabled',
|
||||
));
|
||||
_cycleDetector = CycleDetector(observer: observer);
|
||||
observer.onDiagnostic(
|
||||
'CycleDetection enabled',
|
||||
details: {
|
||||
'event': 'enable',
|
||||
'description': 'cycle detection enabled',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// RU: Отключает обнаружение циклических зависимостей.
|
||||
/// ENG: Disables circular dependency detection.
|
||||
void disableCycleDetection() {
|
||||
_cycleDetector?.clear();
|
||||
logger.info(formatLogMessage(
|
||||
type: 'CycleDetection',
|
||||
params: {'event': 'disable'},
|
||||
description: 'cycle detection disabled',
|
||||
));
|
||||
observer.onDiagnostic(
|
||||
'CycleDetection disabled',
|
||||
details: {
|
||||
'event': 'disable',
|
||||
'description': 'cycle detection disabled',
|
||||
},
|
||||
);
|
||||
_cycleDetector = null;
|
||||
}
|
||||
|
||||
@@ -178,12 +180,14 @@ mixin CycleDetectionMixin {
|
||||
final cycleStartIndex = _cycleDetector!._resolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _cycleDetector!._resolutionHistory.sublist(cycleStartIndex)
|
||||
..add(dependencyKey);
|
||||
logger.error(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
));
|
||||
observer.onCycleDetected(
|
||||
cycle,
|
||||
);
|
||||
observer.onError(
|
||||
'Cycle detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
throw CircularDependencyException(
|
||||
'Circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
import 'dart:collection';
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
|
||||
|
||||
/// RU: Глобальный детектор циклических зависимостей для всей иерархии скоупов.
|
||||
@@ -21,7 +20,7 @@ import 'package:cherrypick/src/log_format.dart';
|
||||
class GlobalCycleDetector {
|
||||
static GlobalCycleDetector? _instance;
|
||||
|
||||
final CherryPickLogger _logger;
|
||||
final CherryPickObserver _observer;
|
||||
|
||||
// Глобальный стек разрешения зависимостей
|
||||
final Set<String> _globalResolutionStack = HashSet<String>();
|
||||
@@ -32,12 +31,12 @@ class GlobalCycleDetector {
|
||||
// Карта активных детекторов по скоупам
|
||||
final Map<String, CycleDetector> _scopeDetectors = HashMap<String, CycleDetector>();
|
||||
|
||||
GlobalCycleDetector._internal({required CherryPickLogger logger}): _logger = logger;
|
||||
GlobalCycleDetector._internal({required CherryPickObserver observer}): _observer = observer;
|
||||
|
||||
/// RU: Получить единственный экземпляр глобального детектора.
|
||||
/// ENG: Get singleton instance of global detector.
|
||||
static GlobalCycleDetector get instance {
|
||||
_instance ??= GlobalCycleDetector._internal(logger: CherryPick.globalLogger);
|
||||
_instance ??= GlobalCycleDetector._internal(observer: CherryPick.globalObserver);
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@@ -59,12 +58,15 @@ class GlobalCycleDetector {
|
||||
// Найдена глобальная циклическая зависимость
|
||||
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)..add(dependencyKey);
|
||||
_logger.error(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
));
|
||||
_observer.onCycleDetected(
|
||||
cycle,
|
||||
scopeName: scopeId,
|
||||
);
|
||||
_observer.onError(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
throw CircularDependencyException(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -102,12 +104,15 @@ class GlobalCycleDetector {
|
||||
final cycleStartIndex = _globalResolutionHistory.indexOf(dependencyKey);
|
||||
final cycle = _globalResolutionHistory.sublist(cycleStartIndex)
|
||||
..add(dependencyKey);
|
||||
_logger.error(formatLogMessage(
|
||||
type: 'CycleDetector',
|
||||
name: dependencyKey.toString(),
|
||||
params: {'chain': cycle.join('->')},
|
||||
description: 'cycle detected',
|
||||
));
|
||||
_observer.onCycleDetected(
|
||||
cycle,
|
||||
scopeName: scopeId,
|
||||
);
|
||||
_observer.onError(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
throw CircularDependencyException(
|
||||
'Global circular dependency detected for $dependencyKey',
|
||||
cycle,
|
||||
@@ -131,7 +136,7 @@ class GlobalCycleDetector {
|
||||
/// RU: Получить детектор для конкретного скоупа.
|
||||
/// ENG: Get detector for specific scope.
|
||||
CycleDetector getScopeDetector(String scopeId) {
|
||||
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(logger: CherryPick.globalLogger));
|
||||
return _scopeDetectors.putIfAbsent(scopeId, () => CycleDetector(observer: CherryPick.globalObserver));
|
||||
}
|
||||
|
||||
/// RU: Удалить детектор для скоупа.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import 'package:cherrypick/src/scope.dart';
|
||||
import 'package:cherrypick/src/global_cycle_detector.dart';
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Scope? _rootScope;
|
||||
/// Global logger for all [Scope]s managed by [CherryPick].
|
||||
///
|
||||
/// Defaults to [SilentLogger] unless set via [setGlobalLogger].
|
||||
CherryPickLogger _globalLogger = const SilentLogger();
|
||||
CherryPickObserver _globalObserver = SilentCherryPickObserver();
|
||||
|
||||
/// Whether global local-cycle detection is enabled for all Scopes ([Scope.enableCycleDetection]).
|
||||
bool _globalCycleDetectionEnabled = false;
|
||||
@@ -59,12 +59,12 @@ class CherryPick {
|
||||
/// ```dart
|
||||
/// CherryPick.setGlobalLogger(DefaultLogger());
|
||||
/// ```
|
||||
static void setGlobalLogger(CherryPickLogger logger) {
|
||||
_globalLogger = logger;
|
||||
static void setGlobalObserver(CherryPickObserver observer) {
|
||||
_globalObserver = observer;
|
||||
}
|
||||
|
||||
/// Returns the current global logger used by CherryPick.
|
||||
static CherryPickLogger get globalLogger => _globalLogger;
|
||||
static CherryPickObserver get globalObserver => _globalObserver;
|
||||
|
||||
/// Returns the singleton root [Scope], creating it if needed.
|
||||
///
|
||||
@@ -75,7 +75,7 @@ class CherryPick {
|
||||
/// final root = CherryPick.openRootScope();
|
||||
/// ```
|
||||
static Scope openRootScope() {
|
||||
_rootScope ??= Scope(null, logger: _globalLogger);
|
||||
_rootScope ??= Scope(null, observer: _globalObserver);
|
||||
// Apply cycle detection settings
|
||||
if (_globalCycleDetectionEnabled && !_rootScope!.isCycleDetectionEnabled) {
|
||||
_rootScope!.enableCycleDetection();
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// CherryPickLogger — интерфейс для логирования событий DI в CherryPick.
|
||||
///
|
||||
/// ENGLISH:
|
||||
/// Interface for dependency injection (DI) logger in CherryPick. Allows you to
|
||||
/// receive information about the internal events and errors in the DI system.
|
||||
/// Your implementation can use any logging framework or UI.
|
||||
///
|
||||
/// RUSSIAN:
|
||||
/// Интерфейс логгера для DI-контейнера CherryPick. Позволяет получать
|
||||
/// сообщения о работе DI-контейнера, его ошибках и событиях, и
|
||||
/// интегрировать любые готовые решения для логирования/сбора ошибок.
|
||||
/// ----------------------------------------------------------------------------
|
||||
abstract class CherryPickLogger {
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// info — Информационное сообщение.
|
||||
///
|
||||
/// ENGLISH:
|
||||
/// Logs an informational message about DI operation or state.
|
||||
///
|
||||
/// RUSSIAN:
|
||||
/// Логирование информационного сообщения о событиях DI.
|
||||
/// ----------------------------------------------------------------------------
|
||||
void info(String message);
|
||||
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// warn — Предупреждение.
|
||||
///
|
||||
/// ENGLISH:
|
||||
/// Logs a warning related to DI events (for example, possible misconfiguration).
|
||||
///
|
||||
/// RUSSIAN:
|
||||
/// Логирование предупреждения, связанного с DI (например, возможная ошибка
|
||||
/// конфигурации).
|
||||
/// ----------------------------------------------------------------------------
|
||||
void warn(String message);
|
||||
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// error — Ошибка.
|
||||
///
|
||||
/// ENGLISH:
|
||||
/// Logs an error message, may include error object and stack trace.
|
||||
///
|
||||
/// RUSSIAN:
|
||||
/// Логирование ошибки, дополнительно может содержать объект ошибки
|
||||
/// и StackTrace.
|
||||
/// ----------------------------------------------------------------------------
|
||||
void error(String message, [Object? error, StackTrace? stackTrace]);
|
||||
}
|
||||
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// SilentLogger — «тихий» логгер CherryPick. Сообщения игнорируются.
|
||||
///
|
||||
/// ENGLISH:
|
||||
/// SilentLogger ignores all log messages. Used by default in production to
|
||||
/// avoid polluting logs with DI events.
|
||||
///
|
||||
/// RUSSIAN:
|
||||
/// SilentLogger игнорирует все события логгирования. Используется по умолчанию
|
||||
/// на production, чтобы не засорять логи техническими сообщениями DI.
|
||||
/// ----------------------------------------------------------------------------
|
||||
class SilentLogger implements CherryPickLogger {
|
||||
const SilentLogger();
|
||||
@override
|
||||
void info(String message) {}
|
||||
@override
|
||||
void warn(String message) {}
|
||||
@override
|
||||
void error(String message, [Object? error, StackTrace? stackTrace]) {}
|
||||
}
|
||||
|
||||
/// ----------------------------------------------------------------------------
|
||||
/// PrintLogger — логгер CherryPick, выводящий все сообщения через print.
|
||||
///
|
||||
/// ENGLISH:
|
||||
/// PrintLogger outputs all log messages to the console using `print()`.
|
||||
/// Suitable for debugging, prototyping, or simple console applications.
|
||||
///
|
||||
/// RUSSIAN:
|
||||
/// PrintLogger выводит все сообщения (info, warn, error) в консоль через print.
|
||||
/// Удобен для отладки или консольных приложений.
|
||||
/// ----------------------------------------------------------------------------
|
||||
class PrintLogger implements CherryPickLogger {
|
||||
const PrintLogger();
|
||||
@override
|
||||
void info(String message) => print('[info][CherryPick] $message');
|
||||
@override
|
||||
void warn(String message) => print('[warn][CherryPick] $message');
|
||||
@override
|
||||
void error(String message, [Object? error, StackTrace? stackTrace]) {
|
||||
print('[error][CherryPick] $message');
|
||||
if (error != null) print(' error: $error');
|
||||
if (stackTrace != null) print(' stack: $stackTrace');
|
||||
}
|
||||
}
|
||||
119
cherrypick/lib/src/observer.dart
Normal file
119
cherrypick/lib/src/observer.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
/// Observer for DI container (CherryPick): lifecycle, cache, modules, errors, etc.
|
||||
abstract class CherryPickObserver {
|
||||
// === Registration and instance lifecycle ===
|
||||
void onBindingRegistered(String name, Type type, {String? scopeName});
|
||||
void onInstanceRequested(String name, Type type, {String? scopeName});
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName});
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName});
|
||||
|
||||
// === Module events ===
|
||||
void onModulesInstalled(List<String> moduleNames, {String? scopeName});
|
||||
void onModulesRemoved(List<String> moduleNames, {String? scopeName});
|
||||
|
||||
// === Scope lifecycle ===
|
||||
void onScopeOpened(String name);
|
||||
void onScopeClosed(String name);
|
||||
|
||||
// === Cycle detection ===
|
||||
void onCycleDetected(List<String> chain, {String? scopeName});
|
||||
|
||||
// === Cache events ===
|
||||
void onCacheHit(String name, Type type, {String? scopeName});
|
||||
void onCacheMiss(String name, Type type, {String? scopeName});
|
||||
|
||||
// === Диагностика ===
|
||||
void onDiagnostic(String message, {Object? details});
|
||||
|
||||
// === Warnings & errors ===
|
||||
void onWarning(String message, {Object? details});
|
||||
void onError(String message, Object? error, StackTrace? stackTrace);
|
||||
}
|
||||
|
||||
/// Diagnostic/Debug observer that prints all events
|
||||
class PrintCherryPickObserver implements CherryPickObserver {
|
||||
@override
|
||||
void onBindingRegistered(String name, Type type, {String? scopeName}) =>
|
||||
print('[binding][CherryPick] $name — $type (scope: $scopeName)');
|
||||
|
||||
@override
|
||||
void onInstanceRequested(String name, Type type, {String? scopeName}) =>
|
||||
print('[request][CherryPick] $name — $type (scope: $scopeName)');
|
||||
|
||||
@override
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) =>
|
||||
print('[create][CherryPick] $name — $type => $instance (scope: $scopeName)');
|
||||
|
||||
@override
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) =>
|
||||
print('[dispose][CherryPick] $name — $type => $instance (scope: $scopeName)');
|
||||
|
||||
@override
|
||||
void onModulesInstalled(List<String> modules, {String? scopeName}) =>
|
||||
print('[modules installed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
|
||||
@override
|
||||
void onModulesRemoved(List<String> modules, {String? scopeName}) =>
|
||||
print('[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
|
||||
|
||||
@override
|
||||
void onScopeOpened(String name) => print('[scope opened][CherryPick] $name');
|
||||
|
||||
@override
|
||||
void onScopeClosed(String name) => print('[scope closed][CherryPick] $name');
|
||||
|
||||
@override
|
||||
void onCycleDetected(List<String> chain, {String? scopeName}) =>
|
||||
print('[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
|
||||
|
||||
@override
|
||||
void onCacheHit(String name, Type type, {String? scopeName}) =>
|
||||
print('[cache hit][CherryPick] $name — $type (scope: $scopeName)');
|
||||
@override
|
||||
void onCacheMiss(String name, Type type, {String? scopeName}) =>
|
||||
print('[cache miss][CherryPick] $name — $type (scope: $scopeName)');
|
||||
|
||||
@override
|
||||
void onDiagnostic(String message, {Object? details}) =>
|
||||
print('[diagnostic][CherryPick] $message ${details ?? ''}');
|
||||
|
||||
@override
|
||||
void onWarning(String message, {Object? details}) =>
|
||||
print('[warn][CherryPick] $message ${details ?? ''}');
|
||||
@override
|
||||
void onError(String message, Object? error, StackTrace? stackTrace) {
|
||||
print('[error][CherryPick] $message');
|
||||
if (error != null) print(' error: $error');
|
||||
if (stackTrace != null) print(' stack: $stackTrace');
|
||||
}
|
||||
}
|
||||
|
||||
/// Silent observer: игнорирует все события
|
||||
class SilentCherryPickObserver implements CherryPickObserver {
|
||||
@override
|
||||
void onBindingRegistered(String name, Type type, {String? scopeName}) {}
|
||||
@override
|
||||
void onInstanceRequested(String name, Type type, {String? scopeName}) {}
|
||||
@override
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {}
|
||||
@override
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {}
|
||||
@override
|
||||
void onModulesInstalled(List<String> modules, {String? scopeName}) {}
|
||||
@override
|
||||
void onModulesRemoved(List<String> modules, {String? scopeName}) {}
|
||||
@override
|
||||
void onScopeOpened(String name) {}
|
||||
@override
|
||||
void onScopeClosed(String name) {}
|
||||
@override
|
||||
void onCycleDetected(List<String> chain, {String? scopeName}) {}
|
||||
@override
|
||||
void onCacheHit(String name, Type type, {String? scopeName}) {}
|
||||
@override
|
||||
void onCacheMiss(String name, Type type, {String? scopeName}) {}
|
||||
@override
|
||||
void onDiagnostic(String message, {Object? details}) {}
|
||||
@override
|
||||
void onWarning(String message, {Object? details}) {}
|
||||
@override
|
||||
void onError(String message, Object? error, StackTrace? stackTrace) {}
|
||||
}
|
||||
@@ -18,16 +18,16 @@ import 'package:cherrypick/src/disposable.dart';
|
||||
import 'package:cherrypick/src/global_cycle_detector.dart';
|
||||
import 'package:cherrypick/src/binding_resolver.dart';
|
||||
import 'package:cherrypick/src/module.dart';
|
||||
import 'package:cherrypick/src/logger.dart';
|
||||
import 'package:cherrypick/src/log_format.dart';
|
||||
import 'package:cherrypick/src/observer.dart';
|
||||
// import 'package:cherrypick/src/log_format.dart';
|
||||
|
||||
class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
final Scope? _parentScope;
|
||||
|
||||
late final CherryPickLogger _logger;
|
||||
late final CherryPickObserver _observer;
|
||||
|
||||
@override
|
||||
CherryPickLogger get logger => _logger;
|
||||
CherryPickObserver get observer => _observer;
|
||||
|
||||
/// COLLECTS all resolved instances that implement [Disposable].
|
||||
final Set<Disposable> _disposables = HashSet();
|
||||
@@ -41,16 +41,17 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
|
||||
final Map<String, Scope> _scopeMap = HashMap();
|
||||
|
||||
Scope(this._parentScope, {required CherryPickLogger logger}) : _logger = logger {
|
||||
Scope(this._parentScope, {required CherryPickObserver observer}) : _observer = observer {
|
||||
setScopeId(_generateScopeId());
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId ?? 'NO_ID',
|
||||
params: {
|
||||
observer.onDiagnostic(
|
||||
'Scope created: ${scopeId ?? 'NO_ID'}',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId ?? 'NO_ID',
|
||||
if (_parentScope?.scopeId != null) 'parent': _parentScope!.scopeId,
|
||||
'description': 'scope created',
|
||||
},
|
||||
description: 'scope created',
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
final Set<Module> _modulesList = HashSet();
|
||||
@@ -75,7 +76,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// return [Scope]
|
||||
Scope openSubScope(String name) {
|
||||
if (!_scopeMap.containsKey(name)) {
|
||||
final childScope = Scope(this, logger: logger); // Наследуем логгер вниз по иерархии
|
||||
final childScope = Scope(this, observer: observer); // Наследуем observer вниз по иерархии
|
||||
// print removed (trace)
|
||||
// Наследуем настройки обнаружения циклических зависимостей
|
||||
if (isCycleDetectionEnabled) {
|
||||
@@ -85,15 +86,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
childScope.enableGlobalCycleDetection();
|
||||
}
|
||||
_scopeMap[name] = childScope;
|
||||
logger.info(formatLogMessage(
|
||||
type: 'SubScope',
|
||||
name: name,
|
||||
params: {
|
||||
observer.onDiagnostic(
|
||||
'SubScope created: $name',
|
||||
details: {
|
||||
'type': 'SubScope',
|
||||
'name': name,
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
'description': 'subscope created',
|
||||
},
|
||||
description: 'subscope created',
|
||||
));
|
||||
);
|
||||
}
|
||||
return _scopeMap[name]!;
|
||||
}
|
||||
@@ -111,15 +113,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
if (childScope.scopeId != null) {
|
||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||
}
|
||||
logger.info(formatLogMessage(
|
||||
type: 'SubScope',
|
||||
name: name,
|
||||
params: {
|
||||
observer.onDiagnostic(
|
||||
'SubScope closed: $name',
|
||||
details: {
|
||||
'type': 'SubScope',
|
||||
'name': name,
|
||||
'id': childScope.scopeId,
|
||||
if (scopeId != null) 'parent': scopeId,
|
||||
'description': 'subscope closed',
|
||||
},
|
||||
description: 'subscope closed',
|
||||
));
|
||||
);
|
||||
}
|
||||
_scopeMap.remove(name);
|
||||
}
|
||||
@@ -132,18 +135,19 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
Scope installModules(List<Module> modules) {
|
||||
_modulesList.addAll(modules);
|
||||
for (var module in modules) {
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Module',
|
||||
name: module.runtimeType.toString(),
|
||||
params: {
|
||||
observer.onDiagnostic(
|
||||
'Module installed: ${module.runtimeType}',
|
||||
details: {
|
||||
'type': 'Module',
|
||||
'name': module.runtimeType.toString(),
|
||||
'scope': scopeId,
|
||||
'description': 'module installed',
|
||||
},
|
||||
description: 'module installed',
|
||||
));
|
||||
);
|
||||
module.builder(this);
|
||||
// После builder: для всех новых биндингов
|
||||
for (final binding in module.bindingSet) {
|
||||
binding.logger = logger;
|
||||
binding.observer = observer;
|
||||
binding.logAllDeferred();
|
||||
}
|
||||
}
|
||||
@@ -157,11 +161,14 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
///
|
||||
/// return [Scope]
|
||||
Scope dropModules() {
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
description: 'modules dropped',
|
||||
));
|
||||
observer.onDiagnostic(
|
||||
'Modules dropped for scope: $scopeId',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId,
|
||||
'description': 'modules dropped',
|
||||
},
|
||||
);
|
||||
_modulesList.clear();
|
||||
_rebuildResolversIndex();
|
||||
return this;
|
||||
@@ -187,13 +194,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
} catch (e, s) {
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {'resolve': T.toString()},
|
||||
description: 'global cycle detection failed during resolve',
|
||||
),
|
||||
observer.onError(
|
||||
'Global cycle detection failed during resolve: $T',
|
||||
e,
|
||||
s,
|
||||
);
|
||||
@@ -203,13 +205,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
try {
|
||||
result = _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
} catch (e, s) {
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {'resolve': T.toString()},
|
||||
description: 'failed to resolve',
|
||||
),
|
||||
observer.onError(
|
||||
'Failed to resolve: $T',
|
||||
e,
|
||||
s,
|
||||
);
|
||||
@@ -226,27 +223,22 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return withCycleDetection<T>(T, named, () {
|
||||
var resolved = _tryResolveInternal<T>(named: named, params: params);
|
||||
if (resolved != null) {
|
||||
logger.info(formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {
|
||||
observer.onDiagnostic(
|
||||
'Successfully resolved: $T',
|
||||
details: {
|
||||
'type': 'Scope',
|
||||
'name': scopeId,
|
||||
'resolve': T.toString(),
|
||||
if (named != null) 'named': named,
|
||||
'description': 'successfully resolved',
|
||||
},
|
||||
description: 'successfully resolved',
|
||||
));
|
||||
);
|
||||
return resolved;
|
||||
} else {
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
type: 'Scope',
|
||||
name: scopeId,
|
||||
params: {
|
||||
'resolve': T.toString(),
|
||||
if (named != null) 'named': named,
|
||||
},
|
||||
description: 'failed to resolve',
|
||||
),
|
||||
observer.onError(
|
||||
'Failed to resolve: $T',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
throw StateError(
|
||||
'Can\'t resolve dependency `$T`. Maybe you forget register it?');
|
||||
|
||||
@@ -23,38 +23,27 @@ class CyclicModule extends Module {
|
||||
}
|
||||
|
||||
void main() {
|
||||
late MockLogger logger;
|
||||
late MockObserver observer;
|
||||
|
||||
setUp(() {
|
||||
logger = MockLogger();
|
||||
observer = MockObserver();
|
||||
});
|
||||
|
||||
test('Global logger receives Scope and Binding events', () {
|
||||
final scope = Scope(null, logger: logger);
|
||||
test('Global logger receives Binding events', () {
|
||||
final scope = Scope(null, observer: observer);
|
||||
scope.installModules([DummyModule()]);
|
||||
final _ = scope.resolve<DummyService>(named: 'test');
|
||||
|
||||
// Новый стиль проверки для formatLogMessage:
|
||||
// Проверяем, что биндинг DummyService зарегистрирован
|
||||
expect(
|
||||
logger.infos.any((m) => m.startsWith('[Scope:') && m.contains('created')),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
logger.infos.any((m) => m.startsWith('[Binding:DummyService') && m.contains('created')),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
logger.infos.any((m) => m.startsWith('[Binding:DummyService') && m.contains('named') && m.contains('name=test')),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
logger.infos.any((m) => m.startsWith('[Scope:') && m.contains('resolve=DummyService') && m.contains('successfully resolved')),
|
||||
observer.bindings.any((m) => m.contains('DummyService')),
|
||||
isTrue,
|
||||
);
|
||||
// Можно добавить проверки diagnostics, если Scope что-то пишет туда
|
||||
});
|
||||
|
||||
test('CycleDetector logs cycle detection error', () {
|
||||
final scope = Scope(null, logger: logger);
|
||||
final scope = Scope(null, observer: observer);
|
||||
// print('[DEBUG] TEST SCOPE logger type=${scope.logger.runtimeType} hash=${scope.logger.hashCode}');
|
||||
scope.enableCycleDetection();
|
||||
scope.installModules([CyclicModule()]);
|
||||
@@ -62,12 +51,11 @@ void main() {
|
||||
() => scope.resolve<A>(),
|
||||
throwsA(isA<CircularDependencyException>()),
|
||||
);
|
||||
// Дополнительно ищем и среди info на случай если лог от CycleDetector ошибочно не попал в errors
|
||||
final foundInErrors = logger.errors.any((m) =>
|
||||
m.startsWith('[CycleDetector:') && m.contains('cycle detected'));
|
||||
final foundInInfos = logger.infos.any((m) =>
|
||||
m.startsWith('[CycleDetector:') && m.contains('cycle detected'));
|
||||
expect(foundInErrors || foundInInfos, isTrue,
|
||||
reason: 'Ожидаем хотя бы один лог о цикле на уровне error или info; вот все errors: ${logger.errors}\ninfos: ${logger.infos}');
|
||||
// Проверяем, что цикл зафиксирован либо в errors, либо в diagnostics либо cycles
|
||||
final foundInErrors = observer.errors.any((m) => m.contains('cycle detected'));
|
||||
final foundInDiagnostics = observer.diagnostics.any((m) => m.contains('cycle detected'));
|
||||
final foundCycleNotified = observer.cycles.isNotEmpty;
|
||||
expect(foundInErrors || foundInDiagnostics || foundCycleNotified, isTrue,
|
||||
reason: 'Ожидаем хотя бы один лог о цикле! errors: ${observer.errors}\ndiag: ${observer.diagnostics}\ncycles: ${observer.cycles}');
|
||||
});
|
||||
}
|
||||
@@ -1,16 +1,48 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
|
||||
class MockLogger implements CherryPickLogger {
|
||||
final List<String> infos = [];
|
||||
final List<String> warns = [];
|
||||
class MockObserver implements CherryPickObserver {
|
||||
final List<String> diagnostics = [];
|
||||
final List<String> warnings = [];
|
||||
final List<String> errors = [];
|
||||
final List<List<String>> cycles = [];
|
||||
final List<String> bindings = [];
|
||||
|
||||
@override
|
||||
void info(String message) => infos.add(message);
|
||||
void onDiagnostic(String message, {Object? details}) =>
|
||||
diagnostics.add(message);
|
||||
|
||||
@override
|
||||
void warn(String message) => warns.add(message);
|
||||
void onWarning(String message, {Object? details}) => warnings.add(message);
|
||||
|
||||
@override
|
||||
void error(String message, [Object? e, StackTrace? s]) =>
|
||||
void onError(String message, Object? error, StackTrace? stackTrace) =>
|
||||
errors.add(
|
||||
'$message${e != null ? ' $e' : ''}${s != null ? '\n$s' : ''}');
|
||||
'$message${error != null ? ' $error' : ''}${stackTrace != null ? '\n$stackTrace' : ''}');
|
||||
|
||||
@override
|
||||
void onCycleDetected(List<String> chain, {String? scopeName}) =>
|
||||
cycles.add(chain);
|
||||
|
||||
@override
|
||||
void onBindingRegistered(String name, Type type, {String? scopeName}) =>
|
||||
bindings.add('$name $type');
|
||||
|
||||
@override
|
||||
void onInstanceRequested(String name, Type type, {String? scopeName}) {}
|
||||
@override
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {}
|
||||
@override
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {}
|
||||
@override
|
||||
void onModulesInstalled(List<String> moduleNames, {String? scopeName}) {}
|
||||
@override
|
||||
void onModulesRemoved(List<String> moduleNames, {String? scopeName}) {}
|
||||
@override
|
||||
void onScopeOpened(String name) {}
|
||||
@override
|
||||
void onScopeClosed(String name) {}
|
||||
@override
|
||||
void onCacheHit(String name, Type type, {String? scopeName}) {}
|
||||
@override
|
||||
void onCacheMiss(String name, Type type, {String? scopeName}) {}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import 'package:cherrypick/cherrypick.dart';
|
||||
import '../mock_logger.dart';
|
||||
|
||||
void main() {
|
||||
late MockLogger logger;
|
||||
late MockObserver observer;
|
||||
setUp(() {
|
||||
logger = MockLogger();
|
||||
CherryPick.setGlobalLogger(logger);
|
||||
observer = MockObserver();
|
||||
CherryPick.setGlobalObserver(observer);
|
||||
});
|
||||
group('CycleDetector', () {
|
||||
late CycleDetector detector;
|
||||
|
||||
setUp(() {
|
||||
detector = CycleDetector(logger: logger);
|
||||
detector = CycleDetector(observer: observer);
|
||||
});
|
||||
|
||||
test('should detect simple circular dependency', () {
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'package:test/test.dart';
|
||||
import '../mock_logger.dart';
|
||||
|
||||
void main() {
|
||||
late MockLogger logger;
|
||||
late MockObserver observer;
|
||||
setUp(() {
|
||||
logger = MockLogger();
|
||||
CherryPick.setGlobalLogger(logger);
|
||||
observer = MockObserver();
|
||||
CherryPick.setGlobalObserver(observer);
|
||||
});
|
||||
group('CherryPick Cycle Detection Helper Methods', () {
|
||||
setUp(() {
|
||||
|
||||
@@ -110,14 +110,14 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Scope & Subscope Management', () {
|
||||
test('Scope has no parent if constructed with null', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(scope.parentScope, null);
|
||||
});
|
||||
test('Can open and retrieve the same subScope by key', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(Scope(scope, observer: observer), isNotNull); // эквивалент
|
||||
});
|
||||
test('closeSubScope removes subscope so next openSubScope returns new', () async {
|
||||
final logger = MockLogger();
|
||||
@@ -130,9 +130,9 @@ void main() {
|
||||
});
|
||||
|
||||
test('closeSubScope removes subscope so next openSubScope returns new', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(Scope(scope, observer: observer), isNotNull); // эквивалент
|
||||
// Нет необходимости тестировать open/closeSubScope в этом юните
|
||||
});
|
||||
});
|
||||
@@ -140,48 +140,48 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Dependency Resolution (standard)', () {
|
||||
test("Throws StateError if value can't be resolved", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
test('Resolves value after adding a dependency', () {
|
||||
final logger = MockLogger();
|
||||
final observer = MockObserver();
|
||||
final expectedValue = 'test string';
|
||||
final scope = Scope(null, logger: logger)
|
||||
final scope = Scope(null, observer: observer)
|
||||
.installModules([TestModule<String>(value: expectedValue)]);
|
||||
expect(scope.resolve<String>(), expectedValue);
|
||||
});
|
||||
test('Returns a value from parent scope', () {
|
||||
final logger = MockLogger();
|
||||
final observer = MockObserver();
|
||||
final expectedValue = 5;
|
||||
final parentScope = Scope(null, logger: logger);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
final parentScope = Scope(null, observer: observer);
|
||||
final scope = Scope(parentScope, observer: observer);
|
||||
|
||||
parentScope.installModules([TestModule<int>(value: expectedValue)]);
|
||||
expect(scope.resolve<int>(), expectedValue);
|
||||
});
|
||||
test('Returns several values from parent container', () {
|
||||
final logger = MockLogger();
|
||||
final observer = MockObserver();
|
||||
final expectedIntValue = 5;
|
||||
final expectedStringValue = 'Hello world';
|
||||
final parentScope = Scope(null, logger: logger).installModules([
|
||||
final parentScope = Scope(null, observer: observer).installModules([
|
||||
TestModule<int>(value: expectedIntValue),
|
||||
TestModule<String>(value: expectedStringValue)
|
||||
]);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
final scope = Scope(parentScope, observer: observer);
|
||||
|
||||
expect(scope.resolve<int>(), expectedIntValue);
|
||||
expect(scope.resolve<String>(), expectedStringValue);
|
||||
});
|
||||
test("Throws StateError if parent hasn't value too", () {
|
||||
final logger = MockLogger();
|
||||
final parentScope = Scope(null, logger: logger);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final parentScope = Scope(null, observer: observer);
|
||||
final scope = Scope(parentScope, observer: observer);
|
||||
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
test("After dropModules resolves fail", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)..installModules([TestModule<int>(value: 5)]);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)..installModules([TestModule<int>(value: 5)]);
|
||||
expect(scope.resolve<int>(), 5);
|
||||
scope.dropModules();
|
||||
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
||||
@@ -191,8 +191,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Named Dependencies', () {
|
||||
test('Resolve named binding', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
TestModule<String>(value: "first"),
|
||||
TestModule<String>(value: "second", name: "special")
|
||||
@@ -201,8 +201,8 @@ void main() {
|
||||
expect(scope.resolve<String>(), "first");
|
||||
});
|
||||
test('Named binding does not clash with unnamed', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
TestModule<String>(value: "foo", name: "bar"),
|
||||
]);
|
||||
@@ -210,8 +210,8 @@ void main() {
|
||||
expect(scope.resolve<String>(named: "bar"), "foo");
|
||||
});
|
||||
test("tryResolve returns null for missing named", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
TestModule<String>(value: "foo"),
|
||||
]);
|
||||
@@ -222,8 +222,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Provider with parameters', () {
|
||||
test('Resolve dependency using providerWithParams', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvideWithParams((param) => (param as int) * 2);
|
||||
@@ -237,8 +237,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Async Resolution', () {
|
||||
test('Resolve async instance', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<String>().toInstance(Future.value('async value'));
|
||||
@@ -247,8 +247,8 @@ void main() {
|
||||
expect(await scope.resolveAsync<String>(), "async value");
|
||||
});
|
||||
test('Resolve async provider', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvide(() async => 7);
|
||||
@@ -257,8 +257,8 @@ void main() {
|
||||
expect(await scope.resolveAsync<int>(), 7);
|
||||
});
|
||||
test('Resolve async provider with param', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvideWithParams((x) async => (x as int) * 3);
|
||||
@@ -268,8 +268,8 @@ void main() {
|
||||
expect(() => scope.resolveAsync<int>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
test('tryResolveAsync returns null for missing', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
final result = await scope.tryResolveAsync<String>();
|
||||
expect(result, isNull);
|
||||
});
|
||||
@@ -278,8 +278,8 @@ void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Optional resolution and error handling', () {
|
||||
test("tryResolve returns null for missing dependency", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
expect(scope.tryResolve<int>(), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,8 @@ import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:talker/talker.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
|
||||
|
||||
import 'domain/repository/post_repository.dart';
|
||||
import 'presentation/bloc/post_bloc.dart';
|
||||
@@ -12,7 +13,7 @@ part 'app.inject.cherrypick.g.dart';
|
||||
|
||||
class TalkerProvider extends InheritedWidget {
|
||||
final Talker talker;
|
||||
const TalkerProvider({required this.talker, required Widget child, Key? key}) : super(key: key, child: child);
|
||||
const TalkerProvider({required this.talker, required super.child, super.key});
|
||||
static Talker of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<TalkerProvider>()!.talker;
|
||||
@override
|
||||
bool updateShouldNotify(TalkerProvider oldWidget) => oldWidget.talker != talker;
|
||||
|
||||
@@ -11,12 +11,12 @@ import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
|
||||
|
||||
void main() {
|
||||
final talker = Talker();
|
||||
final talkerLogger = TalkerCherryPickLogger(talker);
|
||||
final talkerLogger = TalkerCherryPickObserver(talker);
|
||||
|
||||
|
||||
Bloc.observer = TalkerBlocObserver(talker: talker);
|
||||
|
||||
CherryPick.setGlobalLogger(talkerLogger);
|
||||
CherryPick.setGlobalObserver(talkerLogger);
|
||||
// Включаем cycle-detection только в debug/test
|
||||
if (kDebugMode) {
|
||||
CherryPick.enableGlobalCycleDetection();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:postly/app.dart';
|
||||
|
||||
import '../../router/app_router.gr.dart';
|
||||
import '../bloc/post_bloc.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import '../presentation/pages/logs_page.dart';
|
||||
import 'app_router.gr.dart';
|
||||
|
||||
@AutoRouterConfig()
|
||||
|
||||
@@ -3,11 +3,11 @@ import 'package:talker/talker.dart';
|
||||
|
||||
void main() {
|
||||
final talker = Talker();
|
||||
final logger = TalkerCherryPickLogger(talker);
|
||||
final logger = TalkerCherryPickObserver(talker);
|
||||
|
||||
logger.info('Hello from CherryPickLogger!');
|
||||
logger.warn('Something might be wrong...');
|
||||
logger.error('Oops! An error occurred', Exception('Test error'));
|
||||
logger.onDiagnostic('Hello from CherryPickLogger!');
|
||||
logger.onWarning('Something might be wrong...');
|
||||
logger.onError('Oops! An error occurred', Exception('Test error'), null);
|
||||
|
||||
// Вывод всех логов
|
||||
print('\nВсе сообщения логирования через Talker:');
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:talker/talker.dart';
|
||||
|
||||
/// Реализация CherryPickLogger для логирования через Talker
|
||||
class TalkerCherryPickLogger implements CherryPickLogger {
|
||||
final Talker talker;
|
||||
|
||||
TalkerCherryPickLogger(this.talker);
|
||||
|
||||
@override
|
||||
void info(String message) => talker.info('[CherryPick] $message');
|
||||
|
||||
@override
|
||||
void warn(String message) => talker.warning('[CherryPick] $message');
|
||||
|
||||
@override
|
||||
void error(String message, [Object? error, StackTrace? stackTrace]) {
|
||||
talker.handle(
|
||||
error ?? '[CherryPick] $message',
|
||||
stackTrace,
|
||||
'[CherryPick] $message',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:talker/talker.dart';
|
||||
|
||||
/// CherryPickObserver-адаптер для логирования событий CherryPick через Talker
|
||||
class TalkerCherryPickObserver implements CherryPickObserver {
|
||||
final Talker talker;
|
||||
|
||||
TalkerCherryPickObserver(this.talker);
|
||||
|
||||
@override
|
||||
void onBindingRegistered(String name, Type type, {String? scopeName}) {
|
||||
talker.info('[binding][CherryPick] $name — $type (scope: $scopeName)');
|
||||
}
|
||||
@override
|
||||
void onInstanceRequested(String name, Type type, {String? scopeName}) {
|
||||
talker.info('[request][CherryPick] $name — $type (scope: $scopeName)');
|
||||
}
|
||||
@override
|
||||
void onInstanceCreated(String name, Type type, Object instance, {String? scopeName}) {
|
||||
talker.info('[create][CherryPick] $name — $type => $instance (scope: $scopeName)');
|
||||
}
|
||||
@override
|
||||
void onInstanceDisposed(String name, Type type, Object instance, {String? scopeName}) {
|
||||
talker.info('[dispose][CherryPick] $name — $type => $instance (scope: $scopeName)');
|
||||
}
|
||||
@override
|
||||
void onModulesInstalled(List<String> modules, {String? scopeName}) {
|
||||
talker.info('[modules installed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
|
||||
}
|
||||
@override
|
||||
void onModulesRemoved(List<String> modules, {String? scopeName}) {
|
||||
talker.info('[modules removed][CherryPick] ${modules.join(', ')} (scope: $scopeName)');
|
||||
}
|
||||
@override
|
||||
void onScopeOpened(String name) {
|
||||
talker.info('[scope opened][CherryPick] $name');
|
||||
}
|
||||
@override
|
||||
void onScopeClosed(String name) {
|
||||
talker.info('[scope closed][CherryPick] $name');
|
||||
}
|
||||
@override
|
||||
void onCycleDetected(List<String> chain, {String? scopeName}) {
|
||||
talker.warning('[cycle][CherryPick] Detected: ${chain.join(' -> ')}${scopeName != null ? ' (scope: $scopeName)' : ''}');
|
||||
}
|
||||
@override
|
||||
void onCacheHit(String name, Type type, {String? scopeName}) {
|
||||
talker.info('[cache hit][CherryPick] $name — $type (scope: $scopeName)');
|
||||
}
|
||||
@override
|
||||
void onCacheMiss(String name, Type type, {String? scopeName}) {
|
||||
talker.info('[cache miss][CherryPick] $name — $type (scope: $scopeName)');
|
||||
}
|
||||
@override
|
||||
void onDiagnostic(String message, {Object? details}) {
|
||||
talker.verbose('[diagnostic][CherryPick] $message ${details ?? ''}');
|
||||
}
|
||||
@override
|
||||
void onWarning(String message, {Object? details}) {
|
||||
talker.warning('[warn][CherryPick] $message ${details ?? ''}');
|
||||
}
|
||||
@override
|
||||
void onError(String message, Object? error, StackTrace? stackTrace) {
|
||||
talker.handle(error ?? '[CherryPick] $message', stackTrace, '[error][CherryPick] $message');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
/// More dartdocs go here.
|
||||
library;
|
||||
|
||||
export 'src/talker_cherrypick_logger.dart';
|
||||
export 'src/talker_cherrypick_observer.dart';
|
||||
|
||||
// TODO: Export any libraries intended for clients of this package.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: talker_cherrypick_logger
|
||||
description: A starting point for Dart libraries or applications.
|
||||
version: 1.0.0
|
||||
publish_to: none
|
||||
# repository: https://github.com/my_org/my_repo
|
||||
|
||||
environment:
|
||||
|
||||
@@ -3,38 +3,44 @@ import 'package:talker/talker.dart';
|
||||
import 'package:talker_cherrypick_logger/talker_cherrypick_logger.dart';
|
||||
|
||||
void main() {
|
||||
group('TalkerCherryPickLogger', () {
|
||||
group('TalkerCherryPickObserver', () {
|
||||
late Talker talker;
|
||||
late TalkerCherryPickLogger logger;
|
||||
late TalkerCherryPickObserver observer;
|
||||
|
||||
setUp(() {
|
||||
talker = Talker();
|
||||
logger = TalkerCherryPickLogger(talker);
|
||||
observer = TalkerCherryPickObserver(talker);
|
||||
});
|
||||
|
||||
test('logs info messages correctly', () {
|
||||
logger.info('Test info');
|
||||
test('onInstanceRequested logs info', () {
|
||||
observer.onInstanceRequested('A', String, scopeName: 'test');
|
||||
final log = talker.history.last;
|
||||
expect(log.message, contains('[CherryPick] Test info'));
|
||||
//xpect(log.level, TalkerLogLevel.info);
|
||||
expect(log.message, contains('[request][CherryPick] A — String (scope: test)'));
|
||||
});
|
||||
|
||||
test('logs warning messages correctly', () {
|
||||
logger.warn('Danger!');
|
||||
test('onCycleDetected logs warning', () {
|
||||
observer.onCycleDetected(['A', 'B'], scopeName: 's');
|
||||
final log = talker.history.last;
|
||||
expect(log.message, contains('[CherryPick] Danger!'));
|
||||
expect(log.message, contains('[cycle][CherryPick] Detected'));
|
||||
//expect(log.level, TalkerLogLevel.warning);
|
||||
});
|
||||
|
||||
test('logs error messages correctly', () {
|
||||
final error = Exception('some error');
|
||||
test('onError calls handle', () {
|
||||
final error = Exception('fail');
|
||||
final stack = StackTrace.current;
|
||||
logger.error('ERR', error, stack);
|
||||
observer.onError('Oops', error, stack);
|
||||
final log = talker.history.last;
|
||||
//expect(log.level, TalkerLogLevel.error);
|
||||
expect(log.message, contains('[CherryPick] ERR'));
|
||||
expect(log.message, contains('[error][CherryPick] Oops'));
|
||||
expect(log.exception, error);
|
||||
expect(log.stackTrace, stack);
|
||||
});
|
||||
|
||||
test('onDiagnostic logs verbose', () {
|
||||
observer.onDiagnostic('hello', details: 123);
|
||||
final log = talker.history.last;
|
||||
//expect(log.level, TalkerLogLevel.verbose);
|
||||
expect(log.message, contains('hello'));
|
||||
expect(log.message, contains('123'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user