diff --git a/README.md b/README.md index c5326f3c..435f4ddf 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.8.1 +git checkout v2.0.0 ./gradlew :cli:installDist alias lcaac=$GIT_ROOT/cli/build/install/lcaac/bin/lcaac lcaac version diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 1048554c..626a5b65 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -47,6 +47,8 @@ dependencies { implementation("org.apache.commons:commons-csv:1.10.0") implementation("com.charleskorn.kaml:kaml:0.59.0") + + implementation("org.apache.commons:commons-compress:1.28.0") } tasks.build { diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommand.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommand.kt index bbc6c89c..1bea377c 100644 --- a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommand.kt +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommand.kt @@ -1,73 +1,41 @@ package ch.kleis.lcaac.cli.cmd -import ch.kleis.lcaac.cli.csv.assess.AssessCsvProcessor import ch.kleis.lcaac.cli.csv.CsvRequest import ch.kleis.lcaac.cli.csv.CsvRequestReader +import ch.kleis.lcaac.cli.csv.assess.AssessCsvProcessor import ch.kleis.lcaac.cli.csv.assess.AssessCsvResultWriter -import ch.kleis.lcaac.core.config.LcaacConfig import ch.kleis.lcaac.core.math.basic.BasicOperations import ch.kleis.lcaac.grammar.Loader import ch.kleis.lcaac.grammar.LoaderOption -import com.charleskorn.kaml.decodeFromStream import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.help -import com.github.ajalt.clikt.parameters.options.associate -import com.github.ajalt.clikt.parameters.options.default -import com.github.ajalt.clikt.parameters.options.help -import com.github.ajalt.clikt.parameters.options.option -import com.github.ajalt.clikt.parameters.types.file import java.io.File +const val assessCommandName = "assess" + @Suppress("MemberVisibilityCanBePrivate", "DuplicatedCode") -class AssessCommand : CliktCommand(name = "assess", help = "Returns the unitary impacts of a process in CSV format") { +class AssessCommand : CliktCommand(name = assessCommandName, help = "Returns the unitary impacts of a process in CSV format") { val name: String by argument().help("Process name") - val labels: Map by option("-l", "--label") - .help( - """ - Specify a process label as a key value pair. - Example: lcaac assess -l model="ABC" -l geo="FR". - """.trimIndent()) - .associate() - private val getProjectPath = option("-p", "--project").file() - .default(File(defaultLcaacFilename)) - .help("Path to project folder or yaml file.") - val projectPath: File by getProjectPath - - val file: File? by option("-f", "--file").file(canBeDir = false) - .help(""" - CSV file with parameter values. - Example: `lcaac assess -f params.csv`. - """.trimIndent()) - val arguments: Map by option("-D", "--parameter") - .help( - """ - Override parameter value as a key value pair. - Example: `lcaac assess -D x="12 kg" -D geo="UK" -f params.csv`. - """.trimIndent()) - .associate() - val globals: Map by option("-G", "--global") - .help( - """ - Override global variable as a key value pair. - Example: `lcaac assess -G x="12 kg"`. - """.trimIndent() - ).associate() + val configFile: File by configFileOption() + val source: File by sourceOption() + val file: File? by fileOption(assessCommandName) + val labels: Map by labelsOption(assessCommandName) + val arguments: Map by argumentsOption(assessCommandName) + val globals: Map by globalsOption(assessCommandName) override fun run() { - val (workingDirectory, lcaacConfigFile) = parseProjectPath(projectPath) - val yamlConfig = if (lcaacConfigFile.exists()) projectPath.inputStream().use { - yaml.decodeFromStream(LcaacConfig.serializer(), it) - } - else LcaacConfig() + val sourceDirectory = parseSource(source) + val projectDirectory = configFile.parentFile + val yamlConfig = parseLcaacConfig(configFile) - val files = lcaFiles(workingDirectory) + val files = lcaFiles(sourceDirectory) val symbolTable = Loader( ops = BasicOperations, overriddenGlobals = dataExpressionMap(BasicOperations, globals), ).load(files, listOf(LoaderOption.WITH_PRELUDE)) - val processor = AssessCsvProcessor(yamlConfig, symbolTable, workingDirectory.path) + val processor = AssessCsvProcessor(yamlConfig, symbolTable, projectDirectory.path) val iterator = loadRequests() val writer = AssessCsvResultWriter() var first = true diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/SharedOptions.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/SharedOptions.kt new file mode 100644 index 00000000..cfe0fa1d --- /dev/null +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/SharedOptions.kt @@ -0,0 +1,58 @@ +package ch.kleis.lcaac.cli.cmd + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.associate +import com.github.ajalt.clikt.parameters.options.convert +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.help +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.file +import java.io.File +import kotlin.io.path.Path + +fun CliktCommand.configFileOption() = option("-c", "--config", help = "Path to LCAAC config file") + .file(canBeDir = false) + .convert { it.absoluteFile } // allow to retrieve its parent file when a relative path is given (useful for finding the project directory) + .default(File("./$defaultLcaacFilename")) + .help(""" + Path to LCAAC config file. Defaults to 'lcaac.yaml' + The location of the config file is the project directory. Defaults to current working directory. + """.trimIndent()) + +fun CliktCommand.sourceOption() = option("-s", "--source", help = "Path to LCA source folder or zip/tar.gz/tgz file") + .file() + .default(Path(".").toFile()) + .help("Path to LCAAC source folder or zip/tar.gz/tgz file. Defaults to current working directory.") + +fun CliktCommand.fileOption(commandName: String) = option("-f", "--file") + .file(canBeDir = false) + .help(""" + CSV file with parameter values. + Example: `lcaac $commandName -f params.csv` + """.trimIndent() + ) + +fun CliktCommand.labelsOption(commandName: String) = option("-l", "--label") + .help(""" + Specify a process label as a key value pair. + Example: lcaac $commandName -l model="ABC" -l geo="FR". + """.trimIndent()) + .associate() + +fun CliktCommand.argumentsOption(commandName: String) = option("-D", "--parameter") + .help( + """ + Override parameter value as a key value pair. + Example: `lcaac $commandName -D x="12 kg" -D geo="UK" -f params.csv`. + """.trimIndent()) + .associate() + +fun CliktCommand.globalsOption(commandName: String) = option("-G", "--global") + .help( + """ + Override global variable as a key value pair. + Example: `lcaac $commandName -G x="12 kg"`. + """.trimIndent() + ).associate() + + diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TestCommand.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TestCommand.kt index ac9136c8..d1fb8bde 100644 --- a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TestCommand.kt +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TestCommand.kt @@ -1,6 +1,5 @@ package ch.kleis.lcaac.cli.cmd -import ch.kleis.lcaac.core.config.LcaacConfig import ch.kleis.lcaac.core.datasource.ConnectorFactory import ch.kleis.lcaac.core.datasource.DefaultDataSourceOperations import ch.kleis.lcaac.core.datasource.csv.CsvConnectorBuilder @@ -13,62 +12,46 @@ import ch.kleis.lcaac.grammar.CoreTestMapper import ch.kleis.lcaac.grammar.Loader import ch.kleis.lcaac.grammar.LoaderOption import ch.kleis.lcaac.grammar.parser.LcaLangParser -import com.charleskorn.kaml.decodeFromStream import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.ProgramResult import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.default import com.github.ajalt.clikt.parameters.arguments.help -import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.option -import com.github.ajalt.clikt.parameters.types.file import java.io.File private const val greenTick = "\u2705" private const val redCross = "\u274C" +const val testCommandName = "test" + @Suppress("MemberVisibilityCanBePrivate", "DuplicatedCode") -class TestCommand : CliktCommand(name = "test", help = "Run specified tests") { +class TestCommand : CliktCommand(name = testCommandName, help = "Run specified tests") { val name: String by argument().help("Process name").default("") - - private val getProjectPath = option("-p", "--project").file() - .default(File(defaultLcaacFilename)) - .help("Path to project folder or yaml file.") - val projectPath: File by getProjectPath - - val file: File? by option("-f", "--file").file(canBeDir = false) - .help(""" - CSV file with parameter values. - Example: `lcaac assess -f params.csv`. - """.trimIndent()) + val configFile: File by configFileOption() + val source: File by sourceOption() + val file: File? by fileOption(testCommandName) val showSuccess: Boolean by option("--show-success").flag(default = false).help("Show successful assertions") override fun run() { - val (workingDirectory, lcaacConfigFile) = parseProjectPath(projectPath) - - val yamlConfig = if (lcaacConfigFile.exists()) projectPath.inputStream().use { - yaml.decodeFromStream(LcaacConfig.serializer(), it) - } - else LcaacConfig() + val sourceDirectory = parseSource(source) + val projectDirectory = configFile.parentFile + val yamlConfig = parseLcaacConfig(configFile) val ops = BasicOperations - val files = lcaFiles(workingDirectory) + val files = lcaFiles(sourceDirectory) val symbolTable = Loader(ops).load(files, listOf(LoaderOption.WITH_PRELUDE)) val factory = ConnectorFactory( - workingDirectory.path, + projectDirectory.path, yamlConfig, ops, symbolTable, listOf(CsvConnectorBuilder()) ) - val sourceOps = DefaultDataSourceOperations( - ops, - yamlConfig, - factory.buildConnectors(), - ) + val sourceOps = DefaultDataSourceOperations(ops,yamlConfig, factory.buildConnectors()) val mapper = CoreTestMapper() val cases = files diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TraceCommand.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TraceCommand.kt index 815ac0b6..e1084427 100644 --- a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TraceCommand.kt +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/TraceCommand.kt @@ -4,11 +4,9 @@ import ch.kleis.lcaac.cli.csv.CsvRequest import ch.kleis.lcaac.cli.csv.CsvRequestReader import ch.kleis.lcaac.cli.csv.trace.TraceCsvProcessor import ch.kleis.lcaac.cli.csv.trace.TraceCsvResultWriter -import ch.kleis.lcaac.core.config.LcaacConfig import ch.kleis.lcaac.core.math.basic.BasicOperations import ch.kleis.lcaac.grammar.Loader import ch.kleis.lcaac.grammar.LoaderOption -import com.charleskorn.kaml.decodeFromStream import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.help @@ -18,56 +16,32 @@ import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.file import java.io.File +import kotlin.io.path.Path + +const val traceCommandName = "trace" @Suppress("MemberVisibilityCanBePrivate", "DuplicatedCode") -class TraceCommand : CliktCommand(name = "trace", help = "Trace the contributions") { +class TraceCommand : CliktCommand(name = traceCommandName, help = "Trace the contributions") { val name: String by argument().help("Process name") - val labels: Map by option("-l", "--label") - .help( - """ - Specify a process label as a key value pair. - Example: lcaac assess -l model="ABC" -l geo="FR". - """.trimIndent()) - .associate() - private val getProjectPath = option("-p", "--project").file() - .default(File(defaultLcaacFilename)) - .help("Path to project folder or yaml file.") - val projectPath: File by getProjectPath - - val file: File? by option("-f", "--file").file(canBeDir = false) - .help(""" - CSV file with parameter values. - Example: `lcaac trace -f params.csv`. - """.trimIndent()) - val arguments: Map by option("-D", "--parameter") - .help( - """ - Override parameter value as a key value pair. - Example: `lcaac assess -D x="12 kg" -D geo="UK" -f params.csv`. - """.trimIndent()) - .associate() - val globals: Map by option("-G", "--global") - .help( - """ - Override global variable as a key value pair. - Example: `lcaac assess -G x="12 kg"`. - """.trimIndent() - ).associate() + val configFile: File by configFileOption() + val source: File by sourceOption() + val file: File? by fileOption(traceCommandName) + val labels: Map by labelsOption(traceCommandName) + val arguments: Map by argumentsOption(traceCommandName) + val globals: Map by globalsOption(traceCommandName) override fun run() { - val (workingDirectory, lcaacConfigFile) = parseProjectPath(projectPath) - val yamlConfig = if (lcaacConfigFile.exists()) projectPath.inputStream().use { - yaml.decodeFromStream(LcaacConfig.serializer(), it) - } - else LcaacConfig() + val sourceDirectory = parseSource(source) + val projectDirectory = configFile.parentFile + val yamlConfig = parseLcaacConfig(configFile) - val files = lcaFiles(workingDirectory) + val files = lcaFiles(sourceDirectory) val symbolTable = Loader( ops = BasicOperations, overriddenGlobals = dataExpressionMap(BasicOperations, globals), ).load(files, listOf(LoaderOption.WITH_PRELUDE)) - val processor = TraceCsvProcessor(yamlConfig, symbolTable, workingDirectory.path) + val processor = TraceCsvProcessor(yamlConfig, symbolTable, projectDirectory.path) val iterator = loadRequests() val writer = TraceCsvResultWriter() var first = true diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt index 44568ab2..550020ab 100644 --- a/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/cmd/Utils.kt @@ -1,5 +1,6 @@ package ch.kleis.lcaac.cli.cmd +import ch.kleis.lcaac.core.config.LcaacConfig import ch.kleis.lcaac.core.lang.evaluator.EvaluatorException import ch.kleis.lcaac.core.lang.evaluator.reducer.DataExpressionReducer import ch.kleis.lcaac.core.lang.expression.* @@ -14,27 +15,68 @@ import ch.kleis.lcaac.grammar.parser.LcaLangLexer import ch.kleis.lcaac.grammar.parser.LcaLangParser import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration +import com.charleskorn.kaml.decodeFromStream import org.antlr.v4.runtime.CharStreams import org.antlr.v4.runtime.CommonTokenStream +import org.apache.commons.compress.archivers.examples.Expander +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream +import java.io.BufferedInputStream import java.io.File +import java.io.FileInputStream import java.io.InputStream import java.lang.Double.parseDouble import java.nio.file.Files -import kotlin.io.path.Path import kotlin.io.path.isRegularFile + val yaml = Yaml(configuration = YamlConfiguration( strictMode = false )) const val defaultLcaacFilename = "lcaac.yaml" -fun parseProjectPath(path: File): Pair { - if (path.isDirectory) { - val configFile = Path(defaultLcaacFilename).toFile() - return path to configFile +fun parseSource(path: File): File { + if (path.isDirectory) return path + + val name = path.name.lowercase() + return when { + name.endsWith(".zip") -> extractZip(path) + name.endsWith(".tar.gz") || name.endsWith(".tgz") -> extractTarGz(path) + else -> + error("Unsupported file format: ${path.name}. Supported file formats are zip, tar.gz and tgz.") + } +} + +private fun extractZip(zipFile: File): File { + val outputDir = Files.createTempDirectory("lca_zip_source_").toFile() + Expander().expand(zipFile, outputDir) + return outputDir +} + +private fun extractTarGz(tarGzFile: File): File { + val outputDir = Files.createTempDirectory("lca_targz_source_").toFile() + TarArchiveInputStream(GzipCompressorInputStream(BufferedInputStream(FileInputStream(tarGzFile)))).use { tarInput -> + var entry = tarInput.nextEntry + while (entry != null) { + val outputFile = File(outputDir, entry.name) + if (entry.isDirectory) { + outputFile.mkdirs() + } else { + outputFile.parentFile.mkdirs() + outputFile.outputStream().use { output -> + tarInput.copyTo(output) + } + } + entry = tarInput.nextEntry + } } - val workingDirectory = path.parentFile ?: Path(".").toFile() - return workingDirectory to path + return outputDir +} + +fun parseLcaacConfig(path: File): LcaacConfig { + return if (path.exists()) path.inputStream().use { + yaml.decodeFromStream(LcaacConfig.serializer(), it) + } else LcaacConfig() } fun lcaFiles(root: File): Sequence { diff --git a/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/assess/AssessCsvProcessor.kt b/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/assess/AssessCsvProcessor.kt index e2104df3..c400f6f1 100644 --- a/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/assess/AssessCsvProcessor.kt +++ b/cli/src/main/kotlin/ch/kleis/lcaac/cli/csv/assess/AssessCsvProcessor.kt @@ -17,17 +17,17 @@ import ch.kleis.lcaac.core.math.basic.BasicOperations class AssessCsvProcessor( config: LcaacConfig, private val symbolTable: SymbolTable, - workingDirectory: String, + projectDirectory: String, ) { private val ops = BasicOperations private val factory = ConnectorFactory( - workingDirectory, + projectDirectory, config, ops, symbolTable, listOf(CsvConnectorBuilder()) ) - private val sourceOps = DefaultDataSourceOperations(ops, config, factory.buildConnectors()) + private val sourceOps = DefaultDataSourceOperations(ops, config,factory.buildConnectors()) private val dataReducer = DataExpressionReducer(symbolTable.data, symbolTable.dataSources, ops, sourceOps) private val evaluator = Evaluator(symbolTable, ops, sourceOps) diff --git a/cli/src/main/resources/META-INF/lcaac.properties b/cli/src/main/resources/META-INF/lcaac.properties index f095f5b2..9c39f8ba 100644 --- a/cli/src/main/resources/META-INF/lcaac.properties +++ b/cli/src/main/resources/META-INF/lcaac.properties @@ -1,3 +1,3 @@ author=Kleis Technology description=LCA as Code CLI -version=1.8.1 +version=2.0.0 diff --git a/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommandTest.kt b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommandTest.kt new file mode 100644 index 00000000..e1370563 --- /dev/null +++ b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/AssessCommandTest.kt @@ -0,0 +1,42 @@ +package ch.kleis.lcaac.cli.cmd + +import com.github.ajalt.clikt.testing.test +import kotlin.test.Test +import kotlin.test.assertEquals + +class AssessCommandTest { + @Test + fun `simple assessment`() { + // given + val cmd = AssessCommand() + val argv = arrayOf("-s", "src/test/resources/main.zip", "main") + + // when + val result = cmd.test(argv) + + // then + assertEquals("product,amount,reference unit,grass,grass_unit\n" + + "bread,1.0,kg,2.0,kg\n", + result.output + ) + } + + @Test + fun `assessment with data and src split`() { + // given + val cmd = AssessCommand() + val argv = arrayOf( + "-s", "src/test/resources/data-src-split/src", + "-c", "src/test/resources/data-src-split/lcaac.yaml", + "main") + + // when + val result = cmd.test(argv) + + // then + assertEquals("product,amount,reference unit,foo_fn,foo_fn_unit\n" + + "main,1.0,u,6.0,u\n", + result.output + ) + } +} \ No newline at end of file diff --git a/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/SharedOptionTest.kt b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/SharedOptionTest.kt new file mode 100644 index 00000000..fb82ff2c --- /dev/null +++ b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/SharedOptionTest.kt @@ -0,0 +1,281 @@ +package ch.kleis.lcaac.cli.cmd + +import com.github.ajalt.clikt.core.BadParameterValue +import com.github.ajalt.clikt.core.CliktCommand +import org.junit.jupiter.api.Nested +import java.io.File +import kotlin.io.path.createTempDirectory +import kotlin.io.path.createTempFile +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class SharedOptionTest { + private class DummyCommand : CliktCommand() { + val configFile by configFileOption() + val source by sourceOption() + val file by fileOption("dummy") + val labels by labelsOption("dummy") + val arguments by argumentsOption("dummy") + val globals by globalsOption("dummy") + override fun run() { + // no-op + } + } + + @Nested + inner class ConfigFileOption { + @Test + fun `when no option is provided uses default config file`() { + // given + val cmd = DummyCommand() + + // when + cmd.parse(emptyArray()) + + // then + val expected = File("./$defaultLcaacFilename") + assertEquals(expected, cmd.configFile) + } + + @Test + fun `when directory provided should throw`() { + // given + val tmpDir = createTempDirectory().toAbsolutePath().toString() + val cmd = DummyCommand() + + // when + then + val exception = assertFailsWith { + cmd.parse(arrayOf("--config", tmpDir)) + } + assertEquals(exception.message, "file \"$tmpDir\" is a directory.") + } + + @Test + fun `when valid file provided should parse successfully`() { + // given + val tmpFile = createTempFile().toAbsolutePath().toString() + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("--config", tmpFile)) + + assertEquals(tmpFile, cmd.configFile.absoluteFile.toString()) + } + } + + @Nested + inner class SourceOption { + @Test + fun `when no option is provided uses current work directory`() { + // given + val cmd = DummyCommand() + + // when + cmd.parse(emptyArray()) + + // then + val expected = File(".") + assertEquals(expected, cmd.source) + } + + @Test + fun `when valid file provided should parse successfully`() { + // given + val tmpFile = createTempFile().toAbsolutePath().toString() + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("--source", tmpFile)) + + assertEquals(tmpFile, cmd.source.absoluteFile.toString()) + } + + @Test + fun `when valid directory provided should parse successfully`() { + // given + val tmpFile = createTempDirectory().toAbsolutePath().toString() + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("--source", tmpFile)) + + assertEquals(tmpFile, cmd.source.absoluteFile.toString()) + } + } + + @Nested + inner class FileOption { + @Test + fun `when directory provided should throw`() { + // given + val tmpDir = createTempDirectory().toAbsolutePath().toString() + val cmd = DummyCommand() + + // when + then + val exception = assertFailsWith { + cmd.parse(arrayOf("--file", tmpDir)) + } + assertEquals(exception.message, "file \"$tmpDir\" is a directory.") + } + + @Test + fun `when valid file provided should parse successfully`() { + // given + val tmpFile = createTempFile().toAbsolutePath().toString() + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("--file", tmpFile)) + + assertEquals(tmpFile, cmd.file?.absoluteFile.toString()) + } + + @Test + fun `when display help should write example with the corresponding command`() { + // given + val cmd = DummyCommand() + + // when + val helpText = cmd.getFormattedHelp() + + assertTrue( + helpText!!.contains("-f, --file= CSV file with parameter values. Example: lcaac dummy"), + "Help for -f options should contain an example with the corresponding command." + ) + } + } + + @Nested + inner class LabelsOption { + @Test + fun `when single label provided should parse correctly`() { + // given + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("-l", """model="ABC"""")) + + // then + val expected = mapOf("model" to "\"ABC\"") + assertEquals(expected, cmd.labels) + } + + @Test + fun `when multiple arguments provided should parse correctly`() { + // given + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("-l", """model="ABC"""", "-l", """geo=UK""")) + + // then + val expected = mapOf("model" to "\"ABC\"", "geo" to "UK") + assertEquals(expected, cmd.labels) + } + + @Test + fun `when display help should write example with the corresponding command`() { + // given + val cmd = DummyCommand() + + // when + val helpText = cmd.getFormattedHelp() + + assertTrue( + helpText!!.contains("-l, --label= Specify a process label as a key value pair.\n" + + " Example: lcaac dummy"), + "Help for -l options should contain an example with the corresponding command." + ) + } + } + + @Nested + inner class ArgumentsOption { + @Test + fun `when single argument provided should parse correctly`() { + // given + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("-D", """x="12 kg"""")) + + // then + val expected = mapOf("x" to "\"12 kg\"") + assertEquals(expected, cmd.arguments) + } + + @Test + fun `when multiple arguments provided should parse correctly`() { + // given + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("-D", """x="12 kg"""", "-D", """y=42""")) + + // then + val expected = mapOf("x" to "\"12 kg\"", "y" to "42") + assertEquals(expected, cmd.arguments) + } + + @Test + fun `when display help should write example with the corresponding command`() { + // given + val cmd = DummyCommand() + + // when + val helpText = cmd.getFormattedHelp() + + assertTrue( + helpText!!.contains("-D, --parameter= Override parameter value as a key value pair.\n" + + " Example: lcaac dummy -D"), + "Help for -l options should contain an example with the corresponding command." + ) + } + } + + @Nested + inner class GlobalsOption { + @Test + fun `when single global provided should parse correctly`() { + // given + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("-G", """x="12 kg"""")) + + // then + val expected = mapOf("x" to "\"12 kg\"") + assertEquals(expected, cmd.globals) + } + + @Test + fun `when multiple globals provided should parse correctly`() { + // given + val cmd = DummyCommand() + + // when + cmd.parse(arrayOf("-G", """x="12 kg"""", "-G", """y=42""")) + + // then + val expected = mapOf("x" to "\"12 kg\"", "y" to "42") + assertEquals(expected, cmd.globals) + } + + @Test + fun `when display help should write example with the corresponding command`() { + // given + val cmd = DummyCommand() + + // when + val helpText = cmd.getFormattedHelp() + + assertTrue( + helpText!!.contains("-G, --global= Override global variable as a key value pair.\n" + + " Example: lcaac dummy"), + "Help for -l options should contain an example with the corresponding command." + ) + } + } +} \ No newline at end of file diff --git a/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/TestCommandTest.kt b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/TestCommandTest.kt new file mode 100644 index 00000000..7d00c77c --- /dev/null +++ b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/TestCommandTest.kt @@ -0,0 +1,49 @@ +package ch.kleis.lcaac.cli.cmd + +import com.github.ajalt.clikt.testing.test +import kotlin.test.Test +import kotlin.test.assertEquals + +class TestCommandTest { + @Test + fun `no test`() { + // given + val cmd = TestCommand() + val argv = arrayOf("-s", "src/test/resources/main.zip", "main") + + // when + val result = cmd.test(argv) + + // then + assertEquals("Run 0 tests, 0 passed, 0 failed\n", result.output) + } + + @Test + fun `test simple project`() { + // given + val cmd = TestCommand() + val argv = arrayOf("-s", "src/test/resources/simple-project", "bake") + + // when + val result = cmd.test(argv) + + // then + assertEquals("Run 1 tests, 1 passed, 0 failed\n", result.output) + } + + @Test + fun `test with data and src split`() { + // given + val cmd = TestCommand() + val argv = arrayOf( + "-s", "src/test/resources/data-src-split/src", + "-c", "src/test/resources/data-src-split/lcaac.yaml", + "main") + + // when + val result = cmd.test(argv) + + // then + assertEquals("Run 1 tests, 1 passed, 0 failed\n", result.output) + } +} \ No newline at end of file diff --git a/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/TraceCommandTest.kt b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/TraceCommandTest.kt new file mode 100644 index 00000000..25d35fa2 --- /dev/null +++ b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/TraceCommandTest.kt @@ -0,0 +1,45 @@ +package ch.kleis.lcaac.cli.cmd + +import com.github.ajalt.clikt.testing.test +import kotlin.test.Test +import kotlin.test.assertEquals + +class TraceCommandTest { + @Test + fun `simple trace`() { + // given + val cmd = TraceCommand() + val argv = arrayOf("-s", "src/test/resources/main.zip", "main") + + // when + val result = cmd.test(argv) + + // then + assertEquals( + "depth,d_amount,d_unit,d_product,alloc,name,a,b,c,amount,unit,grass,grass_unit\n" + + "0,1.0,kg,bread,1.0,bread,main,{},{},1.0,kg,2.0,kg\n" + + "1,1.0,kg,bread,1.0,flour,mill,{},{},1.0,kg,2.0,kg\n" + + "2,1.0,kg,bread,1.0,wheat,wheat,{},{},2.0,kg,2.0,kg\n", + result.output + ) + } + + @Test + fun `trace with data and src split`() { + // given + val cmd = TraceCommand() + val argv = arrayOf( + "-s", "src/test/resources/data-src-split/src", + "-c", "src/test/resources/data-src-split/lcaac.yaml", + "main") + + // when + val result = cmd.test(argv) + + // then + assertEquals("depth,d_amount,d_unit,d_product,alloc,name,a,b,c,amount,unit,foo_fn,foo_fn_unit\n" + + "0,1.0,u,main,1.0,main,main,{},{},1.0,u,6.0,u\n", + result.output + ) + } +} \ No newline at end of file diff --git a/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsKtTest.kt b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsTest.kt similarity index 57% rename from cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsKtTest.kt rename to cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsTest.kt index c455f8d6..f42d218c 100644 --- a/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsKtTest.kt +++ b/cli/src/test/kotlin/ch/kleis/lcaac/cli/cmd/UtilsTest.kt @@ -6,64 +6,86 @@ import ch.kleis.lcaac.core.lang.expression.EQuantityMul import ch.kleis.lcaac.core.lang.expression.EQuantityScale import ch.kleis.lcaac.core.math.basic.BasicNumber import ch.kleis.lcaac.core.prelude.Prelude -import io.mockk.every -import io.mockk.mockk +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.assertThrows import java.io.File -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.io.path.Path +import java.nio.file.Files +import kotlin.io.path.createTempDirectory import kotlin.test.Test import kotlin.test.assertEquals - - -class UtilsKtTest { - - @Test - fun parseProjectPath_whenSimpleFile() { - // given - val path = mockk() - every { path.isDirectory } returns false - every { path.parentFile } returns null - every { path.path } returns "lcaac.yaml" - - // when - val (workingDir, configFile) = parseProjectPath(path) - - // then - assertEquals(".", workingDir.path) - assertEquals("lcaac.yaml", configFile.path) +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + + +class UtilsTest { + @Nested + inner class ParseSourceTest { + @Test + fun `directory should be returned as-is`() { + // given + val tmpDir = createTempDirectory().toFile() + + // when + val result = parseSource(tmpDir) + + // then + assertEquals(tmpDir.absoluteFile, result.absoluteFile) + } + + @Test + fun `archived sources should be extracted`() { + val extensions = listOf("zip", "tar.gz", "tgz") + extensions.forEach { extension -> + // given + val zipFile = File("src/test/resources/main.$extension") + + // when + val result = parseSource(zipFile) + + // then + val extractedMainFile = File(result, "src/main.lca") + assertTrue( + extractedMainFile.exists() && extractedMainFile.isFile, + "Expected main.lca to exist inside extracted directory" + ) + } + } + + @Test + fun `unsupported file format should throw`() { + val tmpFile = Files.createTempFile("test", ".txt").toFile() + val exception = assertFailsWith { + parseSource(tmpFile) + } + assertEquals(exception.message, "Unsupported file format: ${tmpFile.name}. Supported file formats are zip, tar.gz and tgz.") + } } - @Test - fun parseProjectPath_whenFileWithParentDirectory() { - // given - val path = mockk() - every { path.isDirectory } returns false - every { path.parentFile } returns Paths.get("some", "directory").toFile() - every { path.path } returns "lcaac.yaml" + @Nested + inner class ParseLcaacConfig { + @Test + fun `when file exists decode it`() { + // given + val path = File("src/test/resources/validLcaacConfig.yaml") - // when - val (workingDir, configFile) = parseProjectPath(path) + // when + val config = parseLcaacConfig(path) - // then - assertEquals("some/directory", workingDir.path) - assertEquals("lcaac.yaml", configFile.path) - } + // + assertEquals("Valid LCAAC Config", config.name) + } - @Test - fun parseProjectPath_whenDirectory() { - // given - val path = mockk() - every { path.isDirectory } returns true - every { path.path } returns "some/directory" + @Test + fun `when file does not exist return default config`() { + // given + val path = File("") - // when - val (workingDir, configFile) = parseProjectPath(path) + // when + val config = parseLcaacConfig(path) - // then - assertEquals("some/directory", workingDir.path) - assertEquals("lcaac.yaml", configFile.path) + // + assertEquals("", config.name) + } } @Test diff --git a/cli/src/test/resources/data-src-split/data/foo_inventory.csv b/cli/src/test/resources/data-src-split/data/foo_inventory.csv new file mode 100644 index 00000000..0d6552a4 --- /dev/null +++ b/cli/src/test/resources/data-src-split/data/foo_inventory.csv @@ -0,0 +1,4 @@ +id,quantity +foo-01,1 +foo-02,2 +foo-03,3 \ No newline at end of file diff --git a/cli/src/test/resources/data-src-split/lcaac.yaml b/cli/src/test/resources/data-src-split/lcaac.yaml new file mode 100644 index 00000000..49c1ec4f --- /dev/null +++ b/cli/src/test/resources/data-src-split/lcaac.yaml @@ -0,0 +1,13 @@ +name: Cloud Assess Trusted Library +description: Reference models for Cloud Assess + +# -------------------------------------- # +# Generic +# -------------------------------------- # + +connectors: + - name: csv + cache: + enabled: true + options: + directory: data diff --git a/cli/src/test/resources/data-src-split/src/main.lca b/cli/src/test/resources/data-src-split/src/main.lca new file mode 100644 index 00000000..af7ee6d3 --- /dev/null +++ b/cli/src/test/resources/data-src-split/src/main.lca @@ -0,0 +1,25 @@ +datasource foo_inventory { + schema { + id = "foo-01" + quantity = 1 u + } +} +process main { + products { + 1 u main + } + inputs { + for_each foo from foo_inventory { + foo.quantity foo_fn + } + } +} + +test main { + given { + 1 u main + } + assert { + foo_fn between 6 u and 6 u + } +} \ No newline at end of file diff --git a/cli/src/test/resources/main.tar.gz b/cli/src/test/resources/main.tar.gz new file mode 100644 index 00000000..74217364 Binary files /dev/null and b/cli/src/test/resources/main.tar.gz differ diff --git a/cli/src/test/resources/main.tgz b/cli/src/test/resources/main.tgz new file mode 100644 index 00000000..74217364 Binary files /dev/null and b/cli/src/test/resources/main.tgz differ diff --git a/cli/src/test/resources/main.zip b/cli/src/test/resources/main.zip new file mode 100644 index 00000000..4469149f Binary files /dev/null and b/cli/src/test/resources/main.zip differ diff --git a/cli/src/test/resources/simple-project/main.lca b/cli/src/test/resources/simple-project/main.lca new file mode 100644 index 00000000..64d4661a --- /dev/null +++ b/cli/src/test/resources/simple-project/main.lca @@ -0,0 +1,27 @@ +process bake { + products { + 1 kg bread + } + inputs { + 1 kg flour + } +} + +process mill { + products { + 1 kg flour + } + inputs { + 2 kg wheat + } +} + +test bake { + given { + 1 kg bread + } + assert { + wheat between 2 kg and 2 kg + } +} + diff --git a/cli/src/test/resources/validLcaacConfig.yaml b/cli/src/test/resources/validLcaacConfig.yaml new file mode 100644 index 00000000..e641d1f0 --- /dev/null +++ b/cli/src/test/resources/validLcaacConfig.yaml @@ -0,0 +1,8 @@ +name: Valid LCAAC Config +description: Valid LCAAC Config for test purposes +connectors: + - name: csv + cache: + enabled: true + options: + directory: data diff --git a/gradle.properties b/gradle.properties index 368f657e..b6053d68 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.8.1 +lcaacVersion=2.0.0 diff --git a/tutorials/03-advanced/03-project-file/README.md b/tutorials/03-advanced/03-project-file/README.md index 6d3c404e..598fc887 100644 --- a/tutorials/03-advanced/03-project-file/README.md +++ b/tutorials/03-advanced/03-project-file/README.md @@ -27,8 +27,8 @@ connectors: ``` Here each file specifies a different location for the folder containing the CSV files supporting the datasources. -You can choose which settings to use with the cli option `-p` or `--project`. +You can choose which settings to use with the cli option `-c` or `--config`. ```bash -lcaac assess --project lcaac.yaml main -lcaac assess --project lcaac-mock.yaml main -``` +lcaac assess --config lcaac.yaml main +lcaac assess --config lcaac-mock.yaml main +``` \ No newline at end of file diff --git a/tutorials/README.md b/tutorials/README.md index 57d458fb..1048bd28 100644 --- a/tutorials/README.md +++ b/tutorials/README.md @@ -4,8 +4,9 @@ This folder contains multiple code samples covering the main LCAAC language feat Each code sample takes the form a `.lca` file. Use the [cli](../cli/README.md) to interact with the code. Moreover, each code sample contains tests. You can run the tests manually, e.g., + ```bash -lcaac test -p 01-basics/01-getting-started +lcaac test -s 01-basics/01-getting-started ``` The script `run.sh` runs all the tests. diff --git a/tutorials/run.sh b/tutorials/run.sh index d1704979..5a10729f 100755 --- a/tutorials/run.sh +++ b/tutorials/run.sh @@ -2,45 +2,54 @@ set -euxo -export GIT_ROOT=$(git rev-parse --show-toplevel) +GIT_ROOT=$(git rev-parse --show-toplevel) +export GIT_ROOT LCAAC_PATH=$GIT_ROOT/cli/build/install/lcaac/bin TUTORIALS_PATH=$GIT_ROOT/tutorials function setup() { - $GIT_ROOT/gradlew :cli:installDist + "$GIT_ROOT"/gradlew :cli:installDist } function lcaac() { - $LCAAC_PATH/lcaac $@ + "$LCAAC_PATH"/lcaac "$@" } -if ! [ -f $LCAAC_PATH/lcaac ]; then +if ! [ -f "$LCAAC_PATH"/lcaac ]; then setup fi # Check all lca tests -lcaac test -p $TUTORIALS_PATH/01-basics/01-getting-started -lcaac test -p $TUTORIALS_PATH/01-basics/02-biosphere -lcaac test -p $TUTORIALS_PATH/01-basics/03-impacts +lcaac test -s "$TUTORIALS_PATH"/01-basics/01-getting-started +lcaac test -s "$TUTORIALS_PATH"/01-basics/02-biosphere +lcaac test -s "$TUTORIALS_PATH"/01-basics/03-impacts -lcaac test -p $TUTORIALS_PATH/02-language-features/01-parametrized-process -lcaac test -p $TUTORIALS_PATH/02-language-features/02-variables -lcaac test -p $TUTORIALS_PATH/02-language-features/03-units -lcaac test -p $TUTORIALS_PATH/02-language-features/04-labels -lcaac test -p $TUTORIALS_PATH/02-language-features/05-datasources +lcaac test -s "$TUTORIALS_PATH"/02-language-features/01-parametrized-process +lcaac test -s "$TUTORIALS_PATH"/02-language-features/02-variables +lcaac test -s "$TUTORIALS_PATH"/02-language-features/03-units +lcaac test -s "$TUTORIALS_PATH"/02-language-features/04-labels -lcaac test -p $TUTORIALS_PATH/03-advanced/01-relational-modeling -lcaac test -p $TUTORIALS_PATH/03-advanced/02-circular-footprint-formula +cd "$TUTORIALS_PATH"/02-language-features/05-datasources +lcaac test +cd - -lcaac test -p $TUTORIALS_PATH/03-advanced/03-project-file/lcaac.yaml main_with_data -lcaac test -p $TUTORIALS_PATH/03-advanced/03-project-file/lcaac-mock.yaml main_with_mock_data +cd "$TUTORIALS_PATH"/03-advanced/01-relational-modeling +lcaac test +cd - -lcaac test -p $TUTORIALS_PATH/03-advanced/04-cached-processes +cd "$TUTORIALS_PATH"/03-advanced/02-circular-footprint-formula +lcaac test +cd - + +lcaac test -s "$TUTORIALS_PATH"/03-advanced/03-project-file -c "$TUTORIALS_PATH"/03-advanced/03-project-file/lcaac.yaml main_with_data +lcaac test -s "$TUTORIALS_PATH"/03-advanced/03-project-file -c "$TUTORIALS_PATH"/03-advanced/03-project-file/lcaac-mock.yaml main_with_mock_data + +lcaac test -s "$TUTORIALS_PATH"/03-advanced/04-cached-processes # Check custom dimensions tutorial set -euo # The following assessment is expected to fail. -if lcaac assess -p $TUTORIALS_PATH/02-language-features/03-units customer; then +if lcaac assess -s "$TUTORIALS_PATH"/02-language-features/03-units customer; then exit 0 fi 2> /dev/null