Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split spec files to examples programmatically #36

Closed
wants to merge 5 commits into from
Closed
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
62 changes: 30 additions & 32 deletions lib/rspecq/worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,39 @@ def try_publish_queue!(queue)
puts "Published queue (size=#{queue.publish(jobs, fail_fast)})"
end

# Split a list of spec files into their individual examples.
#
# NOTE: RSpec has to load the files before we can split them as individual
# examples. In case a file to be splitted fails to be loaded
# (e.g. contains a syntax error), we return the files unchanged, thereby
# falling back to scheduling them as whole files. Their errors will be
# reported in the normal flow when they're eventually picked up by a worker.
def files_to_example_ids(files)
out = StringIO.new

reset_rspec_state!

dry_run_before = RSpec.configuration.dry_run

RSpec.configuration.add_formatter(RSpec::Core::Formatters::JsonFormatter.new(out))
RSpec.configuration.files_or_directories_to_run = files_or_dirs_to_run
RSpec.configuration.dry_run = true

opts = RSpec::Core::ConfigurationOptions.new(files)
result = RSpec::Core::Runner.new(opts).run($stderr, $stdout)

return files if result != 0

JSON.parse(out.string)["examples"].map { |e| e["id"] }
ensure
RSpec.configuration.dry_run = dry_run_before
reset_rspec_state!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain, why do you need to set the original value for the dry_run (only)?

Copy link
Collaborator Author

@agis agis Oct 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that's the one we're messing with (we set it to true so that we can fetch the example ids without executing any tests). On the contrary, configuration.files_or_directories_to_run and the formatter we add, are things that will get taken care of by reset_rspec_state!.

end

private

def reset_rspec_state!
RSpec.reset
RSpec.clear_examples

# see https://github.com/rspec/rspec-core/pull/2723
Expand All @@ -194,38 +224,6 @@ def reset_rspec_state!
RSpec.world.wants_to_quit = false
end

# NOTE: RSpec has to load the files before we can split them as individual
# examples. In case a file to be splitted fails to be loaded
# (e.g. contains a syntax error), we return the files unchanged, thereby
# falling back to scheduling them as whole files. Their errors will be
# reported in the normal flow when they're eventually picked up by a worker.
def files_to_example_ids(files)
cmd = "DISABLE_SPRING=1 bundle exec rspec --dry-run --format json #{files.join(' ')}"
out, err, cmd_result = Open3.capture3(cmd)

if !cmd_result.success?
rspec_output = begin
JSON.parse(out)
rescue JSON::ParserError
out
end

log_event(
"Failed to split slow files, falling back to regular scheduling.\n #{err}",
"error",
rspec_stdout: rspec_output,
rspec_stderr: err,
cmd_result: cmd_result.inspect
)

pp rspec_output

return files
end

JSON.parse(out)["examples"].map { |e| e["id"] }
end

def relative_path(job)
@cwd ||= Pathname.new(Dir.pwd)
"./#{Pathname.new(job).relative_path_from(@cwd)}"
Expand Down
3 changes: 3 additions & 0 deletions test/sample_suites/files_to_examples/spec/bar_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RSpec.describe "baz" do
it { expect(1).to eq 2 }
end
15 changes: 15 additions & 0 deletions test/sample_suites/files_to_examples/spec/foo_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
RSpec.describe "foo" do
it "works" do
expect(true).to be true
end

context "hmm" do
it "or not" do
expect(true).to be true
end

it "well, maybe" do
expect(true).to be true
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RSpec.describe "baz" do
it { expect(1).to eq 2 }
end
15 changes: 15 additions & 0 deletions test/sample_suites/files_to_examples_fallback/spec/foo_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
IdontExistz1337.describe "foo" do
it "works" do
expect(true).to be true
end

context "hmm" do
it "or not" do
expect(true).to be true
end

it "well, maybe" do
expect(true).to be true
end
end
end
7 changes: 5 additions & 2 deletions test/test_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,14 @@ def silent
end

begin
orig = $stdout.clone
orig_out = $stdout.clone
orig_err = $stderr.clone
$stdout.reopen(File::NULL, "w")
$stderr.reopen(File::NULL, "w")
yield
ensure
$stdout.reopen(orig)
$stdout.reopen(orig_out)
$stderr.reopen(orig_err)
end
end
end
Expand Down
41 changes: 41 additions & 0 deletions test/test_worker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require "test_helpers"

class TestWorker < RSpecQTest
def test_files_to_example_ids
worker = new_worker("files_to_examples")

expected = [
"./test/sample_suites/files_to_examples/spec/foo_spec.rb[1:1]",
"./test/sample_suites/files_to_examples/spec/foo_spec.rb[1:2:1]",
"./test/sample_suites/files_to_examples/spec/foo_spec.rb[1:2:2]",
"./test/sample_suites/files_to_examples/spec/bar_spec.rb[1:1]",
].sort

actual = worker.files_to_example_ids(
[
"./test/sample_suites/files_to_examples/spec/foo_spec.rb",
"./test/sample_suites/files_to_examples/spec/bar_spec.rb",
]
).sort

assert_equal expected, actual
end

def test_files_to_example_ids_failure_fallback
worker = new_worker("files_to_examples_fallback")

expected = [
"./test/sample_suites/files_to_examples_fallback/spec/foo_spec.rb",
"./test/sample_suites/files_to_examples_fallback/spec/bar_spec.rb"
].sort

actual = worker.files_to_example_ids(
[
"./test/sample_suites/files_to_examples_fallback/spec/foo_spec.rb",
"./test/sample_suites/files_to_examples_fallback/spec/bar_spec.rb",
]
).sort

assert_equal expected, actual
end
end