Files
Githug-Android/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt
Joe Tretter 8873755490 Compile Git for host and Android target platforms
- Rename CrossCompileGitForAndroid.sh to CompileGitForAllTargetPlatforms.sh and support host, Android-only, and all-target modes.
- Compile a development-host Git binary at build/host-git/libgit.so for JVM tests.
- Route level and sandbox tests through GITHUG_TEST_GIT_BINARY so they exercise the compiled host Git rather than the system Git fallback.
- Cross-compile packaged Git binaries for arm64-v8a, armeabi-v7a, x86, and x86_64 Android targets.
- Remove the temporary device-bound Pull rebase instrumentation path from the previous debugging attempt.
2026-05-07 15:26:38 -05:00

251 lines
9.0 KiB
Kotlin

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 levelOrderMatchesUpstreamRubyGithug() {
assertEquals(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(" <no output>")
} 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(),
)
}
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("pushedBranches=${pushedBranches.sorted()}")
appendLine("pushedTags=${pushedTags.sorted()}")
appendLine("submodules=${submodules.toSortedMap()}")
appendLine("maintenanceActions=${maintenanceActions.sorted()}")
appendLine("files=")
if (files.isEmpty()) {
appendLine(" <none>")
} 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(" <none>")
} else {
commits.forEach { commit ->
appendLine(" ${commit.id} ${commit.message}")
}
}
}
fun String.toEvidenceValue(): String {
if (isEmpty()) return "\"\""
return lineSequence().joinToString("\\n", prefix = "\"", postfix = "\"")
}
val 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",
"contribute",
)
}
}