hooked 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|