From 231882be1771f1e5f854cad4b87cb34cd6d72999 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 17 Dec 2025 19:57:17 +0530 Subject: [PATCH 1/4] Add tests for handling missing and non-executable test.sh in build process. --- builder/tests/build_image_test.rb | 65 +++++++++++++++++-- .../tests/lib/build_image_command_runner.rb | 4 +- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/builder/tests/build_image_test.rb b/builder/tests/build_image_test.rb index 109dcb5..975ea88 100644 --- a/builder/tests/build_image_test.rb +++ b/builder/tests/build_image_test.rb @@ -138,15 +138,72 @@ def test_removes_dockerignore_file create_build! run_script! stream_output = Timeout.timeout(5) { `logstream -url #{@build.local_logstream_url} follow` } - + assert_match(/Build successful/, stream_output) assert_equal "success", @build.reload.status - + ls_output = `docker run --rm registry.fly.io/test-depot-push:test-runner-tests ls -la /app` refute_match(/.dockerignore/, ls_output, ".dockerignore should not exist in the built image") assert_match(/control.txt/, ls_output, "control.txt should still exist in the built image") end + def test_handles_missing_test_sh + create_and_push_git_repository!("redis-ruby") + + # Create a tester directory without test.sh + tester_dir = Dir.mktmpdir + # Create a dummy tester binary (validation only checks for test.sh) + File.write(File.join(tester_dir, "tester"), "#!/bin/sh\necho 'dummy tester'\n") + File.chmod(0o755, File.join(tester_dir, "tester")) + + create_build! + + begin + run_script!(tester_dir: tester_dir) + flunk "Expected build to fail with missing test.sh" + rescue + # Expected to fail + end + + assert_equal "error", @build.reload.status + + output = Timeout.timeout(5) { `logstream -url #{@build.local_logstream_url} follow` } + assert_match(/CodeCrafters internal error/i, output) + assert_match(/test\.sh does not exist in tester dir/, output) + assert_match(/CodeCrafters internal error/i, @build.reload.parsed_logs) + assert_match(/test\.sh does not exist in tester dir/, @build.reload.parsed_logs) + end + + def test_handles_non_executable_test_sh + create_and_push_git_repository!("redis-ruby") + + # Create a tester directory with non-executable test.sh + tester_dir = Dir.mktmpdir + # Create a dummy tester binary (validation only checks for test.sh) + File.write(File.join(tester_dir, "tester"), "#!/bin/sh\necho 'dummy tester'\n") + File.chmod(0o755, File.join(tester_dir, "tester")) + test_sh_path = File.join(tester_dir, "test.sh") + File.write(test_sh_path, "#!/bin/sh\nexec \"${TESTER_DIR}/tester\"\n") + File.chmod(0o644, test_sh_path) # Not executable + + create_build! + + begin + run_script!(tester_dir: tester_dir) + flunk "Expected build to fail with non-executable test.sh" + rescue + # Expected to fail + end + + assert_equal "error", @build.reload.status + + output = Timeout.timeout(5) { `logstream -url #{@build.local_logstream_url} follow` } + assert_match(/CodeCrafters internal error/i, output) + assert_match(/test\.sh is not executable in tester dir/, output) + assert_match(/CodeCrafters internal error/i, @build.reload.parsed_logs) + assert_match(/test\.sh is not executable in tester dir/, @build.reload.parsed_logs) + end + def create_build!(commit_sha: nil) commit_sha ||= @git_repository.head_commit_sha @build = TestRunnerBuild.create!(test_runner: @repository.test_runners.first, id: SecureRandom.uuid, commit_sha: commit_sha) @@ -160,9 +217,9 @@ def create_and_push_git_repository!(code_fixture_key) @git_repository.push_to_git_daemon! end - def run_script!(test_run: nil) + def run_script!(test_run: nil, tester_dir: nil) BuildImageCommandRunner .new(git_repository: @git_repository, repository: @repository) - .run(build: @build, test_run: test_run) + .run(build: @build, test_run: test_run, tester_dir: tester_dir) end end diff --git a/builder/tests/lib/build_image_command_runner.rb b/builder/tests/lib/build_image_command_runner.rb index 1e3e35a..2effb5c 100644 --- a/builder/tests/lib/build_image_command_runner.rb +++ b/builder/tests/lib/build_image_command_runner.rb @@ -11,9 +11,9 @@ def initialize(git_repository:, repository:) self.repository = repository end - def run(build:, test_run: nil) + def run(build:, test_run: nil, tester_dir: nil) tmp_dir = Dir.mktmpdir - tester_dir = TesterDownloader.new(course: OpenStruct.new(slug: repository.course_slug), testers_root_dir: TESTERS_DIR).download_if_needed + tester_dir ||= TesterDownloader.new(course: OpenStruct.new(slug: repository.course_slug), testers_root_dir: TESTERS_DIR).download_if_needed FileUtils.rm_rf(tmp_dir) FileUtils.cp_r(git_repository.tmp_dir, tmp_dir) From 7151610b059f26157cd2ee9be76450f026c498e7 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 17 Dec 2025 20:06:24 +0530 Subject: [PATCH 2/4] Update code fixtures --- builder/Makefile | 8 ++++---- builder/tests/lib/code_fixtures.rb | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/builder/Makefile b/builder/Makefile index 7b26481..09abb04 100644 --- a/builder/Makefile +++ b/builder/Makefile @@ -29,7 +29,7 @@ refresh_test_fixtures: rm -rf /tmp/byo-redis git clone https://github.com/codecrafters-io/build-your-own-redis /tmp/byo-redis - cd /tmp/byo-redis && git checkout 5ce64229f59108851de28c4f083f3459d58eff34 + cd /tmp/byo-redis && git checkout 5fccb61555a659223eff1d3b96c437ce82704d48 rm -rf /tmp/byo-sqlite git clone https://github.com/codecrafters-io/build-your-own-sqlite /tmp/byo-sqlite @@ -37,15 +37,15 @@ refresh_test_fixtures: rm -rf ./tests/fixtures/redis-ruby-pass-stage-1 cp -R /tmp/byo-redis/solutions/ruby/01-jm1/code ./tests/fixtures/redis-ruby-pass-stage-1 - cp /tmp/byo-redis/dockerfiles/ruby-3.3.Dockerfile ./tests/fixtures/dockerfiles/redis-ruby-3.3.Dockerfile + cp /tmp/byo-redis/dockerfiles/ruby-3.4.Dockerfile ./tests/fixtures/dockerfiles/redis-ruby-3.4.Dockerfile rm -rf ./tests/fixtures/redis-rust-pass-stage-1 cp -R /tmp/byo-redis/solutions/rust/01-jm1/code ./tests/fixtures/redis-rust-pass-stage-1 - cp /tmp/byo-redis/dockerfiles/rust-1.88.Dockerfile ./tests/fixtures/dockerfiles/redis-rust-1.88.Dockerfile + cp /tmp/byo-redis/dockerfiles/rust-1.91.Dockerfile ./tests/fixtures/dockerfiles/redis-rust-1.91.Dockerfile rm -rf ./tests/fixtures/redis-go-pass-stage-1 cp -R /tmp/byo-redis/solutions/go/01-jm1/code ./tests/fixtures/redis-go-pass-stage-1 - cp /tmp/byo-redis/dockerfiles/go-1.24.Dockerfile ./tests/fixtures/dockerfiles/redis-go-1.24.Dockerfile + cp /tmp/byo-redis/dockerfiles/go-1.25.Dockerfile ./tests/fixtures/dockerfiles/redis-go-1.25.Dockerfile rm -rf ./tests/fixtures/sqlite-python-pass-stage-1 cp -R /tmp/byo-sqlite/solutions/python/01-dr6/code ./tests/fixtures/sqlite-python-pass-stage-1 diff --git a/builder/tests/lib/code_fixtures.rb b/builder/tests/lib/code_fixtures.rb index 4468b8f..790f5de 100644 --- a/builder/tests/lib/code_fixtures.rb +++ b/builder/tests/lib/code_fixtures.rb @@ -3,22 +3,22 @@ module CodeFixtures DB = { "redis-ruby" => { - "dockerfile_contents" => File.read(File.join(repository_root, "tests/fixtures/dockerfiles/redis-ruby-3.3.Dockerfile")), - "buildpack_slug" => "ruby-3.3", + "dockerfile_contents" => File.read(File.join(repository_root, "tests/fixtures/dockerfiles/redis-ruby-3.4.Dockerfile")), + "buildpack_slug" => "ruby-3.4", "code_dir" => File.join(repository_root, "tests/fixtures/redis-ruby-pass-stage-1"), "course_slug" => "redis", "language_slug" => "ruby" }, "redis-rust" => { - "dockerfile_contents" => File.read(File.join(repository_root, "tests/fixtures/dockerfiles/redis-rust-1.88.Dockerfile")), - "buildpack_slug" => "rust-1.88", + "dockerfile_contents" => File.read(File.join(repository_root, "tests/fixtures/dockerfiles/redis-rust-1.91.Dockerfile")), + "buildpack_slug" => "rust-1.91", "code_dir" => File.join(repository_root, "tests/fixtures/redis-rust-pass-stage-1"), "course_slug" => "redis", "language_slug" => "rust" }, "redis-go" => { - "dockerfile_contents" => File.read(File.join(repository_root, "tests/fixtures/dockerfiles/redis-go-1.24.Dockerfile")), - "buildpack_slug" => "go-1.24", + "dockerfile_contents" => File.read(File.join(repository_root, "tests/fixtures/dockerfiles/redis-go-1.25.Dockerfile")), + "buildpack_slug" => "go-1.25", "code_dir" => File.join(repository_root, "tests/fixtures/redis-go-pass-stage-1"), "course_slug" => "redis", "language_slug" => "go" From 4fb0323b4d8373d9af10d71fefc779ce2e42c950 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 17 Dec 2025 19:26:28 +0530 Subject: [PATCH 3/4] Add checks for test.sh existence and executable permissions in build command. --- builder/internal/commands/build_image_command.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/builder/internal/commands/build_image_command.go b/builder/internal/commands/build_image_command.go index 5a96276..fac8d4b 100644 --- a/builder/internal/commands/build_image_command.go +++ b/builder/internal/commands/build_image_command.go @@ -168,6 +168,18 @@ func BuildImageCommand() int { return handleErrorDuringBuild(build, "", fmt.Errorf("tester dir (%s) does not exist", testerDir)) } + testShPath := filepath.Join(testerDir, "test.sh") + fileInfo, err := os.Stat(testShPath) + if os.IsNotExist(err) { + return handleErrorDuringBuild(build, "", fmt.Errorf("test.sh does not exist in tester dir (%s)", testerDir)) + } + if err != nil { + return handleErrorDuringBuild(build, "", fmt.Errorf("failed to stat test.sh in tester dir (%s): %w", testerDir, err)) + } + if fileInfo.Mode()&0111 == 0 { + return handleErrorDuringBuild(build, "", fmt.Errorf("test.sh is not executable in tester dir (%s)", testerDir)) + } + if _, err := os.Stat(testRunnerDir); os.IsNotExist(err) { return handleErrorDuringBuild(build, "", fmt.Errorf("test runner dir (%s) does not exist", testRunnerDir)) } From 1f3adae88a9e6e63340d1e5a6894b328eff5be8a Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Wed, 17 Dec 2025 20:13:14 +0530 Subject: [PATCH 4/4] Executable --- builder/tests/lib/buildpack_dockerfile_processor.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builder/tests/lib/buildpack_dockerfile_processor.rb b/builder/tests/lib/buildpack_dockerfile_processor.rb index bebaf56..01c154e 100644 --- a/builder/tests/lib/buildpack_dockerfile_processor.rb +++ b/builder/tests/lib/buildpack_dockerfile_processor.rb @@ -41,12 +41,12 @@ def self.build_prelude_lines(from_line) case from_line when /alpine/ "RUN apk add --update-cache --upgrade git curl" - when /buster/, /slim/, /focal/, /bullseye/, /bookworm/ + when /debian/, /buster/, /slim/, /focal/, /noble/, /bullseye/, /bookworm/, /trixie/ "RUN apt-get update && apt-get install -y git curl" when /dart/ "RUN apt-get update && apt-get install -y git curl" # Dart doesn't have OS type in its FROM line else - raise "Unknown from_line: #{from_line}" + raise "Expected FROM line (#{from_line}) to contain a distribution identifier like alpine, debian, buster, etc." end, "CMD [\"/var/opt/test-runner\", \"run_tests\"]",