Files
FlClash/test/common/boot_record_test.dart
T
chen08209 bc72bba981 feat(android): drop Firebase analytics and crash reporting
Changelog: No more Firebase analytics or crash reporting
2026-10-17 17:21:43 +08:00

179 lines
5.7 KiB
Dart

import 'dart:convert';
import 'package:fl_clash/common/boot_record.dart';
import 'package:flutter_test/flutter_test.dart';
const _startedAt = 1000;
BootRecord _starting({
int failureCount = 0,
int? profileId = 7,
int handledExitAt = 0,
}) {
return BootRecord(
stage: BootStage.starting,
profileId: profileId,
startedAt: _startedAt,
failureCount: failureCount,
handledExitAt: handledExitAt,
);
}
AppExitInfo _exit(AppExitReason reason, {int timestamp = _startedAt + 500}) {
return AppExitInfo(reason: reason, timestamp: timestamp);
}
void main() {
group('AppExitReason', () {
test('maps the ApplicationExitInfo codes it is given', () {
expect(AppExitReason.fromCode(4), AppExitReason.crash);
expect(AppExitReason.fromCode(5), AppExitReason.crashNative);
expect(AppExitReason.fromCode(6), AppExitReason.anr);
expect(AppExitReason.fromCode(10), AppExitReason.userRequested);
expect(AppExitReason.fromCode(null), AppExitReason.unknown);
expect(AppExitReason.fromCode('4'), AppExitReason.unknown);
expect(AppExitReason.fromCode(99), AppExitReason.unknown);
});
test('separates crashes, external stops and undecidable reasons', () {
expect(AppExitReason.crashNative.isCrash, isTrue);
expect(AppExitReason.initializationFailure.isCrash, isTrue);
expect(AppExitReason.crash.isExternalStop, isFalse);
expect(AppExitReason.lowMemory.isExternalStop, isTrue);
expect(AppExitReason.signaled.isExternalStop, isTrue);
expect(AppExitReason.unknown.isCrash, isFalse);
expect(AppExitReason.unknown.isExternalStop, isFalse);
expect(AppExitReason.other.isExternalStop, isFalse);
});
});
group('AppExitInfo', () {
test('parses a platform map', () {
final info = AppExitInfo.fromJson({
'reason': 6,
'timestamp': 42,
'description': 'anr',
});
expect(info?.reason, AppExitReason.anr);
expect(info?.timestamp, 42);
expect(info?.description, 'anr');
});
test('rejects payloads without a usable timestamp', () {
expect(AppExitInfo.fromJson(null), isNull);
expect(AppExitInfo.fromJson('crash'), isNull);
expect(AppExitInfo.fromJson({'reason': 4}), isNull);
});
});
group('BootRecord', () {
test('round-trips through JSON', () {
const record = BootRecord(
stage: BootStage.running,
profileId: 3,
startedAt: 12,
failureCount: 2,
lastFailedProfileId: 9,
handledExitAt: 34,
);
expect(BootRecord.fromJson(json.decode(json.encode(record))), record);
});
test('falls back to a closed record when fields are missing or wrong', () {
final record = BootRecord.fromJson({'stage': 'gone', 'profileId': '3'});
expect(record, const BootRecord());
expect(BootRecord.fromJson('boot'), isNull);
});
});
group('resolveBootDecision', () {
test('does nothing when the last run reached the running stage', () {
final decision = resolveBootDecision(
record: const BootRecord(stage: BootStage.running, profileId: 7),
exitInfo: _exit(AppExitReason.crash),
);
expect(decision.recovery, BootRecovery.none);
expect(decision.failureCount, 0);
});
test('does nothing on a first launch', () {
final decision = resolveBootDecision(record: null, exitInfo: null);
expect(decision.recovery, BootRecovery.none);
});
test('skips automatic setup after one interrupted launch', () {
final decision = resolveBootDecision(record: _starting(), exitInfo: null);
expect(decision.recovery, BootRecovery.skipAutoSetup);
expect(decision.failureCount, 1);
expect(decision.failedProfileId, 7);
expect(decision.crashConfirmed, isFalse);
});
test('clears the profile once launches fail twice in a row', () {
final decision = resolveBootDecision(
record: _starting(failureCount: 1),
exitInfo: _exit(AppExitReason.crashNative),
);
expect(decision.recovery, BootRecovery.clearProfile);
expect(decision.failureCount, 2);
expect(decision.crashConfirmed, isTrue);
expect(decision.exitReason, AppExitReason.crashNative);
});
test('keeps skipping when there is no profile left to clear', () {
final decision = resolveBootDecision(
record: _starting(failureCount: 3, profileId: null),
exitInfo: null,
);
expect(decision.recovery, BootRecovery.skipAutoSetup);
expect(decision.failureCount, 4);
});
test('an external stop is not counted as a failed launch', () {
for (final reason in [
AppExitReason.userRequested,
AppExitReason.userStopped,
AppExitReason.lowMemory,
AppExitReason.signaled,
AppExitReason.exitSelf,
]) {
final decision = resolveBootDecision(
record: _starting(failureCount: 1),
exitInfo: _exit(reason),
);
expect(decision.recovery, BootRecovery.none, reason: reason.name);
expect(decision.failureCount, 0, reason: reason.name);
}
});
test('an already consumed exit record cannot veto or confirm again', () {
final decision = resolveBootDecision(
record: _starting(handledExitAt: _startedAt + 600),
exitInfo: _exit(AppExitReason.userRequested),
);
expect(decision.recovery, BootRecovery.skipAutoSetup);
expect(decision.exitReason, isNull);
});
test('an exit older than the interrupted run is ignored', () {
final decision = resolveBootDecision(
record: _starting(),
exitInfo: _exit(AppExitReason.userRequested, timestamp: _startedAt - 1),
);
expect(decision.recovery, BootRecovery.skipAutoSetup);
expect(decision.exitReason, isNull);
});
});
}