cucumber-wire 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|