parallel_tests 1.3.7 → 3.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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