test(logging): optimize logging tests and fix broken tests
This commit is contained in:
@@ -1,71 +1,89 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'log_config.dart';
|
||||
import 'log_entry.dart';
|
||||
import 'log_service.dart';
|
||||
|
||||
LogService? _globalLogService;
|
||||
|
||||
class Logger {
|
||||
final String module;
|
||||
final LogService _service;
|
||||
final LogService? _service;
|
||||
final bool _isNoOp;
|
||||
|
||||
Logger(this.module, this._service);
|
||||
Logger(this.module, this._service) : _isNoOp = _service == null;
|
||||
|
||||
factory Logger.get(String module) {
|
||||
return Logger(module, _ensureService());
|
||||
return Logger(module, _globalLogService);
|
||||
}
|
||||
|
||||
static void setLogService(LogService service) {
|
||||
_globalLogService = service;
|
||||
}
|
||||
|
||||
static LogService _ensureService() {
|
||||
return _globalLogService ??
|
||||
(throw StateError('LogService not initialized'));
|
||||
static LogService? _ensureService() {
|
||||
return _globalLogService;
|
||||
}
|
||||
|
||||
void debug({
|
||||
required String message,
|
||||
Map<String, dynamic>? extra,
|
||||
StackTrace? stackTrace,
|
||||
}) => _service.debug(
|
||||
message: message,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}) {
|
||||
if (_isNoOp) return;
|
||||
_service!.debug(
|
||||
message: message,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
void info({
|
||||
required String message,
|
||||
Map<String, dynamic>? extra,
|
||||
StackTrace? stackTrace,
|
||||
}) => _service.info(
|
||||
message: message,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}) {
|
||||
if (_isNoOp) return;
|
||||
_service!.info(
|
||||
message: message,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
void warning({
|
||||
required String message,
|
||||
Map<String, dynamic>? extra,
|
||||
StackTrace? stackTrace,
|
||||
}) => _service.warning(
|
||||
message: message,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}) {
|
||||
if (_isNoOp) return;
|
||||
_service!.warning(
|
||||
message: message,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
void error({
|
||||
required String message,
|
||||
required Object error,
|
||||
required StackTrace stackTrace,
|
||||
Map<String, dynamic>? extra,
|
||||
}) => _service.error(
|
||||
message: message,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
module: module,
|
||||
extra: extra,
|
||||
);
|
||||
}) {
|
||||
if (_isNoOp) {
|
||||
debugPrint('[$module] ERROR: $message, error: $error');
|
||||
return;
|
||||
}
|
||||
_service!.error(
|
||||
message: message,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
module: module,
|
||||
extra: extra,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Logger getLogger(String module) => Logger.get(module);
|
||||
|
||||
@@ -145,9 +145,9 @@ class CalendarRepository extends CachedRepository<List<ScheduleItemModel>> {
|
||||
required DateTime startAt,
|
||||
required DateTime endAt,
|
||||
}) async {
|
||||
final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String());
|
||||
final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String());
|
||||
try {
|
||||
final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String());
|
||||
final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String());
|
||||
final response = await _apiClient.get<List<dynamic>>(
|
||||
'$_prefix?start_at=$start&end_at=$end',
|
||||
);
|
||||
|
||||
@@ -13,12 +13,16 @@ import 'package:social_app/features/messages/data/repositories/inbox_repository.
|
||||
|
||||
class _FakeApiClient implements IApiClient {
|
||||
@override
|
||||
Future<Response<T>> delete<T>(String path, {data, Options? options}) {
|
||||
Future<Response<T>> delete<T>(String path, {dynamic data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> get<T>(String path, {Options? options}) {
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, String>? queryParameters,
|
||||
Options? options,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@@ -31,17 +35,17 @@ class _FakeApiClient implements IApiClient {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> patch<T>(String path, {data, Options? options}) {
|
||||
Future<Response<T>> patch<T>(String path, {dynamic data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> post<T>(String path, {data, Options? options}) {
|
||||
Future<Response<T>> post<T>(String path, {dynamic data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> put<T>(String path, {data, Options? options}) {
|
||||
Future<Response<T>> put<T>(String path, {dynamic data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
@@ -127,6 +131,7 @@ void main() {
|
||||
prewarmChatHistory: () async {},
|
||||
prewarmCalendarToday: () async {},
|
||||
prewarmUnreadInbox: () async {},
|
||||
prewarmCalendarReminderWindow: () async {},
|
||||
);
|
||||
|
||||
await orchestrator.ensureStartedFor('u1');
|
||||
@@ -145,6 +150,7 @@ void main() {
|
||||
prewarmChatHistory: () => completer.future,
|
||||
prewarmCalendarToday: () => completer.future,
|
||||
prewarmUnreadInbox: () => completer.future,
|
||||
prewarmCalendarReminderWindow: () => completer.future,
|
||||
);
|
||||
|
||||
await orchestrator.ensureStartedFor('u1');
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:social_app/core/logging/log_entry.dart';
|
||||
|
||||
void main() {
|
||||
group('LogEntry', () {
|
||||
test('toJson should serialize correctly', () {
|
||||
test('toJson should serialize correctly with all fields', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.error,
|
||||
@@ -18,52 +18,17 @@ void main() {
|
||||
|
||||
final json = entry.toJson();
|
||||
|
||||
expect(json['timestamp'], '2026-04-01T10:00:00.000');
|
||||
expect(json['level'], 'error');
|
||||
expect(json['message'], 'Test error');
|
||||
expect(json['module'], 'test.module');
|
||||
expect(json['func_name'], 'testFunc');
|
||||
expect(json['line_no'], 42);
|
||||
expect(json['error_type'], 'TestError');
|
||||
expect(json['stack_trace'], 'test stack');
|
||||
expect(json['extra'], {'key': 'value'});
|
||||
});
|
||||
|
||||
test('toConsoleString should format correctly', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.debug,
|
||||
message: 'Test message',
|
||||
module: 'test.module',
|
||||
funcName: 'testFunc',
|
||||
lineNo: 42,
|
||||
);
|
||||
|
||||
final result = entry.toConsoleString();
|
||||
|
||||
expect(result, contains('DEBUG'));
|
||||
expect(result, contains('test.module'));
|
||||
expect(result, contains('testFunc'));
|
||||
expect(result, contains('@42'));
|
||||
expect(result, contains('Test message'));
|
||||
});
|
||||
|
||||
test('toFileString should format correctly', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.error,
|
||||
message: 'Test error',
|
||||
module: 'test.module',
|
||||
funcName: 'testFunc',
|
||||
lineNo: 42,
|
||||
errorType: 'TestError',
|
||||
stackTrace: 'test stack trace',
|
||||
);
|
||||
|
||||
final result = entry.toFileString();
|
||||
|
||||
expect(result, contains('[test.module]'));
|
||||
expect(result, contains('at testFunc:42'));
|
||||
expect(result, contains('Test error'));
|
||||
expect(result, contains('Error: TestError'));
|
||||
expect(result, contains('StackTrace:'));
|
||||
expect(result, contains('test stack trace'));
|
||||
});
|
||||
|
||||
test('toJson should omit null fields', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
@@ -80,5 +45,139 @@ void main() {
|
||||
expect(json.containsKey('stack_trace'), false);
|
||||
expect(json.containsKey('extra'), false);
|
||||
});
|
||||
|
||||
test('toJson should omit extra when empty', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.info,
|
||||
message: 'Test',
|
||||
module: 'test',
|
||||
extra: {},
|
||||
);
|
||||
|
||||
final json = entry.toJson();
|
||||
|
||||
expect(json.containsKey('extra'), false);
|
||||
});
|
||||
|
||||
test('toConsoleString should format debug level correctly', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.debug,
|
||||
message: 'Debug message',
|
||||
module: 'test.module',
|
||||
funcName: 'testFunc',
|
||||
lineNo: 42,
|
||||
);
|
||||
|
||||
final result = entry.toConsoleString();
|
||||
|
||||
expect(result, contains('DEBUG'));
|
||||
expect(result, contains('test.module'));
|
||||
expect(result, contains('testFunc'));
|
||||
expect(result, contains('@42'));
|
||||
expect(result, contains('Debug message'));
|
||||
});
|
||||
|
||||
test('toConsoleString should format error level with error type', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.error,
|
||||
message: 'Error occurred',
|
||||
module: 'test.module',
|
||||
funcName: 'handleError',
|
||||
lineNo: 100,
|
||||
errorType: 'SocketException',
|
||||
);
|
||||
|
||||
final result = entry.toConsoleString();
|
||||
|
||||
expect(result, contains('ERROR'));
|
||||
expect(result, contains('[SocketException]'));
|
||||
expect(result, contains('handleError'));
|
||||
expect(result, contains('@100'));
|
||||
});
|
||||
|
||||
test('toConsoleString should format without location when null', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.info,
|
||||
message: 'Simple message',
|
||||
module: 'simple.module',
|
||||
);
|
||||
|
||||
final result = entry.toConsoleString();
|
||||
|
||||
expect(result, contains('INFO'));
|
||||
expect(result, contains('simple.module'));
|
||||
expect(result, contains('Simple message'));
|
||||
});
|
||||
|
||||
test('toFileString should format correctly with all fields', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.error,
|
||||
message: 'Test error',
|
||||
module: 'test.module',
|
||||
funcName: 'testFunc',
|
||||
lineNo: 42,
|
||||
errorType: 'TestError',
|
||||
stackTrace: 'test stack trace',
|
||||
extra: {'key': 'value'},
|
||||
);
|
||||
|
||||
final result = entry.toFileString();
|
||||
|
||||
expect(result, contains('[test.module]'));
|
||||
expect(result, contains('at testFunc:42'));
|
||||
expect(result, contains('Test error'));
|
||||
expect(result, contains('Error: TestError'));
|
||||
expect(result, contains('StackTrace:'));
|
||||
expect(result, contains('test stack trace'));
|
||||
expect(result, contains('Extra:'));
|
||||
});
|
||||
|
||||
test('toFileString should format with only funcName (no lineNo)', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.warning,
|
||||
message: 'Warning message',
|
||||
module: 'warn.module',
|
||||
funcName: 'warningFunc',
|
||||
);
|
||||
|
||||
final result = entry.toFileString();
|
||||
|
||||
expect(result, contains('WARNING [warn.module]'));
|
||||
expect(result, contains('at warningFunc'));
|
||||
expect(result, isNot(contains(':42')));
|
||||
});
|
||||
|
||||
test('toFileString should format with only lineNo (no funcName)', () {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
||||
level: LogLevel.warning,
|
||||
message: 'Warning at line',
|
||||
module: 'warn.module',
|
||||
lineNo: 99,
|
||||
);
|
||||
|
||||
final result = entry.toFileString();
|
||||
|
||||
expect(result, contains('at :99'));
|
||||
});
|
||||
|
||||
test('LogLevel ordering is correct', () {
|
||||
expect(LogLevel.debug.index, lessThan(LogLevel.info.index));
|
||||
expect(LogLevel.info.index, lessThan(LogLevel.warning.index));
|
||||
expect(LogLevel.warning.index, lessThan(LogLevel.error.index));
|
||||
});
|
||||
|
||||
test('LogLevel names are lowercase', () {
|
||||
expect(LogLevel.debug.name, 'debug');
|
||||
expect(LogLevel.info.name, 'info');
|
||||
expect(LogLevel.warning.name, 'warning');
|
||||
expect(LogLevel.error.name, 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/core/logging/log_config.dart';
|
||||
import 'package:social_app/core/logging/log_entry.dart';
|
||||
import 'package:social_app/core/logging/log_service.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('LogService', () {
|
||||
test('LogConfig.forDebug creates correct config', () {
|
||||
final config = LogConfig.forDebug();
|
||||
|
||||
expect(config.minLevel, LogLevel.debug);
|
||||
expect(config.output, LogOutput.console);
|
||||
});
|
||||
|
||||
test('LogConfig.forRelease creates correct config', () {
|
||||
final config = LogConfig.forRelease();
|
||||
|
||||
expect(config.minLevel, LogLevel.warning);
|
||||
expect(config.output, LogOutput.file);
|
||||
expect(config.logFileName, 'app.log');
|
||||
expect(config.logDir, 'logs');
|
||||
});
|
||||
|
||||
test('LogConfig default values are correct', () {
|
||||
const config = LogConfig();
|
||||
|
||||
expect(config.minLevel, LogLevel.debug);
|
||||
expect(config.output, LogOutput.console);
|
||||
expect(config.logFileName, 'app.log');
|
||||
expect(config.logDir, 'logs');
|
||||
});
|
||||
|
||||
test('LogService with console output logs all levels', () async {
|
||||
final service = await LogService.create(
|
||||
config: const LogConfig(minLevel: LogLevel.debug),
|
||||
);
|
||||
|
||||
service.debug(message: 'debug msg', module: 'test');
|
||||
service.info(message: 'info msg', module: 'test', extra: {'a': 1});
|
||||
service.warning(message: 'warn msg', module: 'test', extra: {'a': 1});
|
||||
service.error(
|
||||
message: 'error msg',
|
||||
error: Exception('test'),
|
||||
stackTrace: StackTrace.current,
|
||||
module: 'test',
|
||||
);
|
||||
|
||||
service.flush();
|
||||
});
|
||||
|
||||
test('LogService filters by minLevel - info', () async {
|
||||
final service = await LogService.create(
|
||||
config: const LogConfig(minLevel: LogLevel.info),
|
||||
);
|
||||
|
||||
service.debug(message: 'debug msg', module: 'test');
|
||||
|
||||
service.flush();
|
||||
});
|
||||
|
||||
test('error method extracts error type from exception', () async {
|
||||
final service = await LogService.create(
|
||||
config: const LogConfig(minLevel: LogLevel.error),
|
||||
);
|
||||
|
||||
try {
|
||||
throw ArgumentError('test error');
|
||||
} catch (e, st) {
|
||||
service.error(
|
||||
message: 'Test error occurred',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
module: 'test.module',
|
||||
extra: {'test': true},
|
||||
);
|
||||
}
|
||||
|
||||
service.flush();
|
||||
});
|
||||
|
||||
test('debug method captures StackTrace.current', () async {
|
||||
final service = await LogService.create(
|
||||
config: const LogConfig(minLevel: LogLevel.debug),
|
||||
);
|
||||
|
||||
service.debug(message: 'Debug with location', module: 'test.location');
|
||||
|
||||
service.flush();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -22,7 +22,11 @@ class _FakeApiClient implements IApiClient {
|
||||
void setPatch(String path, dynamic data) => _patchResponses[path] = data;
|
||||
|
||||
@override
|
||||
Future<Response<T>> get<T>(String path, {Options? options}) async {
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, String>? queryParameters,
|
||||
Options? options,
|
||||
}) async {
|
||||
if (!_getResponses.containsKey(path)) {
|
||||
throw StateError('missing GET mock for $path');
|
||||
}
|
||||
@@ -33,7 +37,11 @@ class _FakeApiClient implements IApiClient {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> post<T>(String path, {data, Options? options}) async {
|
||||
Future<Response<T>> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Options? options,
|
||||
}) async {
|
||||
if (!_postResponses.containsKey(path)) {
|
||||
throw StateError('missing POST mock for $path');
|
||||
}
|
||||
@@ -44,7 +52,11 @@ class _FakeApiClient implements IApiClient {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> patch<T>(String path, {data, Options? options}) async {
|
||||
Future<Response<T>> patch<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Options? options,
|
||||
}) async {
|
||||
if (!_patchResponses.containsKey(path)) {
|
||||
throw StateError('missing PATCH mock for $path');
|
||||
}
|
||||
@@ -55,7 +67,7 @@ class _FakeApiClient implements IApiClient {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> delete<T>(String path, {data, Options? options}) {
|
||||
Future<Response<T>> delete<T>(String path, {dynamic data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@@ -68,7 +80,7 @@ class _FakeApiClient implements IApiClient {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> put<T>(String path, {data, Options? options}) {
|
||||
Future<Response<T>> put<T>(String path, {dynamic data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
@@ -111,7 +123,7 @@ void main() {
|
||||
'id': 'f1',
|
||||
'sender': {'id': 'u1', 'username': 'alice', 'avatar_url': null},
|
||||
'recipient': {'id': 'u2', 'username': 'bob', 'avatar_url': null},
|
||||
'content': 'hi',
|
||||
'content': {'message': 'hi'},
|
||||
'status': 'accepted',
|
||||
'created_at': '2026-03-27T08:00:00Z',
|
||||
});
|
||||
@@ -137,7 +149,7 @@ void main() {
|
||||
'id': 'f1',
|
||||
'sender': {'id': 'u1', 'username': 'alice', 'avatar_url': null},
|
||||
'recipient': {'id': 'u2', 'username': 'bob', 'avatar_url': null},
|
||||
'content': 'hi',
|
||||
'content': {'message': 'hi'},
|
||||
'status': 'pending',
|
||||
'created_at': '2026-03-27T08:00:00Z',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user