dagnabit 2.2.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.
Files changed (51) hide show
  1. data/.autotest +5 -0
  2. data/.document +5 -0
  3. data/.gitignore +6 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +186 -0
  6. data/Rakefile +69 -0
  7. data/VERSION.yml +5 -0
  8. data/bin/dagnabit-test +53 -0
  9. data/dagnabit.gemspec +124 -0
  10. data/init.rb +1 -0
  11. data/lib/dagnabit.rb +17 -0
  12. data/lib/dagnabit/activation.rb +60 -0
  13. data/lib/dagnabit/link/associations.rb +18 -0
  14. data/lib/dagnabit/link/class_methods.rb +43 -0
  15. data/lib/dagnabit/link/configuration.rb +40 -0
  16. data/lib/dagnabit/link/cycle_prevention.rb +31 -0
  17. data/lib/dagnabit/link/named_scopes.rb +65 -0
  18. data/lib/dagnabit/link/transitive_closure_link_model.rb +86 -0
  19. data/lib/dagnabit/link/transitive_closure_recalculation.rb +17 -0
  20. data/lib/dagnabit/link/transitive_closure_recalculation/on_create.rb +104 -0
  21. data/lib/dagnabit/link/transitive_closure_recalculation/on_destroy.rb +125 -0
  22. data/lib/dagnabit/link/transitive_closure_recalculation/on_update.rb +13 -0
  23. data/lib/dagnabit/link/transitive_closure_recalculation/utilities.rb +56 -0
  24. data/lib/dagnabit/link/validations.rb +26 -0
  25. data/lib/dagnabit/node/associations.rb +84 -0
  26. data/lib/dagnabit/node/class_methods.rb +74 -0
  27. data/lib/dagnabit/node/configuration.rb +26 -0
  28. data/lib/dagnabit/node/neighbors.rb +73 -0
  29. data/test/connections/native_postgresql/connection.rb +17 -0
  30. data/test/connections/native_sqlite3/connection.rb +24 -0
  31. data/test/dagnabit/link/test_associations.rb +61 -0
  32. data/test/dagnabit/link/test_class_methods.rb +102 -0
  33. data/test/dagnabit/link/test_configuration.rb +38 -0
  34. data/test/dagnabit/link/test_cycle_prevention.rb +64 -0
  35. data/test/dagnabit/link/test_named_scopes.rb +32 -0
  36. data/test/dagnabit/link/test_transitive_closure_link_model.rb +69 -0
  37. data/test/dagnabit/link/test_transitive_closure_recalculation.rb +139 -0
  38. data/test/dagnabit/link/test_validations.rb +39 -0
  39. data/test/dagnabit/node/test_associations.rb +147 -0
  40. data/test/dagnabit/node/test_class_methods.rb +49 -0
  41. data/test/dagnabit/node/test_configuration.rb +29 -0
  42. data/test/dagnabit/node/test_neighbors.rb +91 -0
  43. data/test/helper.rb +27 -0
  44. data/test/models/beta_node.rb +3 -0
  45. data/test/models/custom_data_link.rb +4 -0
  46. data/test/models/customized_link.rb +7 -0
  47. data/test/models/customized_link_node.rb +4 -0
  48. data/test/models/link.rb +4 -0
  49. data/test/models/node.rb +3 -0
  50. data/test/schema/schema.rb +51 -0
  51. metadata +165 -0
@@ -0,0 +1,5 @@
1
+ require 'redgreen/autotest'
2
+
3
+ Autotest.add_hook(:initialize) do |autotest|
4
+ autotest.libs += File::PATH_SEPARATOR + File.join(File.dirname(__FILE__), 'test/connections/native_sqlite3')
5
+ end
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ *.log
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 David Yip
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,186 @@
1
+ = This is the GitHub branch
2
+
3
+ This branch is updated irregularly. The canonical dagnabit repository is hosted on Gitorious:
4
+
5
+ http://gitorious.org/dagnabit/dagnabit
6
+
7
+ = dagnabit
8
+
9
+ dagnabit is (yet another) ActiveRecord plugin for directed acyclic graphs. It
10
+ provides an underlying representation that facilitates fast reachability
11
+ queries at the expense of edge insertion/deletion speed and execution time.
12
+
13
+ The data structure used by dagnabit is the structure described in:
14
+
15
+ DONG, G., LIBKIN, L., SU, J., and WONG, L. "Maintaining the
16
+ transitive closure of graphs in SQL." In Int. J. Information Technology, vol.
17
+ 5, 1999.
18
+
19
+ dagnabit was developed for a Ruby on Rails application designed for management
20
+ and querying of large-and-rapidly-growing biospecimen banks (i.e. for cancer
21
+ research). The application uses directed acyclic graphs for knowledge
22
+ representation (storage and application of workflows) and representing
23
+ biospecimen heritage.
24
+
25
+ dagnabit was developed at the Northwestern University Biomedical Informatics
26
+ Center <http://www.nucats.northwestern.edu/centers/nubic/index.html>.
27
+
28
+ == Related work
29
+
30
+ This plugin was inspired by Matthew Leventi's acts-as-dag plugin
31
+ <http://github.com/mleventi/acts-as-dag/tree/master>. Indeed, Leventi's
32
+ acts-as-dag plugin was originally used in the application from which this
33
+ plugin was extracted, and dagnabit's node and edge interfaces were modeled
34
+ after the interfaces provided by acts-as-dag.
35
+
36
+ The primary differences between dagnabit and acts-as-dag are:
37
+
38
+ * acts-as-dag does not permit linking of unpersisted nodes, i.e.
39
+
40
+ n1 = Node.new
41
+ n2 = Node.new
42
+ e1 = Link.new(:ancestor => n1, :descendant => n2)
43
+ ... other code ...
44
+
45
+ [n1, n2, e1].map { |o| o.save }
46
+
47
+ With acts-as-dag, one must save the nodes _before_ creating the edge.
48
+ The above code segment works in dagnabit.
49
+
50
+ * acts-as-dag issues O(x*y) queries on edge insert, update, and delete, where x
51
+ and y are the number of ancestors and descendants of a node. In environments
52
+ where network latency between database and application server is considerable,
53
+ this can adversely affect application performance.
54
+
55
+ dagnabit uses a constant number of queries for insert, update, and delete.
56
+ (The execution time and memory usage of queries are, of course, still
57
+ affected by the size of the transitive closure set being updated.)
58
+
59
+ == Database compatibility
60
+
61
+ dagnabit is only known to work with SQLite and PostgreSQL. Other databases may
62
+ work, but they have not been tested.
63
+
64
+ dagnabit is known to _not_ work on Oracle XE 10g databases via the ActiveRecord
65
+ oracle-enhanced adapter.
66
+
67
+ == Setup
68
+
69
+ dagnabit is distributed as a gem plugin; you can therefore install it as you
70
+ would any other RubyGem. Integration with your Rails application can be
71
+ achieved by adding dagnabit to your application's gem dependencies list.
72
+
73
+ Details for creating the link tables are discussed below.
74
+
75
+ In your link model, put
76
+
77
+ acts_as_dag_link
78
+
79
+ In your node models, put
80
+
81
+ acts_as_dag_node_linked_by (your link model name)
82
+
83
+ +acts_as_dag_link+ accepts some configuration options. These configuration
84
+ options are discussed below.
85
+
86
+ === Link table schemas
87
+
88
+ Link storage is accomplished with two tables having identical schemas. One
89
+ table holds explicitly specified links between nodes. The other table contains
90
+ the _transitive closure_ of all graphs, i.e. one tuple per path in the graph.
91
+ (The transitive closure table is therefore a superset of the direct link
92
+ table.)
93
+
94
+ The transitive closure table is automatically maintained by dagnabit and should
95
+ not be manually modified. The transitive closure table does not need to be
96
+ mapped to a model in your application.
97
+
98
+ The expected schema for both tables is:
99
+
100
+ t.integer :ancestor_id # or configured foreign key ID
101
+ t.integer :descendant_id # or configured foreign key ID
102
+ t.string :ancestor_type # not configurable
103
+ t.string :descendant_type # not configurable
104
+
105
+ The behavior of the plugin with a deviant schema is unspecified.
106
+
107
+ === Configuration options for +acts_as_dag_link+
108
+
109
+ * +transitive_closure_table_name+: The table where tuples in the transitive
110
+ closure of the graph should be stored. Defaults to
111
+ <tt>#{link_table_name}_transitive_closure_tuples</tt>.
112
+ * +descendant_id_column+: Column in the link tables for recording the ID of a
113
+ descendant. Defaults to +descendant_id+.
114
+ * +ancestor_id_column+: Column in the link tables for recording the ID of a
115
+ ancestor. Defaults to +ancestor_id+.
116
+
117
+ == Usage
118
+
119
+ The recommended usage pattern is (in pseudocode)
120
+
121
+ nodes = make_nodes
122
+ links = connect_up(nodes)
123
+ save(nodes)
124
+ save(links)
125
+
126
+ Node and edge creation is identical to creation of any ActiveRecord model. For
127
+ example, if your Node models were called +AlphaNode+ and +BetaNode+ and your
128
+ edge model was called +Link+, these would be valid expressions:
129
+
130
+ n1 = AlphaNode.new
131
+ n2 = BetaNode.new
132
+
133
+ e1 = Link.new(:ancestor => n1, :descendant => n2)
134
+ n1.save
135
+ n2.save
136
+ e1.save
137
+
138
+ === Node interface
139
+
140
+ +acts_as_dag_node+ adds the following associations to nodes:
141
+
142
+ * +links_as_child+: Links for which the node is a child.
143
+ * +links_as_parent+: Links for which the node is a parent.
144
+ * +links_as_ancestor+: Links for which the node is an ancestor. Read-only.
145
+ * +links_as_descendant+: Links for which the node is a descendant. Read-only.
146
+
147
+ +acts_as_dag_node+ adds the following instance methods to nodes:
148
+
149
+ * +descendants+: All descendants of the node. Read-only.
150
+ * +ancestors+: All ancestors of the node. Read-only.
151
+
152
+ === Link interface
153
+
154
+ The following class methods are available on links:
155
+
156
+ * +paths+: Retrieves all paths from one node to another.
157
+ * +path+: Returns whether a node is reachable from another node.
158
+ * +find_edge+: Finds an edge from one node to another, or nil if none exists.
159
+ * +build_edge+: Builds an edge from one node to another node.
160
+ * +connect+: Builds and saves an edge from one node to another. Identical to
161
+ <tt>build_edge(nodeA, nodeB).save</tt>.
162
+ * <tt>connect!</tt>: Builds and saves an edge from one node to another, raising
163
+ an exception if save fails. Identical to <tt>build_edge(nodeA,
164
+ nodeB).save!</tt>.
165
+
166
+ == Introduction to the codebase
167
+
168
+ Here are some starting points to become acquainted with the dagnabit codebase:
169
+
170
+ [Dagnabit::Activation]
171
+ How to mix this functionality into your models.
172
+ [Dagnabit::Link::Configuration]
173
+ Link configuration options.
174
+ [Dagnabit::Link::Associations]
175
+ Associations exposed on links.
176
+ [Dagnabit::Node::Associations]
177
+ Associations exposed on nodes.
178
+ [Dagnabit::Node::Neighbors]
179
+ How to locate a node's neighbors.
180
+ [Dagnabit::Link::TransitiveClosureRecalculation]
181
+ How the transitive closure is maintained.
182
+
183
+ == Copyright
184
+
185
+ Copyright (c) 2009 David Yip. Released under the MIT License; see LICENSE for
186
+ details.
@@ -0,0 +1,69 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ gem 'net-ssh', '>= 1.99.1'
5
+ require 'net/ssh'
6
+ require 'net/scp'
7
+ require 'yaml'
8
+
9
+ begin
10
+ require 'jeweler'
11
+ Jeweler::Tasks.new do |gem|
12
+ gem.name = "dagnabit"
13
+ gem.summary = %Q{Directed acyclic graph plugin for ActiveRecord}
14
+ gem.email = "yipdw@northwestern.edu"
15
+ gem.homepage = "http://gitorious.org/dagnabit/dagnabit"
16
+ gem.authors = ["David Yip"]
17
+ gem.add_dependency 'activerecord', '>= 2.1.0'
18
+ gem.add_dependency 'activesupport', '>= 2.3.2'
19
+ gem.add_development_dependency 'mocha', '= 0.9.8'
20
+ gem.add_development_dependency 'shoulda', '= 2.10.2'
21
+
22
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
23
+ end
24
+ rescue LoadError
25
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
26
+ end
27
+
28
+ task :test => [:test_sqlite3, :test_postgresql]
29
+
30
+ require 'rake/testtask'
31
+ ['sqlite3', 'postgresql'].map { |adapter| ["test_#{adapter}", "native_#{adapter}"] }.each do |task_name, adapter_name|
32
+ Rake::TestTask.new(task_name) do |test|
33
+ test.libs << 'lib' << 'test' << "test/connections/#{adapter_name}"
34
+ test.pattern = 'test/dagnabit/**/test_*.rb'
35
+ test.verbose = false
36
+ end
37
+ end
38
+
39
+ begin
40
+ require 'rcov/rcovtask'
41
+ Rcov::RcovTask.new do |test|
42
+ test.libs << 'test' << 'test/connections/native_sqlite3'
43
+ test.pattern = 'test/dagnabit/**/test_*.rb'
44
+ test.verbose = true
45
+ test.rcov_opts << '--exclude gems/*'
46
+ end
47
+ rescue LoadError
48
+ task :rcov do
49
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
50
+ end
51
+ end
52
+
53
+
54
+ task :default => :test
55
+
56
+ require 'rake/rdoctask'
57
+ Rake::RDocTask.new do |rdoc|
58
+ if File.exist?('VERSION.yml')
59
+ config = YAML.load(File.read('VERSION.yml'))
60
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
61
+ else
62
+ version = ""
63
+ end
64
+
65
+ rdoc.rdoc_dir = 'rdoc'
66
+ rdoc.title = "dagnabit #{version}"
67
+ rdoc.rdoc_files.include('README*')
68
+ rdoc.rdoc_files.include('lib/**/*.rb')
69
+ end
@@ -0,0 +1,5 @@
1
+ ---
2
+ :major: 2
3
+ :minor: 2
4
+ :patch: 1
5
+ :build:
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # Launches an irb session with an in-memory SQLite3 database and Node and Link
5
+ # ActiveRecord models.
6
+ #
7
+
8
+ require 'rubygems'
9
+ require 'activerecord'
10
+
11
+ src = File.dirname(__FILE__) + '/../lib/dagnabit.rb'
12
+ if File.exists?(src)
13
+ require src
14
+ else
15
+ require 'dagnabit'
16
+ end
17
+
18
+ require 'irb'
19
+
20
+ begin
21
+ require 'sqlite3'
22
+ rescue LoadError
23
+ gem 'sqlite3-ruby'
24
+ require 'sqlite3'
25
+ end
26
+
27
+ ActiveRecord::Base.logger = Logger.new("#{File.basename(__FILE__)}.log")
28
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
29
+
30
+ ActiveRecord::Schema.define do
31
+ [:edges, :edges_transitive_closure_tuples].each do |table|
32
+ create_table table do |t|
33
+ t.integer :ancestor_id
34
+ t.integer :descendant_id
35
+ t.string :ancestor_type
36
+ t.string :descendant_type
37
+ end
38
+ end
39
+
40
+ create_table :nodes do |t|
41
+ t.string :data
42
+ end
43
+ end
44
+
45
+ class Edge < ActiveRecord::Base
46
+ acts_as_dag_link
47
+ end
48
+
49
+ class Node < ActiveRecord::Base
50
+ acts_as_dag_node_linked_by 'Edge'
51
+ end
52
+
53
+ IRB.start
@@ -0,0 +1,124 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{dagnabit}
8
+ s.version = "2.2.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["David Yip"]
12
+ s.date = %q{2009-12-07}
13
+ s.default_executable = %q{dagnabit-test}
14
+ s.email = %q{yipdw@northwestern.edu}
15
+ s.executables = ["dagnabit-test"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE",
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".autotest",
22
+ ".document",
23
+ ".gitignore",
24
+ "LICENSE",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION.yml",
28
+ "bin/dagnabit-test",
29
+ "dagnabit.gemspec",
30
+ "init.rb",
31
+ "lib/dagnabit.rb",
32
+ "lib/dagnabit/activation.rb",
33
+ "lib/dagnabit/link/associations.rb",
34
+ "lib/dagnabit/link/class_methods.rb",
35
+ "lib/dagnabit/link/configuration.rb",
36
+ "lib/dagnabit/link/cycle_prevention.rb",
37
+ "lib/dagnabit/link/named_scopes.rb",
38
+ "lib/dagnabit/link/transitive_closure_link_model.rb",
39
+ "lib/dagnabit/link/transitive_closure_recalculation.rb",
40
+ "lib/dagnabit/link/transitive_closure_recalculation/on_create.rb",
41
+ "lib/dagnabit/link/transitive_closure_recalculation/on_destroy.rb",
42
+ "lib/dagnabit/link/transitive_closure_recalculation/on_update.rb",
43
+ "lib/dagnabit/link/transitive_closure_recalculation/utilities.rb",
44
+ "lib/dagnabit/link/validations.rb",
45
+ "lib/dagnabit/node/associations.rb",
46
+ "lib/dagnabit/node/class_methods.rb",
47
+ "lib/dagnabit/node/configuration.rb",
48
+ "lib/dagnabit/node/neighbors.rb",
49
+ "test/connections/native_postgresql/connection.rb",
50
+ "test/connections/native_sqlite3/connection.rb",
51
+ "test/dagnabit/link/test_associations.rb",
52
+ "test/dagnabit/link/test_class_methods.rb",
53
+ "test/dagnabit/link/test_configuration.rb",
54
+ "test/dagnabit/link/test_cycle_prevention.rb",
55
+ "test/dagnabit/link/test_named_scopes.rb",
56
+ "test/dagnabit/link/test_transitive_closure_link_model.rb",
57
+ "test/dagnabit/link/test_transitive_closure_recalculation.rb",
58
+ "test/dagnabit/link/test_validations.rb",
59
+ "test/dagnabit/node/test_associations.rb",
60
+ "test/dagnabit/node/test_class_methods.rb",
61
+ "test/dagnabit/node/test_configuration.rb",
62
+ "test/dagnabit/node/test_neighbors.rb",
63
+ "test/helper.rb",
64
+ "test/models/beta_node.rb",
65
+ "test/models/custom_data_link.rb",
66
+ "test/models/customized_link.rb",
67
+ "test/models/customized_link_node.rb",
68
+ "test/models/link.rb",
69
+ "test/models/node.rb",
70
+ "test/schema/schema.rb"
71
+ ]
72
+ s.homepage = %q{http://gitorious.org/dagnabit/dagnabit}
73
+ s.rdoc_options = ["--charset=UTF-8"]
74
+ s.require_paths = ["lib"]
75
+ s.rubygems_version = %q{1.3.5}
76
+ s.summary = %q{Directed acyclic graph plugin for ActiveRecord}
77
+ s.test_files = [
78
+ "test/connections/native_postgresql/connection.rb",
79
+ "test/connections/native_sqlite3/connection.rb",
80
+ "test/dagnabit/link/test_associations.rb",
81
+ "test/dagnabit/link/test_class_methods.rb",
82
+ "test/dagnabit/link/test_configuration.rb",
83
+ "test/dagnabit/link/test_cycle_prevention.rb",
84
+ "test/dagnabit/link/test_named_scopes.rb",
85
+ "test/dagnabit/link/test_transitive_closure_link_model.rb",
86
+ "test/dagnabit/link/test_transitive_closure_recalculation.rb",
87
+ "test/dagnabit/link/test_validations.rb",
88
+ "test/dagnabit/node/test_associations.rb",
89
+ "test/dagnabit/node/test_class_methods.rb",
90
+ "test/dagnabit/node/test_configuration.rb",
91
+ "test/dagnabit/node/test_neighbors.rb",
92
+ "test/helper.rb",
93
+ "test/models/beta_node.rb",
94
+ "test/models/custom_data_link.rb",
95
+ "test/models/customized_link.rb",
96
+ "test/models/customized_link_node.rb",
97
+ "test/models/link.rb",
98
+ "test/models/node.rb",
99
+ "test/schema/schema.rb"
100
+ ]
101
+
102
+ if s.respond_to? :specification_version then
103
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
104
+ s.specification_version = 3
105
+
106
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
107
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.1.0"])
108
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.3.2"])
109
+ s.add_development_dependency(%q<mocha>, ["= 0.9.8"])
110
+ s.add_development_dependency(%q<shoulda>, ["= 2.10.2"])
111
+ else
112
+ s.add_dependency(%q<activerecord>, [">= 2.1.0"])
113
+ s.add_dependency(%q<activesupport>, [">= 2.3.2"])
114
+ s.add_dependency(%q<mocha>, ["= 0.9.8"])
115
+ s.add_dependency(%q<shoulda>, ["= 2.10.2"])
116
+ end
117
+ else
118
+ s.add_dependency(%q<activerecord>, [">= 2.1.0"])
119
+ s.add_dependency(%q<activesupport>, [">= 2.3.2"])
120
+ s.add_dependency(%q<mocha>, ["= 0.9.8"])
121
+ s.add_dependency(%q<shoulda>, ["= 2.10.2"])
122
+ end
123
+ end
124
+