some speed optimisations. added 'use_dirty' configuration variable
This commit is contained in:
parent
ce2e2fc9a6
commit
dcf43e3641
18
Gemfile.lock
18
Gemfile.lock
|
@ -12,9 +12,9 @@ GEM
|
||||||
remote: http://rubygems.org/
|
remote: http://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
abstract (1.0.0)
|
abstract (1.0.0)
|
||||||
actionpack (3.0.5)
|
actionpack (3.0.4)
|
||||||
activemodel (= 3.0.5)
|
activemodel (= 3.0.4)
|
||||||
activesupport (= 3.0.5)
|
activesupport (= 3.0.4)
|
||||||
builder (~> 2.1.2)
|
builder (~> 2.1.2)
|
||||||
erubis (~> 2.6.6)
|
erubis (~> 2.6.6)
|
||||||
i18n (~> 0.4)
|
i18n (~> 0.4)
|
||||||
|
@ -22,11 +22,11 @@ GEM
|
||||||
rack-mount (~> 0.6.13)
|
rack-mount (~> 0.6.13)
|
||||||
rack-test (~> 0.5.7)
|
rack-test (~> 0.5.7)
|
||||||
tzinfo (~> 0.3.23)
|
tzinfo (~> 0.3.23)
|
||||||
activemodel (3.0.5)
|
activemodel (3.0.4)
|
||||||
activesupport (= 3.0.5)
|
activesupport (= 3.0.4)
|
||||||
builder (~> 2.1.2)
|
builder (~> 2.1.2)
|
||||||
i18n (~> 0.4)
|
i18n (~> 0.4)
|
||||||
activesupport (3.0.5)
|
activesupport (3.0.4)
|
||||||
builder (2.1.2)
|
builder (2.1.2)
|
||||||
couchrest (1.0.1)
|
couchrest (1.0.1)
|
||||||
json (>= 1.4.6)
|
json (>= 1.4.6)
|
||||||
|
@ -43,9 +43,9 @@ GEM
|
||||||
rack (>= 1.0.0)
|
rack (>= 1.0.0)
|
||||||
rack-test (0.5.7)
|
rack-test (0.5.7)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
railties (3.0.5)
|
railties (3.0.4)
|
||||||
actionpack (= 3.0.5)
|
actionpack (= 3.0.4)
|
||||||
activesupport (= 3.0.5)
|
activesupport (= 3.0.4)
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (~> 0.14.4)
|
thor (~> 0.14.4)
|
||||||
rake (0.8.7)
|
rake (0.8.7)
|
||||||
|
|
|
@ -20,13 +20,27 @@ class BenchmarkModel < CouchRest::Model::Base
|
||||||
property :casted_list, [BenchmarkCasted]
|
property :casted_list, [BenchmarkCasted]
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
# set dirty configuration, return previous configuration setting
|
||||||
n = 50000
|
def set_dirty(value)
|
||||||
|
orig = nil
|
||||||
|
CouchRest::Model::Base.configure do |config|
|
||||||
|
orig = config.use_dirty
|
||||||
|
config.use_dirty = value
|
||||||
|
end
|
||||||
|
BenchmarkModel.instance_eval do
|
||||||
|
self.use_dirty = value
|
||||||
|
end
|
||||||
|
orig
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_benchmark
|
||||||
|
n = 50000 # property operation count
|
||||||
|
db_n = 1000 # database operation count
|
||||||
b = BenchmarkModel.new
|
b = BenchmarkModel.new
|
||||||
|
|
||||||
Benchmark.bm(25) do |x|
|
Benchmark.bm(30) do |x|
|
||||||
|
|
||||||
# assigning
|
# property assigning
|
||||||
|
|
||||||
x.report("assign string:") do
|
x.report("assign string:") do
|
||||||
n.times { b.string = "test" }
|
n.times { b.string = "test" }
|
||||||
|
@ -44,7 +58,7 @@ begin
|
||||||
n.times { b.casted_list = [{ 'name' => 'test' }] }
|
n.times { b.casted_list = [{ 'name' => 'test' }] }
|
||||||
end
|
end
|
||||||
|
|
||||||
# reading
|
# property reading
|
||||||
|
|
||||||
x.report("read string") do
|
x.report("read string") do
|
||||||
n.times { b.string }
|
n.times { b.string }
|
||||||
|
@ -62,13 +76,32 @@ begin
|
||||||
n.times { b.casted_list }
|
n.times { b.casted_list }
|
||||||
end
|
end
|
||||||
|
|
||||||
# db writing
|
|
||||||
if ENV['BENCHMARK_DB']
|
if ENV['BENCHMARK_DB']
|
||||||
x.report("write record") do
|
# db writing
|
||||||
# need to make change before it will save
|
x.report("write changed record to db") do
|
||||||
n.times { b.string = "test#{n}"; b.save }
|
db_n.times { |i| b.string = "test#{i}"; b.save }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
x.report("write unchanged record to db") do
|
||||||
|
db_n.times { b.save }
|
||||||
|
end
|
||||||
|
|
||||||
|
# db reading
|
||||||
|
x.report("read record from db") do
|
||||||
|
db_n.times { BenchmarkModel.find(b.id) }
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
puts "with use_dirty true"
|
||||||
|
set_dirty(true)
|
||||||
|
run_benchmark
|
||||||
|
|
||||||
|
puts "\nwith use_dirty false"
|
||||||
|
set_dirty(false)
|
||||||
|
run_benchmark
|
||||||
|
end
|
||||||
|
|
|
@ -17,6 +17,7 @@ module CouchRest
|
||||||
include CouchRest::Model::Associations
|
include CouchRest::Model::Associations
|
||||||
include CouchRest::Model::Validations
|
include CouchRest::Model::Validations
|
||||||
include CouchRest::Model::Dirty
|
include CouchRest::Model::Dirty
|
||||||
|
include CouchRest::Model::CastedBy
|
||||||
|
|
||||||
def self.subclasses
|
def self.subclasses
|
||||||
@subclasses ||= []
|
@subclasses ||= []
|
||||||
|
@ -25,6 +26,7 @@ module CouchRest
|
||||||
def self.inherited(subklass)
|
def self.inherited(subklass)
|
||||||
super
|
super
|
||||||
subklass.send(:include, CouchRest::Model::Properties)
|
subklass.send(:include, CouchRest::Model::Properties)
|
||||||
|
|
||||||
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
def self.inherited(subklass)
|
def self.inherited(subklass)
|
||||||
super
|
super
|
||||||
|
@ -73,15 +75,8 @@ module CouchRest
|
||||||
|
|
||||||
### instance methods
|
### instance methods
|
||||||
|
|
||||||
# Gets a reference to the actual document in the DB
|
|
||||||
# Calls up to the next document if there is one,
|
|
||||||
# Otherwise we're at the top and we return self
|
|
||||||
def base_doc
|
|
||||||
return self if base_doc?
|
|
||||||
@casted_by.base_doc
|
|
||||||
end
|
|
||||||
|
|
||||||
# Checks if we're the top document
|
# Checks if we're the top document
|
||||||
|
# (overrides base_doc? in casted_by.rb)
|
||||||
def base_doc?
|
def base_doc?
|
||||||
!@casted_by
|
!@casted_by
|
||||||
end
|
end
|
||||||
|
|
23
lib/couchrest/model/casted_by.rb
Normal file
23
lib/couchrest/model/casted_by.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
module CouchRest::Model
|
||||||
|
module CastedBy
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
included do
|
||||||
|
self.send(:attr_accessor, :casted_by)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gets a reference to the actual document in the DB
|
||||||
|
# Calls up to the next document if there is one,
|
||||||
|
# Otherwise we're at the top and we return self
|
||||||
|
def base_doc
|
||||||
|
return self if base_doc?
|
||||||
|
@casted_by ? @casted_by.base_doc : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if we're the top document
|
||||||
|
def base_doc?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,6 +7,7 @@ module CouchRest::Model
|
||||||
attr_accessor :casted_by
|
attr_accessor :casted_by
|
||||||
|
|
||||||
def []= index, obj
|
def []= index, obj
|
||||||
|
return super(index, obj) unless use_dirty?
|
||||||
couchrest_parent_will_change! if obj != self[index]
|
couchrest_parent_will_change! if obj != self[index]
|
||||||
super(index, obj)
|
super(index, obj)
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ module CouchRest::Model
|
||||||
end
|
end
|
||||||
|
|
||||||
def []= key, value
|
def []= key, value
|
||||||
couchrest_attribute_will_change!(key) unless self[key] == value
|
couchrest_attribute_will_change!(key) if use_dirty && self[key] != value
|
||||||
super(key.to_s, value)
|
super(key.to_s, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,12 @@ module CouchRest
|
||||||
included do
|
included do
|
||||||
add_config :model_type_key
|
add_config :model_type_key
|
||||||
add_config :mass_assign_any_attribute
|
add_config :mass_assign_any_attribute
|
||||||
|
add_config :use_dirty
|
||||||
|
|
||||||
configure do |config|
|
configure do |config|
|
||||||
config.model_type_key = 'couchrest-type' # 'model'?
|
config.model_type_key = 'couchrest-type' # 'model'?
|
||||||
config.mass_assign_any_attribute = false
|
config.mass_assign_any_attribute = false
|
||||||
|
config.use_dirty = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,24 @@ module CouchRest
|
||||||
# This applies to both Model::Base and Model::CastedModel
|
# This applies to both Model::Base and Model::CastedModel
|
||||||
module Dirty
|
module Dirty
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
include CouchRest::Model::CastedBy # needed for base_doc
|
||||||
|
include ActiveModel::Dirty
|
||||||
|
|
||||||
|
def use_dirty?
|
||||||
|
bdoc = base_doc
|
||||||
|
bdoc && !bdoc.disable_dirty && bdoc.use_dirty
|
||||||
|
end
|
||||||
|
|
||||||
included do
|
included do
|
||||||
include ActiveModel::Dirty
|
# internal dirty setting - overrides global setting.
|
||||||
|
# this is used to temporarily disable dirty tracking when setting
|
||||||
|
# attributes directly, for performance reasons.
|
||||||
|
self.send(:attr_accessor, :disable_dirty)
|
||||||
end
|
end
|
||||||
|
|
||||||
def couchrest_attribute_will_change!(attr)
|
def couchrest_attribute_will_change!(attr)
|
||||||
return if attr.nil?
|
return if attr.nil? || !use_dirty?
|
||||||
self.send("#{attr}_will_change!")
|
attribute_will_change!(attr)
|
||||||
couchrest_parent_will_change!
|
couchrest_parent_will_change!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,7 +39,7 @@ module CouchRest
|
||||||
|
|
||||||
# return the attribute name this object is referenced by in the parent
|
# return the attribute name this object is referenced by in the parent
|
||||||
def casted_by_attribute
|
def casted_by_attribute
|
||||||
return @casted_by_attribute if @casted_by_attribute_set
|
return @casted_by_attribute if @casted_by_attribute
|
||||||
attr = @casted_by.attributes
|
attr = @casted_by.attributes
|
||||||
@casted_by_attribute = attr.keys.detect { |k| attr[k] == self }
|
@casted_by_attribute = attr.keys.detect { |k| attr[k] == self }
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,11 +2,6 @@ module CouchRest
|
||||||
module Model
|
module Model
|
||||||
module ExtendedAttachments
|
module ExtendedAttachments
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include ActiveModel::Dirty
|
|
||||||
included do
|
|
||||||
# for _attachments_will_change!
|
|
||||||
define_attribute_methods [:_attachments]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add a file attachment to the current document. Expects
|
# Add a file attachment to the current document. Expects
|
||||||
# :file and :name to be included in the arguments.
|
# :file and :name to be included in the arguments.
|
||||||
|
@ -41,8 +36,10 @@ module CouchRest
|
||||||
# deletes a file attachment from the current doc
|
# deletes a file attachment from the current doc
|
||||||
def delete_attachment(attachment_name)
|
def delete_attachment(attachment_name)
|
||||||
return unless attachments
|
return unless attachments
|
||||||
_attachments_will_change! if attachments.include?(attachment_name)
|
if attachments.include?(attachment_name)
|
||||||
attachments.delete attachment_name
|
attribute_will_change!("_attachments")
|
||||||
|
attachments.delete attachment_name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns true if attachment_name exists
|
# returns true if attachment_name exists
|
||||||
|
@ -74,7 +71,7 @@ module CouchRest
|
||||||
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
|
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
|
||||||
content_type ||= (get_mime_type(args[:name]) || 'text/plain')
|
content_type ||= (get_mime_type(args[:name]) || 'text/plain')
|
||||||
|
|
||||||
_attachments_will_change!
|
attribute_will_change!("_attachments")
|
||||||
attachments[args[:name]] = {
|
attachments[args[:name]] = {
|
||||||
'content_type' => content_type,
|
'content_type' => content_type,
|
||||||
'data' => args[:file].read
|
'data' => args[:file].read
|
||||||
|
|
|
@ -30,7 +30,7 @@ module CouchRest
|
||||||
def update(options = {})
|
def update(options = {})
|
||||||
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
|
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
|
||||||
return false unless perform_validations(options)
|
return false unless perform_validations(options)
|
||||||
return true unless self.changed?
|
return true if use_dirty? && !self.changed?
|
||||||
_run_update_callbacks do
|
_run_update_callbacks do
|
||||||
_run_save_callbacks do
|
_run_save_callbacks do
|
||||||
result = database.save_doc(self)
|
result = database.save_doc(self)
|
||||||
|
|
|
@ -3,7 +3,6 @@ module CouchRest
|
||||||
module Model
|
module Model
|
||||||
module Properties
|
module Properties
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include ActiveModel::Dirty
|
|
||||||
|
|
||||||
included do
|
included do
|
||||||
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
|
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
|
||||||
|
@ -48,11 +47,14 @@ module CouchRest
|
||||||
def write_attribute_dirty(property, value)
|
def write_attribute_dirty(property, value)
|
||||||
prop = find_property!(property)
|
prop = find_property!(property)
|
||||||
value = prop.is_a?(String) ? value : prop.cast(self, value)
|
value = prop.is_a?(String) ? value : prop.cast(self, value)
|
||||||
self.send("#{prop}_will_change!") unless self[prop.to_s] == value
|
propname = prop.to_s
|
||||||
write_attribute(property, value)
|
attribute_will_change!(propname) if use_dirty? && self[propname] != value
|
||||||
|
self[propname] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
def []=(key,value)
|
def []=(key,value)
|
||||||
|
return super(key,value) unless use_dirty?
|
||||||
|
|
||||||
has_changes = self.changed?
|
has_changes = self.changed?
|
||||||
if !has_changes && self.respond_to?(:get_unique_id)
|
if !has_changes && self.respond_to?(:get_unique_id)
|
||||||
check_id_change = true
|
check_id_change = true
|
||||||
|
@ -82,14 +84,8 @@ module CouchRest
|
||||||
end
|
end
|
||||||
alias :attributes= :update_attributes_without_saving
|
alias :attributes= :update_attributes_without_saving
|
||||||
|
|
||||||
# needed for Dirty
|
# 'attributes' needed for Dirty
|
||||||
def attributes
|
alias :attributes :properties_with_values
|
||||||
ret = {}
|
|
||||||
self.class.properties.each do |property|
|
|
||||||
ret[property.name] = read_attribute(property)
|
|
||||||
end
|
|
||||||
ret
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_property(property)
|
def find_property(property)
|
||||||
property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
|
property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
|
||||||
|
@ -123,18 +119,10 @@ module CouchRest
|
||||||
# Set all the attributes and return a hash with the attributes
|
# Set all the attributes and return a hash with the attributes
|
||||||
# that have not been accepted.
|
# that have not been accepted.
|
||||||
def directly_set_attributes(hash, options = {})
|
def directly_set_attributes(hash, options = {})
|
||||||
hash.reject do |attribute_name, attribute_value|
|
self.disable_dirty = !options[:dirty]
|
||||||
|
ret = hash.reject do |attribute_name, attribute_value|
|
||||||
if self.respond_to?("#{attribute_name}=")
|
if self.respond_to?("#{attribute_name}=")
|
||||||
if find_property(attribute_name)
|
self.send("#{attribute_name}=", attribute_value)
|
||||||
if options[:dirty]
|
|
||||||
self.write_attribute_dirty(attribute_name, attribute_value)
|
|
||||||
else
|
|
||||||
# set attribute without updating dirty status
|
|
||||||
self.write_attribute(attribute_name, attribute_value)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
self.send("#{attribute_name}=", attribute_value)
|
|
||||||
end
|
|
||||||
true
|
true
|
||||||
elsif mass_assign_any_attribute # config option
|
elsif mass_assign_any_attribute # config option
|
||||||
self[attribute_name] = attribute_value
|
self[attribute_name] = attribute_value
|
||||||
|
@ -143,6 +131,8 @@ module CouchRest
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
self.disable_dirty = false
|
||||||
|
ret
|
||||||
end
|
end
|
||||||
|
|
||||||
def directly_set_read_only_attributes(hash)
|
def directly_set_read_only_attributes(hash)
|
||||||
|
|
|
@ -27,10 +27,11 @@ require 'couchrest/model'
|
||||||
require 'couchrest/model/errors'
|
require 'couchrest/model/errors'
|
||||||
require "couchrest/model/persistence"
|
require "couchrest/model/persistence"
|
||||||
require "couchrest/model/typecast"
|
require "couchrest/model/typecast"
|
||||||
|
require "couchrest/model/casted_by"
|
||||||
|
require "couchrest/model/dirty"
|
||||||
require "couchrest/model/property"
|
require "couchrest/model/property"
|
||||||
require "couchrest/model/property_protection"
|
require "couchrest/model/property_protection"
|
||||||
require "couchrest/model/properties"
|
require "couchrest/model/properties"
|
||||||
require "couchrest/model/dirty"
|
|
||||||
require "couchrest/model/casted_array"
|
require "couchrest/model/casted_array"
|
||||||
require "couchrest/model/casted_hash"
|
require "couchrest/model/casted_hash"
|
||||||
require "couchrest/model/casted_model"
|
require "couchrest/model/casted_model"
|
||||||
|
@ -43,7 +44,7 @@ require "couchrest/model/extended_attachments"
|
||||||
require "couchrest/model/class_proxy"
|
require "couchrest/model/class_proxy"
|
||||||
require "couchrest/model/collection"
|
require "couchrest/model/collection"
|
||||||
require "couchrest/model/associations"
|
require "couchrest/model/associations"
|
||||||
require "couchrest/model/configuration"
|
require 'couchrest/model/configuration'
|
||||||
|
|
||||||
# Monkey patches applied to couchrest
|
# Monkey patches applied to couchrest
|
||||||
require "couchrest/model/support/couchrest"
|
require "couchrest/model/support/couchrest"
|
||||||
|
@ -52,7 +53,6 @@ require "couchrest/model/support/hash"
|
||||||
# Base libraries
|
# Base libraries
|
||||||
require "couchrest/model/casted_model"
|
require "couchrest/model/casted_model"
|
||||||
require "couchrest/model/base"
|
require "couchrest/model/base"
|
||||||
|
|
||||||
# Add rails support *after* everything has loaded
|
# Add rails support *after* everything has loaded
|
||||||
|
|
||||||
require "couchrest/railtie"
|
require "couchrest/railtie"
|
||||||
|
|
|
@ -24,8 +24,72 @@ class DummyModel < CouchRest::Model::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# set dirty configuration, return previous configuration setting
|
||||||
|
def set_dirty(value)
|
||||||
|
orig = nil
|
||||||
|
CouchRest::Model::Base.configure do |config|
|
||||||
|
orig = config.use_dirty
|
||||||
|
config.use_dirty = value
|
||||||
|
end
|
||||||
|
Card.instance_eval do
|
||||||
|
self.use_dirty = value
|
||||||
|
end
|
||||||
|
orig
|
||||||
|
end
|
||||||
|
|
||||||
describe "Dirty" do
|
describe "With use_dirty(off)" do
|
||||||
|
|
||||||
|
before(:all) do
|
||||||
|
@use_dirty_orig = set_dirty(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
# turn dirty back to default
|
||||||
|
after(:all) do
|
||||||
|
set_dirty(@use_dirty_orig)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "changes" do
|
||||||
|
|
||||||
|
it "should not respond to the changes method" do
|
||||||
|
@card = Card.new
|
||||||
|
@card.first_name = "andrew"
|
||||||
|
@card.changes.should == {}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "changed?" do
|
||||||
|
|
||||||
|
it "should not record changes" do
|
||||||
|
@card = Card.new
|
||||||
|
@card.first_name = "andrew"
|
||||||
|
@card.changed?.should be_false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "save" do
|
||||||
|
|
||||||
|
it "should save unchanged records" do
|
||||||
|
@card = Card.create!(:first_name => "matt")
|
||||||
|
@card = Card.find(@card.id)
|
||||||
|
@card.database.should_receive(:save_doc).and_return({"ok" => true})
|
||||||
|
@card.save
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "With use_dirty(on)" do
|
||||||
|
|
||||||
|
before(:all) do
|
||||||
|
@use_dirty_orig = set_dirty(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
# turn dirty back to default
|
||||||
|
after(:all) do
|
||||||
|
set_dirty(@use_dirty_orig)
|
||||||
|
end
|
||||||
|
|
||||||
describe "changes" do
|
describe "changes" do
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue