aygabtu 0.2.0
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 +24 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +245 -0
- data/Rakefile +10 -0
- data/aygabtu.gemspec +23 -0
- data/lib/aygabtu.rb +5 -0
- data/lib/aygabtu/generator.rb +64 -0
- data/lib/aygabtu/handle.rb +38 -0
- data/lib/aygabtu/point_of_call.rb +28 -0
- data/lib/aygabtu/route_mark.rb +18 -0
- data/lib/aygabtu/route_wrapper.rb +96 -0
- data/lib/aygabtu/rspec.rb +119 -0
- data/lib/aygabtu/scope/action.rb +38 -0
- data/lib/aygabtu/scope/base.rb +85 -0
- data/lib/aygabtu/scope/named.rb +38 -0
- data/lib/aygabtu/scope/namespace_controller.rb +50 -0
- data/lib/aygabtu/scope/remaining.rb +27 -0
- data/lib/aygabtu/scope/requiring.rb +26 -0
- data/lib/aygabtu/scope/static_dynamic.rb +33 -0
- data/lib/aygabtu/scope/visiting_with.rb +19 -0
- data/lib/aygabtu/scope_actor.rb +104 -0
- data/lib/aygabtu/scope_chain.rb +39 -0
- data/lib/aygabtu/version.rb +3 -0
- data/spec/actions_spec.rb +152 -0
- data/spec/example_spec.rb +61 -0
- data/spec/lib/route_wrapper_spec.rb +140 -0
- data/spec/matching_routes_spec.rb +384 -0
- data/spec/nesting_spec.rb +59 -0
- data/spec/rails_application_helper.rb +8 -0
- data/spec/support/aygabtu_sees_routes.rb +17 -0
- data/spec/support/identifies_routes.rb +18 -0
- data/spec/support/invokes_rspec.rb +93 -0
- data/spec/support/matcher_shims.rb +39 -0
- data/spec/support_spec/identifies_routes_spec.rb +49 -0
- data/spec/visiting_routes_spec.rb +57 -0
- metadata +123 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'route_wrapper'
|
2
|
+
require_relative 'scope_actor'
|
3
|
+
|
4
|
+
module Aygabtu
|
5
|
+
class Handle
|
6
|
+
|
7
|
+
def routes
|
8
|
+
@routes ||= rails_application_routes.set.map do |journey_route|
|
9
|
+
RouteWrapper.new(journey_route)
|
10
|
+
end.select(&:get?).reject(&:internal?)
|
11
|
+
end
|
12
|
+
|
13
|
+
def checkpoint
|
14
|
+
@checkpoint || 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_checkpoint
|
18
|
+
@checkpoint = (checkpoint + 1).tap do |new_checkpoint|
|
19
|
+
puts "New checkpoint #{new_checkpoint}" if verbose?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def verbose!
|
24
|
+
@verbose = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def verbose?
|
28
|
+
!!@verbose
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def rails_application_routes
|
34
|
+
@rails_application_routes ||= Rails.application.routes
|
35
|
+
end
|
36
|
+
attr_writer :rails_application_routes
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'scope/base'
|
2
|
+
|
3
|
+
module Aygabtu
|
4
|
+
|
5
|
+
module PointOfCall
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def point_of_call
|
9
|
+
caller.drop_while { |point| point.start_with?(PointOfCall.gem_root) }.first
|
10
|
+
end
|
11
|
+
|
12
|
+
def file_and_line_at_point_of_call
|
13
|
+
filename, line_and_context = point_of_call.split(':', 2)
|
14
|
+
[filename, line_and_context.to_i] # make use of the fact to_i tolerates being passed a string only _beginning_ with a number
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.gem_root
|
18
|
+
return @gem_root if @gem_root
|
19
|
+
|
20
|
+
path = Pathname(__FILE__)
|
21
|
+
while new_path = path.parent and new_path.to_s.include?('lib/aygabtu')
|
22
|
+
path = new_path
|
23
|
+
end
|
24
|
+
|
25
|
+
@gem_root= path.to_s
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Aygabtu
|
2
|
+
|
3
|
+
class RouteMark
|
4
|
+
def initialize(action, poc, checkpoint)
|
5
|
+
@action, @poc, @checkpoint = action, poc, checkpoint
|
6
|
+
end
|
7
|
+
attr_reader :action, :poc, :checkpoint
|
8
|
+
|
9
|
+
def conflicting?(other)
|
10
|
+
!(action == :visit && other.action == :visit)
|
11
|
+
end
|
12
|
+
|
13
|
+
def description
|
14
|
+
"#{action} action at #{poc} (CP #{checkpoint})"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Aygabtu
|
2
|
+
class RouteWrapper
|
3
|
+
# Wraps a Journey route
|
4
|
+
|
5
|
+
attr_reader :journey_route # ease debugging
|
6
|
+
|
7
|
+
def initialize(journey_route)
|
8
|
+
@journey_route = journey_route
|
9
|
+
|
10
|
+
@marks = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def conflicting_marks(mark)
|
14
|
+
@marks.select { |m| m.conflicting?(mark) }
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :marks
|
18
|
+
|
19
|
+
def get?
|
20
|
+
@journey_route.verb.match('GET')
|
21
|
+
end
|
22
|
+
|
23
|
+
def internal?
|
24
|
+
# this particular route is hard to filter by any sensible criterion
|
25
|
+
@journey_route.path.to_regexp == %r{\A/assets}
|
26
|
+
end
|
27
|
+
|
28
|
+
# array of parameter names (symbols) required for generating URL
|
29
|
+
def required_parts
|
30
|
+
@journey_route.required_parts
|
31
|
+
end
|
32
|
+
|
33
|
+
def controller
|
34
|
+
@journey_route.requirements[:controller]
|
35
|
+
end
|
36
|
+
|
37
|
+
def controller_namespace
|
38
|
+
return @controller_namespace if defined? @controller_namespace
|
39
|
+
return @controller_namespace = nil unless controller
|
40
|
+
|
41
|
+
@controller_namespace = Pathname('/').join(controller).dirname.to_s[1..-1]
|
42
|
+
@controller_namespace = nil if @controller_namespace.empty?
|
43
|
+
@controller_namespace
|
44
|
+
end
|
45
|
+
|
46
|
+
def controller_basename
|
47
|
+
Pathname(controller).basename.to_s if controller
|
48
|
+
end
|
49
|
+
|
50
|
+
def action
|
51
|
+
# sanity condition needed for Rails 4.1
|
52
|
+
@journey_route.requirements[:action] if controller
|
53
|
+
end
|
54
|
+
|
55
|
+
def name
|
56
|
+
@journey_route.name.to_s if @journey_route.name
|
57
|
+
end
|
58
|
+
|
59
|
+
# def matches_string?(string)
|
60
|
+
# @journey_route.path.to_regexp.match(string)
|
61
|
+
# end
|
62
|
+
|
63
|
+
# this assumes parameters.keys == required_parts
|
64
|
+
def generate_url_with_proper_parameters(parameters)
|
65
|
+
@journey_route.format(parameters)
|
66
|
+
end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
if @journey_route.name
|
70
|
+
"route named :#{@journey_route.name}"
|
71
|
+
else
|
72
|
+
"route matching #{@journey_route.path.to_regexp.inspect}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def format(visiting_data)
|
77
|
+
visiting_data = visiting_data.stringify_keys
|
78
|
+
|
79
|
+
query_data = visiting_data.except(*@journey_route.parts.map(&:to_s))
|
80
|
+
visiting_data = visiting_data.except(*query_data.keys)
|
81
|
+
|
82
|
+
visiting_data.symbolize_keys! # format expects symbols, but we deal with strings in all other places
|
83
|
+
path = @journey_route.format(visiting_data)
|
84
|
+
|
85
|
+
if query_data.empty?
|
86
|
+
path
|
87
|
+
else
|
88
|
+
"#{path}?#{Rack::Utils.build_query(query_data)}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def really_required_keys
|
93
|
+
@journey_route.path.required_names
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require_relative 'handle'
|
2
|
+
require_relative 'scope/base'
|
3
|
+
require_relative 'scope_chain'
|
4
|
+
require_relative 'point_of_call'
|
5
|
+
|
6
|
+
module Aygabtu
|
7
|
+
module RSpec
|
8
|
+
|
9
|
+
module ExampleGroupMethods
|
10
|
+
delegate(*ScopeChain.scope_methods, to: :aygabtu_scope_chain)
|
11
|
+
|
12
|
+
def aygabtu_scope
|
13
|
+
@aygabtu_scope ||= if superclass.respond_to?(:aygabtu_scope)
|
14
|
+
superclass.aygabtu_scope
|
15
|
+
else
|
16
|
+
Scope::Base.blank_slate
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def aygabtu_scope_chain
|
21
|
+
@aygabtu_scope_chain ||= ScopeChain.new(self, aygabtu_scope)
|
22
|
+
end
|
23
|
+
|
24
|
+
def aygabtu_enter_context(block, scope)
|
25
|
+
context "Context defined at #{PointOfCall.point_of_call}" do
|
26
|
+
self.aygabtu_scope = scope
|
27
|
+
class_exec(&block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def aygabtu_action(action, scope, *args)
|
32
|
+
puts "Action #{action} after checkpoint #{aygabtu_handle.checkpoint} from #{PointOfCall.point_of_call}" if aygabtu_handle.verbose?
|
33
|
+
ScopeActor.new(scope, self).public_send(action, *args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def aygabtu_handle
|
37
|
+
if superclass.respond_to?(:aygabtu_handle)
|
38
|
+
superclass.aygabtu_handle
|
39
|
+
else
|
40
|
+
@_aygabtu_handle ||= Handle.new
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def aygabtu_matching_routes(scope = aygabtu_scope)
|
45
|
+
scope = scope.scope if scope.respond_to?(:scope) # a scope chain can be pased as well
|
46
|
+
aygabtu_handle.routes.select do |route|
|
47
|
+
scope.matches_route?(route)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
ScopeActor.actions.each do |action|
|
52
|
+
define_method(action) do |*args|
|
53
|
+
aygabtu_action(action, aygabtu_scope, *args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_writer :aygabtu_scope
|
60
|
+
end
|
61
|
+
|
62
|
+
module ExampleGroupModule
|
63
|
+
def self.included(group)
|
64
|
+
group.extend ExampleGroupMethods
|
65
|
+
end
|
66
|
+
|
67
|
+
def aygabtu_example_for(path)
|
68
|
+
visit path
|
69
|
+
aygabtu_assertions
|
70
|
+
end
|
71
|
+
|
72
|
+
attr_accessor :aygabtu_path_to_visit
|
73
|
+
|
74
|
+
def aygabtu_pass_to_route(id, visiting_data)
|
75
|
+
route = self.class.aygabtu_handle.routes.find { |a_route| a_route.object_id == id }
|
76
|
+
|
77
|
+
visiting_data = visiting_data.clone
|
78
|
+
visiting_data.keys.each do |key|
|
79
|
+
value = visiting_data[key]
|
80
|
+
visiting_data[key] = aygabtu_fetch_symbolic_pass_value(value) if value.is_a?(Symbol)
|
81
|
+
end
|
82
|
+
|
83
|
+
missing_keys = route.really_required_keys - visiting_data.keys.map(&:to_s)
|
84
|
+
|
85
|
+
if missing_keys.empty?
|
86
|
+
route.format(visiting_data)
|
87
|
+
else
|
88
|
+
raise "Route is missing required key(s) #{missing_keys.map(&:inspect).join(', ')}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def aygabtu_fetch_symbolic_pass_value(symbol)
|
93
|
+
raise "Symbolic pass value #{symbol} given, but no such method defined" unless respond_to?(symbol)
|
94
|
+
send(symbol)
|
95
|
+
end
|
96
|
+
|
97
|
+
def aygabtu_assert_status_success
|
98
|
+
expect(page.status_code).to be 200
|
99
|
+
end
|
100
|
+
|
101
|
+
def aygabtu_assert_not_redirected_away # @TODO create custom rspec matcher to provide cleaner error messages
|
102
|
+
uri = URI(current_url)
|
103
|
+
uri.host = nil
|
104
|
+
uri.scheme = nil
|
105
|
+
expect(uri.to_s).to be == aygabtu_path_to_visit
|
106
|
+
end
|
107
|
+
|
108
|
+
def aygabtu_assertions
|
109
|
+
raise "Hey aygabtu user, please implement this yourself by overriding the aygabtu_assertions method!"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class << self
|
114
|
+
def example_group_module
|
115
|
+
ExampleGroupModule
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Aygabtu
|
2
|
+
module Scope
|
3
|
+
module Action
|
4
|
+
def action(*actions)
|
5
|
+
raise "nesting/chaining action scopes makes no sense" if @data[:action]
|
6
|
+
|
7
|
+
new_data = @data.dup.merge(actions: actions.map(&:to_s))
|
8
|
+
self.class.new(new_data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches_route?(route)
|
12
|
+
if @data[:actions]
|
13
|
+
@data[:actions].include?(route.action)
|
14
|
+
else
|
15
|
+
true
|
16
|
+
end && super
|
17
|
+
end
|
18
|
+
|
19
|
+
def segments_split_once
|
20
|
+
return super unless Array(@data[:actions]).length > 1
|
21
|
+
|
22
|
+
@data[:actions].map do |action|
|
23
|
+
new_data = @data.merge(actions: [action])
|
24
|
+
self.class.new(new_data)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def inspect_data
|
29
|
+
return super unless actions = @data[:actions]
|
30
|
+
super.merge(action: actions.map(&:inspect).join('; '))
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.factory_methods
|
34
|
+
[:action]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require_relative 'namespace_controller'
|
2
|
+
require_relative 'action'
|
3
|
+
require_relative 'named'
|
4
|
+
require_relative 'visiting_with'
|
5
|
+
require_relative 'requiring'
|
6
|
+
require_relative 'static_dynamic'
|
7
|
+
require_relative 'remaining'
|
8
|
+
|
9
|
+
module Aygabtu
|
10
|
+
module Scope
|
11
|
+
class Base
|
12
|
+
def initialize(data)
|
13
|
+
@data = data
|
14
|
+
end
|
15
|
+
|
16
|
+
def visiting_data
|
17
|
+
@data.fetch(:visiting_data, {})
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.blank_slate
|
21
|
+
new(filters: [])
|
22
|
+
end
|
23
|
+
|
24
|
+
COMPONENTS = [
|
25
|
+
NamespaceController,
|
26
|
+
Action,
|
27
|
+
Named,
|
28
|
+
VisitingWith,
|
29
|
+
Requiring,
|
30
|
+
StaticDynamic,
|
31
|
+
Remaining
|
32
|
+
]
|
33
|
+
|
34
|
+
module BasicBehaviour
|
35
|
+
# defines methods below COMPONENTS in the inheritance chain
|
36
|
+
# so components can override and call super
|
37
|
+
|
38
|
+
def matches_route?(route)
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def segments_split_once
|
43
|
+
end
|
44
|
+
|
45
|
+
def inspect_data
|
46
|
+
{}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
include BasicBehaviour
|
50
|
+
|
51
|
+
include(*COMPONENTS)
|
52
|
+
|
53
|
+
def segments
|
54
|
+
if split_once = segments_split_once
|
55
|
+
split_once.map(&:segments).reduce(:+)
|
56
|
+
else
|
57
|
+
[self]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect
|
62
|
+
data = inspect_data
|
63
|
+
data.keys.each { |key| data.delete(key) if data[key].nil? }
|
64
|
+
message = if data.empty?
|
65
|
+
"nothing specified"
|
66
|
+
else
|
67
|
+
data.map { |key, value| "#{key}: #{value}" }.join(', ')
|
68
|
+
end
|
69
|
+
"\#<Aygabtu scope (#{message})>"
|
70
|
+
end
|
71
|
+
|
72
|
+
@factory_methods = COMPONENTS.map(&:factory_methods).reduce([], :+)
|
73
|
+
|
74
|
+
class << self
|
75
|
+
attr_reader :factory_methods
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def inspected_or_nil(obj)
|
81
|
+
obj.inspect unless obj.nil?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Aygabtu
|
2
|
+
module Scope
|
3
|
+
module Named
|
4
|
+
def named(*names)
|
5
|
+
raise "nesting/chaining named in/after named makes no sense" if @data[:names]
|
6
|
+
|
7
|
+
new_data = @data.dup.merge(names: names.map(&:to_s))
|
8
|
+
self.class.new(new_data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches_route?(route)
|
12
|
+
if @data[:names]
|
13
|
+
@data[:names].include?(route.name)
|
14
|
+
else
|
15
|
+
true
|
16
|
+
end && super
|
17
|
+
end
|
18
|
+
|
19
|
+
def segments_split_once
|
20
|
+
return super unless Array(@data[:names]).length > 1
|
21
|
+
|
22
|
+
@data[:names].map do |name|
|
23
|
+
new_data = @data.merge(names: [name])
|
24
|
+
self.class.new(new_data)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def inspect_data
|
29
|
+
return super unless names = @data[:names]
|
30
|
+
super.merge(name: names.map(&:inspect).join('; '))
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.factory_methods
|
34
|
+
[:named]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|