test_launcher 1.5.0 → 1.5.1

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -0
  3. data/bin/test_launcher +2 -5
  4. data/lib/test_launcher/cli/input_parser.rb +37 -12
  5. data/lib/test_launcher/cli/query.rb +30 -0
  6. data/lib/test_launcher/cli/request.rb +61 -0
  7. data/lib/test_launcher/cli.rb +44 -0
  8. data/lib/test_launcher/frameworks/base.rb +26 -7
  9. data/lib/test_launcher/frameworks/elixir.rb +84 -0
  10. data/lib/test_launcher/frameworks/implementation/test_case.rb +4 -7
  11. data/lib/test_launcher/frameworks/minitest.rb +80 -23
  12. data/lib/test_launcher/frameworks/rspec.rb +38 -13
  13. data/lib/test_launcher/queries.rb +346 -0
  14. data/lib/test_launcher/rubymine/launcher.rb +1 -12
  15. data/lib/test_launcher/rubymine/request.rb +15 -0
  16. data/lib/test_launcher/rubymine.rb +20 -13
  17. data/lib/test_launcher/search/ag.rb +96 -0
  18. data/lib/test_launcher/search/git.rb +6 -2
  19. data/lib/test_launcher/search.rb +18 -0
  20. data/lib/test_launcher/shell/runner.rb +2 -1
  21. data/lib/test_launcher/version.rb +1 -1
  22. data/lib/test_launcher.rb +0 -26
  23. data/test/test_helper.rb +2 -1
  24. data/test/test_helpers/integration_helper.rb +40 -0
  25. data/test/test_helpers/mock.rb +59 -0
  26. data/test/test_helpers/mock_searcher.rb +1 -0
  27. data/test/test_helpers/mocks/searcher_mock.rb +82 -0
  28. data/test/test_helpers/mocks.rb +76 -0
  29. data/test/test_launcher/cli/input_parser_test.rb +72 -0
  30. data/test/test_launcher/frameworks/minitest/runner_test.rb +72 -0
  31. data/test/test_launcher/frameworks/minitest/searcher_test.rb +109 -0
  32. data/test/test_launcher/frameworks/rspec/runner_test.rb +76 -0
  33. data/test/test_launcher/frameworks/rspec/searcher_test.rb +54 -0
  34. data/test/test_launcher/minitest_integration_test.rb +31 -40
  35. data/test/test_launcher/queries/example_name_query_test.rb +217 -0
  36. data/test/test_launcher/queries/full_regex_query_test.rb +153 -0
  37. data/test/test_launcher/queries/generic_query_test.rb +23 -0
  38. data/test/test_launcher/queries/line_number_query_test.rb +107 -0
  39. data/test/test_launcher/queries/multi_term_query_test.rb +138 -0
  40. data/test/test_launcher/queries/path_query_test.rb +192 -0
  41. data/test/test_launcher/queries/search_query_test.rb +54 -0
  42. data/test/test_launcher/queries/single_term_query_test.rb +36 -0
  43. data/test/test_launcher/queries/specified_name_query_test.rb +112 -0
  44. data/test/test_launcher/rspec_integration_test.rb +27 -41
  45. data/test/test_launcher/search/git_test.rb +2 -0
  46. metadata +49 -10
  47. data/lib/test_launcher/frameworks/implementation/collection.rb +0 -36
  48. data/lib/test_launcher/frameworks/implementation/consolidator.rb +0 -83
  49. data/lib/test_launcher/frameworks/implementation/locator.rb +0 -118
  50. data/lib/test_launcher/frameworks.rb +0 -20
  51. data/lib/test_launcher/request.rb +0 -36
  52. data/test/test_launcher/frameworks/implementation/locator_test.rb +0 -166
@@ -0,0 +1,346 @@
1
+ module TestLauncher
2
+ module Queries
3
+ class CommandFinder
4
+ def initialize(request)
5
+ @request = request
6
+ end
7
+
8
+ def specified_name
9
+ commandify(SpecifiedNameQuery)
10
+ end
11
+
12
+ def multi_search_term
13
+ commandify(MultiTermQuery)
14
+ end
15
+
16
+ def by_path
17
+ commandify(PathQuery)
18
+ end
19
+
20
+ def example_name
21
+ commandify(ExampleNameQuery)
22
+ end
23
+
24
+ def from_full_regex
25
+ commandify(FullRegexQuery)
26
+ end
27
+
28
+ def single_search_term
29
+ commandify(SingleTermQuery)
30
+ end
31
+
32
+ def full_search
33
+ commandify(SearchQuery)
34
+ end
35
+
36
+ def generic_search
37
+ commandify(GenericQuery)
38
+ end
39
+
40
+ def line_number
41
+ commandify(LineNumberQuery)
42
+ end
43
+
44
+ def request
45
+ @request
46
+ end
47
+
48
+ def commandify(klass)
49
+ klass.new(
50
+ request,
51
+ self
52
+ ).command
53
+ end
54
+ end
55
+
56
+ class BaseQuery
57
+ attr_reader :shell, :searcher, :request
58
+ def initialize(request, command_finder)
59
+ @request = request
60
+ @command_finder = command_finder
61
+ end
62
+
63
+ def command
64
+ raise NotImplementedError
65
+ end
66
+
67
+ private
68
+
69
+ def test_cases
70
+ raise NotImplementedError
71
+ end
72
+
73
+ def runner
74
+ request.runner
75
+ end
76
+
77
+ def shell
78
+ request.shell
79
+ end
80
+
81
+ def searcher
82
+ request.searcher
83
+ end
84
+
85
+ def one_file?
86
+ file_count == 1
87
+ end
88
+
89
+ def file_count
90
+ @file_count ||= test_cases.map {|tc| tc.file }.uniq.size
91
+ end
92
+
93
+ def most_recently_edited_test_case
94
+ @most_recently_edited_test_case ||= test_cases.sort_by(&:mtime).last
95
+ end
96
+
97
+ def pluralize(count, singular)
98
+ phrase = "#{count} #{singular}"
99
+ if count == 1
100
+ phrase
101
+ else
102
+ "#{phrase}s"
103
+ end
104
+ end
105
+
106
+ def command_finder
107
+ @command_finder
108
+ end
109
+ end
110
+
111
+ class SpecifiedNameQuery < BaseQuery
112
+ def command
113
+ return unless file
114
+
115
+ shell.notify("Found matching test.")
116
+ runner.single_example(test_case)
117
+ end
118
+
119
+ def test_case
120
+ request.test_case(
121
+ file: file,
122
+ example: request.example_name,
123
+ request: request,
124
+ )
125
+ end
126
+
127
+ def file
128
+ if potential_files.size == 0
129
+ shell.warn("Could not locate file: #{request.search_string}")
130
+ elsif potential_files.size > 1
131
+ shell.warn("Too many files matched: #{request.search_string}")
132
+ else
133
+ potential_files.first
134
+ end
135
+ end
136
+
137
+ def potential_files
138
+ @potential_files ||= searcher.test_files(request.search_string)
139
+ end
140
+ end
141
+
142
+ class MultiTermQuery < BaseQuery
143
+ def command
144
+ return if test_cases.empty?
145
+
146
+ shell.notify("Found #{pluralize(file_count, "file")}.")
147
+ runner.multiple_files(test_cases)
148
+ end
149
+
150
+ def test_cases
151
+ @test_cases ||= files.map { |file_path|
152
+ request.test_case(
153
+ file: file_path,
154
+ request: request,
155
+ )
156
+ }
157
+ end
158
+
159
+ def files
160
+ if found_files.any? {|files_array| files_array.empty? }
161
+ if !found_files.all? {|files_array| files_array.empty? }
162
+ shell.warn("It looks like you're searching for multiple files, but we couldn't identify them all.")
163
+ end
164
+ []
165
+ else
166
+ found_files.flatten.uniq
167
+ end
168
+ end
169
+
170
+ def found_files
171
+ @found_files ||= queries.map {|query|
172
+ searcher.test_files(query)
173
+ }
174
+ end
175
+
176
+ def queries
177
+ @queries ||= request.search_string.split(" ")
178
+ end
179
+ end
180
+
181
+ class PathQuery < BaseQuery
182
+ def command
183
+ return if test_cases.empty?
184
+
185
+ if one_file?
186
+ shell.notify "Found #{pluralize(file_count, "file")}."
187
+ runner.single_file(test_cases.first)
188
+ elsif request.run_all?
189
+ shell.notify "Found #{pluralize(file_count, "file")}."
190
+ runner.multiple_files(test_cases)
191
+ else
192
+ shell.notify "Found #{pluralize(file_count, "file")}."
193
+ shell.notify "Running most recently edited. Run with '--all' to run all the tests."
194
+ runner.single_file(most_recently_edited_test_case)
195
+ end
196
+ end
197
+
198
+ def test_cases
199
+ @test_cases ||= files_found_by_path.map { |file_path|
200
+ request.test_case(file: file_path, request: request)
201
+ }
202
+ end
203
+
204
+ def files_found_by_path
205
+ @files_found_by_path ||= searcher.test_files(request.search_string)
206
+ end
207
+ end
208
+
209
+ class ExampleNameQuery < BaseQuery
210
+ def command
211
+ return if test_cases.empty?
212
+
213
+ if one_example?
214
+ shell.notify("Found 1 example in 1 file.")
215
+ runner.single_example(test_cases.first)
216
+ elsif one_file?
217
+ shell.notify("Found #{test_cases.size} examples in 1 file.")
218
+ runner.multiple_examples_same_file(test_cases) # it will regex with the query
219
+ elsif request.run_all?
220
+ shell.notify "Found #{pluralize(test_cases.size, "example")} in #{pluralize(file_count, "file")}."
221
+ runner.multiple_files(test_cases)
222
+ else
223
+ shell.notify "Found #{pluralize(test_cases.size, "example")} in #{pluralize(file_count, "file")}."
224
+ shell.notify "Running most recently edited. Run with '--all' to run all the tests."
225
+ runner.single_example(most_recently_edited_test_case) # let it regex the query
226
+ end
227
+ end
228
+
229
+ def test_cases
230
+ @test_cases ||=
231
+ examples_found_by_name.map { |grep_result|
232
+ request.test_case(
233
+ file: grep_result[:file],
234
+ example: request.search_string,
235
+ line_number: grep_result[:line_number],
236
+ request: request
237
+ )
238
+ }
239
+ end
240
+
241
+ def examples_found_by_name
242
+ @examples_found_by_name ||= searcher.examples(request.search_string)
243
+ end
244
+
245
+ def one_example?
246
+ test_cases.size == 1
247
+ end
248
+ end
249
+
250
+ class FullRegexQuery < BaseQuery
251
+ def command
252
+ return if test_cases.empty?
253
+
254
+ if one_file?
255
+ shell.notify "Found #{pluralize(file_count, "file")}."
256
+ runner.single_file(test_cases.first)
257
+ elsif request.run_all?
258
+ shell.notify "Found #{pluralize(file_count, "file")}."
259
+ runner.multiple_files(test_cases)
260
+ else
261
+ shell.notify "Found #{pluralize(file_count, "file")}."
262
+ shell.notify "Running most recently edited. Run with '--all' to run all the tests."
263
+ runner.single_file(most_recently_edited_test_case)
264
+ end
265
+ end
266
+
267
+ def test_cases
268
+ @test_cases ||=
269
+ files_found_by_full_regex
270
+ .uniq { |grep_result| grep_result[:file] }
271
+ .map { |grep_result|
272
+ request.test_case(
273
+ file: grep_result[:file],
274
+ request: request
275
+ )
276
+ }
277
+ end
278
+
279
+ def files_found_by_full_regex
280
+ @files_found_by_full_regex ||= searcher.grep(request.search_string)
281
+ end
282
+ end
283
+
284
+ class LineNumberQuery < BaseQuery
285
+ LINE_SPLIT_REGEX = /\A(?<file>.*):(?<line_number>\d+)\Z/
286
+
287
+ def command
288
+ match = request.search_string.match(LINE_SPLIT_REGEX)
289
+ return unless match
290
+
291
+ search_result = searcher.by_line(match[:file], match[:line_number].to_i)
292
+ return unless search_result
293
+
294
+ if search_result[:line_number]
295
+ shell.notify("Found 1 example on line #{search_result[:line_number]}.")
296
+ runner.single_example(request.test_case(file: search_result[:file], line_number: search_result[:line_number], example: search_result[:example_name], request: request))
297
+ else
298
+ shell.notify("Found file, but line is not inside an example.")
299
+ runner.single_file(request.test_case(file: search_result[:file], request: request))
300
+ end
301
+
302
+ end
303
+ end
304
+
305
+ class SingleTermQuery < BaseQuery
306
+ def command
307
+ [
308
+ :by_path,
309
+ :example_name,
310
+ :from_full_regex,
311
+ ]
312
+ .each { |command_type|
313
+ command = command_finder.public_send(command_type)
314
+ return command if command
315
+ }
316
+ nil
317
+ end
318
+ end
319
+
320
+ class SearchQuery < BaseQuery
321
+ def command
322
+ {
323
+ multi_search_term: request.search_string.include?(" "),
324
+ line_number: request.search_string.include?(":"),
325
+ single_search_term: true
326
+ }.each {|command_type, valid|
327
+ next unless valid
328
+
329
+ command = command_finder.public_send(command_type)
330
+ return command if command
331
+ }
332
+ nil
333
+ end
334
+ end
335
+
336
+ class GenericQuery < BaseQuery
337
+ def command
338
+ if request.example_name
339
+ command_finder.specified_name
340
+ else
341
+ command_finder.full_search
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
@@ -1,19 +1,8 @@
1
- require "test_launcher/request"
2
1
  require "test_launcher/frameworks/minitest"
3
2
  require "test_launcher/shell/runner"
4
3
 
5
4
  module TestLauncher
6
5
  module Rubymine
7
- class MinimalRequest
8
- def initialize(disable_spring:)
9
- @disable_spring = disable_spring
10
- end
11
-
12
- def disable_spring?
13
- @disable_spring
14
- end
15
- end
16
-
17
6
  class Launcher
18
7
  def initialize(args:, shell:, request:)
19
8
  @args = args
@@ -40,7 +29,7 @@ module TestLauncher
40
29
 
41
30
  def command
42
31
  if test_case.is_example?
43
- Frameworks::Minitest::Runner.new.single_example(test_case, exact_match: true)
32
+ Frameworks::Minitest::Runner.new.single_example(test_case)
44
33
  else
45
34
  Frameworks::Minitest::Runner.new.single_file(test_case)
46
35
  end
@@ -0,0 +1,15 @@
1
+ module TestLauncher
2
+ module Rubymine
3
+ class Request
4
+ class MinimalRequest
5
+ def initialize(disable_spring:)
6
+ @disable_spring = disable_spring
7
+ end
8
+
9
+ def disable_spring?
10
+ @disable_spring
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,6 +1,6 @@
1
- require "test_launcher/rubymine/launcher"
2
1
  require "test_launcher/shell/runner"
3
- require "test_launcher/request"
2
+ require "test_launcher/rubymine/launcher"
3
+ require "test_launcher/rubymine/request"
4
4
 
5
5
  # To allow us to simply specify our run configuration as:
6
6
  #
@@ -20,15 +20,22 @@ require "test_launcher/request"
20
20
  # So we throw them in the same bucket and let the launcher figure it
21
21
  # out. It doesn't matter since we will `exec` a new command anyway.
22
22
 
23
- dummy_request = TestLauncher::Request.new(
24
- query: nil,
25
- framework: nil,
26
- run_all: false,
27
- disable_spring: ENV["DISABLE_SPRING"]
28
- )
23
+ module TestLauncher
24
+ module Rubymine
25
+ def self.launch
26
+ shell = TestLauncher::Shell::Runner.new(log_path: "/dev/null")
27
+
28
+ request = Request.new(
29
+ disable_spring: ENV["DISABLE_SPRING"]
30
+ )
31
+
32
+ Launcher.new(
33
+ args: [$0].concat(ARGV),
34
+ shell: shell,
35
+ request: request
36
+ ).launch
37
+ end
38
+ end
39
+ end
29
40
 
30
- TestLauncher::Rubymine::Launcher.new(
31
- args: [$0].concat(ARGV),
32
- shell: TestLauncher::Shell::Runner.new(log_path: "/dev/null"),
33
- request: dummy_request
34
- ).launch
41
+ TestLauncher::Rubymine.launch
@@ -0,0 +1,96 @@
1
+ require "test_launcher/base_error"
2
+
3
+ module TestLauncher
4
+ module Search
5
+ class Ag
6
+ NotInRepoError = Class.new(BaseError)
7
+ class Interface
8
+ attr_reader :shell
9
+
10
+ def initialize(shell)
11
+ @shell = shell
12
+ end
13
+
14
+ def ls_files(pattern)
15
+ shell.run("ag -g '.*#{pattern_to_regex(pattern)}.*'")
16
+ end
17
+
18
+ def grep(regex, file_pattern)
19
+ shell.run("ag '#{regex}' --file-search-regex '#{pattern_to_regex(file_pattern)}'")
20
+ end
21
+
22
+ def root_path
23
+ shell.run("git rev-parse --show-toplevel").first.tap do
24
+ if $? != 0
25
+ raise NotInRepoError, "test_launcher must be used in a git repository"
26
+ end
27
+ end
28
+ end
29
+
30
+ def pattern_to_regex(pattern)
31
+ pattern.gsub("*", ".*")
32
+ end
33
+ end
34
+
35
+ attr_reader :interface
36
+
37
+ def initialize(shell, interface=Interface.new(shell))
38
+ @interface = interface
39
+ Dir.chdir(root_path) # MOVE ME!
40
+ end
41
+
42
+ def find_files(pattern)
43
+ relative_pattern = strip_system_path(pattern)
44
+ interface.ls_files(relative_pattern).map {|f| system_path(f)}
45
+ end
46
+
47
+ def grep(regex, file_pattern: '*')
48
+ results = interface.grep(regex, file_pattern)
49
+ results.map do |result|
50
+ interpret_grep_result(result)
51
+ end
52
+ end
53
+
54
+
55
+ private
56
+
57
+ def interpret_grep_result(grep_result)
58
+ splits = grep_result.split(/:/)
59
+ file = splits.shift.strip
60
+ line_number = splits.shift.strip.to_i
61
+ # we rejoin on ':' because our
62
+ # code may have colons inside of it.
63
+ #
64
+ # example:
65
+ # path/to/file:126: run_method(a: A, b: B)
66
+ #
67
+ # so shift the first one out, then
68
+ # rejoin the rest
69
+ line = splits.join(':').strip
70
+
71
+ # TODO: Oh goodness, why is this not a class
72
+ {
73
+ :file => system_path(file),
74
+ :line_number => line_number.to_i,
75
+ :line => line,
76
+ }
77
+ end
78
+
79
+ def system_path(file)
80
+ File.join(root_path, file)
81
+ end
82
+
83
+ def strip_system_path(file)
84
+ file.sub(/^#{root_path}\//, '')
85
+ end
86
+
87
+ def root_path
88
+ @root_path ||= interface.root_path
89
+ end
90
+
91
+ def shell
92
+ @shell
93
+ end
94
+ end
95
+ end
96
+ end
@@ -17,7 +17,7 @@ module TestLauncher
17
17
  end
18
18
 
19
19
  def grep(regex, file_pattern)
20
- shell.run("git grep --untracked --extended-regexp '#{regex}' -- '#{file_pattern}'")
20
+ shell.run("git grep --line-number --untracked --extended-regexp '#{regex}' -- '#{file_pattern}'")
21
21
  end
22
22
 
23
23
  def root_path
@@ -48,23 +48,27 @@ module TestLauncher
48
48
  end
49
49
  end
50
50
 
51
+
51
52
  private
52
53
 
53
54
  def interpret_grep_result(grep_result)
54
55
  splits = grep_result.split(/:/)
55
56
  file = splits.shift.strip
57
+ line_number = splits.shift.strip.to_i
56
58
  # we rejoin on ':' because our
57
59
  # code may have colons inside of it.
58
60
  #
59
61
  # example:
60
- # path/to/file: run_method(a: A, b: B)
62
+ # path/to/file:126: run_method(a: A, b: B)
61
63
  #
62
64
  # so shift the first one out, then
63
65
  # rejoin the rest
64
66
  line = splits.join(':').strip
65
67
 
68
+ # TODO: Oh goodness, why is this not a class
66
69
  {
67
70
  :file => system_path(file),
71
+ :line_number => line_number.to_i,
68
72
  :line => line,
69
73
  }
70
74
  end
@@ -0,0 +1,18 @@
1
+ require 'test_launcher/search/ag'
2
+ require 'test_launcher/search/git'
3
+
4
+ module TestLauncher
5
+ module Search
6
+ def self.searcher(shell)
7
+ `which ag`
8
+ implementation =
9
+ if $?.success?
10
+ Search::Ag
11
+ else
12
+ Search::Git
13
+ end
14
+
15
+ implementation.new(shell)
16
+ end
17
+ end
18
+ end
@@ -11,7 +11,7 @@ module TestLauncher
11
11
  attr_accessor :log_path, :queue
12
12
  private :log_path, :queue
13
13
 
14
- def initialize(log_path:)
14
+ def initialize(log_path: "/dev/null")
15
15
  @log_path = log_path
16
16
  %x{echo "" > #{log_path}}
17
17
  end
@@ -25,6 +25,7 @@ module TestLauncher
25
25
 
26
26
  def exec(cmd)
27
27
  notify cmd
28
+ $stdout.flush
28
29
  Bundler.clean_exec(cmd)
29
30
  end
30
31
 
@@ -1,3 +1,3 @@
1
1
  module TestLauncher
2
- VERSION = "1.5.0"
2
+ VERSION = "1.5.1"
3
3
  end
data/lib/test_launcher.rb CHANGED
@@ -1,27 +1 @@
1
1
  require "test_launcher/version"
2
-
3
- require "test_launcher/base_error"
4
- require "test_launcher/shell/runner"
5
- require "test_launcher/search/git"
6
- require "test_launcher/frameworks"
7
-
8
- module TestLauncher
9
- def self.launch(request)
10
- shell = Shell::Runner.new(log_path: "/tmp/test_launcher.log")
11
- searcher = Search::Git.new(shell)
12
-
13
- command = Frameworks.locate(
14
- request: request,
15
- shell: shell,
16
- searcher: searcher
17
- )
18
-
19
- if command
20
- shell.exec command
21
- else
22
- shell.warn "No tests found."
23
- end
24
- rescue BaseError => e
25
- shell.warn(e)
26
- end
27
- end
data/test/test_helper.rb CHANGED
@@ -4,6 +4,7 @@ require "mocha/mini_test"
4
4
  require "pry"
5
5
 
6
6
  require "test_launcher"
7
+ require "test_launcher/shell/runner"
7
8
 
8
9
  class TestLauncher::Shell::Runner
9
10
  def exec(cmd)
@@ -48,7 +49,7 @@ class TestCase < Minitest::Test
48
49
  end
49
50
 
50
51
  def recall(method)
51
- instance_variable_get(:"@#{method}")
52
+ instance_variable_get(:"@#{method}") || []
52
53
  end
53
54
  end
54
55
 
@@ -0,0 +1,40 @@
1
+ require "test_launcher/search/git"
2
+ require "test_launcher/shell/runner"
3
+
4
+ require "test_launcher/cli"
5
+ require "test_helpers/mocks"
6
+
7
+ module TestLauncher
8
+ module IntegrationHelper
9
+ include DefaultMocks
10
+
11
+ class IntegrationShell < Shell::Runner
12
+ def exec(string)
13
+ raise "Cannot exec twice!" if defined?(@exec)
14
+ @exec = string
15
+ end
16
+
17
+ def recall_exec
18
+ @exec
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def system_path(relative_dir)
25
+ File.join(Dir.pwd, relative_dir)
26
+ end
27
+
28
+ def launch(search_string, run_all: false, framework:, name: nil)
29
+ argv = [search_string, "--framework", framework]
30
+ argv << "--all" if run_all
31
+ argv.concat(["--name", name]) if name
32
+ env = {}
33
+ CLI.launch(argv, env, shell: shell_mock)
34
+ end
35
+
36
+ def shell_mock
37
+ @shell_mock ||= IntegrationShell.new
38
+ end
39
+ end
40
+ end