Skip to content

Commit bee3eff

Browse files
committed
feat(ci): added managed devices, fixed some tests
1 parent 058ad88 commit bee3eff

28 files changed

Lines changed: 304 additions & 234 deletions

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ small-espresso:
4444
single-espresso:
4545
{{gradlec}} clean createGmsDebugCoverageReport -Pandroid.testInstrumentationRunnerArguments.annotation=org.owntracks.android.testutils.JustThisTestPlease
4646

47+
[group('testing')]
48+
managed-device-test:
49+
{{gradlec}} app:phoneAtdGmsDebugAndroidTest
50+
4751
[group('formatting')]
4852
ktfmt:
4953
{{gradlec}} ktfmtFormat

project/app/build.gradle.kts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,24 @@ android {
157157
animationsDisabled = true
158158
unitTests { isIncludeAndroidResources = true }
159159
managedDevices {
160-
localDevices {}
161-
groups {}
160+
val pixel2api27 =
161+
localDevices.create("pixel2api27") {
162+
device = "Pixel 2"
163+
apiLevel = 27
164+
systemImageSource = "aosp"
165+
}
166+
val pixel8api34 =
167+
localDevices.create("pixel8api34") {
168+
device = "Pixel 8"
169+
apiLevel = 34
170+
systemImageSource = "google-atd"
171+
}
172+
groups {
173+
create("phoneAtd") {
174+
targetDevices.add(pixel2api27)
175+
targetDevices.add(pixel8api34)
176+
}
177+
}
162178
}
163179
}
164180

@@ -212,6 +228,8 @@ tasks.withType<Test> {
212228

213229
tasks.withType<JavaCompile>().configureEach { options.isFork = true }
214230

231+
tasks.matching { it.name.endsWith("AndroidTest") }.configureEach { outputs.upToDateWhen { false } }
232+
215233
dependencies {
216234
implementation(libs.bundles.kotlin)
217235
implementation(libs.bundles.androidx)

project/app/src/androidTest/kotlin/org/owntracks/android/testutils/BottomSheetSetStateAction.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import org.hamcrest.Matchers
1414
* @property desiredState
1515
* @constructor Create empty Bottom sheet set state action
1616
*/
17-
class BottomSheetSetStateAction(@BottomSheetBehavior.State private val desiredState: Int) :
17+
class BottomSheetSetStateAction(@param:BottomSheetBehavior.State private val desiredState: Int) :
1818
ViewAction {
1919

2020
override fun getConstraints(): Matcher<View> {

project/app/src/androidTest/kotlin/org/owntracks/android/ui/WelcomeActivityTests.kt

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -162,46 +162,6 @@ class WelcomeActivityTests : TestWithAnActivity<WelcomeActivity>() {
162162
assertDisplayed(R.id.btn_done)
163163
}
164164

165-
@Test
166-
@SdkSuppress(minSdkVersion = 24, maxSdkVersion = 33)
167-
fun welcome_activity_displays_correct_fragments() {
168-
// Intro fragment
169-
assertDisplayed(R.string.welcome_heading)
170-
R.id.btn_next.run {
171-
assertDisplayed(this)
172-
clickOn(this)
173-
}
174-
175-
// Connection setup fragment
176-
assertDisplayed(R.string.welcome_connection_setup_title)
177-
178-
assertDisplayed(R.string.welcome_connection_setup_description)
179-
R.id.btn_next.run {
180-
assertDisplayed(this)
181-
clickOn(this)
182-
}
183-
184-
// Location permissions fragment
185-
assertDisplayed(R.string.welcome_location_permission_description)
186-
doIfViewNotVisible(R.id.btn_next) {
187-
R.id.ui_fragment_welcome_location_permissions_request.run {
188-
assertDisplayed(this)
189-
clickOn(this)
190-
}
191-
allowPermissionsIfNeeded(Manifest.permission.ACCESS_FINE_LOCATION)
192-
}
193-
R.id.btn_next.run {
194-
assertDisplayed(this)
195-
clickOn(this)
196-
}
197-
198-
// Done fragment
199-
assertDisplayed(R.string.done_heading)
200-
assertDisplayed(R.string.enjoy_description)
201-
assertDisplayed(R.string.welcome_finish_open_preferences_button_label)
202-
assertDisplayed(R.id.btn_done)
203-
}
204-
205165
@Test
206166
fun welcome_activity_can_be_swiped_back_to_start() {
207167
clickOn(R.id.btn_next)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.owntracks.android.ui
2+
3+
import android.Manifest
4+
import androidx.test.filters.MediumTest
5+
import androidx.test.filters.SdkSuppress
6+
import androidx.test.platform.app.InstrumentationRegistry
7+
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
8+
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
9+
import com.adevinta.android.barista.interaction.PermissionGranter.allowPermissionsIfNeeded
10+
import com.google.android.gms.common.ConnectionResult
11+
import com.google.android.gms.common.GoogleApiAvailability
12+
import dagger.hilt.android.testing.HiltAndroidTest
13+
import org.junit.Assume
14+
import org.junit.Test
15+
import org.owntracks.android.R
16+
import org.owntracks.android.testutils.TestWithAnActivity
17+
import org.owntracks.android.testutils.doIfViewNotVisible
18+
import org.owntracks.android.ui.welcome.WelcomeActivity
19+
20+
@MediumTest
21+
@HiltAndroidTest
22+
class WelcomeActivityGmsTests : TestWithAnActivity<WelcomeActivity>() {
23+
24+
@Test
25+
@SdkSuppress(minSdkVersion = 24, maxSdkVersion = 33)
26+
fun welcome_activity_displays_correct_fragments() {
27+
// On AOSP images without GMS, PlayFragment blocks navigation and the Done fragment is
28+
// unreachable. Skip rather than fail on such devices.
29+
Assume.assumeTrue(
30+
"Google Play Services not available",
31+
GoogleApiAvailability.getInstance()
32+
.isGooglePlayServicesAvailable(
33+
InstrumentationRegistry.getInstrumentation().targetContext) ==
34+
ConnectionResult.SUCCESS)
35+
36+
// Intro fragment
37+
assertDisplayed(R.string.welcome_heading)
38+
R.id.btn_next.run {
39+
assertDisplayed(this)
40+
clickOn(this)
41+
}
42+
43+
// Connection setup fragment
44+
assertDisplayed(R.string.welcome_connection_setup_title)
45+
46+
assertDisplayed(R.string.welcome_connection_setup_description)
47+
R.id.btn_next.run {
48+
assertDisplayed(this)
49+
clickOn(this)
50+
}
51+
52+
// Location permissions fragment
53+
assertDisplayed(R.string.welcome_location_permission_description)
54+
doIfViewNotVisible(R.id.btn_next) {
55+
R.id.ui_fragment_welcome_location_permissions_request.run {
56+
assertDisplayed(this)
57+
clickOn(this)
58+
}
59+
allowPermissionsIfNeeded(Manifest.permission.ACCESS_FINE_LOCATION)
60+
}
61+
R.id.btn_next.run {
62+
assertDisplayed(this)
63+
clickOn(this)
64+
}
65+
66+
// Done fragment
67+
assertDisplayed(R.string.done_heading)
68+
assertDisplayed(R.string.enjoy_description)
69+
assertDisplayed(R.string.welcome_finish_open_preferences_button_label)
70+
assertDisplayed(R.id.btn_done)
71+
}
72+
}

project/app/src/main/java/org/owntracks/android/data/waypoints/RoomWaypointsRepo.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ import timber.log.Timber
3636
class RoomWaypointsRepo
3737
@Inject
3838
constructor(
39-
@ApplicationContext private val applicationContext: Context,
40-
@CoroutineScopes.IoDispatcher private val ioDispatcher: CoroutineDispatcher,
41-
@ApplicationScope private val scope: CoroutineScope,
42-
@Named("waypointsMigrationIdlingResource")
39+
@param:ApplicationContext private val applicationContext: Context,
40+
@param:CoroutineScopes.IoDispatcher private val ioDispatcher: CoroutineDispatcher,
41+
@param:ApplicationScope private val scope: CoroutineScope,
42+
@param:Named("waypointsMigrationIdlingResource")
4343
private val migrationIdlingResource: SimpleIdlingResource
4444
) : WaypointsRepo(applicationContext, migrationIdlingResource) {
4545
@Dao

project/app/src/main/java/org/owntracks/android/net/http/HttpMessageProcessorEndpoint.kt

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ import org.owntracks.android.support.interfaces.ConfigurationIncompleteException
3838
import timber.log.Timber
3939

4040
class HttpMessageProcessorEndpoint(
41-
messageProcessor: MessageProcessor,
42-
private val parser: Parser,
43-
private val preferences: Preferences,
44-
private val applicationContext: Context,
45-
private val endpointStateRepo: EndpointStateRepo,
46-
private val caKeyStore: KeyStore,
47-
@ApplicationScope private val scope: CoroutineScope,
48-
@CoroutineScopes.IoDispatcher private val ioDispatcher: CoroutineDispatcher
41+
messageProcessor: MessageProcessor,
42+
private val parser: Parser,
43+
private val preferences: Preferences,
44+
private val applicationContext: Context,
45+
private val endpointStateRepo: EndpointStateRepo,
46+
private val caKeyStore: KeyStore,
47+
@param:ApplicationScope private val scope: CoroutineScope,
48+
@param:CoroutineScopes.IoDispatcher private val ioDispatcher: CoroutineDispatcher
4949
) : MessageProcessorEndpoint(messageProcessor), Preferences.OnPreferenceChangeListener {
5050
override val modeId: ConnectionMode = ConnectionMode.HTTP
5151
internal var httpClientAndConfiguration: HttpClientAndConfiguration? = null
@@ -132,44 +132,35 @@ class HttpMessageProcessorEndpoint(
132132
messageProcessor.onMessageDeliveryFailed(message)
133133
Result.failure(OutgoingMessageSendingException(httpException))
134134
} else {
135-
if (response.body != null) {
136-
try {
137-
val responseString = response.body!!.string()
138-
Timber.d("HTTP response body: ${responseString.take(1000)}")
139-
val responseStream = ByteArrayInputStream(responseString.toByteArray())
140-
val result = parser.fromJson(responseStream)
141-
// TODO apply i18n here
142-
scope.launch {
143-
endpointStateRepo.setState(
144-
EndpointState.IDLE.withMessage(
145-
String.format(
146-
Locale.ROOT,
147-
"Response %d, (%d msgs received)",
148-
response.code,
149-
result.size,
150-
),
151-
),
152-
)
153-
result.forEach { onMessageReceived(it) }
154-
}
155-
return Result.success(Unit)
156-
} catch (e: IOException) {
157-
Timber.w(e, "HTTP response body could not be parsed, ignoring")
135+
try {
136+
val responseString = response.body.string()
137+
Timber.d("HTTP response body: ${responseString.take(1000)}")
138+
val responseStream = ByteArrayInputStream(responseString.toByteArray())
139+
val result = parser.fromJson(responseStream)
140+
// TODO apply i18n here
141+
scope.launch {
158142
endpointStateRepo.setState(
159143
EndpointState.IDLE.withMessage(
160144
String.format(
161-
Locale.ROOT, "Response %d (response not parseable)", response.code),
145+
Locale.ROOT,
146+
"Response %d, (%d msgs received)",
147+
response.code,
148+
result.size,
149+
),
162150
),
163151
)
164-
return Result.success(Unit)
152+
result.forEach { onMessageReceived(it) }
165153
}
166-
} else {
154+
return Result.success(Unit)
155+
} catch (e: IOException) {
156+
Timber.w(e, "HTTP response body could not be parsed, ignoring")
167157
endpointStateRepo.setState(
168158
EndpointState.IDLE.withMessage(
169-
String.format(Locale.ROOT, "Response %d", response.code),
159+
String.format(
160+
Locale.ROOT, "Response %d (response not parseable)", response.code),
170161
),
171162
)
172-
Result.success(Unit)
163+
return Result.success(Unit)
173164
}
174165
}
175166
}

project/app/src/main/java/org/owntracks/android/net/mqtt/MQTTMessageProcessorEndpoint.kt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,16 @@ import org.owntracks.android.test.SimpleIdlingResource
6464
import timber.log.Timber
6565

6666
class MQTTMessageProcessorEndpoint(
67-
messageProcessor: MessageProcessor,
68-
private val endpointStateRepo: EndpointStateRepo,
69-
private val scheduler: Scheduler,
70-
private val preferences: Preferences,
71-
private val parser: Parser,
72-
private val caKeyStore: KeyStore,
73-
@ApplicationScope private val scope: CoroutineScope,
74-
@CoroutineScopes.IoDispatcher private val ioDispatcher: CoroutineDispatcher,
75-
@ApplicationContext private val applicationContext: Context,
76-
private val mqttConnectionIdlingResource: SimpleIdlingResource
67+
messageProcessor: MessageProcessor,
68+
private val endpointStateRepo: EndpointStateRepo,
69+
private val scheduler: Scheduler,
70+
private val preferences: Preferences,
71+
private val parser: Parser,
72+
private val caKeyStore: KeyStore,
73+
@param:ApplicationScope private val scope: CoroutineScope,
74+
@param:CoroutineScopes.IoDispatcher private val ioDispatcher: CoroutineDispatcher,
75+
@param:ApplicationContext private val applicationContext: Context,
76+
private val mqttConnectionIdlingResource: SimpleIdlingResource
7777
) :
7878
MessageProcessorEndpoint(messageProcessor),
7979
StatefulServiceMessageProcessor,
@@ -167,7 +167,7 @@ class MQTTMessageProcessorEndpoint(
167167
try {
168168
pingAlarmReceiver?.run(applicationContext::unregisterReceiver)
169169
Timber.d("Unregistered ping alarm receiver")
170-
} catch (e: IllegalArgumentException) {
170+
} catch (_: IllegalArgumentException) {
171171
Timber.d("Ping alarm receiver already unregistered")
172172
}
173173
}
@@ -316,9 +316,9 @@ class MQTTMessageProcessorEndpoint(
316316
this.qos = message.qos
317317
}
318318
.also { Timber.d("Parsed message: $it") })
319-
} catch (e: Parser.EncryptionException) {
319+
} catch (_: Parser.EncryptionException) {
320320
Timber.e("Unable to decrypt received message ${message.id} on $topic")
321-
} catch (e: SerializationException) {
321+
} catch (_: SerializationException) {
322322
Timber.w("Malformed JSON message received ${message.id} on $topic")
323323
}
324324
}

project/app/src/main/java/org/owntracks/android/preferences/Preferences.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ import timber.log.Timber
3434
class Preferences
3535
@Inject
3636
constructor(
37-
private val preferencesStore: PreferencesStore,
38-
@Named("importConfigurationIdlingResource")
37+
private val preferencesStore: PreferencesStore,
38+
@param:Named("importConfigurationIdlingResource")
3939
private val importConfigurationIdlingResource: SimpleIdlingResource
4040
) {
4141
val allConfigKeys =
@@ -111,7 +111,7 @@ constructor(
111111
// preference is
112112
// actually an enum
113113
importPreference(it, configValue)
114-
} catch (e: java.lang.IllegalArgumentException) {
114+
} catch (_: java.lang.IllegalArgumentException) {
115115
Timber.w(
116116
"Trying to import wrong type of preference for ${it.name}. " +
117117
"Expected ${it.getter.returnType} but given ${configValue.javaClass}. Ignoring.")
@@ -129,7 +129,7 @@ constructor(
129129
Timber.d("Importing configuration key $key -> $configValue")
130130
try {
131131
importPreference(property, configValue)
132-
} catch (e: java.lang.IllegalArgumentException) {
132+
} catch (_: java.lang.IllegalArgumentException) {
133133
Timber.w(
134134
"Trying to import wrong type of preference for $key. " +
135135
"Expected ${property.getter.returnType} but given ${configValue.javaClass}. Ignoring.")

0 commit comments

Comments
 (0)