test-loop 3.0.2 → 4.0.0

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 (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: