diff --git a/dev/cli_tests/patrol_develop_test.dart b/dev/cli_tests/patrol_develop_test.dart index c61102c66..677283a1c 100644 --- a/dev/cli_tests/patrol_develop_test.dart +++ b/dev/cli_tests/patrol_develop_test.dart @@ -38,7 +38,7 @@ void main() { void main(List args) async { _verifyWorkingDirectory(); - const afterBuildCompletedTimeout = Duration(minutes: 4); + const afterBuildCompletedTimeout = Duration(minutes: 2); const inactivityTimeout = Duration(minutes: 15); var isFirstTestPassed = false; diff --git a/dev_docs/GUIDE.md b/dev_docs/GUIDE.md new file mode 100644 index 000000000..1e1cc5d9b --- /dev/null +++ b/dev_docs/GUIDE.md @@ -0,0 +1,53 @@ +# Working on the test bundling feature + +_Test bundling_, also known as _native automation_, is a core feature of Patrol. +It bridges the native world of tests on Android and iOS with the Flutter/Dart +world of tests. + +It lives in the [patrol package](../packages/patrol). + +To learn more about test bundling, [read this article][test_bundling_article]. + +This document is a collection of tips and tricks to make it easier to work on +test bundling-related code. + +### Tools + +`adb logcat` is your friend. Spice it up with `-v color`. If you need something +more powerful, check out [`purr`](https://github.com/google/purr). + +### Show Dart-side logs only + +Search for `flutter :`. + +### Find out when a test starts + +Search for `TestRunner: started`. + +``` +09-21 12:24:09.223 23387 23406 I TestRunner: started: runDartTest[callbacks_test testA](pl.leancode.patrol.example.MainActivityTest) +``` + +### Find out when a test ends + +Search for `TestRunner: finished`. + +### I made some changes to test bundling code that result in a deadlock + +This can often happen when editing test bundling code. Because of various +limitations of the `test` package, which Patrol has to base on, test bundling +code is full of shared global mutable state and unobvious things happening in +parallel. + +When trying to find the cause of a deadlock: + +- search for `await`s in custom functions provided by Patrol (e.g. + `patrolTest()` and `patrolSetUpAll()`) and global lifecycle callbacks + registered by the generated Dart test bundle or PatrolBinding (e.g. + `tearDown()`s) +- Use `print`s amply to pinpint where the code is stuck. + +In the future, we should think about how to refactor this code to be more +maintainable and simpler. + +[test_bundling_article]: https://leancode.co/blog/patrol-2-0-improved-flutter-ui-testing diff --git a/melos.yaml b/melos.yaml index 0b462d833..5aeb96cd0 100644 --- a/melos.yaml +++ b/melos.yaml @@ -8,6 +8,11 @@ scripts: run: melos run analyze && melos run format-check description: Run all static analysis checks. + get:all: + run: melos exec -- \ + flutter pub get + description: Run `flutter pub get` for all packages. + analyze: run: | melos exec -- \ diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 82f84e5fe..14be96f03 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -1,3 +1,10 @@ +## 3.1.0-dev.1 + +- Add `patrolSetUp()` and `patrolTearDown()` (#1721) +- Add `patrolSetUpAll()` (#1751) + +This version requires version 2.4.0-dev.1 of `patrol_cli` package. + ## 3.0.3 - Implement `$.native.swipe()` on iOS (#1927) @@ -30,6 +37,7 @@ Other changes: - Bump minimum supported Flutter version to 3.16 - **BREAKING:** + - Remove `bindingType` parameter from `patrolTest()` function. Now only `PatrolBinding` is used and it's automatically initialized (#1882) - Remove `nativeAutomation` parameter from `patrolTest()` function. Now it's diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt index c2edb6640..598e73eb9 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt @@ -1,5 +1,6 @@ package pl.leancode.patrol +import androidx.test.platform.app.InstrumentationRegistry import pl.leancode.patrol.contracts.Contracts.ConfigureRequest import pl.leancode.patrol.contracts.Contracts.DarkModeRequest import pl.leancode.patrol.contracts.Contracts.EnterTextRequest @@ -11,6 +12,7 @@ import pl.leancode.patrol.contracts.Contracts.GetNotificationsRequest import pl.leancode.patrol.contracts.Contracts.GetNotificationsResponse import pl.leancode.patrol.contracts.Contracts.HandlePermissionRequest import pl.leancode.patrol.contracts.Contracts.HandlePermissionRequestCode +import pl.leancode.patrol.contracts.Contracts.MarkLifecycleCallbackExecutedRequest import pl.leancode.patrol.contracts.Contracts.OpenAppRequest import pl.leancode.patrol.contracts.Contracts.OpenQuickSettingsRequest import pl.leancode.patrol.contracts.Contracts.PermissionDialogVisibleRequest @@ -217,4 +219,9 @@ class AutomatorServer(private val automation: Automator) : NativeAutomatorServer override fun markPatrolAppServiceReady() { PatrolServer.appReady.open() } + + override fun markLifecycleCallbackExecuted(request: MarkLifecycleCallbackExecutedRequest) { + val instrumentation = InstrumentationRegistry.getInstrumentation() as PatrolJUnitRunner + instrumentation.markLifecycleCallbackExecuted(request.name) + } } diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolAppServiceClient.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolAppServiceClient.kt index 7bdfa7bad..f072b97e0 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolAppServiceClient.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolAppServiceClient.kt @@ -33,6 +33,20 @@ class PatrolAppServiceClient { return result.group } + @Throws(PatrolAppServiceClientException::class) + fun listDartLifecycleCallbacks(): Contracts.ListDartLifecycleCallbacksResponse { + Logger.i("PatrolAppServiceClient.listDartLifecycleCallbacks()") + val result = client.listDartLifecycleCallbacks() + return result + } + + @Throws(PatrolAppServiceClientException::class) + fun setLifecycleCallbacksState(data: Map): Contracts.Empty { + Logger.i("PatrolAppServiceClient.setLifecycleCallbacksState()") + val result = client.setLifecycleCallbacksState(Contracts.SetLifecycleCallbacksStateRequest(data)) + return result + } + @Throws(PatrolAppServiceClientException::class) fun runDartTest(name: String): Contracts.RunDartTestResponse { Logger.i("PatrolAppServiceClient.runDartTest($name)") 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 03c969a7e..91ea9bcef 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 @@ -5,19 +5,33 @@ package pl.leancode.patrol; +import android.annotation.SuppressLint; import android.app.Instrumentation; +import android.content.ContentResolver; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnitRunner; - +import androidx.test.services.storage.file.HostedFile; +import androidx.test.services.storage.internal.TestStorageUtil; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import pl.leancode.patrol.contracts.Contracts; +import pl.leancode.patrol.contracts.Contracts.ListDartLifecycleCallbacksResponse; import pl.leancode.patrol.contracts.PatrolAppServiceClientException; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; -import java.util.concurrent.ExecutionException; +import java.util.Map; +import java.util.Scanner; import static pl.leancode.patrol.contracts.Contracts.DartGroupEntry; import static pl.leancode.patrol.contracts.Contracts.RunDartTestResponse; @@ -27,9 +41,30 @@ * A customized AndroidJUnitRunner that enables Patrol on Android. *

*/ +@SuppressLint({"UnsafeOptInUsageError", "RestrictedApi"}) public class PatrolJUnitRunner extends AndroidJUnitRunner { public PatrolAppServiceClient patrolAppServiceClient; + /** + *

+ * Available only after onCreate() has been run. + *

+ */ + protected boolean isInitialRun; + + private ContentResolver getContentResolver() { + return InstrumentationRegistry.getInstrumentation().getTargetContext().getContentResolver(); + } + + private Uri stateFileUri = HostedFile.buildUri( + HostedFile.FileHost.OUTPUT, + "patrol_callbacks.json" + ); + + public boolean isInitialRun() { + return isInitialRun; + } + @Override protected boolean shouldWaitForActivitiesToComplete() { return false; @@ -40,10 +75,10 @@ public void onCreate(Bundle arguments) { super.onCreate(arguments); // This is only true when the ATO requests a list of tests from the app during the initial run. - boolean isInitialRun = Boolean.parseBoolean(arguments.getString("listTestsForOrchestrator")); + this.isInitialRun = Boolean.parseBoolean(arguments.getString("listTestsForOrchestrator")); Logger.INSTANCE.i("--------------------------------"); - Logger.INSTANCE.i("PatrolJUnitRunner.onCreate() " + (isInitialRun ? "(initial run)" : "")); + Logger.INSTANCE.i("PatrolJUnitRunner.onCreate() " + (this.isInitialRun ? "(initial run)" : "")); } /** @@ -69,6 +104,7 @@ public void setUp(Class activityClass) { // Currently, the only synchronization point we're interested in is when the app under test returns the list of tests. Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); Intent intent = new Intent(Intent.ACTION_MAIN); + intent.putExtra("isInitialRun", isInitialRun); intent.setClassName(instrumentation.getTargetContext(), activityClass.getCanonicalName()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); instrumentation.getTargetContext().startActivity(intent); @@ -94,7 +130,7 @@ public PatrolAppServiceClient createAppServiceClient() { *

*/ public void waitForPatrolAppService() { - final String TAG = "PatrolJUnitRunner.setUp(): "; + final String TAG = "PatrolJUnitRunner.waitForPatrolAppService(): "; Logger.INSTANCE.i(TAG + "Waiting for PatrolAppService to report its readiness..."); PatrolServer.Companion.getAppReady().block(); @@ -105,6 +141,11 @@ public void waitForPatrolAppService() { public Object[] listDartTests() { final String TAG = "PatrolJUnitRunner.listDartTests(): "; + // This call should be in MainActivityTest.java, but that would require + // users to change that file in their projects, thus breaking backward + // compatibility. + handleLifecycleCallbacks(); + try { final DartGroupEntry dartTestGroup = patrolAppServiceClient.listDartTests(); List dartTestCases = ContractsExtensionsKt.listTestsFlat(dartTestGroup, ""); @@ -121,12 +162,99 @@ public Object[] listDartTests() { } } + private void handleLifecycleCallbacks() { + if (isInitialRun()) { + Object[] lifecycleCallbacks = listLifecycleCallbacks(); + saveLifecycleCallbacks(lifecycleCallbacks); + } else { + setLifecycleCallbacksState(); + } + } + + public Object[] listLifecycleCallbacks() { + final String TAG = "PatrolJUnitRunner.listLifecycleCallbacks(): "; + + try { + final ListDartLifecycleCallbacksResponse response = patrolAppServiceClient.listDartLifecycleCallbacks(); + final List setUpAlls = response.getSetUpAlls(); + Logger.INSTANCE.i(TAG + "Got Dart lifecycle callbacks: " + setUpAlls); + + return setUpAlls.toArray(); + } catch (PatrolAppServiceClientException e) { + Logger.INSTANCE.e(TAG + "Failed to list Dart lifecycle callbacks: ", e); + throw new RuntimeException(e); + } + } + + public void saveLifecycleCallbacks(Object[] callbacks) { + Map callbackMap = new HashMap<>(); + for (Object callback : callbacks) { + callbackMap.put((String) callback, false); + } + + writeStateFile(callbackMap); + } + + public void markLifecycleCallbackExecuted(String name) { + Logger.INSTANCE.i("PatrolJUnitRunnerMarking.markLifecycleCallbackExecuted(" + name + ")"); + Map state = readStateFile(); + state.put(name, true); + writeStateFile(state); + } + + private Map readStateFile() { + try { + InputStream inputStream = TestStorageUtil.getInputStream(stateFileUri, getContentResolver()); + String content = convertStreamToString(inputStream); + Gson gson = new Gson(); + Type typeOfHashMap = new TypeToken>() {}.getType(); + Map data = gson.fromJson(content, typeOfHashMap); + return data; + } catch (FileNotFoundException e) { + throw new RuntimeException("Failed to read state file", e); + } + } + + private void writeStateFile(Map data) { + try { + OutputStream outputStream = TestStorageUtil.getOutputStream(stateFileUri, getContentResolver()); + Gson gson = new Gson(); + Type typeOfHashMap = new TypeToken>() {}.getType(); + String json = gson.toJson(data, typeOfHashMap); + outputStream.write(json.getBytes()); + outputStream.write("\n".getBytes()); + } catch (IOException e) { + throw new RuntimeException("Failed to write state file", e); + } + } + + static String convertStreamToString(InputStream inputStream) { + Scanner s = new Scanner(inputStream).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + + /** + * Sets the state of lifecycle callbacks in the app. + *

+ * This is required because the app is launched in a new process for each test. + */ + public void setLifecycleCallbacksState() { + final String TAG = "PatrolJUnitRunner.setLifecycleCallbacksStateInApp(): "; + + try { + patrolAppServiceClient.setLifecycleCallbacksState(readStateFile()); + } catch (PatrolAppServiceClientException e) { + Logger.INSTANCE.e(TAG + "Failed to set lifecycle callbacks state in app: ", e); + throw new RuntimeException(e); + } + } + /** * Requests execution of a Dart test and waits for it to finish. * Throws AssertionError if the test fails. */ public RunDartTestResponse runDartTest(String name) { - final String TAG = "PatrolJUnitRunner.runDartTest(" + name + "): "; + final String TAG = "PatrolJUnitRunner.runDartTest(\"" + name + "\"): "; try { Logger.INSTANCE.i(TAG + "Requested execution"); diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolPlugin.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolPlugin.kt index 998420419..c930fd0ec 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolPlugin.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolPlugin.kt @@ -1,24 +1,52 @@ package pl.leancode.patrol import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -class PatrolPlugin : FlutterPlugin, MethodCallHandler { +class PatrolPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel: MethodChannel + private var isInitialRun: Boolean? = null + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "pl.leancode.patrol/main") channel.setMethodCallHandler(this) } override fun onMethodCall(call: MethodCall, result: Result) { - result.notImplemented() + when (call.method) { + "isInitialRun" -> result.success(isInitialRun) + else -> result.notImplemented() + } } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + val intent = binding.activity.intent + if (!intent.hasExtra("isInitialRun")) { + throw IllegalStateException("PatrolPlugin must be initialized with intent having isInitialRun boolean") + } + + isInitialRun = intent.getBooleanExtra("isInitialRun", false) + } + + override fun onDetachedFromActivityForConfigChanges() { + // Do nothing + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + // Do nothing + } + + override fun onDetachedFromActivity() { + // Do nothing + } } diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt index 83b6daa3b..10b1cbe3b 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt @@ -33,6 +33,10 @@ class Contracts { fine, } + class Empty ( + + ) + data class DartGroupEntry ( val name: String, val type: GroupEntryType, @@ -43,6 +47,11 @@ class Contracts { val group: DartGroupEntry ) + data class ListDartLifecycleCallbacksResponse ( + val setUpAlls: List, + val tearDownAlls: List + ) + data class RunDartTestRequest ( val name: String ) @@ -56,6 +65,10 @@ class Contracts { } } + data class SetLifecycleCallbacksStateRequest ( + val state: Map + ) + data class ConfigureRequest ( val findTimeoutMillis: Long ) @@ -260,4 +273,8 @@ class Contracts { val locationAccuracy: SetLocationAccuracyRequestLocationAccuracy ) + data class MarkLifecycleCallbackExecutedRequest ( + val name: String + ) + } diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt index c553b00be..9e76ffe0c 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt @@ -48,6 +48,7 @@ abstract class NativeAutomatorServer { abstract fun setLocationAccuracy(request: Contracts.SetLocationAccuracyRequest) abstract fun debug() abstract fun markPatrolAppServiceReady() + abstract fun markLifecycleCallbackExecuted(request: Contracts.MarkLifecycleCallbackExecutedRequest) val router = routes( "initialize" bind POST to { @@ -206,6 +207,11 @@ abstract class NativeAutomatorServer { "markPatrolAppServiceReady" bind POST to { markPatrolAppServiceReady() Response(OK) + }, + "markLifecycleCallbackExecuted" bind POST to { + val body = json.fromJson(it.bodyString(), Contracts.MarkLifecycleCallbackExecutedRequest::class.java) + markLifecycleCallbackExecuted(body) + Response(OK) } ) diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt index 3896e9f77..12bdc9bf8 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt @@ -19,6 +19,16 @@ class PatrolAppServiceClient(address: String, port: Int, private val timeout: Lo return json.fromJson(response, Contracts.ListDartTestsResponse::class.java) } + fun listDartLifecycleCallbacks(): Contracts.ListDartLifecycleCallbacksResponse { + val response = performRequest("listDartLifecycleCallbacks") + return json.fromJson(response, Contracts.ListDartLifecycleCallbacksResponse::class.java) + } + + fun setLifecycleCallbacksState(request: Contracts.SetLifecycleCallbacksStateRequest): Contracts.Empty { + val response = performRequest("setLifecycleCallbacksState", json.toJson(request)) + return json.fromJson(response, Contracts.Empty::class.java) + } + fun runDartTest(request: Contracts.RunDartTestRequest): Contracts.RunDartTestResponse { val response = performRequest("runDartTest", json.toJson(request)) return json.fromJson(response, Contracts.RunDartTestResponse::class.java) diff --git a/packages/patrol/example/integration_test/internal/callbacks_all_test.dart b/packages/patrol/example/integration_test/internal/callbacks_all_test.dart new file mode 100644 index 000000000..b927ff425 --- /dev/null +++ b/packages/patrol/example/integration_test/internal/callbacks_all_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:patrol/src/extensions.dart'; +// ignore: depend_on_referenced_packages +import 'package:test_api/src/backend/invoker.dart'; + +import '../common.dart'; + +String get currentTest => Invoker.current!.fullCurrentTestName(); + +void _print(String text) => print('TEST_DEBUG: $text'); + +void main() { + group('parent', () { + patrolSetUpAll(() async { + await Future.delayed(Duration(seconds: 1)); + _print('ran patrolSetUpAll (1) before "$currentTest"'); + }); + + patrolSetUpAll(() async { + await Future.delayed(Duration(seconds: 1)); + _print('ran patrolSetUpAll (2) before "$currentTest"'); + }); + + patrolTest('testA', _body); + patrolTest('testB', _body); + patrolTest('testC', _body); + }); +} + +Future _body(PatrolIntegrationTester $) async { + final testName = Invoker.current!.fullCurrentTestName(); + _print('ran body of test "$testName"'); + + await createApp($); + + await $(FloatingActionButton).tap(); + expect($(#counterText).text, '1'); + + await $(#textField).enterText(testName); +} diff --git a/packages/patrol/example/integration_test/internal/callbacks_test.dart b/packages/patrol/example/integration_test/internal/callbacks_test.dart new file mode 100644 index 000000000..a1cd8183d --- /dev/null +++ b/packages/patrol/example/integration_test/internal/callbacks_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:patrol/src/extensions.dart'; +// ignore: depend_on_referenced_packages +import 'package:test_api/src/backend/invoker.dart'; + +import '../common.dart'; + +String get currentTest => Invoker.current!.fullCurrentTestName(); + +void _print(String text) => print('TEST_DEBUG: $text'); + +void main() { + patrolSetUp(() async { + await Future.delayed(Duration(seconds: 1)); + _print('ran patrolSetUp (1) up before "$currentTest"'); + }); + + patrolTearDown(() async { + await Future.delayed(Duration(seconds: 1)); + _print('ran patrolTearDown (1) after "$currentTest"'); + }); + + patrolTest('testFirst', _body); + + group('groupA', () { + patrolSetUp(() async { + if (currentTest == 'internal.callbacks_test groupA testB') { + throw Exception('TEST_DEBUG: "$currentTest" crashed on purpose'); + } + + _print('ran patrolSetUp (2) before "$currentTest"'); + }); + + patrolTearDown(() async { + _print('ran patrolTearDown (2) after "$currentTest"'); + }); + + patrolTest('testA', _body); + patrolTest('testB', _body); + patrolTest('testC', _body); + }); + + patrolTest('testLast', _body); +} + +Future _body(PatrolIntegrationTester $) async { + final testName = Invoker.current!.fullCurrentTestName(); + _print('ran body of test "$testName"'); + + await createApp($); + + await $(FloatingActionButton).tap(); + expect($(#counterText).text, '1'); + + await $(#textField).enterText(testName); + + await $.pumpAndSettle(duration: Duration(seconds: 2)); +} diff --git a/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift b/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift index ea8f5df94..d90b65ef1 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift @@ -5,11 +5,18 @@ final class AutomatorServer: NativeAutomatorServer { private let automator: Automator - private let onAppReady: (Bool) -> Void + private let onAppReady: () -> Void - init(automator: Automator, onAppReady: @escaping (Bool) -> Void) { + private let onDartLifecycleCallbackExecuted: (String) -> Void + + init( + automator: Automator, + onAppReady: @escaping () -> Void, + onDartLifecycleCallbackExecuted: @escaping (String) -> Void + ) { self.automator = automator self.onAppReady = onAppReady + self.onDartLifecycleCallbackExecuted = onDartLifecycleCallbackExecuted } func initialize() throws {} @@ -304,7 +311,11 @@ } func markPatrolAppServiceReady() throws { - onAppReady(true) + onAppReady() + } + + func markLifecycleCallbackExecuted(request: MarkLifecycleCallbackExecutedRequest) throws { + onDartLifecycleCallbackExecuted(request.name) } } diff --git a/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift b/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift index 3b575e968..cd4857e14 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift @@ -32,6 +32,10 @@ enum SetLocationAccuracyRequestLocationAccuracy: String, Codable { case fine } +struct Empty: Codable { + +} + struct DartGroupEntry: Codable { var name: String var type: GroupEntryType @@ -42,6 +46,11 @@ struct ListDartTestsResponse: Codable { var group: DartGroupEntry } +struct ListDartLifecycleCallbacksResponse: Codable { + var setUpAlls: [String] + var tearDownAlls: [String] +} + struct RunDartTestRequest: Codable { var name: String } @@ -51,6 +60,10 @@ struct RunDartTestResponse: Codable { var details: String? } +struct SetLifecycleCallbacksStateRequest: Codable { + var state: [String: Bool] +} + struct ConfigureRequest: Codable { var findTimeoutMillis: Int } @@ -174,3 +187,7 @@ struct SetLocationAccuracyRequest: Codable { var locationAccuracy: SetLocationAccuracyRequestLocationAccuracy } +struct MarkLifecycleCallbackExecutedRequest: Codable { + var name: String +} + diff --git a/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift b/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift index 9e40b9d9d..35afaa05a 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift @@ -43,6 +43,7 @@ protocol NativeAutomatorServer { func setLocationAccuracy(request: SetLocationAccuracyRequest) throws func debug() throws func markPatrolAppServiceReady() throws + func markLifecycleCallbackExecuted(request: MarkLifecycleCallbackExecutedRequest) throws } extension NativeAutomatorServer { @@ -241,6 +242,12 @@ extension NativeAutomatorServer { try markPatrolAppServiceReady() return HTTPResponse(.ok) } + + private func markLifecycleCallbackExecutedHandler(request: HTTPRequest) throws -> HTTPResponse { + let requestArg = try JSONDecoder().decode(MarkLifecycleCallbackExecutedRequest.self, from: request.body) + try markLifecycleCallbackExecuted(request: requestArg) + return HTTPResponse(.ok) + } } extension NativeAutomatorServer { @@ -420,6 +427,11 @@ extension NativeAutomatorServer { request: request, handler: markPatrolAppServiceReadyHandler) } + server.route(.POST, "markLifecycleCallbackExecuted") { + request in handleRequest( + request: request, + handler: markLifecycleCallbackExecutedHandler) + } } } diff --git a/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift b/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift index f48791431..66ae8b542 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift @@ -20,6 +20,19 @@ class PatrolAppServiceClient { performRequestWithResult(requestName: "listDartTests", completion: completion) } + func listDartLifecycleCallbacks(completion: @escaping (Result) -> Void) { + performRequestWithResult(requestName: "listDartLifecycleCallbacks", completion: completion) + } + + func setLifecycleCallbacksState(request: SetLifecycleCallbacksStateRequest, completion: @escaping (Result) -> Void) { + do { + let body = try JSONEncoder().encode(request) + performRequestWithResult(requestName: "setLifecycleCallbacksState", body: body, completion: completion) + } catch let err { + completion(.failure(err)) + } + } + func runDartTest(request: RunDartTestRequest, completion: @escaping (Result) -> Void) { do { let body = try JSONEncoder().encode(request) diff --git a/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift b/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift index 9c9b28d61..8841ae504 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift @@ -12,8 +12,9 @@ import Telegraph private let server: Server #endif - @objc - public private(set) var appReady = false + private let onAppReady: () -> Void + + private let onDartLifecycleCallbackExecuted: (String) -> Void private var passedPort: Int = { guard let portStr = ProcessInfo.processInfo.environment[envPortKey] else { @@ -31,7 +32,12 @@ import Telegraph return portInt }() - @objc public override init() { + @objc public init( + withOnAppReadyCallback onAppReady: @escaping () -> Void, + onDartLifecycleCallbackExecuted: @escaping (String) -> Void + ) { + self.onDartLifecycleCallbackExecuted = onDartLifecycleCallbackExecuted + self.onAppReady = onAppReady Logger.shared.i("PatrolServer constructor called") #if PATROL_ENABLED @@ -48,10 +54,18 @@ import Telegraph #if PATROL_ENABLED Logger.shared.i("Starting server...") - let provider = AutomatorServer(automator: automator) { appReady in - Logger.shared.i("App reported that it is ready") - self.appReady = appReady - } + let provider = AutomatorServer( + automator: automator, + onAppReady: { + Logger.shared.i("App reported that it is ready") + self.onAppReady() + }, + onDartLifecycleCallbackExecuted: { callback in + Logger.shared.i( + "App reported that Dart lifecycle callback \(format: callback) was executed") + self.onDartLifecycleCallbackExecuted(callback) + } + ) provider.setupRoutes(server: server) diff --git a/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift b/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift index 9b2b3290a..1c5c20c02 100644 --- a/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift +++ b/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift @@ -45,28 +45,53 @@ } } + @objc public func listDartLifecycleCallbacks( + completion: @escaping ([String]?, [String]?, Error?) -> Void + ) { + NSLog("PatrolAppService.listDartLifecycleCallbacks()") + + client.listDartLifecycleCallbacks { result in + switch result { + case .success(let result): + completion(result.setUpAlls, result.tearDownAlls, nil) + case .failure(let error): + completion(nil, nil, error) + } + } + } + + @objc public func setDartLifecycleCallbacksState( + _ state: [String: Bool], completion: @escaping (Error?) -> Void + ) { + NSLog("PatrolAppService.setDartLifecycleCallbacksState(\(state)") + + let request = SetLifecycleCallbacksStateRequest(state: state) + self.client.setLifecycleCallbacksState(request: request) { result in + switch result { + case .success(_): + completion(nil) + case .failure(let error): + completion(error) + } + } + } + @objc public func runDartTest( - name: String, completion: @escaping (ObjCRunDartTestResponse?, Error?) -> Void + _ name: String, completion: @escaping (ObjCRunDartTestResponse?, Error?) -> Void ) { - // TODO: simple workaround - patrolAppService starts running too slowly. - // We should wait for appReady in the dynamically created test case method, - // before calling runDartTest() (in PATROL_INTEGRATION_TEST_IOS_MACRO) - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - NSLog("PatrolAppServiceClient.runDartTest(\(name))") - - let request = RunDartTestRequest(name: name) - self.client.runDartTest(request: request) { result in - switch result { - case .success(let result): - let testRespone = ObjCRunDartTestResponse( - passed: result.result == .success, - details: result.details - ) - completion(testRespone, nil) - case .failure(let error): - completion(nil, error) - } + NSLog("PatrolAppServiceClient.runDartTest(\(format: name))") + let request = RunDartTestRequest(name: name) + self.client.runDartTest(request: request) { result in + switch result { + case .success(let result): + let testRespone = ObjCRunDartTestResponse( + passed: result.result == .success, + details: result.details + ) + completion(testRespone, nil) + case .failure(let error): + completion(nil, error) } } } @@ -85,7 +110,7 @@ extension DartGroupEntry { // This case is invalid, because every test will have at least // 1 named group - its filename. - fatalError("Invariant violated: test \(test.name) has no named parent group") + fatalError("Invariant violated: test \(format: test.name) has no named parent group") } test.name = "\(parentGroupName) \(test.name)" diff --git a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h index d7f6c0b05..14e7a6fc5 100644 --- a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h +++ b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h @@ -1,9 +1,9 @@ // This file is a one giant macro to make the setup as easy as possible for the developer. // To edit it: -// 1. Remove the trailing backslashes: $ sed 's/\\$//' ios/Classes/PatrolIntegrationTestRunner.h +// 1. Remove the trailing backslashes: $ sed 's/\\$//' PatrolIntegrationTestRunner.h // 2. Paste its contents into the RunnerUITests.m in the RunnerUITests target // 3. Make the changes, make sure it works -// 4. Re-add trailing backslashes: $ sed 's/$/\\/' ios/Classes/PatrolIntegrationTestRunner.h +// 4. Re-add trailing backslashes: $ sed 's/$/\\/' RunnerUITests.m // 5. Copy the contents from RunnerUITests.m back here // 6. Go back to using a macro in RunnerTests.m @@ -16,8 +16,20 @@ @implementation __test_class \ \ +(NSArray *)testInvocations { \ + __block NSMutableDictionary *callbacksState = NULL; \ + __block NSArray *dartTests = NULL; \ + __block BOOL appReady = NO; \ + \ /* Start native automation server */ \ - PatrolServer *server = [[PatrolServer alloc] init]; \ + PatrolServer *server = [[PatrolServer alloc] \ + initWithOnAppReadyCallback:^{ \ + appReady = YES; \ + } \ + onDartLifecycleCallbackExecuted:^(NSString *_Nonnull callbackName) { \ + /* callbacksState dictionary will have been already initialized when this callback is executed */ \ + NSLog(@"onLifecycleCallbackExecuted for %@", callbackName); \ + [callbacksState setObject:@YES forKey:callbackName]; \ + }]; \ \ NSError *_Nullable __autoreleasing *_Nullable err = NULL; \ [server startAndReturnError:err]; \ @@ -35,15 +47,45 @@ [systemAlerts.buttons[@"Allow"] tap]; \ } \ \ + NSLog(@"Performing initial run of the app..."); \ + \ + /* MARK: Start initial run */ \ /* Run the app for the first time to gather Dart tests */ \ - [[[XCUIApplication alloc] init] launch]; \ + XCUIApplication *app = [[XCUIApplication alloc] init]; \ + NSDictionary *args = @{@"PATROL_INITIAL_RUN" : @"true"}; \ + app.launchEnvironment = args; \ + [app launch]; \ \ - /* Spin the runloop waiting until the app reports that it is ready to report Dart tests */ \ - while (!server.appReady) { \ + NSLog(@"Waiting for PatrolAppService (running in the app under test) to report its readiness..."); \ + \ + /* Spin the runloop waiting until the app reports that PatrolAppService is up */ \ + while (!appReady) { \ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ } \ \ - __block NSArray *dartTests = NULL; \ + /* MARK: List Dart lifecycle callbacks */ \ + \ + [appServiceClient listDartLifecycleCallbacksWithCompletion:^(NSArray *_Nullable setUpAlls, \ + NSArray *_Nullable tearDownAlls, \ + NSError *_Nullable err) { \ + if (err != NULL) { \ + NSLog(@"listDartLifecycleCallbacks(): failed, err: %@", err); \ + } \ + \ + callbacksState = [[NSMutableDictionary alloc] init]; \ + for (NSString * setUpAll in setUpAlls) { \ + [callbacksState setObject:@NO forKey:setUpAll]; \ + } \ + }]; \ + \ + /* Spin the runloop waiting until the app reports the Dart lifecycle callbacks it contains */ \ + while (!callbacksState) { \ + [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ + } \ + NSLog(@"Got %lu Dart lifecycle callbacks: %@", callbacksState.count, callbacksState); \ + \ + /* MARK: List Dart tests */ \ + \ [appServiceClient listDartTestsWithCompletion:^(NSArray *_Nullable tests, NSError *_Nullable err) { \ if (err != NULL) { \ NSLog(@"listDartTests(): failed, err: %@", err); \ @@ -56,12 +98,14 @@ while (!dartTests) { \ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ } \ - \ NSLog(@"Got %lu Dart tests: %@", dartTests.count, dartTests); \ \ + /* MARK: Create tests at runtime */ \ + \ NSMutableArray *invocations = [[NSMutableArray alloc] init]; \ \ /** \ + * \ * Once Dart tests are available, we: \ * \ * Step 1. Dynamically add test case methods that request execution of an individual Dart test file. \ @@ -73,25 +117,63 @@ /* Step 1 - dynamically create test cases */ \ \ IMP implementation = imp_implementationWithBlock(^(id _self) { \ - [[[XCUIApplication alloc] init] launch]; \ + /* Reset server's appReady state, because new app process will be started */ \ + appReady = NO; \ \ - __block ObjCRunDartTestResponse *response = NULL; \ - [appServiceClient runDartTestWithName:dartTest \ - completion:^(ObjCRunDartTestResponse *_Nullable r, NSError *_Nullable err) { \ - if (err != NULL) { \ - NSLog(@"runDartTestWithName(%@): failed, err: %@", dartTest, err); \ - } \ + XCUIApplication *app = [[XCUIApplication alloc] init]; \ + NSDictionary *args = @{@"PATROL_INITIAL_RUN" : @"false"}; \ + [app setLaunchEnvironment:args]; \ + [app launch]; \ + \ + /* Spin the runloop waiting until the app reports that PatrolAppService is up */ \ + while (!appReady) { \ + [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ + } \ \ - response = r; \ - }]; \ + __block BOOL callbacksSet = NO; \ + [appServiceClient \ + setDartLifecycleCallbacksState:callbacksState \ + completion:^(NSError *_Nullable err) { \ + if (err != NULL) { \ + NSLog(@"setDartLifecycleCallbacksState(): call failed, err: %@", err); \ + } \ \ - /* Wait until Dart test finishes */ \ - while (!response) { \ + NSLog(@"setDartLifecycleCallbacksState(): call succeeded"); \ + callbacksSet = YES; \ + }]; \ + \ + /*Wait until lifecycle callbacks are set*/ \ + while (!callbacksSet) { \ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ } \ \ - XCTAssertTrue(response.passed, @"%@", response.details); \ + __block ObjCRunDartTestResponse *response = NULL; \ + __block NSError *error; \ + [appServiceClient runDartTest:dartTest \ + completion:^(ObjCRunDartTestResponse *_Nullable r, NSError *_Nullable err) { \ + NSString *status; \ + if (err != NULL) { \ + error = err; \ + status = @"CRASHED"; \ + } else { \ + response = r; \ + status = response.passed ? @"PASSED" : @"FAILED"; \ + } \ + \ + NSLog(@"runDartTest(\"%@\"): call finished, test result: %@", dartTest, status); \ + }]; \ + \ + /* Wait until Dart test finishes (either fails or passes) or crashes */ \ + while (!response && !error) { \ + [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ + } \ + \ + BOOL passed = response ? response.passed : NO; \ + NSString *details = response ? response.details : @"(no details - app likely crashed)"; \ + \ + XCTAssertTrue(passed, @"%@", details); \ }); \ + \ NSString *selectorName = [PatrolUtils createMethodNameFromPatrolGeneratedGroup:dartTest]; \ SEL selector = NSSelectorFromString(selectorName); \ class_addMethod(self, selector, implementation, "v@:"); \ diff --git a/packages/patrol/ios/Classes/SwiftPatrolPlugin.swift b/packages/patrol/ios/Classes/SwiftPatrolPlugin.swift index c07ab9da1..0e99b7fff 100644 --- a/packages/patrol/ios/Classes/SwiftPatrolPlugin.swift +++ b/packages/patrol/ios/Classes/SwiftPatrolPlugin.swift @@ -2,10 +2,9 @@ import Flutter import UIKit let kChannelName = "pl.leancode.patrol/main" -let kMethodAllTestsFinished = "allTestsFinished" -let kErrorCreateChannelFailed = "create_channel_failed" -let kErrorCreateChannelFailedMsg = "Failed to create GRPC channel" +let kErrorInvalidValue = "invalid value" +let kErrorInvalidValueMsg = "isInitialRun env var is not a bool" /// A Flutter plugin that was responsible for communicating the test results back /// to iOS XCUITest. @@ -23,6 +22,22 @@ public class SwiftPatrolPlugin: NSObject, FlutterPlugin { } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - result(FlutterMethodNotImplemented) + switch call.method { + case "isInitialRun": + let rawInitialRun = ProcessInfo.processInfo.environment["PATROL_INITIAL_RUN"] + let initialRun = Bool(rawInitialRun ?? "invalid") + if initialRun == nil { + result( + FlutterError( + code: kErrorInvalidValue, + message: "PATROL_INITIAL_RUN value is invalid: \(String(describing: rawInitialRun))", + details: nil) + ) + } else { + result(initialRun) + } + default: + result(FlutterMethodNotImplemented) + } } } diff --git a/packages/patrol/lib/src/binding.dart b/packages/patrol/lib/src/binding.dart index 0511a6ad0..df4dd2ef7 100644 --- a/packages/patrol/lib/src/binding.dart +++ b/packages/patrol/lib/src/binding.dart @@ -9,8 +9,6 @@ import 'package:patrol/src/devtools_service_extensions/devtools_service_extensio // ignore: implementation_imports, depend_on_referenced_packages import 'package:patrol/src/global_state.dart' as global_state; -import 'constants.dart' as constants; - const _success = 'success'; void _defaultPrintLogger(String message) { @@ -38,56 +36,74 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding { /// Creates a new [PatrolBinding]. /// /// You most likely don't want to call it yourself. - PatrolBinding(NativeAutomatorConfig config) - : _serviceExtensions = DevtoolsServiceExtensions(config) { + + PatrolBinding( + this.patrolAppService, + this.nativeAutomator, + NativeAutomatorConfig config, + ) : _serviceExtensions = DevtoolsServiceExtensions(config) { + logger('created'); shouldPropagateDevicePointerEvents = true; final oldTestExceptionReporter = reportTestException; reportTestException = (details, testDescription) { final currentDartTest = _currentDartTest; if (currentDartTest != null) { - assert(!constants.hotRestartEnabled); + assert(!global_state.hotRestartEnabled); _testResults[currentDartTest] = Failure(testDescription, '$details'); } oldTestExceptionReporter(details, testDescription); }; setUp(() { - if (constants.hotRestartEnabled) { + if (global_state.hotRestartEnabled) { // Sending results ends the test, which we don't want for Hot Restart return; } + if (global_state.currentTestIndividualName == 'patrol_test_explorer') { + // Ignore the fake test. + return; + } + _currentDartTest = global_state.currentTestFullName; + logger('setUp(): called with current Dart test = "$_currentDartTest"'); }); tearDown(() async { - if (constants.hotRestartEnabled) { + if (global_state.hotRestartEnabled) { // Sending results ends the test, which we don't want for Hot Restart return; } + if (await global_state.isInitialRun) { + // If this is the initial run, then no test has been requested to + // execute. Return to avoid blocking on didRequestTestExecution below. + return; + } + final testName = global_state.currentTestIndividualName; final isTestExplorer = testName == 'patrol_test_explorer'; if (isTestExplorer) { + // Ignore the fake test. return; - } else { - logger( - 'tearDown(): count: ${_testResults.length}, results: $_testResults', - ); } - final nameOfRequestedTest = await patrolAppService.testExecutionRequested; + logger('tearDown(): called with current Dart test = "$_currentDartTest"'); + logger('tearDown(): there are ${_testResults.length} test results:'); + _testResults.forEach((dartTestName, result) { + logger('tearDown(): test "$dartTestName": "$result"'); + }); + + final requestedDartTest = await patrolAppService.didRequestTestExecution; - if (nameOfRequestedTest == _currentDartTest) { + if (requestedDartTest == _currentDartTest) { logger( - 'finished test $_currentDartTest. Will report its status back to the native side', + 'tearDown(): finished test "$_currentDartTest". Will report its status back to the native side', ); final passed = global_state.isCurrentTestPassing; - logger( - 'tearDown(): test "$testName" in group "$_currentDartTest", passed: $passed', - ); + logger('tearDown(): test "$_currentDartTest", passed: $passed'); await patrolAppService.markDartTestAsCompleted( dartFileName: _currentDartTest!, passed: passed, @@ -97,7 +113,7 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding { ); } else { logger( - 'finished test $_currentDartTest, but it was not requested, so its status will not be reported back to the native side', + 'tearDown(): finished test "$_currentDartTest", but it was not requested, so its status will not be reported back to the native side', ); } }); @@ -107,9 +123,13 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding { /// if necessary. /// /// This method is idempotent. - factory PatrolBinding.ensureInitialized(NativeAutomatorConfig config) { + factory PatrolBinding.ensureInitialized( + PatrolAppService patrolAppService, + NativeAutomator nativeAutomator, + NativeAutomatorConfig config, + ) { if (_instance == null) { - PatrolBinding(config); + PatrolBinding(patrolAppService, nativeAutomator, config); } return _instance!; } @@ -128,7 +148,11 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding { /// /// It's only for test reporting purposes and should not be used for anything /// else. - late PatrolAppService patrolAppService; + final PatrolAppService patrolAppService; + + /// The [NativeAutomator] used by this binding to interact with the native + /// side. + final NativeAutomator nativeAutomator; /// The singleton instance of this object. /// @@ -200,12 +224,12 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding { @override Widget wrapWithDefaultView(Widget rootWidget) { assert( - (_currentDartTest != null) != (constants.hotRestartEnabled), + (_currentDartTest != null) != (global_state.hotRestartEnabled), '_currentDartTest can be null if and only if Hot Restart is enabled', ); const testLabelEnabled = bool.fromEnvironment('PATROL_TEST_LABEL_ENABLED'); - if (!testLabelEnabled || constants.hotRestartEnabled) { + if (!testLabelEnabled || global_state.hotRestartEnabled) { return super.wrapWithDefaultView(RepaintBoundary(child: rootWidget)); } else { return super.wrapWithDefaultView( diff --git a/packages/patrol/lib/src/common.dart b/packages/patrol/lib/src/common.dart index 16a8c9a97..c9beb87dd 100644 --- a/packages/patrol/lib/src/common.dart +++ b/packages/patrol/lib/src/common.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:meta/meta.dart'; import 'package:patrol/src/binding.dart'; import 'package:patrol/src/global_state.dart' as global_state; +import 'package:patrol/src/logs.dart'; import 'package:patrol/src/native/contracts/contracts.dart'; import 'package:patrol/src/native/native.dart'; import 'package:patrol_finders/patrol_finders.dart' as finders; @@ -13,13 +14,108 @@ import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports import 'package:test_api/src/backend/test.dart'; -import 'constants.dart' as constants; import 'custom_finders/patrol_integration_tester.dart'; /// Signature for callback to [patrolTest]. typedef PatrolTesterCallback = Future Function(PatrolIntegrationTester $); -/// Like [testWidgets], but with support for Patrol custom finders. +/// A modification of [setUp] that works with Patrol's native automation. +void patrolSetUp(dynamic Function() body) { + setUp(() async { + if (global_state.hotRestartEnabled) { + await body(); + return; + } + + if (await global_state.isInitialRun) { + // Skip calling body if we're in test discovery phase + return; + } + + final currentTest = global_state.currentTestFullName; + + final requestedToExecute = await PatrolBinding.instance.patrolAppService + .waitForExecutionRequest(currentTest); + + if (requestedToExecute) { + await body(); + } + }); +} + +/// A modification of [tearDown] that works with Patrol's native automation. +void patrolTearDown(dynamic Function() body) { + tearDown(() async { + if (global_state.hotRestartEnabled) { + await body(); + return; + } + + if (await global_state.isInitialRun) { + // Skip calling body if we're in test discovery phase + return; + } + + final currentTest = global_state.currentTestFullName; + + final requestedToExecute = await PatrolBinding.instance.patrolAppService + .waitForExecutionRequest(currentTest); + + if (requestedToExecute) { + await body(); + } + }); +} + +/// A modification of [setUpAll] that works with Patrol's native automation. +/// +/// It keeps track of calls made to setUpAll. +void patrolSetUpAll(dynamic Function() body) { + setUpAll(() async { + final patrolAppService = PatrolBinding.instance.patrolAppService; + final parentGroupsName = global_state.currentGroupFullName; + final setUpAllName = patrolAppService.addSetUpAll(parentGroupsName); + + if (await global_state.isInitialRun) { + // Skip calling body if we're in test discovery phase + patrolDebug( + 'skipping setUpAll "$setUpAllName" because we are in the initial run', + ); + return; + } + + patrolDebug('Waiting for lifecycle callbacks state...'); + final callbacksState = + await patrolAppService.didReceiveLifecycleCallbacksState; + + assert( + callbacksState[setUpAllName] != null, + 'setUpAll "$setUpAllName" was not registered in PatrolAppService. This looks very nasty.', + ); + + if (callbacksState[setUpAllName] ?? false) { + // Skip calling body if this setUpAll was already executed + patrolDebug('skipping setUpAll "$setUpAllName" because it already ran'); + return; + } + + final requestedTest = await patrolAppService.didRequestTestExecution; + + // Skip calling if parentGroupName is not a substring of requestedTestName + if (!requestedTest.startsWith(parentGroupsName)) { + // This is not exhaustive. + return; + } + + // Mark this setUpAll as executed + final nativeAutomator = PatrolBinding.instance.nativeAutomator; + await nativeAutomator.markLifecycleCallbackExecuted(setUpAllName); + + await body(); + }); +} + +/// Like [testWidgets], but with support for Patrol custom fiders. /// /// To customize the Patrol-specific configuration, set [config]. /// @@ -50,14 +146,8 @@ void patrolTest( LiveTestWidgetsFlutterBindingFramePolicy framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fadePointers, }) { - NativeAutomator automator; - - PatrolBinding? patrolBinding; - - automator = NativeAutomator(config: nativeAutomatorConfig); - final binding = - patrolBinding = PatrolBinding.ensureInitialized(nativeAutomatorConfig) - ..framePolicy = framePolicy; + final automator = NativeAutomator(config: nativeAutomatorConfig); + final binding = PatrolBinding.instance..framePolicy = framePolicy; testWidgets( description, @@ -67,31 +157,30 @@ void patrolTest( variant: variant, tags: tags, (widgetTester) async { - if (patrolBinding != null && !constants.hotRestartEnabled) { + if (!global_state.hotRestartEnabled) { + if (await global_state.isInitialRun) { + patrolDebug( + 'skippng test "${global_state.currentTestFullName}" because this is the initial run', + ); + // Fall through tests during the initial run that discovers tests. + // + // This is required to be able to find all setUpAll callbacks. + return; + } + // If Patrol's native automation feature is enabled, then this test will // be executed only if the native side requested it to be executed. // Otherwise, it returns early. - // - // The assumption here is that this test doesn't have any extra parent - // groups. Every Dart test suite has an implict, unnamed, top-level - // group. An additional group is present in the bundled_test.dart, and - // its name is equal to the path to the Dart test file in the - // integration_test directory. - // - // In other words, the developer cannot use `group()` in the tests. - // - // Example: if this function is called from the Dart test file named - // "example_test.dart", and that file is located in the - // "integration_test/examples" directory, we assume that the name of the - // immediate parent group is "examples.example_test". - - final requestedToExecute = await patrolBinding.patrolAppService + + final isRequestedToExecute = await PatrolBinding + .instance.patrolAppService .waitForExecutionRequest(global_state.currentTestFullName); - if (!requestedToExecute) { + if (!isRequestedToExecute) { return; } } + if (!kIsWeb && io.Platform.isIOS) { widgetTester.binding.platformDispatcher.onSemanticsEnabledChanged = () { // This callback is empty on purpose. It's a workaround for tests @@ -104,7 +193,7 @@ void patrolTest( final patrolTester = PatrolIntegrationTester( tester: widgetTester, - nativeAutomator: automator, + nativeAutomator: PatrolBinding.instance.nativeAutomator, config: config, ); await callback(patrolTester); @@ -175,7 +264,6 @@ DartGroupEntry createDartTestGroup( ); } else if (entry is Test) { if (entry.name == 'patrol_test_explorer') { - // throw StateError('Expected group, got test: ${entry.name}'); // Ignore the bogus test that is used to discover the test structure. continue; } diff --git a/packages/patrol/lib/src/constants.dart b/packages/patrol/lib/src/constants.dart deleted file mode 100644 index 01440e6ce..000000000 --- a/packages/patrol/lib/src/constants.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:meta/meta.dart'; - -/// Whether Hot Restart is enabled. -@internal -const bool hotRestartEnabled = bool.fromEnvironment('PATROL_HOT_RESTART'); diff --git a/packages/patrol/lib/src/extensions.dart b/packages/patrol/lib/src/extensions.dart new file mode 100644 index 000000000..701b7b959 --- /dev/null +++ b/packages/patrol/lib/src/extensions.dart @@ -0,0 +1,19 @@ +import 'package:meta/meta.dart'; +// ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart'; + +/// Provides convenience methods for [Invoker]. +@internal +extension InvokerX on Invoker { + /// Returns the full name of the current test (names of all ancestor groups + + /// name of the current test). + String fullCurrentTestName() { + final parentGroupName = liveTest.groups.last.name; + final testName = liveTest.individualName; + + return '$parentGroupName $testName'; + } + + /// Returns the name of the current test only. No group prefixes. + String get currentTestName => liveTest.individualName; +} diff --git a/packages/patrol/lib/src/global_state.dart b/packages/patrol/lib/src/global_state.dart index 318574d3e..a4479ce1a 100644 --- a/packages/patrol/lib/src/global_state.dart +++ b/packages/patrol/lib/src/global_state.dart @@ -1,3 +1,5 @@ +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; // ignore: implementation_imports import 'package:test_api/src/backend/invoker.dart'; @@ -6,6 +8,10 @@ import 'package:test_api/src/backend/invoker.dart'; /// See https://github.com/leancodepl/patrol/issues/1725 const maxTestLength = 190; +/// Whether Hot Restart is enabled. +@internal +const bool hotRestartEnabled = bool.fromEnvironment('PATROL_HOT_RESTART'); + /// This file wraps the [Invoker] API, which is internal to package:test. We /// want to minimize the usage of internal APIs to a minimum. @@ -24,7 +30,16 @@ String get currentTestFullName { return nameCandidate; } -/// Returns the individual name of the current test. Omits all ancestor groups. +/// Returns the name of the current group. +/// +/// Includes all ancestor groups. +String get currentGroupFullName { + return Invoker.current!.liveTest.groups.last.name; +} + +/// Returns the individual name of the current test. +/// +/// Omits all ancestor groups. String get currentTestIndividualName { return Invoker.current!.liveTest.individualName; } @@ -33,3 +48,11 @@ String get currentTestIndividualName { bool get isCurrentTestPassing { return Invoker.current!.liveTest.state.result.isPassing; } + +const _channel = MethodChannel('pl.leancode.patrol/main'); + +/// Returns whether this is the first run of the app under test during which +/// test discovery happens. +Future get isInitialRun async { + return await _channel.invokeMethod('isInitialRun') as bool; +} diff --git a/packages/patrol/lib/src/logs.dart b/packages/patrol/lib/src/logs.dart new file mode 100644 index 000000000..474b7d31f --- /dev/null +++ b/packages/patrol/lib/src/logs.dart @@ -0,0 +1,14 @@ +import 'package:meta/meta.dart'; + +final _runKey = Object().hashCode.toRadixString(16).padLeft(4, '0'); + +/// Logs a message with a tag that's constant in the single app process +/// instance. +/// +/// Helps to differentiate same logs that happen in different app process runs. +@internal +void patrolDebug(String message) { + // TODO: Enable only if debug dart define is passed + // ignore: avoid_print + print('PATROL_DEBUG($_runKey): $message'); +} diff --git a/packages/patrol/lib/src/native/contracts/contracts.dart b/packages/patrol/lib/src/native/contracts/contracts.dart index 599d77066..3723967c4 100644 --- a/packages/patrol/lib/src/native/contracts/contracts.dart +++ b/packages/patrol/lib/src/native/contracts/contracts.dart @@ -48,6 +48,18 @@ enum SetLocationAccuracyRequestLocationAccuracy { fine } +@JsonSerializable() +class Empty with EquatableMixin { + Empty(); + + factory Empty.fromJson(Map json) => _$EmptyFromJson(json); + + Map toJson() => _$EmptyToJson(this); + + @override + List get props => const []; +} + @JsonSerializable() class DartGroupEntry with EquatableMixin { DartGroupEntry({ @@ -92,6 +104,31 @@ class ListDartTestsResponse with EquatableMixin { ]; } +@JsonSerializable() +class ListDartLifecycleCallbacksResponse with EquatableMixin { + ListDartLifecycleCallbacksResponse({ + required this.setUpAlls, + required this.tearDownAlls, + }); + + factory ListDartLifecycleCallbacksResponse.fromJson( + Map json, + ) => + _$ListDartLifecycleCallbacksResponseFromJson(json); + + final List setUpAlls; + final List tearDownAlls; + + Map toJson() => + _$ListDartLifecycleCallbacksResponseToJson(this); + + @override + List get props => [ + setUpAlls, + tearDownAlls, + ]; +} + @JsonSerializable() class RunDartTestRequest with EquatableMixin { RunDartTestRequest({ @@ -133,6 +170,28 @@ class RunDartTestResponse with EquatableMixin { ]; } +@JsonSerializable() +class SetLifecycleCallbacksStateRequest with EquatableMixin { + SetLifecycleCallbacksStateRequest({ + required this.state, + }); + + factory SetLifecycleCallbacksStateRequest.fromJson( + Map json, + ) => + _$SetLifecycleCallbacksStateRequestFromJson(json); + + final Map state; + + Map toJson() => + _$SetLifecycleCallbacksStateRequestToJson(this); + + @override + List get props => [ + state, + ]; +} + @JsonSerializable() class ConfigureRequest with EquatableMixin { ConfigureRequest({ @@ -644,3 +703,25 @@ class SetLocationAccuracyRequest with EquatableMixin { locationAccuracy, ]; } + +@JsonSerializable() +class MarkLifecycleCallbackExecutedRequest with EquatableMixin { + MarkLifecycleCallbackExecutedRequest({ + required this.name, + }); + + factory MarkLifecycleCallbackExecutedRequest.fromJson( + Map json, + ) => + _$MarkLifecycleCallbackExecutedRequestFromJson(json); + + final String name; + + Map toJson() => + _$MarkLifecycleCallbackExecutedRequestToJson(this); + + @override + List get props => [ + name, + ]; +} diff --git a/packages/patrol/lib/src/native/contracts/contracts.g.dart b/packages/patrol/lib/src/native/contracts/contracts.g.dart index 62436ad5e..5cd8550f8 100644 --- a/packages/patrol/lib/src/native/contracts/contracts.g.dart +++ b/packages/patrol/lib/src/native/contracts/contracts.g.dart @@ -6,6 +6,10 @@ part of 'contracts.dart'; // JsonSerializableGenerator // ************************************************************************** +Empty _$EmptyFromJson(Map json) => Empty(); + +Map _$EmptyToJson(Empty instance) => {}; + DartGroupEntry _$DartGroupEntryFromJson(Map json) => DartGroupEntry( name: json['name'] as String, @@ -39,6 +43,23 @@ Map _$ListDartTestsResponseToJson( 'group': instance.group, }; +ListDartLifecycleCallbacksResponse _$ListDartLifecycleCallbacksResponseFromJson( + Map json) => + ListDartLifecycleCallbacksResponse( + setUpAlls: + (json['setUpAlls'] as List).map((e) => e as String).toList(), + tearDownAlls: (json['tearDownAlls'] as List) + .map((e) => e as String) + .toList(), + ); + +Map _$ListDartLifecycleCallbacksResponseToJson( + ListDartLifecycleCallbacksResponse instance) => + { + 'setUpAlls': instance.setUpAlls, + 'tearDownAlls': instance.tearDownAlls, + }; + RunDartTestRequest _$RunDartTestRequestFromJson(Map json) => RunDartTestRequest( name: json['name'] as String, @@ -68,6 +89,18 @@ const _$RunDartTestResponseResultEnumMap = { RunDartTestResponseResult.failure: 'failure', }; +SetLifecycleCallbacksStateRequest _$SetLifecycleCallbacksStateRequestFromJson( + Map json) => + SetLifecycleCallbacksStateRequest( + state: Map.from(json['state'] as Map), + ); + +Map _$SetLifecycleCallbacksStateRequestToJson( + SetLifecycleCallbacksStateRequest instance) => + { + 'state': instance.state, + }; + ConfigureRequest _$ConfigureRequestFromJson(Map json) => ConfigureRequest( findTimeoutMillis: json['findTimeoutMillis'] as int, @@ -404,3 +437,15 @@ const _$SetLocationAccuracyRequestLocationAccuracyEnumMap = { SetLocationAccuracyRequestLocationAccuracy.coarse: 'coarse', SetLocationAccuracyRequestLocationAccuracy.fine: 'fine', }; + +MarkLifecycleCallbackExecutedRequest + _$MarkLifecycleCallbackExecutedRequestFromJson(Map json) => + MarkLifecycleCallbackExecutedRequest( + name: json['name'] as String, + ); + +Map _$MarkLifecycleCallbackExecutedRequestToJson( + MarkLifecycleCallbackExecutedRequest instance) => + { + 'name': instance.name, + }; diff --git a/packages/patrol/lib/src/native/contracts/native_automator_client.dart b/packages/patrol/lib/src/native/contracts/native_automator_client.dart index aa0433a40..53e06ed8c 100644 --- a/packages/patrol/lib/src/native/contracts/native_automator_client.dart +++ b/packages/patrol/lib/src/native/contracts/native_automator_client.dart @@ -304,6 +304,15 @@ class NativeAutomatorClient { ); } + Future markLifecycleCallbackExecuted( + MarkLifecycleCallbackExecutedRequest request, + ) { + return _sendRequest( + 'markLifecycleCallbackExecuted', + request.toJson(), + ); + } + Future> _sendRequest( String requestName, [ Map? request, diff --git a/packages/patrol/lib/src/native/contracts/patrol_app_service_server.dart b/packages/patrol/lib/src/native/contracts/patrol_app_service_server.dart index dca57118d..55b631b60 100644 --- a/packages/patrol/lib/src/native/contracts/patrol_app_service_server.dart +++ b/packages/patrol/lib/src/native/contracts/patrol_app_service_server.dart @@ -16,6 +16,22 @@ abstract class PatrolAppServiceServer { if ('listDartTests' == request.url.path) { final result = await listDartTests(); + final body = jsonEncode(result.toJson()); + return Response.ok(body); + } else if ('listDartLifecycleCallbacks' == request.url.path) { + final result = await listDartLifecycleCallbacks(); + + final body = jsonEncode(result.toJson()); + return Response.ok(body); + } else if ('setLifecycleCallbacksState' == request.url.path) { + final stringContent = await request.readAsString(utf8); + final json = jsonDecode(stringContent); + final requestObj = SetLifecycleCallbacksStateRequest.fromJson( + json as Map, + ); + + final result = await setLifecycleCallbacksState(requestObj); + final body = jsonEncode(result.toJson()); return Response.ok(body); } else if ('runDartTest' == request.url.path) { @@ -34,5 +50,9 @@ abstract class PatrolAppServiceServer { } Future listDartTests(); + Future listDartLifecycleCallbacks(); + Future setLifecycleCallbacksState( + SetLifecycleCallbacksStateRequest request, + ); Future runDartTest(RunDartTestRequest request); } diff --git a/packages/patrol/lib/src/native/native_automator.dart b/packages/patrol/lib/src/native/native_automator.dart index a4ea2d865..d095f3929 100644 --- a/packages/patrol/lib/src/native/native_automator.dart +++ b/packages/patrol/lib/src/native/native_automator.dart @@ -769,7 +769,7 @@ class NativeAutomator { ); } - /// Tells the AndroidJUnitRunner that PatrolAppService is ready to answer + /// Tells the native test runner that PatrolAppService is ready to answer /// requests about the structure of Dart tests. @internal Future markPatrolAppServiceReady() async { @@ -778,4 +778,17 @@ class NativeAutomator { _client.markPatrolAppServiceReady, ); } + + /// Tells the native test runner that callback [name] has been executed. + /// + /// The native test runner persists that information. + @internal + Future markLifecycleCallbackExecuted(String name) async { + await _wrapRequest( + 'markLifecycleCallbackExecuted', + () => _client.markLifecycleCallbackExecuted( + MarkLifecycleCallbackExecutedRequest(name: name), + ), + ); + } } diff --git a/packages/patrol/lib/src/native/patrol_app_service.dart b/packages/patrol/lib/src/native/patrol_app_service.dart index a4aa6699e..eb8a83f31 100644 --- a/packages/patrol/lib/src/native/patrol_app_service.dart +++ b/packages/patrol/lib/src/native/patrol_app_service.dart @@ -3,14 +3,15 @@ // TODO: Use a logger instead of print import 'dart:async'; -import 'dart:io'; +import 'dart:io' as 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:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; +// ignore: implementation_imports +import 'package:test_api/src/backend/live_test.dart'; const _port = 8082; const _idleTimeout = Duration(hours: 2); @@ -30,7 +31,7 @@ Future runAppService(PatrolAppService service) async { final server = await shelf_io.serve( pipeline, - InternetAddress.anyIPv4, + io.InternetAddress.anyIPv4, _port, poweredByHeader: null, ); @@ -44,34 +45,78 @@ Future runAppService(PatrolAppService service) async { ); } -/// Implements a stateful gRPC service for querying and executing Dart tests. +/// Implements a stateful HTTP service for querying and executing Dart tests. /// /// This is an internal class and you don't want to use it. It's public so that /// the generated code can access it. class PatrolAppService extends PatrolAppServiceServer { /// Creates a new [PatrolAppService]. - PatrolAppService({required this.topLevelDartTestGroup}); + PatrolAppService(); /// The ambient test group that wraps all the other groups and tests in the /// bundled Dart test file. - final DartGroupEntry topLevelDartTestGroup; + DartGroupEntry? topLevelDartTestGroup; + + /// The names of all setUpAll callbacks. + /// + /// setUpAlls, unlike setUps, aren't executed in the [LiveTest] context. + /// Because of this, we can't depend on the [LiveTest]'s name, so we identify + /// them by the parent group ID instead. + /// + /// This is a list of groups containing setUpAllCallbacks with an index + /// appended. + List setUpAlls = []; + + final _didReceiveLifecycleCallbacksState = Completer>(); + + /// A completer that completes when the native side sets the state of + /// lifecycle callbacks. + /// + /// Completes with a map that knows which lifecycle callbacks have been + /// already executed. + Future> get didReceiveLifecycleCallbacksState { + return _didReceiveLifecycleCallbacksState.future; + } /// A completer that completes with the name of the Dart test file that was /// requested to execute by the native side. - final _testExecutionRequested = Completer(); + final _didRequestTestExecution = Completer(); /// A future that completes with the name of the Dart test file that was /// requested to execute by the native side. - Future get testExecutionRequested => _testExecutionRequested.future; + Future get didRequestTestExecution => _didRequestTestExecution.future; - final _testExecutionCompleted = Completer<_TestExecutionResult>(); + final _didCompleteTestExecution = Completer<_TestExecutionResult>(); /// A future that completes when the Dart test file (whose execution was /// requested by the native side) completes. /// /// Returns true if the test passed, false otherwise. - Future<_TestExecutionResult> get testExecutionCompleted { - return _testExecutionCompleted.future; + Future<_TestExecutionResult> get didCompleteTestExecution { + return _didCompleteTestExecution.future; + } + + /// Adds a setUpAll callback to the list of all setUpAll callbacks. + /// + /// Returns the name under which this setUpAll callback was added. + String addSetUpAll(String group) { + // Not optimal, but good enough for now. + + // Go over all groups, checking if the group is already in the list. + var groupIndex = 0; + for (final setUpAll in setUpAlls) { + final parts = setUpAll.split(' '); + final groupName = parts.sublist(0, parts.length - 1).join(' '); + + if (groupName == group) { + groupIndex++; + } + } + + final name = '$group $groupIndex'; + + setUpAlls.add(name); + return name; } /// Marks [dartFileName] as completed with the given [passed] status. @@ -84,19 +129,20 @@ class PatrolAppService extends PatrolAppServiceServer { required String? details, }) async { print('PatrolAppService.markDartTestAsCompleted(): $dartFileName'); + assert( - _testExecutionRequested.isCompleted, + _didRequestTestExecution.isCompleted, 'Tried to mark a test as completed, but no tests were requested to run', ); - final requestedDartTestName = await testExecutionRequested; + final requestedDartTestName = await didRequestTestExecution; assert( requestedDartTestName == dartFileName, 'Tried to mark test $dartFileName as completed, but the test ' 'that was most recently requested to run was $requestedDartTestName', ); - _testExecutionCompleted.complete( + _didCompleteTestExecution.complete( _TestExecutionResult(passed: passed, details: details), ); } @@ -113,7 +159,7 @@ class PatrolAppService extends PatrolAppServiceServer { Future waitForExecutionRequest(String dartTest) async { print('PatrolAppService: registered "$dartTest"'); - final requestedDartTest = await testExecutionRequested; + final requestedDartTest = await didRequestTestExecution; if (requestedDartTest != dartTest) { // If the requested Dart test is not the one we're waiting for now, it // means that dartTest was already executed. Return false so that callers @@ -134,18 +180,26 @@ class PatrolAppService extends PatrolAppServiceServer { @override Future listDartTests() async { print('PatrolAppService.listDartTests() called'); + + final topLevelDartTestGroup = this.topLevelDartTestGroup; + if (topLevelDartTestGroup == null) { + throw StateError( + 'PatrolAppService.listDartTests(): tests not discovered yet', + ); + } + return ListDartTestsResponse(group: topLevelDartTestGroup); } @override Future runDartTest(RunDartTestRequest request) async { - assert(_testExecutionCompleted.isCompleted == false); - // patrolTest() always calls this method. - print('PatrolAppService.runDartTest(${request.name}) called'); - _testExecutionRequested.complete(request.name); - final testExecutionResult = await testExecutionCompleted; + assert(_didCompleteTestExecution.isCompleted == false); + + _didRequestTestExecution.complete(request.name); + + final testExecutionResult = await didCompleteTestExecution; return RunDartTestResponse( result: testExecutionResult.passed ? RunDartTestResponseResult.success @@ -153,4 +207,32 @@ class PatrolAppService extends PatrolAppServiceServer { details: testExecutionResult.details, ); } + + @override + Future + listDartLifecycleCallbacks() async { + print('PatrolAppService.listDartLifecycleCallbacks() called'); + + assert(_didRequestTestExecution.isCompleted == false); + assert(_didCompleteTestExecution.isCompleted == false); + assert(_didReceiveLifecycleCallbacksState.isCompleted == false); + + return ListDartLifecycleCallbacksResponse( + setUpAlls: setUpAlls, + tearDownAlls: [], + ); + } + + @override + Future setLifecycleCallbacksState( + SetLifecycleCallbacksStateRequest request, + ) async { + print('PatrolAppService.setLifecycleCallbacksState() called'); + assert(_didRequestTestExecution.isCompleted == false); + assert(_didCompleteTestExecution.isCompleted == false); + assert(_didReceiveLifecycleCallbacksState.isCompleted == false); + + _didReceiveLifecycleCallbacksState.complete(request.state); + return Empty(); + } } diff --git a/packages/patrol/pubspec.yaml b/packages/patrol/pubspec.yaml index 6581c5d41..acd1c3c74 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.0.3 +version: 3.1.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 diff --git a/packages/patrol_cli/CHANGELOG.md b/packages/patrol_cli/CHANGELOG.md index a788f2ed6..b5efa5c88 100644 --- a/packages/patrol_cli/CHANGELOG.md +++ b/packages/patrol_cli/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.4.0-dev.1 + +- Add support for `patrolSetUpAll` (#1751) + +This version requires version 3.1.0-dev1 of `patrol` package. + ## 2.3.1+1 - Add screenshots to `pubspec.yaml` (#1917) diff --git a/packages/patrol_cli/lib/src/base/constants.dart b/packages/patrol_cli/lib/src/base/constants.dart index 7ddfcd114..e199caacb 100644 --- a/packages/patrol_cli/lib/src/base/constants.dart +++ b/packages/patrol_cli/lib/src/base/constants.dart @@ -1,2 +1,2 @@ /// Version of Patrol CLI. Must be kept in sync with pubspec.yaml. -const version = '2.3.1+1'; +const version = '2.4.0-dev.1'; diff --git a/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart b/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart index 0fd7ababc..65277fae6 100644 --- a/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart +++ b/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart @@ -10,7 +10,7 @@ import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:patrol_cli/src/analytics/analytics.dart'; import 'package:patrol_cli/src/android/android_test_backend.dart'; -import 'package:patrol_cli/src/base/constants.dart'; +import 'package:patrol_cli/src/base/constants.dart' as constants; import 'package:patrol_cli/src/base/exceptions.dart'; import 'package:patrol_cli/src/base/logger.dart'; import 'package:patrol_cli/src/base/process.dart'; @@ -86,6 +86,7 @@ class PatrolCommandRunner extends CompletionCommandRunner { required Analytics analytics, required Logger logger, required bool isCI, + String version = constants.version, }) : _platform = platform, _pubUpdater = pubUpdater, _fs = fs, @@ -94,6 +95,7 @@ class PatrolCommandRunner extends CompletionCommandRunner { _disposeScope = DisposeScope(), _logger = logger, _isCI = isCI, + _version = version, super( 'patrol', 'Tool for running Flutter-native UI tests with superpowers', @@ -220,6 +222,7 @@ class PatrolCommandRunner extends CompletionCommandRunner { final DisposeScope _disposeScope; final Logger _logger; final bool _isCI; + final String _version; Future dispose() async { try { @@ -312,7 +315,7 @@ Ask questions, get support at https://github.com/leancodepl/patrol/discussions'' final int? exitCode; if (topLevelResults['version'] == true) { - _logger.info('patrol_cli v$version'); + _logger.info('patrol_cli v$_version'); exitCode = 0; } else { exitCode = await super.runCommand(topLevelResults); @@ -372,7 +375,7 @@ Ask questions, get support at https://github.com/leancodepl/patrol/discussions'' } final latestVersion = await _pubUpdater.getLatestVersion('patrol_cli'); - final isUpToDate = version == latestVersion; + final isUpToDate = _version == latestVersion; if (isUpToDate) { return; @@ -382,7 +385,7 @@ Ask questions, get support at https://github.com/leancodepl/patrol/discussions'' ..info('') ..info( ''' -${lightYellow.wrap('Update available!')} ${lightCyan.wrap(version)} \u2192 ${lightCyan.wrap(latestVersion)} +${lightYellow.wrap('Update available!')} ${lightCyan.wrap(_version)} \u2192 ${lightCyan.wrap(latestVersion)} Run ${lightCyan.wrap('patrol update')} to update''', ) ..info(''); diff --git a/packages/patrol_cli/lib/src/test_bundler.dart b/packages/patrol_cli/lib/src/test_bundler.dart index bc8674c7a..8e39d0184 100644 --- a/packages/patrol_cli/lib/src/test_bundler.dart +++ b/packages/patrol_cli/lib/src/test_bundler.dart @@ -28,6 +28,7 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; +import 'package:patrol/src/global_state.dart' as global_state; import 'package:patrol/src/native/contracts/contracts.dart'; import 'package:test_api/src/backend/invoker.dart'; @@ -70,8 +71,15 @@ Future main() async { final nativeAutomator = NativeAutomator(config: NativeAutomatorConfig()); await nativeAutomator.initialize(); - final binding = PatrolBinding.ensureInitialized(NativeAutomatorConfig()); - final testExplorationCompleter = Completer(); + + final appService = PatrolAppService(); + await runAppService(appService); + + final config = NativeAutomatorConfig(); + PatrolBinding.ensureInitialized(appService, nativeAutomator, config); + + final didExploreTests = Completer(); + final didExploreLifecycleCallbacks = Completer(); // A special test to explore the hierarchy of groups and tests. This is a hack // around https://github.com/dart-lang/test/issues/1998. @@ -83,7 +91,7 @@ Future main() async { // to group() below. final topLevelGroup = Invoker.current!.liveTest.groups.first; final dartTestGroup = createDartTestGroup(topLevelGroup); - testExplorationCompleter.complete(dartTestGroup); + didExploreTests.complete(dartTestGroup); print('patrol_test_explorer: obtained Dart-side test hierarchy:'); printGroupStructure(dartTestGroup); }); @@ -92,17 +100,25 @@ Future main() async { ${generateGroupsCode(testFilePaths).split('\n').map((e) => ' $e').join('\n')} // END: GENERATED TEST GROUPS - final dartTestGroup = await testExplorationCompleter.future; - final appService = PatrolAppService(topLevelDartTestGroup: dartTestGroup); - binding.patrolAppService = appService; - await runAppService(appService); + // An additional callback to discover setUpAlls. + tearDownAll(() async { + if (await global_state.isInitialRun) { + didExploreLifecycleCallbacks.complete(); + } + }); + + appService.topLevelDartTestGroup = await didExploreTests.future; + + if (await global_state.isInitialRun) { + await didExploreLifecycleCallbacks.future; + } // Until now, the native test runner was waiting for us, the Dart side, to // come alive. Now that we did, let's tell it that we're ready to be asked // about Dart tests. await nativeAutomator.markPatrolAppServiceReady(); - await appService.testExecutionCompleted; + await appService.didCompleteTestExecution; } '''; @@ -139,7 +155,12 @@ ${generateImports([testFilePath])} Future main() async { final nativeAutomator = NativeAutomator(config: NativeAutomatorConfig()); await nativeAutomator.initialize(); - PatrolBinding.ensureInitialized(NativeAutomatorConfig()) + + final appService = PatrolAppService(); + await runAppService(appService); + + final config = NativeAutomatorConfig(); + PatrolBinding.ensureInitialized(appService, nativeAutomator, config) ..workaroundDebugDefaultTargetPlatformOverride = debugDefaultTargetPlatformOverride; diff --git a/packages/patrol_cli/lib/src/test_runner.dart b/packages/patrol_cli/lib/src/test_runner.dart index cf96acd44..74e3df8d5 100644 --- a/packages/patrol_cli/lib/src/test_runner.dart +++ b/packages/patrol_cli/lib/src/test_runner.dart @@ -3,7 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:path/path.dart' show basename; import 'package:patrol_cli/src/devices.dart'; -// TODO(bartekpacia): Lots of this code is not needed after #1004 is done. +// TODO: Lots of this code is not needed after #1004 is done. enum TargetRunStatus { failedToBuild, failedToExecute, passed, canceled } diff --git a/packages/patrol_cli/pubspec.yaml b/packages/patrol_cli/pubspec.yaml index 902d17ee4..1bb759525 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: 2.3.1+1 # Must be kept in sync with constants.dart +version: 2.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 diff --git a/packages/patrol_cli/test/commands/patrol_command_runner_test.dart b/packages/patrol_cli/test/commands/patrol_command_runner_test.dart index 790303d49..19014a7ad 100644 --- a/packages/patrol_cli/test/commands/patrol_command_runner_test.dart +++ b/packages/patrol_cli/test/commands/patrol_command_runner_test.dart @@ -1,7 +1,6 @@ import 'package:args/command_runner.dart'; import 'package:file/memory.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:patrol_cli/src/base/constants.dart'; import 'package:patrol_cli/src/base/extensions/command_runner.dart'; import 'package:patrol_cli/src/base/logger.dart'; import 'package:patrol_cli/src/runner/patrol_command_runner.dart'; @@ -12,10 +11,11 @@ import 'package:test/test.dart'; import '../ios/ios_test_backend_test.dart'; import '../src/mocks.dart'; -const latestVersion = '0.0.0'; +const currentVersion = '1.2.0'; +const latestVersion = '1.3.0'; final updatePrompt = ''' -${lightYellow.wrap('Update available!')} ${lightCyan.wrap(version)} \u2192 ${lightCyan.wrap(latestVersion)} +${lightYellow.wrap('Update available!')} ${lightCyan.wrap(currentVersion)} \u2192 ${lightCyan.wrap(latestVersion)} Run ${lightCyan.wrap('patrol update')} to update'''; void main() { @@ -31,16 +31,17 @@ void main() { when( () => pubUpdater.getLatestVersion(any()), - ).thenAnswer((_) async => version); + ).thenAnswer((_) async => currentVersion); commandRunner = PatrolCommandRunner( - platform: FakePlatform(), + platform: FakePlatform(environment: {}), processManager: FakeProcessManager(), pubUpdater: pubUpdater, fs: MemoryFileSystem.test(), analytics: MockAnalytics(), logger: logger, isCI: false, + version: currentVersion, ); }); @@ -91,7 +92,7 @@ void main() { }); test( - 'prints error message and usage when command option is passed', + 'prints error message and usage when invalid command is invoked', () async { final result = await commandRunner.run(['foo']); expect(result, equals(1)); @@ -122,7 +123,7 @@ void main() { test('prints current version', () async { final result = await commandRunner.run(['--version']); expect(result, equals(0)); - verify(() => logger.info('patrol_cli v$version')).called(1); + verify(() => logger.info('patrol_cli v$currentVersion')).called(1); }); }); diff --git a/packages/patrol_gen/.gitignore b/packages/patrol_gen/.gitignore new file mode 100644 index 000000000..e5208175c --- /dev/null +++ b/packages/patrol_gen/.gitignore @@ -0,0 +1,3 @@ +# Omit committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/patrol_gen/pubspec.lock b/packages/patrol_gen/pubspec.lock deleted file mode 100644 index 8651b828f..000000000 --- a/packages/patrol_gen/pubspec.lock +++ /dev/null @@ -1,349 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "36a321c3d2cbe01cbcb3540a87b8843846e0206df3e691fa7b23e19e78de6d49" - url: "https://pub.dev" - source: hosted - version: "65.0.0" - analyzer: - dependency: "direct main" - description: - name: analyzer - sha256: dfe03b90ec022450e22513b5e5ca1f01c0c01de9c3fba2f7fd233cb57a6b9a07 - url: "https://pub.dev" - source: hosted - version: "6.3.0" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" - url: "https://pub.dev" - source: hosted - version: "0.11.3" - args: - dependency: transitive - description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" - ci: - dependency: transitive - description: - name: ci - sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" - url: "https://pub.dev" - source: hosted - version: "0.1.0" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 - url: "https://pub.dev" - source: hosted - version: "0.4.0" - collection: - dependency: transitive - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - custom_lint: - dependency: transitive - description: - name: custom_lint - sha256: f9a828b696930cf8307f9a3617b2b65c9b370e484dc845d69100cadb77506778 - url: "https://pub.dev" - source: hosted - version: "0.5.6" - custom_lint_builder: - dependency: transitive - description: - name: custom_lint_builder - sha256: c6f656a4d83385fc0656ae60410ed06bb382898c45627bfb8bbaa323aea97883 - url: "https://pub.dev" - source: hosted - version: "0.5.6" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: e20a67737adcf0cf2465e734dd624af535add11f9edd1f2d444909b5b0749650 - url: "https://pub.dev" - source: hosted - version: "0.5.6" - dart_style: - dependency: "direct main" - description: - name: dart_style - sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334 - url: "https://pub.dev" - source: hosted - version: "2.3.3" - file: - dependency: transitive - description: - name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d - url: "https://pub.dev" - source: hosted - version: "2.4.1" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - hotreloader: - dependency: transitive - description: - name: hotreloader - sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" - leancode_lint: - dependency: "direct dev" - description: - name: leancode_lint - sha256: "1e99cba16e084a18ce966a7df270d6da6a11ab236caac716aa1fb2359eb277eb" - url: "https://pub.dev" - source: hosted - version: "7.0.0+1" - logging: - dependency: transitive - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.dev" - source: hosted - version: "0.12.16" - meta: - dependency: "direct main" - description: - name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 - url: "https://pub.dev" - source: hosted - version: "1.11.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path: - dependency: "direct main" - description: - name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" - url: "https://pub.dev" - source: hosted - version: "1.8.3" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 - url: "https://pub.dev" - source: hosted - version: "1.2.3" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.dev" - source: hosted - version: "0.27.7" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - uuid: - dependency: transitive - description: - name: uuid - sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 - url: "https://pub.dev" - source: hosted - version: "4.2.1" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 - url: "https://pub.dev" - source: hosted - version: "13.0.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.2.0 <4.0.0" diff --git a/schema.dart b/schema.dart index 0a7efb8b1..4d2003a31 100644 --- a/schema.dart +++ b/schema.dart @@ -7,6 +7,9 @@ // - Generic types (IOSServer, IOSClient, AndroidServer, AndroidClient, DartServer, DartClient) // control where we need clients and servers +// void doesn't work +class Empty {} + class DartGroupEntry { late String name; late GroupEntryType type; @@ -19,6 +22,11 @@ class ListDartTestsResponse { late DartGroupEntry group; } +class ListDartLifecycleCallbacksResponse { + late List setUpAlls; + late List tearDownAlls; +} + enum RunDartTestResponseResult { success, skipped, @@ -34,8 +42,14 @@ class RunDartTestResponse { String? details; } +class SetLifecycleCallbacksStateRequest { + late Map state; +} + abstract class PatrolAppService { ListDartTestsResponse listDartTests(); + ListDartLifecycleCallbacksResponse listDartLifecycleCallbacks(); + Empty setLifecycleCallbacksState(SetLifecycleCallbacksStateRequest request); RunDartTestResponse runDartTest(RunDartTestRequest request); } @@ -174,6 +188,10 @@ class SetLocationAccuracyRequest { late SetLocationAccuracyRequestLocationAccuracy locationAccuracy; } +class MarkLifecycleCallbackExecutedRequest { + late String name; +} + abstract class NativeAutomator { void initialize(); void configure(ConfigureRequest request); @@ -223,6 +241,8 @@ abstract class NativeAutomator { // other void debug(); -// TODO(bartekpacia): Move this RPC into a new PatrolNativeTestService service because it doesn't fit here +// TODO: Move these RPCc into a new service (PatrolNativeTestService) because it doesn't fit here void markPatrolAppServiceReady(); + void markLifecycleCallbackExecuted( + MarkLifecycleCallbackExecutedRequest request); }