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

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