turbo_tests 1.2.2 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92f70e0d1c7d01b5f9d17960ddcf2418c8c4fa620e8c46d0bb81df21b9193452
4
- data.tar.gz: 0a21dd64949d53a8045e167754f19b698356f2a5c088f6ad56b83297d42932a6
3
+ metadata.gz: ed1e26234763e4fd425447465a01b091f3b895c5c315beaf06e03b96225a2b28
4
+ data.tar.gz: c37798c25bab9b136a0768795d3f2c45d65de8d6b2a2c48a9e1fd7ed30a6a5eb
5
5
  SHA512:
6
- metadata.gz: 0d0e28e448d9b3f6e8dc5f47ef6b6308d3a8a08ffdcdcacdddc20f979af2a1f6d7e0827afbe5cc1fd09293efa3abed703cf7c0082a5d5cc400dd32fb9c9672a0
7
- data.tar.gz: 6efbeb21524f9973d759914766cf0966f4b3da0d0c68dab75c4efe0c412808df6a979324793ce3de21ff77745ca7f5bffda605f3a6e73635839e14e84b1d4658
6
+ metadata.gz: 3381d49ecc0e38cdfa0a71cce9961d29bfb41b0ccb4d1b9abae13ac4202c90ea59f8b3951292e8a1f89d298d8ffbd426bead4a8135c51b8b675793018eadb239
7
+ data.tar.gz: '085230889cbe5749e66d140ecd32fa3ba0bfc4ab7bbf117e1b400cf48e65e1590de959ffaac6cfb7c95faf4b5db09147282eb26deb4c021f749a9fb9c269fc70'
data/.rspec CHANGED
@@ -1,3 +1,4 @@
1
1
  --format documentation
2
2
  --color
3
+ --tty
3
4
  --require spec_helper
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in turbo_tests.gemspec
4
4
  gemspec
5
5
 
6
- gem "rake", "~> 12.0"
6
+ gem "rake", "~> 13.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- turbo_tests (1.2.2)
4
+ turbo_tests (1.2.3)
5
5
  bundler
6
6
  parallel_tests (~> 3.3)
7
7
  rspec (~> 3.10.0)
@@ -13,12 +13,12 @@ GEM
13
13
  diff-lcs (1.4.4)
14
14
  method_source (1.0.0)
15
15
  parallel (1.20.1)
16
- parallel_tests (3.5.0)
16
+ parallel_tests (3.5.2)
17
17
  parallel
18
18
  pry (0.14.0)
19
19
  coderay (~> 1.1)
20
20
  method_source (~> 1.0)
21
- rake (12.3.3)
21
+ rake (13.0.3)
22
22
  rspec (3.10.0)
23
23
  rspec-core (~> 3.10.0)
24
24
  rspec-expectations (~> 3.10.0)
@@ -37,8 +37,8 @@ PLATFORMS
37
37
  ruby
38
38
 
39
39
  DEPENDENCIES
40
- pry (~> 0.13)
41
- rake (~> 12.0)
40
+ pry (~> 0.14)
41
+ rake (~> 13.0)
42
42
  turbo_tests!
43
43
 
44
44
  BUNDLED WITH
@@ -0,0 +1,5 @@
1
+ RSpec.describe "Fixture of spec file with errors outside of examples" do
2
+ it("passes") { expect(2 * 2).to eql(4) }
3
+
4
+ 1 / 0
5
+ end
data/lib/turbo_tests.rb CHANGED
@@ -23,14 +23,14 @@ module TurboTests
23
23
  klass =
24
24
  Class.new(FakeException) {
25
25
  define_singleton_method(:name) do
26
- obj["class_name"]
26
+ obj[:class_name]
27
27
  end
28
28
  }
29
29
 
30
30
  klass.new(
31
- obj["backtrace"],
32
- obj["message"],
33
- FakeException.from_obj(obj["cause"])
31
+ obj[:backtrace],
32
+ obj[:message],
33
+ FakeException.from_obj(obj[:cause])
34
34
  )
35
35
  end
36
36
  end
@@ -40,11 +40,11 @@ module TurboTests
40
40
  class FakeExecutionResult
41
41
  def self.from_obj(obj)
42
42
  new(
43
- obj["example_skipped?"],
44
- obj["pending_message"],
45
- obj["status"].to_sym,
46
- obj["pending_fixed?"],
47
- FakeException.from_obj(obj["exception"])
43
+ obj[:example_skipped?],
44
+ obj[:pending_message],
45
+ obj[:status].to_sym,
46
+ obj[:pending_fixed?],
47
+ FakeException.from_obj(obj[:exception])
48
48
  )
49
49
  end
50
50
  end
@@ -52,24 +52,24 @@ module TurboTests
52
52
  FakeExample = Struct.new(:execution_result, :location, :description, :full_description, :metadata, :location_rerun_argument)
53
53
  class FakeExample
54
54
  def self.from_obj(obj)
55
- metadata = obj["metadata"]
55
+ metadata = obj[:metadata]
56
56
 
57
- metadata["shared_group_inclusion_backtrace"].map! do |frame|
57
+ metadata[:shared_group_inclusion_backtrace].map! do |frame|
58
58
  RSpec::Core::SharedExampleGroupInclusionStackFrame.new(
59
- frame["shared_group_name"],
60
- frame["inclusion_location"]
59
+ frame[:shared_group_name],
60
+ frame[:inclusion_location]
61
61
  )
62
62
  end
63
63
 
64
- metadata[:shared_group_inclusion_backtrace] = metadata.delete("shared_group_inclusion_backtrace")
64
+ metadata[:shared_group_inclusion_backtrace] = metadata.delete(:shared_group_inclusion_backtrace)
65
65
 
66
66
  new(
67
- FakeExecutionResult.from_obj(obj["execution_result"]),
68
- obj["location"],
69
- obj["description"],
70
- obj["full_description"],
67
+ FakeExecutionResult.from_obj(obj[:execution_result]),
68
+ obj[:location],
69
+ obj[:description],
70
+ obj[:full_description],
71
71
  metadata,
72
- obj["location_rerun_argument"]
72
+ obj[:location_rerun_argument]
73
73
  )
74
74
  end
75
75
 
@@ -30,6 +30,7 @@ module TurboTests
30
30
  :example_group_started,
31
31
  :example_group_finished,
32
32
  :example_pending,
33
+ :message,
33
34
  :seed
34
35
  )
35
36
 
@@ -39,59 +40,65 @@ module TurboTests
39
40
  @output = output
40
41
  end
41
42
 
42
-
43
43
  def start(notification)
44
44
  output_row(
45
- "type" => :load_summary,
46
- "summary" => load_summary_to_json(notification)
45
+ type: :load_summary,
46
+ summary: load_summary_to_json(notification)
47
47
  )
48
48
  end
49
49
 
50
50
  def example_group_started(notification)
51
51
  output_row(
52
- "type" => :group_started,
53
- "group" => group_to_json(notification)
52
+ type: :group_started,
53
+ group: group_to_json(notification)
54
54
  )
55
55
  end
56
56
 
57
57
  def example_group_finished(notification)
58
58
  output_row(
59
- "type" => :group_finished,
60
- "group" => group_to_json(notification)
59
+ type: :group_finished,
60
+ group: group_to_json(notification)
61
61
  )
62
62
  end
63
63
 
64
64
  def example_passed(notification)
65
65
  output_row(
66
- "type" => :example_passed,
67
- "example" => example_to_json(notification.example)
66
+ type: :example_passed,
67
+ example: example_to_json(notification.example)
68
68
  )
69
69
  end
70
70
 
71
71
  def example_pending(notification)
72
72
  output_row(
73
- "type" => :example_pending,
74
- "example" => example_to_json(notification.example)
73
+ type: :example_pending,
74
+ example: example_to_json(notification.example)
75
75
  )
76
76
  end
77
77
 
78
78
  def example_failed(notification)
79
79
  output_row(
80
- "type" => :example_failed,
81
- "example" => example_to_json(notification.example)
80
+ type: :example_failed,
81
+ example: example_to_json(notification.example)
82
82
  )
83
83
  end
84
84
 
85
85
  def seed(notification)
86
86
  output_row(
87
- "type" => :seed,
88
- "seed" => notification.seed
87
+ type: :seed,
88
+ seed: notification.seed
89
89
  )
90
90
  end
91
91
 
92
92
  def close(notification)
93
93
  output_row(
94
- "type" => :close
94
+ type: :close
95
+ )
96
+ end
97
+
98
+ def message(notification)
99
+ output_row(
100
+ type: :message,
101
+ message: notification.message
95
102
  )
96
103
  end
97
104
 
@@ -100,62 +107,65 @@ module TurboTests
100
107
  def exception_to_json(exception)
101
108
  if exception
102
109
  {
103
- "class_name" => exception.class.name.to_s,
104
- "backtrace" => exception.backtrace,
105
- "message" => exception.message,
106
- "cause" => exception_to_json(exception.cause)
110
+ class_name: exception.class.name.to_s,
111
+ backtrace: exception.backtrace,
112
+ message: exception.message,
113
+ cause: exception_to_json(exception.cause)
107
114
  }
108
115
  end
109
116
  end
110
117
 
111
118
  def execution_result_to_json(result)
112
119
  {
113
- "example_skipped?" => result.example_skipped?,
114
- "pending_message" => result.pending_message,
115
- "status" => result.status,
116
- "pending_fixed?" => result.pending_fixed?,
117
- "exception" => exception_to_json(result.exception)
120
+ example_skipped?: result.example_skipped?,
121
+ pending_message: result.pending_message,
122
+ status: result.status,
123
+ pending_fixed?: result.pending_fixed?,
124
+ exception: exception_to_json(result.exception)
118
125
  }
119
126
  end
120
127
 
121
128
  def stack_frame_to_json(frame)
122
129
  {
123
- "shared_group_name" => frame.shared_group_name,
124
- "inclusion_location" => frame.inclusion_location
130
+ shared_group_name: frame.shared_group_name,
131
+ inclusion_location: frame.inclusion_location
125
132
  }
126
133
  end
127
134
 
128
135
  def example_to_json(example)
129
136
  {
130
- "execution_result" => execution_result_to_json(example.execution_result),
131
- "location" => example.location,
132
- "description" => example.description,
133
- "full_description" => example.full_description,
134
- "metadata" => {
135
- "shared_group_inclusion_backtrace" =>
136
- example.metadata[:shared_group_inclusion_backtrace].map { |frame| stack_frame_to_json(frame) }
137
+ execution_result: execution_result_to_json(example.execution_result),
138
+ location: example.location,
139
+ description: example.description,
140
+ full_description: example.full_description,
141
+ metadata: {
142
+ shared_group_inclusion_backtrace:
143
+ example
144
+ .metadata[:shared_group_inclusion_backtrace]
145
+ .map { |frame| stack_frame_to_json(frame) }
137
146
  },
138
- "location_rerun_argument" => example.location_rerun_argument
147
+ location_rerun_argument: example.location_rerun_argument
139
148
  }
140
149
  end
141
150
 
142
151
  def load_summary_to_json(notification)
143
152
  {
144
153
  count: notification.count,
145
- load_time: notification.load_time
154
+ load_time: notification.load_time,
146
155
  }
147
156
  end
148
157
 
149
158
  def group_to_json(notification)
150
159
  {
151
- "group": {
152
- "description": notification.group.description
160
+ group: {
161
+ description: notification.group.description
153
162
  }
154
163
  }
155
164
  end
156
165
 
157
166
  def output_row(obj)
158
- output.puts ENV["RSPEC_FORMATTER_OUTPUT_ID"] + obj.to_json
167
+ output.puts(obj.to_json)
168
+ output.flush
159
169
  end
160
170
  end
161
171
  end
@@ -28,8 +28,10 @@ module TurboTests
28
28
  @pending_examples = []
29
29
  @failed_examples = []
30
30
  @all_examples = []
31
+ @messages = []
31
32
  @start_time = start_time
32
33
  @load_time = 0
34
+ @errors_outside_of_examples_count = 0
33
35
  end
34
36
 
35
37
  def add(name, outputs)
@@ -76,6 +78,15 @@ module TurboTests
76
78
  @failed_examples << example
77
79
  end
78
80
 
81
+ def message(message)
82
+ delegate_to_formatters(:message, RSpec::Core::Notifications::MessageNotification.new(message))
83
+ @messages << message
84
+ end
85
+
86
+ def error_outside_of_examples
87
+ @errors_outside_of_examples_count += 12
88
+ end
89
+
79
90
  def finish
80
91
  # SEE: https://bit.ly/2NP87Cz
81
92
  end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -97,7 +108,7 @@ module TurboTests
97
108
  @failed_examples,
98
109
  @pending_examples,
99
110
  @load_time,
100
- 0
111
+ @errors_outside_of_examples_count
101
112
  ))
102
113
  delegate_to_formatters(:close,
103
114
  RSpec::Core::Notifications::NullNotification)
@@ -20,6 +20,10 @@ module TurboTests
20
20
  fail_fast = opts.fetch(:fail_fast, nil)
21
21
  count = opts.fetch(:count, nil)
22
22
 
23
+ if verbose
24
+ STDERR.puts "VERBOSE"
25
+ end
26
+
23
27
  reporter = Reporter.from_config(formatters, start_time)
24
28
 
25
29
  new(
@@ -41,12 +45,11 @@ module TurboTests
41
45
  @count = opts[:count]
42
46
  @load_time = 0
43
47
  @load_count = 0
44
-
45
48
  @failure_count = 0
46
- @runtime_log = "tmp/parallel_runtime_rspec.log"
47
49
 
48
50
  @messages = Queue.new
49
51
  @threads = []
52
+ @error = false
50
53
  end
51
54
 
52
55
  def run
@@ -55,17 +58,33 @@ module TurboTests
55
58
  ParallelTests::RSpec::Runner.tests_with_size(@files, {}).size
56
59
  ].min
57
60
 
61
+ use_runtime_info = @files == ["spec"]
62
+
63
+ group_opts = {}
64
+
65
+ if use_runtime_info
66
+ group_opts[:runtime_log] = "tmp/turbo_rspec_runtime.log"
67
+ else
68
+ group_opts[:group_by] = :filesize
69
+ end
70
+
58
71
  tests_in_groups =
59
72
  ParallelTests::RSpec::Runner.tests_in_groups(
60
73
  @files,
61
74
  @num_processes,
62
- runtime_log: @runtime_log
75
+ **group_opts
63
76
  )
64
77
 
78
+ setup_tmp_dir
79
+
80
+ subprocess_opts = {
81
+ record_runtime: use_runtime_info
82
+ }
83
+
65
84
  report_number_of_tests(tests_in_groups)
66
85
 
67
- tests_in_groups.each_with_index do |tests, process_id|
68
- start_regular_subprocess(tests, process_id + 1)
86
+ wait_threads = tests_in_groups.map.with_index do |tests, process_id|
87
+ start_regular_subprocess(tests, process_id + 1, **subprocess_opts)
69
88
  end
70
89
 
71
90
  handle_messages
@@ -74,38 +93,64 @@ module TurboTests
74
93
 
75
94
  @threads.each(&:join)
76
95
 
77
- @reporter.failed_examples.empty?
96
+ @reporter.failed_examples.empty? && wait_threads.map(&:value).all?(&:success?)
78
97
  end
79
98
 
80
- protected
99
+ private
100
+
101
+ def setup_tmp_dir
102
+ begin
103
+ FileUtils.rm_r("tmp/test-pipes")
104
+ rescue Errno::ENOENT
105
+ end
106
+
107
+ FileUtils.mkdir_p("tmp/test-pipes/")
108
+ end
81
109
 
82
- def start_regular_subprocess(tests, process_id)
110
+ def start_regular_subprocess(tests, process_id, **opts)
83
111
  start_subprocess(
84
112
  {"TEST_ENV_NUMBER" => process_id.to_s},
85
113
  @tags.map { |tag| "--tag=#{tag}" },
86
114
  tests,
87
- process_id
115
+ process_id,
116
+ **opts
88
117
  )
89
118
  end
90
119
 
91
- def start_subprocess(env, extra_args, tests, process_id)
120
+ def start_subprocess(env, extra_args, tests, process_id, record_runtime:)
92
121
  if tests.empty?
93
122
  @messages << {
94
- "type" => "exit",
95
- "process_id" => process_id
123
+ type: "exit",
124
+ process_id: process_id
96
125
  }
97
126
  else
98
- require "securerandom"
99
- env["RSPEC_FORMATTER_OUTPUT_ID"] = SecureRandom.uuid
100
- env["RUBYOPT"] = "-I#{File.expand_path("..", __dir__)}"
127
+ tmp_filename = "tmp/test-pipes/subprocess-#{process_id}"
128
+
129
+ begin
130
+ File.mkfifo(tmp_filename)
131
+ rescue Errno::EEXIST
132
+ end
133
+
134
+ env["RUBYOPT"] = ["-I#{File.expand_path("..", __dir__)}", ENV["RUBYOPT"]].compact.join(" ")
135
+ env["RSPEC_SILENCE_FILTER_ANNOUNCEMENTS"] = "1"
136
+
137
+ record_runtime_options =
138
+ if record_runtime
139
+ [
140
+ "--format", "ParallelTests::RSpec::RuntimeLogger",
141
+ "--out", "tmp/turbo_rspec_runtime.log",
142
+ ]
143
+ else
144
+ []
145
+ end
101
146
 
102
147
  command = [
103
148
  ENV["BUNDLE_BIN_PATH"], "exec", "rspec",
104
149
  *extra_args,
105
- "--seed", rand(2**16).to_s,
106
- "--format", "ParallelTests::RSpec::RuntimeLogger",
107
- "--out", @runtime_log,
150
+ "--seed", rand(0xFFFF).to_s,
108
151
  "--format", "TurboTests::JsonRowsFormatter",
152
+ "--out", tmp_filename,
153
+ *record_runtime_options,
109
154
  *tests
110
155
  ]
111
156
 
@@ -118,29 +163,33 @@ module TurboTests
118
163
  STDERR.puts "Process #{process_id}: #{command_str}"
119
164
  end
120
165
 
121
- _stdin, stdout, stderr, _wait_thr = Open3.popen3(env, *command)
166
+ stdin, stdout, stderr, wait_thr = Open3.popen3(env, *command)
167
+ stdin.close
122
168
 
123
169
  @threads <<
124
- Thread.new {
125
- require "json"
126
- stdout.each_line do |line|
127
- result = line.split(env["RSPEC_FORMATTER_OUTPUT_ID"])
128
-
129
- output = result.shift
130
- STDOUT.print(output) unless output.empty?
131
-
132
- message = result.shift
133
- next unless message
134
-
135
- message = JSON.parse(message)
136
- message["process_id"] = process_id
137
- @messages << message
170
+ Thread.new do
171
+ File.open(tmp_filename) do |fd|
172
+ fd.each_line do |line|
173
+ message = JSON.parse(line, symbolize_names: true)
174
+
175
+ message[:process_id] = process_id
176
+ @messages << message
177
+ end
138
178
  end
139
179
 
140
- @messages << {"type" => "exit", "process_id" => process_id}
141
- }
180
+ @messages << {type: "exit", process_id: process_id}
181
+ end
142
182
 
183
+ @threads << start_copy_thread(stdout, STDOUT)
143
184
  @threads << start_copy_thread(stderr, STDERR)
185
+
186
+ @threads << Thread.new {
187
+ unless wait_thr.value.success?
188
+ @messages << {type: "error"}
189
+ end
190
+ }
191
+
192
+ wait_thr
144
193
  end
145
194
  end
146
195
 
@@ -149,6 +198,7 @@ module TurboTests
149
198
  loop do
150
199
  msg = src.readpartial(4096)
151
200
  rescue EOFError
201
+ src.close
152
202
  break
153
203
  else
154
204
  dst.write(msg)
@@ -161,40 +211,47 @@ module TurboTests
161
211
 
162
212
  loop do
163
213
  message = @messages.pop
164
- case message["type"]
214
+ case message[:type]
165
215
  when "example_passed"
166
- example = FakeExample.from_obj(message["example"])
216
+ example = FakeExample.from_obj(message[:example])
167
217
  @reporter.example_passed(example)
168
218
  when "group_started"
169
- @reporter.group_started(message["group"].to_struct)
219
+ @reporter.group_started(message[:group].to_struct)
170
220
  when "group_finished"
171
221
  @reporter.group_finished
172
222
  when "example_pending"
173
- example = FakeExample.from_obj(message["example"])
223
+ example = FakeExample.from_obj(message[:example])
174
224
  @reporter.example_pending(example)
175
225
  when "load_summary"
176
- message = message["summary"]
226
+ message = message[:summary]
177
227
  # NOTE: notifications order and content is not guaranteed hence the fetch
178
228
  # and count increment tracking to get the latest accumulated load time
179
- @reporter.load_time = message["load_time"] if message.fetch("count", 0) > @load_count
229
+ @reporter.load_time = message[:load_time] if message.fetch(:count, 0) > @load_count
180
230
  when "example_failed"
181
- example = FakeExample.from_obj(message["example"])
231
+ example = FakeExample.from_obj(message[:example])
182
232
  @reporter.example_failed(example)
183
233
  @failure_count += 1
184
234
  if fail_fast_met
185
235
  @threads.each(&:kill)
186
236
  break
187
237
  end
238
+ when "message"
239
+ @reporter.message(message[:message])
188
240
  when "seed"
189
241
  when "close"
242
+ when "error"
243
+ @reporter.error_outside_of_examples
244
+ @error = true
190
245
  when "exit"
191
246
  exited += 1
192
247
  if exited == @num_processes
193
248
  break
194
249
  end
195
250
  else
196
- warn("Unhandled message in main process: #{message}")
251
+ STDERR.puts("Unhandled message in main process: #{message}")
197
252
  end
253
+
254
+ STDOUT.flush
198
255
  end
199
256
  rescue Interrupt
200
257
  end
@@ -203,8 +260,6 @@ module TurboTests
203
260
  !@fail_fast.nil? && @fail_fast >= @failure_count
204
261
  end
205
262
 
206
- private
207
-
208
263
  def report_number_of_tests(groups)
209
264
  name = ParallelTests::RSpec::Runner.test_file_name
210
265
 
@@ -1,3 +1,3 @@
1
1
  module TurboTests
2
- VERSION = "1.2.2"
2
+ VERSION = "1.2.3"
3
3
  end
data/turbo_tests.gemspec CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.add_dependency "rspec", "~> 3.10.0"
20
20
  spec.add_dependency "parallel_tests", "~> 3.3"
21
21
 
22
- spec.add_development_dependency "pry", "~> 0.13"
22
+ spec.add_development_dependency "pry", "~> 0.14"
23
23
 
24
24
  spec.add_runtime_dependency "bundler"
25
25
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo_tests
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ilya Zub
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-02 00:00:00.000000000 Z
11
+ date: 2021-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.13'
47
+ version: '0.14'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.13'
54
+ version: '0.14'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +86,7 @@ files:
86
86
  - README.md
87
87
  - Rakefile
88
88
  - bin/turbo_tests
89
+ - fixtures/rspec/errors_outside_of_examples_spec.rb
89
90
  - lib/turbo_tests.rb
90
91
  - lib/turbo_tests/cli.rb
91
92
  - lib/turbo_tests/json_rows_formatter.rb