refactor(mobile): log service (#16383)

refactor: log service

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2025-02-28 01:48:49 +05:30
committed by GitHub
parent fbd85a89e0
commit 28c664c769
24 changed files with 656 additions and 201 deletions
@@ -0,0 +1,186 @@
import 'package:collection/collection.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:logging/logging.dart';
import 'package:mocktail/mocktail.dart';
import '../../infrastructure/repository.mock.dart';
import '../../test_utils.dart';
final _kInfoLog = LogMessage(
message: '#Info Message',
level: LogLevel.INFO,
createdAt: DateTime(2025, 2, 26),
logger: 'Info Logger',
);
final _kWarnLog = LogMessage(
message: '#Warn Message',
level: LogLevel.WARNING,
createdAt: DateTime(2025, 2, 27),
logger: 'Warn Logger',
);
void main() {
late LogService sut;
late ILogRepository mockLogRepo;
late IStoreRepository mockStoreRepo;
setUp(() async {
mockLogRepo = MockLogRepository();
mockStoreRepo = MockStoreRepository();
registerFallbackValue(_kInfoLog);
when(() => mockLogRepo.truncate(limit: any(named: 'limit')))
.thenAnswer((_) async => {});
when(() => mockStoreRepo.tryGet<int>(StoreKey.logLevel))
.thenAnswer((_) async => LogLevel.FINE.index);
when(() => mockLogRepo.getAll()).thenAnswer((_) async => []);
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
sut =
await LogService.create(logRepo: mockLogRepo, storeRepo: mockStoreRepo);
});
tearDown(() async {
await sut.dispose();
});
group("Log Service Init:", () {
test('Truncates the existing logs on init', () {
final limit =
verify(() => mockLogRepo.truncate(limit: captureAny(named: 'limit')))
.captured
.firstOrNull as int?;
expect(limit, kLogTruncateLimit);
});
test('Sets log level based on the store setting', () {
verify(() => mockStoreRepo.tryGet<int>(StoreKey.logLevel)).called(1);
expect(Logger.root.level, Level.FINE);
});
});
group("Log Service Set Level:", () {
setUp(() async {
when(() => mockStoreRepo.insert<int>(StoreKey.logLevel, any()))
.thenAnswer((_) async => true);
await sut.setlogLevel(LogLevel.SHOUT);
});
test('Updates the log level in store', () {
final index = verify(
() => mockStoreRepo.insert<int>(StoreKey.logLevel, captureAny()),
).captured.firstOrNull;
expect(index, LogLevel.SHOUT.index);
});
test('Sets log level on logger', () {
expect(Logger.root.level, Level.SHOUT);
});
});
group("Log Service Buffer:", () {
test('Buffers logs until timer elapses', () {
TestUtils.fakeAsync((time) async {
sut = await LogService.create(
logRepo: mockLogRepo,
storeRepo: mockStoreRepo,
shouldBuffer: true,
);
final logger = Logger(_kInfoLog.logger!);
logger.info(_kInfoLog.message);
expect(await sut.getMessages(), hasLength(1));
logger.warning(_kWarnLog.message);
expect(await sut.getMessages(), hasLength(2));
time.elapse(const Duration(seconds: 6));
expect(await sut.getMessages(), isEmpty);
});
});
test('Batch inserts all logs on timer', () {
TestUtils.fakeAsync((time) async {
sut = await LogService.create(
logRepo: mockLogRepo,
storeRepo: mockStoreRepo,
shouldBuffer: true,
);
final logger = Logger(_kInfoLog.logger!);
logger.info(_kInfoLog.message);
time.elapse(const Duration(seconds: 6));
final insert = verify(() => mockLogRepo.insertAll(captureAny()));
insert.called(1);
// ignore: prefer-correct-json-casts
final captured = insert.captured.firstOrNull as List<LogMessage>;
expect(captured.firstOrNull?.message, _kInfoLog.message);
expect(captured.firstOrNull?.logger, _kInfoLog.logger);
verifyNever(() => mockLogRepo.insert(captureAny()));
});
});
test('Does not buffer when off', () {
TestUtils.fakeAsync((time) async {
sut = await LogService.create(
logRepo: mockLogRepo,
storeRepo: mockStoreRepo,
shouldBuffer: false,
);
final logger = Logger(_kInfoLog.logger!);
logger.info(_kInfoLog.message);
// Ensure nothing gets buffer. This works because we mock log repo getAll to return nothing
expect(await sut.getMessages(), isEmpty);
final insert = verify(() => mockLogRepo.insert(captureAny()));
insert.called(1);
final captured = insert.captured.firstOrNull as LogMessage;
expect(captured.message, _kInfoLog.message);
expect(captured.logger, _kInfoLog.logger);
verifyNever(() => mockLogRepo.insertAll(captureAny()));
});
});
});
group("Log Service Get messages:", () {
setUp(() {
when(() => mockLogRepo.getAll()).thenAnswer((_) async => [_kInfoLog]);
});
test('Fetches result from DB', () async {
expect(await sut.getMessages(), hasLength(1));
verify(() => mockLogRepo.getAll()).called(1);
});
test('Combines result from both DB + Buffer', () {
TestUtils.fakeAsync((time) async {
sut = await LogService.create(
logRepo: mockLogRepo,
storeRepo: mockStoreRepo,
shouldBuffer: true,
);
final logger = Logger(_kWarnLog.logger!);
logger.warning(_kWarnLog.message);
expect(await sut.getMessages(), hasLength(2)); // 1 - DB, 1 - Buff
final messages = await sut.getMessages();
// Logged time is assigned in the service for messages in the buffer, so compare manually
expect(messages.firstOrNull?.message, _kWarnLog.message);
expect(messages.firstOrNull?.logger, _kWarnLog.logger);
expect(messages.elementAtOrNull(1), _kInfoLog);
});
});
});
}
@@ -1,4 +1,7 @@
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:mocktail/mocktail.dart';
class MockStoreRepository extends Mock implements IStoreRepository {}
class MockLogRepository extends Mock implements ILogRepository {}
@@ -1,15 +1,16 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/services/immich_logger.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:mocktail/mocktail.dart';
@@ -70,7 +71,10 @@ void main() {
db.writeTxnSync(() => db.clearSync());
await StoreService.init(storeRepository: IsarStoreRepository(db));
await Store.put(StoreKey.currentUser, owner);
ImmichLogger();
await LogService.init(
logRepo: IsarLogRepository(db),
storeRepo: IsarStoreRepository(db),
);
});
final List<Asset> initialAssets = [
makeAsset(checksum: "a", remoteId: "0-1"),
+35 -1
View File
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@@ -11,8 +13,8 @@ import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/entities/logger_message.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:isar/isar.dart';
import 'package:mocktail/mocktail.dart';
@@ -88,4 +90,36 @@ abstract final class TestUtils {
WidgetController.hitTestWarningShouldBeFatal = true;
HttpOverrides.global = MockHttpOverrides();
}
// Workaround till the following issue is resolved
// https://github.com/dart-lang/test/issues/2307
static T fakeAsync<T>(
Future<T> Function(FakeAsync _) callback, {
DateTime? initialTime,
}) {
late final T result;
Object? error;
StackTrace? stack;
FakeAsync(initialTime: initialTime).run((FakeAsync async) {
bool shouldPump = true;
unawaited(
callback(async).then<void>(
(value) => result = value,
onError: (e, s) {
error = e;
stack = s;
},
).whenComplete(() => shouldPump = false),
);
while (shouldPump) {
async.flushMicrotasks();
}
});
if (error != null) {
Error.throwWithStackTrace(error!, stack!);
}
return result;
}
}