nestor 0.1.1 → 0.2.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.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
|