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 package alfred.web.core.build
import alfred.web.core.AlfredHome
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.ResponseStatus
import java.nio.file.Files.createFile import java.nio.file.Files.createFile
import java.nio.file.Paths
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.* import java.util.*
@Service @Service
class Builds( class Builds(
@Value("\${ALFRED_HOME}") val alfredHome: AlfredHome
private val home: String
) { ) {
private val logger: Logger = LoggerFactory.getLogger(this::class.java) private val logger: Logger = LoggerFactory.getLogger(this::class.java)
init { init {
logger.info("ALFRED_HOME is $home") logger.info("ALFRED_HOME is ${alfredHome.homePath.toFile().absolutePath}")
} }
fun buildConfig(build: BuildId): BuildConfig { fun buildConfig(build: BuildId): BuildConfig {
val path = Paths.get(home, "builds", "$build.properties") val path = alfredHome.buildConfig(build)
if (!path.toFile().exists()) { if (!path.toFile().exists()) {
throw UnknownBuild(build) throw UnknownBuild(build)
@ -47,10 +45,11 @@ class Builds(
val nowStr = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) val nowStr = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
val fileName = "$build.$nowStr.log" val fileName = "$build.$nowStr.log"
val path = Paths.get(home, "logs", fileName) val logsDir = alfredHome.logsDir()
path.toFile().parentFile.mkdirs() 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) 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("At your service.")
append("Build $buildId, rev [${rev ?: "-none-"}] started at ${ZonedDateTime.now()}") append("Build $buildId, rev [${rev ?: "-none-"}] started at ${ZonedDateTime.now()}")
append("Workspace directory is: ${workspace.toFile().absolutePath}")
append("------------------------------------------------------------") append("------------------------------------------------------------")
} }

@ -16,7 +16,7 @@ class Workspaces {
try { try {
block(wsDir) block(wsDir)
} finally { } 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 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.BuildContext
import alfred.web.core.build.BuildId import alfred.web.core.build.BuildId
import alfred.web.core.build.Builds 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.Workspace
import alfred.web.core.build.Workspaces import alfred.web.core.build.Workspaces
import alfred.web.core.event.BuildFinished import alfred.web.core.event.BuildFinished
import alfred.web.core.process.Git
import alfred.web.core.process.Processes import alfred.web.core.process.Processes
import alfred.web.core.process.Script
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.util.concurrent.TimeUnit
import kotlin.collections.forEach import kotlin.collections.forEach
import kotlin.jvm.java
@Service @Service
class GitRunner( class GitRunner(
val builds: Builds, val builds: Builds,
val handles: Handles,
val processes: Processes, val processes: Processes,
val workspaces: Workspaces, val workspaces: Workspaces,
val eventPublisher: ApplicationEventPublisher val eventPublisher: ApplicationEventPublisher,
val git: Git,
val script: Script
) { ) {
val scriptsDir = ".alfred" val scriptsDir = ".alfred"
@ -36,7 +35,7 @@ class GitRunner(
val logFile = builds.createLogFile(buildId) val logFile = builds.createLogFile(buildId)
logger.info("preparing process for build $buildId") logger.info("preparing process for build $buildId")
logger.info("log file: $logFile") logger.info("log file: ${logFile.backingFile.absolutePath}")
val ctx = BuildContext( val ctx = BuildContext(
buildId = buildId, buildId = buildId,
@ -46,74 +45,37 @@ class GitRunner(
) )
processes.startThread { processes.startThread {
workspaces.withWorkspace(ctx) { wsPath -> logFile.header(buildId, rev)
logFile.header(buildId, rev, wsPath)
clone(ctx, wsPath) workspaces.withWorkspace(ctx) { ws ->
checkout(ctx, wsPath)
execScripts(ctx, wsPath) git.clone(ctx, ws)
git.checkout(ctx, ws)
execScripts(ctx, ws)
logFile.footer() logFile.footer()
}
eventPublisher.publishEvent(BuildFinished(ctx)) eventPublisher.publishEvent(BuildFinished(ctx))
} }
}
return ProcessInfo(-1, logFile) 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) { private fun execScripts(ctx: BuildContext, ws: Workspace) {
ctx.log("looking for scripts $scriptFiles in $scriptsDir/") ctx.log("looking for scripts $scriptFiles in $scriptsDir/")
scriptFiles.forEach { script -> scriptFiles.forEach { scriptName ->
val scriptFile = shFile(ws, script) val scriptFile = shFile(ws, scriptName)
if (scriptFile.exists()) { if (script.exists(scriptFile)) {
ctx.log("found script $script, running it") ctx.log("found script $scriptName, running it")
ctx.log("Running build file: $script") val proc = script.execute(ctx, ws, scriptFile)
proc.waitFor()
val scriptProcess = processes.builder(ctx.config, ctx.logFile, ctx.rev) ctx.log("$scriptName returned ${proc.exitValue()}")
.command(scriptFile.absolutePath)
.directory(ws.toFile())
.start()
handles.add(Handle(scriptProcess.toHandle(), ctx.buildId))
val ret = scriptProcess.waitFor()
ctx.log("script $script returned $ret")
ctx.log("$script returned $ret")
} else { } 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) 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 package alfred.web.core.runner
import alfred.web.core.Handle
import alfred.web.core.Handles
import alfred.web.core.build.* import alfred.web.core.build.*
import alfred.web.core.event.BuildFinished 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.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.File
@Service @Service
class ScriptRunner( class ScriptRunner(
val builds: Builds, val builds: Builds,
val handles: Handles,
val processes: Processes,
val workspaces: Workspaces, val workspaces: Workspaces,
val eventPublisher: ApplicationEventPublisher val eventPublisher: ApplicationEventPublisher,
val script: Script
) { ) {
fun run(buildId: BuildId, rev: String?): ProcessInfo { fun run(buildId: BuildId, rev: String?): ProcessInfo {
@ -41,27 +39,27 @@ class ScriptRunner(
return ProcessInfo(pid = pid, logFile = logFile) return ProcessInfo(pid = pid, logFile = logFile)
} }
private fun runScript( private fun runScript(ctx: BuildContext): Process {
ctx: BuildContext ctx.logFile.header(ctx.buildId, ctx.rev)
): Process {
val wsDir = workspaces.prepare(ctx) val wsDir = workspaces.prepare(ctx)
ctx.logFile.header(ctx.buildId, ctx.rev, wsDir)
ctx.log("running ${ctx.config.script} in ${wsDir.toAbsolutePath()}") 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() ctx.logFile.footer()
workspaces.cleanUp(wsDir) workspaces.cleanUp(wsDir)
eventPublisher.publishEvent(BuildFinished(ctx)) eventPublisher.publishEvent(BuildFinished(ctx))
} }
return process return proc
} }
val logger: Logger = LoggerFactory.getLogger(this::class.java) val logger: Logger = LoggerFactory.getLogger(this::class.java)

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