rspec-background-process 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ require 'open-uri'
2
+ require 'file-tail'
3
+ require 'retries'
4
+
5
+ require_relative 'process_pool'
6
+
7
+ module RSpecBackgroundProcess
8
+ class ProcessPool
9
+ class ProcessDefinition
10
+ def ready_when_log_includes(log_line)
11
+ ready_test do |instance|
12
+ log_line = instance.render(log_line)
13
+
14
+ # NOTE: log file my not be crated just after process is started (spawned) so we need to retry
15
+ with_retries(
16
+ max_tries: 10000,
17
+ base_sleep_seconds: 0.01,
18
+ max_sleep_seconds: 0.2,
19
+ rescue: Errno::ENOENT
20
+ ) do
21
+ File::Tail::Logfile.tail(instance.log_file, forward: 0, interval: 0.01, max_interval: 1, suspicious_interval: 4) do |line|
22
+ line.include?(log_line) and break true
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def ready_when_url_response_status(uri, status = 'OK')
29
+ ready_test do |instance|
30
+ _uri = instance.render(uri) # NOTE: new variable (_uri) is needed or strange things happen...
31
+
32
+ begin
33
+ with_retries(
34
+ max_tries: 10000,
35
+ base_sleep_seconds: 0.06,
36
+ max_sleep_seconds: 0.2,
37
+ rescue: Errno::ECONNREFUSED
38
+ ) do
39
+ open(_uri).status.last.strip == status and break true
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+
@@ -0,0 +1,15 @@
1
+ require_relative 'process_pool'
2
+
3
+ module RSpecBackgroundProcess
4
+ class ProcessPool
5
+ class ProcessDefinition
6
+ def refresh_command(command)
7
+ refresh_action do |instance|
8
+ _command = instance.render(command)
9
+ system _command
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,49 @@
1
+ module RSpecBackgroundProcess
2
+ class ProcessPool
3
+ class ProcessDefinition
4
+ def http_port_allocated_form(base_port, port_count = 1)
5
+ extend RSpecBackgroundProcess::BackgroundProcess::Server, base_port: base_port, port_count: port_count
6
+ end
7
+ end
8
+ end
9
+
10
+ class BackgroundProcess
11
+ module Server
12
+ def self.extended(mod)
13
+ mod.allocate_ports
14
+ end
15
+
16
+ def template_variables
17
+ super.merge(
18
+ /allocated port (\d)/ => ->(port_no) { allocated_port(port_no) }
19
+ )
20
+ end
21
+
22
+ def allocate_ports
23
+ base_port = @options[:base_port] or fail "no base_port option set for #{self}: #{@options}"
24
+ port_count = @options[:port_count] or fail "no port_count option set for #{self}: #{@options}"
25
+
26
+ global_ports = @options[:global_context][:ports] ||= Set[]
27
+
28
+ begin
29
+ @ports = (base_port ... base_port + port_count).to_a
30
+ base_port += port_count
31
+ end until (global_ports & @ports).empty?
32
+
33
+ @options[:global_context][:ports] = global_ports + @ports
34
+ end
35
+
36
+ def ports
37
+ @ports
38
+ end
39
+
40
+ def allocated_port(port_no)
41
+ @ports[port_no.to_i - 1] or fail "no port #{port_no} allocated: #{@ports}"
42
+ end
43
+
44
+ def to_s
45
+ super + "{ports: #{ports.join(', ')}}"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,96 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "rspec-background-process"
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jakub Pastuszek"]
12
+ s.date = "2014-09-29"
13
+ s.description = "RSpec and Cucumber DSL that allows definition of processes with their arguments, working directory, time outs, port numbers etc. and start/stop them during test runs. Processes with same definitions can pooled and reused between example runs to save time on startup/shutdown. Pooling supports process number limiting with LRU to limit memory used."
14
+ s.email = "jpastuszek@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "lib/rspec-background-process.rb",
29
+ "lib/rspec-background-process/background_process.rb",
30
+ "lib/rspec-background-process/background_process_helpers.rb",
31
+ "lib/rspec-background-process/process_pool.rb",
32
+ "lib/rspec-background-process/readiness_checks.rb",
33
+ "lib/rspec-background-process/refresh_actions.rb",
34
+ "lib/rspec-background-process/server.rb",
35
+ "rspec-background-process.gemspec",
36
+ "spec/background_process_helpers_spec.rb",
37
+ "spec/background_process_spec.rb",
38
+ "spec/features/cwd_spec.rb",
39
+ "spec/features/dead_detection_spec.rb",
40
+ "spec/features/exec_and_loading_spec.rb",
41
+ "spec/features/pool_lru_spec.rb",
42
+ "spec/features/ready_test_spec.rb",
43
+ "spec/features/refresh_spec.rb",
44
+ "spec/features/reuse_spec.rb",
45
+ "spec/features/server_spec.rb",
46
+ "spec/features/variables_replacement_spec.rb",
47
+ "spec/process_definition_spec.rb",
48
+ "spec/spec_helper.rb",
49
+ "spec/support/test_die",
50
+ "spec/support/test_http_server",
51
+ "spec/support/test_process",
52
+ "spec/support/test_slow_die"
53
+ ]
54
+ s.homepage = "http://github.com/jpastuszek/rspec-background-process"
55
+ s.licenses = ["MIT"]
56
+ s.require_paths = ["lib"]
57
+ s.rubygems_version = "1.8.23"
58
+ s.summary = "RSpec and Cucumber DSL library that helps managing background processes during test runs"
59
+
60
+ if s.respond_to? :specification_version then
61
+ s.specification_version = 3
62
+
63
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
64
+ s.add_development_dependency(%q<faraday>, [">= 0.8"])
65
+ s.add_development_dependency(%q<rspec>, ["~> 3.1"])
66
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
67
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
68
+ s.add_development_dependency(%q<daemon>, ["~> 1.2"])
69
+ s.add_development_dependency(%q<micromachine>, ["~> 1.1"])
70
+ s.add_development_dependency(%q<rufus-lru>, ["~> 1.0"])
71
+ s.add_development_dependency(%q<file-tail>, ["~> 1.0"])
72
+ s.add_development_dependency(%q<retries>, ["~> 0.0.5"])
73
+ else
74
+ s.add_dependency(%q<faraday>, [">= 0.8"])
75
+ s.add_dependency(%q<rspec>, ["~> 3.1"])
76
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
77
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
78
+ s.add_dependency(%q<daemon>, ["~> 1.2"])
79
+ s.add_dependency(%q<micromachine>, ["~> 1.1"])
80
+ s.add_dependency(%q<rufus-lru>, ["~> 1.0"])
81
+ s.add_dependency(%q<file-tail>, ["~> 1.0"])
82
+ s.add_dependency(%q<retries>, ["~> 0.0.5"])
83
+ end
84
+ else
85
+ s.add_dependency(%q<faraday>, [">= 0.8"])
86
+ s.add_dependency(%q<rspec>, ["~> 3.1"])
87
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
88
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
89
+ s.add_dependency(%q<daemon>, ["~> 1.2"])
90
+ s.add_dependency(%q<micromachine>, ["~> 1.1"])
91
+ s.add_dependency(%q<rufus-lru>, ["~> 1.0"])
92
+ s.add_dependency(%q<file-tail>, ["~> 1.0"])
93
+ s.add_dependency(%q<retries>, ["~> 0.0.5"])
94
+ end
95
+ end
96
+
@@ -0,0 +1,32 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe BackgroundProcessHelpers, with: :background_process do
4
+ describe '#process_pool' do
5
+ it 'should provide singleton pool object ' do
6
+ expect {
7
+ process_pool
8
+ }.not_to change {
9
+ process_pool
10
+ }
11
+ end
12
+ end
13
+
14
+ describe '#background_process' do
15
+ it 'should allow specifying executable to run' do
16
+ process = background_process('features/support/test_process')
17
+ expect(process.instance.command).to include 'features/support/test_process'
18
+ end
19
+
20
+ describe 'load option' do
21
+ it 'when set to true will change instance type to LoadedBackgroundProcess' do
22
+ process = background_process('features/support/test_process', load: true)
23
+ expect(process.instance).to be_a RSpecBackgroundProcess::LoadedBackgroundProcess
24
+ end
25
+ end
26
+
27
+ it 'should return process definition' do
28
+ process = background_process('features/support/test_process')
29
+ expect(process).to be_a RSpecBackgroundProcess::ProcessPool::ProcessDefinition
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,90 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe RSpecBackgroundProcess::BackgroundProcess, subject: :instance do
4
+ describe 'running' do
5
+ it '#command should represent command to be executed' do
6
+ expect(subject.command).to include 'test_process'
7
+ end
8
+
9
+ it '#start should spawn the process by calling #spawn' do
10
+ expect(subject).to receive(:spawn).once.and_return([0, Thread.new{}])
11
+ subject.start
12
+ end
13
+
14
+ it '#stop should stop the process by sending TERM signal' do
15
+ expect(subject).to receive(:spawn).once.and_return([0, Thread.new{sleep 0.1}])
16
+ subject.start
17
+
18
+ expect(Process).to receive(:kill).once.with('TERM', kind_of(Numeric))
19
+ subject.stop
20
+ end
21
+
22
+ it '#restart should stop and then start the process' do
23
+ expect(subject).to receive(:spawn).once.and_return([0, Thread.new{sleep 0.1}])
24
+ subject.start
25
+
26
+ expect(subject).to receive(:spawn).once.and_return([1, Thread.new{sleep 0.1}])
27
+ expect(Process).to receive(:kill).once.with('TERM', kind_of(Numeric))
28
+
29
+ expect {
30
+ subject.restart
31
+ }.to change {
32
+ subject.pid
33
+ }.from(0).to(1)
34
+ end
35
+
36
+ describe 'related predicates' do
37
+ it '#pid should be integer when process is running and nil otherwise' do
38
+ expect(subject.pid).to be_nil
39
+
40
+ expect(subject).to receive(:spawn).once.and_return([42, Thread.new{sleep 0.1}])
41
+ subject.start
42
+
43
+ expect(subject.pid).to be_a(Integer).and eq(42)
44
+
45
+ expect(Process).to receive(:kill).once.with('TERM', kind_of(Numeric))
46
+ subject.stop
47
+ expect(subject.pid).to be_nil
48
+ end
49
+
50
+ it '#running? should be true when process is running' do
51
+ expect(subject).not_to be_running
52
+ expect(subject).to receive(:spawn).once.and_return([42, Thread.new{sleep 0.1}])
53
+ subject.start
54
+ expect(subject).to be_running
55
+ end
56
+
57
+ it '#dead? should be true when process exited but was not stopped by us' do
58
+ expect(subject).not_to be_dead
59
+ expect(subject).to receive(:spawn).once.and_return([42, Thread.new{sleep 0.1}])
60
+ subject.start
61
+ expect(subject).not_to be_dead
62
+
63
+ expect(Process).to receive(:kill).once.with('TERM', kind_of(Numeric))
64
+ subject.stop
65
+ expect(subject).not_to be_dead
66
+
67
+ expect(subject).to receive(:spawn).once.and_return([42, Thread.new{}])
68
+ subject.start
69
+ sleep 0.2
70
+ expect(subject).to be_dead
71
+ end
72
+ end
73
+ end
74
+
75
+ describe 'readiness', subject: :process do
76
+ it 'instance #wait_ready should call readiness block with self as argument' do
77
+ expect { |b|
78
+ subject.ready_test do |*args|
79
+ b.to_proc.call(*args)
80
+ # need to return true
81
+ true
82
+ end
83
+ instance = subject.instance
84
+ expect(instance).to receive(:spawn).once.and_return([42, Thread.new{sleep 0.1}])
85
+ instance.start
86
+ instance.wait_ready
87
+ }.to yield_with_args(RSpecBackgroundProcess::BackgroundProcess)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,51 @@
1
+ require_relative '../spec_helper'
2
+
3
+ def process_cwd_from_log(log)
4
+ log.readlines.grep(/^cwd:/).first.split(': ', 2).last.match(/'(.*)'/).captures.first.strip || fail('cannot extract cwd from log')
5
+ end
6
+
7
+ feature 'auto managing current working directory', subject: :process_ready_variables do
8
+ scenario 'processes by default run in own different directory' do
9
+ instance1 = subject.start.wait_ready
10
+ instance2 = subject.with{|p| p.argument 'blah'}.start.wait_ready
11
+
12
+ # reported cwd
13
+ expect(instance1.working_directory.to_s).not_to eq(instance2.working_directory.to_s)
14
+
15
+ # actual cwd
16
+ expect(process_cwd_from_log(instance1.log_file)).to eq(instance1.working_directory.realpath.to_s)
17
+ expect(process_cwd_from_log(instance2.log_file)).to eq(instance2.working_directory.realpath.to_s)
18
+ end
19
+ end
20
+
21
+ feature 'process current working directory does not affect test current directory', subject: :process_ready_variables do
22
+ scenario 'master process current working directory unchanged over instance start' do
23
+ expect {
24
+ subject.start.wait_ready
25
+ }.not_to change {
26
+ Dir.pwd
27
+ }
28
+
29
+ # reported cwd
30
+ expect(subject.instance.working_directory.to_s).not_to eq(Dir.pwd)
31
+
32
+ # actual cwd
33
+ expect(process_cwd_from_log(subject.instance.log_file)).to eq(subject.instance.working_directory.realpath.to_s)
34
+ end
35
+ end
36
+
37
+ feature 'current working directory can be configured to custom directory', subject: :process_ready_variables do
38
+ let :test_dir do
39
+ Pathname.new(Dir.pwd) + 'tmp'
40
+ end
41
+
42
+ scenario 'setting current working directory to test directory' do
43
+ instance = subject.with{|p| p.working_directory test_dir}.start.wait_ready
44
+
45
+ # reported cwd
46
+ expect(instance.working_directory.to_s).to eq(test_dir.to_s)
47
+
48
+ # actual cwd
49
+ expect(process_cwd_from_log(instance.log_file)).to eq(instance.working_directory.realpath.to_s)
50
+ end
51
+ end
@@ -0,0 +1,81 @@
1
+ require_relative '../spec_helper'
2
+
3
+ feature 'background process is marked as dead if it exits before stop was called', subject: :dying_process do
4
+ scenario 'running dieing process renders it marked as dead' do
5
+ instance = subject.start
6
+ expect(instance).not_to be_dead
7
+
8
+ # git it some time to spawn and exit
9
+ sleep 0.5
10
+ expect(instance).to be_dead
11
+ end
12
+ end
13
+
14
+ feature 'dead instance should not be usable any more', subject: :dying_process do
15
+ scenario 'trying to #start dead instance' do
16
+ instance = subject.start
17
+
18
+ # git it some time to spawn and exit
19
+ sleep 0.5
20
+
21
+ expect {
22
+ instance.start
23
+ }.to raise_error RSpecBackgroundProcess::BackgroundProcess::StateError, /can't start when in state: dead/
24
+ end
25
+
26
+ scenario 'trying to #stop or #restart dead instance' do
27
+ instance = subject.start
28
+
29
+ # git it some time to spawn and exit
30
+ sleep 0.5
31
+
32
+ expect {
33
+ instance.stop
34
+ }.to raise_error RSpecBackgroundProcess::BackgroundProcess::StateError, /can't stop when in state: dead/
35
+
36
+ expect {
37
+ instance.restart
38
+ }.to raise_error RSpecBackgroundProcess::BackgroundProcess::StateError, /can't stop when in state: dead/
39
+ end
40
+
41
+ scenario 'trying to #wait_ready on dead instance' do
42
+ instance = subject.start
43
+
44
+ # git it some time to spawn and exit
45
+ sleep 0.5
46
+
47
+ expect {
48
+ instance.wait_ready
49
+ }.to raise_error RSpecBackgroundProcess::BackgroundProcess::StateError, /can't wait ready when in state: dead/
50
+ end
51
+ end
52
+
53
+ feature 'detecting instance death wile waiting for it to become ready', subject: :slowly_dying_process do
54
+ context 'with log file based check' do
55
+ scenario 'instance dying while we wait for it to become ready' do
56
+ subject.ready_when_log_includes 'bye'
57
+
58
+ instance = subject.start
59
+
60
+ expect {
61
+ instance.wait_ready
62
+ }.to raise_error RSpecBackgroundProcess::BackgroundProcess::ProcessExitedError, /exited with exit code: 0/
63
+
64
+ expect(instance).to be_dead
65
+ end
66
+ end
67
+
68
+ context 'with URL based check' do
69
+ scenario 'instance dying while we wait for it to become ready' do
70
+ subject.ready_when_url_response_status 'http://localhost:1234/health_check', 'OK'
71
+
72
+ instance = subject.start
73
+
74
+ expect {
75
+ instance.wait_ready
76
+ }.to raise_error RSpecBackgroundProcess::BackgroundProcess::ProcessExitedError, /exited with exit code: 0/
77
+
78
+ expect(instance).to be_dead
79
+ end
80
+ end
81
+ end