acts_as_ordered_tree 1.3.1 → 2.0.0.beta3
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 +4 -4
- data/lib/acts_as_ordered_tree.rb +22 -100
- data/lib/acts_as_ordered_tree/adapters.rb +17 -0
- data/lib/acts_as_ordered_tree/adapters/abstract.rb +23 -0
- data/lib/acts_as_ordered_tree/adapters/postgresql.rb +150 -0
- data/lib/acts_as_ordered_tree/adapters/recursive.rb +157 -0
- data/lib/acts_as_ordered_tree/compatibility.rb +22 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/association_scope.rb +9 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/default_scoped.rb +19 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/null_relation.rb +71 -0
- data/lib/acts_as_ordered_tree/compatibility/features.rb +153 -0
- data/lib/acts_as_ordered_tree/deprecate.rb +24 -0
- data/lib/acts_as_ordered_tree/hooks.rb +38 -0
- data/lib/acts_as_ordered_tree/hooks/update.rb +86 -0
- data/lib/acts_as_ordered_tree/instance_methods.rb +92 -453
- data/lib/acts_as_ordered_tree/iterators/arranger.rb +35 -0
- data/lib/acts_as_ordered_tree/iterators/level_calculator.rb +52 -0
- data/lib/acts_as_ordered_tree/iterators/orphans_pruner.rb +58 -0
- data/lib/acts_as_ordered_tree/node.rb +78 -0
- data/lib/acts_as_ordered_tree/node/attributes.rb +48 -0
- data/lib/acts_as_ordered_tree/node/movement.rb +62 -0
- data/lib/acts_as_ordered_tree/node/movements.rb +111 -0
- data/lib/acts_as_ordered_tree/node/predicates.rb +98 -0
- data/lib/acts_as_ordered_tree/node/reloading.rb +49 -0
- data/lib/acts_as_ordered_tree/node/siblings.rb +139 -0
- data/lib/acts_as_ordered_tree/node/traversals.rb +53 -0
- data/lib/acts_as_ordered_tree/persevering_transaction.rb +93 -0
- data/lib/acts_as_ordered_tree/position.rb +143 -0
- data/lib/acts_as_ordered_tree/relation/arrangeable.rb +33 -0
- data/lib/acts_as_ordered_tree/relation/iterable.rb +41 -0
- data/lib/acts_as_ordered_tree/relation/preloaded.rb +46 -11
- data/lib/acts_as_ordered_tree/transaction/base.rb +57 -0
- data/lib/acts_as_ordered_tree/transaction/callbacks.rb +67 -0
- data/lib/acts_as_ordered_tree/transaction/create.rb +68 -0
- data/lib/acts_as_ordered_tree/transaction/destroy.rb +34 -0
- data/lib/acts_as_ordered_tree/transaction/dsl.rb +214 -0
- data/lib/acts_as_ordered_tree/transaction/factory.rb +67 -0
- data/lib/acts_as_ordered_tree/transaction/move.rb +70 -0
- data/lib/acts_as_ordered_tree/transaction/passthrough.rb +12 -0
- data/lib/acts_as_ordered_tree/transaction/reorder.rb +42 -0
- data/lib/acts_as_ordered_tree/transaction/save.rb +64 -0
- data/lib/acts_as_ordered_tree/transaction/update.rb +78 -0
- data/lib/acts_as_ordered_tree/tree.rb +148 -0
- data/lib/acts_as_ordered_tree/tree/association.rb +20 -0
- data/lib/acts_as_ordered_tree/tree/callbacks.rb +57 -0
- data/lib/acts_as_ordered_tree/tree/children_association.rb +120 -0
- data/lib/acts_as_ordered_tree/tree/columns.rb +102 -0
- data/lib/acts_as_ordered_tree/tree/deprecated_columns_accessors.rb +24 -0
- data/lib/acts_as_ordered_tree/tree/parent_association.rb +31 -0
- data/lib/acts_as_ordered_tree/tree/perseverance.rb +19 -0
- data/lib/acts_as_ordered_tree/tree/scopes.rb +56 -0
- data/lib/acts_as_ordered_tree/validators.rb +1 -1
- data/lib/acts_as_ordered_tree/version.rb +1 -1
- data/spec/acts_as_ordered_tree_spec.rb +80 -909
- data/spec/adapters/postgresql_spec.rb +14 -0
- data/spec/adapters/recursive_spec.rb +12 -0
- data/spec/adapters/shared.rb +272 -0
- data/spec/callbacks_spec.rb +177 -0
- data/spec/counter_cache_spec.rb +31 -0
- data/spec/create_spec.rb +110 -0
- data/spec/destroy_spec.rb +57 -0
- data/spec/inheritance_spec.rb +176 -0
- data/spec/move_spec.rb +94 -0
- data/spec/node/movements/concurrent_movements_spec.rb +354 -0
- data/spec/node/movements/move_higher_spec.rb +46 -0
- data/spec/node/movements/move_lower_spec.rb +46 -0
- data/spec/node/movements/move_to_child_of_spec.rb +147 -0
- data/spec/node/movements/move_to_child_with_index_spec.rb +124 -0
- data/spec/node/movements/move_to_child_with_position_spec.rb +85 -0
- data/spec/node/movements/move_to_left_of_spec.rb +120 -0
- data/spec/node/movements/move_to_right_of_spec.rb +120 -0
- data/spec/node/movements/move_to_root_spec.rb +67 -0
- data/spec/node/predicates_spec.rb +211 -0
- data/spec/node/reloading_spec.rb +42 -0
- data/spec/node/siblings_spec.rb +193 -0
- data/spec/node/traversals_spec.rb +71 -0
- data/spec/persevering_transaction_spec.rb +98 -0
- data/spec/relation/arrangeable_spec.rb +88 -0
- data/spec/relation/iterable_spec.rb +104 -0
- data/spec/relation/preloaded_spec.rb +57 -0
- data/spec/reorder_spec.rb +83 -0
- data/spec/spec_helper.rb +30 -38
- data/spec/support/db/boot.rb +22 -0
- data/spec/{db → support/db}/config.travis.yml +2 -0
- data/spec/{db → support/db}/config.yml +1 -0
- data/spec/{db → support/db}/schema.rb +9 -0
- data/spec/support/factories.rb +2 -2
- data/spec/support/matchers.rb +67 -58
- data/spec/support/models.rb +6 -14
- data/spec/support/tree_factory.rb +315 -0
- data/spec/tree/children_association_spec.rb +72 -0
- data/spec/tree/columns_spec.rb +65 -0
- data/spec/tree/scopes_spec.rb +39 -0
- metadata +161 -43
- data/lib/acts_as_ordered_tree/adapters/postgresql_adapter.rb +0 -104
- data/lib/acts_as_ordered_tree/arrangeable.rb +0 -80
- data/lib/acts_as_ordered_tree/class_methods.rb +0 -72
- data/lib/acts_as_ordered_tree/relation/base.rb +0 -26
- data/lib/acts_as_ordered_tree/tenacious_transaction.rb +0 -30
- data/spec/concurrency_support_spec.rb +0 -156
data/spec/spec_helper.rb
CHANGED
@@ -1,62 +1,54 @@
|
|
1
1
|
ENV['DB'] ||= 'pg'
|
2
|
-
test_dir = File.dirname(__FILE__)
|
3
2
|
|
4
3
|
require 'rubygems'
|
5
4
|
require 'bundler/setup'
|
6
5
|
|
7
6
|
require 'rspec'
|
8
|
-
require 'rspec
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
require 'rspec/expectations'
|
8
|
+
|
9
|
+
if ENV['COVERAGE'].to_i.nonzero?
|
10
|
+
begin
|
11
|
+
require 'simplecov'
|
12
|
+
SimpleCov.command_name "rspec/#{File.basename(ENV['BUNDLE_GEMFILE'])}/#{ENV['DB']}"
|
13
|
+
SimpleCov.start 'test_frameworks' do
|
14
|
+
add_filter 'vendor/'
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
#ignore
|
18
|
+
end
|
15
19
|
end
|
16
20
|
|
17
|
-
require '
|
18
|
-
require 'factory_girl'
|
19
|
-
|
20
|
-
require 'acts_as_ordered_tree'
|
21
|
-
require 'logger'
|
22
|
-
require 'yaml'
|
23
|
-
require 'erb'
|
24
|
-
|
25
|
-
config_file = ENV['DBCONF'] || 'config.yml'
|
21
|
+
require 'support/db/boot'
|
26
22
|
|
27
|
-
|
28
|
-
ActiveRecord::Base.establish_connection(ENV['DB'])
|
29
|
-
ActiveRecord::Base.logger = Logger.new(ENV['DEBUG'] ? $stderr : '/dev/null')
|
30
|
-
ActiveRecord::Migration.verbose = false
|
31
|
-
I18n.enforce_available_locales = false if I18n.respond_to?(:enforce_available_locales=)
|
32
|
-
load(File.join(test_dir, 'db', 'schema.rb'))
|
33
|
-
|
34
|
-
require 'shoulda-matchers'
|
35
|
-
require 'support/models'
|
23
|
+
require 'factory_girl'
|
36
24
|
require 'support/factories'
|
37
25
|
require 'support/matchers'
|
26
|
+
require 'support/tree_factory'
|
27
|
+
require 'database_cleaner'
|
38
28
|
|
39
29
|
RSpec.configure do |config|
|
40
30
|
config.include FactoryGirl::Syntax::Methods
|
31
|
+
config.extend TreeFactory
|
41
32
|
|
42
33
|
config.treat_symbols_as_metadata_keys_with_true_values = true
|
43
34
|
|
44
35
|
config.around :each, :transactional do |example|
|
45
|
-
|
46
|
-
example.run
|
36
|
+
DatabaseCleaner.strategy = :transaction
|
47
37
|
|
48
|
-
|
49
|
-
|
38
|
+
DatabaseCleaner.start
|
39
|
+
|
40
|
+
example.run
|
41
|
+
|
42
|
+
DatabaseCleaner.clean
|
50
43
|
end
|
51
44
|
|
52
45
|
config.around :each, :non_transactional do |example|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
46
|
+
DatabaseCleaner.strategy = :truncation
|
47
|
+
|
48
|
+
DatabaseCleaner.start
|
49
|
+
|
50
|
+
example.run
|
51
|
+
|
52
|
+
DatabaseCleaner.clean
|
61
53
|
end
|
62
54
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This script establishes connection, creates DB schema and loads models definitions.
|
2
|
+
# Used by both rspec and cucumber
|
3
|
+
|
4
|
+
require 'acts_as_ordered_tree'
|
5
|
+
require 'active_record'
|
6
|
+
|
7
|
+
require 'logger'
|
8
|
+
require 'yaml'
|
9
|
+
require 'erb'
|
10
|
+
|
11
|
+
base_dir = File.dirname(__FILE__)
|
12
|
+
config_file = File.join(base_dir, ENV['DBCONF'] || 'config.yml')
|
13
|
+
|
14
|
+
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(config_file)).result)
|
15
|
+
ActiveRecord::Base.establish_connection(ENV['DB'].to_sym)
|
16
|
+
ActiveRecord::Base.logger = Logger.new(ENV['DEBUG'] ? $stderr : '/dev/null')
|
17
|
+
ActiveRecord::Migration.verbose = false
|
18
|
+
I18n.enforce_available_locales = false if I18n.respond_to?(:enforce_available_locales=)
|
19
|
+
|
20
|
+
load(File.join(base_dir, 'schema.rb'))
|
21
|
+
|
22
|
+
require File.join(base_dir, '..', 'models')
|
@@ -2,11 +2,13 @@ pg:
|
|
2
2
|
adapter: postgresql
|
3
3
|
username: postgres
|
4
4
|
database: acts_as_ordered_tree_test
|
5
|
+
allow_concurrency: true
|
5
6
|
min_messages: ERROR
|
6
7
|
mysql:
|
7
8
|
adapter: mysql2
|
8
9
|
database: acts_as_ordered_tree_test
|
9
10
|
username: travis
|
11
|
+
password:
|
10
12
|
encoding: utf8
|
11
13
|
sqlite3:
|
12
14
|
adapter: sqlite3
|
@@ -20,4 +20,13 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
20
20
|
t.column :parent_id, :integer
|
21
21
|
t.column :position, :integer
|
22
22
|
end
|
23
|
+
|
24
|
+
create_table :sti_examples, :force => true do |t|
|
25
|
+
t.column :type, :string, :null => false
|
26
|
+
t.column :name, :string
|
27
|
+
t.column :parent_id, :integer
|
28
|
+
t.column :position, :integer
|
29
|
+
t.column :depth, :integer
|
30
|
+
t.column :children_count, :integer
|
31
|
+
end
|
23
32
|
end
|
data/spec/support/factories.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
FactoryGirl.define do
|
2
|
-
factory :default do
|
2
|
+
factory :default, :aliases => [:node] do
|
3
3
|
sequence(:name) { |n| "category #{n}" }
|
4
4
|
end
|
5
5
|
|
@@ -7,7 +7,7 @@ FactoryGirl.define do
|
|
7
7
|
sequence(:name) { |n| "category #{n}" }
|
8
8
|
end
|
9
9
|
|
10
|
-
factory :
|
10
|
+
factory :default_without_depth do
|
11
11
|
sequence(:name) { |n| "category #{n}" }
|
12
12
|
end
|
13
13
|
|
data/spec/support/matchers.rb
CHANGED
@@ -1,14 +1,9 @@
|
|
1
1
|
module RSpec::Matchers
|
2
|
-
|
3
|
-
# it {
|
4
|
-
# it {
|
5
|
-
|
6
|
-
|
7
|
-
# it { should fire_callback(:around_move).when_calling(:save).exactly(2).times }
|
8
|
-
# it { should fire_callback(:around_move).when_calling(:save).once }
|
9
|
-
# it { should fire_callback(:around_move).when_calling(:save).twice }
|
10
|
-
def fire_callback(name)
|
11
|
-
FireCallbackMatcher.new(name)
|
2
|
+
# it { expect{...}.to query_database.once }
|
3
|
+
# it { expect{...}.to query_database.at_most(2).times }
|
4
|
+
# it { expect{...}.not_to query_database }
|
5
|
+
def query_database(regexp = nil)
|
6
|
+
QueryDatabaseMatcher.new(regexp)
|
12
7
|
end
|
13
8
|
|
14
9
|
# example { expect(record1, record2, record3).to be_sorted }
|
@@ -16,22 +11,11 @@ module RSpec::Matchers
|
|
16
11
|
OrderMatcher.new
|
17
12
|
end
|
18
13
|
|
19
|
-
class
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
@
|
24
|
-
@method = :save
|
25
|
-
@args = []
|
26
|
-
|
27
|
-
@limit_min = 1
|
28
|
-
@limit_max = nil
|
29
|
-
end
|
30
|
-
|
31
|
-
def when_calling(method, *args)
|
32
|
-
@method = method
|
33
|
-
@args = args
|
34
|
-
self
|
14
|
+
class QueryDatabaseMatcher
|
15
|
+
def initialize(regexp)
|
16
|
+
@min = nil
|
17
|
+
@max = nil
|
18
|
+
@regexp = regexp
|
35
19
|
end
|
36
20
|
|
37
21
|
def times
|
@@ -40,12 +24,12 @@ module RSpec::Matchers
|
|
40
24
|
alias time times
|
41
25
|
|
42
26
|
def at_least(times)
|
43
|
-
@
|
27
|
+
@min = times == :once ? 1 : times
|
44
28
|
self
|
45
29
|
end
|
46
30
|
|
47
31
|
def at_most(times)
|
48
|
-
@
|
32
|
+
@max = times == :once ? 1 : times
|
49
33
|
self
|
50
34
|
end
|
51
35
|
|
@@ -62,55 +46,80 @@ module RSpec::Matchers
|
|
62
46
|
exactly(2)
|
63
47
|
end
|
64
48
|
|
65
|
-
def
|
66
|
-
|
49
|
+
def matches?(subject)
|
50
|
+
record_queries { subject.call }
|
51
|
+
|
52
|
+
result = expected_queries_count.include?(@queries.size)
|
53
|
+
result &&= @queries.any? { |q| @regexp === q } if @regexp
|
54
|
+
result
|
67
55
|
end
|
68
56
|
|
69
|
-
def
|
70
|
-
|
57
|
+
def description
|
58
|
+
desc = 'query database'
|
59
|
+
|
60
|
+
if @min && !@max
|
61
|
+
desc << ' at least ' << human_readable_count(@min)
|
62
|
+
end
|
71
63
|
|
72
|
-
|
64
|
+
if @max && !@min
|
65
|
+
desc << ' at most ' << human_readable_count(@max)
|
66
|
+
end
|
73
67
|
|
74
|
-
|
75
|
-
@
|
68
|
+
if @min && @max && @min != @max
|
69
|
+
desc << " #{@min}..#{@max} times"
|
70
|
+
end
|
76
71
|
|
77
|
-
|
72
|
+
if @min && @max && @min == @max
|
73
|
+
desc << ' ' << human_readable_count(@min)
|
74
|
+
end
|
78
75
|
|
79
|
-
|
76
|
+
if @regexp
|
77
|
+
desc << ' and match ' << @regexp.to_s
|
80
78
|
end
|
79
|
+
|
80
|
+
desc
|
81
81
|
end
|
82
82
|
|
83
|
-
def failure_message_for_should
|
84
|
-
|
83
|
+
def failure_message_for_should(negative = false)
|
84
|
+
verb = negative ? 'not to' : 'to'
|
85
|
+
message = "expected given block #{verb} #{description}, but #{@queries.size} queries sent"
|
86
|
+
|
87
|
+
if @queries.any?
|
88
|
+
message << ":\n#{@queries.each_with_index.map { |q, i| "#{i+1}. #{q}"}.join("\n")}"
|
89
|
+
end
|
90
|
+
|
91
|
+
message
|
85
92
|
end
|
86
93
|
|
87
94
|
def failure_message_for_should_not
|
88
|
-
|
95
|
+
failure_message_for_should(true)
|
96
|
+
end
|
97
|
+
|
98
|
+
def supports_block_expectations?
|
99
|
+
true
|
89
100
|
end
|
90
101
|
|
91
102
|
private
|
92
|
-
def
|
93
|
-
|
103
|
+
def record_queries
|
104
|
+
@queries = []
|
94
105
|
|
95
|
-
|
106
|
+
subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |*, sql|
|
107
|
+
next if sql[:name] == 'SCHEMA'
|
96
108
|
|
97
|
-
|
98
|
-
|
99
|
-
@#{method_name} ||= 0
|
100
|
-
@#{method_name} += 1
|
101
|
-
yield if block_given?
|
102
|
-
end
|
103
|
-
#{kind}_#{name} :#{method_name}
|
104
|
-
CODE
|
109
|
+
@queries << sql[:sql]
|
110
|
+
end
|
105
111
|
|
106
|
-
|
112
|
+
yield
|
113
|
+
ensure
|
114
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
115
|
+
end
|
107
116
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
CODE
|
117
|
+
def expected_queries_count
|
118
|
+
((@min||1)..@max || 10000)
|
119
|
+
end
|
112
120
|
|
113
|
-
|
121
|
+
def human_readable_count(n)
|
122
|
+
n == 1 ? 'once' : "#{n} times"
|
114
123
|
end
|
115
124
|
end
|
116
125
|
|
@@ -118,7 +127,7 @@ module RSpec::Matchers
|
|
118
127
|
def matches?(*records)
|
119
128
|
@records = Array.wrap(records).flatten
|
120
129
|
|
121
|
-
@records.sort_by { |record| record.reload
|
130
|
+
@records.sort_by { |record| record.reload.ordered_tree_node.position } == @records
|
122
131
|
end
|
123
132
|
|
124
133
|
def failure_message_for_should
|
data/spec/support/models.rb
CHANGED
@@ -22,22 +22,10 @@ class DefaultWithCounterCache < ActiveRecord::Base
|
|
22
22
|
default_scope { where('1=1') }
|
23
23
|
end
|
24
24
|
|
25
|
-
class
|
25
|
+
class DefaultWithoutDepth < ActiveRecord::Base
|
26
26
|
self.table_name = 'categories'
|
27
27
|
|
28
|
-
acts_as_ordered_tree
|
29
|
-
|
30
|
-
default_scope { where('1=1') }
|
31
|
-
|
32
|
-
after_move :after_move
|
33
|
-
before_move :before_move
|
34
|
-
after_reorder :after_reorder
|
35
|
-
before_reorder :before_reorder
|
36
|
-
|
37
|
-
def after_move; end
|
38
|
-
def before_move; end
|
39
|
-
def after_reorder; end
|
40
|
-
def before_reorder; end
|
28
|
+
acts_as_ordered_tree :depth_column => false
|
41
29
|
end
|
42
30
|
|
43
31
|
class Scoped < ActiveRecord::Base
|
@@ -46,4 +34,8 @@ class Scoped < ActiveRecord::Base
|
|
46
34
|
default_scope { where('1=1') }
|
47
35
|
|
48
36
|
acts_as_ordered_tree :scope => :scope_type
|
37
|
+
end
|
38
|
+
|
39
|
+
class StiExample < ActiveRecord::Base
|
40
|
+
acts_as_ordered_tree :counter_cache => :children_count
|
49
41
|
end
|
@@ -0,0 +1,315 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
# @example
|
4
|
+
# describe Something do
|
5
|
+
# tree :factory => :my_model_factory, :attributes => {:scope_type => 'xxx'} do
|
6
|
+
# root {
|
7
|
+
# child_1
|
8
|
+
# child_2 {
|
9
|
+
# child_3 :name => 'a child'
|
10
|
+
# }
|
11
|
+
# }
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# it 'should have root' do
|
15
|
+
# expect(root).to have(2).children
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# it 'should move node' do
|
19
|
+
# child_2.move_higher
|
20
|
+
# expect(current_tree).to match_tree ->{
|
21
|
+
# root {
|
22
|
+
# child_2, :position => 1 do
|
23
|
+
# child_3 :name => 'a child'
|
24
|
+
# end
|
25
|
+
# child_1, :position => 2
|
26
|
+
# }
|
27
|
+
# }
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
module TreeFactory
|
31
|
+
module AbstractNode
|
32
|
+
attr_reader :parent
|
33
|
+
attr_accessor :position
|
34
|
+
|
35
|
+
def children
|
36
|
+
@children ||= []
|
37
|
+
end
|
38
|
+
|
39
|
+
def descendants
|
40
|
+
children.map { |child| [child] + child.descendants }.flatten
|
41
|
+
end
|
42
|
+
|
43
|
+
def self_and_descendants
|
44
|
+
[self] + descendants
|
45
|
+
end
|
46
|
+
|
47
|
+
def parent=(value)
|
48
|
+
@parent = value
|
49
|
+
|
50
|
+
if @parent
|
51
|
+
@parent.children << self
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def ancestors
|
56
|
+
parent ? parent.ancestors + [parent] : []
|
57
|
+
end
|
58
|
+
|
59
|
+
def level
|
60
|
+
ancestors.size
|
61
|
+
end
|
62
|
+
|
63
|
+
def indentation
|
64
|
+
' ' * 2 * level
|
65
|
+
end
|
66
|
+
|
67
|
+
def inspect_attributes
|
68
|
+
attributes.map do |k, v|
|
69
|
+
" / #{k} = #{v}"
|
70
|
+
end.join
|
71
|
+
end
|
72
|
+
|
73
|
+
def inspect_children
|
74
|
+
result = String.new
|
75
|
+
|
76
|
+
if children.any?
|
77
|
+
result << "\n"
|
78
|
+
result << children.map(&:inspect).join("\n")
|
79
|
+
end
|
80
|
+
|
81
|
+
result
|
82
|
+
end
|
83
|
+
|
84
|
+
def matches?(record)
|
85
|
+
record &&
|
86
|
+
record.level == level &&
|
87
|
+
record.ordered_tree_node.position == position &&
|
88
|
+
attributes_matches?(record) &&
|
89
|
+
record.children.size == children.size &&
|
90
|
+
children.zip(record.children).all? { |n, r| n.matches?(r) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def attributes_matches?(record)
|
94
|
+
attributes.all? do |attr, value|
|
95
|
+
if attr.is_a?(Symbol)
|
96
|
+
record.__send__(attr)
|
97
|
+
else
|
98
|
+
record.instance_eval(attr)
|
99
|
+
end == value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class Node < Struct.new(:name, :attributes)
|
105
|
+
include AbstractNode
|
106
|
+
|
107
|
+
attr_accessor :context
|
108
|
+
|
109
|
+
def inspect
|
110
|
+
if context
|
111
|
+
result = indentation
|
112
|
+
result << record_name
|
113
|
+
result << " / id = #{as_record.id}"
|
114
|
+
result << inspect_attributes
|
115
|
+
result << inspect_children
|
116
|
+
result
|
117
|
+
else
|
118
|
+
super
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def matches?(record)
|
123
|
+
record && record == as_record && super
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def as_record
|
128
|
+
@record ||= context.__send__(name)
|
129
|
+
end
|
130
|
+
|
131
|
+
def record_name
|
132
|
+
to_s = [:name, :to_str, :to_s].detect { |m| as_record.respond_to?(m) }
|
133
|
+
as_record.__send__(to_s)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class AnyNode < Struct.new(:attributes)
|
138
|
+
include AbstractNode
|
139
|
+
|
140
|
+
def inspect
|
141
|
+
result = indentation
|
142
|
+
result << '*'
|
143
|
+
result << inspect_attributes
|
144
|
+
result << inspect_children
|
145
|
+
result
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class Parser
|
150
|
+
attr_reader :ast
|
151
|
+
|
152
|
+
def initialize(options = {})
|
153
|
+
@attributes = options.fetch(:attributes, {})
|
154
|
+
@parent = nil
|
155
|
+
end
|
156
|
+
|
157
|
+
def parse(context, &tree)
|
158
|
+
@ast = []
|
159
|
+
@context = context
|
160
|
+
instance_exec(&tree)
|
161
|
+
ast
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
def with_parent(parent_node, &block)
|
166
|
+
old, @parent = @parent, parent_node
|
167
|
+
instance_exec(&block) if block_given?
|
168
|
+
ensure
|
169
|
+
@parent = old
|
170
|
+
end
|
171
|
+
|
172
|
+
def node(name, attributes = {}, &block)
|
173
|
+
build_node(block) do
|
174
|
+
Node.new(name.to_sym, @attributes.merge(attributes))
|
175
|
+
.tap { |x| x.context = @context }
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def any(attributes = {}, &block)
|
180
|
+
build_node(block) { AnyNode.new(@attributes.merge(attributes)) }
|
181
|
+
end
|
182
|
+
|
183
|
+
def method_missing(name, attributes = {}, &block)
|
184
|
+
node(name, attributes, &block)
|
185
|
+
end
|
186
|
+
|
187
|
+
def build_node(children_block)
|
188
|
+
node = yield
|
189
|
+
|
190
|
+
node.parent = @parent
|
191
|
+
|
192
|
+
if node.parent
|
193
|
+
node.position = node.parent.children.size
|
194
|
+
else
|
195
|
+
@ast << node
|
196
|
+
node.position = @ast.size
|
197
|
+
end
|
198
|
+
|
199
|
+
with_parent(node, &children_block)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
class Builder
|
204
|
+
attr_reader :factory
|
205
|
+
|
206
|
+
def initialize(test_suite, options)
|
207
|
+
@parser = Parser.new(options)
|
208
|
+
@suite = test_suite
|
209
|
+
@factory = options.fetch(:factory, @suite.described_class.name.underscore)
|
210
|
+
end
|
211
|
+
|
212
|
+
def build(&tree)
|
213
|
+
memoize_nodes_class
|
214
|
+
|
215
|
+
ast = @parser.parse(@suite, &tree)
|
216
|
+
ast.each { |o| build_node(o) }
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
def build_node(node)
|
221
|
+
factory = @factory
|
222
|
+
|
223
|
+
raise 'Cannot build ANY node in BEFORE section' if node.is_a?(AnyNode)
|
224
|
+
|
225
|
+
@suite.let!(node.name) do
|
226
|
+
parent = node.parent && send(node.parent.name)
|
227
|
+
|
228
|
+
create factory, node.attributes.merge(:parent => parent)
|
229
|
+
end
|
230
|
+
|
231
|
+
node.children.each { |o| build_node(o) }
|
232
|
+
end
|
233
|
+
|
234
|
+
def memoize_nodes_class
|
235
|
+
factory = FactoryGirl.factory_by_name(@factory)
|
236
|
+
@suite.let(:current_tree) { factory.build_class }
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Class level helper
|
241
|
+
def tree(options, &block)
|
242
|
+
Builder.new(self, options).build(&block)
|
243
|
+
end
|
244
|
+
|
245
|
+
def expect_tree_to_match(&block)
|
246
|
+
caller = caller(1).first
|
247
|
+
file, line, * = caller.split(':')
|
248
|
+
location = [file, line].join(':')
|
249
|
+
example = it { expect(current_tree).to match_tree(block) }
|
250
|
+
example.metadata[:location] = location
|
251
|
+
example.metadata[:file_path] = file
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
RSpec::Matchers.define :match_tree do |tree|
|
256
|
+
match do |tree_klass|
|
257
|
+
@expected_tree = tree
|
258
|
+
@klass = tree_klass
|
259
|
+
|
260
|
+
ast.zip(@klass.roots).all? { |node, record| node.matches?(record) }
|
261
|
+
end
|
262
|
+
|
263
|
+
failure_message_for_should do |tree_klass|
|
264
|
+
message = "expected actual tree\n\n"
|
265
|
+
message << inspect_actual_tree(tree_klass)
|
266
|
+
message << "\nto match\n\n"
|
267
|
+
message << ast.map(&:inspect).join("\n")
|
268
|
+
end
|
269
|
+
|
270
|
+
def ast
|
271
|
+
@ast ||= TreeFactory::Parser.new.parse(self, &@expected_tree)
|
272
|
+
end
|
273
|
+
|
274
|
+
def attributes_to_expose
|
275
|
+
@attributes_to_expose ||= flatten_ast.each_with_object(Set[:id]) do |node, attrs|
|
276
|
+
attrs.merge(node.attributes.keys)
|
277
|
+
end.to_a
|
278
|
+
end
|
279
|
+
|
280
|
+
def flatten_ast
|
281
|
+
ast.map(&:self_and_descendants).flatten
|
282
|
+
end
|
283
|
+
|
284
|
+
def inspect_actual_tree(tree_klass)
|
285
|
+
result = String.new
|
286
|
+
|
287
|
+
tree_klass.roots.each do |root|
|
288
|
+
root.self_and_descendants.each do |record|
|
289
|
+
result << "#{inspect_record(record)}\n"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
result
|
294
|
+
end
|
295
|
+
|
296
|
+
def inspect_record(record)
|
297
|
+
result = String.new
|
298
|
+
result << indentation(record.level)
|
299
|
+
result << record_name(record)
|
300
|
+
|
301
|
+
attributes_to_expose.each do |attr|
|
302
|
+
result << " / #{attr} = #{record.send(attr)}"
|
303
|
+
end
|
304
|
+
result
|
305
|
+
end
|
306
|
+
|
307
|
+
def record_name(record)
|
308
|
+
to_s = [:name, :to_str, :to_s].detect { |m| record.respond_to?(m) }
|
309
|
+
record.send(to_s)
|
310
|
+
end
|
311
|
+
|
312
|
+
def indentation(level)
|
313
|
+
' ' * level * 2
|
314
|
+
end
|
315
|
+
end
|