Skip to content

Commit aa687c6

Browse files
committed
Add tests infrastructure
1 parent 6b66051 commit aa687c6

72 files changed

Lines changed: 6566 additions & 67 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ obj
1717
/captures
1818
/local.properties
1919

20+
/tests/*/checks
21+
2022
libc++_shared.so

‎.gitmodules‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
url = https://github.com/TGX-Android/jni-utils
5959
shallow = true
6060
branch = main
61+
[submodule "vkryl/annotation"]
62+
path = vkryl/annotation
63+
url = https://github.com/TGX-Android/X-Annotation
64+
shallow = true
65+
branch = main
6166
[submodule "vkryl/leveldb"]
6267
path = vkryl/leveldb
6368
url = https://github.com/TGX-Android/leveldb

‎README.md‎

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,118 @@ This is the complete source code and the build instructions for the official alt
6767
telegram.api_hash=YOUR_TELEGRAM_API_HASH</pre>
6868
3. [Setup Firebase](https://firebase.google.com/docs/android/setup) and replace `google-services.json` with the one that's suitable for your `app.id`
6969

70+
### Commands
71+
72+
<details>
73+
<summary>Reference of commands for building, testing and maintenance</summary>
74+
75+
`-Pabi` limits native libraries to given ABIs, separated by commas: `arm64`, `arm32`, `x64` or `x86`. Without it, all ABIs are built. `Latest` in task names can be replaced with another flavor, see [flavors](/tests/README.md#flavors).
76+
77+
#### Building
78+
79+
Set up Android SDK, NDK and `local.properties`, on Windows in MSYS2:
80+
```shell
81+
scripts/setup.sh
82+
```
83+
84+
Check that submodules and Git LFS files are fetched:
85+
```shell
86+
./gradlew :app:validateGitSetup
87+
```
88+
89+
Build and install debug build on connected device:
90+
```shell
91+
./gradlew :app:installLatestDebug -Pabi=arm64
92+
```
93+
94+
Build `legacy` flavor for Android 4.x, which requires older NDK:
95+
```shell
96+
./gradlew :app:assembleLegacyDebug -PuseLegacyNdk=true -Pabi=arm32
97+
```
98+
99+
Build libvpx and FFmpeg for all flavors and ABIs, add `-PuseLegacyNdk=true` for `legacy` flavor:
100+
```shell
101+
./gradlew :app:buildLibvpx
102+
./gradlew :app:buildFfmpeg
103+
```
104+
105+
#### Checking
106+
107+
Run unit tests:
108+
```shell
109+
./gradlew :app:testLatestDebugUnitTest
110+
```
111+
112+
Run integration tests and UI tests on emulator:
113+
```shell
114+
./gradlew :app:emulatorLatestDebugAndroidTest -Pabi=arm64
115+
./gradlew :tests:ui:emulatorLatestNonMinifiedReleaseAndroidTest -Pabi=arm64
116+
```
117+
118+
Run one test on device, e.g. on connected one:
119+
```shell
120+
./gradlew :tests:ui:connectedLatestNonMinifiedReleaseAndroidTest -Pabi=arm64 -Pandroid.testInstrumentationRunnerArguments.class=tgx.test.UnauthorizedLaunchTest
121+
```
122+
123+
Find classes added after minSdk of each flavor in declarations and, for Android 4.x, in imports, and calls to them without version checks:
124+
```shell
125+
./gradlew checkMinSdk
126+
./gradlew checkMinSdk -PuseLegacyNdk=true
127+
./gradlew :app:lintLegacyDebug -PuseLegacyNdk=true
128+
```
129+
130+
Describe results of checks before and after a change for pull request, see [tests](/tests/README.md#tests-and-checks):
131+
```shell
132+
./gradlew clearCheckResults
133+
# Run checks without the change
134+
./gradlew saveCheckResults
135+
# Run checks with the change
136+
./gradlew describeCheckResults
137+
```
138+
139+
Check test tools on connected test device after changing them, see [tests](/tests/README.md#running-tests):
140+
```shell
141+
scripts/check-test-tools.sh
142+
```
143+
144+
#### Performance
145+
146+
Run benchmarks on connected device and compare results before and after a change:
147+
```shell
148+
./gradlew :tests:benchmark:connectedLatestBenchmarkReleaseAndroidTest -Pabi=arm64
149+
./gradlew :tests:benchmark:saveBenchmarkResults
150+
# Apply the change and run benchmarks again
151+
./gradlew :tests:benchmark:compareBenchmarkResults -Pmetric=frameDurationCpuMsP90
152+
```
153+
154+
Run microbenchmarks of app code and show implementations side by side:
155+
```shell
156+
./gradlew :tests:microbenchmark:connectedLatestNonMinifiedReleaseAndroidTest -Pabi=arm64 -Pandroid.testInstrumentationRunnerArguments.class=tgx.test.TextBenchmarks
157+
./gradlew :tests:microbenchmark:describeBenchmarkResults
158+
```
159+
160+
Optimize `Text`: save results before a change, then run microbenchmarks after it and compare, see [tests](/tests/README.md#optimizing-code):
161+
```shell
162+
./gradlew -q :tests:microbenchmark:connectedLatestNonMinifiedReleaseAndroidTest :tests:microbenchmark:saveBenchmarkResults -Pabi=arm64 -Pandroid.testInstrumentationRunnerArguments.class=tgx.test.TextSuite
163+
# Change Text and run them again
164+
./gradlew -q :tests:microbenchmark:connectedLatestNonMinifiedReleaseAndroidTest :tests:microbenchmark:compareBenchmarkResults -Pabi=arm64 -Pandroid.testInstrumentationRunnerArguments.class=tgx.test.TextSuite -Pimplementation=Text -Pcontrol=BoringOrStaticLayout
165+
```
166+
167+
List devices and variants for which baseline profiles will be generated, without generating them, as tasks named `<device><Variant>AndroidTest`:
168+
```shell
169+
./gradlew :app:generateBaselineProfile -Pabi=arm64 -m | grep AndroidTest
170+
```
171+
172+
Generate baseline profiles for all variants on emulator and connected device, keeping computer awake and saving log. ABIs must suit both devices: on x86_64 computers emulator is `x64`, unlike phones:
173+
```shell
174+
# macOS with Apple silicon
175+
caffeinate -i ./gradlew :app:generateBaselineProfile -Pabi=arm64 --continue 2>&1 | tee ~/baseline-profile.log
176+
# Ubuntu
177+
systemd-inhibit --what=idle:sleep ./gradlew :app:generateBaselineProfile -Pabi=arm64,x64 --continue 2>&1 | tee ~/baseline-profile.log
178+
```
179+
180+
</details>
181+
70182
## Reproducing public builds
71183

72184
In order to verify that there is no additional source code injected inside official APKs, you must use one of the following versions of **Ubuntu**:
@@ -116,6 +228,6 @@ List of third-party components used in **Telegram X** can be found [here](/docs/
116228

117229
## Contributions
118230

119-
**Telegram X** welcomes contributions. Check out [pull request template](/docs/PULL_REQUEST_TEMPLATE.md) and [guide for contributors](/docs/GUIDE.md) to learn more about Telegram X internals before creating the first pull request.
231+
**Telegram X** welcomes contributions. Check out [pull request template](/docs/PULL_REQUEST_TEMPLATE.md), [code style](/docs/CODE_STYLE.md) and [guide for contributors](/docs/GUIDE.md) to learn more about Telegram X internals before creating the first pull request.
120232

121233
If you are a regular user and experience a problem with Telegram X, the best place to look for solution is [Telegram X chat](https://t.me/tgandroidtests) — a community with over 4 thousand members. Please do not use this repository to ask questions: if you have general issue with Telegram, refer to [FAQ](http://telegram.org/faq) or contact [Telegram Support](https://telegram.org/faq#telegram-support).

‎app/build.gradle.kts‎

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ import org.gradle.kotlin.dsl.support.uppercaseFirstChar
99
import tgx.gradle.*
1010
import tgx.gradle.source.GitInformationSource
1111
import tgx.gradle.task.*
12+
import tgx.test.configureTests
1213
import java.util.*
1314

1415
plugins {
1516
id("java-toolchain-convention")
1617
id(libs.plugins.android.application.get().pluginId)
18+
id(libs.plugins.androidx.baselineprofile.get().pluginId)
1719
id("tgx-config")
1820
id("tgx-module")
1921
}
2022

2123
val config = tgxConfig.config.get()
22-
val generateBaselineProfile = tgxConfig.generateBaselineProfile.get()
2324
val useLegacyPackaging = tgxConfig.useLegacyPackaging.get()
2425
val useLegacyNdk = tgxConfig.useLegacyNdk.get()
2526
val globalAbiFilters = tgxConfig.abiFilters.get()
@@ -386,7 +387,6 @@ android {
386387
buildConfigString("NDK_VERSION", ndkVersion)
387388
buildConfigBool("SHARED_STL", true)
388389
buildConfigBool("WEBP_ENABLED", true)
389-
buildConfigBool("GENERATING_BASELINE_PROFILE", generateBaselineProfile)
390390

391391
// Library versions in BuildConfig.java
392392

@@ -526,9 +526,6 @@ android {
526526
dimension = "SDK"
527527
isDefault = sdkIndex == Sdk.LATEST
528528

529-
if (generateBaselineProfile && !variant.isLatest) {
530-
matchingFallbacks += Sdk.VARIANTS[Sdk.LATEST]!!.flavor
531-
}
532529
Sdk.VARIANTS.forEach { (subSdkIndex, subVariant) ->
533530
buildConfigBool("${subVariant.flavor.uppercase()}_FLAVOR", sdkIndex == subSdkIndex)
534531
}
@@ -620,6 +617,14 @@ android {
620617

621618
var extraProguardFileCount = 0
622619

620+
findExtraFolders(variant).forEach { folderName ->
621+
val proguardFile = project.layout.projectDirectory.file("src/$folderName/proguard-rules.pro").asFile
622+
if (proguardFile.exists()) {
623+
proguardFile(proguardFile)
624+
extraProguardFileCount++
625+
}
626+
}
627+
623628
arrayOf(
624629
"exoplayer",
625630
"common",
@@ -833,6 +838,10 @@ android {
833838
}
834839

835840
variant.buildConfigFields!!.apply {
841+
// Build types added by baseline profile plugin, which only tests use
842+
put("TEST_HOOKS_ENABLED", BuildConfigField(
843+
"boolean", variant.buildType == "nonMinifiedRelease" || variant.buildType == "benchmarkRelease", null
844+
))
836845
put("RECAPTCHA_VERSION", BuildConfigField(
837846
"String", "\"$recaptchaVersion\"", null
838847
))
@@ -907,19 +916,29 @@ android {
907916
}
908917
}
909918

910-
if (generateBaselineProfile) {
911-
apply(plugin = libs.plugins.androidx.baselineprofile.get().pluginId)
912-
913-
extensions.configure<BaselineProfileConsumerExtension> {
914-
mergeIntoMain = true
915-
automaticGenerationDuringBuild = false
916-
saveInSrc = true
917-
warnings.disabledVariants = false
919+
configureTests()
920+
921+
extensions.configure<BaselineProfileConsumerExtension> {
922+
mergeIntoMain = false
923+
automaticGenerationDuringBuild = false
924+
saveInSrc = true
925+
warnings.disabledVariants = false
926+
warnings.variantHasNoBaselineProfileDependency = false
927+
Sdk.VARIANTS.values.filter {
928+
it.usesBaselineProfile
929+
}.forEach { variant ->
930+
variants.create("${variant.flavor}Release").from(project(":tests:baseline-profile"))
918931
}
932+
}
919933

920-
afterEvaluate {
921-
dependencies.add("latestReleaseBaselineProfile", project(":baseline-profile"))
922-
}
934+
// Plugin merges and copies profiles of all release variants, which would leave empty folders in src for variants without profiles
935+
val variantsWithoutBaselineProfile = Sdk.VARIANTS.values.filter {
936+
!it.usesBaselineProfile
937+
}.map { "${it.flavor.uppercaseFirstChar()}Release" }
938+
tasks.named { name ->
939+
variantsWithoutBaselineProfile.any { name == "merge${it}BaselineProfile" || name == "copy${it}BaselineProfileIntoSrc" }
940+
}.configureEach {
941+
enabled = false
923942
}
924943

925944
afterEvaluate {
@@ -991,7 +1010,12 @@ dependencies {
9911010
libs.androidx.exifinterface.latest
9921011
)
9931012
implementation(libs.androidx.biometric)
994-
implementation(libs.androidx.palette)
1013+
flavorImplementation(
1014+
libs.androidx.palette.legacy,
1015+
libs.androidx.palette.legacy,
1016+
libs.androidx.palette.legacy,
1017+
libs.androidx.palette.latest
1018+
)
9951019
implementation(libs.androidx.collection)
9961020
implementation(libs.androidx.interpolator)
9971021
// CameraX: https://developer.android.com/jetpack/androidx/releases/camera
@@ -1031,6 +1055,7 @@ dependencies {
10311055
flavorImplementation(
10321056
libs.google.play.services.maps.legacy,
10331057
libs.google.play.services.maps.lollipop,
1058+
libs.google.play.services.maps.marshmallow,
10341059
libs.google.play.services.maps.latest
10351060
)
10361061
flavorImplementation(
@@ -1144,6 +1169,10 @@ dependencies {
11441169
compileOnly(libs.androidx.room.latest)
11451170
compileOnly(libs.annotations.jsr305)
11461171
compileOnly(libs.annotations.kotlin)
1172+
1173+
// Tests: tests/README.md
1174+
testImplementation(libs.bundles.unit.test)
1175+
androidTestImplementation(project(":tests:core"))
11471176
}
11481177

11491178
if (!config.isExperimentalBuild) {

‎app/src/main/java/org/thunderdog/challegram/component/attach/MediaBottomFilesController.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ private void addApplicationFolders (List<ListItem> items) {
926926
} catch (Throwable t) {
927927
Log.e(t);
928928
}
929-
if (Config.ENABLE_BASELINE_PROFILE_HOOKS || tdlib.isDeveloper()) {
929+
if (Config.ENABLE_TEST_HOOKS || tdlib.isDeveloper()) {
930930
InlineResultCommon snapshot = createItem(context, tdlib, KEY_SNAPSHOT, R.drawable.baseline_lock_24, Lang.getString(R.string.SessionSnapshot), Lang.getString(R.string.SessionSnapshotDesc));
931931
items.add(createItem(snapshot, R.id.btn_folder));
932932
}

‎app/src/main/java/org/thunderdog/challegram/config/Config.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public class Config {
6464
// The only way around for the sender is to transcode video fully even when there's no need at all.
6565
public static final boolean TRANSCODE_ROTATED_VIDEOS_FOR_IOS_CLIENT = true;
6666

67-
public static final boolean ENABLE_BASELINE_PROFILE_HOOKS = BuildConfig.GENERATING_BASELINE_PROFILE;
67+
public static final boolean ENABLE_TEST_HOOKS = BuildConfig.TEST_HOOKS_ENABLED;
6868

6969
public static final boolean ENABLE_DELETE_CALL_HISTORY = BuildConfig.DEBUG;
7070

‎app/src/main/java/org/thunderdog/challegram/navigation/ViewController.java‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3488,6 +3488,9 @@ public void onFocus () {
34883488
onFocusStateChanged();
34893489
notifyFocusChanged(true);
34903490
context.addKeyEventListener(this);
3491+
if (Config.ENABLE_TEST_HOOKS) {
3492+
updateScreenMarker();
3493+
}
34913494
}
34923495

34933496
protected final void trackUserActivity () {
@@ -3622,18 +3625,38 @@ public final void removeDestroyListener (Destroyable delegate) {
36223625
}
36233626

36243627
protected final View newStartupMarker () {
3628+
return newTestMarker(R.id.startup_marker);
3629+
}
3630+
3631+
private View newTestMarker (@IdRes int id) {
36253632
View marker = new View(context) {
36263633
@Override
36273634
public boolean onTouchEvent (MotionEvent event) {
36283635
return false;
36293636
}
36303637
};
3631-
marker.setId(R.id.startup_marker);
3638+
marker.setId(id);
36323639
marker.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
36333640
marker.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL));
36343641
return marker;
36353642
}
36363643

3644+
private void updateScreenMarker () {
3645+
ViewGroup rootView = context.getRootView();
3646+
View marker = rootView.findViewById(R.id.screen_marker);
3647+
if (marker == null) {
3648+
marker = newTestMarker(R.id.screen_marker);
3649+
rootView.addView(marker);
3650+
}
3651+
String screenName;
3652+
try {
3653+
screenName = context.getResources().getResourceEntryName(getId());
3654+
} catch (Throwable t) {
3655+
screenName = getClass().getSimpleName();
3656+
}
3657+
marker.setContentDescription(screenName);
3658+
}
3659+
36373660
@Override
36383661
public final void performDestroy () {
36393662
destroy();

‎app/src/main/java/org/thunderdog/challegram/ui/MainController.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,7 +1324,7 @@ private void syncContacts (Runnable after) {
13241324
}
13251325

13261326
private void addStartupMarker () {
1327-
if (Config.ENABLE_BASELINE_PROFILE_HOOKS) {
1327+
if (Config.ENABLE_TEST_HOOKS) {
13281328
tdlib.awaitConnection(() -> runOnUiThreadOptional(() -> {
13291329
ViewGroup group = (ViewGroup) getWrapUnchecked();
13301330
if (group.findViewById(R.id.startup_marker) == null) {
@@ -1337,7 +1337,7 @@ private void addStartupMarker () {
13371337
private boolean notificationsRequested;
13381338

13391339
private void showAnnoyingAlertsForCompliance (boolean fromAppResume) {
1340-
if (Config.ENABLE_BASELINE_PROFILE_HOOKS) {
1340+
if (Config.ENABLE_TEST_HOOKS) {
13411341
addStartupMarker();
13421342
}
13431343
if (checkSyncAlert()) {

‎app/src/main/java/org/thunderdog/challegram/ui/PhoneController.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1430,7 +1430,7 @@ public void onFocus () {
14301430
if (UI.inTestMode()) {
14311431
makeTestRequest();
14321432
}
1433-
if (Config.ENABLE_BASELINE_PROFILE_HOOKS) {
1433+
if (Config.ENABLE_TEST_HOOKS) {
14341434
tdlib.awaitConnection(() -> runOnUiThreadOptional(this::addStartupMarker));
14351435
}
14361436
break;

‎app/src/main/java/org/thunderdog/challegram/unsorted/Settings.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ private int getSettings () {
574574
}
575575

576576
private boolean checkLogSetting (int flag) {
577-
if (BuildConfig.GENERATING_BASELINE_PROFILE) {
577+
if (BuildConfig.TEST_HOOKS_ENABLED) {
578578
if (flag == FLAG_TDLIB_OTHER_ENABLE_ANDROID_LOG) {
579579
return true;
580580
}
@@ -839,7 +839,7 @@ public File getDirectory () {
839839
private Settings () {
840840
File pmcDir = new File(AppContext.get().getFilesDir(), "pmc");
841841
boolean didNotExist = !pmcDir.exists();
842-
if (Config.ENABLE_BASELINE_PROFILE_HOOKS && didNotExist) {
842+
if (Config.ENABLE_TEST_HOOKS && didNotExist) {
843843
if (SessionSnapshot.restoreSnapshot()) {
844844
didNotExist = false;
845845
}

0 commit comments

Comments
 (0)