Skip to content

Commit 1920a9f

Browse files
authored
Merge branch 'main' into fix-back-navigation
2 parents b52ea89 + 5d14367 commit 1920a9f

File tree

37 files changed

+784
-374
lines changed

37 files changed

+784
-374
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ jobs:
8787
disable-animations: false
8888
script: echo "Generated AVD snapshot for caching."
8989

90-
- name: Run Engine module unit and instrumentation tests and generate coverage report
90+
- name: Run instrumentation tests
9191
uses: reactivecircus/android-emulator-runner@v2
9292
with:
9393
working-directory: android
@@ -96,7 +96,11 @@ jobs:
9696
force-avd-creation: false
9797
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
9898
disable-animations: true
99-
script: ./gradlew -PlocalPropertiesFile=local.properties :engine:clean :engine:fhircoreJacocoReport --stacktrace
99+
script: ./gradlew -PlocalPropertiesFile=local.properties :engine:clean :engine:connectedDebugAndroidTest --stacktrace
100+
101+
- name: Generate coverage report
102+
working-directory: android
103+
run: ./gradlew -PlocalPropertiesFile=local.properties :engine:fhircoreJacocoReport --stacktrace
100104

101105
- name: Upload Test reports
102106
if: ${{ !cancelled() }}
@@ -175,7 +179,7 @@ jobs:
175179
disable-animations: false
176180
script: echo "Generated AVD snapshot for caching."
177181

178-
- name: Run Geowidget module unit and instrumentation tests and generate coverage report
182+
- name: Run instrumentation tests
179183
uses: reactivecircus/android-emulator-runner@v2
180184
with:
181185
working-directory: android
@@ -184,7 +188,11 @@ jobs:
184188
force-avd-creation: false
185189
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
186190
disable-animations: true
187-
script: ./gradlew -PlocalPropertiesFile=local.properties :geowidget:clean :geowidget:fhircoreJacocoReport --stacktrace
191+
script: ./gradlew -PlocalPropertiesFile=local.properties :geowidget:clean :geowidget:connectedDebugAndroidTest --stacktrace
192+
193+
- name: Generate coverage report
194+
working-directory: android
195+
run: ./gradlew -PlocalPropertiesFile=local.properties :geowidget:fhircoreJacocoReport --stacktrace
188196

189197
- name: Upload Test reports
190198
if: ${{ !cancelled() }}
@@ -199,7 +207,6 @@ jobs:
199207
run: bash <(curl -s https://codecov.io/bash) -F geowidget -f "geowidget/build/reports/jacoco/fhircoreJacocoReport/fhircoreJacocoReport.xml"
200208

201209
quest-tests:
202-
timeout-minutes: 90 # Extend timeout to 90 minutes
203210
runs-on: ubuntu-latest
204211
strategy:
205212
matrix:
@@ -261,11 +268,13 @@ jobs:
261268
api-level: ${{ matrix.api-level }}
262269
arch: x86_64
263270
force-avd-creation: false
271+
heap-size: 4608M
264272
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
265273
disable-animations: false
266274
script: echo "Generated AVD snapshot for caching."
267275

268-
- name: Run Quest module unit and instrumentation tests and generate unit tests coverage report
276+
- name: Run instrumentation tests
277+
continue-on-error: true
269278
uses: reactivecircus/android-emulator-runner@v2
270279
with:
271280
working-directory: android
@@ -275,11 +284,17 @@ jobs:
275284
heap-size: 4608M
276285
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
277286
disable-animations: true
287+
# Run instrumentation tests for the Quest module, excluding the long-running performance tests.
278288
script: >-
279-
./gradlew -PlocalPropertiesFile=local.properties :quest:fhircoreJacocoReport
289+
./gradlew -PlocalPropertiesFile=local.properties :quest:connectedOpensrpDebugAndroidTest
280290
-Pandroid.testInstrumentationRunnerArguments.notPackage=org.smartregister.fhircore.quest.performance
281291
-Pandroid.testInstrumentationRunnerArguments.numShards=${{ matrix.num-shards }}
282292
-Pandroid.testInstrumentationRunnerArguments.shardIndex=${{ matrix.shard }}
293+
--stacktrace
294+
295+
- name: Generate coverage report
296+
working-directory: android
297+
run: ./gradlew -PlocalPropertiesFile=local.properties :quest:fhircoreJacocoReport --stacktrace
283298

284299
- name: Upload Test reports
285300
if: ${{ !cancelled() }}

android/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ buildscript {
99
classpath(libs.coveralls.gradle.plugin)
1010
classpath(libs.gradle)
1111
classpath(libs.dokka.base)
12+
classpath("com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.1.20-2.0.1")
1213
}
1314
}
1415

@@ -22,6 +23,7 @@ plugins {
2223
alias(libs.plugins.org.owasp.dependencycheck)
2324
alias(libs.plugins.com.diffplug.spotless) apply false
2425
alias(libs.plugins.android.junit5) apply false
26+
alias(libs.plugins.org.jetbrains.kotlin.plugin.compose) apply false
2527
}
2628

2729
tasks.dokkaHtmlMultiModule {
@@ -46,8 +48,8 @@ allprojects {
4648
mavenCentral()
4749
maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
4850
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots")
49-
maven(url = "https://repo.spring.io/plugins-release")
5051
maven(url = "https://repository.liferay.com/nexus/content/repositories/public")
52+
maven(url = "https://central.sonatype.com/repository/maven-snapshots")
5153
apply(plugin = "org.owasp.dependencycheck")
5254
tasks.dependencyCheckAggregate{
5355
dependencyCheck.formats.add("XML")

android/buildSrc/src/main/kotlin/jacoco-report.gradle.kts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,11 @@ import org.gradle.testing.jacoco.tasks.JacocoReport
33
val isApplication = (project.name == "quest")
44
val actualProjectName : String = if(isApplication) "opensrp" else project.name
55

6-
project.tasks.create("fhircoreJacocoReport", JacocoReport::class.java) {
7-
val tasksList = mutableSetOf(
8-
"test${if(isApplication) actualProjectName.replaceFirstChar { it.uppercase() } else ""}DebugUnitTest", // Generates unit test coverage report
6+
project.tasks.register<JacocoReport>("fhircoreJacocoReport") {
7+
val tasksList = setOf(
8+
"test${if(isApplication) actualProjectName.replaceFirstChar { it.uppercase() } else ""}DebugUnitTest",
99
)
1010

11-
/**
12-
* Runs instrumentation tests for all modules except quest. Quest instrumentation tests are divided
13-
* into functional tests and performance tests. Performance tests can take upto 1 hr and are not required
14-
* while functional tests alone will take ~40 mins and they are required.
15-
*/
16-
tasksList += "connected${if (isApplication) actualProjectName.replaceFirstChar { it.uppercase() } else ""}DebugAndroidTest"
17-
1811
dependsOn(
1912
tasksList
2013
)

android/engine/build.gradle.kts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ plugins {
1212
id("de.mannodermaus.android-junit5")
1313
id("dagger.hilt.android.plugin")
1414
id("androidx.navigation.safeargs")
15+
id("com.google.devtools.ksp")
16+
id("com.google.dagger.hilt.android")
17+
alias(libs.plugins.org.jetbrains.kotlin.plugin.compose)
1518
}
1619

1720
tasks.named("dokkaHtmlPartial") {
@@ -236,12 +239,12 @@ dependencies {
236239
}
237240

238241
// Annotation processors
239-
kapt(libs.hilt.compiler)
240-
kapt(libs.dagger.hilt.compiler)
242+
ksp(libs.hilt.compiler)
243+
ksp(libs.dagger.hilt.compiler)
241244

242245
// Annotation processors for test
243-
kaptTest(libs.dagger.hilt.android.compiler)
244-
kaptAndroidTest(libs.dagger.hilt.android.compiler)
246+
kspTest(libs.dagger.hilt.android.compiler)
247+
kspAndroidTest(libs.dagger.hilt.android.compiler)
245248

246249
testRuntimeOnly(libs.bundles.junit.jupiter.runtime)
247250

android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,19 @@ constructor(
292292
withContext(dispatcherProvider.main()) { configsLoadedCallback(false) }
293293
}
294294
} else {
295-
fhirEngine.searchCompositionByIdentifier(parsedAppId)?.run {
296-
populateConfigurationsMap(context, this, false, parsedAppId, configsLoadedCallback)
295+
fhirEngine.searchCompositionByIdentifier(parsedAppId)?.let { foundComposition ->
296+
populateConfigurationsMap(
297+
context,
298+
foundComposition,
299+
false,
300+
parsedAppId,
301+
configsLoadedCallback,
302+
)
297303
}
304+
?: run {
305+
Timber.w("Composition not found for appId: $appId", parsedAppId)
306+
configsLoadedCallback(false)
307+
}
298308
}
299309
}
300310

@@ -504,7 +514,7 @@ constructor(
504514
Timber.i("Fetching ImplementationGuide config for app $appId version $appVersionCode")
505515

506516
val urlPath =
507-
"ImplementationGuide?&name=$appId&context-quantity=le$appVersionCode&_sort=-context-quantity&_count=1"
517+
"ImplementationGuide?&name:exact=$appId&context-quantity=le$appVersionCode&_sort=-context-quantity&_count=1"
508518
return fhirResourceDataSource.getResource(urlPath).entryFirstRep.let {
509519
if (!it.hasResource()) {
510520
Timber.w("No response for ImplementationGuide resource on path $urlPath")

android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import java.util.UUID
4848
import javax.inject.Inject
4949
import javax.inject.Singleton
5050
import kotlin.math.ceil
51+
import kotlinx.coroutines.CoroutineScope
52+
import kotlinx.coroutines.launch
5153
import kotlinx.coroutines.withContext
5254
import kotlinx.serialization.json.JsonArray
5355
import kotlinx.serialization.json.JsonElement
@@ -254,11 +256,15 @@ constructor(
254256
suspend fun <R : Resource> addOrUpdate(addMandatoryTags: Boolean = true, resource: R) {
255257
resource.updateLastUpdated()
256258
try {
257-
fhirEngine.get(resource.resourceType, resource.logicalId).run {
258-
val updateFrom = updateFrom(resource)
259-
fhirEngine.update(updateFrom)
259+
val previousResourceCopy = fhirEngine.get(resource.resourceType, resource.logicalId)
260+
fhirEngine.update(resource)
261+
262+
// Offload merging and updating of the merged resource to a separate context
263+
CoroutineScope(dispatcherProvider.default()).launch {
264+
val processedUpdatedFromResource = previousResourceCopy.updateFrom(resource, parser)
265+
fhirEngine.update(processedUpdatedFromResource)
260266
}
261-
} catch (resourceNotFoundException: ResourceNotFoundException) {
267+
} catch (_: ResourceNotFoundException) {
262268
create(addMandatoryTags, resource)
263269
}
264270
}

android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.smartregister.fhircore.engine.util.extension
1818

1919
import android.content.Context
2020
import ca.uhn.fhir.context.FhirContext
21+
import ca.uhn.fhir.parser.IParser
2122
import ca.uhn.fhir.rest.gclient.ReferenceClientParam
2223
import com.google.android.fhir.datacapture.extensions.logicalId
2324
import com.google.android.fhir.get
@@ -116,6 +117,10 @@ fun CodeableConcept.stringValue(): String =
116117
fun Resource.encodeResourceToString(): String =
117118
FhirContext.forR4().getCustomJsonParser().encodeResourceToString(this.copy())
118119

120+
fun Resource.encodeResourceToString(
121+
parser: IParser = FhirContext.forR4().getCustomJsonParser(),
122+
): String = parser.encodeResourceToString(this.copy())
123+
119124
fun StructureMap.encodeResourceToString(): String =
120125
FhirContext.forR4()
121126
.getCustomJsonParser()
@@ -128,49 +133,45 @@ fun StructureMap.encodeResourceToString(): String =
128133
fun <T> String.decodeResourceFromString(): T =
129134
FhirContext.forR4().getCustomJsonParser().parseResource(this) as T
130135

131-
fun <T : Resource> T.updateFrom(updatedResource: Resource): T {
132-
var extensionUpdateFrom = listOf<Extension>()
133-
if (updatedResource is Patient) {
134-
extensionUpdateFrom = updatedResource.extension
135-
}
136-
var extension = listOf<Extension>()
137-
if (this is Patient) {
138-
extension = this.extension
139-
}
140-
val stringJson = encodeResourceToString()
136+
fun <T : Resource> T.updateFrom(
137+
updatedResource: Resource,
138+
parser: IParser = FhirContext.forR4().getCustomJsonParser(),
139+
): T {
140+
val extensionUpdateFrom: List<Extension> =
141+
if (updatedResource is Patient) updatedResource.extension else emptyList()
142+
val extension: List<Extension> = if (this is Patient) this.extension else emptyList()
143+
144+
val stringJson = this.encodeResourceToString(parser)
141145
val originalResourceJson = JSONObject(stringJson)
142146

143-
originalResourceJson.updateFrom(JSONObject(updatedResource.encodeResourceToString()))
144-
return FhirContext.forR4()
145-
.getCustomJsonParser()
146-
.parseResource(this::class.java, originalResourceJson.toString())
147-
.apply {
148-
val meta = this.meta
149-
val metaUpdateFrom = this@updateFrom.meta
150-
if ((meta == null || meta.isEmpty)) {
151-
if (metaUpdateFrom != null) {
152-
this.meta = metaUpdateFrom
153-
this.meta.tag = metaUpdateFrom.tag
154-
}
155-
} else {
156-
val setOfTags = mutableSetOf<Coding>()
157-
setOfTags.addAll(meta.tag)
158-
setOfTags.addAll(metaUpdateFrom.tag)
159-
this.meta.tag = setOfTags.distinctBy { it.code + it.system }
147+
originalResourceJson.updateFrom(JSONObject(updatedResource.encodeResourceToString(parser)))
148+
return parser.parseResource(this::class.java, originalResourceJson.toString()).apply {
149+
val meta = this.meta
150+
val metaUpdateFrom = this@updateFrom.meta
151+
if ((meta == null || meta.isEmpty)) {
152+
if (metaUpdateFrom != null) {
153+
this.meta = metaUpdateFrom
154+
this.meta.tag = metaUpdateFrom.tag
160155
}
161-
if (this is Patient && this@updateFrom is Patient && updatedResource is Patient) {
162-
if (extension.isEmpty()) {
163-
if (extensionUpdateFrom.isNotEmpty()) {
164-
this.extension = extensionUpdateFrom
165-
}
166-
} else {
167-
val setOfExtension = mutableSetOf<Extension>()
168-
setOfExtension.addAll(extension)
169-
setOfExtension.addAll(extensionUpdateFrom)
170-
this.extension = setOfExtension.distinct()
156+
} else {
157+
val setOfTags = mutableSetOf<Coding>()
158+
setOfTags.addAll(meta.tag)
159+
setOfTags.addAll(metaUpdateFrom.tag)
160+
this.meta.tag = setOfTags.distinctBy { it.code + it.system }
161+
}
162+
if (this is Patient && this@updateFrom is Patient && updatedResource is Patient) {
163+
if (extension.isEmpty()) {
164+
if (extensionUpdateFrom.isNotEmpty()) {
165+
this.extension = extensionUpdateFrom
171166
}
167+
} else {
168+
val setOfExtension = mutableSetOf<Extension>()
169+
setOfExtension.addAll(extension)
170+
setOfExtension.addAll(extensionUpdateFrom)
171+
this.extension = setOfExtension.distinct()
172172
}
173173
}
174+
}
174175
}
175176

176177
@Throws(JSONException::class)
@@ -452,6 +453,28 @@ fun String.resourceClassType(): Class<out Resource> =
452453
*/
453454
fun String.extractLogicalIdUuid() = this.substringAfter("/").substringBefore("/")
454455

456+
/**
457+
* A function that extracts only the UUID part of a resource logicalId from a URI.
458+
*
459+
* Examples:
460+
* 1. "http://smartreg.org/Library/3e2bb367-b9bb-4c30-9033-8f42657c5df7/history/3" returns
461+
*
462+
* ```
463+
* "3e2bb367-b9bb-4c30-9033-8f42657c5df7".
464+
* ```
465+
*/
466+
fun String.extractLogicalIdUuidFromURI(resourceType: ResourceType): String {
467+
val pathSegments: List<String> = this.split("/")
468+
val resourceIndex = pathSegments.indexOf(resourceType.name)
469+
470+
// Check if the resource type was found and if there's a segment after it
471+
return if (resourceIndex != -1 && resourceIndex + 1 < pathSegments.size) {
472+
pathSegments[resourceIndex + 1]
473+
} else {
474+
"" // Return empty if the resource type or its ID is not found
475+
}
476+
}
477+
455478
/**
456479
* This suspend function updates the due date of the dependents of the current [Task], based on the
457480
* date of a related [Immunization] [Task]. The function loops through all the tasks that are
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="14dp"
3+
android:height="10dp"
4+
android:viewportWidth="14"
5+
android:viewportHeight="10">
6+
<path
7+
android:pathData="M12.834,0.774L4.813,8.795L1.167,5.149"
8+
android:strokeLineJoin="round"
9+
android:strokeWidth="1.1221"
10+
android:fillColor="#00000000"
11+
android:strokeColor="#F3F3F3"
12+
android:strokeLineCap="round"/>
13+
</vector>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="26dp"
3+
android:height="26dp"
4+
android:viewportWidth="26"
5+
android:viewportHeight="26">
6+
<path
7+
android:pathData="M9.982,23.517V12.891H16.357V23.517M3.606,9.703L13.17,2.265L22.733,9.703V21.392C22.733,21.956 22.509,22.496 22.111,22.895C21.712,23.294 21.171,23.517 20.608,23.517H5.731C5.168,23.517 4.627,23.294 4.228,22.895C3.83,22.496 3.606,21.956 3.606,21.392V9.703Z"
8+
android:strokeLineJoin="round"
9+
android:strokeWidth="1.1221"
10+
android:fillColor="#00000000"
11+
android:strokeColor="#F3F3F3"
12+
android:strokeLineCap="round"/>
13+
</vector>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="10dp"
3+
android:height="11dp"
4+
android:viewportWidth="10"
5+
android:viewportHeight="11">
6+
<path
7+
android:pathData="M9.375,0.774L0.625,9.524M0.625,0.774L9.375,9.524"
8+
android:strokeLineJoin="round"
9+
android:strokeWidth="1.1221"
10+
android:fillColor="#00000000"
11+
android:strokeColor="#F3F3F3"
12+
android:strokeLineCap="round"/>
13+
</vector>

0 commit comments

Comments
 (0)