nestor 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +7 -1
- data/Rakefile +19 -15
- data/VERSION +1 -1
- data/doc/state-diagram.graffle +1788 -1776
- data/doc/state-diagram.png +0 -0
- data/lib/nestor/cli.rb +61 -25
- data/lib/nestor/machine.rb +57 -30
- data/lib/nestor/mappers/rails/test/rails_test_unit.rb +58 -0
- data/lib/nestor/mappers/rails/test/unit.rb +259 -0
- data/lib/nestor/mappers.rb +26 -0
- data/lib/nestor/script.rb +9 -0
- data/lib/nestor.rb +1 -0
- data/test/machine_test.rb +20 -0
- data/test/rails_mapper_test.rb +88 -0
- data/test/riot_macros/map.rb +13 -0
- data/test/script_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +20 -17
- data/lib/nestor/strategies/test/unit.rb +0 -127
- data/lib/nestor/strategies.rb +0 -18
- data/lib/nestor/watchers/rails.rb +0 -56
- data/lib/nestor/watchers/rails_script.rb +0 -89
- data/lib/nestor/watchers.rb +0 -1
- data/spec/machine_spec.rb +0 -9
- data/spec/spec_helper.rb +0 -9
- data/spec/test_unit_strategy_spec.rb +0 -41
data/doc/state-diagram.png
CHANGED
Binary file
|
data/lib/nestor/cli.rb
CHANGED
@@ -3,28 +3,21 @@ require "thor"
|
|
3
3
|
|
4
4
|
module Nestor
|
5
5
|
class Cli < Thor # :nodoc:
|
6
|
+
default_task :start
|
7
|
+
|
6
8
|
desc("start", <<-EODESC.gsub(/^\s{6}/, ""))
|
7
9
|
Starts a continuous test server.
|
8
|
-
EODESC
|
9
|
-
method_options :strategy => "test/unit", :watcher => "rails", :script => nil, :debug => false, :include => []
|
10
|
-
def start
|
11
|
-
puts "Using #{options[:strategy].inspect} strategy"
|
12
|
-
begin
|
13
|
-
# Try the internal version
|
14
|
-
require "nestor/strategies/#{options[:strategy]}"
|
15
|
-
rescue LoadError
|
16
|
-
# Else fallback to something I'm not aware of right now
|
17
|
-
require options[:strategy]
|
18
|
-
end
|
19
10
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
# Fallback to something external again
|
25
|
-
require options[:watcher]
|
26
|
-
end
|
11
|
+
Specify the framework and test library using --framework and --testlib.
|
12
|
+
Valid options are:
|
13
|
+
--framework: rails
|
14
|
+
--testlib: test/unit
|
27
15
|
|
16
|
+
Use --quick to boot without running the full test suite on startup.
|
17
|
+
--debug writes extra Watchr debug messages to STDOUT.
|
18
|
+
EODESC
|
19
|
+
method_options :framework => "rails", :testlib => "test/unit", :script => nil, :debug => false, :quick => false, :require => []
|
20
|
+
def start
|
28
21
|
Watchr.options.debug = options[:debug]
|
29
22
|
|
30
23
|
if options[:script] then
|
@@ -32,21 +25,64 @@ module Nestor
|
|
32
25
|
else
|
33
26
|
puts "Launching..."
|
34
27
|
end
|
35
|
-
|
28
|
+
|
29
|
+
puts "Using #{options[:framework].inspect} framework with #{options[:testlib].inspect} as the testing library"
|
30
|
+
mapper = mapper_instance(options[:framework], options[:testlib])
|
31
|
+
machine = Nestor::Machine.new(mapper, :quick => options[:quick])
|
32
|
+
|
33
|
+
script_path = options[:script] ? Pathname.new(options[:script]) : nil
|
34
|
+
script = Nestor::Script.new(script_path || mapper.class.default_script_path)
|
35
|
+
|
36
|
+
options[:require].each do |path|
|
37
|
+
puts "Loading #{path.inspect} plugin"
|
38
|
+
require path
|
39
|
+
end
|
40
|
+
|
41
|
+
script.nestor_machine = machine
|
42
|
+
Watchr::Controller.new(script, Watchr.handler.new).run
|
36
43
|
end
|
37
44
|
|
38
45
|
desc("customize PATH", <<-EODESC.gsub(/^\s{6}/, ""))
|
39
46
|
Copies the named script file to PATH to allow customizing.
|
47
|
+
Will not overwrite existing files, unless --force is specified.
|
40
48
|
EODESC
|
41
|
-
method_options :
|
49
|
+
method_options :framework => "rails", :testlib => "test/unit", :force => false
|
42
50
|
def customize(path)
|
43
|
-
|
44
|
-
require "nestor/watchers/#{options[:watcher]}"
|
51
|
+
raise "Destination #{path.inspect} already exists: will not overwrite" if !options[:force] && File.file?(path)
|
45
52
|
|
46
|
-
|
47
|
-
|
53
|
+
puts "Using #{options[:framework].inspect} framework with #{options[:testlib].inspect} as the testing library"
|
54
|
+
klass = mapper_class(options[:framework], options[:testlib])
|
55
|
+
FileUtils.cp(klass.default_script_path, path)
|
48
56
|
|
49
|
-
puts "Wrote #{
|
57
|
+
puts "Wrote #{klass.name} script to #{path.inspect}"
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def require_path(*paths)
|
63
|
+
require "nestor/mappers/#{paths.join("/")}"
|
64
|
+
rescue LoadError
|
65
|
+
require paths.join("/")
|
66
|
+
end
|
67
|
+
|
68
|
+
def mapper_class(framework, testlib)
|
69
|
+
require_path(options[:framework], options[:testlib])
|
70
|
+
[framework.split("/"), testlib.split("/")].flatten.inject(Nestor::Mappers) do |root, component|
|
71
|
+
root.const_get(camelize(component))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def mapper_instance(framework, testlib)
|
76
|
+
mapper_class(framework, testlib).new
|
77
|
+
end
|
78
|
+
|
79
|
+
# Copied from ActiveSupport 2.3.4
|
80
|
+
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
81
|
+
if first_letter_in_uppercase
|
82
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
83
|
+
else
|
84
|
+
lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
|
85
|
+
end
|
50
86
|
end
|
51
87
|
end
|
52
88
|
end
|
data/lib/nestor/machine.rb
CHANGED
@@ -5,7 +5,7 @@ module Nestor
|
|
5
5
|
#
|
6
6
|
# == Usage
|
7
7
|
#
|
8
|
-
# In the Watchr script, use +@
|
8
|
+
# In the Watchr script, use +@mapper+ to access an instance of this class.
|
9
9
|
#
|
10
10
|
# The available events you may call are:
|
11
11
|
#
|
@@ -13,12 +13,12 @@ module Nestor
|
|
13
13
|
# call +ready+ to indicate you are ready to process events. The default
|
14
14
|
# rails template calls #ready when +test/test_helper.rb+ is loaded.
|
15
15
|
#
|
16
|
-
# <tt>changed!</tt>:: Tells the Machine a file changed. The Watchr script and
|
16
|
+
# <tt>changed!</tt>:: Tells the Machine a file changed. The Watchr script and mapper are
|
17
17
|
# responsible for assigning meaning to the file. The default Watchr
|
18
18
|
# script knows how to map model, controller and view files to given
|
19
19
|
# tests, and the script thus only tells the Machine about test files.
|
20
20
|
# Nothing prevents another implementation from providing the actual
|
21
|
-
# implementation files and letting the
|
21
|
+
# implementation files and letting the mapper decide later what to
|
22
22
|
# do about those.
|
23
23
|
#
|
24
24
|
# <tt>run_successful!</tt>:: Tells the Machine that the last build was successful. This
|
@@ -29,14 +29,14 @@ module Nestor
|
|
29
29
|
# Again, this doesn't mean the whole build failed: only the last couple
|
30
30
|
# of files had something that caused a failure.
|
31
31
|
#
|
32
|
-
# <tt>run!</tt>:: Tells the machine to tell the +#
|
32
|
+
# <tt>run!</tt>:: Tells the machine to tell the +#mapper+ to run the tests, given the current
|
33
33
|
# state of affairs. This might be running all tests, or a subset if the Machine
|
34
34
|
# is currently focusing on some items. A separate event is required by the
|
35
35
|
# Machine to allow coalescing multiple change events together.
|
36
36
|
#
|
37
37
|
class Machine
|
38
38
|
# The Machine actually delegates running the tests to another object, and this is it's reference.
|
39
|
-
attr_reader :
|
39
|
+
attr_reader :mapper # :nodoc:
|
40
40
|
|
41
41
|
# The list of files we are focusing on, as received by #changed!
|
42
42
|
attr_reader :focused_files # :nodoc:
|
@@ -47,19 +47,24 @@ module Nestor
|
|
47
47
|
# The list of failing tests or examples being focused on right now
|
48
48
|
attr_reader :focuses # :nodoc:
|
49
49
|
|
50
|
-
#
|
51
|
-
|
50
|
+
# The options as passed to #new.
|
51
|
+
attr_reader :options # :nodoc:
|
52
|
+
|
53
|
+
# +mapper+ is required, and must implement a couple of methods. See {Nestor::Mappers} for the required calls.
|
54
|
+
def initialize(mapper, options={})
|
55
|
+
@options = options
|
52
56
|
super() # Have to specify no-args, or else it'll raise an ArgumentError
|
53
57
|
|
54
|
-
@
|
58
|
+
@mapper = mapper
|
55
59
|
@focused_files, @focuses = [], []
|
56
60
|
|
57
61
|
log_state_change
|
58
62
|
end
|
59
63
|
|
60
|
-
state_machine :initial => :booting do
|
64
|
+
state_machine :initial => lambda {|machine| machine.options[:quick] ? :green : :booting} do
|
61
65
|
event :ready do
|
62
|
-
transition
|
66
|
+
transition :booting => :running_all
|
67
|
+
transition :green => same # Quick boot: skips initial test run
|
63
68
|
end
|
64
69
|
|
65
70
|
event :failed do
|
@@ -73,6 +78,7 @@ module Nestor
|
|
73
78
|
end
|
74
79
|
|
75
80
|
event :file_changed do
|
81
|
+
transition [:running_all, :running_multi, :running_focused] => same
|
76
82
|
transition [:run_focused, :run_focused_pending] => :run_focused_pending, :if => :changed_file_in_focused_files?
|
77
83
|
transition [:run_focused_pending, :run_multi_pending, :run_focused] => :run_multi_pending
|
78
84
|
transition :green => :run_multi_pending
|
@@ -84,6 +90,10 @@ module Nestor
|
|
84
90
|
transition :run_multi_pending => :running_multi
|
85
91
|
end
|
86
92
|
|
93
|
+
event :unfocus do
|
94
|
+
transition any => :running_all
|
95
|
+
end
|
96
|
+
|
87
97
|
after_transition any => any, :do => :log_state_change
|
88
98
|
after_transition :to => :running_all, :do => :run_all_tests
|
89
99
|
after_transition :to => :running_focused, :do => :run_focused_tests
|
@@ -94,23 +104,31 @@ module Nestor
|
|
94
104
|
after_transition :on => :file_changed, :do => :add_changed_file_to_focused_files
|
95
105
|
end
|
96
106
|
|
97
|
-
#
|
98
|
-
|
99
|
-
|
100
|
-
successful!
|
101
|
-
end
|
102
|
-
|
103
|
-
# Indicates there were one or more failures. +files+ lists the actual files
|
104
|
-
# that failed, while +tests+ indicates the test names or examples that failed.
|
105
|
-
def run_failed!(files, tests)
|
106
|
-
@focused_files, @focuses = files, tests
|
107
|
-
failed!
|
107
|
+
# Delegate to +@mapper+.
|
108
|
+
def log(*args)
|
109
|
+
@mapper.log(*args)
|
108
110
|
end
|
109
111
|
|
110
112
|
# Notifies the Machine that a file changed. This might trigger a state change and schedule a build.
|
111
113
|
def changed!(file)
|
114
|
+
mapped_files = mapper.map(file)
|
115
|
+
case mapped_files
|
116
|
+
when [] # Run all tests
|
117
|
+
unfocus!
|
118
|
+
when nil # Reset
|
119
|
+
reset!
|
120
|
+
else
|
121
|
+
mapped_files.each do |mapped_file|
|
122
|
+
mapper.log "#{file} => #{mapped_file}"
|
123
|
+
self.changed_file = mapped_file
|
124
|
+
file_changed!
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def changed_file=(file)
|
130
|
+
raise ArgumentError, "Only accepts a single file at a time, got #{file.inspect}" unless String === file
|
112
131
|
@changed_file = file
|
113
|
-
file_changed!
|
114
132
|
end
|
115
133
|
|
116
134
|
private
|
@@ -118,16 +136,25 @@ module Nestor
|
|
118
136
|
def run_all_tests
|
119
137
|
reset_focused_files
|
120
138
|
reset_focuses
|
121
|
-
@
|
139
|
+
process!(@mapper.run_all)
|
122
140
|
end
|
123
141
|
|
124
142
|
def run_multi_tests
|
125
143
|
reset_focuses
|
126
|
-
@
|
144
|
+
process!(@mapper.run(focused_files))
|
127
145
|
end
|
128
146
|
|
129
147
|
def run_focused_tests
|
130
|
-
@
|
148
|
+
process!(@mapper.run(focused_files, focuses))
|
149
|
+
end
|
150
|
+
|
151
|
+
def process!(info)
|
152
|
+
@mapper.log(info.inspect)
|
153
|
+
return successful! if info[:passed]
|
154
|
+
|
155
|
+
files, tests = info[:failures].values.uniq, info[:failures].keys
|
156
|
+
@focused_files, @focuses = files, tests
|
157
|
+
failed!
|
131
158
|
end
|
132
159
|
|
133
160
|
def reset_focused_files
|
@@ -135,11 +162,11 @@ module Nestor
|
|
135
162
|
end
|
136
163
|
|
137
164
|
def add_changed_file_to_focused_files
|
138
|
-
@focused_files <<
|
165
|
+
@focused_files << changed_file unless @focused_files.include?(changed_file)
|
139
166
|
end
|
140
167
|
|
141
168
|
def changed_file_in_focused_files?
|
142
|
-
@
|
169
|
+
@mapper.log("changed_file #{changed_file}, in focused_files? #{focused_files.inspect}")
|
143
170
|
focused_files.include?(changed_file)
|
144
171
|
end
|
145
172
|
|
@@ -148,15 +175,15 @@ module Nestor
|
|
148
175
|
end
|
149
176
|
|
150
177
|
def log_state_change
|
151
|
-
@
|
178
|
+
@mapper.log("Machine entering state: #{state.inspect}")
|
152
179
|
end
|
153
180
|
|
154
181
|
def log_focus
|
155
|
-
@
|
182
|
+
@mapper.log("Focusing on #{focuses.inspect}")
|
156
183
|
end
|
157
184
|
|
158
185
|
def log_pending_run
|
159
|
-
@
|
186
|
+
@mapper.log("Run pending... Waiting for go ahead")
|
160
187
|
end
|
161
188
|
end
|
162
189
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
def log(message) #:nodoc:
|
2
|
+
@machine.log(message)
|
3
|
+
end
|
4
|
+
|
5
|
+
RAILS_ENV = "test" unless defined?(RAILS_ENV)
|
6
|
+
log "Entering #{RAILS_ENV.inspect} environment"
|
7
|
+
|
8
|
+
log "Creating tmp/ if it doesn't exist"
|
9
|
+
Dir.mkdir("tmp") unless File.directory?("tmp")
|
10
|
+
|
11
|
+
log "Preloading test/test_helper.rb"
|
12
|
+
start_load_at = Time.now
|
13
|
+
$LOAD_PATH.unshift "test" unless $LOAD_PATH.include?("test")
|
14
|
+
require "test_helper"
|
15
|
+
|
16
|
+
end_load_at = Time.now
|
17
|
+
log "Waiting for changes (saving #{end_load_at - start_load_at} seconds per run)..."
|
18
|
+
|
19
|
+
def sendoff(timeout=0.8, path="tmp/nestor-sendoff") #:nodoc:
|
20
|
+
Thread.start(timeout, path) do |timeout, path|
|
21
|
+
log "Sendoff pending in #{timeout}s..."
|
22
|
+
sleep timeout
|
23
|
+
File.open(path, "w") {|io| io.write(rand.to_s)}
|
24
|
+
log "Sendoff fired on #{path}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def changed!(filename) #:nodoc:
|
29
|
+
return if File.directory?(filename)
|
30
|
+
@machine.changed! filename
|
31
|
+
sendoff
|
32
|
+
end
|
33
|
+
|
34
|
+
watch 'config/(?:.+)\.(?:rb|ya?ml)' do |md|
|
35
|
+
changed! md[0]
|
36
|
+
end
|
37
|
+
|
38
|
+
watch '(?:app|test)/.+\.rb' do |md|
|
39
|
+
changed! md[0]
|
40
|
+
end
|
41
|
+
|
42
|
+
watch 'app/views/.+' do |md|
|
43
|
+
changed! md[0]
|
44
|
+
end
|
45
|
+
|
46
|
+
watch 'db/schema.rb' do |md|
|
47
|
+
log "Detected changed schema: preparing test DB"
|
48
|
+
system("rake db:test:prepare")
|
49
|
+
changed! md[0]
|
50
|
+
end
|
51
|
+
|
52
|
+
# This is only to trigger the tests after a slight delay, but from the main thread.
|
53
|
+
watch 'tmp/nestor-sendoff' do |_|
|
54
|
+
log "Sendoff"
|
55
|
+
@machine.run!
|
56
|
+
end
|
57
|
+
|
58
|
+
@machine.ready!
|
@@ -0,0 +1,259 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "pathname"
|
3
|
+
require "test/unit/ui/console/testrunner"
|
4
|
+
|
5
|
+
# Just declare the module to prevent deeply nested code below
|
6
|
+
module Nestor
|
7
|
+
module Mappers
|
8
|
+
module Rails
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Nestor::Mappers::Rails
|
14
|
+
module Test
|
15
|
+
class Unit
|
16
|
+
# Returns the path to the script this {Mapper} uses.
|
17
|
+
def self.default_script_path
|
18
|
+
Pathname.new(File.dirname(__FILE__) + "/rails_test_unit.rb")
|
19
|
+
end
|
20
|
+
|
21
|
+
# Utility method to extract data from a Test::Unit failure.
|
22
|
+
#
|
23
|
+
# @param failure [Test::Unit::Failure, Test::Unit::Error] The Test::Unit failure or error from which to extract information.
|
24
|
+
# @param test_files [Array<String>] The list of files that might have generated this failure. This is used to detect the file that caused the failure.
|
25
|
+
#
|
26
|
+
# @return [String, String] Returns the filename and test name as a 2 element Array.
|
27
|
+
def self.parse_failure(failure, test_files)
|
28
|
+
filename = if failure.respond_to?(:location) then
|
29
|
+
failure.location.map do |loc|
|
30
|
+
filename = loc.split(":", 2).first
|
31
|
+
test_files.detect {|tf| filename.include?(tf)}
|
32
|
+
end.compact.first
|
33
|
+
elsif failure.respond_to?(:exception) then
|
34
|
+
failure.exception.backtrace.map do |loc|
|
35
|
+
filename = loc.split(":", 2).first
|
36
|
+
loc = loc[1..-1] if loc[0,1] == "/"
|
37
|
+
test_files.detect {|tf| filename.include?(tf)}
|
38
|
+
end.compact.first
|
39
|
+
else
|
40
|
+
raise "Unknown object type received as failure: #{failure.inspect} doesn't have #exception or #location methods."
|
41
|
+
end
|
42
|
+
|
43
|
+
test_name = failure.test_name.split("(", 2).first.strip.sub(/\.$/, "")
|
44
|
+
|
45
|
+
[filename, test_name]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Logs a message to STDOUT. This implementation forks, so the #log method also
|
49
|
+
# provides the PID of the logger.
|
50
|
+
def log(message)
|
51
|
+
STDOUT.printf "[%d] %s - %s\n", Process.pid, Time.now.strftime("%H:%M:%S"), message
|
52
|
+
STDOUT.flush
|
53
|
+
end
|
54
|
+
|
55
|
+
# Runs absolutely all tests as found by walking test/.
|
56
|
+
def run_all
|
57
|
+
receive_results do
|
58
|
+
log "Run all tests"
|
59
|
+
test_files = load_test_files(["test"])
|
60
|
+
|
61
|
+
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
62
|
+
test_runner = ::Nestor::Mappers::Rails::Test::TestRunner.new(nil)
|
63
|
+
result = ::Test::Unit::AutoRunner.run(false, nil, []) do |autorunner|
|
64
|
+
autorunner.runner = lambda { test_runner }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns a Hash which the parent process will retrieve
|
68
|
+
report(test_runner, test_files)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Runs only the named files, and optionally focuses on only a couple of tests
|
73
|
+
# within the loaded test cases.
|
74
|
+
def run(test_files, focuses=[])
|
75
|
+
receive_results do
|
76
|
+
log "Running #{focuses.length} focused tests"
|
77
|
+
load_test_files(test_files)
|
78
|
+
|
79
|
+
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
80
|
+
test_runner = ::Nestor::Mappers::Rails::Test::TestRunner.new(nil)
|
81
|
+
result = ::Test::Unit::AutoRunner.run(false, nil, []) do |autorunner|
|
82
|
+
autorunner.runner = lambda { test_runner }
|
83
|
+
autorunner.filters << proc{|t| focuses.include?(t.method_name)} unless focuses.empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns a Hash the parent process will retrieve
|
87
|
+
report(test_runner, test_files)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Given a path, returns an Array of strings for the tests that should be run.
|
92
|
+
#
|
93
|
+
# This implementation maps +app/models+ to +test/unit+, +app/controllers+ and +app/views+ to +test/functional+.
|
94
|
+
# It is not the responsibility of #map to determine if the files actually exist on disk. The {Nestor::Machine}
|
95
|
+
# will verify the files exist before attempting to run them.
|
96
|
+
#
|
97
|
+
#
|
98
|
+
# @example
|
99
|
+
#
|
100
|
+
# mapper = Nestor::Mappers::Rails::Test::Unit.new
|
101
|
+
# mapper.map("app/models/user.rb")
|
102
|
+
# #=> ["test/unit/user_test.rb"]
|
103
|
+
# mapper.map("app/helpers/users_helper.rb")
|
104
|
+
# #=> ["test/unit/helpers/users_helper.rb", "test/functional/users_controller_test.rb"]
|
105
|
+
#
|
106
|
+
# # Mapper is saying to re-run all functional tests
|
107
|
+
# mapper.map("app/controller/application_controller.rb")
|
108
|
+
# #=> ["test/functional/"]
|
109
|
+
#
|
110
|
+
# # Mapper is saying to run all tests
|
111
|
+
# mapper.map("config/environment.rb")
|
112
|
+
# #=> []
|
113
|
+
#
|
114
|
+
# @param path [String] A relative path to a file in the project.
|
115
|
+
# @return [Array<String>, nil] One or more paths the {Nestor::Machine} should run in response to the change.
|
116
|
+
# It is entirely possible and appropriate that this method return an empty array, which implies to run
|
117
|
+
# all tests. If a path points to a directory, this implies running all tests under that directory.
|
118
|
+
# Returning +nil+ implies no tests need to run (such as when editing a README).
|
119
|
+
def map(path)
|
120
|
+
case path
|
121
|
+
when %r{^app/.+/(.+_observer)\.rb$} # Has to be first, or app/models might kick in first
|
122
|
+
orig, plain = $1, $1.sub("_observer", "")
|
123
|
+
["test/unit/#{orig}_test.rb", "test/unit/#{plain}_test.rb"]
|
124
|
+
|
125
|
+
when "app/controllers/application_controller.rb" # Again, special cases first
|
126
|
+
["test/functional/"]
|
127
|
+
|
128
|
+
when "app/helpers/application_helper.rb" # Again, special cases first
|
129
|
+
["test/unit/helpers/", "test/functional/"]
|
130
|
+
|
131
|
+
when %r{^app/models/(.+)\.rb$}, %r{^lib/(.+)\.rb$}
|
132
|
+
["test/unit/#{$1}_test.rb"]
|
133
|
+
|
134
|
+
when %r{^app/controllers/(.+)\.rb$}
|
135
|
+
["test/functional/#{$1}_test.rb"]
|
136
|
+
|
137
|
+
when %r{^app/views/(.+)/(.+)\.\w+$}
|
138
|
+
["test/functional/#{$1}_controller_test.rb"]
|
139
|
+
|
140
|
+
when %r{^app/helpers/(.+)_helper\.rb$}
|
141
|
+
["test/unit/helpers/#{$1}_helper_test.rb", "test/functional/#{$1}_controller_test.rb"]
|
142
|
+
|
143
|
+
when %r{^(?:test/test_helper.rb|config/.+\.(?:rb|ya?ml|xml)|db/schema\.rb)$}
|
144
|
+
# Rerun all tests because something fundamental changed
|
145
|
+
[]
|
146
|
+
|
147
|
+
when %r{^test/.+_test.rb}
|
148
|
+
# Rerun the sole test when it's a test
|
149
|
+
Array(path)
|
150
|
+
|
151
|
+
else
|
152
|
+
# I don't know how to map this, so it's probably a README or something
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def setup_lifeline
|
160
|
+
ppid = Process.ppid
|
161
|
+
log "Setting up lifeline on #{Process.pid} for #{Process.ppid}"
|
162
|
+
|
163
|
+
Thread.start do
|
164
|
+
sleep 0.5
|
165
|
+
next if ppid == Process.ppid
|
166
|
+
|
167
|
+
# Parent must have died because we don't have the same parent PID
|
168
|
+
# Die ourselves
|
169
|
+
log "Dying because parent changed"
|
170
|
+
exit!
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def receive_results
|
175
|
+
rd, wr = IO.pipe
|
176
|
+
fork do
|
177
|
+
log "Setting up lifeline"
|
178
|
+
setup_lifeline
|
179
|
+
|
180
|
+
log "Closing read-end of the pipe"
|
181
|
+
rd.close
|
182
|
+
|
183
|
+
log "Doing whatever..."
|
184
|
+
info = yield
|
185
|
+
|
186
|
+
log "Returning YAML info to parent process"
|
187
|
+
wr.write info.to_yaml
|
188
|
+
end
|
189
|
+
|
190
|
+
log "Closing write-end of the pipe"
|
191
|
+
wr.close
|
192
|
+
|
193
|
+
log "Waiting for child process"
|
194
|
+
Process.wait
|
195
|
+
|
196
|
+
info = YAML.load(rd.read)
|
197
|
+
end
|
198
|
+
|
199
|
+
def load_test_files(test_files)
|
200
|
+
test_files.inject([]) do |memo, f|
|
201
|
+
case
|
202
|
+
when File.directory?(f)
|
203
|
+
Dir["#{f}/**/*_test.rb"].each do |f1|
|
204
|
+
log(f1)
|
205
|
+
load f1
|
206
|
+
memo << f1
|
207
|
+
end
|
208
|
+
when File.file?(f)
|
209
|
+
log(f)
|
210
|
+
load f
|
211
|
+
memo << f
|
212
|
+
else
|
213
|
+
# Ignore
|
214
|
+
end
|
215
|
+
|
216
|
+
memo
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Print to STDOUT the results of the run. The parent's listening on the pipe to get the data.
|
221
|
+
def report(test_runner, test_files, io=STDOUT)
|
222
|
+
info = {:passed => test_runner.passed?, :failures => {}}
|
223
|
+
failures = info[:failures]
|
224
|
+
test_runner.faults.each do |failure|
|
225
|
+
filename, test_name = self.class.parse_failure(failure, test_files)
|
226
|
+
if filename.nil? then
|
227
|
+
log("Could not map #{failure.test_name.inspect} to a specific test file: mapping to #{test_files.length} files")
|
228
|
+
failures[test_name] = test_files
|
229
|
+
else
|
230
|
+
log("Failed #{failure.test_name.inspect} in #{filename.inspect}")
|
231
|
+
failures[test_name] = filename
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
info
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# A helper class that allows me to get more information from the build.
|
240
|
+
#
|
241
|
+
# This is something that definitely will change when Nestor is tested on Ruby 1.9.
|
242
|
+
class TestRunner < ::Test::Unit::UI::Console::TestRunner #:nodoc:
|
243
|
+
attr_reader :faults
|
244
|
+
|
245
|
+
# This is a duck-typing method. Test::Unit's design requiers a #run method,
|
246
|
+
# but it is implemented as a class method. I fake it here to allow me to
|
247
|
+
# pass an instance and have the actual TestRunner instance available afterwards.
|
248
|
+
def run(suite, output_level=NORMAL)
|
249
|
+
@suite = suite.respond_to?(:suite) ? suite.suite : suite
|
250
|
+
start
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns pass/fail status.
|
254
|
+
def passed?
|
255
|
+
@faults.empty?
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Nestor
|
2
|
+
# {Nestor::Cli} will require a file named +nestor/mappers/#{framework}/#{test_framework}+. If you want
|
3
|
+
# to provide custom mappers, it is possible to do so.
|
4
|
+
#
|
5
|
+
# Mappers are simple objects that implement the following protocol:
|
6
|
+
#
|
7
|
+
# <tt>default_script_path</tt>:: Class method to return the path to the default Watchr script.
|
8
|
+
# This *must* be a physical file on the filesystem, as Watchr
|
9
|
+
# cannot accept anything else at the moment.
|
10
|
+
#
|
11
|
+
# <tt>log(message)</tt>:: Logs a simple message, either to the console or a logfile.
|
12
|
+
# The {Nestor::Machine} will use the +log+ method to notify about it's
|
13
|
+
# state transitions.
|
14
|
+
#
|
15
|
+
# <tt>run_all</tt>:: Runs all the tests, no matter what. In the {Nestor::Mappers::Rails::Test::Unit}
|
16
|
+
# case, this means <tt>Dir["test/**/*_test.rb"]</tt>.
|
17
|
+
#
|
18
|
+
# <tt>run(tests_files, focused_cases=[])</tt>:: Runs only a subset of the tests, maybe
|
19
|
+
# focusing on only a couple of tests / examples.
|
20
|
+
#
|
21
|
+
# <tt>map(file)</tt>:: Given an implementation file, returns the corresponding test to run.
|
22
|
+
# {Nestor::Mappers::Rails::Test::Unit} maps +app/models/user.rb+ to
|
23
|
+
# +test/unit/user_test.rb+.
|
24
|
+
module Mappers
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Nestor
|
2
|
+
class Script < Watchr::Script
|
3
|
+
# Let's the script have a reference to the machine, to generate events.
|
4
|
+
# The actual instance variable name is +@machine+, not +@nestor_machine+.
|
5
|
+
def nestor_machine=(machine)
|
6
|
+
@machine = machine
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|