Fix upstream fixture-backed level setups

This commit is contained in:
Joe Tretter
2026-05-19 15:52:22 -05:00
parent 2091713409
commit 5db02adbe9
13 changed files with 462 additions and 95 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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" -> {

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"),

View File

@@ -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')",
)

View File

@@ -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"),

View File

@@ -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"

View File

@@ -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"

View File

@@ -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()