Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 0ee413e

Browse files
authored
A native Android unit-testing harness. (#51479)
Sets up rules to create an APK that is comprised of solely native code. Existing executable targets (like GTests) can then use this to run on Android devices while having access to activities, windows, etc.. This allows for broader test coverage. Basically, anything that needed an ANativeWindow could only be tested in an integration test. Executables that need access to the native activity must provide an implementation of `NativeActivityMain` that returns a custom subclass of `flutter::NativeActivity`. The `native_activity_apk` reads like an `executable` or `shared_library` target. Just one that packages that executable in an APK. The APK is built using the Android Tools and does not use Gradle. Creating a new APK after invalidating some code takes ~200ms on my machine. The edit, compile, run cycle for only a tiny bit worse than testing on the host. Builds on top of this new infrastructure to create a `GTestActivity` that runs an existing test suites. This works really well except the GTest suite logs to `STDOUT` whereas the engine logs to `logcat`. To quickly work around this, a custom test status listener has been wired up. This only displays the test results to logcat today but a similar mechanism can be used to talk to the test runner in the host. I will wire this up in an upcoming patch as there is no hooks into this from CI right now. Creates an APK variant of the `impeller_toolkit_android_unittests` harness.
1 parent 3afc7b1 commit 0ee413e

18 files changed

+829
-4
lines changed

BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ group("unittests") {
168168
public_deps = []
169169
if (is_android) {
170170
public_deps += [
171+
"//flutter/impeller/toolkit/android:apk_unittests",
171172
"//flutter/impeller/toolkit/android:unittests",
172173
"//flutter/shell/platform/android:flutter_shell_native_unittests",
173174
]

impeller/toolkit/android/BUILD.gn

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Use of this source code is governed by a BSD-style license that can be
33
# found in the LICENSE file.
44

5+
import("//flutter/testing/android/native_activity/native_activity.gni")
56
import("../../tools/impeller.gni")
67

78
config("public_android_config") {
@@ -37,13 +38,11 @@ test_fixtures("unittests_fixtures") {
3738
fixtures = []
3839
}
3940

40-
executable("unittests") {
41-
assert(is_android)
41+
source_set("unittests_lib") {
42+
visibility = [ ":*" ]
4243

4344
testonly = true
4445

45-
output_name = "impeller_toolkit_android_unittests"
46-
4746
sources = [ "toolkit_android_unittests.cc" ]
4847

4948
deps = [
@@ -52,3 +51,24 @@ executable("unittests") {
5251
"//flutter/testing",
5352
]
5453
}
54+
55+
executable("unittests") {
56+
assert(is_android)
57+
58+
testonly = true
59+
60+
output_name = "impeller_toolkit_android_unittests"
61+
62+
deps = [ ":unittests_lib" ]
63+
}
64+
65+
native_activity_apk("apk_unittests") {
66+
apk_name = "impeller_toolkit_android_unittests"
67+
68+
testonly = true
69+
70+
deps = [
71+
":unittests_lib",
72+
"//flutter/testing/android/native_activity:gtest_activity",
73+
]
74+
}

testing/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ source_set("testing_lib") {
2121
"debugger_detection.h",
2222
"display_list_testing.cc",
2323
"display_list_testing.h",
24+
"logger_listener.cc",
25+
"logger_listener.h",
2426
"mock_canvas.cc",
2527
"mock_canvas.h",
2628
"post_task_sync.cc",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="dev.flutter.testing.{{apk-library-name}}"
4+
android:versionCode="1"
5+
android:versionName="1.0"
6+
>
7+
<uses-sdk android:minSdkVersion="23"/>
8+
<application android:hasCode="false">
9+
<activity android:name="android.app.NativeActivity"
10+
android:configChanges="orientation|keyboardHidden"
11+
android:exported="true">
12+
<meta-data android:name="android.app.lib_name"
13+
android:value="{{apk-library-name}}" />
14+
<intent-filter>
15+
<action android:name="android.intent.action.MAIN" />
16+
<category android:name="android.intent.category.LAUNCHER" />
17+
</intent-filter>
18+
</activity>
19+
</application>
20+
</manifest>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright 2013 The Flutter Authors. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
# To create an native activity, deps in this source set in a
6+
# `native_activity_apk` target and make sure to add the implementation of
7+
# `NativeActivityMain` which returns a `flutter::NativeActivity` subclass.
8+
source_set("native_activity") {
9+
assert(is_android)
10+
11+
sources = [
12+
"native_activity.cc",
13+
"native_activity.h",
14+
]
15+
16+
public_deps = [
17+
"//flutter/fml",
18+
"//flutter/impeller/toolkit/android",
19+
]
20+
21+
libs = [
22+
"android",
23+
"log",
24+
]
25+
}
26+
27+
source_set("gtest_activity") {
28+
assert(is_android)
29+
30+
testonly = true
31+
32+
sources = [
33+
"gtest_activity.cc",
34+
"gtest_activity.h",
35+
]
36+
37+
public_deps = [
38+
":native_activity",
39+
"//flutter/testing:testing_lib",
40+
]
41+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Native Activity
2+
===============
3+
4+
Executables packaged as native activities in an Android APK. These activities
5+
contain no Java code.
6+
7+
To create an APK of your existing `exectuable` target, replace `exectuable` with
8+
`native_activity_apk` from the `native_activity.gni` template and give it an
9+
`apk_name`.
10+
11+
## Example
12+
13+
```
14+
native_activity_apk("apk_unittests") {
15+
apk_name = "toolkit_unittests"
16+
17+
testonly = true
18+
19+
sources = [ "toolkit_android_unittests.cc" ]
20+
21+
deps = [
22+
":unittests_lib",
23+
"//flutter/testing/android/native_activity:gtest_activity",
24+
]
25+
}
26+
```
27+
28+
One of the translation units in must contain an implementation of
29+
`flutter::NativeActivityMain`. The `gtest_activity` target contains an
30+
implementation of an activity that run GoogleTests. That can be used off the
31+
shelf.
2.56 KB
Binary file not shown.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include "flutter/testing/android/native_activity/gtest_activity.h"
6+
7+
#include "flutter/impeller/toolkit/android/native_window.h"
8+
#include "flutter/testing/logger_listener.h"
9+
#include "flutter/testing/test_timeout_listener.h"
10+
11+
namespace flutter {
12+
13+
GTestActivity::GTestActivity(ANativeActivity* activity)
14+
: NativeActivity(activity) {}
15+
16+
GTestActivity::~GTestActivity() = default;
17+
18+
static void StartTestSuite(const impeller::android::NativeWindow& window) {
19+
auto timeout_listener = new flutter::testing::TestTimeoutListener(
20+
fml::TimeDelta::FromSeconds(120u));
21+
auto logger_listener = new flutter::testing::LoggerListener();
22+
23+
auto& listeners = ::testing::UnitTest::GetInstance()->listeners();
24+
25+
listeners.Append(timeout_listener);
26+
listeners.Append(logger_listener);
27+
28+
int result = RUN_ALL_TESTS();
29+
30+
delete listeners.Release(timeout_listener);
31+
delete listeners.Release(logger_listener);
32+
33+
FML_CHECK(result == 0);
34+
}
35+
36+
// |NativeActivity|
37+
void GTestActivity::OnNativeWindowCreated(ANativeWindow* window) {
38+
auto handle = std::make_shared<impeller::android::NativeWindow>(window);
39+
background_thread_.GetTaskRunner()->PostTask(
40+
[handle]() { StartTestSuite(*handle); });
41+
}
42+
43+
std::unique_ptr<NativeActivity> NativeActivityMain(
44+
ANativeActivity* activity,
45+
std::unique_ptr<fml::Mapping> saved_state) {
46+
return std::make_unique<GTestActivity>(activity);
47+
}
48+
49+
} // namespace flutter
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_GTEST_ACTIVITY_H_
6+
#define FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_GTEST_ACTIVITY_H_
7+
8+
#include "flutter/fml/macros.h"
9+
#include "flutter/fml/thread.h"
10+
#include "flutter/testing/android/native_activity/native_activity.h"
11+
12+
namespace flutter {
13+
14+
//------------------------------------------------------------------------------
15+
/// @brief A native activity subclass an in implementation of
16+
/// `flutter::NativeActivityMain` that return it.
17+
///
18+
/// This class runs a Google Test harness on a background thread and
19+
/// redirects progress updates to `logcat` instead of STDOUT.
20+
///
21+
class GTestActivity final : public NativeActivity {
22+
public:
23+
explicit GTestActivity(ANativeActivity* activity);
24+
25+
~GTestActivity() override;
26+
27+
GTestActivity(const GTestActivity&) = delete;
28+
29+
GTestActivity& operator=(const GTestActivity&) = delete;
30+
31+
// |NativeActivity|
32+
void OnNativeWindowCreated(ANativeWindow* window) override;
33+
34+
private:
35+
fml::Thread background_thread_;
36+
};
37+
38+
} // namespace flutter
39+
40+
#endif // FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_GTEST_ACTIVITY_H_
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include "flutter/testing/android/native_activity/native_activity.h"
6+
7+
#include "flutter/fml/message_loop.h"
8+
9+
namespace flutter {
10+
11+
NativeActivity::NativeActivity(ANativeActivity* activity)
12+
: activity_(activity) {
13+
fml::MessageLoop::EnsureInitializedForCurrentThread();
14+
15+
activity->instance = this;
16+
17+
activity->callbacks->onStart = [](ANativeActivity* activity) {
18+
reinterpret_cast<NativeActivity*>(activity->instance)->OnStart();
19+
};
20+
activity->callbacks->onStop = [](ANativeActivity* activity) {
21+
reinterpret_cast<NativeActivity*>(activity->instance)->OnStop();
22+
};
23+
activity->callbacks->onPause = [](ANativeActivity* activity) {
24+
reinterpret_cast<NativeActivity*>(activity->instance)->OnPause();
25+
};
26+
activity->callbacks->onResume = [](ANativeActivity* activity) {
27+
reinterpret_cast<NativeActivity*>(activity->instance)->OnResume();
28+
};
29+
activity->callbacks->onDestroy = [](ANativeActivity* activity) {
30+
delete reinterpret_cast<NativeActivity*>(activity->instance);
31+
};
32+
activity->callbacks->onSaveInstanceState = [](ANativeActivity* activity,
33+
size_t* out_size) -> void* {
34+
auto mapping = reinterpret_cast<NativeActivity*>(activity->instance)
35+
->OnSaveInstanceState();
36+
if (mapping == nullptr || mapping->GetMapping() == nullptr) {
37+
*out_size = 0;
38+
return nullptr;
39+
}
40+
41+
// This will be `free`d by the framework.
42+
auto copied = malloc(mapping->GetSize());
43+
FML_CHECK(copied != nullptr)
44+
<< "Allocation failure while saving instance state.";
45+
memcpy(copied, mapping->GetMapping(), mapping->GetSize());
46+
*out_size = mapping->GetSize();
47+
return copied;
48+
};
49+
activity->callbacks->onWindowFocusChanged = [](ANativeActivity* activity,
50+
int has_focus) {
51+
reinterpret_cast<NativeActivity*>(activity->instance)
52+
->OnWindowFocusChanged(has_focus);
53+
};
54+
activity->callbacks->onNativeWindowCreated = [](ANativeActivity* activity,
55+
ANativeWindow* window) {
56+
reinterpret_cast<NativeActivity*>(activity->instance)
57+
->OnNativeWindowCreated(window);
58+
};
59+
activity->callbacks->onNativeWindowResized = [](ANativeActivity* activity,
60+
ANativeWindow* window) {
61+
reinterpret_cast<NativeActivity*>(activity->instance)
62+
->OnNativeWindowResized(window);
63+
};
64+
activity->callbacks->onNativeWindowRedrawNeeded =
65+
[](ANativeActivity* activity, ANativeWindow* window) {
66+
reinterpret_cast<NativeActivity*>(activity->instance)
67+
->OnNativeWindowRedrawNeeded(window);
68+
};
69+
activity->callbacks->onNativeWindowDestroyed = [](ANativeActivity* activity,
70+
ANativeWindow* window) {
71+
reinterpret_cast<NativeActivity*>(activity->instance)
72+
->OnNativeWindowDestroyed(window);
73+
};
74+
activity->callbacks->onInputQueueCreated = [](ANativeActivity* activity,
75+
AInputQueue* queue) {
76+
reinterpret_cast<NativeActivity*>(activity->instance)
77+
->OnInputQueueCreated(queue);
78+
};
79+
activity->callbacks->onInputQueueDestroyed = [](ANativeActivity* activity,
80+
AInputQueue* queue) {
81+
reinterpret_cast<NativeActivity*>(activity->instance)
82+
->OnInputQueueDestroyed(queue);
83+
};
84+
activity->callbacks->onConfigurationChanged = [](ANativeActivity* activity) {
85+
reinterpret_cast<NativeActivity*>(activity->instance)
86+
->OnConfigurationChanged();
87+
};
88+
activity->callbacks->onLowMemory = [](ANativeActivity* activity) {
89+
reinterpret_cast<NativeActivity*>(activity->instance)->OnLowMemory();
90+
};
91+
}
92+
93+
NativeActivity::~NativeActivity() = default;
94+
95+
void NativeActivity::OnStart() {}
96+
97+
void NativeActivity::OnStop() {}
98+
99+
void NativeActivity::OnPause() {}
100+
101+
void NativeActivity::OnResume() {}
102+
103+
std::shared_ptr<fml::Mapping> NativeActivity::OnSaveInstanceState() {
104+
return nullptr;
105+
}
106+
107+
void NativeActivity::OnWindowFocusChanged(bool has_focus) {}
108+
109+
void NativeActivity::OnNativeWindowCreated(ANativeWindow* window) {}
110+
111+
void NativeActivity::OnNativeWindowResized(ANativeWindow* window) {}
112+
113+
void NativeActivity::OnNativeWindowRedrawNeeded(ANativeWindow* window) {}
114+
115+
void NativeActivity::OnNativeWindowDestroyed(ANativeWindow* window) {}
116+
117+
void NativeActivity::OnInputQueueCreated(AInputQueue* queue) {}
118+
119+
void NativeActivity::OnInputQueueDestroyed(AInputQueue* queue) {}
120+
121+
void NativeActivity::OnConfigurationChanged() {}
122+
123+
void NativeActivity::OnLowMemory() {}
124+
125+
void NativeActivity::Terminate() {
126+
ANativeActivity_finish(activity_);
127+
}
128+
129+
} // namespace flutter
130+
131+
extern "C" __attribute__((visibility("default"))) void ANativeActivity_onCreate(
132+
ANativeActivity* activity,
133+
void* saved_state,
134+
size_t saved_state_size) {
135+
std::unique_ptr<fml::Mapping> saved_state_mapping;
136+
if (saved_state_size > 0u) {
137+
saved_state_mapping = std::make_unique<fml::MallocMapping>(
138+
fml::MallocMapping::Copy(saved_state, saved_state_size));
139+
}
140+
flutter::NativeActivityMain(activity, std::move(saved_state_mapping))
141+
.release(); // Will be freed when the frame calls the onDestroy. See the
142+
// delete in that callback.
143+
}

0 commit comments

Comments
 (0)