mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
Sync 'rails/rails/master'
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
unless defined? ApplicationController
|
||||
class ApplicationController < ActionController::Base
|
||||
end
|
||||
end
|
||||
|
||||
class UploadTestController < ActionController::Base
|
||||
def update
|
||||
SessionUploadTest.last_request_type = ActionController::Base.param_parsers[request.content_type]
|
||||
render :text => "got here"
|
||||
end
|
||||
|
||||
def read
|
||||
render :text => "File: #{params[:uploaded_data].read}"
|
||||
end
|
||||
end
|
||||
|
||||
class SessionUploadTest < ActionController::IntegrationTest
|
||||
FILES_DIR = File.dirname(__FILE__) + '/../fixtures/multipart'
|
||||
|
||||
class << self
|
||||
attr_accessor :last_request_type
|
||||
end
|
||||
|
||||
def test_upload_and_read_file
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FILES_DIR + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
|
||||
# The lint wrapper is used in integration tests
|
||||
# instead of a normal StringIO class
|
||||
InputWrapper = Rack::Lint::InputWrapper
|
||||
|
||||
def test_post_with_upload_with_unrewindable_input
|
||||
InputWrapper.any_instance.expects(:rewind).raises(Errno::ESPIPE)
|
||||
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FILES_DIR + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
|
||||
def test_post_with_upload_with_params_parsing
|
||||
with_test_routing do
|
||||
params = { :uploaded_data => fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") }
|
||||
post '/update', params, :location => 'blah'
|
||||
assert_equal(:multipart_form, SessionUploadTest.last_request_type)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def with_test_routing
|
||||
with_routing do |set|
|
||||
set.draw do |map|
|
||||
map.update 'update', :controller => "upload_test", :action => "update", :method => :post
|
||||
map.read 'read', :controller => "upload_test", :action => "read", :method => :post
|
||||
end
|
||||
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,167 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class MultipartParamsParsingTest < ActionController::IntegrationTest
|
||||
class TestController < ActionController::Base
|
||||
class << self
|
||||
attr_accessor :last_request_parameters, :last_request_type
|
||||
end
|
||||
|
||||
def parse
|
||||
self.class.last_request_type = ActionController::Base.param_parsers[request.content_type]
|
||||
self.class.last_request_parameters = request.request_parameters
|
||||
head :ok
|
||||
end
|
||||
|
||||
def read
|
||||
render :text => "File: #{params[:uploaded_data].read}"
|
||||
end
|
||||
end
|
||||
|
||||
FIXTURE_PATH = File.dirname(__FILE__) + '/../../fixtures/multipart'
|
||||
|
||||
def teardown
|
||||
TestController.last_request_parameters = nil
|
||||
TestController.last_request_type = nil
|
||||
end
|
||||
|
||||
test "parses single parameter" do
|
||||
assert_equal({ 'foo' => 'bar' }, parse_multipart('single_parameter'))
|
||||
end
|
||||
|
||||
test "parses bracketed parameters" do
|
||||
assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
|
||||
end
|
||||
|
||||
test "parses text file" do
|
||||
params = parse_multipart('text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
assert_equal 'contents', file.read
|
||||
end
|
||||
|
||||
test "parses boundary problem file" do
|
||||
params = parse_multipart('boundary_problem_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
|
||||
file = params['file']
|
||||
foo = params['foo']
|
||||
|
||||
assert_kind_of Tempfile, file
|
||||
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
|
||||
assert_equal 'bar', foo
|
||||
end
|
||||
|
||||
test "parses large text file" do
|
||||
params = parse_multipart('large_text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
|
||||
assert_kind_of Tempfile, file
|
||||
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
assert ('a' * 20480) == file.read
|
||||
end
|
||||
|
||||
test "parses binary file" do
|
||||
params = parse_multipart('binary_file')
|
||||
assert_equal %w(file flowers foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'file.csv', file.original_filename
|
||||
assert_nil file.content_type
|
||||
assert_equal 'contents', file.read
|
||||
|
||||
file = params['flowers']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'flowers.jpg', file.original_filename
|
||||
assert_equal "image/jpeg", file.content_type
|
||||
assert_equal 19512, file.size
|
||||
end
|
||||
|
||||
test "parses mixed files" do
|
||||
params = parse_multipart('mixed_files')
|
||||
assert_equal %w(files foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
# Ruby CGI doesn't handle multipart/mixed for us.
|
||||
files = params['files']
|
||||
assert_kind_of String, files
|
||||
files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding)
|
||||
assert_equal 19756, files.size
|
||||
end
|
||||
|
||||
test "uploads and parses parameters" do
|
||||
with_test_routing do
|
||||
params = { :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/mona_lisa.jpg", "image/jpg") }
|
||||
post '/parse', params, :location => 'blah'
|
||||
assert_equal(:multipart_form, TestController.last_request_type)
|
||||
end
|
||||
end
|
||||
|
||||
test "uploads and reads file" do
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
|
||||
# The lint wrapper is used in integration tests
|
||||
# instead of a normal StringIO class
|
||||
InputWrapper = Rack::Lint::InputWrapper
|
||||
|
||||
test "parses unwindable stream" do
|
||||
InputWrapper.any_instance.expects(:rewind).raises(Errno::ESPIPE)
|
||||
params = parse_multipart('large_text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
end
|
||||
|
||||
test "uploads and reads file with unwindable input" do
|
||||
InputWrapper.any_instance.expects(:rewind).raises(Errno::ESPIPE)
|
||||
|
||||
with_test_routing do
|
||||
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
|
||||
assert_equal "File: Hello", response.body
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def fixture(name)
|
||||
File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
|
||||
{ "rack.input" => file.read,
|
||||
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
|
||||
"CONTENT_LENGTH" => file.stat.size.to_s }
|
||||
end
|
||||
end
|
||||
|
||||
def parse_multipart(name)
|
||||
with_test_routing do
|
||||
headers = fixture(name)
|
||||
post "/parse", headers.delete("rack.input"), headers
|
||||
assert_response :ok
|
||||
TestController.last_request_parameters
|
||||
end
|
||||
end
|
||||
|
||||
def with_test_routing
|
||||
with_routing do |set|
|
||||
set.draw do |map|
|
||||
map.connect ':action', :controller => "multipart_params_parsing_test/test"
|
||||
end
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,171 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class UrlEncodedParamsParsingTest < ActionController::IntegrationTest
|
||||
class TestController < ActionController::Base
|
||||
class << self
|
||||
attr_accessor :last_request_parameters, :last_request_type
|
||||
end
|
||||
|
||||
def parse
|
||||
self.class.last_request_parameters = request.request_parameters
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
def teardown
|
||||
TestController.last_request_parameters = nil
|
||||
end
|
||||
|
||||
test "parses unbalanced query string with array" do
|
||||
assert_parses(
|
||||
{'location' => ["1", "2"], 'age_group' => ["2"]},
|
||||
"location[]=1&location[]=2&age_group[]=2"
|
||||
)
|
||||
end
|
||||
|
||||
test "parses nested hash" do
|
||||
query = [
|
||||
"note[viewers][viewer][][type]=User",
|
||||
"note[viewers][viewer][][id]=1",
|
||||
"note[viewers][viewer][][type]=Group",
|
||||
"note[viewers][viewer][][id]=2"
|
||||
].join("&")
|
||||
|
||||
expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
|
||||
assert_parses(expected, query)
|
||||
end
|
||||
|
||||
test "parses more complex nesting" do
|
||||
query = [
|
||||
"customers[boston][first][name]=David",
|
||||
"customers[boston][first][url]=http://David",
|
||||
"customers[boston][second][name]=Allan",
|
||||
"customers[boston][second][url]=http://Allan",
|
||||
"something_else=blah",
|
||||
"something_nil=",
|
||||
"something_empty=",
|
||||
"products[first]=Apple Computer",
|
||||
"products[second]=Pc",
|
||||
"=Save"
|
||||
].join("&")
|
||||
|
||||
expected = {
|
||||
"customers" => {
|
||||
"boston" => {
|
||||
"first" => {
|
||||
"name" => "David",
|
||||
"url" => "http://David"
|
||||
},
|
||||
"second" => {
|
||||
"name" => "Allan",
|
||||
"url" => "http://Allan"
|
||||
}
|
||||
}
|
||||
},
|
||||
"something_else" => "blah",
|
||||
"something_empty" => "",
|
||||
"something_nil" => "",
|
||||
"products" => {
|
||||
"first" => "Apple Computer",
|
||||
"second" => "Pc"
|
||||
}
|
||||
}
|
||||
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with array" do
|
||||
query = "selected[]=1&selected[]=2&selected[]=3"
|
||||
expected = { "selected" => [ "1", "2", "3" ] }
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with non alphanumeric name" do
|
||||
query = "a/b[c]=d"
|
||||
expected = { "a/b" => { "c" => "d" }}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with single brackets in the middle" do
|
||||
query = "a/b[c]d=e"
|
||||
expected = { "a/b" => {} }
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with separated brackets" do
|
||||
query = "a/b@[c]d[e]=f"
|
||||
expected = { "a/b@" => { }}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with separated brackets and array" do
|
||||
query = "a/b@[c]d[e][]=f"
|
||||
expected = { "a/b@" => { }}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with unmatched brackets and array" do
|
||||
query = "a/b@[c][d[e][]=f"
|
||||
expected = { "a/b@" => { "c" => { }}}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with nil key" do
|
||||
query = "=&test2=value1"
|
||||
expected = { "test2" => "value1" }
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with array prefix and hashes" do
|
||||
query = "a[][b][c]=d"
|
||||
expected = {"a" => [{"b" => {"c" => "d"}}]}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with complex nesting" do
|
||||
query = "a[][b][c][][d][]=e"
|
||||
expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
test "parses params with file path" do
|
||||
query = [
|
||||
"customers[boston][first][name]=David",
|
||||
"something_else=blah",
|
||||
"logo=#{File.expand_path(__FILE__)}"
|
||||
].join("&")
|
||||
|
||||
expected = {
|
||||
"customers" => {
|
||||
"boston" => {
|
||||
"first" => {
|
||||
"name" => "David"
|
||||
}
|
||||
}
|
||||
},
|
||||
"something_else" => "blah",
|
||||
"logo" => File.expand_path(__FILE__),
|
||||
}
|
||||
|
||||
assert_parses expected, query
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def with_test_routing
|
||||
with_routing do |set|
|
||||
set.draw do |map|
|
||||
map.connect ':action', :controller => "url_encoded_params_parsing_test/test"
|
||||
end
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def assert_parses(expected, actual)
|
||||
with_test_routing do
|
||||
post "/parse", actual
|
||||
assert_response :ok
|
||||
assert_equal(expected, TestController.last_request_parameters)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -405,308 +405,3 @@ class RequestTest < ActiveSupport::TestCase
|
||||
@request.request_method(true)
|
||||
end
|
||||
end
|
||||
|
||||
class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase
|
||||
def test_unbalanced_query_string_with_array
|
||||
assert_equal(
|
||||
{'location' => ["1", "2"], 'age_group' => ["2"]},
|
||||
ActionController::RequestParser.parse_request_parameters({'location[]' => ["1", "2"], 'age_group[]' => ["2"]})
|
||||
)
|
||||
end
|
||||
|
||||
def test_request_hash_parsing
|
||||
query = {
|
||||
"note[viewers][viewer][][type]" => ["User", "Group"],
|
||||
"note[viewers][viewer][][id]" => ["1", "2"]
|
||||
}
|
||||
|
||||
expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
|
||||
|
||||
assert_equal(expected, ActionController::RequestParser.parse_request_parameters(query))
|
||||
end
|
||||
|
||||
def test_parse_params
|
||||
input = {
|
||||
"customers[boston][first][name]" => [ "David" ],
|
||||
"customers[boston][first][url]" => [ "http://David" ],
|
||||
"customers[boston][second][name]" => [ "Allan" ],
|
||||
"customers[boston][second][url]" => [ "http://Allan" ],
|
||||
"something_else" => [ "blah" ],
|
||||
"something_nil" => [ nil ],
|
||||
"something_empty" => [ "" ],
|
||||
"products[first]" => [ "Apple Computer" ],
|
||||
"products[second]" => [ "Pc" ],
|
||||
"" => [ 'Save' ]
|
||||
}
|
||||
|
||||
expected_output = {
|
||||
"customers" => {
|
||||
"boston" => {
|
||||
"first" => {
|
||||
"name" => "David",
|
||||
"url" => "http://David"
|
||||
},
|
||||
"second" => {
|
||||
"name" => "Allan",
|
||||
"url" => "http://Allan"
|
||||
}
|
||||
}
|
||||
},
|
||||
"something_else" => "blah",
|
||||
"something_empty" => "",
|
||||
"something_nil" => "",
|
||||
"products" => {
|
||||
"first" => "Apple Computer",
|
||||
"second" => "Pc"
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
UploadedStringIO = ActionController::UploadedStringIO
|
||||
class MockUpload < UploadedStringIO
|
||||
def initialize(content_type, original_path, *args)
|
||||
self.content_type = content_type
|
||||
self.original_path = original_path
|
||||
super *args
|
||||
end
|
||||
end
|
||||
|
||||
def test_parse_params_from_multipart_upload
|
||||
file = MockUpload.new('img/jpeg', 'foo.jpg')
|
||||
ie_file = MockUpload.new('img/jpeg', 'c:\\Documents and Settings\\foo\\Desktop\\bar.jpg')
|
||||
non_file_text_part = MockUpload.new('text/plain', '', 'abc')
|
||||
|
||||
input = {
|
||||
"something" => [ UploadedStringIO.new("") ],
|
||||
"array_of_stringios" => [[ UploadedStringIO.new("One"), UploadedStringIO.new("Two") ]],
|
||||
"mixed_types_array" => [[ UploadedStringIO.new("Three"), "NotStringIO" ]],
|
||||
"mixed_types_as_checkboxes[strings][nested]" => [[ file, "String", UploadedStringIO.new("StringIO")]],
|
||||
"ie_mixed_types_as_checkboxes[strings][nested]" => [[ ie_file, "String", UploadedStringIO.new("StringIO")]],
|
||||
"products[string]" => [ UploadedStringIO.new("Apple Computer") ],
|
||||
"products[file]" => [ file ],
|
||||
"ie_products[string]" => [ UploadedStringIO.new("Microsoft") ],
|
||||
"ie_products[file]" => [ ie_file ],
|
||||
"text_part" => [non_file_text_part]
|
||||
}
|
||||
|
||||
expected_output = {
|
||||
"something" => "",
|
||||
"array_of_stringios" => ["One", "Two"],
|
||||
"mixed_types_array" => [ "Three", "NotStringIO" ],
|
||||
"mixed_types_as_checkboxes" => {
|
||||
"strings" => {
|
||||
"nested" => [ file, "String", "StringIO" ]
|
||||
},
|
||||
},
|
||||
"ie_mixed_types_as_checkboxes" => {
|
||||
"strings" => {
|
||||
"nested" => [ ie_file, "String", "StringIO" ]
|
||||
},
|
||||
},
|
||||
"products" => {
|
||||
"string" => "Apple Computer",
|
||||
"file" => file
|
||||
},
|
||||
"ie_products" => {
|
||||
"string" => "Microsoft",
|
||||
"file" => ie_file
|
||||
},
|
||||
"text_part" => "abc"
|
||||
}
|
||||
|
||||
params = ActionController::RequestParser.parse_request_parameters(input)
|
||||
assert_equal expected_output, params
|
||||
|
||||
# Lone filenames are preserved.
|
||||
assert_equal 'foo.jpg', params['mixed_types_as_checkboxes']['strings']['nested'].first.original_filename
|
||||
assert_equal 'foo.jpg', params['products']['file'].original_filename
|
||||
|
||||
# But full Windows paths are reduced to their basename.
|
||||
assert_equal 'bar.jpg', params['ie_mixed_types_as_checkboxes']['strings']['nested'].first.original_filename
|
||||
assert_equal 'bar.jpg', params['ie_products']['file'].original_filename
|
||||
end
|
||||
|
||||
def test_parse_params_with_file
|
||||
input = {
|
||||
"customers[boston][first][name]" => [ "David" ],
|
||||
"something_else" => [ "blah" ],
|
||||
"logo" => [ File.new(File.dirname(__FILE__) + "/rack_test.rb").path ]
|
||||
}
|
||||
|
||||
expected_output = {
|
||||
"customers" => {
|
||||
"boston" => {
|
||||
"first" => {
|
||||
"name" => "David"
|
||||
}
|
||||
}
|
||||
},
|
||||
"something_else" => "blah",
|
||||
"logo" => File.new(File.dirname(__FILE__) + "/rack_test.rb").path,
|
||||
}
|
||||
|
||||
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_array
|
||||
input = { "selected[]" => [ "1", "2", "3" ] }
|
||||
|
||||
expected_output = { "selected" => [ "1", "2", "3" ] }
|
||||
|
||||
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_non_alphanumeric_name
|
||||
input = { "a/b[c]" => %w(d) }
|
||||
expected = { "a/b" => { "c" => "d" }}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_single_brackets_in_middle
|
||||
input = { "a/b[c]d" => %w(e) }
|
||||
expected = { "a/b" => {} }
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_separated_brackets
|
||||
input = { "a/b@[c]d[e]" => %w(f) }
|
||||
expected = { "a/b@" => { }}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_separated_brackets_and_array
|
||||
input = { "a/b@[c]d[e][]" => %w(f) }
|
||||
expected = { "a/b@" => { }}
|
||||
assert_equal expected , ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_unmatched_brackets_and_array
|
||||
input = { "a/b@[c][d[e][]" => %w(f) }
|
||||
expected = { "a/b@" => { "c" => { }}}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_nil_key
|
||||
input = { nil => nil, "test2" => %w(value1) }
|
||||
expected = { "test2" => "value1" }
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_array_prefix_and_hashes
|
||||
input = { "a[][b][c]" => %w(d) }
|
||||
expected = {"a" => [{"b" => {"c" => "d"}}]}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
|
||||
def test_parse_params_with_complex_nesting
|
||||
input = { "a[][b][c][][d][]" => %w(e) }
|
||||
expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
|
||||
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
|
||||
end
|
||||
end
|
||||
|
||||
class MultipartRequestParameterParsingTest < ActiveSupport::TestCase
|
||||
FIXTURE_PATH = File.dirname(__FILE__) + '/../fixtures/multipart'
|
||||
|
||||
def test_single_parameter
|
||||
params = parse_multipart('single_parameter')
|
||||
assert_equal({ 'foo' => 'bar' }, params)
|
||||
end
|
||||
|
||||
def test_bracketed_param
|
||||
assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
|
||||
end
|
||||
|
||||
def test_text_file
|
||||
params = parse_multipart('text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
assert_equal 'contents', file.read
|
||||
end
|
||||
|
||||
def test_boundary_problem_file
|
||||
params = parse_multipart('boundary_problem_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
|
||||
file = params['file']
|
||||
foo = params['foo']
|
||||
|
||||
assert_kind_of Tempfile, file
|
||||
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
|
||||
assert_equal 'bar', foo
|
||||
end
|
||||
|
||||
def test_large_text_file
|
||||
params = parse_multipart('large_text_file')
|
||||
assert_equal %w(file foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
|
||||
assert_kind_of Tempfile, file
|
||||
|
||||
assert_equal 'file.txt', file.original_filename
|
||||
assert_equal "text/plain", file.content_type
|
||||
assert ('a' * 20480) == file.read
|
||||
end
|
||||
|
||||
uses_mocha "test_no_rewind_stream" do
|
||||
def test_no_rewind_stream
|
||||
# Ensures that parse_multipart_form_parameters works with streams that cannot be rewound
|
||||
file = File.open(File.join(FIXTURE_PATH, 'large_text_file'), 'rb')
|
||||
file.expects(:rewind).raises(Errno::ESPIPE)
|
||||
params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {})
|
||||
assert_not_equal 0, file.pos # file was not rewound after reading
|
||||
end
|
||||
end
|
||||
|
||||
def test_binary_file
|
||||
params = parse_multipart('binary_file')
|
||||
assert_equal %w(file flowers foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
file = params['file']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'file.csv', file.original_filename
|
||||
assert_nil file.content_type
|
||||
assert_equal 'contents', file.read
|
||||
|
||||
file = params['flowers']
|
||||
assert_kind_of StringIO, file
|
||||
assert_equal 'flowers.jpg', file.original_filename
|
||||
assert_equal "image/jpeg", file.content_type
|
||||
assert_equal 19512, file.size
|
||||
#assert_equal File.read(File.dirname(__FILE__) + '/../../../activerecord/test/fixtures/flowers.jpg'), file.read
|
||||
end
|
||||
|
||||
def test_mixed_files
|
||||
params = parse_multipart('mixed_files')
|
||||
assert_equal %w(files foo), params.keys.sort
|
||||
assert_equal 'bar', params['foo']
|
||||
|
||||
# Ruby CGI doesn't handle multipart/mixed for us.
|
||||
files = params['files']
|
||||
assert_kind_of String, files
|
||||
files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding)
|
||||
assert_equal 19756, files.size
|
||||
end
|
||||
|
||||
private
|
||||
def parse_multipart(name)
|
||||
File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
|
||||
params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {})
|
||||
assert_equal 0, file.pos # file was rewound after reading
|
||||
params
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
*2.3.0/3.0*
|
||||
|
||||
* Support nested transactions using database savepoints. #383 [Jonathan Viney, Hongli Lai]
|
||||
|
||||
* Added dynamic scopes ala dynamic finders #1648 [Yaroslav Markin]
|
||||
|
||||
* Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 [Yaroslav Markin]
|
||||
|
||||
@@ -53,36 +53,120 @@ module ActiveRecord
|
||||
def delete(sql, name = nil)
|
||||
delete_sql(sql, name)
|
||||
end
|
||||
|
||||
# Checks whether there is currently no transaction active. This is done
|
||||
# by querying the database driver, and does not use the transaction
|
||||
# house-keeping information recorded by #increment_open_transactions and
|
||||
# friends.
|
||||
#
|
||||
# Returns true if there is no transaction active, false if there is a
|
||||
# transaction active, and nil if this information is unknown.
|
||||
#
|
||||
# Not all adapters supports transaction state introspection. Currently,
|
||||
# only the PostgreSQL adapter supports this.
|
||||
def outside_transaction?
|
||||
nil
|
||||
end
|
||||
|
||||
# Runs the given block in a database transaction, and returns the result
|
||||
# of the block.
|
||||
#
|
||||
# == Nested transactions support
|
||||
#
|
||||
# Most databases don't support true nested transactions. At the time of
|
||||
# writing, the only database that supports true nested transactions that
|
||||
# we're aware of, is MS-SQL.
|
||||
#
|
||||
# In order to get around this problem, #transaction will emulate the effect
|
||||
# of nested transactions, by using savepoints:
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
|
||||
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
|
||||
#
|
||||
# It is safe to call this method if a database transaction is already open,
|
||||
# i.e. if #transaction is called within another #transaction block. In case
|
||||
# of a nested call, #transaction will behave as follows:
|
||||
#
|
||||
# - The block will be run without doing anything. All database statements
|
||||
# that happen within the block are effectively appended to the already
|
||||
# open database transaction.
|
||||
# - However, if +requires_new+ is set, the block will be wrapped in a
|
||||
# database savepoint acting as a sub-transaction.
|
||||
#
|
||||
# === Caveats
|
||||
#
|
||||
# MySQL doesn't support DDL transactions. If you perform a DDL operation,
|
||||
# then any created savepoints will be automatically released. For example,
|
||||
# if you've created a savepoint, then you execute a CREATE TABLE statement,
|
||||
# then the savepoint that was created will be automatically released.
|
||||
#
|
||||
# This means that, on MySQL, you shouldn't execute DDL operations inside
|
||||
# a #transaction call that you know might create a savepoint. Otherwise,
|
||||
# #transaction will raise exceptions when it tries to release the
|
||||
# already-automatically-released savepoints:
|
||||
#
|
||||
# Model.connection.transaction do # BEGIN
|
||||
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
|
||||
# Model.connection.create_table(...)
|
||||
# # active_record_1 now automatically released
|
||||
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
|
||||
# end
|
||||
def transaction(options = {})
|
||||
options.assert_valid_keys :requires_new, :joinable
|
||||
|
||||
last_transaction_joinable, @transaction_joinable =
|
||||
@transaction_joinable, options[:joinable] || true
|
||||
requires_new = options[:requires_new] || !last_transaction_joinable
|
||||
|
||||
# Wrap a block in a transaction. Returns result of block.
|
||||
def transaction(start_db_transaction = true)
|
||||
transaction_open = false
|
||||
begin
|
||||
if block_given?
|
||||
if start_db_transaction
|
||||
begin_db_transaction
|
||||
if requires_new || open_transactions == 0
|
||||
if open_transactions == 0
|
||||
begin_db_transaction
|
||||
elsif requires_new
|
||||
create_savepoint
|
||||
end
|
||||
increment_open_transactions
|
||||
transaction_open = true
|
||||
end
|
||||
yield
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
if transaction_open
|
||||
if transaction_open && !outside_transaction?
|
||||
transaction_open = false
|
||||
rollback_db_transaction
|
||||
decrement_open_transactions
|
||||
if open_transactions == 0
|
||||
rollback_db_transaction
|
||||
else
|
||||
rollback_to_savepoint
|
||||
end
|
||||
end
|
||||
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
|
||||
end
|
||||
ensure
|
||||
if transaction_open
|
||||
@transaction_joinable = last_transaction_joinable
|
||||
|
||||
if outside_transaction?
|
||||
@open_transactions = 0
|
||||
elsif transaction_open
|
||||
decrement_open_transactions
|
||||
begin
|
||||
commit_db_transaction
|
||||
if open_transactions == 0
|
||||
commit_db_transaction
|
||||
else
|
||||
release_savepoint
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
rollback_db_transaction
|
||||
if open_transactions == 0
|
||||
rollback_db_transaction
|
||||
else
|
||||
rollback_to_savepoint
|
||||
end
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Begins the transaction (and turns off auto-committing).
|
||||
def begin_db_transaction() end
|
||||
|
||||
|
||||
@@ -66,6 +66,12 @@ module ActiveRecord
|
||||
def supports_ddl_transactions?
|
||||
false
|
||||
end
|
||||
|
||||
# Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite
|
||||
# does not.
|
||||
def supports_savepoints?
|
||||
false
|
||||
end
|
||||
|
||||
# Should primary key values be selected from their corresponding
|
||||
# sequence before the insert statement? If true, next_sequence_value
|
||||
@@ -160,6 +166,23 @@ module ActiveRecord
|
||||
@open_transactions -= 1
|
||||
end
|
||||
|
||||
def transaction_joinable=(joinable)
|
||||
@transaction_joinable = joinable
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
end
|
||||
|
||||
def current_savepoint_name
|
||||
"active_record_#{open_transactions}"
|
||||
end
|
||||
|
||||
def log_info(sql, name, ms)
|
||||
if @logger && @logger.debug?
|
||||
name = '%s (%.1fms)' % [name || 'SQL', ms]
|
||||
|
||||
@@ -210,6 +210,10 @@ module ActiveRecord
|
||||
def supports_migrations? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def supports_savepoints? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def native_database_types #:nodoc:
|
||||
NATIVE_DATABASE_TYPES
|
||||
@@ -349,6 +353,17 @@ module ActiveRecord
|
||||
# Transactions aren't supported
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
execute("SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def add_limit_offset!(sql, options) #:nodoc:
|
||||
if limit = options[:limit]
|
||||
|
||||
@@ -272,6 +272,10 @@ module ActiveRecord
|
||||
def supports_ddl_transactions?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_savepoints?
|
||||
true
|
||||
end
|
||||
|
||||
# Returns the configured supported identifier length supported by PostgreSQL,
|
||||
# or report the default of 63 on PostgreSQL 7.x.
|
||||
@@ -528,45 +532,28 @@ module ActiveRecord
|
||||
def rollback_db_transaction
|
||||
execute "ROLLBACK"
|
||||
end
|
||||
|
||||
# ruby-pg defines Ruby constants for transaction status,
|
||||
# ruby-postgres does not.
|
||||
PQTRANS_IDLE = defined?(PGconn::PQTRANS_IDLE) ? PGconn::PQTRANS_IDLE : 0
|
||||
|
||||
# Check whether a transaction is active.
|
||||
def transaction_active?
|
||||
@connection.transaction_status != PQTRANS_IDLE
|
||||
end
|
||||
|
||||
# Wrap a block in a transaction. Returns result of block.
|
||||
def transaction(start_db_transaction = true)
|
||||
transaction_open = false
|
||||
begin
|
||||
if block_given?
|
||||
if start_db_transaction
|
||||
begin_db_transaction
|
||||
transaction_open = true
|
||||
end
|
||||
yield
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
if transaction_open && transaction_active?
|
||||
transaction_open = false
|
||||
rollback_db_transaction
|
||||
end
|
||||
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
|
||||
end
|
||||
ensure
|
||||
if transaction_open && transaction_active?
|
||||
begin
|
||||
commit_db_transaction
|
||||
rescue Exception => database_transaction_rollback
|
||||
rollback_db_transaction
|
||||
raise
|
||||
end
|
||||
|
||||
if PGconn.public_method_defined?(:transaction_status)
|
||||
# ruby-pg defines Ruby constants for transaction status,
|
||||
# ruby-postgres does not.
|
||||
PQTRANS_IDLE = defined?(PGconn::PQTRANS_IDLE) ? PGconn::PQTRANS_IDLE : 0
|
||||
|
||||
def outside_transaction?
|
||||
@connection.transaction_status == PQTRANS_IDLE
|
||||
end
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
execute("SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
# SCHEMA STATEMENTS ========================================
|
||||
|
||||
|
||||
@@ -516,7 +516,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
|
||||
|
||||
all_loaded_fixtures.update(fixtures_map)
|
||||
|
||||
connection.transaction(connection.open_transactions.zero?) do
|
||||
connection.transaction(:requires_new => true) do
|
||||
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
|
||||
fixtures.each { |fixture| fixture.insert_fixtures }
|
||||
|
||||
@@ -937,6 +937,7 @@ module ActiveRecord
|
||||
@@already_loaded_fixtures[self.class] = @loaded_fixtures
|
||||
end
|
||||
ActiveRecord::Base.connection.increment_open_transactions
|
||||
ActiveRecord::Base.connection.transaction_joinable = false
|
||||
ActiveRecord::Base.connection.begin_db_transaction
|
||||
# Load fixtures for every test.
|
||||
else
|
||||
|
||||
@@ -120,16 +120,66 @@ module ActiveRecord
|
||||
# end
|
||||
#
|
||||
# One should restart the entire transaction if a StatementError occurred.
|
||||
#
|
||||
# == Nested transactions
|
||||
#
|
||||
# #transaction calls can be nested. By default, this makes all database
|
||||
# statements in the nested transaction block become part of the parent
|
||||
# transaction. For example:
|
||||
#
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Kotori')
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Nemu')
|
||||
# raise ActiveRecord::Rollback
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# User.find(:all) # => empty
|
||||
#
|
||||
# It is also possible to requires a sub-transaction by passing
|
||||
# <tt>:requires_new => true</tt>. If anything goes wrong, the
|
||||
# database rolls back to the beginning of the sub-transaction
|
||||
# without rolling back the parent transaction. For example:
|
||||
#
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Kotori')
|
||||
# User.transaction(:requires_new => true) do
|
||||
# User.create(:username => 'Nemu')
|
||||
# raise ActiveRecord::Rollback
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# User.find(:all) # => Returns only Kotori
|
||||
#
|
||||
# Most databases don't support true nested transactions. At the time of
|
||||
# writing, the only database that we're aware of that supports true nested
|
||||
# transactions, is MS-SQL. Because of this, Active Record emulates nested
|
||||
# transactions by using savepoints. See
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
|
||||
# for more information about savepoints.
|
||||
#
|
||||
# === Caveats
|
||||
#
|
||||
# If you're on MySQL, then do not use DDL operations in nested transactions
|
||||
# blocks that are emulated with savepoints. That is, do not execute statements
|
||||
# like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
|
||||
# releases all savepoints upon executing a DDL operation. When #transaction
|
||||
# is finished and tries to release the savepoint it created earlier, a
|
||||
# database error will occur because the savepoint has already been
|
||||
# automatically released. The following example demonstrates the problem:
|
||||
#
|
||||
# Model.connection.transaction do # BEGIN
|
||||
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
|
||||
# Model.connection.create_table(...) # active_record_1 now automatically released
|
||||
# end # RELEASE savepoint active_record_1
|
||||
# # ^^^^ BOOM! database error!
|
||||
# end
|
||||
module ClassMethods
|
||||
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
||||
def transaction(&block)
|
||||
connection.increment_open_transactions
|
||||
|
||||
begin
|
||||
connection.transaction(connection.open_transactions == 1, &block)
|
||||
ensure
|
||||
connection.decrement_open_transactions
|
||||
end
|
||||
def transaction(options = {}, &block)
|
||||
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
|
||||
connection.transaction(options, &block)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -18,11 +18,43 @@ class DefaultTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:MysqlAdapter)
|
||||
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
|
||||
def test_default_integers
|
||||
default = Default.new
|
||||
assert_instance_of Fixnum, default.positive_integer
|
||||
assert_equal 1, default.positive_integer
|
||||
assert_instance_of Fixnum, default.negative_integer
|
||||
assert_equal -1, default.negative_integer
|
||||
assert_instance_of BigDecimal, default.decimal_number
|
||||
assert_equal BigDecimal.new("2.78"), default.decimal_number
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
def test_multiline_default_text
|
||||
# older postgres versions represent the default with escapes ("\\012" for a newline)
|
||||
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
|
||||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#MySQL 5 and higher is quirky with not null text/blob columns.
|
||||
#With MySQL Text/blob columns cannot have defaults. If the column is not null MySQL will report that the column has a null default
|
||||
#but it behaves as though the column had a default of ''
|
||||
if current_adapter?(:MysqlAdapter)
|
||||
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
|
||||
# ActiveRecord::Base#create! (and #save and other related methods) will
|
||||
# open a new transaction. When in transactional fixtures mode, this will
|
||||
# cause ActiveRecord to create a new savepoint. However, since MySQL doesn't
|
||||
# support DDL transactions, creating a table will result in any created
|
||||
# savepoints to be automatically released. This in turn causes the savepoint
|
||||
# release code in AbstractAdapter#transaction to fail.
|
||||
#
|
||||
# We don't want that to happen, so we disable transactional fixtures here.
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
# MySQL 5 and higher is quirky with not null text/blob columns.
|
||||
# With MySQL Text/blob columns cannot have defaults. If the column is not
|
||||
# null MySQL will report that the column has a null default
|
||||
# but it behaves as though the column had a default of ''
|
||||
def test_mysql_text_not_null_defaults
|
||||
klass = Class.new(ActiveRecord::Base)
|
||||
klass.table_name = 'test_mysql_text_not_null_defaults'
|
||||
@@ -48,8 +80,7 @@ class DefaultTest < ActiveRecord::TestCase
|
||||
ensure
|
||||
klass.connection.drop_table(klass.table_name) rescue nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
# MySQL uses an implicit default 0 rather than NULL unless in strict mode.
|
||||
# We use an implicit NULL so schema.rb is compatible with other databases.
|
||||
def test_mysql_integer_not_null_defaults
|
||||
@@ -77,24 +108,4 @@ class DefaultTest < ActiveRecord::TestCase
|
||||
klass.connection.drop_table(klass.table_name) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
|
||||
def test_default_integers
|
||||
default = Default.new
|
||||
assert_instance_of Fixnum, default.positive_integer
|
||||
assert_equal 1, default.positive_integer
|
||||
assert_instance_of Fixnum, default.negative_integer
|
||||
assert_equal -1, default.negative_integer
|
||||
assert_instance_of BigDecimal, default.decimal_number
|
||||
assert_equal BigDecimal.new("2.78"), default.decimal_number
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
def test_multiline_default_text
|
||||
# older postgres versions represent the default with escapes ("\\012" for a newline)
|
||||
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
|
||||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,7 +34,7 @@ rescue LoadError
|
||||
end
|
||||
|
||||
ActiveRecord::Base.connection.class.class_eval do
|
||||
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/]
|
||||
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/]
|
||||
|
||||
def execute_with_query_record(sql, name = nil, &block)
|
||||
$queries_executed ||= []
|
||||
|
||||
@@ -213,11 +213,104 @@ class TransactionTest < ActiveRecord::TestCase
|
||||
assert Topic.find(2).approved?, "Second should still be approved"
|
||||
end
|
||||
|
||||
def test_invalid_keys_for_transaction
|
||||
assert_raises ArgumentError do
|
||||
Topic.transaction :nested => true do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_force_savepoint_in_nested_transaction
|
||||
Topic.transaction do
|
||||
@first.approved = true
|
||||
@second.approved = false
|
||||
@first.save!
|
||||
@second.save!
|
||||
|
||||
begin
|
||||
Topic.transaction :requires_new => true do
|
||||
@first.happy = false
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
assert @first.reload.approved?
|
||||
assert !@second.reload.approved?
|
||||
end if Topic.connection.supports_savepoints?
|
||||
|
||||
def test_no_savepoint_in_nested_transaction_without_force
|
||||
Topic.transaction do
|
||||
@first.approved = true
|
||||
@second.approved = false
|
||||
@first.save!
|
||||
@second.save!
|
||||
|
||||
begin
|
||||
Topic.transaction do
|
||||
@first.approved = false
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
assert !@first.reload.approved?
|
||||
assert !@second.reload.approved?
|
||||
end if Topic.connection.supports_savepoints?
|
||||
|
||||
def test_many_savepoints
|
||||
Topic.transaction do
|
||||
@first.content = "One"
|
||||
@first.save!
|
||||
|
||||
begin
|
||||
Topic.transaction :requires_new => true do
|
||||
@first.content = "Two"
|
||||
@first.save!
|
||||
|
||||
begin
|
||||
Topic.transaction :requires_new => true do
|
||||
@first.content = "Three"
|
||||
@first.save!
|
||||
|
||||
begin
|
||||
Topic.transaction :requires_new => true do
|
||||
@first.content = "Four"
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
|
||||
@three = @first.reload.content
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
|
||||
@two = @first.reload.content
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
|
||||
@one = @first.reload.content
|
||||
end
|
||||
|
||||
assert_equal "One", @one
|
||||
assert_equal "Two", @two
|
||||
assert_equal "Three", @three
|
||||
end if Topic.connection.supports_savepoints?
|
||||
|
||||
uses_mocha 'mocking connection.commit_db_transaction' do
|
||||
def test_rollback_when_commit_raises
|
||||
Topic.connection.expects(:begin_db_transaction)
|
||||
Topic.connection.expects(:transaction_active?).returns(true) if current_adapter?(:PostgreSQLAdapter)
|
||||
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
|
||||
Topic.connection.expects(:outside_transaction?).returns(false)
|
||||
Topic.connection.expects(:rollback_db_transaction)
|
||||
|
||||
assert_raise RuntimeError do
|
||||
@@ -227,6 +320,39 @@ class TransactionTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter) && PGconn.public_method_defined?(:transaction_status)
|
||||
def test_outside_transaction_works
|
||||
Topic.logger.info("-------------")
|
||||
assert Topic.connection.outside_transaction?
|
||||
Topic.connection.begin_db_transaction
|
||||
assert !Topic.connection.outside_transaction?
|
||||
Topic.connection.rollback_db_transaction
|
||||
assert Topic.connection.outside_transaction?
|
||||
end
|
||||
|
||||
uses_mocha 'mocking connection.rollback_db_transaction' do
|
||||
def test_rollback_wont_be_executed_if_no_transaction_active
|
||||
assert_raise RuntimeError do
|
||||
Topic.transaction do
|
||||
Topic.connection.rollback_db_transaction
|
||||
Topic.connection.expects(:rollback_db_transaction).never
|
||||
raise "Rails doesn't scale!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active
|
||||
Topic.transaction do
|
||||
Topic.transaction do
|
||||
Topic.connection.rollback_db_transaction
|
||||
end
|
||||
assert_equal 0, Topic.connection.open_transactions
|
||||
end
|
||||
assert_equal 0, Topic.connection.open_transactions
|
||||
end
|
||||
end
|
||||
|
||||
def test_sqlite_add_column_in_transaction_raises_statement_invalid
|
||||
return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
|
||||
@@ -282,6 +408,45 @@ class TransactionTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = true
|
||||
fixtures :topics
|
||||
|
||||
def test_automatic_savepoint_in_outer_transaction
|
||||
@first = Topic.find(1)
|
||||
|
||||
begin
|
||||
Topic.transaction do
|
||||
@first.approved = true
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
assert !@first.reload.approved?
|
||||
end
|
||||
end
|
||||
|
||||
def test_no_automatic_savepoint_for_inner_transaction
|
||||
@first = Topic.find(1)
|
||||
|
||||
Topic.transaction do
|
||||
@first.approved = true
|
||||
@first.save!
|
||||
|
||||
begin
|
||||
Topic.transaction do
|
||||
@first.approved = false
|
||||
@first.save!
|
||||
raise
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
assert !@first.reload.approved?
|
||||
end
|
||||
end if Topic.connection.supports_savepoints?
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
class ConcurrentTransactionTest < TransactionTest
|
||||
use_concurrent_connections
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
*2.3.0 [Edge]*
|
||||
|
||||
* TimeWithZone#xmlschema accepts optional fraction_digits argument [#1725 state:resolved] [Nicholas Dainty]
|
||||
|
||||
* Object#tap shim for Ruby < 1.8.7. Similar to Object#returning, tap yields self then returns self. [Jeremy Kemper]
|
||||
array.select { ... }.tap(&:inspect).map { ... }
|
||||
|
||||
|
||||
@@ -99,8 +99,12 @@ module ActiveSupport
|
||||
"#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}"
|
||||
end
|
||||
|
||||
def xmlschema
|
||||
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, 'Z')}"
|
||||
def xmlschema(fraction_digits = 0)
|
||||
fraction = if fraction_digits > 0
|
||||
".%i" % time.usec.to_s[0, fraction_digits]
|
||||
end
|
||||
|
||||
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
|
||||
end
|
||||
alias_method :iso8601, :xmlschema
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ class TimeWithZoneTest < Test::Unit::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
def test_xmlschema_with_fractional_seconds
|
||||
silence_warnings do # silence warnings raised by tzinfo gem
|
||||
@twz += 0.123456 # advance the time by a fraction of a second
|
||||
assert_equal "1999-12-31T19:00:00.123-05:00", @twz.xmlschema(3)
|
||||
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(6)
|
||||
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(12)
|
||||
end
|
||||
end
|
||||
|
||||
def test_to_yaml
|
||||
silence_warnings do # silence warnings raised by tzinfo gem
|
||||
assert_equal "--- 1999-12-31 19:00:00 -05:00\n", @twz.to_yaml
|
||||
|
||||
Reference in New Issue
Block a user