tphases 0.1.2
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.
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +72 -0
- data/Rakefile +1 -0
- data/lib/tphases/config.rb +41 -0
- data/lib/tphases/initialization.rb +26 -0
- data/lib/tphases/modes/collect_mode.rb +60 -0
- data/lib/tphases/modes/exceptions_mode.rb +30 -0
- data/lib/tphases/modes/helpers/transactional_violations_helper.rb +133 -0
- data/lib/tphases/modes/pass_through_mode.rb +22 -0
- data/lib/tphases/transactional_violation.rb +3 -0
- data/lib/tphases/version.rb +3 -0
- data/lib/tphases.rb +9 -0
- data/spec/fixtures/database.sqlite3 +0 -0
- data/spec/fixtures/database.yml +4 -0
- data/spec/lib/tphases/modes/collect_mode_spec.rb +65 -0
- data/spec/lib/tphases/modes/exceptions_mode_spec.rb +61 -0
- data/spec/lib/tphases/modes/helpers/transactional_violations_helper_spec.rb +38 -0
- data/spec/lib/tphases/modes/pass_through_mode_spec.rb +30 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/setup_modes_specs_context.rb +12 -0
- data/test_script.rb +5 -0
- data/tphases.gemspec +29 -0
- metadata +176 -0
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
coverage
|
6
|
+
InstalledFiles
|
7
|
+
lib/bundler/man
|
8
|
+
pkg
|
9
|
+
rdoc
|
10
|
+
spec/reports
|
11
|
+
test/tmp
|
12
|
+
test/version_tmp
|
13
|
+
tmp
|
14
|
+
|
15
|
+
# YARD artifacts
|
16
|
+
.yardoc
|
17
|
+
_yardoc
|
18
|
+
doc/
|
19
|
+
|
20
|
+
Gemfile.lock
|
21
|
+
.idea
|
22
|
+
|
23
|
+
spec/fixtures/database.sqlite3
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Charles Finkel
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# TPhases
|
2
|
+
|
3
|
+
TPhases (Transactional Phases) is a support framework that helps you build your Rails request life cycles into read-only, write-only, and no-transaction-allowed phases.
|
4
|
+
|
5
|
+
The way it accomplishes this is with the methods `TPhases.read_phase`, `TPhases.write_phase` and `TPhases.no_transactions_phase` which take blocks. Here is a simple example inside of a controller action:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class BarsController < ApplicationController
|
9
|
+
|
10
|
+
def update
|
11
|
+
TPhases.read_phase do
|
12
|
+
@bar = Bar.find(params[:id])
|
13
|
+
end
|
14
|
+
|
15
|
+
TPhases.write_phase do
|
16
|
+
@bar.update_attributes(params[:bar])
|
17
|
+
end
|
18
|
+
|
19
|
+
TPhases.no_transactions_phase do
|
20
|
+
process(@bar)
|
21
|
+
redirect_to @bar
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
** Currently supports ActiveRecord only. Planned support includes Memcached, Redis, MongoDB, and more.
|
28
|
+
|
29
|
+
## Installation
|
30
|
+
|
31
|
+
Add this line to your application's Gemfile:
|
32
|
+
|
33
|
+
gem 'tphases'
|
34
|
+
|
35
|
+
And then execute:
|
36
|
+
|
37
|
+
$ bundle
|
38
|
+
|
39
|
+
Or install it yourself as:
|
40
|
+
|
41
|
+
$ gem install tphases
|
42
|
+
|
43
|
+
Somewhere in the initialization process of your app, call
|
44
|
+
|
45
|
+
TPhases.initiate!
|
46
|
+
|
47
|
+
## Usage
|
48
|
+
|
49
|
+
### In production:
|
50
|
+
The `read_phase`, `write_phase`, and `no_transaction_phase` methods simply yield to the block given.
|
51
|
+
|
52
|
+
### In development:
|
53
|
+
|
54
|
+
#### `read_phase`
|
55
|
+
throws an exception if a database transaction is attempted within its block which is a write. This is known as a "read transactional violation".
|
56
|
+
|
57
|
+
#### `write_phase`
|
58
|
+
throws an exception if a database transaction is attempted within its block which is a read. This is a write transactional violation.
|
59
|
+
|
60
|
+
#### `no_transactions_phase`
|
61
|
+
throws an exception if any database transaction is attempted within its block.
|
62
|
+
|
63
|
+
### In test:
|
64
|
+
If a transactional violation occurs in a TPhase, the code will continue to run, but the test will fail at the end citing the list of transactional violations that occurred.
|
65
|
+
|
66
|
+
## Contributing
|
67
|
+
|
68
|
+
1. Fork it
|
69
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
70
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
71
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
72
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module TPhases
|
2
|
+
module Config
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
# allow for configuration of TPhases
|
7
|
+
def configure(&block)
|
8
|
+
yield config
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
# the config
|
13
|
+
# sets default value `mode` value based on presence of Rails and environment type
|
14
|
+
# the default setting is the safest, :pass_through, which means TPhases does nothing.
|
15
|
+
def config
|
16
|
+
@config ||= begin
|
17
|
+
|
18
|
+
default_mode = begin
|
19
|
+
if defined? Rails
|
20
|
+
case Rails.env
|
21
|
+
when 'production', 'staging', 'demo'
|
22
|
+
:pass_through
|
23
|
+
when 'development'
|
24
|
+
:exceptions
|
25
|
+
when 'test'
|
26
|
+
:collect
|
27
|
+
else
|
28
|
+
:pass_through
|
29
|
+
end
|
30
|
+
else
|
31
|
+
:pass_through
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Struct.new(:mode).new(default_mode)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module TPhases
|
2
|
+
module Initialization
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
# initiates TPhases. Any overrides to config mode need to be made prior to running this.
|
7
|
+
def initiate!
|
8
|
+
case config.mode
|
9
|
+
when :pass_through
|
10
|
+
require 'tphases/modes/pass_through_mode'
|
11
|
+
include TPhases::Modes::PassThroughMode
|
12
|
+
when :exceptions
|
13
|
+
require 'tphases/modes/exceptions_mode'
|
14
|
+
include TPhases::Modes::ExceptionsMode
|
15
|
+
when :collect
|
16
|
+
require 'tphases/modes/collect_mode'
|
17
|
+
include TPhases::Modes::CollectMode
|
18
|
+
else
|
19
|
+
raise "TPhases mode must be one of :pass_through, :exceptions, or :collect, but instead is #{config.mode}"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'tphases/transactional_violation'
|
2
|
+
require 'tphases/modes/helpers/transactional_violations_helper'
|
3
|
+
|
4
|
+
# the default 'test' mode, Collect Mode collects incidents of
|
5
|
+
# immediately inside of a TPhase block if a transactional violation occurs
|
6
|
+
module TPhases
|
7
|
+
module Modes
|
8
|
+
module CollectMode
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
include Helpers::TransactionalViolationsHelper
|
11
|
+
|
12
|
+
included do
|
13
|
+
add_rspec_after! if defined?(RSpec)
|
14
|
+
@violations = []
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
attr_accessor :violations
|
20
|
+
|
21
|
+
private
|
22
|
+
def write_violation_action(sql, call_stack)
|
23
|
+
violations << { :type => :write, :call_stack => call_stack, :sql => sql }
|
24
|
+
end
|
25
|
+
|
26
|
+
def read_violation_action(sql, call_stack)
|
27
|
+
violations << { :type => :read, :call_stack => call_stack, :sql => sql }
|
28
|
+
end
|
29
|
+
|
30
|
+
def no_transactions_violation_action(sql, call_stack)
|
31
|
+
violations << { :type => :no_transactions, :call_stack => call_stack, :sql => sql }
|
32
|
+
end
|
33
|
+
|
34
|
+
# adds an after block for all rspec tests that cause them to fail if any transactional violations are present
|
35
|
+
def add_rspec_after!
|
36
|
+
RSpec.configure do |config|
|
37
|
+
config.after(:each) do
|
38
|
+
begin
|
39
|
+
unless TPhases.violations.empty?
|
40
|
+
#fail "This spec had #{TPhases.violations.count} transactional violations: \n\t#{TPhases.violations.map(&:inspect).join("\n\t")}"
|
41
|
+
fail <<-FAILURE_MESSAGE
|
42
|
+
This spec had #{TPhases.violations.count} transactional violations:
|
43
|
+
#{TPhases.violations.each_with_index.map do |violation,index|
|
44
|
+
"#{index}: Violation Type: #{violation[:type]},\nSQL: #{violation[:sql]}\nCall Stack:\n\t#{violation[:call_stack].join("\n\t")}"
|
45
|
+
end.join("\n*********************************************************\n")}
|
46
|
+
end
|
47
|
+
FAILURE_MESSAGE
|
48
|
+
end
|
49
|
+
ensure
|
50
|
+
# reset violations list:
|
51
|
+
TPhases.violations = []
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'tphases/transactional_violation'
|
2
|
+
require 'tphases/modes/helpers/transactional_violations_helper'
|
3
|
+
|
4
|
+
# the default 'development' mode, Exceptions Mode means that an exception will be raised
|
5
|
+
# immediately inside of a TPhase block if a transactional violation occurs
|
6
|
+
module TPhases
|
7
|
+
module Modes
|
8
|
+
module ExceptionsMode
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
include Helpers::TransactionalViolationsHelper
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
private
|
14
|
+
|
15
|
+
def write_violation_action(sql, caller)
|
16
|
+
raise TransactionalViolation.new "#{sql} ran inside of a 'write_phase' block."
|
17
|
+
end
|
18
|
+
|
19
|
+
def read_violation_action(sql, caller)
|
20
|
+
raise TransactionalViolation.new "#{sql} ran inside of a 'read_phase' block."
|
21
|
+
end
|
22
|
+
|
23
|
+
def no_transactions_violation_action(sql, caller)
|
24
|
+
raise TransactionalViolation.new "#{sql} ran inside of a 'no_transactions_phase' block."
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'active_support/notifications'
|
2
|
+
require 'active_support/version'
|
3
|
+
|
4
|
+
module TPhases
|
5
|
+
module Modes
|
6
|
+
module Helpers
|
7
|
+
|
8
|
+
# this helper is included by the CollectMode and the ExceptionsMode modules.
|
9
|
+
# methods expected to be implemented by those modes are #write_violation_action, #read_violation_action, and
|
10
|
+
# #no_transactions_violation_action
|
11
|
+
module TransactionalViolationsHelper
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
included do
|
15
|
+
define_phase_methods!
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
# if version of activesupport is 3.2.1, it has the subscribed method. else, it doesn't
|
22
|
+
def define_phase_methods!
|
23
|
+
if ActiveSupport::VERSION::MAJOR > 3
|
24
|
+
define_phase_methods_with_subscribed_method!
|
25
|
+
elsif ActiveSupport::VERSION::MAJOR == 3
|
26
|
+
if ActiveSupport::VERSION::MINOR > 2
|
27
|
+
define_phase_methods_with_subscribed_method!
|
28
|
+
elsif ActiveSupport::VERSION::MINOR == 2
|
29
|
+
if ActiveSupport::VERSION::TINY >= 1
|
30
|
+
define_phase_methods_with_subscribed_method!
|
31
|
+
else
|
32
|
+
define_phase_methods_without_subscribed_method!
|
33
|
+
end
|
34
|
+
else
|
35
|
+
define_phase_methods_without_subscribed_method!
|
36
|
+
end
|
37
|
+
else
|
38
|
+
define_phase_methods_without_subscribed_method!
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# adds methods using the subscribed method
|
43
|
+
def define_phase_methods_with_subscribed_method!
|
44
|
+
define_singleton_method(:read_phase) do |&block|
|
45
|
+
ActiveSupport::Notifications.subscribed(read_phase_subscription_callback, "sql.active_record", &block)
|
46
|
+
end
|
47
|
+
|
48
|
+
define_singleton_method(:write_phase) do |&block|
|
49
|
+
ActiveSupport::Notifications.subscribed(write_phase_subscription_callback, "sql.active_record", &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
define_singleton_method(:no_transactions_phase) do |&block|
|
53
|
+
ActiveSupport::Notifications.subscribed(no_transactions_phase_subscription_callback, "sql.active_record", &block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def define_phase_methods_without_subscribed_method!
|
58
|
+
define_singleton_method(:read_phase) do |&block|
|
59
|
+
begin
|
60
|
+
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record", &read_phase_subscription_callback)
|
61
|
+
block.call
|
62
|
+
ensure
|
63
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
define_singleton_method(:write_phase) do |&block|
|
68
|
+
begin
|
69
|
+
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record", &write_phase_subscription_callback)
|
70
|
+
block.call
|
71
|
+
ensure
|
72
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
define_singleton_method(:no_transactions_phase) do |&block|
|
77
|
+
begin
|
78
|
+
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record", &no_transactions_phase_subscription_callback)
|
79
|
+
block.call
|
80
|
+
ensure
|
81
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# the set of blocks that run when an ActiveSupport notification is fired on sql.active_record
|
87
|
+
# each call *_violation_action methods which are defined in the implementing module
|
88
|
+
|
89
|
+
def write_phase_subscription_callback
|
90
|
+
Proc.new do |name, date, date2, sha, args|
|
91
|
+
if write_transactional_violation?(args[:sql])
|
92
|
+
write_violation_action(args[:sql], caller)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def read_phase_subscription_callback
|
98
|
+
Proc.new do |name, date, date2, sha, args|
|
99
|
+
if read_transactional_violation?(args[:sql])
|
100
|
+
read_violation_action(args[:sql], caller)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def no_transactions_phase_subscription_callback
|
106
|
+
Proc.new do |name, date, date2, sha, args|
|
107
|
+
no_transactions_violation_action(args[:sql], caller)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
READ_QUERIES = %w{update commit insert delete}
|
112
|
+
WRITE_QUERIES = %w{show select}
|
113
|
+
|
114
|
+
# determines if this query is a read transactional violation (if it is anything besides a read)
|
115
|
+
def read_transactional_violation?(sql)
|
116
|
+
READ_QUERIES.include?(first_word(sql))
|
117
|
+
end
|
118
|
+
|
119
|
+
# determines if this query is a write transactional violation (if it is anything besides a write)
|
120
|
+
def write_transactional_violation?(sql)
|
121
|
+
WRITE_QUERIES.include?(first_word(sql))
|
122
|
+
end
|
123
|
+
|
124
|
+
def first_word(str)
|
125
|
+
str.split(' ').first.downcase
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# the default 'production' mode, PassThrough mode does nothing but called the yielded block
|
2
|
+
module TPhases
|
3
|
+
module Modes
|
4
|
+
module PassThroughMode
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def read_phase
|
9
|
+
yield
|
10
|
+
end
|
11
|
+
|
12
|
+
def write_phase
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
|
16
|
+
def no_transactions_phase
|
17
|
+
yield
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/tphases.rb
ADDED
Binary file
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_record'
|
3
|
+
require 'tphases/modes/collect_mode'
|
4
|
+
|
5
|
+
describe TPhases::Modes::CollectMode do
|
6
|
+
subject { Module.new { include TPhases::Modes::CollectMode } }
|
7
|
+
|
8
|
+
include_context "setup mode specs"
|
9
|
+
|
10
|
+
describe '.no_transactions_phase' do
|
11
|
+
it "should add to the violations list for all violations" do
|
12
|
+
expect {
|
13
|
+
subject.no_transactions_phase do
|
14
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
15
|
+
end
|
16
|
+
}.to change { subject.send(:violations).size }.from(0).to(1)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should add multiple violations if there are multiple" do
|
20
|
+
expect {
|
21
|
+
subject.no_transactions_phase do
|
22
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
23
|
+
ActiveRecord::Base.connection.select_all(write_sql)
|
24
|
+
end
|
25
|
+
}.to change { subject.send(:violations).size }.from(0).to(2)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '.read_phase' do
|
30
|
+
it "should not add a violation for read transactions" do
|
31
|
+
expect {
|
32
|
+
subject.read_phase do
|
33
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
34
|
+
end
|
35
|
+
}.to_not change { subject.send(:violations).size }
|
36
|
+
end
|
37
|
+
it "should add a violation for write transactions" do
|
38
|
+
expect {
|
39
|
+
subject.read_phase do
|
40
|
+
ActiveRecord::Base.connection.select_all(write_sql)
|
41
|
+
end
|
42
|
+
}.to change { subject.send(:violations).size }.from(0).to(1)
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '.write_phase' do
|
48
|
+
it "should not add a violation for write transactions" do
|
49
|
+
expect {
|
50
|
+
subject.write_phase do
|
51
|
+
ActiveRecord::Base.connection.select_all(write_sql)
|
52
|
+
end
|
53
|
+
}.to_not change { subject.send(:violations).size }
|
54
|
+
end
|
55
|
+
it "should add a violation for read transactions" do
|
56
|
+
expect {
|
57
|
+
subject.write_phase do
|
58
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
59
|
+
end
|
60
|
+
}.to change { subject.send(:violations).size }.from(0).to(1)
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_record'
|
3
|
+
require 'tphases/modes/exceptions_mode'
|
4
|
+
|
5
|
+
describe TPhases::Modes::ExceptionsMode do
|
6
|
+
subject { Module.new { include TPhases::Modes::ExceptionsMode } }
|
7
|
+
|
8
|
+
include_context "setup mode specs"
|
9
|
+
|
10
|
+
describe '.no_transactions_phase' do
|
11
|
+
it "should throw an exception disallow read and write transactions from running in this phase" do
|
12
|
+
expect {
|
13
|
+
subject.no_transactions_phase do
|
14
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
15
|
+
end
|
16
|
+
}.to raise_error(ActiveRecord::StatementInvalid, "TransactionalViolation: #{read_sql} ran inside of a 'no_transactions_phase' block.: #{read_sql}")
|
17
|
+
|
18
|
+
expect {
|
19
|
+
subject.no_transactions_phase do
|
20
|
+
ActiveRecord::Base.connection.select_all(write_sql)
|
21
|
+
end
|
22
|
+
}.to raise_error(ActiveRecord::StatementInvalid, "TransactionalViolation: #{write_sql} ran inside of a 'no_transactions_phase' block.: #{write_sql}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.read_phase' do
|
27
|
+
it "should allow read transactions" do
|
28
|
+
expect {
|
29
|
+
subject.read_phase do
|
30
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
31
|
+
end
|
32
|
+
}.to_not raise_error
|
33
|
+
end
|
34
|
+
it "should disallow write transactions" do
|
35
|
+
expect {
|
36
|
+
subject.read_phase do
|
37
|
+
ActiveRecord::Base.connection.select_all(write_sql)
|
38
|
+
end
|
39
|
+
}.to raise_error(ActiveRecord::StatementInvalid, "TransactionalViolation: #{write_sql} ran inside of a 'read_phase' block.: #{write_sql}")
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '.write_phase' do
|
45
|
+
it "should allow write transactions" do
|
46
|
+
expect {
|
47
|
+
subject.write_phase do
|
48
|
+
ActiveRecord::Base.connection.select_all(write_sql)
|
49
|
+
end
|
50
|
+
}.to_not raise_error
|
51
|
+
end
|
52
|
+
it "should disallow read transactions" do
|
53
|
+
expect {
|
54
|
+
subject.write_phase do
|
55
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
56
|
+
end
|
57
|
+
}.to raise_error(ActiveRecord::StatementInvalid, "TransactionalViolation: #{read_sql} ran inside of a 'write_phase' block.: #{read_sql}")
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tphases/modes/helpers/transactional_violations_helper'
|
3
|
+
|
4
|
+
describe TPhases::Modes::Helpers::TransactionalViolationsHelper do
|
5
|
+
describe TPhases::Modes::Helpers::TransactionalViolationsHelper do
|
6
|
+
subject { Module.new { include TPhases::Modes::Helpers::TransactionalViolationsHelper } }
|
7
|
+
|
8
|
+
let(:write_queries) {
|
9
|
+
[
|
10
|
+
"UPDATE `users` SET `email` = 'woifjwe@owiejf.com' WHERE `users`.`id` = 1",
|
11
|
+
"INSERT INTO tablename (col1, col2) VALUES('data1', 'data2' )",
|
12
|
+
"COMMIT",
|
13
|
+
"DELETE FROM example WHERE age='15'"
|
14
|
+
]
|
15
|
+
}
|
16
|
+
let(:read_queries) {
|
17
|
+
[
|
18
|
+
"select * from foobar",
|
19
|
+
"show variables like 'version'"
|
20
|
+
]
|
21
|
+
}
|
22
|
+
|
23
|
+
describe "#read_transactional_violation?" do
|
24
|
+
it "should detect correctly" do
|
25
|
+
read_queries.each { |read_query| expect(subject.send(:read_transactional_violation?, read_query)).to eq(false) }
|
26
|
+
write_queries.each { |write_query| expect(subject.send(:read_transactional_violation?, write_query)).to eq(true) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe " #write_transactional_violation?" do
|
31
|
+
it "should detect correctly" do
|
32
|
+
read_queries.each { |read_query| expect(subject.send(:write_transactional_violation?, read_query)).to be_true }
|
33
|
+
write_queries.each { |write_query| expect(subject.send(:write_transactional_violation?, write_query)).to be_false }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_record'
|
3
|
+
require 'tphases/modes/pass_through_mode'
|
4
|
+
|
5
|
+
describe TPhases::Modes::PassThroughMode do
|
6
|
+
subject { Module.new { include TPhases::Modes::PassThroughMode } }
|
7
|
+
|
8
|
+
include_context "setup mode specs"
|
9
|
+
|
10
|
+
describe '.no_transactions_phase, .read_phase, .write_phase' do
|
11
|
+
it "should allow anything" do
|
12
|
+
subject.no_transactions_phase do
|
13
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
14
|
+
ActiveRecord::Base.connection.select_all(write_sql)
|
15
|
+
end
|
16
|
+
|
17
|
+
subject.write_phase do
|
18
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
19
|
+
ActiveRecord::Base.connection.select_all(write_sql)
|
20
|
+
end
|
21
|
+
|
22
|
+
subject.read_phase do
|
23
|
+
ActiveRecord::Base.connection.select_all(read_sql)
|
24
|
+
ActiveRecord::Base.connection.select_all(write_sql)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
LIB_ROOT = File.expand_path(File.dirname(__FILE__) + "/..")
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
6
|
+
# loaded once.
|
7
|
+
#
|
8
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
|
14
|
+
# Run specs in random order to surface order dependencies. If you find an
|
15
|
+
# order dependency and want to debug it, you can fix the order by providing
|
16
|
+
# the seed, which is printed after each run.
|
17
|
+
# --seed 1234
|
18
|
+
config.order = 'random'
|
19
|
+
end
|
20
|
+
|
21
|
+
Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each { |f| require f }
|
22
|
+
|
23
|
+
require File.join(File.expand_path('../../', __FILE__), 'lib', 'tphases')
|
@@ -0,0 +1,12 @@
|
|
1
|
+
shared_context "setup mode specs" do
|
2
|
+
|
3
|
+
before do
|
4
|
+
dbconfig = YAML::load(File.open(LIB_ROOT + '/spec/fixtures/database.yml'))
|
5
|
+
dbconfig['database'] = LIB_ROOT + '/' + dbconfig['database']
|
6
|
+
ActiveRecord::Base.establish_connection(dbconfig)
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:read_sql) { 'select * from posts' }
|
10
|
+
let(:write_sql) { "insert into posts values ('foobaz')" }
|
11
|
+
|
12
|
+
end
|
data/test_script.rb
ADDED
data/tphases.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tphases/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "tphases"
|
8
|
+
gem.version = Tphases::VERSION
|
9
|
+
gem.authors = ["Charles Finkel"]
|
10
|
+
gem.email = ["charles.finkel@gmail.com"]
|
11
|
+
|
12
|
+
description = %q{TPhases (Transactional Phases) is a support framework that helps you build your Rails request life cycles into read-only and write-only phases.}
|
13
|
+
gem.description = description
|
14
|
+
gem.summary = description
|
15
|
+
gem.homepage = "https://github.com/charleseff/tphases"
|
16
|
+
|
17
|
+
gem.files = `git ls-files`.split($/)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.require_paths = ["lib"]
|
21
|
+
|
22
|
+
gem.add_dependency 'activesupport'
|
23
|
+
|
24
|
+
gem.add_development_dependency 'rspec'
|
25
|
+
gem.add_development_dependency 'debugger'
|
26
|
+
gem.add_development_dependency 'sqlite3'
|
27
|
+
gem.add_development_dependency 'activerecord'
|
28
|
+
gem.add_development_dependency 'pry'
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tphases
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Charles Finkel
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: debugger
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: sqlite3
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: activerecord
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: pry
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: TPhases (Transactional Phases) is a support framework that helps you
|
111
|
+
build your Rails request life cycles into read-only and write-only phases.
|
112
|
+
email:
|
113
|
+
- charles.finkel@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- .rspec
|
120
|
+
- Gemfile
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- lib/tphases.rb
|
125
|
+
- lib/tphases/config.rb
|
126
|
+
- lib/tphases/initialization.rb
|
127
|
+
- lib/tphases/modes/collect_mode.rb
|
128
|
+
- lib/tphases/modes/exceptions_mode.rb
|
129
|
+
- lib/tphases/modes/helpers/transactional_violations_helper.rb
|
130
|
+
- lib/tphases/modes/pass_through_mode.rb
|
131
|
+
- lib/tphases/transactional_violation.rb
|
132
|
+
- lib/tphases/version.rb
|
133
|
+
- spec/fixtures/database.sqlite3
|
134
|
+
- spec/fixtures/database.yml
|
135
|
+
- spec/lib/tphases/modes/collect_mode_spec.rb
|
136
|
+
- spec/lib/tphases/modes/exceptions_mode_spec.rb
|
137
|
+
- spec/lib/tphases/modes/helpers/transactional_violations_helper_spec.rb
|
138
|
+
- spec/lib/tphases/modes/pass_through_mode_spec.rb
|
139
|
+
- spec/spec_helper.rb
|
140
|
+
- spec/support/setup_modes_specs_context.rb
|
141
|
+
- test_script.rb
|
142
|
+
- tphases.gemspec
|
143
|
+
homepage: https://github.com/charleseff/tphases
|
144
|
+
licenses: []
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
none: false
|
151
|
+
requirements:
|
152
|
+
- - ! '>='
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
none: false
|
157
|
+
requirements:
|
158
|
+
- - ! '>='
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
161
|
+
requirements: []
|
162
|
+
rubyforge_project:
|
163
|
+
rubygems_version: 1.8.24
|
164
|
+
signing_key:
|
165
|
+
specification_version: 3
|
166
|
+
summary: TPhases (Transactional Phases) is a support framework that helps you build
|
167
|
+
your Rails request life cycles into read-only and write-only phases.
|
168
|
+
test_files:
|
169
|
+
- spec/fixtures/database.sqlite3
|
170
|
+
- spec/fixtures/database.yml
|
171
|
+
- spec/lib/tphases/modes/collect_mode_spec.rb
|
172
|
+
- spec/lib/tphases/modes/exceptions_mode_spec.rb
|
173
|
+
- spec/lib/tphases/modes/helpers/transactional_violations_helper_spec.rb
|
174
|
+
- spec/lib/tphases/modes/pass_through_mode_spec.rb
|
175
|
+
- spec/spec_helper.rb
|
176
|
+
- spec/support/setup_modes_specs_context.rb
|