closure_tree 6.5.0 → 7.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +98 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +1 -1
  5. data/Appraisals +90 -7
  6. data/CHANGELOG.md +100 -42
  7. data/Gemfile +3 -11
  8. data/README.md +68 -24
  9. data/Rakefile +16 -10
  10. data/_config.yml +1 -0
  11. data/bin/appraisal +29 -0
  12. data/bin/rake +29 -0
  13. data/bin/rspec +29 -0
  14. data/closure_tree.gemspec +16 -9
  15. data/lib/closure_tree/finders.rb +32 -9
  16. data/lib/closure_tree/has_closure_tree.rb +4 -0
  17. data/lib/closure_tree/has_closure_tree_root.rb +5 -7
  18. data/lib/closure_tree/hash_tree_support.rb +4 -4
  19. data/lib/closure_tree/hierarchy_maintenance.rb +28 -8
  20. data/lib/closure_tree/model.rb +42 -16
  21. data/lib/closure_tree/numeric_deterministic_ordering.rb +20 -6
  22. data/lib/closure_tree/numeric_order_support.rb +7 -3
  23. data/lib/closure_tree/support.rb +18 -12
  24. data/lib/closure_tree/support_attributes.rb +10 -1
  25. data/lib/closure_tree/support_flags.rb +1 -4
  26. data/lib/closure_tree/version.rb +1 -1
  27. data/lib/generators/closure_tree/migration_generator.rb +8 -0
  28. data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +1 -1
  29. metadata +78 -79
  30. data/.travis.yml +0 -29
  31. data/gemfiles/activerecord_4.2.gemfile +0 -19
  32. data/gemfiles/activerecord_5.0.gemfile +0 -19
  33. data/gemfiles/activerecord_5.0_foreigner.gemfile +0 -20
  34. data/gemfiles/activerecord_edge.gemfile +0 -20
  35. data/img/example.png +0 -0
  36. data/img/preorder.png +0 -0
  37. data/spec/cache_invalidation_spec.rb +0 -39
  38. data/spec/cuisine_type_spec.rb +0 -38
  39. data/spec/db/database.yml +0 -21
  40. data/spec/db/models.rb +0 -128
  41. data/spec/db/schema.rb +0 -166
  42. data/spec/fixtures/tags.yml +0 -98
  43. data/spec/generators/migration_generator_spec.rb +0 -48
  44. data/spec/has_closure_tree_root_spec.rb +0 -154
  45. data/spec/hierarchy_maintenance_spec.rb +0 -16
  46. data/spec/label_spec.rb +0 -554
  47. data/spec/matcher_spec.rb +0 -34
  48. data/spec/metal_spec.rb +0 -55
  49. data/spec/model_spec.rb +0 -9
  50. data/spec/namespace_type_spec.rb +0 -13
  51. data/spec/parallel_spec.rb +0 -159
  52. data/spec/spec_helper.rb +0 -24
  53. data/spec/support/database.rb +0 -52
  54. data/spec/support/database_cleaner.rb +0 -14
  55. data/spec/support/exceed_query_limit.rb +0 -18
  56. data/spec/support/hash_monkey_patch.rb +0 -13
  57. data/spec/support/query_counter.rb +0 -18
  58. data/spec/support/sqlite3_with_advisory_lock.rb +0 -10
  59. data/spec/support_spec.rb +0 -14
  60. data/spec/tag_examples.rb +0 -665
  61. data/spec/tag_spec.rb +0 -6
  62. data/spec/user_spec.rb +0 -174
  63. data/spec/uuid_tag_spec.rb +0 -6
@@ -1,159 +0,0 @@
1
- require 'spec_helper'
2
-
3
- # We don't need to run the expensive parallel tests for every combination of prefix/suffix.
4
- # Those affect SQL generation, not parallelism.
5
- # SQLite doesn't support concurrency reliably, either.
6
- def run_parallel_tests?
7
- !sqlite? &&
8
- ActiveRecord::Base.table_name_prefix.empty? &&
9
- ActiveRecord::Base.table_name_suffix.empty?
10
- end
11
-
12
- def max_threads
13
- 5
14
- end
15
-
16
- class WorkerBase
17
- extend Forwardable
18
- attr_reader :name
19
- def_delegators :@thread, :join, :wakeup, :status, :to_s
20
-
21
- def log(msg)
22
- puts("#{Thread.current}: #{msg}") if ENV['VERBOSE']
23
- end
24
-
25
- def initialize(target, name)
26
- @target = target
27
- @name = name
28
- @thread = Thread.new do
29
- ActiveRecord::Base.connection_pool.with_connection { before_work } if respond_to? :before_work
30
- log 'going to sleep...'
31
- sleep
32
- log 'woke up...'
33
- ActiveRecord::Base.connection_pool.with_connection { work }
34
- log 'done.'
35
- end
36
- end
37
- end
38
-
39
- class FindOrCreateWorker < WorkerBase
40
- def work
41
- path = [name, :a, :b, :c]
42
- log "making #{path}..."
43
- t = (@target || Tag).find_or_create_by_path(path)
44
- log "made #{t.id}, #{t.ancestry_path}"
45
- end
46
- end
47
-
48
- describe 'Concurrent creation' do
49
- before :each do
50
- @target = nil
51
- @iterations = 5
52
- end
53
-
54
- def log(msg)
55
- puts(msg) if ENV['VERBOSE']
56
- end
57
-
58
- def run_workers(worker_class = FindOrCreateWorker)
59
- @names = @iterations.times.map { |iter| "iteration ##{iter}" }
60
- @names.each do |name|
61
- workers = max_threads.times.map { worker_class.new(@target, name) }
62
- # Wait for all the threads to get ready:
63
- while true
64
- unready_workers = workers.select { |ea| ea.status != 'sleep' }
65
- if unready_workers.empty?
66
- break
67
- else
68
- log "Not ready to wakeup: #{unready_workers.map { |ea| [ea.to_s, ea.status] }}"
69
- sleep(0.1)
70
- end
71
- end
72
- sleep(0.25)
73
- # OK, GO!
74
- log 'Calling .wakeup on all workers...'
75
- workers.each(&:wakeup)
76
- sleep(0.25)
77
- # Then wait for them to finish:
78
- log 'Calling .join on all workers...'
79
- workers.each(&:join)
80
- end
81
- # Ensure we're still connected:
82
- ActiveRecord::Base.connection_pool.connection
83
- end
84
-
85
- it 'will not create dupes from class methods' do
86
- run_workers
87
- expect(Tag.roots.collect { |ea| ea.name }).to match_array(@names)
88
- # No dupe children:
89
- %w(a b c).each do |ea|
90
- expect(Tag.where(name: ea).size).to eq(@iterations)
91
- end
92
- end
93
-
94
- it 'will not create dupes from instance methods' do
95
- @target = Tag.create!(name: 'root')
96
- run_workers
97
- expect(@target.reload.children.collect { |ea| ea.name }).to match_array(@names)
98
- expect(Tag.where(name: @names).size).to eq(@iterations)
99
- %w(a b c).each do |ea|
100
- expect(Tag.where(name: ea).size).to eq(@iterations)
101
- end
102
- end
103
-
104
- it 'creates dupe roots without advisory locks' do
105
- # disable with_advisory_lock:
106
- allow(Tag).to receive(:with_advisory_lock) { |_lock_name, &block| block.call }
107
- run_workers
108
- # duplication from at least one iteration:
109
- expect(Tag.where(name: @names).size).to be > @iterations
110
- end
111
-
112
- class SiblingPrependerWorker < WorkerBase
113
- def before_work
114
- @target.reload
115
- @sibling = Label.new(name: SecureRandom.hex(10))
116
- end
117
-
118
- def work
119
- @target.prepend_sibling @sibling
120
- end
121
- end
122
-
123
- it 'fails to deadlock while simultaneously deleting items from the same hierarchy' do
124
- target = User.find_or_create_by_path((1..200).to_a.map { |ea| ea.to_s })
125
- emails = target.self_and_ancestors.to_a.map(&:email).shuffle
126
- Parallel.map(emails, :in_threads => max_threads) do |email|
127
- ActiveRecord::Base.connection_pool.with_connection do
128
- User.transaction do
129
- log "Destroying #{email}..."
130
- User.where(email: email).destroy_all
131
- end
132
- end
133
- end
134
- User.connection.reconnect!
135
- expect(User.all).to be_empty
136
- end
137
-
138
- class SiblingPrependerWorker < WorkerBase
139
- def before_work
140
- @target.reload
141
- @sibling = Label.new(name: SecureRandom.hex(10))
142
- end
143
-
144
- def work
145
- @target.prepend_sibling @sibling
146
- end
147
- end
148
-
149
- it 'fails to deadlock from prepending siblings' do
150
- @target = Label.find_or_create_by_path %w(root parent)
151
- run_workers(SiblingPrependerWorker)
152
- children = Label.roots
153
- uniq_order_values = children.collect { |ea| ea.order_value }.uniq
154
- expect(children.size).to eq(uniq_order_values.size)
155
-
156
- # The only non-root node should be "root":
157
- expect(Label.all.select { |ea| ea.root? }).to eq([@target.parent])
158
- end
159
- end if run_parallel_tests?
data/spec/spec_helper.rb DELETED
@@ -1,24 +0,0 @@
1
- $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
2
-
3
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
4
- require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5
-
6
- require 'database_cleaner'
7
- require 'closure_tree/test/matcher'
8
- require 'tmpdir'
9
- require 'timecop'
10
- require 'forwardable'
11
- require 'parallel'
12
- begin
13
- require 'foreigner'
14
- rescue LoadError
15
- #Foreigner is not needed in ActiveRecord 4.2+
16
- end
17
-
18
- Thread.abort_on_exception = true
19
-
20
- Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
21
-
22
- RSpec.configure do |config|
23
- config.include ClosureTree::Test::Matcher
24
- end
@@ -1,52 +0,0 @@
1
- database_folder = "#{File.dirname(__FILE__)}/../db"
2
- database_adapter = ENV['DB'] ||= 'postgresql'
3
-
4
- def sqlite?
5
- ENV['DB'] == 'sqlite'
6
- end
7
-
8
- log = Logger.new('db.log')
9
- log.sev_threshold = Logger::DEBUG
10
- ActiveRecord::Base.logger = log
11
-
12
- ActiveRecord::Migration.verbose = false
13
- ActiveRecord::Base.table_name_prefix = ENV['DB_PREFIX'].to_s
14
- ActiveRecord::Base.table_name_suffix = ENV['DB_SUFFIX'].to_s
15
-
16
- def db_name
17
- @db_name ||= "closure_tree_test_#{rand(1..2**31)}"
18
- end
19
-
20
- ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read("#{database_folder}/database.yml")).result)
21
-
22
- config = ActiveRecord::Base.configurations[database_adapter]
23
-
24
- begin
25
- case database_adapter
26
- when 'sqlite'
27
- ActiveRecord::Base.establish_connection(database_adapter.to_sym)
28
- when 'mysql'
29
- ActiveRecord::Base.establish_connection(config.merge('database' => nil))
30
- ActiveRecord::Base.connection.recreate_database(config['database'], {charset: 'utf8', collation: 'utf8_unicode_ci'})
31
- when 'postgresql'
32
- ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
33
- ActiveRecord::Base.connection.recreate_database(config['database'], config.merge('encoding' => 'utf8'))
34
- end
35
- end unless ENV['NONUKES']
36
-
37
- ActiveRecord::Base.establish_connection(config)
38
- # Drop this when support for ActiveRecord 4.1 is removed
39
- Foreigner.load if defined?(Foreigner)
40
-
41
- require "#{database_folder}/schema"
42
- require "#{database_folder}/models"
43
-
44
- # See http://stackoverflow.com/a/22388177/1268016
45
- def count_queries(&block)
46
- count = 0
47
- counter_fn = ->(name, started, finished, unique_id, payload) do
48
- count += 1 unless %w[CACHE SCHEMA].include? payload[:name]
49
- end
50
- ActiveSupport::Notifications.subscribed(counter_fn, 'sql.active_record', &block)
51
- count
52
- end
@@ -1,14 +0,0 @@
1
- RSpec.configure do |config|
2
-
3
- DatabaseCleaner.strategy = :truncation
4
-
5
- config.before(:each) do
6
- ActiveRecord::Base.connection_pool.connection
7
- DatabaseCleaner.start
8
- end
9
-
10
- config.after(:each) do
11
- ActiveRecord::Base.connection_pool.connection
12
- DatabaseCleaner.clean
13
- end
14
- end
@@ -1,18 +0,0 @@
1
- # Derived from http://stackoverflow.com/a/13423584/153896. Updated for RSpec 3.
2
- RSpec::Matchers.define :exceed_query_limit do |expected|
3
- supports_block_expectations
4
-
5
- match do |block|
6
- query_count(&block) > expected
7
- end
8
-
9
- failure_message_when_negated do |actual|
10
- "Expected to run maximum #{expected} queries, got #{@counter.query_count}"
11
- end
12
-
13
- def query_count(&block)
14
- @counter = ActiveRecord::QueryCounter.new
15
- ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block)
16
- @counter.query_count
17
- end
18
- end
@@ -1,13 +0,0 @@
1
- class Hash
2
- def render_from_yield(&block)
3
- reduce({}) do |h, entry|
4
- k, v = entry
5
- h[block.call(k)] = if v.is_a?(Hash) then
6
- v.render_from_yield(&block)
7
- else
8
- block.call(v)
9
- end
10
- h
11
- end
12
- end
13
- end
@@ -1,18 +0,0 @@
1
- # From http://stackoverflow.com/a/13423584/153896
2
- module ActiveRecord
3
- class QueryCounter
4
- attr_reader :query_count
5
-
6
- def initialize
7
- @query_count = 0
8
- end
9
-
10
- def to_proc
11
- lambda(&method(:callback))
12
- end
13
-
14
- def callback(name, start, finish, message_id, values)
15
- @query_count += 1 unless %w(CACHE SCHEMA).include?(values[:name])
16
- end
17
- end
18
- end
@@ -1,10 +0,0 @@
1
- if sqlite?
2
- RSpec.configure do |config|
3
- config.before(:suite) do
4
- ENV['FLOCK_DIR'] = Dir.mktmpdir
5
- end
6
- config.after(:suite) do
7
- FileUtils.remove_entry_secure ENV['FLOCK_DIR']
8
- end
9
- end
10
- end
data/spec/support_spec.rb DELETED
@@ -1,14 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe ClosureTree::Support do
4
- let(:sut) { Tag._ct }
5
- it 'passes through table names without prefix and suffix' do
6
- expected = 'some_random_table_name'
7
- expect(sut.remove_prefix_and_suffix(expected)).to eq(expected)
8
- end
9
- it 'extracts through table name with prefix and suffix' do
10
- expected = 'some_random_table_name'
11
- tn = ActiveRecord::Base.table_name_prefix + expected + ActiveRecord::Base.table_name_suffix
12
- expect(sut.remove_prefix_and_suffix(tn)).to eq(expected)
13
- end
14
- end