>() {}.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 820eca3f8..e1545c5f7 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
)
@@ -247,4 +260,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 617aee7cc..defb029aa 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
@@ -47,6 +47,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 {
@@ -200,6 +201,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..b5b3654f2
--- /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', nativeAutomation: true, _body);
+ patrolTest('testB', nativeAutomation: true, _body);
+ patrolTest('testC', nativeAutomation: true, _body);
+ });
+}
+
+Future _body(PatrolTester $) 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/callbacks_test.dart b/packages/patrol/example/integration_test/internal/callbacks_test.dart
similarity index 69%
rename from packages/patrol/example/integration_test/callbacks_test.dart
rename to packages/patrol/example/integration_test/internal/callbacks_test.dart
index 95d69ef4d..06fc129e8 100644
--- a/packages/patrol/example/integration_test/callbacks_test.dart
+++ b/packages/patrol/example/integration_test/internal/callbacks_test.dart
@@ -3,35 +3,36 @@ import 'package:patrol/src/extensions.dart';
// ignore: depend_on_referenced_packages
import 'package:test_api/src/backend/invoker.dart';
-import 'common.dart';
+import '../common.dart';
String get currentTest => Invoker.current!.fullCurrentTestName();
-void _print(String text) => print('PATROL_DEBUG: $text');
+void _print(String text) => print('TEST_DEBUG: $text');
void main() {
patrolSetUp(() async {
await Future.delayed(Duration(seconds: 1));
- _print('setting up before $currentTest');
+ _print('ran patrolSetUp (1) up before "$currentTest"');
});
patrolTearDown(() async {
await Future.delayed(Duration(seconds: 1));
- _print('tearing down after $currentTest');
+ _print('ran patrolTearDown (1) after "$currentTest"');
});
patrolTest('testFirst', nativeAutomation: true, _body);
group('groupA', () {
patrolSetUp(() async {
- if (currentTest == 'callbacks_test groupA testB') {
- throw Exception('PATROL_DEBUG: Crashing testB on purpose!');
+ if (currentTest == 'internal.callbacks_test groupA testB') {
+ throw Exception('TEST_DEBUG: "$currentTest" crashed on purpose');
}
- _print('setting up before $currentTest');
+
+ _print('ran patrolSetUp (2) before "$currentTest"');
});
patrolTearDown(() async {
- _print('tearing down after $currentTest');
+ _print('ran patrolTearDown (2) after "$currentTest"');
});
patrolTest('testA', nativeAutomation: true, _body);
@@ -44,7 +45,7 @@ void main() {
Future _body(PatrolTester $) async {
final testName = Invoker.current!.fullCurrentTestName();
- _print('test body: name=$testName');
+ _print('ran body of test "$testName"');
await createApp($);
diff --git a/packages/patrol/example/ios/Podfile.lock b/packages/patrol/example/ios/Podfile.lock
index 18e033a3c..5bd9677ea 100644
--- a/packages/patrol/example/ios/Podfile.lock
+++ b/packages/patrol/example/ios/Podfile.lock
@@ -70,4 +70,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: b2bb71756d032256bcb4043384dd40772d5e6a93
-COCOAPODS: 1.12.1
+COCOAPODS: 1.13.0
diff --git a/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift b/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift
index 0f67dea3a..8c4cd1a36 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 {}
@@ -294,7 +301,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 7a7e4ac5c..543adcdc2 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
}
@@ -165,3 +178,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 6343bc8a8..4611b7385 100644
--- a/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift
+++ b/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift
@@ -42,6 +42,7 @@ protocol NativeAutomatorServer {
func setLocationAccuracy(request: SetLocationAccuracyRequest) throws
func debug() throws
func markPatrolAppServiceReady() throws
+ func markLifecycleCallbackExecuted(request: MarkLifecycleCallbackExecutedRequest) throws
}
extension NativeAutomatorServer {
@@ -233,6 +234,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 {
@@ -407,6 +414,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 f6da06cbe..66ae8b542 100644
--- a/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift
+++ b/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift
@@ -17,20 +17,64 @@ class PatrolAppServiceClient {
}
func listDartTests(completion: @escaping (Result) -> Void) {
- performRequest(requestName: "listDartTests", completion: completion)
+ 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)
- performRequest(requestName: "runDartTest", body: body, completion: completion)
+ performRequestWithResult(requestName: "runDartTest", body: body, completion: completion)
} catch let err {
completion(.failure(err))
}
}
- private func performRequest(
+ private func performRequestWithResult(
requestName: String, body: Data? = nil, completion: @escaping (Result) -> Void
+ ) {
+ performRequest(requestName: requestName, body: body) { result in
+ switch result {
+ case .success(let data):
+ do {
+ let object = try JSONDecoder().decode(TResult.self, from: data)
+ completion(.success(object))
+ } catch let err {
+ completion(.failure(err))
+ }
+ case .failure(let error):
+ completion(.failure(error))
+ }
+ }
+ }
+
+ private func performRequestWithEmptyResult(
+ requestName: String, body: Data? = nil, completion: @escaping (Error?) -> Void
+ ) {
+ performRequest(requestName: requestName, body: body) { result in
+ switch result {
+ case .success(_):
+ completion(nil)
+ case .failure(let error):
+ completion(error)
+ }
+ }
+ }
+
+ private func performRequest(
+ requestName: String, body: Data? = nil, completion: @escaping (Result) -> Void
) {
let url = URL(string: "http://\(address):\(port)/\(requestName)")!
@@ -47,12 +91,7 @@ class PatrolAppServiceClient {
session.dataTask(with: request) { data, response, error in
if (response as? HTTPURLResponse)?.statusCode == 200 {
- do {
- let object = try JSONDecoder().decode(TResult.self, from: data!)
- completion(.success(object))
- } catch let err {
- completion(.failure(err))
- }
+ completion(.success(data!))
} else {
let message =
"Invalid response: \(String(describing: response)) \(String(describing: data))"
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..172811a02 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,41 @@
[systemAlerts.buttons[@"Allow"] tap]; \
} \
\
+ /* 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) { \
+ /* 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 +94,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 +113,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 a64eb18d1..e366dd8a9 100644
--- a/packages/patrol/lib/src/binding.dart
+++ b/packages/patrol/lib/src/binding.dart
@@ -6,8 +6,6 @@ import 'package:integration_test/integration_test.dart';
import 'package:patrol/patrol.dart';
import 'package:patrol/src/global_state.dart' as global_state;
-import 'constants.dart' as constants;
-
const _success = 'success';
void _defaultPrintLogger(String message) {
@@ -35,20 +33,20 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding {
/// Creates a new [PatrolBinding].
///
/// You most likely don't want to call it yourself.
- PatrolBinding() {
+ PatrolBinding(this.patrolAppService, this.nativeAutomator) {
logger('created');
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;
}
@@ -63,11 +61,17 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding {
});
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) {
@@ -81,7 +85,8 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding {
logger('tearDown(): test "$dartTestName": "$result"');
});
- final requestedDartTest = await patrolAppService.testExecutionRequested;
+ final requestedDartTest = await patrolAppService.didRequestTestExecution;
+
if (requestedDartTest == _currentDartTest) {
logger(
'tearDown(): finished test "$_currentDartTest". Will report its status back to the native side',
@@ -108,9 +113,12 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding {
/// if necessary.
///
/// This method is idempotent.
- factory PatrolBinding.ensureInitialized() {
+ factory PatrolBinding.ensureInitialized(
+ PatrolAppService patrolAppService,
+ NativeAutomator nativeAutomator,
+ ) {
if (_instance == null) {
- PatrolBinding();
+ PatrolBinding(patrolAppService, nativeAutomator);
}
return _instance!;
}
@@ -123,7 +131,11 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding {
///
/// 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.
///
@@ -170,12 +182,12 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding {
@override
void attachRootWidget(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) {
super.attachRootWidget(RepaintBoundary(child: rootWidget));
} else {
super.attachRootWidget(
diff --git a/packages/patrol/lib/src/common.dart b/packages/patrol/lib/src/common.dart
index b6b479e00..4ed80dc7a 100644
--- a/packages/patrol/lib/src/common.dart
+++ b/packages/patrol/lib/src/common.dart
@@ -6,6 +6,7 @@ import 'package:integration_test/integration_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;
@@ -14,7 +15,6 @@ 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].
@@ -23,6 +23,16 @@ typedef PatrolTesterCallback = Future Function(PatrolIntegrationTester $);
/// A modification of [setUp] that works with Patrol's native automation.
void patrolSetUp(Future 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
@@ -37,6 +47,16 @@ void patrolSetUp(Future Function() body) {
/// A modification of [tearDown] that works with Patrol's native automation.
void patrolTearDown(Future 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
@@ -48,7 +68,55 @@ void patrolTearDown(Future Function() body) {
});
}
-/// Like [testWidgets], but with support for Patrol custom finders.
+/// A modification of [setUpAll] that works with Patrol's native automation.
+///
+/// It keeps track of calls made to setUpAll.
+void patrolSetUpAll(Future 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].
///
@@ -84,8 +152,6 @@ void patrolTest(
LiveTestWidgetsFlutterBindingFramePolicy framePolicy =
LiveTestWidgetsFlutterBindingFramePolicy.fadePointers,
}) {
- NativeAutomator? automator;
-
if (!nativeAutomation) {
debugPrint('''
╔════════════════════════════════════════════════════════════════════════════════════╗
@@ -100,8 +166,6 @@ void patrolTest(
if (nativeAutomation) {
switch (bindingType) {
case BindingType.patrol:
- automator = NativeAutomator(config: nativeAutomatorConfig);
-
// PatrolBinding is initialized in the generated test bundle file.
PatrolBinding.instance.framePolicy = framePolicy;
break;
@@ -123,7 +187,17 @@ void patrolTest(
variant: variant,
tags: tags,
(widgetTester) async {
- if (!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.
@@ -136,6 +210,7 @@ void patrolTest(
return;
}
}
+
if (!kIsWeb && io.Platform.isIOS) {
widgetTester.binding.platformDispatcher.onSemanticsEnabledChanged = () {
// This callback is empty on purpose. It's a workaround for tests
@@ -144,11 +219,11 @@ void patrolTest(
// See https://github.com/leancodepl/patrol/issues/1474
};
}
- await automator?.configure();
+ await PatrolBinding.instance.nativeAutomator.configure();
final patrolTester = PatrolIntegrationTester(
tester: widgetTester,
- nativeAutomator: automator,
+ nativeAutomator: PatrolBinding.instance.nativeAutomator,
config: config,
);
await callback(patrolTester);
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/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 27fb69c01..6bfbb5770 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({
@@ -603,3 +662,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 02a6c767f..7fec943ac 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,
@@ -374,3 +407,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 ae1c10d6a..70a245d87 100644
--- a/packages/patrol/lib/src/native/contracts/native_automator_client.dart
+++ b/packages/patrol/lib/src/native/contracts/native_automator_client.dart
@@ -294,6 +294,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 cde6bfaf5..493890a85 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 f67806ff7..eb8a83f31 100644
--- a/packages/patrol/lib/src/native/patrol_app_service.dart
+++ b/packages/patrol/lib/src/native/patrol_app_service.dart
@@ -10,6 +10,8 @@ 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);
@@ -43,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.
@@ -83,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),
);
}
@@ -112,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
@@ -133,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
@@ -152,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_cli/lib/src/test_bundler.dart b/packages/patrol_cli/lib/src/test_bundler.dart
index 472eb66b9..656a15e86 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,14 @@ Future main() async {
final nativeAutomator = NativeAutomator(config: NativeAutomatorConfig());
await nativeAutomator.initialize();
- final binding = PatrolBinding.ensureInitialized();
- final testExplorationCompleter = Completer();
+
+ final appService = PatrolAppService();
+ await runAppService(appService);
+
+ PatrolBinding.ensureInitialized(appService, nativeAutomator);
+
+ final didExploreTests = Completer();
+ final didExploreLifecycleCallbacks = Completer();
// A special test to expore the hierarchy of groups and tests. This is a hack
// around https://github.com/dart-lang/test/issues/1998.
@@ -83,7 +90,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 +99,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;
}
''';
@@ -138,7 +153,11 @@ ${generateImports([testFilePath])}
Future main() async {
final nativeAutomator = NativeAutomator(config: NativeAutomatorConfig());
await nativeAutomator.initialize();
- PatrolBinding.ensureInitialized();
+
+ final appService = PatrolAppService();
+ await runAppService(appService);
+
+ PatrolBinding.ensureInitialized(appService, nativeAutomator);
// START: GENERATED TEST GROUPS
${generateGroupsCode([testFilePath]).split('\n').map((e) => ' $e').join('\n')}
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_gen/lib/src/generators/android/android_contracts_generator.dart b/packages/patrol_gen/lib/src/generators/android/android_contracts_generator.dart
index c2a1ca671..efbfabd1c 100644
--- a/packages/patrol_gen/lib/src/generators/android/android_contracts_generator.dart
+++ b/packages/patrol_gen/lib/src/generators/android/android_contracts_generator.dart
@@ -39,9 +39,14 @@ package ${config.package};
String _createMessage(Message message) {
final fields = message.fields.map((e) {
final optional = e.isOptional ? '? = null' : '';
- return e.isList
- ? ' val ${e.name}: List<${_transformType(e.type)}>$optional'
- : ' val ${e.name}: ${_transformType(e.type)}$optional';
+ return switch (e.type) {
+ MapFieldType(keyType: final keyType, valueType: final valueType) =>
+ ' val ${e.name}: Map<${_transformType(keyType)}, ${_transformType(valueType)}>$optional',
+ ListFieldType(type: final type) =>
+ ' val ${e.name}: List<${_transformType(type)}>$optional',
+ OrdinaryFieldType(type: final type) =>
+ ' val ${e.name}: ${_transformType(type)}$optional',
+ };
}).join(',\n');
final dataKeyword = fields.isNotEmpty ? 'data ' : '';
diff --git a/packages/patrol_gen/lib/src/generators/android/android_http4k_client_generator.dart b/packages/patrol_gen/lib/src/generators/android/android_http4k_client_generator.dart
index d4ce57e3a..e1e42b9c0 100644
--- a/packages/patrol_gen/lib/src/generators/android/android_http4k_client_generator.dart
+++ b/packages/patrol_gen/lib/src/generators/android/android_http4k_client_generator.dart
@@ -98,7 +98,7 @@ $endpoints
val response = performRequest("${endpoint.name}"$serializeParameter)
return json.fromJson(response, Contracts.${endpoint.response!.name}::class.java)'''
: '''
- return performRequest("${endpoint.name}"$serializeParameter)''';
+ performRequest("${endpoint.name}"$serializeParameter)''';
return '''
fun ${endpoint.name}($parameterDef)$returnDef {
diff --git a/packages/patrol_gen/lib/src/generators/dart/dart_contracts_generator.dart b/packages/patrol_gen/lib/src/generators/dart/dart_contracts_generator.dart
index 7a467ea6b..5898293f9 100644
--- a/packages/patrol_gen/lib/src/generators/dart/dart_contracts_generator.dart
+++ b/packages/patrol_gen/lib/src/generators/dart/dart_contracts_generator.dart
@@ -53,9 +53,14 @@ enum ${enumDefinition.name} {
String? _createMessage(Message message) {
final fieldsContent = message.fields
.map(
- (f) => f.isList
- ? 'final List<${f.type}>${f.isOptional ? '?' : ''} ${f.name};'
- : 'final ${f.type}${f.isOptional ? '?' : ''} ${f.name};',
+ (f) => switch (f.type) {
+ ListFieldType(type: final type) =>
+ 'final List<$type>${f.isOptional ? '?' : ''} ${f.name};',
+ MapFieldType(keyType: final keyType, valueType: final valueType) =>
+ 'final Map<$keyType,$valueType>${f.isOptional ? '?' : ''} ${f.name};',
+ OrdinaryFieldType(type: final type) =>
+ 'final $type${f.isOptional ? '?' : ''} ${f.name};'
+ },
)
.join('\n');
diff --git a/packages/patrol_gen/lib/src/generators/dart/dart_shelf_server_generator.dart b/packages/patrol_gen/lib/src/generators/dart/dart_shelf_server_generator.dart
index a4a06da53..861816b9a 100644
--- a/packages/patrol_gen/lib/src/generators/dart/dart_shelf_server_generator.dart
+++ b/packages/patrol_gen/lib/src/generators/dart/dart_shelf_server_generator.dart
@@ -39,7 +39,6 @@ import '${path.basename(config.contractsFilename)}';
String _generateHandlerCalls(Service service) {
return service.endpoints.map((e) {
var requestDeserialization = '';
- var responseSerialization = '';
if (e.request != null) {
requestDeserialization = '''
@@ -49,16 +48,19 @@ final requestObj = ${e.request!.name}.fromJson(json as Map);
''';
}
- final handlerCall = e.request != null
- ? 'final result = await ${e.name}(requestObj);'
- : 'final result = await ${e.name}();';
-
+ var handlerCall = e.request != null
+ ? 'await ${e.name}(requestObj);'
+ : 'await ${e.name}();';
if (e.response != null) {
- responseSerialization = '''
-final body = jsonEncode(result.toJson());
-return Response.ok(body);''';
+ handlerCall = 'final result = $handlerCall';
}
+ final responseSerialization = e.response != null
+ ? '''
+final body = jsonEncode(result.toJson());
+return Response.ok(body);'''
+ : "return Response.ok('');";
+
final elseKeyword = e == service.endpoints.first ? '' : 'else';
return '''
diff --git a/packages/patrol_gen/lib/src/generators/ios/ios_contracts_generator.dart b/packages/patrol_gen/lib/src/generators/ios/ios_contracts_generator.dart
index d6288532d..1f1fd9392 100644
--- a/packages/patrol_gen/lib/src/generators/ios/ios_contracts_generator.dart
+++ b/packages/patrol_gen/lib/src/generators/ios/ios_contracts_generator.dart
@@ -34,9 +34,14 @@ class IOSContractsGenerator {
String _createMessage(Message message) {
final fields = message.fields.map((e) {
final optional = e.isOptional ? '?' : '';
- return e.isList
- ? ' var ${e.name}: [${_transformType(e.type)}]$optional'
- : ' var ${e.name}: ${_transformType(e.type)}$optional';
+ return switch (e.type) {
+ MapFieldType(keyType: final keyType, valueType: final valueType) =>
+ ' var ${e.name}: [${_transformType(keyType)}: ${_transformType(valueType)}]$optional',
+ ListFieldType(type: final type) =>
+ ' var ${e.name}: [${_transformType(type)}]$optional',
+ OrdinaryFieldType(type: final type) =>
+ ' var ${e.name}: ${_transformType(type)}$optional',
+ };
}).join('\n');
return '''
diff --git a/packages/patrol_gen/lib/src/generators/ios/ios_url_session_client_generator.dart b/packages/patrol_gen/lib/src/generators/ios/ios_url_session_client_generator.dart
index 037b5ee18..69aacd46d 100644
--- a/packages/patrol_gen/lib/src/generators/ios/ios_url_session_client_generator.dart
+++ b/packages/patrol_gen/lib/src/generators/ios/ios_url_session_client_generator.dart
@@ -48,8 +48,39 @@ class ${service.name}Client {
$endpoints
- private func performRequest(
+ private func performRequestWithResult(
requestName: String, body: Data? = nil, completion: @escaping (Result) -> Void
+ ) {
+ performRequest(requestName: requestName, body: body) { result in
+ switch result {
+ case .success(let data):
+ do {
+ let object = try JSONDecoder().decode(TResult.self, from: data)
+ completion(.success(object))
+ } catch let err {
+ completion(.failure(err))
+ }
+ case .failure(let error):
+ completion(.failure(error))
+ }
+ }
+ }
+
+ private func performRequestWithEmptyResult(
+ requestName: String, body: Data? = nil, completion: @escaping (Error?) -> Void
+ ) {
+ performRequest(requestName: requestName, body: body) { result in
+ switch result {
+ case .success(_):
+ completion(nil)
+ case .failure(let error):
+ completion(error)
+ }
+ }
+ }
+
+ private func performRequest(
+ requestName: String, body: Data? = nil, completion: @escaping (Result) -> Void
) {
let url = URL(string: "$url")!
@@ -66,12 +97,7 @@ $endpoints
session.dataTask(with: request) { data, response, error in
if (response as? HTTPURLResponse)?.statusCode == 200 {
- do {
- let object = try JSONDecoder().decode(TResult.self, from: data!)
- completion(.success(object))
- } catch let err {
- completion(.failure(err))
- }
+ completion(.success(data!))
} else {
let message =
"$exceptionMessage"
@@ -89,7 +115,7 @@ $endpoints
final completionDef = endpoint.response != null
? 'completion: @escaping (Result<${endpoint.response!.name}, Error>) -> Void'
- : 'completion: @escaping (Result) -> Void';
+ : 'completion: @escaping (Error?) -> Void';
final parameters = endpoint.request != null
? '$requestDef, $completionDef'
@@ -98,18 +124,25 @@ $endpoints
final arguments = [
'requestName: "${endpoint.name}"',
if (endpoint.request != null) 'body: body',
- 'completion: completion'
+ 'completion: completion',
].join(', ');
+ final performRequest = endpoint.response != null
+ ? 'performRequestWithResult'
+ : 'performRequestWithEmptyResult';
+ final failureCompletion = endpoint.response != null
+ ? 'completion(.failure(err))'
+ : 'completion(err)';
+
final bodyCode = endpoint.request != null
? '''
do {
let body = try JSONEncoder().encode(request)
- performRequest($arguments)
+ $performRequest($arguments)
} catch let err {
- completion(.failure(err))
+ $failureCompletion
}'''
- : ' performRequest($arguments)';
+ : ' $performRequest($arguments)';
return '''
func ${endpoint.name}($parameters) {
diff --git a/packages/patrol_gen/lib/src/resolve_schema.dart b/packages/patrol_gen/lib/src/resolve_schema.dart
index ad1b515c0..0be68dd0d 100644
--- a/packages/patrol_gen/lib/src/resolve_schema.dart
+++ b/packages/patrol_gen/lib/src/resolve_schema.dart
@@ -106,20 +106,28 @@ Message _createMessage(ClassDeclaration declaration) {
final isOptional = type.question != null;
final fieldName = e.variables.first.name.lexeme;
- if (type.type?.isDartCoreList ?? false) {
+ if (type.type?.isDartCoreMap ?? false) {
+ final arguments = type.typeArguments!.arguments;
+ return MessageField(
+ isOptional: isOptional,
+ name: fieldName,
+ type: MapFieldType(
+ keyType: (arguments[0] as NamedType).name2.lexeme,
+ valueType: (arguments[1] as NamedType).name2.lexeme,
+ ),
+ );
+ } else if (type.type?.isDartCoreList ?? false) {
final genericType = type.typeArguments!.arguments.first as NamedType;
return MessageField(
isOptional: isOptional,
name: fieldName,
- type: genericType.name2.lexeme,
- isList: true,
+ type: ListFieldType(type: genericType.name2.lexeme),
);
} else {
return MessageField(
isOptional: isOptional,
name: fieldName,
- type: type.name2.lexeme,
- isList: false,
+ type: OrdinaryFieldType(type: type.name2.lexeme),
);
}
} else {
diff --git a/packages/patrol_gen/lib/src/schema.dart b/packages/patrol_gen/lib/src/schema.dart
index 7d40d3179..8aab84578 100644
--- a/packages/patrol_gen/lib/src/schema.dart
+++ b/packages/patrol_gen/lib/src/schema.dart
@@ -5,18 +5,37 @@ class Enum {
final List fields;
}
+sealed class MessageFieldType {}
+
+class OrdinaryFieldType implements MessageFieldType {
+ const OrdinaryFieldType({required this.type});
+
+ final String type;
+}
+
+class ListFieldType implements MessageFieldType {
+ const ListFieldType({required this.type});
+
+ final String type;
+}
+
+class MapFieldType implements MessageFieldType {
+ const MapFieldType({required this.keyType, required this.valueType});
+
+ final String keyType;
+ final String valueType;
+}
+
class MessageField {
const MessageField({
required this.name,
required this.type,
required this.isOptional,
- required this.isList,
});
final bool isOptional;
- final bool isList;
final String name;
- final String type;
+ final MessageFieldType type;
}
class Message {
diff --git a/packages/patrol_gen/pubspec.lock b/packages/patrol_gen/pubspec.lock
index a7f24b26e..b7dda4027 100644
--- a/packages/patrol_gen/pubspec.lock
+++ b/packages/patrol_gen/pubspec.lock
@@ -170,7 +170,7 @@ packages:
source: hosted
version: "1.3.2"
watcher:
- dependency: transitive
+ dependency: "direct overridden"
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
diff --git a/packages/patrol_gen/pubspec.yaml b/packages/patrol_gen/pubspec.yaml
index 363972ed6..ee58d0387 100644
--- a/packages/patrol_gen/pubspec.yaml
+++ b/packages/patrol_gen/pubspec.yaml
@@ -11,10 +11,13 @@ environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
- analyzer: ^6.1.0
+ analyzer: ^6.2.0
dart_style: ^2.3.2
meta: ^1.7.0
path: ^1.8.2
-
+
dev_dependencies:
leancode_lint: ^3.0.0
+
+dependency_overrides:
+ watcher: ^1.1.0
diff --git a/schema.dart b/schema.dart
index d117a8707..68ff1d99c 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);
}
@@ -165,6 +179,10 @@ class SetLocationAccuracyRequest {
late SetLocationAccuracyRequestLocationAccuracy locationAccuracy;
}
+class MarkLifecycleCallbackExecutedRequest {
+ late String name;
+}
+
abstract class NativeAutomator {
void initialize();
void configure(ConfigureRequest request);
@@ -213,6 +231,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);
}