Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for advanced test lifecycle callbacks - setUpAll #1751

Merged
merged 82 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
bdf12ee
create CustomPatrolJUnitRunner
bartekpacia Sep 28, 2023
a84464f
write persistent file to `/sdcard/googletest/test_outputfiles`
bartekpacia Sep 29, 2023
2142bc6
satisfy ktlint
bartekpacia Oct 2, 2023
0e9b69d
move callbacks_test to internal/
bartekpacia Oct 2, 2023
40bd9a4
WIP (try out `currentGroupFullName` approach)
bartekpacia Oct 2, 2023
f6a570a
schema.dart: add `ListDartLifecycleCallbacksResponse`
bartekpacia Oct 5, 2023
8c5e8d5
patrol_gen: bump dependency versions
bartekpacia Oct 5, 2023
e77de99
regenerate contracts
bartekpacia Oct 5, 2023
38fcdf3
PatrolAppService: start implementing tracking of setUpAlls
bartekpacia Oct 5, 2023
cd0dac6
wip
bartekpacia Oct 9, 2023
6045fa7
remember setUpAll callbacks
bartekpacia Oct 9, 2023
7683960
add missing comments
bartekpacia Oct 9, 2023
9edac16
WIP: forward isInitialRun from instrumentation app to app under test
bartekpacia Oct 10, 2023
7ed9235
delete `example/test/callbacks_test.dart` (moved to bartekpacia/test_…
bartekpacia Oct 10, 2023
56dca3b
implement PatrolAppService.addSetUpAll and remembering setUpAlls
bartekpacia Oct 10, 2023
2a7ae16
PatrolJUnitRunner: fix log tag in waitForPatrolAppService()
bartekpacia Oct 11, 2023
1f2eaa4
PatrolBinding.setUp(): fix patrol_test_explorer not being ignored
bartekpacia Oct 11, 2023
69a89f3
PatrolBinding.tearDown(): don't block on testExecutionRequested durin…
bartekpacia Oct 11, 2023
cf0e8d4
move hotRestartEnabled from constants.dart to global_state.dart, dele…
bartekpacia Oct 11, 2023
3ba9ae1
add patrolDebug() log helper to differentiat between app process runs
bartekpacia Oct 12, 2023
6ce85db
update dev_docs/GUIDE.md
bartekpacia Oct 12, 2023
6f0125b
PatrolBinding: remove unneeded debug logs in tearDown()
bartekpacia Oct 12, 2023
0f61514
WIP: start adding PatrolJUnitRunner.listLifecycleCallbacks()
bartekpacia Oct 12, 2023
2172995
Merge branch 'develop' into lifecycle_callbacks_setupall
bartekpacia Oct 13, 2023
f1ccf53
PatrolAppServiceClient: fix duplicated override of runDartTest()
bartekpacia Oct 13, 2023
f9bd800
PatrolBinding: accept PatrolAppService in constructor
bartekpacia Oct 13, 2023
1b19ddd
PatrolJUnitRunner: implement basic listDartLifecycleCallbacks()
bartekpacia Oct 13, 2023
7122587
PatrolAppService: remove topLevelGroup from constructor
bartekpacia Oct 13, 2023
c89b10d
test_bundler: adjust generated bundle to PatrolBinding and PatrolAppS…
bartekpacia Oct 13, 2023
67c37b7
native Android side: only call listDartLifecycleCallbacks() during in…
bartekpacia Oct 13, 2023
f558713
PatrolJUnitRunner: add comment about where patrol.txt is created
bartekpacia Oct 16, 2023
6114b30
PatrolJUnitRunner: save lifecycle callbacks to JSON during initial run
bartekpacia Oct 16, 2023
d877bca
PatrolBinding: add dependency on NativeAutomator
bartekpacia Oct 16, 2023
716c13f
schema: add NativeAutomator.markLifecycleCallbackExecuted()
bartekpacia Oct 16, 2023
37d012d
implement NativeAutomator.markLifecycleCallbackExecuted
bartekpacia Oct 16, 2023
2cc9def
implement clearing state on initial run (might be unneeded?)
bartekpacia Oct 16, 2023
e62d0a0
schema: add PatrolAppService.setLifecycleCallbacksState
bartekpacia Oct 16, 2023
ae9c3da
progress with PatrolAppService.setLifecycleCallbacksState
bartekpacia Oct 16, 2023
1aadc02
fix bug with white screen on normal test run
bartekpacia Oct 17, 2023
636a476
logs.dart: fix analyzer warnings
bartekpacia Oct 17, 2023
f629d7a
Add map support
zltnDC Oct 17, 2023
1b718a1
remove PatrolJUnitRunner
bartekpacia Oct 17, 2023
005ae57
delete cleaning state file on initial run (not needed)
bartekpacia Oct 17, 2023
d4151de
remove redundant debugLog from generated test_bundle
bartekpacia Oct 17, 2023
cef44a7
remove some debugging aids
bartekpacia Oct 17, 2023
9c748e7
update completer names to be `didX` - more readable
bartekpacia Oct 17, 2023
d50706d
Fix endpoints that do not return results
zltnDC Oct 17, 2023
957c4d1
MainActivityTest.java: care about backward compatibility
bartekpacia Oct 17, 2023
68b969b
Provide redirects for link changes from 2138733
Albert221 Oct 17, 2023
f0af317
Merge pull request #1811 from leancodepl/fix/historical-redirects
bartekpacia Oct 17, 2023
54b2715
Revert "Provide redirects for link changes from 2138733"
bartekpacia Oct 17, 2023
7a74aeb
Merge pull request #1812 from leancodepl/revert-1811-fix/historical-r…
bartekpacia Oct 17, 2023
13f674c
Merge pull request #1810 from leancodepl/task/map-support
bartekpacia Oct 17, 2023
ddcb70f
update guide
bartekpacia Oct 17, 2023
36ab209
PatrolJUnitRunner: update comment
bartekpacia Oct 17, 2023
33caf18
fix generated test_bundle referencing Completers with changede names
bartekpacia Oct 18, 2023
16f0b8f
make Hot Restart work again
bartekpacia Oct 18, 2023
cf42a71
fix setUpAll and tearDownAll - don't block when Hot Restart is enabled
bartekpacia Oct 18, 2023
53b467b
Merge branch 'master' into lifecycle_callbacks_setupall
bartekpacia Oct 19, 2023
5e73037
regenerate contracts
bartekpacia Oct 19, 2023
102f95b
copy PATROL_INTEGRATION_TEST_IOS_RUNNER macro to RunnerUITests
bartekpacia Oct 19, 2023
bc8a773
ObjCPatrolAppServiceClient: implement listDartLifecycleCallbacks and …
bartekpacia Oct 19, 2023
dac6907
AutomatorServer: fix compile errors
bartekpacia Oct 19, 2023
eb2f236
forward PATROL_INITIAL_RUN to app and expose it on method channel
bartekpacia Oct 19, 2023
7cb5984
ObjCPatrolAppServiceClient.swift: add 1 sec timeout to wait for patro…
bartekpacia Oct 19, 2023
5f61d5c
common.dart: print before setUpAll() - debugging aid
bartekpacia Oct 19, 2023
289aaaa
RunnerUITests: call listDartLifecycleCallbacks and setDartLifecycleCa…
bartekpacia Oct 19, 2023
f571f2b
PatrolJUnitRunner: fix sending lifecycle callback status as String in…
bartekpacia Oct 19, 2023
00c9178
RunnerUITests.m: minor improvements
bartekpacia Oct 19, 2023
b77e74b
update podfile
bartekpacia Oct 23, 2023
ae51ac2
logs.dart: format _runKey as 4-digit hex
bartekpacia Oct 23, 2023
004fddc
format code in SwiftPatrolPlugin.swift
bartekpacia Oct 23, 2023
4cb5a39
implement NativeAutomator.markLifecycleCallbackExecuted
bartekpacia Oct 23, 2023
6f9e68e
remove too much docs
bartekpacia Oct 23, 2023
60c53d8
update todo
bartekpacia Oct 23, 2023
7e7e9ef
patrolTest(): remove 2 sec wait
bartekpacia Oct 24, 2023
5ba8427
callbacks_all_test: remove 2 sec wait
bartekpacia Oct 24, 2023
d205d7a
fix possible race condition
bartekpacia Oct 24, 2023
aef572c
remove PatrolServer.appReady, replace with onAppReady callback
bartekpacia Oct 24, 2023
2d2a7e3
PATROL_INTEGRATION_TEST_IOS_RUNNER: improve comments
bartekpacia Oct 24, 2023
ae868ab
move code from RunnerUITests.m to PatrolIntegrationTestRunner.h
bartekpacia Oct 24, 2023
6a6615e
final cleanup
bartekpacia Oct 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion dev_docs/GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,17 +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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,6 +10,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
Expand Down Expand Up @@ -210,4 +212,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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Boolean>): 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)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,9 +41,30 @@
* A customized AndroidJUnitRunner that enables Patrol on Android.
* </p>
*/
@SuppressLint({"UnsafeOptInUsageError", "RestrictedApi"})
public class PatrolJUnitRunner extends AndroidJUnitRunner {
public PatrolAppServiceClient patrolAppServiceClient;

/**
* <p>
* Available only after onCreate() has been run.
* </p>
*/
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;
Expand All @@ -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)" : ""));
}

/**
Expand All @@ -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);
Expand All @@ -94,7 +130,7 @@ public PatrolAppServiceClient createAppServiceClient() {
* </p>
*/
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();
Expand All @@ -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<DartGroupEntry> dartTestCases = ContractsExtensionsKt.listTestsFlat(dartTestGroup, "");
Expand All @@ -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<String> 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<String, Boolean> 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<String, Boolean> state = readStateFile();
state.put(name, true);
writeStateFile(state);
}

private Map<String, Boolean> readStateFile() {
try {
InputStream inputStream = TestStorageUtil.getInputStream(stateFileUri, getContentResolver());
String content = convertStreamToString(inputStream);
Gson gson = new Gson();
Type typeOfHashMap = new TypeToken<Map<String, Boolean>>() {}.getType();
Map<String, Boolean> data = gson.fromJson(content, typeOfHashMap);
return data;
} catch (FileNotFoundException e) {
throw new RuntimeException("Failed to read state file", e);
}
}

private void writeStateFile(Map<String, Boolean> data) {
try {
OutputStream outputStream = TestStorageUtil.getOutputStream(stateFileUri, getContentResolver());
Gson gson = new Gson();
Type typeOfHashMap = new TypeToken<Map<String, Boolean>>() {}.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.
* <p>
* 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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Loading