parallel_tests 1.3.7 → 3.7.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,49 +1,48 @@
1
- require 'gherkin/tag_expression'
2
-
1
+ # frozen_string_literal: true
3
2
  module ParallelTests
4
3
  module Cucumber
5
4
  module Formatters
6
5
  class ScenarioLineLogger
7
6
  attr_reader :scenarios
8
7
 
9
- def initialize(tag_expression = ::Gherkin::TagExpression.new([]))
8
+ def initialize(tag_expression = nil)
10
9
  @scenarios = []
11
10
  @tag_expression = tag_expression
12
11
  end
13
12
 
14
- def visit_feature_element(feature_element)
15
- return unless @tag_expression.evaluate(feature_element.source_tags)
13
+ def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
14
+ scenario_tags = feature_element.tags.map(&:name)
15
+ scenario_tags = feature_tags + scenario_tags
16
+ if feature_element.is_a?(CukeModeler::Scenario) # :Scenario
17
+ test_line = feature_element.source_line
16
18
 
17
- case feature_element
18
- when ::Cucumber::Ast::Scenario
19
- line = if feature_element.respond_to?(:line)
20
- feature_element.line
21
- else
22
- feature_element.instance_variable_get(:@line)
23
- end
24
- @scenarios << [feature_element.feature.file, line].join(":")
25
- when ::Cucumber::Ast::ScenarioOutline
26
- sections = feature_element.instance_variable_get(:@example_sections)
27
- sections.each { |section|
28
- rows = if section[1].respond_to?(:rows)
29
- section[1].rows
30
- else
31
- section[1].instance_variable_get(:@rows)
19
+ # We don't accept the feature_element if the current tags are not valid
20
+ return unless matches_tags?(scenario_tags)
21
+ # or if it is not at the correct location
22
+ return if line_numbers.any? && !line_numbers.include?(test_line)
23
+
24
+ @scenarios << [uri, feature_element.source_line].join(":")
25
+ else # :ScenarioOutline
26
+ feature_element.examples.each do |example|
27
+ example_tags = example.tags.map(&:name)
28
+ example_tags = scenario_tags + example_tags
29
+ next unless matches_tags?(example_tags)
30
+ example.rows[1..-1].each do |row|
31
+ test_line = row.source_line
32
+ next if line_numbers.any? && !line_numbers.include?(test_line)
33
+
34
+ @scenarios << [uri, test_line].join(':')
32
35
  end
33
- rows.each_with_index { |row, index|
34
- next if index == 0 # slices didn't work with jruby data structure
35
- line = if row.respond_to?(:line)
36
- row.line
37
- else
38
- row.instance_variable_get(:@line)
39
- end
40
- @scenarios << [feature_element.feature.file, line].join(":")
41
- }
42
- }
36
+ end
43
37
  end
44
38
  end
45
39
 
46
- def method_missing(*args)
40
+ def method_missing(*); end # # rubocop:disable Style/MissingRespondToMissing
41
+
42
+ private
43
+
44
+ def matches_tags?(tags)
45
+ @tag_expression.nil? || @tag_expression.evaluate(tags)
47
46
  end
48
47
  end
49
48
  end
@@ -1,29 +1,62 @@
1
- require 'gherkin/tag_expression'
1
+ # frozen_string_literal: true
2
+ require 'cucumber/tag_expressions/parser'
2
3
  require 'cucumber/runtime'
3
4
  require 'cucumber'
4
5
  require 'parallel_tests/cucumber/scenario_line_logger'
5
6
  require 'parallel_tests/gherkin/listener'
7
+ require 'shellwords'
8
+
9
+ begin
10
+ gem "cuke_modeler", "~> 3.0"
11
+ require 'cuke_modeler'
12
+ rescue LoadError
13
+ raise 'Grouping by individual cucumber scenarios requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.'
14
+ end
6
15
 
7
16
  module ParallelTests
8
17
  module Cucumber
9
18
  class Scenarios
10
19
  class << self
11
- def all(files, options={})
20
+ def all(files, options = {})
21
+ # Parse tag expression from given test options and ignore tag pattern. Refer here to understand how new tag expression syntax works - https://github.com/cucumber/cucumber/tree/master/tag-expressions
12
22
  tags = []
13
- tags.concat options[:ignore_tag_pattern].to_s.split(/\s*,\s*/).map {|tag| "~#{tag}" }
14
- tags.concat options[:test_options].to_s.scan(/(?:-t|--tags) (~?@[\w,~@]+)/).flatten
15
- split_into_scenarios files, tags.uniq
23
+ words = options[:test_options].to_s.shellsplit
24
+ words.each_with_index { |w, i| tags << words[i + 1] if ["-t", "--tags"].include?(w) }
25
+ if ignore = options[:ignore_tag_pattern]
26
+ tags << "not (#{ignore})"
27
+ end
28
+ tags_exp = tags.compact.join(" and ")
29
+
30
+ split_into_scenarios files, tags_exp
16
31
  end
17
32
 
18
33
  private
19
34
 
20
- def split_into_scenarios(files, tags=[])
21
- tag_expression = ::Gherkin::TagExpression.new(tags)
22
- scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(tag_expression)
23
- loader = ::Cucumber::Runtime::FeaturesLoader.new(files, [], tag_expression)
35
+ def split_into_scenarios(files, tags = '')
36
+ # Create the tag expression instance from cucumber tag expressions parser, this is needed to know if the scenario matches with the tags invoked by the request
37
+ # Create the ScenarioLineLogger which will filter the scenario we want
38
+ args = []
39
+ args << ::Cucumber::TagExpressions::Parser.new.parse(tags) unless tags.empty?
40
+ scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(*args)
41
+
42
+ # here we loop on the files map, each file will contain one or more scenario
43
+ files.each do |path|
44
+ # Gather up any line numbers attached to the file path
45
+ path, *test_lines = path.split(/:(?=\d+)/)
46
+ test_lines.map!(&:to_i)
47
+
48
+ # We create a Gherkin document, this will be used to decode the details of each scenario
49
+ document = ::CukeModeler::FeatureFile.new(path)
50
+ feature = document.feature
51
+
52
+ # We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
53
+ feature_tags = feature.tags.map(&:name)
24
54
 
25
- loader.features.each do |feature|
26
- feature.accept(scenario_line_logger)
55
+ # We loop on each children of the feature
56
+ feature.tests.each do |test|
57
+ # It's a scenario, we add it to the scenario_line_logger
58
+ scenario_line_logger.visit_feature_element(document.path, test, feature_tags, line_numbers: test_lines)
59
+ end
27
60
  end
28
61
 
29
62
  scenario_line_logger.scenarios
@@ -1,9 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  require 'parallel_tests'
2
3
 
3
4
  module ParallelTests
4
5
  module Gherkin
5
6
  module Io
6
-
7
7
  def prepare_io(path_or_io)
8
8
  if path_or_io.respond_to?(:write)
9
9
  path_or_io
@@ -24,7 +24,7 @@ module ParallelTests
24
24
 
25
25
  # do not let multiple processes get in each others way
26
26
  def lock_output
27
- if File === @io
27
+ if @io.is_a?(File)
28
28
  begin
29
29
  @io.flock File::LOCK_EX
30
30
  yield
@@ -35,7 +35,6 @@ module ParallelTests
35
35
  yield
36
36
  end
37
37
  end
38
-
39
38
  end
40
39
  end
41
40
  end
@@ -1,5 +1,4 @@
1
- require 'gherkin'
2
-
1
+ # frozen_string_literal: true
3
2
  module ParallelTests
4
3
  module Gherkin
5
4
  class Listener
@@ -8,8 +7,10 @@ module ParallelTests
8
7
  attr_writer :ignore_tag_pattern
9
8
 
10
9
  def initialize
11
- @steps, @uris = [], []
10
+ @steps = []
11
+ @uris = []
12
12
  @collect = {}
13
+ @feature, @ignore_tag_pattern = nil
13
14
  reset_counters!
14
15
  end
15
16
 
@@ -17,7 +18,7 @@ module ParallelTests
17
18
  @feature = feature
18
19
  end
19
20
 
20
- def background(*args)
21
+ def background(*)
21
22
  @background = 1
22
23
  end
23
24
 
@@ -32,7 +33,7 @@ module ParallelTests
32
33
  @outline = 1
33
34
  end
34
35
 
35
- def step(*args)
36
+ def step(*)
36
37
  return if @ignoring
37
38
  if @background == 1
38
39
  @background_steps += 1
@@ -52,12 +53,10 @@ module ParallelTests
52
53
  # @param [Gherkin::Formatter::Model::Examples] examples
53
54
  #
54
55
  def examples(examples)
55
- if examples.rows.size > 0
56
- @collect[@uri] += (@outline_steps * examples.rows.size)
57
- end
56
+ @collect[@uri] += (@outline_steps * examples.rows.size) unless examples.rows.empty?
58
57
  end
59
58
 
60
- def eof(*args)
59
+ def eof(*)
61
60
  @collect[@uri] += (@background_steps * @scenarios)
62
61
  reset_counters!
63
62
  end
@@ -68,8 +67,7 @@ module ParallelTests
68
67
  end
69
68
 
70
69
  # ignore lots of other possible callbacks ...
71
- def method_missing(*args)
72
- end
70
+ def method_missing(*); end # rubocop:disable Style/MissingRespondToMissing
73
71
 
74
72
  private
75
73
 
@@ -80,7 +78,7 @@ module ParallelTests
80
78
 
81
79
  # Set @ignoring if we should ignore this scenario/outline based on its tags
82
80
  def should_ignore(scenario)
83
- @ignoring = @ignore_tag_pattern && all_tags(scenario).find{ |tag| @ignore_tag_pattern === tag.name }
81
+ @ignoring = @ignore_tag_pattern && all_tags(scenario).find { |tag| @ignore_tag_pattern === tag.name }
84
82
  end
85
83
  end
86
84
  end
@@ -1,29 +1,31 @@
1
+ # frozen_string_literal: true
1
2
  require "parallel_tests/test/runner"
2
3
  require 'shellwords'
3
4
 
4
5
  module ParallelTests
5
6
  module Gherkin
6
7
  class Runner < ParallelTests::Test::Runner
7
-
8
8
  class << self
9
9
  def run_tests(test_files, process_number, num_processes, options)
10
10
  combined_scenarios = test_files
11
11
 
12
12
  if options[:group_by] == :scenarios
13
13
  grouped = test_files.map { |t| t.split(':') }.group_by(&:first)
14
- combined_scenarios = grouped.map {|file,files_and_lines| "#{file}:#{files_and_lines.map(&:last).join(':')}" }
14
+ combined_scenarios = grouped.map do |file, files_and_lines|
15
+ "#{file}:#{files_and_lines.map(&:last).join(':')}"
16
+ end
15
17
  end
16
18
 
17
19
  sanitized_test_files = combined_scenarios.map { |val| WINDOWS ? "\"#{val}\"" : Shellwords.escape(val) }
18
20
 
19
21
  options[:env] ||= {}
20
- options[:env] = options[:env].merge({'AUTOTEST' => '1'}) if $stdout.tty? # display color when we are in a terminal
22
+ options[:env] = options[:env].merge({ 'AUTOTEST' => '1' }) if $stdout.tty?
21
23
 
22
24
  cmd = [
23
25
  executable,
24
26
  (runtime_logging if File.directory?(File.dirname(runtime_log))),
25
- cucumber_opts(options[:test_options]),
26
- *sanitized_test_files
27
+ *sanitized_test_files,
28
+ cucumber_opts(options[:test_options])
27
29
  ].compact.reject(&:empty?).join(' ')
28
30
  execute_command(cmd, process_number, num_processes, options)
29
31
  end
@@ -32,6 +34,10 @@ module ParallelTests
32
34
  @test_file_name || 'feature'
33
35
  end
34
36
 
37
+ def default_test_folder
38
+ 'features'
39
+ end
40
+
35
41
  def test_suffix
36
42
  /\.feature$/
37
43
  end
@@ -44,16 +50,16 @@ module ParallelTests
44
50
  # 1 scenario (1 failed)
45
51
  # 1 step (1 failed)
46
52
  def summarize_results(results)
47
- sort_order = %w[scenario step failed undefined skipped pending passed]
53
+ sort_order = ['scenario', 'step', 'failed', 'flaky', 'undefined', 'skipped', 'pending', 'passed']
48
54
 
49
- %w[scenario step].map do |group|
55
+ ['scenario', 'step'].map do |group|
50
56
  group_results = results.grep(/^\d+ #{group}/)
51
57
  next if group_results.empty?
52
58
 
53
59
  sums = sum_up_results(group_results)
54
60
  sums = sums.sort_by { |word, _| sort_order.index(word) || 999 }
55
61
  sums.map! do |word, number|
56
- plural = "s" if word == group and number != 1
62
+ plural = "s" if (word == group) && (number != 1)
57
63
  "#{number} #{word}#{plural}"
58
64
  end
59
65
  "#{sums[0]} (#{sums[1..-1].join(", ")})"
@@ -61,7 +67,7 @@ module ParallelTests
61
67
  end
62
68
 
63
69
  def cucumber_opts(given)
64
- if given =~ /--profile/ or given =~ /(^|\s)-p /
70
+ if given =~ (/--profile/) || given =~ (/(^|\s)-p /)
65
71
  given
66
72
  else
67
73
  [given, profile_from_config].compact.join(" ")
@@ -71,15 +77,11 @@ module ParallelTests
71
77
  def profile_from_config
72
78
  # copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85
73
79
  config = Dir.glob("{,.config/,config/}#{name}{.yml,.yaml}").first
74
- if config && File.read(config) =~ /^parallel:/
75
- "--profile parallel"
76
- end
80
+ "--profile parallel" if config && File.read(config) =~ /^parallel:/
77
81
  end
78
82
 
79
- def tests_in_groups(tests, num_groups, options={})
80
- if options[:group_by] == :scenarios
81
- @test_file_name = "scenario"
82
- end
83
+ def tests_in_groups(tests, num_groups, options = {})
84
+ @test_file_name = "scenario" if options[:group_by] == :scenarios
83
85
  method = "by_#{options[:group_by]}"
84
86
  if Grouper.respond_to?(method)
85
87
  Grouper.send(method, find_tests(tests, options), num_groups, options)
@@ -88,7 +90,6 @@ module ParallelTests
88
90
  end
89
91
  end
90
92
 
91
-
92
93
  def runtime_logging
93
94
  "--format ParallelTests::Gherkin::RuntimeLogger --out #{runtime_log}"
94
95
  end
@@ -98,18 +99,16 @@ module ParallelTests
98
99
  end
99
100
 
100
101
  def determine_executable
101
- case
102
- when File.exist?("bin/#{name}")
103
- "bin/#{name}"
104
- when ParallelTests.bundler_enabled?
102
+ if File.exist?("bin/#{name}")
103
+ ParallelTests.with_ruby_binary("bin/#{name}")
104
+ elsif ParallelTests.bundler_enabled?
105
105
  "bundle exec #{name}"
106
- when File.file?("script/#{name}")
107
- "script/#{name}"
106
+ elsif File.file?("script/#{name}")
107
+ ParallelTests.with_ruby_binary("script/#{name}")
108
108
  else
109
- "#{name}"
109
+ name.to_s
110
110
  end
111
111
  end
112
-
113
112
  end
114
113
  end
115
114
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'parallel_tests/gherkin/io'
2
3
 
3
4
  module ParallelTests
@@ -5,22 +6,22 @@ module ParallelTests
5
6
  class RuntimeLogger
6
7
  include Io
7
8
 
8
- def initialize(step_mother, path_or_io, options=nil)
9
- @io = prepare_io(path_or_io)
9
+ def initialize(config)
10
+ @io = prepare_io(config.out_stream)
10
11
  @example_times = Hash.new(0)
11
- end
12
12
 
13
- def before_feature(_)
14
- @start_at = ParallelTests.now.to_f
15
- end
13
+ config.on_event :test_case_started do |_|
14
+ @start_at = ParallelTests.now.to_f
15
+ end
16
16
 
17
- def after_feature(feature)
18
- @example_times[feature.file] += ParallelTests.now.to_f - @start_at
19
- end
17
+ config.on_event :test_case_finished do |event|
18
+ @example_times[event.test_case.location.file] += ParallelTests.now.to_f - @start_at
19
+ end
20
20
 
21
- def after_features(*args)
22
- lock_output do
23
- @io.puts @example_times.map { |file, time| "#{file}:#{time}" }
21
+ config.on_event :test_run_finished do |_|
22
+ lock_output do
23
+ @io.puts(@example_times.map { |file, time| "#{file}:#{time}" })
24
+ end
24
25
  end
25
26
  end
26
27
  end
@@ -1,39 +1,117 @@
1
+ # frozen_string_literal: true
1
2
  module ParallelTests
2
3
  class Grouper
3
4
  class << self
4
5
  def by_steps(tests, num_groups, options)
5
- features_with_steps = build_features_with_steps(tests, options)
6
+ features_with_steps = group_by_features_with_steps(tests, options)
6
7
  in_even_groups_by_size(features_with_steps, num_groups)
7
8
  end
8
9
 
9
- def by_scenarios(tests, num_groups, options={})
10
+ def by_scenarios(tests, num_groups, options = {})
10
11
  scenarios = group_by_scenarios(tests, options)
11
12
  in_even_groups_by_size(scenarios, num_groups)
12
13
  end
13
14
 
14
- def in_even_groups_by_size(items, num_groups, options= {})
15
- groups = Array.new(num_groups) { {:items => [], :size => 0} }
15
+ def in_even_groups_by_size(items, num_groups, options = {})
16
+ groups = Array.new(num_groups) { { items: [], size: 0 } }
17
+
18
+ return specify_groups(items, num_groups, options, groups) if options[:specify_groups]
16
19
 
17
20
  # add all files that should run in a single process to one group
18
- (options[:single_process] || []).each do |pattern|
19
- matched, items = items.partition { |item, size| item =~ pattern }
20
- matched.each { |item, size| add_to_group(groups.first, item, size) }
21
+ single_process_patterns = options[:single_process] || []
22
+
23
+ single_items, items = items.partition do |item, _size|
24
+ single_process_patterns.any? { |pattern| item =~ pattern }
25
+ end
26
+
27
+ isolate_count = isolate_count(options)
28
+
29
+ if isolate_count >= num_groups
30
+ raise 'Number of isolated processes must be less than total the number of processes'
31
+ end
32
+
33
+ if isolate_count >= num_groups
34
+ raise 'Number of isolated processes must be >= total number of processes'
21
35
  end
22
36
 
23
- groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
24
- group_features_by_size(items_to_group(items), groups_to_fill)
37
+ if isolate_count >= 1
38
+ # add all files that should run in a multiple isolated processes to their own groups
39
+ group_features_by_size(items_to_group(single_items), groups[0..(isolate_count - 1)])
40
+ # group the non-isolated by size
41
+ group_features_by_size(items_to_group(items), groups[isolate_count..-1])
42
+ else
43
+ # add all files that should run in a single non-isolated process to first group
44
+ single_items.each { |item, size| add_to_group(groups.first, item, size) }
25
45
 
26
- groups.map!{|g| g[:items].sort }
46
+ # group all by size
47
+ group_features_by_size(items_to_group(items), groups)
48
+ end
49
+
50
+ groups.map! { |g| g[:items].sort }
27
51
  end
28
52
 
29
53
  private
30
54
 
55
+ def specify_groups(items, num_groups, options, groups)
56
+ specify_test_process_groups = options[:specify_groups].split('|')
57
+ if specify_test_process_groups.count > num_groups
58
+ raise 'Number of processes separated by pipe must be less than or equal to the total number of processes'
59
+ end
60
+
61
+ all_specified_tests = specify_test_process_groups.map { |group| group.split(',') }.flatten
62
+ specified_items_found, items = items.partition { |item, _size| all_specified_tests.include?(item) }
63
+
64
+ specified_specs_not_found = all_specified_tests - specified_items_found.map(&:first)
65
+ if specified_specs_not_found.any?
66
+ raise "Could not find #{specified_specs_not_found} from --specify-groups in the selected files & folders"
67
+ end
68
+
69
+ if specify_test_process_groups.count == num_groups && items.flatten.any?
70
+ raise(
71
+ <<~ERROR
72
+ The number of groups in --specify-groups matches the number of groups from -n but there were other specs
73
+ found in the selected files & folders not specified in --specify-groups. Make sure -n is larger than the
74
+ number of processes in --specify-groups if there are other specs that need to be run. The specs that aren't run:
75
+ #{items.map(&:first)}
76
+ ERROR
77
+ )
78
+ end
79
+
80
+ # First order the specify_groups into the main groups array
81
+ specify_test_process_groups.each_with_index do |specify_test_process, i|
82
+ groups[i] = specify_test_process.split(',')
83
+ end
84
+
85
+ # Return early when processed specify_groups tests exactly match the items passed in
86
+ return groups if specify_test_process_groups.count == num_groups
87
+
88
+ # Now sort the rest of the items into the main groups array
89
+ specified_range = specify_test_process_groups.count..-1
90
+ remaining_groups = groups[specified_range]
91
+ group_features_by_size(items_to_group(items), remaining_groups)
92
+ # Don't sort all the groups, only sort the ones not specified in specify_groups
93
+ sorted_groups = remaining_groups.map { |g| g[:items].sort }
94
+ groups[specified_range] = sorted_groups
95
+
96
+ groups
97
+ end
98
+
99
+ def isolate_count(options)
100
+ if options[:isolate_count] && options[:isolate_count] > 1
101
+ options[:isolate_count]
102
+ elsif options[:isolate]
103
+ 1
104
+ else
105
+ 0
106
+ end
107
+ end
108
+
31
109
  def largest_first(files)
32
- files.sort_by{|item, size| size }.reverse
110
+ files.sort_by { |_item, size| size }.reverse
33
111
  end
34
112
 
35
113
  def smallest_group(groups)
36
- groups.min_by{|g| g[:size] }
114
+ groups.min_by { |g| g[:size] }
37
115
  end
38
116
 
39
117
  def add_to_group(group, item, size)
@@ -41,18 +119,12 @@ module ParallelTests
41
119
  group[:size] += size
42
120
  end
43
121
 
44
- def build_features_with_steps(tests, options)
45
- require 'parallel_tests/gherkin/listener'
46
- listener = ParallelTests::Gherkin::Listener.new
47
- listener.ignore_tag_pattern = Regexp.compile(options[:ignore_tag_pattern]) if options[:ignore_tag_pattern]
48
- parser = ::Gherkin::Parser::Parser.new(listener, true, 'root')
49
- tests.each{|file|
50
- parser.parse(File.read(file), file, 0)
51
- }
52
- listener.collect.sort_by{|_,value| -value }
122
+ def group_by_features_with_steps(tests, options)
123
+ require 'parallel_tests/cucumber/features_with_steps'
124
+ ParallelTests::Cucumber::FeaturesWithSteps.all(tests, options)
53
125
  end
54
126
 
55
- def group_by_scenarios(tests, options={})
127
+ def group_by_scenarios(tests, options = {})
56
128
  require 'parallel_tests/cucumber/scenarios'
57
129
  ParallelTests::Cucumber::Scenarios.all(tests, options)
58
130
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ module ParallelTests
5
+ class Pids
6
+ attr_reader :file_path, :mutex
7
+
8
+ def initialize(file_path)
9
+ @file_path = file_path
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def add(pid)
14
+ pids << pid.to_i
15
+ save
16
+ end
17
+
18
+ def delete(pid)
19
+ pids.delete(pid.to_i)
20
+ save
21
+ end
22
+
23
+ def count
24
+ read
25
+ pids.count
26
+ end
27
+
28
+ def all
29
+ read
30
+ pids
31
+ end
32
+
33
+ private
34
+
35
+ def pids
36
+ @pids ||= []
37
+ end
38
+
39
+ def clear
40
+ @pids = []
41
+ save
42
+ end
43
+
44
+ def read
45
+ sync do
46
+ contents = IO.read(file_path)
47
+ return if contents.empty?
48
+ @pids = JSON.parse(contents)
49
+ end
50
+ end
51
+
52
+ def save
53
+ sync { IO.write(file_path, pids.to_json) }
54
+ end
55
+
56
+ def sync(&block)
57
+ mutex.synchronize(&block)
58
+ end
59
+ end
60
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # rake tasks for Rails 3+
2
3
  module ParallelTests
3
4
  class Railtie < ::Rails::Railtie