happybara 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f07111257fd0128b45efe622094b7cbadb1904e2
4
+ data.tar.gz: d9dd81e7fe06f279ae1590f17ae5513ba686e0c7
5
+ SHA512:
6
+ metadata.gz: ceae202a3842453f47586a01e7a5818e26f3b96e7da9a226d162edee0153f8a6ef7a6eab40c4216fc8c20dafbc1d4edfeb90e52f2ca47ee0c75d712cfdac8db1
7
+ data.tar.gz: 84b24d10b0b2af0a9a4f1583b99d4cefe71e7ff17067c6464078487266f376994a97d8fccc76479e0653ced15288b68335d0593b0f1ff9d9199d0856d8587c2f
data/lib/happybara.rb ADDED
@@ -0,0 +1,9 @@
1
+ require_relative './happybara/agent'
2
+ require_relative './happybara/callbacks'
3
+ require_relative './happybara/executor'
4
+ require_relative './happybara/serializer'
5
+ require_relative './happybara/synchronizer'
6
+ require_relative './happybara/version'
7
+
8
+ module Happybara
9
+ end
@@ -0,0 +1,117 @@
1
+ require 'colorize'
2
+ require 'singleton'
3
+ require_relative './callbacks'
4
+ require_relative './serializer'
5
+ require_relative './synchronizer'
6
+ require_relative './registry'
7
+
8
+ module Happybara
9
+ class Agent
10
+ include Callbacks
11
+
12
+ attr_accessor :grace_timeout
13
+
14
+ # Create a new Agent instance and configure it.
15
+ #
16
+ # @yield [Happybara::Agent] instance
17
+ # The new agent instance. The block will evaluated in this instance's
18
+ # scope so you can call methods like #on and #before() globally.
19
+ #
20
+ # @return [Happybara::Agent]
21
+ def self.create(&block)
22
+ Happybara::Agent.new.tap do |instance|
23
+ instance.instance_exec do
24
+ block[self]
25
+ end
26
+ end
27
+ end
28
+
29
+ def initialize(serializer: Serializer.new)
30
+ @serializer = serializer
31
+ @reflections = {}
32
+ @mutex = Synchronizer.new({
33
+ on_abrupt_termination: ->() {
34
+ run_callbacks(:abrupt_termination)
35
+ run_after_example
36
+ },
37
+
38
+ on_forced_release: ->() {
39
+ run_callbacks(:forced_release)
40
+ run_after_example
41
+ },
42
+
43
+ on_race_condition: ->() {
44
+ run_callbacks(:race_condition)
45
+ }
46
+ })
47
+ end
48
+
49
+ # Bind the Agent to a websocket to start a testing session.
50
+ #
51
+ # @param [Websocket] socket
52
+ # @param [Object] actor
53
+ # An instance of an object that will be receiving the RPC calls. Any
54
+ # `eval` RPCs will be evaluated in this object's context. If you're
55
+ # using Tubesock and hijacking a Rails controller, you can pass the
56
+ # controller itself as an actor, then you also have access to
57
+ # ApplicationController methods and helpers as RPCs.
58
+ #
59
+ # @return [NilClass]
60
+ def bind(socket, actor)
61
+ @mutex.lock(socket: socket, timeout: grace_timeout) do
62
+ executor = Executor.new(actor: actor, socket: socket, serializer: @serializer)
63
+ executor.run_before_example = method(:run_before_example)
64
+ executor.run_after_example = method(:run_after_example)
65
+ executor.run_around_command = method(:run_around_command)
66
+
67
+ socket.onopen do
68
+ puts "Happybara: client connected.".green
69
+ end
70
+
71
+ socket.onmessage do |payload|
72
+ executor.handle_message(payload)
73
+ end
74
+
75
+ socket.onclose do
76
+ puts "Happybara: client disconnected.".green
77
+ end
78
+ end
79
+
80
+ nil
81
+ end
82
+
83
+ def on_forced_release(&callback)
84
+ on(:forced_release, &callback)
85
+ end
86
+
87
+ def on_abrupt_termination(&callback)
88
+ on(:abrupt_termination, &callback)
89
+ end
90
+
91
+ def on_race_condition(&callback)
92
+ on(:race_condition, &callback)
93
+ end
94
+
95
+ private
96
+
97
+ def run_before_example
98
+ run_callbacks(:before_each)
99
+ end
100
+
101
+ def run_after_example
102
+ run_callbacks(:after_each)
103
+ @mutex.release
104
+ end
105
+
106
+ def run_around_command(message, &block)
107
+ # TODO: support nested procs.. ?
108
+ callback = callbacks[:around_command] && callbacks[:around_command].first
109
+
110
+ if callback
111
+ callback.call(message, block)
112
+ else
113
+ block.call
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,49 @@
1
+ module Happybara
2
+ module Callbacks
3
+ def before(stage, &callback)
4
+ case stage.to_sym
5
+ when :each
6
+ on(:before_each, &callback)
7
+ when :all
8
+ Rails.configuration.after_initialize(&callback)
9
+ else
10
+ fail "Unknown :before stage '#{stage}'"
11
+ end
12
+ end
13
+
14
+ def after(stage, &callback)
15
+ case stage.to_sym
16
+ when :each
17
+ on(:after_each, &callback)
18
+ else
19
+ fail "Unknown :after stage '#{stage}'"
20
+ end
21
+ end
22
+
23
+ def around(stage, &callback)
24
+ case stage.to_sym
25
+ when :command
26
+ on(:around_command, &callback)
27
+ else
28
+ fail "Unknown :around stage '#{stage}'"
29
+ end
30
+ end
31
+
32
+ def on(event, &handler)
33
+ callbacks[event.to_sym] ||= []
34
+ callbacks[event.to_sym] << handler
35
+ end
36
+
37
+ protected
38
+
39
+ def callbacks
40
+ @callbacks ||= {}
41
+ end
42
+
43
+ def run_callbacks(event)
44
+ callbacks.fetch(event).each do |callback|
45
+ callback.call
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,133 @@
1
+ module Happybara
2
+ class Executor
3
+ attr_accessor :run_before_example, :run_after_example, :run_around_command
4
+
5
+ def initialize(actor:, socket:, serializer:)
6
+ @actor = actor
7
+ @socket = socket
8
+ @serializer = serializer
9
+ @registry = Registry.new
10
+ end
11
+
12
+ def handle_message(payload)
13
+ message = @serializer.deserialize payload
14
+ respond = method(:respond_to_message).curry[message['id']]
15
+
16
+ case message['type']
17
+ when 'SETUP'
18
+ handle_setup(message, &respond)
19
+ when 'RPC'
20
+ handle_rpc(message, &respond)
21
+ when 'EVAL'
22
+ handle_eval(message, &respond)
23
+ when 'QUERY'
24
+ handle_query(message, &respond)
25
+ when 'REFLECT'
26
+ handle_reflect(message, &respond)
27
+ when 'TEARDOWN'
28
+ handle_teardown(message, &respond)
29
+ else
30
+ fail "unknown message type #{message['type']}"
31
+ end
32
+ rescue StandardError => e
33
+ puts "#{'=' * 80}\nHandler error!\n#{'-' * 80}"
34
+ puts e.message
35
+ puts e.backtrace
36
+
37
+ respond['error', { details: e.message, backtrace: e.backtrace }]
38
+ end
39
+
40
+ private
41
+
42
+ def respond_to_message(message_id, type, data)
43
+ @socket.send_data({
44
+ id: message_id,
45
+ type: type,
46
+ data: data
47
+ }.to_json)
48
+ end
49
+
50
+ def handle_setup(message, &respond)
51
+ run_before_example[]
52
+
53
+ respond['SETUP_RESPONSE', @serializer.serialize_value(true)]
54
+ end
55
+
56
+ def handle_query(message, &respond)
57
+ @registry.open!(message['data']['klass_name'])
58
+
59
+ klass = message['data']['klass_name'].constantize
60
+ klass.public_instance_methods(true)
61
+ .sort
62
+ .map(&:to_s)
63
+ .reject { |s| s[0] =~ /[\W|_]/ }
64
+ .tap do |visible_instance_methods|
65
+ respond['QUERY_RESPONSE', @serializer.serialize({
66
+ instance_methods: visible_instance_methods
67
+ })]
68
+ end
69
+ end
70
+
71
+ def handle_rpc(message, &respond)
72
+ method_name = message['data']['procedure']
73
+ method_args = message['data']['payload'] || {}
74
+ method_opts = message['data']['options'] || {}
75
+
76
+ fail "Unknown RPC #{method_name}" unless @actor.respond_to?(method_name)
77
+
78
+ result = around_command(message) do
79
+ @actor.send(message['data']['procedure'], **method_args.symbolize_keys)
80
+ end
81
+
82
+ @registry << result
83
+
84
+ respond['RPC_RESPONSE', @serializer.serialize_value(result)]
85
+ end
86
+
87
+ def handle_eval(message, &respond)
88
+ string = message['data']['string']
89
+ options = message['data']['options'] || {}
90
+
91
+ around_command(message) do
92
+ result = @actor.instance_eval { eval(string) }
93
+ @registry << result
94
+ respond['EVAL_RESPONSE', @serializer.serialize_value(result)]
95
+ end
96
+ end
97
+
98
+ def handle_reflect(message, &respond)
99
+ object_id = message['data']['object_id']
100
+ klass_name = message['data']['klass_name']
101
+ method_name = message['data']['method']
102
+ method_args = message['data']['arguments']
103
+ method_opts = message['data']['options'] || {}
104
+
105
+ ref = resolve_reference(message['data'])
106
+
107
+ fail "Object #{object_id} of type #{klass_name} could not be reflected" if ref.nil?
108
+
109
+ rc = around_command(message) do
110
+ ref.send(method_name, *method_args)
111
+ end
112
+
113
+ @registry << rc
114
+
115
+ respond['REFLECT_RESPONSE', @serializer.serialize_value(rc)]
116
+ end
117
+
118
+ def handle_teardown(message, &respond)
119
+ @registry.release!
120
+ run_after_example[]
121
+
122
+ respond['TEARDOWN_RESPONSE', @serializer.serialize_value(true)]
123
+ end
124
+
125
+ def around_command(message, &block)
126
+ run_around_command[message['data'], &block]
127
+ end
128
+
129
+ def resolve_reference(descriptor)
130
+ @registry[descriptor['object_id']]
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,29 @@
1
+ module Happybara
2
+ class Registry
3
+ def initialize()
4
+ @reflections = {}
5
+ end
6
+
7
+ def open!(klass_name)
8
+ @reflections[klass_name.to_s] = {}
9
+ end
10
+
11
+ def <<(object)
12
+ self.tap do
13
+ klass_name = object.class.name.to_s
14
+
15
+ if @reflections[klass_name]
16
+ @reflections[klass_name][object.object_id] = object
17
+ end
18
+ end
19
+ end
20
+
21
+ def [](object_id)
22
+ ObjectSpace._id2ref(object_id) || nil
23
+ end
24
+
25
+ def release!()
26
+ @reflections.clear
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ module Happybara
2
+ class Serializer
3
+ def serialize(result)
4
+ result.as_json
5
+ end
6
+
7
+ def serialize_value(result)
8
+ {
9
+ klass: result.class.name,
10
+ object_id: result.object_id,
11
+ value: result.as_json
12
+ }
13
+ end
14
+
15
+ def serialize_reference(ref)
16
+ {
17
+ klass: ref.class.name,
18
+ object_id: ref.object_id,
19
+ }
20
+ end
21
+
22
+ def deserialize(payload)
23
+ JSON.load(payload, ->(datum) {
24
+ case datum
25
+ when Hash
26
+ datum.each_pair do |key, value|
27
+ if value.is_a?(Hash) && value['$$ref'] == true && value['$$object_id']
28
+ datum[key] = ObjectSpace._id2ref(value['$$object_id']) || nil
29
+ end
30
+ end
31
+
32
+ datum
33
+ else
34
+ datum
35
+ end
36
+ })
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,94 @@
1
+ module Happybara
2
+ class Synchronizer
3
+ def initialize(
4
+ on_forced_release:,
5
+ on_abrupt_termination:,
6
+ on_race_condition: nil
7
+ )
8
+ @on_forced_release = on_forced_release || ->() {}
9
+ @on_abrupt_termination = on_abrupt_termination || ->() {}
10
+ @on_race_condition = on_race_condition || ->() {}
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def lock(socket:, timeout: 5.0, frequency: 0.1, &block)
15
+ socket.onopen do
16
+ acquire!(timeout: timeout, frequency: frequency, &block)
17
+ end
18
+
19
+ socket.onclose do
20
+ @on_abrupt_termination[] if @mutex.owned? && @mutex.locked?
21
+ end
22
+ end
23
+
24
+ def release
25
+ @mutex.unlock
26
+ end
27
+
28
+ private
29
+
30
+ def acquire!(timeout:, frequency:, &block)
31
+ if @mutex.locked? && !@mutex.owned?
32
+ fail "
33
+ The Happybara agent is being accessed by multiple threads. This
34
+ mode of operation is not currently supported.
35
+
36
+ (This most likely indicates that you have multiple client runners
37
+ communicating with this Rails server concurrently.)
38
+ "
39
+ elsif @mutex.locked?
40
+ @on_race_condition[]
41
+
42
+ begin
43
+ Timer.new(frequency: frequency, timeout: timeout)
44
+ .until { vacant? }
45
+ .otherwise { @on_forced_release[] }
46
+ .start!
47
+ rescue TimeoutError
48
+ @on_forced_release[]
49
+ end
50
+ end
51
+
52
+ @mutex.lock
53
+
54
+ yield
55
+ end
56
+
57
+ def vacant?
58
+ !@mutex.locked?
59
+ end
60
+
61
+ class Timer
62
+ def initialize(frequency:, timeout:)
63
+ @frequency = frequency
64
+ @timeout = timeout
65
+ @until = nil
66
+ @otherwise = nil
67
+ end
68
+
69
+ def until(&block)
70
+ @until = block
71
+ end
72
+
73
+ def otherwise(&block)
74
+ @otherwise = block
75
+ end
76
+
77
+ def start!
78
+ elapsed = 0.0
79
+
80
+ while !@until[]
81
+ elapsed += frequency
82
+
83
+ if elapsed < timeout
84
+ sleep frequency
85
+ elsif @otherwise
86
+ @otherwise[]
87
+ else
88
+ raise TimeoutError.new
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,3 @@
1
+ module Happybara
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'fileutils'
2
+ require 'spec_helper'
3
+
4
+ describe Happybara::Agent do
5
+ it 'is constructible' do
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ require 'fileutils'
2
+ require 'spec_helper'
3
+
4
+ describe Happybara::Serializer do
5
+ describe '#deserialize' do
6
+ it 'resolves references' do
7
+ some_object = Object.new
8
+
9
+ result = subject.deserialize({
10
+ foo: {
11
+ '$$ref': true,
12
+ '$$object_id': some_object.object_id
13
+ }
14
+ }.to_json)
15
+
16
+ expect(result['foo']).to be(some_object)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ ENV['RAILS_ENV'] ||= "test"
2
+
3
+ require 'pry'
4
+ require_relative '../lib/happybara'
5
+ require_relative './support/bootstrap'
6
+
7
+ RSpec.configure do |config|
8
+ config.expect_with :rspec do |expectations|
9
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
10
+ end
11
+
12
+ config.mock_with :rspec do |mocks|
13
+ mocks.verify_partial_doubles = true
14
+ end
15
+
16
+ config.shared_context_metadata_behavior = :apply_to_host_groups
17
+ end
@@ -0,0 +1,38 @@
1
+ # This dance is necessary to subvert railtie from resolving get_smart's Rails
2
+ # application as the "Rails.application"; we really only need a dummy
3
+ # application that can host some routes, and if this gem was sourced elsewhere
4
+ # we could just define such an application directly but since it's sourced
5
+ # within get_smart, railtie will resolve the root `config.ru` and cause sadness.
6
+ #
7
+ # Be happy, unless you're upgrading Rails and this gets broken. :-)
8
+
9
+ require 'rails/application'
10
+ require 'rails/configuration'
11
+ require 'rails/railtie'
12
+ require 'active_support'
13
+
14
+ module Rails
15
+ class << self
16
+ attr_writer :app_class, :application
17
+ end
18
+ end
19
+
20
+ module Happybara
21
+ class TestApplication < ::Rails::Application
22
+ class << self
23
+ def find_root(*)
24
+ Pathname.new File.realpath File.expand_path(File.dirname(__FILE__))
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ require 'rails'
31
+ require 'action_view'
32
+ require 'action_controller'
33
+
34
+ Happybara::TestApplication.configure do
35
+ config.eager_load = ENV['COVERAGE'] != '1'
36
+ end
37
+
38
+ Happybara::TestApplication.initialize!
File without changes
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: happybara
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ahmad Amireh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '6'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '6'
33
+ - !ruby/object:Gem::Dependency
34
+ name: rspec
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.5'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.5'
47
+ description: |
48
+ Happybara is an integration testing platform providing a webserver in Ruby that
49
+ accepts connections over a websocket where test clients can dispatch RPCs to
50
+ Ruby from a different language like JavaScript.
51
+ email:
52
+ - ahmad@amireh.net
53
+ executables: []
54
+ extensions: []
55
+ extra_rdoc_files: []
56
+ files:
57
+ - lib/happybara.rb
58
+ - lib/happybara/agent.rb
59
+ - lib/happybara/callbacks.rb
60
+ - lib/happybara/executor.rb
61
+ - lib/happybara/registry.rb
62
+ - lib/happybara/serializer.rb
63
+ - lib/happybara/synchronizer.rb
64
+ - lib/happybara/version.rb
65
+ - spec/lib/happybara/agent_spec.rb
66
+ - spec/lib/happybara/serializer_spec.rb
67
+ - spec/spec_helper.rb
68
+ - spec/support/bootstrap.rb
69
+ - spec/support/log/test.log
70
+ homepage: https://github.com/amireh/happybara
71
+ licenses:
72
+ - BSD-3
73
+ metadata: {}
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 2.4.5.1
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: An integration test server using Websockets.
94
+ test_files:
95
+ - spec/lib/happybara/agent_spec.rb
96
+ - spec/lib/happybara/serializer_spec.rb
97
+ - spec/spec_helper.rb
98
+ - spec/support/bootstrap.rb
99
+ - spec/support/log/test.log
100
+ has_rdoc: