import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/analytics/analytics_service.dart'; /// 定时冲刷与失败退避测试(M3 T3-19,13 号 §3.4 第 4 触发点 + /// iteration-3 06 号 §2.3):fakeAsync 驱动 Timer.periodic, /// 时钟注入照 SessionTracker 先例,假上传子类免起真实 HttpServer。 class _FakeUploadService extends AnalyticsService { _FakeUploadService({super.now}) : super( apiBaseUrl: 'http://unused', getAccessToken: null, getSessionId: () => 'session-x', ); int uploadAttempts = 0; bool failUploads = false; final List uploadedBatchSizes = []; @override Future uploadBatch(List> events) async { uploadAttempts++; if (failUploads) { throw Exception('simulated network failure'); } uploadedBatchSizes.add(events.length); return false; } } void main() { /// 逐秒推进假时间轴并同步注入时钟,保证定时器回调读到的 now 与 /// 已流逝时间一致(整段 elapse 会让回调读到未更新的旧时钟)。 (void Function(Duration), _FakeUploadService) setup(FakeAsync async) { var clock = DateTime.utc(2026, 9, 8, 10); final service = _FakeUploadService(now: () => clock); void elapse(Duration duration) { final target = clock.add(duration); while (clock.isBefore(target)) { clock = clock.add(const Duration(seconds: 1)); async.elapse(const Duration(seconds: 1)); } } return (elapse, service); } group('AnalyticsService 定时冲刷', () { test('前台每 30 秒冲刷不满 20 条的队列', () { fakeAsync((async) { final (elapse, service) = setup(async); service.startPeriodicFlush(); service.trackEvent('page_viewed', {'pageName': 'home'}); async.flushMicrotasks(); elapse(const Duration(seconds: 29)); expect(service.uploadAttempts, 0); elapse(const Duration(seconds: 1)); expect(service.uploadedBatchSizes, [1]); expect(service.pendingEvents, isEmpty); service.stopPeriodicFlush(); }); }); test('队列为空时定时器不发起上传', () { fakeAsync((async) { final (elapse, service) = setup(async); service.startPeriodicFlush(); elapse(const Duration(minutes: 2)); expect(service.uploadAttempts, 0); service.stopPeriodicFlush(); }); }); test('stopPeriodicFlush 停止触发(退后台),start 恢复(回前台)', () { fakeAsync((async) { final (elapse, service) = setup(async); service.trackEvent('page_viewed', {'pageName': 'home'}); async.flushMicrotasks(); service.startPeriodicFlush(); service.stopPeriodicFlush(); elapse(const Duration(minutes: 2)); expect(service.uploadAttempts, 0); service.startPeriodicFlush(); elapse(const Duration(seconds: 30)); expect(service.uploadedBatchSizes, [1]); service.stopPeriodicFlush(); }); }); test('startPeriodicFlush 幂等,不叠加多个定时器', () { fakeAsync((async) { final (elapse, service) = setup(async); service.startPeriodicFlush(); service.startPeriodicFlush(); service.trackEvent('page_viewed', {'pageName': 'home'}); async.flushMicrotasks(); elapse(const Duration(seconds: 30)); expect(service.uploadAttempts, 1); service.stopPeriodicFlush(); }); }); }); group('AnalyticsService 失败退避', () { test('上传失败按 30s→60s→120s 指数退避,期间定时冲刷跳过', () { fakeAsync((async) { final (elapse, service) = setup(async); service.failUploads = true; service.trackEvent('page_viewed', {'pageName': 'home'}); async.flushMicrotasks(); service.startPeriodicFlush(); // t=30s 首次尝试失败 → 退避 30s(下次可试 t=60s)。 elapse(const Duration(seconds: 30)); expect(service.uploadAttempts, 1); // t=60s 第二次失败 → 退避 60s(下次 t=120s);t=90s 被跳过。 elapse(const Duration(seconds: 30)); expect(service.uploadAttempts, 2); elapse(const Duration(seconds: 30)); expect(service.uploadAttempts, 2); // t=120s 第三次失败 → 退避 120s;t=150/180/210s 均跳过。 elapse(const Duration(seconds: 30)); expect(service.uploadAttempts, 3); elapse(const Duration(seconds: 90)); expect(service.uploadAttempts, 3); // t=240s 第四次尝试;事件始终保留在队列。 elapse(const Duration(seconds: 30)); expect(service.uploadAttempts, 4); expect(service.pendingEvents.length, 1); service.stopPeriodicFlush(); }); }); test('退避封顶 5 分钟', () { fakeAsync((async) { final (elapse, service) = setup(async); service.failUploads = true; service.trackEvent('page_viewed', {'pageName': 'home'}); async.flushMicrotasks(); service.startPeriodicFlush(); // 失败序列 t=30/60/120/240s(退避 30/60/120/240s), // 第五次 t=480s:240s×2=480s 超帽,封顶 300s。 elapse(const Duration(minutes: 8)); expect(service.uploadAttempts, 5); // t=780s 前(480+300s 窗口内)不再尝试,到点第六次。 elapse(const Duration(seconds: 299)); expect(service.uploadAttempts, 5); elapse(const Duration(seconds: 1)); expect(service.uploadAttempts, 6); service.stopPeriodicFlush(); }); }); test('退避只挡定时冲刷,flushNow 显式触发不受限', () { fakeAsync((async) { final (elapse, service) = setup(async); service.failUploads = true; service.trackEvent('page_viewed', {'pageName': 'home'}); async.flushMicrotasks(); service.startPeriodicFlush(); elapse(const Duration(seconds: 30)); expect(service.uploadAttempts, 1); // 退避窗口内(t=45s)退后台显式冲刷仍然尝试。 elapse(const Duration(seconds: 15)); service.flushNow(); async.flushMicrotasks(); expect(service.uploadAttempts, 2); service.stopPeriodicFlush(); }); }); test('上传成功即重置退避,恢复 30 秒节奏', () { fakeAsync((async) { final (elapse, service) = setup(async); service.failUploads = true; service.trackEvent('page_viewed', {'pageName': 'home'}); async.flushMicrotasks(); service.startPeriodicFlush(); // t=30/60s 两次失败后网络恢复,t=120s 第三次成功。 elapse(const Duration(seconds: 60)); expect(service.uploadAttempts, 2); service.failUploads = false; elapse(const Duration(seconds: 60)); expect(service.uploadedBatchSizes, [1]); // 退避已重置:新事件在下一个 30 秒刻度即上传,无残留等待。 service.trackEvent('page_viewed', {'pageName': 'feed'}); async.flushMicrotasks(); elapse(const Duration(seconds: 30)); expect(service.uploadedBatchSizes, [1, 1]); service.stopPeriodicFlush(); }); }); }); }