package solutions.tretter.githugandroid import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import java.io.File class LevelSolutionsTest { @Test fun everyLevelHasEmbeddedSolutionScenarios() { val missing = allGithugLevels().filter { it.testCases.isEmpty() }.map { it.id } assertTrue( "Expected every level source file to define at least one solution scenario. Missing:\n${missing.joinToString("\n")}", missing.isEmpty(), ) } @Test fun levelIdsAreUnique() { val ids = allGithugLevels().map { it.id } assertEquals(ids, ids.distinct()) } @Test fun implementedLevelOrderMatchesUpstreamExercises() { assertEquals(IMPLEMENTED_UPSTREAM_LEVEL_ORDER, allGithugLevels().map { it.id }) } @Test fun knownSolutionsCompleteEveryLevel() { val evidence = StringBuilder() val gitBinary = testGitBinary() val runtimeRoot = testSandboxRoot().apply { deleteRecursively() mkdirs() } evidence.appendLine("GitHug Android level solution evidence") evidence.appendLine("Engine: GitRepositoryRuntime filesystem sandbox") evidence.appendLine("Git binary: ${gitBinary.absolutePath}") evidence.appendLine("Sandbox root: ${runtimeRoot.absolutePath}") evidence.appendLine("Scope: executes embedded solution scenarios through the same runtime and per-level sandbox path used by the app.") evidence.appendLine("Levels: ${allGithugLevels().size}") evidence.appendLine() val failures = allGithugLevels().flatMap { level -> val runtime = GitRepositoryRuntime(runtimeRoot, gitBinary) evidence.appendLine("================================================================================") evidence.appendLine("Level: ${level.id}") evidence.appendLine("Title: ${level.title}") evidence.appendLine("Initial repo:") evidence.append(runtime.prepareLevel(level).describeForEvidence().prependIndent(" ")) evidence.appendLine() level.testCases.mapNotNull { testCase -> var repo = runtime.prepareLevel(level) var solved = false evidence.appendLine("Scenario: ${testCase.name}") evidence.appendLine("Solution commands:") testCase.commands.forEachIndexed { index, command -> evidence.appendLine(" ${index + 1}. $command") } evidence.appendLine() testCase.commands.forEachIndexed { index, command -> val (nextRepo, output) = runtime.execute(level, repo, command) repo = nextRepo solved = level.validator(repo, command) evidence.appendLine("Command ${index + 1}: $command") evidence.appendLine("Output:") if (output.isEmpty()) { evidence.appendLine(" ") } else { output.forEach { line -> evidence.appendLine(" $line") } } evidence.appendLine("Repo after command:") evidence.append(repo.describeForEvidence().prependIndent(" ")) evidence.appendLine("Validator passed after command: $solved") evidence.appendLine() } evidence.appendLine("Scenario result: ${if (solved) "PASS" else "FAIL"}") evidence.appendLine() if (solved) null else "${level.id} / ${testCase.name}: ${testCase.commands.joinToString(" && ")}" } } writeEvidenceLog(evidence.toString()) assertTrue( "Expected every known solution to complete its level. Failures:\n${failures.joinToString("\n")}", failures.isEmpty(), ) } @Test fun embeddedSetupChecksPass() { val gitBinary = testGitBinary() val runtimeRoot = testSandboxRoot().apply { deleteRecursively() mkdirs() } val failures = allGithugLevels().flatMap { level -> if (level.setupChecks.isEmpty()) return@flatMap emptyList() val repo = GitRepositoryRuntime(runtimeRoot, gitBinary).prepareLevel(level) level.setupChecks .filterNot { check -> check.predicate(repo) } .map { check -> "${level.id} / ${check.name}: ${check.failureMessage}" } } assertTrue( "Expected every embedded setup check to pass. Failures:\n${failures.joinToString("\n")}", failures.isEmpty(), ) } @Test fun embeddedNegativeScenariosDoNotSolveLevels() { val gitBinary = testGitBinary() val runtimeRoot = testSandboxRoot().apply { deleteRecursively() mkdirs() } val failures = allGithugLevels().flatMap { level -> level.negativeTestCases.mapNotNull { testCase -> val runtime = GitRepositoryRuntime(runtimeRoot, gitBinary) var repo = runtime.prepareLevel(level) var solved = false testCase.commands.forEach { command -> val (nextRepo, _) = runtime.execute(level, repo, command) repo = nextRepo solved = level.validator(repo, command) } if (solved) "${level.id} / ${testCase.name}: ${testCase.commands.joinToString(" && ")}" else null } } assertTrue( "Expected every embedded negative scenario to remain unsolved. Failures:\n${failures.joinToString("\n")}", failures.isEmpty(), ) } private companion object { fun writeEvidenceLog(content: String) { val repoRoot = File(System.getProperty("user.dir") ?: ".") val appDir = if (File(repoRoot, "app/build.gradle.kts").exists()) { File(repoRoot, "app") } else { repoRoot } val reportDir = File(appDir, "build/reports/level-solutions") reportDir.mkdirs() File(reportDir, "level-solutions.log").writeText(content) } fun testSandboxRoot(): File { val repoRoot = File(System.getProperty("user.dir") ?: ".") val appDir = if (File(repoRoot, "app/build.gradle.kts").exists()) { File(repoRoot, "app") } else { repoRoot } return File(appDir, "build/test-sandboxes/level-solutions") } fun testGitBinary(): File { System.getenv("GITHUG_TEST_GIT_BINARY") ?.takeIf { it.isNotBlank() } ?.let { File(it) } ?.takeIf { it.exists() && it.canExecute() } ?.let { return it } val repoHostGit = File(repoRoot(), "build/host-git/libgit.so") if (repoHostGit.exists() && repoHostGit.canExecute()) return repoHostGit val candidates = listOf( File("/usr/bin/git"), File("/usr/local/bin/git"), ) candidates.firstOrNull { it.exists() && it.canExecute() }?.let { return it } val process = ProcessBuilder("sh", "-c", "command -v git") .redirectErrorStream(true) .start() val output = process.inputStream.bufferedReader().readText().trim() val exitCode = process.waitFor() assertTrue("A real git executable is required for level solution tests.", exitCode == 0 && output.isNotBlank()) return File(output) } fun repoRoot(): File { val userDir = File(System.getProperty("user.dir") ?: ".") return if (File(userDir, "app/build.gradle.kts").exists()) userDir else userDir.parentFile ?: userDir } fun RepoState.describeForEvidence(): String = buildString { appendLine("initialized=$initialized") appendLine("headBranch=$headBranch") appendLine("currentDir=$currentDir") appendLine("branches=${branches.toSortedMap()}") appendLine("tags=${tags.sorted()}") appendLine("remotes=${remotes.toSortedMap()}") appendLine("config=${config.toSortedMap()}") appendLine("stashes=$stashes") appendLine("fetchedBranches=${fetchedBranches.sorted()}") appendLine("fetchHeadCount=$fetchHeadCount") appendLine("pushedBranches=${pushedBranches.sorted()}") appendLine("pushedTags=${pushedTags.sorted()}") appendLine("submodules=${submodules.toSortedMap()}") appendLine("maintenanceActions=${maintenanceActions.sorted()}") appendLine("files=") if (files.isEmpty()) { appendLine(" ") } else { files.sortedBy { it.name }.forEach { file -> appendLine(" ${file.name} staged=${file.staged} tracked=${file.tracked} content=${file.content.toEvidenceValue()}") } } appendLine("commits=") if (commits.isEmpty()) { appendLine(" ") } else { commits.forEach { commit -> appendLine(" ${commit.id} parents=${commit.parentCount} ${commit.authorTimestampSeconds ?: "unknown-time"} ${commit.message}") } } } fun String.toEvidenceValue(): String { if (isEmpty()) return "\"\"" return lineSequence().joinToString("\\n", prefix = "\"", postfix = "\"") } val IMPLEMENTED_UPSTREAM_LEVEL_ORDER = listOf( "init", "config", "add", "commit", "clone", "clone_to_folder", "ignore", "include", "status", "number_of_files_committed", "rm", "rm_cached", "stash", "rename", "restructure", "log", "tag", "push_tags", "commit_amend", "commit_in_future", "reset", "reset_soft", "checkout_file", "remote", "remote_url", "pull", "remote_add", "push", "diff", "blame", "branch", "checkout", "checkout_tag", "checkout_tag_over_branch", "branch_at", "delete_branch", "push_branch", "merge", "fetch", "rebase", "rebase_onto", "repack", "cherry-pick", "grep", "rename_commit", "squash", "merge_squash", "reorder", "bisect", "stage_lines", "find_old_branch", "revert", "restore", "conflict", "submodule", ) } }