add tests for ScriptRunner and GitRunner, major moving around of logic

master
Josha von Gizycki 3 weeks ago
parent a70ae91ce4
commit 928a08b842

@ -0,0 +1,22 @@
package alfred.web.core
import alfred.web.core.build.BuildId
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.nio.file.Paths
@Service
class AlfredHome(
@Value("\${ALFRED_HOME}")
private val home: String,
) {
val homePath = Paths.get(home)
fun logsDir() =
homePath.resolve("logs")
fun buildConfig(buildId: BuildId) =
homePath.resolve("builds/${buildId}.properties")
}

@ -1,32 +1,30 @@
package alfred.web.core.build
import alfred.web.core.AlfredHome
import com.fasterxml.jackson.annotation.JsonIgnore
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ResponseStatus
import java.nio.file.Files.createFile
import java.nio.file.Paths
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.*
@Service
class Builds(
@Value("\${ALFRED_HOME}")
private val home: String
val alfredHome: AlfredHome
) {
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
init {
logger.info("ALFRED_HOME is $home")
logger.info("ALFRED_HOME is ${alfredHome.homePath.toFile().absolutePath}")
}
fun buildConfig(build: BuildId): BuildConfig {
val path = Paths.get(home, "builds", "$build.properties")
val path = alfredHome.buildConfig(build)
if (!path.toFile().exists()) {
throw UnknownBuild(build)
@ -47,10 +45,11 @@ class Builds(
val nowStr = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
val fileName = "$build.$nowStr.log"
val path = Paths.get(home, "logs", fileName)
path.toFile().parentFile.mkdirs()
val logsDir = alfredHome.logsDir()
logsDir.toFile().mkdir()
return LogFile(createFile(path).toFile())
val logFilePath = logsDir.resolve(fileName)
return LogFile(createFile(logFilePath).toFile())
}
}

@ -17,10 +17,9 @@ class LogFile(
val logger: Logger = LoggerFactory.getLogger(this::class.java)
fun header(buildId: BuildId, rev: String?, workspace: Path) {
fun header(buildId: BuildId, rev: String?) {
append("At your service.")
append("Build $buildId, rev [${rev ?: "-none-"}] started at ${ZonedDateTime.now()}")
append("Workspace directory is: ${workspace.toFile().absolutePath}")
append("------------------------------------------------------------")
}

@ -16,7 +16,7 @@ class Workspaces {
try {
block(wsDir)
} finally {
wsDir.toFile().deleteRecursively()
cleanUp(wsDir)
}
}

@ -0,0 +1,64 @@
package alfred.web.core.process
import alfred.web.core.Handle
import alfred.web.core.Handles
import alfred.web.core.build.BuildContext
import alfred.web.core.build.BuildId
import alfred.web.core.build.Workspace
import org.springframework.stereotype.Service
import java.util.concurrent.TimeUnit
@Service
class Git(
val processes: Processes,
val handles: Handles,
) {
fun clone(ctx: BuildContext, ws: Workspace) {
ctx.log("cloning ${ctx.config.gitRepo} into $ws")
val proc = processes.builder(ctx.config, ctx.logFile, "")
.command("git", "clone", ctx.config.gitRepo, ".")
.directory(ws.toFile())
.start()
handles.add(Handle(proc.toHandle(), ctx.buildId))
val cloneSuccess = proc.waitFor(ctx.config.gitCloneTimeout, TimeUnit.SECONDS)
if (!cloneSuccess) {
throw FailedToClone(ctx.buildId, ctx.config.gitRepo ?: "[no repo configured]")
}
}
fun checkout(ctx: BuildContext, ws: Workspace) {
ctx.log("checkout rev ${ctx.rev}")
val proc = processes.builder(ctx.config, ctx.logFile, "")
.command("git", "checkout", ctx.rev)
.directory(ws.toFile())
.start()
handles.add(Handle(proc.toHandle(), ctx.buildId))
val checkoutSuccess = proc.waitFor(ctx.config.gitCloneTimeout, TimeUnit.SECONDS)
if (!checkoutSuccess) {
throw FailedToCheckout(ctx.buildId, ctx.config.gitRepo!!, ctx.rev)
}
}
sealed class GitException(msg: String): Exception(msg)
private class FailedToClone(
buildId: BuildId,
gitRepo: String
) : GitException(
"failed to clone $gitRepo for build id $buildId"
)
private class FailedToCheckout(
buildId: BuildId,
gitRepo: String,
rev: String
) : GitException(
"failed to checkout revision $rev on repo $gitRepo for build id $buildId"
)
}

@ -0,0 +1,34 @@
package alfred.web.core.process
import alfred.web.core.Handle
import alfred.web.core.Handles
import alfred.web.core.build.BuildContext
import alfred.web.core.build.Workspace
import org.springframework.stereotype.Service
import java.io.File
@Service
class Script(
val handles: Handles,
val processes: Processes
) {
fun execute(ctx: BuildContext, ws: Workspace, scriptFile: File): Process {
ctx.log("Running build file: ${scriptFile.name}")
val scriptProcess = processes.builder(ctx.config, ctx.logFile, ctx.rev)
.command(scriptFile.absolutePath)
.directory(ws.toFile())
.start()
handles.add(Handle(scriptProcess.toHandle(), ctx.buildId))
return scriptProcess
}
fun exists(scriptFile: File) = scriptFile.exists()
fun onExit(process: Process, block: () -> Unit) {
process.onExit().whenComplete { _, _ -> block() }
}
}

@ -1,7 +1,5 @@
package alfred.web.core.runner
import alfred.web.core.Handle
import alfred.web.core.Handles
import alfred.web.core.build.BuildContext
import alfred.web.core.build.BuildId
import alfred.web.core.build.Builds
@ -9,22 +7,23 @@ import alfred.web.core.build.ProcessInfo
import alfred.web.core.build.Workspace
import alfred.web.core.build.Workspaces
import alfred.web.core.event.BuildFinished
import alfred.web.core.process.Git
import alfred.web.core.process.Processes
import alfred.web.core.process.Script
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import java.util.concurrent.TimeUnit
import kotlin.collections.forEach
import kotlin.jvm.java
@Service
class GitRunner(
val builds: Builds,
val handles: Handles,
val processes: Processes,
val workspaces: Workspaces,
val eventPublisher: ApplicationEventPublisher
val eventPublisher: ApplicationEventPublisher,
val git: Git,
val script: Script
) {
val scriptsDir = ".alfred"
@ -36,7 +35,7 @@ class GitRunner(
val logFile = builds.createLogFile(buildId)
logger.info("preparing process for build $buildId")
logger.info("log file: $logFile")
logger.info("log file: ${logFile.backingFile.absolutePath}")
val ctx = BuildContext(
buildId = buildId,
@ -46,74 +45,37 @@ class GitRunner(
)
processes.startThread {
workspaces.withWorkspace(ctx) { wsPath ->
logFile.header(buildId, rev, wsPath)
logFile.header(buildId, rev)
clone(ctx, wsPath)
checkout(ctx, wsPath)
execScripts(ctx, wsPath)
workspaces.withWorkspace(ctx) { ws ->
git.clone(ctx, ws)
git.checkout(ctx, ws)
execScripts(ctx, ws)
logFile.footer()
}
eventPublisher.publishEvent(BuildFinished(ctx))
}
}
return ProcessInfo(-1, logFile)
}
private fun clone(ctx: BuildContext, ws: Workspace) {
ctx.log("cloning ${ctx.config.gitRepo} into $ws")
val proc = processes.builder(ctx.config, ctx.logFile, "")
.command("git", "clone", ctx.config.gitRepo, ".")
.directory(ws.toFile())
.start()
handles.add(Handle(proc.toHandle(), ctx.buildId))
val cloneSuccess = proc.waitFor(ctx.config.gitCloneTimeout, TimeUnit.SECONDS)
if (!cloneSuccess) {
throw FailedToClone(ctx.buildId, ctx.config.gitRepo ?: "[no repo configured]")
}
}
private fun checkout(ctx: BuildContext, ws: Workspace) {
ctx.log("checkout rev ${ctx.rev}")
val proc = processes.builder(ctx.config, ctx.logFile, "")
.command("git", "checkout", ctx.rev)
.directory(ws.toFile())
.start()
handles.add(Handle(proc.toHandle(), ctx.buildId))
val checkoutSuccess = proc.waitFor(ctx.config.gitCloneTimeout, TimeUnit.SECONDS)
if (!checkoutSuccess) {
throw FailedToCheckout(ctx.buildId, ctx.config.gitRepo!!, ctx.rev)
}
}
private fun execScripts(ctx: BuildContext, ws: Workspace) {
ctx.log("looking for scripts $scriptFiles in $scriptsDir/")
scriptFiles.forEach { script ->
val scriptFile = shFile(ws, script)
if (scriptFile.exists()) {
ctx.log("found script $script, running it")
ctx.log("Running build file: $script")
val scriptProcess = processes.builder(ctx.config, ctx.logFile, ctx.rev)
.command(scriptFile.absolutePath)
.directory(ws.toFile())
.start()
handles.add(Handle(scriptProcess.toHandle(), ctx.buildId))
scriptFiles.forEach { scriptName ->
val scriptFile = shFile(ws, scriptName)
if (script.exists(scriptFile)) {
ctx.log("found script $scriptName, running it")
val ret = scriptProcess.waitFor()
ctx.log("script $script returned $ret")
val proc = script.execute(ctx, ws, scriptFile)
proc.waitFor()
ctx.log("$script returned $ret")
ctx.log("$scriptName returned ${proc.exitValue()}")
} else {
ctx.log("Build file $scriptsDir/$script not found")
ctx.log("Build file $scriptsDir/$scriptName not found")
}
}
}
@ -124,19 +86,3 @@ class GitRunner(
val logger: Logger = LoggerFactory.getLogger(this::class.java)
}
class FailedToClone(
buildId: BuildId,
gitRepo: String
) : RuntimeException(
"failed to clone $gitRepo for build id $buildId"
)
class FailedToCheckout(
buildId: BuildId,
gitRepo: String,
rev: String
) : RuntimeException(
"failed to checkout revision $rev on repo $gitRepo for build id $buildId"
)

@ -1,22 +1,20 @@
package alfred.web.core.runner
import alfred.web.core.Handle
import alfred.web.core.Handles
import alfred.web.core.build.*
import alfred.web.core.event.BuildFinished
import alfred.web.core.process.Processes
import alfred.web.core.process.Script
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import java.io.File
@Service
class ScriptRunner(
val builds: Builds,
val handles: Handles,
val processes: Processes,
val workspaces: Workspaces,
val eventPublisher: ApplicationEventPublisher
val eventPublisher: ApplicationEventPublisher,
val script: Script
) {
fun run(buildId: BuildId, rev: String?): ProcessInfo {
@ -41,27 +39,27 @@ class ScriptRunner(
return ProcessInfo(pid = pid, logFile = logFile)
}
private fun runScript(
ctx: BuildContext
): Process {
private fun runScript(ctx: BuildContext): Process {
ctx.logFile.header(ctx.buildId, ctx.rev)
val wsDir = workspaces.prepare(ctx)
ctx.logFile.header(ctx.buildId, ctx.rev, wsDir)
ctx.log("running ${ctx.config.script} in ${wsDir.toAbsolutePath()}")
val process = processes.builder(ctx)
.command(ctx.config.script)
.directory(wsDir.toFile())
.start()
handles.add(Handle(process.toHandle(), ctx.buildId))
process.onExit().whenComplete { _, _ ->
val proc = script.execute(
ctx,
wsDir,
File(ctx.config.script!!)
)
script.onExit(proc) {
ctx.logFile.footer()
workspaces.cleanUp(wsDir)
eventPublisher.publishEvent(BuildFinished(ctx))
}
return process
return proc
}
val logger: Logger = LoggerFactory.getLogger(this::class.java)

@ -1,5 +1,6 @@
package alfred.web.core.build
import alfred.web.core.AlfredHome
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.nio.file.Paths
@ -44,10 +45,12 @@ class BuildsTest {
private fun builds() =
Builds(
AlfredHome(
Paths.get("")
.toAbsolutePath()
.resolve("src/test/resources/home-1")
.toString()
)
)
}

@ -0,0 +1,12 @@
package alfred.web.core
import io.mockk.justRun
import io.mockk.mockk
import org.springframework.context.ApplicationEventPublisher
import kotlin.reflect.KClass
fun <T : Any> eventPublisher(eventType: KClass<T>): ApplicationEventPublisher {
val eventPublisher = mockk<ApplicationEventPublisher>()
justRun { eventPublisher.publishEvent(any(eventType)) }
return eventPublisher
}

@ -0,0 +1,73 @@
package alfred.web.core.runner
import alfred.web.core.AlfredHome
import alfred.web.core.build.Builds
import alfred.web.core.build.Workspaces
import alfred.web.core.event.BuildFinished
import alfred.web.core.eventPublisher
import alfred.web.core.process.Git
import alfred.web.core.process.ProcessEnvironment
import alfred.web.core.process.Processes
import alfred.web.core.process.Script
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
import java.nio.file.Paths
class GitRunnerTest {
@Test
fun runs(@TempDir logsDir: Path) {
// Given
val eventPublisher = eventPublisher(BuildFinished::class)
val git = mockk<Git>()
justRun { git.clone(any(), any()) }
justRun { git.checkout(any(), any()) }
val script = mockk<Script>()
every { script.execute(any(), any(), any()) } returns (mockk<Process>(relaxed = true))
every { script.exists(any()) } returns (true)
val workspacesSpy = spyk(Workspaces())
val homeDir = Paths.get("").toAbsolutePath().resolve("src/test/resources/home-1")
val home = spyk(AlfredHome(homeDir.toFile().absolutePath))
every { home.logsDir() } returns logsDir
val runner = GitRunner(
builds = Builds(home),
processes = Processes(ProcessEnvironment()),
workspaces = workspacesSpy,
eventPublisher = eventPublisher,
git = git,
script = script
)
// When
runner.run(
buildId = "simple-git",
rev = "master"
)
// Then
verify(timeout = 1_000, exactly = 1) {
git.clone(any(), any())
git.checkout(any(), any())
listOf("pre.sh", "job.sh", "post.sh").forEach { scriptName ->
script.exists(match { it.name == scriptName })
script.execute(any(), any(), match { it.name == scriptName })
}
eventPublisher.publishEvent(any(BuildFinished::class))
workspacesSpy.cleanUp(any())
}
}
}

@ -0,0 +1,65 @@
package alfred.web.core.runner
import alfred.web.core.AlfredHome
import alfred.web.core.build.Builds
import alfred.web.core.build.Workspaces
import alfred.web.core.event.BuildFinished
import alfred.web.core.eventPublisher
import alfred.web.core.process.Script
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.springframework.test.web.client.ExpectedCount.once
import java.nio.file.Path
import java.nio.file.Paths
class ScriptRunnerTest {
@Test
fun runs(@TempDir logDir: Path) {
// Given
val home = spyk(
AlfredHome(
Paths.get("").toAbsolutePath().resolve("src/test/resources/home-1").toString()
)
)
every { home.logsDir() } returns logDir
val eventPublisher = eventPublisher(BuildFinished::class)
val script = mockk<Script>()
every { script.execute(any(), any(), any()) } returns mockk<Process>(relaxed = true)
val onExit = slot<() -> Unit>()
every { script.onExit(any(), capture(onExit)) } answers { onExit.captured() }
val workspaces = spyk(Workspaces())
val runner = ScriptRunner(
builds = Builds(home),
workspaces = workspaces,
eventPublisher = eventPublisher,
script = script
)
// When
runner.run(
buildId = "simple-script",
rev = null
)
// Then
verify(timeout = 1_000, exactly = 1) {
script.execute(any(), any(), match { it.name == "some-script" })
workspaces.cleanUp(any())
eventPublisher.publishEvent(any(BuildFinished::class))
}
}
}

@ -0,0 +1,5 @@
user=alfred
workspace=/tmp
apikey=Mellon
git.repo.url=some.url

@ -0,0 +1,5 @@
user=alfred
workspace=/tmp
apikey=Mellon
script=some-script
Loading…
Cancel
Save