Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions script/ci_parts/run_specs_file_by_file
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,15 @@ source "script/ci_parts/setup_env" "test" $1 $2
# of entire gems when just one spec file is run.
unset COVERAGE

# Setup standalone binstubs for RSpec that we use below.
bundle install --standalone && bundle binstubs rspec-core --standalone

function run_gem_specs_file_by_file() {
gem=$1
pushd $gem
for file in `find spec -iname '*_spec.rb'`; do
echo "Running $file"

# Note: here we avoid using `bundle exec`, opting for a binstub instead.
# Our `bundle install` command (with `--standalone` and `binstubs` options) creates the
# rspec binstub in a way where it won't actually load bundler at runtime. This is
# *slightly* faster. Not enough to usually matter, but it adds up when we boot rspec
# once for each spec file as we do here!
../bin/rspec $file -b --format progress --no-profile
done
# Note: here we avoid booting `rspec` (and paying for `bundle exec`) once per spec
# file. The runner script boots bundler and rspec-core a single time and then runs
# each spec file in a `fork`, so every file still executes in its own isolated
# process. That saves 1-2 seconds per spec file, which adds up over the hundreds of
# spec files this build part covers.
../script/rspec_file_by_file `find spec -iname '*_spec.rb'`
popd
}

Expand Down
50 changes: 50 additions & 0 deletions script/rspec_file_by_file
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env ruby

# Copyright 2024 - 2026 Block, Inc.
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#
# frozen_string_literal: true

# Runs each given spec file in a separate, isolated process -- equivalent to booting
# `rspec <file>` once per file, but far cheaper: bundler and rspec-core are loaded only
# once, in this parent process, and each spec file then runs in a `fork` of it.
#
# The parent never loads any ElasticGraph code or spec code, so each child starts from
# the same clean slate as a freshly booted `rspec` process. That preserves what
# file-by-file runs are meant to verify: that every spec file requires everything it
# needs and can run in isolation.
#
# Unlike our previous approach (`bundle install --standalone` with binstubs, which skips
# RubyGems gem activation entirely), children inherit a normally-activated bundle, so
# gems that consult `Gem.loaded_specs` behave the same as they do in production. See
# https://github.com/elastic/elastic-transport-ruby/pull/128 for an example of a gem
# that breaks when loaded without activation.

invocation_env = ENV.to_h

ENV["BUNDLE_GEMFILE"] ||= ::File.expand_path("../Gemfile", __dir__)
require "bundler/setup"
require "rspec/core"

# Booting bundler above mutated ENV (BUNDLE_GEMFILE, RUBYOPT, etc.) and captured that
# mutated state in `Bundler::ORIGINAL_ENV`, which children inherit. A freshly booted
# `rspec` process wouldn't have any of that: it would load bundler lazily (if at all)
# with the unmodified environment, so e.g. `Bundler.with_original_env` in a spec would
# restore the clean invocation environment for shelled-out commands. Restore both here
# so forked children observe the same environment a fresh process would.
ENV.replace(invocation_env)
::Bundler::ORIGINAL_ENV.replace(invocation_env)

ARGV.each do |spec_file|
puts "Running #{spec_file}"

pid = fork do
exit ::RSpec::Core::Runner.run([spec_file, "-b", "--format", "progress", "--no-profile"])
end

_, status = ::Process.wait2(pid)
exit(status.exitstatus || 1) unless status.success?
end