556 lines
21 KiB
Kotlin
556 lines
21 KiB
Kotlin
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
|
|
}
|
|
}
|