Skip to content

Commit

Permalink
Introduce instrumented espresso CI tests
Browse files Browse the repository at this point in the history
* all provided locales on all fragments and preferences sections
* csv export
* step count test
  • Loading branch information
morckx committed Feb 3, 2024
1 parent bc46aa1 commit cd35d4d
Show file tree
Hide file tree
Showing 13 changed files with 714 additions and 14 deletions.
79 changes: 79 additions & 0 deletions .github/workflows/android-instrumented-tests-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Instrumented CI Tests

on:
push:
branches:
- '*'
pull_request:
branches:
- master

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
api-level: [17, 19, 34]
steps:
- name: checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Restore Android virtual device
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: pfa-pedometer-${{ runner.os }}-avd-api${{ matrix.api-level }}

- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'zulu'

- name: Set up Android virtual device if not cached
uses: reactivecircus/android-emulator-runner@v2
if: steps.avd-cache.outputs.cache-hit != 'true'
with:
api-level: ${{ matrix.api-level }}
arch: ${{ matrix.api-level < 21 && 'x86' || 'x86_64' }}
target: ${{ matrix.api-level >= 30 && 'google_apis' || 'default' }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
sdcard-path-or-size: 64M
script: echo "Generated AVD snapshot for caching."

- name: Run instrumented tests on Android virtual device
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: ${{ matrix.api-level < 21 && 'x86' || 'x86_64' }}
target: ${{ matrix.api-level >= 30 && 'google_apis' || 'default' }}
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
sdcard-path-or-size: 64M
script: |
adb uninstall org.secuso.privacyfriendlyactivitytracker.test || true
touch emulator.log # create log file
chmod 777 emulator.log # allow writing to log file
adb logcat >> emulator.log & # pipe all logcat messages into log file as a background process
./gradlew connectedAndroidTest --no-build-cache --no-daemon
- name: Upload logs
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.api-level }}-${{ matrix.arch }}-instrumentation-test-results
path: |
emulator.log
./**/build/reports/androidTests/connected/**
44 changes: 44 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'


def pfaFile = rootProject.file('pfa.properties')

def getAvailableLocales() {
def tree = fileTree(dir: 'src/main/res', include: '**/strings.xml')
return tree.collect {
def lang = it.getParentFile().getName() - "values-" - "values"

// We want a name which would be understood by Locale::forLanguageTag, so we should do
// do a simple conversion for an edge case:
// "pt-rBR" -> "pt-BR"
// "zh-rCN" -> "zh-CN"
// and so on
lang = lang.replace("-r", "-")

if (lang.empty) {
lang = "en"
}

lang
}
}

android {
namespace 'org.secuso.privacyfriendlyactivitytracker'

Expand All @@ -28,6 +49,11 @@ android {
versionCode 16
versionName "3.1.0"
multiDexEnabled true

def escapedLocales = getAvailableLocales().collect { "\"" + it + "\"" }
buildConfigField "String[]", "AVAILABLE_LOCALES", String.format("{ %s }", escapedLocales.join(","))

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

applicationVariants.all { variant ->
Expand All @@ -54,8 +80,20 @@ android {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
}

gradle.taskGraph.whenReady { taskGraph ->
project(':backup-api') {
tasks.named('compileDebugAndroidTestKotlin').configure {
enabled = false
}
}
}


repositories {
maven { url "https://jitpack.io" }
}
Expand All @@ -70,6 +108,8 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.github.PhilJay:MPAndroidChart:v3.0.0-beta1'
implementation 'androidx.multidex:multidex:2.0.1' //with androidx libraries
implementation 'androidx.test.ext:junit:1.1.5'
implementation 'androidx.test.espresso:espresso-contrib:3.5.1'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.5.2'
Expand All @@ -81,5 +121,9 @@ dependencies {

implementation 'androidx.sqlite:sqlite-ktx:2.4.0'
implementation 'androidx.sqlite:sqlite-framework:2.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation("com.github.YarikSOffice:lingver:1.3.0")
}

Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
package org.secuso.privacyfriendlyactivitytracker;

import static junit.framework.TestCase.assertEquals;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import android.Manifest;

import androidx.test.espresso.action.ViewActions;
import androidx.test.espresso.assertion.ViewAssertions;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.rule.GrantPermissionRule;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.secuso.privacyfriendlyactivitytracker.tutorial.TutorialActivity;

/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
@RunWith(AndroidJUnit4.class)
public class ApplicationTest {
@Rule
public ActivityScenarioRule<TutorialActivity> activityRule =
new ActivityScenarioRule<>(TutorialActivity.class);

@Rule
public GrantPermissionRule activityRecognitionPermission = (android.os.Build.VERSION.SDK_INT >= 29 ? GrantPermissionRule.grant(Manifest.permission.ACTIVITY_RECOGNITION) : null);
@Rule
public GrantPermissionRule foregroundServicePermission = (android.os.Build.VERSION.SDK_INT >= 34 ? GrantPermissionRule.grant(Manifest.permission.FOREGROUND_SERVICE_HEALTH) : null);
@Rule
public GrantPermissionRule postNotificatuionsPermission = (android.os.Build.VERSION.SDK_INT >= 32 ? GrantPermissionRule.grant(Manifest.permission.POST_NOTIFICATIONS) : null);

@Test
public void instrumentationTest() throws Exception {
assertEquals("org.secuso.privacyfriendlyactivitytracker", InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName());
public void canStartApp() {
onView(withText(R.string.skip)).perform(ViewActions.click());
onView(withText(R.string.day)).perform(ViewActions.click());
onView(withText(R.string.day)).check(ViewAssertions.matches(ViewMatchers.isSelected()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package org.secuso.privacyfriendlyactivitytracker;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.swipeLeft;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import android.Manifest;
import android.os.Build;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.action.ViewActions;
import androidx.test.espresso.assertion.ViewAssertions;
import androidx.test.espresso.contrib.DrawerActions;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import androidx.viewpager.widget.ViewPager;

import com.yariksoffice.lingver.Lingver;

import org.hamcrest.core.Is;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.secuso.privacyfriendlyactivitytracker.activities.MainActivity;

import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;

@LargeTest
@RunWith(Parameterized.class)

public class LocalesTest {
@Parameterized.Parameter(value = 0)
public static Locale locale = Locale.ENGLISH;
@Rule
public ActivityTestRule<MainActivity> activityRule
= new ActivityTestRule<MainActivity>(MainActivity.class) {
@Override
protected void beforeActivityLaunched() {
Lingver.getInstance().setLocale(ApplicationProvider.getApplicationContext(), locale);
super.beforeActivityLaunched();
}
};
@Rule
public GrantPermissionRule activityRecognitionPermission = (android.os.Build.VERSION.SDK_INT >= 29 ? GrantPermissionRule.grant(Manifest.permission.ACTIVITY_RECOGNITION) : null);

@BeforeClass
public static void initAll() {
Lingver.init(ApplicationProvider.getApplicationContext(), locale);
}

@Parameterized.Parameters(name = "locale={0}")
public static CopyOnWriteArrayList<Object[]> initParameters() {
CopyOnWriteArrayList<Object[]> params = new CopyOnWriteArrayList<>();
for (String availableLocale : BuildConfig.AVAILABLE_LOCALES) {
params.add(new Object[]{parseLocale(availableLocale)});
}

return params;
}

private static Locale parseLocale(String str) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return Locale.forLanguageTag(str);
} else {
if (str.contains("-")) {
String[] args = str.split("-");
if (args.length > 2) {
return new Locale(args[0], args[1], args[3]);
} else if (args.length > 1) {
return new Locale(args[0], args[1]);
} else if (args.length == 1) {
return new Locale(args[0]);
}
}

return new Locale(str);
}
}

Locale getCurrentLocale() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return ApplicationProvider.getApplicationContext()
.getResources().getConfiguration().getLocales()
.get(0);
} else {
return ApplicationProvider.getApplicationContext()
.getResources().getConfiguration().locale;
}
}

@Before
public void setUp() {
ViewMatchers.assertThat("Locale is not supported", getCurrentLocale(), Is.is(locale));
}

// works on local emulators > 31, but not in GH workflow
@SdkSuppress(maxSdkVersion = 31)
@Test
public void application_ShouldNotCrash_WithLanguage() {
ViewPagerIdlingResource idlingResource = new ViewPagerIdlingResource(activityRule.getActivity().findViewById(R.id.pager), "ViewPager");
IdlingRegistry.getInstance().register(idlingResource);

onView(withText(R.string.day)).perform(ViewActions.click());
onView(withText(R.string.day)).check(ViewAssertions.matches(ViewMatchers.isSelected()));

onView(withId(R.id.pager)).perform(swipeLeft());
onView(withText(R.string.week)).check(ViewAssertions.matches(ViewMatchers.isSelected()));
onView(withId(R.id.pager)).perform(swipeLeft());
onView(withText(R.string.month)).check(ViewAssertions.matches(ViewMatchers.isSelected()));
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open());
onView(withId(R.id.menu_settings)).perform(click());
onView(withText(R.string.pref_header_general)).perform(click());
onView(isRoot()).perform(ViewActions.pressBack());
onView(withText(R.string.pref_header_notifications)).perform(click());
onView(isRoot()).perform(ViewActions.pressBack());
onView(withText(R.string.pref_header_walking_modes)).perform(click());
}

public static class ViewPagerIdlingResource implements IdlingResource {
private final String resourceName;

private boolean isIdle = true;

private ResourceCallback resourceCallback;

public ViewPagerIdlingResource(ViewPager viewPager, String name) {
viewPager.addOnPageChangeListener(new ViewPagerListener());
resourceName = name;
}

@Override
public String getName() {
return resourceName;
}

@Override
public boolean isIdleNow() {
return isIdle;
}

@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}

private class ViewPagerListener extends ViewPager.SimpleOnPageChangeListener {

@Override
public void onPageScrollStateChanged(int state) {
isIdle = (state == ViewPager.SCROLL_STATE_IDLE);
if (isIdle && resourceCallback != null) {
resourceCallback.onTransitionToIdle();
}
}
}
}
}
Loading

0 comments on commit cd35d4d

Please sign in to comment.