testbot 0.5.6 → 0.5.7

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/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ 0.5.7
2
+
3
+ Added @meeiw's patch to support ERB in config. Added test result summarization for RSpec and Cucumber.
4
+
1
5
  0.5.6
2
6
 
3
7
  Removed CPU usage check before running jobs (issue #25).
@@ -32,8 +36,7 @@ Removed all remaining native dependencies to make testbot simpler to install.
32
36
 
33
37
  0.4.7
34
38
 
35
- - Refactored the code into modules with one directory for each.
36
- - No longer dependent on mongel, now using webrat.
39
+ Refactored the code into modules with one directory for each. No longer dependent on mongel, now using webrick.
37
40
 
38
41
  0.4.6
39
42
 
@@ -2,6 +2,8 @@ Testbot is a test distribution tool that works with Rails, RSpec, RSpec2, Test::
2
2
 
3
3
  Using testbot on 11 machines (25 cores) we got our test suite down to **2 minutes from 30**. [More examples of how testbot is used](http://github.com/joakimk/testbot/wiki/How-testbot-is-used).
4
4
 
5
+ If you intend to use testbot with cloud computing (like EC2), take a look at [TestbotCloud](https://github.com/joakimk/testbot_cloud).
6
+
5
7
  Installing
6
8
  ----
7
9
 
@@ -60,7 +62,9 @@ Running tests:
60
62
 
61
63
  Using testbot with Rails 2:
62
64
 
63
- ruby script/plugin install git://github.com/joakimk/testbot.git -r 'refs/tags/v0.5.6'
65
+ # Add testbot to your Gemfile if you use bundler. You also need the plugin because
66
+ # Rails 2 does not load raketasks from gems.
67
+ ruby script/plugin install git://github.com/joakimk/testbot.git -r 'refs/tags/v0.5.7'
64
68
  script/generate testbot --connect 192.168.0.100
65
69
 
66
70
  rake testbot:spec (or :rspec, :test, :features)
@@ -109,11 +113,14 @@ Contributing to testbot
109
113
  ----
110
114
 
111
115
  First, get the tests to run:
116
+
112
117
  bundle
113
118
  rake
114
119
 
115
- For development I recommend: [grosser/autotest](http://github.com/grosser/autotest)
116
- autotest -f -c
120
+ For development I recommend using guard.
121
+
122
+ # OSX needs: gem install rb-fsevent
123
+ guard
117
124
 
118
125
  Make your change (don't forget to write tests) and send me a pull request.
119
126
 
@@ -122,7 +129,7 @@ You can also contribute by adding to the [wiki](http://github.com/joakimk/testbo
122
129
  How to add support for more test frameworks and/or programming languages
123
130
  ----
124
131
 
125
- Add a **lib/adapters/framework_name_adapter.rb** file and update this readme.
132
+ Add a **lib/shared/adapters/framework_name_adapter.rb** file and update this readme.
126
133
 
127
134
  More
128
135
  ----
data/Rakefile CHANGED
@@ -11,7 +11,7 @@ end
11
11
 
12
12
  desc "Run Test::Unit tests"
13
13
  task :test do
14
- Dir["test/**/test_*.rb"].each { |test| require(File.expand_path(test)) }
14
+ Dir["test/**/*_test.rb"].each { |test| require(File.expand_path(test)) }
15
15
  end
16
16
 
17
17
 
@@ -1,3 +1,5 @@
1
+ # You can use ERB here.
2
+
1
3
  # Which server to use.
2
4
  server_host: <%= options[:connect] %>
3
5
  <%- if options[:user] -%>
@@ -2,6 +2,7 @@ require 'rubygems'
2
2
  require 'httparty'
3
3
  require 'macaddr'
4
4
  require 'ostruct'
5
+ require 'erb'
5
6
  require File.dirname(__FILE__) + '/../shared/ssh_tunnel'
6
7
  require File.expand_path(File.dirname(__FILE__) + '/../shared/testbot')
7
8
 
@@ -90,16 +91,15 @@ module Testbot::Requester
90
91
 
91
92
  puts if config.simple_output
92
93
 
94
+ if adapter.respond_to?(:sum_results)
95
+ puts "\n" + adapter.sum_results(@build['results'])
96
+ end
97
+
93
98
  @build["success"]
94
99
  end
95
100
 
96
101
  def self.create_by_config(path)
97
- config = YAML.load_file(path)
98
- Requester.new(config)
99
- end
100
-
101
- def result_lines
102
- @build['results'].split("\n").find_all { |line| line_is_result?(line) }.map { |line| line.chomp }
102
+ Requester.new(YAML.load(ERB.new(File.open(path).read).result))
103
103
  end
104
104
 
105
105
  private
@@ -120,10 +120,6 @@ module Testbot::Requester
120
120
  [ '0.0.0.0', 'localhost', '127.0.0.1' ].include?(config.server_host)
121
121
  end
122
122
 
123
- def line_is_result?(line)
124
- line =~ /\d+ fail/
125
- end
126
-
127
123
  def jruby?
128
124
  RUBY_PLATFORM =~ /java/ || !!ENV['USE_JRUBY']
129
125
  end
@@ -1,8 +1,6 @@
1
1
  module Testbot::Server
2
2
 
3
3
  class Job < MemoryModel
4
-
5
- #attribute :success, :boolean
6
4
 
7
5
  def update(hash)
8
6
  super(hash)
@@ -42,6 +40,7 @@ module Testbot::Server
42
40
  Job.all.find_all { |job| job.taken_by == runner }.each { |job| job.update(:taken_at => nil) }
43
41
  }
44
42
  end
43
+
45
44
  end
46
45
 
47
46
  end
@@ -5,7 +5,6 @@
5
5
 
6
6
  <link rel="stylesheet" href="/status/stylesheets/status.css" type="text/css" media="screen" charset="utf-8" />
7
7
  <script type="text/javascript" charset="utf-8" src="/status/javascripts/jquery-1.4.4.min.js"></script>
8
- <script type="text/javascript" charset="utf-8" src="/status/javascripts/sammy-0.6.2.min.js"></script>
9
8
  <script type="text/javascript" charset="utf-8">
10
9
  $(function () {
11
10
  var status = new function() {
@@ -4,7 +4,7 @@ class Adapter
4
4
  FILES.each { |file| require(file) }
5
5
 
6
6
  def self.all
7
- FILES.map { |file| load_adapter(file) }
7
+ FILES.map { |file| load_adapter(file) }
8
8
  end
9
9
 
10
10
  def self.find(type)
@@ -1,4 +1,5 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env"))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), "../color"))
2
3
 
3
4
  class CucumberAdapter
4
5
 
@@ -36,8 +37,54 @@ class CucumberAdapter
36
37
  pluralized
37
38
  end
38
39
 
40
+ # This is an optional method. It gets passed the entire test result and summarizes it. See the tests.
41
+ def self.sum_results(text)
42
+ scenarios, steps = parse_scenarios_and_steps(text)
43
+
44
+ scenarios_line = "#{scenarios[:total]} scenarios (" + [
45
+ (Color.colorize("#{scenarios[:failed]} failed", :red) if scenarios[:failed] > 0),
46
+ (Color.colorize("#{scenarios[:undefined]} undefined", :orange) if scenarios[:undefined] > 0),
47
+ (Color.colorize("#{scenarios[:passed]} passed", :green) if scenarios[:passed] > 0)
48
+ ].compact.join(', ') + ")"
49
+
50
+ steps_line = "#{steps[:total]} steps (" + [
51
+ (Color.colorize("#{steps[:failed]} failed", :red) if steps[:failed] > 0),
52
+ (Color.colorize("#{steps[:skipped]} skipped", :cyan) if steps[:skipped] > 0),
53
+ (Color.colorize("#{steps[:undefined]} undefined", :orange) if steps[:undefined] > 0),
54
+ (Color.colorize("#{steps[:passed]} passed", :green) if steps[:passed] > 0)
55
+ ].compact.join(', ') + ")"
56
+
57
+ scenarios_line + "\n" + steps_line
58
+ end
59
+
39
60
  private
40
61
 
62
+ def self.parse_scenarios_and_steps(text)
63
+ results = {
64
+ :scenarios => { :total => 0, :passed => 0, :failed => 0, :undefined => 0 },
65
+ :steps => { :total => 0, :passed => 0, :failed => 0, :skipped => 0, :undefined => 0 }
66
+ }
67
+
68
+ Color.strip(text).split("\n").each do |line|
69
+ type = line.include?("scenarios") ? :scenarios : :steps
70
+
71
+ if match = line.match(/\((.+)\)/)
72
+ results[type][:total] += line.split.first.to_i
73
+ parse_status_counts(results[type], match[1])
74
+ end
75
+ end
76
+
77
+ [ results[:scenarios], results[:steps] ]
78
+ end
79
+
80
+ def self.parse_status_counts(results, status_counts)
81
+ status_counts.split(', ').each do |part|
82
+ results.keys.each do |key|
83
+ results[key] += part.split.first.to_i if part.include?(key.to_s)
84
+ end
85
+ end
86
+ end
87
+
41
88
  def self.file_pattern
42
89
  '**/**/*.feature'
43
90
  end
@@ -1,4 +1,5 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env"))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), "../color"))
2
3
 
3
4
  class RspecAdapter
4
5
 
@@ -40,8 +41,37 @@ class RspecAdapter
40
41
  'spec'
41
42
  end
42
43
 
44
+ # This is an optional method. It gets passed the entire test result and summarizes it. See the tests.
45
+ def self.sum_results(results)
46
+ examples, failures, pending = 0, 0, 0
47
+ results.split("\n").each do |line|
48
+ line =~ /(\d+) examples?, (\d+) failures?(, (\d+) pending)?/
49
+ next unless $1
50
+ examples += $1.to_i
51
+ failures += $2.to_i
52
+ pending += $4.to_i
53
+ end
54
+
55
+ result = [ pluralize(examples, 'example'), pluralize(failures, 'failure'), (pending > 0 ? "#{pending} pending" : nil) ].compact.join(', ')
56
+ if failures == 0 && pending == 0
57
+ Color.colorize(result, :green)
58
+ elsif failures == 0 && pending > 0
59
+ Color.colorize(result, :orange)
60
+ else
61
+ Color.colorize(result, :red)
62
+ end
63
+ end
64
+
43
65
  private
44
66
 
67
+ def self.pluralize(count, singular)
68
+ if count == 1
69
+ "#{count} #{singular}"
70
+ else
71
+ "#{count} #{singular}s"
72
+ end
73
+ end
74
+
45
75
  def self.file_pattern
46
76
  '**/**/*_spec.rb'
47
77
  end
@@ -0,0 +1,16 @@
1
+ class Color
2
+ def self.colorize(text, color)
3
+ colors = { :green => 32, :orange => 33, :red => 31, :cyan => 36 }
4
+
5
+ if colors[color]
6
+ "\033[#{colors[color]}m#{text}\033[0m"
7
+ else
8
+ raise "Color not implemented: #{color}"
9
+ end
10
+ end
11
+
12
+ def self.strip(text)
13
+ text.gsub(/\e.+?m/, '')
14
+ end
15
+ end
16
+
@@ -1,7 +1,7 @@
1
1
  module Testbot
2
2
  # Don't forget to update readme and changelog
3
3
  def self.version
4
- version = "0.5.6"
4
+ version = "0.5.7"
5
5
  dev_version_file = File.join(File.dirname(__FILE__), '..', '..', 'DEV_VERSION')
6
6
  if File.exists?(dev_version_file)
7
7
  version += File.read(dev_version_file)
@@ -14,8 +14,6 @@ namespace :testbot do
14
14
  path = custom_path ? "#{adapter.base_path}/#{custom_path}" : adapter.base_path
15
15
  success = requester.run_tests(adapter, path)
16
16
 
17
- puts
18
- puts requester.result_lines.join("\n")
19
17
  puts
20
18
  puts "Finished in #{Time.now - start_time} seconds."
21
19
  success
@@ -39,21 +39,28 @@ module Testbot::Requester
39
39
  flexmock(mock).should_receive(:size).and_return(0)
40
40
  end
41
41
 
42
+ def fixture_path(local_path)
43
+ File.join(File.dirname(__FILE__), local_path)
44
+ end
45
+
42
46
  context "self.create_by_config" do
43
47
 
44
48
  should 'create a requester from config' do
45
- flexmock(YAML).should_receive(:load_file).once.with("testbot.yml").
46
- and_return({ :server_host => 'hostname', :rsync_path => '/path',
47
- :rsync_ignores => ".git tmp", :available_runner_usage => '50%',
48
- :ssh_tunnel => false, :project => "appname", :server_user => "user" })
49
- flexmock(Requester).should_receive(:new).once.with({ :server_host => 'hostname',
50
- :rsync_path => '/path', :rsync_ignores => '.git tmp',
51
- :available_runner_usage => '50%', :ssh_tunnel => false, :project => "appname",
52
- :server_user => "user" })
53
- Requester.create_by_config("testbot.yml")
49
+ requester = Requester.create_by_config(fixture_path("testbot.yml"))
50
+ assert_equal 'hostname', requester.config.server_host
51
+ assert_equal '/path', requester.config.rsync_path
52
+ assert_equal '.git tmp', requester.config.rsync_ignores
53
+ assert_equal 'appname', requester.config.project
54
+ assert_equal false, requester.config.ssh_tunnel
55
+ assert_equal 'user', requester.config.server_user
56
+ assert_equal '50%', requester.config.available_runner_usage
54
57
  end
55
58
 
56
-
59
+ should 'accept ERB-snippets in testbot.yml' do
60
+ requester = Requester.create_by_config(fixture_path("testbot_with_erb.yml"))
61
+ assert_equal 'dynamic_host', requester.config.server_host
62
+ assert_equal '50%', requester.config.available_runner_usage
63
+ end
57
64
  end
58
65
 
59
66
  context "initialize" do
@@ -93,15 +100,15 @@ module Testbot::Requester
93
100
  :sizes => "10 20",
94
101
  :jruby => false })
95
102
 
96
- flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, 'results' => '', "success" => true })
97
- flexmock(requester).should_receive(:sleep)
98
- flexmock(requester).should_receive(:puts)
99
- flexmock(requester).should_receive(:system)
103
+ flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, 'results' => '', "success" => true })
104
+ flexmock(requester).should_receive(:sleep)
105
+ flexmock(requester).should_receive(:puts)
106
+ flexmock(requester).should_receive(:system)
100
107
 
101
- assert_equal true, requester.run_tests(RspecAdapter, 'spec')
108
+ assert_equal true, requester.run_tests(RspecAdapter, 'spec')
102
109
  end
103
110
 
104
- should "keep calling the server for results until done" do
111
+ should "print the sum of results formatted by the adapter" do
105
112
  requester = Requester.new(:server_host => "192.168.1.100")
106
113
 
107
114
  flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
@@ -114,11 +121,34 @@ module Testbot::Requester
114
121
  { "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
115
122
  mock_file_sizes
116
123
 
117
- flexmock(requester).should_receive(:sleep).times(2).with(1)
118
- flexmock(requester).should_receive(:puts).once.with("job 2 done: ....")
119
- flexmock(requester).should_receive(:puts).once.with("job 1 done: ....")
124
+ flexmock(requester).should_receive(:sleep).times(2).with(1)
125
+ flexmock(requester).should_receive(:puts).once.with("job 2 done: ....")
126
+ flexmock(requester).should_receive(:puts).once.with("job 1 done: ....")
127
+ flexmock(requester).should_receive(:puts).once.with("\nformatted result")
120
128
 
121
- requester.run_tests(RspecAdapter, 'spec')
129
+ flexmock(RspecAdapter).should_receive(:sum_results).with("job 2 done: ....job 1 done: ....").and_return("formatted result")
130
+ requester.run_tests(RspecAdapter, 'spec')
131
+ end
132
+
133
+ should "keep calling the server for results until done" do
134
+ requester = Requester.new(:server_host => "192.168.1.100")
135
+
136
+ flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ])
137
+ flexmock(requester).should_receive(:system)
138
+
139
+ flexmock(HTTParty).should_receive(:post).and_return('5')
140
+
141
+ flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
142
+ :format => :json).and_return({ "done" => false, "results" => "job 2 done: ...." },
143
+ { "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
144
+ mock_file_sizes
145
+
146
+ flexmock(requester).should_receive(:sleep).times(2).with(1)
147
+ flexmock(requester).should_receive(:puts).once.with("job 2 done: ....")
148
+ flexmock(requester).should_receive(:puts).once.with("job 1 done: ....")
149
+ flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
150
+
151
+ requester.run_tests(RspecAdapter, 'spec')
122
152
  end
123
153
 
124
154
  should "return false if not successful" do
@@ -132,11 +162,12 @@ module Testbot::Requester
132
162
  flexmock(HTTParty).should_receive(:get).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
133
163
  :format => :json).and_return({ "success" => false, "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
134
164
 
135
- flexmock(requester).should_receive(:sleep).once.with(1)
136
- flexmock(requester).should_receive(:puts).once.with("job 2 done: ....job 1 done: ....")
137
- mock_file_sizes
165
+ flexmock(requester).should_receive(:sleep).once.with(1)
166
+ flexmock(requester).should_receive(:puts).once.with("job 2 done: ....job 1 done: ....")
167
+ flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
168
+ mock_file_sizes
138
169
 
139
- assert_equal false, requester.run_tests(RspecAdapter, 'spec')
170
+ assert_equal false, requester.run_tests(RspecAdapter, 'spec')
140
171
  end
141
172
 
142
173
  should "not print empty lines when there is no result" do
@@ -151,11 +182,12 @@ module Testbot::Requester
151
182
  :format => :json).and_return({ "done" => false, "results" => "" },
152
183
  { "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
153
184
 
154
- flexmock(requester).should_receive(:sleep).times(2).with(1)
155
- flexmock(requester).should_receive(:puts).once.with("job 2 done: ....job 1 done: ....")
156
- mock_file_sizes
185
+ flexmock(requester).should_receive(:sleep).times(2).with(1)
186
+ flexmock(requester).should_receive(:puts).once.with("job 2 done: ....job 1 done: ....")
187
+ flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
188
+ mock_file_sizes
157
189
 
158
- requester.run_tests(RspecAdapter, 'spec')
190
+ requester.run_tests(RspecAdapter, 'spec')
159
191
  end
160
192
 
161
193
  should "sync the files to the server" do
@@ -166,13 +198,14 @@ module Testbot::Requester
166
198
 
167
199
  flexmock(HTTParty).should_receive(:post).and_return('5')
168
200
  flexmock(requester).should_receive(:sleep).once
201
+ flexmock(requester).should_receive(:puts)
169
202
  flexmock(HTTParty).should_receive(:get).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
170
203
  :format => :json).and_return({ "done" => true, "results" => "" })
171
204
 
172
- flexmock(requester).should_receive('system').with("rsync -az --delete -e ssh --exclude='.git' --exclude='tmp' . testbot@192.168.1.100:/path")
173
- mock_file_sizes
205
+ flexmock(requester).should_receive('system').with("rsync -az --delete -e ssh --exclude='.git' --exclude='tmp' . testbot@192.168.1.100:/path")
206
+ mock_file_sizes
174
207
 
175
- requester.run_tests(RspecAdapter, 'spec')
208
+ requester.run_tests(RspecAdapter, 'spec')
176
209
  end
177
210
 
178
211
  should "just try again if the request encounters an error while running and print on the fith time" do
@@ -188,12 +221,13 @@ module Testbot::Requester
188
221
  flexmock(HTTParty).should_receive(:get).times(1).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5",
189
222
  :format => :json).and_return({ "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
190
223
 
191
- flexmock(requester).should_receive(:sleep).times(6).with(1)
192
- flexmock(requester).should_receive(:puts).once.with("Failed to get status: some connection error")
193
- flexmock(requester).should_receive(:puts).once.with("job 2 done: ....job 1 done: ....")
194
- mock_file_sizes
224
+ flexmock(requester).should_receive(:sleep).times(6).with(1)
225
+ flexmock(requester).should_receive(:puts).once.with("Failed to get status: some connection error")
226
+ flexmock(requester).should_receive(:puts).once.with("job 2 done: ....job 1 done: ....")
227
+ flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
228
+ mock_file_sizes
195
229
 
196
- requester.run_tests(RspecAdapter, 'spec')
230
+ requester.run_tests(RspecAdapter, 'spec')
197
231
  end
198
232
 
199
233
  should "just try again if the status returns as nil" do
@@ -208,11 +242,12 @@ module Testbot::Requester
208
242
  :format => :json).and_return(nil,
209
243
  { "done" => true, "results" => "job 2 done: ....job 1 done: ...." })
210
244
 
211
- flexmock(requester).should_receive(:sleep).times(2).with(1)
212
- flexmock(requester).should_receive(:puts).once.with("job 2 done: ....job 1 done: ....")
213
- mock_file_sizes
245
+ flexmock(requester).should_receive(:sleep).times(2).with(1)
246
+ flexmock(requester).should_receive(:puts).once.with("job 2 done: ....job 1 done: ....")
247
+ flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m")
248
+ mock_file_sizes
214
249
 
215
- requester.run_tests(RspecAdapter, 'spec')
250
+ requester.run_tests(RspecAdapter, 'spec')
216
251
  end
217
252
 
218
253
  should "remove unnessesary output from rspec when told to do so" do
@@ -227,14 +262,14 @@ module Testbot::Requester
227
262
  :format => :json).and_return(nil,
228
263
  { "done" => true, "results" => "testbot4:\n....\n\nFinished in 84.333 seconds\n\n206 examples, 0 failures, 2 pending; testbot4:\n.F..\n\nFinished in 84.333 seconds\n\n206 examples, 0 failures, 2 pending" })
229
264
 
230
- flexmock(requester).should_receive(:sleep).times(2).with(1)
265
+ flexmock(requester).should_receive(:sleep).times(2).with(1)
231
266
 
232
- # Imperfect match, includes "." in 84.333, but good enough.
233
- flexmock(requester).should_receive(:print).once.with("......F...")
234
- flexmock(requester).should_receive(:puts)
235
- mock_file_sizes
267
+ # Imperfect match, includes "." in 84.333, but good enough.
268
+ flexmock(requester).should_receive(:print).once.with("......F...")
269
+ flexmock(requester).should_receive(:puts)
270
+ mock_file_sizes
236
271
 
237
- requester.run_tests(RspecAdapter, 'spec')
272
+ requester.run_tests(RspecAdapter, 'spec')
238
273
  end
239
274
 
240
275
  should "use SSHTunnel when specified (with a port that does not collide with the runner)" do
@@ -330,17 +365,6 @@ module Testbot::Requester
330
365
 
331
366
  end
332
367
 
333
- context "result_lines" do
334
-
335
- should "return all lines with results in them" do
336
- results = "one\ntwo..\n... 0 failures\nthree"
337
- requester = requester_with_result(results)
338
- requester.run_tests(RspecAdapter, 'spec')
339
- assert_equal [ '... 0 failures' ], requester.result_lines
340
- end
341
-
342
- end
343
-
344
368
  end
345
369
 
346
370
  end
@@ -0,0 +1,7 @@
1
+ server_host: hostname
2
+ rsync_path: /path
3
+ rsync_ignores: .git tmp
4
+ project: appname
5
+ ssh_tunnel: false
6
+ server_user: user
7
+ available_runner_usage: 50%
@@ -0,0 +1,2 @@
1
+ server_host: <%= "dynamic_host" %>
2
+ available_runner_usage: "<%= 50 %>%"
File without changes
@@ -0,0 +1,72 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../../../lib/shared/adapters/cucumber_adapter.rb'))
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ class CucumberAdapterTest < Test::Unit::TestCase
6
+
7
+ context "sum_results" do
8
+
9
+ should "be able to parse and sum results" do
10
+ results =<<STR
11
+ testbot4:/tmp/testbot
12
+ ............................................................................................................................................................
13
+
14
+ 13 scenarios (\033[32m13 passed\033[0m)
15
+ 153 steps (\033[32m153 passed\033[0m)
16
+ 0m25.537s
17
+
18
+ testbot3:/tmp/testbot
19
+ ................................................................................................................
20
+
21
+ 12 scenarios (\033[32m12 passed\033[0m)
22
+ 109 steps (\033[32m109 passed\033[0m)
23
+ 1m28.472s
24
+ STR
25
+
26
+ assert_equal "25 scenarios (25 passed)\n262 steps (262 passed)", Color.strip(CucumberAdapter.sum_results(results))
27
+ end
28
+
29
+
30
+ should "should handle undefined steps" do
31
+ results =<<STR
32
+ 5 scenarios (1 failed, 1 undefined, 3 passed)
33
+ 42 steps (1 failed, 3 skipped, 1 undefined, 37 passed)
34
+
35
+ 5 scenarios (1 failed, 1 undefined, 3 passed)
36
+ 42 steps (1 failed, 3 skipped, 1 undefined, 37 passed)
37
+
38
+ 6 scenarios (6 passed)
39
+ 80 steps (80 passed)
40
+ STR
41
+
42
+ assert_equal "16 scenarios (2 failed, 2 undefined, 12 passed)\n164 steps (2 failed, 6 skipped, 2 undefined, 154 passed)", Color.strip(CucumberAdapter.sum_results(results))
43
+ end
44
+
45
+ should "handle other combinations" do
46
+ results =<<STR
47
+ 5 scenarios (1 failed, 1 undefined, 3 passed)
48
+ 42 steps (1 failed, 1 undefined, 37 passed)
49
+
50
+ 5 scenarios (1 failed, 1 undefined, 3 passed)
51
+ 42 steps (3 skipped, 1 undefined, 37 passed)
52
+
53
+ 6 scenarios (6 passed)
54
+ 80 steps (80 passed)
55
+ STR
56
+
57
+ assert_equal "16 scenarios (2 failed, 2 undefined, 12 passed)\n164 steps (1 failed, 3 skipped, 2 undefined, 154 passed)", Color.strip(CucumberAdapter.sum_results(results))
58
+ end
59
+
60
+ should "colorize" do
61
+ results =<<STR
62
+ 5 scenarios (1 failed, 1 undefined, 3 passed)
63
+ 42 steps (1 failed, 3 skipped, 1 undefined, 37 passed)
64
+ STR
65
+
66
+ assert_equal "5 scenarios (\e[31m1 failed\e[0m, \e[33m1 undefined\e[0m, \e[32m3 passed\e[0m)\n42 steps (\e[31m1 failed\e[0m, \e[36m3 skipped\e[0m, \e[33m1 undefined\e[0m, \e[32m37 passed\e[0m)", CucumberAdapter.sum_results(results)
67
+ end
68
+
69
+ end
70
+
71
+
72
+ end
@@ -0,0 +1,109 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../../../lib/shared/adapters/rspec_adapter.rb'))
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ class RspecAdapterTest < Test::Unit::TestCase
6
+
7
+ context "sum_results" do
8
+
9
+ should "be able to parse and sum results" do
10
+ results =<<STR
11
+ srv-y5ei5:/tmp/testbot
12
+ ..................FF..................................................
13
+
14
+ Finished in 4.962975 seconds
15
+
16
+ 69 examples, 2 failures
17
+
18
+
19
+ testbot1:/tmp/testbot
20
+ .............F...........*........................
21
+
22
+ Finished in 9.987141 seconds
23
+
24
+ 50 examples, 1 failure, 1 pending
25
+
26
+ testbot1:/tmp/testbot
27
+ .............FF.......****........................
28
+
29
+ Finished in 9.987141 seconds
30
+
31
+ 50 examples, 2 failures, 3 pending
32
+
33
+ testbot1:/tmp/testbot
34
+ .
35
+
36
+ Finished in 9.987141 seconds
37
+
38
+ 1 example, 0 failures, 0 pending
39
+ STR
40
+ assert_equal "170 examples, 5 failures, 4 pending", Color.strip(RspecAdapter.sum_results(results))
41
+ end
42
+
43
+ should "return 0 examples and failures for an empty resultset" do
44
+ assert_equal "0 examples, 0 failures", Color.strip(RspecAdapter.sum_results(""))
45
+ end
46
+
47
+ should "print in singular for examples" do
48
+ str =<<STR
49
+ testbot1:/tmp/testbot
50
+ .
51
+
52
+ Finished in 9.987141 seconds
53
+
54
+ 1 example, 0 failures
55
+ STR
56
+ assert_equal "1 example, 0 failures", Color.strip(RspecAdapter.sum_results(str))
57
+ end
58
+
59
+ should "print in singular for failures" do
60
+ str =<<STR
61
+ testbot1:/tmp/testbot
62
+ F
63
+
64
+ Finished in 9.987141 seconds
65
+
66
+ 0 example, 1 failures
67
+ STR
68
+ assert_equal "0 examples, 1 failure", Color.strip(RspecAdapter.sum_results(str))
69
+ end
70
+
71
+ should "make the result green if there is no failed or pending examples" do
72
+ str =<<STR
73
+ testbot1:/tmp/testbot
74
+ .
75
+
76
+ Finished in 9.987141 seconds
77
+
78
+ 1 example, 0 failures
79
+ STR
80
+ assert_equal "\033[32m1 example, 0 failures\033[0m", RspecAdapter.sum_results(str)
81
+ end
82
+
83
+ should "make the result orange if there is pending examples" do
84
+ str =<<STR
85
+ testbot1:/tmp/testbot
86
+ *
87
+
88
+ Finished in 9.987141 seconds
89
+
90
+ 1 example, 0 failures, 1 pending
91
+ STR
92
+ assert_equal "\033[33m1 example, 0 failures, 1 pending\033[0m", RspecAdapter.sum_results(str)
93
+ end
94
+
95
+ should "make the results red if there is failed examples" do
96
+ str = <<STR
97
+ testbot1:/tmp/testbot
98
+ F
99
+
100
+ Finished in 9.987141 seconds
101
+
102
+ 1 example, 1 failures
103
+ STR
104
+ assert_equal "\033[31m1 example, 1 failure\033[0m", RspecAdapter.sum_results(str)
105
+ end
106
+
107
+ end
108
+
109
+ end
@@ -28,5 +28,7 @@ Gem::Specification.new do |s|
28
28
  s.add_development_dependency("rvm")
29
29
  s.add_development_dependency("rake")
30
30
  s.add_development_dependency("bundler")
31
+ s.add_development_dependency("guard")
32
+ s.add_development_dependency("guard-test")
31
33
  end
32
34
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testbot
3
3
  version: !ruby/object:Gem::Version
4
- hash: 7
4
+ hash: 5
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 5
9
- - 6
10
- version: 0.5.6
9
+ - 7
10
+ version: 0.5.7
11
11
  platform: ruby
12
12
  authors:
13
13
  - "Joakim Kolsj\xC3\xB6"
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-04-11 00:00:00 +02:00
18
+ date: 2011-06-19 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -226,6 +226,34 @@ dependencies:
226
226
  version: "0"
227
227
  type: :development
228
228
  version_requirements: *id014
229
+ - !ruby/object:Gem::Dependency
230
+ name: guard
231
+ prerelease: false
232
+ requirement: &id015 !ruby/object:Gem::Requirement
233
+ none: false
234
+ requirements:
235
+ - - ">="
236
+ - !ruby/object:Gem::Version
237
+ hash: 3
238
+ segments:
239
+ - 0
240
+ version: "0"
241
+ type: :development
242
+ version_requirements: *id015
243
+ - !ruby/object:Gem::Dependency
244
+ name: guard-test
245
+ prerelease: false
246
+ requirement: &id016 !ruby/object:Gem::Requirement
247
+ none: false
248
+ requirements:
249
+ - - ">="
250
+ - !ruby/object:Gem::Version
251
+ hash: 3
252
+ segments:
253
+ - 0
254
+ version: "0"
255
+ type: :development
256
+ version_requirements: *id016
229
257
  description: Testbot is a test distribution tool that works with Rails, RSpec, RSpec2, Test::Unit and Cucumber.
230
258
  email:
231
259
  - joakim.kolsjo@gmail.com
@@ -250,7 +278,6 @@ files:
250
278
  - lib/server/runner.rb
251
279
  - lib/server/server.rb
252
280
  - lib/server/status/javascripts/jquery-1.4.4.min.js
253
- - lib/server/status/javascripts/sammy-0.6.2.min.js
254
281
  - lib/server/status/status.html
255
282
  - lib/server/status/stylesheets/status.css
256
283
  - lib/shared/adapters/adapter.rb
@@ -259,6 +286,7 @@ files:
259
286
  - lib/shared/adapters/rspec2_adapter.rb
260
287
  - lib/shared/adapters/rspec_adapter.rb
261
288
  - lib/shared/adapters/test_unit_adapter.rb
289
+ - lib/shared/color.rb
262
290
  - lib/shared/simple_daemonize.rb
263
291
  - lib/shared/ssh_tunnel.rb
264
292
  - lib/shared/testbot.rb
@@ -273,14 +301,18 @@ files:
273
301
  - test/fixtures/local/spec/models/house_spec.rb
274
302
  - test/fixtures/local/spec/spec.opts
275
303
  - test/fixtures/local/tmp/restart.txt
276
- - test/requester/test_requester.rb
277
- - test/runner/test_job.rb
278
- - test/server/test_group.rb
279
- - test/server/test_server.rb
280
- - test/shared/adapters/helpers/test_ruby_env.rb
281
- - test/shared/adapters/test_adapter.rb
282
- - test/shared/test_testbot.rb
283
- - test/test_integration.rb
304
+ - test/integration_test.rb
305
+ - test/requester/requester_test.rb
306
+ - test/requester/testbot.yml
307
+ - test/requester/testbot_with_erb.yml
308
+ - test/runner/job_test.rb
309
+ - test/server/group_test.rb
310
+ - test/server/server_test.rb
311
+ - test/shared/adapters/adapter_test.rb
312
+ - test/shared/adapters/cucumber_adapter_test.rb
313
+ - test/shared/adapters/helpers/ruby_env_test.rb
314
+ - test/shared/adapters/rspec_adapter_test.rb
315
+ - test/shared/testbot_test.rb
284
316
  - Gemfile
285
317
  - .gemtest
286
318
  - Rakefile
@@ -318,7 +350,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
318
350
  requirements: []
319
351
 
320
352
  rubyforge_project:
321
- rubygems_version: 1.5.3
353
+ rubygems_version: 1.6.2
322
354
  signing_key:
323
355
  specification_version: 3
324
356
  summary: A test distribution tool.
@@ -1,5 +0,0 @@
1
- // -- Sammy -- /sammy.js
2
- // http://code.quirkey.com/sammy
3
- // Version: 0.6.2
4
- // Built: Mon Oct 11 12:41:51 -0700 2010
5
- (function(g,i){var n,f="([^/]+)",j=/:([\w\d]+)/g,k=/\?([^#]*)$/,b=function(o){return Array.prototype.slice.call(o)},c=function(o){return Object.prototype.toString.call(o)==="[object Function]"},l=function(o){return Object.prototype.toString.call(o)==="[object Array]"},h=decodeURIComponent,e=function(o){return o.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")},m=function(o){return function(p,q){return this.route.apply(this,[o,p,q])}},a={},d=[];n=function(){var p=b(arguments),q,o;n.apps=n.apps||{};if(p.length===0||p[0]&&c(p[0])){return n.apply(n,["body"].concat(p))}else{if(typeof(o=p.shift())=="string"){q=n.apps[o]||new n.Application();q.element_selector=o;if(p.length>0){g.each(p,function(r,s){q.use(s)})}if(q.element_selector!=o){delete n.apps[o]}n.apps[q.element_selector]=q;return q}}};n.VERSION="0.6.2";n.addLogger=function(o){d.push(o)};n.log=function(){var o=b(arguments);o.unshift("["+Date()+"]");g.each(d,function(q,p){p.apply(n,o)})};if(typeof i.console!="undefined"){if(c(console.log.apply)){n.addLogger(function(){i.console.log.apply(console,arguments)})}else{n.addLogger(function(){i.console.log(arguments)})}}else{if(typeof console!="undefined"){n.addLogger(function(){console.log.apply(console,arguments)})}}g.extend(n,{makeArray:b,isFunction:c,isArray:l});n.Object=function(o){return g.extend(this,o||{})};g.extend(n.Object.prototype,{escapeHTML:e,h:e,toHash:function(){var o={};g.each(this,function(q,p){if(!c(p)){o[q]=p}});return o},toHTML:function(){var o="";g.each(this,function(q,p){if(!c(p)){o+="<strong>"+q+"</strong> "+p+"<br />"}});return o},keys:function(o){var p=[];for(var q in this){if(!c(this[q])||!o){p.push(q)}}return p},has:function(o){return this[o]&&g.trim(this[o].toString())!=""},join:function(){var p=b(arguments);var o=p.shift();return p.join(o)},log:function(){n.log.apply(n,arguments)},toString:function(o){var p=[];g.each(this,function(r,q){if(!c(q)||o){p.push('"'+r+'": '+q.toString())}});return"Sammy.Object: {"+p.join(",")+"}"}});n.HashLocationProxy=function(p,o){this.app=p;this.is_native=false;this._startPolling(o)};n.HashLocationProxy.prototype={bind:function(){var o=this,p=this.app;g(i).bind("hashchange."+this.app.eventNamespace(),function(r,q){if(o.is_native===false&&!q){n.log("native hash change exists, using");o.is_native=true;i.clearInterval(n.HashLocationProxy._interval)}p.trigger("location-changed")});if(!n.HashLocationProxy._bindings){n.HashLocationProxy._bindings=0}n.HashLocationProxy._bindings++},unbind:function(){g(i).unbind("hashchange."+this.app.eventNamespace());n.HashLocationProxy._bindings--;if(n.HashLocationProxy._bindings<=0){i.clearInterval(n.HashLocationProxy._interval)}},getLocation:function(){var o=i.location.toString().match(/^[^#]*(#.+)$/);return o?o[1]:""},setLocation:function(o){return(i.location=o)},_startPolling:function(q){var p=this;if(!n.HashLocationProxy._interval){if(!q){q=10}var o=function(){var r=p.getLocation();if(!n.HashLocationProxy._last_location||r!=n.HashLocationProxy._last_location){i.setTimeout(function(){g(i).trigger("hashchange",[true])},13)}n.HashLocationProxy._last_location=r};o();n.HashLocationProxy._interval=i.setInterval(o,q)}}};n.Application=function(o){var p=this;this.routes={};this.listeners=new n.Object({});this.arounds=[];this.befores=[];this.namespace=(new Date()).getTime()+"-"+parseInt(Math.random()*1000,10);this.context_prototype=function(){n.EventContext.apply(this,arguments)};this.context_prototype.prototype=new n.EventContext();if(c(o)){o.apply(this,[this])}if(!this._location_proxy){this.setLocationProxy(new n.HashLocationProxy(this,this.run_interval_every))}if(this.debug){this.bindToAllEvents(function(r,q){p.log(p.toString(),r.cleaned_type,q||{})})}};n.Application.prototype=g.extend({},n.Object.prototype,{ROUTE_VERBS:["get","post","put","delete"],APP_EVENTS:["run","unload","lookup-route","run-route","route-found","event-context-before","event-context-after","changed","error","check-form-submission","redirect","location-changed"],_last_route:null,_location_proxy:null,_running:false,element_selector:"body",debug:false,raise_errors:false,run_interval_every:50,template_engine:null,toString:function(){return"Sammy.Application:"+this.element_selector},$element:function(){return g(this.element_selector)},use:function(){var o=b(arguments),q=o.shift(),p=q||"";try{o.unshift(this);if(typeof q=="string"){p="Sammy."+q;q=n[q]}q.apply(this,o)}catch(r){if(typeof q==="undefined"){this.error("Plugin Error: called use() but plugin ("+p.toString()+") is not defined",r)}else{if(!c(q)){this.error("Plugin Error: called use() but '"+p.toString()+"' is not a function",r)}else{this.error("Plugin Error",r)}}}return this},setLocationProxy:function(o){var p=this._location_proxy;this._location_proxy=o;if(this.isRunning()){if(p){p.unbind()}this._location_proxy.bind()}},route:function(s,p,u){var r=this,t=[],o,q;if(!u&&c(p)){p=s;u=p;s="any"}s=s.toLowerCase();if(p.constructor==String){j.lastIndex=0;while((q=j.exec(p))!==null){t.push(q[1])}p=new RegExp("^"+p.replace(j,f)+"$")}if(typeof u=="string"){u=r[u]}o=function(v){var w={verb:v,path:p,callback:u,param_names:t};r.routes[v]=r.routes[v]||[];r.routes[v].push(w)};if(s==="any"){g.each(this.ROUTE_VERBS,function(x,w){o(w)})}else{o(s)}return this},get:m("get"),post:m("post"),put:m("put"),del:m("delete"),any:m("any"),mapRoutes:function(p){var o=this;g.each(p,function(q,r){o.route.apply(o,r)});return this},eventNamespace:function(){return["sammy-app",this.namespace].join("-")},bind:function(o,q,s){var r=this;if(typeof s=="undefined"){s=q}var p=function(){var v,t,u;v=arguments[0];u=arguments[1];if(u&&u.context){t=u.context;delete u.context}else{t=new r.context_prototype(r,"bind",v.type,u,v.target)}v.cleaned_type=v.type.replace(r.eventNamespace(),"");s.apply(t,[v,u])};if(!this.listeners[o]){this.listeners[o]=[]}this.listeners[o].push(p);if(this.isRunning()){this._listen(o,p)}return this},trigger:function(o,p){this.$element().trigger([o,this.eventNamespace()].join("."),[p]);return this},refresh:function(){this.last_location=null;this.trigger("location-changed");return this},before:function(o,p){if(c(o)){p=o;o={}}this.befores.push([o,p]);return this},after:function(o){return this.bind("event-context-after",o)},around:function(o){this.arounds.push(o);return this},isRunning:function(){return this._running},helpers:function(o){g.extend(this.context_prototype.prototype,o);return this},helper:function(o,p){this.context_prototype.prototype[o]=p;return this},run:function(o){if(this.isRunning()){return false}var p=this;g.each(this.listeners.toHash(),function(q,r){g.each(r,function(t,s){p._listen(q,s)})});this.trigger("run",{start_url:o});this._running=true;this.last_location=null;if(this.getLocation()==""&&typeof o!="undefined"){this.setLocation(o)}this._checkLocation();this._location_proxy.bind();this.bind("location-changed",function(){p._checkLocation()});this.bind("submit",function(r){var q=p._checkFormSubmission(g(r.target).closest("form"));return(q===false)?r.preventDefault():false});g(i).bind("beforeunload",function(){p.unload()});return this.trigger("changed")},unload:function(){if(!this.isRunning()){return false}var o=this;this.trigger("unload");this._location_proxy.unbind();this.$element().unbind("submit").removeClass(o.eventNamespace());g.each(this.listeners.toHash(),function(p,q){g.each(q,function(s,r){o._unlisten(p,r)})});this._running=false;return this},bindToAllEvents:function(p){var o=this;g.each(this.APP_EVENTS,function(q,r){o.bind(r,p)});g.each(this.listeners.keys(true),function(r,q){if(o.APP_EVENTS.indexOf(q)==-1){o.bind(q,p)}});return this},routablePath:function(o){return o.replace(k,"")},lookupRoute:function(r,p){var q=this,o=false;this.trigger("lookup-route",{verb:r,path:p});if(typeof this.routes[r]!="undefined"){g.each(this.routes[r],function(t,s){if(q.routablePath(p).match(s.path)){o=s;return false}})}return o},runRoute:function(q,D,s,v){var r=this,B=this.lookupRoute(q,D),p,y,t,x,C,z,w,A,o;this.log("runRoute",[q,D].join(" "));this.trigger("run-route",{verb:q,path:D,params:s});if(typeof s=="undefined"){s={}}g.extend(s,this._parseQueryString(D));if(B){this.trigger("route-found",{route:B});if((A=B.path.exec(this.routablePath(D)))!==null){A.shift();g.each(A,function(E,F){if(B.param_names[E]){s[B.param_names[E]]=h(F)}else{if(!s.splat){s.splat=[]}s.splat.push(h(F))}})}p=new this.context_prototype(this,q,D,s,v);t=this.arounds.slice(0);C=this.befores.slice(0);w=[p].concat(s.splat);y=function(){var E;while(C.length>0){z=C.shift();if(r.contextMatchesOptions(p,z[0])){E=z[1].apply(p,[p]);if(E===false){return false}}}r.last_route=B;p.trigger("event-context-before",{context:p});E=B.callback.apply(p,w);p.trigger("event-context-after",{context:p});return E};g.each(t.reverse(),function(E,F){var G=y;y=function(){return F.apply(p,[G])}});try{o=y()}catch(u){this.error(["500 Error",q,D].join(" "),u)}return o}else{return this.notFound(q,D)}},contextMatchesOptions:function(r,t,p){var q=t;if(typeof q==="undefined"||q=={}){return true}if(typeof p==="undefined"){p=true}if(typeof q==="string"||c(q.test)){q={path:q}}if(q.only){return this.contextMatchesOptions(r,q.only,true)}else{if(q.except){return this.contextMatchesOptions(r,q.except,false)}}var o=true,s=true;if(q.path){if(c(q.path.test)){o=q.path.test(r.path)}else{o=(q.path.toString()===r.path)}}if(q.verb){s=q.verb===r.verb}return p?(s&&o):!(s&&o)},getLocation:function(){return this._location_proxy.getLocation()},setLocation:function(o){return this._location_proxy.setLocation(o)},swap:function(o){return this.$element().html(o)},templateCache:function(o,p){if(typeof p!="undefined"){return a[o]=p}else{return a[o]}},clearTemplateCache:function(){return a={}},notFound:function(q,p){var o=this.error(["404 Not Found",q,p].join(" "));return(q==="get")?o:true},error:function(p,o){if(!o){o=new Error()}o.message=[p,o.message].join(" ");this.trigger("error",{message:o.message,error:o});if(this.raise_errors){throw (o)}else{this.log(o.message,o)}},_checkLocation:function(){var o,p;o=this.getLocation();if(!this.last_location||this.last_location[0]!="get"||this.last_location[1]!=o){this.last_location=["get",o];p=this.runRoute("get",o)}return p},_getFormVerb:function(q){var p=g(q),r,o;o=p.find('input[name="_method"]');if(o.length>0){r=o.val()}if(!r){r=p[0].getAttribute("method")}return g.trim(r.toString().toLowerCase())},_checkFormSubmission:function(q){var o,r,t,s,p;this.trigger("check-form-submission",{form:q});o=g(q);r=o.attr("action");t=this._getFormVerb(o);if(!t||t==""){t="get"}this.log("_checkFormSubmission",o,r,t);if(t==="get"){this.setLocation(r+"?"+o.serialize());p=false}else{s=g.extend({},this._parseFormParams(o));p=this.runRoute(t,r,s,q.get(0))}return(typeof p=="undefined")?false:p},_parseFormParams:function(o){var r={},q=o.serializeArray(),p;for(p=0;p<q.length;p++){r=this._parseParamPair(r,q[p].name,q[p].value)}return r},_parseQueryString:function(r){var t={},q,p,s,o;q=r.match(k);if(q){p=q[1].split("&");for(o=0;o<p.length;o++){s=p[o].split("=");t=this._parseParamPair(t,h(s[0]),h(s[1]))}}return t},_parseParamPair:function(q,o,p){if(q[o]){if(l(q[o])){q[o].push(p)}else{q[o]=[q[o],p]}}else{q[o]=p}return q},_listen:function(o,p){return this.$element().bind([o,this.eventNamespace()].join("."),p)},_unlisten:function(o,p){return this.$element().unbind([o,this.eventNamespace()].join("."),p)}});n.RenderContext=function(o){this.event_context=o;this.callbacks=[];this.previous_content=null;this.content=null;this.next_engine=false;this.waiting=false};g.extend(n.RenderContext.prototype,{then:function(q){if(!c(q)){if(typeof q==="string"&&q in this.event_context){var p=this.event_context[q];q=function(r){return p.apply(this.event_context,[r])}}else{return this}}var o=this;if(this.waiting){this.callbacks.push(q)}else{this.wait();i.setTimeout(function(){var r=q.apply(o,[o.content,o.previous_content]);if(r!==false){o.next(r)}},13)}return this},wait:function(){this.waiting=true},next:function(o){this.waiting=false;if(typeof o!=="undefined"){this.previous_content=this.content;this.content=o}if(this.callbacks.length>0){this.then(this.callbacks.shift())}},load:function(o,p,r){var q=this;return this.then(function(){var s,t,v,u;if(c(p)){r=p;p={}}else{p=g.extend({},p)}if(r){this.then(r)}if(typeof o==="string"){v=(o.match(/\.json$/)||p.json);s=((v&&p.cache===true)||p.cache!==false);q.next_engine=q.event_context.engineFor(o);delete p.cache;delete p.json;if(p.engine){q.next_engine=p.engine;delete p.engine}if(s&&(t=this.event_context.app.templateCache(o))){return t}this.wait();g.ajax(g.extend({url:o,data:{},dataType:v?"json":null,type:"get",success:function(w){if(s){q.event_context.app.templateCache(o,w)}q.next(w)}},p));return false}else{if(o.nodeType){return o.innerHTML}if(o.selector){q.next_engine=o.attr("data-engine");if(p.clone===false){return o.remove()[0].innerHTML.toString()}else{return o[0].innerHTML.toString()}}}})},render:function(o,p,q){if(c(o)&&!p){return this.then(o)}else{if(!p&&this.content){p=this.content}return this.load(o).interpolate(p,o).then(q)}},partial:function(o,p){return this.render(o,p).swap()},send:function(){var q=this,p=b(arguments),o=p.shift();if(l(p[0])){p=p[0]}return this.then(function(r){p.push(function(s){q.next(s)});q.wait();o.apply(o,p);return false})},collect:function(s,r,o){var q=this;var p=function(){if(c(s)){r=s;s=this.content}var t=[],u=false;g.each(s,function(v,x){var w=r.apply(q,[v,x]);if(w.jquery&&w.length==1){w=w[0];u=true}t.push(w);return w});return u?t:t.join("")};return o?p():this.then(p)},renderEach:function(o,p,q,r){if(l(p)){r=q;q=p;p=null}return this.load(o).then(function(t){var s=this;if(!q){q=l(this.previous_content)?this.previous_content:[]}if(r){g.each(q,function(u,w){var x={},v=this.next_engine||o;p?(x[p]=w):(x=w);r(w,s.event_context.interpolate(t,x,v))})}else{return this.collect(q,function(u,w){var x={},v=this.next_engine||o;p?(x[p]=w):(x=w);return this.event_context.interpolate(t,x,v)},true)}})},interpolate:function(r,q,o){var p=this;return this.then(function(t,s){if(!r&&s){r=s}if(this.next_engine){q=this.next_engine;this.next_engine=false}var u=p.event_context.interpolate(t,r,q);return o?s+u:u})},swap:function(){return this.then(function(o){this.event_context.swap(o)}).trigger("changed",{})},appendTo:function(o){return this.then(function(p){g(o).append(p)}).trigger("changed",{})},prependTo:function(o){return this.then(function(p){g(o).prepend(p)}).trigger("changed",{})},replace:function(o){return this.then(function(p){g(o).html(p)}).trigger("changed",{})},trigger:function(o,p){return this.then(function(q){if(typeof p=="undefined"){p={content:q}}this.event_context.trigger(o,p)})}});n.EventContext=function(s,r,p,q,o){this.app=s;this.verb=r;this.path=p;this.params=new n.Object(q);this.target=o};n.EventContext.prototype=g.extend({},n.Object.prototype,{$element:function(){return this.app.$element()},engineFor:function(q){var p=this,o;if(c(q)){return q}q=q.toString();if((o=q.match(/\.([^\.]+)$/))){q=o[1]}if(q&&c(p[q])){return p[q]}if(p.app.template_engine){return this.engineFor(p.app.template_engine)}return function(r,s){return r}},interpolate:function(p,q,o){return this.engineFor(o).apply(this,[p,q])},render:function(o,p,q){return new n.RenderContext(this).render(o,p,q)},renderEach:function(o,p,q,r){return new n.RenderContext(this).renderEach(o,p,q,r)},load:function(o,p,q){return new n.RenderContext(this).load(o,p,q)},partial:function(o,p){return new n.RenderContext(this).partial(o,p)},send:function(){var o=new n.RenderContext(this);return o.send.apply(o,arguments)},redirect:function(){var q,p=b(arguments),o=this.app.getLocation();if(p.length>1){p.unshift("/");q=this.join.apply(this,p)}else{q=p[0]}this.trigger("redirect",{to:q});this.app.last_location=[this.verb,this.path];this.app.setLocation(q);if(o==q){this.app.trigger("location-changed")}},trigger:function(o,p){if(typeof p=="undefined"){p={}}if(!p.context){p.context=this}return this.app.trigger(o,p)},eventNamespace:function(){return this.app.eventNamespace()},swap:function(o){return this.app.swap(o)},notFound:function(){return this.app.notFound(this.verb,this.path)},json:function(o){return g.parseJSON(o)},toString:function(){return"Sammy.EventContext: "+[this.verb,this.path,this.params].join(" ")}});g.sammy=i.Sammy=n})(jQuery,window);