wait_for_it 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +4 -0
- data/README.md +135 -5
- data/lib/wait_for_it.rb +173 -2
- data/lib/wait_for_it/version.rb +2 -2
- data/wait_for_it.gemspec +3 -3
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0f834a28065f71917777d4683074b39c94a23da
|
4
|
+
data.tar.gz: 6d9c2443ce1c46e603da9d8e7a34bbaea979a06a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: abfdd59d18cb4747bb284c5edd0342fc59ccdb623ab61d6bc0920e1259adfa5ba86bf7324848836170f3f27bfc729dfe2648daa2b82d054cb17483aeedf96ee7
|
7
|
+
data.tar.gz: 5d8f53f05f3ff7f186089c3260c29445bfc012cace2195cc3566092c85ba2e34915d9d58f1c826983847fc41769870aca7c216a4cf4a1c69150313adee77bd6f
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
# WaitForIt
|
2
2
|
|
3
|
-
|
3
|
+
[![Build Status](https://travis-ci.org/schneems/wait_for_it.svg?branch=master)](https://travis-ci.org/schneems/wait_for_it)
|
4
4
|
|
5
|
-
|
5
|
+
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.
|
6
|
+
|
7
|
+
Don't add `sleep` to your tests, instead...
|
8
|
+
|
9
|
+
![](https://media.giphy.com/media/RL9YUXgD6a3du/giphy.gif)
|
6
10
|
|
7
11
|
## Installation
|
8
12
|
|
@@ -22,7 +26,134 @@ Or install it yourself as:
|
|
22
26
|
|
23
27
|
## Usage
|
24
28
|
|
25
|
-
|
29
|
+
> For actual usage examples check out the [specs](https://github.com/schneems/wait_for_it/blob/master/spec/wait_for_it_spec.rb).
|
30
|
+
|
31
|
+
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
|
32
|
+
|
33
|
+
```sh
|
34
|
+
$ bundle exec puma
|
35
|
+
[5322] Puma starting in cluster mode...
|
36
|
+
[5322] * Version 2.15.3 (ruby 2.3.0-p0), codename: Autumn Arbor Airbrush
|
37
|
+
[5322] * Min threads: 5, max threads: 5
|
38
|
+
[5322] * Environment: development
|
39
|
+
[5322] * Process workers: 2
|
40
|
+
[5322] * Preloading application
|
41
|
+
[5322] * Listening on tcp://0.0.0.0:3000
|
42
|
+
[5322] Use Ctrl-C to stop
|
43
|
+
[5322] - Worker 0 (pid: 5323) booted, phase: 0
|
44
|
+
[5322] - Worker 1 (pid: 5324) booted, phase: 0
|
45
|
+
```
|
46
|
+
|
47
|
+
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
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
require 'wait_for_it'
|
51
|
+
|
52
|
+
WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn|
|
53
|
+
# ...
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
> 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.
|
58
|
+
|
59
|
+
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
|
60
|
+
|
61
|
+
```sh
|
62
|
+
[5324] 127.0.0.1 - - [02/Feb/2016:12:35:15 -0600] "GET /repos/new HTTP/1.1" 302 - 0.0183
|
63
|
+
```
|
64
|
+
|
65
|
+
You can now assert that is found in your puma output
|
66
|
+
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn|
|
70
|
+
`curl http://localhost:3000/repos/new`
|
71
|
+
assert_equal 1, spawn.count("302")
|
72
|
+
end
|
73
|
+
# ...
|
74
|
+
spawn.cleanup
|
75
|
+
```
|
76
|
+
|
77
|
+
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.
|
78
|
+
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn
|
82
|
+
if spawn.wait("PumaWorkerKiller: Rolling Restart")
|
83
|
+
# ...
|
84
|
+
end
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
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.
|
89
|
+
|
90
|
+
You can also assert if the output contains a phrase a string or regex:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn|
|
94
|
+
spawn.contains?("PumaWorkerKiller: Rolling Restart")
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
You can directly read from the log if you want
|
99
|
+
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
WaitForIt.new("bundle exec puma", wait_for: "booted") do |spawn|
|
103
|
+
spawn.log.read
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
The `log` method returns a `Pathname` object.
|
108
|
+
|
109
|
+
## Config
|
110
|
+
|
111
|
+
You can send environment variables to your process using the `env` key
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
WaitForIt.new("bundle exec puma", wait_for: "booted", env: { RACK_ENV: "production "}) do
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
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:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
spawn = WaitForIt.new("bundle exec puma", wait_for: "booted", redirection: "2>>") do
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
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).
|
126
|
+
|
127
|
+
You can change the default timeout using the `timeout` key (default is 10 seconds).
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
spawn = WaitForIt.new("bundle exec puma", wait_for: "booted", timeout: 60) do
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
If you need an individual `wait` have a different timeout you can pass in a timeout value
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
WaitForIt.new("bundle exec puma", wait_for: "booted", timeout: 60) do |spawn|
|
138
|
+
spawn.wait("GET /repos/new", 2) # timeout after 2 seconds
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
142
|
+
## Global config
|
143
|
+
|
144
|
+
If you're doing a lot of "waiting for it" you can supply default arguments globally
|
145
|
+
|
146
|
+
```
|
147
|
+
WaitForIt.config do |config|
|
148
|
+
config.timeout = 60
|
149
|
+
config.redirection = "2>>"
|
150
|
+
config.env = { RACK_ENV: "production"}
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
## Concurrency Issues
|
155
|
+
|
156
|
+
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.
|
26
157
|
|
27
158
|
## Development
|
28
159
|
|
@@ -32,8 +163,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
163
|
|
33
164
|
## Contributing
|
34
165
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
36
|
-
|
166
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/schneems/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.
|
37
167
|
|
38
168
|
## License
|
39
169
|
|
data/lib/wait_for_it.rb
CHANGED
@@ -1,5 +1,176 @@
|
|
1
1
|
require "wait_for_it/version"
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'pathname'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'timeout'
|
7
|
+
|
8
|
+
class WaitForIt
|
9
|
+
class WaitForItTimeoutError < StandardError
|
10
|
+
def initialize(options = {})
|
11
|
+
command = options[:command]
|
12
|
+
input = options[:input]
|
13
|
+
timeout = options[:timeout]
|
14
|
+
log = options[:log]
|
15
|
+
super "Running command: '#{ command }', waiting for '#{ input }' did not occur within #{ timeout } seconds:\n#{ log.read }"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
DEFAULT_TIMEOUT = 10 # seconds
|
20
|
+
DEFAULT_OUT = ">>"
|
21
|
+
DEFAULT_ENV = {}
|
22
|
+
|
23
|
+
# Configure global WaitForIt settings
|
24
|
+
def self.config
|
25
|
+
yield self
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# The default output is expected in the logs before the process is considered "booted"
|
30
|
+
def self.wait_for=(wait_for)
|
31
|
+
@wait_for = wait_for
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.wait_for
|
35
|
+
@wait_for
|
36
|
+
end
|
37
|
+
|
38
|
+
# The default timeout that is waited for a process to boot
|
39
|
+
def self.timeout=(timeout)
|
40
|
+
@timeout = timeout
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.timeout
|
44
|
+
@timeout || DEFAULT_TIMEOUT
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# The default shell redirect to the logs
|
49
|
+
def self.redirection=(redirection)
|
50
|
+
@redirection = redirection
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.redirection
|
54
|
+
@redirection || DEFAULT_OUT
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# Default environment variables under which commands should be executed.
|
59
|
+
def self.env=(env)
|
60
|
+
@env = env
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.env
|
64
|
+
@env || DEFAULT_ENV
|
65
|
+
end
|
66
|
+
|
67
|
+
# Creates a new WaitForIt instance
|
68
|
+
#
|
69
|
+
# @param [String] command Command to spawn
|
70
|
+
# @param [Hash] options
|
71
|
+
# @options options [Fixnum] :timeout The duration to wait a commmand to boot, default is 10 seconds
|
72
|
+
# @options options [String] :wait_for The output the process emits when it has successfully booted.
|
73
|
+
# When present the calling process will block until the message is received in the log output
|
74
|
+
# or until the timeout is hit.
|
75
|
+
# @options options [String] :redirection The shell redirection used to pipe to log file
|
76
|
+
# @options options [Hash] :env Keys and values for environment variables in the process
|
77
|
+
def initialize(command, options = {})
|
78
|
+
@command = command
|
79
|
+
@timeout = options[:timeout] || WaitForIt.timeout
|
80
|
+
@wait_for = options[:wait_for] || WaitForIt.wait_for
|
81
|
+
redirection = options[:redirection] || WaitForIt.redirection
|
82
|
+
env = options[:env] || WaitForIt.env
|
83
|
+
@log = set_log
|
84
|
+
@pid = nil
|
85
|
+
|
86
|
+
raise "Must provide a wait_for: option" unless @wait_for
|
87
|
+
spawn(command, redirection, env)
|
88
|
+
wait!(@wait_for)
|
89
|
+
|
90
|
+
if block_given?
|
91
|
+
begin
|
92
|
+
yield self
|
93
|
+
ensure
|
94
|
+
cleanup
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
attr_reader :timeout, :log
|
100
|
+
|
101
|
+
# Checks the logs of the process to see if they contain a match.
|
102
|
+
# Can use a string or a regular expression.
|
103
|
+
def contains?(input)
|
104
|
+
log.read.match convert_to_regex(input)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns a count of the number of times logs match the input.
|
108
|
+
# Can use a string or a regular expression.
|
109
|
+
def count(input)
|
110
|
+
log.read.scan(convert_to_regex(input)).count
|
111
|
+
end
|
112
|
+
|
113
|
+
# Blocks parent process until given message appears at the
|
114
|
+
def wait(input, t = timeout)
|
115
|
+
regex = convert_to_regex(input)
|
116
|
+
Timeout::timeout(t) do
|
117
|
+
until log.read.match regex
|
118
|
+
sleep 0.01
|
119
|
+
end
|
120
|
+
end
|
121
|
+
sleep 0.01
|
122
|
+
self
|
123
|
+
rescue Timeout::Error
|
124
|
+
puts "Timeout waiting for #{input.inspect} to find a match using #{ regex } in \n'#{ log.read }'"
|
125
|
+
false
|
126
|
+
end
|
127
|
+
|
128
|
+
# Same as `wait` but raises an error if timeout is reached
|
129
|
+
def wait!(input, t = timeout)
|
130
|
+
unless wait(input)
|
131
|
+
options = {}
|
132
|
+
options[:command] = @command
|
133
|
+
options[:input] = input
|
134
|
+
options[:timeout] = t
|
135
|
+
options[:log] = @log
|
136
|
+
raise WaitForItTimeoutError.new(options)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Kills the process and removes temporary files
|
141
|
+
def cleanup
|
142
|
+
shutdown
|
143
|
+
@tmp_file.close
|
144
|
+
@log.unlink
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
def set_log
|
149
|
+
@tmp_file = Tempfile.new(["wait_for_it", ".log"])
|
150
|
+
log_file = Pathname.new(@tmp_file)
|
151
|
+
log_file.mkpath unless log_file.exist?
|
152
|
+
log_file
|
153
|
+
end
|
154
|
+
|
155
|
+
def spawn(command, redirection, env_hash = {})
|
156
|
+
env = env_hash.map {|key, value| "#{ key.to_s.shellescape }=#{ value.to_s.shellescape }" }.join(" ")
|
157
|
+
command = "/usr/bin/env #{ env } bash -c #{ command.shellescape } #{ redirection } #{ log }"
|
158
|
+
@pid = Process.spawn("#{ command }")
|
159
|
+
end
|
160
|
+
|
161
|
+
def convert_to_regex(input)
|
162
|
+
return input if input.is_a?(Regexp)
|
163
|
+
Regexp.new(Regexp.escape(input))
|
164
|
+
end
|
165
|
+
|
166
|
+
# Kills the process and waits for it to exit
|
167
|
+
def shutdown
|
168
|
+
if @pid
|
169
|
+
Process.kill('TERM', @pid)
|
170
|
+
Process.wait(@pid)
|
171
|
+
@pid = nil
|
172
|
+
end
|
173
|
+
rescue Errno::ESRCH
|
174
|
+
# Process doesn't exist, nothing to kill
|
175
|
+
end
|
5
176
|
end
|
data/lib/wait_for_it/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = "0.1.
|
1
|
+
class WaitForIt
|
2
|
+
VERSION = "0.1.1"
|
3
3
|
end
|
data/wait_for_it.gemspec
CHANGED
@@ -9,9 +9,9 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["schneems"]
|
10
10
|
spec.email = ["richard.schneeman@gmail.com"]
|
11
11
|
|
12
|
-
spec.summary = %q{
|
13
|
-
spec.description = %q{
|
14
|
-
spec.homepage = ""
|
12
|
+
spec.summary = %q{ Stop sleeping in your tests, instead wait for it... }
|
13
|
+
spec.description = %q{ Make your complicated integration tests more deterministic with wait for it}
|
14
|
+
spec.homepage = "https://github.com/schneems/wait_for_it"
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
17
17
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wait_for_it
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- schneems
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-06-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,7 +52,8 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.0'
|
55
|
-
description: "
|
55
|
+
description: " Make your complicated integration tests more deterministic with wait
|
56
|
+
for it"
|
56
57
|
email:
|
57
58
|
- richard.schneeman@gmail.com
|
58
59
|
executables: []
|
@@ -72,7 +73,7 @@ files:
|
|
72
73
|
- lib/wait_for_it.rb
|
73
74
|
- lib/wait_for_it/version.rb
|
74
75
|
- wait_for_it.gemspec
|
75
|
-
homepage:
|
76
|
+
homepage: https://github.com/schneems/wait_for_it
|
76
77
|
licenses:
|
77
78
|
- MIT
|
78
79
|
metadata: {}
|
@@ -95,5 +96,5 @@ rubyforge_project:
|
|
95
96
|
rubygems_version: 2.5.1
|
96
97
|
signing_key:
|
97
98
|
specification_version: 4
|
98
|
-
summary:
|
99
|
+
summary: Stop sleeping in your tests, instead wait for it...
|
99
100
|
test_files: []
|