Loren M. Lang 2025-02-09 08:07:56 +08:00 zatwierdzone przez GitHub
commit 49ece3a7f6
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
12 zmienionych plików z 1015 dodań i 9 usunięć

188
.github/workflows/android.yml vendored 100644
Wyświetl plik

@ -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'

Wyświetl plik

@ -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)

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -2,8 +2,8 @@ package org.aprsdroid.app;
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;

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -0,0 +1,464 @@
package org.aprsdroid.app;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.test.espresso.action.GeneralLocation;
import androidx.test.espresso.action.GeneralSwipeAction;
import androidx.test.espresso.action.Press;
import androidx.test.espresso.action.Swipe;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import org.aprsdroid.app.testing.DMSLocationAssertion;
import org.aprsdroid.app.testing.SharedPreferencesRule;
import org.aprsdroid.app.testing.SpecificDMSLocationAssertion;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
@RunWith(Enclosed.class)
public class MapModeTest {
private static final String TAG = "APRSdroid-MapModeTest";
private static final Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
private static final ActivityScenarioRule<GoogleMapAct> activityRule =
new ActivityScenarioRule<>(new Intent(appContext, GoogleMapAct.class)
.putExtra("info", R.string.p_source_from_map_save));
@RunWith(AndroidJUnit4.class)
public static class GivenDefaultHomeLocation {
private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() {
@Override
protected void modifyPreferences(SharedPreferences preferences) {
preferences.edit().clear().commit();
}
};
@Rule
public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule);
@Test
public void whenFirstLoaded_thenSaveDisabled() {
onView(withId(R.id.info))
.check(matches(withText("")));
onView(withId(R.id.accept))
.check(matches(not(isEnabled())));
}
@Test
public void whenMapIsDragged_thenPositionAndButtonShown() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.info))
.check(matches(not(withText(""))));
onView(withId(R.id.accept))
.check(matches(isEnabled()));
}
@Test
public void whenMapIsDragged_thenPositionIsValidCoordinates() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.info))
.check(new DMSLocationAssertion());
}
}
@RunWith(AndroidJUnit4.class)
public static class GivenSavedLocationInNEHemisphere {
private static final float expectedLatitude = 37.50123f;
private static final float expectedLongitude = 88.25034f;
private static final float expectedZoom = 4.0f;
private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() {
@Override
protected void modifyPreferences(SharedPreferences preferences) {
preferences.edit()
.putFloat("map_lat", expectedLatitude)
.putFloat("map_lon", expectedLongitude)
.putFloat("map_zoom", expectedZoom)
.commit();
}
};
@Rule
public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule);
@Test
public void whenFirstLoaded_thenSaveDisabled() {
onView(withId(R.id.info))
.check(matches(withText("")));
onView(withId(R.id.accept))
.check(matches(not(isEnabled())));
}
@Test
public void whenMapIsDragged_thenPositionAndButtonShown() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.info))
.check(matches(not(withText(""))));
onView(withId(R.id.accept))
.check(matches(isEnabled()));
}
@Test
@FlakyTest
public void whenMapIsDraggedBackAndForth_thenPositionIsOriginalCoordinates() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER, Press.THUMB));
onView(withId(R.id.info))
.check(new SpecificDMSLocationAssertion(expectedLatitude, expectedLongitude));
}
@Test
@FlakyTest
public void whenMapIsDraggedBackAndForthAndSaved_thenPositionIsSavedCorrectly() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Log.w(TAG, "Sleep was interrupted: " + ex);
}
prefsRule.getPreferences()
.edit()
.remove("map_lat")
.remove("map_lon")
.remove("map_zoom")
.commit();
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER, Press.THUMB));
onView(withId(R.id.accept))
.perform(click());
float actualLatitude = prefsRule.getPreferences().getFloat("map_lat", 0.0f);
float actualLongitude = prefsRule.getPreferences().getFloat("map_lon", 0.0f);
float actualZoom = prefsRule.getPreferences().getFloat("map_zoom", 0.0f);
assertThat("Latitude", (double) actualLatitude, closeTo(expectedLatitude, 5e-2));
assertThat("Longitude", (double) actualLongitude, closeTo(expectedLongitude, 5e-2));
assertThat("Zoom", (double) actualZoom, closeTo(expectedZoom, 1e-7));
}
}
@RunWith(AndroidJUnit4.class)
public static class GivenSavedLocationInSEHemisphere {
private static final float expectedLatitude = -37.50123f;
private static final float expectedLongitude = 88.25034f;
private static final float expectedZoom = 4.5f;
private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() {
@Override
protected void modifyPreferences(SharedPreferences preferences) {
preferences.edit()
.putFloat("map_lat", expectedLatitude)
.putFloat("map_lon", expectedLongitude)
.putFloat("map_zoom", expectedZoom)
.commit();
}
};
@Rule
public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule);
@Test
public void whenFirstLoaded_thenSaveDisabled() {
onView(withId(R.id.info))
.check(matches(withText("")));
onView(withId(R.id.accept))
.check(matches(not(isEnabled())));
}
@Test
public void whenMapIsDragged_thenPositionAndButtonShown() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.info))
.check(matches(not(withText(""))));
onView(withId(R.id.accept))
.check(matches(isEnabled()));
}
@Test
@FlakyTest
public void whenMapIsDraggedBackAndForth_thenPositionIsOriginalCoordinates() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER, Press.THUMB));
onView(withId(R.id.info))
.check(new SpecificDMSLocationAssertion(expectedLatitude, expectedLongitude));
}
@Test
@FlakyTest
public void whenMapIsDraggedBackAndForthAndSaved_thenPositionIsSavedCorrectly() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Log.w(TAG, "Sleep was interrupted: " + ex);
}
prefsRule.getPreferences()
.edit()
.remove("map_lat")
.remove("map_lon")
.remove("map_zoom")
.commit();
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER, Press.THUMB));
onView(withId(R.id.accept))
.perform(click());
float actualLatitude = prefsRule.getPreferences().getFloat("map_lat", 0.0f);
float actualLongitude = prefsRule.getPreferences().getFloat("map_lon", 0.0f);
float actualZoom = prefsRule.getPreferences().getFloat("map_zoom", 0.0f);
assertThat("Latitude", (double) actualLatitude, closeTo(expectedLatitude, 5e-2));
assertThat("Longitude", (double) actualLongitude, closeTo(expectedLongitude, 5e-2));
assertThat("Zoom", (double) actualZoom, closeTo(expectedZoom, 1e-7));
}
}
@RunWith(AndroidJUnit4.class)
public static class GivenSavedLocationInNWHemisphere {
private static final float expectedLatitude = 37.50123f;
private static final float expectedLongitude = -88.25034f;
private static final float expectedZoom = 5.0f;
private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() {
@Override
protected void modifyPreferences(SharedPreferences preferences) {
preferences.edit()
.putFloat("map_lat", expectedLatitude)
.putFloat("map_lon", expectedLongitude)
.putFloat("map_zoom", expectedZoom)
.commit();
}
};
@Rule
public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule);
@Test
public void whenFirstLoaded_thenSaveDisabled() {
onView(withId(R.id.info))
.check(matches(withText("")));
onView(withId(R.id.accept))
.check(matches(not(isEnabled())));
}
@Test
public void whenMapIsDragged_thenPositionAndButtonShown() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.info))
.check(matches(not(withText(""))));
onView(withId(R.id.accept))
.check(matches(isEnabled()));
}
@Test
@FlakyTest
public void whenMapIsDraggedBackAndForth_thenPositionIsOriginalCoordinates() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER, Press.THUMB));
onView(withId(R.id.info))
.check(new SpecificDMSLocationAssertion(expectedLatitude, expectedLongitude));
}
@Test
@FlakyTest
public void whenMapIsDraggedBackAndForthAndSaved_thenPositionIsSavedCorrectly() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Log.w(TAG, "Sleep was interrupted: " + ex);
}
prefsRule.getPreferences()
.edit()
.remove("map_lat")
.remove("map_lon")
.remove("map_zoom")
.commit();
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER, Press.THUMB));
onView(withId(R.id.accept))
.perform(click());
float actualLatitude = prefsRule.getPreferences().getFloat("map_lat", 0.0f);
float actualLongitude = prefsRule.getPreferences().getFloat("map_lon", 0.0f);
float actualZoom = prefsRule.getPreferences().getFloat("map_zoom", 0.0f);
assertThat("Latitude", (double) actualLatitude, closeTo(expectedLatitude, 5e-2));
assertThat("Longitude", (double) actualLongitude, closeTo(expectedLongitude, 5e-2));
assertThat("Zoom", (double) actualZoom, closeTo(expectedZoom, 1e-7));
}
}
@RunWith(AndroidJUnit4.class)
public static class GivenSavedLocationInSWHemisphere {
private static final float expectedLatitude = -37.50123f;
private static final float expectedLongitude = -88.25034f;
private static final float expectedZoom = 5.5f;
private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() {
@Override
protected void modifyPreferences(SharedPreferences preferences) {
preferences.edit()
.putFloat("map_lat", expectedLatitude)
.putFloat("map_lon", expectedLongitude)
.putFloat("map_zoom", expectedZoom)
.commit();
}
};
@Rule
public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule);
@Test
public void whenFirstLoaded_thenSaveDisabled() {
onView(withId(R.id.info))
.check(matches(withText("")));
onView(withId(R.id.accept))
.check(matches(not(isEnabled())));
}
@Test
public void whenMapIsDragged_thenPositionAndButtonShown() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.info))
.check(matches(not(withText(""))));
onView(withId(R.id.accept))
.check(matches(isEnabled()));
}
@Test
@FlakyTest
public void whenMapIsDraggedBackAndForth_thenPositionIsOriginalCoordinates() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER, Press.THUMB));
onView(withId(R.id.info))
.check(new SpecificDMSLocationAssertion(expectedLatitude, expectedLongitude));
}
@Test
@FlakyTest
public void whenMapIsDraggedBackAndForthAndSaved_thenPositionIsSavedCorrectly() {
Assume.assumeThat("Google Play Services requires",
GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext),
equalTo(ConnectionResult.SUCCESS));
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Log.w(TAG, "Sleep was interrupted: " + ex);
}
prefsRule.getPreferences()
.edit()
.remove("map_lat")
.remove("map_lon")
.remove("map_zoom")
.commit();
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER,
GeneralLocation.CENTER_LEFT, Press.THUMB));
onView(withId(R.id.mapview))
.perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
GeneralLocation.CENTER, Press.THUMB));
onView(withId(R.id.accept))
.perform(click());
float actualLatitude = prefsRule.getPreferences().getFloat("map_lat", 0.0f);
float actualLongitude = prefsRule.getPreferences().getFloat("map_lon", 0.0f);
float actualZoom = prefsRule.getPreferences().getFloat("map_zoom", 0.0f);
assertThat("Latitude", (double) actualLatitude, closeTo(expectedLatitude, 5e-2));
assertThat("Longitude", (double) actualLongitude, closeTo(expectedLongitude, 5e-2));
assertThat("Zoom", (double) actualZoom, closeTo(expectedZoom, 1e-7));
}
}
}

Wyświetl plik

@ -0,0 +1,27 @@
package org.aprsdroid.app.testing;
import static org.hamcrest.Matchers.instanceOf;
import android.view.View;
import android.widget.TextView;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.espresso.ViewAssertion;
import org.junit.Assert;
public class DMSLocationAssertion implements ViewAssertion {
protected void checkCoordinates(float latitude, float longitude) {
}
@Override
public void check(View view, NoMatchingViewException noViewFoundException) {
if (view == null)
throw noViewFoundException;
Assert.assertThat(view, instanceOf(TextView.class));
TextView text = (TextView) view;
float latitude = CoordinateMatcher.matchLatitude(text.getText());
float longitude = CoordinateMatcher.matchLongitude(text.getText());
checkCoordinates(latitude, longitude);
}
}

Wyświetl plik

@ -0,0 +1,29 @@
package org.aprsdroid.app.testing;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public abstract class SharedPreferencesRule implements TestRule {
private SharedPreferences preferences;
protected abstract void modifyPreferences(SharedPreferences preferences);
public SharedPreferences getPreferences() {
return preferences;
}
@Override
public Statement apply(Statement base, Description description) {
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
preferences = PreferenceManager.getDefaultSharedPreferences(appContext);
modifyPreferences(preferences);
return base;
}
}

Wyświetl plik

@ -0,0 +1,22 @@
package org.aprsdroid.app.testing;
import static org.hamcrest.Matchers.closeTo;
import org.junit.Assert;
public class SpecificDMSLocationAssertion extends DMSLocationAssertion {
private final float expectedLatitude;
private final float expectedLongitude;
public SpecificDMSLocationAssertion(float myExpectedLatitude, float myExpectedLongitude) {
expectedLatitude = myExpectedLatitude;
expectedLongitude = myExpectedLongitude;
}
@Override
protected void checkCoordinates(float latitude, float longitude) {
super.checkCoordinates(latitude, longitude);
Assert.assertThat("Latitude", (double) latitude, closeTo((double) expectedLatitude, 0.05));
Assert.assertThat("Longitude", (double) longitude, closeTo((double) expectedLongitude, 0.05));
}
}

Wyświetl plik

@ -6,7 +6,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.android.tools.build:gradle:3.5.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -81,7 +81,7 @@ android {
resValue "string", "google_maps_key", mapsApiKey()
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
useLibrary 'org.apache.http.legacy'
compileOptions {
@ -131,10 +131,10 @@ android {
jniLibs.srcDirs = ['libs']
}
androidTest {
java.srcDirs = ['androidTest/java']
java.srcDirs = ['androidTest/java', 'sharedTest/java']
}
test {
java.srcDirs = ['test/java']
java.srcDirs = ['test/java', 'sharedTest/java']
}
}
lintOptions {
@ -158,7 +158,12 @@ dependencies {
implementation 'com.squareup.okio:okio:2.1.0'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.hamcrest:hamcrest-core:1.3'
testImplementation 'org.hamcrest:hamcrest-library:1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
}

Wyświetl plik

@ -0,0 +1,57 @@
package org.aprsdroid.app.testing;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import org.junit.Assert;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CoordinateMatcher {
private static final String NUMBER = "(-?\\d+(?:\\.\\d*)?)";
private static final String dms_latitude_pattern = NUMBER + "°\\s*" + NUMBER + "'\\s*" + NUMBER + "\"\\s*([NS])";
private static final String dms_longitude_pattern = NUMBER + "°\\s*" + NUMBER + "'\\s*" + NUMBER + "\"\\s*([EW])";
private static final Pattern dms_latitude_regex = Pattern.compile(dms_latitude_pattern, Pattern.CASE_INSENSITIVE);
private static final Pattern dms_longitude_regex = Pattern.compile(dms_longitude_pattern, Pattern.CASE_INSENSITIVE);
private static float convertField(CharSequence string, Pattern regex, String name) {
Matcher matcher = regex.matcher(string);
Assert.assertTrue(name + " not found", matcher.find());
float value = 0;
try {
int degrees = Integer.parseInt(Objects.requireNonNull(matcher.group(1)));
Assert.assertThat(name + " degrees", degrees, greaterThanOrEqualTo(0));
value = (float) degrees;
} catch (NumberFormatException ex) {
Assert.fail(name + " degree field not an integer");
}
try {
int minutes = Integer.parseInt(Objects.requireNonNull(matcher.group(2)));
Assert.assertThat(name + " minutes", minutes, greaterThanOrEqualTo(0));
value += (float) minutes / 60.0f;
} catch (NumberFormatException ex) {
Assert.fail(name + " minute field not an integer");
}
try {
float seconds = Float.parseFloat(Objects.requireNonNull(matcher.group(3)));
Assert.assertThat(name + " seconds", seconds, greaterThanOrEqualTo(0.0f));
value += seconds / 3600.0f;
} catch (NumberFormatException ex) {
Assert.fail(name + " seconds field not an number");
}
String direction = Objects.requireNonNull(matcher.group(4));
if (direction.equalsIgnoreCase("S") || direction.equalsIgnoreCase("W")) {
value *= -1.0f;
}
return value;
}
public static float matchLatitude(CharSequence string) {
return convertField(string, dms_latitude_regex, "Latitude");
}
public static float matchLongitude(CharSequence string) {
return convertField(string, dms_longitude_regex, "Longitude");
}
}

Wyświetl plik

@ -0,0 +1,43 @@
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;
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 givenDMSLatitudeInN_whenParsingString_ThenShouldMatchDecimal() {
float value = CoordinateMatcher.matchLatitude(providedNLatitude);
assertThat("Latitude", (double) value, closeTo((double) expectedNLatitude, 1e-7));
}
@Test
public void givenDMSLongitudeInE_whenParsingString_ThenShouldMatchDecimal() {
float value = CoordinateMatcher.matchLongitude(providedELongitude);
assertThat("Longitude", (double) value, closeTo((double) expectedELongitude, 1e-7));
}
@Test
public void givenDMSLatitudeInS_whenParsingString_ThenShouldMatchDecimal() {
float value = CoordinateMatcher.matchLatitude(providedSLatitude);
assertThat("Latitude", (double) value, closeTo((double) expectedSLatitude, 1e-7));
}
@Test
public void givenDMSLongitudeInW_whenParsingString_ThenShouldMatchDecimal() {
float value = CoordinateMatcher.matchLongitude(providedWLongitude);
assertThat("Longitude", (double) value, closeTo((double) expectedWLongitude, 1e-7));
}
}