diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 67cbe0b..1377e07 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,4 +19,4 @@ jobs: with: python-version: '3.13' - - run: make test + - run: sudo make test diff --git a/Makefile b/Makefile index 28950c9..03339f4 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ build: go build -o dist/main.out ./cmd/tester test: - go test -v ./internal/ + go test -v -count=1 ./internal/ test_with_git: build CODECRAFTERS_REPOSITORY_DIR=$(shell pwd)/internal/test_helpers/pass_all \ diff --git a/internal/stage_clone_repository.go b/internal/stage_clone_repository.go index a0cb9fa..afa588f 100644 --- a/internal/stage_clone_repository.go +++ b/internal/stage_clone_repository.go @@ -31,7 +31,7 @@ func (r TestRepo) randomFile() TestFile { return r.exampleFiles[random.RandomInt(0, len(r.exampleFiles))] } -var testRepos []TestRepo = []TestRepo{ +var testRepos = []TestRepo{ { url: "https://github.com/codecrafters-io/git-sample-1", exampleCommits: []string{ @@ -79,6 +79,7 @@ func randomRepo() TestRepo { func testCloneRepository(harness *test_case_harness.TestCaseHarness) error { logger := harness.Logger executable := harness.Executable + RelocateSystemGit(harness, logger) tempDir, err := os.MkdirTemp("", "worktree") if err != nil { diff --git a/internal/stage_create_blob.go b/internal/stage_create_blob.go index e65c144..99b656d 100644 --- a/internal/stage_create_blob.go +++ b/internal/stage_create_blob.go @@ -14,6 +14,7 @@ import ( func testCreateBlob(harness *test_case_harness.TestCaseHarness) error { logger := harness.Logger executable := harness.Executable + RelocateSystemGit(harness, logger) tempDir, err := os.MkdirTemp("", "worktree") if err != nil { diff --git a/internal/stage_create_commit.go b/internal/stage_create_commit.go index a7d8659..024c393 100644 --- a/internal/stage_create_commit.go +++ b/internal/stage_create_commit.go @@ -17,6 +17,7 @@ import ( func testCreateCommit(harness *test_case_harness.TestCaseHarness) error { logger := harness.Logger executable := harness.Executable + RelocateSystemGit(harness, logger) tempDir, err := os.MkdirTemp("", "worktree") if err != nil { diff --git a/internal/stage_init.go b/internal/stage_init.go index 5e2f922..2b813c1 100644 --- a/internal/stage_init.go +++ b/internal/stage_init.go @@ -12,6 +12,7 @@ import ( func testInit(harness *test_case_harness.TestCaseHarness) error { logger := harness.Logger executable := harness.Executable + RelocateSystemGit(harness, logger) tempDir, err := os.MkdirTemp("", "worktree") if err != nil { diff --git a/internal/stage_read_blob.go b/internal/stage_read_blob.go index c3340bb..a9e3335 100644 --- a/internal/stage_read_blob.go +++ b/internal/stage_read_blob.go @@ -17,6 +17,7 @@ import ( func testReadBlob(harness *test_case_harness.TestCaseHarness) error { logger := harness.Logger executable := harness.Executable + RelocateSystemGit(harness, logger) tempDir, err := os.MkdirTemp("", "worktree") if err != nil { diff --git a/internal/stage_read_tree.go b/internal/stage_read_tree.go index ebc7dd1..0669665 100644 --- a/internal/stage_read_tree.go +++ b/internal/stage_read_tree.go @@ -21,6 +21,7 @@ import ( func testReadTree(harness *test_case_harness.TestCaseHarness) error { logger := harness.Logger executable := harness.Executable + RelocateSystemGit(harness, logger) tempDir, err := os.MkdirTemp("", "worktree") if err != nil { diff --git a/internal/stage_write_tree.go b/internal/stage_write_tree.go index e965080..2dfe4ba 100644 --- a/internal/stage_write_tree.go +++ b/internal/stage_write_tree.go @@ -21,6 +21,8 @@ import ( func testWriteTree(harness *test_case_harness.TestCaseHarness) error { logger := harness.Logger executable := harness.Executable + // This stage Requires the git binary for verifying the git object + // So, no relocation is done for this stage tempDir, err := os.MkdirTemp("", "worktree") if err != nil { diff --git a/internal/stages_test.go b/internal/stages_test.go index e220a07..d53fb88 100644 --- a/internal/stages_test.go +++ b/internal/stages_test.go @@ -2,6 +2,7 @@ package internal import ( "os" + "regexp" "testing" tester_utils_testing "github.com/codecrafters-io/tester-utils/testing" @@ -102,11 +103,29 @@ func TestStages(t *testing.T) { StdoutFixturePath: "./test_helpers/fixtures/write_tree", NormalizeOutputFunc: normalizeTesterOutput, }, + "pass_all": { + UntilStageSlug: "mg6", + CodePath: "./test_helpers/pass_all", + ExpectedExitCode: 0, + StdoutFixturePath: "./test_helpers/fixtures/pass_all", + NormalizeOutputFunc: normalizeTesterOutput, + }, } tester_utils_testing.TestTesterOutput(t, testerDefinition, testCases) } func normalizeTesterOutput(testerOutput []byte) []byte { + replacements := map[string][]*regexp.Regexp{ + "initalize_line": {regexp.MustCompile(`Initialized empty Git repository in .*.git/`)}, + "[your_program] commit-tree-sha": {regexp.MustCompile(`\[your_program\] .{4}[a-z0-9]{40}`)}, + } + + for replacement, regexes := range replacements { + for _, regex := range regexes { + testerOutput = regex.ReplaceAll(testerOutput, []byte(replacement)) + } + } + return testerOutput } diff --git a/internal/test_helpers/fixtures/pass_all b/internal/test_helpers/fixtures/pass_all new file mode 100644 index 0000000..bf808fd --- /dev/null +++ b/internal/test_helpers/fixtures/pass_all @@ -0,0 +1,70 @@ +Debug = true + +[tester::#MG6] Running tests for Stage #MG6 (mg6) +[tester::#MG6] $ ./your_program.sh clone https://github.com/codecrafters-io/git-sample-1  +[your_program] Cloning into 'test_dir'... +[tester::#MG6] $ git cat-file commit 3b0466d22854e57bf9ad3ccf82008a2d3f199550 +[tester::#MG6] Commit contents verified +[tester::#MG6] Reading contents of a sample file +[tester::#MG6] File contents verified +[tester::#MG6] Test passed. + +[tester::#JM9] Running tests for Stage #JM9 (jm9) +[tester::#JM9] Running git init +[tester::#JM9] Creating some files & directories +[tester::#JM9] Running git commit --all +[tester::#JM9] Creating another file +[tester::#JM9] $ ./your_program.sh commit-tree -p -m  +[your_program] 1b6618b6c3647ff62b3941c13245f89ff309f1d6 +[tester::#JM9] Running git cat-file commit  +[tester::#JM9] Test passed. + +[tester::#FE4] Running tests for Stage #FE4 (fe4) +[tester::#FE4] $ ./your_program.sh init +[your_program] Initialized empty Git repository in /tmp/worktree576017363/.git/ +[tester::#FE4] Creating some files & directories +[tester::#FE4] $ ./your_program.sh write-tree +[your_program] 5794e0cecb636294c4c6753742dcf616f21b7f8e +[tester::#FE4] Reading file at .git/objects/57/94e0cecb636294c4c6753742dcf616f21b7f8e +[tester::#FE4] Found git object file written at .git/objects/57/94e0cecb636294c4c6753742dcf616f21b7f8e. +[tester::#FE4] $ git ls-tree --name-only 5794e0cecb636294c4c6753742dcf616f21b7f8e +[tester::#FE4] Test passed. + +[tester::#KP1] Running tests for Stage #KP1 (kp1) +[tester::#KP1] $ ./your_program.sh init +[your_program] Initialized empty Git repository in /tmp/worktree1592248827/.git/ +[tester::#KP1] Writing a tree to git storage.. +[tester::#KP1] $ ./your_program.sh ls-tree --name-only b8b2ce4032667654a481b94b38818b24fe84c460 +[your_program] grape +[your_program] orange +[your_program] strawberry +[tester::#KP1] Test passed. + +[tester::#JT4] Running tests for Stage #JT4 (jt4) +[tester::#JT4] $ ./your_program.sh init +[your_program] Initialized empty Git repository in /tmp/worktree1527710594/.git/ +[tester::#JT4] $ echo "raspberry blueberry banana pear pineapple apple" > banana.txt +[tester::#JT4] $ ./your_program.sh hash-object -w banana.txt +[your_program] 27ece14445d59efd51d58d64afe55b2d912b2503 +[tester::#JT4] Output is a 40-char SHA. +[tester::#JT4] Blob file contents are valid. +[tester::#JT4] Returned SHA matches expected SHA. +[tester::#JT4] Test passed. + +[tester::#IC4] Running tests for Stage #IC4 (ic4) +[tester::#IC4] $ ./your_program.sh init +[your_program] Initialized empty Git repository in /tmp/worktree1802656906/.git/ +[tester::#IC4] Added blob object to .git/objects: da6ad811e0463b5b347359cc21fb53a3e5010d10 +[tester::#IC4] $ ./your_program.sh cat-file -p da6ad811e0463b5b347359cc21fb53a3e5010d10 +[your_program] pineapple grape orange blueberry apple mango +[tester::#IC4] Output is valid. +[tester::#IC4] Test passed. + +[tester::#GG4] Running tests for Stage #GG4 (gg4) +[tester::#GG4] $ ./your_program.sh init +[your_program] Initialized empty Git repository in /tmp/worktree164141277/.git/ +[tester::#GG4] .git directory found. +[tester::#GG4] .git/objects directory found. +[tester::#GG4] .git/refs directory found. +[tester::#GG4] .git/HEAD file is valid. +[tester::#GG4] Test passed. diff --git a/internal/test_helpers/pass_all/codecrafters.yml b/internal/test_helpers/pass_all/codecrafters.yml index 533cd7a..cf09760 100644 --- a/internal/test_helpers/pass_all/codecrafters.yml +++ b/internal/test_helpers/pass_all/codecrafters.yml @@ -1,11 +1,11 @@ -# This is the current stage that you're on. -# -# Whenever you want to advance to the next stage, -# bump this to the next number. -current_stage: 1 - # Set this to true if you want debug logs. # # These can be VERY verbose, so we suggest turning them off # unless you really need them. debug: true + +# Use this to change the Go version used to run your code +# on Codecrafters. +# +# Available versions: go-1.24 +language_pack: go-1.24 diff --git a/internal/test_helpers/pass_all/your_git.sh b/internal/test_helpers/pass_all/your_git.sh deleted file mode 100755 index d7d4c22..0000000 --- a/internal/test_helpers/pass_all/your_git.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -if [ "$1" = "write-tree" ] -then - git add . -fi - -exec git "$@" diff --git a/internal/test_helpers/pass_all/your_program.sh b/internal/test_helpers/pass_all/your_program.sh new file mode 100755 index 0000000..f0705bf --- /dev/null +++ b/internal/test_helpers/pass_all/your_program.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +# Check if git from PATH is working first +if command -v git >/dev/null 2>&1; then + if [ "$1" = "write-tree" ]; then + git add . + fi + exec git "$@" +fi + +# Find git binary in /tmp locations +for tmpdir in /tmp/git-*/git; do + if [ -x "$tmpdir" ]; then + # If defaultBranch config is not set, we set it to main (doesn't work without global config) + if ! "$tmpdir" config --global --get init.defaultBranch >/dev/null 2>&1; then + "$tmpdir" config --global init.defaultBranch main + fi + + # commit-tree stage doesn't use this script for init + # So we need to run this setup again + if [ "$1" = "commit-tree" ]; then + "$tmpdir" config --local user.email "hello@codecrafters.io" + "$tmpdir" config --local user.name "CodeCrafters-Bot" + fi + + if [ "$1" = "write-tree" ]; then + "$tmpdir" add . + fi + + exec "$tmpdir" "$@" + fi +done + +echo "git binary not found in PATH or /tmp directories" +exit 1 diff --git a/internal/utils.go b/internal/utils.go new file mode 100644 index 0000000..ff8d1cd --- /dev/null +++ b/internal/utils.go @@ -0,0 +1,55 @@ +package internal + +import ( + "fmt" + "io" + "os" + "os/exec" + "path" + + "github.com/codecrafters-io/tester-utils/logger" + "github.com/codecrafters-io/tester-utils/test_case_harness" +) + +// RelocateSystemGit moves the system git binary to a temporary directory +func RelocateSystemGit(harness *test_case_harness.TestCaseHarness, logger *logger.Logger) { + oldGitPath, err := exec.LookPath("git") + if err != nil { + panic(fmt.Sprintf("CodeCrafters Internal Error: git executable not found: %v", err)) + } + + tmpGitDir, err := os.MkdirTemp("/tmp", "git-*") + if err != nil { + panic(fmt.Sprintf("CodeCrafters Internal Error: create tmp git directory failed: %v", err)) + } + tmpGitPath := path.Join(tmpGitDir, "git") + + command := fmt.Sprintf("mv %s %s", oldGitPath, tmpGitPath) + moveCmd := exec.Command("sh", "-c", command) + moveCmd.Stdout = io.Discard + moveCmd.Stderr = io.Discard + if err := moveCmd.Run(); err != nil { + os.RemoveAll(tmpGitDir) + panic(fmt.Sprintf("CodeCrafters Internal Error: mv git to tmp directory failed: %v", err)) + } + + // Register teardown function to automatically restore git + harness.RegisterTeardownFunc(func() { restoreSystemGit(tmpGitPath, oldGitPath) }) +} + +// RestoreSystemGit moves the git binary back to its original location and cleans up +func restoreSystemGit(newPath string, originalPath string) error { + command := fmt.Sprintf("mv %s %s", newPath, originalPath) + moveCmd := exec.Command("sh", "-c", command) + moveCmd.Stdout = io.Discard + moveCmd.Stderr = io.Discard + if err := moveCmd.Run(); err != nil { + panic(fmt.Sprintf("CodeCrafters Internal Error: mv restore for git failed: %v", err)) + } + + if err := os.RemoveAll(path.Dir(newPath)); err != nil { + panic(fmt.Sprintf("CodeCrafters Internal Error: delete tmp git directory failed: %s", path.Dir(newPath))) + } + + return nil +} diff --git a/local_testing/Dockerfile b/local_testing/Dockerfile new file mode 100644 index 0000000..cd4200c --- /dev/null +++ b/local_testing/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1.24 + +# Install required packages +RUN apt-get update && apt-get install -y \ + # Required for make test + make \ + # Required to run bash tests + bash \ + # Required for fixtures + python3 \ + # Required for testing + git \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Starting from Go 1.20, the go standard library is no loger compiled. +# Setting GODEBUG to "installgoroot=all" restores the old behavior +RUN GODEBUG="installgoroot=all" go install std + +# Copy go.mod and go.sum first to cache dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Default command +CMD ["/bin/bash"] diff --git a/local_testing/test.sh b/local_testing/test.sh new file mode 100755 index 0000000..1ba7785 --- /dev/null +++ b/local_testing/test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +# Ensure we're in the correct directory +cd "$(dirname "$0")/.." + +# Build and run +docker build -t local-git-tester -f local_testing/Dockerfile . +# Run make test +# docker run --rm -it -v $(pwd):/app local-git-tester make test +# Generate fixtures +# docker run --rm -it -e CODECRAFTERS_RECORD_FIXTURES=true -v $(pwd):/app local-git-tester make test +# Run test_with_git +docker run --rm -it -v $(pwd):/app local-git-tester make test_with_git