Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 8ec2c99

Browse files
committed
Merge pull request #2029 from brigade/lock-example-status
Lock example_status_persistence_file to prevent race conditions
2 parents 929c200 + 7f8746f commit 8ec2c99

File tree

2 files changed

+43
-7
lines changed

2 files changed

+43
-7
lines changed

lib/rspec/core/example_status_persister.rb

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,22 @@ def initialize(examples, file_name)
2121
end
2222

2323
def persist
24-
write dumped_statuses
24+
RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(@file_name))
25+
File.open(@file_name, File::RDWR | File::CREAT) do |f|
26+
# lock the file while reading / persisting to avoid a race
27+
# condition where parallel or unrelated spec runs race to
28+
# update the same file
29+
f.flock(File::LOCK_EX)
30+
@previous_runs = f.read
31+
f.rewind
32+
f.write(dumped_statuses)
33+
f.flush
34+
f.truncate(f.pos)
35+
end
2536
end
2637

2738
private
2839

29-
def write(statuses)
30-
RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(@file_name))
31-
File.open(@file_name, "w") { |f| f.write(statuses) }
32-
end
33-
3440
def dumped_statuses
3541
ExampleStatusDumper.dump(merged_statuses)
3642
end
@@ -52,7 +58,7 @@ def statuses_from_this_run
5258
end
5359

5460
def statuses_from_previous_runs
55-
self.class.load_from(@file_name)
61+
ExampleStatusParser.parse(@previous_runs)
5662
end
5763
end
5864

spec/rspec/core/example_status_persister_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,36 @@ def new_example(id, metadata = {})
5858
)
5959
end
6060

61+
it 'prevents simultaneous access to the file' do
62+
# This tests whether a certain race condition is prevented:
63+
# - read 1
64+
# - read 2
65+
# - write 1
66+
# - write 2 - write 1 is lost
67+
ex_1 = new_example("#{existing_spec_file}[1:1]", :status => :passed)
68+
ex_2 = new_example("spec_1.rb[1:1]", :status => :failed)
69+
70+
persister_1 = ExampleStatusPersister.new([ex_1], file.path)
71+
persister_2 = ExampleStatusPersister.new([ex_2], file.path)
72+
persister_2_thread = nil
73+
74+
# dumped_statuses is called after the file is locked but
75+
# before the output is written
76+
allow(persister_1).to receive(:dumped_statuses).and_wrap_original do |m, *args|
77+
persister_2_thread = Thread.new { persister_2.persist }
78+
m.call(*args)
79+
end
80+
persister_1.persist
81+
persister_2_thread.join
82+
83+
loaded = ExampleStatusPersister.load_from(file.path)
84+
85+
expect(loaded).to contain_exactly(
86+
a_hash_including(:example_id => ex_1.id, :status => "passed"),
87+
a_hash_including(:example_id => ex_2.id, :status => "failed")
88+
)
89+
end
90+
6191
it 'merges the example statuses with the existing records in the named file' do
6292
ex_1 = new_example("#{existing_spec_file}[1:1]", :status => :passed)
6393
ex_2 = new_example("spec_1.rb[1:1]", :status => :failed)

0 commit comments

Comments
 (0)