Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ kover {
excludes {
classes(
// Kotlin serialization generated classes (JVM binary name patterns)
"**\$\$serializer",
"**$\$serializer",
"**\$serializer",
"**Companion\$serializer",

Expand All @@ -91,7 +91,7 @@ kover {
"**\$DefaultImpls",
"**\$WhenMappings",
"**\$inlined*",
"**\$sam\$*",
"**\$sam$*",

// data class copy$default bridge methods (JVM only)
"**\$copy\$default*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package carp.dsp.core.application.environment

import dk.cachet.carp.analytics.domain.environment.EnvironmentDefinition
import dk.cachet.carp.common.application.UUID
import kotlinx.serialization.Serializable

/**
* Declarative author-time definition of an R execution environment.
*
* Contains only specification data (R version, packages, lock files).
* Does not represent a materialized/runtime environment.
*/
@Serializable
data class REnvironmentDefinition(
override val id: UUID,
override val name: String,
val rVersion: String,
val rPackages: List<String> = emptyList(),
val renvLockFile: String? = null,
val installationPath: String? = null,
override val dependencies: List<String> = emptyList(),
override val environmentVariables: Map<String, String> = emptyMap()
) : EnvironmentDefinition {

private companion object {
private const val MIN_VERSION_SEGMENTS = 2
private const val MAX_VERSION_SEGMENTS = 3
}

/**
* Validate R environment specification.
*/
fun validate(): List<String> {
val errors = mutableListOf<String>()

if (rVersion.isBlank()) {
errors.add("R version cannot be blank")
}

if (renvLockFile == null && rPackages.isEmpty()) {
errors.add("Either renvLockFile or rPackages must be specified")
}

// Validate version format
if (!isValidRVersion(rVersion)) {
errors.add("Invalid R version format: $rVersion")
}

return errors
}

/**
* Check if version string is valid.
* Expected format: MAJOR.MINOR.PATCH (e.g., "4.3.0")
*/
private fun isValidRVersion(version: String): Boolean {
val parts = version.split(".")
if (parts.size !in MIN_VERSION_SEGMENTS..MAX_VERSION_SEGMENTS) return false
return parts.all { it.toIntOrNull() != null }
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package carp.dsp.core.application.environment

import dk.cachet.carp.common.application.UUID
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class REnvironmentDefinitionTest {

@Test
fun createsREnvironmentDefinition() {
val definition = REnvironmentDefinition(
id = UUID.randomUUID(),
name = "analysis-env",
rVersion = "4.3.0",
rPackages = listOf("ggplot2", "dplyr", "tidyr")
)

assertEquals("4.3.0", definition.rVersion)
assertEquals(3, definition.rPackages.size)
}

@Test
fun validatesValidRVersion() {
val definition = REnvironmentDefinition(
id = UUID.randomUUID(),
name = "env",
rVersion = "4.3.0",
rPackages = listOf("ggplot2")
)

val errors = definition.validate()

assertTrue(errors.isEmpty())
}

@Test
fun rejectsInvalidRVersion() {
val definition = REnvironmentDefinition(
id = UUID.randomUUID(),
name = "env",
rVersion = "invalid-version",
rPackages = listOf("ggplot2")
)

val errors = definition.validate()

assertTrue(errors.any { it.contains("Invalid R version format") })
}

@Test
fun rejectsBlankRVersion() {
val definition = REnvironmentDefinition(
id = UUID.randomUUID(),
name = "env",
rVersion = "",
rPackages = listOf("ggplot2")
)

val errors = definition.validate()

assertTrue(errors.any { it.contains("R version cannot be blank") })
}

@Test
fun acceptsRenvLockFile() {
val definition = REnvironmentDefinition(
id = UUID.randomUUID(),
name = "env",
rVersion = "4.3.0",
renvLockFile = "/path/to/renv.lock"
)

val errors = definition.validate()

assertTrue(errors.isEmpty())
}

@Test
fun rejectsMissingPackagesAndLockFile() {
val definition = REnvironmentDefinition(
id = UUID.randomUUID(),
name = "env",
rVersion = "4.3.0",
rPackages = emptyList(),
renvLockFile = null
)

val errors = definition.validate()

assertTrue(errors.any { it.contains("renvLockFile or rPackages") })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package carp.dsp.core.infrastructure.execution

import dk.cachet.carp.analytics.application.plan.EnvironmentRef

/**
* Command generation from environment template.
*
* Encapsulates the logic of expanding {executable} and {args}
* into the final command string.
*/
data class CommandTemplate(
val environmentRef: EnvironmentRef,
val executable: String,
val args: List<String>
) {

/**
* Expand template into final command string.
*
* Example:
* Input: environmentRef=CondaRef("myenv"),
* executable="python",
* args=["script.py", "arg1"]
* Output: "conda run -n myenv python script.py arg1"
*/
fun toCommandString(): String {
val template = environmentRef.generateExecutionTemplate()
return template
.replace("{executable}", executable)
.replace("{args}", args.joinToString(" "))
.trimEnd()
}

/**
* Get as bash command for ProcessBuilder.
*/
fun toBashCommand(): List<String> {
return listOf("bash", "-c", toCommandString())
}
}
Loading
Loading