package solutions.tretter.githugandroid import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assume.assumeTrue import org.junit.Test import java.io.File import java.nio.file.Files class GitSandboxEngineTest { @Test fun initLevelStartsInsideCreatedGitHugDirectory() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-init-level").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = initLevel() val repo = runtime.prepareLevel(level) val (_, pwdOutput) = runtime.execute(level, repo, "pwd") val (initializedRepo, _) = runtime.execute(level, repo, "git init") assertEquals("git_hug", repo.currentDir) assertFalse(repo.initialized) assertTrue(File(root, "init/git_hug").isDirectory) assertEquals(listOf("/sandbox/git_hug"), pwdOutput) assertTrue(initializedRepo.initialized) assertTrue(File(root, "init/git_hug/.git").isDirectory) assertTrue(level.validator(initializedRepo, "git init")) } finally { root.deleteRecursively() } } @Test fun statusShowsDeletedTrackedFiles() { val repo = RepoState( initialized = true, files = listOf(GitFile("deleteme.rb", tracked = true, deleted = true)), commits = listOf(CommitNode("0000001", "Added a temp file")), branches = mapOf("master" to 1), ) val (_, output) = GitSandboxEngine.execute(repo, "git status") assertTrue(output.any { it.contains("deleted:") && it.contains("deleteme.rb") }) } @Test fun gitRmRemovesDeletedTrackedFileFromIndex() { val repo = RepoState( initialized = true, files = listOf(GitFile("deleteme.rb", tracked = true, deleted = true)), commits = listOf(CommitNode("0000001", "Added a temp file")), branches = mapOf("master" to 1), ) val (updatedRepo, _) = GitSandboxEngine.execute(repo, "git rm deleteme.rb") assertFalse(updatedRepo.files.any { it.name == "deleteme.rb" }) } @Test fun rmLevelIsNotSolvedByReadOnlyCommands() { val level = rmLevel() val repo = level.setup() val (updatedRepo, _) = GitSandboxEngine.execute(repo, "ls") assertFalse(level.validator(updatedRepo, "ls")) } @Test fun gitMvExpandsWildcardSourcesIntoDestinationDirectory() { val repo = RepoState( initialized = true, files = listOf( GitFile("about.html", tracked = true), GitFile("contact.html", tracked = true), GitFile("index.html", tracked = true), GitFile("README", tracked = true), ), branches = mapOf("master" to 1), ) val (updatedRepo, _) = GitSandboxEngine.execute(repo, "git mv *.html src") assertTrue(updatedRepo.files.any { it.name == "src/about.html" }) assertTrue(updatedRepo.files.any { it.name == "src/contact.html" }) assertTrue(updatedRepo.files.any { it.name == "src/index.html" }) assertTrue(updatedRepo.files.any { it.name == "README" }) } @Test fun singleQuotedWildcardIsNotExpanded() { val repo = RepoState( initialized = true, files = listOf(GitFile("README"), GitFile("main.kt")), ) val (_, output) = GitSandboxEngine.execute(repo, "echo '*'") assertEquals(listOf("*"), output) } @Test fun echoRedirectAcceptsNoSurroundingSpaces() { val repo = RepoState(initialized = true) val (updatedRepo, output) = GitSandboxEngine.execute(repo, "echo '*.swp'>.gitignore") assertTrue(output.isEmpty()) assertEquals("*.swp", updatedRepo.files.single { it.name == ".gitignore" }.content) } @Test fun commitHashAnswerAcceptsFullHashThatStartsWithDisplayedShortHash() { val repo = RepoState( initialized = true, commits = listOf(CommitNode("abc1234", "Latest commit")), branches = mapOf("master" to 1), ) assertTrue(commitHashAnswer("0000001")(repo, "abc1234fedcba9876543210fedcba9876543210")) } @Test fun mdIsAcceptedAsMkdirAlias() { val repo = RepoState(initialized = true) val (_, output) = GitSandboxEngine.execute(repo, "md src") assertTrue(output.isEmpty()) } @Test fun dirIsAcceptedAsLsAlias() { val repo = RepoState( initialized = true, files = listOf(GitFile("README"), GitFile("src/main.kt")), ) val (_, output) = GitSandboxEngine.execute(repo, "dir") assertTrue("README" in output) assertTrue("src/main.kt" in output) } @Test fun lsShowsDotfiles() { val repo = RepoState( initialized = true, files = listOf(GitFile(".gitignore"), GitFile("README")), ) val (_, output) = GitSandboxEngine.execute(repo, "ls") assertTrue(".gitignore" in output) } @Test fun lsShowsGitDirectoryEntry() { val repo = RepoState(initialized = true) val (_, output) = GitSandboxEngine.execute(repo, "ls") assertTrue(".git" in output) } @Test fun fileCompletionUsesCurrentDirectory() { val repo = RepoState( initialized = true, currentDir = "src", files = listOf( GitFile("README"), GitFile("src/main.kt"), GitFile("src/model/User.kt"), ), ) assertEquals(listOf("main.kt", "model/User.kt"), fileCompletionCandidates(repo).sorted()) assertEquals(listOf("model/"), directoryCompletionCandidates(repo)) } @Test fun nativeCompletionUsesCurrentDirectoryIncludingGitMetadata() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-completion").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = level( id = "completion-test", title = "Completion Test", description = "", hints = emptyList(), commandSuggestions = emptyList(), setup = { RepoState(initialized = true) }, validator = { _, _ -> false }, ) val repo = runtime.prepareLevel(level) val (gitDirRepo, _) = runtime.execute(level, repo, "cd .git") assertTrue("HEAD" in runtime.completionCandidates(level, gitDirRepo, directoriesOnly = false)) } finally { root.deleteRecursively() } } @Test fun nativeCompletionIncludesExecutableShortcutPaths() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-script-completion").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = bisectLevel() val repo = runtime.prepareLevel(level) val candidates = runtime.completionCandidates(level, repo, directoriesOnly = false) assertTrue("./test-balance.sh" in candidates) } finally { root.deleteRecursively() } } @Test fun nativeGitHelpListsCommandsAndGuides() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-help-listing").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = level( id = "help-listing-test", title = "Help Listing Test", description = "", hints = emptyList(), commandSuggestions = emptyList(), setup = { RepoState(initialized = true) }, validator = { _, _ -> false }, ) val repo = runtime.prepareLevel(level) val (_, commandOutput) = runtime.execute(level, repo, "git help -a") val (_, guideOutput) = runtime.execute(level, repo, "git help -g") assertTrue(commandOutput.any { it.contains("Main Porcelain Commands") }) assertTrue(commandOutput.any { it.contains("commit") && it.contains("Record changes") }) assertTrue(guideOutput.any { it.contains("Git concept guides") }) assertTrue(guideOutput.any { it.contains("tutorial") }) } finally { root.deleteRecursively() } } @Test fun nativeConfigLevelSolvesAfterSettingNameAndEmail() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-config-level").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = configLevel() val repo = runtime.prepareLevel(level) val (namedRepo, _) = runtime.execute(level, repo, "git config user.name githug") val (configuredRepo, _) = runtime.execute(level, namedRepo, "git config user.email xxx@yy.com") assertEquals("githug", configuredRepo.config["user.name"]) assertEquals("xxx@yy.com", configuredRepo.config["user.email"]) assertTrue(level.validator(configuredRepo, "git config user.email xxx@yy.com")) } finally { root.deleteRecursively() } } @Test fun nativeReadOnlyHelperCommandsDoNotRefreshAwayTransientState() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-read-only-helper").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = configLevel() val repo = runtime.prepareLevel(level).copy( maintenanceActions = setOf("transient-marker"), ) val (listedRepo, output) = runtime.execute(level, repo, "ls") assertTrue(".git" in output) assertEquals(setOf("transient-marker"), listedRepo.maintenanceActions) } finally { root.deleteRecursively() } } @Test fun nativeGitExecAliasesRefreshWhenBinaryChanges() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-exec-refresh").toFile() try { val firstGit = File(root, "first-git").also { git.copyTo(it); it.setExecutable(true) } val secondGit = File(root, "second-git").also { git.copyTo(it); it.setExecutable(true) } val level = level( id = "exec-refresh-test", title = "Exec Refresh Test", description = "", hints = emptyList(), commandSuggestions = emptyList(), setup = { RepoState(initialized = true) }, validator = { _, _ -> false }, ) GitRepositoryRuntime(root, firstGit).prepareLevel(level) val alias = File(root, "git-exec/git") assertTrue(alias.exists()) assertTrue(File(root, "git-exec/.binary-fingerprint").readText().contains(firstGit.absolutePath)) GitRepositoryRuntime(root, secondGit).prepareLevel(level) assertTrue(alias.exists()) assertTrue(File(root, "git-exec/.binary-fingerprint").readText().contains(secondGit.absolutePath)) } finally { root.deleteRecursively() } } @Test fun nativePushLevelHasDivergedStatusAndRebasesCleanly() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-push-level").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = pushLevel() val repo = runtime.prepareLevel(level) val (_, statusOutput) = runtime.execute(level, repo, "git status") assertTrue(statusOutput.any { it.contains("diverged", ignoreCase = true) }) val (_, diffOutput) = runtime.execute(level, repo, "git diff") assertTrue(diffOutput.isNotEmpty()) val (rebasedRepo, pullOutput) = runtime.execute(level, repo, "git pull --rebase origin master") assertFalse(pullOutput.any { it.contains("fatal:", ignoreCase = true) || it.contains("error:", ignoreCase = true) }) assertTrue(rebasedRepo.files.any { it.name == "file4" && it.tracked }) } finally { root.deleteRecursively() } } @Test fun nativeDiffLevelShowsLine26Change() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-diff-level").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = diffLevel() val repo = runtime.prepareLevel(level) val appFile = repo.files.single { it.name == "app.rb" } assertTrue(appFile.content.contains("server.json")) val (_, diffOutput) = runtime.execute(level, repo, "git diff") assertTrue(diffOutput.any { it.contains("- @message = get_response('data.json')") }) assertTrue(diffOutput.any { it.contains("+ @message = get_response('server.json')") }) assertTrue(diffOutput.any { it.contains("@@ -23,7 +23,7 @@") }) } finally { root.deleteRecursively() } } @Test fun nativeBlameLevelShowsPasswordAuthor() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-blame-level").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = blameLevel() val repo = runtime.prepareLevel(level) assertTrue(repo.files.single { it.name == "config.rb" }.content.contains("password")) val (_, blameOutput) = runtime.execute(level, repo, "git blame config.rb") assertTrue(blameOutput.any { it.contains("Spider Man") && it.contains("password") }) assertTrue(level.validator(repo, "Spider Man")) } finally { root.deleteRecursively() } } @Test fun cdDotDotShortcutMovesToParentDirectory() { val repo = RepoState(initialized = true, currentDir = "src/main") val (updatedRepo, output) = GitSandboxEngine.execute(repo, "cd..") assertTrue(output.isEmpty()) assertEquals("src", updatedRepo.currentDir) } @Test fun delIsAcceptedAsRmAlias() { val repo = RepoState( initialized = true, files = listOf(GitFile("deleteme.txt")), ) val (updatedRepo, output) = GitSandboxEngine.execute(repo, "del deleteme.txt") assertTrue(output.isEmpty()) assertFalse(updatedRepo.files.any { it.name == "deleteme.txt" }) } @Test fun nativeInteractiveRebaseUsesAppSequenceEditorContent() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-interactive-rebase-editor").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = reorderLevel() val repo = runtime.prepareLevel(level) val invocation = parseGitEditorInvocation("git rebase -i HEAD~3") ?: error("Expected interactive rebase editor invocation") val todo = runtime.gitEditorInitialContent(level, repo, invocation) val reorderedTodo = todo.lines().toMutableList().also { lines -> val secondPick = lines.indexOfFirst { it.contains("Third commit") } val thirdPick = lines.indexOfFirst { it.contains("Second commit") } val thirdLine = lines[thirdPick] lines[thirdPick] = lines[secondPick] lines[secondPick] = thirdLine }.joinToString("\n") val (updatedRepo, output) = runtime.executeGitEditorCommand(level, repo, invocation, reorderedTodo) assertFalse(output.any { it.contains("Terminal is dumb", ignoreCase = true) }) assertTrue(level.validator(updatedRepo, invocation.command)) } finally { root.deleteRecursively() } } @Test fun nativeInteractiveRebaseRewordRequestsCommitMessageEditorAndContinues() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-interactive-rebase-reword-editor").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = renameCommitLevel() val repo = runtime.prepareLevel(level) val invocation = parseGitEditorInvocation("git rebase -i HEAD~2") ?: error("Expected interactive rebase editor invocation") val todo = runtime.gitEditorInitialContent(level, repo, invocation) val rewordTodo = todo.replaceFirst(Regex("(?m)^pick "), "reword ") val rebaseResult = runtime.executeGitEditorCommandWithResult(level, repo, invocation, rewordTodo) val nextEditor = rebaseResult.nextEditor assertNotNull(nextEditor) val message = nextEditor!!.content.replaceFirst("First coommit", "First commit") val completedResult = runtime.executeGitEditorCommandWithResult( level = level, currentRepo = rebaseResult.repo, invocation = nextEditor.invocation, message = message, ) assertEquals(".git/COMMIT_EDITMSG", nextEditor.invocation.displayPath) assertNull(completedResult.nextEditor) assertTrue(level.validator(completedResult.repo, nextEditor.invocation.command)) } finally { root.deleteRecursively() } } @Test fun nativeExecutableShortcutRunsScriptThroughShell() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-script-shortcut").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = bisectLevel() val repo = runtime.prepareLevel(level) val (_, output) = runtime.execute(level, repo, "./test-balance.sh") assertTrue(output.joinToString("\n"), output.any { it.contains("balance broken") }) } finally { root.deleteRecursively() } } @Test fun nativeBisectRunScriptShortcutRunsThroughShell() { val git = testGitBinary() assumeTrue(git.exists() && git.canExecute()) val root = Files.createTempDirectory("githug-bisect-run-script").toFile() try { val runtime = GitRepositoryRuntime(root, git) val level = bisectLevel() var repo = runtime.prepareLevel(level) listOf( "git bisect start", "git bisect bad HEAD", "git bisect good known-good", ).forEach { command -> val (nextRepo, _) = runtime.execute(level, repo, command) repo = nextRepo } val (_, output) = runtime.execute(level, repo, "git bisect run ./test-balance.sh") val text = output.joinToString("\n") assertFalse(text, text.contains("Permission denied", ignoreCase = true)) assertFalse(text, text.contains("can't execute", ignoreCase = true)) assertFalse(text, text.contains("bogus exit code", ignoreCase = true)) assertTrue(text, text.contains("first bad commit", ignoreCase = true)) } finally { root.deleteRecursively() } } private 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 return File("/usr/bin/git") } private fun repoRoot(): File { val userDir = File(System.getProperty("user.dir") ?: ".") return if (File(userDir, "app/build.gradle.kts").exists()) userDir else userDir.parentFile ?: userDir } }