Skip to content

Commit fbdb4e5

Browse files
tjleingThomas Leing
andauthored
feat(liveness): integration tests (#118)
Co-authored-by: Thomas Leing <[email protected]>
1 parent 797ab82 commit fbdb4e5

File tree

9 files changed

+349
-11
lines changed

9 files changed

+349
-11
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Integration tests
2+
run-name: Integration tests for ${{ github.ref }}
3+
4+
on:
5+
pull_request:
6+
branches:
7+
- 'main'
8+
merge_group:
9+
types: [checks_requested]
10+
11+
env:
12+
AWS_REGION: "us-east-1"
13+
14+
jobs:
15+
ci-integration-tests:
16+
permissions:
17+
id-token: write # This is required for requesting the JWT with configure-aws-credentials
18+
contents: read # This is required for actions/checkout
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Checkout Source Code
22+
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
23+
- name: Configure AWS Credentials
24+
uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2
25+
with:
26+
role-to-assume: ${{ vars.AMPLIFY_UI_ANDROID_CI_TESTS_ROLE }}
27+
aws-region: ${{ env.AWS_REGION }}
28+
- name: Run Integration Tests
29+
uses: aws-actions/aws-codebuild-run-build@f202c327329cbbebd13f986f74af162a8539b5fd # v1
30+
with:
31+
project-name: Amplify-UI-Android-Integration-Test

authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class AuthenticatorViewModelTest {
5757

5858
@Test
5959
fun `start only executes once`() = runTest {
60-
coEvery {authProvider.fetchAuthSession()}.returns(AmplifyResult.Error(mockAuthException()))
60+
coEvery { authProvider.fetchAuthSession() }.returns(AmplifyResult.Error(mockAuthException()))
6161
viewModel.start(mockAuthConfiguration())
6262
viewModel.start(mockAuthConfiguration())
6363
advanceUntilIdle()

build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,11 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
113113
// Needed when running integration tests. The oauth2 library uses relies on two
114114
// dependencies (Apache's httpcore and httpclient), both of which include
115115
// META-INF/DEPENDENCIES. Tried a couple other options to no avail.
116+
// More collisions occurred using JUnit, so also adding LICENSE-*.md files.
116117
packaging {
117118
resources.excludes.add("META-INF/DEPENDENCIES")
119+
resources.excludes.add("META-INF/LICENSE.md")
120+
resources.excludes.add("META-INF/LICENSE-notice.md")
118121
}
119122

120123
buildFeatures {

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,5 @@ dependencies {
7979
kover(project(":authenticator"))
8080
kover(project(":liveness"))
8181
}
82+
83+
apply(from = rootProject.file("configuration/instrumentation-tests.gradle"))
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Need to create backends for s3, pinpoint, predictions, core
3+
*/
4+
def module_backends = [
5+
'liveness' : 'LivenessIntegTests',
6+
]
7+
8+
def canaryTests = [
9+
// 'liveness' : [''],
10+
]
11+
12+
subprojects {
13+
afterEvaluate { project ->
14+
if (module_backends.containsKey(project.name)) {
15+
task runTestsInDeviceFarm {
16+
doLast {
17+
exec {
18+
commandLine("$rootDir.path/scripts/run_test_in_devicefarm.sh")
19+
args([project.name])
20+
}
21+
}
22+
}
23+
// task runNightlyTestsInDeviceFarmPool {
24+
// dependsOn(assembleAndroidTest)
25+
// doLast {
26+
// exec {
27+
// commandLine("$rootDir.path/scripts/run_nightly_tests_in_devicefarm_pool.sh")
28+
// args([project.name])
29+
// }
30+
// }
31+
// }
32+
// task runCanaryInDeviceFarm {
33+
// dependsOn(assembleAndroidTest)
34+
// doLast {
35+
// for (canary in canaryTests[project.name]) {
36+
// exec {
37+
// commandLine("$rootDir.path/scripts/run_canary_in_devicefarm.sh")
38+
// args(project.name, canary)
39+
// }
40+
// }
41+
// }
42+
// }
43+
}
44+
}
45+
}

gradle/libs.versions.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,8 @@ test-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ve
5454
test-espresso = "androidx.test.espresso:espresso-core:3.5.1"
5555
test-junit = "junit:junit:4.13.2"
5656
test-kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
57-
# downgrading mockk due to https://github.com/mockk/mockk/issues/1035
58-
test-mockk = "io.mockk:mockk:1.13.2"
59-
test-mockk-android = "io.mockk:mockk-android:1.13.2"
57+
test-mockk = "io.mockk:mockk:1.13.8"
58+
test-mockk-android = "io.mockk:mockk-android:1.13.8"
6059
test-robolectric = "org.robolectric:robolectric:4.9.2"
6160
test-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
6261
debug-ui-test-manifest = "androidx.compose.ui:ui-test-manifest:1.5.0-beta01"

liveness/src/androidTest/java/com/amplifyframework/ui/liveness/LivenessFlowInstrumentationTest.kt

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import com.amplifyframework.predictions.options.FaceLivenessSessionOptions
3131
import com.amplifyframework.ui.liveness.camera.FrameAnalyzer
3232
import com.amplifyframework.ui.liveness.ml.FaceDetector
3333
import com.amplifyframework.ui.liveness.model.LivenessCheckState
34-
import com.amplifyframework.ui.liveness.model.FaceLivenessDetectionException
3534
import com.amplifyframework.ui.liveness.state.LivenessState
3635
import com.amplifyframework.ui.liveness.ui.FaceLivenessDetector
3736
import io.mockk.CapturingSlot
@@ -46,6 +45,7 @@ import io.mockk.mockkObject
4645
import io.mockk.mockkStatic
4746
import io.mockk.slot
4847
import io.mockk.unmockkConstructor
48+
import java.util.Date
4949
import org.junit.Assert.assertEquals
5050
import org.junit.Assert.assertTrue
5151
import org.junit.Before
@@ -75,6 +75,7 @@ class LivenessFlowInstrumentationTest {
7575
private lateinit var connectingString: String
7676
private lateinit var moveCloserString: String
7777
private lateinit var holdStillString: String
78+
private lateinit var verifyingString: String
7879
private lateinit var mockCredentialsProvider: MockCredentialsProvider
7980

8081
private var framesSent = 0
@@ -123,6 +124,7 @@ class LivenessFlowInstrumentationTest {
123124
holdStillString = context.getString(
124125
R.string.amplify_ui_liveness_challenge_instruction_hold_face_during_freshness,
125126
)
127+
verifyingString = context.getString(R.string.amplify_ui_liveness_challenge_verifying)
126128

127129
mockCredentialsProvider = MockCredentialsProvider()
128130
}
@@ -255,7 +257,7 @@ class LivenessFlowInstrumentationTest {
255257
composeTestRule.setContent {
256258
FaceLivenessDetector(sessionId = sessionId, region = "us-east-1", onComplete = {
257259
completesSuccessfully = true
258-
}, onError = {assertTrue(false) })
260+
}, onError = { assertTrue(false) })
259261
}
260262

261263
composeTestRule.onNodeWithText(beginCheckString).assertExists()
@@ -306,10 +308,15 @@ class LivenessFlowInstrumentationTest {
306308
val sessionId = "sessionId"
307309
var completesSuccessfully = false
308310
composeTestRule.setContent {
309-
FaceLivenessDetector(sessionId = sessionId, region = "us-east-1", credentialsProvider = mockCredentialsProvider,
311+
FaceLivenessDetector(
312+
sessionId = sessionId,
313+
region = "us-east-1",
314+
credentialsProvider = mockCredentialsProvider,
310315
onComplete = {
311-
completesSuccessfully = true
312-
}, onError = { assertTrue(false) })
316+
completesSuccessfully = true
317+
},
318+
onError = { assertTrue(false) },
319+
)
313320
}
314321

315322
composeTestRule.onNodeWithText(beginCheckString).assertExists()
@@ -403,11 +410,23 @@ class LivenessFlowInstrumentationTest {
403410
)
404411
faceUpdates += 1
405412

406-
// now, the face is inside the oval. wait for the colors to finish
407413
composeTestRule.waitForIdle()
408414

409-
assertEquals(livenessState?.readyToSendFinalEvents, true)
415+
// countdown is now invsible, wait one second so that we can start freshness
416+
composeTestRule.waitUntil(2000) {
417+
livenessState?.faceMatchOvalStart?.let { (Date().time - it) > 1000 } ?: false
418+
}
419+
livenessState?.onFrameAvailable()
420+
assert(livenessState?.runningFreshness!!)
421+
422+
// now, freshness is running. wait for the colors to finish
423+
composeTestRule.waitUntil(10000) {
424+
composeTestRule.onAllNodesWithText(verifyingString)
425+
.fetchSemanticsNodes().size == 1
426+
}
427+
410428
val state = livenessState?.livenessCheckState?.value
429+
assertEquals(livenessState?.readyToSendFinalEvents, true)
411430
assertTrue(state is LivenessCheckState.Success)
412431
assertTrue((state as LivenessCheckState.Success).faceGuideRect == faceRect)
413432
// inconsistent number of frames sent

scripts/generate_df_testrun_report

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env python3
2+
"""Python script that generates a user-readable report for a given DeviceFarm test run.
3+
"""
4+
5+
import os
6+
import sys
7+
import subprocess
8+
import argparse
9+
import logging
10+
import boto3
11+
from botocore.config import Config
12+
from junit_xml import TestSuite, TestCase
13+
14+
LOG_FORMATTER = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
15+
CONSOLE_HANDLER = logging.StreamHandler()
16+
CONSOLE_HANDLER.setFormatter(LOG_FORMATTER)
17+
LOGGER = logging.getLogger("DeviceFarmTestRunReportGenerator")
18+
LOGGER.setLevel(os.getenv("LOG_LEVEL") if os.getenv("LOG_LEVEL") is not None else "INFO")
19+
LOGGER.addHandler(CONSOLE_HANDLER)
20+
21+
client = boto3.client('devicefarm', config=Config(region_name='us-west-2'))
22+
23+
def parse_arguments():
24+
parser = argparse.ArgumentParser(description="Utility that generates a report for a DeviceFarm test run.")
25+
parser.add_argument("--run_arn", help="The ARN of the DeviceFarm test run.", required=True)
26+
parser.add_argument("--module_name", help="The module name for the test suite.", required=True)
27+
parser.add_argument("--output_path", help="Destination path for the build reports.", required=True)
28+
parser.add_argument("--pr", help="The github PR number.")
29+
return parser.parse_args()
30+
31+
def generate_junit_report(run_arn, output_path):
32+
LOGGER.debug(f"Retrieving test jobs for run {run_arn}")
33+
jobs = get_test_jobs(run_arn)
34+
for job_no, job in enumerate(jobs):
35+
LOGGER.debug(f"Retrieving test suites for job {job['arn']}")
36+
suites = get_test_suites(job['arn'])
37+
for suite in suites:
38+
LOGGER.debug(f"Retrieving tests for suite {suite['arn']}")
39+
tests = get_tests(suite['arn'])
40+
test_cases = []
41+
for test in tests:
42+
tc = TestCase(test['name'],
43+
classname=suite['name'],
44+
elapsed_sec=test['deviceMinutes']['total']*60,
45+
stdout=test['message'],
46+
status=test['result'] )
47+
if test['result'] == 'FAILED':
48+
tc.add_failure_info(message=test['message'])
49+
if test['result'] == 'ERROR':
50+
tc.add_error_info(message=test['message'])
51+
test_cases.append(tc)
52+
ts = TestSuite(suite['name'] + "-" + str(job_no),test_cases=test_cases)
53+
ts_output = TestSuite.to_xml_string([ts])
54+
LOGGER.info(f"Saving test suite {suite['name']} report.")
55+
if not os.path.exists(output_path):
56+
os.makedirs(output_path)
57+
f = open(output_path + suite['name'] + "-" + str(job_no) + ".xml", "w")
58+
f.write(ts_output)
59+
f.close()
60+
61+
def get_test_jobs(run_arn):
62+
result = client.list_jobs(arn=run_arn)
63+
return result['jobs'] if result is not None else []
64+
65+
def get_test_suites(job_arn):
66+
result = client.list_suites(arn=job_arn)
67+
return result['suites'] if result is not None else []
68+
69+
def get_tests(suite_arn):
70+
result = client.list_tests(arn=suite_arn)
71+
return result['tests'] if result is not None else []
72+
73+
def get_problems(run_arn):
74+
return client.list_unique_problems(
75+
arn=run_arn
76+
)
77+
78+
def main(arguments):
79+
args = parse_arguments()
80+
build_id = os.getenv("CODEBUILD_BUILD_ID")
81+
source_version = os.getenv("CODEBUILD_SOURCE_VERSION")
82+
arn_suffix = args.run_arn.split(':')[-1]
83+
LOGGER.info(f"devicefarm_run: {arn_suffix} build_id: {build_id} source_version: {source_version}")
84+
generate_junit_report(run_arn=args.run_arn,
85+
output_path=args.output_path)
86+
87+
if __name__ == '__main__':
88+
sys.exit(main(sys.argv[1:]))

0 commit comments

Comments
 (0)