cucumber-wire 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.travis.yml +18 -0
- data/Gemfile +20 -0
- data/README.md +1 -0
- data/Rakefile +13 -0
- data/cucumber-wire.gemspec +28 -0
- data/features/erb_configuration.feature +54 -0
- data/features/handle_unexpected_response.feature +29 -0
- data/features/invoke_message.feature +212 -0
- data/features/readme.md +26 -0
- data/features/snippets_message.feature +47 -0
- data/features/step_definitions/aruba_steps.rb +1 -0
- data/features/step_definitions/wire_steps.rb +58 -0
- data/features/step_matches_message.feature +79 -0
- data/features/support/fake_wire_server.rb +80 -0
- data/features/table_diffing.feature +124 -0
- data/features/tags.feature +86 -0
- data/features/timeouts.feature +63 -0
- data/lib/cucumber/wire.rb +5 -0
- data/lib/cucumber/wire/add_hooks_filter.rb +25 -0
- data/lib/cucumber/wire/configuration.rb +38 -0
- data/lib/cucumber/wire/connection.rb +63 -0
- data/lib/cucumber/wire/connections.rb +50 -0
- data/lib/cucumber/wire/data_packet.rb +34 -0
- data/lib/cucumber/wire/exception.rb +32 -0
- data/lib/cucumber/wire/plugin.rb +32 -0
- data/lib/cucumber/wire/protocol.rb +43 -0
- data/lib/cucumber/wire/protocol/requests.rb +128 -0
- data/lib/cucumber/wire/request_handler.rb +32 -0
- data/lib/cucumber/wire/snippet.rb +35 -0
- data/lib/cucumber/wire/step_definition.rb +21 -0
- data/lib/cucumber/wire/version +1 -0
- data/spec/cucumber/wire/configuration_spec.rb +63 -0
- data/spec/cucumber/wire/connection_spec.rb +64 -0
- data/spec/cucumber/wire/connections_spec.rb +23 -0
- data/spec/cucumber/wire/data_packet_spec.rb +43 -0
- data/spec/cucumber/wire/exception_spec.rb +50 -0
- metadata +157 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module Cucumber
|
2
|
+
module Wire
|
3
|
+
class AddHooksFilter < Core::Filter.new(:connections)
|
4
|
+
def test_case(test_case)
|
5
|
+
test_case.
|
6
|
+
with_steps([before_hook(test_case)] + test_case.test_steps + [after_hook(test_case)]).
|
7
|
+
describe_to receiver
|
8
|
+
end
|
9
|
+
|
10
|
+
def before_hook(test_case)
|
11
|
+
# TODO: is this dependency on Cucumber::Hooks OK? Feels a bit internal..
|
12
|
+
# TODO: how do we express the location of the hook? Should we create one hook per connection so we can use the host:port of the connection?
|
13
|
+
Cucumber::Hooks.before_hook(test_case.source, Core::Ast::Location.new('TODO:wire')) do
|
14
|
+
connections.begin_scenario(test_case)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def after_hook(test_case)
|
19
|
+
Cucumber::Hooks.after_hook(test_case.source, Core::Ast::Location.new('TODO:wire')) do
|
20
|
+
connections.end_scenario(test_case)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
module Cucumber
|
5
|
+
module Wire
|
6
|
+
class Configuration
|
7
|
+
attr_reader :host, :port, :unix
|
8
|
+
|
9
|
+
def self.from_file(wire_file)
|
10
|
+
settings = YAML.load(ERB.new(File.read(wire_file)).result)
|
11
|
+
new(settings)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(args)
|
15
|
+
@host = args['host']
|
16
|
+
@port = args['port']
|
17
|
+
@unix = args['unix'] if RUBY_PLATFORM !~ /mingw|mswin/
|
18
|
+
@timeouts = DEFAULT_TIMEOUTS.merge(args['timeout'] || {})
|
19
|
+
end
|
20
|
+
|
21
|
+
def timeout(message = nil)
|
22
|
+
return @timeouts[message.to_s] || 3
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
return @unix if @unix
|
27
|
+
"#{@host}:#{@port}"
|
28
|
+
end
|
29
|
+
|
30
|
+
DEFAULT_TIMEOUTS = {
|
31
|
+
'connect' => 11,
|
32
|
+
'invoke' => 120,
|
33
|
+
'begin_scenario' => 120,
|
34
|
+
'end_scenario' => 120
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'cucumber/wire/protocol'
|
3
|
+
require 'cucumber/wire/exception'
|
4
|
+
require 'cucumber/wire/data_packet'
|
5
|
+
|
6
|
+
module Cucumber
|
7
|
+
module Wire
|
8
|
+
class Connection
|
9
|
+
class ConnectionError < StandardError; end
|
10
|
+
|
11
|
+
include Wire::Protocol
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def call_remote(request_handler, message, params)
|
18
|
+
packet = DataPacket.new(message, params)
|
19
|
+
|
20
|
+
begin
|
21
|
+
send_data_to_socket(packet.to_json)
|
22
|
+
response = fetch_data_from_socket(@config.timeout(message))
|
23
|
+
response.handle_with(request_handler)
|
24
|
+
rescue Timeout::Error => e
|
25
|
+
backtrace = e.backtrace ; backtrace.shift # because Timeout puts some wierd stuff in there
|
26
|
+
raise Timeout::Error, "Timed out calling wire server with message '#{message}'", backtrace
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def exception(params)
|
31
|
+
Wire::Exception.new(params, @config)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def send_data_to_socket(data)
|
37
|
+
Timeout.timeout(@config.timeout('connect')) { socket.puts(data) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch_data_from_socket(timeout)
|
41
|
+
raw_response =
|
42
|
+
if timeout == :never
|
43
|
+
socket.gets
|
44
|
+
else
|
45
|
+
Timeout.timeout(timeout) { socket.gets }
|
46
|
+
end
|
47
|
+
raise exception({'message' => "Remote Socket with #{@config.host}:#{@config.port} closed."}) if raw_response.nil?
|
48
|
+
DataPacket.parse(raw_response)
|
49
|
+
end
|
50
|
+
|
51
|
+
def socket
|
52
|
+
return @socket if @socket
|
53
|
+
if @config.unix
|
54
|
+
@socket = UNIXSocket.new(@config.unix)
|
55
|
+
else
|
56
|
+
@socket = TCPSocket.new(@config.host, @config.port)
|
57
|
+
end
|
58
|
+
rescue Errno::ECONNREFUSED => exception
|
59
|
+
raise(ConnectionError, "Unable to contact the wire server at #{@config}. Is it up?")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
require 'socket'
|
3
|
+
require 'cucumber/wire/connection'
|
4
|
+
require 'cucumber/wire/configuration'
|
5
|
+
require 'cucumber/wire/data_packet'
|
6
|
+
require 'cucumber/wire/exception'
|
7
|
+
require 'cucumber/wire/step_definition'
|
8
|
+
require 'cucumber/wire/snippet'
|
9
|
+
require 'cucumber/configuration'
|
10
|
+
require 'cucumber/step_match'
|
11
|
+
|
12
|
+
module Cucumber
|
13
|
+
module Wire
|
14
|
+
|
15
|
+
class Connections
|
16
|
+
attr_reader :connections
|
17
|
+
private :connections
|
18
|
+
|
19
|
+
def initialize(connections, configuration)
|
20
|
+
raise ArgumentError unless connections
|
21
|
+
@connections = connections
|
22
|
+
@configuration = configuration
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_match(test_step)
|
26
|
+
matches = step_matches(test_step.name)
|
27
|
+
return unless matches.any?
|
28
|
+
# TODO: handle ambiguous matches (push to cucumber?)
|
29
|
+
matches.first
|
30
|
+
end
|
31
|
+
|
32
|
+
def step_matches(step_name)
|
33
|
+
connections.map{ |c| c.step_matches(step_name)}.flatten
|
34
|
+
end
|
35
|
+
|
36
|
+
def begin_scenario(test_case)
|
37
|
+
connections.each { |c| c.begin_scenario(test_case) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def end_scenario(test_case)
|
41
|
+
connections.each { |c| c.end_scenario(test_case) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def snippets(code_keyword, step_name, multiline_arg_class_name)
|
45
|
+
connections.map { |c| c.snippet_text(code_keyword, step_name, multiline_arg_class_name) }.flatten
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module Cucumber
|
4
|
+
module Wire
|
5
|
+
# Represents the packet of data sent over the wire as JSON data, containing
|
6
|
+
# a message and a hash of arguments
|
7
|
+
class DataPacket
|
8
|
+
class << self
|
9
|
+
def parse(raw)
|
10
|
+
attributes = MultiJson.load(raw.strip)
|
11
|
+
message = attributes[0]
|
12
|
+
params = attributes[1]
|
13
|
+
new(message, params)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :message, :params
|
18
|
+
|
19
|
+
def initialize(message, params = nil)
|
20
|
+
@message, @params = message, params
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_json
|
24
|
+
packet = [@message]
|
25
|
+
packet << @params if @params
|
26
|
+
MultiJson.dump(packet)
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle_with(handler)
|
30
|
+
handler.send("handle_#{@message}", @params)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Cucumber
|
2
|
+
module Wire
|
3
|
+
# Proxy for an exception that occured at the remote end of the wire
|
4
|
+
class Exception < StandardError
|
5
|
+
module CanSetName
|
6
|
+
attr_writer :exception_name
|
7
|
+
def to_s
|
8
|
+
@exception_name
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(args, config)
|
13
|
+
super args['message']
|
14
|
+
if args['exception']
|
15
|
+
self.class.extend(CanSetName)
|
16
|
+
self.class.exception_name = "#{args['exception']} from #{config}"
|
17
|
+
end
|
18
|
+
if args['backtrace']
|
19
|
+
@backtrace = if args['backtrace'].is_a?(String)
|
20
|
+
args['backtrace'].split("\n") # TODO: change cuke4nuke to pass an array instead of a big string
|
21
|
+
else
|
22
|
+
args['backtrace']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def backtrace
|
28
|
+
@backtrace || super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'cucumber/wire/connections'
|
2
|
+
require 'cucumber/wire/add_hooks_filter'
|
3
|
+
require 'cucumber/step_match_search'
|
4
|
+
|
5
|
+
module Cucumber
|
6
|
+
module Wire
|
7
|
+
class Plugin
|
8
|
+
attr_reader :config
|
9
|
+
private :config
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def install
|
16
|
+
connections = Connections.new(wire_files.map { |f| create_connection(f) }, @config)
|
17
|
+
config.filters << Filters::ActivateSteps.new(StepMatchSearch.new(connections.method(:step_matches), @config), @config)
|
18
|
+
config.filters << AddHooksFilter.new(connections) unless @config.dry_run?
|
19
|
+
config.register_snippet_generator Snippet::Generator.new(connections)
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_connection(wire_file)
|
23
|
+
Connection.new(Configuration.from_file(wire_file))
|
24
|
+
end
|
25
|
+
|
26
|
+
def wire_files
|
27
|
+
# TODO: change Cucumber's config object to allow us to get this information
|
28
|
+
config.send(:require_dirs).map { |dir| Dir.glob("#{dir}/**/*.wire") }.flatten
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'cucumber/wire/protocol/requests'
|
2
|
+
|
3
|
+
module Cucumber
|
4
|
+
module Wire
|
5
|
+
module Protocol
|
6
|
+
def step_matches(name_to_match)
|
7
|
+
handler = Requests::StepMatches.new(self)
|
8
|
+
handler.execute(name_to_match)
|
9
|
+
end
|
10
|
+
|
11
|
+
def snippet_text(step_keyword, step_name, multiline_arg_class_name)
|
12
|
+
handler = Requests::SnippetText.new(self)
|
13
|
+
handler.execute(step_keyword, step_name, multiline_arg_class_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def invoke(step_definition_id, args)
|
17
|
+
handler = Requests::Invoke.new(self)
|
18
|
+
handler.execute(step_definition_id, args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def diff_failed
|
22
|
+
handler = Requests::DiffFailed.new(self)
|
23
|
+
handler.execute
|
24
|
+
end
|
25
|
+
|
26
|
+
def diff_ok
|
27
|
+
handler = Requests::DiffOk.new(self)
|
28
|
+
handler.execute
|
29
|
+
end
|
30
|
+
|
31
|
+
def begin_scenario(scenario)
|
32
|
+
handler = Requests::BeginScenario.new(self)
|
33
|
+
handler.execute(scenario)
|
34
|
+
end
|
35
|
+
|
36
|
+
def end_scenario(scenario)
|
37
|
+
handler = Requests::EndScenario.new(self)
|
38
|
+
handler.execute(scenario)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'cucumber/wire/request_handler'
|
2
|
+
require 'cucumber/step_argument'
|
3
|
+
|
4
|
+
module Cucumber
|
5
|
+
module Wire
|
6
|
+
module Protocol
|
7
|
+
module Requests
|
8
|
+
class StepMatches < RequestHandler
|
9
|
+
def execute(name_to_match)
|
10
|
+
@name_to_match = name_to_match
|
11
|
+
request_params = {
|
12
|
+
:name_to_match => name_to_match
|
13
|
+
}
|
14
|
+
super(request_params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle_success(params)
|
18
|
+
params.map do |raw_step_match|
|
19
|
+
create_step_match(raw_step_match)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
alias :handle_step_matches :handle_success
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def create_step_match(raw_step_match)
|
28
|
+
step_definition = StepDefinition.new(@connection, raw_step_match)
|
29
|
+
step_args = raw_step_match['args'].map do |raw_arg|
|
30
|
+
StepArgument.new(raw_arg['pos'], raw_arg['val'])
|
31
|
+
end
|
32
|
+
step_match(step_definition, step_args)
|
33
|
+
end
|
34
|
+
|
35
|
+
def step_match(step_definition, step_args)
|
36
|
+
StepMatch.new(step_definition, @name_to_match, step_args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class SnippetText < RequestHandler
|
41
|
+
def execute(step_keyword, step_name, multiline_arg_class_name)
|
42
|
+
request_params = {
|
43
|
+
:step_keyword => step_keyword,
|
44
|
+
:step_name => step_name,
|
45
|
+
:multiline_arg_class => multiline_arg_class_name
|
46
|
+
}
|
47
|
+
super(request_params)
|
48
|
+
end
|
49
|
+
|
50
|
+
def handle_success(snippet_text)
|
51
|
+
snippet_text
|
52
|
+
end
|
53
|
+
|
54
|
+
alias :handle_snippet_text :handle_success
|
55
|
+
end
|
56
|
+
|
57
|
+
class Invoke < RequestHandler
|
58
|
+
def execute(step_definition_id, args)
|
59
|
+
request_params = {
|
60
|
+
:id => step_definition_id,
|
61
|
+
:args => args
|
62
|
+
}
|
63
|
+
super(request_params)
|
64
|
+
end
|
65
|
+
|
66
|
+
def handle_pending(message)
|
67
|
+
raise Pending, message || "TODO"
|
68
|
+
end
|
69
|
+
|
70
|
+
def handle_diff!(tables)
|
71
|
+
# TODO: figure out if / how we could get a location for a table from the wire (or make a null location)
|
72
|
+
location = Core::Ast::Location.new(__FILE__, __LINE__)
|
73
|
+
table1 = table(tables[0], location)
|
74
|
+
table2 = table(tables[1], location)
|
75
|
+
table1.diff!(table2)
|
76
|
+
end
|
77
|
+
|
78
|
+
def handle_diff(tables)
|
79
|
+
begin
|
80
|
+
handle_diff!(tables)
|
81
|
+
rescue Cucumber::MultilineArgument::DataTable::Different
|
82
|
+
@connection.diff_failed
|
83
|
+
end
|
84
|
+
@connection.diff_ok
|
85
|
+
end
|
86
|
+
|
87
|
+
alias :handle_step_failed :handle_fail
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def table(data, location)
|
92
|
+
Cucumber::MultilineArgument.from_core(Core::Ast::DataTable.new(data, location))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class DiffFailed < RequestHandler
|
97
|
+
alias :handle_step_failed :handle_fail
|
98
|
+
end
|
99
|
+
|
100
|
+
class DiffOk < RequestHandler
|
101
|
+
alias :handle_step_failed :handle_fail
|
102
|
+
end
|
103
|
+
|
104
|
+
class HookRequestHandler < RequestHandler
|
105
|
+
def execute(test_case)
|
106
|
+
super(request_params(test_case))
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def request_params(test_case)
|
112
|
+
return nil unless test_case.tags.any?
|
113
|
+
{ "tags" => clean_tag_names(test_case.tags) }
|
114
|
+
end
|
115
|
+
|
116
|
+
def clean_tag_names(tags)
|
117
|
+
tags.map { |tag| tag.name.gsub(/^@/, '') }.sort
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
BeginScenario = Class.new(HookRequestHandler)
|
122
|
+
|
123
|
+
EndScenario = Class.new(HookRequestHandler)
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|