test_launcher 1.5.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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