[flutter_tools] rewrite Usage.test to capture objects instead of print logs (#74829)
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 0391511..88d1280 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -296,7 +296,7 @@ fileSystem: MemoryFileSystem.test(), platform: platform, processManager: processManager, - usage: Usage.test(), + usage: TestUsage(), logger: BufferLogger.test(), terminal: Terminal.test(), majorVersion: majorVersion,
diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart index 7dfbd7e..a12f8f9 100644 --- a/packages/flutter_tools/lib/src/reporting/usage.dart +++ b/packages/flutter_tools/lib/src/reporting/usage.dart
@@ -91,8 +91,6 @@ runningOnBot: runningOnBot, firstRunMessenger: firstRunMessenger); - factory Usage.test() => _DefaultUsage.test(); - /// Uses the global [Usage] instance to send a 'command' to analytics. static void command(String command, { Map<CustomDimensions, Object> parameters, @@ -276,12 +274,6 @@ _analytics.analyticsOpt = AnalyticsOpt.optOut; } - _DefaultUsage.test() : - _suppressAnalytics = false, - _analytics = AnalyticsMock(true), - firstRunMessenger = null, - _clock = SystemClock.fixed(DateTime(2020, 10, 8)); - Analytics _analytics; final FirstRunMessenger firstRunMessenger; @@ -469,3 +461,163 @@ _sessionValues[param] = value.toString(); } } + + +/// Create a testing Usage instance. +/// +/// All sent events, exceptions, timings, and pages are +/// buffered on the object and can be inspected later. +@visibleForTesting +class TestUsage implements Usage { + final List<TestUsageCommand> commands = <TestUsageCommand>[]; + final List<TestUsageEvent> events = <TestUsageEvent>[]; + final List<dynamic> exceptions = <dynamic>[]; + final List<TestTimingEvent> timings = <TestTimingEvent>[]; + + @override + bool enabled = true; + + @override + bool suppressAnalytics = false; + + @override + String get clientId => 'test-client'; + + @override + Future<void> ensureAnalyticsSent() { + throw UnimplementedError(); + } + + @override + Stream<Map<String, dynamic>> get onSend => throw UnimplementedError(); + + @override + void printWelcome() { } + + @override + void sendCommand(String command, {Map<String, String> parameters}) { + commands.add(TestUsageCommand(command, parameters: parameters)); + } + + @override + void sendEvent(String category, String parameter, {String label, int value, Map<String, String> parameters}) { + events.add(TestUsageEvent(category, parameter, label: label, value: value, parameters: parameters)); + } + + @override + void sendException(dynamic exception) { + exceptions.add(exception); + } + + @override + void sendTiming(String category, String variableName, Duration duration, {String label}) { + timings.add(TestTimingEvent(category, variableName, duration, label: label)); + } +} + +@visibleForTesting +@immutable +class TestUsageCommand { + const TestUsageCommand(this.command, {this.parameters}); + + final String command; + final Map<String, String> parameters; + + @override + bool operator ==(Object other) { + return other is TestUsageCommand && + other.command == command && + _mapsEqual(other.parameters, parameters); + } + + @override + int get hashCode => command.hashCode ^ parameters.hashCode; + + @override + String toString() => 'TestUsageCommand($command, parameters:$parameters)'; +} + +@visibleForTesting +@immutable +class TestUsageEvent { + const TestUsageEvent(this.category, this.parameter, {this.label, this.value, this.parameters}); + + final String category; + final String parameter; + final String label; + final int value; + final Map<String, String> parameters; + + @override + bool operator ==(Object other) { + return other is TestUsageEvent && + other.category == category && + other.parameter == parameter && + other.label == label && + other.value == value && + _mapsEqual(other.parameters, parameters); + } + + @override + int get hashCode => category.hashCode ^ + parameter.hashCode ^ + label.hashCode ^ + value.hashCode ^ + parameters.hashCode; + + @override + String toString() => 'TestUsageEvent($category, $parameter, label:$label, value:$value, parameters:$parameters)'; +} + +@visibleForTesting +@immutable +class TestTimingEvent { + const TestTimingEvent(this.category, this.variableName, this.duration, {this.label}); + + final String category; + final String variableName; + final Duration duration; + final String label; + + @override + bool operator ==(Object other) { + return other is TestTimingEvent && + other.category == category && + other.variableName == variableName && + other.duration == duration && + other.label == label; + } + + @override + int get hashCode => category.hashCode ^ + variableName.hashCode ^ + duration.hashCode ^ + label.hashCode; + + @override + String toString() => 'TestTimingEvent($category, $variableName, $duration, label:$label)'; +} + +bool _mapsEqual(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + if (a.length != b.length) { + return false; + } + + for (final dynamic k in a.keys) { + final dynamic bValue = b[k]; + if (bValue == null && !b.containsKey(k)) { + return false; + } + if (bValue != a[k]) { + return false; + } + } + + return true; +}
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart index abe3452..1f459c0 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart
@@ -48,7 +48,7 @@ void main() { FileSystem fileSystem; - Usage usage; + TestUsage usage; BufferLogger logger; setUpAll(() { @@ -57,7 +57,7 @@ setUp(() { fileSystem = MemoryFileSystem.test(); - usage = Usage.test(); + usage = TestUsage(); logger = BufferLogger.test(); }); @@ -242,16 +242,15 @@ ..createSync(recursive: true) ..writeAsBytesSync(List<int>.generate(10000, (int index) => 0)); - // Capture Usage.test() events. - final StringBuffer buffer = await capturedConsolePrint(() => - createTestCommandRunner(command).run( - const <String>['build', 'ipa', '--no-pub', '--analyze-size'] - ) + await createTestCommandRunner(command).run( + const <String>['build', 'ipa', '--no-pub', '--analyze-size'] ); expect(testLogger.statusText, contains('A summary of your iOS bundle analysis can be found at')); expect(testLogger.statusText, contains('flutter pub global activate devtools; flutter pub global run devtools --appSizeBase=')); - expect(buffer.toString(), contains('event {category: code-size-analysis, action: ios, label: null, value: null, cd33: ')); + expect(usage.events, contains( + const TestUsageEvent('code-size-analysis', 'ios'), + )); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart index a38c01c..3ef45ed 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart
@@ -47,12 +47,12 @@ FileSystem fileSystem; ProcessManager processManager; - Usage usage; + TestUsage usage; setUp(() { fileSystem = MemoryFileSystem.test(); Cache.flutterRoot = _kTestFlutterRoot; - usage = Usage.test(); + usage = TestUsage(); }); // Creates the mock files necessary to look like a Flutter project. @@ -457,16 +457,15 @@ ..createSync(recursive: true) ..writeAsBytesSync(List<int>.filled(10000, 0)); - // Capture Usage.test() events. - final StringBuffer buffer = await capturedConsolePrint(() => - createTestCommandRunner(command).run( - const <String>['build', 'linux', '--no-pub', '--analyze-size'] - ) + await createTestCommandRunner(command).run( + const <String>['build', 'linux', '--no-pub', '--analyze-size'] ); expect(testLogger.statusText, contains('A summary of your Linux bundle analysis can be found at')); expect(testLogger.statusText, contains('flutter pub global activate devtools; flutter pub global run devtools --appSizeBase=')); - expect(buffer.toString(), contains('event {category: code-size-analysis, action: linux, label: null, value: null, cd33:')); + expect(usage.events, contains( + const TestUsageEvent('code-size-analysis', 'linux'), + )); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => processManager,
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart index 52f1ad5..d42e24f 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart
@@ -54,7 +54,7 @@ void main() { FileSystem fileSystem; - Usage usage; + TestUsage usage; setUpAll(() { Cache.disableLocking(); @@ -62,7 +62,7 @@ setUp(() { fileSystem = MemoryFileSystem.test(); - usage = Usage.test(); + usage = TestUsage(); }); // Sets up the minimal mock project files necessary to look like a Flutter project. @@ -338,16 +338,15 @@ ..createSync(recursive: true) ..writeAsBytesSync(List<int>.generate(10000, (int index) => 0)); - // Capture Usage.test() events. - final StringBuffer buffer = await capturedConsolePrint(() => - createTestCommandRunner(command).run( - const <String>['build', 'macos', '--no-pub', '--analyze-size'] - ) + await createTestCommandRunner(command).run( + const <String>['build', 'macos', '--no-pub', '--analyze-size'] ); expect(testLogger.statusText, contains('A summary of your macOS bundle analysis can be found at')); expect(testLogger.statusText, contains('flutter pub global activate devtools; flutter pub global run devtools --appSizeBase=')); - expect(buffer.toString(), contains('event {category: code-size-analysis, action: macos, label: null, value: null, cd33:')); + expect(usage.events, contains( + const TestUsageEvent('code-size-analysis', 'macos'), + )); }, overrides: <Type, Generator>{ FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart index 64da20c..b577778 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart
@@ -45,7 +45,7 @@ ProcessManager processManager; MockVisualStudio mockVisualStudio; - Usage usage; + TestUsage usage; setUpAll(() { Cache.disableLocking(); @@ -55,7 +55,7 @@ fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); Cache.flutterRoot = flutterRoot; mockVisualStudio = MockVisualStudio(); - usage = Usage.test(); + usage = TestUsage(); }); // Creates the mock files necessary to look like a Flutter project. @@ -401,16 +401,15 @@ }), ]); - // Capture Usage.test() events. - final StringBuffer buffer = await capturedConsolePrint(() => - createTestCommandRunner(command).run( - const <String>['windows', '--no-pub', '--analyze-size'] - ) + await createTestCommandRunner(command).run( + const <String>['windows', '--no-pub', '--analyze-size'] ); expect(testLogger.statusText, contains('A summary of your Windows bundle analysis can be found at')); expect(testLogger.statusText, contains('flutter pub global activate devtools; flutter pub global run devtools --appSizeBase=')); - expect(buffer.toString(), contains('event {category: code-size-analysis, action: windows, label: null, value: null, cd33:')); + expect(usage.events, contains( + const TestUsageEvent('code-size-analysis', 'windows'), + )); }, overrides: <Type, Generator>{ FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true), FileSystem: () => fileSystem,
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 9a597bc..78caba8 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
@@ -141,13 +141,13 @@ Artifacts artifacts; MockCache mockCache; MockProcessManager mockProcessManager; - Usage usage; + TestUsage usage; Directory tempDir; setUp(() { artifacts = Artifacts.test(); mockCache = MockCache(); - usage = Usage.test(); + usage = TestUsage(); fs = MemoryFileSystem.test(); mockProcessManager = MockProcessManager(); @@ -368,19 +368,18 @@ ..writeAsStringSync('# Hello, World'); globals.fs.currentDirectory = tempDir; - // Capture Usage.test() events. - final StringBuffer buffer = await capturedConsolePrint(() => - expectToolExitLater(createTestCommandRunner(command).run(<String>[ - 'run', - '--no-pub', - '--no-hot', - ]), isNull) - ); - // Allow any CustomDimensions.localTime (cd33) timestamp. - final RegExp usageRegexp = RegExp( - 'screenView {cd3: false, cd4: ios, cd22: iOS 13, cd23: debug, cd18: false, cd15: swift, cd31: false, cd33: .*, viewName: run' - ); - expect(buffer.toString(), matches(usageRegexp)); + await expectToolExitLater(createTestCommandRunner(command).run(<String>[ + 'run', + '--no-pub', + '--no-hot', + ]), isNull); + + expect(usage.commands, contains( + const TestUsageCommand('run', parameters: <String, String>{ + 'cd3': 'false', 'cd4': 'ios', 'cd22': 'iOS 13', + 'cd23': 'debug', 'cd18': 'false', 'cd15': 'swift', 'cd31': 'false', + } + ))); }, overrides: <Type, Generator>{ Artifacts: () => artifacts, Cache: () => mockCache,
diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart index 72a4245..cff5cb8 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -226,7 +226,7 @@ platform: platform, processManager: fakeProcessManager, terminal: terminal, - usage: Usage.test(), + usage: TestUsage(), ); fileSystem.file(xcodebuild).deleteSync(); @@ -430,7 +430,7 @@ platform: platform, processManager: fakeProcessManager, terminal: terminal, - usage: Usage.test(), + usage: TestUsage(), ); expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull); @@ -457,7 +457,7 @@ platform: platform, processManager: fakeProcessManager, terminal: terminal, - usage: Usage.test(), + usage: TestUsage(), ); expect(
diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart index c2ab75b..b8f886b 100644 --- a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
@@ -34,7 +34,7 @@ CocoaPods cocoaPodsUnderTest; InvokeProcess resultOfPodVersion; BufferLogger logger; - Usage usage; + TestUsage usage; void pretendPodVersionFails() { resultOfPodVersion = () async => exitsWithError(); @@ -71,7 +71,7 @@ projectUnderTest = FlutterProject.fromDirectory(fileSystem.directory('project')); projectUnderTest.ios.xcodeProject.createSync(recursive: true); projectUnderTest.macos.xcodeProject.createSync(recursive: true); - usage = Usage.test(); + usage = TestUsage(); cocoaPodsUnderTest = CocoaPods( fileSystem: fileSystem, processManager: mockProcessManager, @@ -486,21 +486,18 @@ 'LoadError - dlsym(0x7fbbeb6837d0, Init_ffi_c): symbol not found - /Library/Ruby/Gems/2.6.0/gems/ffi-1.13.1/lib/ffi_c.bundle', )); - // Capture Usage.test() events. - final StringBuffer buffer = - await capturedConsolePrint(() => expectToolExitLater( - cocoaPodsUnderTest.processPods( - xcodeProject: projectUnderTest.ios, - buildMode: BuildMode.debug, - ), - equals('Error running pod install'), - )); + await expectToolExitLater( + cocoaPodsUnderTest.processPods( + xcodeProject: projectUnderTest.ios, + buildMode: BuildMode.debug, + ), + equals('Error running pod install'), + ); expect( logger.errorText, contains('set up CocoaPods for ARM macOS'), ); - expect(buffer.toString(), - contains('event {category: pod-install-failure, action: arm-ffi')); + expect(usage.events, contains(const TestUsageEvent('pod-install-failure', 'arm-ffi'))); }); testWithoutContext('ffi failure on x86 macOS does not prompt gem install', () async {
diff --git a/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart index 433a725..0c49132 100644 --- a/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart +++ b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart
@@ -64,7 +64,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query())?.port; expect(port, isNull); @@ -76,7 +76,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: logger, - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); final Uri uri = await portDiscovery.getObservatoryUri( '', @@ -101,7 +101,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query())?.port; expect(port, 123); @@ -127,7 +127,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); final MDnsObservatoryDiscoveryResult result = await portDiscovery.query(); expect(result?.port, 123); @@ -153,7 +153,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); expect(portDiscovery.query, throwsToolExit()); }); @@ -177,7 +177,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query(applicationId: 'fiz'))?.port; expect(port, 321); @@ -204,7 +204,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; expect(port, 1234); @@ -219,7 +219,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; expect(port, isNull); @@ -234,7 +234,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); expect( () async => await portDiscovery.query(), @@ -259,7 +259,7 @@ final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), - flutterUsage: Usage.test(), + flutterUsage: TestUsage(), ); final Uri uri = await portDiscovery.getObservatoryUri('bar', mockDevice, hostVmservicePort: 0); expect(uri.toString(), 'http://127.0.0.1:123/');