wait-for-it-0.2.1/0000755000175000017500000000000013716537077012720 5ustar pravipraviwait-for-it-0.2.1/lib/0000755000175000017500000000000013716537077013466 5ustar pravipraviwait-for-it-0.2.1/lib/wait_for_it/0000755000175000017500000000000013716537077015774 5ustar pravipraviwait-for-it-0.2.1/lib/wait_for_it/version.rb0000644000175000017500000000005013716537077020001 0ustar pravipraviclass WaitForIt VERSION = "0.2.1" end wait-for-it-0.2.1/lib/wait_for_it.rb0000644000175000017500000001145413716537077016326 0ustar pravipravirequire "wait_for_it/version" require 'pathname' require 'shellwords' require 'tempfile' require 'timeout' class WaitForIt class WaitForItTimeoutError < StandardError def initialize(options = {}) command = options[:command] input = options[:input] timeout = options[:timeout] log = options[:log] super "Running command: '#{ command }', waiting for '#{ input }' did not occur within #{ timeout } seconds:\n#{ log.read }" end end DEFAULT_TIMEOUT = 10 # seconds DEFAULT_OUT = ">>" DEFAULT_ENV = {} @wait_for = nil @timeout = nil @redirection = nil @env = nil # Configure global WaitForIt settings def self.config yield self self end # The default output is expected in the logs before the process is considered "booted" def self.wait_for=(wait_for) @wait_for = wait_for end def self.wait_for @wait_for end # The default timeout that is waited for a process to boot def self.timeout=(timeout) @timeout = timeout end def self.timeout @timeout || DEFAULT_TIMEOUT end # The default shell redirect to the logs def self.redirection=(redirection) @redirection = redirection end def self.redirection @redirection || DEFAULT_OUT end # Default environment variables under which commands should be executed. def self.env=(env) @env = env end def self.env @env || DEFAULT_ENV end # Creates a new WaitForIt instance # # @param [String] command Command to spawn # @param [Hash] options # @options options [Fixnum] :timeout The duration to wait a commmand to boot, default is 10 seconds # @options options [String] :wait_for The output the process emits when it has successfully booted. # When present the calling process will block until the message is received in the log output # or until the timeout is hit. # @options options [String] :redirection The shell redirection used to pipe to log file # @options options [Hash] :env Keys and values for environment variables in the process def initialize(command, options = {}) @command = command @timeout = options[:timeout] || WaitForIt.timeout @wait_for = options[:wait_for] || WaitForIt.wait_for redirection = options[:redirection] || WaitForIt.redirection env = options[:env] || WaitForIt.env @log = set_log @pid = nil raise "Must provide a wait_for: option" unless @wait_for spawn(command, redirection, env) wait!(@wait_for) if block_given? begin yield self ensure cleanup end end rescue WaitForItTimeoutError => e cleanup raise e end attr_reader :timeout, :log # Checks the logs of the process to see if they contain a match. # Can use a string or a regular expression. def contains?(input) log.read.match convert_to_regex(input) end # Returns a count of the number of times logs match the input. # Can use a string or a regular expression. def count(input) log.read.scan(convert_to_regex(input)).count end # Blocks parent process until given message appears at the def wait(input, t = timeout) regex = convert_to_regex(input) Timeout::timeout(t) do until log.read.match regex sleep 0.01 end end sleep 0.01 self rescue Timeout::Error puts "Timeout waiting for #{input.inspect} to find a match using #{ regex } in \n'#{ log.read }'" false end # Same as `wait` but raises an error if timeout is reached def wait!(input, t = timeout) unless wait(input, t) options = {} options[:command] = @command options[:input] = input options[:timeout] = t options[:log] = @log raise WaitForItTimeoutError.new(options) end end # Kills the process and removes temporary files def cleanup shutdown close_log unlink_log end private def close_log @tmp_file.close if @tmp_file end def unlink_log @log.unlink if @log rescue Errno::ENOENT # File already unlinked end def set_log @tmp_file = Tempfile.new(["wait_for_it", ".log"]) log_file = Pathname.new(@tmp_file) log_file.mkpath unless log_file.exist? log_file end def spawn(command, redirection, env_hash = {}) env = {} env_hash.each {|k, v| env[k.to_s] = v.nil? ? v : v.to_s } # Must exec so when we kill the PID it kills the child process @pid = Process.spawn(env, "exec #{ command } #{ redirection } #{ log }") end def convert_to_regex(input) return input if input.is_a?(Regexp) Regexp.new(Regexp.escape(input)) end # Kills the process and waits for it to exit def shutdown if @pid Process.kill('TERM', @pid) Process.wait(@pid) @pid = nil end rescue Errno::ESRCH # Process doesn't exist, nothing to kill end end wait-for-it-0.2.1/.travis.yml0000644000175000017500000000014213716537077015026 0ustar pravipravilanguage: ruby rvm: - 2.2 - 2.5 - 2.6 - 2.7 before_install: gem install bundler -v 1.11.2 wait-for-it-0.2.1/CODE_OF_CONDUCT.md0000644000175000017500000000453313716537077015524 0ustar pravipravi# Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at richard.schneeman@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [http://contributor-covenant.org/version/1/3/0/][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/3/0/wait-for-it-0.2.1/Gemfile0000644000175000017500000000014013716537077014206 0ustar pravipravisource 'https://rubygems.org' # Specify your gem's dependencies in wait_for_it.gemspec gemspec wait-for-it-0.2.1/.github/0000755000175000017500000000000013716537077014260 5ustar pravipraviwait-for-it-0.2.1/.github/workflows/0000755000175000017500000000000013716537077016315 5ustar pravipraviwait-for-it-0.2.1/.github/workflows/check_changelog.yml0000644000175000017500000000057413716537077022132 0ustar pravipraviname: Check Changelog on: [pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Check that CHANGELOG is touched run: | cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md wait-for-it-0.2.1/README.md0000644000175000017500000001542613716537077014207 0ustar pravipravi# WaitForIt [![Build Status](https://travis-ci.org/zombocom/wait_for_it.svg?branch=master)](https://travis-ci.org/zombocom/wait_for_it) Spawns processes and waits for them so you can integration test really complicated things with determinism. For inspiration behind why you should use something like this check out my talk [Testing the Untestable](https://www.youtube.com/watch?v=QHMKIHkY1nM). You can test long running processes such as webservers, or features that require concurrency or libraries that use global configuration. Don't add `sleep` to your tests, instead... ![](https://media.giphy.com/media/RL9YUXgD6a3du/giphy.gif) ## Installation Add this line to your application's Gemfile: ```ruby gem 'wait_for_it' ``` And then execute: $ bundle Or install it yourself as: $ gem install wait_for_it ## Usage > For actual usage examples check out the [specs](https://github.com/zombocom/wait_for_it/blob/master/spec/wait_for_it_spec.rb). This library spawns processes (sorry, doesn't work on windows) and instead of sleeping a predetermined time to wait for that process to do something it reads in a log file until certain outputs are received. For example if you wanted to test booting up a puma webserver, manually when you start it you might get this output ```sh $ bundle exec puma [5322] Puma starting in cluster mode... [5322] * Version 2.15.3 (ruby 2.3.0-p0), codename: Autumn Arbor Airbrush [5322] * Min threads: 5, max threads: 5 [5322] * Environment: development [5322] * Process workers: 2 [5322] * Preloading application [5322] * Listening on tcp://0.0.0.0:3000 [5322] Use Ctrl-C to stop [5322] - Worker 0 (pid: 5323) booted, phase: 0 [5322] - Worker 1 (pid: 5324) booted, phase: 0 ``` So you can see that when `booted` makes its way to the stdout we know it has fully launched and now we can start to use this running process. To do the same thing using this library we could ```ruby require 'wait_for_it' WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn| # ... end ``` > NOTE: If you don't use the block syntax you must call `cleanup` on the object, otherwise you may have stray files or process around after you code exits. I recommend calling it in an `ensure` block of code. Your main code will wait until it receives an output of "booted" from the `bundle exec puma` command. Now the process is running, you could programatically send it a request via `$ curl http://localhost:3000/repos/new` and verify the output using helper methods. Let's say you expect this to trigger a `302` response, the log would look like ```sh [5324] 127.0.0.1 - - [02/Feb/2016:12:35:15 -0600] "GET /repos/new HTTP/1.1" 302 - 0.0183 ``` You can now assert that is found in your puma output ```ruby WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn| `curl http://localhost:3000/repos/new` assert_equal 1, spawn.count("302") end # ... spawn.cleanup ``` If you have a background thread that sporatically emits information to the logs like [Puma Worker Killer](https://github.com/schneems/puma_worker_killer), if you configure it to do a rolling restart, you could either wait for that to happen. ```ruby WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn if spawn.wait("PumaWorkerKiller: Rolling Restart") # ... end end ``` The `wait` command will return a false if it reaches a timeout before finding the output, If you prefer you can raise an exception by using `wait!` method. You can also assert if the output contains a phrase a string or regex: ```ruby WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn| spawn.contains?("PumaWorkerKiller: Rolling Restart") end ``` You can directly read from the log if you want ```ruby WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn| spawn.log.read end ``` The `log` method returns a `Pathname` object. ## Config You can send environment variables to your process using the `env` key ```ruby WaitForIt.new("bundle exec puma", wait_for: "booted", env: { RACK_ENV: "production "}) do end ``` By default redirection is performed using `" >> "` you can change the [IO redirection](http://www.tldp.org/LDP/abs/html/io-redirection.html) by setting the `redirection` key. For example if you wanted to capture STDERR in addition to stdout: ```ruby spawn = WaitForIt.new("bundle exec puma", wait_for: "booted", redirection: "2>>") do end ``` If you're using Bash 4 you can get STDERR and STDOUT using `"&>>"` [Stack Overflow](http://stackoverflow.com/questions/876239/how-can-i-redirect-and-append-both-stdout-and-stderr-to-a-file-with-bash). You can change the default timeout using the `timeout` key (default is 10 seconds). ```ruby spawn = WaitForIt.new("bundle exec puma", wait_for: "booted", timeout: 60) do end ``` If you need an individual `wait` have a different timeout you can pass in a timeout value ```ruby WaitForIt.new("bundle exec puma", wait_for: "booted", timeout: 60) do |spawn| spawn.wait("GET /repos/new", 2) # timeout after 2 seconds end ``` ## Global config If you're doing a lot of "waiting for it" you can supply default arguments globally ``` WaitForIt.config do |config| config.timeout = 60 config.redirection = "2>>" config.env = { RACK_ENV: "production"} end ``` ## Concurrency Issues You should be aware of cases where your tests might be run concurrently. For example if you're testing something that uses a lock in postgres, when you run your tests on a CI server it may spin up multiple tests at the same time that all try to grab the same lock. Most CI servers provide unique build IDs that you could use in this case to generate unique keys. Another thing to watch out for is files, if you're tesing a process that writes a `pidfile` you probably want to do something like make a temporary directory and copy files into that directory so that multiple tests could run at the same time and not try to write to the same file. ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/zombocom/wait_for_it. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). wait-for-it-0.2.1/CHANGELOG.md0000644000175000017500000000022713716537077014532 0ustar pravipravi# Changelog ## 0.2.1 - Fix variable not initialized warnings ## 0.2.0 - Allow unsetting env vars (https://github.com/schneems/wait_for_it/pull/3) wait-for-it-0.2.1/.gitignore0000644000175000017500000000012713716537077014710 0ustar pravipravi/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ wait-for-it-0.2.1/bin/0000755000175000017500000000000013716537077013470 5ustar pravipraviwait-for-it-0.2.1/bin/setup0000755000175000017500000000020313716537077014551 0ustar pravipravi#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here wait-for-it-0.2.1/bin/console0000755000175000017500000000052013716537077015055 0ustar pravipravi#!/usr/bin/env ruby require "bundler/setup" require "wait_for_it" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start wait-for-it-0.2.1/wait_for_it.gemspec0000644000175000017500000000172513716537077016600 0ustar pravipravi# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'wait_for_it/version' Gem::Specification.new do |spec| spec.name = "wait_for_it" spec.version = WaitForIt::VERSION spec.authors = ["schneems"] spec.email = ["richard.schneeman@gmail.com"] spec.summary = %q{ Stop sleeping in your tests, instead wait for it... } spec.description = %q{ Make your complicated integration tests more deterministic with wait for it} spec.homepage = "https://github.com/zombocom/wait_for_it" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec", "~> 3.0" end wait-for-it-0.2.1/LICENSE.txt0000644000175000017500000000206313716537077014544 0ustar pravipraviThe MIT License (MIT) Copyright (c) 2016 schneems Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wait-for-it-0.2.1/Rakefile0000644000175000017500000000016513716537077014367 0ustar pravipravirequire "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task :default => :spec wait-for-it-0.2.1/.rspec0000644000175000017500000000003713716537077014035 0ustar pravipravi--format documentation --color