├── .github └── workflows │ └── dynamic-security.yml ├── .gitignore ├── .rspec ├── .travis.yml ├── Appraisals ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── SECURITY.md ├── capybara_discoball.gemspec ├── gemfiles ├── capybara_2.gemfile └── capybara_3.gemfile ├── lib ├── capybara_discoball.rb └── capybara_discoball │ ├── retryable.rb │ ├── runner.rb │ ├── server.rb │ └── version.rb └── spec ├── black_box └── rails_app_spec.rb ├── lib ├── capybara_discoball │ └── runner_spec.rb └── capybara_discoball_spec.rb └── spec_helper.rb /.github/workflows/dynamic-security.yml: -------------------------------------------------------------------------------- 1 | name: update-security 2 | 3 | on: 4 | push: 5 | paths: 6 | - SECURITY.md 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-security: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | tmp/* 6 | gemfiles/*.lock 7 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.3" 4 | - "2.4" 5 | - "2.5" 6 | sudo: false 7 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "capybara-2" do 2 | gem "capybara", "~> 2.7" 3 | end 4 | 5 | appraise "capybara-3" do 6 | gem "capybara", "~> 3.0" 7 | end 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.1.0 4 | 5 | - Runner retries finding an unused port [#15] 6 | 7 | ## v0.0.3 8 | 9 | - Add Capybara 3.x support [#19] 10 | 11 | ## v0.0.2 12 | 13 | - Use `Capybara.register_server`, introduced in Capybara 2.7.0 14 | - Require Capybara ~> 2.7 15 | 16 | ## v0.0.1 17 | 18 | - Initial release 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We love pull requests from everyone. Follow the thoughtbot [code of conduct] 2 | while contributing. 3 | 4 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 5 | 6 | ## Contributing 7 | 8 | 1. Fork the repo. 9 | 2. Run the tests. We only take pull requests with passing tests, and it's 10 | great to know that you have a clean slate. 11 | 3. Add a test for your change. Only refactoring and documentation changes 12 | require no new tests. If you are adding functionality or fixing a bug, we 13 | need a test! 14 | 4. Make the test pass. 15 | 5. Push to your fork and submit a pull request. 16 | 17 | At this point you're waiting on us. We like to at least comment on, if not 18 | accept, pull requests within three business days (and, typically, one business 19 | day). We may suggest some changes or improvements or alternatives. 20 | 21 | Some things that will increase the chance that your pull request is accepted, 22 | 23 | * Include tests that fail without your code, and pass with it 24 | * Update the documentation, the surrounding one, examples elsewhere, guides, 25 | whatever is affected by your contribution 26 | * Follow the existing style of the project 27 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in capybara_discoball.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012-2015 thoughtbot, inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | capybara_discoball 2 | ================== 3 | 4 | [![Build Status](https://travis-ci.org/thoughtbot/capybara_discoball.svg?branch=master)](https://travis-ci.org/thoughtbot/capybara_discoball) 5 | [![Code Climate](https://codeclimate.com/github/thoughtbot/capybara_discoball/badges/gpa.svg)](https://codeclimate.com/github/thoughtbot/capybara_discoball) 6 | 7 | Spin up a rack app just for Capybara. 8 | 9 | This is useful for when ShamRack won't cut it: when your JavaScript hits 10 | an external service, or you need to load an image or iframe from 11 | elsewhere, or in general something outside of your Ruby code needs to 12 | talk with an API. 13 | 14 | Synopsis 15 | -------- 16 | 17 | ```ruby 18 | # Use Sinatra, for example 19 | require 'sinatra/base' 20 | require 'capybara_discoball' 21 | 22 | # Define a quick Rack app 23 | class FakeMusicDB < Sinatra::Base 24 | cattr_reader :albums 25 | 26 | get '/musicians/:musician/albums' do |musician| 27 | <<-XML 28 | 29 | #{@albums.map { |album| "#{album}" }.join} 30 | 31 | XML 32 | end 33 | end 34 | 35 | # Spin up the Rack app, then update the imaginary library we're 36 | # using to point to the new URL. 37 | Capybara::Discoball.spin(FakeMusicDB) do |server| 38 | MusicDB.endpoint_url = server.url 39 | end 40 | ``` 41 | 42 | More details 43 | ------------ 44 | 45 | You can instantiate a `Capybara::Discoball::Runner`, passing in a 46 | factory which will create a Rack app: 47 | 48 | ```ruby 49 | FakeMusicDBRunner = Capybara::Discoball::Runner.new(FakeMusicDB) do 50 | # tests to perform after server boot 51 | end 52 | ``` 53 | 54 | This gives you back a runner, which you can boot from your features, 55 | specs, tests, console, whatever: 56 | 57 | ```ruby 58 | FakeMusicDBRunner.boot 59 | ``` 60 | 61 | These two steps can be merged with the `spin` class method: 62 | 63 | ```ruby 64 | Capybara::Discoball.spin(FakeMusicDB) do 65 | # tests to perform while server is spinning 66 | end 67 | ``` 68 | 69 | It is always the case that you need to know the URL for the external 70 | API. We provide a way to access that URL; in fact, we offer the whole 71 | `Capybara::Server` for you to play with. In this example, we are using 72 | some `MusicDB` library in the code that knows to hit the 73 | `.endpoint_url`: 74 | 75 | ```ruby 76 | FakeMusicDBRunner = Capybara::Discoball::Runner.new(FakeMusicDB) do |server| 77 | MusicDB.endpoint_url = server.url 78 | end 79 | ``` 80 | 81 | If no block is provided, the URL is also returned by `#spin`: 82 | 83 | ```ruby 84 | MusicDB.endpoint_url = Capybara::Discoball.spin(FakeMusicDB) 85 | ``` 86 | 87 | Integrating into your app 88 | ------------------------- 89 | 90 | All of this means that you must be able to set the endpoint URL. There 91 | are two tricky cases: 92 | 93 | *When the third-party library does not have hooks to set the endpoint 94 | URL*. 95 | 96 | Open the class and add the hooks yourself. This requires understanding 97 | the source of the library. Here's an example where the library uses 98 | `@@endpoint_url` everywhere to refer to the endpoint URL: 99 | 100 | ```ruby 101 | class MusicDB 102 | def self.endpoint_url=(endpoint_url) 103 | @@endpoint_url = endpoint_url 104 | end 105 | end 106 | ``` 107 | 108 | *When your JavaScript needs to talk to the endpoint URL*. 109 | 110 | For this you must thread the URL through your app so that the JavaScript 111 | can find it: 112 | 113 | ```ruby 114 | <% content_for :javascript do %> 115 | <% javascript_tag do %> 116 | albumShower = new AlbumShower(<%= MusicDB.endpoint_url.to_json %>); 117 | albumShower.show(); 118 | <% end %> 119 | <% end %> 120 | 121 | class @AlbumShower 122 | constructor: (@endpointUrl) -> 123 | show: -> 124 | $.get(@endpointUrl, (data) -> $('#albums').html(data)) 125 | ``` 126 | 127 | Contributing 128 | ------------ 129 | 130 | See the [CONTRIBUTING] document. Thank you, [contributors]! 131 | 132 | [CONTRIBUTING]: /CONTRIBUTING.md 133 | [contributors]: https://github.com/thoughtbot/capybara_discoball/graphs/contributors 134 | 135 | License 136 | ------- 137 | 138 | capybara_discoball is Copyright (c) 2012-2018 thoughtbot, inc. It is free software, 139 | and may be redistributed under the terms specified in the [LICENSE] file. 140 | 141 | [LICENSE]: /LICENSE 142 | 143 | About 144 | ----- 145 | 146 | ![thoughtbot](https://thoughtbot.com/logo.png) 147 | 148 | capybara_discoball is maintained and funded by thoughtbot, inc. 149 | The names and logos for thoughtbot are trademarks of thoughtbot, inc. 150 | 151 | We love open source software! 152 | See [our other projects][community] 153 | or [hire us][hire] to help build your product. 154 | 155 | [community]: https://thoughtbot.com/community?utm_source=github 156 | [hire]: https://thoughtbot.com/hire-us?utm_source=github 157 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | desc "Appraisals: Test different dependency versions" 4 | task :run_appraisals do 5 | sh("bundle exec appraisal install") 6 | sh("bundle exec appraisal rspec --tag ~type:black_box") 7 | end 8 | 9 | begin 10 | require "rspec/core/rake_task" 11 | RSpec::Core::RakeTask.new(:spec) 12 | task default: [:spec, :run_appraisals] 13 | rescue LoadError 14 | end 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | # Security Policy 3 | 4 | ## Supported Versions 5 | 6 | Only the the latest version of this project is supported at a given time. If 7 | you find a security issue with an older version, please try updating to the 8 | latest version first. 9 | 10 | If for some reason you can't update to the latest version, please let us know 11 | your reasons so that we can have a better understanding of your situation. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | For security inquiries or vulnerability reports, visit 16 | . 17 | 18 | If you have any suggestions to improve this policy, visit . 19 | 20 | 21 | -------------------------------------------------------------------------------- /capybara_discoball.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "capybara_discoball/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "capybara_discoball" 7 | s.version = Capybara::Discoball::VERSION 8 | s.authors = ["Mike Burns"] 9 | s.email = ["mburns@thoughtbot.com"] 10 | s.homepage = "" 11 | s.summary = %q{Spin up an external server just for Capybara} 12 | s.description = <<-DESC 13 | When ShamRack doesn't quite cut it; when your JavaScript and non-Ruby 14 | code needs to hit an external API for your tests; when you're excited 15 | about spinning up a full server instead of faking out Net::HTTP: we 16 | present the Discoball. 17 | DESC 18 | 19 | s.rubyforge_project = "capybara_discoball" 20 | 21 | s.files = `git ls-files`.split("\n") 22 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 23 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 24 | s.require_paths = ["lib"] 25 | 26 | s.add_dependency 'capybara', '>= 2.7', '< 4' 27 | 28 | s.add_development_dependency 'appraisal' 29 | s.add_development_dependency 'jet_black', '~> 0.2' 30 | s.add_development_dependency 'pry' 31 | s.add_development_dependency 'rake' 32 | s.add_development_dependency 'rspec' 33 | s.add_development_dependency 'sinatra' 34 | end 35 | -------------------------------------------------------------------------------- /gemfiles/capybara_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "capybara", "~> 2.7" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/capybara_3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "capybara", "~> 3.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /lib/capybara_discoball.rb: -------------------------------------------------------------------------------- 1 | require "capybara_discoball/version" 2 | require "capybara_discoball/server" 3 | require "capybara_discoball/runner" 4 | 5 | module Capybara 6 | module Discoball 7 | def self.spin(app, &block) 8 | Runner.new(app, &block).boot 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/capybara_discoball/retryable.rb: -------------------------------------------------------------------------------- 1 | module Capybara 2 | module Discoball 3 | module Retryable 4 | def with_retries(retry_count, *rescuable_exceptions) 5 | yield 6 | rescue *rescuable_exceptions => e 7 | if retry_count > 0 8 | retry_count -= 1 9 | puts e.inspect if ENV.key?("DEBUG") 10 | retry 11 | else 12 | raise 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/capybara_discoball/runner.rb: -------------------------------------------------------------------------------- 1 | require "capybara" 2 | require_relative "retryable" 3 | 4 | module Capybara 5 | module Discoball 6 | class Runner 7 | include Capybara::Discoball::Retryable 8 | 9 | RETRY_COUNT = 3 10 | 11 | def initialize(server_factory, &block) 12 | @server_factory = server_factory 13 | @after_server = block || Proc.new {} 14 | end 15 | 16 | def boot 17 | with_webrick_runner do 18 | @server = Server.new(@server_factory.new) 19 | @server.boot 20 | end 21 | 22 | @after_server.call(@server) 23 | @server.url 24 | end 25 | 26 | private 27 | 28 | def with_webrick_runner 29 | default_server_process = Capybara.server 30 | Capybara.server = :webrick 31 | 32 | with_retries(RETRY_COUNT, Errno::EADDRINUSE) { yield } 33 | ensure 34 | Capybara.server = default_server_process 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/capybara_discoball/server.rb: -------------------------------------------------------------------------------- 1 | require "capybara/server" 2 | 3 | module Capybara 4 | module Discoball 5 | class Server < ::Capybara::Server 6 | def url 7 | "http://#{host}:#{port}" 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/capybara_discoball/version.rb: -------------------------------------------------------------------------------- 1 | module Capybara 2 | module Discoball 3 | VERSION = "0.1.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/black_box/rails_app_spec.rb: -------------------------------------------------------------------------------- 1 | require "jet_black" 2 | 3 | RSpec.describe "Using Discoball in a Rails app" do 4 | let(:session) do 5 | JetBlack::Session.new(options: { clean_bundler_env: true }) 6 | end 7 | 8 | it "works with a block" do 9 | create_rails_application 10 | setup_discoball 11 | setup_rspec_rails 12 | install_success_api 13 | 14 | setup_controller_action(<<~RUBY) 15 | render plain: SuccessAPI.get 16 | RUBY 17 | 18 | setup_integration_spec(<<~RUBY) 19 | visit "/successes" 20 | expect(page).to have_content("success") 21 | RUBY 22 | 23 | setup_spec_supporter(<<-RUBY) 24 | require "sinatra/base" 25 | require "capybara_discoball" 26 | require "success_api" 27 | 28 | class FakeSuccess < Sinatra::Base 29 | get("/") { "success" } 30 | end 31 | 32 | Capybara::Discoball.spin(FakeSuccess) do |server| 33 | SuccessAPI.endpoint_url = server.url 34 | end 35 | RUBY 36 | 37 | expect(run_integration_test). 38 | to be_a_success.and have_stdout(/1 example, 0 failures/) 39 | end 40 | 41 | it "works without a block" do 42 | create_rails_application 43 | setup_discoball 44 | setup_rspec_rails 45 | install_success_api 46 | 47 | setup_controller_action(<<~RUBY) 48 | render :plain => SuccessAPI.get 49 | RUBY 50 | 51 | setup_integration_spec(<<~RUBY) 52 | visit "/successes" 53 | expect(page).to have_content("success") 54 | RUBY 55 | 56 | setup_spec_supporter(<<-RUBY) 57 | require "sinatra/base" 58 | require "capybara_discoball" 59 | require "success_api" 60 | 61 | class FakeSuccess < Sinatra::Base 62 | get("/") { "success" } 63 | end 64 | 65 | SuccessAPI.endpoint_url = Capybara::Discoball.spin(FakeSuccess) 66 | RUBY 67 | 68 | expect(run_integration_test). 69 | to be_a_success.and have_stdout("1 example, 0 failures") 70 | end 71 | 72 | private 73 | 74 | def create_rails_application 75 | session.create_file("Gemfile", <<~RUBY) 76 | source "http://rubygems.org" 77 | 78 | gem "rails" 79 | RUBY 80 | 81 | session.run("bundle install") 82 | 83 | rails_new_cmd = [ 84 | "bundle exec rails new .", 85 | "--skip-bundle", 86 | "--skip-test", 87 | "--skip-coffee", 88 | "--skip-turbolinks", 89 | "--skip-spring", 90 | "--skip-bootsnap", 91 | "--force", 92 | ].join(" ") 93 | 94 | expect(session.run(rails_new_cmd)). 95 | to be_a_success.and have_stdout("force Gemfile") 96 | end 97 | 98 | def setup_discoball 99 | discoball_path = File.expand_path("../../", __dir__) 100 | 101 | session.append_to_file "Gemfile", <<~RUBY 102 | gem "capybara_discoball", path: "#{discoball_path}" 103 | gem "sinatra" 104 | RUBY 105 | 106 | expect(session.run("bundle install")). 107 | to be_a_success.and have_stdout(/capybara_discoball .* from source at/) 108 | end 109 | 110 | def setup_rspec_rails 111 | session.append_to_file("Gemfile", <<~RUBY) 112 | gem "rspec-rails" 113 | RUBY 114 | 115 | session.run("bundle install") 116 | 117 | expect(session.run("bundle exec rails g rspec:install")). 118 | to be_a_success.and have_stdout("create spec/rails_helper.rb") 119 | end 120 | 121 | def install_success_api 122 | session.create_file("app/models/success_api.rb", <<~RUBY) 123 | require "net/http" 124 | require "uri" 125 | 126 | class SuccessAPI 127 | @@endpoint_url = "http://yahoo.com/" 128 | 129 | def self.endpoint_url=(endpoint_url) 130 | @@endpoint_url = endpoint_url 131 | end 132 | 133 | def self.get 134 | Net::HTTP.get(uri) 135 | end 136 | 137 | private 138 | 139 | def self.uri 140 | URI.parse(@@endpoint_url) 141 | end 142 | end 143 | RUBY 144 | end 145 | 146 | def setup_controller_action(content) 147 | session.create_file("app/controllers/whatever_controller.rb", <<~RUBY) 148 | class WhateverController < ApplicationController 149 | def the_action 150 | #{content} 151 | end 152 | end 153 | RUBY 154 | 155 | session.create_file("config/routes.rb", <<~RUBY) 156 | Rails.application.routes.draw do 157 | get "/successes", to: "whatever#the_action" 158 | end 159 | RUBY 160 | end 161 | 162 | def setup_integration_spec(spec_content) 163 | session.create_file("spec/integration/whatever_spec.rb", <<~RUBY) 164 | require "rails_helper" 165 | require "capybara/rspec" 166 | require "support/whatever" 167 | 168 | RSpec.describe "whatever", type: :feature do 169 | it "does the thing" do 170 | #{spec_content} 171 | end 172 | end 173 | RUBY 174 | end 175 | 176 | def setup_spec_supporter(support_content) 177 | session.create_file("spec/support/whatever.rb", support_content) 178 | end 179 | 180 | def run_integration_test 181 | session.run("bundle exec rspec spec/integration") 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /spec/lib/capybara_discoball/runner_spec.rb: -------------------------------------------------------------------------------- 1 | require "capybara_discoball" 2 | require "sinatra/base" 3 | 4 | RSpec.describe Capybara::Discoball::Runner do 5 | describe "when Capybara fails to find an unused port" do 6 | it "retries up to 3 times" do 7 | expected_url = "http://localhost:9999" 8 | 9 | allow(Capybara::Discoball::Server).to receive(:new).and_return( 10 | unbootable_server, 11 | unbootable_server, 12 | unbootable_server, 13 | bootable_server(url: expected_url), 14 | ) 15 | 16 | runner = described_class.new(Sinatra::Base) 17 | 18 | expect(runner.boot).to eq expected_url 19 | end 20 | end 21 | 22 | private 23 | 24 | def bootable_server(url:) 25 | instance_double(Capybara::Discoball::Server, boot: nil, url: url) 26 | end 27 | 28 | def unbootable_server 29 | server = instance_double(Capybara::Discoball::Server) 30 | allow(server).to receive(:boot).and_raise(Errno::EADDRINUSE) 31 | server 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/lib/capybara_discoball_spec.rb: -------------------------------------------------------------------------------- 1 | require "capybara_discoball" 2 | require "net/http" 3 | require "sinatra/base" 4 | 5 | RSpec.describe Capybara::Discoball do 6 | it "spins up a server" do 7 | example_discoball_app = Class.new(Sinatra::Base) do 8 | get("/") { "success" } 9 | end 10 | 11 | server_url = described_class.spin(example_discoball_app) 12 | response = Net::HTTP.get(URI(server_url)) 13 | 14 | expect(response).to eq "success" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "jet_black/rspec" 2 | 3 | RSpec.configure do |config| 4 | config.expect_with :rspec do |expectations| 5 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 6 | end 7 | 8 | config.mock_with :rspec do |mocks| 9 | mocks.verify_partial_doubles = true 10 | end 11 | 12 | config.shared_context_metadata_behavior = :apply_to_host_groups 13 | config.disable_monkey_patching! 14 | config.warnings = true 15 | 16 | config.order = :random 17 | Kernel.srand config.seed 18 | end 19 | --------------------------------------------------------------------------------