Fix upstream fixture-backed level setups
This commit is contained in:
@@ -22,7 +22,7 @@ This file records the upstream Ruby setup and validation intent beside the Andro
|
||||
| `number_of_files_committed` | Initializes repo with committed modified files plus staged/untracked files, leaving exactly two files staged for commit. | Initializes equivalent file states with two staged files. | Equivalent. |
|
||||
| `rm` | Initializes repo, commits `deleteme.rb`, then deletes it from the working tree. | Initializes repo with tracked deleted `deleteme.rb`. | Equivalent. |
|
||||
| `rm_cached` | Initializes repo, stages `.gitignore` and `deleteme.rb`. | Initializes repo with both files staged. | Equivalent. |
|
||||
| `stash` | Copies upstream fixture containing committed `lyrics.txt` plus local modifications. | Initializes repo with tracked modified `lyrics.txt` and matching native fixture. | Equivalent. |
|
||||
| `stash` | Copies upstream fixture containing committed `lyrics.txt` plus local modifications. | Native setup creates the same committed lyrics text and the same trailing `Hey!` working-tree modification. | Equivalent. |
|
||||
| `rename` | Initializes repo, commits `oldfile.txt`. | Initializes repo with tracked `oldfile.txt` and commit. | Equivalent. |
|
||||
| `restructure` | Initializes repo with tracked `about.html`, `contact.html`, `index.html`. | Same tracked HTML files. | Equivalent. |
|
||||
| `log` | Initializes repo with one commit whose message identifies the target. | Same one modeled commit. | Equivalent. |
|
||||
@@ -38,32 +38,32 @@ This file records the upstream Ruby setup and validation intent beside the Andro
|
||||
| `pull` | Initializes repo and adds remote URL `https://github.com/pull-this/thing-to-pull`. | Native setup creates local synthetic remote with a remote commit. | Deliberate offline equivalent to upstream network remote. |
|
||||
| `remote_add` | Initializes repo with no origin remote. | Same. | Equivalent. |
|
||||
| `push` | Creates divergent local and remote master histories: local has `file3`, remote has `file4`. | Native setup creates synthetic divergent origin with the same file/commit shape. | Equivalent. |
|
||||
| `diff` | Copies upstream fixture with committed `app.rb` and one changed line. | Native setup creates committed then modified `app.rb`. | Equivalent. |
|
||||
| `blame` | Copies upstream fixture where password line is authored by Spider Man. | Native setup creates deterministic author history for `config.rb`. | Equivalent. |
|
||||
| `diff` | Copies upstream fixture with committed `app.rb` and one changed line. | Native setup creates the same committed `app.rb` and the same line 26 working-tree modification. | Equivalent. |
|
||||
| `blame` | Copies upstream fixture where password line is authored by Spider Man. | Native setup creates the same `config.rb` content with the password line authored by Spider Man. | Equivalent. |
|
||||
| `branch` | Initializes repo with one `README` commit. | Same. | Equivalent. |
|
||||
| `checkout` | Initializes repo with one `README` commit. | Same. | Equivalent. |
|
||||
| `checkout_tag` | Creates five commits and tags `v1.0`, `v1.2`, `v1.5`. | Same commit/tag shape. | Equivalent. |
|
||||
| `checkout_tag_over_branch` | Creates same tags and also a branch named `v1.2`. | Same tag-plus-branch ambiguity. | Equivalent. |
|
||||
| `branch_at` | Creates three commits on `file1`. | Same three modeled commits and native setup. | Equivalent. |
|
||||
| `delete_branch` | Copies fixture with `delete_me` branch. | Initializes repo with `delete_me` branch. | Equivalent. |
|
||||
| `delete_branch` | Copies fixture with committed `readme` and `delete_me` branch at the same commit as `master`. | Native setup creates committed `readme` with `master` and `delete_me` pointing at it. | Equivalent. |
|
||||
| `push_branch` | Creates local and remote repos with multiple branches and target `test_branch`. | Native setup creates synthetic remote and same branch intent. | Equivalent state projection. |
|
||||
| `merge` | Copies fixture with `file1` on master and `file2` on feature. | Native setup creates master `file1` and feature `file2`. | Equivalent. |
|
||||
| `fetch` | Creates local master tracking origin/master; remote later gains `new_branch` with `file1`. | Native setup creates the same synthetic remote branch scenario; model starts with one fetched head. | Equivalent after latest update. |
|
||||
| `rebase` | Copies fixture with divergent `master` and `feature`. | Native setup creates matching divergent `master` and `feature`. | Equivalent. |
|
||||
| `rebase_onto` | Creates `authors.md`, bad `wrong_branch`, and `readme-update` branch needing `--onto`. | Native setup creates the same branch graph and files. | Equivalent setup. |
|
||||
| `repack` | Initializes repo with one committed `foo`. | Same. | Equivalent. |
|
||||
| `cherry-pick` | Copies fixture with `master` and `new-feature` commits. | Native setup creates equivalent branches/files. | Equivalent. |
|
||||
| `grep` | Copies fixture with four TODO entries. | Initializes `app.rb` with four TODO entries. | Equivalent. |
|
||||
| `cherry-pick` | Copies fixture with `master` and `new-feature`; the desired README commit is not the branch tip. | Native setup creates the same branch shape: learner must cherry-pick the earlier README commit from `new-feature`. | Equivalent. |
|
||||
| `grep` | Copies fixture with four TODO entries across `app.rb` and `config.rb`. | Native setup creates both source files with the same four TODO entries. | Equivalent. |
|
||||
| `rename_commit` | Creates commits `Initial commit`, `First coommit`, `Second commit`. | Same typo commit sequence. | Equivalent. |
|
||||
| `squash` | Creates initial hidden commit plus README and three squash-target README updates. | Same commit sequence. | Equivalent. |
|
||||
| `merge_squash` | Creates master and `long-feature-branch` with multiple feature changes. | Native setup creates the same branch and `file3` feature effect. | Mostly equivalent; Android currently validates less detail. |
|
||||
| `reorder` | Creates commits `Initial Setup`, `First commit`, `Third commit`, `Second commit`. | Same order. | Equivalent. |
|
||||
| `bisect` | Copies upstream Ruby fixture where `ruby prog.rb 5` or `make test` identifies bad hash `18ed2ac`. | Creates a native Git history with `balance.txt`, `test-balance.sh`, and `known-good` tag. | Intentional Android adaptation: no Ruby or make dependency, but real `git bisect` remains playable. |
|
||||
| `stage_lines` | Commits initial `feature.rb`, then leaves two unstaged feature lines. | Initializes tracked `feature.rb` containing both feature lines. | Setup is close, but Android does not yet model partial staged vs unstaged hunks. |
|
||||
| `find_old_branch` | Copies fixture with recoverable `solve_world_hunger` branch. | Initializes branch map with `solve_world_hunger`. | Equivalent for visible exercise state. |
|
||||
| `find_old_branch` | Copies fixture whose active distractor branch is renamed to `master`, with other distractor branches and recoverable `solve_world_hunger`. | Native setup creates `master`, `solve_world_hunger`, `cure_common_cold`, and `blowup_sun_for_ransom` branches with matching working-tree files. | Equivalent. |
|
||||
| `revert` | Creates commits `First commit`, `Bad commit`, `Second commit`. | Same commit messages. | Equivalent. |
|
||||
| `restore` | Creates `file1`, `file2`, then creates and removes `file3` so it is recoverable from reflog/history. | Native setup creates matching history and removes `file3`; model starts without `file3`. | Equivalent. |
|
||||
| `conflict` | Copies fixture with `master` and `mybranch` conflict in `poem.txt`. | Initializes equivalent branch/file conflict fixture. | Equivalent setup. |
|
||||
| `conflict` | Copies fixture with `master` and `mybranch` conflict in non-empty `poem.txt`. | Native setup creates the conflicting poem history, leaving `master` with `Categorized shoes by color` and `mybranch` with the correct `Sat on a wall` line. | Equivalent setup. |
|
||||
| `submodule` | Initializes empty repo. | Same. | Equivalent; network submodule operation is modeled. |
|
||||
| `contribute` | No local setup; solution clones upstream externally. | Empty state. | Deliberate simplification for mobile/offline final prompt. |
|
||||
|
||||
@@ -124,7 +124,7 @@ This file records the upstream Ruby setup and validation intent beside the Andro
|
||||
| `find_old_branch` | Current branch is `solve_world_hunger`. | Same. | Equivalent. |
|
||||
| `revert` | More than three commits and a revert of "Bad commit" exists. | A commit message starts with `Revert`. | Slightly looser; sufficient for current fixture. |
|
||||
| `restore` | `file3` exists. | `file3` is tracked. | Equivalent. |
|
||||
| `conflict` | On `master`, merge commit has two parents, conflict markers removed, both poem lines preserved. | `poem.txt` contains expected merged poem text. | Known gap: Android does not model merge-parent count for conflict resolution. |
|
||||
| `conflict` | On `master`, merge commit has two parents, conflict markers removed, both poem lines preserved. | Requires a merge action on `master`, conflict markers removed, and the correct `Sat on a wall` poem line preserved. | Known gap: Android does not expose merge-parent count in `RepoState`, so it tracks the merge command instead. |
|
||||
| `submodule` | `githug-include-me` directory exists, has README, and is a gitlink/submodule. | `submodules` contains `githug-include-me` URL. | Equivalent state projection. |
|
||||
| `contribute` | Clones upstream and checks for a commit authored by configured user. | Any nonblank command. | Deliberate mobile/offline simplification for the final contribution prompt. |
|
||||
|
||||
@@ -159,6 +159,6 @@ These are the remaining known non-parity items that need additional model suppor
|
||||
|
||||
- `stage_lines`: model partial staged vs unstaged hunks.
|
||||
- `merge_squash`: verify the exact squashed file/content effects.
|
||||
- `conflict`: model merge parent count or inspect merge commit parents.
|
||||
- `conflict`: expose merge parent count in `RepoState` instead of relying on tracked merge-command evidence.
|
||||
- `rebase_onto`: verify final commit count/content and removal of "Wrong changes".
|
||||
- `contribute`, `clone`, `clone_to_folder`: current Android behavior intentionally avoids real network-dependent validation.
|
||||
|
||||
@@ -142,7 +142,8 @@ The Android port keeps the upstream GitHug level order, but some upstream fixtur
|
||||
| `pull`, `fetch`, `push`, `push_branch`, `push_tags` | Use remote-style workflows from upstream fixtures. | Use local synthetic remotes created inside the sandbox and validate fetched/pushed refs through `RepoState`. | This preserves Git behavior without external network dependencies. |
|
||||
| `contribute` | Expects cloning upstream and finding a commit authored by the configured user. | Treated as a mobile/offline final prompt with a nonblank response. | The original workflow leaves the sandbox and depends on external contribution infrastructure. |
|
||||
| `stage_lines` | Requires partial hunk staging: one feature line staged and another left unstaged. | Currently validates that `feature.rb` is staged. | Android does not yet expose enough index-vs-working-tree hunk detail in `RepoState` to validate partial staging precisely. |
|
||||
| `rebase_onto`, `merge_squash`, `conflict`, `repack` | Upstream validates detailed object graph, file content, merge-parent, or object database details. | Android validates the relevant user-facing action or resulting state, but with less object-level detail in some cases. | The current `RepoState` projection does not expose every low-level Git object fact. These should be tightened when the state surface grows. |
|
||||
| `rebase_onto`, `merge_squash`, `repack` | Upstream validates detailed object graph, merge-parent, or object database details. | Android validates the relevant user-facing action or resulting state, but with less object-level detail in some cases. | The current `RepoState` projection does not expose every low-level Git object fact. These should be tightened when the state surface grows. |
|
||||
| `conflict` | Copies the upstream conflicting poem fixture and validates that the merge commit has two parents, conflict markers are removed, and the correct poem line remains. | Recreates the conflicting poem history natively and validates a merge action on `master`, no conflict markers, and the correct `Sat on a wall` line. | Android does not yet expose merge-parent count in `RepoState`, so it uses observed merge-command evidence plus file state. |
|
||||
|
||||
## Level Authoring
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ android {
|
||||
applicationId = "solutions.tretter.githugandroid"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 161
|
||||
versionName = "0.1.160"
|
||||
versionCode = 162
|
||||
versionName = "0.1.161"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
@@ -359,7 +359,7 @@ class GitRepositoryRuntime private constructor(
|
||||
val shouldUseSandboxSemantics = when (gitCommand) {
|
||||
"add" -> tokens.any { it == "-p" || it == "--patch" || it == "-i" || it == "--interactive" }
|
||||
"rebase" -> "--onto" in tokens
|
||||
"merge" -> "--squash" in tokens || tokens.lastOrNull() == "mybranch" || tokens.lastOrNull() == "feature"
|
||||
"merge" -> "--squash" in tokens || tokens.lastOrNull() == "feature"
|
||||
"revert", "stash" -> true
|
||||
"checkout" -> tokens.any { it == "file3" } || tokens.takeLast(2) == listOf("--", "config.rb")
|
||||
"submodule" -> tokens.getOrNull(2) == "add"
|
||||
@@ -553,7 +553,17 @@ class GitRepositoryRuntime private constructor(
|
||||
tokens: List<String>,
|
||||
outputLines: List<String> = emptyList(),
|
||||
): RepoState {
|
||||
if (tokens.firstOrNull() != "git") return inspectedRepo
|
||||
if (tokens.firstOrNull() != "git") {
|
||||
return inspectedRepo.copy(
|
||||
stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes),
|
||||
fetchedBranches = previousRepo.fetchedBranches + inspectedRepo.fetchedBranches,
|
||||
fetchHeadCount = inspectedRepo.fetchHeadCount,
|
||||
pushedBranches = previousRepo.pushedBranches + inspectedRepo.pushedBranches,
|
||||
pushedTags = previousRepo.pushedTags + inspectedRepo.pushedTags,
|
||||
submodules = previousRepo.submodules + inspectedRepo.submodules,
|
||||
maintenanceActions = previousRepo.maintenanceActions + inspectedRepo.maintenanceActions,
|
||||
)
|
||||
}
|
||||
|
||||
return when (tokens.getOrNull(1)) {
|
||||
"bisect" -> {
|
||||
|
||||
@@ -18,11 +18,13 @@ internal fun blameLevel(): Level = level(
|
||||
initialized = true,
|
||||
files = listOf(GitFile("config.rb", blameLevelFinalConfigRb(), tracked = true)),
|
||||
commits = listOf(
|
||||
CommitNode("0000001", "Add default config"),
|
||||
CommitNode("0000002", "Add secret config"),
|
||||
CommitNode("0000003", "Document timeout"),
|
||||
CommitNode("0000001", "added more options (no really)"),
|
||||
CommitNode("0000002", "added more options"),
|
||||
CommitNode("0000003", "added password"),
|
||||
CommitNode("0000004", "added options"),
|
||||
CommitNode("0000005", "added config with name"),
|
||||
),
|
||||
branches = mapOf("master" to 3),
|
||||
branches = mapOf("master" to 5),
|
||||
)
|
||||
},
|
||||
nativeSetup = {
|
||||
@@ -30,26 +32,67 @@ internal fun blameLevel(): Level = level(
|
||||
write(
|
||||
"config.rb",
|
||||
"""
|
||||
Githug::Application.configure do
|
||||
config.cache_classes = true
|
||||
config.log_level = :info
|
||||
class Config
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
end
|
||||
""".trimIndent() + "\n",
|
||||
)
|
||||
addCommit("Add default config", "config.rb", author = "Peter Parker <peter@example.com>")
|
||||
addCommit("added config with name", "config.rb", author = "Gary Rennie <webmaster@gazler.com>")
|
||||
write(
|
||||
"config.rb",
|
||||
"""
|
||||
Githug::Application.configure do
|
||||
config.cache_classes = true
|
||||
config.log_level = :info
|
||||
config.password = "correct horse battery staple"
|
||||
class Config
|
||||
def initialize(name, options = {})
|
||||
@name = name
|
||||
|
||||
if options[:downcase]
|
||||
@name.downcase!
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
""".trimIndent() + "\n",
|
||||
)
|
||||
addCommit("Add secret config", "config.rb", author = "Spider Man <spider@example.com>")
|
||||
append("config.rb", "config.timeout = 30\n")
|
||||
addCommit("Document timeout", "config.rb", author = "Mary Jane <mary@example.com>")
|
||||
addCommit("added options", "config.rb", author = "Spider Man <spidey-sense@tingling.com>")
|
||||
write(
|
||||
"config.rb",
|
||||
"""
|
||||
class Config
|
||||
attr_accessor :name, :password
|
||||
def initialize(name, password = nil, options = {})
|
||||
@name = name
|
||||
|
||||
if options[:downcase]
|
||||
@name.downcase!
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
""".trimIndent() + "\n",
|
||||
)
|
||||
addCommit("added password", "config.rb", author = "Bruce Banner <hulk@smash.com>")
|
||||
write(
|
||||
"config.rb",
|
||||
"""
|
||||
class Config
|
||||
attr_accessor :name, :password
|
||||
def initialize(name, password = nil, options = {})
|
||||
@name = name
|
||||
@password = password || "i<3evil"
|
||||
|
||||
if options[:downcase]
|
||||
@name.downcase!
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
""".trimIndent() + "\n",
|
||||
)
|
||||
addCommit("added more options", "config.rb", author = "Spider Man <spidey-sense@tingling.com>")
|
||||
write("config.rb", blameLevelFinalConfigRb())
|
||||
addCommit("added more options (no really)", "config.rb", author = "Gary Rennie <webmaster@gazler.com>")
|
||||
true
|
||||
},
|
||||
validator = commandAnswer("Spider Man"),
|
||||
@@ -60,10 +103,20 @@ internal fun blameLevel(): Level = level(
|
||||
|
||||
private fun blameLevelFinalConfigRb(): String =
|
||||
"""
|
||||
Githug::Application.configure do
|
||||
config.cache_classes = true
|
||||
config.log_level = :info
|
||||
config.password = "correct horse battery staple"
|
||||
class Config
|
||||
attr_accessor :name, :password
|
||||
def initialize(name, password = nil, options = {})
|
||||
@name = name
|
||||
@password = password || "i<3evil"
|
||||
|
||||
if options[:downcase]
|
||||
@name.downcase!
|
||||
end
|
||||
|
||||
if options[:upcase]
|
||||
@name.upcase!
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
config.timeout = 30
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
@@ -12,8 +12,24 @@ internal fun cherryPickLevel(): Level = level(
|
||||
title = "Cherry Pick",
|
||||
description = "Your new feature isn't worth the time and you're going to delete it. But it has one commit that fills in `README` file, and you want this commit to be on the master as well.",
|
||||
hints = listOf("Sneak a peek at the `git help cherry-pick` command."),
|
||||
commandSuggestions = listOf("git log --oneline new-feature", "git cherry-pick new-feature"),
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("nokia.js", tracked = true)), commits = listOf(CommitNode("1", "Added fancy branded output"), CommitNode("2", "Filled in README.md with proper input")), branches = mapOf("master" to 1, "new-feature" to 2)) },
|
||||
commandSuggestions = listOf("git log --oneline new-feature", "git cherry-pick <commit>"),
|
||||
setup = {
|
||||
RepoState(
|
||||
initialized = true,
|
||||
files = listOf(
|
||||
GitFile("README.md", "I'll fill in the file some time later..\n", tracked = true),
|
||||
GitFile("hardcore-math.js", cherryPickLevelHardcoreMathJs(), tracked = true),
|
||||
GitFile("nokia.js", "console.log(\"[NOKIA] Connecting people\");\n", tracked = true),
|
||||
),
|
||||
commits = listOf(
|
||||
CommitNode("0000001", "Added fancy branded output"),
|
||||
CommitNode("0000002", "Renamed project.js -> herdcore-math.js"),
|
||||
CommitNode("0000003", "Added a hardcore math module"),
|
||||
CommitNode("0000004", "Initial commit"),
|
||||
),
|
||||
branches = mapOf("master" to 4, "new-feature" to 5),
|
||||
)
|
||||
},
|
||||
validator = repoPredicate { repo ->
|
||||
repo.files.any { it.name == "README.md" && it.tracked } &&
|
||||
repo.headBranch == "master" &&
|
||||
@@ -21,17 +37,35 @@ internal fun cherryPickLevel(): Level = level(
|
||||
},
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write("nokia.js", "console.log('Nokia tune')\n")
|
||||
write("README.md", "I'll fill in the file some time later..\n")
|
||||
addCommit("Initial commit", "README.md")
|
||||
write("project.js", "for(var i = 0; i < 10; i++) {\n console.log(42 * i);\n}\n")
|
||||
addCommit("Added a hardcore math module", "project.js")
|
||||
git("mv", "project.js", "hardcore-math.js")
|
||||
commit("Renamed project.js -> herdcore-math.js")
|
||||
write("nokia.js", "console.log(\"[NOKIA] Connecting people\");\n")
|
||||
addCommit("Added fancy branded output", "nokia.js")
|
||||
checkoutNew("new-feature")
|
||||
write("README.md", "Proper input instructions\n")
|
||||
git("branch", "new-feature", "HEAD~3")
|
||||
checkout("new-feature")
|
||||
write("feature.js", "function connect() {\n}\n")
|
||||
addCommit("Added a stub for the feature", "feature.js")
|
||||
write("README.md", "This project now documents its proper input.\n")
|
||||
addCommit("Filled in README.md with proper input", "README.md")
|
||||
append("feature.js", "connect();\n")
|
||||
addCommit("Fixed feature", "feature.js")
|
||||
append("feature.js", "console.log('done');\n")
|
||||
addCommit("some small fixes", "feature.js")
|
||||
checkout("master")
|
||||
git("branch", "-f", "feature", "new-feature")
|
||||
true
|
||||
},
|
||||
testCases = listOf(
|
||||
levelTestCase("cherry pick new feature tip", "git cherry-pick new-feature"),
|
||||
levelTestCase("cherry pick feature alias", "git cherry-pick feature"),
|
||||
levelTestCase("cherry pick README commit", "git cherry-pick new-feature~2"),
|
||||
),
|
||||
)
|
||||
|
||||
private fun cherryPickLevelHardcoreMathJs(): String =
|
||||
"""
|
||||
for(var i = 0; i < 10; i++) {
|
||||
console.log(42 * i);
|
||||
}
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
@@ -13,9 +13,78 @@ internal fun conflictLevel(): Level = level(
|
||||
description = "You need to merge mybranch into the current branch (master). But there may be some incorrect changes in mybranch which may cause conflicts. Solve any merge-conflicts you come across and finish the merge.",
|
||||
hints = emptyList(),
|
||||
commandSuggestions = listOf("git merge mybranch"),
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("poem.txt", tracked = true)), branches = mapOf("master" to 2, "mybranch" to 2)) },
|
||||
validator = repoPredicate { repo -> repo.files.find { it.name == "poem.txt" }?.content?.contains("Humpty Dumpty") == true },
|
||||
setup = {
|
||||
RepoState(
|
||||
initialized = true,
|
||||
files = listOf(GitFile("poem.txt", conflictLevelMasterPoem(), tracked = true)),
|
||||
branches = mapOf("master" to 3, "mybranch" to 4),
|
||||
)
|
||||
},
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write("poem.txt", conflictLevelInitialPoem())
|
||||
addCommit("Initial commit", "poem.txt")
|
||||
checkoutNew("mybranch")
|
||||
write("poem.txt", conflictLevelBranchWrongPoem())
|
||||
addCommit("Added lines", "poem.txt")
|
||||
append("poem.txt", "\nThis is a cool poem everyone should learn.\n")
|
||||
addCommit("Added comment", "poem.txt")
|
||||
write("poem.txt", conflictLevelSolvedPoem())
|
||||
addCommit("Changed the poem", "poem.txt")
|
||||
checkout("master")
|
||||
write("poem.txt", conflictLevelSolvedPoem())
|
||||
addCommit("Added two lines", "poem.txt")
|
||||
write("poem.txt", conflictLevelMasterPoem())
|
||||
addCommit("Updated the poem", "poem.txt")
|
||||
true
|
||||
},
|
||||
validator = repoPredicate { repo ->
|
||||
val poem = repo.files.find { it.name == "poem.txt" }?.content.orEmpty()
|
||||
repo.headBranch == "master" &&
|
||||
"merge" in repo.maintenanceActions &&
|
||||
"Sat on a wall" in poem &&
|
||||
poem.none { it in "<>=|" }
|
||||
},
|
||||
testCases = listOf(
|
||||
levelTestCase("merge and resolve cleanly", "git merge mybranch"),
|
||||
levelTestCase(
|
||||
"merge and resolve cleanly",
|
||||
"git merge mybranch",
|
||||
"echo \"Humpty dumpty\" > poem.txt",
|
||||
"echo \"Sat on a wall\" >> poem.txt",
|
||||
"echo \"Humpty dumpty\" >> poem.txt",
|
||||
"echo \"Had a great fall\" >> poem.txt",
|
||||
"git add poem.txt",
|
||||
"git commit --no-edit",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun conflictLevelInitialPoem(): String =
|
||||
"""
|
||||
Humpty dumpty
|
||||
Had a great fall
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
private fun conflictLevelBranchWrongPoem(): String =
|
||||
"""
|
||||
Humpty dumpty
|
||||
Fell on a doll
|
||||
Humpty dumpty
|
||||
Had a great fall
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
private fun conflictLevelSolvedPoem(): String =
|
||||
"""
|
||||
Humpty dumpty
|
||||
Sat on a wall
|
||||
Humpty dumpty
|
||||
Had a great fall
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
private fun conflictLevelMasterPoem(): String =
|
||||
"""
|
||||
Humpty dumpty
|
||||
Categorized shoes by color
|
||||
Humpty dumpty
|
||||
Had a great fall
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
@@ -13,7 +13,21 @@ internal fun deleteBranchLevel(): Level = level(
|
||||
description = "You have created too many branches for your project. There is an old branch in your repo called 'delete_me', you should delete it.",
|
||||
hints = listOf("Running 'git --help branch' will give you a list of branch commands."),
|
||||
commandSuggestions = listOf("git branch -d delete_me"),
|
||||
setup = { RepoState(initialized = true, branches = mapOf("master" to 1, "delete_me" to 1)) },
|
||||
setup = {
|
||||
RepoState(
|
||||
initialized = true,
|
||||
files = listOf(GitFile("readme", tracked = true)),
|
||||
commits = listOf(CommitNode("0000001", "first commit")),
|
||||
branches = mapOf("master" to 1, "delete_me" to 1),
|
||||
)
|
||||
},
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write("readme")
|
||||
addCommit("first commit", "readme")
|
||||
git("branch", "delete_me")
|
||||
true
|
||||
},
|
||||
validator = repoPredicate { repo -> "delete_me" !in repo.branches },
|
||||
testCases = listOf(
|
||||
levelTestCase("delete branch", "git branch -d delete_me"),
|
||||
|
||||
@@ -17,14 +17,14 @@ internal fun diffLevel(): Level = level(
|
||||
RepoState(
|
||||
initialized = true,
|
||||
files = listOf(GitFile("app.rb", diffLevelModifiedAppRb(), tracked = true)),
|
||||
commits = listOf(CommitNode("0000001", "Add app routes")),
|
||||
commits = listOf(CommitNode("0000001", "added app.rb")),
|
||||
branches = mapOf("master" to 1),
|
||||
)
|
||||
},
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write("app.rb", diffLevelBaselineAppRb())
|
||||
addCommit("Add app routes", "app.rb")
|
||||
addCommit("added app.rb", "app.rb")
|
||||
write("app.rb", diffLevelModifiedAppRb())
|
||||
true
|
||||
},
|
||||
@@ -34,38 +34,53 @@ internal fun diffLevel(): Level = level(
|
||||
),
|
||||
)
|
||||
|
||||
internal fun diffLevelBaselineAppRb(): String = buildString {
|
||||
appendLine("require 'sinatra'")
|
||||
appendLine("require 'json'")
|
||||
appendLine()
|
||||
appendLine("helpers do")
|
||||
appendLine(" def get_response(source)")
|
||||
appendLine(" JSON.parse(File.read(source))['message']")
|
||||
appendLine(" end")
|
||||
appendLine("end")
|
||||
appendLine()
|
||||
appendLine("get '/' do")
|
||||
appendLine(" @message = 'hello'")
|
||||
appendLine(" erb :index")
|
||||
appendLine("end")
|
||||
appendLine()
|
||||
appendLine("get '/page' do")
|
||||
appendLine(" @message = 'page'")
|
||||
appendLine(" erb :page")
|
||||
appendLine("end")
|
||||
appendLine()
|
||||
appendLine("get '/yet_another' do")
|
||||
appendLine(" @message = 'another'")
|
||||
appendLine(" erb :success")
|
||||
appendLine("end")
|
||||
appendLine()
|
||||
appendLine("get '/another_page' do")
|
||||
appendLine(" @message = get_response('data.json')")
|
||||
appendLine(" erb :another")
|
||||
appendLine("end")
|
||||
appendLine()
|
||||
appendLine("# end of application")
|
||||
}
|
||||
internal fun diffLevelBaselineAppRb(): String =
|
||||
"""
|
||||
require 'sinatra'
|
||||
require 'oauth2'
|
||||
require 'json'
|
||||
enable :sessions
|
||||
|
||||
def client
|
||||
OAuth2::Client.new("mTeZFqkCmzc8JnjKXaSww95bFFxhUpp1wwmSi8vG", "a9OMyEdW7JvWThHmmvFcShR9P2dyad3EGuA2ULDh", :site => "http://localhost:3000")
|
||||
end
|
||||
|
||||
get "/auth/test" do
|
||||
redirect client.auth_code.authorize_url(:redirect_uri => redirect_uri)
|
||||
end
|
||||
|
||||
get '/auth/test/callback' do
|
||||
access_token = client.auth_code.get_token(params[:code], :redirect_uri => redirect_uri)
|
||||
session[:access_token] = access_token.token
|
||||
@message = "Successfully authenticated with the server"
|
||||
erb :success
|
||||
end
|
||||
|
||||
get '/yet_another' do
|
||||
@message = get_response('data.json')
|
||||
erb :success
|
||||
end
|
||||
get '/another_page' do
|
||||
@message = get_response('data.json')
|
||||
erb :another
|
||||
end
|
||||
|
||||
def get_response(url)
|
||||
access_token = OAuth2::AccessToken.new(client, session[:access_token])
|
||||
JSON.parse(access_token.get("/api/v1/#{url}").body)
|
||||
end
|
||||
|
||||
|
||||
def redirect_uri
|
||||
uri = URI.parse(request.url)
|
||||
uri.path = '/auth/test/callback'
|
||||
uri.query = nil
|
||||
uri.to_s
|
||||
end
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
internal fun diffLevelModifiedAppRb(): String =
|
||||
diffLevelBaselineAppRb().replace("get_response('data.json')", "get_response('server.json')")
|
||||
diffLevelBaselineAppRb().replace(
|
||||
"get '/another_page' do\n @message = get_response('data.json')",
|
||||
"get '/another_page' do\n @message = get_response('server.json')",
|
||||
)
|
||||
|
||||
@@ -13,7 +13,36 @@ internal fun findOldBranchLevel(): Level = level(
|
||||
description = "You have been working on a branch but got distracted by a major issue. Switch back to that branch even though you forgot the name of it.",
|
||||
hints = listOf("Ever played with the `git reflog` command?"),
|
||||
commandSuggestions = listOf("git checkout solve_world_hunger"),
|
||||
setup = { RepoState(initialized = true, branches = mapOf("master" to 1, "solve_world_hunger" to 2)) },
|
||||
setup = {
|
||||
RepoState(
|
||||
initialized = true,
|
||||
headBranch = "master",
|
||||
files = listOf(
|
||||
GitFile("myfile.txt", "THIS TEXT DOESN'T MATTER\n", tracked = true),
|
||||
GitFile("TODO", "FIND THE JOKER\n", tracked = true),
|
||||
),
|
||||
branches = mapOf(
|
||||
"blowup_sun_for_ransom" to 1,
|
||||
"cure_common_cold" to 1,
|
||||
"master" to 2,
|
||||
"solve_world_hunger" to 2,
|
||||
),
|
||||
)
|
||||
},
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write("myfile.txt", "THIS TEXT DOESN'T MATTER\n")
|
||||
addCommit("initial commit", "myfile.txt")
|
||||
git("branch", "blowup_sun_for_ransom")
|
||||
git("branch", "cure_common_cold")
|
||||
checkoutNew("solve_world_hunger")
|
||||
write("TODO", "FIX WORLD HUNGER\n")
|
||||
addCommit("commit todo", "TODO")
|
||||
checkout("master")
|
||||
write("TODO", "FIND THE JOKER\n")
|
||||
addCommit("commit another todo", "TODO")
|
||||
true
|
||||
},
|
||||
validator = repoPredicate { repo -> repo.headBranch == "solve_world_hunger" },
|
||||
testCases = listOf(
|
||||
levelTestCase("checkout old branch", "git checkout solve_world_hunger"),
|
||||
|
||||
@@ -13,9 +13,95 @@ internal fun grepLevel(): Level = level(
|
||||
description = "Your project's deadline approaches, you should evaluate how many TODOs are left in your code",
|
||||
hints = listOf("You want to research the `git grep` command."),
|
||||
commandSuggestions = listOf("git grep TODO"),
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("app.rb", "# TODO\n# TODO\n# TODO\n# TODO", tracked = true)), branches = mapOf("master" to 1)) },
|
||||
setup = {
|
||||
RepoState(
|
||||
initialized = true,
|
||||
files = listOf(
|
||||
GitFile("app.rb", grepLevelAppRb(), tracked = true),
|
||||
GitFile("config.rb", grepLevelConfigRb(), tracked = true),
|
||||
),
|
||||
commits = listOf(CommitNode("0000001", "Add application files.")),
|
||||
branches = mapOf("master" to 1),
|
||||
)
|
||||
},
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write("app.rb", grepLevelAppRb())
|
||||
write("config.rb", grepLevelConfigRb())
|
||||
addCommit("Add application files.", "app.rb", "config.rb")
|
||||
true
|
||||
},
|
||||
validator = commandAnswer("4"),
|
||||
testCases = listOf(
|
||||
levelTestCase("answer todo count", "4"),
|
||||
),
|
||||
)
|
||||
|
||||
private fun grepLevelAppRb(): String =
|
||||
"""
|
||||
require 'sinatra'
|
||||
require 'oauth2'
|
||||
require 'json'
|
||||
enable :sessions
|
||||
|
||||
# TODO Make site url variable.
|
||||
def client
|
||||
OAuth2::Client.new("mTeZFqkCmzc8JnjKXaSww95bFFxhUpp1wwmSi8vG", "a9OMyEdW7JvWThHmmvFcShR9P2dyad3EGuA2ULDh", :site => "http://localhost:3000")
|
||||
end
|
||||
|
||||
get "/auth/test" do
|
||||
redirect client.auth_code.authorize_url(:redirect_uri => redirect_uri)
|
||||
end
|
||||
|
||||
get '/auth/test/callback' do
|
||||
access_token = client.auth_code.get_token(params[:code], :redirect_uri => redirect_uri)
|
||||
session[:access_token] = access_token.token
|
||||
@message = "Successfully authenticated with the server"
|
||||
erb :success
|
||||
end
|
||||
|
||||
get '/yet_another' do
|
||||
@message = get_response('data.json')
|
||||
erb :success
|
||||
end
|
||||
|
||||
get '/another_page' do
|
||||
@message = get_response('server.json')
|
||||
erb :another
|
||||
end
|
||||
|
||||
# TODO Make API version variable.
|
||||
def get_response(url)
|
||||
access_token = OAuth2::AccessToken.new(client, session[:access_token])
|
||||
JSON.parse(access_token.get("/api/v1/#{url}").body)
|
||||
end
|
||||
|
||||
# TODO Redirecting queries could be useful.
|
||||
def redirect_uri
|
||||
uri = URI.parse(request.url)
|
||||
uri.path = '/auth/test/callback'
|
||||
uri.query = nil
|
||||
uri.to_s
|
||||
end
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
private fun grepLevelConfigRb(): String =
|
||||
"""
|
||||
class Config
|
||||
attr_accessor :name, :password
|
||||
def initialize(name, password = nil, options = {})
|
||||
@name = name
|
||||
# TODO Move password to a configuration file.
|
||||
@password = password || "i<3evil"
|
||||
|
||||
if options[:downcase]
|
||||
@name.downcase!
|
||||
end
|
||||
|
||||
if options[:upcase]
|
||||
@name.upcase!
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
@@ -13,20 +13,12 @@ internal fun stashLevel(): Level = level(
|
||||
description = "You've made some changes and want to work on them later. You should save them, but don't commit them.",
|
||||
hints = listOf("It's like stashing. Try finding an appropriate git command."),
|
||||
commandSuggestions = listOf("git stash", "git status"),
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("lyrics.txt", "modified lyrics", tracked = true)), commits = listOf(CommitNode("0000001", "Initial commit")), branches = mapOf("master" to 1)) },
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("lyrics.txt", stashLevelModifiedLyrics(), tracked = true)), commits = listOf(CommitNode("0000001", "Add some lyrics")), branches = mapOf("master" to 1)) },
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write(
|
||||
"lyrics.txt",
|
||||
"""
|
||||
Down in Louisiana in that sunny clime,
|
||||
They play a class of music that is super fine,
|
||||
And it makes no difference if its rain or shine,
|
||||
You can hear that that jazz band music playing all the time.
|
||||
""".trimIndent() + "\n",
|
||||
)
|
||||
write("lyrics.txt", stashLevelCommittedLyrics())
|
||||
addCommit("Add some lyrics", "lyrics.txt")
|
||||
append("lyrics.txt", "\nHey!\n")
|
||||
write("lyrics.txt", stashLevelModifiedLyrics())
|
||||
true
|
||||
},
|
||||
validator = repoPredicate { repo -> repo.stashes.isNotEmpty() && repo.files.none { it.staged } },
|
||||
@@ -34,3 +26,21 @@ internal fun stashLevel(): Level = level(
|
||||
levelTestCase("stash changes", "git stash"),
|
||||
),
|
||||
)
|
||||
|
||||
private fun stashLevelCommittedLyrics(): String =
|
||||
"""
|
||||
Down in Louisiana in that sunny clime,
|
||||
They play a class of music that is super fine,
|
||||
And it makes no difference if its rain or shine,
|
||||
You can hear that that jazz band music playing all the time.
|
||||
It sounds so peculiar cause the music's queer.
|
||||
How its sweet vibration seem to fill the air.
|
||||
Then to you the whole world seems to be in rhyme.
|
||||
You want nothing else but blues-band music all the time.
|
||||
|
||||
Ev'ry one that's nigh
|
||||
Never seems to sigh,
|
||||
Hear them loudly cry:
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
private fun stashLevelModifiedLyrics(): String = stashLevelCommittedLyrics() + "Hey!\n"
|
||||
|
||||
@@ -98,6 +98,52 @@ class LevelSolutionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun upstreamFixtureBackedLevelsExposeSourceShapedSetup() {
|
||||
val gitBinary = testGitBinary()
|
||||
val runtimeRoot = testSandboxRoot().apply {
|
||||
deleteRecursively()
|
||||
mkdirs()
|
||||
}
|
||||
|
||||
fun prepared(level: Level): RepoState = GitRepositoryRuntime(runtimeRoot, gitBinary).prepareLevel(level)
|
||||
fun RepoState.fileContent(path: String): String = files.firstOrNull { it.name == path }?.content.orEmpty()
|
||||
|
||||
val conflict = prepared(conflictLevel())
|
||||
assertEquals("master", conflict.headBranch)
|
||||
assertTrue(conflict.fileContent("poem.txt").contains("Categorized shoes by color"))
|
||||
assertTrue(conflict.branches.containsKey("mybranch"))
|
||||
|
||||
val grep = prepared(grepLevel())
|
||||
assertTrue(grep.fileContent("app.rb").contains("# TODO Make site url variable."))
|
||||
assertTrue(grep.fileContent("config.rb").contains("# TODO Move password to a configuration file."))
|
||||
|
||||
val findOldBranch = prepared(findOldBranchLevel())
|
||||
assertEquals("master", findOldBranch.headBranch)
|
||||
assertEquals(
|
||||
setOf("blowup_sun_for_ransom", "cure_common_cold", "master", "solve_world_hunger"),
|
||||
findOldBranch.branches.keys,
|
||||
)
|
||||
|
||||
val deleteBranch = prepared(deleteBranchLevel())
|
||||
assertTrue(deleteBranch.files.any { it.name == "readme" && it.tracked })
|
||||
assertTrue(deleteBranch.branches.containsKey("delete_me"))
|
||||
|
||||
val diff = prepared(diffLevel())
|
||||
assertTrue(diff.fileContent("app.rb").contains("@message = get_response('server.json')"))
|
||||
|
||||
val stash = prepared(stashLevel())
|
||||
assertTrue(stash.fileContent("lyrics.txt").contains("Hear them loudly cry:\nHey!"))
|
||||
|
||||
val cherryPick = prepared(cherryPickLevel())
|
||||
assertTrue(cherryPick.fileContent("README.md").contains("I'll fill in the file some time later.."))
|
||||
assertTrue(cherryPick.fileContent("hardcore-math.js").contains("console.log(42 * i);"))
|
||||
assertTrue(cherryPick.branches.containsKey("new-feature"))
|
||||
|
||||
val blame = prepared(blameLevel())
|
||||
assertTrue(blame.fileContent("config.rb").contains("@password = password || \"i<3evil\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reportedRegressionCommandsDoNotSolveLevels() {
|
||||
val statusRepo = statusLevel().setup()
|
||||
|
||||
Reference in New Issue
Block a user