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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +18 -0
  4. data/Gemfile +20 -0
  5. data/README.md +1 -0
  6. data/Rakefile +13 -0
  7. data/cucumber-wire.gemspec +28 -0
  8. data/features/erb_configuration.feature +54 -0
  9. data/features/handle_unexpected_response.feature +29 -0
  10. data/features/invoke_message.feature +212 -0
  11. data/features/readme.md +26 -0
  12. data/features/snippets_message.feature +47 -0
  13. data/features/step_definitions/aruba_steps.rb +1 -0
  14. data/features/step_definitions/wire_steps.rb +58 -0
  15. data/features/step_matches_message.feature +79 -0
  16. data/features/support/fake_wire_server.rb +80 -0
  17. data/features/table_diffing.feature +124 -0
  18. data/features/tags.feature +86 -0
  19. data/features/timeouts.feature +63 -0
  20. data/lib/cucumber/wire.rb +5 -0
  21. data/lib/cucumber/wire/add_hooks_filter.rb +25 -0
  22. data/lib/cucumber/wire/configuration.rb +38 -0
  23. data/lib/cucumber/wire/connection.rb +63 -0
  24. data/lib/cucumber/wire/connections.rb +50 -0
  25. data/lib/cucumber/wire/data_packet.rb +34 -0
  26. data/lib/cucumber/wire/exception.rb +32 -0
  27. data/lib/cucumber/wire/plugin.rb +32 -0
  28. data/lib/cucumber/wire/protocol.rb +43 -0
  29. data/lib/cucumber/wire/protocol/requests.rb +128 -0
  30. data/lib/cucumber/wire/request_handler.rb +32 -0
  31. data/lib/cucumber/wire/snippet.rb +35 -0
  32. data/lib/cucumber/wire/step_definition.rb +21 -0
  33. data/lib/cucumber/wire/version +1 -0
  34. data/spec/cucumber/wire/configuration_spec.rb +63 -0
  35. data/spec/cucumber/wire/connection_spec.rb +64 -0
  36. data/spec/cucumber/wire/connections_spec.rb +23 -0
  37. data/spec/cucumber/wire/data_packet_spec.rb +43 -0
  38. data/spec/cucumber/wire/exception_spec.rb +50 -0
  39. metadata +157 -0
@@ -0,0 +1,5 @@
1
+ require 'cucumber/wire/plugin'
2
+
3
+ AfterConfiguration do |config|
4
+ Cucumber::Wire::Plugin.new(config).install
5
+ end
@@ -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