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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 348eca90f6ed848d47e98f5e155fdc23d71574f0
4
- data.tar.gz: b962f7ff42d548d8d82523c61defc1485166730e
3
+ metadata.gz: d0f834a28065f71917777d4683074b39c94a23da
4
+ data.tar.gz: 6d9c2443ce1c46e603da9d8e7a34bbaea979a06a
5
5
  SHA512:
6
- metadata.gz: 07f41e55079029af4357841f9f11c6fdb410385f6bc09a7544cbeb78470bf0acf0b8f6afc793abe6054c7d7385c319fe8dbc0f3d6273d0b23e6a2199138e0559
7
- data.tar.gz: 1c3df44812ce0c82ba8d8dd9bdfdb2fa86583132610dd4d9ce4897d54e2790a82b71e47bf81e865eff1a8254a1ba3e4d25a824e1cdcd6b7db71e9cefa78392b0
6
+ metadata.gz: abfdd59d18cb4747bb284c5edd0342fc59ccdb623ab61d6bc0920e1259adfa5ba86bf7324848836170f3f27bfc729dfe2648daa2b82d054cb17483aeedf96ee7
7
+ data.tar.gz: 5d8f53f05f3ff7f186089c3260c29445bfc012cace2195cc3566092c85ba2e34915d9d58f1c826983847fc41769870aca7c216a4cf4a1c69150313adee77bd6f
@@ -1,4 +1,8 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 1.9.3
3
4
  - 2.3.0
5
+ - 2.0
6
+ - 2.1
7
+ - 2.2
4
8
  before_install: gem install bundler -v 1.11.2
data/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # WaitForIt
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/wait_for_it`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
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
- TODO: Write usage instructions here
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/[USERNAME]/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.
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
 
@@ -1,5 +1,176 @@
1
1
  require "wait_for_it/version"
2
2
 
3
- module WaitForIt
4
- # Your code goes here...
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
@@ -1,3 +1,3 @@
1
- module WaitForIt
2
- VERSION = "0.1.0"
1
+ class WaitForIt
2
+ VERSION = "0.1.1"
3
3
  end
@@ -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{: Write a short summary, because Rubygems requires one.}
13
- spec.description = %q{: Write a longer description or delete this line.}
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.0
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-02-02 00:00:00.000000000 Z
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: ": Write a longer description or delete this line."
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: ": Write a short summary, because Rubygems requires one."
99
+ summary: Stop sleeping in your tests, instead wait for it...
99
100
  test_files: []