test-loop 3.0.2 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.md +53 -22
  2. data/bin/test-loop +79 -13
  3. metadata +20 -6
data/README.md CHANGED
@@ -6,28 +6,33 @@ and tests changes in your Ruby application in an efficient manner, whereby it:
6
6
 
7
7
  1. Absorbs the test execution overhead into the main Ruby process.
8
8
  2. Forks to evaluate your test files directly and without overhead.
9
+ 3. Tries to run only the test blocks that changed in your test files.
9
10
 
10
11
  It relies on file modification times to determine what parts of your Ruby
11
- application have changed and then uses a simple lambda mapping function to
12
- determine which test files in your test suite correspond to those changes.
12
+ application have changed, applies a lambda mapping function to determine which
13
+ test files in your test suite correspond to those changes, and uses diffing to
14
+ find and run only those test blocks that have changed inside your test files.
13
15
 
14
16
 
15
17
  Features
16
18
  --------
17
19
 
18
- * Tests *changes* in your Ruby application; does not run all tests every time.
20
+ * Tests *changes* in your Ruby application: ignores unmodified test files
21
+ as well as unmodified test blocks inside modified test files.
19
22
 
20
23
  * Reabsorbs test execution overhead if the test or spec helper file changes.
21
24
 
25
+ * Evaluates test files in parallel, making full use of multiple processors.
26
+
22
27
  * Mostly I/O bound, so you can have it always running without CPU slowdowns.
23
28
 
24
- * Supports any testing framework that (1) reflects failures in the process'
25
- exit status and (2) is loaded by your application's `test/test_helper.rb`
26
- or `spec/spec_helper.rb` file.
29
+ * Supports Test::Unit, RSpec, and any other testing framework that (1)
30
+ reflects failures in the process' exit status and (2) is loaded by your
31
+ application's `test/test_helper.rb` or `spec/spec_helper.rb` file.
27
32
 
28
33
  * Configurable through a `.test-loop` file in your current working directory.
29
34
 
30
- * Implemented in less than 60 (SLOC) lines of code! :-)
35
+ * Implemented in less than 100 (SLOC) lines of code! :-)
31
36
 
32
37
 
33
38
  Installation
@@ -98,25 +103,51 @@ define the following instance variables:
98
103
  end
99
104
  }
100
105
 
101
- * `@after_test_execution` is a lambda function that is executed after tests
102
- are run. It is passed three things: the status of the test execution
103
- subprocess, the time when the tests were run, and the list of test files
104
- that were run.
105
-
106
- For example, to get on-screen-display notifications through libnotify,
107
- add the following to your `.test-loop` file:
108
-
109
- @after_test_execution = lambda do |status, ran_at, files|
110
- if status.success?
111
- result, icon = 'PASS', 'apple-green'
112
- else
113
- result, icon = 'FAIL', 'apple-red'
106
+ * `@test_name_parser` is a lambda function that is passed a line of source
107
+ code to determine whether that line can be considered as a test definition,
108
+ in which case it must return the name of the test being defined.
109
+
110
+ * `@before_each_test` is a lambda function that is executed inside the worker
111
+ process before loading the test file. It is passed the path to the test
112
+ file and the names of tests (identified by `@test_name_parser`) inside the
113
+ test file that have changed since the last time the test file was run.
114
+
115
+ These test names should be passed down to your chosen testing library,
116
+ instructing it to skip all other tests except those passed down to it. This
117
+ accelerates your test-driven development cycle and improves productivity!
118
+
119
+ * `@after_all_tests` is a lambda function that is executed inside the master
120
+ process after all tests have finished running. It is passed four things:
121
+ whether all tests had passed, the time when test execution began, a list of
122
+ test files, and the exit statuses of the worker processes that evaluated
123
+ those test files.
124
+
125
+ For example, to display a summary of the test execution results as an OSD
126
+ notification via libnotify, add the following to your `.test-loop` file:
127
+
128
+ @after_all_tests = lambda do |success, ran_at, files, statuses|
129
+ icon = success ? 'apple-green' : 'apple-red'
130
+ title = "#{success ? 'PASS' : 'FAIL'} at #{ran_at}"
131
+ details = files.zip(statuses).map do |file, status|
132
+ "#{status.success? ? '✔' : '✘'} #{file}"
114
133
  end
115
- system 'notify-send', '-i', icon, "#{result} at #{ran_at}", files.join("\n")
134
+ system 'notify-send', '-i', icon, title, details.join("\n")
116
135
  end
117
136
 
137
+ Also add the following at the very top if you use Ruby 1.9.x:
138
+
139
+ # encoding: utf-8
140
+
141
+ That will prevent the following errors from occurring:
142
+
143
+ invalid multibyte char (US-ASCII)
144
+
145
+ syntax error, unexpected $end, expecting ':'
146
+ "#{status.success? ? '✔' : '✘'} #{file}"
147
+ ^>
148
+
118
149
 
119
150
  License
120
151
  -------
121
152
 
122
- See the `bin/test-loop` file.
153
+ Released under the ISC license. See the `bin/test-loop` file for details.
data/bin/test-loop CHANGED
@@ -24,9 +24,12 @@
24
24
 
25
25
  process_invocation_vector = [$0, *ARGV].map! {|s| s.dup }
26
26
 
27
+ require 'diff'
28
+
27
29
  begin
28
30
  notify = lambda {|message| puts "test-loop: #{message}" }
29
31
 
32
+ # load user's configuration and supply default values
30
33
  notify.call 'Loading configuration...'
31
34
  config_file = File.join(Dir.pwd, '.test-loop')
32
35
  load config_file if File.exist? config_file
@@ -34,8 +37,7 @@ begin
34
37
  (@overhead_file_globs ||= []).
35
38
  push('{test,spec}/*{test,spec}_helper.rb').uniq!
36
39
 
37
- (@reabsorb_file_globs ||= []).
38
- concat(@overhead_file_globs).
40
+ (@reabsorb_file_globs ||= []).concat(@overhead_file_globs).
39
41
  push(config_file, 'config/*.{rb,yml}', 'Gemfile').uniq!
40
42
 
41
43
  (@source_file_to_test_file_mapping ||= {}).merge!(
@@ -50,7 +52,30 @@ begin
50
52
  '{test,spec}/**/*_{test,spec}.rb' => lambda {|path| path }
51
53
  )
52
54
 
53
- @after_test_execution ||= lambda {|status, ran_at, files|}
55
+ @test_file_cache = {} # path => readlines
56
+
57
+ @test_name_parser ||= lambda do |line|
58
+ case line
59
+ when /^\s*def test_(\w+)/ then $1
60
+ when /^\s*(test|context|should|describe|it)\b (['"])(.*?)\2/ then $3
61
+ end
62
+ end
63
+
64
+ @before_each_test ||= lambda do |test_file, test_names|
65
+ unless test_names.empty?
66
+ case File.basename(test_file)
67
+
68
+ when /(\b|_)test(\b|_)/ # Minitest
69
+ test_methods = test_names.map {|name| name.strip.gsub(/\W+/, '_') }
70
+ ARGV.push '-n', "/#{test_methods.join('|')}/"
71
+
72
+ when /(\b|_)spec(\b|_)/ # RSpec
73
+ ARGV.push '-e', test_names.map {|name| Regexp.quote(name) }.join('|')
74
+ end
75
+ end
76
+ end
77
+
78
+ @after_all_tests ||= lambda {|success, ran_at, files, statuses|}
54
79
 
55
80
  # absorb test execution overhead into master process
56
81
  $LOAD_PATH.unshift 'lib' # for non-Rails applications
@@ -65,25 +90,66 @@ begin
65
90
  epoch_time = Time.at(0)
66
91
  started_at = last_ran_at = Time.now
67
92
  trap(:QUIT) { started_at = epoch_time }
68
- trap(:TSTP) { last_ran_at = epoch_time }
93
+ trap(:TSTP) { last_ran_at = epoch_time; @test_file_cache.clear }
69
94
 
70
95
  notify.call 'Ready for testing!'
71
96
  loop do
72
97
  # figure out what test files need to be run
73
98
  test_files = @source_file_to_test_file_mapping.
74
- map do |source_file_glob, test_file_glob_mapper|
75
- Dir[source_file_glob].
76
- select {|file| File.mtime(file) > last_ran_at }.
77
- map {|path| Dir[test_file_glob_mapper.call(path)] }
78
- end.flatten.uniq
99
+ map do |source_file_glob, test_file_glob_mapper|
100
+ Dir[source_file_glob].
101
+ select {|file| File.mtime(file) > last_ran_at }.
102
+ map {|path| Dir[test_file_glob_mapper.call(path)] }
103
+ end.flatten.uniq
79
104
 
80
- # fork worker process to run the test files
105
+ # fork worker processes to run the test files in parallel
81
106
  unless test_files.empty?
82
107
  notify.call 'Running tests...'
83
108
  last_ran_at = Time.now
84
- fork { test_files.each {|file| notify.call file; load file } }
85
- Process.wait
86
- @after_test_execution.call($?, last_ran_at, test_files)
109
+
110
+ test_files.each do |test_file|
111
+ notify.call test_file
112
+
113
+ # cache the contents of the test file for diffing below
114
+ new_lines = File.readlines(test_file)
115
+ old_lines = @test_file_cache[test_file] || new_lines
116
+ @test_file_cache[test_file] = new_lines
117
+
118
+ fork do
119
+ # determine which test blocks have changed inside the test file
120
+ test_names = Diff.new(old_lines, new_lines).diffs.map do |diff|
121
+ catch :found do
122
+ # search backwards from the line that changed up to
123
+ # the first line in the file for test definitions
124
+ diff[0][1].downto(0) do |i| # [[+/-, line number, line value]]
125
+ if test_name = @test_name_parser.call(new_lines[i])
126
+ throw :found, test_name
127
+ end
128
+ end; nil # prevent unsuccessful search from returning an integer
129
+ end
130
+ end.compact.uniq
131
+
132
+ @before_each_test.call test_file, test_names
133
+
134
+ load test_file
135
+
136
+ # at this point, the at_exit() hook of the testing framework used by
137
+ # the user's test suite will take care of running all tests defined
138
+ # by the test file loaded above and will also reflect any failures
139
+ # in the worker's exit status
140
+ end
141
+ end
142
+
143
+ # wait for worker processes to finish and report results
144
+ statuses = Process.waitall.map {|pid, status| status }
145
+
146
+ success = true
147
+ test_files.zip(statuses).map do |file, status|
148
+ success &&= test_passed = status.success?
149
+ notify.call "#{test_passed ? 'PASS' : 'FAIL'} #{file}"
150
+ end
151
+
152
+ @after_all_tests.call(success, last_ran_at, test_files, statuses)
87
153
  end
88
154
 
89
155
  # reabsorb test execution overhead as necessary
metadata CHANGED
@@ -3,10 +3,10 @@ name: test-loop
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
- - 3
6
+ - 4
7
7
  - 0
8
- - 2
9
- version: 3.0.2
8
+ - 0
9
+ version: 4.0.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Suraj N. Kurapati
@@ -14,10 +14,24 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-01-11 00:00:00 -08:00
17
+ date: 2011-01-13 00:00:00 -08:00
18
18
  default_executable:
19
- dependencies: []
20
-
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: diff
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 3
31
+ - 6
32
+ version: 0.3.6
33
+ type: :runtime
34
+ version_requirements: *id001
21
35
  description:
22
36
  email:
23
37
  executables: