double_entry 1.0.1 → 2.0.0.beta5
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 +5 -5
- data/CHANGELOG.md +497 -0
- data/README.md +107 -44
- data/double_entry.gemspec +22 -49
- data/lib/active_record/locking_extensions.rb +3 -3
- data/lib/active_record/locking_extensions/log_subscriber.rb +1 -1
- data/lib/double_entry.rb +29 -21
- data/lib/double_entry/account.rb +39 -46
- data/lib/double_entry/account_balance.rb +20 -3
- data/lib/double_entry/balance_calculator.rb +5 -5
- data/lib/double_entry/configurable.rb +1 -0
- data/lib/double_entry/configuration.rb +8 -2
- data/lib/double_entry/errors.rb +13 -13
- data/lib/double_entry/line.rb +7 -6
- data/lib/double_entry/locking.rb +5 -5
- data/lib/double_entry/transfer.rb +37 -30
- data/lib/double_entry/validation.rb +1 -0
- data/lib/double_entry/validation/account_fixer.rb +36 -0
- data/lib/double_entry/validation/line_check.rb +25 -43
- data/lib/double_entry/version.rb +1 -1
- data/lib/generators/double_entry/install/install_generator.rb +22 -1
- data/lib/generators/double_entry/install/templates/initializer.rb +20 -0
- data/lib/generators/double_entry/install/templates/migration.rb +45 -55
- metadata +35 -256
- data/.gitignore +0 -32
- data/.rspec +0 -2
- data/.travis.yml +0 -29
- data/.yardopts +0 -2
- data/Gemfile +0 -2
- data/Rakefile +0 -15
- data/lib/double_entry/reporting.rb +0 -181
- data/lib/double_entry/reporting/aggregate.rb +0 -110
- data/lib/double_entry/reporting/aggregate_array.rb +0 -76
- data/lib/double_entry/reporting/day_range.rb +0 -42
- data/lib/double_entry/reporting/hour_range.rb +0 -45
- data/lib/double_entry/reporting/line_aggregate.rb +0 -16
- data/lib/double_entry/reporting/line_aggregate_filter.rb +0 -79
- data/lib/double_entry/reporting/month_range.rb +0 -94
- data/lib/double_entry/reporting/time_range.rb +0 -59
- data/lib/double_entry/reporting/time_range_array.rb +0 -49
- data/lib/double_entry/reporting/week_range.rb +0 -107
- data/lib/double_entry/reporting/year_range.rb +0 -40
- data/script/jack_hammer +0 -210
- data/script/setup.sh +0 -8
- data/spec/active_record/locking_extensions_spec.rb +0 -110
- data/spec/double_entry/account_balance_spec.rb +0 -7
- data/spec/double_entry/account_spec.rb +0 -130
- data/spec/double_entry/balance_calculator_spec.rb +0 -88
- data/spec/double_entry/configuration_spec.rb +0 -50
- data/spec/double_entry/line_spec.rb +0 -80
- data/spec/double_entry/locking_spec.rb +0 -214
- data/spec/double_entry/performance/double_entry_performance_spec.rb +0 -32
- data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +0 -50
- data/spec/double_entry/reporting/aggregate_array_spec.rb +0 -123
- data/spec/double_entry/reporting/aggregate_spec.rb +0 -205
- data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +0 -90
- data/spec/double_entry/reporting/line_aggregate_spec.rb +0 -39
- data/spec/double_entry/reporting/month_range_spec.rb +0 -139
- data/spec/double_entry/reporting/time_range_array_spec.rb +0 -169
- data/spec/double_entry/reporting/time_range_spec.rb +0 -45
- data/spec/double_entry/reporting/week_range_spec.rb +0 -103
- data/spec/double_entry/reporting_spec.rb +0 -181
- data/spec/double_entry/transfer_spec.rb +0 -93
- data/spec/double_entry/validation/line_check_spec.rb +0 -99
- data/spec/double_entry_spec.rb +0 -428
- data/spec/generators/double_entry/install/install_generator_spec.rb +0 -30
- data/spec/spec_helper.rb +0 -118
- data/spec/support/accounts.rb +0 -21
- data/spec/support/blueprints.rb +0 -43
- data/spec/support/database.example.yml +0 -21
- data/spec/support/database.travis.yml +0 -24
- data/spec/support/double_entry_spec_helper.rb +0 -27
- data/spec/support/gemfiles/Gemfile.rails-3.2.x +0 -8
- data/spec/support/gemfiles/Gemfile.rails-4.1.x +0 -6
- data/spec/support/gemfiles/Gemfile.rails-4.2.x +0 -5
- data/spec/support/gemfiles/Gemfile.rails-5.0.x +0 -5
- data/spec/support/performance_helper.rb +0 -26
- data/spec/support/reporting_configuration.rb +0 -6
- data/spec/support/schema.rb +0 -74
@@ -1,88 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
RSpec.describe DoubleEntry::BalanceCalculator do
|
4
|
-
describe '#calculate' do
|
5
|
-
let(:account) { DoubleEntry.account(:test, :scope => scope) }
|
6
|
-
let(:scope) { User.make! }
|
7
|
-
let(:from) { nil }
|
8
|
-
let(:to) { nil }
|
9
|
-
let(:at) { nil }
|
10
|
-
let(:code) { nil }
|
11
|
-
let(:codes) { nil }
|
12
|
-
let(:relation) { double.as_null_object }
|
13
|
-
|
14
|
-
before do
|
15
|
-
allow(DoubleEntry::Line).to receive(:where).and_return(relation)
|
16
|
-
DoubleEntry::BalanceCalculator.calculate(
|
17
|
-
account,
|
18
|
-
:scope => scope,
|
19
|
-
:from => from,
|
20
|
-
:to => to,
|
21
|
-
:at => at,
|
22
|
-
:code => code,
|
23
|
-
:codes => codes,
|
24
|
-
)
|
25
|
-
end
|
26
|
-
|
27
|
-
describe 'what happens with different times' do
|
28
|
-
context 'when we want to sum the lines before a given created_at date' do
|
29
|
-
let(:at) { Time.parse('2014-06-19 15:09:18 +1000') }
|
30
|
-
|
31
|
-
it 'scopes the lines summed to times before (or at) the given time' do
|
32
|
-
expect(relation).to have_received(:where).with(
|
33
|
-
'created_at <= ?', Time.parse('2014-06-19 15:09:18 +1000')
|
34
|
-
)
|
35
|
-
end
|
36
|
-
|
37
|
-
context 'when a time range is also specified' do
|
38
|
-
let(:from) { Time.parse('2014-06-19 10:09:18 +1000') }
|
39
|
-
let(:to) { Time.parse('2014-06-19 20:09:18 +1000') }
|
40
|
-
|
41
|
-
it 'ignores the time range when summing the lines' do
|
42
|
-
expect(relation).to_not have_received(:where).with(
|
43
|
-
:created_at => Time.parse('2014-06-19 10:09:18 +1000')..Time.parse('2014-06-19 20:09:18 +1000'),
|
44
|
-
)
|
45
|
-
expect(relation).to_not have_received(:sum)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
context 'when we want to sum the lines between a given range' do
|
51
|
-
let(:from) { Time.parse('2014-06-19 10:09:18 +1000') }
|
52
|
-
let(:to) { Time.parse('2014-06-19 20:09:18 +1000') }
|
53
|
-
|
54
|
-
it 'scopes the lines summed to times within the given range' do
|
55
|
-
expect(relation).to have_received(:where).with(
|
56
|
-
:created_at => Time.parse('2014-06-19 10:09:18 +1000')..Time.parse('2014-06-19 20:09:18 +1000'),
|
57
|
-
)
|
58
|
-
expect(relation).to have_received(:sum).with(:amount)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
context 'when a single code is provided' do
|
64
|
-
let(:code) { 'code1' }
|
65
|
-
|
66
|
-
it 'scopes the lines summed by the given code' do
|
67
|
-
expect(relation).to have_received(:where).with(:code => ['code1'])
|
68
|
-
expect(relation).to have_received(:sum).with(:amount)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
context 'when a list of codes is provided' do
|
73
|
-
let(:codes) { %w(code1 code2) }
|
74
|
-
|
75
|
-
it 'scopes the lines summed by the given codes' do
|
76
|
-
expect(relation).to have_received(:where).with(:code => %w(code1 code2))
|
77
|
-
expect(relation).to have_received(:sum).with(:amount)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
context 'when no codes are provided' do
|
82
|
-
it 'does not scope the lines summed by any code' do
|
83
|
-
expect(relation).to_not have_received(:where).with(:code => anything)
|
84
|
-
expect(relation).to_not have_received(:sum).with(:amount)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
@@ -1,50 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
RSpec.describe DoubleEntry::Configuration do
|
3
|
-
its(:accounts) { should be_a DoubleEntry::Account::Set }
|
4
|
-
its(:transfers) { should be_a DoubleEntry::Transfer::Set }
|
5
|
-
|
6
|
-
describe 'max lengths' do
|
7
|
-
context 'given a max length has not been set' do
|
8
|
-
its(:code_max_length) { should be 47 }
|
9
|
-
its(:scope_identifier_max_length) { should be 23 }
|
10
|
-
its(:account_identifier_max_length) { should be 31 }
|
11
|
-
end
|
12
|
-
|
13
|
-
context 'given a code max length of 10 has been set' do
|
14
|
-
before { subject.code_max_length = 10 }
|
15
|
-
its(:code_max_length) { should be 10 }
|
16
|
-
end
|
17
|
-
|
18
|
-
context 'given a scope identifier max length of 11 has been set' do
|
19
|
-
before { subject.scope_identifier_max_length = 11 }
|
20
|
-
its(:scope_identifier_max_length) { should be 11 }
|
21
|
-
end
|
22
|
-
|
23
|
-
context 'given an account identifier max length of 9 has been set' do
|
24
|
-
before { subject.account_identifier_max_length = 9 }
|
25
|
-
its(:account_identifier_max_length) { should be 9 }
|
26
|
-
end
|
27
|
-
|
28
|
-
after do
|
29
|
-
subject.code_max_length = nil
|
30
|
-
subject.scope_identifier_max_length = nil
|
31
|
-
subject.account_identifier_max_length = nil
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
describe '#define_accounts' do
|
36
|
-
it 'yields the accounts set' do
|
37
|
-
expect do |block|
|
38
|
-
subject.define_accounts(&block)
|
39
|
-
end.to yield_with_args(be_a DoubleEntry::Account::Set)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
describe '#define_transfers' do
|
44
|
-
it 'yields the transfers set' do
|
45
|
-
expect do |block|
|
46
|
-
subject.define_transfers(&block)
|
47
|
-
end.to yield_with_args(be_a DoubleEntry::Transfer::Set)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
@@ -1,80 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
RSpec.describe DoubleEntry::Line do
|
3
|
-
it 'has a table name prefixed with double_entry_' do
|
4
|
-
expect(DoubleEntry::Line.table_name).to eq 'double_entry_lines'
|
5
|
-
end
|
6
|
-
|
7
|
-
describe 'persistance' do
|
8
|
-
let(:line_to_persist) do
|
9
|
-
DoubleEntry::Line.new(
|
10
|
-
:amount => Money.new(10_00),
|
11
|
-
:balance => Money.zero,
|
12
|
-
:account => account,
|
13
|
-
:partner_account => partner_account,
|
14
|
-
:code => code,
|
15
|
-
)
|
16
|
-
end
|
17
|
-
let(:account) { DoubleEntry.account(:test, :scope => '17') }
|
18
|
-
let(:partner_account) { DoubleEntry.account(:test, :scope => '72') }
|
19
|
-
let(:code) { :test_code }
|
20
|
-
|
21
|
-
subject(:persisted_line) do
|
22
|
-
line_to_persist.save!
|
23
|
-
line_to_persist.reload
|
24
|
-
end
|
25
|
-
|
26
|
-
describe 'attributes' do
|
27
|
-
context 'given code = :the_code' do
|
28
|
-
let(:code) { :the_code }
|
29
|
-
its(:code) { should eq :the_code }
|
30
|
-
end
|
31
|
-
|
32
|
-
context 'given code = nil' do
|
33
|
-
let(:code) { nil }
|
34
|
-
specify { expect { line_to_persist.save! }.to raise_error }
|
35
|
-
end
|
36
|
-
|
37
|
-
context 'given account = :test, 54 ' do
|
38
|
-
let(:account) { DoubleEntry.account(:test, :scope => '54') }
|
39
|
-
its('account.account.identifier') { should eq :test }
|
40
|
-
its('account.scope') { should eq '54' }
|
41
|
-
end
|
42
|
-
|
43
|
-
context 'given partner_account = :test, 91 ' do
|
44
|
-
let(:partner_account) { DoubleEntry.account(:test, :scope => '91') }
|
45
|
-
its('partner_account.account.identifier') { should eq :test }
|
46
|
-
its('partner_account.scope') { should eq '91' }
|
47
|
-
end
|
48
|
-
|
49
|
-
context 'currency' do
|
50
|
-
let(:account) { DoubleEntry.account(:btc_test, :scope => '17') }
|
51
|
-
let(:partner_account) { DoubleEntry.account(:btc_test, :scope => '72') }
|
52
|
-
its(:currency) { should eq 'BTC' }
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
context 'when balance is sent negative' do
|
57
|
-
before { DoubleEntry::Account.accounts.define(:identifier => :a_positive_only_acc, :positive_only => true) }
|
58
|
-
let(:account) { DoubleEntry.account(:a_positive_only_acc) }
|
59
|
-
let(:line) { DoubleEntry::Line.new(:balance => Money.new(-1), :account => account) }
|
60
|
-
|
61
|
-
it 'raises AccountWouldBeSentNegative error' do
|
62
|
-
expect { line.save }.to raise_error DoubleEntry::AccountWouldBeSentNegative
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
context 'when balance is sent positive' do
|
67
|
-
before { DoubleEntry::Account.accounts.define(:identifier => :a_negative_only_acc, :negative_only => true) }
|
68
|
-
let(:account) { DoubleEntry.account(:a_negative_only_acc) }
|
69
|
-
let(:line) { DoubleEntry::Line.new(:balance => Money.new(1), :account => account) }
|
70
|
-
|
71
|
-
it 'raises AccountWouldBeSentPositiveError' do
|
72
|
-
expect { line.save }.to raise_error DoubleEntry::AccountWouldBeSentPositiveError
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
it 'has a table name prefixed with double_entry_' do
|
77
|
-
expect(DoubleEntry::Line.table_name).to eq 'double_entry_lines'
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
@@ -1,214 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
RSpec.describe DoubleEntry::Locking do
|
4
|
-
before do
|
5
|
-
@config_accounts = DoubleEntry.configuration.accounts
|
6
|
-
@config_transfers = DoubleEntry.configuration.transfers
|
7
|
-
DoubleEntry.configuration.accounts = DoubleEntry::Account::Set.new
|
8
|
-
DoubleEntry.configuration.transfers = DoubleEntry::Transfer::Set.new
|
9
|
-
end
|
10
|
-
|
11
|
-
after do
|
12
|
-
DoubleEntry.configuration.accounts = @config_accounts
|
13
|
-
DoubleEntry.configuration.transfers = @config_transfers
|
14
|
-
end
|
15
|
-
|
16
|
-
before do
|
17
|
-
scope = ->(x) { x }
|
18
|
-
|
19
|
-
DoubleEntry.configure do |config|
|
20
|
-
config.define_accounts do |accounts|
|
21
|
-
accounts.define(:identifier => :account_a, :scope_identifier => scope)
|
22
|
-
accounts.define(:identifier => :account_b, :scope_identifier => scope)
|
23
|
-
accounts.define(:identifier => :account_c, :scope_identifier => scope)
|
24
|
-
accounts.define(:identifier => :account_d, :scope_identifier => scope)
|
25
|
-
accounts.define(:identifier => :account_e)
|
26
|
-
end
|
27
|
-
|
28
|
-
config.define_transfers do |transfers|
|
29
|
-
transfers.define(:from => :account_a, :to => :account_b, :code => :test)
|
30
|
-
transfers.define(:from => :account_c, :to => :account_d, :code => :test)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
@account_a = DoubleEntry.account(:account_a, :scope => '1')
|
35
|
-
@account_b = DoubleEntry.account(:account_b, :scope => '2')
|
36
|
-
@account_c = DoubleEntry.account(:account_c, :scope => '3')
|
37
|
-
@account_d = DoubleEntry.account(:account_d, :scope => '4')
|
38
|
-
@account_e = DoubleEntry.account(:account_e)
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'creates missing account balance records' do
|
42
|
-
expect do
|
43
|
-
DoubleEntry::Locking.lock_accounts(@account_a) {}
|
44
|
-
end.to change(DoubleEntry::AccountBalance, :count).by(1)
|
45
|
-
|
46
|
-
account_balance = DoubleEntry::AccountBalance.find_by_account(@account_a)
|
47
|
-
expect(account_balance).to_not be_nil
|
48
|
-
expect(account_balance.balance).to eq Money.new(0)
|
49
|
-
end
|
50
|
-
|
51
|
-
it 'takes the balance for new account balance records from the lines table' do
|
52
|
-
DoubleEntry::Line.create!(
|
53
|
-
:account => @account_a,
|
54
|
-
:partner_account => @account_b,
|
55
|
-
:amount => Money.new(3_00),
|
56
|
-
:balance => Money.new(3_00),
|
57
|
-
:code => :test,
|
58
|
-
)
|
59
|
-
DoubleEntry::Line.create!(
|
60
|
-
:account => @account_a,
|
61
|
-
:partner_account => @account_b,
|
62
|
-
:amount => Money.new(7_00),
|
63
|
-
:balance => Money.new(10_00),
|
64
|
-
:code => :test,
|
65
|
-
)
|
66
|
-
|
67
|
-
expect do
|
68
|
-
DoubleEntry::Locking.lock_accounts(@account_a) {}
|
69
|
-
end.to change(DoubleEntry::AccountBalance, :count).by(1)
|
70
|
-
|
71
|
-
account_balance = DoubleEntry::AccountBalance.find_by_account(@account_a)
|
72
|
-
expect(account_balance).to_not be_nil
|
73
|
-
expect(account_balance.balance).to eq Money.new(10_00)
|
74
|
-
end
|
75
|
-
|
76
|
-
it 'prohibits locking inside a regular transaction' do
|
77
|
-
expect do
|
78
|
-
DoubleEntry::AccountBalance.transaction do
|
79
|
-
DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end.to raise_error(DoubleEntry::Locking::LockMustBeOutermostTransaction)
|
83
|
-
end
|
84
|
-
|
85
|
-
it 'prohibits a transfer inside a regular transaction' do
|
86
|
-
expect do
|
87
|
-
DoubleEntry::AccountBalance.transaction do
|
88
|
-
DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
|
89
|
-
end
|
90
|
-
end.to raise_error(DoubleEntry::Locking::LockMustBeOutermostTransaction)
|
91
|
-
end
|
92
|
-
|
93
|
-
it "allows a transfer inside a lock if we've locked the transaction accounts" do
|
94
|
-
expect do
|
95
|
-
DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
|
96
|
-
DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
|
97
|
-
end
|
98
|
-
end.to_not raise_error
|
99
|
-
end
|
100
|
-
|
101
|
-
it "does not allow a transfer inside a lock if the right locks aren't held" do
|
102
|
-
expect do
|
103
|
-
DoubleEntry::Locking.lock_accounts(@account_a, @account_c) do
|
104
|
-
DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
|
105
|
-
end
|
106
|
-
end.to raise_error(DoubleEntry::Locking::LockNotHeld, 'No lock held for account: account_b, scope 2')
|
107
|
-
end
|
108
|
-
|
109
|
-
it 'allows nested locks if the outer lock locks all the accounts' do
|
110
|
-
expect do
|
111
|
-
DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
|
112
|
-
DoubleEntry::Locking.lock_accounts(@account_a, @account_b) {}
|
113
|
-
end
|
114
|
-
end.to_not raise_error
|
115
|
-
end
|
116
|
-
|
117
|
-
it "prohibits nested locks if the out lock doesn't lock all the accounts" do
|
118
|
-
expect do
|
119
|
-
DoubleEntry::Locking.lock_accounts(@account_a) do
|
120
|
-
DoubleEntry::Locking.lock_accounts(@account_a, @account_b) {}
|
121
|
-
end
|
122
|
-
end.to raise_error(DoubleEntry::Locking::LockNotHeld, 'No lock held for account: account_b, scope 2')
|
123
|
-
end
|
124
|
-
|
125
|
-
it 'rolls back a locking transaction' do
|
126
|
-
DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
|
127
|
-
DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
|
128
|
-
fail ActiveRecord::Rollback
|
129
|
-
end
|
130
|
-
expect(DoubleEntry.balance(@account_a)).to eq Money.new(0)
|
131
|
-
expect(DoubleEntry.balance(@account_b)).to eq Money.new(0)
|
132
|
-
end
|
133
|
-
|
134
|
-
it "rolls back a locking transaction if there's an exception" do
|
135
|
-
expect do
|
136
|
-
DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
|
137
|
-
DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
|
138
|
-
fail 'Yeah, right'
|
139
|
-
end
|
140
|
-
end.to raise_error('Yeah, right')
|
141
|
-
expect(DoubleEntry.balance(@account_a)).to eq Money.new(0)
|
142
|
-
expect(DoubleEntry.balance(@account_b)).to eq Money.new(0)
|
143
|
-
end
|
144
|
-
|
145
|
-
it 'allows locking a scoped account and a non scoped account' do
|
146
|
-
expect do
|
147
|
-
DoubleEntry::Locking.lock_accounts(@account_d, @account_e) {}
|
148
|
-
end.to_not raise_error
|
149
|
-
end
|
150
|
-
|
151
|
-
context 'handling ActiveRecord::StatementInvalid errors' do
|
152
|
-
context 'non lock wait timeout errors' do
|
153
|
-
let(:error) { ActiveRecord::StatementInvalid.new('some other error') }
|
154
|
-
before do
|
155
|
-
allow(DoubleEntry::AccountBalance).to receive(:with_restart_on_deadlock).
|
156
|
-
and_raise(error)
|
157
|
-
end
|
158
|
-
|
159
|
-
it 're-raises the ActiveRecord::StatementInvalid error' do
|
160
|
-
expect do
|
161
|
-
DoubleEntry::Locking.lock_accounts(@account_d, @account_e) {}
|
162
|
-
end.to raise_error(error)
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
context 'lock wait timeout errors' do
|
167
|
-
before do
|
168
|
-
allow(DoubleEntry::AccountBalance).to receive(:with_restart_on_deadlock).
|
169
|
-
and_raise(ActiveRecord::StatementInvalid, 'lock wait timeout')
|
170
|
-
end
|
171
|
-
|
172
|
-
it 'raises a LockWaitTimeout error' do
|
173
|
-
expect do
|
174
|
-
DoubleEntry::Locking.lock_accounts(@account_d, @account_e) {}
|
175
|
-
end.to raise_error(DoubleEntry::Locking::LockWaitTimeout)
|
176
|
-
end
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# sqlite cannot handle these cases so they don't run when DB=sqlite
|
181
|
-
describe 'concurrent locking', :unless => ENV['DB'] == 'sqlite' do
|
182
|
-
it 'allows multiple threads to lock at the same time' do
|
183
|
-
expect do
|
184
|
-
threads = []
|
185
|
-
|
186
|
-
threads << Thread.new do
|
187
|
-
sleep 0.05
|
188
|
-
DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
|
189
|
-
DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
threads << Thread.new do
|
194
|
-
DoubleEntry::Locking.lock_accounts(@account_c, @account_d) do
|
195
|
-
sleep 0.1
|
196
|
-
DoubleEntry.transfer(Money.new(10_00), :from => @account_c, :to => @account_d, :code => :test)
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
threads.each(&:join)
|
201
|
-
end.to_not raise_error
|
202
|
-
end
|
203
|
-
|
204
|
-
it 'allows multiple threads to lock accounts without balances at the same time' do
|
205
|
-
threads = []
|
206
|
-
expect do
|
207
|
-
threads << Thread.new { DoubleEntry::Locking.lock_accounts(@account_a, @account_b) { sleep 0.1 } }
|
208
|
-
threads << Thread.new { DoubleEntry::Locking.lock_accounts(@account_c, @account_d) { sleep 0.1 } }
|
209
|
-
|
210
|
-
threads.each(&:join)
|
211
|
-
end.to_not raise_error
|
212
|
-
end
|
213
|
-
end
|
214
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module DoubleEntry
|
2
|
-
RSpec.describe DoubleEntry do
|
3
|
-
describe 'transfer performance' do
|
4
|
-
include PerformanceHelper
|
5
|
-
let(:user) { User.make! }
|
6
|
-
let(:amount) { Money.new(10_00) }
|
7
|
-
let(:test) { DoubleEntry.account(:test, :scope => user) }
|
8
|
-
let(:savings) { DoubleEntry.account(:savings, :scope => user) }
|
9
|
-
|
10
|
-
it 'creates a lot of transfers quickly without metadata' do
|
11
|
-
profile_transfers_with_metadata(nil)
|
12
|
-
# local results: 6.44, 5.93, 5.94
|
13
|
-
end
|
14
|
-
|
15
|
-
it 'creates a lot of transfers quickly with metadata' do
|
16
|
-
big_metadata = {}
|
17
|
-
8.times { |i| big_metadata["key#{i}".to_sym] = "value#{i}" }
|
18
|
-
profile_transfers_with_metadata(big_metadata)
|
19
|
-
# local results: 21.2, 21.6, 20.9
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def profile_transfers_with_metadata(metadata)
|
24
|
-
start_profiling
|
25
|
-
options = { :from => test, :to => savings, :code => :bonus }
|
26
|
-
options[:metadata] = metadata if metadata
|
27
|
-
100.times { Transfer.transfer(amount, options) }
|
28
|
-
profile_name = metadata ? 'transfer-with-metadata' : 'transfer'
|
29
|
-
stop_profiling(profile_name)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|