hooked 0.1.2 → 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.
- data/.rspec +1 -0
- data/Gemfile +1 -0
- data/LICENSE +1 -1
- data/README.md +46 -131
- data/Rakefile +6 -7
- data/hooked.gemspec +3 -8
- data/lib/hooked/aspect.rb +35 -0
- data/lib/hooked/graph.rb +57 -0
- data/lib/hooked/hook.rb +13 -25
- data/lib/hooked/version.rb +1 -1
- data/lib/hooked.rb +37 -6
- data/spec/aspect_spec.rb +119 -0
- data/spec/graph_spec.rb +66 -0
- data/spec/hook_spec.rb +69 -0
- data/spec/hooked_spec.rb +64 -0
- data/spec/spec_helper.rb +6 -0
- metadata +52 -85
- data/lib/hooked/container.rb +0 -52
- data/lib/hooked/context.rb +0 -39
- data/lib/hooked/controller.rb +0 -80
- data/lib/hooked/hookable.rb +0 -20
- data/test/container_test.rb +0 -71
- data/test/controller_test.rb +0 -234
- data/test/helper.rb +0 -10
- data/test/hook_test.rb +0 -90
- data/test/hookable_test.rb +0 -66
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
CHANGED
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,170 +1,85 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
Aspect Orientation Made Simple
|
2
|
+
==============================
|
3
3
|
|
4
|
-
Hooked
|
5
|
-
parts of your application can hook onto.
|
4
|
+
Hooked lets you transparently aspectify your methods and blocks.
|
6
5
|
|
7
6
|
Getting Started
|
8
7
|
---------------
|
9
8
|
|
9
|
+
By including `Hooked` into a class you basically say "everything can attach code
|
10
|
+
to instance methods of this class".
|
11
|
+
|
10
12
|
require "hooked"
|
11
13
|
|
12
|
-
class
|
13
|
-
include Hooked
|
14
|
-
|
15
|
-
def save
|
16
|
-
hookable(:save_user, self)
|
17
|
-
end
|
14
|
+
class Foo
|
15
|
+
include Hooked
|
18
16
|
|
19
|
-
|
20
|
-
|
17
|
+
def breakfast
|
18
|
+
puts "No milk?!"
|
21
19
|
end
|
22
20
|
end
|
23
21
|
|
24
|
-
class
|
25
|
-
|
26
|
-
|
27
|
-
def has_permissions?
|
28
|
-
false
|
22
|
+
class BetterFoo
|
23
|
+
def shower
|
24
|
+
puts "Oooh..."
|
29
25
|
end
|
30
26
|
|
31
|
-
|
32
|
-
|
27
|
+
def fuuu(inner)
|
28
|
+
puts "FUUU!"
|
29
|
+
inner.call
|
30
|
+
puts "FUUU!"
|
33
31
|
end
|
34
32
|
end
|
35
33
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
Defining Hookable Code
|
41
|
-
----------------------
|
42
|
-
|
43
|
-
To be able to define hookable code (as well as hooks) you need to mixin the
|
44
|
-
Hooked::Container module. Afterwards you can use the `::hookable` method which
|
45
|
-
takes a symbol as the hookable's name as well as a block.
|
46
|
-
|
47
|
-
class Something
|
48
|
-
include Hooked::Container
|
49
|
-
|
50
|
-
hookable :breakfast do |ctx|
|
51
|
-
puts "I'm having breakfast"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
The hookable will always be executed in the context of its container's object.
|
56
|
-
|
57
|
-
Defining Hooks
|
58
|
-
--------------
|
59
|
-
|
60
|
-
For defining hooks you can use the methods `::before`, `::after` and
|
61
|
-
`::around`. They are shortcuts to `::hook` and take the arguments
|
62
|
-
`hookable_name` (name of the hookable you want to hook onto), `hook_name` and
|
63
|
-
an optional hash of before/after options.
|
64
|
-
|
65
|
-
before :breakfast, :get_up do |ctx|
|
66
|
-
puts "I'm getting out of the bed"
|
67
|
-
end
|
34
|
+
foo, better_foo = Foo.new, BetterFoo.new
|
35
|
+
foo.before :breakfast, better_foo.method(:shower)
|
36
|
+
foo.after :breakfast, proc { puts "Mmmh..." }
|
37
|
+
foo.around :breakfast, better_foo.method(:fuuu)
|
68
38
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
`:before` and `:after` can be a single hook name, an array of hook names or
|
74
|
-
`:all`. More documentation on before/after relations and how they are being
|
75
|
-
resolved can be found in the
|
76
|
-
[depression gem](http://rubygems.org/gems/depression).
|
77
|
-
|
78
|
-
Hooks will always be executed in the context of their container's object (just
|
79
|
-
as hookables).
|
80
|
-
|
81
|
-
**Note:** Hooked will at no point guarantee that hooks will be executed in the
|
82
|
-
order of definition. If you rely on execution order, you should use the
|
83
|
-
`:before` and `:after` options.
|
84
|
-
|
85
|
-
Hooking Controller
|
86
|
-
------------------
|
87
|
-
|
88
|
-
The controller is responsible for bringing hooks into the desired order and
|
89
|
-
executing them correctly.
|
39
|
+
foo.breakfast
|
40
|
+
|
41
|
+
This will output:
|
90
42
|
|
91
|
-
|
92
|
-
|
43
|
+
FUUU!
|
44
|
+
Oooh...
|
45
|
+
No milk?!
|
46
|
+
Mmmh...
|
47
|
+
FUUU!
|
93
48
|
|
94
|
-
|
95
|
-
|
49
|
+
Execution Model
|
50
|
+
---------------
|
96
51
|
|
97
|
-
|
98
|
-
context object that is being passed between the hooks as well as the hookable.
|
99
|
-
You can pass in and out whatever you want.
|
52
|
+
TOOD
|
100
53
|
|
101
|
-
|
102
|
-
|
103
|
-
end
|
54
|
+
Dependencies / Execution Order of Aspects
|
55
|
+
-----------------------------------------
|
104
56
|
|
105
|
-
|
106
|
-
ctx.result = ctx.args.map {|s| s.to_s.lower }
|
107
|
-
end
|
108
|
-
|
109
|
-
p res # => ["ASDF", "FOO", "123"]
|
57
|
+
TODO
|
110
58
|
|
111
|
-
|
112
|
-
|
59
|
+
Possible Use Cases
|
60
|
+
------------------
|
113
61
|
|
114
|
-
|
115
|
-
next one, use `Context#next`. You would probably want to set a return value
|
116
|
-
first with `Context#result=`. `Context#return` basically does the same thing,
|
117
|
-
but additionally it always sets the return value to whatever you pass as an
|
118
|
-
argument.
|
119
|
-
|
120
|
-
# this
|
121
|
-
ctx.return "asd"
|
122
|
-
# is equal to
|
123
|
-
ctx.result = "asd"
|
124
|
-
ctx.next
|
125
|
-
|
126
|
-
# and this will result in ctx.result being nil after returning
|
127
|
-
ctx.result = 123
|
128
|
-
ctx.return
|
129
|
-
|
130
|
-
If you want to cancel the whole invocation you can use `Context#break`. This
|
131
|
-
will immediately return to where `Controller#invoke` was called. So if you call
|
132
|
-
it before the hookable was executed, it won't be executed at all.
|
133
|
-
|
134
|
-
hookable :foo do |ctx|
|
135
|
-
puts "You won't see me :/"
|
136
|
-
end
|
137
|
-
|
138
|
-
before :foo, :break_the_chain do |ctx|
|
139
|
-
ctx.break
|
140
|
-
end
|
62
|
+
You can use the `Hook` class to programmatically build graphs of aspects.
|
141
63
|
|
142
|
-
|
143
|
-
stack depth you want to jump. If you jump to high (e.g. you're hooking two
|
144
|
-
invocations deep but do `ctx.break 3`) a `NameError: uncaught throw 'hooked'`
|
145
|
-
will be raised.
|
64
|
+
# code
|
146
65
|
|
147
|
-
|
148
|
-
may cause serious trouble that can be hard to debug. This may change in future
|
149
|
-
versions of Hooked. (I appreciate Pull Requests! :>)
|
66
|
+
TODO
|
150
67
|
|
151
68
|
Dependencies
|
152
69
|
------------
|
153
70
|
|
154
|
-
* [
|
155
|
-
* [
|
156
|
-
[test-unit](https://rubygems.org/gems/test-unit) for development
|
71
|
+
* stdlib's [TSort](http://rubydoc.info/stdlib/tsort/1.9.2/frames)
|
72
|
+
* [RSpec](http://relishapp.com/rspec) for development
|
157
73
|
|
158
|
-
|
74
|
+
Specs pass on MRI 1.9.2, others have not been tested yet.
|
159
75
|
|
160
76
|
To do & Ideas
|
161
77
|
-------------
|
162
78
|
|
163
|
-
*
|
79
|
+
* Let before, after, around and Hook.new also take a block
|
80
|
+
* Use insertion order as execution order if no aspect has dependencies
|
164
81
|
* Visualization of hook flow
|
165
82
|
* Make backtraces more readable
|
166
|
-
* Make Ruby's control flow constructs work for hooks
|
167
|
-
* Allow control flow jumps with hookable names
|
168
83
|
|
169
84
|
Contributing
|
170
85
|
------------
|
@@ -179,4 +94,4 @@ You can also open an issue for discussion first, if you like.
|
|
179
94
|
License
|
180
95
|
-------
|
181
96
|
|
182
|
-
Hooked is subject to an MIT-style license
|
97
|
+
Hooked is subject to an MIT-style license (see LICENSE).
|
data/Rakefile
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
require "bundler"
|
2
|
-
Bundler
|
2
|
+
Bundler.setup :development
|
3
|
+
|
4
|
+
require "rspec/core/rake_task"
|
3
5
|
|
4
|
-
task :default => :
|
6
|
+
task :default => :spec
|
7
|
+
RSpec::Core::RakeTask.new :spec
|
5
8
|
|
6
|
-
|
7
|
-
Rake::TestTask.new do |t|
|
8
|
-
t.libs = ["lib"]
|
9
|
-
t.test_files = FileList["test/*_test.rb"]
|
10
|
-
end
|
9
|
+
Bundler::GemHelper.install_tasks
|
data/hooked.gemspec
CHANGED
@@ -10,15 +10,10 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.authors = ["Lars Gierth"]
|
11
11
|
s.email = ["lars.gierth@gmail.com"]
|
12
12
|
s.homepage = "http://rubygems.org/gems/hooked"
|
13
|
-
s.summary = %q{
|
14
|
-
s.description = %q{Hooked
|
15
|
-
code that gems or other parts of your application can hook
|
16
|
-
onto.}
|
13
|
+
s.summary = %q{Aspect Orientation Made Simple}
|
14
|
+
s.description = %q{Hooked lets you transparently aspectify your methods and blocks.}
|
17
15
|
|
18
|
-
s.
|
19
|
-
|
20
|
-
s.add_development_dependency "test-unit"
|
21
|
-
s.add_development_dependency "mocha"
|
16
|
+
s.add_development_dependency "rspec"
|
22
17
|
|
23
18
|
s.files = `git ls-files`.split("\n") - [".gitignore", ".rvmrc"]
|
24
19
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Hooked
|
2
|
+
class Aspect
|
3
|
+
attr_reader :type, :advice, :dependencies
|
4
|
+
attr_accessor :pointcut
|
5
|
+
|
6
|
+
def initialize(type, advice, dependencies = {})
|
7
|
+
[:before, :after].each {|d| dependencies[d] ||= [] }
|
8
|
+
@type, @advice, @dependencies = type, advice, dependencies
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(*args, &block)
|
12
|
+
args = advice.call(*args, &block) if before?
|
13
|
+
|
14
|
+
result = if around?
|
15
|
+
advice.call pointcut, *args, &block
|
16
|
+
else
|
17
|
+
pointcut.call *args, &block
|
18
|
+
end
|
19
|
+
|
20
|
+
advice.call result if after?
|
21
|
+
end
|
22
|
+
|
23
|
+
def before?
|
24
|
+
type == :before
|
25
|
+
end
|
26
|
+
|
27
|
+
def after?
|
28
|
+
type == :after
|
29
|
+
end
|
30
|
+
|
31
|
+
def around?
|
32
|
+
type == :around
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/hooked/graph.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require "tsort"
|
2
|
+
|
3
|
+
module Hooked
|
4
|
+
class Graph
|
5
|
+
class Node < Struct.new(:children, :aspect); end
|
6
|
+
|
7
|
+
class CircularDependencyError < TSort::Cyclic; end
|
8
|
+
|
9
|
+
include TSort
|
10
|
+
|
11
|
+
attr_reader :input, :output
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@input = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(node)
|
18
|
+
@changed = true
|
19
|
+
@input << node
|
20
|
+
end
|
21
|
+
|
22
|
+
def changed?
|
23
|
+
!!@changed
|
24
|
+
end
|
25
|
+
|
26
|
+
def sort
|
27
|
+
@nodes = input.inject({}) do |nodes, node|
|
28
|
+
children = node.dependencies[:after].map {|d| d.object_id }
|
29
|
+
nodes[node.advice.object_id] = Node.new(children, node); nodes
|
30
|
+
end
|
31
|
+
@nodes.each do |name, node|
|
32
|
+
node.aspect.dependencies[:before].each do |dep|
|
33
|
+
dep_name = dep.object_id
|
34
|
+
next unless @nodes[dep_name]
|
35
|
+
@nodes[dep_name].children << name
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
strongly_connected_components.each do |name|
|
40
|
+
next unless Array === name && name.length > 1
|
41
|
+
raise CircularDependencyError,
|
42
|
+
"Sorting failed: #{name.map {|n| @nodes[n].aspect.advice.inspect }.join ', '}"
|
43
|
+
end
|
44
|
+
|
45
|
+
@output = tsort.map {|name| @nodes[name].aspect }
|
46
|
+
@changed = false
|
47
|
+
end
|
48
|
+
|
49
|
+
def tsort_each_node(&block)
|
50
|
+
@nodes.each_key &block
|
51
|
+
end
|
52
|
+
|
53
|
+
def tsort_each_child(name, &block)
|
54
|
+
@nodes[name].children.each &block if @nodes[name]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/hooked/hook.rb
CHANGED
@@ -1,37 +1,25 @@
|
|
1
1
|
module Hooked
|
2
2
|
class Hook
|
3
|
-
attr_reader :
|
4
|
-
attr_accessor :container, :weight
|
3
|
+
attr_reader :pointcut, :graph
|
5
4
|
|
6
|
-
def initialize(
|
7
|
-
|
8
|
-
:before, :after, :around
|
9
|
-
].include?(type.to_sym)
|
10
|
-
@type, @name, @relations = type.to_sym, name.to_sym, relations
|
11
|
-
|
12
|
-
raise ArgumentError, "Hooked::Hookable.new expects a block." unless block
|
13
|
-
@container, @block = container, block
|
5
|
+
def initialize(pointcut)
|
6
|
+
@pointcut, @graph = pointcut, Graph.new
|
14
7
|
end
|
15
8
|
|
16
|
-
def
|
17
|
-
|
18
|
-
if hookable
|
19
|
-
container.instance_exec(context, hookable, &@block)
|
20
|
-
else
|
21
|
-
container.instance_exec(context, &@block)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def before?
|
26
|
-
type == :before
|
9
|
+
def add_aspect(*args)
|
10
|
+
graph << Aspect.new(*args)
|
27
11
|
end
|
28
12
|
|
29
|
-
def
|
30
|
-
|
13
|
+
def call(*args, &block)
|
14
|
+
@chain = build_chain if graph.changed? || !@chain
|
15
|
+
@chain.call *args, &block
|
31
16
|
end
|
32
17
|
|
33
|
-
def
|
34
|
-
|
18
|
+
def build_chain
|
19
|
+
graph.sort
|
20
|
+
graph.output.reverse.inject(pointcut) do |pointcut, aspect|
|
21
|
+
aspect.pointcut = pointcut; aspect
|
22
|
+
end
|
35
23
|
end
|
36
24
|
end
|
37
25
|
end
|
data/lib/hooked/version.rb
CHANGED
data/lib/hooked.rb
CHANGED
@@ -1,7 +1,38 @@
|
|
1
|
-
require "
|
2
|
-
|
3
|
-
require "hooked/container"
|
4
|
-
require "hooked/context"
|
5
|
-
require "hooked/controller"
|
1
|
+
require "hooked/aspect"
|
2
|
+
require "hooked/graph"
|
6
3
|
require "hooked/hook"
|
7
|
-
|
4
|
+
|
5
|
+
module Hooked
|
6
|
+
attr_reader :hooked
|
7
|
+
|
8
|
+
[:before, :after, :around].each do |type|
|
9
|
+
define_method type do |pointcut, *args|
|
10
|
+
hook! pointcut
|
11
|
+
hooked[pointcut].add_aspect type, *args
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def hook!(pointcut)
|
16
|
+
@hooked ||= {}
|
17
|
+
return if hooked[pointcut]
|
18
|
+
|
19
|
+
hooked[pointcut] = Hooked::Hook.new method(pointcut)
|
20
|
+
singleton_class.class_eval do
|
21
|
+
undef_method pointcut
|
22
|
+
define_method pointcut do |*args, &block|
|
23
|
+
hooked[pointcut].call *args, &block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def unhook!(pointcut)
|
29
|
+
return unless hooked && hooked[pointcut]
|
30
|
+
|
31
|
+
p = hooked[pointcut].pointcut
|
32
|
+
singleton_class.class_eval do
|
33
|
+
remove_method pointcut
|
34
|
+
define_method pointcut, &p
|
35
|
+
end
|
36
|
+
hooked.delete pointcut
|
37
|
+
end
|
38
|
+
end
|
data/spec/aspect_spec.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Hooked::Aspect do
|
4
|
+
describe "#initialize" do
|
5
|
+
it "sets type, advice and depencency list" do
|
6
|
+
type, advice = stub("type"), stub("advice")
|
7
|
+
deps = {:before => [:foo], :after => [:bar]}
|
8
|
+
obj = Hooked::Aspect.new type, advice, deps
|
9
|
+
|
10
|
+
obj.type.should == type
|
11
|
+
obj.advice.should == advice
|
12
|
+
obj.dependencies.should == deps
|
13
|
+
end
|
14
|
+
|
15
|
+
it "sets default dependency lists" do
|
16
|
+
obj = Hooked::Aspect.new nil, nil
|
17
|
+
obj.dependencies[:before].should == []
|
18
|
+
obj.dependencies[:after].should == []
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#call" do
|
23
|
+
order = []
|
24
|
+
advice = proc { order << :advice }
|
25
|
+
pointcut = proc { order << :pointcut }
|
26
|
+
|
27
|
+
before :each do
|
28
|
+
order.clear
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "with type == :before" do
|
32
|
+
it "calls the advice before the pointcut" do
|
33
|
+
obj = Hooked::Aspect.new :before, advice
|
34
|
+
obj.pointcut = pointcut
|
35
|
+
obj.call
|
36
|
+
|
37
|
+
order.should == [:advice, :pointcut]
|
38
|
+
end
|
39
|
+
|
40
|
+
it "manipulates the arguments" do
|
41
|
+
args_before, args_after = [:foo, 123], [456, :bar]
|
42
|
+
obj = Hooked::Aspect.new :before, mock("advice")
|
43
|
+
obj.pointcut = mock "pointcut"
|
44
|
+
|
45
|
+
obj.advice.should_receive(:call).with(*args_before).and_return args_after
|
46
|
+
obj.pointcut.should_receive(:call).with *args_after
|
47
|
+
|
48
|
+
obj.call *args_before
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "with type == :after" do
|
53
|
+
it "calls the advice after the pointcut" do
|
54
|
+
obj = Hooked::Aspect.new :after, advice
|
55
|
+
obj.pointcut = pointcut
|
56
|
+
obj.call
|
57
|
+
|
58
|
+
order.should == [:pointcut, :advice]
|
59
|
+
end
|
60
|
+
|
61
|
+
it "manipulates the return value" do
|
62
|
+
return_before, return_after = :foo, :bar
|
63
|
+
obj = Hooked::Aspect.new :after, mock("advice")
|
64
|
+
obj.pointcut = mock "pointcut"
|
65
|
+
|
66
|
+
obj.pointcut.stub(:call).and_return return_before
|
67
|
+
obj.advice.should_receive(:call).with(return_before).and_return return_after
|
68
|
+
|
69
|
+
obj.call.should == return_after
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "with type == :around" do
|
74
|
+
it "doesn't call the pointcut but passes it to the advice" do
|
75
|
+
advice, pointcut = mock("advice"), mock("pointcut")
|
76
|
+
obj = Hooked::Aspect.new :around, advice
|
77
|
+
obj.pointcut = pointcut
|
78
|
+
|
79
|
+
advice.should_receive(:call).with pointcut
|
80
|
+
pointcut.should_not_receive :call
|
81
|
+
|
82
|
+
obj.call
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "with unknown type" do
|
87
|
+
it "doesn't call the advice" do
|
88
|
+
obj = Hooked::Aspect.new :foo, stub("advice")
|
89
|
+
obj.pointcut = stub("pointcut")
|
90
|
+
|
91
|
+
obj.advice.should_not_receive :call
|
92
|
+
obj.pointcut.should_receive :call
|
93
|
+
|
94
|
+
obj.call
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "#before?" do
|
100
|
+
it "returns true if #type == :before" do
|
101
|
+
Hooked::Aspect.new(:before, nil).before?.should be_true
|
102
|
+
Hooked::Aspect.new(:foo, nil).before?.should_not be_true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "#after?" do
|
107
|
+
it "returns true if #type == :after" do
|
108
|
+
Hooked::Aspect.new(:after, nil).after?.should be_true
|
109
|
+
Hooked::Aspect.new(:foo, nil).after?.should_not be_true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "#around?" do
|
114
|
+
it "returns true if #type == :around" do
|
115
|
+
Hooked::Aspect.new(:around, nil).around?.should be_true
|
116
|
+
Hooked::Aspect.new(:foo, nil).around?.should_not be_true
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/spec/graph_spec.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Hooked::Graph do
|
4
|
+
describe "#initialize" do
|
5
|
+
it "starts with empty input" do
|
6
|
+
Hooked::Graph.new.input.should be_empty
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#<<" do
|
11
|
+
before :each do
|
12
|
+
@aspect, @graph = Hooked::Aspect.new(nil, nil), Hooked::Graph.new
|
13
|
+
@graph << @aspect
|
14
|
+
end
|
15
|
+
|
16
|
+
it "sets the changed flag" do
|
17
|
+
@graph.changed?.should be_true
|
18
|
+
end
|
19
|
+
|
20
|
+
it "adds the node to the input" do
|
21
|
+
@graph.input.last.should == @aspect
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#sort" do
|
26
|
+
it "removes the changed flag" do
|
27
|
+
graph = Hooked::Graph.new
|
28
|
+
graph << Hooked::Aspect.new(nil, mock("advice"))
|
29
|
+
graph.sort
|
30
|
+
graph.changed?.should == false
|
31
|
+
end
|
32
|
+
|
33
|
+
it "performs a topological sort" do
|
34
|
+
advices = (0..3).map {|i| mock "advice##{i}", :inspect => "advice##{i}" }
|
35
|
+
aspects = [
|
36
|
+
Hooked::Aspect.new(nil, advices[0], :after => [advices[2]]),
|
37
|
+
Hooked::Aspect.new(nil, advices[1], :after => [advices[0]]),
|
38
|
+
Hooked::Aspect.new(nil, advices[2], :before => [advices[3]]),
|
39
|
+
Hooked::Aspect.new(nil, advices[3], :before => [advices[1]])
|
40
|
+
]
|
41
|
+
|
42
|
+
graph = Hooked::Graph.new
|
43
|
+
aspects.each {|a| graph << a }
|
44
|
+
graph.sort
|
45
|
+
|
46
|
+
graph.output.should == [aspects[2], aspects[0], aspects[3], aspects[1]]
|
47
|
+
end
|
48
|
+
|
49
|
+
it "detects circular dependencies" do
|
50
|
+
advices = (0..3).map {|i| mock "advice##{i}", :inspect => "advice##{i}" }
|
51
|
+
aspects = [
|
52
|
+
Hooked::Aspect.new(nil, advices[0], :before => [advices[1]]),
|
53
|
+
Hooked::Aspect.new(nil, advices[1], :before => [advices[2]]),
|
54
|
+
Hooked::Aspect.new(nil, advices[2], :before => [advices[0]])
|
55
|
+
]
|
56
|
+
|
57
|
+
graph = Hooked::Graph.new
|
58
|
+
aspects.each {|a| graph << a }
|
59
|
+
proc do
|
60
|
+
graph.sort
|
61
|
+
end.should raise_error(Hooked::Graph::CircularDependencyError) {|error|
|
62
|
+
error.message =~ /failed: advice#0, advice#2, advice#1/
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|