capybarbecue 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.txt +17 -16
- data/README.md +89 -0
- data/lib/capybarbecue.rb +12 -2
- data/lib/capybarbecue/async_call.rb +10 -14
- data/lib/capybarbecue/async_delegate_class.rb +18 -2
- data/lib/capybarbecue/async_executer.rb +35 -0
- data/lib/capybarbecue/server.rb +3 -0
- data/lib/capybarbecue/version.rb +1 -1
- data/spec/capybarbecue/async_call_spec.rb +8 -32
- data/spec/capybarbecue/async_delegate_class_spec.rb +23 -5
- data/spec/capybarbecue/async_executer_spec.rb +39 -0
- data/spec/capybarbecue/server_spec.rb +12 -0
- data/spec/capybarbecue_spec.rb +1 -1
- data/spec/integration/session_spec.rb +3 -0
- metadata +8 -44
- data/.document +0 -5
- data/.rspec +0 -1
- data/Gemfile +0 -15
- data/Gemfile.lock +0 -78
- data/NOTES.txt +0 -37
- data/README.rdoc +0 -19
- data/Rakefile +0 -44
- data/lib/capybarbecue/rack_runner.rb +0 -7
- data/spec/capybarbecue/rack_runner_spec.rb +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2678761d8bc81927ea82cb9bca880c1a8818043
|
4
|
+
data.tar.gz: 6d41973f08aa3b7f8bd69cb0bae7fe88db3e77c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef670b9982bc9a534a8b36b1f5edcf05c99e0a22d334bb8e88af8d6505ffee4dd0d169f156a7bf8ce29b214bcbff6771a74e1bc7c26f2518a1af50ead9f58b89
|
7
|
+
data.tar.gz: 63a06f7afeffa2d0b9d66f15d62dd0767efc01842d15c73a8483c06ff912cad74af0e2f7aa9773a9aa138ebad0230b021d03ca615635e75471c5a0b944758718
|
data/LICENSE.txt
CHANGED
@@ -1,20 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
1
3
|
Copyright (c) 2013 Andrew DiMichele
|
2
4
|
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
the following conditions:
|
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:
|
10
11
|
|
11
|
-
The above copyright notice and this permission notice shall be
|
12
|
-
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
13
14
|
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
OF
|
20
|
-
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Capybarbecue
|
2
|
+
|
3
|
+
_Capybara the way you like it... skinned, butchered, and cooked to perfection._
|
4
|
+
|
5
|
+
## What is Capybarbecue?
|
6
|
+
|
7
|
+
*Capybarbecue* makes adds reliability and flexibility to your Capybara test suite by silently changing the way
|
8
|
+
Capybara works. It gives you a shared database connection and allows you to safely write tests that make use of it
|
9
|
+
while eliminating race conditions.
|
10
|
+
|
11
|
+
For example, the following Capybara test only works with a shared database connection and without *Capybarbecue* it
|
12
|
+
will randomly fail every now and then due to the race condition that your typical shared database connection
|
13
|
+
introduces:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
user = User.first
|
17
|
+
visit edit_user_path(user)
|
18
|
+
fill_in 'first_name', with: 'Andrew' # Edit the user's name
|
19
|
+
click_on 'Submit' # Triggers POST request
|
20
|
+
expect(user.reload.first_name).to eq 'Andrew' # This line will cause you some headaches
|
21
|
+
```
|
22
|
+
|
23
|
+
Now in principle, this isn't the "right" way to perform automated acceptance testing since you would only simulate
|
24
|
+
actual user behavior which can only occur through a browser, but let's face it: this paradigm doesn't work for all
|
25
|
+
testing use cases and writing tests like the one above is sometimes just too damn convenient.
|
26
|
+
|
27
|
+
## Setup
|
28
|
+
|
29
|
+
This is so easy... you're gonna love it.
|
30
|
+
|
31
|
+
To install *Capybarbecue*, add this line to your `Gemfile` and then `bundle install`
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
gem 'capybarbecue'
|
35
|
+
```
|
36
|
+
|
37
|
+
Then at the bottom of your test set up file (e.g. RSpec's `spec_helper.rb` or Cucumber's `env.rb`) after you've done
|
38
|
+
your Capybara setup, add this line:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
Capybarbecue.activate!
|
42
|
+
```
|
43
|
+
|
44
|
+
That's it. Just continue to write your Capybara tests as you normally would. Welcome to the BBQ!
|
45
|
+
|
46
|
+
## Beware Javascript
|
47
|
+
|
48
|
+
AJAX and other javascript interactions may cause you problems since Capybara has no way to know when your javascript has
|
49
|
+
finished executing. To prevent issues with this, make sure you check the state of the page to ensure that your
|
50
|
+
javascript has finished running.
|
51
|
+
|
52
|
+
For example, let's say we have a button with an `onclick` handler that simply visits a new page. Here's the
|
53
|
+
_wrong_ way to test:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
visit '/page/1'
|
57
|
+
click_on 'Go to /page/2' # Triggers an `onclick` handler
|
58
|
+
click_on 'Go to /page/3' # OOPS! No guarantee that /page/2 has been loaded yet!
|
59
|
+
```
|
60
|
+
|
61
|
+
Here's the _right_ way:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
visit '/page/1'
|
65
|
+
click_on 'Go to /page/2' # Triggers an `onclick` handler
|
66
|
+
page.should have_content 'Page 2' # GREAT! Capybara will actually wait until this is true (or fail after some timeout)
|
67
|
+
click_on 'Go to /page/3' # We know we're on the right page and that this button exists!
|
68
|
+
```
|
69
|
+
|
70
|
+
Note that this only works with Capybara matchers. For example, the following will _not_ work:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
visit '/page/1'
|
74
|
+
click_on 'Go to /page/2' # Triggers an `onclick` handler
|
75
|
+
current_path.should start_with '/page/2' # OOPS! The new page may not have loaded yet!
|
76
|
+
click_on 'Go to /page/3' # We may not even get here
|
77
|
+
```
|
78
|
+
|
79
|
+
## How it works
|
80
|
+
|
81
|
+
*Capybarbecue* works by running your tests and handling Rack requests in the same thread. When you make a call to the
|
82
|
+
Capybara API, your call is delegated to another thread while the main thread is used to handle web requests, which are
|
83
|
+
themselves queued by third thread which acts as the web server. Once the Capybara matcher returns, control is returned
|
84
|
+
to your test.
|
85
|
+
|
86
|
+
## Copyright
|
87
|
+
|
88
|
+
Copyright (c) 2013 Andrew DiMichele. See LICENSE.txt for further details.
|
89
|
+
|
data/lib/capybarbecue.rb
CHANGED
@@ -10,16 +10,23 @@ module Capybarbecue
|
|
10
10
|
return if activated?
|
11
11
|
require 'capybarbecue/async_call'
|
12
12
|
require 'capybarbecue/async_delegate_class'
|
13
|
-
require 'capybarbecue/
|
13
|
+
require 'capybarbecue/async_executer'
|
14
14
|
require 'capybarbecue/server'
|
15
15
|
|
16
16
|
(class << Capybara; self end).instance_eval do
|
17
17
|
alias_method :original_session, :current_session
|
18
18
|
define_method :current_session do
|
19
|
-
Capybarbecue::AsyncDelegateClass.new(original_session) do
|
19
|
+
bbq_lookup[original_session] ||= Capybarbecue::AsyncDelegateClass.new(original_session, bbq_executer) do
|
20
20
|
Capybara.app.handle_requests
|
21
21
|
end
|
22
22
|
end
|
23
|
+
# TODO: Consider nesting these under an object that's easy to remove
|
24
|
+
define_method :bbq_lookup do
|
25
|
+
@bbq_lookup ||= {}
|
26
|
+
end
|
27
|
+
define_method :bbq_executer do
|
28
|
+
@bbq_executer ||= Capybarbecue::AsyncExecuter.new
|
29
|
+
end
|
23
30
|
end
|
24
31
|
|
25
32
|
Capybara.send(:session_pool).clear
|
@@ -36,6 +43,9 @@ module Capybarbecue
|
|
36
43
|
(class << Capybara; self end).instance_eval do
|
37
44
|
alias_method :current_session, :original_session
|
38
45
|
remove_method :original_session
|
46
|
+
remove_method :bbq_lookup
|
47
|
+
remove_method :bbq_executer
|
48
|
+
# TODO: Remove instance variables as well
|
39
49
|
end
|
40
50
|
Capybara.send(:session_pool).clear
|
41
51
|
Capybara.app = Capybara.app.app
|
@@ -1,8 +1,7 @@
|
|
1
1
|
require 'thread'
|
2
2
|
|
3
|
-
class AsyncCall
|
4
|
-
DEFAULT_TIMEOUT =
|
5
|
-
attr_reader :thread
|
3
|
+
class AsyncCall
|
4
|
+
DEFAULT_TIMEOUT = 30
|
6
5
|
attr_reader :ready
|
7
6
|
attr_reader :to_s
|
8
7
|
alias :ready? :ready
|
@@ -12,12 +11,13 @@ class AsyncCall # Asynchronously calls a method
|
|
12
11
|
@ready = false
|
13
12
|
@response = nil
|
14
13
|
@exception = nil
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
14
|
+
end
|
15
|
+
|
16
|
+
def run # Should be called from the executer
|
17
|
+
begin
|
18
|
+
respond_with @obj.send(@method, *@args, &@block)
|
19
|
+
rescue Exception => e
|
20
|
+
respond_with_exception e
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -29,7 +29,7 @@ class AsyncCall # Asynchronously calls a method
|
|
29
29
|
# It feels dangerous not to sleep here... keep a pulse on this (sleep causes performance problems)
|
30
30
|
Thread.pass
|
31
31
|
end
|
32
|
-
|
32
|
+
raise Timeout::Error.new('Timeout expired before response received') unless ready?
|
33
33
|
if @exception.present?
|
34
34
|
# Add the backtrace from this thread to make it useful
|
35
35
|
backtrace = @exception.backtrace + Kernel.caller
|
@@ -39,10 +39,6 @@ class AsyncCall # Asynchronously calls a method
|
|
39
39
|
@response
|
40
40
|
end
|
41
41
|
|
42
|
-
def kill!
|
43
|
-
@thread.kill
|
44
|
-
end
|
45
|
-
|
46
42
|
def to_s
|
47
43
|
s = "#{@obj.class.name}##{@method}(#{@args.map{ |a| a.class.name }.join(', ')})"
|
48
44
|
s += ' { ... }' if @block.present?
|
@@ -2,16 +2,23 @@ module Capybarbecue
|
|
2
2
|
class AsyncDelegateClass
|
3
3
|
attr_reader :instance
|
4
4
|
|
5
|
-
def initialize(instance, &wait_proc)
|
5
|
+
def initialize(instance, executer, &wait_proc)
|
6
6
|
@instance = instance
|
7
|
+
@executer = executer
|
7
8
|
@wait_proc = wait_proc # Called repeatedly while waiting
|
8
9
|
end
|
9
10
|
|
10
11
|
def method_missing(method, *args, &block)
|
11
12
|
call = AsyncCall.new(@instance, method, *args, &block)
|
13
|
+
@executer.execute(call)
|
12
14
|
wrap_response(call.wait_for_response(&@wait_proc))
|
13
15
|
end
|
14
16
|
|
17
|
+
# This is our hint that we are an AsyncDelegateClass
|
18
|
+
def __async_delegate__
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
15
22
|
private
|
16
23
|
|
17
24
|
def respond_to_missing?(method, include_all=false)
|
@@ -21,10 +28,19 @@ module Capybarbecue
|
|
21
28
|
# Wrap anything that looks like Capybara
|
22
29
|
def wrap_response(value)
|
23
30
|
if value.class.name.include?('Capybara')
|
24
|
-
AsyncDelegateClass.new(value, &@wait_proc)
|
31
|
+
AsyncDelegateClass.new(value, @executer, &@wait_proc)
|
25
32
|
else
|
26
33
|
value
|
27
34
|
end
|
28
35
|
end
|
36
|
+
|
37
|
+
## For instance methods of Object, delegate these to the object we've wrapped
|
38
|
+
super_methods = superclass.instance_methods - (instance_methods - superclass.instance_methods)
|
39
|
+
super_methods -= [:methods, :__send__, :__id__, :respond_to?] # Don't override these bad boys
|
40
|
+
super_methods.each do |method|
|
41
|
+
define_method method do |*args, &block|
|
42
|
+
method_missing(method, *args, &block)
|
43
|
+
end
|
44
|
+
end
|
29
45
|
end
|
30
46
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Capybarbecue
|
4
|
+
class AsyncExecuter
|
5
|
+
attr_reader :thread
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@queue = Queue.new
|
9
|
+
@thread = Thread.new do
|
10
|
+
while true
|
11
|
+
# This #run method should handle its own exceptions
|
12
|
+
@queue.deq.run
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute(async_call)
|
18
|
+
if in_executer?
|
19
|
+
async_call.run
|
20
|
+
else
|
21
|
+
@queue.enq(async_call)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def kill!
|
26
|
+
@thread.kill
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def in_executer?
|
32
|
+
Thread.current == @thread
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/capybarbecue/server.rb
CHANGED
data/lib/capybarbecue/version.rb
CHANGED
@@ -6,22 +6,15 @@ describe AsyncCall do
|
|
6
6
|
let(:args) { [] }
|
7
7
|
let(:block) { nil }
|
8
8
|
subject{ AsyncCall.new(obj, method, *args, &block) }
|
9
|
-
after{ subject.kill! }
|
10
|
-
describe '#thread' do
|
11
|
-
it 'should be a thread' do
|
12
|
-
expect(subject.thread).to be_a Thread
|
13
|
-
end
|
14
|
-
end
|
15
9
|
describe '#ready' do
|
16
10
|
let(:method){ :test }
|
17
11
|
before do
|
18
12
|
stub(obj).test do
|
19
|
-
sleep 0.1
|
20
13
|
end
|
21
14
|
end
|
22
15
|
it 'should be true once the response is ready' do
|
23
16
|
expect(subject).to_not be_ready
|
24
|
-
|
17
|
+
subject.run
|
25
18
|
expect(subject).to be_ready
|
26
19
|
end
|
27
20
|
end
|
@@ -29,28 +22,21 @@ describe AsyncCall do
|
|
29
22
|
let(:obj){ Object }
|
30
23
|
let(:method){ :name }
|
31
24
|
it 'returns the value' do
|
25
|
+
subject.run
|
32
26
|
expect(subject.wait_for_response).to eq 'Object'
|
33
27
|
end
|
34
28
|
context 'with a block' do
|
35
|
-
before do
|
36
|
-
stub(obj).name { sleep 0.3 }
|
37
|
-
end
|
38
29
|
it 'calls the block repeatedly while waiting' do
|
39
30
|
mock(obj).foo.at_least(2)
|
40
|
-
|
31
|
+
begin
|
32
|
+
subject.wait_for_response(0.5) { obj.foo }
|
33
|
+
rescue Timeout::Error
|
34
|
+
end
|
41
35
|
end
|
42
36
|
end
|
43
37
|
context 'when the timeout expires' do
|
44
|
-
|
45
|
-
before do
|
46
|
-
stub(obj).test do
|
47
|
-
sleep 1
|
48
|
-
end
|
49
|
-
end
|
50
|
-
it 'raises an error and kills the thread' do
|
38
|
+
it 'raises an error' do
|
51
39
|
expect{ subject.wait_for_response(0.01) }.to raise_error Timeout::Error
|
52
|
-
sleep 0.1
|
53
|
-
expect(subject.thread).to_not be_alive
|
54
40
|
end
|
55
41
|
end
|
56
42
|
context 'when the call raises an exception' do
|
@@ -61,21 +47,11 @@ describe AsyncCall do
|
|
61
47
|
end
|
62
48
|
end
|
63
49
|
it 'also raises the exception' do
|
50
|
+
subject.run
|
64
51
|
expect{ subject.wait_for_response }.to raise_error ZeroDivisionError
|
65
52
|
end
|
66
53
|
end
|
67
54
|
end
|
68
|
-
describe '#kill!' do
|
69
|
-
let(:method){ :test }
|
70
|
-
before do
|
71
|
-
stub(obj).test do
|
72
|
-
sleep 1
|
73
|
-
end
|
74
|
-
end
|
75
|
-
it 'kills the thread' do
|
76
|
-
expect{ subject.kill!; sleep 0.01 }.to change{ subject.thread.stop? }.from(false).to(true)
|
77
|
-
end
|
78
|
-
end
|
79
55
|
describe '#to_s' do
|
80
56
|
let(:obj) { Object.new }
|
81
57
|
let(:method) { :foo }
|
@@ -2,7 +2,8 @@ require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
|
2
2
|
|
3
3
|
describe Capybarbecue::AsyncDelegateClass do
|
4
4
|
let(:obj){ Object.new }
|
5
|
-
|
5
|
+
let(:executer){ Capybarbecue::AsyncExecuter.new }
|
6
|
+
subject{ Capybarbecue::AsyncDelegateClass.new(obj, executer) }
|
6
7
|
|
7
8
|
it 'calls the method on the session asynchronously' do
|
8
9
|
mock(obj).foo{ 'cats' }
|
@@ -19,18 +20,19 @@ describe Capybarbecue::AsyncDelegateClass do
|
|
19
20
|
context 'when the object returned is a Capybara object' do
|
20
21
|
before { mock(obj).foo{ Capybara::Driver::Base.new } }
|
21
22
|
it 'wraps the return value in AsyncDelegateClass' do
|
22
|
-
expect(subject.foo).to
|
23
|
+
expect(subject.foo).to respond_to :__async_delegate__
|
23
24
|
end
|
24
25
|
context 'when a block is given' do
|
25
26
|
subject do
|
26
|
-
Capybarbecue::AsyncDelegateClass.new(obj) do
|
27
|
+
Capybarbecue::AsyncDelegateClass.new(obj, executer) do
|
27
28
|
obj.wait_func
|
28
29
|
end
|
29
30
|
end
|
30
31
|
it 'preserves the wait_proc' do
|
31
|
-
|
32
|
+
stub(obj).wait_func
|
32
33
|
resp = subject.foo
|
33
34
|
stub(resp.instance).foo
|
35
|
+
mock(obj).wait_func
|
34
36
|
resp.foo
|
35
37
|
end
|
36
38
|
end
|
@@ -49,9 +51,25 @@ describe Capybarbecue::AsyncDelegateClass do
|
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
54
|
+
describe 'instance_methods of object' do
|
55
|
+
let(:obj){ Hash.new }
|
56
|
+
it 'delegates these to the wrapped object' do
|
57
|
+
expect(subject).to be_a Hash
|
58
|
+
expect(subject).to be_a_kind_of Hash
|
59
|
+
expect(subject).to be_an_instance_of Hash
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#__async_delegate__' do
|
64
|
+
it 'is defined and returns true' do
|
65
|
+
expect(subject).to respond_to :__async_delegate__
|
66
|
+
expect(subject.__async_delegate__).to be_true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
52
70
|
context 'when a block is given' do
|
53
71
|
subject do
|
54
|
-
Capybarbecue::AsyncDelegateClass.new(obj) do
|
72
|
+
Capybarbecue::AsyncDelegateClass.new(obj, executer) do
|
55
73
|
obj.wait_func
|
56
74
|
end
|
57
75
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Capybarbecue::AsyncExecuter do
|
4
|
+
subject { Capybarbecue::AsyncExecuter.new }
|
5
|
+
describe '#thread' do
|
6
|
+
it 'should be alive' do
|
7
|
+
subject
|
8
|
+
sleep 0.01
|
9
|
+
expect(subject.thread).to be_alive
|
10
|
+
end
|
11
|
+
end
|
12
|
+
describe '#execute' do
|
13
|
+
let(:async_call){ AsyncCall.new(Object, :name) }
|
14
|
+
it 'eventually executes the call' do
|
15
|
+
subject.execute(async_call)
|
16
|
+
expect(async_call.wait_for_response(0.1)).to eq 'Object'
|
17
|
+
end
|
18
|
+
context 'with nested calls' do
|
19
|
+
let(:obj) do
|
20
|
+
Object.new.tap do |o|
|
21
|
+
stub(o).foo do
|
22
|
+
subject.execute(async_call)
|
23
|
+
async_call.wait_for_response(0.1)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
let(:first_async_call){ AsyncCall.new(obj, :foo) }
|
28
|
+
it 'executes both calls without deadlocking' do
|
29
|
+
subject.execute(first_async_call)
|
30
|
+
expect(async_call.wait_for_response(0.1)).to eq 'Object'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
describe '#kill!' do
|
35
|
+
it 'kills the thread' do
|
36
|
+
expect{ subject.kill!; sleep 0.01 }.to change{ subject.thread.stop? }.from(false).to(true)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'rack'
|
2
3
|
|
3
4
|
describe Capybarbecue::Server do
|
4
5
|
let(:app) { Object.new }
|
@@ -18,6 +19,17 @@ describe Capybarbecue::Server do
|
|
18
19
|
subject.call(request2)
|
19
20
|
subject.handle_requests
|
20
21
|
end
|
22
|
+
context 'when Rack returns a Rack::BodyProxy object' do
|
23
|
+
let(:body){ Rack::BodyProxy.new('body') }
|
24
|
+
before do
|
25
|
+
stub(app).call { [200, {}, body] }
|
26
|
+
subject.call(nil)
|
27
|
+
end
|
28
|
+
it 'closes the BodyProxy' do
|
29
|
+
mock(body).close
|
30
|
+
subject.handle_requests
|
31
|
+
end
|
32
|
+
end
|
21
33
|
end
|
22
34
|
describe '#call' do
|
23
35
|
context 'when another thread handles the request' do
|
data/spec/capybarbecue_spec.rb
CHANGED
@@ -21,7 +21,7 @@ describe Capybarbecue do
|
|
21
21
|
after{ Capybarbecue.deactivate! }
|
22
22
|
it 'redefines Capybara#current_session' do
|
23
23
|
subject
|
24
|
-
expect(Capybara.current_session).to
|
24
|
+
expect(Capybara.current_session).to respond_to :__async_delegate__
|
25
25
|
expect(Capybara.current_session.instance).to be Capybara.original_session
|
26
26
|
end
|
27
27
|
it 'saves old Capybara#current_session Capybara#original_session' do
|
@@ -111,6 +111,9 @@ describe 'With a real life app', :type => :feature, :capybara_feature => true do
|
|
111
111
|
within 'div#div1' do
|
112
112
|
expect(page).to have_css 'div.divclass2'
|
113
113
|
end
|
114
|
+
within find('div#div1') do
|
115
|
+
expect(page).to have_css 'div.divclass2'
|
116
|
+
end
|
114
117
|
within_fieldset 'fieldset1' do
|
115
118
|
expect(page).to have_field 'file_field'
|
116
119
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: capybarbecue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew DiMichele
|
@@ -80,20 +80,6 @@ dependencies:
|
|
80
80
|
- - '>='
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rdoc
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - '>='
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - '>='
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
83
|
- !ruby/object:Gem::Dependency
|
98
84
|
name: bundler
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,20 +94,6 @@ dependencies:
|
|
108
94
|
- - '>='
|
109
95
|
- !ruby/object:Gem::Version
|
110
96
|
version: '0'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: jeweler
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - '>='
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - '>='
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
97
|
- !ruby/object:Gem::Dependency
|
126
98
|
name: poltergeist
|
127
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -155,33 +127,25 @@ description: Makes fundamental changes to Capybara's threading architecture so y
|
|
155
127
|
email: backflip@gmail.com
|
156
128
|
executables: []
|
157
129
|
extensions: []
|
158
|
-
extra_rdoc_files:
|
159
|
-
- LICENSE.txt
|
160
|
-
- README.rdoc
|
130
|
+
extra_rdoc_files: []
|
161
131
|
files:
|
162
|
-
- .document
|
163
|
-
- .rspec
|
164
|
-
- Gemfile
|
165
|
-
- Gemfile.lock
|
166
|
-
- LICENSE.txt
|
167
|
-
- NOTES.txt
|
168
|
-
- README.rdoc
|
169
|
-
- Rakefile
|
170
|
-
- lib/capybarbecue.rb
|
171
132
|
- lib/capybarbecue/async_call.rb
|
172
133
|
- lib/capybarbecue/async_delegate_class.rb
|
173
|
-
- lib/capybarbecue/
|
134
|
+
- lib/capybarbecue/async_executer.rb
|
174
135
|
- lib/capybarbecue/server.rb
|
175
136
|
- lib/capybarbecue/version.rb
|
137
|
+
- lib/capybarbecue.rb
|
176
138
|
- spec/capybarbecue/async_call_spec.rb
|
177
139
|
- spec/capybarbecue/async_delegate_class_spec.rb
|
178
|
-
- spec/capybarbecue/
|
140
|
+
- spec/capybarbecue/async_executer_spec.rb
|
179
141
|
- spec/capybarbecue/server_spec.rb
|
180
142
|
- spec/capybarbecue_spec.rb
|
181
143
|
- spec/integration/element_spec.rb
|
182
144
|
- spec/integration/session_spec.rb
|
183
145
|
- spec/spec_helper.rb
|
184
146
|
- spec/support/test_rack_app.rb
|
147
|
+
- README.md
|
148
|
+
- LICENSE.txt
|
185
149
|
homepage: http://github.com/adimichele/capybarbecue
|
186
150
|
licenses:
|
187
151
|
- MIT
|
@@ -194,7 +158,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
194
158
|
requirements:
|
195
159
|
- - '>='
|
196
160
|
- !ruby/object:Gem::Version
|
197
|
-
version:
|
161
|
+
version: 1.9.3
|
198
162
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
163
|
requirements:
|
200
164
|
- - '>='
|
data/.document
DELETED
data/.rspec
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
--color
|
data/Gemfile
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
source "http://rubygems.org"
|
2
|
-
|
3
|
-
group :development do
|
4
|
-
gem "rspec"
|
5
|
-
gem "rr", require: false
|
6
|
-
gem "yard"
|
7
|
-
gem "rdoc"
|
8
|
-
gem "bundler"
|
9
|
-
gem "jeweler"
|
10
|
-
gem "poltergeist"
|
11
|
-
gem "launchy"
|
12
|
-
end
|
13
|
-
|
14
|
-
gem "activesupport"
|
15
|
-
gem "capybara", "~>2.1.0"
|
data/Gemfile.lock
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
GEM
|
2
|
-
remote: http://rubygems.org/
|
3
|
-
specs:
|
4
|
-
activesupport (4.0.0)
|
5
|
-
i18n (~> 0.6, >= 0.6.4)
|
6
|
-
minitest (~> 4.2)
|
7
|
-
multi_json (~> 1.3)
|
8
|
-
thread_safe (~> 0.1)
|
9
|
-
tzinfo (~> 0.3.37)
|
10
|
-
addressable (2.3.5)
|
11
|
-
atomic (1.1.10)
|
12
|
-
capybara (2.1.0)
|
13
|
-
mime-types (>= 1.16)
|
14
|
-
nokogiri (>= 1.3.3)
|
15
|
-
rack (>= 1.0.0)
|
16
|
-
rack-test (>= 0.5.4)
|
17
|
-
xpath (~> 2.0)
|
18
|
-
diff-lcs (1.2.4)
|
19
|
-
eventmachine (1.0.3)
|
20
|
-
faye-websocket (0.4.7)
|
21
|
-
eventmachine (>= 0.12.0)
|
22
|
-
git (1.2.5)
|
23
|
-
http_parser.rb (0.5.3)
|
24
|
-
i18n (0.6.4)
|
25
|
-
jeweler (1.8.4)
|
26
|
-
bundler (~> 1.0)
|
27
|
-
git (>= 1.2.5)
|
28
|
-
rake
|
29
|
-
rdoc
|
30
|
-
json (1.8.0)
|
31
|
-
launchy (2.3.0)
|
32
|
-
addressable (~> 2.3)
|
33
|
-
mime-types (1.23)
|
34
|
-
mini_portile (0.5.1)
|
35
|
-
minitest (4.7.5)
|
36
|
-
multi_json (1.7.7)
|
37
|
-
nokogiri (1.6.0)
|
38
|
-
mini_portile (~> 0.5.0)
|
39
|
-
poltergeist (1.3.0)
|
40
|
-
capybara (~> 2.1.0)
|
41
|
-
faye-websocket (>= 0.4.4, < 0.5.0)
|
42
|
-
http_parser.rb (~> 0.5.3)
|
43
|
-
rack (1.5.2)
|
44
|
-
rack-test (0.6.2)
|
45
|
-
rack (>= 1.0)
|
46
|
-
rake (10.1.0)
|
47
|
-
rdoc (4.0.1)
|
48
|
-
json (~> 1.4)
|
49
|
-
rr (1.1.1)
|
50
|
-
rspec (2.14.1)
|
51
|
-
rspec-core (~> 2.14.0)
|
52
|
-
rspec-expectations (~> 2.14.0)
|
53
|
-
rspec-mocks (~> 2.14.0)
|
54
|
-
rspec-core (2.14.4)
|
55
|
-
rspec-expectations (2.14.0)
|
56
|
-
diff-lcs (>= 1.1.3, < 2.0)
|
57
|
-
rspec-mocks (2.14.2)
|
58
|
-
thread_safe (0.1.2)
|
59
|
-
atomic
|
60
|
-
tzinfo (0.3.37)
|
61
|
-
xpath (2.0.0)
|
62
|
-
nokogiri (~> 1.3)
|
63
|
-
yard (0.8.7)
|
64
|
-
|
65
|
-
PLATFORMS
|
66
|
-
ruby
|
67
|
-
|
68
|
-
DEPENDENCIES
|
69
|
-
activesupport
|
70
|
-
bundler
|
71
|
-
capybara (~> 2.1.0)
|
72
|
-
jeweler
|
73
|
-
launchy
|
74
|
-
poltergeist
|
75
|
-
rdoc
|
76
|
-
rr
|
77
|
-
rspec
|
78
|
-
yard
|
data/NOTES.txt
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
= Rails Thread =
|
2
|
-
Put a proxy class in front of Capybara::Driver::Base and Capybara::Driver::Node
|
3
|
-
Capybara:Session#driver is the instance of driver
|
4
|
-
Probably need to create a new driver which is a proxy for another driver
|
5
|
-
Proxy class puts method_missing args on MQ to be sent to Driver thread
|
6
|
-
Needs to yield to Driver thread and then execute the RackRunner
|
7
|
-
RackRunner reads rack requests from a MQ, calling the rack endpoint with those; returns when done
|
8
|
-
This should probably use the rack-test gem? This probably isn't what we want since this simulates a browser
|
9
|
-
|
10
|
-
= Driver Thread =
|
11
|
-
Reads RPCs off a MQ, calls the commands in the appropriate class, and puts the response on another MQ
|
12
|
-
Part of the new driver passed to Capybara
|
13
|
-
Should be able to use the Capybara driver of your choice
|
14
|
-
How is a Node passed back to the Rails thread? Maybe it's OK for Node queries to go straight through to the webdriver?
|
15
|
-
These could trigger web requests (like a click) that would not be processed... maybe wrap in the Barbecue driver
|
16
|
-
so that the RackRunner is triggered appropriately? That still causes a problem when clicking a link since the
|
17
|
-
webdriver might block... need to think about this some more
|
18
|
-
|
19
|
-
= Capybara::Server =
|
20
|
-
Rewrite this class entirely
|
21
|
-
Should spawn a simple Rack webserver that just take the #call Args and puts them on the MQ
|
22
|
-
Needs to block until MQ response is received
|
23
|
-
Webserver could be Puma or EventMachine
|
24
|
-
|
25
|
-
|
26
|
-
Notes:
|
27
|
-
Queue class provided by 'thread' library
|
28
|
-
|
29
|
-
----- Latest Notes -----
|
30
|
-
* Interface with Session instead
|
31
|
-
* Don't try to mimic interfaces - just ferry calls over
|
32
|
-
* Add field to the base node class so we can tell it's a node
|
33
|
-
* If return value is a Node then wrap it
|
34
|
-
* If return value is enumerable, wrap its Node values via #map
|
35
|
-
|
36
|
-
---- TODO ----
|
37
|
-
* Raise errors during timeouts if Time is frozen
|
data/README.rdoc
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
= capybarbecue
|
2
|
-
|
3
|
-
Description goes here.
|
4
|
-
|
5
|
-
== Contributing to capybarbecue
|
6
|
-
|
7
|
-
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
8
|
-
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
9
|
-
* Fork the project.
|
10
|
-
* Start a feature/bugfix branch.
|
11
|
-
* Commit and push until you are happy with your contribution.
|
12
|
-
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
13
|
-
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
14
|
-
|
15
|
-
== Copyright
|
16
|
-
|
17
|
-
Copyright (c) 2013 Andrew DiMichele. See LICENSE.txt for
|
18
|
-
further details.
|
19
|
-
|
data/Rakefile
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
require 'rubygems'
|
4
|
-
require 'bundler'
|
5
|
-
begin
|
6
|
-
Bundler.setup(:default, :development)
|
7
|
-
rescue Bundler::BundlerError => e
|
8
|
-
$stderr.puts e.message
|
9
|
-
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
-
exit e.status_code
|
11
|
-
end
|
12
|
-
require 'rake'
|
13
|
-
|
14
|
-
require 'jeweler'
|
15
|
-
require './lib/capybarbecue/version'
|
16
|
-
Jeweler::Tasks.new do |gem|
|
17
|
-
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
18
|
-
gem.name = "capybarbecue"
|
19
|
-
gem.homepage = "http://github.com/adimichele/capybarbecue"
|
20
|
-
gem.license = "MIT"
|
21
|
-
gem.summary = %Q{Makes your Capybara test suite work better}
|
22
|
-
gem.description = %Q{Makes fundamental changes to Capybara's threading architecture so you can write stable tests with a shared database connection.}
|
23
|
-
gem.email = "backflip@gmail.com"
|
24
|
-
gem.authors = ["Andrew DiMichele"]
|
25
|
-
gem.version = Capybarbecue::VERSION
|
26
|
-
# dependencies defined in Gemfile
|
27
|
-
end
|
28
|
-
Jeweler::RubygemsDotOrgTasks.new
|
29
|
-
|
30
|
-
require 'rspec/core'
|
31
|
-
require 'rspec/core/rake_task'
|
32
|
-
RSpec::Core::RakeTask.new(:spec) do |spec|
|
33
|
-
spec.pattern = FileList['spec/**/*_spec.rb']
|
34
|
-
end
|
35
|
-
|
36
|
-
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
37
|
-
spec.pattern = 'spec/**/*_spec.rb'
|
38
|
-
spec.rcov = true
|
39
|
-
end
|
40
|
-
|
41
|
-
task :default => :spec
|
42
|
-
|
43
|
-
require 'yard'
|
44
|
-
YARD::Rake::YardocTask.new
|
@@ -1,7 +0,0 @@
|
|
1
|
-
module Capybarbecue
|
2
|
-
class RackRunner
|
3
|
-
# Rack runner portion of main thread
|
4
|
-
# Reads rack requests of a message queue, runs them, and puts the result on another MQ
|
5
|
-
# Should have some sort of delay or signaling system to eliminate race conditions when waiting for requests
|
6
|
-
end
|
7
|
-
end
|