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

Adding UI testing of APRSdroid #316

Draft
wants to merge 34 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
65e134c
Added in GitHub Continuous Integration
penguin359 Jan 3, 2022
df2fdfb
Enable Git submodules on CI
penguin359 Jan 3, 2022
42bda35
Gradle needs all commit history to build
penguin359 Jan 3, 2022
bcd3b7b
Enabled stacktraces of failed builds
penguin359 Jan 3, 2022
d4c027c
Use real Google Maps API key with Github secrets
penguin359 Jan 3, 2022
ac190f1
Build/test release and debug configurations and enable linter
penguin359 Jan 3, 2022
7c0faec
Enable job matrix for testing on several API levels
penguin359 Jan 3, 2022
459df5e
Broke it up into multiple, dependent jobs
penguin359 Jan 3, 2022
f7737f5
Added CI badge to README
penguin359 Jan 3, 2022
98c7b56
Fixes
penguin359 Jan 3, 2022
a87f955
Save test reports on failure and upload APK on success
penguin359 Jan 3, 2022
9819158
Added Android emulator snapshotting to improve start-up performance
penguin359 Jan 3, 2022
61c771a
Exclude build combinations that won't start in emulator
penguin359 Jan 3, 2022
2230a21
API level 30 and above only supported with 64-bit
penguin359 Jan 3, 2022
6951ebc
Only upload test reports on failure
penguin359 Jan 3, 2022
68d42fc
Save logcat logs from failed test runs
penguin359 Jan 30, 2022
e9acefd
Enable SD card needed for profile tests
penguin359 Jan 30, 2022
b85d281
Make the README CI badge only show the status of pushes to master
penguin359 Feb 1, 2022
f6327e8
Bumped several GitHub Actions to newer releases
penguin359 Aug 4, 2024
d709423
Fixed exclude matcher for default target on API 31
penguin359 Aug 6, 2024
b554f97
Attempt to bump Java version for Emulator
penguin359 Aug 4, 2024
57a544d
Switched to Linux for better emulator performance
penguin359 Aug 5, 2024
355aefc
Switched to AndroidX for test support library
penguin359 Jan 4, 2022
bf9ebfd
Implemented some basic tests with the first-run dialog
penguin359 Jan 4, 2022
7a50a74
Initial testing of the Google Map manual location setting
penguin359 Jan 4, 2022
0f64bd3
Refactored code and added testing for specific map coordinates
penguin359 Jan 4, 2022
1df09f4
Check moving in all 4 hemispheres
penguin359 Jan 4, 2022
b491e99
Refactored code to support unit testing of coordinates as well as ins…
penguin359 Jan 5, 2022
4a699d2
Hide software keyboard to avoid security exception on older Android b…
penguin359 Jan 5, 2022
05d7112
Use the full Google APIs with Playstore images on API levels 24+
penguin359 Jan 5, 2022
12bf4de
Verify that the correct position is saved when apply pressed
penguin359 Jan 5, 2022
399bf66
Added code for directly testing the back-end formatting of coordinates
penguin359 Jan 5, 2022
0213cff
Added assumption to all maps tests that expect Google Play Services t…
penguin359 Jan 17, 2022
936f588
Mark select map tests as flaky
penguin359 Jan 18, 2022
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
188 changes: 188 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: Android CI

on:
push:
branches: [ '**' ]
pull_request:
branches: [ '**' ]
workflow_dispatch:

jobs:
compile:
runs-on: ubuntu-latest
name: "Compile all sources"

steps:
- name: Checkout project
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive

- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'adopt'
cache: gradle

- name: Load build outputs
uses: actions/cache@v4
with:
path: build
key: build-${{ github.sha }}

- name: Create properties file with empty API key
run: echo mapsApiKey="\"${{ secrets.mapsApiKey }}\"" >> local.properties

- name: Build App
run: ./gradlew assemble --stacktrace

- name: Build unit tests
run: ./gradlew assembleDebugUnitTest assembleReleaseUnitTest --stacktrace

- name: Build instrumentation tests
run: ./gradlew assembleAndroidTest --stacktrace

unit-test:
name: "Run all unit tests"
needs: compile
runs-on: ubuntu-latest

steps:
- name: Checkout project
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive

- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'adopt'
cache: gradle

- name: Load build outputs
uses: actions/cache@v4
with:
path: build
key: build-${{ github.sha }}

- name: Run Unit Tests
run: ./gradlew test --stacktrace

- name: Run Linter
run: ./gradlew lint --stacktrace
continue-on-error: true

- name: Upload reports
uses: actions/upload-artifact@v4
with:
name: Unit Test Reports
path: build/reports
if: failure()

instrumentation:
name: "Testing on API ${{ matrix.api-level }} for ${{ matrix.target }}"
needs: compile
# macOS provided hardware-accelerated emulator
# but now so does Linux
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
api-level: [ 15, 21, 24, 31 ]
target: [ default, google_apis, google_apis_playstore ]
exclude:
- api-level: 15
target: google_apis_playstore
- api-level: 21
target: google_apis_playstore
- api-level: 24
target: google_apis
- api-level: 31
target: google_apis

steps:
- name: Checkout project
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive

- name: Enable KVM
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: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'adopt'
cache: gradle

- name: Load build outputs
uses: actions/cache@v4
with:
path: build
key: build-${{ github.sha }}

- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}-${{ matrix.target }}-sd

- name: Set up JRE 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
java-package: 'jre'
#cache: gradle

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

- name: Run Instrumented Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: ${{ matrix.api-level >= 30 && 'x86_64' || 'x86' }}
profile: Nexus 6
force-avd-creation: false
sdcard-path-or-size: '64M'
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: adb logcat -c && adb logcat -f /sdcard/logcat.txt & JAVA_HOME="${JAVA_HOME_11_X64}" ./gradlew connectedCheck --stacktrace || ( adb pull /sdcard/logcat.txt build/reports/; exit 1 )

- name: Upload reports
uses: actions/upload-artifact@v4
with:
name: Instrument Test Reports API ${{ matrix.api-level }} ${{ matrix.target }}
path: build/reports
if: failure()

- name: Save successful debug APK
uses: actions/upload-artifact@v4
with:
name: Debug APK
path: build/outputs/apk/debug/aprsdroid-debug.apk
if: matrix.api-level == 31 && matrix.target == 'google_apis'
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ messages.

APRSdroid is Open Source Software written in Scala and licensed under the GPLv2.

master: [![Android CI](../../actions/workflows/android.yml/badge.svg?branch=master&event=push)](../../actions/workflows/android.yml)

Quick links:

- [Google Play](https://play.google.com/store/apps/details?id=org.aprsdroid.app)
Expand Down
57 changes: 57 additions & 0 deletions androidTest/java/org/aprsdroid/app/CoordinateTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.aprsdroid.app;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.closeTo;

import org.aprsdroid.app.testing.CoordinateMatcher;
import org.junit.Test;

import scala.Tuple2;

public class CoordinateTest {
// Reference data generated from https://www.pgc.umn.edu/apps/convert/
private static final String providedNLatitude = "77° 15' 30\" N";
private static final float expectedNLatitude = 77.258333f;
private static final String providedELongitude = "164° 45' 15\" E";
private static final float expectedELongitude = 164.754167f;
private static final String providedSLatitude = "45° 30' 45\" S";
private static final float expectedSLatitude = -45.5125f;
private static final String providedWLongitude = "97° 20' 40\" W";
private static final float expectedWLongitude = -97.344444f;

@Test
public void givenLocationInNEHemisphere_whenFormattedAsDMSString_thenParseBackIntoDecimalValue() {
Tuple2<String, String> actual = AprsPacket$.MODULE$.formatCoordinates(expectedNLatitude, expectedELongitude);
float floatLatitude = CoordinateMatcher.matchLatitude(actual._1);
float floatLongitude = CoordinateMatcher.matchLongitude(actual._2);
assertThat("Latitude", (double) floatLatitude, closeTo((double) expectedNLatitude, 1e-7));
assertThat("Longitude", (double) floatLongitude, closeTo((double) expectedELongitude, 1e-7));
}

@Test
public void givenLocationInNWHemisphere_whenFormattedAsDMSString_thenParseBackIntoDecimalValue() {
Tuple2<String, String> actual = AprsPacket$.MODULE$.formatCoordinates(expectedNLatitude, expectedWLongitude);
float floatLatitude = CoordinateMatcher.matchLatitude(actual._1);
float floatLongitude = CoordinateMatcher.matchLongitude(actual._2);
assertThat("Latitude", (double) floatLatitude, closeTo((double) expectedNLatitude, 1e-7));
assertThat("Longitude", (double) floatLongitude, closeTo((double) expectedWLongitude, 1e-7));
}

@Test
public void givenLocationInSEHemisphere_whenFormattedAsDMSString_thenParseBackIntoDecimalValue() {
Tuple2<String, String> actual = AprsPacket$.MODULE$.formatCoordinates(expectedSLatitude, expectedELongitude);
float floatLatitude = CoordinateMatcher.matchLatitude(actual._1);
float floatLongitude = CoordinateMatcher.matchLongitude(actual._2);
assertThat("Latitude", (double) floatLatitude, closeTo((double) expectedSLatitude, 1e-7));
assertThat("Longitude", (double) floatLongitude, closeTo((double) expectedELongitude, 1e-7));
}

@Test
public void givenLocationInSWHemisphere_whenFormattedAsDMSString_thenParseBackIntoDecimalValue() {
Tuple2<String, String> actual = AprsPacket$.MODULE$.formatCoordinates(expectedSLatitude, expectedWLongitude);
float floatLatitude = CoordinateMatcher.matchLatitude(actual._1);
float floatLongitude = CoordinateMatcher.matchLongitude(actual._2);
assertThat("Latitude", (double) floatLatitude, closeTo((double) expectedSLatitude, 1e-7));
assertThat("Longitude", (double) floatLongitude, closeTo((double) expectedWLongitude, 1e-7));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import android.content.Context;

import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down
112 changes: 112 additions & 0 deletions androidTest/java/org/aprsdroid/app/FirstRunDialog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package org.aprsdroid.app;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withHint;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.core.AllOf.allOf;
import static org.hamcrest.core.StringContains.containsString;

import android.content.SharedPreferences;

import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.aprsdroid.app.testing.SharedPreferencesRule;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class FirstRunDialog {
private final String pref_callsign = "callsign";
private final String pref_passcode = "passcode";
public ActivityScenarioRule activityRule = new ActivityScenarioRule<>(LogActivity.class);
public SharedPreferencesRule prefsRule = new SharedPreferencesRule() {
@Override
protected void modifyPreferences(SharedPreferences preferences) {
preferences.edit().clear().commit();
}
};
@Rule
public RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule);

@Test
public void givenAFirstTimeRun_whenProvidedABadPasscode_ThenDialogStaysOpen() {
onView(isRoot())
.inRoot(isDialog())
.check(matches(allOf(
hasDescendant(withText(containsString("Welcome to APRSdroid"))),
hasDescendant(withId(R.id.callsign)),
hasDescendant(withId(R.id.passcode)))));
onView(withId(R.id.callsign))
.check(matches(withHint(containsString("Callsign"))))
.perform(typeText("XA1AAA"), closeSoftKeyboard());
onView(withId(R.id.passcode))
.check(matches(withHint(containsString("Passcode"))))
.perform(typeText("12345"), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click()); // OK Button
onView(isRoot())
.inRoot(isDialog())
.check(matches(allOf(
hasDescendant(withText(containsString("Welcome to APRSdroid"))),
hasDescendant(withId(R.id.callsign)),
hasDescendant(withId(R.id.passcode)))));
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
}
Assert.assertTrue(true);
}

@Test
public void givenAFirstTimeRun_whenProvidedAGoodPasscode_ThenDialogCloses() {
onView(isRoot())
.inRoot(isDialog())
.check(matches(allOf(
hasDescendant(withText(containsString("Welcome to APRSdroid"))),
hasDescendant(withId(R.id.callsign)),
hasDescendant(withId(R.id.passcode)))));
onView(withId(R.id.callsign))
.check(matches(withHint(containsString("Callsign"))))
.perform(typeText("XA1AAA"), closeSoftKeyboard());
onView(withId(R.id.passcode))
.check(matches(withHint(containsString("Passcode"))))
.perform(typeText("23459"), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click()); // OK Button
onView(allOf(
isRoot(),
hasDescendant(withText(containsString("Welcome to APRSdroid"))),
hasDescendant(withId(R.id.callsign)),
hasDescendant(withId(R.id.passcode))))
.check(doesNotExist());
}

@Test
public void givenAFirstTimeRun_whenProvidedAGoodPasscode_ThenPrefsSaved() {
String expected_callsign = "XA1AAA";
String expected_passcode = "23459";
SharedPreferences prefs = prefsRule.getPreferences();
Assert.assertNull("Callsign", prefs.getString(pref_callsign, null));
Assert.assertNull("Passcode", prefs.getString(pref_passcode, null));
onView(withId(R.id.callsign))
.check(matches(withHint(containsString("Callsign"))))
.perform(typeText(expected_callsign), closeSoftKeyboard());
onView(withId(R.id.passcode))
.check(matches(withHint(containsString("Passcode"))))
.perform(typeText(expected_passcode), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click()); // OK Button
Assert.assertEquals("Callsign", expected_callsign, prefs.getString(pref_callsign, null));
Assert.assertEquals("Passcode", expected_passcode, prefs.getString(pref_passcode, null));
}
}
Loading