diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt index c30bf12910..60b79961e6 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt @@ -20,7 +20,6 @@ import ca.uhn.fhir.rest.gclient.UriClientParam import com.google.android.fhir.FhirEngine import com.google.android.fhir.getResourceType import com.google.android.fhir.search.Search -import kotlinx.coroutines.runBlocking import org.hl7.fhir.instance.model.api.IBaseResource import org.hl7.fhir.instance.model.api.IIdType import org.hl7.fhir.r4.model.Library @@ -28,37 +27,38 @@ import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType import org.opencds.cqf.cql.evaluator.fhir.dal.FhirDal -class FhirEngineDal(private val fhirEngine: FhirEngine) : FhirDal { +internal class FhirEngineDal(private val fhirEngine: FhirEngine) : FhirDal { val libs = mutableMapOf() - override fun read(id: IIdType): IBaseResource = runBlocking { + override fun read(id: IIdType): IBaseResource = runBlockingOrThrowMainThreadException { val clazz = id.getResourceClass() fhirEngine.get(getResourceType(clazz), id.idPart) } - override fun create(resource: IBaseResource): Unit = runBlocking { + override fun create(resource: IBaseResource): Unit = runBlockingOrThrowMainThreadException { fhirEngine.create(resource as Resource) } - override fun update(resource: IBaseResource) = runBlocking { + override fun update(resource: IBaseResource) = runBlockingOrThrowMainThreadException { fhirEngine.update(resource as Resource) } - override fun delete(id: IIdType) = runBlocking { + override fun delete(id: IIdType) = runBlockingOrThrowMainThreadException { val clazz = id.getResourceClass() fhirEngine.delete(getResourceType(clazz), id.idPart) } - override fun search(resourceType: String): Iterable = runBlocking { - val search = Search(type = ResourceType.fromCode(resourceType)) - when (resourceType) { - "Library" -> libs.values.plus(fhirEngine.search(search)) - else -> fhirEngine.search(search) - }.toMutableList() - } + override fun search(resourceType: String): Iterable = + runBlockingOrThrowMainThreadException { + val search = Search(type = ResourceType.fromCode(resourceType)) + when (resourceType) { + "Library" -> libs.values.plus(fhirEngine.search(search)) + else -> fhirEngine.search(search) + }.toMutableList() + } override fun searchByUrl(resourceType: String, url: String): Iterable = - runBlocking { + runBlockingOrThrowMainThreadException { val search = Search(type = ResourceType.fromCode(resourceType)) search.filter(UriClientParam("url"), { value = url }) diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineLibraryContentProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineLibraryContentProvider.kt index 18eb40f1d9..52590f38ff 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineLibraryContentProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineLibraryContentProvider.kt @@ -22,7 +22,7 @@ import org.hl7.fhir.r4.model.Library import org.opencds.cqf.cql.evaluator.cql2elm.content.fhir.BaseFhirLibrarySourceProvider import org.opencds.cqf.cql.evaluator.fhir.adapter.r4.AdapterFactory -class FhirEngineLibraryContentProvider(adapterFactory: AdapterFactory) : +internal class FhirEngineLibraryContentProvider(adapterFactory: AdapterFactory) : BaseFhirLibrarySourceProvider(adapterFactory) { val libs = mutableMapOf() diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt index 524df097c6..d7a5717952 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt @@ -29,7 +29,6 @@ import com.google.android.fhir.search.filter.TokenParamFilterCriterion import com.google.android.fhir.search.query.XFhirQueryTranslator.applyFilterParam import java.math.BigDecimal import java.util.Date -import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.Enumerations @@ -41,7 +40,7 @@ import org.opencds.cqf.cql.engine.runtime.DateTime import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.terminology.ValueSetInfo -class FhirEngineRetrieveProvider(private val fhirEngine: FhirEngine) : +internal class FhirEngineRetrieveProvider(private val fhirEngine: FhirEngine) : TerminologyAwareRetrieveProvider() { override fun retrieve( context: String?, @@ -56,37 +55,26 @@ class FhirEngineRetrieveProvider(private val fhirEngine: FhirEngine) : dateLowPath: String?, dateHighPath: String?, dateRange: Interval? - ): Iterable { - return runBlocking { - if (dataType == null) { - emptyList() - } else if (contextPath == "id" && contextValue == null) { - emptyList() - } else if (contextPath == "id" && contextValue != null) { - listOfNotNull( - safeGet(fhirEngine, ResourceType.fromCode(dataType), "$contextValue"), - safeGet(fhirEngine, ResourceType.fromCode(dataType), "urn:uuid:$contextValue"), - safeGet(fhirEngine, ResourceType.fromCode(dataType), "urn:oid:$contextValue") - ) - } else if (codePath == "id" && codes != null) { - codes.mapNotNull { safeGet(fhirEngine, ResourceType.fromCode(dataType), it.code) } - } else { - val search = Search(ResourceType.fromCode(dataType)) - - // filter by context - filterByContext(context, contextPath, contextValue, dataType, search) - - // filter by code in codes - filterByCode(codePath, codes, search) - - // filter by code into valueSet - filterByValueSet(codePath, valueSet, search) - - // filter by date in range - filterByDateRange(datePath, dateLowPath, dateHighPath, dateRange, search) - - fhirEngine.search(search) - } + ): Iterable = runBlockingOrThrowMainThreadException { + if (dataType == null) { + emptyList() + } else if (contextPath == "id" && contextValue == null) { + emptyList() + } else if (contextPath == "id") { + listOfNotNull( + safeGet(fhirEngine, ResourceType.fromCode(dataType), "$contextValue"), + safeGet(fhirEngine, ResourceType.fromCode(dataType), "urn:uuid:$contextValue"), + safeGet(fhirEngine, ResourceType.fromCode(dataType), "urn:oid:$contextValue") + ) + } else if (codePath == "id" && codes != null) { + codes.mapNotNull { safeGet(fhirEngine, ResourceType.fromCode(dataType), it.code) } + } else { + val search = Search(ResourceType.fromCode(dataType)) + filterByContext(context, contextPath, contextValue, dataType, search) + filterByCode(codePath, codes, search) + filterByValueSet(codePath, valueSet, search) + filterByDateRange(datePath, dateLowPath, dateHighPath, dateRange, search) + fhirEngine.search(search) } } diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt index 3c1fef7456..c4858ba2c3 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt @@ -20,7 +20,6 @@ import ca.uhn.fhir.context.FhirContext import com.google.android.fhir.FhirEngine import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.search.search -import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.CodeSystem import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType @@ -32,7 +31,7 @@ import org.opencds.cqf.cql.engine.terminology.TerminologyProvider import org.opencds.cqf.cql.engine.terminology.ValueSetInfo import org.opencds.cqf.cql.evaluator.engine.util.ValueSetUtil -class FhirEngineTerminologyProvider( +internal class FhirEngineTerminologyProvider( private val fhirContext: FhirContext, private val fhirEngine: FhirEngine ) : TerminologyProvider { @@ -45,6 +44,8 @@ class FhirEngineTerminologyProvider( override fun `in`(code: Code, valueSet: ValueSetInfo): Boolean { try { return expand(valueSet).any { it.code == code.code && it.system == code.system } + } catch (b: BlockingMainThreadException) { + throw b } catch (e: Exception) { throw TerminologyProviderException( "Error performing membership check of Code: $code in ValueSet: ${valueSet.id}", @@ -53,63 +54,62 @@ class FhirEngineTerminologyProvider( } } - override fun expand(valueSetInfo: ValueSetInfo): MutableIterable { - try { - val valueSet = resolveValueSet(valueSetInfo) - return ValueSetUtil.getCodesInExpansion(fhirContext, valueSet) - ?: ValueSetUtil.getCodesInCompose(fhirContext, valueSet) - } catch (e: Exception) { - throw TerminologyProviderException( - "Error performing expansion of ValueSet: ${valueSetInfo.id}", - e - ) - } - } - - override fun lookup(code: Code, codeSystem: CodeSystemInfo): Code { - try { - val codeSystems = runBlocking { - fhirEngine.search { - filter(CodeSystem.CODE, { value = of(code.code) }) - filter(CodeSystem.SYSTEM, { value = codeSystem.id }) + override fun expand(valueSetInfo: ValueSetInfo): MutableIterable = + runBlockingOrThrowMainThreadException { + try { + resolveValueSet(valueSetInfo).let { + ValueSetUtil.getCodesInExpansion(fhirContext, it) + ?: ValueSetUtil.getCodesInCompose(fhirContext, it) } + } catch (e: Exception) { + throw TerminologyProviderException( + "Error performing expansion of ValueSet: ${valueSetInfo.id}", + e + ) } + } - val concept = codeSystems.first().concept.first { it.code == code.code } - - return Code().apply { - this.code = code.code - display = concept.display - system = codeSystem.id + override fun lookup(code: Code, codeSystem: CodeSystemInfo): Code = + runBlockingOrThrowMainThreadException { + try { + fhirEngine + .search { + filter(CodeSystem.CODE, { value = of(code.code) }) + filter(CodeSystem.SYSTEM, { value = codeSystem.id }) + } + .first() + .concept + .first { it.code == code.code } + .let { + Code().apply { + this.code = code.code + display = it.display + system = codeSystem.id + } + } + } catch (e: Exception) { + throw TerminologyProviderException( + "Error performing lookup of Code: $code in CodeSystem: ${codeSystem.id}", + e + ) } - } catch (e: Exception) { - throw TerminologyProviderException( - "Error performing lookup of Code: $code in CodeSystem: ${codeSystem.id}", - e - ) } - } - private fun searchByUrl(url: String?): List { + private suspend fun searchByUrl(url: String?): List { if (url == null) return emptyList() - return runBlocking { fhirEngine.search { filter(ValueSet.URL, { value = url }) } } + return fhirEngine.search { filter(ValueSet.URL, { value = url }) } } - private fun searchByIdentifier(identifier: String?): List { + private suspend fun searchByIdentifier(identifier: String?): List { if (identifier == null) return emptyList() - return runBlocking { - fhirEngine.search { filter(ValueSet.IDENTIFIER, { value = of(identifier) }) } - } + return fhirEngine.search { filter(ValueSet.IDENTIFIER, { value = of(identifier) }) } } - private fun searchById(id: String): List { - return runBlocking { - listOfNotNull( - safeGet(fhirEngine, ResourceType.ValueSet, id.removePrefix(URN_OID).removePrefix(URN_UUID)) - as? ValueSet - ) - } - } + private suspend fun searchById(id: String): List = + listOfNotNull( + safeGet(fhirEngine, ResourceType.ValueSet, id.removePrefix(URN_OID).removePrefix(URN_UUID)) + as? ValueSet + ) private suspend fun safeGet(fhirEngine: FhirEngine, type: ResourceType, id: String): Resource? { return try { @@ -119,7 +119,7 @@ class FhirEngineTerminologyProvider( } } - fun resolveValueSet(valueSet: ValueSetInfo): ValueSet { + private suspend fun resolveValueSet(valueSet: ValueSetInfo): ValueSet { if (valueSet.version != null || (valueSet.codeSystems != null && valueSet.codeSystems.isNotEmpty()) ) { @@ -146,7 +146,7 @@ class FhirEngineTerminologyProvider( } } - fun resolveValueSetId(valueSet: ValueSetInfo): String { + suspend fun resolveValueSetId(valueSet: ValueSetInfo): String { return resolveValueSet(valueSet).idElement.idPart } } diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirOperator.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirOperator.kt index d2bcc1da6b..17f98be3b6 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirOperator.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirOperator.kt @@ -16,6 +16,7 @@ package com.google.android.fhir.workflow +import androidx.annotation.WorkerThread import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import com.google.android.fhir.FhirEngine @@ -185,22 +186,30 @@ class FhirOperator(fhirContext: FhirContext, fhirEngine: FhirEngine) { } /** - * The function evaluates a FHIR library against the database + * The function evaluates a FHIR library against the database. + * + * NOTE: The API may internally result in a blocking IO operation. The user should call the API + * from a worker thread or it may throw [BlockingMainThreadException] exception. * @param libraryUrl the url of the Library to evaluate * @param expressions names of expressions in the Library to evaluate. - * @return a Parameters resource that contains an evaluation result for each expression requested + * @return a Parameters resource that contains an evaluation result for each expression requested. */ + @WorkerThread fun evaluateLibrary(libraryUrl: String, expressions: Set): IBaseParameters { return evaluateLibrary(libraryUrl, null, null, expressions) } /** * The function evaluates a FHIR library against a patient's records. + * + * NOTE: The API may internally result in a blocking IO operation. The user should call the API + * from a worker thread or it may throw [BlockingMainThreadException] exception. * @param libraryUrl the url of the Library to evaluate * @param patientId the Id of the patient to be evaluated * @param expressions names of expressions in the Library to evaluate. * @return a Parameters resource that contains an evaluation result for each expression requested */ + @WorkerThread fun evaluateLibrary( libraryUrl: String, patientId: String, @@ -210,12 +219,16 @@ class FhirOperator(fhirContext: FhirContext, fhirEngine: FhirEngine) { } /** - * The function evaluates a FHIR library against the database + * The function evaluates a FHIR library against the database. + * + * NOTE: The API may internally result in a blocking IO operation. The user should call the API + * from a worker thread or it may throw [BlockingMainThreadException] exception. * @param libraryUrl the url of the Library to evaluate * @param parameters list of parameters to be passed to the CQL library * @param expressions names of expressions in the Library to evaluate. * @return a Parameters resource that contains an evaluation result for each expression requested */ + @WorkerThread fun evaluateLibrary( libraryUrl: String, parameters: Parameters, @@ -225,13 +238,17 @@ class FhirOperator(fhirContext: FhirContext, fhirEngine: FhirEngine) { } /** - * The function evaluates a FHIR library against the database + * The function evaluates a FHIR library against the database. + * + * NOTE: The API may internally result in a blocking IO operation. The user should call the API + * from a worker thread or it may throw [BlockingMainThreadException] exception. * @param libraryUrl the url of the Library to evaluate * @param patientId the Id of the patient to be evaluated, if applicable * @param parameters list of parameters to be passed to the CQL library, if applicable * @param expressions names of expressions in the Library to evaluate. * @return a Parameters resource that contains an evaluation result for each expression requested */ + @WorkerThread fun evaluateLibrary( libraryUrl: String, patientId: String?, @@ -255,6 +272,13 @@ class FhirOperator(fhirContext: FhirContext, fhirEngine: FhirEngine) { ) } + /** + * Generates a [MeasureReport] based on the provided inputs. + * + * NOTE: The API may internally result in a blocking IO operation. The user should call the API + * from a worker thread or it may throw [BlockingMainThreadException] exception. + */ + @WorkerThread fun evaluateMeasure( measureUrl: String, start: String, @@ -279,10 +303,24 @@ class FhirOperator(fhirContext: FhirContext, fhirEngine: FhirEngine) { ) } + /** + * Generates a [CarePlan] based on the provided inputs. + * + * NOTE: The API may internally result in a blocking IO operation. The user should call the API + * from a worker thread or it may throw [BlockingMainThreadException] exception. + */ + @WorkerThread fun generateCarePlan(planDefinitionId: String, patientId: String): IBaseResource { return generateCarePlan(planDefinitionId, patientId, encounterId = null) } + /** + * Generates a [CarePlan] based on the provided inputs. + * + * NOTE: The API may internally result in a blocking IO operation. The user should call the API + * from a worker thread or it may throw [BlockingMainThreadException] exception. + */ + @WorkerThread fun generateCarePlan( planDefinitionId: String, patientId: String, diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/MoreBuilders.kt b/workflow/src/main/java/com/google/android/fhir/workflow/MoreBuilders.kt new file mode 100644 index 0000000000..c482db1f56 --- /dev/null +++ b/workflow/src/main/java/com/google/android/fhir/workflow/MoreBuilders.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.workflow + +import android.os.Looper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking + +private const val CLASS = "com.google.android.fhir.workflow.FhirOperator" + +/** + * Blocks the current thread and runs the code in a [CoroutineScope]. + * @throws BlockingMainThreadException if the calling thread is the main thread. + */ +internal fun runBlockingOrThrowMainThreadException(block: suspend (CoroutineScope) -> T): T { + if (Looper.myLooper() == Looper.getMainLooper()) { + throw BlockingMainThreadException( + "The $CLASS API has been called from the main thread resulting in a blocking call. Make sure that the $CLASS API is called from a worker thread instead. See https://developer.android.com/kotlin/coroutines for more details." + ) + } + return runBlocking { block(this) } +} + +/** + * The exception that is thrown when an application attempts to perform [runBlocking] operation its + * main thread. + */ +internal class BlockingMainThreadException(message: String) : RuntimeException(message) diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt index 6dc62dbdf1..f49fbcfeee 100644 --- a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt +++ b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt @@ -50,7 +50,7 @@ class FhirEngineDalTest { } @Test - fun testDalRead() = runBlocking { + fun testDalRead() = runBlockingOnWorkerThread { val result = fhirEngineDal.read(IdType("Patient/${testPatient.id}")) assertThat(result).isInstanceOf(Patient::class.java) @@ -58,8 +58,14 @@ class FhirEngineDalTest { .isEqualTo(testPatient.nameFirstRep.givenAsSingleString) } + @Test(expected = BlockingMainThreadException::class) + fun `testDalRead when called from main thread should throw BlockingMainThreadException`(): Unit = + runBlocking { + fhirEngineDal.read(IdType("Patient/${testPatient.id}")) + } + @Test - fun testDalCreate() = runBlocking { + fun testDalCreate() = runBlockingOnWorkerThread { val patient = Patient().apply { id = "Patient/2" @@ -73,8 +79,12 @@ class FhirEngineDalTest { .isEqualTo(patient.nameFirstRep.givenAsSingleString) } + @Test(expected = BlockingMainThreadException::class) + fun `testDalCreate when called from main thread should throw BlockingMainThreadException`(): + Unit = runBlocking { fhirEngineDal.create(testPatient) } + @Test - fun testDalUpdate() = runBlocking { + fun testDalUpdate() = runBlockingOnWorkerThread { testPatient.name = listOf(HumanName().addGiven("Eve")) fhirEngineDal.update(testPatient) @@ -83,8 +93,12 @@ class FhirEngineDalTest { assertThat(result.nameFirstRep.givenAsSingleString).isEqualTo("Eve") } + @Test(expected = BlockingMainThreadException::class) + fun `testDalUpdate when called from main thread should throw BlockingMainThreadException`(): + Unit = runBlocking { fhirEngineDal.update(testPatient) } + @Test - fun testDalDelete() = runBlocking { + fun testDalDelete() = runBlockingOnWorkerThread { fhirEngineDal.delete(testPatient.idElement) val result = fhirEngine.search {} @@ -92,6 +106,12 @@ class FhirEngineDalTest { assertThat(result).isEmpty() } + @Test(expected = BlockingMainThreadException::class) + fun `testDalDelete when called from main thread should throw BlockingMainThreadException`() = + runBlocking { + fhirEngineDal.delete(testPatient.idElement) + } + @After fun fhirEngine() = runBlocking { fhirEngine.delete(ResourceType.Patient, "Patient/1") } companion object { diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineRetrieveProviderTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineRetrieveProviderTest.kt index beb0af6131..850b4b39dc 100644 --- a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineRetrieveProviderTest.kt +++ b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineRetrieveProviderTest.kt @@ -70,8 +70,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testFilterToDataTypeDataTypeNotPresent() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) } + fun testFilterToDataTypeDataTypeNotPresent() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) assertThat( retrieveProvider @@ -95,8 +95,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testNoResultsReturnsEmptySet() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) } + fun testNoResultsReturnsEmptySet() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) val results: Iterable = retrieveProvider.retrieve( @@ -119,8 +119,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testFilterToDataType() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) } + fun testFilterToDataType() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) val resultList = retrieveProvider @@ -146,8 +146,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testFilterToContext() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) } + fun testFilterToContext() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) val resultList = retrieveProvider @@ -175,8 +175,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testFilterToContextNoContextRelation() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) } + fun testFilterToContextNoContextRelation() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) val resultList = retrieveProvider @@ -201,8 +201,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testFilterById() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) } + fun testFilterById() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) // Id does exist var codes = mutableListOf(Code().withCode("test-med")) @@ -251,8 +251,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testFilterToCodes() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) } + fun testFilterToCodes() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) // Code doesn't match var code = Code().withCode("not-a-code").withSystem("not-a-system") @@ -301,8 +301,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test(expected = TerminologyProviderException::class) - fun testFilterToValueSetNoTerminologyProvider() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) } + fun testFilterToValueSetNoTerminologyProvider(): Unit = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) retrieveProvider.retrieve( context = "Patient", @@ -321,11 +321,9 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testFilterToValueSet() { - runBlocking { - loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) - loadBundle(parseJson("/retrieve-provider/TestBundleValueSets.json")) - } + fun testFilterToValueSet() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) + loadBundle(parseJson("/retrieve-provider/TestBundleValueSets.json")) // Not in the value set var results: Iterable = @@ -368,8 +366,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testRetrieveByUrn() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleUrns.json")) } + fun testRetrieveByUrn() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleUrns.json")) var resultList = retrieveProvider @@ -415,8 +413,8 @@ class FhirEngineRetrieveProviderTest : Loadable() { } @Test - fun testRetrieveByDate() { - runBlocking { loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) } + fun testRetrieveByDate() = runBlockingOnWorkerThread { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) // searching between 2020 and 2021. val start: OffsetDateTime = OffsetDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC) @@ -465,4 +463,24 @@ class FhirEngineRetrieveProviderTest : Loadable() { assertThat(resultList.size).isEqualTo(1) assertThat(resultList.first()).isInstanceOf(Condition::class.java) } + + @Test(expected = BlockingMainThreadException::class) + fun `retrieve when called from main thread should throw BlockingMainThreadException`(): Unit = + runBlocking { + loadBundle(parseJson("/retrieve-provider/TestBundleTwoPatients.json")) + retrieveProvider.retrieve( + context = null, + contextPath = null, + contextValue = null, + dataType = null, + templateId = null, + codePath = null, + codes = null, + valueSet = null, + datePath = null, + dateLowPath = null, + dateHighPath = null, + dateRange = null + ) + } } diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineTerminologyProviderTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineTerminologyProviderTest.kt index 0845a067bc..0dd89b5ed8 100644 --- a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineTerminologyProviderTest.kt +++ b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineTerminologyProviderTest.kt @@ -23,9 +23,6 @@ import com.google.android.fhir.FhirEngineProvider import com.google.android.fhir.testing.FhirEngineProviderTestRule import com.google.android.fhir.workflow.testing.Loadable import com.google.common.truth.Truth.assertThat -import java.lang.Exception -import java.lang.IllegalArgumentException -import java.lang.UnsupportedOperationException import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.CodeSystem import org.hl7.fhir.r4.model.ValueSet @@ -60,7 +57,7 @@ class FhirEngineTerminologyProviderTest : Loadable() { @Test @Throws(Exception::class) - fun resolveByUrlUsingUrlSucceeds() { + fun resolveByUrlUsingUrlSucceeds() = runBlockingOnWorkerThread { val info: ValueSetInfo = ValueSetInfo().withId("https://cts.nlm.nih.gov/fhir/ValueSet/1.2.3.4") val response = @@ -68,13 +65,13 @@ class FhirEngineTerminologyProviderTest : Loadable() { id = "1.2.3.4" url = info.id } - runBlocking { fhirEngine.create(response) } + fhirEngine.create(response) assertThat(provider.resolveValueSetId(info)).isEqualTo(response.id) } @Test - fun resolveByUrlUsingIdentifierSucceeds() { + fun resolveByUrlUsingIdentifierSucceeds() = runBlockingOnWorkerThread { val info: ValueSetInfo = ValueSetInfo().withId("urn:oid:1.2.3.4") val response = @@ -82,36 +79,36 @@ class FhirEngineTerminologyProviderTest : Loadable() { id = "1.2.3.4" addIdentifier().value = info.id } - runBlocking { fhirEngine.create(response) } + fhirEngine.create(response) assertThat(provider.resolveValueSetId(info)).isEqualTo(response.id) } @Test - fun resolveByUrlUsingResourceIdSucceeds() { + fun resolveByUrlUsingResourceIdSucceeds() = runBlockingOnWorkerThread { val info: ValueSetInfo = ValueSetInfo().withId("1.2.3.4") val response = ValueSet().apply { id = "1.2.3.4" } - runBlocking { fhirEngine.create(response) } + fhirEngine.create(response) assertThat(provider.resolveValueSetId(info)).isEqualTo(response.id) } @Test(expected = IllegalArgumentException::class) - fun resolveByUrlNoMatchesThrowsException() { + fun resolveByUrlNoMatchesThrowsException(): Unit = runBlockingOnWorkerThread { val info: ValueSetInfo = ValueSetInfo().withId("urn:oid:1.2.3.4") provider.resolveValueSetId(info) } @Test(expected = TerminologyProviderException::class) - fun expandByUrlNoMatchesThrowsException() { + fun expandByUrlNoMatchesThrowsException(): Unit = runBlockingOnWorkerThread { val info: ValueSetInfo = ValueSetInfo().withId("urn:oid:1.2.3.4") provider.expand(info) } @Test(expected = UnsupportedOperationException::class) - fun nonNullVersionUnsupported() { + fun nonNullVersionUnsupported(): Unit = runBlockingOnWorkerThread { val info = ValueSetInfo().apply { id = "urn:oid:Test" @@ -121,7 +118,7 @@ class FhirEngineTerminologyProviderTest : Loadable() { } @Test(expected = UnsupportedOperationException::class) - fun nonNullCodeSystemsUnsupported() { + fun nonNullCodeSystemsUnsupported(): Unit = runBlockingOnWorkerThread { val codeSystem = CodeSystemInfo().apply { id = "SNOMED-CT" @@ -136,7 +133,7 @@ class FhirEngineTerminologyProviderTest : Loadable() { } @Test - fun urnOidPrefixIsStripped() { + fun urnOidPrefixIsStripped() = runBlockingOnWorkerThread { val info = ValueSetInfo().withId("urn:oid:Test") val response = @@ -145,13 +142,13 @@ class FhirEngineTerminologyProviderTest : Loadable() { expansion.containsFirstRep.setSystem(TEST_SYSTEM).code = TEST_CODE } - runBlocking { fhirEngine.create(response) } + fhirEngine.create(response) assertThat(provider.resolveValueSetId(info)).isEqualTo(response.id) } @Test(expected = IllegalArgumentException::class) - fun moreThanOneURLSearchResultIsError() { + fun moreThanOneURLSearchResultIsError(): Unit = runBlockingOnWorkerThread { val info = ValueSetInfo().withId("http://localhost/fhir/ValueSet/1.2.3.4") val response1 = @@ -166,22 +163,20 @@ class FhirEngineTerminologyProviderTest : Loadable() { url = info.id } - runBlocking { - fhirEngine.create(response1) - fhirEngine.create(response2) - } + fhirEngine.create(response1) + fhirEngine.create(response2) provider.resolveValueSetId(info) } @Test(expected = IllegalArgumentException::class) - fun zeroURLSearchResultIsError() { + fun zeroURLSearchResultIsError(): Unit = runBlockingOnWorkerThread { val info = ValueSetInfo().withId("http://localhost/fhir/ValueSet/1.2.3.4") provider.resolveValueSetId(info) } @Test - fun expandOperationReturnsCorrectCodesMoreThanZero() { + fun expandOperationReturnsCorrectCodesMoreThanZero() = runBlockingOnWorkerThread { val info = ValueSetInfo().withId("urn:oid:Test") val response = ValueSet().apply { @@ -189,7 +184,7 @@ class FhirEngineTerminologyProviderTest : Loadable() { expansion.containsFirstRep.setSystem(TEST_SYSTEM).code = TEST_CODE } - runBlocking { fhirEngine.create(response) } + fhirEngine.create(response) val list = provider.expand(info).toList() assertThat(list.size).isEqualTo(1) @@ -198,7 +193,7 @@ class FhirEngineTerminologyProviderTest : Loadable() { } @Test - fun inOperationReturnsTrueWhenFhirReturnsTrue() { + fun inOperationReturnsTrueWhenFhirReturnsTrue() = runBlockingOnWorkerThread { val info = ValueSetInfo().withId("urn:oid:Test") val response = @@ -211,7 +206,7 @@ class FhirEngineTerminologyProviderTest : Loadable() { } } - runBlocking { fhirEngine.create(response) } + fhirEngine.create(response) val code = Code().apply { @@ -223,7 +218,7 @@ class FhirEngineTerminologyProviderTest : Loadable() { } @Test - fun inOperationReturnsFalseCodeIsNotInTheValueSet() { + fun inOperationReturnsFalseCodeIsNotInTheValueSet() = runBlockingOnWorkerThread { val info = ValueSetInfo().withId("urn:oid:Test") val response = @@ -242,13 +237,13 @@ class FhirEngineTerminologyProviderTest : Loadable() { display = TEST_DISPLAY } - runBlocking { fhirEngine.create(response) } + fhirEngine.create(response) assertThat(provider.`in`(code, info)).isFalse() } @Test - fun inOperationHandlesNullSystem() { + fun inOperationHandlesNullSystem() = runBlockingOnWorkerThread { val info = ValueSetInfo().withId("urn:oid:Test") val response = @@ -266,13 +261,13 @@ class FhirEngineTerminologyProviderTest : Loadable() { display = TEST_DISPLAY } - runBlocking { fhirEngine.create(response) } + fhirEngine.create(response) assertThat(provider.`in`(code, info)).isTrue() } @Test - fun lookupOperationSuccess() { + fun lookupOperationSuccess() = runBlockingOnWorkerThread { val info = CodeSystemInfo().apply { id = TEST_SYSTEM @@ -292,7 +287,7 @@ class FhirEngineTerminologyProviderTest : Loadable() { } } - runBlocking { fhirEngine.create(codeSystem) } + fhirEngine.create(codeSystem) val result: Code = provider.lookup(code, info) assertThat(result).isNotNull() @@ -300,4 +295,40 @@ class FhirEngineTerminologyProviderTest : Loadable() { assertThat(result.code).isEqualTo(TEST_CODE) assertThat(result.display).isEqualTo(TEST_DISPLAY) } + + @Test(expected = BlockingMainThreadException::class) + fun `in when called from main thread should throw BlockingMainThreadException`(): Unit = + runBlocking { + val info = ValueSetInfo().withId("urn:oid:Test") + val code = + Code().apply { + code = TEST_CODE + display = TEST_DISPLAY + } + + provider.`in`(code, info) + } + + @Test(expected = BlockingMainThreadException::class) + fun `expand when called from main thread should throw BlockingMainThreadException`(): Unit = + runBlocking { + val info: ValueSetInfo = ValueSetInfo().withId("urn:oid:1.2.3.4") + provider.expand(info) + } + + @Test(expected = BlockingMainThreadException::class) + fun `lookup when called from main thread should throw BlockingMainThreadException`(): Unit = + runBlocking { + val info = + CodeSystemInfo().apply { + id = TEST_SYSTEM + version = TEST_SYSTEM_VERSION + } + val code = + Code().apply { + code = TEST_CODE + display = TEST_DISPLAY + } + provider.lookup(code, info) + } } diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorLibraryEvaluateJavaTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorLibraryEvaluateJavaTest.kt index 8d4ff56522..dc65637cc7 100644 --- a/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorLibraryEvaluateJavaTest.kt +++ b/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorLibraryEvaluateJavaTest.kt @@ -98,7 +98,7 @@ class FhirOperatorLibraryEvaluateJavaTest { * ``` */ @Test - fun evaluateImmunityCheck() = runBlocking { + fun evaluateImmunityCheck() = runBlockingOnWorkerThread { // Load patient val patientImmunizationHistory = load("/immunity-check/ImmunizationHistory.json") for (entry in patientImmunizationHistory.entry) { @@ -120,7 +120,7 @@ class FhirOperatorLibraryEvaluateJavaTest { } @Test - fun evaluateCQL() = runBlocking { + fun evaluateCQL() = runBlockingOnWorkerThread { @Language("CQL") val cql = """ @@ -140,7 +140,7 @@ class FhirOperatorLibraryEvaluateJavaTest { } @Test - fun evaluateCQLWithParameters() = runBlocking { + fun evaluateCQLWithParameters() = runBlockingOnWorkerThread { @Language("CQL") val cql = """ diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorTest.kt index 2f1af532a5..f2f07c8d94 100644 --- a/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorTest.kt +++ b/workflow/src/test/java/com/google/android/fhir/workflow/FhirOperatorTest.kt @@ -25,7 +25,6 @@ import com.google.android.fhir.workflow.testing.CqlBuilder import com.google.common.truth.Truth.assertThat import java.io.InputStream import java.util.TimeZone -import kotlinx.coroutines.runBlocking import org.hl7.fhir.instance.model.api.IBaseResource import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Library @@ -64,14 +63,14 @@ class FhirOperatorTest { } @Before - fun setUp() = runBlocking { + fun setUp() = runBlockingOnWorkerThread { fhirEngine = FhirEngineProvider.getInstance(ApplicationProvider.getApplicationContext()) fhirOperator = FhirOperator(fhirContext, fhirEngine) TimeZone.setDefault(TimeZone.getTimeZone("GMT")) } @Test - fun generateCarePlan() = runBlocking { + fun generateCarePlan() = runBlockingOnWorkerThread { loadBundle(libraryBundle) fhirEngine.run { loadBundle(parseJson("/plan-definition/rule-filters/RuleFilters-1.0.0-bundle.json")) @@ -95,7 +94,7 @@ class FhirOperatorTest { } @Test - fun generateCarePlanWithoutEncounter() = runBlocking { + fun generateCarePlanWithoutEncounter() = runBlockingOnWorkerThread { loadBundle(parseJson("/plan-definition/med-request/med_request_patient.json")) loadBundle(parseJson("/plan-definition/med-request/med_request_plan_definition.json")) @@ -115,7 +114,7 @@ class FhirOperatorTest { } @Test - fun evaluatePopulationMeasure() = runBlocking { + fun evaluatePopulationMeasure() = runBlockingOnWorkerThread { loadBundle(libraryBundle) fhirEngine.run { loadFile("/first-contact/01-registration/patient-charity-otala-1.json") @@ -145,7 +144,7 @@ class FhirOperatorTest { } @Test - fun evaluateGroupPopulationMeasure() = runBlocking { + fun evaluateGroupPopulationMeasure() = runBlockingOnWorkerThread { val resourceBundle = Bundle().apply { addEntry().apply { @@ -181,7 +180,7 @@ class FhirOperatorTest { } @Test - fun evaluateIndividualSubjectMeasure() = runBlocking { + fun evaluateIndividualSubjectMeasure() = runBlockingOnWorkerThread { loadBundle(libraryBundle) fhirEngine.run { loadFile("/first-contact/01-registration/patient-charity-otala-1.json") diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/MoreBuildersTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/MoreBuildersTest.kt new file mode 100644 index 0000000000..458df0478c --- /dev/null +++ b/workflow/src/test/java/com/google/android/fhir/workflow/MoreBuildersTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.workflow + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MoreBuildersTest { + + @Test(expected = BlockingMainThreadException::class) + fun `runBlockingOrThrowMainThreadException should throw exception when called from main thread`(): + Unit = runBlockingOrThrowMainThreadException { 1 + 1 } + + @Test + fun `runBlockingOrThrowMainThreadException should return 2`() = runBlockingOnWorkerThread { + val result = runBlockingOrThrowMainThreadException { 1 + 1 } + assertThat(result).isEqualTo(2) + } +} diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/TestingUtil.kt b/workflow/src/test/java/com/google/android/fhir/workflow/TestingUtil.kt new file mode 100644 index 0000000000..81db738ee2 --- /dev/null +++ b/workflow/src/test/java/com/google/android/fhir/workflow/TestingUtil.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.workflow + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking + +internal fun runBlockingOnWorkerThread(block: suspend (CoroutineScope) -> T) = + runBlocking(Dispatchers.IO) { block(this) }