hypercuke 0.4.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/.gitignore +23 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +142 -0
- data/Rakefile +11 -0
- data/bin/hcu +19 -0
- data/hypercuke.gemspec +28 -0
- data/lib/hypercuke.rb +43 -0
- data/lib/hypercuke/adapter_definition.rb +29 -0
- data/lib/hypercuke/cli.rb +53 -0
- data/lib/hypercuke/cli/builder.rb +68 -0
- data/lib/hypercuke/cli/parser.rb +76 -0
- data/lib/hypercuke/config.rb +20 -0
- data/lib/hypercuke/context.rb +36 -0
- data/lib/hypercuke/cucumber_integration.rb +15 -0
- data/lib/hypercuke/exceptions.rb +42 -0
- data/lib/hypercuke/mini_inflector.rb +9 -0
- data/lib/hypercuke/name_list.rb +38 -0
- data/lib/hypercuke/step_adapter.rb +12 -0
- data/lib/hypercuke/step_adapters.rb +126 -0
- data/lib/hypercuke/step_driver.rb +52 -0
- data/lib/hypercuke/version.rb +3 -0
- data/spec/cli_spec.rb +135 -0
- data/spec/context_spec.rb +46 -0
- data/spec/hypercuke_spec.rb +29 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/step_adapter_definition_spec.rb +131 -0
- data/spec/step_driver_spec.rb +99 -0
- metadata +154 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
module Hypercuke
|
2
|
+
class CLI
|
3
|
+
|
4
|
+
# I extract relevant information from a 'hcu' command line
|
5
|
+
class Parser
|
6
|
+
attr_reader :options
|
7
|
+
def initialize(hcu_command)
|
8
|
+
@tokens = tokenize(hcu_command)
|
9
|
+
parse_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def layer_name
|
13
|
+
options[:layer_name]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
attr_reader :tokens
|
18
|
+
|
19
|
+
def parse_options
|
20
|
+
@options = Hash.new
|
21
|
+
|
22
|
+
ignore_hcu
|
23
|
+
set_layer_name
|
24
|
+
set_mode_if_present
|
25
|
+
set_profile_if_present
|
26
|
+
set_other_args
|
27
|
+
|
28
|
+
options
|
29
|
+
end
|
30
|
+
|
31
|
+
def tokenize(input)
|
32
|
+
args =
|
33
|
+
case input
|
34
|
+
when Array ; input
|
35
|
+
when String ; input.split(/\s+/) # That's 4 years of CS education right there, baby
|
36
|
+
else fail "Don't know how to parse #{input.inspect}"
|
37
|
+
end
|
38
|
+
args.compact
|
39
|
+
end
|
40
|
+
|
41
|
+
# We might get the 'hcu' command name itself; just drop it on the floor
|
42
|
+
def ignore_hcu
|
43
|
+
tokens.shift if tokens.first =~ /\bhcu$/i
|
44
|
+
end
|
45
|
+
|
46
|
+
# This is the only required argument.
|
47
|
+
# TODO: Validate this against the list of known layers?
|
48
|
+
# ^ Would require loading local app's hypercuke config.
|
49
|
+
# ^ Would require allowing local app to *have* hypercuke config.
|
50
|
+
def set_layer_name
|
51
|
+
fail "Layer name is required" if tokens.empty?
|
52
|
+
options[:layer_name] = tokens.shift
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_mode_if_present
|
56
|
+
unless tokens.first =~ /^-/
|
57
|
+
options[:mode] = tokens.shift
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_profile_if_present
|
62
|
+
if profile_index = ( tokens.index('--profile') || tokens.index('-p') )
|
63
|
+
tokens.delete_at(profile_index) # don't care
|
64
|
+
options[:profile] = tokens.delete_at(profile_index)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def set_other_args
|
69
|
+
options[:other_args] = Array.new.tap do |rest|
|
70
|
+
rest << tokens.shift until tokens.empty?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Hypercuke
|
2
|
+
|
3
|
+
class Config
|
4
|
+
attr_reader :layers, :topics
|
5
|
+
def initialize
|
6
|
+
@layers = NameList.new
|
7
|
+
@topics = NameList.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def layer_names
|
11
|
+
layers.to_a
|
12
|
+
end
|
13
|
+
|
14
|
+
def topic_names
|
15
|
+
topics.to_a
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Hypercuke
|
4
|
+
# I provide a way of passing state around between tests
|
5
|
+
# that isn't instance variables.
|
6
|
+
#
|
7
|
+
# This is handy even in plain old Cucumber-land (where if you typo an
|
8
|
+
# instance variable, you just get nil), and essential in a set of
|
9
|
+
# mostly-independent step adapter objects, each with their own private
|
10
|
+
# state.
|
11
|
+
class Context
|
12
|
+
def initialize
|
13
|
+
@hash = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
extend Forwardable
|
17
|
+
# I support:
|
18
|
+
# - Hash-style getting and setting via square brackets,
|
19
|
+
# - fetch (as a pass-through to Hash),
|
20
|
+
def_delegators :@hash, *[
|
21
|
+
:[],
|
22
|
+
:[]=,
|
23
|
+
:fetch,
|
24
|
+
]
|
25
|
+
|
26
|
+
# - And a variant of fetch that, if the key is not found, sets it
|
27
|
+
# for the next caller.
|
28
|
+
#
|
29
|
+
# This behavior is in the spirit of the ||= operator, except that
|
30
|
+
# it won't short-circuit and call the default value if the key is
|
31
|
+
# present, but set to nil or false.
|
32
|
+
def fetch_or_default(key, &block)
|
33
|
+
@hash[key] = @hash.fetch(key, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# NOTE: this file should not be required from the Hypercuke gem itself.
|
2
|
+
# It's intended to be required from within the Cucumber environment
|
3
|
+
# (e.g., in features/support/env.rb or equivalent).
|
4
|
+
|
5
|
+
module Hypercuke
|
6
|
+
module CucumberIntegration
|
7
|
+
module WorldMixin
|
8
|
+
def step_driver
|
9
|
+
@step_driver ||= Hypercuke::StepDriver.new
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
World( Hypercuke::CucumberIntegration::WorldMixin )
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Hypercuke
|
2
|
+
module Error
|
3
|
+
def self.included(receiver)
|
4
|
+
receiver.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def wrap(exception)
|
9
|
+
message = translate_message(exception.message)
|
10
|
+
new(message).tap do |wrapping_exception|
|
11
|
+
wrapping_exception.set_backtrace exception.backtrace
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def translate_message(message)
|
16
|
+
message # just here to be overridden
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class LayerNotDefinedError < NameError
|
22
|
+
include Hypercuke::Error
|
23
|
+
end
|
24
|
+
|
25
|
+
class TopicNotDefinedError < NameError
|
26
|
+
include Hypercuke::Error
|
27
|
+
end
|
28
|
+
|
29
|
+
class StepAdapterNotDefinedError < NameError
|
30
|
+
include Hypercuke::Error
|
31
|
+
|
32
|
+
def self.translate_message(message)
|
33
|
+
step_adapter_name =
|
34
|
+
if md = /(Hypercuke::StepAdapters::\S*)/.match(message)
|
35
|
+
md.captures.first
|
36
|
+
else
|
37
|
+
message
|
38
|
+
end
|
39
|
+
"Step adapter not defined: '#{step_adapter_name}'"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Hypercuke
|
4
|
+
class NameList
|
5
|
+
attr_reader :names
|
6
|
+
private :names
|
7
|
+
|
8
|
+
def initialize(*names)
|
9
|
+
@names = names.flatten
|
10
|
+
end
|
11
|
+
|
12
|
+
def define(new_name)
|
13
|
+
name = new_name.to_sym
|
14
|
+
names << name unless names.include?(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate(name)
|
18
|
+
if valid_name?(name)
|
19
|
+
return name.to_sym
|
20
|
+
else
|
21
|
+
yield if block_given?
|
22
|
+
return nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def valid_name?(name)
|
27
|
+
names.include?(name.to_sym)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_a
|
31
|
+
names.dup
|
32
|
+
end
|
33
|
+
|
34
|
+
def empty?
|
35
|
+
names.empty?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Hypercuke
|
2
|
+
# I am the superclass for all of the generated step adapters.
|
3
|
+
class StepAdapter
|
4
|
+
def initialize(context, step_driver)
|
5
|
+
@context = context
|
6
|
+
@step_driver = step_driver
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
attr_reader :context, :step_driver
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Hypercuke
|
2
|
+
# Greetings, dear reader. There's a lot going in this file, so it's
|
3
|
+
# been organized in a more conversational style than most of the Ruby
|
4
|
+
# code I write. I hope it helps.
|
5
|
+
|
6
|
+
# First things first: the point of this entire file.
|
7
|
+
#
|
8
|
+
# Should you find yourself in possession of a [topic, layer] name
|
9
|
+
# pair, this method allows you to redeem it for VALUABLE PRIZES! (And
|
10
|
+
# by "valuable prizes", I mean "a reference to a step adapter class,
|
11
|
+
# if one has already been defined.")
|
12
|
+
#
|
13
|
+
# This method (on Hypercuke itself) is but a facade...
|
14
|
+
def self.step_adapter_class(topic_name, layer_name)
|
15
|
+
topic_module = StepAdapters.fetch_topic_module(topic_name)
|
16
|
+
topic_module.fetch_step_adapter(layer_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
# We start out with a namespace for step adapters. As new step
|
20
|
+
# adapter classes are created (see Hypercuke::AdapterDefinition), they
|
21
|
+
# will be assigned to constants in this module's namespace (so that
|
22
|
+
# they can have human-friendly names when something goes wrong and
|
23
|
+
# #inspect gets called on them).
|
24
|
+
#
|
25
|
+
# Class names will be of the form:
|
26
|
+
# Hypercuke::StepAdapters::<TopicName>::<LayerName>
|
27
|
+
module StepAdapters
|
28
|
+
extend self # BTW, I hate typing "self." in modules.
|
29
|
+
|
30
|
+
# We'll get to what makes a topic module special is in a moment, but
|
31
|
+
# here's how we fetch one:
|
32
|
+
def fetch_topic_module(topic_name)
|
33
|
+
# FIXME: cyclomatic complexity
|
34
|
+
Hypercuke.topics.validate(topic_name) do
|
35
|
+
fail TopicNotDefinedError, "Topic not defined: #{topic_name}"
|
36
|
+
end
|
37
|
+
validate_topic_module \
|
38
|
+
begin
|
39
|
+
const_get( MiniInflector.camelize(topic_name) )
|
40
|
+
rescue NameError => e
|
41
|
+
raise Hypercuke::TopicNotDefinedError.wrap(e)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def validate_topic_module(candidate)
|
48
|
+
return candidate if candidate.kind_of?(::Hypercuke::TopicModule)
|
49
|
+
fail Hypercuke::TopicNotDefinedError
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# So, a TopicModule is (a) a namespace for holding step adapters, and
|
54
|
+
# (b) a slightly specialized type of Module that knows enough to be
|
55
|
+
# able to fetch step adapters.
|
56
|
+
class TopicModule < Module
|
57
|
+
def fetch_step_adapter(layer_name)
|
58
|
+
# FIXME: cyclomatic complexity
|
59
|
+
Hypercuke.layers.validate(layer_name) do
|
60
|
+
fail LayerNotDefinedError, "Layer not defined: #{layer_name}"
|
61
|
+
end
|
62
|
+
validate_step_adapter \
|
63
|
+
begin
|
64
|
+
const_get( MiniInflector.camelize(layer_name) )
|
65
|
+
rescue NameError => e
|
66
|
+
raise Hypercuke::StepAdapterNotDefinedError.wrap(e)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def validate_step_adapter(candidate)
|
73
|
+
return candidate if candidate.kind_of?(Class) && candidate.ancestors.include?(::Hypercuke::StepAdapter)
|
74
|
+
fail Hypercuke::StepAdapterNotDefinedError
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Okay. That covers fetching step adapters once they've been defined.
|
79
|
+
# Now let's talk about how we define new ones.
|
80
|
+
|
81
|
+
module StepAdapters
|
82
|
+
# Here's the entry point for the adapter definition API. It's
|
83
|
+
# entirely possible that a user might want to define their step
|
84
|
+
# adapters in a re-entrant way (much like we've been reopening the
|
85
|
+
# same modules and classes in this file), so this bit of the code
|
86
|
+
# will either create a new step adapter, or return one that's
|
87
|
+
# already been defined.
|
88
|
+
def define(topic_name, layer_name)
|
89
|
+
topic_module = define_topic_module(topic_name)
|
90
|
+
step_adapter = topic_module.define_step_adapter( layer_name )
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# The next two bits follow this pattern:
|
95
|
+
# 1) attempt to fetch the requested thing.
|
96
|
+
# 2) if fetch fails, define and return it.
|
97
|
+
|
98
|
+
module StepAdapters
|
99
|
+
def define_topic_module(topic_name)
|
100
|
+
fetch_topic_module(topic_name)
|
101
|
+
rescue Hypercuke::TopicNotDefinedError
|
102
|
+
const_name = MiniInflector.camelize(topic_name)
|
103
|
+
const_set const_name, TopicModule.new
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class TopicModule < Module
|
108
|
+
def define_step_adapter(layer_name)
|
109
|
+
fetch_step_adapter(layer_name)
|
110
|
+
rescue Hypercuke::StepAdapterNotDefinedError
|
111
|
+
const_name = MiniInflector.camelize(layer_name)
|
112
|
+
const_set const_name, Class.new(StepAdapter)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# One final bit of business before we go: when testing code that
|
117
|
+
# defines classes and binds them to constants, it is occasionally
|
118
|
+
# useful to reset to a blank slate.
|
119
|
+
module StepAdapters
|
120
|
+
def clear
|
121
|
+
constants.each do |c|
|
122
|
+
remove_const c
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Hypercuke
|
2
|
+
|
3
|
+
# The step driver serves as the entry point from Cucumber step
|
4
|
+
# definition bodies to the Hypercuke API. An instance of this object
|
5
|
+
# will be made available to Cucumber::World via the #step_driver
|
6
|
+
# method.
|
7
|
+
#
|
8
|
+
# The StepDriver will interpret any message sent to it as a topic
|
9
|
+
# name, combine that with the current_layer name from Hypercuke, and
|
10
|
+
# use that to instantitate a StepAdapter for the appropriate
|
11
|
+
# topic/layer combo.
|
12
|
+
class StepDriver
|
13
|
+
def layer_name
|
14
|
+
Hypercuke.current_layer
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(method, *_O) # No arguments for you, Mister Bond! *adjusts monocle*
|
18
|
+
topic_name = method.to_sym
|
19
|
+
|
20
|
+
# Define a method for the topic name so that future requests for
|
21
|
+
# the same step adapter don't pay the method_missing tax.
|
22
|
+
self.class.send(:define_method, topic_name) do
|
23
|
+
# Within the defined method, memoize the step adapter so that
|
24
|
+
# future requests also don't pay the GC tax.
|
25
|
+
key = [topic_name, layer_name] # key on both names in case someone changes the layer on us
|
26
|
+
__step_adapters__[key] ||=
|
27
|
+
begin
|
28
|
+
klass = Hypercuke.step_adapter_class(*key)
|
29
|
+
klass.new(__context__, self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# And don't forget to invoke the newly-created method.
|
34
|
+
send(method)
|
35
|
+
end
|
36
|
+
|
37
|
+
# StepDriver is eager to please.
|
38
|
+
def respond_to_missing?(_)
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def __context__
|
45
|
+
@__context__ ||= Hypercuke::Context.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def __step_adapters__
|
49
|
+
@__step_adapters__ ||= {}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|