diff --git a/README.md b/README.md index 6bea1386..f20e6ddf 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ See our [book](https://lca-as-code.com/book) to learn more about the language. From the source ```bash -git checkout v1.7.12 +git checkout v1.8.0 ./gradlew :cli:installDist alias lcaac=$GIT_ROOT/cli/build/install/lcaac/bin/lcaac lcaac version diff --git a/cli/src/main/resources/META-INF/lcaac.properties b/cli/src/main/resources/META-INF/lcaac.properties index 354ef0ac..da203a83 100644 --- a/cli/src/main/resources/META-INF/lcaac.properties +++ b/cli/src/main/resources/META-INF/lcaac.properties @@ -1,4 +1,4 @@ #Thu Aug 14 10:41:08 CEST 2025 author=Kleis Technology description=LCA as Code CLI -version=1.7.13 +version=1.8.0 diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/Evaluator.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/Evaluator.kt index d0fed351..7c96c307 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/Evaluator.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/Evaluator.kt @@ -2,22 +2,32 @@ package ch.kleis.lcaac.core.lang.evaluator import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.SymbolTable -import ch.kleis.lcaac.core.lang.evaluator.protocol.CachedOracle import ch.kleis.lcaac.core.lang.evaluator.protocol.Learner +import ch.kleis.lcaac.core.lang.evaluator.protocol.Oracle import ch.kleis.lcaac.core.lang.expression.* import ch.kleis.lcaac.core.lang.register.ProcessKey import ch.kleis.lcaac.core.math.Operations -import ch.kleis.lcaac.core.math.QuantityOperations +import com.mayakapps.kache.InMemoryKache +import com.mayakapps.kache.KacheStrategy +import com.mayakapps.kache.ObjectKache import org.slf4j.LoggerFactory -class Evaluator( +class Evaluator private constructor( private val symbolTable: SymbolTable, private val ops: Operations, private val sourceOps: DataSourceOperations, + private val cache: ObjectKache, EProductSpec>, EProcess> ) { @Suppress("PrivatePropertyName") private val LOG = LoggerFactory.getLogger(Evaluator::class.java) - private val oracle = CachedOracle(symbolTable, ops, sourceOps) + private val oracle = Oracle(symbolTable, ops, sourceOps, cache) + + constructor(symbolTable: SymbolTable, ops: Operations, sourceOps: DataSourceOperations) : this( + symbolTable, + ops, + sourceOps, + InMemoryKache(maxSize = 1024) { strategy = KacheStrategy.LRU } + ) fun trace(initialRequests: Set>): EvaluationTrace { val learner = Learner(initialRequests, ops) @@ -44,7 +54,7 @@ class Evaluator( mapOf(processKey to template) ) ) - return Evaluator(st, ops, sourceOps) + return Evaluator(st, ops, sourceOps, cache) } fun trace( diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Oracle.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Oracle.kt index a8d1568e..4790250b 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Oracle.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/Oracle.kt @@ -5,64 +5,34 @@ import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.evaluator.step.CompleteTerminals import ch.kleis.lcaac.core.lang.evaluator.step.Reduce import ch.kleis.lcaac.core.lang.expression.EProcess +import ch.kleis.lcaac.core.lang.expression.EProcessTemplate +import ch.kleis.lcaac.core.lang.expression.EProductSpec import ch.kleis.lcaac.core.lang.expression.ProcessAnnotation import ch.kleis.lcaac.core.lang.resolver.BareProcessResolver import ch.kleis.lcaac.core.lang.resolver.CachedProcessResolver import ch.kleis.lcaac.core.lang.resolver.ProcessTemplateResolver import ch.kleis.lcaac.core.lang.resolver.SubstanceCharacterizationResolver import ch.kleis.lcaac.core.math.Operations -import com.mayakapps.kache.InMemoryKache -import com.mayakapps.kache.KacheStrategy import com.mayakapps.kache.ObjectKache -import kotlinx.coroutines.runBlocking -interface Oracle { +class Oracle( + private val symbolTable: SymbolTable, + private val ops: Operations, + private val sourceOps: DataSourceOperations, + private val cache: ObjectKache, EProductSpec>, EProcess> +) { + private val reduceDataExpressions = Reduce(symbolTable, ops, sourceOps) + private val completeTerminals = CompleteTerminals(ops) + private val processTemplateResolver = ProcessTemplateResolver(symbolTable) + private val substanceCharacterizationResolver = SubstanceCharacterizationResolver(symbolTable) + fun answer(ports: Set>): Set> { return ports.mapNotNull { answerRequest(it) }.toSet() } - fun answerRequest(request: Request): Response? -} -class CachedOracle( - private val inner: Oracle, - private val cache: ObjectKache, Response> = InMemoryKache(maxSize = 1024) { - strategy = KacheStrategy.LRU - } -) : Oracle { - constructor( - symbolTable: SymbolTable, - ops: Operations, - sourceOps: DataSourceOperations, - cache: ObjectKache, Response> = InMemoryKache(maxSize = 1024) { - strategy = KacheStrategy.LRU - } - ): this( - inner = BareOracle(symbolTable, ops, sourceOps), - cache = cache, - ) - - override fun answerRequest(request: Request): Response? { - return runBlocking { - cache.getOrPut(request) { - inner.answerRequest(request) - } - } - } -} - -class BareOracle( - val symbolTable: SymbolTable, - val ops: Operations, - val sourceOps: DataSourceOperations, -): Oracle { - private val reduceDataExpressions = Reduce(symbolTable, ops, sourceOps) - private val completeTerminals = CompleteTerminals(ops) - private val processTemplateResolver = ProcessTemplateResolver(symbolTable) - private val substanceCharacterizationResolver = SubstanceCharacterizationResolver(symbolTable) - - override fun answerRequest(request: Request): Response? { + fun answerRequest(request: Request): Response? { return when (request) { is ProductRequest -> answerProductRequest(request) is SubstanceRequest -> answerSubstanceRequest(request) @@ -75,7 +45,7 @@ class BareOracle( val template = processTemplateResolver.resolve(spec) ?: return null val processResolver = if (template.annotations.contains(ProcessAnnotation.CACHED)) { - CachedProcessResolver(symbolTable, ops, sourceOps) + CachedProcessResolver(symbolTable, ops, sourceOps, cache) } else { BareProcessResolver(symbolTable, ops, sourceOps) } @@ -98,4 +68,4 @@ class BareOracle( private fun indexOf(productName: String, process: EProcess): Int { return process.products.indexOfFirst { it.product.name == productName } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolver.kt b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolver.kt index ac0c3d28..dc187a6c 100644 --- a/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolver.kt +++ b/core/src/main/kotlin/ch/kleis/lcaac/core/lang/resolver/ProcessResolver.kt @@ -5,6 +5,7 @@ import ch.kleis.lcaac.core.datasource.DataSourceOperations import ch.kleis.lcaac.core.lang.SymbolTable import ch.kleis.lcaac.core.lang.evaluator.EvaluationTrace import ch.kleis.lcaac.core.lang.evaluator.Evaluator +import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.evaluator.step.CompleteTerminals import ch.kleis.lcaac.core.lang.evaluator.step.Reduce import ch.kleis.lcaac.core.lang.expression.* @@ -15,40 +16,50 @@ import ch.kleis.lcaac.core.lang.value.QuantityValueOperations import ch.kleis.lcaac.core.lang.value.TechnoExchangeValue import ch.kleis.lcaac.core.math.Operations import ch.kleis.lcaac.core.matrix.ImpactFactorMatrix +import com.mayakapps.kache.InMemoryKache +import com.mayakapps.kache.KacheStrategy +import com.mayakapps.kache.ObjectKache +import kotlinx.coroutines.runBlocking interface ProcessResolver { fun resolve(template: EProcessTemplate, spec: EProductSpec): EProcess } class CachedProcessResolver( - val symbolTable: SymbolTable, - val ops: Operations, - val sourceOps: DataSourceOperations, + private val symbolTable: SymbolTable, + private val ops: Operations, + private val sourceOps: DataSourceOperations, + private val cache: ObjectKache, EProductSpec>, EProcess> ) : ProcessResolver { + override fun resolve(template: EProcessTemplate, spec: EProductSpec): EProcess { - val trace = getTrace(template, spec) - val entryPoint = trace.getEntryPoint() - val analysis = AnalysisProgram(trace.getSystemValue(), entryPoint, ops).run() - val inputQuantity = inputQuantityAnalysis(entryPoint.products, analysis.impactFactors) + return runBlocking { + cache.getOrPut(template to spec) { + val trace = getTrace(template, spec) + val entryPoint = trace.getEntryPoint() + val analysis = AnalysisProgram(trace.getSystemValue(), entryPoint, ops).run() + val inputQuantity = inputQuantityAnalysis(entryPoint.products, analysis.impactFactors) - val inputs = analysis.impactFactors.getInputProducts().map { - EMapper.toETechnoExchange(inputQuantity(it), it) - } + val inputs = analysis.impactFactors.getInputProducts().map { + EMapper.toETechnoExchange(inputQuantity(it), it) + } - val biosphere = analysis.impactFactors.getSubstances().map { - EMapper.toEBioExchange(inputQuantity(it), it) - } + val biosphere = analysis.impactFactors.getSubstances().map { + EMapper.toEBioExchange(inputQuantity(it), it) + } - val impacts = analysis.impactFactors.getIndicators().map { - EMapper.toEImpact(inputQuantity(it), it) - } + val impacts = analysis.impactFactors.getIndicators().map { + EMapper.toEImpact(inputQuantity(it), it) + } - return template.body.copy( - products = entryPoint.products.map { EMapper.toETechnoExchange(it)}, - inputs = inputs.map { ETechnoBlockEntry(it) }, - biosphere = biosphere.map { EBioBlockEntry(it) }, - impacts = impacts.map { EImpactBlockEntry(it)} - ) + template.body.copy( + products = entryPoint.products.map { EMapper.toETechnoExchange(it) }, + inputs = inputs.map { ETechnoBlockEntry(it) }, + biosphere = biosphere.map { EBioBlockEntry(it) }, + impacts = impacts.map { EImpactBlockEntry(it) } + ) + } ?: throw EvaluatorException("Failed to resolve process for spec $spec") + } } private fun getTrace(template: EProcessTemplate, spec: EProductSpec): EvaluationTrace { @@ -56,10 +67,12 @@ class CachedProcessResolver( val newAnnotations = template.annotations.filter { it != ProcessAnnotation.CACHED }.toSet() val newTemplate = template.copy(annotations = newAnnotations) - val newSymbolTable = symbolTable.copy(processTemplates = symbolTable.processTemplates.override( - ProcessKey(newTemplate.body.name, newTemplate.body.labels.mapValues { it.value.value }), - newTemplate - )) + val newSymbolTable = symbolTable.copy( + processTemplates = symbolTable.processTemplates.override( + ProcessKey(newTemplate.body.name, newTemplate.body.labels.mapValues { it.value.value }), + newTemplate + ) + ) val evaluator = Evaluator(newSymbolTable, ops, sourceOps) return evaluator.trace(newTemplate, arguments) @@ -80,9 +93,9 @@ class CachedProcessResolver( } class BareProcessResolver( - val symbolTable: SymbolTable, - val ops: Operations, - val sourceOps: DataSourceOperations, + symbolTable: SymbolTable, + ops: Operations, + sourceOps: DataSourceOperations, ) : ProcessResolver { private val reduceDataExpressions = Reduce(symbolTable, ops, sourceOps) private val completeTerminals = CompleteTerminals(ops) @@ -94,4 +107,4 @@ class BareProcessResolver( .let(reduceDataExpressions::apply) .let(completeTerminals::apply) } -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/OracleTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/OracleTest.kt index 0eb38923..04f8db5a 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/OracleTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/evaluator/protocol/OracleTest.kt @@ -25,45 +25,6 @@ import org.junit.jupiter.api.Test import kotlin.test.assertTrue class OracleTest { - @Test - fun cachedOracle() { - // given - val template = EProcessTemplate( - body = EProcess( - name = "eProcess", - products = listOf( - ETechnoExchange(QuantityFixture.oneKilogram, ProductFixture.carrot) - ), - impacts = listOf( - ImpactBlockFixture.oneClimateChange - ), - ) - ) - val symbolTable = SymbolTable( - processTemplates = ProcessTemplateRegister.from(mapOf( - ProcessKey("eProcess") to template - )) - ) - val inner = spyk(BareOracle(symbolTable, BasicOperations, mockk())) - val oracle = CachedOracle(inner = inner) - val requests = setOf( - ProductRequest( - address = Address(0, 0), - value = EProductSpec( - "carrot", - ) - ) - ) - // when - oracle.answer(requests) - oracle.answer(requests) - - // then - verify(exactly = 1) { - inner.answerRequest(any()) - } - } - @Nested inner class AnswerProductRequest { private val spec = EProductSpec("carrot") @@ -90,7 +51,7 @@ class OracleTest { ProcessKey("eProcess") to template )) ) - val oracle = spyk(BareOracle(symbolTable, BasicOperations, mockk())) + val oracle = spyk(Oracle(symbolTable, BasicOperations, mockk(), mockk())) mockkConstructor(CachedProcessResolver::class) every { @@ -128,7 +89,7 @@ class OracleTest { ProcessKey("eProcess") to template )) ) - val oracle = spyk(BareOracle(symbolTable, BasicOperations, mockk())) + val oracle = spyk(Oracle(symbolTable, BasicOperations, mockk(), mockk())) mockkConstructor(BareProcessResolver::class) every { diff --git a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/CachedProcessResolverTest.kt b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/CachedProcessResolverTest.kt index d5295308..784c78a5 100644 --- a/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/CachedProcessResolverTest.kt +++ b/core/src/test/kotlin/ch/kleis/lcaac/core/lang/resolver/CachedProcessResolverTest.kt @@ -21,7 +21,13 @@ import ch.kleis.lcaac.core.lang.register.ProcessKey import ch.kleis.lcaac.core.lang.register.ProcessTemplateRegister import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.math.basic.BasicOperations +import com.mayakapps.kache.InMemoryKache +import com.mayakapps.kache.KacheStrategy import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Nested import kotlin.test.Test import kotlin.test.assertEquals @@ -29,230 +35,289 @@ class CachedProcessResolverTest { private val ops = BasicOperations private val sourceOps = mockk>() - @Test - fun `when process without dependencies should return same process`() { - // Given - val template = EProcessTemplate( - body = EProcess( - name = "carrot_production", - products = listOf( - ETechnoExchange(twoKilograms, carrot, fiftyPercent), - ETechnoExchange(threeKilograms, salad, fiftyPercent), - ), - inputs = listOf( - ETechnoBlockEntry(ETechnoExchange(twoLitres, water)), - ), - biosphere = listOf( - EBioBlockEntry(EBioExchange(oneKilogram, propanol)) - ), - impacts = listOf( - EImpactBlockEntry(EImpact(oneUnit, climateChange)), + @Nested + inner class Resolve { + @Test + fun `when process without dependencies should return same process`() { + // Given + val cache = InMemoryKache, EProductSpec>, EProcess>( + maxSize = 1024 + ) { + strategy = KacheStrategy.LRU + } + val template = EProcessTemplate( + body = EProcess( + name = "carrot_production", + products = listOf( + ETechnoExchange(twoKilograms, carrot, fiftyPercent), + ETechnoExchange(threeKilograms, salad, fiftyPercent), + ), + inputs = listOf( + ETechnoBlockEntry(ETechnoExchange(twoLitres, water)), + ), + biosphere = listOf( + EBioBlockEntry(EBioExchange(oneKilogram, propanol)) + ), + impacts = listOf( + EImpactBlockEntry(EImpact(oneUnit, climateChange)), + ) ) ) - ) - val spec = carrot - val symbolTable = SymbolTable( - processTemplates = ProcessTemplateRegister.from( - mapOf( - ProcessKey("carrot_production") to template, + val spec = carrot + val symbolTable = SymbolTable( + processTemplates = ProcessTemplateRegister.from( + mapOf( + ProcessKey("carrot_production") to template, + ) ) ) - ) - val sut = CachedProcessResolver(symbolTable, ops, sourceOps) + val sut = CachedProcessResolver(symbolTable, ops, sourceOps, cache) - // When - val actual = sut.resolve(template, spec) + // When + val actual = sut.resolve(template, spec) - // Then - assertEquals(2, actual.products.size) - assertEquals(twoKilograms, actual.products[0].quantity) - assertEquals(carrot.name, actual.products[0].product.name) - assertEquals(threeKilograms, actual.products[1].quantity) - assertEquals(salad.name, actual.products[1].product.name) + // Then + assertEquals(2, actual.products.size) + assertEquals(twoKilograms, actual.products[0].quantity) + assertEquals(carrot.name, actual.products[0].product.name) + assertEquals(threeKilograms, actual.products[1].quantity) + assertEquals(salad.name, actual.products[1].product.name) - assertEquals(1, actual.inputs.size) - assertEquals(twoLitres, (actual.inputs[0] as ETechnoBlockEntry).entry.quantity) - assertEquals(water.name, (actual.inputs[0] as ETechnoBlockEntry).entry.product.name) + assertEquals(1, actual.inputs.size) + assertEquals(twoLitres, (actual.inputs[0] as ETechnoBlockEntry).entry.quantity) + assertEquals(water.name, (actual.inputs[0] as ETechnoBlockEntry).entry.product.name) - assertEquals(1, actual.biosphere.size) - assertEquals(oneKilogram, (actual.biosphere[0] as EBioBlockEntry).entry.quantity) - assertEquals(propanol.name, (actual.biosphere[0] as EBioBlockEntry).entry.substance.name) + assertEquals(1, actual.biosphere.size) + assertEquals(oneKilogram, (actual.biosphere[0] as EBioBlockEntry).entry.quantity) + assertEquals(propanol.name, (actual.biosphere[0] as EBioBlockEntry).entry.substance.name) - assertEquals(1, actual.impacts.size) - assertEquals(oneUnit, (actual.impacts[0] as EImpactBlockEntry).entry.quantity) - assertEquals(climateChange.name, (actual.impacts[0] as EImpactBlockEntry).entry.indicator.name) - } + assertEquals(1, actual.impacts.size) + assertEquals(oneUnit, (actual.impacts[0] as EImpactBlockEntry).entry.quantity) + assertEquals(climateChange.name, (actual.impacts[0] as EImpactBlockEntry).entry.indicator.name) + } - @Test - fun `when process with dependencies should return process without dependencies`() { - // Given - val template = EProcessTemplate( - body = EProcess( - name = "carrot_production", - products = listOf( - ETechnoExchange(twoKilograms, carrot, fiftyPercent), - ETechnoExchange(threeKilograms, salad, fiftyPercent), - ), - inputs = listOf( - ETechnoBlockEntry(ETechnoExchange(twoLitres, water)), - ), - biosphere = listOf( - EBioBlockEntry(EBioExchange(oneKilogram, propanol)) - ), - impacts = listOf( - EImpactBlockEntry(EImpact(oneUnit, climateChange)), + @Test + fun `when process with dependencies should return process without dependencies`() { + // Given + val cache = InMemoryKache, EProductSpec>, EProcess>( + maxSize = 1024 + ) { + strategy = KacheStrategy.LRU + } + val template = EProcessTemplate( + body = EProcess( + name = "carrot_production", + products = listOf( + ETechnoExchange(twoKilograms, carrot, fiftyPercent), + ETechnoExchange(threeKilograms, salad, fiftyPercent), + ), + inputs = listOf( + ETechnoBlockEntry(ETechnoExchange(twoLitres, water)), + ), + biosphere = listOf( + EBioBlockEntry(EBioExchange(oneKilogram, propanol)) + ), + impacts = listOf( + EImpactBlockEntry(EImpact(oneUnit, climateChange)), + ) ) ) - ) - val waterTemplate = EProcessTemplate( - body = EProcess( - name = "water_production", - products = listOf( - ETechnoExchange(oneLitre, water) - ), - inputs = listOf( - ETechnoBlockEntry( - ETechnoExchange( - twoKilograms, EProductSpec( - "detergent", oneKilogram + val waterTemplate = EProcessTemplate( + body = EProcess( + name = "water_production", + products = listOf( + ETechnoExchange(oneLitre, water) + ), + inputs = listOf( + ETechnoBlockEntry( + ETechnoExchange( + twoKilograms, EProductSpec( + "detergent", oneKilogram + ) ) - ) + ), + ), + biosphere = listOf( + EBioBlockEntry(EBioExchange(twoKilograms, propanol)) ), - ), - biosphere = listOf( - EBioBlockEntry(EBioExchange(twoKilograms, propanol)) - ), - impacts = listOf( - EImpactBlockEntry(EImpact(twoUnits, climateChange)), + impacts = listOf( + EImpactBlockEntry(EImpact(twoUnits, climateChange)), + ) ) ) - ) - val spec = carrot - val symbolTable = SymbolTable( - processTemplates = ProcessTemplateRegister.from( - mapOf( - ProcessKey("carrot_production") to template, - ProcessKey("water_production") to waterTemplate + val spec = carrot + val symbolTable = SymbolTable( + processTemplates = ProcessTemplateRegister.from( + mapOf( + ProcessKey("carrot_production") to template, + ProcessKey("water_production") to waterTemplate + ) ) ) - ) - val sut = CachedProcessResolver(symbolTable, ops, sourceOps) + val sut = CachedProcessResolver(symbolTable, ops, sourceOps, cache) - // When - val actual = sut.resolve(template, spec) + // When + val actual = sut.resolve(template, spec) - // Then - assertEquals(2, actual.products.size) - assertEquals(twoKilograms, actual.products[0].quantity) - assertEquals(carrot.name, actual.products[0].product.name) - assertEquals(threeKilograms, actual.products[1].quantity) - assertEquals(salad.name, actual.products[1].product.name) + // Then + assertEquals(2, actual.products.size) + assertEquals(twoKilograms, actual.products[0].quantity) + assertEquals(carrot.name, actual.products[0].product.name) + assertEquals(threeKilograms, actual.products[1].quantity) + assertEquals(salad.name, actual.products[1].product.name) - assertEquals(1, actual.inputs.size) - assertEquals( - EQuantityScale( - ops.pure(4.0), - UnitFixture.kg - ), (actual.inputs[0] as ETechnoBlockEntry).entry.quantity - ) - assertEquals("detergent", (actual.inputs[0] as ETechnoBlockEntry).entry.product.name) + assertEquals(1, actual.inputs.size) + assertEquals( + EQuantityScale( + ops.pure(4.0), + UnitFixture.kg + ), (actual.inputs[0] as ETechnoBlockEntry).entry.quantity + ) + assertEquals("detergent", (actual.inputs[0] as ETechnoBlockEntry).entry.product.name) - assertEquals(1, actual.biosphere.size) - // 2 * 2 propanol from water biosphere + 1 propanol from carrot biosphere - assertEquals( - EQuantityScale( - ops.pure(5.0), - UnitFixture.kg - ), (actual.biosphere[0] as EBioBlockEntry).entry.quantity - ) - assertEquals(propanol.name, (actual.biosphere[0] as EBioBlockEntry).entry.substance.name) + assertEquals(1, actual.biosphere.size) + // 2 * 2 propanol from water biosphere + 1 propanol from carrot biosphere + assertEquals( + EQuantityScale( + ops.pure(5.0), + UnitFixture.kg + ), (actual.biosphere[0] as EBioBlockEntry).entry.quantity + ) + assertEquals(propanol.name, (actual.biosphere[0] as EBioBlockEntry).entry.substance.name) - assertEquals(1, actual.impacts.size) - assertEquals( - // 2 * 2 climate change from water impact + 1 climate change from carrot impact - EQuantityScale( - ops.pure(5.0), - UnitFixture.unit - ), (actual.impacts[0] as EImpactBlockEntry).entry.quantity - ) - assertEquals(climateChange.name, (actual.impacts[0] as EImpactBlockEntry).entry.indicator.name) - } + assertEquals(1, actual.impacts.size) + assertEquals( + // 2 * 2 climate change from water impact + 1 climate change from carrot impact + EQuantityScale( + ops.pure(5.0), + UnitFixture.unit + ), (actual.impacts[0] as EImpactBlockEntry).entry.quantity + ) + assertEquals(climateChange.name, (actual.impacts[0] as EImpactBlockEntry).entry.indicator.name) + } - @Test - fun `when process with deep dependencies should return process without dependencies`() { - // Given - val template = EProcessTemplate( - body = EProcess( - name = "carrot_production", - products = listOf( - ETechnoExchange(twoKilograms, carrot, fiftyPercent), - ETechnoExchange(threeKilograms, salad, fiftyPercent), - ), - inputs = listOf( - ETechnoBlockEntry(ETechnoExchange(twoLitres, water)), + @Test + fun `when process with deep dependencies should return process without dependencies`() { + // Given + val cache = InMemoryKache, EProductSpec>, EProcess>( + maxSize = 1024 + ) { + strategy = KacheStrategy.LRU + } + val template = EProcessTemplate( + body = EProcess( + name = "carrot_production", + products = listOf( + ETechnoExchange(twoKilograms, carrot, fiftyPercent), + ETechnoExchange(threeKilograms, salad, fiftyPercent), + ), + inputs = listOf( + ETechnoBlockEntry(ETechnoExchange(twoLitres, water)), + ) ) ) - ) - val detergent = EProductSpec("detergent", oneKilogram) - val waterTemplate = EProcessTemplate( - body = EProcess( - name = "water_production", - products = listOf( - ETechnoExchange(oneLitre, water) - ), - inputs = listOf( - ETechnoBlockEntry(ETechnoExchange(twoKilograms, detergent)), + val detergent = EProductSpec("detergent", oneKilogram) + val waterTemplate = EProcessTemplate( + body = EProcess( + name = "water_production", + products = listOf( + ETechnoExchange(oneLitre, water) + ), + inputs = listOf( + ETechnoBlockEntry(ETechnoExchange(twoKilograms, detergent)), + ) ) ) - ) - val detergentTemplate = EProcessTemplate( - body = EProcess( - name = "detergent_production", - products = listOf( - ETechnoExchange(oneKilogram, detergent), - ), - impacts = listOf( - EImpactBlockEntry(EImpact(twoUnits, climateChange)), + val detergentTemplate = EProcessTemplate( + body = EProcess( + name = "detergent_production", + products = listOf( + ETechnoExchange(oneKilogram, detergent), + ), + impacts = listOf( + EImpactBlockEntry(EImpact(twoUnits, climateChange)), + ) ) ) - ) - val spec = carrot - val symbolTable = SymbolTable( - processTemplates = ProcessTemplateRegister.from( - mapOf( - ProcessKey("carrot_production") to template, - ProcessKey("water_production") to waterTemplate, - ProcessKey("detergent_production") to detergentTemplate + val spec = carrot + val symbolTable = SymbolTable( + processTemplates = ProcessTemplateRegister.from( + mapOf( + ProcessKey("carrot_production") to template, + ProcessKey("water_production") to waterTemplate, + ProcessKey("detergent_production") to detergentTemplate + ) ) ) - ) - val sut = CachedProcessResolver(symbolTable, ops, sourceOps) + val sut = CachedProcessResolver(symbolTable, ops, sourceOps, cache) + + // When + val actual = sut.resolve(template, spec) - // When - val actual = sut.resolve(template, spec) + // Then + assertEquals(2, actual.products.size) + assertEquals(twoKilograms, actual.products[0].quantity) + assertEquals(carrot.name, actual.products[0].product.name) + assertEquals(threeKilograms, actual.products[1].quantity) + assertEquals(salad.name, actual.products[1].product.name) + + assertEquals(0, actual.inputs.size) + assertEquals(0, actual.biosphere.size) + + assertEquals(1, actual.impacts.size) + assertEquals( + // 2 * 2 * 2 climate change from detergent impact + EQuantityScale( + ops.pure(8.0), + UnitFixture.unit + ), (actual.impacts[0] as EImpactBlockEntry).entry.quantity + ) + assertEquals(climateChange.name, (actual.impacts[0] as EImpactBlockEntry).entry.indicator.name) + } + + @Test + fun `when resolve cached process twice should use cache`() { + val cache = spyk(InMemoryKache, EProductSpec>, EProcess>( + maxSize = 1024 + ) { + strategy = KacheStrategy.LRU + }) + val template = EProcessTemplate( + body = EProcess( + name = "carrot_production", + products = listOf( + ETechnoExchange(twoKilograms, carrot), + ), + inputs = listOf(), + biosphere = listOf(), + impacts = listOf() + ) + ) + val spec = carrot + val symbolTable = SymbolTable( + processTemplates = ProcessTemplateRegister.from( + mapOf( + ProcessKey("carrot_production") to template, + ) + ) + ) - // Then - assertEquals(2, actual.products.size) - assertEquals(twoKilograms, actual.products[0].quantity) - assertEquals(carrot.name, actual.products[0].product.name) - assertEquals(threeKilograms, actual.products[1].quantity) - assertEquals(salad.name, actual.products[1].product.name) + val sut = CachedProcessResolver(symbolTable, ops, sourceOps, cache) - assertEquals(0, actual.inputs.size) - assertEquals(0, actual.biosphere.size) + // When + sut.resolve(template, spec) + sut.resolve(template, spec) - assertEquals(1, actual.impacts.size) - assertEquals( - // 2 * 2 * 2 climate change from detergent impact - EQuantityScale( - ops.pure(8.0), - UnitFixture.unit - ), (actual.impacts[0] as EImpactBlockEntry).entry.quantity - ) - assertEquals(climateChange.name, (actual.impacts[0] as EImpactBlockEntry).entry.indicator.name) + // Then + verify(exactly = 2) { + runBlocking { + cache.getOrPut(template to spec, any()) + } + } + } } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 01040e09..e29bb368 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,4 +2,4 @@ javaVersion=17 gradleVersion=7.6 org.gradle.jvmargs=-Xmx4096m lcaacGroup=ch.kleis.lcaac -lcaacVersion=1.7.13 +lcaacVersion=1.8.0