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.
- data/README.md +53 -22
- data/bin/test-loop +79 -13
- 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
|
12
|
-
|
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
|
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)
|
25
|
-
exit status and (2) is loaded by your
|
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
|
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
|
-
* `@
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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,
|
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
|
-
@
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
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
|
-
|
85
|
-
|
86
|
-
|
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
|
-
-
|
6
|
+
- 4
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version:
|
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-
|
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:
|