aquarium 0.1.0 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGES +53 -0
  2. data/README +20 -4
  3. data/Rakefile +19 -4
  4. data/UPGRADE +41 -1
  5. data/examples/aspect_design_example.rb +4 -1
  6. data/examples/aspect_design_example_spec.rb +41 -0
  7. data/examples/design_by_contract_example.rb +2 -3
  8. data/examples/design_by_contract_example_spec.rb +92 -0
  9. data/examples/method_missing_example.rb +1 -1
  10. data/examples/method_missing_example_spec.rb +59 -0
  11. data/examples/method_tracing_example.rb +4 -2
  12. data/examples/method_tracing_example_spec.rb +141 -0
  13. data/lib/aquarium/aspects/advice.rb +2 -2
  14. data/lib/aquarium/aspects/aspect.rb +20 -35
  15. data/lib/aquarium/aspects/dsl.rb +2 -1
  16. data/lib/aquarium/aspects/dsl/aspect_dsl.rb +12 -8
  17. data/lib/aquarium/aspects/dsl/object_dsl.rb +8 -0
  18. data/lib/aquarium/aspects/join_point.rb +16 -10
  19. data/lib/aquarium/aspects/pointcut.rb +3 -3
  20. data/lib/aquarium/extras/design_by_contract.rb +20 -11
  21. data/lib/aquarium/utils.rb +1 -0
  22. data/lib/aquarium/utils/method_utils.rb +41 -0
  23. data/lib/aquarium/utils/name_utils.rb +35 -0
  24. data/lib/aquarium/utils/type_utils.rb +9 -0
  25. data/lib/aquarium/version.rb +3 -5
  26. data/rake_tasks/examples.rake +1 -1
  27. data/spec/aquarium/aspects/aspect_invocation_spec.rb +30 -28
  28. data/spec/aquarium/aspects/aspect_spec.rb +90 -0
  29. data/spec/aquarium/aspects/concurrent_aspects_spec.rb +5 -3
  30. data/spec/aquarium/aspects/concurrent_aspects_with_objects_and_types_spec.rb +3 -1
  31. data/spec/aquarium/aspects/dsl/aspect_dsl_spec.rb +174 -110
  32. data/spec/aquarium/aspects/join_point_spec.rb +120 -19
  33. data/spec/aquarium/aspects/pointcut_spec.rb +24 -14
  34. data/spec/aquarium/extras/design_by_contract_spec.rb +3 -0
  35. data/spec/aquarium/finders/finder_result_spec.rb +1 -1
  36. data/spec/aquarium/finders/method_finder_spec.rb +3 -4
  37. data/spec/aquarium/spec_example_classes.rb +4 -0
  38. data/spec/aquarium/utils/method_utils_spec.rb +124 -1
  39. data/spec/aquarium/utils/name_utils_spec.rb +56 -0
  40. data/spec/aquarium/utils/type_utils_spec.rb +17 -0
  41. metadata +12 -4
  42. data/spec/aquarium/finders/method_sorting_spec.rb +0 -16
data/CHANGES CHANGED
@@ -1,3 +1,56 @@
1
+ == Version 0.1.5
2
+
3
+ Bug fixes:
4
+ 13514 Protected and private methods are made public when advised and left that way when unadvised
5
+ 13650 Loading Aquarium interferes with Rails filters
6
+ 13864 Bug with negative object_id
7
+
8
+ Enhancements:
9
+ 13392 Convert examples to specs.
10
+ 13463 Support running in JRuby
11
+
12
+ Fixing 13650 required an API change, which is why I've tagged this release "0.1.5" instead of
13
+ something like "0.1.1" (and the changes don't seem big enough to warrant "0.2.0"...).
14
+
15
+ Previously, requiring "aquarium.rb" in the top-level "lib" directory would implicitly require
16
+ lib/aquarium/aspects/dsl/aspect_dsl.rb, which
17
+ has Object include the AspectDSL module. This module adds methods like :before and :after to Object.
18
+ Unfortunately, those methods collide with methods of the same name that Rails adds to Object. It was
19
+ also a bit presumptuous of me to assume that everyone wanted those methods on Object ;)
20
+
21
+ In this release, aspect_dsl.rb is still implicitly included and it still defines the AspectDSL
22
+ module. Now, however, it does not include the AspectDSL module in Object. Instead, if you want this
23
+ behavior for all types, you must require the new lib/aquarium/aspects/dsl/object_dsl.rb explicitly.
24
+
25
+ As an alternative, if you just want the AspectDSL module included selectively in certain types,
26
+ then do the following:
27
+
28
+ class MyClass # reopen "MyClass"
29
+ # Add the methods as _class_ methods
30
+ include Aquarium::Aspects::DSL::AspectDSL
31
+ end
32
+
33
+ or, use (class|module)_eval:
34
+
35
+ require 'aquarium/aspects/dsl/aspect_dsl'
36
+
37
+ MyClass.class_eval do
38
+ # Add the methods as _class_ methods
39
+ include Aquarium::Aspects::DSL::AspectDSL
40
+ end
41
+
42
+ To add the methods as _instance_ methods on individual objects:
43
+
44
+ object = MyClass.new
45
+ object.extend(Aquarium::Aspects::DSL::AspectDSL)
46
+
47
+
48
+ Note: as discussed at http://practicalruby.blogspot.com/2007/02/reopen-with-moduleeval.html,
49
+ using "class_eval" or "module_eval" is safer that just reopening a class if
50
+ you're not sure that "MyClass" has actually been defined yet. However, in our particular case, it
51
+ probably doesn't matter, as AspectDSL doesn't change anything about the type, like aliasing existing
52
+ methods. Still, we can't guarantee that this won't change in the future.
53
+
1
54
  == Version 0.1.0
2
55
 
3
56
  This is the initial version.
data/README CHANGED
@@ -53,7 +53,7 @@ Many of AspectJ's behaviors that aren't currently supported are planned for futu
53
53
 
54
54
  Several complete examples are provided in the "examples" directory.
55
55
 
56
- In most cases, you can either declare the appropriate classes or use the optional DSL, which adds methods to Object.
56
+ In most cases, you can either declare the appropriate classes or use the optional DSL, which adds methods to classes, objects, or even Object itself.
57
57
 
58
58
  Here is an example that traces invocations of all public instance methods of the classes or modules Foo and Bar.
59
59
 
@@ -64,9 +64,9 @@ Here is an example that traces invocations of all public instance methods of the
64
64
  p "Leaving: #{execution_point.type.name}##{execution_point.method_name}"
65
65
  end
66
66
 
67
- The advice to execute at each join point is the block. The pointcut is the set of all public instance methods in Foo and Bar. (There are additional options available for specifying class methods, protected methods, etc.) Here is the same example using the convenience DSL that adds aspectual behavior to Object.
67
+ The advice to execute at each join point is the block. The pointcut is the set of all public instance methods in Foo and Bar. (There are additional options available for specifying class methods, protected methods, etc.) Here is the same example using the convenience DSL that adds aspect methods to Object (available only if you require aquarium/aspects/dsl/object_dsl', since other toolkits, like Rails, define similar methods on Object!).
68
68
 
69
- require 'aquarium'
69
+ require 'aquarium/aspects/dsl/object_dsl'
70
70
  around :types => [Foo, Bar], :methods => :all do |execution_point, *args|
71
71
  p "Entering: #{execution_point.type.name}##{execution_point.method_name}"
72
72
  execution_point.proceed
@@ -75,9 +75,25 @@ The advice to execute at each join point is the block. The pointcut is the set o
75
75
 
76
76
  See "examples/method_tracing_example.rb" for a more detailed version of this example.
77
77
 
78
+ If you don't want to add these methods to Object, you can also add them individually to modules, classes, or objects:
79
+
80
+ require 'aquarium/aspects/dsl/aspect_dsl'
81
+ ...
82
+ module MyModule
83
+ include Aquarium::Aspects::DSL::AspectDSL
84
+ end
85
+
86
+ class MyClass
87
+ include Aquarium::Aspects::DSL::AspectDSL
88
+ end
89
+
90
+ my_object = MyOtherClass.new
91
+ my_object.extend (Aquarium::Aspects::DSL::AspectDSL)
92
+
78
93
  If you use the DSL inside a class and omit the :type(s) and :object(s) options, "self" is assumed.
79
94
 
80
95
  class Foo
96
+ include Aquarium::Aspects::DSL::AspectDSL
81
97
  ...
82
98
  def critical_operation *args
83
99
  ...
@@ -92,7 +108,7 @@ If you use the DSL inside a class and omit the :type(s) and :object(s) options,
92
108
  end
93
109
  end
94
110
 
95
- Here are some more succinct examples, illustrating the API.
111
+ Here are some more succinct examples, illustrating the API (using the DSL methods).
96
112
 
97
113
  You can pass in pointcuts defined elsewhere:
98
114
 
data/Rakefile CHANGED
@@ -31,7 +31,7 @@ task :default => [:verify_rcov]
31
31
 
32
32
  desc "Run all specs"
33
33
  Spec::Rake::SpecTask.new do |t|
34
- t.spec_files = FileList['spec/**/*_spec.rb']
34
+ t.spec_files = FileList['spec/**/*_spec.rb', 'examples/**/*_spec.rb']
35
35
  t.spec_opts = ['--options', 'spec.opts']
36
36
  t.rcov = true
37
37
  t.rcov_dir = '../doc/output/coverage'
@@ -40,7 +40,7 @@ end
40
40
 
41
41
  desc "Run all specs and store html output in doc/output/report.html"
42
42
  Spec::Rake::SpecTask.new('spec_html') do |t|
43
- t.spec_files = FileList['spec/**/*_spec.rb']
43
+ t.spec_files = FileList['spec/**/*_spec.rb', 'examples/**/*_spec.rb']
44
44
  t.spec_opts = ['--format html:../doc/output/report.html','--backtrace']
45
45
  end
46
46
 
@@ -168,8 +168,10 @@ end
168
168
  desc "Upload Website to RubyForge"
169
169
  task :publish_website => [:verify_user, :website] do
170
170
  unless Aquarium::VERSION::RELEASE_CANDIDATE
171
+ # host = "aquarium-website@rubyforge.org"
172
+ host = "#{ENV['RUBYFORGE_USER']}@rubyforge.org"
171
173
  publisher = Rake::SshDirPublisher.new(
172
- "aquarium-website@rubyforge.org",
174
+ "#{host}",
173
175
  "/var/www/gforge-projects/#{PKG_NAME}",
174
176
  "../doc/output"
175
177
  )
@@ -179,6 +181,18 @@ task :publish_website => [:verify_user, :website] do
179
181
  end
180
182
  end
181
183
 
184
+ desc "Upload Website archive to RubyForge"
185
+ task :archive_website => [:verify_user, :website] do
186
+ # host = "aquarium-website@rubyforge.org"
187
+ host = "#{ENV['RUBYFORGE_USER']}@rubyforge.org"
188
+ publisher = Rake::SshDirPublisher.new(
189
+ "#{host}",
190
+ "/var/www/gforge-projects/#{PKG_NAME}/#{Spec::VERSION::TAG}",
191
+ "../doc/output"
192
+ )
193
+ publisher.upload
194
+ end
195
+
182
196
  desc "Publish gem+tgz+zip on RubyForge. You must make sure lib/version.rb is aligned with the CHANGELOG file"
183
197
  task :publish_packages => [:verify_user, :package] do
184
198
  release_files = FileList[
@@ -200,7 +214,8 @@ task :publish_packages => [:verify_user, :package] do
200
214
  puts "SINCE THIS IS A PRERELEASE, FILES ARE UPLOADED WITH SSH, NOT TO THE RUBYFORGE FILE SECTION"
201
215
  puts "YOU MUST TYPE THE PASSWORD #{release_files.length} TIMES..."
202
216
 
203
- host = "aquarium-website@rubyforge.org"
217
+ # host = "aquarium-website@rubyforge.org"
218
+ host = "#{ENV['RUBYFORGE_USER']}@rubyforge.org"
204
219
  remote_dir = "/var/www/gforge-projects/#{PKG_NAME}"
205
220
 
206
221
  publisher = Rake::SshFilePublisher.new(
data/UPGRADE CHANGED
@@ -1,3 +1,43 @@
1
- = Upgrading existing code to Aquarium-0.1.0
1
+ == Upgrading existing code to Aquarium-0.1.5
2
+
3
+ This is mostly a bug-fix release, but it did have to introduce one API change, as described in the
4
+ CHANGES. In particular, the aspect "DSL" methods are no longer automatically to Object, as some of
5
+ their names overlap with methods added by Rails.
6
+
7
+ Now, if you want these methods added to Object, you must require the new
8
+ lib/aquarium/aspects/dsl/object_dsl.rb explicitly.
9
+
10
+ As an alternative, if you just want these methods added selectively in certain types, then do the
11
+ following:
12
+
13
+ <ruby>
14
+ require 'aquarium/aspects/dsl/aspect_dsl'
15
+
16
+ class MyClass # reopen "MyClass"
17
+ # Add the methods as _class_ methods
18
+ include Aquarium::Aspects::DSL::AspectDSL
19
+ end
20
+ </ruby>
21
+
22
+ or, use (class|module)_eval:
23
+ <ruby>
24
+ require 'aquarium/aspects/dsl/aspect_dsl'
25
+
26
+ MyClass.class_eval do
27
+ # Add the methods as _class_ methods
28
+ include Aquarium::Aspects::DSL::AspectDSL
29
+ end
30
+ </ruby>
31
+
32
+ To add the methods as _instance_ methods on individual objects:
33
+
34
+ <ruby>
35
+ object = MyClass.new
36
+ object.extend(Aquarium::Aspects::DSL::AspectDSL)
37
+ </ruby>
38
+
39
+ See the CHANGES for more details.
40
+
41
+ == Upgrading existing code to Aquarium-0.1.0
2
42
 
3
43
  This is the first release of Aquarium.
@@ -12,6 +12,7 @@ require 'aquarium'
12
12
 
13
13
  module Aquarium
14
14
  class ClassWithStateAndBehavior
15
+ include Aquarium::Aspects::DSL::AspectDSL
15
16
  def initialize *args
16
17
  @state = args
17
18
  p "Initializing: #{args.inspect}"
@@ -24,9 +25,11 @@ module Aquarium
24
25
  end
25
26
  end
26
27
 
28
+ include Aquarium::Aspects
29
+
27
30
  # Observe state changes in the class, using the class-defined pointcut.
28
31
 
29
- observer = after :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, *args|
32
+ observer = Aspect.new :after, :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, *args|
30
33
  p "State has changed. "
31
34
  p " New state is #{jp.context.advised_object.state.inspect}"
32
35
  p " Equivalent to *args: #{args.inspect}"
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + '/../spec/aquarium/spec_helper.rb'
2
+ require 'aquarium'
3
+
4
+ # Example demonstrating emerging ideas about good aspect-oriented design. Specifically, this
5
+ # example follows ideas of Jonathan Aldrich on "Open Modules", where a "module" (in the generic
6
+ # sense of the word...) is responsible for defining and maintaining the pointcuts that it is
7
+ # willing to expose to potential aspects. Aspects are only allowed to advise the module through
8
+ # the pointcut. (Enforcing this constraint is TBD)
9
+ # Griswold, Sullivan, and collaborators have expanded on these ideas. See their IEEE Software,
10
+ # March 2006 paper.
11
+
12
+ module Aquarium
13
+ class ClassWithStateAndBehavior
14
+ include Aquarium::Aspects::DSL::AspectDSL
15
+ def initialize *args
16
+ @state = args
17
+ end
18
+ attr_accessor :state
19
+
20
+ # A simpler version of the following would be
21
+ # STATE_CHANGE = pointcut :method => :state
22
+ STATE_CHANGE = pointcut :attribute => :state, :attribute_options => :writer
23
+ end
24
+ end
25
+
26
+ include Aquarium::Aspects
27
+
28
+ describe "An example of an aspect using a class-defined pointcut." do
29
+ it "should observe state changes in the class." do
30
+ @new_state = nil
31
+ observer = Aspect.new :after, :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, *args|
32
+ @new_state = jp.context.advised_object.state
33
+ @new_state.should be_eql(*args)
34
+ end
35
+ object = Aquarium::ClassWithStateAndBehavior.new(:a1, :a2, :a3)
36
+ object.state = [:b1, :b2]
37
+ @new_state.should == [:b1, :b2]
38
+ observer.unadvise
39
+ end
40
+ end
41
+
@@ -3,9 +3,8 @@
3
3
  # specifying the contract of use for a class or module and testing it at runtime (usually
4
4
  # during the testing process)
5
5
  # This example is adapted from spec/extras/design_by_contract_spec.rb.
6
- # Note: the DesignByContract module uses the AspectDSL module. The #precondition, #postcondition,
7
- # and #invariant methods shown below delegate to AspectDSL methods. Those methods implicitly use
8
- # "self" as the :object to advise.
6
+ # Note: the DesignByContract module adds the #precondition, #postcondition, and #invariant
7
+ # methods shown below to Object and they use "self" as the :object to advise.
9
8
 
10
9
  $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
11
10
  require 'aquarium/extras/design_by_contract'
@@ -0,0 +1,92 @@
1
+ require File.dirname(__FILE__) + '/../spec/aquarium/spec_helper.rb'
2
+ require 'aquarium'
3
+ require 'aquarium/extras/design_by_contract'
4
+
5
+ # Example demonstrating "Design by Contract", Bertrand Meyer's idea for programmatically-
6
+ # specifying the contract of use for a class or module and testing it at runtime (usually
7
+ # during the testing process)
8
+ # This example is adapted from spec/extras/design_by_contract_spec.rb.
9
+ # Note: the DesignByContract module adds the #precondition, #postcondition, and #invariant
10
+ # methods shown below to Object and they use "self" as the :object to advise.
11
+
12
+ module Aquarium
13
+ class PreCondExample
14
+ def action *args
15
+ @state = *args
16
+ end
17
+ attr_reader :state
18
+
19
+ precondition :method => :action, :message => "Must pass more than one argument." do |jp, *args|
20
+ args.size > 0
21
+ end
22
+ end
23
+ end
24
+
25
+ describe "An example using a precondition" do
26
+ it "should fail at the call entry point if the precondition is not satisfied." do
27
+ lambda { Aquarium::PreCondExample.new.action }.should raise_error(Aquarium::Extras::DesignByContract::ContractError)
28
+ end
29
+ end
30
+
31
+ describe "An example using a precondition" do
32
+ it "should not fail at the call entry point if the precondition is satisfied." do
33
+ Aquarium::PreCondExample.new.action :a1
34
+ end
35
+ end
36
+
37
+ module Aquarium
38
+ class PostCondExample
39
+ def action *args
40
+ @state = *args
41
+ end
42
+ attr_reader :state
43
+
44
+ postcondition :method => :action,
45
+ :message => "Must pass more than one argument and first argument must be non-empty." do |jp, *args|
46
+ args.size > 0 && ! args[0].empty?
47
+ end
48
+ end
49
+ end
50
+
51
+ describe "An example using a postcondition" do
52
+ it "should fail at the call exit point if the postcondition is not satisfied." do
53
+ lambda { Aquarium::PostCondExample.new.action }.should raise_error(Aquarium::Extras::DesignByContract::ContractError)
54
+ lambda { Aquarium::PostCondExample.new.action "" }.should raise_error(Aquarium::Extras::DesignByContract::ContractError)
55
+ end
56
+ end
57
+
58
+ describe "An example using a postcondition" do
59
+ it "should not fail at the call entry point if the postcondition is satisfied." do
60
+ Aquarium::PostCondExample.new.action :a1
61
+ end
62
+ end
63
+
64
+ module Aquarium
65
+ class InvarCondExample
66
+ def initialize
67
+ @invar = 0
68
+ end
69
+ attr_reader :invar
70
+ def good_action
71
+ end
72
+ def bad_action
73
+ @invar = 1
74
+ end
75
+
76
+ invariant :methods => /action$/, :message => "Must not change the @invar value." do |jp, *args|
77
+ jp.context.advised_object.invar == 0
78
+ end
79
+ end
80
+ end
81
+
82
+ describe "An example using an invariant" do
83
+ it "should fail at the call entry or exit point if the invariant is not satisfied." do
84
+ lambda { Aquarium::InvarCondExample.new.bad_action }.should raise_error(Aquarium::Extras::DesignByContract::ContractError)
85
+ end
86
+ end
87
+
88
+ describe "An example using an invariant" do
89
+ it "should pass at the call entry and exit point if the invariant is satisfied." do
90
+ Aquarium::InvarCondExample.new.good_action
91
+ end
92
+ end
@@ -28,7 +28,7 @@ echo1.say "hello", "world!"
28
28
  echo1.log "something", "interesting..."
29
29
  echo1.shout "theater", "in", "a", "crowded", "firehouse!"
30
30
 
31
- around :type => Aquarium::Echo, :method => :method_missing do |join_point, sym, *args|
31
+ Aquarium::Aspects::Aspect.new :around, :type => Aquarium::Echo, :method => :method_missing do |join_point, sym, *args|
32
32
  if sym == :log
33
33
  p "--- Sending to log: #{args.join(" ")}"
34
34
  else
@@ -0,0 +1,59 @@
1
+ require File.dirname(__FILE__) + '/../spec/aquarium/spec_helper.rb'
2
+ require 'aquarium'
3
+
4
+ # Example demonstrating "around" advice for method_missing. This is a technique for
5
+ # avoiding collisions when different toolkits want to override method_missing in the
6
+ # same classes, e.g., Object. Using around advice as shown allows a toolkit to add
7
+ # custom behavior while invoking the "native" method_missing to handle unrecognized
8
+ # method calls.
9
+ # Note that it is essential to use around advice, not before or after advice, because
10
+ # neither can prevent the call to the "wrapped" method_missing, which is presumably
11
+ # not what you want.
12
+ # In this (contrived) example, an Echo class uses method_missing to simply echo
13
+ # the method name and arguments. An aspect is used to intercept any calls to a
14
+ # fictitious "log" method and handle those in a different way.
15
+
16
+ module Aquarium
17
+ class Echo
18
+ def method_missing sym, *args
19
+ @log ||= []
20
+ @log << "Echoing: #{sym.to_s}: #{args.join(" ")}"
21
+ end
22
+ def logged_messages; @log; end
23
+ end
24
+ end
25
+
26
+ describe "An example of a class' method_missing without around advice" do
27
+ it "should handle all invocations of method_missing." do
28
+ echo = Aquarium::Echo.new
29
+ echo.say "hello", "world!"
30
+ echo.log "something", "interesting..."
31
+ echo.shout "theater", "in", "a", "crowded", "firehouse!"
32
+ echo.logged_messages.size.should == 3
33
+ echo.logged_messages[0].should == "Echoing: say: hello world!"
34
+ echo.logged_messages[1].should == "Echoing: log: something interesting..."
35
+ echo.logged_messages[2].should == "Echoing: shout: theater in a crowded firehouse!"
36
+ end
37
+ end
38
+
39
+ describe "An example of a class' method_missing with around advice" do
40
+ it "should only handle invocations not processed by the around advice." do
41
+ @intercepted_message = nil
42
+ aspect = Aquarium::Aspects::Aspect.new :around, :type => Aquarium::Echo, :method => :method_missing do |join_point, sym, *args|
43
+ if sym == :log
44
+ @intercepted_message = "log: #{args.join(" ")}"
45
+ else
46
+ join_point.proceed
47
+ end
48
+ end
49
+ echo = Aquarium::Echo.new
50
+ echo.say "hello", "world!"
51
+ echo.log "something", "interesting..."
52
+ echo.shout "theater", "in", "a", "crowded", "firehouse!"
53
+ echo.logged_messages.size.should == 2
54
+ echo.logged_messages[0].should == "Echoing: say: hello world!"
55
+ echo.logged_messages[1].should == "Echoing: shout: theater in a crowded firehouse!"
56
+ @intercepted_message.should == "log: something interesting..."
57
+ aspect.unadvise
58
+ end
59
+ end