diff --git a/dev/e2e_app/integration_test/example_test.dart b/dev/e2e_app/integration_test/example_test.dart index 096142ad0..c4bfdbe0d 100644 --- a/dev/e2e_app/integration_test/example_test.dart +++ b/dev/e2e_app/integration_test/example_test.dart @@ -19,6 +19,7 @@ void main() { await $(#textField).enterText('Hello, Flutter!'); expect($('Hello, Flutter!'), findsOneWidget); + $.log('Tapped the button'); await $.native.pressHome(); await $.native.openApp(); @@ -32,6 +33,7 @@ void main() { patrol( 'short test with two tags', + skip: true, tags: ['smoke', 'fume'], ($) async { await createApp($); diff --git a/dev/e2e_app/macos/Flutter/GeneratedPluginRegistrant.swift b/dev/e2e_app/macos/Flutter/GeneratedPluginRegistrant.swift index 571b1e023..9b746f0de 100644 --- a/dev/e2e_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/dev/e2e_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import flutter_local_notifications import flutter_timezone import geolocator_apple import patrol +import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) @@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) PatrolPlugin.register(with: registry.registrar(forPlugin: "PatrolPlugin")) + FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin")) } diff --git a/dev/e2e_app/pubspec.lock b/dev/e2e_app/pubspec.lock index f585db467..d298e677d 100644 --- a/dev/e2e_app/pubspec.lock +++ b/dev/e2e_app/pubspec.lock @@ -198,6 +198,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + dispose_scope: + dependency: transitive + description: + name: dispose_scope + sha256: "48ec38ca2631c53c4f8fa96b294c801e55c335db5e3fb9f82cede150cfe5a2af" + url: "https://pub.dev" + source: hosted + version: "2.1.0" equatable: dependency: transitive description: @@ -491,15 +499,21 @@ packages: path: "../../packages/patrol" relative: true source: path - version: "3.11.1" + version: "3.11.2" patrol_finders: dependency: transitive description: - name: patrol_finders - sha256: "6bf2c3093fbccd02f80f73fafc1bd021d76410cbab6e329be220b5e3bc58f072" - url: "https://pub.dev" - source: hosted + path: "../../packages/patrol_finders" + relative: true + source: path version: "2.1.2" + patrol_log: + dependency: transitive + description: + path: "../../packages/patrol_log" + relative: true + source: path + version: "0.0.1" permission_handler: dependency: "direct main" description: diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 904e07a9f..10b1bd394 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -1,5 +1,6 @@ -## Unreleased +## 3.13.0-dev.1 +- Add support for the patrol_log package. (#2387) - Fix tapping on notification on iOS 18. (#2394) ## 3.12.0 diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolJUnitRunner.java b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolJUnitRunner.java index 1e5b35648..47e79b3f8 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolJUnitRunner.java +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolJUnitRunner.java @@ -149,6 +149,7 @@ public RunDartTestResponse runDartTest(String name) { if (response.getResult() == Contracts.RunDartTestResponseResult.failure) { throw new AssertionError("Dart test failed: " + name + "\n" + response.getDetails()); } + Logger.INSTANCE.i(TAG + "Test execution succeeded"); return response; } catch (PatrolAppServiceClientException e) { Logger.INSTANCE.e(TAG + e.getMessage(), e.getCause()); diff --git a/packages/patrol/lib/src/common.dart b/packages/patrol/lib/src/common.dart index a3f54a88f..1ba923717 100644 --- a/packages/patrol/lib/src/common.dart +++ b/packages/patrol/lib/src/common.dart @@ -10,6 +10,7 @@ import 'package:patrol/src/global_state.dart' as global_state; import 'package:patrol/src/native/contracts/contracts.dart'; import 'package:patrol/src/native/native.dart'; import 'package:patrol_finders/patrol_finders.dart' as finders; +import 'package:patrol_log/patrol_log.dart'; /// We need [Group] to recreate test hierarchy. // ignore: implementation_imports @@ -94,11 +95,15 @@ void patrolTest( LiveTestWidgetsFlutterBindingFramePolicy framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fadePointers, }) { + final patrolLog = PatrolLogWriter(); final automator = NativeAutomator(config: nativeAutomatorConfig); final automator2 = NativeAutomator2(config: nativeAutomatorConfig); final patrolBinding = PatrolBinding.ensureInitialized(nativeAutomatorConfig) ..framePolicy = framePolicy; + if (skip ?? false) { + patrolLog.log(TestEntry(name: description, status: TestEntryStatus.skip)); + } testWidgets( description, skip: skip, @@ -131,6 +136,9 @@ void patrolTest( // We don't have to call this line because automator.configure() does the same. // await automator2.configure(); + patrolLog.log( + TestEntry(name: description, status: TestEntryStatus.start), + ); final patrolTester = PatrolIntegrationTester( tester: widgetTester, nativeAutomator: automator, diff --git a/packages/patrol/lib/src/custom_finders/patrol_integration_tester.dart b/packages/patrol/lib/src/custom_finders/patrol_integration_tester.dart index eacdd4ca7..c70fa1acd 100644 --- a/packages/patrol/lib/src/custom_finders/patrol_integration_tester.dart +++ b/packages/patrol/lib/src/custom_finders/patrol_integration_tester.dart @@ -1,17 +1,21 @@ import 'package:patrol/src/native/native_automator.dart'; import 'package:patrol/src/native/native_automator2.dart'; import 'package:patrol_finders/patrol_finders.dart' as finders; +import 'package:patrol_log/patrol_log.dart'; /// PatrolIntegrationTester extends the capabilities of [finders.PatrolTester] /// with the ability to interact with native platform features via [native]. class PatrolIntegrationTester extends finders.PatrolTester { /// Creates a new [PatrolIntegrationTester] which wraps [tester]. - const PatrolIntegrationTester({ + PatrolIntegrationTester({ required super.tester, required super.config, required this.nativeAutomator, required this.nativeAutomator2, - }); + }) : _patrolLog = PatrolLogWriter(); + + /// The log for the patrol. + final PatrolLogWriter _patrolLog; /// Native automator that allows for interaction with OS the app is running /// on. @@ -28,4 +32,9 @@ class PatrolIntegrationTester extends finders.PatrolTester { /// Shorthand for [nativeAutomator2]. NativeAutomator2 get native2 => nativeAutomator2; + + /// Logs a message to the patrol log. + void log(String message) { + _patrolLog.log(LogEntry(message: message)); + } } diff --git a/packages/patrol/lib/src/native/native_automator.dart b/packages/patrol/lib/src/native/native_automator.dart index e390a09aa..f17b6e1d0 100644 --- a/packages/patrol/lib/src/native/native_automator.dart +++ b/packages/patrol/lib/src/native/native_automator.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import 'package:patrol/src/native/contracts/contracts.dart' as contracts; import 'package:patrol/src/native/contracts/contracts.dart'; import 'package:patrol/src/native/contracts/native_automator_client.dart'; +import 'package:patrol_log/patrol_log.dart'; /// Thrown when a native action fails. class PatrolActionException implements Exception { @@ -199,6 +200,7 @@ class NativeAutomator { _config.logger('NativeAutomatorClient created, port: ${_config.port}'); } + final PatrolLogWriter _patrolLog = PatrolLogWriter(); final NativeAutomatorConfig _config; late final NativeAutomatorClient _client; @@ -214,19 +216,53 @@ class NativeAutomator { throw StateError('unsupported platform'); } - Future _wrapRequest(String name, Future Function() request) async { + Future _wrapRequest( + String name, + Future Function() request, { + bool enablePatrolLog = true, + }) async { _config.logger('$name() started'); + final text = + '${AnsiCodes.lightBlue}$name${AnsiCodes.reset} ${AnsiCodes.gray}(native)${AnsiCodes.reset}'; + + if (enablePatrolLog) { + _patrolLog.log(StepEntry(action: text, status: StepEntryStatus.start)); + } try { final result = await request(); _config.logger('$name() succeeded'); + if (enablePatrolLog) { + _patrolLog + .log(StepEntry(action: text, status: StepEntryStatus.success)); + } return result; } on NativeAutomatorClientException catch (err) { _config.logger('$name() failed'); final log = 'NativeAutomatorClientException: ' '$name() failed with $err'; + + if (enablePatrolLog) { + _patrolLog.log( + StepEntry( + action: text, + status: StepEntryStatus.failure, + exception: log, + ), + ); + } throw PatrolActionException(log); } catch (err) { _config.logger('$name() failed'); + + if (enablePatrolLog) { + _patrolLog.log( + StepEntry( + action: text, + status: StepEntryStatus.failure, + exception: err.toString(), + ), + ); + } rethrow; } } @@ -241,7 +277,11 @@ class NativeAutomator { /// See also: /// * https://github.com/flutter/flutter/issues/129231 Future initialize() async { - await _wrapRequest('initialize', _client.initialize); + await _wrapRequest( + 'initialize', + _client.initialize, + enablePatrolLog: false, + ); } /// Configures the native automator. @@ -260,6 +300,7 @@ class NativeAutomator { findTimeoutMillis: _config.findTimeout.inMilliseconds, ), ), + enablePatrolLog: false, ); exception = null; break; @@ -940,6 +981,7 @@ class NativeAutomator { await _wrapRequest( 'markPatrolAppServiceReady', _client.markPatrolAppServiceReady, + enablePatrolLog: false, ); } } diff --git a/packages/patrol/lib/src/native/native_automator2.dart b/packages/patrol/lib/src/native/native_automator2.dart index 745c1d82f..cfe9fd48f 100644 --- a/packages/patrol/lib/src/native/native_automator2.dart +++ b/packages/patrol/lib/src/native/native_automator2.dart @@ -8,6 +8,7 @@ import 'package:patrol/src/native/contracts/contracts.dart' as contracts; import 'package:patrol/src/native/contracts/native_automator_client.dart'; import 'package:patrol/src/native/native_automator.dart'; import 'package:patrol/src/native/native_automator.dart' as native_automator; +import 'package:patrol_log/patrol_log.dart'; /// This class represents the result of [NativeAutomator.getNativeViews]. class GetNativeViewsResult { @@ -81,6 +82,7 @@ class NativeAutomator2 { _config.logger('NativeAutomatorClient created, port: ${_config.port}'); } + final PatrolLogWriter _patrolLog = PatrolLogWriter(); final NativeAutomatorConfig _config; late final NativeAutomatorClient _client; @@ -96,19 +98,53 @@ class NativeAutomator2 { throw StateError('unsupported platform'); } - Future _wrapRequest(String name, Future Function() request) async { + Future _wrapRequest( + String name, + Future Function() request, { + bool enablePatrolLog = true, + }) async { _config.logger('$name() started'); + final text = + '${AnsiCodes.lightBlue}$name${AnsiCodes.reset} ${AnsiCodes.gray}(native)${AnsiCodes.reset}'; + + if (enablePatrolLog) { + _patrolLog.log(StepEntry(action: text, status: StepEntryStatus.start)); + } try { final result = await request(); _config.logger('$name() succeeded'); + if (enablePatrolLog) { + _patrolLog + .log(StepEntry(action: text, status: StepEntryStatus.success)); + } return result; } on NativeAutomatorClientException catch (err) { _config.logger('$name() failed'); final log = 'NativeAutomatorClientException: ' '$name() failed with $err'; + + if (enablePatrolLog) { + _patrolLog.log( + StepEntry( + action: text, + status: StepEntryStatus.failure, + exception: log, + ), + ); + } throw PatrolActionException(log); } catch (err) { _config.logger('$name() failed'); + + if (enablePatrolLog) { + _patrolLog.log( + StepEntry( + action: text, + status: StepEntryStatus.failure, + exception: err.toString(), + ), + ); + } rethrow; } } @@ -123,7 +159,11 @@ class NativeAutomator2 { /// See also: /// * https://github.com/flutter/flutter/issues/129231 Future initialize() async { - await _wrapRequest('initialize', _client.initialize); + await _wrapRequest( + 'initialize', + _client.initialize, + enablePatrolLog: false, + ); } /// Configures the native automator. @@ -142,6 +182,7 @@ class NativeAutomator2 { findTimeoutMillis: _config.findTimeout.inMilliseconds, ), ), + enablePatrolLog: false, ); exception = null; break; @@ -819,6 +860,7 @@ class NativeAutomator2 { await _wrapRequest( 'markPatrolAppServiceReady', _client.markPatrolAppServiceReady, + enablePatrolLog: false, ); } } diff --git a/packages/patrol/lib/src/native/patrol_app_service.dart b/packages/patrol/lib/src/native/patrol_app_service.dart index a3485d32e..090af209d 100644 --- a/packages/patrol/lib/src/native/patrol_app_service.dart +++ b/packages/patrol/lib/src/native/patrol_app_service.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:patrol/src/common.dart'; import 'package:patrol/src/native/contracts/contracts.dart'; import 'package:patrol/src/native/contracts/patrol_app_service_server.dart'; +import 'package:patrol_log/patrol_log.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; @@ -78,6 +79,8 @@ class PatrolAppService extends PatrolAppServiceServer { return _testExecutionCompleted.future; } + final PatrolLogWriter _patrolLog = PatrolLogWriter(); + /// Marks [dartFileName] as completed with the given [passed] status. /// /// If an exception was thrown during the test, [details] should contain the @@ -150,6 +153,25 @@ class PatrolAppService extends PatrolAppServiceServer { _testExecutionRequested.complete(request.name); final testExecutionResult = await testExecutionCompleted; + if (!testExecutionResult.passed) { + _patrolLog.log( + TestEntry( + name: request.name, + status: TestEntryStatus.failure, + ), + ); + testExecutionResult.details?.split('\n').forEach( + (e) => _patrolLog.log(ErrorEntry(message: e)), + ); + } else { + _patrolLog.log( + TestEntry( + name: request.name, + status: TestEntryStatus.success, + ), + ); + } + return RunDartTestResponse( result: testExecutionResult.passed ? RunDartTestResponseResult.success diff --git a/packages/patrol/pubspec.yaml b/packages/patrol/pubspec.yaml index a36c2d895..086de991f 100644 --- a/packages/patrol/pubspec.yaml +++ b/packages/patrol/pubspec.yaml @@ -2,7 +2,7 @@ name: patrol description: > Powerful Flutter-native UI testing framework overcoming limitations of existing Flutter testing tools. Ready for action! -version: 3.12.0 +version: 3.13.0-dev.1 homepage: https://patrol.leancode.co repository: https://github.com/leancodepl/patrol/tree/master/packages/patrol issue_tracker: https://github.com/leancodepl/patrol/issues @@ -26,7 +26,8 @@ dependencies: http: '^1.1.0' json_annotation: ^4.8.1 meta: ^1.10.0 - patrol_finders: ^2.1.2 + patrol_finders: ^2.2.0 + patrol_log: ^0.0.1+1 shelf: ^1.4.1 test_api: '^0.7.0' diff --git a/packages/patrol_cli/CHANGELOG.md b/packages/patrol_cli/CHANGELOG.md index b94636c57..42fcf70a4 100644 --- a/packages/patrol_cli/CHANGELOG.md +++ b/packages/patrol_cli/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.4.0-dev.1 + +- Add support for the patrol_log package. (#2387) + ## 3.3.0 - Add `clear-permissions` flag on ios commands. (#2367) diff --git a/packages/patrol_cli/lib/src/android/android_test_backend.dart b/packages/patrol_cli/lib/src/android/android_test_backend.dart index 6f63715d3..f4761cda7 100644 --- a/packages/patrol_cli/lib/src/android/android_test_backend.dart +++ b/packages/patrol_cli/lib/src/android/android_test_backend.dart @@ -11,6 +11,7 @@ import 'package:patrol_cli/src/base/process.dart'; import 'package:patrol_cli/src/crossplatform/app_options.dart'; import 'package:patrol_cli/src/devices.dart'; import 'package:patrol_cli/src/runner/flutter_command.dart'; +import 'package:patrol_log/patrol_log.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; @@ -94,6 +95,7 @@ class AndroidTestBackend { ..disposedBy(scope); process.listenStdOut((l) => _logger.detail('\t: $l')).disposedBy(scope); process.listenStdErr((l) => _logger.err('\t$l')).disposedBy(scope); + exitCode = await process.exitCode; if (exitCode == 0) { task.complete('Completed building $subject'); @@ -186,9 +188,42 @@ class AndroidTestBackend { Future execute( AndroidAppOptions options, Device device, { + String? flavor, bool interruptible = false, + required bool showFlutterLogs, + required bool hideTestSteps, }) async { await _disposeScope.run((scope) async { + // Read patrol logs from logcat + final processLogcat = await _adb.logcat( + device: device.id, + arguments: { + '-T': '1', + }, + filter: 'PatrolServer:I Patrol:I flutter:I *:S', + ) + ..disposedBy(scope); + + var flavorPath = ''; + if (flavor != null) { + flavorPath = 'flavors/$flavor/'; + } + final path = + 'file://${_rootDirectory.path}/build/app/reports/androidTests/connected/${flavorPath}index.html'; + final reportPath = + _platform.isWindows ? path.replaceAll(r'\', '/') : path; + + final patrolLogReader = PatrolLogReader( + listenStdOut: processLogcat.listenStdOut, + scope: scope, + log: _logger.info, + reportPath: reportPath, + showFlutterLogs: showFlutterLogs, + hideTestSteps: hideTestSteps, + ) + ..listen() + ..startTimer(); + final subject = '${options.description} on ${device.description}'; final task = _logger.task('Executing tests of $subject'); @@ -211,13 +246,21 @@ class AndroidTestBackend { const prefix = 'There were failing tests. '; if (l.contains(prefix)) { final msg = l.substring(prefix.length + 2); - _logger.err('\t$msg'); + _logger.detail('\t$msg'); } else { _logger.detail('\t$l'); } }).disposedBy(scope); final exitCode = await process.exitCode; + patrolLogReader.stopTimer(); + processLogcat.kill(); + + // Don't print the summary in develop + if (!interruptible) { + _logger.info(patrolLogReader.summary); + } + if (exitCode == 0) { task.complete('Completed executing $subject'); } else if (exitCode != 0 && interruptible) { diff --git a/packages/patrol_cli/lib/src/base/constants.dart b/packages/patrol_cli/lib/src/base/constants.dart index 34fa1e61e..267c66140 100644 --- a/packages/patrol_cli/lib/src/base/constants.dart +++ b/packages/patrol_cli/lib/src/base/constants.dart @@ -1,3 +1,3 @@ /// Version of Patrol CLI. Must be kept in sync with pubspec.yaml. /// If you update this, make sure that compatibility-table.mdx is updated (if needed) -const version = '3.3.0'; +const version = '3.4.0-dev.1'; diff --git a/packages/patrol_cli/lib/src/commands/develop.dart b/packages/patrol_cli/lib/src/commands/develop.dart index b20ec5690..6accebf7c 100644 --- a/packages/patrol_cli/lib/src/commands/develop.dart +++ b/packages/patrol_cli/lib/src/commands/develop.dart @@ -54,6 +54,7 @@ class DevelopCommand extends PatrolCommand { usesWaitOption(); usesPortOptions(); usesTagsOption(); + usesHideTestSteps(); usesUninstallOption(); @@ -244,6 +245,8 @@ class DevelopCommand extends PatrolCommand { uninstall: uninstall, device: device, openDevtools: boolArg('open-devtools'), + showFlutterLogs: false, + hideTestSteps: boolArg('hide-test-steps'), ); return 0; // for now, all exit codes are 0 @@ -319,6 +322,8 @@ class DevelopCommand extends PatrolCommand { required bool uninstall, required Device device, required bool openDevtools, + required bool showFlutterLogs, + required bool hideTestSteps, }) async { Future Function() action; Future Function()? finalizer; @@ -327,8 +332,14 @@ class DevelopCommand extends PatrolCommand { switch (device.targetPlatform) { case TargetPlatform.android: appId = android.packageName; - action = () => - _androidTestBackend.execute(android, device, interruptible: true); + action = () => _androidTestBackend.execute( + android, + device, + interruptible: true, + showFlutterLogs: showFlutterLogs, + hideTestSteps: hideTestSteps, + flavor: flutterOpts.flavor, + ); final package = android.packageName; if (package != null && uninstall) { finalizer = () => _androidTestBackend.uninstall(package, device); @@ -339,8 +350,13 @@ class DevelopCommand extends PatrolCommand { _macosTestBackend.execute(macos, device, interruptible: true); case TargetPlatform.iOS: appId = iosOpts.bundleId; - action = () async => - _iosTestBackend.execute(iosOpts, device, interruptible: true); + action = () async => _iosTestBackend.execute( + iosOpts, + device, + interruptible: true, + showFlutterLogs: showFlutterLogs, + hideTestSteps: hideTestSteps, + ); final bundleId = iosOpts.bundleId; if (bundleId != null && uninstall) { finalizer = () => _iosTestBackend.uninstall( diff --git a/packages/patrol_cli/lib/src/commands/test.dart b/packages/patrol_cli/lib/src/commands/test.dart index acde73363..78ea968fc 100644 --- a/packages/patrol_cli/lib/src/commands/test.dart +++ b/packages/patrol_cli/lib/src/commands/test.dart @@ -56,6 +56,8 @@ class TestCommand extends PatrolCommand { usesTagsOption(); usesExcludeTagsOption(); useCoverageOptions(); + usesShowFlutterLogs(); + usesHideTestSteps(); usesUninstallOption(); @@ -266,6 +268,8 @@ See https://github.com/leancodepl/patrol/issues/1316 to learn more. macosOpts, uninstall: uninstall, device: device, + showFlutterLogs: boolArg('show-flutter-logs'), + hideTestSteps: boolArg('hide-test-steps'), ); return allPassed ? 0 : 1; @@ -340,13 +344,21 @@ See https://github.com/leancodepl/patrol/issues/1316 to learn more. MacOSAppOptions macos, { required bool uninstall, required Device device, + required bool showFlutterLogs, + required bool hideTestSteps, }) async { Future Function() action; Future Function()? finalizer; switch (device.targetPlatform) { case TargetPlatform.android: - action = () => _androidTestBackend.execute(android, device); + action = () => _androidTestBackend.execute( + android, + device, + showFlutterLogs: showFlutterLogs, + hideTestSteps: hideTestSteps, + flavor: flutterOpts.flavor, + ); final package = android.packageName; if (package != null && uninstall) { finalizer = () => _androidTestBackend.uninstall(package, device); @@ -354,7 +366,12 @@ See https://github.com/leancodepl/patrol/issues/1316 to learn more. case TargetPlatform.macOS: action = () async => _macosTestBackend.execute(macos, device); case TargetPlatform.iOS: - action = () async => _iosTestBackend.execute(ios, device); + action = () async => _iosTestBackend.execute( + ios, + device, + showFlutterLogs: showFlutterLogs, + hideTestSteps: hideTestSteps, + ); final bundleId = ios.bundleId; if (bundleId != null && uninstall) { finalizer = () => _iosTestBackend.uninstall( diff --git a/packages/patrol_cli/lib/src/crossplatform/flutter_tool.dart b/packages/patrol_cli/lib/src/crossplatform/flutter_tool.dart index 1999059e9..11beccc4d 100644 --- a/packages/patrol_cli/lib/src/crossplatform/flutter_tool.dart +++ b/packages/patrol_cli/lib/src/crossplatform/flutter_tool.dart @@ -222,6 +222,12 @@ class FlutterTool { completer.complete(); } + // Skip the log line that contains "PATROL_LOG" prefix + const patrolLogPrefix = 'PATROL_LOG'; + if (line.contains(patrolLogPrefix)) { + return; + } + // On iOS, "flutter" is not prefixed final flutterPrefix = RegExp('flutter: '); diff --git a/packages/patrol_cli/lib/src/ios/ios_test_backend.dart b/packages/patrol_cli/lib/src/ios/ios_test_backend.dart index b95294dea..e6ee8652e 100644 --- a/packages/patrol_cli/lib/src/ios/ios_test_backend.dart +++ b/packages/patrol_cli/lib/src/ios/ios_test_backend.dart @@ -11,6 +11,7 @@ import 'package:patrol_cli/src/base/logger.dart'; import 'package:patrol_cli/src/base/process.dart'; import 'package:patrol_cli/src/crossplatform/app_options.dart'; import 'package:patrol_cli/src/devices.dart'; +import 'package:patrol_log/patrol_log.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; @@ -103,7 +104,7 @@ class IOSTestBackend { process.kill(); flutterBuildKilled = true; // `flutter build` has exit code 0 on SIGINT }); - process.listenStdOut((l) => _logger.detail('\t$l')).disposedBy(scope); + process.listenStdOut((l) => _logger.info('\t$l')).disposedBy(scope); process.listenStdErr((l) => _logger.err('\t$l')).disposedBy(scope); var exitCode = await process.exitCode; final flutterCommand = options.flutter.command; @@ -152,15 +153,38 @@ class IOSTestBackend { IOSAppOptions options, Device device, { bool interruptible = false, + required bool showFlutterLogs, + required bool hideTestSteps, }) async { await _disposeScope.run((scope) async { - final subject = '${options.description} on ${device.description}'; - final task = _logger.task('Running $subject'); + // Read patrol logs from log stream + final processLogs = await _processManager.start( + [ + 'log', + 'stream', + ], + runInShell: true, + ) + ..disposedBy(scope); - final resultsPath = resultBundlePath( + final reportPath = resultBundlePath( timestamp: DateTime.now().millisecondsSinceEpoch, ); + final patrolLogReader = PatrolLogReader( + listenStdOut: processLogs.listenStdOut, + scope: scope, + log: _logger.info, + reportPath: reportPath, + showFlutterLogs: showFlutterLogs, + hideTestSteps: hideTestSteps, + ) + ..listen() + ..startTimer(); + + final subject = '${options.description} on ${device.description}'; + final task = _logger.task('Running $subject'); + final sdkVersion = await getSdkVersion(real: device.real); final process = await _processManager.start( options.testWithoutBuildingInvocation( @@ -170,7 +194,7 @@ class IOSTestBackend { scheme: options.scheme, sdkVersion: sdkVersion, ), - resultBundlePath: resultsPath, + resultBundlePath: reportPath, ), runInShell: true, environment: { @@ -182,13 +206,19 @@ class IOSTestBackend { ) ..disposedBy(_disposeScope); process.listenStdOut((l) => _logger.detail('\t$l')).disposedBy(scope); - process.listenStdErr((l) => _logger.err('\t$l')).disposedBy(scope); + process.listenStdErr((l) => _logger.detail('\t$l')).disposedBy(scope); final exitCode = await process.exitCode; + patrolLogReader.stopTimer(); + processLogs.kill(); + + // Don't print the summary in develop + if (!interruptible) { + _logger.info(patrolLogReader.summary); + } if (exitCode == 0) { task.complete('Completed executing $subject'); - _logger.info('See the native Xcode report at $resultsPath'); } else if (exitCode != 0 && interruptible) { task.complete('App shut down on request'); } else if (exitCode == _xcodebuildInterrupted) { diff --git a/packages/patrol_cli/lib/src/runner/patrol_command.dart b/packages/patrol_cli/lib/src/runner/patrol_command.dart index c3fcf28ee..23f3e1d40 100644 --- a/packages/patrol_cli/lib/src/runner/patrol_command.dart +++ b/packages/patrol_cli/lib/src/runner/patrol_command.dart @@ -188,6 +188,20 @@ abstract class PatrolCommand extends Command { ); } + void usesShowFlutterLogs() { + argParser.addFlag( + 'show-flutter-logs', + help: 'Show Flutter logs while running the tests.', + ); + } + + void usesHideTestSteps() { + argParser.addFlag( + 'hide-test-steps', + help: 'Hide test steps while running the tests.', + ); + } + /// Gets the parsed command-line flag named [name] as a `bool`. /// /// If no flag named [name] was added to the `ArgParser`, an [ArgumentError] diff --git a/packages/patrol_cli/pubspec.yaml b/packages/patrol_cli/pubspec.yaml index a2341aa77..7e1ec8d33 100644 --- a/packages/patrol_cli/pubspec.yaml +++ b/packages/patrol_cli/pubspec.yaml @@ -1,7 +1,7 @@ name: patrol_cli description: > Command-line tool for Patrol, a powerful Flutter-native UI testing framework. -version: 3.3.0 # Must be kept in sync with constants.dart +version: 3.4.0-dev.1 # Must be kept in sync with constants.dart homepage: https://patrol.leancode.co repository: https://github.com/leancodepl/patrol/tree/master/packages/patrol_cli issue_tracker: https://github.com/leancodepl/patrol/issues?q=is%3Aopen+is%3Aissue+label%3A%22package%3A+patrol_cli%22 @@ -15,7 +15,7 @@ environment: sdk: '>=3.5.0 <4.0.0' dependencies: - adb: ^0.4.0 + adb: ^0.5.0 ansi_styles: ^0.3.2+1 args: ^2.4.2 ci: ^0.1.0 @@ -30,6 +30,7 @@ dependencies: mason_logger: ^0.2.10 meta: ^1.10.0 path: ^1.8.3 + patrol_log: ^0.0.1+1 platform: ^3.1.3 process: ^5.0.1 pub_updater: ^0.4.0