├── .autotest ├── .bundle └── config ├── .gemtest ├── .gitignore ├── .rvmrc ├── CHANGELOG ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENCE ├── README.markdown ├── Rakefile ├── bin └── testbot ├── lib ├── generators │ └── testbot │ │ ├── templates │ │ ├── testbot.rake.erb │ │ └── testbot.yml.erb │ │ └── testbot_generator.rb ├── railtie.rb ├── requester │ └── requester.rb ├── runner │ ├── job.rb │ ├── runner.rb │ └── safe_result_text.rb ├── server │ ├── build.rb │ ├── group.rb │ ├── job.rb │ ├── memory_model.rb │ ├── runner.rb │ ├── server.rb │ └── status │ │ ├── javascripts │ │ └── jquery-1.4.4.min.js │ │ ├── status.html │ │ └── stylesheets │ │ └── status.css ├── shared │ ├── adapters │ │ ├── adapter.rb │ │ ├── cucumber_adapter.rb │ │ ├── helpers │ │ │ └── ruby_env.rb │ │ ├── rspec2_adapter.rb │ │ ├── rspec_adapter.rb │ │ └── test_unit_adapter.rb │ ├── color.rb │ ├── simple_daemonize.rb │ ├── ssh_tunnel.rb │ ├── testbot.rb │ └── version.rb ├── tasks │ └── testbot.rake └── testbot.rb ├── script └── test ├── test ├── fixtures │ └── local │ │ ├── Rakefile │ │ ├── config │ │ └── testbot.yml │ │ ├── log │ │ └── test.log │ │ ├── script │ │ └── spec │ │ ├── spec │ │ ├── models │ │ │ ├── car_spec.rb │ │ │ └── house_spec.rb │ │ └── spec.opts │ │ └── tmp │ │ └── restart.txt ├── integration_test.rb ├── requester │ ├── requester_test.rb │ ├── testbot.yml │ └── testbot_with_erb.yml ├── runner │ ├── job_test.rb │ └── safe_result_text_test.rb ├── server │ ├── group_test.rb │ └── server_test.rb └── shared │ ├── adapters │ ├── adapter_test.rb │ ├── cucumber_adapter_test.rb │ ├── helpers │ │ └── ruby_env_test.rb │ └── rspec_adapter_test.rb │ └── testbot_test.rb └── testbot.gemspec /.autotest: -------------------------------------------------------------------------------- 1 | Autotest.add_hook :initialize do |at| 2 | 3 | # Makes change detection much faster (but CPU usage higher) 4 | at.sleep = 0.2 5 | 6 | end 7 | -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_BIN: .bundle/bin 3 | -------------------------------------------------------------------------------- /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/.gemtest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .deploy_config.yml 2 | *.db 3 | pkg 4 | todo.txt 5 | *.pid 6 | instance* 7 | **/*.rbc 8 | *.sh 9 | */*.swp 10 | *.swp 11 | */**/.*.swp 12 | */**/*.swp 13 | */*/*/*.swp 14 | tmp 15 | .bundle/bin 16 | script 17 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm ruby-1.9.3-p484@testbot --create 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.7.9 2 | 3 | Loosen sinatra dependency version constraint. 4 | 5 | 0.7.8 6 | 7 | Fix test job bug that caused multiple jobs to use the same TEST_ENV_NUMBER. 8 | 9 | This caused multiple processes to use the same databases, etc. 10 | 11 | If you're seeing database deadlocks in tests, this will probably fix that. 12 | 13 | 0.7.7 14 | 15 | Exit when file syncing fails so that you don't run tests for a version of the 16 | code you did not expect. 17 | 18 | 0.7.5 19 | 20 | Fixed bug where a runner updating without a result would fail. 21 | 22 | Also dropping official support for 1.8 as it's such a hassle to maintain. 23 | 24 | 0.7.3 25 | 26 | Increased time between updates to avoid timeouts. Allow timeout when posting 27 | incremental build results (but not when posting the final results). 28 | 29 | Should make testbot more stable for some people. 30 | 31 | 0.7.2 32 | 33 | Increased timeout on http requests to the server to 10 seconds. 34 | 35 | 0.7.1 36 | 37 | Fixed a bug introduced in 0.7.0. The TEST_INSTANCES env-variable are now 38 | present again when running before_run scripts. 39 | 40 | 0.7.0 41 | 42 | Added per project rvm support. Using this you can run testbot using system ruby 43 | while being able to run test processes using the project rvm environment. This 44 | is also useful if you have several projects with different rvm rubies and gemsets. 45 | 46 | 0.6.9 47 | 48 | Removed dependency on macaddr as that does not work on some platforms. 49 | 50 | 0.6.8 51 | 52 | Fixed broken gem lookup in ruby 1.9. 53 | 54 | 0.6.6 55 | 56 | The runner will now run config/testbot/before_run.rb if it exists. 57 | 58 | Replaced the old process code with the posix-spawn gem's methods. This fixed 59 | process shutdown in my current setup. Hopefully more stable overall. 60 | 61 | The requester will now request that the build is stopped when exiting early (like when killed), 62 | and not only on ctrl+c (SIGINT). 63 | 64 | Most of this was based on a fork by https://github.com/Skalar. 65 | 66 | 0.6.5 67 | 68 | Fixed test unit require path so that it works with ruby 1.9. 69 | 70 | 0.6.4 71 | 72 | Fixed bug with running test unit tests that caused errors like '/bin/ruby:1: Invalid char `\317' in expression`'. 73 | 74 | Now handling errors creating builds. 75 | 76 | Returning 503 and reporting that there are no runners when that is the case. 77 | 78 | 0.6.3 79 | 80 | Results are now shown as they happen. It takes care not to break escape codes used for 81 | coloring output (only tested with rspec, add a script/spec file containing "rspec --tty $@"). 82 | 83 | Also, you can add a "logging: true" option to testbot.yml to see when files are synced, etc. 84 | 85 | 0.6.2 86 | 87 | Fixed "invalid byte sequence in UTF-8" errors cased by test output in some cases. 88 | 89 | 0.6.1 90 | 91 | Fixed CPU core check for OSX Lion. 92 | 93 | Updated sinatra requirement so that it's a bit less strict 94 | (https://github.com/joakimk/testbot/pull/41). 95 | 96 | 0.6.0 97 | 98 | Fixed bug that caused the runner to crash and block the server after checking for jobs (issue #34). 99 | 100 | Merged in https://github.com/joakimk/testbot/pull/33 (Bugfix: Run rake task testbot:before_run with bundler if present). 101 | 102 | 0.5.9 103 | 104 | When you hit ctrl+c all related test jobs will be stopped (within about 5 seconds). You can 105 | now exit, change code and re-run much faster than before. 106 | 107 | It does not stop before_run or code fetch. But no tests will be run after 108 | those complete when the build is stopped. 109 | 110 | 0.5.8 111 | 112 | Now only running tests from one build on a runner at a time. 113 | 114 | This is mostly usability. Testbot should behave like when you're running your tests locally. 115 | 116 | 0.5.7 117 | 118 | Added @meeiw's patch to support ERB in config. Added test result summarization for RSpec and Cucumber. 119 | 120 | 0.5.6 121 | 122 | Removed CPU usage check before running jobs (issue #25). 123 | 124 | 0.5.5 125 | 126 | Added RSpec2 support. Thanks to Bryan Helmkamp, https://github.com/brynary. 127 | 128 | 0.5.4 129 | 130 | Fixed some typos that caused the ruby interpreter to always be "ruby". 131 | 132 | 0.5.3 133 | 134 | Added support for rubygems-test and http://gem-testers.org. 135 | 136 | 0.5.1 137 | 138 | Fixed a bug that caused exit status to be 1 even if the test run is successful. 139 | 140 | 0.5.0 141 | 142 | Made the status page load faster but check less often. 143 | 144 | 0.4.9 145 | 146 | There is now a very basic status page at http://testbot_server:2288/status. 147 | 148 | 0.4.8 149 | 150 | Removed all remaining native dependencies to make testbot simpler to install. 151 | 152 | 0.4.7 153 | 154 | Refactored the code into modules with one directory for each. No longer dependent on mongel, now using webrick. 155 | 156 | 0.4.6 157 | 158 | Fixed a bug that caused auto_update not to check for an update after a job had been run. 159 | 160 | 0.4.5 161 | 162 | Made auto_update a bit more reliable. 163 | 164 | 0.4.4 165 | 166 | Changed to using the deamons gem. Seems to fix some problems with old processes 167 | not closing down when using auto_update. 168 | 169 | 0.4.2 170 | 171 | Added support for quick deploys when developing testbot. 172 | 173 | 0.4.0 174 | 175 | Fixed RSpec 2 compability issue. 176 | 177 | 0.3.9 178 | 179 | Added support for running tests with bundler exec. 180 | 181 | 0.3.8 182 | 183 | Testbot now uses exit code to determine if the test run has failed. 184 | 185 | 0.3.7 186 | 187 | The runner now checks right away for another job if it got one last time around. 188 | 189 | 0.3.6 190 | 191 | Added Rails 2 generator support. 192 | 193 | 0.3.5 194 | 195 | Fixed ssh_tunnel bug when using a custom user. 196 | 197 | 0.3.4 198 | 199 | Bundler support. Testbot now runs "bundle" before "testbot:before_run" if you 200 | have "bundle" in path and have a "Gemfile" in the project so that the 201 | rails environment can load. 202 | 203 | 0.3.3 204 | 205 | Fixed loading issue in rails 2, testbot depended on having rails installed as a gem. 206 | 207 | 0.3.0 208 | 209 | Made testbot into a gem. Simplified the design. Added a CLI. Removed git support. 210 | 211 | 0.2.3 212 | 213 | Added JRuby support. 214 | 215 | 0.2.2 216 | 217 | Replaced runtime optimization code with a simpler file size based method. 218 | 219 | 0.2.1 220 | 221 | Added support for Test:Unit and multiple projects. 222 | 223 | 0.2.0 224 | 225 | Added "ssh_tunnel" so that you don't have to be able to access the http server directly. 226 | 227 | 0.1.9 228 | 229 | The server can now handle if a runner is shutdown in the middle of a test run (it will give the 230 | job to another runner). 231 | 232 | 0.1.8 233 | 234 | Added cucumber support and failure detection to the client. Added docs. Added code that uses 235 | historic test runtimes to reduce test runtime (in theory). 236 | 237 | 0.1.7 238 | 239 | Added a basic client so that testbot can be installed as a rails plugin. 'rake testbot:spec'. 240 | 241 | 0.1.6 242 | 243 | Now only fetching the code once, and only keeping one copy of it. Less overhead. 244 | 245 | 0.1.5 246 | 247 | Added "/runners/total_instances" to be able to balance requesters by knowing how many instances 248 | there are in total. 249 | 250 | 0.1.4 251 | 252 | Added support for fetching the latest version from git instead of rsync when doing a test run. 253 | 254 | 0.1.3 255 | 256 | Added "/runners/available" to see how many runners are available. 257 | 258 | 0.1.2 259 | 260 | Added cucumber support. 261 | 262 | 0.1.1 263 | 264 | Added a CPU usage check to make sure that the runner does not start a job when the computer is busy. 265 | 266 | 0.1 267 | 268 | Added hostname and STDERR to runner results. 269 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | testbot (0.7.8) 5 | acts_as_rails3_generator 6 | daemons (>= 1.0.10) 7 | httparty (>= 0.6.1) 8 | json_pure (>= 1.4.6) 9 | net-ssh (>= 2.0.23) 10 | posix-spawn (>= 0.3.6) 11 | sinatra (~> 1.0.0) 12 | 13 | GEM 14 | remote: http://rubygems.org/ 15 | specs: 16 | activesupport (4.0.2) 17 | i18n (~> 0.6, >= 0.6.4) 18 | minitest (~> 4.2) 19 | multi_json (~> 1.3) 20 | thread_safe (~> 0.1) 21 | tzinfo (~> 0.3.37) 22 | acts_as_rails3_generator (0.0.1) 23 | atomic (1.1.14) 24 | celluloid (0.15.2) 25 | timers (~> 1.1.0) 26 | celluloid-io (0.15.0) 27 | celluloid (>= 0.15.0) 28 | nio4r (>= 0.5.0) 29 | coderay (1.1.0) 30 | daemons (1.1.9) 31 | ffi (1.9.3) 32 | flexmock (1.3.3) 33 | formatador (0.2.4) 34 | guard (2.4.0) 35 | formatador (>= 0.2.4) 36 | listen (~> 2.1) 37 | lumberjack (~> 1.0) 38 | pry (>= 0.9.12) 39 | thor (>= 0.18.1) 40 | guard-test (2.0.4) 41 | guard (~> 2.0) 42 | test-unit (~> 2.2) 43 | httparty (0.13.3) 44 | json (~> 1.8) 45 | multi_xml (>= 0.5.2) 46 | i18n (0.6.9) 47 | json (1.8.2) 48 | json_pure (1.8.2) 49 | listen (2.5.0) 50 | celluloid (>= 0.15.2) 51 | celluloid-io (>= 0.15.0) 52 | rb-fsevent (>= 0.9.3) 53 | rb-inotify (>= 0.9) 54 | lumberjack (1.0.4) 55 | method_source (0.8.2) 56 | minitest (4.7.5) 57 | multi_json (1.8.4) 58 | multi_xml (0.5.5) 59 | net-ssh (2.9.2) 60 | nio4r (1.0.0) 61 | posix-spawn (0.3.9) 62 | pry (0.9.12.6) 63 | coderay (~> 1.0) 64 | method_source (~> 0.8) 65 | slop (~> 3.4) 66 | rack (1.5.2) 67 | rack-test (0.6.2) 68 | rack (>= 1.0) 69 | rake (0.8.7) 70 | rb-fsevent (0.9.4) 71 | rb-inotify (0.9.3) 72 | ffi (>= 0.5.0) 73 | rvm (1.11.3.9) 74 | shoulda (3.5.0) 75 | shoulda-context (~> 1.0, >= 1.0.1) 76 | shoulda-matchers (>= 1.4.1, < 3.0) 77 | shoulda-context (1.1.6) 78 | shoulda-matchers (2.5.0) 79 | activesupport (>= 3.0.0) 80 | sinatra (1.0) 81 | rack (>= 1.0) 82 | slop (3.4.7) 83 | test-unit (2.5.5) 84 | thor (0.18.1) 85 | thread_safe (0.1.3) 86 | atomic 87 | timers (1.1.0) 88 | tzinfo (0.3.38) 89 | 90 | PLATFORMS 91 | ruby 92 | 93 | DEPENDENCIES 94 | bundler 95 | flexmock 96 | guard 97 | guard-test 98 | rack-test 99 | rake (= 0.8.7) 100 | rvm 101 | shoulda 102 | testbot! 103 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'test', :all_after_pass => false, :all_on_start => false do 2 | watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" } 3 | watch(%r{^test/.+_test\.rb$}) 4 | end 5 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Joakim Kolsjö 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## Not maintained 2 | 3 | This is not maintained by me anymore. If you want to take over as maintainer for the project, please let me know. 4 | 5 | Testbot has helped me ship well tested software for 7+ years, but it hasn't been well maintained for quite a while and I don't use it myself anymore. 6 | 7 | I recommend looking at a CI service like [CircleCI](https://circleci.com/) for parallel tests and [Knapsack Pro](https://knapsackpro.com/) for test balancing. I've seen the same performance of 10 CircleCI containers with Knapsack Pro balancing compared to 16 CPU cores with testbot. It's a bit more expensive to run, but it's easier to maintain. 8 | 9 | ## Old readme 10 | 11 | Testbot is a test distribution tool that works with Rails, RSpec, RSpec2, Test::Unit and Cucumber. The basic idea is that you let testbot spread the load of running your tests across multiple machines to make the tests run faster. 12 | 13 | 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). 14 | 15 | If you intend to use testbot with cloud computing (like EC2), take a look at [TestbotCloud](https://github.com/joakimk/testbot_cloud). 16 | 17 | Installing 18 | ---- 19 | 20 | gem install testbot 21 | 22 | Try it out 23 | ---- 24 | 25 | testbot --server 26 | testbot --runner --connect localhost 27 | sleep 5 # wait for the runner to register with the server 28 | 29 | mkdir -p testbotdemo/test; cd testbotdemo 30 | echo 'require "test/unit"' > test/demo_test.rb 31 | echo 'class DemoTest < Test::Unit::TestCase; def test_first; end; end' >> test/demo_test.rb 32 | 33 | testbot --test --connect localhost 34 | 35 | # Cleanup 36 | testbot --server stop 37 | testbot --runner stop 38 | cd ..; rm -rf testbotdemo 39 | rm -rf /tmp/testbot 40 | 41 | The project files from the demo project are synced to /tmp/testbot/$USER (default). The runner syncs the files to /tmp/testbot/project (default). The tests are then run and the results returned through the server and displayed. 42 | 43 | How it works 44 | ---- 45 | 46 | Testbot is: 47 | 48 | * A **server** to distribute test jobs. 49 | * One or more **runners** to run test jobs and return the results (this is the "worker" process). 50 | * One or more **requesters** that tells the server which tests to distribute and displays the results (the client you use to run tests, for example: **rake testbot:spec**). 51 | 52 |
 53 |     Requester -- (files to run) --> Server -- (files to run) --> (many-)Runner(s)
 54 |         ^                           |    ^                                  |
 55 |         |---------------------------|    |----------------------------------|
 56 |                  (results)                            (results)
 57 | 
58 | 59 | Example setup 60 | ---- 61 | 62 | Here I make the assumption that you have a user called **testbot** on a server at **192.168.0.100** that every computer [can log into without a password](http://github.com/joakimk/testbot/wiki/SSH-Public-Key-Authentication) and that you have **installed testbot** on each computer. 63 | 64 | ssh testbot@192.168.0.100 65 | testbot --server 66 | 67 | On every computer that should share CPU resources run: 68 | 69 | testbot --runner --connect 192.168.0.100 70 | 71 | Running tests: 72 | 73 | testbot --test --connect 192.168.0.100 74 | # --test could also be --spec (RSpec), --rspec (RSpec 2) or --features 75 | 76 | Using testbot with Rails 2: 77 | 78 | # Add testbot to your Gemfile if you use bundler. You also need the plugin because 79 | # Rails 2 does not load raketasks from gems. 80 | ruby script/plugin install git://github.com/joakimk/testbot.git -r 'refs/tags/v0.7.8' 81 | script/generate testbot --connect 192.168.0.100 82 | 83 | rake testbot:spec (or :rspec, :test, :features) 84 | 85 | Using testbot with Rails 3: 86 | 87 | rails g testbot --connect 192.168.0.100 88 | rake testbot:spec (or :rspec, :test, :features) 89 | 90 | # Gemfile: 91 | gem 'testbot' 92 | 93 | You can keep track of the testbots on: 94 | 95 | http://192.168.0.100:2288/status 96 | 97 | Updating testbot 98 | ---- 99 | 100 | To simplify updates there is a **--auto_update** option for the runner. The runner processes that use this option will be automatically updated and restarted when you change the server version. 101 | 102 | This requires testbot to be installed **without sudo** as the update simply runs "gem install testbot -v new_version". I recommend using [RVM](http://rvm.beginrescueend.com/) (it handles paths correctly). 103 | 104 | Example: 105 | testbot --runner --connect 192.168.0.100 --auto_update 106 | 107 | More options 108 | ---- 109 | 110 | testbot (or testbot --help) 111 | 112 | Could this readme be better somehow? 113 | ---- 114 | 115 | If there is anything missing or unclear you can create an [issue](http://github.com/joakimk/testbot/issues) (or send me a pull request). 116 | 117 | Features 118 | ---- 119 | * You can add and remove computers at any time. Testbot simply gives abandoned jobs to other computers. 120 | * Testbot will try to balance the testload so that every computer finishes running the tests at the same time to reduce the time it takes to run the entire test suite. It does a good job, but has potential for further improvement. 121 | * You can access your testbot network through SSH by using the built in SSH tunneling code. 122 | * You can use the same testbot network with multiple projects. 123 | * You can abort a test run with ctrl+c and all remote processes will be stopped. 124 | * It shows you the output as it happens. 125 | 126 | Contributing to testbot 127 | ---- 128 | 129 | First, get the tests to run: 130 | 131 | bundle 132 | rake 133 | 134 | For development I recommend using guard. 135 | 136 | # OSX needs: gem install rb-fsevent 137 | guard 138 | 139 | Make your change (don't forget to write tests) and send me a pull request. 140 | 141 | You can also contribute by adding to the [wiki](http://github.com/joakimk/testbot/wiki). 142 | 143 | How to add support for more test frameworks and/or programming languages 144 | ---- 145 | 146 | Add a **lib/shared/adapters/framework_name_adapter.rb** file and update this readme. 147 | 148 | More 149 | ---- 150 | 151 | * Check the [wiki](http://github.com/joakimk/testbot/wiki) for more info. 152 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | 3 | Bundler::GemHelper.install_tasks 4 | 5 | task :default => [ :test ] do 6 | end 7 | 8 | desc "Run Test::Unit tests" 9 | task :test do 10 | Dir["test/**/*_test.rb"].each { |test| require(File.expand_path(test)) } 11 | end 12 | 13 | 14 | desc "Used for quickly deploying and testing updates without pusing to rubygems.org" 15 | task :deploy do 16 | File.open("DEV_VERSION", "w") { |f| f.write(".DEV.#{Time.now.to_i}") } 17 | 18 | gem_file = "testbot-#{Testbot.version}.gem" 19 | config = YAML.load_file(".deploy_config.yml") 20 | Rake::Task["build"].invoke 21 | 22 | begin 23 | system(config["upload_gem"].gsub(/GEM_FILE/, gem_file)) || fail 24 | system(config["update_server"].gsub(/GEM_FILE/, gem_file)) || fail 25 | system(config["restart_server"]) || fail 26 | ensure 27 | system("rm DEV_VERSION") 28 | end 29 | end 30 | 31 | desc "Used to restart the server when developing testbot" 32 | task :restart do 33 | config = YAML.load_file(".deploy_config.yml") 34 | system(config["restart_server"]) || fail 35 | end 36 | -------------------------------------------------------------------------------- /bin/testbot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '../lib/shared/testbot.rb')) 4 | 5 | def show_help 6 | puts "Testbot #{Testbot.version}" 7 | puts 8 | puts "Testbot is a test distribution tool that works with Rails, RSpec, Test::Unit and Cucumber." 9 | puts 10 | puts "More info: http://github.com/joakimk/testbot" 11 | puts "Wiki: http://github.com/joakimk/testbot/wiki" 12 | puts 13 | puts "Examples:" 14 | puts " testbot --server" 15 | puts " testbot --runner --connect 192.168.0.100" 16 | puts " testbot --test --connect 192.168.0.100" 17 | puts 18 | puts "Types:" 19 | puts " --server " 20 | puts " --runner " 21 | Adapter.all.each do |adapter| 22 | puts " --#{adapter.type}\t\t\t# Run #{adapter.name} tests" 23 | end 24 | puts 25 | puts "Runner options:" 26 | puts " --connect \t# Which server to use (required)" 27 | puts " --working_dir \t\t# Where to store temporary files (default: #{Testbot::DEFAULT_WORKING_DIR})" 28 | puts " --cpus \t\t# The number of CPU cores to use (default: use all)" 29 | puts " --ssh_tunnel\t\t\t# Use a ssh tunnel" 30 | puts " --user \t\t# Use a custom ssh tunnel user (default: testbot)" 31 | puts " --auto_update\t\t\t# Keep testbot updated to the same version as the server." 32 | puts " --max_jruby_instances \t# To use less instances when running JRuby (as it requires more memory)" 33 | puts " --jruby_opts <-J-X...>\t# Options to JRuby." 34 | puts 35 | puts "Test options:" 36 | puts " --connect \t# Which server to use (required)" 37 | puts " --rsync_ignores \t# Files to ignore when syncing (default: include all)" 38 | puts " --rsync_path \t\t# Sync path on the server (default: #{Testbot::DEFAULT_SERVER_PATH})" 39 | puts " --ssh_tunnel\t\t\t# Use a ssh tunnel" 40 | puts " --user \t\t# Use a custom rsync / ssh tunnel user (default: #{Testbot::DEFAULT_USER})" 41 | puts " --project \t# Use a custom project name (default: #{Testbot::DEFAULT_PROJECT})" 42 | puts 43 | puts "Other:" 44 | puts " --help\t\t\t# Show help (this page)" 45 | puts " --version\t\t\t# Show the testbot version" 46 | 47 | # puts " # (when the server gem version is changed," 48 | # puts " # this runs gem install with the same version" 49 | # puts " # and restarts the test runner)" 50 | 51 | 52 | # TODO: 53 | # puts " --use_git_ignore # Don't rsync files that are ignored by git" 54 | # puts " --status # Show running background processes" 55 | # puts " --port # Use a custom port" 56 | 57 | end 58 | 59 | show_help unless Testbot::CLI.run(ARGV) 60 | -------------------------------------------------------------------------------- /lib/generators/testbot/templates/testbot.rake.erb: -------------------------------------------------------------------------------- 1 | namespace :testbot do 2 | task :before_request do 3 | # This is run after you start a request (ex: rake testbot:spec) 4 | end 5 | 6 | task :before_run do 7 | # This is run by the runner after files are synced but before tests are run 8 | 9 | # Example: Setting up a test database 10 | database_yml = <<-DB_CONFIG 11 | test: 12 | adapter: mysql 13 | encoding: utf8 14 | database: <%= options[:project] %>_testbot_test<%%= ENV['TEST_ENV_NUMBER'] %> 15 | username: root 16 | password: 17 | host: localhost 18 | DB_CONFIG 19 | 20 | # database_file_path = "config/database.yml" 21 | # File.open(database_file_path, 'w') { |f| f.write(database_yml) } 22 | # 23 | # # Setup databases for all instances 24 | # 0.upto(ENV['TEST_INSTANCES'].to_i - 1) do |instance| 25 | # test_env_number = (instance == 0) ? '' : instance + 1 26 | # system "mysqladmin -u root -f drop <%= options[:project] %>_testbot_test#{test_env_number} > /dev/null 2>&1" 27 | # system "mysqladmin -u root -f create <%= options[:project] %>_testbot_test#{test_env_number} > /dev/null 2>&1" 28 | # system "export RAILS_ENV=test; export TEST_ENV_NUMBER=#{test_env_number}; rake db:test:load" 29 | # end 30 | 31 | # Example: Building gems 32 | # system "rm vendor/gems/*/ext/**/*.o > /dev/null 2>&1" 33 | # system "rake gems:build:force > /dev/null 2>&1" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/testbot/templates/testbot.yml.erb: -------------------------------------------------------------------------------- 1 | # You can use ERB here. 2 | 3 | # Which server to use. 4 | server_host: <%= options[:connect] %> 5 | <%- if options[:user] -%> 6 | server_user: <%= options[:user] %> 7 | <%- else -%> 8 | # server_user: <%= Testbot::DEFAULT_USER %> 9 | <%- end -%> 10 | 11 | # Project prefix. Used for prefixing project files 12 | # so that you can run multiple projects in the same testbot network. 13 | <%- if options[:project] %> 14 | project: <%= options[:project] %> 15 | <%- else -%> 16 | # project: <%= Testbot::DEFAULT_PROJECT %> 17 | <%- end -%> 18 | 19 | # RSync settings. The folder where your files are synced to 20 | # and then fetched from before running the tests. 21 | <%- if options[:rsync_path] -%> 22 | rsync_path: <%= options[:rsync_path] %> 23 | <%- else -%> 24 | # rsync_path: <%= Testbot::DEFAULT_SERVER_PATH %> 25 | <%- end -%> 26 | <%- if options[:rsync_ignores] -%> 27 | rsync_ignores: <%= options[:rsync_ignores] %> 28 | <%- else -%> 29 | # rsync_ignores: 30 | <%- end -%> 31 | 32 | # To tunnel traffic through SSH 33 | <%- if options[:ssh_tunnel] -%> 34 | ssh_tunnel: true 35 | <%- else -%> 36 | # ssh_tunnel: true 37 | <%- end -%> 38 | 39 | # Runner usage. Set to a lower percentage to not use 40 | # every available instance or higher to create more 41 | # jobs than there are instances. 42 | available_runner_usage: 100% 43 | 44 | # Enable more logging from the requester 45 | # logging: true 46 | -------------------------------------------------------------------------------- /lib/generators/testbot/testbot_generator.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/../../shared/testbot") 2 | require "acts_as_rails3_generator" 3 | 4 | class TestbotGenerator < Rails::Generators::Base 5 | source_root File.expand_path('../templates', __FILE__) 6 | 7 | class_option :connect, :type => :string, :required => true, :desc => "Which server to use (required)" 8 | class_option :project, :type => :string, :default => nil, :desc => "The name of your project (default: #{Testbot::DEFAULT_PROJECT})" 9 | class_option :rsync_path, :type => :string, :default => nil, :desc => "Sync path on the server (default: #{Testbot::DEFAULT_SERVER_PATH})" 10 | class_option :rsync_ignores, :type => :string, :default => nil, :desc => "Files to rsync_ignores when syncing (default: include all)" 11 | class_option :ssh_tunnel, :type => :boolean, :default => nil, :desc => "Use a ssh tunnel" 12 | class_option :user, :type => :string, :default => nil, :desc => "Use a custom rsync / ssh tunnel user (default: #{Testbot::DEFAULT_USER})" 13 | 14 | def generate_config 15 | template "testbot.yml.erb", "config/testbot.yml" 16 | template "testbot.rake.erb", "lib/tasks/testbot.rake" 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /lib/railtie.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rails' 3 | @rails_loaded = true 4 | rescue LoadError => ex 5 | @rails_loaded = false 6 | end 7 | 8 | if @rails_loaded 9 | module Testbot 10 | class Railtie < Rails::Railtie 11 | rake_tasks do 12 | load File.expand_path(File.join(File.dirname(__FILE__), "tasks/testbot.rake")) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/requester/requester.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'httparty' 3 | require 'ostruct' 4 | require 'erb' 5 | require File.dirname(__FILE__) + '/../shared/ssh_tunnel' 6 | require File.expand_path(File.dirname(__FILE__) + '/../shared/testbot') 7 | 8 | class Hash 9 | def symbolize_keys_without_active_support 10 | inject({}) do |options, (key, value)| 11 | options[(key.to_sym rescue key) || key] = value 12 | options 13 | end 14 | end 15 | end 16 | 17 | module Testbot::Requester 18 | 19 | class Requester 20 | 21 | attr_reader :config 22 | 23 | def initialize(config = {}) 24 | config = config.symbolize_keys_without_active_support 25 | config[:rsync_path] ||= Testbot::DEFAULT_SERVER_PATH 26 | config[:project] ||= Testbot::DEFAULT_PROJECT 27 | config[:server_user] ||= Testbot::DEFAULT_USER 28 | config[:available_runner_usage] ||= Testbot::DEFAULT_RUNNER_USAGE 29 | @config = OpenStruct.new(config) 30 | end 31 | 32 | def run_tests(adapter, dir) 33 | puts if config.simple_output || config.logging 34 | 35 | if config.ssh_tunnel 36 | log "Setting up ssh tunnel" do 37 | SSHTunnel.new(config.server_host, config.server_user, adapter.requester_port).open 38 | end 39 | server_uri = "http://127.0.0.1:#{adapter.requester_port}" 40 | else 41 | server_uri = "http://#{config.server_host}:#{Testbot::SERVER_PORT}" 42 | end 43 | 44 | log "Syncing files" do 45 | rsync_ignores = config.rsync_ignores.to_s.split.map { |pattern| "--exclude='#{pattern}'" }.join(' ') 46 | system("rsync -az --delete --delete-excluded -e ssh #{rsync_ignores} . #{rsync_uri}") 47 | 48 | exitstatus = $?.exitstatus 49 | unless exitstatus == 0 50 | puts "rsync failed with exit code #{exitstatus}" 51 | exit 1 52 | end 53 | end 54 | 55 | files = adapter.test_files(dir) 56 | sizes = adapter.get_sizes(files) 57 | 58 | build_id = nil 59 | log "Requesting run" do 60 | response = HTTParty.post("#{server_uri}/builds", :body => { :root => root, 61 | :type => adapter.type.to_s, 62 | :project => config.project, 63 | :available_runner_usage => config.available_runner_usage, 64 | :files => files.join(' '), 65 | :sizes => sizes.join(' '), 66 | :jruby => jruby? }).response 67 | 68 | if response.code == "503" 69 | puts "No runners available. If you just started a runner, try again. It usually takes a few seconds before they're available." 70 | return false 71 | elsif response.code != "200" 72 | puts "Could not create build, #{response.code}: #{response.body}" 73 | return false 74 | else 75 | build_id = response.body 76 | end 77 | end 78 | 79 | at_exit do 80 | unless ENV['IN_TEST'] || @done 81 | log "Notifying server we want to stop the run" do 82 | HTTParty.delete("#{server_uri}/builds/#{build_id}") 83 | end 84 | end 85 | end 86 | 87 | puts if config.logging 88 | 89 | last_results_size = 0 90 | success = true 91 | error_count = 0 92 | while true 93 | sleep 0.5 94 | 95 | begin 96 | @build = HTTParty.get("#{server_uri}/builds/#{build_id}", :format => :json) 97 | next unless @build 98 | rescue Exception => ex 99 | error_count += 1 100 | if error_count > 4 101 | puts "Failed to get status: #{ex.message}" 102 | error_count = 0 103 | end 104 | next 105 | end 106 | 107 | results = @build['results'][last_results_size..-1] 108 | unless results == '' 109 | if config.simple_output 110 | print results.gsub(/[^\.F]|Finished/, '') 111 | STDOUT.flush 112 | else 113 | print results 114 | STDOUT.flush 115 | end 116 | end 117 | 118 | last_results_size = @build['results'].size 119 | 120 | break if @build['done'] 121 | end 122 | 123 | puts if config.simple_output 124 | 125 | if adapter.respond_to?(:sum_results) 126 | puts "\n" + adapter.sum_results(@build['results']) 127 | end 128 | 129 | @done = true 130 | @build["success"] 131 | end 132 | 133 | def self.create_by_config(path) 134 | Requester.new(YAML.load(ERB.new(File.open(path).read).result)) 135 | end 136 | 137 | private 138 | 139 | def log(text) 140 | if config.logging 141 | print "#{text}... "; STDOUT.flush 142 | yield 143 | puts "done" 144 | else 145 | yield 146 | end 147 | end 148 | 149 | def root 150 | if localhost? 151 | config.rsync_path 152 | else 153 | "#{config.server_user}@#{config.server_host}:#{config.rsync_path}" 154 | end 155 | end 156 | 157 | def rsync_uri 158 | localhost? ? config.rsync_path : "#{config.server_user}@#{config.server_host}:#{config.rsync_path}" 159 | end 160 | 161 | def localhost? 162 | [ '0.0.0.0', 'localhost', '127.0.0.1' ].include?(config.server_host) 163 | end 164 | 165 | def jruby? 166 | RUBY_PLATFORM =~ /java/ || !!ENV['USE_JRUBY'] 167 | end 168 | 169 | end 170 | 171 | end 172 | -------------------------------------------------------------------------------- /lib/runner/job.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), 'runner.rb')) 2 | require File.expand_path(File.join(File.dirname(__FILE__), 'safe_result_text.rb')) 3 | require 'posix/spawn' 4 | 5 | module Testbot::Runner 6 | class Job 7 | attr_reader :root, :project, :build_id 8 | 9 | TIME_TO_WAIT_BETWEEN_POSTING_RESULTS = 5 10 | 11 | def initialize(runner, id, build_id, project, root, type, ruby_interpreter, files) 12 | @runner, @id, @build_id, @project, @root, @type, @ruby_interpreter, @files = 13 | runner, id, build_id, project, root, type, ruby_interpreter, files 14 | @success = true 15 | end 16 | 17 | def jruby? 18 | @ruby_interpreter == 'jruby' 19 | end 20 | 21 | def run(instance) 22 | return if @killed 23 | puts "Running job #{@id} (build #{@build_id})... " 24 | test_env_number = (instance == 0) ? '' : instance + 1 25 | result = "\n#{`hostname`.chomp}:#{Dir.pwd}\n" 26 | base_environment = "export RAILS_ENV=test; export TEST_ENV_NUMBER=#{test_env_number}; cd #{@project};" 27 | 28 | adapter = Adapter.find(@type) 29 | run_time = measure_run_time do 30 | result += run_and_return_result("#{base_environment} #{adapter.command(@project, ruby_cmd, @files)}") 31 | end 32 | 33 | Server.put("/jobs/#{@id}", :body => { :result => SafeResultText.clean(result), :status => status, :time => run_time }) 34 | puts "Job #{@id} finished." 35 | end 36 | 37 | def kill!(build_id) 38 | if @build_id == build_id && @pid 39 | kill_processes 40 | @killed = true 41 | end 42 | end 43 | 44 | private 45 | 46 | def kill_processes 47 | # Kill process and its children (processes in the same group) 48 | Process.kill('KILL', -@pid) rescue :failed_to_kill_process 49 | end 50 | 51 | def status 52 | success? ? "successful" : "failed" 53 | end 54 | 55 | def measure_run_time 56 | start_time = Time.now 57 | yield 58 | (Time.now - start_time) * 100 59 | end 60 | 61 | def post_results(output) 62 | Server.put("/jobs/#{@id}", :body => { :result => SafeResultText.clean(output), :status => "building" }) 63 | rescue Timeout::Error 64 | puts "Got a timeout when posting an job result update. This can happen when the server is busy and is not a critical error." 65 | end 66 | 67 | def run_and_return_result(command) 68 | read_pipe = spawn_process(command) 69 | 70 | output = "" 71 | last_post_time = Time.now 72 | while char = read_pipe.getc 73 | char = (char.is_a?(Fixnum) ? char.chr : char) # 1.8 <-> 1.9 74 | output << char 75 | if Time.now - last_post_time > TIME_TO_WAIT_BETWEEN_POSTING_RESULTS 76 | post_results(output) 77 | last_post_time = Time.now 78 | end 79 | end 80 | 81 | # Kill child processes, if any 82 | kill_processes 83 | 84 | output 85 | end 86 | 87 | def spawn_process(command) 88 | read_pipe, write_pipe = IO.pipe 89 | @pid = POSIX::Spawn::spawn(command, :err => write_pipe, :out => write_pipe, :pgroup => true) 90 | 91 | Thread.new do 92 | Process.waitpid(@pid) 93 | @success = ($?.exitstatus == 0) 94 | write_pipe.close 95 | end 96 | 97 | read_pipe 98 | end 99 | 100 | def success? 101 | @success 102 | end 103 | 104 | def ruby_cmd 105 | if @ruby_interpreter == 'jruby' && @runner.config.jruby_opts 106 | 'jruby ' + @runner.config.jruby_opts 107 | else 108 | @ruby_interpreter 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/runner/runner.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'httparty' 3 | require 'ostruct' 4 | require File.expand_path(File.dirname(__FILE__) + '/../shared/ssh_tunnel') 5 | require File.expand_path(File.dirname(__FILE__) + '/../shared/adapters/adapter') 6 | require File.expand_path(File.dirname(__FILE__) + '/job') 7 | 8 | module Testbot::Runner 9 | TIME_BETWEEN_NORMAL_POLLS = 1 10 | TIME_BETWEEN_QUICK_POLLS = 0.1 11 | TIME_BETWEEN_PINGS = 5 12 | TIME_BETWEEN_VERSION_CHECKS = Testbot.version.include?('.DEV.') ? 10 : 60 13 | 14 | class CPU 15 | 16 | def self.count 17 | case RUBY_PLATFORM 18 | when /darwin/ 19 | `sysctl machdep.cpu.core_count | awk '{ print $2 }'`.to_i 20 | when /linux/ 21 | `cat /proc/cpuinfo | grep processor | wc -l`.to_i 22 | end 23 | end 24 | 25 | end 26 | 27 | class Server 28 | include HTTParty 29 | default_timeout 10 30 | end 31 | 32 | class Runner 33 | 34 | def initialize(config) 35 | @instances = [] 36 | @last_build_id = nil 37 | @last_version_check = Time.now - TIME_BETWEEN_VERSION_CHECKS - 1 38 | @config = OpenStruct.new(config) 39 | @config.max_instances = @config.max_instances ? @config.max_instances.to_i : CPU.count 40 | 41 | if @config.ssh_tunnel 42 | server_uri = "http://127.0.0.1:#{Testbot::SERVER_PORT}" 43 | else 44 | server_uri = "http://#{@config.server_host}:#{Testbot::SERVER_PORT}" 45 | end 46 | 47 | Server.base_uri(server_uri) 48 | end 49 | 50 | attr_reader :config 51 | 52 | def run! 53 | # Remove legacy instance* and *_rsync|git style folders 54 | Dir.entries(".").find_all { |name| name.include?('instance') || name.include?('_rsync') || 55 | name.include?('_git') }.each { |folder| 56 | system "rm -rf #{folder}" 57 | } 58 | 59 | SSHTunnel.new(@config.server_host, @config.server_user || Testbot::DEFAULT_USER).open if @config.ssh_tunnel 60 | while true 61 | begin 62 | update_uid! 63 | start_ping 64 | wait_for_jobs 65 | rescue Exception => ex 66 | break if [ 'SignalException', 'Interrupt' ].include?(ex.class.to_s) 67 | puts "The runner crashed, restarting. Error: #{ex.inspect} #{ex.class}" 68 | end 69 | end 70 | end 71 | 72 | private 73 | 74 | def update_uid! 75 | # When a runner crashes or is restarted it might loose current job info. Because 76 | # of this we provide a new unique ID to the server so that it does not wait for 77 | # lost jobs to complete. 78 | @uid = "#{Time.now.to_i * rand}" 79 | end 80 | 81 | def wait_for_jobs 82 | last_check_found_a_job = false 83 | loop do 84 | sleep (last_check_found_a_job ? TIME_BETWEEN_QUICK_POLLS : TIME_BETWEEN_NORMAL_POLLS) 85 | 86 | check_for_update if !last_check_found_a_job && time_for_update? 87 | 88 | # Only get jobs from one build at a time 89 | next_params = base_params 90 | if @instances.size > 0 91 | next_params.merge!({ :build_id => @last_build_id }) 92 | next_params.merge!({ :no_jruby => true }) if max_jruby_instances? 93 | else 94 | @last_build_id = nil 95 | end 96 | 97 | # Makes sure all instances are listed as available after a run 98 | clear_completed_instances 99 | 100 | next_job = Server.get("/jobs/next", :query => next_params) rescue nil 101 | last_check_found_a_job = (next_job != nil && next_job.body != "") 102 | next unless last_check_found_a_job 103 | 104 | job = Job.new(*([ self, next_job.split(',') ].flatten)) 105 | if first_job_from_build? 106 | fetch_code(job) 107 | before_run(job) 108 | end 109 | 110 | @last_build_id = job.build_id 111 | 112 | # Must be outside the thread or it will sometimes run 113 | # multiple jobs using the same instance number. 114 | instance_number = free_instance_number 115 | 116 | @instances << [ Thread.new { job.run(instance_number) }, instance_number, job ] 117 | 118 | loop do 119 | clear_completed_instances 120 | break unless max_instances_running? 121 | end 122 | end 123 | end 124 | 125 | def max_jruby_instances? 126 | return unless @config.max_jruby_instances 127 | @instances.find_all { |thread, n, job| job.jruby? }.size >= @config.max_jruby_instances 128 | end 129 | 130 | def fetch_code(job) 131 | system "rsync -az --delete --delete-excluded -e ssh #{job.root}/ #{job.project}" 132 | end 133 | 134 | def before_run(job) 135 | rvm_prefix = RubyEnv.rvm_prefix(job.project) 136 | bundler_cmd = (RubyEnv.bundler?(job.project) ? [rvm_prefix, "bundle &&", rvm_prefix, "bundle exec"] : [rvm_prefix]).compact.join(" ") 137 | command_prefix = "cd #{job.project} && export RAILS_ENV=test && export TEST_INSTANCES=#{@config.max_instances} && #{bundler_cmd}" 138 | 139 | if File.exists?("#{job.project}/lib/tasks/testbot.rake") 140 | system "#{command_prefix} rake testbot:before_run" 141 | elsif File.exists?("#{job.project}/config/testbot/before_run.rb") 142 | system "#{command_prefix} ruby config/testbot/before_run.rb" 143 | else 144 | # workaround to bundle within the correct env 145 | system "#{command_prefix} ruby -e ''" 146 | end 147 | end 148 | 149 | def first_job_from_build? 150 | @last_build_id == nil 151 | end 152 | 153 | def time_for_update? 154 | time_for_update = ((Time.now - @last_version_check) >= TIME_BETWEEN_VERSION_CHECKS) 155 | @last_version_check = Time.now if time_for_update 156 | time_for_update 157 | end 158 | 159 | def check_for_update 160 | return unless @config.auto_update 161 | version = Server.get('/version') rescue Testbot.version 162 | return unless version != Testbot.version 163 | 164 | # In a PXE cluster with a shared gem folder we only want one of them to do the update 165 | if @config.wait_for_updated_gem 166 | # Gem.available? is cached so it won't detect new gems. 167 | gem = Gem::Dependency.new("testbot", version) 168 | successful_install = !Gem::SourceIndex.from_installed_gems.search(gem).empty? 169 | else 170 | if version.include?(".DEV.") 171 | successful_install = system("wget #{@config.dev_gem_root}/testbot-#{version}.gem && gem install testbot-#{version}.gem --no-ri --no-rdoc && rm testbot-#{version}.gem") 172 | else 173 | successful_install = system "gem install testbot -v #{version} --no-ri --no-rdoc" 174 | end 175 | end 176 | 177 | system "testbot #{ARGV.join(' ')}" if successful_install 178 | end 179 | 180 | def ping_params 181 | { :hostname => (@hostname ||= `hostname`.chomp), :max_instances => @config.max_instances, 182 | :idle_instances => (@config.max_instances - @instances.size), :username => ENV['USER'], :build_id => @last_build_id }.merge(base_params) 183 | end 184 | 185 | def base_params 186 | { :version => Testbot.version, :uid => @uid } 187 | end 188 | 189 | def max_instances_running? 190 | @instances.size == @config.max_instances 191 | end 192 | 193 | def clear_completed_instances 194 | @instances.each_with_index do |data, index| 195 | @instances.delete_at(index) if data.first.join(0.25) 196 | end 197 | end 198 | 199 | def free_instance_number 200 | 0.upto(@config.max_instances - 1) do |number| 201 | return number unless @instances.find { |instance, n, job| n == number } 202 | end 203 | end 204 | 205 | def start_ping 206 | Thread.new do 207 | while true 208 | begin 209 | response = Server.get("/runners/ping", :body => ping_params).body 210 | if response.include?('stop_build') 211 | build_id = response.split(',').last 212 | @instances.each { |instance, n, job| job.kill!(build_id) } 213 | end 214 | rescue 215 | end 216 | sleep TIME_BETWEEN_PINGS 217 | end 218 | end 219 | end 220 | 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /lib/runner/safe_result_text.rb: -------------------------------------------------------------------------------- 1 | require 'iconv' 2 | 3 | module Testbot::Runner 4 | class SafeResultText 5 | def self.clean(text) 6 | clean_escape_sequences(strip_invalid_utf8(text)) 7 | end 8 | 9 | def self.strip_invalid_utf8(text) 10 | # http://po-ru.com/diary/fixing-invalid-utf-8-in-ruby-revisited/ 11 | ic = Iconv.new('UTF-8//IGNORE', 'UTF-8') 12 | ic.iconv(text + ' ')[0..-2] 13 | end 14 | 15 | def self.clean_escape_sequences(text) 16 | tail_marker = "^[[0m" 17 | tail = text.rindex(tail_marker) && text[text.rindex(tail_marker)+tail_marker.length..-1] 18 | if !tail 19 | text 20 | elsif tail.include?("^[[") && !tail.include?("m") 21 | text[0..text.rindex(tail_marker) + tail_marker.length - 1] 22 | elsif text.scan(/\[.*?m/).last != tail_marker 23 | text[0..text.rindex(tail_marker) + tail_marker.length - 1] 24 | else 25 | text 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/server/build.rb: -------------------------------------------------------------------------------- 1 | module Testbot::Server 2 | 3 | class Build < MemoryModel 4 | 5 | def initialize(hash) 6 | super({ :success => true, :done => false, :results => '' }.merge(hash)) 7 | end 8 | 9 | def self.create_and_build_jobs(hash) 10 | hash["jruby"] = (hash["jruby"] == "true") ? 1 : 0 11 | build = create(hash.reject { |k, v| k == 'available_runner_usage' }) 12 | build.create_jobs!(hash['available_runner_usage']) 13 | build 14 | end 15 | 16 | def create_jobs!(available_runner_usage) 17 | groups = Group.build(self.files.split, self.sizes.split.map { |size| size.to_i }, 18 | Runner.total_instances.to_f * (available_runner_usage.to_i / 100.0), self.type) 19 | groups.each do |group| 20 | Job.create(:files => group.join(' '), 21 | :root => self.root, 22 | :project => self.project, 23 | :type => self.type, 24 | :build => self, 25 | :jruby => self.jruby) 26 | end 27 | end 28 | 29 | def destroy 30 | Job.all.find_all { |j| j.build == self }.each { |job| job.destroy } 31 | super 32 | end 33 | 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /lib/server/group.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | module Testbot::Server 4 | 5 | class Group 6 | 7 | DEFAULT = nil 8 | 9 | def self.build(files, sizes, instance_count, type) 10 | tests_with_sizes = slow_tests_first(map_files_and_sizes(files, sizes)) 11 | 12 | groups = [] 13 | current_group, current_size = 0, 0 14 | tests_with_sizes.each do |test, size| 15 | # inserts into next group if current is full and we are not in the last group 16 | if (0.5*size + current_size) > group_size(tests_with_sizes, instance_count) and instance_count > current_group + 1 17 | current_size = size 18 | current_group += 1 19 | else 20 | current_size += size 21 | end 22 | groups[current_group] ||= [] 23 | groups[current_group] << test 24 | end 25 | 26 | groups.compact 27 | end 28 | 29 | private 30 | 31 | def self.group_size(tests_with_sizes, group_count) 32 | total = tests_with_sizes.inject(0) { |sum, test| sum += test[1] } 33 | total / group_count.to_f 34 | end 35 | 36 | def self.map_files_and_sizes(files, sizes) 37 | list = [] 38 | files.each_with_index { |file, i| list << [ file, sizes[i] ] } 39 | list 40 | end 41 | 42 | def self.slow_tests_first(tests) 43 | tests.sort_by { |test, time| time.to_i }.reverse 44 | end 45 | 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/server/job.rb: -------------------------------------------------------------------------------- 1 | module Testbot::Server 2 | 3 | class Job < MemoryModel 4 | 5 | def update(hash) 6 | super(hash) 7 | if self.build 8 | self.done = done? 9 | done = !Job.all.find { |j| !j.done && j.build == self.build } 10 | self.build.update(:results => build_results(build), :done => done) 11 | 12 | build_broken_by_job = (self.status == "failed" && build.success) 13 | self.build.update(:success => false) if build_broken_by_job 14 | end 15 | end 16 | 17 | def self.next(params, remove_addr) 18 | clean_params = params.reject { |k, v| k == "no_jruby" } 19 | runner = Runner.record! clean_params.merge({ :ip => remove_addr, :last_seen_at => Time.now }) 20 | return unless Server.valid_version?(params[:version]) 21 | [ next_job(params["build_id"], params["no_jruby"]), runner ] 22 | end 23 | 24 | private 25 | 26 | def build_results(build) 27 | self.last_result_position ||= 0 28 | new_results = self.result.to_s[self.last_result_position..-1] || "" 29 | self.last_result_position = self.result.to_s.size 30 | 31 | # Don't know why this is needed as the job should cleanup 32 | # escape sequences. 33 | if new_results[0,4] == '[32m' 34 | new_results = new_results[4..-1] 35 | end 36 | 37 | build.results.to_s + new_results 38 | end 39 | 40 | def done? 41 | self.status == "successful" || self.status == "failed" 42 | end 43 | 44 | def self.next_job(build_id, no_jruby) 45 | release_jobs_taken_by_missing_runners! 46 | jobs = Job.all.find_all { |j| 47 | !j.taken_at && 48 | (build_id ? j.build.id.to_s == build_id : true) && 49 | (no_jruby ? j.jruby != 1 : true) 50 | } 51 | 52 | jobs[rand(jobs.size)] 53 | end 54 | 55 | def self.release_jobs_taken_by_missing_runners! 56 | missing_runners = Runner.all.find_all { |r| r.last_seen_at < (Time.now - Runner.timeout) } 57 | missing_runners.each { |runner| 58 | Job.all.find_all { |job| job.taken_by == runner }.each { |job| job.update(:taken_at => nil) } 59 | } 60 | end 61 | 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /lib/server/memory_model.rb: -------------------------------------------------------------------------------- 1 | class MemoryModel < OpenStruct 2 | 3 | @@db = {} 4 | @@types = {} 5 | 6 | def initialize(hash) 7 | @@types[self.class] ||= {} 8 | hash = resolve_types(symbolize_keys(hash)) 9 | super(hash) 10 | end 11 | 12 | def id 13 | object_id 14 | end 15 | 16 | def type 17 | @table[:type] 18 | end 19 | 20 | def attributes 21 | @table 22 | end 23 | 24 | def update(hash) 25 | @table.merge!(resolve_types(symbolize_keys(hash))) 26 | self 27 | end 28 | 29 | def destroy 30 | self.class.all.delete_if { |b| b.id == id } 31 | end 32 | 33 | def self.find(id) 34 | all.find { |r| r.id == id.to_i } 35 | end 36 | 37 | def self.create(hash = {}) 38 | all << new(hash) 39 | all[-1] 40 | end 41 | 42 | def self.all 43 | @@db[self] ||= [] 44 | @@db[self] 45 | end 46 | 47 | def self.first 48 | all.first 49 | end 50 | 51 | def self.delete_all 52 | all.clear 53 | end 54 | 55 | def self.count 56 | all.size 57 | end 58 | 59 | def self.attribute(attribute, type) 60 | @@types[self] ||= {} 61 | @@types[self][attribute] = type 62 | end 63 | 64 | private 65 | 66 | def resolve_types(hash) 67 | hash.each { |attribute, value| 68 | case @@types[self.class][attribute] 69 | when :integer 70 | hash[attribute] = value.to_i 71 | when :boolean 72 | if value == "true" 73 | hash[attribute] = true 74 | elsif value == "false" 75 | hash[attribute] = false 76 | elsif value != true && value != false 77 | hash[attribute] = nil 78 | end 79 | end 80 | } 81 | hash 82 | end 83 | 84 | def symbolize_keys(hash) 85 | h = {} 86 | hash.each { |k, v| h[k.to_sym] = v } 87 | h 88 | end 89 | 90 | end 91 | 92 | -------------------------------------------------------------------------------- /lib/server/runner.rb: -------------------------------------------------------------------------------- 1 | module Testbot::Server 2 | 3 | class Runner < MemoryModel 4 | 5 | attribute :idle_instances, :integer 6 | attribute :max_instances, :integer 7 | 8 | def self.record!(hash) 9 | create_or_update_by_mac!(hash) 10 | end 11 | 12 | def self.create_or_update_by_mac!(hash) 13 | if runner = find_by_uid(hash[:uid]) 14 | runner.update hash 15 | else 16 | Runner.create hash 17 | end 18 | end 19 | 20 | def self.timeout 21 | 10 22 | end 23 | 24 | def self.find_by_uid(uid) 25 | all.find { |r| r.uid == uid } 26 | end 27 | 28 | def self.find_all_outdated 29 | all.find_all { |r| r.version != Testbot.version } 30 | end 31 | 32 | def self.find_all_available 33 | all.find_all { |r| r.idle_instances && r.version == Testbot.version && r.last_seen_at > (Time.now - Runner.timeout) } 34 | end 35 | 36 | def self.available_instances 37 | find_all_available.inject(0) { |sum, r| r.idle_instances + sum } 38 | end 39 | 40 | def self.total_instances 41 | return 1 if ENV['INTEGRATION_TEST'] 42 | find_all_available.inject(0) { |sum, r| r.max_instances + sum } 43 | end 44 | 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /lib/server/server.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra' 3 | require 'yaml' 4 | require 'json' 5 | require File.expand_path(File.join(File.dirname(__FILE__), '/../shared/testbot')) 6 | require File.expand_path(File.join(File.dirname(__FILE__), 'memory_model.rb')) 7 | require File.expand_path(File.join(File.dirname(__FILE__), 'job.rb')) 8 | require File.expand_path(File.join(File.dirname(__FILE__), 'group.rb')) 9 | require File.expand_path(File.join(File.dirname(__FILE__), 'runner.rb')) 10 | require File.expand_path(File.join(File.dirname(__FILE__), 'build.rb')) 11 | 12 | module Testbot::Server 13 | 14 | if ENV['INTEGRATION_TEST'] 15 | set :port, 22880 16 | else 17 | set :port, Testbot::SERVER_PORT 18 | end 19 | 20 | class Server 21 | def self.valid_version?(runner_version) 22 | Testbot.version == runner_version 23 | end 24 | end 25 | 26 | post '/builds' do 27 | if Runner.total_instances == 0 28 | [ 503, "No runners available" ] 29 | else 30 | Build.create_and_build_jobs(params).id.to_s 31 | end 32 | end 33 | 34 | get '/builds/:id' do 35 | build = Build.find(params[:id]) 36 | build.destroy if build.done 37 | { "done" => build.done, "results" => build.results, "success" => build.success }.to_json 38 | end 39 | 40 | delete '/builds/:id' do 41 | build = Build.find(params[:id]) 42 | build.destroy if build 43 | nil 44 | end 45 | 46 | get '/jobs/next' do 47 | next_job, runner = Job.next(params, @env['REMOTE_ADDR']) 48 | if next_job 49 | next_job.update(:taken_at => Time.now, :taken_by => runner) 50 | [ next_job.id, next_job.build.id, next_job.project, next_job.root, next_job.type, (next_job.jruby == 1 ? 'jruby' : 'ruby'), next_job.files ].join(',') 51 | end 52 | end 53 | 54 | put '/jobs/:id' do 55 | Job.find(params[:id]).update(:result => params[:result], :status => params[:status]); nil 56 | end 57 | 58 | get '/runners/ping' do 59 | return unless Server.valid_version?(params[:version]) 60 | runner = Runner.find_by_uid(params[:uid]) 61 | if runner 62 | runner.update(params.reject { |k, v| k == "build_id" }.merge({ :last_seen_at => Time.now, :build => Build.find(params[:build_id]) })) 63 | unless params[:build_id] == '' || params[:build_id] == nil || runner.build 64 | return "stop_build,#{params[:build_id]}" 65 | end 66 | end 67 | nil 68 | end 69 | 70 | get '/runners' do 71 | Runner.find_all_available.map { |r| r.attributes }.to_json 72 | end 73 | 74 | get '/runners/outdated' do 75 | Runner.find_all_outdated.map { |runner| [ runner.ip, runner.hostname, runner.uid ].join(' ') }.join("\n").strip 76 | end 77 | 78 | get '/runners/available_instances' do 79 | Runner.available_instances.to_s 80 | end 81 | 82 | get '/runners/total_instances' do 83 | Runner.total_instances.to_s 84 | end 85 | 86 | get '/runners/available' do 87 | Runner.find_all_available.map { |runner| [ runner.ip, runner.hostname, runner.uid, runner.username, runner.idle_instances ].join(' ') }.join("\n").strip 88 | end 89 | 90 | get '/version' do 91 | Testbot.version 92 | end 93 | 94 | get '/status' do 95 | File.read(File.join(File.dirname(__FILE__), '/status/status.html')) 96 | end 97 | 98 | get '/status/:dir/:file' do 99 | File.read(File.join(File.dirname(__FILE__), "/status/#{params[:dir]}/#{params[:file]}")) 100 | end 101 | 102 | end 103 | 104 | -------------------------------------------------------------------------------- /lib/server/status/status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Testbot status 5 | 6 | 7 | 8 | 41 | 42 | 43 |

Testbot status

44 |
Loading...
45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /lib/server/status/stylesheets/status.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | color: white; 4 | font-family: Monaco, monospace; font-size: 10pt; 5 | } 6 | 7 | #total { 8 | margin-top: 20px; 9 | } 10 | 11 | #runners li { 12 | list-style: none; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /lib/shared/adapters/adapter.rb: -------------------------------------------------------------------------------- 1 | class Adapter 2 | 3 | FILES = Dir[File.dirname(__FILE__) + "/*_adapter.rb"] 4 | FILES.each { |file| require(file) } 5 | 6 | def self.all 7 | FILES.map { |file| load_adapter(file) } 8 | end 9 | 10 | def self.find(type) 11 | if adapter = all.find { |adapter| adapter.type == type.to_s } 12 | adapter 13 | else 14 | raise "Unknown adapter: #{type}" 15 | end 16 | end 17 | 18 | private 19 | 20 | def self.load_adapter(file) 21 | eval("::" + File.basename(file). 22 | gsub(/\.rb/, ''). 23 | gsub(/(?:^|_)(.)/) { $1.upcase }) 24 | end 25 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /lib/shared/adapters/cucumber_adapter.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env")) 2 | require File.expand_path(File.join(File.dirname(__FILE__), "../color")) 3 | 4 | class CucumberAdapter 5 | 6 | def self.command(project_path, ruby_interpreter, files) 7 | cucumber_command = RubyEnv.ruby_command(project_path, :script => "script/cucumber", :bin => "cucumber", 8 | :ruby_interpreter => ruby_interpreter) 9 | "export AUTOTEST=1; #{cucumber_command} -f progress --backtrace -r features/support -r features/step_definitions #{files} -t ~@disabled" 10 | end 11 | 12 | def self.test_files(dir) 13 | Dir["#{dir}/#{file_pattern}"] 14 | end 15 | 16 | def self.get_sizes(files) 17 | files.map { |file| File.stat(file).size } 18 | end 19 | 20 | def self.requester_port 21 | 2230 22 | end 23 | 24 | def self.pluralized 25 | 'features' 26 | end 27 | 28 | def self.base_path 29 | pluralized 30 | end 31 | 32 | def self.name 33 | 'Cucumber' 34 | end 35 | 36 | def self.type 37 | pluralized 38 | end 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 | 60 | private 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 | 88 | def self.file_pattern 89 | '**/**/*.feature' 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/shared/adapters/helpers/ruby_env.rb: -------------------------------------------------------------------------------- 1 | class RubyEnv 2 | def self.bundler?(project_path) 3 | gem_exists?("bundler") && File.exists?("#{project_path}/Gemfile") 4 | end 5 | 6 | def self.gem_exists?(gem) 7 | if Gem::Specification.respond_to?(:find_by_name) 8 | Gem::Specification.find_by_name(gem) 9 | else 10 | # older depricated method 11 | Gem.available?(gem) 12 | end 13 | rescue Gem::LoadError 14 | false 15 | end 16 | 17 | def self.ruby_command(project_path, opts = {}) 18 | ruby_interpreter = opts[:ruby_interpreter] || "ruby" 19 | 20 | if opts[:script] && File.exists?("#{project_path}/#{opts[:script]}") 21 | command = opts[:script] 22 | elsif opts[:bin] 23 | command = opts[:bin] 24 | else 25 | command = nil 26 | end 27 | 28 | if bundler?(project_path) 29 | "#{rvm_prefix(project_path)} #{ruby_interpreter} -S bundle exec #{command}".strip 30 | else 31 | "#{rvm_prefix(project_path)} #{ruby_interpreter} -S #{command}".strip 32 | end 33 | end 34 | 35 | def self.rvm_prefix(project_path) 36 | if rvm? 37 | rvmrc_path = File.join project_path, ".rvmrc" 38 | if File.exists?(rvmrc_path) 39 | File.read(rvmrc_path).to_s.strip + " exec" 40 | end 41 | end 42 | end 43 | 44 | def self.rvm? 45 | system("rvm info") != nil 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/shared/adapters/rspec2_adapter.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env")) 2 | 3 | class Rspec2Adapter 4 | 5 | def self.command(project_path, ruby_interpreter, files) 6 | spec_command = RubyEnv.ruby_command(project_path, 7 | :bin => "rspec", 8 | :ruby_interpreter => ruby_interpreter) 9 | 10 | if File.exists?("#{project_path}/spec/spec.opts") 11 | spec_command += " -O spec/spec.opts" 12 | end 13 | 14 | "export RSPEC_COLOR=true; #{spec_command} #{files}" 15 | end 16 | 17 | def self.test_files(dir) 18 | Dir["#{dir}/#{file_pattern}"] 19 | end 20 | 21 | def self.get_sizes(files) 22 | files.map { |file| File.stat(file).size } 23 | end 24 | 25 | def self.requester_port 26 | 2299 27 | end 28 | 29 | def self.pluralized 30 | 'specs' 31 | end 32 | 33 | def self.base_path 34 | "spec" 35 | end 36 | 37 | def self.name 38 | 'RSpec2' 39 | end 40 | 41 | def self.type 42 | 'rspec' 43 | end 44 | 45 | private 46 | 47 | def self.file_pattern 48 | '**/**/*_spec.rb' 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /lib/shared/adapters/rspec_adapter.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env")) 2 | require File.expand_path(File.join(File.dirname(__FILE__), "../color")) 3 | 4 | class RspecAdapter 5 | 6 | def self.command(project_path, ruby_interpreter, files) 7 | spec_command = RubyEnv.ruby_command(project_path, :script => "script/spec", :bin => "rspec", 8 | :ruby_interpreter => ruby_interpreter) 9 | if File.exists?("#{project_path}/spec/spec.opts") 10 | spec_command += " -O spec/spec.opts" 11 | end 12 | 13 | "export RSPEC_COLOR=true; #{spec_command} #{files}" 14 | end 15 | 16 | def self.test_files(dir) 17 | Dir["#{dir}/#{file_pattern}"] 18 | end 19 | 20 | def self.get_sizes(files) 21 | files.map { |file| File.stat(file).size } 22 | end 23 | 24 | def self.requester_port 25 | 2299 26 | end 27 | 28 | def self.pluralized 29 | 'specs' 30 | end 31 | 32 | def self.base_path 33 | type 34 | end 35 | 36 | def self.name 37 | 'RSpec' 38 | end 39 | 40 | def self.type 41 | 'spec' 42 | end 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 | 65 | private 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 | 75 | def self.file_pattern 76 | '**/**/*_spec.rb' 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /lib/shared/adapters/test_unit_adapter.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "/helpers/ruby_env")) 2 | 3 | class TestUnitAdapter 4 | 5 | def self.command(project_path, ruby_interpreter, files) 6 | ruby_command = RubyEnv.ruby_command(project_path, :ruby_interpreter => ruby_interpreter) 7 | %{#{ruby_command} -Itest -e '%w(#{files}).each { |file| require(Dir.pwd + "/" + file) }'} 8 | end 9 | 10 | def self.test_files(dir) 11 | Dir["#{dir}/#{file_pattern}"] 12 | end 13 | 14 | def self.get_sizes(files) 15 | files.map { |file| File.stat(file).size } 16 | end 17 | 18 | def self.requester_port 19 | 2231 20 | end 21 | 22 | def self.pluralized 23 | 'tests' 24 | end 25 | 26 | def self.base_path 27 | type 28 | end 29 | 30 | def self.name 31 | 'Test::Unit' 32 | end 33 | 34 | def self.type 35 | 'test' 36 | end 37 | 38 | private 39 | 40 | def self.file_pattern 41 | '**/**/*_test.rb' 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lib/shared/color.rb: -------------------------------------------------------------------------------- 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 | 17 | -------------------------------------------------------------------------------- /lib/shared/simple_daemonize.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'daemons' 3 | 4 | class SimpleDaemonize 5 | 6 | def self.start(proc, pid_path, app_name) 7 | working_dir = Dir.pwd 8 | 9 | group = Daemons::ApplicationGroup.new(app_name) 10 | group.new_application(:mode => :none).start 11 | 12 | File.open(pid_path, 'w') { |file| file.write(Process.pid) } 13 | Dir.chdir(working_dir) 14 | proc.call 15 | end 16 | 17 | def self.stop(pid_path) 18 | return unless File.exists?(pid_path) 19 | pid = File.read(pid_path) 20 | 21 | system "kill -9 #{pid} &> /dev/null" 22 | system "rm #{pid_path} &> /dev/null" 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /lib/shared/ssh_tunnel.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'net/ssh' 3 | 4 | class SSHTunnel 5 | def initialize(host, user, local_port = 2288) 6 | @host, @user, @local_port = host, user, local_port 7 | end 8 | 9 | def open 10 | connect 11 | 12 | start_time = Time.now 13 | while true 14 | break if @up 15 | sleep 0.5 16 | 17 | if Time.now - start_time > 5 18 | puts "SSH connection failed, trying again..." 19 | start_time = Time.now 20 | connect 21 | end 22 | end 23 | end 24 | 25 | def connect 26 | @thread.kill if @thread 27 | @thread = Thread.new do 28 | Net::SSH.start(@host, @user, { :timeout => 1 }) do |ssh| 29 | ssh.forward.local(@local_port, 'localhost', Testbot::SERVER_PORT) 30 | ssh.loop { @up = true } 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/shared/testbot.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '/version')) 2 | require File.expand_path(File.join(File.dirname(__FILE__), '/simple_daemonize')) 3 | require File.expand_path(File.join(File.dirname(__FILE__), '/adapters/adapter')) 4 | require 'fileutils' 5 | 6 | module Testbot 7 | require 'railtie' if defined?(Rails) 8 | 9 | if ENV['INTEGRATION_TEST'] 10 | SERVER_PID = "/tmp/integration_test_testbot_server.pid" 11 | RUNNER_PID = "/tmp/integration_test_testbot_runner.pid" 12 | else 13 | SERVER_PID = "/tmp/testbot_server.pid" 14 | RUNNER_PID = "/tmp/testbot_runner.pid" 15 | end 16 | 17 | DEFAULT_WORKING_DIR = "/tmp/testbot" 18 | DEFAULT_SERVER_PATH = "/tmp/testbot/#{ENV['USER']}" 19 | DEFAULT_USER = "testbot" 20 | DEFAULT_PROJECT = "project" 21 | DEFAULT_RUNNER_USAGE = "100%" 22 | SERVER_PORT = ENV['INTEGRATION_TEST'] ? 22880 : 2288 23 | 24 | class CLI 25 | 26 | def self.run(argv) 27 | return false if argv == [] 28 | opts = parse_args(argv) 29 | 30 | if opts[:help] 31 | return false 32 | elsif opts[:version] 33 | puts "Testbot #{Testbot.version}" 34 | elsif [ true, 'run', 'start' ].include?(opts[:server]) 35 | start_server(opts[:server]) 36 | elsif opts[:server] == 'stop' 37 | stop('server', Testbot::SERVER_PID) 38 | elsif [ true, 'run', 'start' ].include?(opts[:runner]) 39 | require File.expand_path(File.join(File.dirname(__FILE__), '/../runner/runner')) 40 | return false unless valid_runner_opts?(opts) 41 | start_runner(opts) 42 | elsif opts[:runner] == 'stop' 43 | stop('runner', Testbot::RUNNER_PID) 44 | elsif adapter = Adapter.all.find { |adapter| opts[adapter.type.to_sym] } 45 | require File.expand_path(File.join(File.dirname(__FILE__), '/../requester/requester')) 46 | start_requester(opts, adapter) 47 | end 48 | 49 | true 50 | end 51 | 52 | def self.parse_args(argv) 53 | last_setter = nil 54 | hash = {} 55 | str = '' 56 | argv.each_with_index do |arg, i| 57 | if arg.include?('--') 58 | str = '' 59 | last_setter = arg.split('--').last.to_sym 60 | hash[last_setter] = true if (i == argv.size - 1) || argv[i+1].include?('--') 61 | else 62 | str += ' ' + arg 63 | hash[last_setter] = str.strip 64 | end 65 | end 66 | hash 67 | end 68 | 69 | def self.start_runner(opts) 70 | stop('runner', Testbot::RUNNER_PID) 71 | 72 | proc = lambda { 73 | working_dir = opts[:working_dir] || Testbot::DEFAULT_WORKING_DIR 74 | FileUtils.mkdir_p(working_dir) 75 | Dir.chdir(working_dir) 76 | runner = Runner::Runner.new(:server_host => opts[:connect], 77 | :auto_update => opts[:auto_update], :max_instances => opts[:cpus], 78 | :ssh_tunnel => opts[:ssh_tunnel], :server_user => opts[:user], 79 | :max_jruby_instances => opts[:max_jruby_instances], 80 | :dev_gem_root => opts[:dev_gem_root], 81 | :wait_for_updated_gem => opts[:wait_for_updated_gem], 82 | :jruby_opts => opts[:jruby_opts]) 83 | runner.run! 84 | } 85 | 86 | if opts[:runner] == 'run' 87 | proc.call 88 | else 89 | puts "Testbot runner started (pid: #{Process.pid})" 90 | SimpleDaemonize.start(proc, Testbot::RUNNER_PID, "testbot (runner)") 91 | end 92 | end 93 | 94 | def self.start_server(type) 95 | stop('server', Testbot::SERVER_PID) 96 | require File.expand_path(File.join(File.dirname(__FILE__), '/../server/server')) 97 | 98 | if type == 'run' 99 | Sinatra::Application.run! :environment => "production" 100 | else 101 | puts "Testbot server started (pid: #{Process.pid})" 102 | SimpleDaemonize.start(lambda { 103 | Sinatra::Application.run! :environment => "production" 104 | }, Testbot::SERVER_PID, "testbot (server)") 105 | end 106 | end 107 | 108 | def self.stop(name, pid) 109 | puts "Testbot #{name} stopped" if SimpleDaemonize.stop(pid) 110 | end 111 | 112 | def self.start_requester(opts, adapter) 113 | requester = Requester::Requester.new(:server_host => opts[:connect], 114 | :rsync_path => opts[:rsync_path], 115 | :rsync_ignores => opts[:rsync_ignores].to_s, 116 | :available_runner_usage => nil, 117 | :project => opts[:project], 118 | :ssh_tunnel => opts[:ssh_tunnel], :server_user => opts[:user]) 119 | requester.run_tests(adapter, adapter.base_path) 120 | end 121 | 122 | def self.valid_runner_opts?(opts) 123 | opts[:connect].is_a?(String) 124 | end 125 | 126 | def self.lib_path 127 | File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 128 | end 129 | 130 | end 131 | 132 | end 133 | -------------------------------------------------------------------------------- /lib/shared/version.rb: -------------------------------------------------------------------------------- 1 | module Testbot 2 | # Don't forget to update readme and changelog 3 | def self.version 4 | version = "0.7.9" 5 | dev_version_file = File.join(File.dirname(__FILE__), '..', '..', 'DEV_VERSION') 6 | if File.exists?(dev_version_file) 7 | version += File.read(dev_version_file) 8 | end 9 | version 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/tasks/testbot.rake: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../shared/adapters/adapter' 2 | 3 | namespace :testbot do 4 | 5 | def run_and_show_results(adapter, custom_path) 6 | 'testbot:before_request'.tap { |t| Rake::Task.task_defined?(t) && Rake::Task[t].invoke } 7 | 8 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'requester', 'requester.rb')) 9 | requester = Testbot::Requester::Requester.create_by_config("#{Rails.root}/config/testbot.yml") 10 | 11 | puts "Running #{adapter.pluralized}..." 12 | start_time = Time.now 13 | 14 | path = custom_path ? "#{adapter.base_path}/#{custom_path}" : adapter.base_path 15 | success = requester.run_tests(adapter, path) 16 | 17 | puts 18 | puts "Finished in #{Time.now - start_time} seconds." 19 | success 20 | end 21 | 22 | Adapter.all.each do |adapter| 23 | 24 | desc "Run the #{adapter.name} tests using testbot" 25 | task adapter.type, :custom_path do |_, args| 26 | exit 1 unless run_and_show_results(adapter, args[:custom_path]) 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/testbot.rb: -------------------------------------------------------------------------------- 1 | # Rails plugin hook 2 | require File.expand_path(File.join(File.dirname(__FILE__), '/shared/testbot')) 3 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | turbux_rspec -------------------------------------------------------------------------------- /test/fixtures/local/Rakefile: -------------------------------------------------------------------------------- 1 | namespace :testbot do 2 | 3 | task :before_run do 4 | puts "prepare got called" 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/local/config/testbot.yml: -------------------------------------------------------------------------------- 1 | server_host: localhost 2 | rsync_path: ../server 3 | available_runner_usage: 100% 4 | rsync_ignores: log/* tmp/* 5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/local/log/test.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/log/test.log -------------------------------------------------------------------------------- /test/fixtures/local/script/spec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | puts "script/spec got called with #{ARGV.inspect}" 3 | -------------------------------------------------------------------------------- /test/fixtures/local/spec/models/car_spec.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/spec/models/car_spec.rb -------------------------------------------------------------------------------- /test/fixtures/local/spec/models/house_spec.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/spec/models/house_spec.rb -------------------------------------------------------------------------------- /test/fixtures/local/spec/spec.opts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/spec/spec.opts -------------------------------------------------------------------------------- /test/fixtures/local/tmp/restart.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakimk/testbot/d200b0ff53b7c1b886ff515fc0d160d41067b13a/test/fixtures/local/tmp/restart.txt -------------------------------------------------------------------------------- /test/integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | require 'fileutils' 4 | require 'shoulda' 5 | 6 | class IntegrationTest < Test::Unit::TestCase 7 | # This is slow, and Test:Unit does not have "before/after :all" method, so I'm using a single testcase for multiple tests 8 | should "be able to send a build request, have it run and show the results" do 9 | Thread.new { 10 | 11 | sleep 30 12 | puts "Still running after 30 secs, stopping..." 13 | stop 14 | } 15 | 16 | cleanup 17 | system "mkdir -p tmp/fixtures; cp -rf test/fixtures/local tmp/local" 18 | system "export INTEGRATION_TEST=true; bin/testbot --runner --connect 127.0.0.1 --working_dir tmp/runner > /dev/null" 19 | system "export INTEGRATION_TEST=true; bin/testbot --server > /dev/null" 20 | 21 | # For debug 22 | # Thread.new do 23 | # system "export INTEGRATION_TEST=true; bin/testbot --runner run --connect 127.0.0.1 --working_dir tmp/runner" 24 | # end 25 | # Thread.new do 26 | # system "export INTEGRATION_TEST=true; bin/testbot --server run" 27 | # end 28 | 29 | sleep 2.0 30 | result = `cd tmp/local; INTEGRATION_TEST=true ../../bin/testbot --spec --connect 127.0.0.1 --rsync_path ../server --rsync_ignores "log/* tmp/*"` 31 | 32 | # Should include the result from script/spec 33 | #puts result.inspect 34 | assert result.include?('script/spec got called with ["-O", "spec/spec.opts", "spec/models/house_spec.rb", "spec/models/car_spec.rb"]') || 35 | result.include?('script/spec got called with ["-O", "spec/spec.opts", "spec/models/car_spec.rb", "spec/models/house_spec.rb"]') 36 | 37 | 38 | # Should not include ignored files 39 | assert !File.exists?("tmp/server/log/test.log") 40 | assert !File.exists?("tmp/server/tmp/restart.txt") 41 | assert !File.exists?("tmp/runner/local/log/test.log") 42 | assert !File.exists?("tmp/runner/local/tmp/restart.txt") 43 | end 44 | 45 | def teardown 46 | stop 47 | cleanup 48 | end 49 | 50 | def stop 51 | system "export INTEGRATION_TEST=true; bin/testbot --server stop > /dev/null" 52 | system "export INTEGRATION_TEST=true; bin/testbot --runner stop > /dev/null" 53 | end 54 | 55 | def cleanup 56 | system "rm -rf tmp/local tmp/fixtures" 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/requester/requester_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/requester/requester.rb')) 2 | require 'test/unit' 3 | require 'shoulda' 4 | require 'flexmock/test_unit' 5 | 6 | # Probably a bug in flexmock, for 1.9.2 7 | unless defined?(Test::Unit::AssertionFailedError) 8 | class Test::Unit::AssertionFailedError 9 | end 10 | end 11 | 12 | module Testbot::Requester 13 | 14 | class RequesterTest < Test::Unit::TestCase 15 | 16 | def requester_with_result(results) 17 | requester = Requester.new(:server_host => "192.168.1.100", :rsync_path => 'user@server:/tmp/somewhere') 18 | 19 | flexmock(requester).should_receive(:find_tests).and_return([]) 20 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id) 21 | flexmock(requester).should_receive(:sleep).once 22 | flexmock(requester).should_receive(:print).once 23 | flexmock(requester).should_receive(:puts).once 24 | flexmock(requester).should_receive(:system) 25 | flexmock(HTTParty).should_receive(:get).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 26 | :format => :json).and_return({ "done" => true, "results" => results }) 27 | requester 28 | end 29 | 30 | def response_with_build_id 31 | OpenStruct.new(:response => OpenStruct.new(:code => "200", :body => "5")) 32 | end 33 | 34 | def error_response(opts = {}) 35 | OpenStruct.new(:response => OpenStruct.new(opts)) 36 | end 37 | 38 | def build_with_result(results) 39 | requester_with_result(results).run_tests(RspecAdapter, 'spec') 40 | end 41 | 42 | def setup 43 | ENV['USE_JRUBY'] = nil 44 | ENV['IN_TEST'] = 'true' 45 | end 46 | 47 | def mock_file_sizes 48 | flexmock(File).should_receive(:stat).and_return(mock = Object.new) 49 | flexmock(mock).should_receive(:size).and_return(0) 50 | end 51 | 52 | def fixture_path(local_path) 53 | File.join(File.dirname(__FILE__), local_path) 54 | end 55 | 56 | context "self.create_by_config" do 57 | 58 | should 'create a requester from config' do 59 | requester = Requester.create_by_config(fixture_path("testbot.yml")) 60 | assert_equal 'hostname', requester.config.server_host 61 | assert_equal '/path', requester.config.rsync_path 62 | assert_equal '.git tmp', requester.config.rsync_ignores 63 | assert_equal 'appname', requester.config.project 64 | assert_equal false, requester.config.ssh_tunnel 65 | assert_equal 'user', requester.config.server_user 66 | assert_equal '50%', requester.config.available_runner_usage 67 | end 68 | 69 | should 'accept ERB-snippets in testbot.yml' do 70 | requester = Requester.create_by_config(fixture_path("testbot_with_erb.yml")) 71 | assert_equal 'dynamic_host', requester.config.server_host 72 | assert_equal '50%', requester.config.available_runner_usage 73 | end 74 | end 75 | 76 | context "initialize" do 77 | 78 | should "use defaults when values are missing" do 79 | expected = { :server_host => 'hostname', 80 | :rsync_path => Testbot::DEFAULT_SERVER_PATH, 81 | :project => Testbot::DEFAULT_PROJECT, 82 | :server_user => Testbot::DEFAULT_USER, 83 | :available_runner_usage => Testbot::DEFAULT_RUNNER_USAGE } 84 | 85 | actual = Requester.new({ "server_host" => 'hostname' }).config 86 | 87 | assert_equal OpenStruct.new(expected), actual 88 | end 89 | 90 | end 91 | 92 | context "run_tests" do 93 | 94 | should "should be able to create a build" do 95 | requester = Requester.new(:server_host => "192.168.1.100", :rsync_path => '/path', :available_runner_usage => '60%', :project => 'things', :server_user => "cruise") 96 | flexmock(RspecAdapter).should_receive(:test_files).with('spec').once.and_return([ 'spec/models/house_spec.rb', 'spec/models/car_spec.rb' ]) 97 | 98 | flexmock(File).should_receive(:stat).once.with("spec/models/house_spec.rb").and_return(mock = Object.new); flexmock(mock).should_receive(:size).and_return(10) 99 | flexmock(File).should_receive(:stat).once.with("spec/models/car_spec.rb").and_return(mock = Object.new); flexmock(mock).should_receive(:size).and_return(20) 100 | 101 | flexmock(HTTParty).should_receive(:post).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds", 102 | :body => { :type => "spec", 103 | :root => "cruise@192.168.1.100:/path", 104 | :project => "things", 105 | :available_runner_usage => "60%", 106 | :files => "spec/models/house_spec.rb" + 107 | " spec/models/car_spec.rb", 108 | :sizes => "10 20", 109 | :jruby => false }).and_return(response_with_build_id) 110 | 111 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, 'results' => '', "success" => true }) 112 | flexmock(requester).should_receive(:sleep) 113 | flexmock(requester).should_receive(:print) 114 | flexmock(requester).should_receive(:puts) 115 | flexmock(requester).should_receive(:system) 116 | 117 | assert_equal true, requester.run_tests(RspecAdapter, 'spec') 118 | end 119 | 120 | should "print a message and exit if the status is 503" do 121 | requester = Requester.new(:server_host => "192.168.1.100") 122 | 123 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 124 | flexmock(requester).should_receive(:system) 125 | 126 | flexmock(HTTParty).should_receive(:post).and_return(error_response(:code => "503")) 127 | flexmock(requester).should_receive(:puts) 128 | assert_equal false, requester.run_tests(RspecAdapter, 'spec') 129 | end 130 | 131 | should "print what the server returns in case there is anything but a 200 response" do 132 | requester = Requester.new(:server_host => "192.168.1.100") 133 | 134 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 135 | flexmock(requester).should_receive(:system) 136 | 137 | flexmock(HTTParty).should_receive(:post).and_return(error_response(:code => "123", :body => "Some error")) 138 | flexmock(requester).should_receive(:puts).with("Could not create build, 123: Some error") 139 | assert_equal false, requester.run_tests(RspecAdapter, 'spec') 140 | end 141 | 142 | should "print the sum of results formatted by the adapter" do 143 | requester = Requester.new(:server_host => "192.168.1.100") 144 | 145 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 146 | flexmock(requester).should_receive(:system) 147 | 148 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id) 149 | 150 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 151 | :format => :json).and_return({ "done" => false, "results" => "job 2 done: ...." }, 152 | { "done" => true, "results" => "job 2 done: ....job 1 done: ...." }) 153 | mock_file_sizes 154 | 155 | flexmock(requester).should_receive(:sleep).times(2).with(0.5) 156 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....") 157 | flexmock(requester).should_receive(:print).once.with("job 1 done: ....") 158 | flexmock(requester).should_receive(:puts).once.with("\nformatted result") 159 | 160 | flexmock(RspecAdapter).should_receive(:sum_results).with("job 2 done: ....job 1 done: ....").and_return("formatted result") 161 | requester.run_tests(RspecAdapter, 'spec') 162 | end 163 | 164 | should "keep calling the server for results until done" do 165 | requester = Requester.new(:server_host => "192.168.1.100") 166 | 167 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 168 | flexmock(requester).should_receive(:system) 169 | 170 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id) 171 | 172 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 173 | :format => :json).and_return({ "done" => false, "results" => "job 2 done: ...." }, 174 | { "done" => true, "results" => "job 2 done: ....job 1 done: ...." }) 175 | mock_file_sizes 176 | 177 | flexmock(requester).should_receive(:sleep).times(2).with(0.5) 178 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....") 179 | flexmock(requester).should_receive(:print).once.with("job 1 done: ....") 180 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m") 181 | 182 | requester.run_tests(RspecAdapter, 'spec') 183 | end 184 | 185 | should "return false if not successful" do 186 | requester = Requester.new(:server_host => "192.168.1.100") 187 | 188 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 189 | flexmock(requester).should_receive(:system) 190 | 191 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id) 192 | 193 | flexmock(HTTParty).should_receive(:get).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 194 | :format => :json).and_return({ "success" => false, "done" => true, "results" => "job 2 done: ....job 1 done: ...." }) 195 | 196 | flexmock(requester).should_receive(:sleep).once.with(0.5) 197 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....job 1 done: ....") 198 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m") 199 | mock_file_sizes 200 | 201 | assert_equal false, requester.run_tests(RspecAdapter, 'spec') 202 | end 203 | 204 | should "not print empty lines when there is no result" do 205 | requester = Requester.new(:server_host => "192.168.1.100") 206 | 207 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 208 | flexmock(requester).should_receive(:system) 209 | 210 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id) 211 | 212 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 213 | :format => :json).and_return({ "done" => false, "results" => "" }, 214 | { "done" => true, "results" => "job 2 done: ....job 1 done: ...." }) 215 | 216 | flexmock(requester).should_receive(:sleep).times(2).with(0.5) 217 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....job 1 done: ....") 218 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m") 219 | mock_file_sizes 220 | 221 | requester.run_tests(RspecAdapter, 'spec') 222 | end 223 | 224 | should "sync the files to the server" do 225 | requester = Requester.new(:server_host => "192.168.1.100", :rsync_path => '/path', :rsync_ignores => '.git tmp') 226 | 227 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 228 | flexmock(requester).should_receive(:system) 229 | 230 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id) 231 | flexmock(requester).should_receive(:sleep).once 232 | flexmock(requester).should_receive(:print) 233 | flexmock(requester).should_receive(:puts) 234 | flexmock(HTTParty).should_receive(:get).once.with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 235 | :format => :json).and_return({ "done" => true, "results" => "" }) 236 | 237 | flexmock(requester).should_receive('system').with("rsync -az --delete --delete-excluded -e ssh --exclude='.git' --exclude='tmp' . testbot@192.168.1.100:/path") 238 | mock_file_sizes 239 | 240 | requester.run_tests(RspecAdapter, 'spec') 241 | end 242 | 243 | should "just try again if the request encounters an error while running and print on the fith time" do 244 | requester = Requester.new(:server_host => "192.168.1.100") 245 | 246 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 247 | flexmock(requester).should_receive(:system) 248 | 249 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id) 250 | 251 | flexmock(HTTParty).should_receive(:get).times(5).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 252 | :format => :json).and_raise('some connection error') 253 | flexmock(HTTParty).should_receive(:get).times(1).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 254 | :format => :json).and_return({ "done" => true, "results" => "job 2 done: ....job 1 done: ...." }) 255 | 256 | flexmock(requester).should_receive(:sleep).times(6).with(0.5) 257 | flexmock(requester).should_receive(:puts).once.with("Failed to get status: some connection error") 258 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....job 1 done: ....") 259 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m") 260 | mock_file_sizes 261 | 262 | requester.run_tests(RspecAdapter, 'spec') 263 | end 264 | 265 | should "just try again if the status returns as nil" do 266 | requester = Requester.new(:server_host => "192.168.1.100") 267 | 268 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 269 | flexmock(requester).should_receive(:system) 270 | 271 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id) 272 | 273 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 274 | :format => :json).and_return(nil, 275 | { "done" => true, "results" => "job 2 done: ....job 1 done: ...." }) 276 | 277 | flexmock(requester).should_receive(:sleep).times(2).with(0.5) 278 | flexmock(requester).should_receive(:print).once.with("job 2 done: ....job 1 done: ....") 279 | flexmock(requester).should_receive(:puts).once.with("\n\033[32m0 examples, 0 failures\033[0m") 280 | mock_file_sizes 281 | 282 | requester.run_tests(RspecAdapter, 'spec') 283 | end 284 | 285 | should "remove unnessesary output from rspec when told to do so" do 286 | requester = Requester.new(:server_host => "192.168.1.100", :simple_output => true) 287 | 288 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb', 'spec_models/car_spec.rb' ]) 289 | flexmock(requester).should_receive(:system) 290 | 291 | flexmock(HTTParty).should_receive(:post).and_return(response_with_build_id) 292 | 293 | flexmock(HTTParty).should_receive(:get).times(2).with("http://192.168.1.100:#{Testbot::SERVER_PORT}/builds/5", 294 | :format => :json).and_return(nil, 295 | { "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" }) 296 | 297 | flexmock(requester).should_receive(:sleep).times(2).with(0.5) 298 | 299 | # Imperfect match, includes "." in 84.333, but good enough. 300 | flexmock(requester).should_receive(:print).once.with("......F...") 301 | flexmock(requester).should_receive(:print) 302 | flexmock(requester).should_receive(:puts) 303 | mock_file_sizes 304 | 305 | requester.run_tests(RspecAdapter, 'spec') 306 | end 307 | 308 | should "use SSHTunnel when specified (with a port that does not collide with the runner)" do 309 | requester = Requester.new(:ssh_tunnel => true, :server_host => "somewhere") 310 | flexmock(requester).should_receive(:system) 311 | 312 | flexmock(SSHTunnel).should_receive(:new).once.with("somewhere", "testbot", 2299).and_return(ssh_tunnel = Object.new) 313 | flexmock(ssh_tunnel).should_receive(:open).once 314 | 315 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb' ]) 316 | flexmock(HTTParty).should_receive(:post).with("http://127.0.0.1:2299/builds", any).and_return(response_with_build_id) 317 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." }) 318 | flexmock(requester).should_receive(:sleep) 319 | flexmock(requester).should_receive(:print) 320 | flexmock(requester).should_receive(:puts) 321 | mock_file_sizes 322 | 323 | requester.run_tests(RspecAdapter, 'spec') 324 | end 325 | 326 | should "use another user for rsync and ssh_tunnel when specified" do 327 | requester = Requester.new(:ssh_tunnel => true, :server_host => "somewhere", 328 | :server_user => "cruise", :rsync_path => "/tmp/testbot/foo") 329 | 330 | flexmock(SSHTunnel).should_receive(:new).once.with("somewhere", "cruise", 2299).and_return(ssh_tunnel = Object.new) 331 | flexmock(ssh_tunnel).should_receive(:open).once 332 | 333 | flexmock(requester).should_receive(:find_tests).and_return([ 'spec/models/house_spec.rb' ]) 334 | flexmock(HTTParty).should_receive(:post).with("http://127.0.0.1:2299/builds", any).and_return(response_with_build_id) 335 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." }) 336 | flexmock(requester).should_receive(:sleep) 337 | flexmock(requester).should_receive(:print) 338 | flexmock(requester).should_receive(:puts) 339 | 340 | flexmock(requester).should_receive('system').with("rsync -az --delete --delete-excluded -e ssh . cruise@somewhere:/tmp/testbot/foo") 341 | mock_file_sizes 342 | 343 | requester.run_tests(RspecAdapter, 'spec') 344 | end 345 | 346 | should "use another port for cucumber to be able to run at the same time as rspec" do 347 | requester = Requester.new(:ssh_tunnel => true, :server_host => "somewhere") 348 | flexmock(requester).should_receive(:system) 349 | 350 | flexmock(SSHTunnel).should_receive(:new).once.with("somewhere", "testbot", 2230).and_return(ssh_tunnel = Object.new) 351 | flexmock(ssh_tunnel).should_receive(:open).once 352 | 353 | flexmock(requester).should_receive(:find_tests).and_return([ 'features/some.feature' ]) 354 | flexmock(HTTParty).should_receive(:post).with("http://127.0.0.1:2230/builds", any).and_return(response_with_build_id) 355 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." }) 356 | flexmock(requester).should_receive(:sleep) 357 | flexmock(requester).should_receive(:print) 358 | flexmock(requester).should_receive(:puts) 359 | mock_file_sizes 360 | 361 | requester.run_tests(CucumberAdapter, 'features') 362 | end 363 | 364 | should "use another port for Test::Unit" do 365 | requester = Requester.new(:ssh_tunnel => true, :server_host => "somewhere") 366 | flexmock(requester).should_receive(:system) 367 | 368 | flexmock(SSHTunnel).should_receive(:new).once.with("somewhere", "testbot", 2231).and_return(ssh_tunnel = Object.new) 369 | flexmock(ssh_tunnel).should_receive(:open).once 370 | 371 | flexmock(requester).should_receive(:find_tests).and_return([ 'test/some_test.rb' ]) 372 | flexmock(HTTParty).should_receive(:post).with("http://127.0.0.1:2231/builds", any).and_return(response_with_build_id) 373 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." }) 374 | flexmock(requester).should_receive(:sleep) 375 | flexmock(requester).should_receive(:print) 376 | flexmock(requester).should_receive(:puts) 377 | mock_file_sizes 378 | 379 | requester.run_tests(TestUnitAdapter, 'test') 380 | end 381 | 382 | should "request a run with jruby if USE_JRUBY is set" do 383 | ENV['USE_JRUBY'] = "true" 384 | requester = Requester.new 385 | flexmock(requester).should_receive(:system) 386 | 387 | # This is quite ugly. I want something like hash_including instead... 388 | other_args = { :type=>"test", :available_runner_usage=>"100%", 389 | :root=>"testbot@:/tmp/testbot/#{ENV['USER']}", :files=>"test/some_test.rb", 390 | :sizes=>"0", :project=>"project" } 391 | 392 | flexmock(TestUnitAdapter).should_receive(:test_files).and_return([ 'test/some_test.rb' ]) 393 | flexmock(HTTParty).should_receive(:post).with(any, :body => other_args.merge({ :jruby => true })).and_return(response_with_build_id) 394 | flexmock(HTTParty).should_receive(:get).and_return({ "done" => true, "results" => "job 1 done: ...." }) 395 | flexmock(requester).should_receive(:sleep) 396 | flexmock(requester).should_receive(:print) 397 | flexmock(requester).should_receive(:puts) 398 | mock_file_sizes 399 | 400 | requester.run_tests(TestUnitAdapter, 'test') 401 | end 402 | 403 | end 404 | 405 | end 406 | 407 | end 408 | -------------------------------------------------------------------------------- /test/requester/testbot.yml: -------------------------------------------------------------------------------- 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% -------------------------------------------------------------------------------- /test/requester/testbot_with_erb.yml: -------------------------------------------------------------------------------- 1 | server_host: <%= "dynamic_host" %> 2 | available_runner_usage: "<%= 50 %>%" -------------------------------------------------------------------------------- /test/runner/job_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/shared/testbot.rb')) 2 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/runner/job.rb')) 3 | require 'test/unit' 4 | require 'shoulda' 5 | require 'flexmock/test_unit' 6 | 7 | module Testbot::Runner 8 | 9 | class JobTest < Test::Unit::TestCase 10 | 11 | def expect_put_with(id, result_text, status, time = 0) 12 | expected_result = "\n#{`hostname`.chomp}:#{Dir.pwd}\n" 13 | expected_result += result_text 14 | flexmock(Server).should_receive(:put).once.with("/jobs/#{id}", :body => 15 | { :result => expected_result, :status => status, :time => time }) 16 | end 17 | 18 | def expect_put 19 | flexmock(Server).should_receive(:put).once 20 | end 21 | 22 | def expect_put_to_timeout 23 | flexmock(Server).should_receive(:put).and_raise(Timeout::Error) 24 | end 25 | 26 | def stub_duration(seconds) 27 | time ||= Time.now 28 | flexmock(Time).should_receive(:now).and_return(time, time + seconds) 29 | end 30 | 31 | should "be able to run a successful job" do 32 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb") 33 | flexmock(job).should_receive(:puts) 34 | stub_duration(0) 35 | 36 | expect_put_with(10, "result text", "successful") 37 | flexmock(job).should_receive(:run_and_return_result).once. 38 | with("export RAILS_ENV=test; export TEST_ENV_NUMBER=; cd project; export RSPEC_COLOR=true; ruby -S bundle exec rspec spec/foo_spec.rb spec/bar_spec.rb"). 39 | and_return('result text') 40 | flexmock(RubyEnv).should_receive(:bundler?).returns(true) 41 | flexmock(RubyEnv).should_receive(:rvm?).returns(false) 42 | job.run(0) 43 | end 44 | 45 | should "not raise an error when posting results time out" do 46 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb") 47 | flexmock(job).should_receive(:puts) 48 | 49 | # We're using send here because triggering post_results though the rest of the 50 | # code requires very complex setup. The code need to be refactored to be more testable. 51 | expect_put 52 | job.send(:post_results, "result text") 53 | expect_put_to_timeout 54 | job.send(:post_results, "result text") 55 | end 56 | 57 | should "not be successful when the job fails" do 58 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb") 59 | flexmock(job).should_receive(:puts) 60 | stub_duration(0) 61 | 62 | expect_put_with(10, "result text", "failed") 63 | flexmock(job).should_receive(:run_and_return_result).and_return('result text') 64 | flexmock(job).should_receive(:success?).and_return(false) 65 | job.run(0) 66 | end 67 | 68 | should "set an instance number when the instance is not 0" do 69 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb") 70 | flexmock(job).should_receive(:puts) 71 | stub_duration(0) 72 | 73 | expect_put_with(10, "result text", "successful") 74 | flexmock(job).should_receive(:run_and_return_result). 75 | with(/TEST_ENV_NUMBER=2/). 76 | and_return('result text') 77 | flexmock(RubyEnv).should_receive(:rvm?).returns(false) 78 | job.run(1) 79 | end 80 | 81 | should "return test runtime in milliseconds" do 82 | job = Job.new(Runner.new({}), 10, "00:00", "project", "/tmp/testbot/user", "spec", "ruby", "spec/foo_spec.rb spec/bar_spec.rb") 83 | flexmock(job).should_receive(:puts) 84 | 85 | stub_duration(10.55) 86 | expect_put_with(10, "result text", "successful", 1055) 87 | flexmock(job).should_receive(:run_and_return_result).and_return('result text') 88 | flexmock(RubyEnv).should_receive(:rvm?).returns(false) 89 | job.run(0) 90 | end 91 | 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /test/runner/safe_result_text_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/shared/testbot.rb')) 2 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/runner/safe_result_text.rb')) 3 | require 'test/unit' 4 | require 'shoulda' 5 | 6 | module Testbot::Runner 7 | 8 | class SafeResultTextTest < Test::Unit::TestCase 9 | 10 | should "not break escape sequences" do 11 | assert_equal "^[[32m.^[[0m^[[32m.^[[0m", SafeResultText.clean("^[[32m.^[[0m^[[32m.^[[0m^[[32m.") 12 | assert_equal "^[[32m.^[[0m^[[32m.^[[0m", SafeResultText.clean("^[[32m.^[[0m^[[32m.^[[0m^[[3") 13 | assert_equal "^[[32m.^[[0m", SafeResultText.clean("^[[32m.^[[0m^[") 14 | assert_equal "[32m.[0m[32m.[0m[3", SafeResultText.clean("[32m.[0m[32m.[0m[3") 15 | assert_equal "...", SafeResultText.clean("...") 16 | end 17 | 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /test/server/group_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/server/group')) 2 | require 'test/unit' 3 | require 'shoulda' 4 | require 'flexmock/test_unit' 5 | 6 | module Testbot::Server 7 | 8 | class GroupTest < Test::Unit::TestCase 9 | 10 | context "self.build" do 11 | 12 | should "create file groups based on the number of instances" do 13 | groups = Group.build([ 'spec/models/car_spec.rb', 'spec/models/car2_spec.rb', 14 | 'spec/models/house_spec.rb', 'spec/models/house2_spec.rb' ], [ 1, 1, 1, 1 ], 2, 'spec') 15 | 16 | assert_equal 2, groups.size 17 | assert_equal [ 'spec/models/house2_spec.rb', 'spec/models/house_spec.rb' ], groups[0] 18 | assert_equal [ 'spec/models/car2_spec.rb', 'spec/models/car_spec.rb' ], groups[1] 19 | end 20 | 21 | should "create a small grop when there isn't enough specs to fill a normal one" do 22 | groups = Group.build(["spec/models/car_spec.rb", "spec/models/car2_spec.rb", 23 | "spec/models/house_spec.rb", "spec/models/house2_spec.rb", 24 | "spec/models/house3_spec.rb"], [ 1, 1, 1, 1, 1 ], 3, 'spec') 25 | 26 | assert_equal 3, groups.size 27 | assert_equal [ "spec/models/car_spec.rb" ], groups[2] 28 | end 29 | 30 | should "use sizes when building groups" do 31 | groups = Group.build([ 'spec/models/car_spec.rb', 'spec/models/car2_spec.rb', 32 | 'spec/models/house_spec.rb', 'spec/models/house2_spec.rb' ], [ 40, 10, 10, 20 ], 2, 'spec') 33 | 34 | assert_equal [ 'spec/models/car_spec.rb' ], groups[0] 35 | assert ![ 'spec/models/house2_spec.rb', 'spec/models/car2_spec.rb', 'spec/models/house_spec.rb' ]. 36 | find { |file| !groups[1].include?(file) } 37 | end 38 | 39 | end 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/server/server_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../lib/server/server')) 2 | require 'test/unit' 3 | require 'rack/test' 4 | require 'shoulda' 5 | require 'flexmock/test_unit' 6 | 7 | set :environment, :test 8 | 9 | module Testbot::Server 10 | 11 | class ServerTest < Test::Unit::TestCase 12 | include Rack::Test::Methods 13 | 14 | def setup 15 | Job.delete_all 16 | Runner.delete_all 17 | Build.delete_all 18 | end 19 | 20 | def app 21 | Sinatra::Application 22 | end 23 | 24 | context "POST /builds" do 25 | 26 | should "create a build and return its id" do 27 | flexmock(Runner).should_receive(:total_instances).and_return(2) 28 | post '/builds', :files => 'spec/models/car_spec.rb spec/models/house_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :available_runner_usage => "100%", :project => 'things', :sizes => "10 20", :jruby => false 29 | 30 | first_build = Build.all.first 31 | assert last_response.ok? 32 | 33 | assert_equal first_build.id.to_s, last_response.body 34 | assert_equal 'spec/models/car_spec.rb spec/models/house_spec.rb', first_build.files 35 | assert_equal '10 20', first_build.sizes 36 | assert_equal 'server:/path/to/project', first_build.root 37 | assert_equal 'spec', first_build.type 38 | assert_equal 'things', first_build.project 39 | assert_equal 0, first_build.jruby 40 | assert_equal '', first_build.results 41 | assert_equal true, first_build.success 42 | end 43 | 44 | should "create jobs from the build based on the number of total instances" do 45 | flexmock(Runner).should_receive(:total_instances).and_return(2) 46 | flexmock(Group).should_receive(:build).with(["spec/models/car_spec.rb", "spec/models/car2_spec.rb", "spec/models/house_spec.rb", "spec/models/house2_spec.rb"], [ 1, 1, 1, 1 ], 2, 'spec').once.and_return([ 47 | ["spec/models/car_spec.rb", "spec/models/car2_spec.rb"], 48 | ["spec/models/house_spec.rb", "spec/models/house2_spec.rb"] 49 | ]) 50 | 51 | post '/builds', :files => 'spec/models/car_spec.rb spec/models/car2_spec.rb spec/models/house_spec.rb spec/models/house2_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :available_runner_usage => "100%", :project => 'things', :sizes => "1 1 1 1", :jruby => true 52 | 53 | assert_equal 2, Job.count 54 | first_job, last_job = Job.all 55 | assert_equal 'spec/models/car_spec.rb spec/models/car2_spec.rb', first_job.files 56 | assert_equal 'spec/models/house_spec.rb spec/models/house2_spec.rb', last_job.files 57 | 58 | assert_equal 'server:/path/to/project', first_job.root 59 | assert_equal 'spec', first_job.type 60 | assert_equal 'things', first_job.project 61 | assert_equal 1, first_job.jruby 62 | assert_equal Build.all.first, first_job.build 63 | end 64 | 65 | should "return a 503 error if there are no known runners" do 66 | flexmock(Runner).should_receive(:total_instances).and_return(0) 67 | post '/builds', :files => 'spec/models/car_spec.rb spec/models/car2_spec.rb spec/models/house_spec.rb spec/models/house2_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :available_runner_usage => "100%", :project => 'things', :sizes => "1 1 1 1", :jruby => true 68 | assert_equal 0, Job.count 69 | assert_equal 503, last_response.status 70 | assert_equal "No runners available", last_response.body 71 | end 72 | 73 | should "only use resources according to available_runner_usage" do 74 | flexmock(Runner).should_receive(:total_instances).and_return(4) 75 | flexmock(Group).should_receive(:build).with(["spec/models/car_spec.rb", "spec/models/car2_spec.rb", "spec/models/house_spec.rb", "spec/models/house2_spec.rb"], [ 1, 1, 1, 1 ], 2, 'spec').and_return([]) 76 | post '/builds', :files => 'spec/models/car_spec.rb spec/models/car2_spec.rb spec/models/house_spec.rb spec/models/house2_spec.rb', :root => 'server:/path/to/project', :type => 'spec', :sizes => "1 1 1 1", :available_runner_usage => "50%" 77 | end 78 | 79 | end 80 | 81 | context "GET /builds/:id" do 82 | 83 | should 'return the build status' do 84 | build = Build.create(:done => false, :results => "testbot5\n..........\ncompleted", :success => false) 85 | get "/builds/#{build.id}" 86 | assert_equal true, last_response.ok? 87 | assert_equal ({ "done" => false, "results" => "testbot5\n..........\ncompleted", "success" => false }), 88 | JSON.parse(last_response.body) 89 | end 90 | 91 | should 'remove a build that is done' do 92 | build = Build.create(:done => true) 93 | get "/builds/#{build.id}" 94 | assert_equal true, JSON.parse(last_response.body)['done'] 95 | assert_equal 0, Build.count 96 | end 97 | 98 | should 'remove all related jobs of a build that is done' do 99 | build = Build.create(:done => true) 100 | related_job = Job.create(:build => build) 101 | other_job = Job.create(:build => nil) 102 | get "/builds/#{build.id}" 103 | assert !Job.find(related_job.id) 104 | assert Job.find(other_job.id) 105 | end 106 | 107 | end 108 | 109 | context "GET /jobs/next" do 110 | 111 | should "be able to return a job and mark it as taken" do 112 | build = Build.create 113 | job1 = Job.create :files => 'spec/models/car_spec.rb', :root => 'server:/project', :type => 'spec', :build => build, :project => 'things', :jruby => 1 114 | 115 | get '/jobs/next', :version => Testbot.version 116 | assert last_response.ok? 117 | 118 | assert_equal [ job1.id, build.id, "things", "server:/project", "spec", "jruby", "spec/models/car_spec.rb" ].join(','), last_response.body 119 | assert job1.taken_at != nil 120 | end 121 | 122 | should "not return a job that has already been taken" do 123 | build = Build.create 124 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now, :type => 'spec', :build => build 125 | job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build, :project => 'things', :jruby => 0 126 | get '/jobs/next', :version => Testbot.version 127 | assert last_response.ok? 128 | assert_equal [ job2.id, build.id, "things", "server:/project", "spec", "ruby", "spec/models/house_spec.rb" ].join(','), last_response.body 129 | assert job2.taken_at != nil 130 | end 131 | 132 | should "not return a job if there isnt any" do 133 | get '/jobs/next', :version => Testbot.version 134 | assert last_response.ok? 135 | assert_equal '', last_response.body 136 | end 137 | 138 | should "save which runner takes a job" do 139 | job = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => Build.create 140 | get '/jobs/next', :version => Testbot.version 141 | assert_equal Runner.first, job.taken_by 142 | end 143 | 144 | should "save information about the runners" do 145 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini.local', :uid => "00:01:...", :idle_instances => 2, :max_instances => 4 146 | runner = Runner.first 147 | assert_equal Testbot.version, runner.version 148 | assert_equal '127.0.0.1', runner.ip 149 | assert_equal 'macmini.local', runner.hostname 150 | assert_equal '00:01:...', runner.uid 151 | assert_equal 2, runner.idle_instances 152 | assert_equal 4, runner.max_instances 153 | assert (Time.now - 5) < runner.last_seen_at 154 | assert (Time.now + 5) > runner.last_seen_at 155 | end 156 | 157 | should "only create one record for the same mac" do 158 | get '/jobs/next', :version => Testbot.version, :uid => "00:01:..." 159 | get '/jobs/next', :version => Testbot.version, :uid => "00:01:..." 160 | assert_equal 1, Runner.count 161 | end 162 | 163 | should "not return anything to outdated clients" do 164 | Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project' 165 | get '/jobs/next', :version => "1", :uid => "00:..." 166 | assert last_response.ok? 167 | assert_equal '', last_response.body 168 | end 169 | 170 | should "only give jobs from the same source to a runner" do 171 | build = Build.create 172 | job1 = Job.create :files => 'spec/models/car_spec.rb', :type => 'spec', :build => build 173 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :build_id => build.id 174 | 175 | # Creating the second job here because of the random lookup. 176 | job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build 177 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :build_id => build.id + 1 178 | 179 | assert last_response.ok? 180 | assert_equal '', last_response.body 181 | end 182 | 183 | should "not give more jruby jobs to an instance that can't take more" do 184 | build = Build.create 185 | job1 = Job.create :files => 'spec/models/car_spec.rb', :type => 'spec', :jruby => 1, :build => build 186 | get '/jobs/next', :version => Testbot.version, :uid => "00:..." 187 | 188 | # Creating the second job here because of the random lookup. 189 | job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :jruby => 1, :build => build 190 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :no_jruby => "true" 191 | 192 | assert last_response.ok? 193 | assert_equal '', last_response.body 194 | end 195 | 196 | should "still return other jobs when the runner cant take more jruby jobs" do 197 | job1 = Job.create :files => 'spec/models/car_spec.rb', :type => 'spec', :jruby => 1, :build => Build.create 198 | get '/jobs/next', :version => Testbot.version, :uid => "00:..." 199 | 200 | # Creating the second job here because of the random lookup. 201 | job2 = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :jruby => 0, :build => Build.create 202 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :no_jruby => "true" 203 | 204 | assert last_response.ok? 205 | assert_equal job2.id.to_s, last_response.body.split(',')[0] 206 | end 207 | 208 | should "return the jobs in random order in order to start working for a new build right away" do 209 | build1, build2 = Build.create, Build.create 210 | 20.times { Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build1 } 211 | 212 | 20.times { Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build2 } 213 | 214 | build_ids = (0...10).map { 215 | get '/jobs/next', :version => Testbot.version, :uid => "00:..." 216 | last_response.body.split(',')[1] 217 | } 218 | 219 | assert build_ids.find { |build_id| build_id == build1.id.to_s } 220 | assert build_ids.find { |build_id| build_id == build2.id.to_s } 221 | end 222 | 223 | should "return the jobs randomly when passing build_id" do 224 | build = Build.create 225 | 20.times { Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build } 226 | 227 | 20.times { Job.create :files => 'spec/models/car_spec.rb', :root => 'server:/project', :type => 'spec', :build => build } 228 | 229 | files = (0...10).map { 230 | get '/jobs/next', :version => Testbot.version, :uid => "00:...", :build_id => build.id 231 | last_response.body.split(',').last 232 | } 233 | 234 | assert files.find { |file| file.include?('car') } 235 | assert files.find { |file| file.include?('house') } 236 | end 237 | 238 | should "return taken jobs to other runners if the runner hasn't been seen for 10 seconds or more" do 239 | missing_runner = Runner.create(:last_seen_at => Time.now - 15) 240 | build = Build.create 241 | old_taken_job = Job.create :files => 'spec/models/house_spec.rb', :root => 'server:/project', :type => 'spec', :build => build, :taken_by => missing_runner, :taken_at => Time.now - 30, :project => 'things' 242 | 243 | new_runner = Runner.create(:uid => "00:01") 244 | get '/jobs/next', :version => Testbot.version, :uid => "00:01" 245 | assert_equal new_runner, old_taken_job.taken_by 246 | 247 | assert last_response.ok? 248 | assert_equal [ old_taken_job.id, build.id.to_s, "things", "server:/project", "spec", "ruby", "spec/models/house_spec.rb" ].join(','), last_response.body 249 | end 250 | 251 | end 252 | 253 | context "/runners/outdated" do 254 | 255 | should "return a list of outdated runners" do 256 | get '/jobs/next', :version => "1", :hostname => 'macmini1.local', :uid => "00:01" 257 | get '/jobs/next', :version => "1", :hostname => 'macmini2.local', :uid => "00:02" 258 | get '/jobs/next' 259 | get '/jobs/next', :version => Testbot.version.to_s, :hostname => 'macmini3.local', :uid => "00:03" 260 | assert_equal 4, Runner.count 261 | get '/runners/outdated' 262 | assert last_response.ok? 263 | assert_equal "127.0.0.1 macmini1.local 00:01\n127.0.0.1 macmini2.local 00:02\n127.0.0.1", last_response.body 264 | end 265 | 266 | end 267 | 268 | context "GET /runners/available_runners" do 269 | 270 | should "return a list of available runners" do 271 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2, :username => 'user1' 272 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4, :username => 'user2' 273 | get '/runners/available' 274 | assert last_response.ok? 275 | assert_equal "127.0.0.1 macmini1.local 00:01 user1 2\n127.0.0.1 macmini2.local 00:02 user2 4", last_response.body 276 | end 277 | 278 | should "not return a runner as available when it hasnt pinged the server yet" do 279 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :username => 'user1' 280 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4, :username => 'user2' 281 | get '/runners/available' 282 | assert last_response.ok? 283 | assert_equal "127.0.0.1 macmini2.local 00:02 user2 4", last_response.body 284 | end 285 | 286 | should "not return runners as available when not seen the last 10 seconds" do 287 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2, :username => "user1" 288 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4 289 | Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10) 290 | get '/runners/available' 291 | assert_equal "127.0.0.1 macmini1.local 00:01 user1 2", last_response.body 292 | end 293 | 294 | end 295 | 296 | context "GET /runners/available_instances" do 297 | 298 | should "return the number of available runner instances" do 299 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2 300 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4 301 | get '/runners/available_instances' 302 | assert last_response.ok? 303 | assert_equal "6", last_response.body 304 | end 305 | 306 | should "not return instances as available when not seen the last 10 seconds" do 307 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2 308 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4 309 | Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10) 310 | get '/runners/available_instances' 311 | assert last_response.ok? 312 | assert_equal "2", last_response.body 313 | end 314 | 315 | end 316 | 317 | context "GET /runners/total_instances" do 318 | 319 | should "return the number of available runner instances" do 320 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :max_instances => 2, :idle_instances => 1 321 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :max_instances => 4, :idle_instances => 2 322 | get '/runners/total_instances' 323 | assert last_response.ok? 324 | assert_equal "6", last_response.body 325 | end 326 | 327 | should "not return instances as available when not seen the last 10 seconds" do 328 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :max_instances => 2, :idle_instances => 1 329 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :max_instances => 4, :idle_instances => 2 330 | Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10) 331 | get '/runners/total_instances' 332 | assert last_response.ok? 333 | assert_equal "2", last_response.body 334 | end 335 | 336 | end 337 | 338 | context "GET /runners/ping" do 339 | 340 | should "update last_seen_at for the runner" do 341 | runner = Runner.create(:uid => 'aa:aa:aa:aa:aa:aa') 342 | get "/runners/ping", :uid => 'aa:aa:aa:aa:aa:aa', :version => Testbot.version 343 | assert last_response.ok? 344 | assert (Time.now - 5) < runner.last_seen_at 345 | assert (Time.now + 5) > runner.last_seen_at 346 | end 347 | 348 | should "update data on the runner" do 349 | build = Build.create 350 | runner = Runner.create(:uid => 'aa:aa:..') 351 | get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => build.id 352 | assert last_response.ok? 353 | assert_equal 'aa:aa:..', runner.uid 354 | assert_equal 4, runner.max_instances 355 | assert_equal 2, runner.idle_instances 356 | assert_equal 'hostname1', runner.hostname 357 | assert_equal Testbot.version, runner.version 358 | assert_equal 'jocke', runner.username 359 | assert_equal build, runner.build 360 | end 361 | 362 | should "do nothing if the version does not match" do 363 | runner = Runner.create(:uid => 'aa:aa:..', :version => Testbot.version) 364 | get "/runners/ping", :uid => 'aa:aa:..', :version => "OLD" 365 | assert last_response.ok? 366 | assert_equal Testbot.version, runner.version 367 | end 368 | 369 | should "do nothing if the runners isnt known yet found" do 370 | get "/runners/ping", :uid => 'aa:aa:aa:aa:aa:aa', :version => Testbot.version 371 | assert last_response.ok? 372 | end 373 | 374 | should "return an order to stop the build if the build id does not exist anymore" do 375 | runner = Runner.create(:uid => 'aa:aa:..') 376 | get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => 1 377 | assert_equal last_response.body, "stop_build,1" 378 | end 379 | 380 | should "not return an order to stop a build without an id" do 381 | runner = Runner.create(:uid => 'aa:aa:..') 382 | get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => '' 383 | assert_equal last_response.body, '' 384 | get "/runners/ping", :uid => 'aa:aa:..', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'jocke', :build_id => nil 385 | assert_equal last_response.body, '' 386 | end 387 | 388 | end 389 | 390 | context "PUT /jobs/:id" do 391 | 392 | should "receive the results of a job" do 393 | job = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30 394 | put "/jobs/#{job.id}", :result => 'test run result', :status => "successful" 395 | assert last_response.ok? 396 | assert_equal 'test run result', job.result 397 | assert_equal 'successful', job.status 398 | end 399 | 400 | should "update the related build" do 401 | build = Build.create 402 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build 403 | job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build 404 | put "/jobs/#{job1.id}", :result => 'test run result 1\n', :status => "successful" 405 | put "/jobs/#{job2.id}", :result => 'test run result 2\n', :status => "successful" 406 | assert_equal 'test run result 1\ntest run result 2\n', build.results 407 | assert_equal true, build.success 408 | end 409 | 410 | should "make the related build done if there are no more jobs for the build" do 411 | build = Build.create 412 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build 413 | job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build 414 | put "/jobs/#{job1.id}", :result => 'test run result 1\n', :status => "successful" 415 | put "/jobs/#{job2.id}", :result => 'test run result 2\n', :status => "successful" 416 | assert_equal true, build.done 417 | end 418 | 419 | should "make the build fail if one of the jobs fail" do 420 | build = Build.create 421 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build 422 | job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build 423 | put "/jobs/#{job1.id}", :result => 'test run result 1\n', :status => "failed" 424 | put "/jobs/#{job2.id}", :result => 'test run result 2\n', :status => "successful" 425 | assert_equal false, build.success 426 | end 427 | 428 | should "be able to update from multiple result postings" do 429 | build = Build.create 430 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build 431 | job2 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build 432 | # maybe later: 433 | # put "/jobs/#{job.id}", :result => 'Preparing, db setup, etc.', :status => "preparing" 434 | put "/jobs/#{job1.id}", :result => 'Running tests..', :status => "running" 435 | put "/jobs/#{job2.id}", :result => 'Running other tests. done.', :status => "successful" 436 | put "/jobs/#{job1.id}", :result => 'Running tests....', :status => "running" 437 | assert_equal false, build.done 438 | assert_equal false, job1.done 439 | assert_equal "Running tests....", job1.result 440 | assert_equal "Running tests..Running other tests. done...", build.results 441 | end 442 | 443 | should "not break when updating without new results" do 444 | build = Build.create 445 | job1 = Job.create :files => 'spec/models/car_spec.rb', :taken_at => Time.now - 30, :build => build 446 | put "/jobs/#{job1.id}", :result => 'Running tests..', :status => "running" 447 | put "/jobs/#{job1.id}", :result => '', :status => "successful" 448 | assert_equal "Running tests..", build.results 449 | end 450 | 451 | end 452 | 453 | context "GET /version" do 454 | 455 | should "return its version" do 456 | get '/version' 457 | assert last_response.ok? 458 | assert_equal Testbot.version.to_s, last_response.body 459 | end 460 | 461 | end 462 | 463 | context "GET /runners" do 464 | 465 | should "return runner information in json format" do 466 | get '/jobs/next', :version => Testbot.version, :uid => "00:01" 467 | get "/runners/ping", :uid => '00:01', :max_instances => 4, :idle_instances => 2, :hostname => "hostname1", :version => Testbot.version, :username => 'testbot', :build_id => nil 468 | get '/runners' 469 | 470 | assert last_response.ok? 471 | assert_equal ([ { "version" => Testbot.version.to_s, "build" => nil, "hostname" => 'hostname1', "uid" => "00:01", 472 | "idle_instances" => 2, "max_instances" => 4, "username" => 'testbot', 473 | "ip" => "127.0.0.1", "last_seen_at" => Runner.first.last_seen_at.to_s } ]), 474 | JSON.parse(last_response.body) 475 | end 476 | 477 | should "not return instances when not seen the last 10 seconds" do 478 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini1.local', :uid => "00:01", :idle_instances => 2 479 | get '/jobs/next', :version => Testbot.version, :hostname => 'macmini2.local', :uid => "00:02", :idle_instances => 4 480 | Runner.find_by_uid("00:02").update(:last_seen_at => Time.now - 10) 481 | get '/runners' 482 | assert last_response.ok? 483 | parsed_body = JSON.parse(last_response.body) 484 | assert_equal 1, parsed_body.size 485 | assert_equal '00:01', parsed_body.first["uid"] 486 | end 487 | 488 | end 489 | 490 | context "GET /status" do 491 | 492 | should "return the contents of the status page" do 493 | get '/status' 494 | assert_equal true, last_response.body.include?('Testbot status') 495 | end 496 | 497 | end 498 | 499 | context "GET /status/:dir/:file" do 500 | 501 | should "return the file" do 502 | get "/status/javascripts/jquery-1.4.4.min.js" 503 | assert_equal true, last_response.body.include?('jQuery JavaScript Library v1.4.4') 504 | end 505 | 506 | end 507 | 508 | end 509 | 510 | end 511 | 512 | -------------------------------------------------------------------------------- /test/shared/adapters/adapter_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '../../../lib/shared/adapters/adapter.rb')) 2 | require 'test/unit' 3 | require 'shoulda' 4 | 5 | class AdapterTest < Test::Unit::TestCase 6 | 7 | should "be able to find adapters" do 8 | assert_equal RspecAdapter, Adapter.find(:spec) 9 | assert_equal TestUnitAdapter, Adapter.find(:test) 10 | end 11 | 12 | should "find be able to find an adapter by string" do 13 | assert_equal RspecAdapter, Adapter.find("spec") 14 | assert_equal TestUnitAdapter, Adapter.find("test") 15 | end 16 | 17 | should "be able to return a list of adapters" do 18 | assert Adapter.all.include?(RspecAdapter) 19 | assert Adapter.all.include?(TestUnitAdapter) 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /test/shared/adapters/cucumber_adapter_test.rb: -------------------------------------------------------------------------------- 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 =< "script/spec", :bin => "rspec") 55 | end 56 | 57 | should "use bundler when available and use the binary when there is no script" do 58 | flexmock(RubyEnv).should_receive(:bundler?).once.with("path/to/project").and_return(true) 59 | flexmock(File).should_receive(:exists?).with("path/to/project/script/spec").and_return(false) 60 | assert_equal 'ruby -S bundle exec rspec', RubyEnv.ruby_command("path/to/project", :script => "script/spec", :bin => "rspec") 61 | end 62 | 63 | should "use the script when it exists when using bundler" do 64 | flexmock(RubyEnv).should_receive(:bundler?).and_return(true) 65 | flexmock(File).should_receive(:exists?).and_return(true) 66 | assert_equal 'ruby -S bundle exec script/spec', RubyEnv.ruby_command("path/to/project", :script => "script/spec", :bin => "rspec") 67 | end 68 | 69 | should "use the script when it exists when not using bundler" do 70 | flexmock(RubyEnv).should_receive(:bundler?).and_return(false) 71 | flexmock(File).should_receive(:exists?).and_return(true) 72 | assert_equal 'ruby -S script/spec', RubyEnv.ruby_command("path/to/project", :script => "script/spec", :bin => "rspec") 73 | end 74 | 75 | should "not look for a script when none is provided" do 76 | assert_equal 'ruby -S rspec', RubyEnv.ruby_command("path/to/project", :bin => "rspec") 77 | end 78 | 79 | should "be able to use jruby" do 80 | flexmock(RubyEnv).should_receive(:bundler?).and_return(false) 81 | flexmock(File).should_receive(:exists?).and_return(true) 82 | assert_equal 'jruby -S script/spec', RubyEnv.ruby_command("path/to/project", :script => "script/spec", 83 | :bin => "rspec", :ruby_interpreter => "jruby") 84 | end 85 | 86 | should "be able to use jruby with bundler" do 87 | flexmock(RubyEnv).should_receive(:bundler?).and_return(true) 88 | flexmock(File).should_receive(:exists?).and_return(true) 89 | assert_equal 'jruby -S bundle exec script/spec', RubyEnv.ruby_command("path/to/project", :script => "script/spec", 90 | :bin => "rspec", :ruby_interpreter => "jruby") 91 | end 92 | 93 | should "work when there is no binary specified and bundler is present" do 94 | flexmock(RubyEnv).should_receive(:bundler?).and_return(true) 95 | flexmock(File).should_receive(:exists?).and_return(false) 96 | assert_equal 'ruby -S bundle exec', RubyEnv.ruby_command("path/to/project") 97 | end 98 | 99 | should "work when there is no binary specified and bundler is not present" do 100 | flexmock(RubyEnv).should_receive(:bundler?).and_return(false) 101 | flexmock(File).should_receive(:exists?).and_return(false) 102 | assert_equal 'ruby -S', RubyEnv.ruby_command("path/to/project") 103 | end 104 | 105 | 106 | end 107 | 108 | end 109 | -------------------------------------------------------------------------------- /test/shared/adapters/rspec_adapter_test.rb: -------------------------------------------------------------------------------- 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 =< "192.168.0.100", 15 | :rsync_path => nil, 16 | :rsync_ignores => '', :server_user => nil, :available_runner_usage => nil, 17 | :project => nil, :ssh_tunnel => nil } 18 | end 19 | 20 | end 21 | 22 | class CLITest < Test::Unit::TestCase 23 | 24 | include TestHelpers 25 | 26 | context "self.run" do 27 | 28 | context "with no args" do 29 | should "return false" do 30 | assert_equal false, CLI.run([]) 31 | end 32 | end 33 | 34 | context "with --help" do 35 | should "return false" do 36 | assert_equal false, CLI.run([ '--help' ]) 37 | end 38 | end 39 | 40 | context "with --version" do 41 | should "print version and return true" do 42 | flexmock(CLI).should_receive(:puts).once.with("Testbot #{Testbot.version}") 43 | assert_equal true, CLI.run([ '--version' ]) 44 | end 45 | end 46 | 47 | context "with --server" do 48 | should "start a server" do 49 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::SERVER_PID) 50 | flexmock(SimpleDaemonize).should_receive(:start).once.with(any, Testbot::SERVER_PID, "testbot (server)") 51 | flexmock(CLI).should_receive(:puts).once.with("Testbot server started (pid: #{Process.pid})") 52 | assert_equal true, CLI.run([ "--server" ]) 53 | end 54 | 55 | should "start a server when start is passed" do 56 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::SERVER_PID) 57 | flexmock(SimpleDaemonize).should_receive(:start).once 58 | flexmock(CLI).should_receive(:puts) 59 | assert_equal true, CLI.run([ "--server", "start" ]) 60 | end 61 | 62 | should "stop a server when stop is passed" do 63 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::SERVER_PID).and_return(true) 64 | flexmock(CLI).should_receive(:puts).once.with("Testbot server stopped") 65 | assert_equal true, CLI.run([ "--server", "stop" ]) 66 | end 67 | 68 | should "not print when SimpleDaemonize.stop returns false" do 69 | flexmock(SimpleDaemonize).should_receive(:stop).and_return(false) 70 | flexmock(CLI).should_receive(:puts).never 71 | CLI.run([ "--stop", "server" ]) 72 | end 73 | 74 | should "start it in the foreground with run" do 75 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::SERVER_PID) 76 | flexmock(SimpleDaemonize).should_receive(:start).never 77 | flexmock(Sinatra::Application).should_receive(:run!).once.with(:environment => "production") 78 | assert_equal true, CLI.run([ "--server", 'run' ]) 79 | end 80 | end 81 | 82 | context "with --runner" do 83 | should "start a runner" do 84 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::RUNNER_PID) 85 | flexmock(SimpleDaemonize).should_receive(:start).once.with(any, Testbot::RUNNER_PID, "testbot (runner)") 86 | flexmock(CLI).should_receive(:puts).once.with("Testbot runner started (pid: #{Process.pid})") 87 | assert_equal true, CLI.run([ "--runner", "--connect", "192.168.0.100", "--working_dir", "/tmp/testbot" ]) 88 | end 89 | 90 | should "start a runner when start is passed" do 91 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::RUNNER_PID) 92 | flexmock(SimpleDaemonize).should_receive(:start).once 93 | flexmock(CLI).should_receive(:puts) 94 | assert_equal true, CLI.run([ "--runner", "start", "--connect", "192.168.0.100" ]) 95 | end 96 | 97 | should "stop a runner when stop is passed" do 98 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::RUNNER_PID).and_return(true) 99 | flexmock(CLI).should_receive(:puts).once.with("Testbot runner stopped") 100 | assert_equal true, CLI.run([ "--runner", "stop" ]) 101 | end 102 | 103 | should "return false without connect" do 104 | assert_equal false, CLI.run([ "--runner", "--connect" ]) 105 | assert_equal false, CLI.run([ "--runner" ]) 106 | end 107 | 108 | should "start it in the foreground with run" do 109 | flexmock(SimpleDaemonize).should_receive(:stop).once.with(Testbot::RUNNER_PID) 110 | flexmock(SimpleDaemonize).should_receive(:start).never 111 | flexmock(Runner::Runner).should_receive(:new).once.and_return(mock = Object.new) 112 | flexmock(mock).should_receive(:run!).once 113 | assert_equal true, CLI.run([ "--runner", 'run', '--connect', '192.168.0.100' ]) 114 | end 115 | end 116 | 117 | Adapter.all.each do |adapter| 118 | context "with --#{adapter.type}" do 119 | should "start a #{adapter.name} requester and return true" do 120 | flexmock(Requester::Requester).should_receive(:new).once. 121 | with(requester_attributes).and_return(mock = Object.new) 122 | flexmock(mock).should_receive(:run_tests).once.with(adapter, adapter.base_path) 123 | assert_equal true, CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100" ]) 124 | end 125 | 126 | should "accept a custom rsync_path" do 127 | flexmock(Requester::Requester).should_receive(:new).once. 128 | with(requester_attributes.merge({ :rsync_path => "/somewhere/else" })). 129 | and_return(mock = Object.new) 130 | flexmock(mock).should_receive(:run_tests) 131 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--rsync_path', '/somewhere/else' ]) 132 | end 133 | 134 | should "accept rsync_ignores" do 135 | flexmock(Requester::Requester).should_receive(:new).once. 136 | with(requester_attributes.merge({ :rsync_ignores => "tmp log" })). 137 | and_return(mock = Object.new) 138 | flexmock(mock).should_receive(:run_tests) 139 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--rsync_ignores', 'tmp', 'log' ]) 140 | end 141 | 142 | should "accept ssh tunnel" do 143 | flexmock(Requester::Requester).should_receive(:new).once. 144 | with(requester_attributes.merge({ :ssh_tunnel => true })). 145 | and_return(mock = Object.new) 146 | flexmock(mock).should_receive(:run_tests) 147 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--ssh_tunnel' ]) 148 | end 149 | 150 | should "accept a custom user" do 151 | flexmock(Requester::Requester).should_receive(:new).once. 152 | with(requester_attributes.merge({ :server_user => "cruise" })). 153 | and_return(mock = Object.new) 154 | flexmock(mock).should_receive(:run_tests) 155 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--user', 'cruise' ]) 156 | end 157 | 158 | should "accept a custom project name" do 159 | flexmock(Requester::Requester).should_receive(:new).once. 160 | with(requester_attributes.merge({ :project => "rspec" })). 161 | and_return(mock = Object.new) 162 | flexmock(mock).should_receive(:run_tests) 163 | CLI.run([ "--#{adapter.type}", "--connect", "192.168.0.100", '--project', 'rspec' ]) 164 | end 165 | end 166 | end 167 | end 168 | 169 | context "self.parse_args" do 170 | 171 | should 'convert ARGV arguments to a hash' do 172 | hash = CLI.parse_args("--runner --connect http://127.0.0.1:#{Testbot::SERVER_PORT} --working_dir ~/testbot --ssh_tunnel user@testbot_server".split) 173 | assert_equal ({ :runner => true, :connect => "http://127.0.0.1:#{Testbot::SERVER_PORT}", :working_dir => "~/testbot", :ssh_tunnel => "user@testbot_server" }), hash 174 | end 175 | 176 | should "handle just a key without a value" do 177 | hash = CLI.parse_args([ "--server" ]) 178 | assert_equal ({ :server => true }), hash 179 | end 180 | 181 | end 182 | 183 | end 184 | 185 | end 186 | -------------------------------------------------------------------------------- /testbot.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path(File.dirname(__FILE__) + '/lib/shared/version') 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "testbot" 6 | s.version = Testbot.version 7 | s.authors = ["Joakim Kolsjö"] 8 | s.email = ["joakim.kolsjo@gmail.com"] 9 | s.homepage = "http://github.com/joakimk/testbot" 10 | s.summary = %q{A test distribution tool.} 11 | s.description = %q{Testbot is a test distribution tool that works with Rails, RSpec, RSpec2, Test::Unit and Cucumber.} 12 | s.bindir = "bin" 13 | s.executables = [ "testbot" ] 14 | s.files = Dir.glob("lib/**/*") + Dir.glob("test/**/*") + %w(Gemfile .gemtest Rakefile testbot.gemspec CHANGELOG README.markdown bin/testbot) + 15 | (File.exists?("DEV_VERSION") ? [ "DEV_VERSION" ] : []) 16 | 17 | s.add_dependency('sinatra', '~> 1.0') 18 | s.add_dependency('httparty', '>= 0.6.1') 19 | s.add_dependency('net-ssh', '>= 2.0.23') 20 | s.add_dependency('json_pure', '>= 1.4.6') 21 | s.add_dependency('daemons', '>= 1.0.10') 22 | s.add_dependency('acts_as_rails3_generator') 23 | s.add_dependency('posix-spawn', '>= 0.3.6') 24 | 25 | s.add_development_dependency("shoulda") 26 | s.add_development_dependency("rack-test") 27 | s.add_development_dependency("flexmock") 28 | s.add_development_dependency("rvm") 29 | s.add_development_dependency("rake", "0.8.7") 30 | s.add_development_dependency("bundler") 31 | s.add_development_dependency("guard") 32 | s.add_development_dependency("guard-test") 33 | end 34 | 35 | --------------------------------------------------------------------------------