diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt index ff627a7..86f3de3 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt @@ -2,8 +2,11 @@ package org.domaindrivenarchitecture.provs.desktop.application import kotlinx.serialization.SerializationException import org.domaindrivenarchitecture.provs.configuration.application.ensureSudoWithoutPassword +import org.domaindrivenarchitecture.provs.desktop.domain.DesktopConfig import org.domaindrivenarchitecture.provs.desktop.domain.provisionDesktopCommand +import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance +import org.domaindrivenarchitecture.provs.framework.core.cli.quit import java.io.FileNotFoundException import kotlin.system.exitProcess @@ -18,22 +21,33 @@ fun main(args: Array) { exitProcess(1) } + val config = if (cmd.configFile == null) DesktopConfig() else + try { + getConfig(cmd.configFile.fileName) + } catch (e: SerializationException) { + println( + "Error: File \"${cmd.configFile.fileName}\" has an invalid format and or invalid data." + ) + null + } catch (e: FileNotFoundException) { + println( + "Error: File\u001b[31m ${cmd.configFile.fileName} \u001b[0m was not found.\n" + + "Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m ${cmd.configFile.fileName} \u001B[0m " + + "and change the content according to your needs." + ) + null + } + + if (config == null) { + println("No suitable config found.") + quit(-1) + } + val prov = createProvInstance(cmd.target) - try { - prov.task { - ensureSudoWithoutPassword(cmd.target.remoteTarget()?.password) - provisionDesktopCommand(cmd) - } - } catch (e: SerializationException) { - println( - "Error: File \"${cmd.configFile?.fileName}\" has an invalid format and or invalid data.\n" - ) - } catch (e: FileNotFoundException) { - println( - "Error: File\u001b[31m ${cmd.configFile?.fileName} \u001b[0m was not found.\n" + - "Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m ${cmd.configFile?.fileName} \u001B[0m " + - "and change the content according to your needs.\n" - ) + prov.session { + ensureSudoWithoutPassword(cmd.target.remoteTarget()?.password) + provisionDesktopCommand(cmd, config) } } + diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/DesktopService.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/DesktopService.kt index cbec35e..db4fb55 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/DesktopService.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/domain/DesktopService.kt @@ -12,11 +12,7 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.provisionKeys import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudoWithoutPassword import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami -internal fun Prov.provisionDesktopCommand(cmd: DesktopCliCommand) = task { - - // retrieve config - val conf = if (cmd.configFile != null) getConfig(cmd.configFile.fileName) else DesktopConfig() - +internal fun Prov.provisionDesktopCommand(cmd: DesktopCliCommand, conf: DesktopConfig) = task { provisionDesktop( cmd.type, conf.ssh?.keyPair(), diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/Prov.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/Prov.kt index adb2f11..63a5e5b 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/Prov.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/Prov.kt @@ -68,6 +68,17 @@ open class Prov protected constructor( private val internalResults = arrayListOf() private val infoTexts = arrayListOf() + /** + * A session is the top-level execution unit in provs. A session can contain tasks. + * Returns success if no sub-tasks are called or if all subtasks finish with success. + */ + fun session(taskLambda: Prov.() -> Unit) = task("session") { + if (level > 1) { + throw IllegalStateException("A session can only be created on the top-level and may not be included in another session or task.") + } + taskLambda() + } + /** * A task is the base execution unit in provs. In the results overview it is represented by one line resp. result (of either success or failure). * Returns success if no sub-tasks are called or if all subtasks finish with success. diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt index bd01c07..30c9c3f 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt @@ -51,4 +51,12 @@ internal fun createRemoteProvInstance( internal fun getPasswordToConfigureSudoWithoutPassword(): Secret { return PromptSecretSource("password to configure sudo without password.").secret() +} + + +/** + * Wrapper for exitProcess, which allows e.g. mocking for test purposes + */ +fun quit(status: Int): Nothing { + exitProcess(status) } \ No newline at end of file diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/ApplicationKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/ApplicationKtTest.kt index 0e260c8..4b2af6a 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/ApplicationKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/ApplicationKtTest.kt @@ -4,16 +4,21 @@ import ch.qos.logback.classic.Level import io.mockk.* import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand -import org.domaindrivenarchitecture.provs.desktop.domain.* +import org.domaindrivenarchitecture.provs.desktop.domain.DesktopCliCommand +import org.domaindrivenarchitecture.provs.desktop.domain.DesktopConfig +import org.domaindrivenarchitecture.provs.desktop.domain.DesktopType +import org.domaindrivenarchitecture.provs.desktop.domain.provisionDesktop import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig import org.domaindrivenarchitecture.provs.framework.core.* import org.domaindrivenarchitecture.provs.framework.core.cli.getPasswordToConfigureSudoWithoutPassword +import org.domaindrivenarchitecture.provs.framework.core.cli.quit import org.domaindrivenarchitecture.provs.framework.core.processors.DummyProcessor import org.domaindrivenarchitecture.provs.test.setRootLoggingLevel import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import java.io.ByteArrayOutputStream import java.io.PrintStream @@ -91,6 +96,9 @@ internal class ApplicationKtTest { @Test fun prints_error_message_if_config_not_found() { + mockkStatic(::quit) + every { quit(any()) } throws RuntimeException("mockked") + // given setRootLoggingLevel(Level.OFF) @@ -103,21 +111,28 @@ internal class ApplicationKtTest { System.setErr(PrintStream(errContent)) // when - main(arrayOf("basic", "someuser@remotehost", "-c", "idontexist.yaml")) + assertThrows { + main(arrayOf("basic", "someuser@remotehost", "-c", "idontexist.yaml")) + } // then System.setOut(originalOut) System.setErr(originalErr) val expectedOutput = - "Error: File\u001B[31m idontexist.yaml \u001B[0m was not found.Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m idontexist.yaml \u001B[0m and change the content according to your needs." + "Error: File\u001B[31m idontexist.yaml \u001B[0m was not found.Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m idontexist.yaml \u001B[0m and change the content according to your needs.No suitable config found." assertEquals(expectedOutput, outContent.toString().replace("\r", "").replace("\n", "")) verify(exactly = 0) { any().provisionDesktop(any(), any(), any(), any(), any(), any()) } + + unmockkStatic(::quit) } @Test fun prints_error_message_if_config_not_parsable() { + mockkStatic(::quit) + every { quit(any()) } throws RuntimeException("mockked") + // given setRootLoggingLevel(Level.OFF) @@ -130,16 +145,20 @@ internal class ApplicationKtTest { System.setErr(PrintStream(errContent)) // when - main(arrayOf("basic", "someuser@remotehost", "-c", "src/test/resources/invalid-desktop-config.yaml")) + assertThrows { + main(arrayOf("basic", "someuser@remotehost", "-c", "src/test/resources/invalid-desktop-config.yaml")) + } // then System.setOut(originalOut) System.setErr(originalErr) val expectedOutput = - "Error: File \"src/test/resources/invalid-desktop-config.yaml\" has an invalid format and or invalid data." + "Error: File \"src/test/resources/invalid-desktop-config.yaml\" has an invalid format and or invalid data.No suitable config found." assertEquals(expectedOutput, outContent.toString().replace("\r", "").replace("\n", "")) verify(exactly = 0) { any().provisionDesktop(any(), any(), any(), any(), any(), any()) } + + unmockkStatic(::quit) } } \ No newline at end of file diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/core/ProvTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/core/ProvTest.kt index cd469d1..c06eadf 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/core/ProvTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/core/ProvTest.kt @@ -658,7 +658,12 @@ internal class ProvTest { } optional("sub2c-optional") { taskWithResult("sub3a-taskWithResult") { - addResultToEval(ProvResult(false, err = "returned-result - error msg B should be once in output - in addResultToEval")) + addResultToEval( + ProvResult( + false, + err = "returned-result - error msg B should be once in output - in addResultToEval" + ) + ) } } requireLast("sub2d-requireLast") { @@ -668,10 +673,16 @@ internal class ProvTest { } task("sub2e-task") { addResultToEval(ProvResult(true)) - ProvResult(false, err = "error should NOT be in output as results of task (not taskWithResult) are ignored") + ProvResult( + false, + err = "error should NOT be in output as results of task (not taskWithResult) are ignored" + ) } taskWithResult("sub2f-taskWithResult") { - ProvResult(false, err = "returned-result - error msg C should be once in output - at the end of sub3taskWithResult ") + ProvResult( + false, + err = "returned-result - error msg C should be once in output - at the end of sub3taskWithResult " + ) } ProvResult(false, err = "returned-result - error msg D should be once in output - at the end of sub1 ") } @@ -725,5 +736,25 @@ internal class ProvTest { assertEquals(expectedOutput, outContent.toString().replace("\r", "")) } -} + @Test + fun session_on_top_level_succeeds() { + // when + val result = Prov.newInstance().session { cmd("echo bla") } + // then + assertTrue(result.success) + } + @Test + fun session_not_on_top_level_throws_an_exception() { + // when + val exception = org.junit.jupiter.api.assertThrows { + local().session { + session { + cmd("echo bla") + } + } + } + // then + assertEquals("A session can only be created on the top-level and may not be included in another session or task.", exception.message) + } +}