Ooops! Fixed upgrade of Rails.

This commit is contained in:
Jacques Distler 2007-02-09 17:12:31 -06:00
parent 5536e6e79e
commit bba0cf6b10
177 changed files with 13221 additions and 0 deletions

View file

@ -0,0 +1,30 @@
require 'test/unit'
$:.unshift "#{File.dirname(__FILE__)}/../lib"
require 'action_mailer'
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
$:.unshift "#{File.dirname(__FILE__)}/fixtures/helpers"
ActionMailer::Base.template_root = "#{File.dirname(__FILE__)}/fixtures"
class MockSMTP
def self.deliveries
@@deliveries
end
def initialize
@@deliveries = []
end
def sendmail(mail, from, to)
@@deliveries << [mail, from, to]
end
end
class Net::SMTP
def self.start(*args)
yield MockSMTP.new
end
end

View file

@ -0,0 +1 @@
first mail

View file

@ -0,0 +1 @@
Have a lovely picture, from me. Enjoy!

View file

@ -0,0 +1,14 @@
Mime-Version: 1.0 (Apple Message framework v730)
Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
From: foo@example.com
Subject: testing
Date: Mon, 6 Jun 2005 22:21:22 +0200
To: blah@example.com
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain
A fax has arrived from remote ID ''.=0D=0A-----------------------=
-------------------------------------=0D=0ATime: 3/9/2006 3:50:52=
PM=0D=0AReceived from remote ID: =0D=0AInbound user ID XXXXXXXXXX, r=
outing code XXXXXXXXX=0D=0AResult: (0/352;0/0) Successful Send=0D=0AP=
age record: 1 - 1=0D=0AElapsed time: 00:58 on channel 11=0D=0A

View file

@ -0,0 +1,14 @@
From jamis@37signals.com Mon May 2 16:07:05 2005
Mime-Version: 1.0 (Apple Message framework v622)
Content-Transfer-Encoding: base64
Message-Id: <d3b8cf8e49f04480850c28713a1f473e@37signals.com>
Content-Type: text/plain;
charset=EUC-KR;
format=flowed
To: jamis@37signals.com
From: Jamis Buck <jamis@37signals.com>
Subject: Re: Test: =?UTF-8?B?Iua8ouWtlyI=?= mid =?UTF-8?B?Iua8ouWtlyI=?= tail
Date: Mon, 2 May 2005 16:07:05 -0600
tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin
wLogSmFtaXPA1LTPtNku

View file

@ -0,0 +1 @@
second mail

View file

@ -0,0 +1 @@
Ignored when searching for implicitly multipart parts.

View file

@ -0,0 +1,3 @@
Hello there,
Mr. <%= @recipient %>. Please see our greeting at <%= @welcome_url %>

View file

@ -0,0 +1,68 @@
require "#{File.dirname(__FILE__)}/abstract_unit"
class TestMailer < ActionMailer::Base
def signed_up_with_url(recipient)
@recipients = recipient
@subject = "[Signed up] Welcome #{recipient}"
@from = "system@loudthinking.com"
@sent_on = Time.local(2004, 12, 12)
@body["recipient"] = recipient
@body["welcome_url"] = url_for :host => "example.com", :controller => "welcome", :action => "greeting"
end
class <<self
attr_accessor :received_body
end
def receive(mail)
self.class.received_body = mail.body
end
end
class ActionMailerUrlTest < Test::Unit::TestCase
include ActionMailer::Quoting
def encode( text, charset="utf-8" )
quoted_printable( text, charset )
end
def new_mail( charset="utf-8" )
mail = TMail::Mail.new
mail.mime_version = "1.0"
if charset
mail.set_content_type "text", "plain", { "charset" => charset }
end
mail
end
def setup
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def test_signed_up_with_url
ActionController::Routing::Routes.draw do |map|
map.connect ':controller/:action/:id'
end
expected = new_mail
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
created = nil
assert_nothing_raised { created = TestMailer.create_signed_up_with_url(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised { TestMailer.deliver_signed_up_with_url(@recipient) }
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
end

View file

@ -0,0 +1,228 @@
require 'rexml/document'
module ActionController #:nodoc:
module Assertions #:nodoc:
module DeprecatedAssertions #:nodoc:
def assert_success(message=nil) #:nodoc:
assert_response(:success, message)
end
deprecate :assert_success => "use assert_response(:success)"
def assert_redirect(message=nil) #:nodoc:
assert_response(:redirect, message)
end
deprecate :assert_redirect => "use assert_response(:redirect)"
def assert_rendered_file(expected=nil, message=nil) #:nodoc:
assert_template(expected, message)
end
deprecate :assert_rendered_file => :assert_template
# ensure that the session has an object with the specified name
def assert_session_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not in the session <?>", key, @response.session)
assert_block(msg) { @response.has_session_object?(key) }
end
deprecate :assert_session_has => "use assert(@response.has_session_object?(key))"
# ensure that the session has no object with the specified name
def assert_session_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is in the session <?>", key, @response.session)
assert_block(msg) { !@response.has_session_object?(key) }
end
deprecate :assert_session_has_no => "use assert(!@response.has_session_object?(key))"
def assert_session_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in session['?'] but was <?>", expected, key, @response.session[key])
assert_block(msg) { expected == @response.session[key] }
end
deprecate :assert_session_equal => "use assert_equal(expected, @response[key])"
# -- cookie assertions ---------------------------------------------------
def assert_no_cookie(key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
msg = build_message(message, "<?> not expected in cookies['?']", actual, key)
assert_block(msg) { actual.nil? or actual.empty? }
end
deprecate :assert_no_cookie => "use assert(!@response.cookies.key?(key))"
def assert_cookie_equal(expected = nil, key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
actual = actual.first if actual
msg = build_message(message, "<?> expected in cookies['?'] but was <?>", expected, key, actual)
assert_block(msg) { expected == actual }
end
deprecate :assert_cookie_equal => "use assert(@response.cookies.key?(key))"
# -- flash assertions ---------------------------------------------------
# ensure that the flash has an object with the specified name
def assert_flash_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not in the flash <?>", key, @response.flash)
assert_block(msg) { @response.has_flash_object?(key) }
end
deprecate :assert_flash_has => "use assert(@response.has_flash_object?(key))"
# ensure that the flash has no object with the specified name
def assert_flash_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is in the flash <?>", key, @response.flash)
assert_block(msg) { !@response.has_flash_object?(key) }
end
deprecate :assert_flash_has_no => "use assert(!@response.has_flash_object?(key))"
# ensure the flash exists
def assert_flash_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash does not exist <?>", @response.session['flash'] )
assert_block(msg) { @response.has_flash? }
end
deprecate :assert_flash_exists => "use assert(@response.has_flash?)"
# ensure the flash does not exist
def assert_flash_not_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash exists <?>", @response.flash)
assert_block(msg) { !@response.has_flash? }
end
deprecate :assert_flash_not_exists => "use assert(!@response.has_flash?)"
# ensure the flash is empty but existent
def assert_flash_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is not empty <?>", @response.flash)
assert_block(msg) { !@response.has_flash_with_contents? }
end
deprecate :assert_flash_empty => "use assert(!@response.has_flash_with_contents?)"
# ensure the flash is not empty
def assert_flash_not_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is empty")
assert_block(msg) { @response.has_flash_with_contents? }
end
deprecate :assert_flash_not_empty => "use assert(@response.has_flash_with_contents?)"
def assert_flash_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in flash['?'] but was <?>", expected, key, @response.flash[key])
assert_block(msg) { expected == @response.flash[key] }
end
deprecate :assert_flash_equal => "use assert_equal(expected, @response.flash[key])"
# ensure our redirection url is an exact match
def assert_redirect_url(url=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "<?> is not the redirected location <?>", url, @response.redirect_url)
assert_block(msg) { @response.redirect_url == url }
end
deprecate :assert_redirect_url => "use assert_equal(url, @response.redirect_url)"
# ensure our redirection url matches a pattern
def assert_redirect_url_match(pattern=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "<?> was not found in the location: <?>", pattern, @response.redirect_url)
assert_block(msg) { @response.redirect_url_match?(pattern) }
end
deprecate :assert_redirect_url_match => "use assert(@response.redirect_url_match?(pattern))"
# -- template assertions ------------------------------------------------
# ensure that a template object with the given name exists
def assert_template_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not a template object", key )
assert_block(msg) { @response.has_template_object?(key) }
end
deprecate :assert_template_has => "use assert(@response.has_template_object?(key))"
# ensure that a template object with the given name does not exist
def assert_template_has_no(key=nil,message=nil) #:nodoc:
msg = build_message(message, "<?> is a template object <?>", key, @response.template_objects[key])
assert_block(msg) { !@response.has_template_object?(key) }
end
deprecate :assert_template_has_no => "use assert(!@response.has_template_object?(key))"
# ensures that the object assigned to the template on +key+ is equal to +expected+ object.
def assert_template_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in assigns['?'] but was <?>", expected, key, @response.template.assigns[key.to_s])
assert_block(msg) { expected == @response.template.assigns[key.to_s] }
end
alias_method :assert_assigned_equal, :assert_template_equal
deprecate :assert_assigned_equal => "use assert_equal(expected, @response.template.assigns[key.to_s])"
deprecate :assert_template_equal => "use assert_equal(expected, @response.template.assigns[key.to_s])"
# Asserts that the template returns the +expected+ string or array based on the XPath +expression+.
# This will only work if the template rendered a valid XML document.
def assert_template_xpath_match(expression=nil, expected=nil, message=nil) #:nodoc:
xml, matches = REXML::Document.new(@response.body), []
xml.elements.each(expression) { |e| matches << e.text }
if matches.empty? then
msg = build_message(message, "<?> not found in document", expression)
flunk(msg)
return
elsif matches.length < 2 then
matches = matches.first
end
msg = build_message(message, "<?> found <?>, not <?>", expression, matches, expected)
assert_block(msg) { matches == expected }
end
deprecate :assert_template_xpath_match => "you should use assert_tag, instead"
# Assert the template object with the given name is an Active Record descendant and is valid.
def assert_valid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is invalid <?>)", record.errors.full_messages)
assert_block(msg) { record.valid? }
end
deprecate :assert_valid_record => "use assert(assigns(key).valid?)"
# Assert the template object with the given name is an Active Record descendant and is invalid.
def assert_invalid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is valid)")
assert_block(msg) { !record.valid? }
end
deprecate :assert_invalid_record => "use assert(!assigns(key).valid?)"
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid.
def assert_valid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| !record.errors.invalid?(col) }
msg = build_message(message, "Active Record has invalid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
deprecate :assert_valid_column_on_record => "use assert(!record.errors.invalid?(column)) instead"
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid.
def assert_invalid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| record.errors.invalid?(col) }
msg = build_message(message, "Active Record has valid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
deprecate :assert_invalid_column_on_record => "use assert(record.errors.invalid?(column)) instead"
private
def glue_columns(columns)
cols = []
cols << columns if columns.class == String
cols += columns if columns.class == Array
cols
end
def find_record_in_template(key = nil)
assert_not_nil assigns(key)
record = @response.template_objects[key]
assert_not_nil(record)
assert_kind_of ActiveRecord::Base, record
return record
end
end
end
end

View file

@ -0,0 +1,25 @@
module ActionController
module Assertions
module DomAssertions
# test 2 html strings to be equivalent, i.e. identical up to reordering of attributes
def assert_dom_equal(expected, actual, message="")
clean_backtrace do
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s)
assert_block(full_message) { expected_dom == actual_dom }
end
end
# negated form of +assert_dom_equivalent+
def assert_dom_not_equal(expected, actual, message="")
clean_backtrace do
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s)
assert_block(full_message) { expected_dom != actual_dom }
end
end
end
end
end

View file

@ -0,0 +1,12 @@
module ActionController
module Assertions
module ModelAssertions
# ensures that the passed record is valid by active record standards. returns the error messages if not
def assert_valid(record)
clean_backtrace do
assert record.valid?, record.errors.full_messages.join("\n")
end
end
end
end
end

View file

@ -0,0 +1,140 @@
require 'rexml/document'
require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document"
module ActionController
module Assertions
module ResponseAssertions
# Asserts that the response is one of the following types:
#
# * <tt>:success</tt>: Status code was 200
# * <tt>:redirect</tt>: Status code was in the 300-399 range
# * <tt>:missing</tt>: Status code was 404
# * <tt>:error</tt>: Status code was in the 500-599 range
#
# You can also pass an explicit status number like assert_response(501)
# or its symbolic equivalent assert_response(:not_implemented).
# See ActionController::StatusCodes for a full list.
def assert_response(type, message = nil)
clean_backtrace do
if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?")
assert_block("") { true } # to count the assertion
elsif type.is_a?(Fixnum) && @response.response_code == type
assert_block("") { true } # to count the assertion
elsif type.is_a?(Symbol) && @response.response_code == ActionController::StatusCodes::SYMBOL_TO_STATUS_CODE[type]
assert_block("") { true } # to count the assertion
else
assert_block(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) { false }
end
end
end
# Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial,
# such that assert_redirected_to(:controller => "weblog") will also match the redirection of
# redirect_to(:controller => "weblog", :action => "show") and so on.
def assert_redirected_to(options = {}, message=nil)
clean_backtrace do
assert_response(:redirect, message)
return true if options == @response.redirected_to
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
begin
url = {}
original = { :expected => options, :actual => @response.redirected_to.is_a?(Symbol) ? @response.redirected_to : @response.redirected_to.dup }
original.each do |key, value|
if value.is_a?(Symbol)
value = @controller.respond_to?(value, true) ? @controller.send(value) : @controller.send("hash_for_#{value}_url")
end
unless value.is_a?(Hash)
request = case value
when NilClass then nil
when /^\w+:\/\// then recognized_request_for(%r{^(\w+://.*?(/|$|\?))(.*)$} =~ value ? $3 : nil)
else recognized_request_for(value)
end
value = request.path_parameters if request
end
if value.is_a?(Hash) # stringify 2 levels of hash keys
if name = value.delete(:use_route)
route = ActionController::Routing::Routes.named_routes[name]
value.update(route.parameter_shell)
end
value.stringify_keys!
value.values.select { |v| v.is_a?(Hash) }.collect { |v| v.stringify_keys! }
if key == :expected && value['controller'] == @controller.controller_name && original[:actual].is_a?(Hash)
original[:actual].stringify_keys!
value.delete('controller') if original[:actual]['controller'].nil? || original[:actual]['controller'] == value['controller']
end
end
if value.respond_to?(:[]) && value['controller']
if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/')
new_controller_path = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path)
value['controller'] = new_controller_path if value['controller'] != new_controller_path && ActionController::Routing.possible_controllers.include?(new_controller_path)
end
value['controller'] = value['controller'][1..-1] if value['controller'].first == '/' # strip leading hash
end
url[key] = value
end
@response_diff = url[:expected].diff(url[:actual]) if url[:actual]
msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>), difference: <?>",
url[:actual], @response_diff)
assert_block(msg) do
url[:expected].keys.all? do |k|
if k == :controller then url[:expected][k] == ActionController::Routing.controller_relative_to(url[:actual][k], @controller.class.controller_path)
else parameterize(url[:expected][k]) == parameterize(url[:actual][k])
end
end
end
rescue ActionController::RoutingError # routing failed us, so match the strings only.
msg = build_message(message, "expected a redirect to <?>, found one to <?>", options, @response.redirect_url)
url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$}
eurl, epath, url, path = [options, @response.redirect_url].collect do |url|
u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url]
[u, (p.first == '/') ? p : '/' + p]
end.flatten
assert_equal(eurl, url, msg) if eurl && url
assert_equal(epath, path, msg) if epath && path
end
end
end
# Asserts that the request was rendered with the appropriate template file.
def assert_template(expected = nil, message=nil)
clean_backtrace do
rendered = expected ? @response.rendered_file(!expected.include?('/')) : @response.rendered_file
msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
assert_block(msg) do
if expected.nil?
!@response.rendered_with_file?
else
expected == rendered
end
end
end
end
private
def recognized_request_for(path, request_method = nil)
path = "/#{path}" unless path.first == '/'
# Assume given controller
request = ActionController::TestRequest.new({}, {}, nil)
request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method
request.path = path
ActionController::Routing::Routes.recognize(request)
request
end
def parameterize(value)
value.respond_to?(:to_param) ? value.to_param : value
end
end
end
end

View file

@ -0,0 +1,98 @@
module ActionController
module Assertions
module RoutingAssertions
# Asserts that the routing of the given path was handled correctly and that the parsed options match.
#
# assert_recognizes({:controller => 'items', :action => 'index'}, 'items') # check the default action
# assert_recognizes({:controller => 'items', :action => 'list'}, 'items/list') # check a specific action
# assert_recognizes({:controller => 'items', :action => 'list', :id => '1'}, 'items/list/1') # check an action with a parameter
#
# Pass a hash in the second argument to specify the request method. This is useful for routes
# requiring a specific HTTP method. The hash should contain a :path with the incoming request path
# and a :method containing the required HTTP verb.
#
# # assert that POSTing to /items will call the create action on ItemsController
# assert_recognizes({:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post})
#
# You can also pass in "extras" with a hash containing URL parameters that would normally be in the query string. This can be used
# to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the
# extras argument, appending the query string on the path directly will not work. For example:
#
# # assert that a path of '/items/list/1?view=print' returns the correct options
# assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" })
def assert_recognizes(expected_options, path, extras={}, message=nil)
if path.is_a? Hash
request_method = path[:method]
path = path[:path]
else
request_method = nil
end
clean_backtrace do
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
request = recognized_request_for(path, request_method)
expected_options = expected_options.clone
extras.each_key { |key| expected_options.delete key } unless extras.nil?
expected_options.stringify_keys!
routing_diff = expected_options.diff(request.path_parameters)
msg = build_message(message, "The recognized options <?> did not match <?>, difference: <?>",
request.path_parameters, expected_options, expected_options.diff(request.path_parameters))
assert_block(msg) { request.path_parameters == expected_options }
end
end
# Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes.
# For example:
#
# assert_generates("/items", :controller => "items", :action => "index")
# assert_generates("/items/list", :controller => "items", :action => "list")
# assert_generates("/items/list/1", { :controller => "items", :action => "list", :id => "1" })
def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
clean_backtrace do
expected_path = "/#{expected_path}" unless expected_path[0] == ?/
# Load routes.rb if it hasn't been loaded.
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, defaults)
found_extras = options.reject {|k, v| ! extra_keys.include? k}
msg = build_message(message, "found extras <?>, not <?>", found_extras, extras)
assert_block(msg) { found_extras == extras }
msg = build_message(message, "The generated path <?> did not match <?>", generated_path,
expected_path)
assert_block(msg) { expected_path == generated_path }
end
end
# Asserts that path and options match both ways; in other words, the URL generated from
# options is the same as path, and also that the options recognized from path are the same as options. This
# essentially combines assert_recognizes and assert_generates into one step.
def assert_routing(path, options, defaults={}, extras={}, message=nil)
assert_recognizes(options, path, extras, message)
controller, default_controller = options[:controller], defaults[:controller]
if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
options[:controller] = "/#{controller}"
end
assert_generates(path, options, defaults, extras, message)
end
private
def recognized_request_for(path, request_method = nil)
path = "/#{path}" unless path.first == '/'
# Assume given controller
request = ActionController::TestRequest.new({}, {}, nil)
request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method
request.path = path
ActionController::Routing::Routes.recognize(request)
request
end
end
end
end

View file

@ -0,0 +1,571 @@
#--
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
# Under MIT and/or CC By license.
#++
require 'rexml/document'
require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document"
module ActionController
module Assertions
unless const_defined?(:NO_STRIP)
NO_STRIP = %w{pre script style textarea}
end
# Adds the #assert_select method for use in Rails functional
# test cases.
#
# Use #assert_select to make assertions on the response HTML of a controller
# action. You can also call #assert_select within another #assert_select to
# make assertions on elements selected by the enclosing assertion.
#
# Use #css_select to select elements without making an assertions, either
# from the response HTML or elements selected by the enclosing assertion.
#
# In addition to HTML responses, you can make the following assertions:
# * #assert_select_rjs -- Assertions on HTML content of RJS update and
# insertion operations.
# * #assert_select_encoded -- Assertions on HTML encoded inside XML,
# for example for dealing with feed item descriptions.
# * #assert_select_email -- Assertions on the HTML body of an e-mail.
#
# Also see HTML::Selector for learning how to use selectors.
module SelectorAssertions
# :call-seq:
# css_select(selector) => array
# css_select(element, selector) => array
#
# Select and return all matching elements.
#
# If called with a single argument, uses that argument as a selector
# to match all elements of the current page. Returns an empty array
# if no match is found.
#
# If called with two arguments, uses the first argument as the base
# element and the second argument as the selector. Attempts to match the
# base element and any of its children. Returns an empty array if no
# match is found.
#
# The selector may be a CSS selector expression (+String+), an expression
# with substitution values (+Array+) or an HTML::Selector object.
#
# For example:
# forms = css_select("form")
# forms.each do |form|
# inputs = css_select(form, "input")
# ...
# end
def css_select(*args)
# See assert_select to understand what's going on here.
arg = args.shift
if arg.is_a?(HTML::Node)
root = arg
arg = args.shift
elsif arg == nil
raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
elsif @selected
matches = []
@selected.each do |selected|
subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup))
subset.each do |match|
matches << match unless matches.any? { |m| m.equal?(match) }
end
end
return matches
else
root = response_from_page_or_rjs
end
case arg
when String
selector = HTML::Selector.new(arg, args)
when Array
selector = HTML::Selector.new(*arg)
when HTML::Selector
selector = arg
else raise ArgumentError, "Expecting a selector as the first argument"
end
selector.select(root)
end
# :call-seq:
# assert_select(selector, equality?, message?)
# assert_select(element, selector, equality?, message?)
#
# An assertion that selects elements and makes one or more equality tests.
#
# If the first argument is an element, selects all matching elements
# starting from (and including) that element and all its children in
# depth-first order.
#
# If no element if specified, calling #assert_select will select from the
# response HTML. Calling #assert_select inside an #assert_select block will
# run the assertion for each element selected by the enclosing assertion.
#
# For example:
# assert_select "ol>li" do |elements|
# elements.each do |element|
# assert_select element, "li"
# end
# end
# Or for short:
# assert_select "ol>li" do
# assert_select "li"
# end
#
# The selector may be a CSS selector expression (+String+), an expression
# with substitution values, or an HTML::Selector object.
#
# === Equality Tests
#
# The equality test may be one of the following:
# * <tt>true</tt> -- Assertion is true if at least one element selected.
# * <tt>false</tt> -- Assertion is true if no element selected.
# * <tt>String/Regexp</tt> -- Assertion is true if the text value of at least
# one element matches the string or regular expression.
# * <tt>Integer</tt> -- Assertion is true if exactly that number of
# elements are selected.
# * <tt>Range</tt> -- Assertion is true if the number of selected
# elements fit the range.
# If no equality test specified, the assertion is true if at least one
# element selected.
#
# To perform more than one equality tests, use a hash with the following keys:
# * <tt>:text</tt> -- Narrow the selection to elements that have this text
# value (string or regexp).
# * <tt>:html</tt> -- Narrow the selection to elements that have this HTML
# content (string or regexp).
# * <tt>:count</tt> -- Assertion is true if the number of selected elements
# is equal to this value.
# * <tt>:minimum</tt> -- Assertion is true if the number of selected
# elements is at least this value.
# * <tt>:maximum</tt> -- Assertion is true if the number of selected
# elements is at most this value.
#
# If the method is called with a block, once all equality tests are
# evaluated the block is called with an array of all matched elements.
#
# === Examples
#
# # At least one form element
# assert_select "form"
#
# # Form element includes four input fields
# assert_select "form input", 4
#
# # Page title is "Welcome"
# assert_select "title", "Welcome"
#
# # Page title is "Welcome" and there is only one title element
# assert_select "title", {:count=>1, :text=>"Welcome"},
# "Wrong title or more than one title element"
#
# # Page contains no forms
# assert_select "form", false, "This page must contain no forms"
#
# # Test the content and style
# assert_select "body div.header ul.menu"
#
# # Use substitution values
# assert_select "ol>li#?", /item-\d+/
#
# # All input fields in the form have a name
# assert_select "form input" do
# assert_select "[name=?]", /.+/ # Not empty
# end
def assert_select(*args, &block)
# Start with optional element followed by mandatory selector.
arg = args.shift
if arg.is_a?(HTML::Node)
# First argument is a node (tag or text, but also HTML root),
# so we know what we're selecting from.
root = arg
arg = args.shift
elsif arg == nil
# This usually happens when passing a node/element that
# happens to be nil.
raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
elsif @selected
root = HTML::Node.new(nil)
root.children.concat @selected
else
# Otherwise just operate on the response document.
root = response_from_page_or_rjs
end
# First or second argument is the selector: string and we pass
# all remaining arguments. Array and we pass the argument. Also
# accepts selector itself.
case arg
when String
selector = HTML::Selector.new(arg, args)
when Array
selector = HTML::Selector.new(*arg)
when HTML::Selector
selector = arg
else raise ArgumentError, "Expecting a selector as the first argument"
end
# Next argument is used for equality tests.
equals = {}
case arg = args.shift
when Hash
equals = arg
when String, Regexp
equals[:text] = arg
when Integer
equals[:count] = arg
when Range
equals[:minimum] = arg.begin
equals[:maximum] = arg.end
when FalseClass
equals[:count] = 0
when NilClass, TrueClass
equals[:minimum] = 1
else raise ArgumentError, "I don't understand what you're trying to match"
end
# By default we're looking for at least one match.
if equals[:count]
equals[:minimum] = equals[:maximum] = equals[:count]
else
equals[:minimum] = 1 unless equals[:minimum]
end
# Last argument is the message we use if the assertion fails.
message = args.shift
#- message = "No match made with selector #{selector.inspect}" unless message
if args.shift
raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
end
matches = selector.select(root)
# If text/html, narrow down to those elements that match it.
content_mismatch = nil
if match_with = equals[:text]
matches.delete_if do |match|
text = ""
stack = match.children.reverse
while node = stack.pop
if node.tag?
stack.concat node.children.reverse
else
text << node.content
end
end
text.strip! unless NO_STRIP.include?(match.name)
unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s)
content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, text)
true
end
end
elsif match_with = equals[:html]
matches.delete_if do |match|
html = match.children.map(&:to_s).join
html.strip! unless NO_STRIP.include?(match.name)
unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s)
content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, html)
true
end
end
end
# Expecting foo found bar element only if found zero, not if
# found one but expecting two.
message ||= content_mismatch if matches.empty?
# Test minimum/maximum occurrence.
if equals[:minimum]
assert matches.size >= equals[:minimum], message ||
"Expected at least #{equals[:minimum]} elements, found #{matches.size}."
end
if equals[:maximum]
assert matches.size <= equals[:maximum], message ||
"Expected at most #{equals[:maximum]} elements, found #{matches.size}."
end
# If a block is given call that block. Set @selected to allow
# nested assert_select, which can be nested several levels deep.
if block_given? && !matches.empty?
begin
in_scope, @selected = @selected, matches
yield matches
ensure
@selected = in_scope
end
end
# Returns all matches elements.
matches
end
# :call-seq:
# assert_select_rjs(id?) { |elements| ... }
# assert_select_rjs(statement, id?) { |elements| ... }
# assert_select_rjs(:insert, position, id?) { |elements| ... }
#
# Selects content from the RJS response.
#
# === Narrowing down
#
# With no arguments, asserts that one or more elements are updated or
# inserted by RJS statements.
#
# Use the +id+ argument to narrow down the assertion to only statements
# that update or insert an element with that identifier.
#
# Use the first argument to narrow down assertions to only statements
# of that type. Possible values are +:replace+, +:replace_html+ and
# +:insert_html+.
#
# Use the argument +:insert+ followed by an insertion position to narrow
# down the assertion to only statements that insert elements in that
# position. Possible values are +:top+, +:bottom+, +:before+ and +:after+.
#
# === Using blocks
#
# Without a block, #assert_select_rjs merely asserts that the response
# contains one or more RJS statements that replace or update content.
#
# With a block, #assert_select_rjs also selects all elements used in
# these statements and passes them to the block. Nested assertions are
# supported.
#
# Calling #assert_select_rjs with no arguments and using nested asserts
# asserts that the HTML content is returned by one or more RJS statements.
# Using #assert_select directly makes the same assertion on the content,
# but without distinguishing whether the content is returned in an HTML
# or JavaScript.
#
# === Examples
#
# # Replacing the element foo.
# # page.replace 'foo', ...
# assert_select_rjs :replace, "foo"
#
# # Replacing with the chained RJS proxy.
# # page[:foo].replace ...
# assert_select_rjs :chained_replace, 'foo'
#
# # Inserting into the element bar, top position.
# assert_select_rjs :insert, :top, "bar"
#
# # Changing the element foo, with an image.
# assert_select_rjs "foo" do
# assert_select "img[src=/images/logo.gif""
# end
#
# # RJS inserts or updates a list with four items.
# assert_select_rjs do
# assert_select "ol>li", 4
# end
#
# # The same, but shorter.
# assert_select "ol>li", 4
def assert_select_rjs(*args, &block)
rjs_type = nil
arg = args.shift
# If the first argument is a symbol, it's the type of RJS statement we're looking
# for (update, replace, insertion, etc). Otherwise, we're looking for just about
# any RJS statement.
if arg.is_a?(Symbol)
rjs_type = arg
if rjs_type == :insert
arg = args.shift
insertion = "insert_#{arg}".to_sym
raise ArgumentError, "Unknown RJS insertion type #{arg}" unless RJS_STATEMENTS[insertion]
statement = "(#{RJS_STATEMENTS[insertion]})"
else
raise ArgumentError, "Unknown RJS statement type #{rjs_type}" unless RJS_STATEMENTS[rjs_type]
statement = "(#{RJS_STATEMENTS[rjs_type]})"
end
arg = args.shift
else
statement = "#{RJS_STATEMENTS[:any]}"
end
# Next argument we're looking for is the element identifier. If missing, we pick
# any element.
if arg.is_a?(String)
id = Regexp.quote(arg)
arg = args.shift
else
id = "[^\"]*"
end
pattern =
case rjs_type
when :chained_replace, :chained_replace_html
Regexp.new("\\$\\(\"#{id}\"\\)#{statement}\\(#{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE)
else
Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE)
end
# Duplicate the body since the next step involves destroying it.
matches = nil
@response.body.gsub(pattern) do |match|
html = unescape_rjs($2)
matches ||= []
matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? }
""
end
if matches
if block_given?
begin
in_scope, @selected = @selected, matches
yield matches
ensure
@selected = in_scope
end
end
matches
else
# RJS statement not found.
flunk args.shift || "No RJS statement that replaces or inserts HTML content."
end
end
# :call-seq:
# assert_select_encoded(element?) { |elements| ... }
#
# Extracts the content of an element, treats it as encoded HTML and runs
# nested assertion on it.
#
# You typically call this method within another assertion to operate on
# all currently selected elements. You can also pass an element or array
# of elements.
#
# The content of each element is un-encoded, and wrapped in the root
# element +encoded+. It then calls the block with all un-encoded elements.
#
# === Example
#
# assert_select_feed :rss, 2.0 do
# # Select description element of each feed item.
# assert_select "channel>item>description" do
# # Run assertions on the encoded elements.
# assert_select_encoded do
# assert_select "p"
# end
# end
# end
def assert_select_encoded(element = nil, &block)
case element
when Array
elements = element
when HTML::Node
elements = [element]
when nil
unless elements = @selected
raise ArgumentError, "First argument is optional, but must be called from a nested assert_select"
end
else
raise ArgumentError, "Argument is optional, and may be node or array of nodes"
end
fix_content = lambda do |node|
# Gets around a bug in the Rails 1.1 HTML parser.
node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { CGI.escapeHTML($1) }
end
selected = elements.map do |element|
text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join
root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root
css_select(root, "encoded:root", &block)[0]
end
begin
old_selected, @selected = @selected, selected
assert_select ":root", &block
ensure
@selected = old_selected
end
end
# :call-seq:
# assert_select_email { }
#
# Extracts the body of an email and runs nested assertions on it.
#
# You must enable deliveries for this assertion to work, use:
# ActionMailer::Base.perform_deliveries = true
#
# === Example
#
# assert_select_email do
# assert_select "h1", "Email alert"
# end
def assert_select_email(&block)
deliveries = ActionMailer::Base.deliveries
assert !deliveries.empty?, "No e-mail in delivery list"
for delivery in deliveries
for part in delivery.parts
if part["Content-Type"].to_s =~ /^text\/html\W/
root = HTML::Document.new(part.body).root
assert_select root, ":root", &block
end
end
end
end
protected
unless const_defined?(:RJS_STATEMENTS)
RJS_STATEMENTS = {
:replace => /Element\.replace/,
:replace_html => /Element\.update/,
:chained_replace => /\.replace/,
:chained_replace_html => /\.update/,
}
RJS_INSERTIONS = [:top, :bottom, :before, :after]
RJS_INSERTIONS.each do |insertion|
RJS_STATEMENTS["insert_#{insertion}".to_sym] = Regexp.new(Regexp.quote("new Insertion.#{insertion.to_s.camelize}"))
end
RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})")
RJS_STATEMENTS[:insert_html] = Regexp.new(RJS_INSERTIONS.collect do |insertion|
Regexp.quote("new Insertion.#{insertion.to_s.camelize}")
end.join('|'))
RJS_PATTERN_HTML = /"((\\"|[^"])*)"/
RJS_PATTERN_EVERYTHING = Regexp.new("#{RJS_STATEMENTS[:any]}\\(\"([^\"]*)\", #{RJS_PATTERN_HTML}\\)",
Regexp::MULTILINE)
RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/
end
# #assert_select and #css_select call this to obtain the content in the HTML
# page, or from all the RJS statements, depending on the type of response.
def response_from_page_or_rjs()
content_type = @response.headers["Content-Type"]
if content_type && content_type =~ /text\/javascript/
body = @response.body.dup
root = HTML::Node.new(nil)
while true
next if body.sub!(RJS_PATTERN_EVERYTHING) do |match|
html = unescape_rjs($3)
matches = HTML::Document.new(html).root.children.select { |n| n.tag? }
root.children.concat matches
""
end
break
end
root
else
html_document.root
end
end
# Unescapes a RJS string.
def unescape_rjs(rjs_string)
# RJS encodes double quotes and line breaks.
unescaped= rjs_string.gsub('\"', '"')
unescaped.gsub!('\n', "\n")
# RJS encodes non-ascii characters.
unescaped.gsub!(RJS_PATTERN_UNICODE_ESCAPED_CHAR) {|u| [$1.hex].pack('U*')}
unescaped
end
end
end
end

View file

@ -0,0 +1,117 @@
require 'rexml/document'
require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document"
module ActionController
module Assertions
module TagAssertions
# Asserts that there is a tag/node/element in the body of the response
# that meets all of the given conditions. The +conditions+ parameter must
# be a hash of any of the following keys (all are optional):
#
# * <tt>:tag</tt>: the node type must match the corresponding value
# * <tt>:attributes</tt>: a hash. The node's attributes must match the
# corresponding values in the hash.
# * <tt>:parent</tt>: a hash. The node's parent must match the
# corresponding hash.
# * <tt>:child</tt>: a hash. At least one of the node's immediate children
# must meet the criteria described by the hash.
# * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
# meet the criteria described by the hash.
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
# must meet the criteria described by the hash.
# * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
# meet the criteria described by the hash.
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:children</tt>: a hash, for counting children of a node. Accepts
# the keys:
# * <tt>:count</tt>: either a number or a range which must equal (or
# include) the number of children that match.
# * <tt>:less_than</tt>: the number of matching children must be less
# than this number.
# * <tt>:greater_than</tt>: the number of matching children must be
# greater than this number.
# * <tt>:only</tt>: another hash consisting of the keys to use
# to match on the children, and only matching children will be
# counted.
# * <tt>:content</tt>: the textual content of the node must match the
# given value. This will not match HTML tags in the body of a
# tag--only text.
#
# Conditions are matched using the following algorithm:
#
# * if the condition is a string, it must be a substring of the value.
# * if the condition is a regexp, it must match the value.
# * if the condition is a number, the value must match number.to_s.
# * if the condition is +true+, the value must not be +nil+.
# * if the condition is +false+ or +nil+, the value must be +nil+.
#
# Usage:
#
# # assert that there is a "span" tag
# assert_tag :tag => "span"
#
# # assert that there is a "span" tag with id="x"
# assert_tag :tag => "span", :attributes => { :id => "x" }
#
# # assert that there is a "span" tag using the short-hand
# assert_tag :span
#
# # assert that there is a "span" tag with id="x" using the short-hand
# assert_tag :span, :attributes => { :id => "x" }
#
# # assert that there is a "span" inside of a "div"
# assert_tag :tag => "span", :parent => { :tag => "div" }
#
# # assert that there is a "span" somewhere inside a table
# assert_tag :tag => "span", :ancestor => { :tag => "table" }
#
# # assert that there is a "span" with at least one "em" child
# assert_tag :tag => "span", :child => { :tag => "em" }
#
# # assert that there is a "span" containing a (possibly nested)
# # "strong" tag.
# assert_tag :tag => "span", :descendant => { :tag => "strong" }
#
# # assert that there is a "span" containing between 2 and 4 "em" tags
# # as immediate children
# assert_tag :tag => "span",
# :children => { :count => 2..4, :only => { :tag => "em" } }
#
# # get funky: assert that there is a "div", with an "ul" ancestor
# # and an "li" parent (with "class" = "enum"), and containing a
# # "span" descendant that contains text matching /hello world/
# assert_tag :tag => "div",
# :ancestor => { :tag => "ul" },
# :parent => { :tag => "li",
# :attributes => { :class => "enum" } },
# :descendant => { :tag => "span",
# :child => /hello world/ }
#
# <strong>Please note</strong: #assert_tag and #assert_no_tag only work
# with well-formed XHTML. They recognize a few tags as implicitly self-closing
# (like br and hr and such) but will not work correctly with tags
# that allow optional closing tags (p, li, td). <em>You must explicitly
# close all of your tags to use these assertions.</em>
def assert_tag(*opts)
clean_backtrace do
opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
tag = find_tag(opts)
assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
end
end
# Identical to #assert_tag, but asserts that a matching tag does _not_
# exist. (See #assert_tag for a full discussion of the syntax.)
def assert_no_tag(*opts)
clean_backtrace do
opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
tag = find_tag(opts)
assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
end
end
end
end
end

View file

@ -0,0 +1,65 @@
module ActionController #:nodoc:
module Dependencies #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# Deprecated module. The responsibility of loading dependencies belong with Active Support now.
module ClassMethods #:nodoc:
# Specifies a variable number of models that this controller depends on. Models are normally Active Record classes or a similar
# backend for modelling entity classes.
def model(*models)
require_dependencies(:model, models)
depend_on(:model, models)
end
deprecate :model
# Specifies a variable number of services that this controller depends on. Services are normally singletons or factories, like
# Action Mailer service or a Payment Gateway service.
def service(*services)
require_dependencies(:service, services)
depend_on(:service, services)
end
deprecate :service
# Specifies a variable number of observers that are to govern when this controller is handling actions. The observers will
# automatically have .instance called on them to make them active on assignment.
def observer(*observers)
require_dependencies(:observer, observers)
depend_on(:observer, observers)
instantiate_observers(observers)
end
deprecate :observer
# Returns an array of symbols that specify the dependencies on a given layer. For the example at the top, calling
# <tt>ApplicationController.dependencies_on(:model)</tt> would return <tt>[:account, :company, :person, :project, :category]</tt>
def dependencies_on(layer)
read_inheritable_attribute("#{layer}_dependencies")
end
deprecate :dependencies_on
def depend_on(layer, dependencies) #:nodoc:
write_inheritable_array("#{layer}_dependencies", dependencies)
end
deprecate :depend_on
private
def instantiate_observers(observers)
observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance }
end
def require_dependencies(layer, dependencies)
dependencies.flatten.each do |dependency|
begin
require_dependency(dependency.to_s)
rescue LoadError => e
raise LoadError.new("Missing #{layer} #{dependency}.rb").copy_blame!(e)
rescue Exception => exception # error from loaded file
exception.blame_file! "=> #{layer} #{dependency}.rb"
raise
end
end
end
end
end
end

View file

@ -0,0 +1,405 @@
module ActionController
module Resources
class Resource #:nodoc:
attr_reader :collection_methods, :member_methods, :new_methods
attr_reader :path_prefix, :name_prefix
attr_reader :plural, :singular
attr_reader :options
def initialize(entities, options)
@plural = entities
@singular = options[:singular] || plural.to_s.singularize
@options = options
arrange_actions
add_default_actions
set_prefixes
end
def controller
@controller ||= (options[:controller] || plural).to_s
end
def path
@path ||= "#{path_prefix}/#{plural}"
end
def new_path
@new_path ||= "#{path}/new"
end
def member_path
@member_path ||= "#{path}/:id"
end
def nesting_path_prefix
@nesting_path_prefix ||= "#{path}/:#{singular}_id"
end
protected
def arrange_actions
@collection_methods = arrange_actions_by_methods(options.delete(:collection))
@member_methods = arrange_actions_by_methods(options.delete(:member))
@new_methods = arrange_actions_by_methods(options.delete(:new))
end
def add_default_actions
add_default_action(member_methods, :get, :edit)
add_default_action(new_methods, :get, :new)
end
def set_prefixes
@path_prefix = options.delete(:path_prefix)
@name_prefix = options.delete(:name_prefix)
end
def arrange_actions_by_methods(actions)
(actions || {}).inject({}) do |flipped_hash, (key, value)|
(flipped_hash[value] ||= []) << key
flipped_hash
end
end
def add_default_action(collection, method, action)
(collection[method] ||= []).unshift(action)
end
end
class SingletonResource < Resource #:nodoc:
def initialize(entity, options)
@plural = @singular = entity
@options = options
arrange_actions
add_default_actions
set_prefixes
end
alias_method :member_path, :path
alias_method :nesting_path_prefix, :path
end
# Creates named routes for implementing verb-oriented controllers. This is
# useful for implementing REST API's, where a single resource has different
# behavior based on the HTTP verb (method) used to access it.
#
# Example:
#
# map.resources :messages
#
# class MessagesController < ActionController::Base
# # GET messages_url
# def index
# # return all messages
# end
#
# # GET new_message_url
# def new
# # return an HTML form for describing a new message
# end
#
# # POST messages_url
# def create
# # create a new message
# end
#
# # GET message_url(:id => 1)
# def show
# # find and return a specific message
# end
#
# # GET edit_message_url(:id => 1)
# def edit
# # return an HTML form for editing a specific message
# end
#
# # PUT message_url(:id => 1)
# def update
# # find and update a specific message
# end
#
# # DELETE message_url(:id => 1)
# def destroy
# # delete a specific message
# end
# end
#
# The #resources method sets HTTP method restrictions on the routes it generates. For example, making an
# HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in
# <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for resource routes.
#
# Along with the routes themselves, #resources generates named routes for use in
# controllers and views. <tt>map.resources :messages</tt> produces the following named routes and helpers:
#
# Named Route Helpers
# messages messages_url, hash_for_messages_url,
# messages_path, hash_for_messages_path
# message message_url(id), hash_for_message_url(id),
# message_path(id), hash_for_message_path(id)
# new_message new_message_url, hash_for_new_message_url,
# new_message_path, hash_for_new_message_path
# edit_message edit_message_url(id), hash_for_edit_message_url(id),
# edit_message_path(id), hash_for_edit_message_path(id)
#
# You can use these helpers instead of #url_for or methods that take #url_for parameters:
#
# redirect_to :controller => 'messages', :action => 'index'
# # becomes
# redirect_to messages_url
#
# <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %>
# # becomes
# <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically
#
# Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your
# form tags. The form helpers make this a little easier. For an update form with a <tt>@message</tt> object:
#
# <%= form_tag message_path(@message), :method => :put %>
#
# or
#
# <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %>
#
# The #resources method accepts various options, too, to customize the resulting
# routes:
# * <tt>:controller</tt> -- specify the controller name for the routes.
# * <tt>:singular</tt> -- specify the singular name used in the member routes.
# * <tt>:path_prefix</tt> -- set a prefix to the routes with required route variables.
# Weblog comments usually belong to a post, so you might use resources like:
#
# map.resources :articles
# map.resources :comments, :path_prefix => '/articles/:article_id'
#
# You can nest resources calls to set this automatically:
#
# map.resources :articles do |article|
# article.resources :comments
# end
#
# The comment resources work the same, but must now include a value for :article_id.
#
# comments_url(@article)
# comment_url(@article, @comment)
#
# comments_url(:article_id => @article)
# comment_url(:article_id => @article, :id => @comment)
#
# * <tt>:name_prefix</tt> -- define a prefix for all generated routes, usually ending in an underscore.
# Use this if you have named routes that may clash.
#
# map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_'
# map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_'
#
# * <tt>:collection</tt> -- add named routes for other actions that operate on the collection.
# Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>
# or <tt>:any</tt> if the method does not matter. These routes map to a URL like /messages;rss, with a route of rss_messages_url.
# * <tt>:member</tt> -- same as :collection, but for actions that operate on a specific member.
# * <tt>:new</tt> -- same as :collection, but for actions that operate on the new resource action.
#
# If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied.
#
# Examples:
#
# map.resources :messages, :path_prefix => "/thread/:thread_id"
# # --> GET /thread/7/messages/1
#
# map.resources :messages, :collection => { :rss => :get }
# # --> GET /messages;rss (maps to the #rss action)
# # also adds a named route called "rss_messages"
#
# map.resources :messages, :member => { :mark => :post }
# # --> POST /messages/1;mark (maps to the #mark action)
# # also adds a named route called "mark_message"
#
# map.resources :messages, :new => { :preview => :post }
# # --> POST /messages/new;preview (maps to the #preview action)
# # also adds a named route called "preview_new_message"
#
# map.resources :messages, :new => { :new => :any, :preview => :post }
# # --> POST /messages/new;preview (maps to the #preview action)
# # also adds a named route called "preview_new_message"
# # --> /messages/new can be invoked via any request method
#
# map.resources :messages, :controller => "categories",
# :path_prefix => "/category/:category_id",
# :name_prefix => "category_"
# # --> GET /categories/7/messages/1
# # has named route "category_message"
def resources(*entities, &block)
options = entities.last.is_a?(Hash) ? entities.pop : { }
entities.each { |entity| map_resource entity, options.dup, &block }
end
# Creates named routes for implementing verb-oriented controllers for a singleton resource.
# A singleton resource is global to the current user visiting the application, such as a user's
# /account profile.
#
# See map.resources for general conventions. These are the main differences:
# - a singular name is given to map.resource. The default controller name is taken from the singular name.
# - To specify a custom plural name, use the :plural option. There is no :singular option
# - No default index, new, or create routes are created for the singleton resource controller.
# - When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1')
#
# Example:
#
# map.resource :account
#
# class AccountController < ActionController::Base
# # POST account_url
# def create
# # create an account
# end
#
# # GET new_account_url
# def new
# # return an HTML form for describing the new account
# end
#
# # GET account_url
# def show
# # find and return the account
# end
#
# # GET edit_account_url
# def edit
# # return an HTML form for editing the account
# end
#
# # PUT account_url
# def update
# # find and update the account
# end
#
# # DELETE account_url
# def destroy
# # delete the account
# end
# end
#
# Along with the routes themselves, #resource generates named routes for use in
# controllers and views. <tt>map.resource :account</tt> produces the following named routes and helpers:
#
# Named Route Helpers
# account account_url, hash_for_account_url,
# account_path, hash_for_account_path
# edit_account edit_account_url, hash_for_edit_account_url,
# edit_account_path, hash_for_edit_account_path
def resource(*entities, &block)
options = entities.last.is_a?(Hash) ? entities.pop : { }
entities.each { |entity| map_singleton_resource entity, options.dup, &block }
end
private
def map_resource(entities, options = {}, &block)
resource = Resource.new(entities, options)
with_options :controller => resource.controller do |map|
map_collection_actions(map, resource)
map_default_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, &block)
end
end
end
def map_singleton_resource(entities, options = {}, &block)
resource = SingletonResource.new(entities, options)
with_options :controller => resource.controller do |map|
map_collection_actions(map, resource)
map_default_singleton_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, &block)
end
end
end
def map_collection_actions(map, resource)
resource.collection_methods.each do |method, actions|
actions.each do |action|
action_options = action_options_for(action, resource, method)
map.named_route("#{resource.name_prefix}#{action}_#{resource.plural}", "#{resource.path};#{action}", action_options)
map.named_route("formatted_#{resource.name_prefix}#{action}_#{resource.plural}", "#{resource.path}.:format;#{action}", action_options)
end
end
end
def map_default_collection_actions(map, resource)
index_action_options = action_options_for("index", resource)
map.named_route("#{resource.name_prefix}#{resource.plural}", resource.path, index_action_options)
map.named_route("formatted_#{resource.name_prefix}#{resource.plural}", "#{resource.path}.:format", index_action_options)
create_action_options = action_options_for("create", resource)
map.connect(resource.path, create_action_options)
map.connect("#{resource.path}.:format", create_action_options)
end
def map_default_singleton_actions(map, resource)
create_action_options = action_options_for("create", resource)
map.connect(resource.path, create_action_options)
map.connect("#{resource.path}.:format", create_action_options)
end
def map_new_actions(map, resource)
resource.new_methods.each do |method, actions|
actions.each do |action|
action_options = action_options_for(action, resource, method)
if action == :new
map.named_route("#{resource.name_prefix}new_#{resource.singular}", resource.new_path, action_options)
map.named_route("formatted_#{resource.name_prefix}new_#{resource.singular}", "#{resource.new_path}.:format", action_options)
else
map.named_route("#{resource.name_prefix}#{action}_new_#{resource.singular}", "#{resource.new_path};#{action}", action_options)
map.named_route("formatted_#{resource.name_prefix}#{action}_new_#{resource.singular}", "#{resource.new_path}.:format;#{action}", action_options)
end
end
end
end
def map_member_actions(map, resource)
resource.member_methods.each do |method, actions|
actions.each do |action|
action_options = action_options_for(action, resource, method)
map.named_route("#{resource.name_prefix}#{action}_#{resource.singular}", "#{resource.member_path};#{action}", action_options)
map.named_route("formatted_#{resource.name_prefix}#{action}_#{resource.singular}", "#{resource.member_path}.:format;#{action}",action_options)
end
end
show_action_options = action_options_for("show", resource)
map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options)
map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options)
update_action_options = action_options_for("update", resource)
map.connect(resource.member_path, update_action_options)
map.connect("#{resource.member_path}.:format", update_action_options)
destroy_action_options = action_options_for("destroy", resource)
map.connect(resource.member_path, destroy_action_options)
map.connect("#{resource.member_path}.:format", destroy_action_options)
end
def conditions_for(method)
{ :conditions => method == :any ? {} : { :method => method } }
end
def action_options_for(action, resource, method = nil)
default_options = { :action => action.to_s }
require_id = resource.kind_of?(SingletonResource) ? {} : { :requirements => { :id => Regexp.new("[^#{Routing::SEPARATORS.join}]+") } }
case default_options[:action]
when "index", "new" : default_options.merge(conditions_for(method || :get))
when "create" : default_options.merge(conditions_for(method || :post))
when "show", "edit" : default_options.merge(conditions_for(method || :get)).merge(require_id)
when "update" : default_options.merge(conditions_for(method || :put)).merge(require_id)
when "destroy" : default_options.merge(conditions_for(method || :delete)).merge(require_id)
else default_options.merge(conditions_for(method))
end
end
end
end
ActionController::Routing::RouteSet::Mapper.send :include, ActionController::Resources

View file

@ -0,0 +1,88 @@
module ActionController
module StatusCodes #:nodoc:
# Defines the standard HTTP status codes, by integer, with their
# corresponding default message texts.
# Source: http://www.iana.org/assignments/http-status-codes
STATUS_CODES = {
100 => "Continue",
101 => "Switching Protocols",
102 => "Processing",
200 => "OK",
201 => "Created",
202 => "Accepted",
203 => "Non-Authoritative Information",
204 => "No Content",
205 => "Reset Content",
206 => "Partial Content",
207 => "Multi-Status",
226 => "IM Used",
300 => "Multiple Choices",
301 => "Moved Permanently",
302 => "Found",
303 => "See Other",
304 => "Not Modified",
305 => "Use Proxy",
307 => "Temporary Redirect",
400 => "Bad Request",
401 => "Unauthorized",
402 => "Payment Required",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptable",
407 => "Proxy Authentication Required",
408 => "Request Timeout",
409 => "Conflict",
410 => "Gone",
411 => "Length Required",
412 => "Precondition Failed",
413 => "Request Entity Too Large",
414 => "Request-URI Too Long",
415 => "Unsupported Media Type",
416 => "Requested Range Not Satisfiable",
417 => "Expectation Failed",
422 => "Unprocessable Entity",
423 => "Locked",
424 => "Failed Dependency",
426 => "Upgrade Required",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
505 => "HTTP Version Not Supported",
507 => "Insufficient Storage",
510 => "Not Extended"
}
# Provides a symbol-to-fixnum lookup for converting a symbol (like
# :created or :not_implemented) into its corresponding HTTP status
# code (like 200 or 501).
SYMBOL_TO_STATUS_CODE = STATUS_CODES.inject({}) do |hash, (code, message)|
hash[message.gsub(/ /, "").underscore.to_sym] = code
hash
end
# Given a status parameter, determine whether it needs to be converted
# to a string. If it is a fixnum, use the STATUS_CODES hash to lookup
# the default message. If it is a symbol, use the SYMBOL_TO_STATUS_CODE
# hash to convert it.
def interpret_status(status)
case status
when Fixnum then
"#{status} #{STATUS_CODES[status]}".strip
when Symbol then
interpret_status(SYMBOL_TO_STATUS_CODE[status] ||
"500 Unknown Status #{status.inspect}")
else
status.to_s
end
end
private :interpret_status
end
end

View file

@ -0,0 +1,823 @@
#--
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
# Under MIT and/or CC By license.
#++
module HTML
# Selects HTML elements using CSS 2 selectors.
#
# The +Selector+ class uses CSS selector expressions to match and select
# HTML elements.
#
# For example:
# selector = HTML::Selector.new "form.login[action=/login]"
# creates a new selector that matches any +form+ element with the class
# +login+ and an attribute +action+ with the value <tt>/login</tt>.
#
# === Matching Elements
#
# Use the #match method to determine if an element matches the selector.
#
# For simple selectors, the method returns an array with that element,
# or +nil+ if the element does not match. For complex selectors (see below)
# the method returns an array with all matched elements, of +nil+ if no
# match found.
#
# For example:
# if selector.match(element)
# puts "Element is a login form"
# end
#
# === Selecting Elements
#
# Use the #select method to select all matching elements starting with
# one element and going through all children in depth-first order.
#
# This method returns an array of all matching elements, an empty array
# if no match is found
#
# For example:
# selector = HTML::Selector.new "input[type=text]"
# matches = selector.select(element)
# matches.each do |match|
# puts "Found text field with name #{match.attributes['name']}"
# end
#
# === Expressions
#
# Selectors can match elements using any of the following criteria:
# * <tt>name</tt> -- Match an element based on its name (tag name).
# For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt>
# to match any element.
# * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the
# <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>.
# * <tt>.class</tt> -- Match an element based on its class name, all
# class names if more than one specified.
# * <tt>[attr]</tt> -- Match an element that has the specified attribute.
# * <tt>[attr=value]</tt> -- Match an element that has the specified
# attribute and value. (More operators are supported see below)
# * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class,
# such as <tt>:nth-child</tt> and <tt>:empty</tt>.
# * <tt>:not(expr)</tt> -- Match an element that does not match the
# negation expression.
#
# When using a combination of the above, the element name comes first
# followed by identifier, class names, attributes, pseudo classes and
# negation in any order. Do not seprate these parts with spaces!
# Space separation is used for descendant selectors.
#
# For example:
# selector = HTML::Selector.new "form.login[action=/login]"
# The matched element must be of type +form+ and have the class +login+.
# It may have other classes, but the class +login+ is required to match.
# It must also have an attribute called +action+ with the value
# <tt>/login</tt>.
#
# This selector will match the following element:
# <form class="login form" method="post" action="/login">
# but will not match the element:
# <form method="post" action="/logout">
#
# === Attribute Values
#
# Several operators are supported for matching attributes:
# * <tt>name</tt> -- The element must have an attribute with that name.
# * <tt>name=value</tt> -- The element must have an attribute with that
# name and value.
# * <tt>name^=value</tt> -- The attribute value must start with the
# specified value.
# * <tt>name$=value</tt> -- The attribute value must end with the
# specified value.
# * <tt>name*=value</tt> -- The attribute value must contain the
# specified value.
# * <tt>name~=word</tt> -- The attribute value must contain the specified
# word (space separated).
# * <tt>name|=word</tt> -- The attribute value must start with specified
# word.
#
# For example, the following two selectors match the same element:
# #my_id
# [id=my_id]
# and so do the following two selectors:
# .my_class
# [class~=my_class]
#
# === Alternatives, siblings, children
#
# Complex selectors use a combination of expressions to match elements:
# * <tt>expr1 expr2</tt> -- Match any element against the second expression
# if it has some parent element that matches the first expression.
# * <tt>expr1 > expr2</tt> -- Match any element against the second expression
# if it is the child of an element that matches the first expression.
# * <tt>expr1 + expr2</tt> -- Match any element against the second expression
# if it immediately follows an element that matches the first expression.
# * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression
# that comes after an element that matches the first expression.
# * <tt>expr1, expr2</tt> -- Match any element against the first expression,
# or against the second expression.
#
# Since children and sibling selectors may match more than one element given
# the first element, the #match method may return more than one match.
#
# === Pseudo classes
#
# Pseudo classes were introduced in CSS 3. They are most often used to select
# elements in a given position:
# * <tt>:root</tt> -- Match the element only if it is the root element
# (no parent element).
# * <tt>:empty</tt> -- Match the element only if it has no child elements,
# and no text content.
# * <tt>:only-child</tt> -- Match the element if it is the only child (element)
# of its parent element.
# * <tt>:only-of-type</tt> -- Match the element if it is the only child (element)
# of its parent element and its type.
# * <tt>:first-child</tt> -- Match the element if it is the first child (element)
# of its parent element.
# * <tt>:first-of-type</tt> -- Match the element if it is the first child (element)
# of its parent element of its type.
# * <tt>:last-child</tt> -- Match the element if it is the last child (element)
# of its parent element.
# * <tt>:last-of-type</tt> -- Match the element if it is the last child (element)
# of its parent element of its type.
# * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element)
# of its parent element. The value <tt>b</tt> specifies its index, starting with 1.
# * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element)
# in each group of <tt>a</tt> child elements of its parent element.
# * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element)
# in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child
# elements of its parent element.
# * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third).
# Same as <tt>:nth-child(2n+1)</tt>.
# * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second,
# fourth). Same as <tt>:nth-child(2n+2)</tt>.
# * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type.
# * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child.
# * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and
# only elements of its type.
# * <tt>:not(selector)</tt> -- Match the element only if the element does not
# match the simple selector.
#
# As you can see, <tt>:nth-child<tt> pseudo class and its varient can get quite
# tricky and the CSS specification doesn't do a much better job explaining it.
# But after reading the examples and trying a few combinations, it's easy to
# figure out.
#
# For example:
# table tr:nth-child(odd)
# Selects every second row in the table starting with the first one.
#
# div p:nth-child(4)
# Selects the fourth paragraph in the +div+, but not if the +div+ contains
# other elements, since those are also counted.
#
# div p:nth-of-type(4)
# Selects the fourth paragraph in the +div+, counting only paragraphs, and
# ignoring all other elements.
#
# div p:nth-of-type(-n+4)
# Selects the first four paragraphs, ignoring all others.
#
# And you can always select an element that matches one set of rules but
# not another using <tt>:not</tt>. For example:
# p:not(.post)
# Matches all paragraphs that do not have the class <tt>.post</tt>.
#
# === Substitution Values
#
# You can use substitution with identifiers, class names and element values.
# A substitution takes the form of a question mark (<tt>?</tt>) and uses the
# next value in the argument list following the CSS expression.
#
# The substitution value may be a string or a regular expression. All other
# values are converted to strings.
#
# For example:
# selector = HTML::Selector.new "#?", /^\d+$/
# matches any element whose identifier consists of one or more digits.
#
# See http://www.w3.org/TR/css3-selectors/
class Selector
# An invalid selector.
class InvalidSelectorError < StandardError #:nodoc:
end
class << self
# :call-seq:
# Selector.for_class(cls) => selector
#
# Creates a new selector for the given class name.
def for_class(cls)
self.new([".?", cls])
end
# :call-seq:
# Selector.for_id(id) => selector
#
# Creates a new selector for the given id.
def for_id(id)
self.new(["#?", id])
end
end
# :call-seq:
# Selector.new(string, [values ...]) => selector
#
# Creates a new selector from a CSS 2 selector expression.
#
# The first argument is the selector expression. All other arguments
# are used for value substitution.
#
# Throws InvalidSelectorError is the selector expression is invalid.
def initialize(selector, *values)
raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
@source = ""
values = values[0] if values.size == 1 && values[0].is_a?(Array)
# We need a copy to determine if we failed to parse, and also
# preserve the original pass by-ref statement.
statement = selector.strip.dup
# Create a simple selector, along with negation.
simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
# Alternative selector.
if statement.sub!(/^\s*,\s*/, "")
second = Selector.new(statement, values)
(@alternates ||= []) << second
# If there are alternate selectors, we group them in the top selector.
if alternates = second.instance_variable_get(:@alternates)
second.instance_variable_set(:@alternates, nil)
@alternates.concat alternates
end
@source << " , " << second.to_s
# Sibling selector: create a dependency into second selector that will
# match element immediately following this one.
elsif statement.sub!(/^\s*\+\s*/, "")
second = next_selector(statement, values)
@depends = lambda do |element, first|
if element = next_element(element)
second.match(element, first)
end
end
@source << " + " << second.to_s
# Adjacent selector: create a dependency into second selector that will
# match all elements following this one.
elsif statement.sub!(/^\s*~\s*/, "")
second = next_selector(statement, values)
@depends = lambda do |element, first|
matches = []
while element = next_element(element)
if subset = second.match(element, first)
if first && !subset.empty?
matches << subset.first
break
else
matches.concat subset
end
end
end
matches.empty? ? nil : matches
end
@source << " ~ " << second.to_s
# Child selector: create a dependency into second selector that will
# match a child element of this one.
elsif statement.sub!(/^\s*>\s*/, "")
second = next_selector(statement, values)
@depends = lambda do |element, first|
matches = []
element.children.each do |child|
if child.tag? && subset = second.match(child, first)
if first && !subset.empty?
matches << subset.first
break
else
matches.concat subset
end
end
end
matches.empty? ? nil : matches
end
@source << " > " << second.to_s
# Descendant selector: create a dependency into second selector that
# will match all descendant elements of this one. Note,
elsif statement =~ /^\s+\S+/ && statement != selector
second = next_selector(statement, values)
@depends = lambda do |element, first|
matches = []
stack = element.children.reverse
while node = stack.pop
next unless node.tag?
if subset = second.match(node, first)
if first && !subset.empty?
matches << subset.first
break
else
matches.concat subset
end
elsif children = node.children
stack.concat children.reverse
end
end
matches.empty? ? nil : matches
end
@source << " " << second.to_s
else
# The last selector is where we check that we parsed
# all the parts.
unless statement.empty? || statement.strip.empty?
raise ArgumentError, "Invalid selector: #{statement}"
end
end
end
# :call-seq:
# match(element, first?) => array or nil
#
# Matches an element against the selector.
#
# For a simple selector this method returns an array with the
# element if the element matches, nil otherwise.
#
# For a complex selector (sibling and descendant) this method
# returns an array with all matching elements, nil if no match is
# found.
#
# Use +first_only=true+ if you are only interested in the first element.
#
# For example:
# if selector.match(element)
# puts "Element is a login form"
# end
def match(element, first_only = false)
# Match element if no element name or element name same as element name
if matched = (!@tag_name || @tag_name == element.name)
# No match if one of the attribute matches failed
for attr in @attributes
if element.attributes[attr[0]] !~ attr[1]
matched = false
break
end
end
end
# Pseudo class matches (nth-child, empty, etc).
if matched
for pseudo in @pseudo
unless pseudo.call(element)
matched = false
break
end
end
end
# Negation. Same rules as above, but we fail if a match is made.
if matched && @negation
for negation in @negation
if negation[:tag_name] == element.name
matched = false
else
for attr in negation[:attributes]
if element.attributes[attr[0]] =~ attr[1]
matched = false
break
end
end
end
if matched
for pseudo in negation[:pseudo]
if pseudo.call(element)
matched = false
break
end
end
end
break unless matched
end
end
# If element matched but depends on another element (child,
# sibling, etc), apply the dependent matches instead.
if matched && @depends
matches = @depends.call(element, first_only)
else
matches = matched ? [element] : nil
end
# If this selector is part of the group, try all the alternative
# selectors (unless first_only).
if @alternates && (!first_only || !matches)
@alternates.each do |alternate|
break if matches && first_only
if subset = alternate.match(element, first_only)
if matches
matches.concat subset
else
matches = subset
end
end
end
end
matches
end
# :call-seq:
# select(root) => array
#
# Selects and returns an array with all matching elements, beginning
# with one node and traversing through all children depth-first.
# Returns an empty array if no match is found.
#
# The root node may be any element in the document, or the document
# itself.
#
# For example:
# selector = HTML::Selector.new "input[type=text]"
# matches = selector.select(element)
# matches.each do |match|
# puts "Found text field with name #{match.attributes['name']}"
# end
def select(root)
matches = []
stack = [root]
while node = stack.pop
if node.tag? && subset = match(node, false)
subset.each do |match|
matches << match unless matches.any? { |item| item.equal?(match) }
end
elsif children = node.children
stack.concat children.reverse
end
end
matches
end
# Similar to #select but returns the first matching element. Returns +nil+
# if no element matches the selector.
def select_first(root)
stack = [root]
while node = stack.pop
if node.tag? && subset = match(node, true)
return subset.first if !subset.empty?
elsif children = node.children
stack.concat children.reverse
end
end
nil
end
def to_s #:nodoc:
@source
end
# Return the next element after this one. Skips sibling text nodes.
#
# With the +name+ argument, returns the next element with that name,
# skipping other sibling elements.
def next_element(element, name = nil)
if siblings = element.parent.children
found = false
siblings.each do |node|
if node.equal?(element)
found = true
elsif found && node.tag?
return node if (name.nil? || node.name == name)
end
end
end
nil
end
protected
# Creates a simple selector given the statement and array of
# substitution values.
#
# Returns a hash with the values +tag_name+, +attributes+,
# +pseudo+ (classes) and +negation+.
#
# Called the first time with +can_negate+ true to allow
# negation. Called a second time with false since negation
# cannot be negated.
def simple_selector(statement, values, can_negate = true)
tag_name = nil
attributes = []
pseudo = []
negation = []
# Element name. (Note that in negation, this can come at
# any order, but for simplicity we allow if only first).
statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match|
match.strip!
tag_name = match.downcase unless match == "*"
@source << match
"" # Remove
end
# Get identifier, class, attribute name, pseudo or negation.
while true
# Element identifier.
next if statement.sub!(/^#(\?|[\w\-]+)/) do |match|
id = $1
if id == "?"
id = values.shift
end
@source << "##{id}"
id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp)
attributes << ["id", id]
"" # Remove
end
# Class name.
next if statement.sub!(/^\.([\w\-]+)/) do |match|
class_name = $1
@source << ".#{class_name}"
class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp)
attributes << ["class", class_name]
"" # Remove
end
# Attribute value.
next if statement.sub!(/^\[\s*([[:alpha:]][\w\-]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match|
name, equality, value = $1, $2, $3
if value == "?"
value = values.shift
else
# Handle single and double quotes.
value.strip!
if (value[0] == ?" || value[0] == ?') && value[0] == value[-1]
value = value[1..-2]
end
end
@source << "[#{name}#{equality}'#{value}']"
attributes << [name.downcase.strip, attribute_match(equality, value)]
"" # Remove
end
# Root element only.
next if statement.sub!(/^:root/) do |match|
pseudo << lambda do |element|
element.parent.nil? || !element.parent.tag?
end
@source << ":root"
"" # Remove
end
# Nth-child including last and of-type.
next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match|
reverse = $1 == "last-"
of_type = $2 == "of-type"
@source << ":nth-#{$1}#{$2}("
case $3
when "odd"
pseudo << nth_child(2, 1, of_type, reverse)
@source << "odd)"
when "even"
pseudo << nth_child(2, 2, of_type, reverse)
@source << "even)"
when /^(\d+|\?)$/ # b only
b = ($1 == "?" ? values.shift : $1).to_i
pseudo << nth_child(0, b, of_type, reverse)
@source << "#{b})"
when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/
a = ($1 == "?" ? values.shift :
$1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i
b = ($2 == "?" ? values.shift : $2).to_i
pseudo << nth_child(a, b, of_type, reverse)
@source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})")
else
raise ArgumentError, "Invalid nth-child #{match}"
end
"" # Remove
end
# First/last child (of type).
next if statement.sub!(/^:(first|last)-(child|of-type)/) do |match|
reverse = $1 == "last"
of_type = $2 == "of-type"
pseudo << nth_child(0, 1, of_type, reverse)
@source << ":#{$1}-#{$2}"
"" # Remove
end
# Only child (of type).
next if statement.sub!(/^:only-(child|of-type)/) do |match|
of_type = $1 == "of-type"
pseudo << only_child(of_type)
@source << ":only-#{$1}"
"" # Remove
end
# Empty: no child elements or meaningful content (whitespaces
# are ignored).
next if statement.sub!(/^:empty/) do |match|
pseudo << lambda do |element|
empty = true
for child in element.children
if child.tag? || !child.content.strip.empty?
empty = false
break
end
end
empty
end
@source << ":empty"
"" # Remove
end
# Content: match the text content of the element, stripping
# leading and trailing spaces.
next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do |match|
content = $1
if content == "?"
content = values.shift
elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1]
content = content[1..-2]
end
@source << ":content('#{content}')"
content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp)
pseudo << lambda do |element|
text = ""
for child in element.children
unless child.tag?
text << child.content
end
end
text.strip =~ content
end
"" # Remove
end
# Negation. Create another simple selector to handle it.
if statement.sub!(/^:not\(\s*/, "")
raise ArgumentError, "Double negatives are not missing feature" unless can_negate
@source << ":not("
negation << simple_selector(statement, values, false)
raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "")
@source << ")"
next
end
# No match: moving on.
break
end
# Return hash. The keys are mapped to instance variables.
{:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation}
end
# Create a regular expression to match an attribute value based
# on the equality operator (=, ^=, |=, etc).
def attribute_match(equality, value)
regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
case equality
when "=" then
# Match the attribute value in full
Regexp.new("^#{regexp}$")
when "~=" then
# Match a space-separated word within the attribute value
Regexp.new("(^|\s)#{regexp}($|\s)")
when "^="
# Match the beginning of the attribute value
Regexp.new("^#{regexp}")
when "$="
# Match the end of the attribute value
Regexp.new("#{regexp}$")
when "*="
# Match substring of the attribute value
regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
when "|=" then
# Match the first space-separated item of the attribute value
Regexp.new("^#{regexp}($|\s)")
else
raise InvalidSelectorError, "Invalid operation/value" unless value.empty?
# Match all attributes values (existence check)
//
end
end
# Returns a lambda that can match an element against the nth-child
# pseudo class, given the following arguments:
# * +a+ -- Value of a part.
# * +b+ -- Value of b part.
# * +of_type+ -- True to test only elements of this type (of-type).
# * +reverse+ -- True to count in reverse order (last-).
def nth_child(a, b, of_type, reverse)
# a = 0 means select at index b, if b = 0 nothing selected
return lambda { |element| false } if a == 0 && b == 0
# a < 0 and b < 0 will never match against an index
return lambda { |element| false } if a < 0 && b < 0
b = a + b + 1 if b < 0 # b < 0 just picks last element from each group
b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based
lambda do |element|
# Element must be inside parent element.
return false unless element.parent && element.parent.tag?
index = 0
# Get siblings, reverse if counting from last.
siblings = element.parent.children
siblings = siblings.reverse if reverse
# Match element name if of-type, otherwise ignore name.
name = of_type ? element.name : nil
found = false
for child in siblings
# Skip text nodes/comments.
if child.tag? && (name == nil || child.name == name)
if a == 0
# Shortcut when a == 0 no need to go past count
if index == b
found = child.equal?(element)
break
end
elsif a < 0
# Only look for first b elements
break if index > b
if child.equal?(element)
found = (index % a) == 0
break
end
else
# Otherwise, break if child found and count == an+b
if child.equal?(element)
found = (index % a) == b
break
end
end
index += 1
end
end
found
end
end
# Creates a only child lambda. Pass +of-type+ to only look at
# elements of its type.
def only_child(of_type)
lambda do |element|
# Element must be inside parent element.
return false unless element.parent && element.parent.tag?
name = of_type ? element.name : nil
other = false
for child in element.parent.children
# Skip text nodes/comments.
if child.tag? && (name == nil || child.name == name)
unless child.equal?(element)
other = true
break
end
end
end
!other
end
end
# Called to create a dependent selector (sibling, descendant, etc).
# Passes the remainder of the statement that will be reduced to zero
# eventually, and array of substitution values.
#
# This method is called from four places, so it helps to put it here
# for resue. The only logic deals with the need to detect comma
# separators (alternate) and apply them to the selector group of the
# top selector.
def next_selector(statement, values)
second = Selector.new(statement, values)
# If there are alternate selectors, we group them in the top selector.
if alternates = second.instance_variable_get(:@alternates)
second.instance_variable_set(:@alternates, nil)
(@alternates ||= []).concat alternates
end
second
end
end
# See HTML::Selector.new
def self.selector(statement, *values)
Selector.new(statement, *values)
end
class Tag
def select(selector, *values)
selector = HTML::Selector.new(selector, values)
selector.select(self)
end
end
end

View file

@ -0,0 +1,34 @@
module ActionView
module Helpers
module PrototypeHelper
def update_element_function(element_id, options = {}, &block)
content = escape_javascript(options[:content] || '')
content = escape_javascript(capture(&block)) if block
javascript_function = case (options[:action] || :update)
when :update
if options[:position]
"new Insertion.#{options[:position].to_s.camelize}('#{element_id}','#{content}')"
else
"$('#{element_id}').innerHTML = '#{content}'"
end
when :empty
"$('#{element_id}').innerHTML = ''"
when :remove
"Element.remove('#{element_id}')"
else
raise ArgumentError, "Invalid action, choose one of :update, :remove, :empty"
end
javascript_function << ";\n"
options[:binding] ? concat(javascript_function, options[:binding]) : javascript_function
end
deprecate :update_element_function => "use RJS instead"
end
end
end

View file

@ -0,0 +1,576 @@
#--
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
# Under MIT and/or CC By license.
#++
require File.dirname(__FILE__) + '/../abstract_unit'
require File.dirname(__FILE__) + '/fake_controllers'
unless defined?(ActionMailer)
begin
$:.unshift(File.dirname(__FILE__) + "/../../../actionmailer/lib")
require 'action_mailer'
rescue LoadError
require 'rubygems'
gem 'actionmailer'
end
end
class AssertSelectTest < Test::Unit::TestCase
class AssertSelectController < ActionController::Base
def response_with=(content)
@content = content
end
def response_with(&block)
@update = block
end
def html()
render :text=>@content, :layout=>false, :content_type=>Mime::HTML
@content = nil
end
def rjs()
render :update do |page|
@update.call page
end
@update = nil
end
def xml()
render :text=>@content, :layout=>false, :content_type=>Mime::XML
@content = nil
end
def rescue_action(e)
raise e
end
end
class AssertSelectMailer < ActionMailer::Base
def test(html)
recipients "test <test@test.host>"
from "test@test.host"
subject "Test e-mail"
part :content_type=>"text/html", :body=>html
end
end
AssertionFailedError = Test::Unit::AssertionFailedError
def setup
@controller = AssertSelectController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
end
def teardown
ActionMailer::Base.deliveries.clear
end
#
# Test assert select.
#
def test_assert_select
render_html %Q{<div id="1"></div><div id="2"></div>}
assert_select "div", 2
assert_raises(AssertionFailedError) { assert_select "div", 3 }
assert_raises(AssertionFailedError){ assert_select "p" }
end
def test_equality_true_false
render_html %Q{<div id="1"></div><div id="2"></div>}
assert_nothing_raised { assert_select "div" }
assert_raises(AssertionFailedError) { assert_select "p" }
assert_nothing_raised { assert_select "div", true }
assert_raises(AssertionFailedError) { assert_select "p", true }
assert_raises(AssertionFailedError) { assert_select "div", false }
assert_nothing_raised { assert_select "p", false }
end
def test_equality_string_and_regexp
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_nothing_raised { assert_select "div", "foo" }
assert_raises(AssertionFailedError) { assert_select "div", "bar" }
assert_nothing_raised { assert_select "div", :text=>"foo" }
assert_raises(AssertionFailedError) { assert_select "div", :text=>"bar" }
assert_nothing_raised { assert_select "div", /(foo|bar)/ }
assert_raises(AssertionFailedError) { assert_select "div", /foobar/ }
assert_nothing_raised { assert_select "div", :text=>/(foo|bar)/ }
assert_raises(AssertionFailedError) { assert_select "div", :text=>/foobar/ }
assert_raises(AssertionFailedError) { assert_select "p", :text=>/foobar/ }
end
def test_equality_of_html
render_html %Q{<p>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</p>}
text = "\"This is not a big problem,\" he said."
html = "<em>\"This is <strong>not</strong> a big problem,\"</em> he said."
assert_nothing_raised { assert_select "p", text }
assert_raises(AssertionFailedError) { assert_select "p", html }
assert_nothing_raised { assert_select "p", :html=>html }
assert_raises(AssertionFailedError) { assert_select "p", :html=>text }
# No stripping for pre.
render_html %Q{<pre>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</pre>}
text = "\n\"This is not a big problem,\" he said.\n"
html = "\n<em>\"This is <strong>not</strong> a big problem,\"</em> he said.\n"
assert_nothing_raised { assert_select "pre", text }
assert_raises(AssertionFailedError) { assert_select "pre", html }
assert_nothing_raised { assert_select "pre", :html=>html }
assert_raises(AssertionFailedError) { assert_select "pre", :html=>text }
end
def test_equality_of_instances
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_nothing_raised { assert_select "div", 2 }
assert_raises(AssertionFailedError) { assert_select "div", 3 }
assert_nothing_raised { assert_select "div", 1..2 }
assert_raises(AssertionFailedError) { assert_select "div", 3..4 }
assert_nothing_raised { assert_select "div", :count=>2 }
assert_raises(AssertionFailedError) { assert_select "div", :count=>3 }
assert_nothing_raised { assert_select "div", :minimum=>1 }
assert_nothing_raised { assert_select "div", :minimum=>2 }
assert_raises(AssertionFailedError) { assert_select "div", :minimum=>3 }
assert_nothing_raised { assert_select "div", :maximum=>2 }
assert_nothing_raised { assert_select "div", :maximum=>3 }
assert_raises(AssertionFailedError) { assert_select "div", :maximum=>1 }
assert_nothing_raised { assert_select "div", :minimum=>1, :maximum=>2 }
assert_raises(AssertionFailedError) { assert_select "div", :minimum=>3, :maximum=>4 }
end
def test_substitution_values
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_select "div#?", /\d+/ do |elements|
assert_equal 2, elements.size
end
assert_select "div" do
assert_select "div#?", /\d+/ do |elements|
assert_equal 2, elements.size
assert_select "#1"
assert_select "#2"
end
end
end
def test_nested_assert_select
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_select "div" do |elements|
assert_equal 2, elements.size
assert_select elements[0], "#1"
assert_select elements[1], "#2"
end
assert_select "div" do
assert_select "div" do |elements|
assert_equal 2, elements.size
# Testing in a group is one thing
assert_select "#1,#2"
# Testing individually is another.
assert_select "#1"
assert_select "#2"
assert_select "#3", false
end
end
end
def test_assert_select_text_match
render_html %Q{<div id="1"><span>foo</span></div><div id="2"><span>bar</span></div>}
assert_select "div" do
assert_nothing_raised { assert_select "div", "foo" }
assert_nothing_raised { assert_select "div", "bar" }
assert_nothing_raised { assert_select "div", /\w*/ }
assert_nothing_raised { assert_select "div", /\w*/, :count=>2 }
assert_raises(AssertionFailedError) { assert_select "div", :text=>"foo", :count=>2 }
assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
assert_nothing_raised { assert_select "div", :html=>/\w*/ }
assert_nothing_raised { assert_select "div", :html=>/\w*/, :count=>2 }
assert_raises(AssertionFailedError) { assert_select "div", :html=>"<span>foo</span>", :count=>2 }
end
end
# With single result.
def test_assert_select_from_rjs_with_single_result
render_rjs do |page|
page.replace_html "test", "<div id=\"1\">foo</div>\n<div id=\"2\">foo</div>"
end
assert_select "div" do |elements|
assert elements.size == 2
assert_select "#1"
assert_select "#2"
end
assert_select "div#?", /\d+/ do |elements|
assert_select "#1"
assert_select "#2"
end
end
# With multiple results.
def test_assert_select_from_rjs_with_multiple_results
render_rjs do |page|
page.replace_html "test", "<div id=\"1\">foo</div>"
page.replace_html "test2", "<div id=\"2\">foo</div>"
end
assert_select "div" do |elements|
assert elements.size == 2
assert_select "#1"
assert_select "#2"
end
end
#
# Test css_select.
#
def test_css_select
render_html %Q{<div id="1"></div><div id="2"></div>}
assert 2, css_select("div").size
assert 0, css_select("p").size
end
def test_nested_css_select
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_select "div#?", /\d+/ do |elements|
assert_equal 1, css_select(elements[0], "div").size
assert_equal 1, css_select(elements[1], "div").size
end
assert_select "div" do
assert_equal 2, css_select("div").size
css_select("div").each do |element|
# Testing as a group is one thing
assert !css_select("#1,#2").empty?
# Testing individually is another
assert !css_select("#1").empty?
assert !css_select("#2").empty?
end
end
end
# With one result.
def test_css_select_from_rjs_with_single_result
render_rjs do |page|
page.replace_html "test", "<div id=\"1\">foo</div>\n<div id=\"2\">foo</div>"
end
assert_equal 2, css_select("div").size
assert_equal 1, css_select("#1").size
assert_equal 1, css_select("#2").size
end
# With multiple results.
def test_css_select_from_rjs_with_multiple_results
render_rjs do |page|
page.replace_html "test", "<div id=\"1\">foo</div>"
page.replace_html "test2", "<div id=\"2\">foo</div>"
end
assert_equal 2, css_select("div").size
assert_equal 1, css_select("#1").size
assert_equal 1, css_select("#2").size
end
#
# Test assert_select_rjs.
#
# Test that we can pick up all statements in the result.
def test_assert_select_rjs_picks_up_all_statements
render_rjs do |page|
page.replace "test", "<div id=\"1\">foo</div>"
page.replace_html "test2", "<div id=\"2\">foo</div>"
page.insert_html :top, "test3", "<div id=\"3\">foo</div>"
end
found = false
assert_select_rjs do
assert_select "#1"
assert_select "#2"
assert_select "#3"
found = true
end
assert found
end
# Test that we fail if there is nothing to pick.
def test_assert_select_rjs_fails_if_nothing_to_pick
render_rjs { }
assert_raises(AssertionFailedError) { assert_select_rjs }
end
def test_assert_select_rjs_with_unicode
# Test that non-ascii characters (which are converted into \uXXXX in RJS) are decoded correctly.
render_rjs do |page|
page.replace "test", "<div id=\"1\">\343\203\201\343\202\261\343\203\203\343\203\210</div>"
end
assert_select_rjs do
assert_select "#1", :text => "\343\203\201\343\202\261\343\203\203\343\203\210"
assert_select "#1", "\343\203\201\343\202\261\343\203\203\343\203\210"
assert_select "#1", Regexp.new("\343\203\201..\343\203\210",0,'U')
assert_raises(AssertionFailedError) { assert_select "#1", Regexp.new("\343\203\201.\343\203\210",0,'U') }
end
end
def test_assert_select_rjs_with_id
# Test that we can pick up all statements in the result.
render_rjs do |page|
page.replace "test1", "<div id=\"1\">foo</div>"
page.replace_html "test2", "<div id=\"2\">foo</div>"
page.insert_html :top, "test3", "<div id=\"3\">foo</div>"
end
assert_select_rjs "test1" do
assert_select "div", 1
assert_select "#1"
end
assert_select_rjs "test2" do
assert_select "div", 1
assert_select "#2"
end
assert_select_rjs "test3" do
assert_select "div", 1
assert_select "#3"
end
assert_raises(AssertionFailedError) { assert_select_rjs "test4" }
end
def test_assert_select_rjs_for_replace
render_rjs do |page|
page.replace "test1", "<div id=\"1\">foo</div>"
page.replace_html "test2", "<div id=\"2\">foo</div>"
page.insert_html :top, "test3", "<div id=\"3\">foo</div>"
end
# Replace.
assert_select_rjs :replace do
assert_select "div", 1
assert_select "#1"
end
assert_select_rjs :replace, "test1" do
assert_select "div", 1
assert_select "#1"
end
assert_raises(AssertionFailedError) { assert_select_rjs :replace, "test2" }
# Replace HTML.
assert_select_rjs :replace_html do
assert_select "div", 1
assert_select "#2"
end
assert_select_rjs :replace_html, "test2" do
assert_select "div", 1
assert_select "#2"
end
assert_raises(AssertionFailedError) { assert_select_rjs :replace_html, "test1" }
end
def test_assert_select_rjs_for_chained_replace
render_rjs do |page|
page['test1'].replace "<div id=\"1\">foo</div>"
page['test2'].replace_html "<div id=\"2\">foo</div>"
page.insert_html :top, "test3", "<div id=\"3\">foo</div>"
end
# Replace.
assert_select_rjs :chained_replace do
assert_select "div", 1
assert_select "#1"
end
assert_select_rjs :chained_replace, "test1" do
assert_select "div", 1
assert_select "#1"
end
assert_raises(AssertionFailedError) { assert_select_rjs :chained_replace, "test2" }
# Replace HTML.
assert_select_rjs :chained_replace_html do
assert_select "div", 1
assert_select "#2"
end
assert_select_rjs :chained_replace_html, "test2" do
assert_select "div", 1
assert_select "#2"
end
assert_raises(AssertionFailedError) { assert_select_rjs :replace_html, "test1" }
end
# Non-positioned insert.
def test_assert_select_rjs_for_nonpositioned_insert
render_rjs do |page|
page.replace "test1", "<div id=\"1\">foo</div>"
page.replace_html "test2", "<div id=\"2\">foo</div>"
page.insert_html :top, "test3", "<div id=\"3\">foo</div>"
end
assert_select_rjs :insert_html do
assert_select "div", 1
assert_select "#3"
end
assert_select_rjs :insert_html, "test3" do
assert_select "div", 1
assert_select "#3"
end
assert_raises(AssertionFailedError) { assert_select_rjs :insert_html, "test1" }
end
# Positioned insert.
def test_assert_select_rjs_for_positioned_insert
render_rjs do |page|
page.insert_html :top, "test1", "<div id=\"1\">foo</div>"
page.insert_html :bottom, "test2", "<div id=\"2\">foo</div>"
page.insert_html :before, "test3", "<div id=\"3\">foo</div>"
page.insert_html :after, "test4", "<div id=\"4\">foo</div>"
end
assert_select_rjs :insert, :top do
assert_select "div", 1
assert_select "#1"
end
assert_select_rjs :insert, :bottom do
assert_select "div", 1
assert_select "#2"
end
assert_select_rjs :insert, :before do
assert_select "div", 1
assert_select "#3"
end
assert_select_rjs :insert, :after do
assert_select "div", 1
assert_select "#4"
end
assert_select_rjs :insert_html do
assert_select "div", 4
end
end
# Simple selection from a single result.
def test_nested_assert_select_rjs_with_single_result
render_rjs do |page|
page.replace_html "test", "<div id=\"1\">foo</div>\n<div id=\"2\">foo</div>"
end
assert_select_rjs "test" do |elements|
assert_equal 2, elements.size
assert_select "#1"
assert_select "#2"
end
end
# Deal with two results.
def test_nested_assert_select_rjs_with_two_results
render_rjs do |page|
page.replace_html "test", "<div id=\"1\">foo</div>"
page.replace_html "test2", "<div id=\"2\">foo</div>"
end
assert_select_rjs "test" do |elements|
assert_equal 1, elements.size
assert_select "#1"
end
assert_select_rjs "test2" do |elements|
assert_equal 1, elements.size
assert_select "#2"
end
end
def test_feed_item_encoded
render_xml <<-EOF
<rss version="2.0">
<channel>
<item>
<description>
<![CDATA[
<p>Test 1</p>
]]>
</description>
</item>
<item>
<description>
<![CDATA[
<p>Test 2</p>
]]>
</description>
</item>
</channel>
</rss>
EOF
assert_select "channel item description" do
# Test element regardless of wrapper.
assert_select_encoded do
assert_select "p", :count=>2, :text=>/Test/
end
# Test through encoded wrapper.
assert_select_encoded do
assert_select "encoded p", :count=>2, :text=>/Test/
end
# Use :root instead (recommended)
assert_select_encoded do
assert_select ":root p", :count=>2, :text=>/Test/
end
# Test individually.
assert_select "description" do |elements|
assert_select_encoded elements[0] do
assert_select "p", "Test 1"
end
assert_select_encoded elements[1] do
assert_select "p", "Test 2"
end
end
end
# Test that we only un-encode element itself.
assert_select "channel item" do
assert_select_encoded do
assert_select "p", 0
end
end
end
#
# Test assert_select_email
#
def test_assert_select_email
assert_raises(AssertionFailedError) { assert_select_email {} }
AssertSelectMailer.deliver_test "<div><p>foo</p><p>bar</p></div>"
assert_select_email do
assert_select "div:root" do
assert_select "p:first-child", "foo"
assert_select "p:last-child", "bar"
end
end
end
protected
def render_html(html)
@controller.response_with = html
get :html
end
def render_rjs(&block)
@controller.response_with &block
get :rjs
end
def render_xml(xml)
@controller.response_with = xml
get :xml
end
end

View file

@ -0,0 +1,228 @@
require 'fileutils'
require File.dirname(__FILE__) + '/../abstract_unit'
CACHE_DIR = 'test_cache'
# Don't change '/../temp/' cavalierly or you might hoze something you don't want hozed
FILE_STORE_PATH = File.join(File.dirname(__FILE__), '/../temp/', CACHE_DIR)
ActionController::Base.perform_caching = true
ActionController::Base.page_cache_directory = FILE_STORE_PATH
ActionController::Base.fragment_cache_store = :file_store, FILE_STORE_PATH
class PageCachingTestController < ActionController::Base
caches_page :ok, :no_content, :found, :not_found
def ok
head :ok
end
def no_content
head :no_content
end
def found
redirect_to :action => 'ok'
end
def not_found
head :not_found
end
end
class PageCachingTest < Test::Unit::TestCase
def setup
ActionController::Routing::Routes.draw do |map|
map.main '', :controller => 'posts'
map.resources :posts
map.connect ':controller/:action/:id'
end
@request = ActionController::TestRequest.new
@request.host = 'hostname.com'
@response = ActionController::TestResponse.new
@controller = PageCachingTestController.new
@params = {:controller => 'posts', :action => 'index', :only_path => true, :skip_relative_url_root => true}
@rewriter = ActionController::UrlRewriter.new(@request, @params)
FileUtils.rm_rf(File.dirname(FILE_STORE_PATH))
FileUtils.mkdir_p(FILE_STORE_PATH)
end
def teardown
FileUtils.rm_rf(File.dirname(FILE_STORE_PATH))
end
def test_page_caching_resources_saves_to_correct_path_with_extension_even_if_default_route
@params[:format] = 'rss'
assert_equal '/posts.rss', @rewriter.rewrite(@params)
@params[:format] = nil
assert_equal '/', @rewriter.rewrite(@params)
end
def test_should_cache_get_with_ok_status
get :ok
assert_response :ok
assert_page_cached :ok, "get with ok status should have been cached"
end
[:ok, :no_content, :found, :not_found].each do |status|
[:get, :post, :put, :delete].each do |method|
unless method == :get and status == :ok
define_method "test_shouldnt_cache_#{method}_with_#{status}_status" do
@request.env['REQUEST_METHOD'] = method.to_s.upcase
process status
assert_response status
assert_page_not_cached status, "#{method} with #{status} status shouldn't have been cached"
end
end
end
end
private
def assert_page_cached(action, message = "#{action} should have been cached")
assert page_cached?(action), message
end
def assert_page_not_cached(action, message = "#{action} shouldn't have been cached")
assert !page_cached?(action), message
end
def page_cached?(action)
File.exist? "#{FILE_STORE_PATH}/page_caching_test/#{action}.html"
end
end
class ActionCachingTestController < ActionController::Base
caches_action :index
def index
@cache_this = Time.now.to_f.to_s
render :text => @cache_this
end
def expire
expire_action :controller => 'action_caching_test', :action => 'index'
render :nothing => true
end
end
class ActionCachingMockController
attr_accessor :mock_url_for
attr_accessor :mock_path
def initialize
yield self if block_given?
end
def url_for(*args)
@mock_url_for
end
def request
mocked_path = @mock_path
Object.new.instance_eval(<<-EVAL)
def path; '#{@mock_path}' end
self
EVAL
end
end
class ActionCacheTest < Test::Unit::TestCase
def setup
reset!
FileUtils.mkdir_p(FILE_STORE_PATH)
@path_class = ActionController::Caching::Actions::ActionCachePath
@mock_controller = ActionCachingMockController.new
end
def teardown
FileUtils.rm_rf(File.dirname(FILE_STORE_PATH))
end
def test_simple_action_cache
get :index
cached_time = content_to_cache
assert_equal cached_time, @response.body
reset!
get :index
assert_equal cached_time, @response.body
end
def test_cache_expiration
get :index
cached_time = content_to_cache
reset!
get :index
assert_equal cached_time, @response.body
reset!
get :expire
reset!
get :index
new_cached_time = content_to_cache
assert_not_equal cached_time, @response.body
reset!
get :index
assert_response :success
assert_equal new_cached_time, @response.body
end
def test_cache_is_scoped_by_subdomain
@request.host = 'jamis.hostname.com'
get :index
jamis_cache = content_to_cache
@request.host = 'david.hostname.com'
get :index
david_cache = content_to_cache
assert_not_equal jamis_cache, @response.body
@request.host = 'jamis.hostname.com'
get :index
assert_equal jamis_cache, @response.body
@request.host = 'david.hostname.com'
get :index
assert_equal david_cache, @response.body
end
def test_xml_version_of_resource_is_treated_as_different_cache
@mock_controller.mock_url_for = 'http://example.org/posts/'
@mock_controller.mock_path = '/posts/index.xml'
path_object = @path_class.new(@mock_controller)
assert_equal 'xml', path_object.extension
assert_equal 'example.org/posts/index.xml', path_object.path
end
def test_empty_path_is_normalized
@mock_controller.mock_url_for = 'http://example.org/'
@mock_controller.mock_path = '/'
assert_equal 'example.org/index', @path_class.path_for(@mock_controller)
end
def test_file_extensions
get :index, :id => 'kitten.jpg'
get :index, :id => 'kitten.jpg'
assert_response :success
end
private
def content_to_cache
assigns(:cache_this)
end
def reset!
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@controller = ActionCachingTestController.new
@request.host = 'hostname.com'
end
end

View file

@ -0,0 +1,139 @@
require File.dirname(__FILE__) + '/../abstract_unit'
class ContentTypeController < ActionController::Base
def render_content_type_from_body
response.content_type = Mime::RSS
render :text => "hello world!"
end
def render_defaults
render :text => "hello world!"
end
def render_content_type_from_render
render :text => "hello world!", :content_type => Mime::RSS
end
def render_charset_from_body
response.charset = "utf-16"
render :text => "hello world!"
end
def render_default_for_rhtml
end
def render_default_for_rxml
end
def render_default_for_rjs
end
def render_change_for_rxml
response.content_type = Mime::HTML
render :action => "render_default_for_rxml"
end
def render_default_content_types_for_respond_to
respond_to do |format|
format.html { render :text => "hello world!" }
format.xml { render :action => "render_default_content_types_for_respond_to.rhtml" }
format.js { render :text => "hello world!" }
format.rss { render :text => "hello world!", :content_type => Mime::XML }
end
end
def rescue_action(e) raise end
end
ContentTypeController.template_root = File.dirname(__FILE__) + "/../fixtures/"
class ContentTypeTest < Test::Unit::TestCase
def setup
@controller = ContentTypeController.new
# enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
# a more accurate simulation of what happens in "real life".
@controller.logger = Logger.new(nil)
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_render_defaults
get :render_defaults
assert_equal "utf-8", @response.charset
assert_equal Mime::HTML, @response.content_type
end
def test_render_changed_charset_default
ContentTypeController.default_charset = "utf-16"
get :render_defaults
assert_equal "utf-16", @response.charset
assert_equal Mime::HTML, @response.content_type
ContentTypeController.default_charset = "utf-8"
end
def test_content_type_from_body
get :render_content_type_from_body
assert_equal "application/rss+xml", @response.content_type
assert_equal "utf-8", @response.charset
end
def test_content_type_from_render
get :render_content_type_from_render
assert_equal "application/rss+xml", @response.content_type
assert_equal "utf-8", @response.charset
end
def test_charset_from_body
get :render_charset_from_body
assert_equal "utf-16", @response.charset
assert_equal Mime::HTML, @response.content_type
end
def test_default_for_rhtml
get :render_default_for_rhtml
assert_equal Mime::HTML, @response.content_type
assert_equal "utf-8", @response.charset
end
def test_default_for_rxml
get :render_default_for_rxml
assert_equal Mime::XML, @response.content_type
assert_equal "utf-8", @response.charset
end
def test_default_for_rjs
xhr :post, :render_default_for_rjs
assert_equal Mime::JS, @response.content_type
assert_equal "utf-8", @response.charset
end
def test_change_for_rxml
get :render_change_for_rxml
assert_equal Mime::HTML, @response.content_type
assert_equal "utf-8", @response.charset
end
def test_render_default_content_types_for_respond_to
@request.env["HTTP_ACCEPT"] = Mime::HTML.to_s
get :render_default_content_types_for_respond_to
assert_equal Mime::HTML, @response.content_type
@request.env["HTTP_ACCEPT"] = Mime::JS.to_s
get :render_default_content_types_for_respond_to
assert_equal Mime::JS, @response.content_type
end
def test_render_default_content_types_for_respond_to_with_template
@request.env["HTTP_ACCEPT"] = Mime::XML.to_s
get :render_default_content_types_for_respond_to
assert_equal Mime::XML, @response.content_type
end
def test_render_default_content_types_for_respond_to_with_overwrite
@request.env["HTTP_ACCEPT"] = Mime::RSS.to_s
get :render_default_content_types_for_respond_to
assert_equal Mime::XML, @response.content_type
end
end

View file

@ -0,0 +1,48 @@
require File.dirname(__FILE__) + '/../abstract_unit'
class DeprecatedControllerInstanceVariablesTest < Test::Unit::TestCase
class Target < ActionController::Base
def initialize(run = nil)
instance_eval(run) if run
super()
end
def noop
render :nothing => true
end
ActionController::Base::DEPRECATED_INSTANCE_VARIABLES.each do |var|
class_eval "def old_#{var}; render :text => @#{var}.to_s end"
class_eval "def new_#{var}; render :text => #{var}.to_s end"
class_eval "def internal_#{var}; render :text => @_#{var}.to_s end"
end
def rescue_action(e) raise e end
end
def setup
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@controller = Target.new
end
ActionController::Base::DEPRECATED_INSTANCE_VARIABLES.each do |var|
class_eval <<-end_eval, __FILE__, __LINE__
def test_old_#{var}_is_deprecated
assert_deprecated('@#{var}') { get :old_#{var} }
end
def test_new_#{var}_isnt_deprecated
assert_not_deprecated { get :new_#{var} }
end
def test_internal_#{var}_isnt_deprecated
assert_not_deprecated { get :internal_#{var} }
end
def test_#{var}_raises_if_already_set
assert_raise(RuntimeError) do
@controller = Target.new '@#{var} = Object.new'
get :noop
end
end
end_eval
end
end

View file

@ -0,0 +1,60 @@
require File.dirname(__FILE__) + '/../../abstract_unit'
class DeprecatedBaseMethodsTest < Test::Unit::TestCase
class Target < ActionController::Base
def deprecated_symbol_parameter_to_url_for
redirect_to(url_for(:home_url, "superstars"))
end
def deprecated_render_parameters
render "fun/games/hello_world"
end
def home_url(greeting)
"http://example.com/#{greeting}"
end
def raises_name_error
this_method_doesnt_exist
end
def rescue_action(e) raise e end
end
Target.template_root = File.dirname(__FILE__) + "/../../fixtures"
def setup
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@controller = Target.new
end
def test_deprecated_symbol_parameter_to_url_for
assert_deprecated("url_for(:home_url)") do
get :deprecated_symbol_parameter_to_url_for
end
assert_redirected_to "http://example.com/superstars"
end
def test_deprecated_render_parameters
assert_deprecated("render('fun/games/hello_world')") do
get :deprecated_render_parameters
end
assert_equal "Living in a nested world", @response.body
end
def test_log_error_silences_deprecation_warnings
get :raises_name_error
rescue => e
assert_not_deprecated { @controller.send :log_error, e }
end
def test_assertion_failed_error_silences_deprecation_warnings
get :raises_name_error
rescue => e
error = Test::Unit::Error.new('testing ur doodz', e)
assert_not_deprecated { error.message }
end
end

View file

@ -0,0 +1,154 @@
require File.dirname(__FILE__) + '/../abstract_unit'
$:.unshift File.dirname(__FILE__) + '/../../../railties/lib'
require 'action_controller/integration'
begin # rescue LoadError
require 'mocha'
require 'stubba'
# Stub process for testing.
module ActionController
module Integration
class Session
def process
end
def generic_url_rewriter
end
end
end
end
class SessionTest < Test::Unit::TestCase
def setup
@session = ActionController::Integration::Session.new
end
def test_https_bang_works_and_sets_truth_by_default
assert !@session.https?
@session.https!
assert @session.https?
@session.https! false
assert !@session.https?
end
def test_host!
assert_not_equal "glu.ttono.us", @session.host
@session.host! "rubyonrails.com"
assert_equal "rubyonrails.com", @session.host
end
def test_follow_redirect_raises_when_no_redirect
@session.stubs(:redirect?).returns(false)
assert_raise(RuntimeError) { @session.follow_redirect! }
end
def test_follow_redirect_calls_get_and_returns_status
@session.stubs(:redirect?).returns(true)
@session.stubs(:headers).returns({"location" => ["www.google.com"]})
@session.stubs(:status).returns(200)
@session.expects(:get)
assert_equal 200, @session.follow_redirect!
end
def test_get_via_redirect
path = "/somepath"; args = {:id => '1'}
@session.expects(:get).with(path,args)
redirects = [true, true, false]
@session.stubs(:redirect?).returns(lambda { redirects.shift })
@session.expects(:follow_redirect!).times(2)
@session.stubs(:status).returns(200)
assert_equal 200, @session.get_via_redirect(path, args)
end
def test_post_via_redirect
path = "/somepath"; args = {:id => '1'}
@session.expects(:post).with(path,args)
redirects = [true, true, false]
@session.stubs(:redirect?).returns(lambda { redirects.shift })
@session.expects(:follow_redirect!).times(2)
@session.stubs(:status).returns(200)
assert_equal 200, @session.post_via_redirect(path, args)
end
def test_url_for_with_controller
options = {:action => 'show'}
mock_controller = mock()
mock_controller.expects(:url_for).with(options).returns('/show')
@session.stubs(:controller).returns(mock_controller)
assert_equal '/show', @session.url_for(options)
end
def test_url_for_without_controller
options = {:action => 'show'}
mock_rewriter = mock()
mock_rewriter.expects(:rewrite).with(options).returns('/show')
@session.stubs(:generic_url_rewriter).returns(mock_rewriter)
@session.stubs(:controller).returns(nil)
assert_equal '/show', @session.url_for(options)
end
def test_redirect_bool_with_status_in_300s
@session.stubs(:status).returns 301
assert @session.redirect?
end
def test_redirect_bool_with_status_in_200s
@session.stubs(:status).returns 200
assert !@session.redirect?
end
def test_get
path = "/index"; params = "blah"; headers = {:location => 'blah'}
@session.expects(:process).with(:get,path,params,headers)
@session.get(path,params,headers)
end
def test_post
path = "/index"; params = "blah"; headers = {:location => 'blah'}
@session.expects(:process).with(:post,path,params,headers)
@session.post(path,params,headers)
end
def test_put
path = "/index"; params = "blah"; headers = {:location => 'blah'}
@session.expects(:process).with(:put,path,params,headers)
@session.put(path,params,headers)
end
def test_delete
path = "/index"; params = "blah"; headers = {:location => 'blah'}
@session.expects(:process).with(:delete,path,params,headers)
@session.delete(path,params,headers)
end
def test_head
path = "/index"; params = "blah"; headers = {:location => 'blah'}
@session.expects(:process).with(:head,path,params,headers)
@session.head(path,params,headers)
end
def test_xml_http_request
path = "/index"; params = "blah"; headers = {:location => 'blah'}
headers_after_xhr = headers.merge(
"X-Requested-With" => "XMLHttpRequest",
"Accept" => "text/javascript, text/html, application/xml, text/xml, */*"
)
@session.expects(:post).with(path,params,headers_after_xhr)
@session.xml_http_request(path,params,headers)
end
end
# TODO
# class MockCGITest < Test::Unit::TestCase
# end
rescue LoadError
$stderr.puts "Skipping integration tests. `gem install mocha` and try again."
end

View file

@ -0,0 +1,482 @@
require File.dirname(__FILE__) + '/../abstract_unit'
class ResourcesController < ActionController::Base
def index() render :nothing => true end
alias_method :show, :index
def rescue_action(e) raise e end
end
class ThreadsController < ResourcesController; end
class MessagesController < ResourcesController; end
class CommentsController < ResourcesController; end
class AccountController < ResourcesController; end
class AdminController < ResourcesController; end
class ResourcesTest < Test::Unit::TestCase
def test_should_arrange_actions
resource = ActionController::Resources::Resource.new(:messages,
:collection => { :rss => :get, :reorder => :post, :csv => :post },
:member => { :rss => :get, :atom => :get, :upload => :post, :fix => :post },
:new => { :preview => :get, :draft => :get })
assert_resource_methods [:rss], resource, :collection, :get
assert_resource_methods [:csv, :reorder], resource, :collection, :post
assert_resource_methods [:edit, :rss, :atom], resource, :member, :get
assert_resource_methods [:upload, :fix], resource, :member, :post
assert_resource_methods [:new, :preview, :draft], resource, :new, :get
end
def test_should_resource_controller_name_equal_resource_name_by_default
resource = ActionController::Resources::Resource.new(:messages, {})
assert_equal 'messages', resource.controller
end
def test_should_resource_controller_name_equal_controller_option
resource = ActionController::Resources::Resource.new(:messages, :controller => 'posts')
assert_equal 'posts', resource.controller
end
def test_should_all_singleton_paths_be_the_same
[ :path, :nesting_path_prefix, :member_path ].each do |method|
resource = ActionController::Resources::SingletonResource.new(:messages, :path_prefix => 'admin')
assert_equal 'admin/messages', resource.send(method)
end
end
def test_default_restful_routes
with_restful_routing :messages do
assert_simply_restful_for :messages
end
end
def test_multiple_default_restful_routes
with_restful_routing :messages, :comments do
assert_simply_restful_for :messages
assert_simply_restful_for :comments
end
end
def test_with_path_prefix
with_restful_routing :messages, :path_prefix => '/thread/:thread_id' do
assert_simply_restful_for :messages, :path_prefix => 'thread/5/', :options => { :thread_id => '5' }
end
end
def test_multile_with_path_prefix
with_restful_routing :messages, :comments, :path_prefix => '/thread/:thread_id' do
assert_simply_restful_for :messages, :path_prefix => 'thread/5/', :options => { :thread_id => '5' }
assert_simply_restful_for :comments, :path_prefix => 'thread/5/', :options => { :thread_id => '5' }
end
end
def test_with_name_prefix
with_restful_routing :messages, :name_prefix => 'post_' do
assert_simply_restful_for :messages, :name_prefix => 'post_'
end
end
def test_with_collection_action
rss_options = {:action => 'rss'}
rss_path = "/messages;rss"
actions = { 'a' => :put, 'b' => :post, 'c' => :delete }
with_restful_routing :messages, :collection => { :rss => :get }.merge(actions) do
assert_restful_routes_for :messages do |options|
assert_routing rss_path, options.merge(rss_options)
actions.each do |action, method|
assert_recognizes(options.merge(:action => action), :path => "/messages;#{action}", :method => method)
end
end
assert_restful_named_routes_for :messages do |options|
assert_named_route rss_path, :rss_messages_path, rss_options
actions.keys.each do |action|
assert_named_route "/messages;#{action}", "#{action}_messages_path", :action => action
end
end
end
end
def test_with_member_action
[:put, :post].each do |method|
with_restful_routing :messages, :member => { :mark => method } do
mark_options = {:action => 'mark', :id => '1'}
mark_path = "/messages/1;mark"
assert_restful_routes_for :messages do |options|
assert_recognizes(options.merge(mark_options), :path => mark_path, :method => method)
end
assert_restful_named_routes_for :messages do |options|
assert_named_route mark_path, :mark_message_path, mark_options
end
end
end
end
def test_with_two_member_actions_with_same_method
[:put, :post].each do |method|
with_restful_routing :messages, :member => { :mark => method, :unmark => method } do
%w(mark unmark).each do |action|
action_options = {:action => action, :id => '1'}
action_path = "/messages/1;#{action}"
assert_restful_routes_for :messages do |options|
assert_recognizes(options.merge(action_options), :path => action_path, :method => method)
end
assert_restful_named_routes_for :messages do |options|
assert_named_route action_path, "#{action}_message_path".to_sym, action_options
end
end
end
end
end
def test_with_new_action
with_restful_routing :messages, :new => { :preview => :post } do
preview_options = {:action => 'preview'}
preview_path = "/messages/new;preview"
assert_restful_routes_for :messages do |options|
assert_recognizes(options.merge(preview_options), :path => preview_path, :method => :post)
end
assert_restful_named_routes_for :messages do |options|
assert_named_route preview_path, :preview_new_message_path, preview_options
end
end
end
def test_override_new_method
with_restful_routing :messages do
assert_restful_routes_for :messages do |options|
assert_recognizes(options.merge(:action => "new"), :path => "/messages/new", :method => :get)
assert_raises(ActionController::RoutingError) do
ActionController::Routing::Routes.recognize_path("/messages/new", :method => :post)
end
end
end
with_restful_routing :messages, :new => { :new => :any } do
assert_restful_routes_for :messages do |options|
assert_recognizes(options.merge(:action => "new"), :path => "/messages/new", :method => :post)
assert_recognizes(options.merge(:action => "new"), :path => "/messages/new", :method => :get)
end
end
end
def test_nested_restful_routes
with_routing do |set|
set.draw do |map|
map.resources :threads do |map|
map.resources :messages do |map|
map.resources :comments
end
end
end
assert_simply_restful_for :threads
assert_simply_restful_for :messages,
:path_prefix => 'threads/1/',
:options => { :thread_id => '1' }
assert_simply_restful_for :comments,
:path_prefix => 'threads/1/messages/2/',
:options => { :thread_id => '1', :message_id => '2' }
end
end
def test_restful_routes_dont_generate_duplicates
with_restful_routing :messages do
routes = ActionController::Routing::Routes.routes
routes.each do |route|
routes.each do |r|
next if route === r # skip the comparison instance
assert distinct_routes?(route, r), "Duplicate Route: #{route}"
end
end
end
end
def test_should_create_singleton_resource_routes
with_singleton_resources :account do
assert_singleton_restful_for :account
end
end
def test_should_create_multiple_singleton_resource_routes
with_singleton_resources :account, :admin do
assert_singleton_restful_for :account
assert_singleton_restful_for :admin
end
end
def test_should_create_nested_singleton_resource_routes
with_routing do |set|
set.draw do |map|
map.resource :admin do |admin|
admin.resource :account
end
end
assert_singleton_restful_for :admin
assert_singleton_restful_for :account, :path_prefix => 'admin/'
end
end
def test_singleton_resource_with_member_action
[:put, :post].each do |method|
with_singleton_resources :account, :member => { :reset => method } do
reset_options = {:action => 'reset'}
reset_path = "/account;reset"
assert_singleton_routes_for :account do |options|
assert_recognizes(options.merge(reset_options), :path => reset_path, :method => method)
end
assert_singleton_named_routes_for :account do |options|
assert_named_route reset_path, :reset_account_path, reset_options
end
end
end
end
def test_singleton_resource_with_two_member_actions_with_same_method
[:put, :post].each do |method|
with_singleton_resources :account, :member => { :reset => method, :disable => method } do
%w(reset disable).each do |action|
action_options = {:action => action}
action_path = "/account;#{action}"
assert_singleton_routes_for :account do |options|
assert_recognizes(options.merge(action_options), :path => action_path, :method => method)
end
assert_singleton_named_routes_for :account do |options|
assert_named_route action_path, "#{action}_account_path".to_sym, action_options
end
end
end
end
end
def test_should_nest_resources_in_singleton_resource
with_routing do |set|
set.draw do |map|
map.resource :account do |account|
account.resources :messages
end
end
assert_singleton_restful_for :account
assert_simply_restful_for :messages, :path_prefix => 'account/'
end
end
def test_should_nest_resources_in_singleton_resource_with_path_prefix
with_routing do |set|
set.draw do |map|
map.resource(:account, :path_prefix => ':site_id') do |account|
account.resources :messages
end
end
assert_singleton_restful_for :account, :path_prefix => '7/', :options => { :site_id => '7' }
assert_simply_restful_for :messages, :path_prefix => '7/account/', :options => { :site_id => '7' }
end
end
def test_should_nest_singleton_resource_in_resources
with_routing do |set|
set.draw do |map|
map.resources :threads do |thread|
thread.resource :admin
end
end
assert_simply_restful_for :threads
assert_singleton_restful_for :admin, :path_prefix => 'threads/5/', :options => { :thread_id => '5' }
end
end
def test_should_not_allow_delete_or_put_on_collection_path
controller_name = :messages
with_restful_routing controller_name do
options = { :controller => controller_name.to_s }
collection_path = "/#{controller_name}"
assert_raises(ActionController::RoutingError) do
assert_recognizes(options.merge(:action => 'update'), :path => collection_path, :method => :put)
end
assert_raises(ActionController::RoutingError) do
assert_recognizes(options.merge(:action => 'destroy'), :path => collection_path, :method => :delete)
end
end
end
protected
def with_restful_routing(*args)
with_routing do |set|
set.draw { |map| map.resources(*args) }
yield
end
end
def with_singleton_resources(*args)
with_routing do |set|
set.draw { |map| map.resource(*args) }
yield
end
end
# runs assert_restful_routes_for and assert_restful_named_routes for on the controller_name and options, without passing a block.
def assert_simply_restful_for(controller_name, options = {})
assert_restful_routes_for controller_name, options
assert_restful_named_routes_for controller_name, options
end
def assert_singleton_restful_for(singleton_name, options = {})
assert_singleton_routes_for singleton_name, options
assert_singleton_named_routes_for singleton_name, options
end
def assert_restful_routes_for(controller_name, options = {})
(options[:options] ||= {})[:controller] = controller_name.to_s
collection_path = "/#{options[:path_prefix]}#{controller_name}"
member_path = "#{collection_path}/1"
new_path = "#{collection_path}/new"
edit_member_path = "#{member_path};edit"
formatted_edit_member_path = "#{member_path}.xml;edit"
with_options(options[:options]) do |controller|
controller.assert_routing collection_path, :action => 'index'
controller.assert_routing new_path, :action => 'new'
controller.assert_routing member_path, :action => 'show', :id => '1'
controller.assert_routing edit_member_path, :action => 'edit', :id => '1'
controller.assert_routing "#{collection_path}.xml", :action => 'index', :format => 'xml'
controller.assert_routing "#{new_path}.xml", :action => 'new', :format => 'xml'
controller.assert_routing "#{member_path}.xml", :action => 'show', :id => '1', :format => 'xml'
controller.assert_routing formatted_edit_member_path, :action => 'edit', :id => '1', :format => 'xml'
end
assert_recognizes(options[:options].merge(:action => 'index'), :path => collection_path, :method => :get)
assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get)
assert_recognizes(options[:options].merge(:action => 'create'), :path => collection_path, :method => :post)
assert_recognizes(options[:options].merge(:action => 'show', :id => '1'), :path => member_path, :method => :get)
assert_recognizes(options[:options].merge(:action => 'edit', :id => '1'), :path => edit_member_path, :method => :get)
assert_recognizes(options[:options].merge(:action => 'update', :id => '1'), :path => member_path, :method => :put)
assert_recognizes(options[:options].merge(:action => 'destroy', :id => '1'), :path => member_path, :method => :delete)
assert_recognizes(options[:options].merge(:action => 'index', :format => 'xml'), :path => "#{collection_path}.xml", :method => :get)
assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get)
assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{collection_path}.xml", :method => :post)
assert_recognizes(options[:options].merge(:action => 'show', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :get)
assert_recognizes(options[:options].merge(:action => 'edit', :id => '1', :format => 'xml'), :path => formatted_edit_member_path, :method => :get)
assert_recognizes(options[:options].merge(:action => 'update', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :put)
assert_recognizes(options[:options].merge(:action => 'destroy', :id => '1', :format => 'xml'), :path => "#{member_path}.xml", :method => :delete)
yield options[:options] if block_given?
end
# test named routes like foo_path and foos_path map to the correct options.
def assert_restful_named_routes_for(controller_name, singular_name = nil, options = {})
if singular_name.is_a?(Hash)
options = singular_name
singular_name = nil
end
singular_name ||= controller_name.to_s.singularize
(options[:options] ||= {})[:controller] = controller_name.to_s
@controller = "#{controller_name.to_s.camelize}Controller".constantize.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
get :index, options[:options]
options[:options].delete :action
full_prefix = "/#{options[:path_prefix]}#{controller_name}"
name_prefix = options[:name_prefix]
assert_named_route "#{full_prefix}", "#{name_prefix}#{controller_name}_path", options[:options]
assert_named_route "#{full_prefix}/new", "#{name_prefix}new_#{singular_name}_path", options[:options]
assert_named_route "#{full_prefix}/1", "#{name_prefix}#{singular_name}_path", options[:options].merge(:id => '1')
assert_named_route "#{full_prefix}/1;edit", "#{name_prefix}edit_#{singular_name}_path", options[:options].merge(:id => '1')
assert_named_route "#{full_prefix}.xml", "formatted_#{name_prefix}#{controller_name}_path", options[:options].merge( :format => 'xml')
assert_named_route "#{full_prefix}/new.xml", "formatted_#{name_prefix}new_#{singular_name}_path", options[:options].merge( :format => 'xml')
assert_named_route "#{full_prefix}/1.xml", "formatted_#{name_prefix}#{singular_name}_path", options[:options].merge(:id => '1', :format => 'xml')
assert_named_route "#{full_prefix}/1.xml;edit", "formatted_#{name_prefix}edit_#{singular_name}_path", options[:options].merge(:id => '1', :format => 'xml')
yield options[:options] if block_given?
end
def assert_singleton_routes_for(singleton_name, options = {})
(options[:options] ||= {})[:controller] ||= singleton_name.to_s
full_path = "/#{options[:path_prefix]}#{singleton_name}"
new_path = "#{full_path}/new"
edit_path = "#{full_path};edit"
formatted_edit_path = "#{full_path}.xml;edit"
with_options options[:options] do |controller|
controller.assert_routing full_path, :action => 'show'
controller.assert_routing new_path, :action => 'new'
controller.assert_routing edit_path, :action => 'edit'
controller.assert_routing "#{full_path}.xml", :action => 'show', :format => 'xml'
controller.assert_routing "#{new_path}.xml", :action => 'new', :format => 'xml'
controller.assert_routing formatted_edit_path, :action => 'edit', :format => 'xml'
end
assert_recognizes(options[:options].merge(:action => 'show'), :path => full_path, :method => :get)
assert_recognizes(options[:options].merge(:action => 'new'), :path => new_path, :method => :get)
assert_recognizes(options[:options].merge(:action => 'edit'), :path => edit_path, :method => :get)
assert_recognizes(options[:options].merge(:action => 'create'), :path => full_path, :method => :post)
assert_recognizes(options[:options].merge(:action => 'update'), :path => full_path, :method => :put)
assert_recognizes(options[:options].merge(:action => 'destroy'), :path => full_path, :method => :delete)
assert_recognizes(options[:options].merge(:action => 'show', :format => 'xml'), :path => "#{full_path}.xml", :method => :get)
assert_recognizes(options[:options].merge(:action => 'new', :format => 'xml'), :path => "#{new_path}.xml", :method => :get)
assert_recognizes(options[:options].merge(:action => 'edit', :format => 'xml'), :path => formatted_edit_path, :method => :get)
assert_recognizes(options[:options].merge(:action => 'create', :format => 'xml'), :path => "#{full_path}.xml", :method => :post)
assert_recognizes(options[:options].merge(:action => 'update', :format => 'xml'), :path => "#{full_path}.xml", :method => :put)
assert_recognizes(options[:options].merge(:action => 'destroy', :format => 'xml'), :path => "#{full_path}.xml", :method => :delete)
yield options[:options] if block_given?
end
def assert_singleton_named_routes_for(singleton_name, options = {})
(options[:options] ||= {})[:controller] ||= singleton_name.to_s
@controller = "#{options[:options][:controller].camelize}Controller".constantize.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
get :show, options[:options]
options[:options].delete :action
full_path = "/#{options[:path_prefix]}#{singleton_name}"
assert_named_route "#{full_path}", "#{singleton_name}_path", options[:options]
assert_named_route "#{full_path}/new", "new_#{singleton_name}_path", options[:options]
assert_named_route "#{full_path};edit", "edit_#{singleton_name}_path", options[:options]
assert_named_route "#{full_path}.xml", "formatted_#{singleton_name}_path", options[:options].merge(:format => 'xml')
assert_named_route "#{full_path}/new.xml", "formatted_new_#{singleton_name}_path", options[:options].merge(:format => 'xml')
assert_named_route "#{full_path}.xml;edit", "formatted_edit_#{singleton_name}_path", options[:options].merge(:format => 'xml')
end
def assert_named_route(expected, route, options)
actual = @controller.send(route, options) rescue $!.class.name
assert_equal expected, actual, "Error on route: #{route}(#{options.inspect})"
end
def assert_resource_methods(expected, resource, action_method, method)
assert_equal expected.length, resource.send("#{action_method}_methods")[method].size, "#{resource.send("#{action_method}_methods")[method].inspect}"
expected.each do |action|
assert resource.send("#{action_method}_methods")[method].include?(action),
"#{method} not in #{action_method} methods: #{resource.send("#{action_method}_methods")[method].inspect}"
end
end
def distinct_routes? (r1, r2)
if r1.conditions == r2.conditions and r1.requirements == r2.requirements then
if r1.segments.collect(&:to_s) == r2.segments.collect(&:to_s) then
return false
end
end
true
end
end

View file

@ -0,0 +1,628 @@
#--
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
# Under MIT and/or CC By license.
#++
require File.dirname(__FILE__) + '/../abstract_unit'
require File.dirname(__FILE__) + '/fake_controllers'
class SelectorTest < Test::Unit::TestCase
#
# Basic selector: element, id, class, attributes.
#
def test_element
parse(%Q{<div id="1"></div><p></p><div id="2"></div>})
# Match element by name.
select("div")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "2", @matches[1].attributes["id"]
# Not case sensitive.
select("DIV")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "2", @matches[1].attributes["id"]
# Universal match (all elements).
select("*")
assert_equal 3, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal nil, @matches[1].attributes["id"]
assert_equal "2", @matches[2].attributes["id"]
end
def test_identifier
parse(%Q{<div id="1"></div><p></p><div id="2"></div>})
# Match element by ID.
select("div#1")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
# Match element by ID, substitute value.
select("div#?", 2)
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
# Element name does not match ID.
select("p#?", 2)
assert_equal 0, @matches.size
# Use regular expression.
select("#?", /\d/)
assert_equal 2, @matches.size
end
def test_class_name
parse(%Q{<div id="1" class=" foo "></div><p id="2" class=" foo bar "></p><div id="3" class="bar"></div>})
# Match element with specified class.
select("div.foo")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
# Match any element with specified class.
select("*.foo")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "2", @matches[1].attributes["id"]
# Match elements with other class.
select("*.bar")
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
# Match only element with both class names.
select("*.bar.foo")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
end
def test_attribute
parse(%Q{<div id="1"></div><p id="2" title="" bar="foo"></p><div id="3" title="foo"></div>})
# Match element with attribute.
select("div[title]")
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
# Match any element with attribute.
select("*[title]")
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
# Match alement with attribute value.
select("*[title=foo]")
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
# Match alement with attribute and attribute value.
select("[bar=foo][title]")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
# Not case sensitive.
select("[BAR=foo][TiTle]")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
end
def test_attribute_quoted
parse(%Q{<div id="1" title="foo"></div><div id="2" title="bar"></div><div id="3" title=" bar "></div>})
# Match without quotes.
select("[title = bar]")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
# Match with single quotes.
select("[title = 'bar' ]")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
# Match with double quotes.
select("[title = \"bar\" ]")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
# Match with spaces.
select("[title = \" bar \" ]")
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
end
def test_attribute_equality
parse(%Q{<div id="1" title="foo bar"></div><div id="2" title="barbaz"></div>})
# Match (fail) complete value.
select("[title=bar]")
assert_equal 0, @matches.size
# Match space-separate word.
select("[title~=foo]")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
select("[title~=bar]")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
# Match beginning of value.
select("[title^=ba]")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
# Match end of value.
select("[title$=ar]")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
# Match text in value.
select("[title*=bar]")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "2", @matches[1].attributes["id"]
# Match first space separated word.
select("[title|=foo]")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
select("[title|=bar]")
assert_equal 0, @matches.size
end
#
# Selector composition: groups, sibling, children
#
def test_selector_group
parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>})
# Simple group selector.
select("h1,h3")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
select("h1 , h3")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
# Complex group selector.
parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>})
select("h1 a, h3 a")
assert_equal 2, @matches.size
assert_equal "foo", @matches[0].attributes["href"]
assert_equal "baz", @matches[1].attributes["href"]
# And now for the three selector challange.
parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>})
select("h1 a, h2 a, h3 a")
assert_equal 3, @matches.size
assert_equal "foo", @matches[0].attributes["href"]
assert_equal "bar", @matches[1].attributes["href"]
assert_equal "baz", @matches[2].attributes["href"]
end
def test_sibling_selector
parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>})
# Test next sibling.
select("h1+*")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
select("h1+h2")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
select("h1+h3")
assert_equal 0, @matches.size
select("*+h3")
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
# Test any sibling.
select("h1~*")
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
select("h2~*")
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
end
def test_children_selector
parse(%Q{<div><p id="1"><span id="2"></span></p></div><div><p id="3"><span id="4" class="foo"></span></p></div>})
# Test child selector.
select("div>p")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
select("div>span")
assert_equal 0, @matches.size
select("div>p#3")
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
select("div>p>span")
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "4", @matches[1].attributes["id"]
# Test descendant selector.
select("div p")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
select("div span")
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "4", @matches[1].attributes["id"]
select("div *#3")
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
select("div *#4")
assert_equal 1, @matches.size
assert_equal "4", @matches[0].attributes["id"]
# This is here because it failed before when whitespaces
# were not properly stripped.
select("div .foo")
assert_equal 1, @matches.size
assert_equal "4", @matches[0].attributes["id"]
end
#
# Pseudo selectors: root, nth-child, empty, content, etc
#
def test_root_selector
parse(%Q{<div id="1"><div id="2"></div></div>})
# Can only find element if it's root.
select(":root")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
select("#1:root")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
select("#2:root")
assert_equal 0, @matches.size
# Opposite for nth-child.
select("#1:nth-child(1)")
assert_equal 0, @matches.size
end
def test_nth_child_odd_even
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# Test odd nth children.
select("tr:nth-child(odd)")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
# Test even nth children.
select("tr:nth-child(even)")
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "4", @matches[1].attributes["id"]
end
def test_nth_child_a_is_zero
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# Test the third child.
select("tr:nth-child(0n+3)")
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
# Same but an can be omitted when zero.
select("tr:nth-child(3)")
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
# Second element (but not every second element).
select("tr:nth-child(0n+2)")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
# Before first and past last returns nothing.:
assert_raises(ArgumentError) { select("tr:nth-child(-1)") }
select("tr:nth-child(0)")
assert_equal 0, @matches.size
select("tr:nth-child(5)")
assert_equal 0, @matches.size
end
def test_nth_child_a_is_one
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# a is group of one, pick every element in group.
select("tr:nth-child(1n+0)")
assert_equal 4, @matches.size
# Same but a can be omitted when one.
select("tr:nth-child(n+0)")
assert_equal 4, @matches.size
# Same but b can be omitted when zero.
select("tr:nth-child(n)")
assert_equal 4, @matches.size
end
def test_nth_child_b_is_zero
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# If b is zero, pick the n-th element (here each one).
select("tr:nth-child(n+0)")
assert_equal 4, @matches.size
# If b is zero, pick the n-th element (here every second).
select("tr:nth-child(2n+0)")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
# If a and b are both zero, no element selected.
select("tr:nth-child(0n+0)")
assert_equal 0, @matches.size
select("tr:nth-child(0)")
assert_equal 0, @matches.size
end
def test_nth_child_a_is_negative
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# Since a is -1, picks the first three elements.
select("tr:nth-child(-n+3)")
assert_equal 3, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "2", @matches[1].attributes["id"]
assert_equal "3", @matches[2].attributes["id"]
# Since a is -2, picks the first in every second of first four elements.
select("tr:nth-child(-2n+3)")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
# Since a is -2, picks the first in every second of first three elements.
select("tr:nth-child(-2n+2)")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
end
def test_nth_child_b_is_negative
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# Select last of four.
select("tr:nth-child(4n-1)")
assert_equal 1, @matches.size
assert_equal "4", @matches[0].attributes["id"]
# Select first of four.
select("tr:nth-child(4n-4)")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
# Select last of every second.
select("tr:nth-child(2n-1)")
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "4", @matches[1].attributes["id"]
# Select nothing since an+b always < 0
select("tr:nth-child(-1n-1)")
assert_equal 0, @matches.size
end
def test_nth_child_substitution_values
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# Test with ?n?.
select("tr:nth-child(?n?)", 2, 1)
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "3", @matches[1].attributes["id"]
select("tr:nth-child(?n?)", 2, 2)
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "4", @matches[1].attributes["id"]
select("tr:nth-child(?n?)", 4, 2)
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
# Test with ? (b only).
select("tr:nth-child(?)", 3)
assert_equal 1, @matches.size
assert_equal "3", @matches[0].attributes["id"]
select("tr:nth-child(?)", 5)
assert_equal 0, @matches.size
end
def test_nth_last_child
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# Last two elements.
select("tr:nth-last-child(-n+2)")
assert_equal 2, @matches.size
assert_equal "3", @matches[0].attributes["id"]
assert_equal "4", @matches[1].attributes["id"]
# All old elements counting from last one.
select("tr:nth-last-child(odd)")
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "4", @matches[1].attributes["id"]
end
def test_nth_of_type
parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# First two elements.
select("tr:nth-of-type(-n+2)")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "2", @matches[1].attributes["id"]
# All old elements counting from last one.
select("tr:nth-last-of-type(odd)")
assert_equal 2, @matches.size
assert_equal "2", @matches[0].attributes["id"]
assert_equal "4", @matches[1].attributes["id"]
end
def test_first_and_last
parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
# First child.
select("tr:first-child")
assert_equal 0, @matches.size
select(":first-child")
assert_equal 1, @matches.size
assert_equal "thead", @matches[0].name
# First of type.
select("tr:first-of-type")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
select("thead:first-of-type")
assert_equal 1, @matches.size
assert_equal "thead", @matches[0].name
select("div:first-of-type")
assert_equal 0, @matches.size
# Last child.
select("tr:last-child")
assert_equal 1, @matches.size
assert_equal "4", @matches[0].attributes["id"]
# Last of type.
select("tr:last-of-type")
assert_equal 1, @matches.size
assert_equal "4", @matches[0].attributes["id"]
select("thead:last-of-type")
assert_equal 1, @matches.size
assert_equal "thead", @matches[0].name
select("div:last-of-type")
assert_equal 0, @matches.size
end
def test_first_and_last
# Only child.
parse(%Q{<table><tr></tr></table>})
select("table:only-child")
assert_equal 0, @matches.size
select("tr:only-child")
assert_equal 1, @matches.size
assert_equal "tr", @matches[0].name
parse(%Q{<table><tr></tr><tr></tr></table>})
select("tr:only-child")
assert_equal 0, @matches.size
# Only of type.
parse(%Q{<table><thead></thead><tr></tr><tr></tr></table>})
select("thead:only-of-type")
assert_equal 1, @matches.size
assert_equal "thead", @matches[0].name
select("td:only-of-type")
assert_equal 0, @matches.size
end
def test_empty
parse(%Q{<table><tr></tr></table>})
select("table:empty")
assert_equal 0, @matches.size
select("tr:empty")
assert_equal 1, @matches.size
parse(%Q{<div> </div>})
select("div:empty")
assert_equal 1, @matches.size
end
def test_content
parse(%Q{<div> </div>})
select("div:content()")
assert_equal 1, @matches.size
parse(%Q{<div>something </div>})
select("div:content()")
assert_equal 0, @matches.size
select("div:content(something)")
assert_equal 1, @matches.size
select("div:content( 'something' )")
assert_equal 1, @matches.size
select("div:content( \"something\" )")
assert_equal 1, @matches.size
select("div:content(?)", "something")
assert_equal 1, @matches.size
select("div:content(?)", /something/)
assert_equal 1, @matches.size
end
#
# Test negation.
#
def test_element_negation
parse(%Q{<p></p><div></div>})
select("*")
assert_equal 2, @matches.size
select("*:not(p)")
assert_equal 1, @matches.size
assert_equal "div", @matches[0].name
select("*:not(div)")
assert_equal 1, @matches.size
assert_equal "p", @matches[0].name
select("*:not(span)")
assert_equal 2, @matches.size
end
def test_id_negation
parse(%Q{<p id="1"></p><p id="2"></p>})
select("p")
assert_equal 2, @matches.size
select(":not(#1)")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
select(":not(#2)")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
end
def test_class_name_negation
parse(%Q{<p class="foo"></p><p class="bar"></p>})
select("p")
assert_equal 2, @matches.size
select(":not(.foo)")
assert_equal 1, @matches.size
assert_equal "bar", @matches[0].attributes["class"]
select(":not(.bar)")
assert_equal 1, @matches.size
assert_equal "foo", @matches[0].attributes["class"]
end
def test_attribute_negation
parse(%Q{<p title="foo"></p><p title="bar"></p>})
select("p")
assert_equal 2, @matches.size
select(":not([title=foo])")
assert_equal 1, @matches.size
assert_equal "bar", @matches[0].attributes["title"]
select(":not([title=bar])")
assert_equal 1, @matches.size
assert_equal "foo", @matches[0].attributes["title"]
end
def test_pseudo_class_negation
parse(%Q{<div><p id="1"></p><p id="2"></p></div>})
select("p")
assert_equal 2, @matches.size
select("p:not(:first-child)")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
select("p:not(:nth-child(2))")
assert_equal 1, @matches.size
assert_equal "1", @matches[0].attributes["id"]
end
def test_negation_details
parse(%Q{<p id="1"></p><p id="2"></p><p id="3"></p>})
assert_raises(ArgumentError) { select(":not(") }
assert_raises(ArgumentError) { select(":not(:not())") }
select("p:not(#1):not(#3)")
assert_equal 1, @matches.size
assert_equal "2", @matches[0].attributes["id"]
end
def test_select_from_element
parse(%Q{<div><p id="1"></p><p id="2"></p></div>})
select("div")
@matches = @matches[0].select("p")
assert_equal 2, @matches.size
assert_equal "1", @matches[0].attributes["id"]
assert_equal "2", @matches[1].attributes["id"]
end
protected
def parse(html)
@html = HTML::Document.new(html).root
end
def select(*selector)
@matches = HTML.selector(*selector).select(@html)
end
end

View file

@ -0,0 +1 @@
<hello>world</hello>

View file

@ -0,0 +1 @@
<%= 'hello world!' %>

View file

@ -0,0 +1 @@
page.alert 'hello world!'

View file

@ -0,0 +1 @@
xml.p "Hello world!"

View file

@ -0,0 +1 @@
<%= @cookies[:test] %>

View file

@ -0,0 +1 @@
<%= cookies[:test] %>

View file

@ -0,0 +1 @@
<%= @flash[:test] %>

View file

@ -0,0 +1 @@
<%= flash[:test] %>

View file

@ -0,0 +1 @@
<%= @headers[:test] %>

View file

@ -0,0 +1 @@
<%= headers[:test] %>

View file

@ -0,0 +1 @@
<%= @params[:test] %>

View file

@ -0,0 +1 @@
<%= params[:test] %>

View file

@ -0,0 +1 @@
<%= @request.method %>

View file

@ -0,0 +1 @@
<%= request.method %>

View file

@ -0,0 +1 @@
<%= @response.body %>

View file

@ -0,0 +1 @@
<%= response.body %>

View file

@ -0,0 +1 @@
<%= @session[:test] %>

View file

@ -0,0 +1 @@
<%= session[:test] %>

View file

@ -0,0 +1 @@
# Test file for javascript_include_tag

View file

@ -0,0 +1 @@
xm.hello

View file

@ -0,0 +1,3 @@
xml.test do
render :partial => 'hello', :locals => { :xm => xml }
end

View file

@ -0,0 +1,36 @@
require File.dirname(__FILE__) + '/../abstract_unit'
class DeprecatedHelperTest < Test::Unit::TestCase
include ActionView::Helpers::JavaScriptHelper
include ActionView::Helpers::CaptureHelper
def test_update_element_function
assert_deprecated 'update_element_function' do
assert_equal %($('myelement').innerHTML = 'blub';\n),
update_element_function('myelement', :content => 'blub')
assert_equal %($('myelement').innerHTML = 'blub';\n),
update_element_function('myelement', :action => :update, :content => 'blub')
assert_equal %($('myelement').innerHTML = '';\n),
update_element_function('myelement', :action => :empty)
assert_equal %(Element.remove('myelement');\n),
update_element_function('myelement', :action => :remove)
assert_equal %(new Insertion.Bottom('myelement','blub');\n),
update_element_function('myelement', :position => 'bottom', :content => 'blub')
assert_equal %(new Insertion.Bottom('myelement','blub');\n),
update_element_function('myelement', :action => :update, :position => :bottom, :content => 'blub')
_erbout = ""
assert_equal %($('myelement').innerHTML = 'test';\n),
update_element_function('myelement') { _erbout << "test" }
_erbout = ""
assert_equal %($('myelement').innerHTML = 'blockstuff';\n),
update_element_function('myelement', :content => 'paramstuff') { _erbout << "blockstuff" }
end
end
end

View file

@ -0,0 +1,43 @@
require File.dirname(__FILE__) + '/../abstract_unit'
class DeprecatedViewInstanceVariablesTest < Test::Unit::TestCase
class DeprecatedInstanceVariablesController < ActionController::Base
self.template_root = "#{File.dirname(__FILE__)}/../fixtures/"
def self.controller_path; 'deprecated_instance_variables' end
ActionController::Base::DEPRECATED_INSTANCE_VARIABLES.each do |var|
class_eval <<-end_eval
def old_#{var}_inline; render :inline => '<%= @#{var}.to_s %>' end
def new_#{var}_inline; render :inline => '<%= #{var}.to_s %>' end
def old_#{var}_partial; render :partial => '#{var}_ivar' end
def new_#{var}_partial; render :partial => '#{var}_method' end
end_eval
end
def rescue_action(e) raise e end
end
def setup
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@controller = DeprecatedInstanceVariablesController.new
end
ActionController::Base::DEPRECATED_INSTANCE_VARIABLES.each do |var|
class_eval <<-end_eval, __FILE__, __LINE__
def test_old_#{var}_is_deprecated
assert_deprecated('@#{var}') { get :old_#{var}_inline }
end
def test_new_#{var}_isnt_deprecated
assert_not_deprecated { get :new_#{var}_inline }
end
def test_old_#{var}_partial_is_deprecated
assert_deprecated('@#{var}') { get :old_#{var}_partial }
end
def test_new_#{var}_partial_isnt_deprecated
assert_not_deprecated { get :new_#{var}_partial }
end
end_eval
end
end

View file

@ -0,0 +1,75 @@
module ActiveRecord
module AttributeMethods #:nodoc:
DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
def self.included(base)
base.extend ClassMethods
base.attribute_method_suffix *DEFAULT_SUFFIXES
end
# Declare and check for suffixed attribute methods.
module ClassMethods
# Declare a method available for all attributes with the given suffix.
# Uses method_missing and respond_to? to rewrite the method
# #{attr}#{suffix}(*args, &block)
# to
# attribute#{suffix}(#{attr}, *args, &block)
#
# An attribute#{suffix} instance method must exist and accept at least
# the attr argument.
#
# For example:
# class Person < ActiveRecord::Base
# attribute_method_suffix '_changed?'
#
# private
# def attribute_changed?(attr)
# ...
# end
# end
#
# person = Person.find(1)
# person.name_changed? # => false
# person.name = 'Hubert'
# person.name_changed? # => true
def attribute_method_suffix(*suffixes)
attribute_method_suffixes.concat suffixes
rebuild_attribute_method_regexp
end
# Returns MatchData if method_name is an attribute method.
def match_attribute_method?(method_name)
rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp
@@attribute_method_regexp.match(method_name)
end
private
# Suffixes a, ?, c become regexp /(a|\?|c)$/
def rebuild_attribute_method_regexp
suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
@@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
end
# Default to =, ?, _before_type_cast
def attribute_method_suffixes
@@attribute_method_suffixes ||= []
end
end
private
# Handle *? for method_missing.
def attribute?(attribute_name)
query_attribute(attribute_name)
end
# Handle *= for method_missing.
def attribute=(attribute_name, value)
write_attribute(attribute_name, value)
end
# Handle *_before_type_cast for method_missing.
def attribute_before_type_cast(attribute_name)
read_attribute_before_type_cast(attribute_name)
end
end
end

View file

@ -0,0 +1,861 @@
# Requires FrontBase Ruby bindings (gem install ruby-frontbase)
require 'active_record/connection_adapters/abstract_adapter'
FB_TRACE = false
module ActiveRecord
class Base
class << self
# Establishes a connection to the database that's used by all Active Record objects.
def frontbase_connection(config) # :nodoc:
# FrontBase only supports one unnamed sequence per table
define_attr_method(:set_sequence_name, :sequence_name, &Proc.new {|*args| nil})
config = config.symbolize_keys
database = config[:database]
port = config[:port]
host = config[:host]
username = config[:username]
password = config[:password]
dbpassword = config[:dbpassword]
session_name = config[:session_name]
dbpassword = '' if dbpassword.nil?
# Turn off colorization since it makes tail/less output difficult
self.colorize_logging = false
require_library_or_gem 'frontbase' unless self.class.const_defined? :FBSQL_Connect
# Check bindings version
version = "0.0.0"
version = FBSQL_Connect::FB_BINDINGS_VERSION if defined? FBSQL_Connect::FB_BINDINGS_VERSION
if ActiveRecord::ConnectionAdapters::FrontBaseAdapter.compare_versions(version,"1.0.0") == -1
raise AdapterNotFound,
'The FrontBase adapter requires ruby-frontbase version 1.0.0 or greater; you appear ' <<
"to be running an older version (#{version}) -- please update ruby-frontbase (gem install ruby-frontbase)."
end
connection = FBSQL_Connect.connect(host, port, database, username, password, dbpassword, session_name)
ConnectionAdapters::FrontBaseAdapter.new(connection, logger, [host, port, database, username, password, dbpassword, session_name], config)
end
end
end
module ConnectionAdapters
# From EOF Documentation....
# buffer should have space for EOUniqueBinaryKeyLength (12) bytes.
# Assigns a world-wide unique ID made up of:
# < Sequence [2], ProcessID [2] , Time [4], IP Addr [4] >
class TwelveByteKey < String #:nodoc:
@@mutex = Mutex.new
@@sequence_number = rand(65536)
@@key_cached_pid_component = nil
@@key_cached_ip_component = nil
def initialize(string = nil)
# Generate a unique key
if string.nil?
new_key = replace('_' * 12)
new_key[0..1] = self.class.key_sequence_component
new_key[2..3] = self.class.key_pid_component
new_key[4..7] = self.class.key_time_component
new_key[8..11] = self.class.key_ip_component
new_key
else
if string.size == 24
string.gsub!(/[[:xdigit:]]{2}/) { |x| x.hex.chr }
end
raise "string is not 12 bytes long" unless string.size == 12
super(string)
end
end
def inspect
unpack("H*").first.upcase
end
alias_method :to_s, :inspect
private
class << self
def key_sequence_component
seq = nil
@@mutex.synchronize do
seq = @@sequence_number
@@sequence_number = (@@sequence_number + 1) % 65536
end
sequence_component = "__"
sequence_component[0] = seq >> 8
sequence_component[1] = seq
sequence_component
end
def key_pid_component
if @@key_cached_pid_component.nil?
@@mutex.synchronize do
pid = $$
pid_component = "__"
pid_component[0] = pid >> 8
pid_component[1] = pid
@@key_cached_pid_component = pid_component
end
end
@@key_cached_pid_component
end
def key_time_component
time = Time.new.to_i
time_component = "____"
time_component[0] = (time & 0xFF000000) >> 24
time_component[1] = (time & 0x00FF0000) >> 16
time_component[2] = (time & 0x0000FF00) >> 8
time_component[3] = (time & 0x000000FF)
time_component
end
def key_ip_component
if @@key_cached_ip_component.nil?
@@mutex.synchronize do
old_lookup_flag = BasicSocket.do_not_reverse_lookup
BasicSocket.do_not_reverse_lookup = true
udpsocket = UDPSocket.new
udpsocket.connect("17.112.152.32",1)
ip_string = udpsocket.addr[3]
BasicSocket.do_not_reverse_lookup = old_lookup_flag
packed = Socket.pack_sockaddr_in(0,ip_string)
addr_subset = packed[4..7]
ip = addr_subset[0] << 24 | addr_subset[1] << 16 | addr_subset[2] << 8 | addr_subset[3]
ip_component = "____"
ip_component[0] = (ip & 0xFF000000) >> 24
ip_component[1] = (ip & 0x00FF0000) >> 16
ip_component[2] = (ip & 0x0000FF00) >> 8
ip_component[3] = (ip & 0x000000FF)
@@key_cached_ip_component = ip_component
end
end
@@key_cached_ip_component
end
end
end
class FrontBaseColumn < Column #:nodoc:
attr_reader :fb_autogen
def initialize(base, name, type, typename, limit, precision, scale, default, nullable)
@base = base
@name = name
@type = simplified_type(type,typename,limit)
@limit = limit
@precision = precision
@scale = scale
@default = default
@null = nullable == "YES"
@text = [:string, :text].include? @type
@number = [:float, :integer, :decimal].include? @type
@fb_autogen = false
if @default
@default.gsub!(/^'(.*)'$/,'\1') if @text
@fb_autogen = @default.include?("SELECT UNIQUE FROM")
case @type
when :boolean
@default = @default == "TRUE"
when :binary
if @default != "X''"
buffer = ""
@default.scan(/../) { |h| buffer << h.hex.chr }
@default = buffer
else
@default = ""
end
else
@default = type_cast(@default)
end
end
end
# Casts value (which is a String) to an appropriate instance.
def type_cast(value)
if type == :twelvebytekey
ActiveRecord::ConnectionAdapters::TwelveByteKey.new(value)
else
super(value)
end
end
def type_cast_code(var_name)
if type == :twelvebytekey
"ActiveRecord::ConnectionAdapters::TwelveByteKey.new(#{var_name})"
else
super(var_name)
end
end
private
def simplified_type(field_type, type_name,limit)
ret_type = :string
puts "typecode: [#{field_type}] [#{type_name}]" if FB_TRACE
# 12 byte primary keys are a special case that Apple's EOF
# used heavily. Optimize for this case
if field_type == 11 && limit == 96
ret_type = :twelvebytekey # BIT(96)
else
ret_type = case field_type
when 1 then :boolean # BOOLEAN
when 2 then :integer # INTEGER
when 4 then :float # FLOAT
when 10 then :string # CHARACTER VARYING
when 11 then :bitfield # BIT
when 13 then :date # DATE
when 14 then :time # TIME
when 16 then :timestamp # TIMESTAMP
when 20 then :text # CLOB
when 21 then :binary # BLOB
when 22 then :integer # TINYINT
else
puts "ERROR: Unknown typecode: [#{field_type}] [#{type_name}]"
end
end
puts "ret_type: #{ret_type.inspect}" if FB_TRACE
ret_type
end
end
class FrontBaseAdapter < AbstractAdapter
class << self
def compare_versions(v1, v2)
v1_seg = v1.split(".")
v2_seg = v2.split(".")
0.upto([v1_seg.length,v2_seg.length].min) do |i|
step = (v1_seg[i].to_i <=> v2_seg[i].to_i)
return step unless step == 0
end
return v1_seg.length <=> v2_seg.length
end
end
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@connection_options, @config = connection_options, config
@transaction_mode = :pessimistic
# Start out in auto-commit mode
self.rollback_db_transaction
# threaded_connections_test.rb will fail unless we set the session
# to optimistic locking mode
# set_pessimistic_transactions
# execute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, LOCKING OPTIMISTIC"
end
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name #:nodoc:
'FrontBase'
end
# Does this adapter support migrations? Backend specific, as the
# abstract adapter always returns +false+.
def supports_migrations? #:nodoc:
true
end
def native_database_types #:nodoc:
{
:primary_key => "INTEGER DEFAULT UNIQUE PRIMARY KEY",
:string => { :name => "VARCHAR", :limit => 255 },
:text => { :name => "CLOB" },
:integer => { :name => "INTEGER" },
:float => { :name => "FLOAT" },
:decimal => { :name => "DECIMAL" },
:datetime => { :name => "TIMESTAMP" },
:timestamp => { :name => "TIMESTAMP" },
:time => { :name => "TIME" },
:date => { :name => "DATE" },
:binary => { :name => "BLOB" },
:boolean => { :name => "BOOLEAN" },
:twelvebytekey => { :name => "BYTE", :limit => 12}
}
end
# QUOTING ==================================================
# Quotes the column value to help prevent
# {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection].
def quote(value, column = nil)
return value.quoted_id if value.respond_to?(:quoted_id)
retvalue = "<INVALID>"
puts "quote(#{value.inspect}(#{value.class}),#{column.type.inspect})" if FB_TRACE
# If a column was passed in, use column type information
unless value.nil?
if column
retvalue = case column.type
when :string
if value.kind_of?(String)
"'#{quote_string(value.to_s)}'" # ' (for ruby-mode)
else
"'#{quote_string(value.to_yaml)}'"
end
when :integer
if value.kind_of?(TrueClass)
'1'
elsif value.kind_of?(FalseClass)
'0'
else
value.to_i.to_s
end
when :float
value.to_f.to_s
when :decimal
value.to_d.to_s("F")
when :datetime, :timestamp
"TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
when :time
"TIME '#{value.strftime("%H:%M:%S")}'"
when :date
"DATE '#{value.strftime("%Y-%m-%d")}'"
when :twelvebytekey
value = value.to_s.unpack("H*").first unless value.kind_of?(TwelveByteKey)
"X'#{value.to_s}'"
when :boolean
value = quoted_true if value.kind_of?(TrueClass)
value = quoted_false if value.kind_of?(FalseClass)
value
when :binary
blob_handle = @connection.create_blob(value.to_s)
puts "SQL -> Insert #{value.to_s.length} byte blob as #{retvalue}" if FB_TRACE
blob_handle.handle
when :text
if value.kind_of?(String)
clobdata = value.to_s # ' (for ruby-mode)
else
clobdata = value.to_yaml
end
clob_handle = @connection.create_clob(clobdata)
puts "SQL -> Insert #{value.to_s.length} byte clob as #{retvalue}" if FB_TRACE
clob_handle.handle
else
raise "*** UNKNOWN TYPE: #{column.type.inspect}"
end # case
# Since we don't have column type info, make a best guess based
# on the Ruby class of the value
else
retvalue = case value
when ActiveRecord::ConnectionAdapters::TwelveByteKey
s = value.unpack("H*").first
"X'#{s}'"
when String
if column && column.type == :binary
s = value.unpack("H*").first
"X'#{s}'"
elsif column && [:integer, :float, :decimal].include?(column.type)
value.to_s
else
"'#{quote_string(value)}'" # ' (for ruby-mode)
end
when NilClass
"NULL"
when TrueClass
(column && column.type == :integer ? '1' : quoted_true)
when FalseClass
(column && column.type == :integer ? '0' : quoted_false)
when Float, Fixnum, Bignum, BigDecimal
value.to_s
when Time, Date, DateTime
if column
case column.type
when :date
"DATE '#{value.strftime("%Y-%m-%d")}'"
when :time
"TIME '#{value.strftime("%H:%M:%S")}'"
when :timestamp
"TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
else
raise NotImplementedError, "Unknown column type!"
end # case
else # Column wasn't passed in, so try to guess the right type
if value.kind_of? Date
"DATE '#{value.strftime("%Y-%m-%d")}'"
else
if [:hour, :min, :sec].all? {|part| value.send(:part).zero? }
"TIME '#{value.strftime("%H:%M:%S")}'"
else
"TIMESTAMP '#{quoted_date(value)}'"
end
end
end #if column
else
"'#{quote_string(value.to_yaml)}'"
end #case
end
else
retvalue = "NULL"
end
retvalue
end # def
# Quotes a string, escaping any ' (single quote) characters.
def quote_string(s)
s.gsub(/'/, "''") # ' (for ruby-mode)
end
def quote_column_name(name) #:nodoc:
%( "#{name}" )
end
def quoted_true
"true"
end
def quoted_false
"false"
end
# CONNECTION MANAGEMENT ====================================
def active?
true if @connection.status == 1
rescue => e
false
end
def reconnect!
@connection.close rescue nil
@connection = FBSQL_Connect.connect(*@connection_options.first(7))
end
# Close this connection
def disconnect!
@connection.close rescue nil
@active = false
end
# DATABASE STATEMENTS ======================================
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select_all(sql, name = nil) #:nodoc:
fbsql = cleanup_fb_sql(sql)
return_value = []
fbresult = execute(sql, name)
puts "select_all SQL -> #{fbsql}" if FB_TRACE
columns = fbresult.columns
fbresult.each do |row|
puts "SQL <- #{row.inspect}" if FB_TRACE
hashed_row = {}
colnum = 0
row.each do |col|
hashed_row[columns[colnum]] = col
if col.kind_of?(FBSQL_LOB)
hashed_row[columns[colnum]] = col.read
end
colnum += 1
end
puts "raw row: #{hashed_row.inspect}" if FB_TRACE
return_value << hashed_row
end
return_value
end
def select_one(sql, name = nil) #:nodoc:
fbsql = cleanup_fb_sql(sql)
return_value = []
fbresult = execute(fbsql, name)
puts "SQL -> #{fbsql}" if FB_TRACE
columns = fbresult.columns
fbresult.each do |row|
puts "SQL <- #{row.inspect}" if FB_TRACE
hashed_row = {}
colnum = 0
row.each do |col|
hashed_row[columns[colnum]] = col
if col.kind_of?(FBSQL_LOB)
hashed_row[columns[colnum]] = col.read
end
colnum += 1
end
return_value << hashed_row
break
end
fbresult.clear
return_value.first
end
def query(sql, name = nil) #:nodoc:
fbsql = cleanup_fb_sql(sql)
puts "SQL(query) -> #{fbsql}" if FB_TRACE
log(fbsql, name) { @connection.query(fbsql) }
rescue => e
puts "FB Exception: #{e.inspect}" if FB_TRACE
raise e
end
def execute(sql, name = nil) #:nodoc:
fbsql = cleanup_fb_sql(sql)
puts "SQL(execute) -> #{fbsql}" if FB_TRACE
log(fbsql, name) { @connection.query(fbsql) }
rescue ActiveRecord::StatementInvalid => e
if e.message.scan(/Table name - \w* - exists/).empty?
puts "FB Exception: #{e.inspect}" if FB_TRACE
raise e
end
end
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
puts "SQL -> #{sql.inspect}" if FB_TRACE
execute(sql, name)
id_value || pk
end
# Executes the update statement and returns the number of rows affected.
def update(sql, name = nil) #:nodoc:
puts "SQL -> #{sql.inspect}" if FB_TRACE
execute(sql, name).num_rows
end
alias_method :delete, :update #:nodoc:
def set_pessimistic_transactions
if @transaction_mode == :optimistic
execute "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, LOCKING PESSIMISTIC, READ WRITE"
@transaction_mode = :pessimistic
end
end
def set_optimistic_transactions
if @transaction_mode == :pessimistic
execute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, LOCKING OPTIMISTIC"
@transaction_mode = :optimistic
end
end
def begin_db_transaction #:nodoc:
execute "SET COMMIT FALSE" rescue nil
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
ensure
execute "SET COMMIT TRUE"
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
ensure
execute "SET COMMIT TRUE"
end
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]
offset = options[:offset] || 0
# Here is the full syntax FrontBase supports:
# (from gclem@frontbase.com)
#
# TOP <limit - unsigned integer>
# TOP ( <offset expr>, <limit expr>)
# "TOP 0" is not allowed, so we have
# to use a cheap trick.
if limit.zero?
case sql
when /WHERE/i
sql.sub!(/WHERE/i, 'WHERE 0 = 1 AND ')
when /ORDER\s+BY/i
sql.sub!(/ORDER\s+BY/i, 'WHERE 0 = 1 ORDER BY')
else
sql << 'WHERE 0 = 1'
end
else
if offset.zero?
sql.replace sql.gsub("SELECT ","SELECT TOP #{limit} ")
else
sql.replace sql.gsub("SELECT ","SELECT TOP(#{offset},#{limit}) ")
end
end
end
end
def prefetch_primary_key?(table_name = nil)
true
end
# Returns the next sequence value from a sequence generator. Not generally
# called directly; used by ActiveRecord to get the next primary key value
# when inserting a new database record (see #prefetch_primary_key?).
def next_sequence_value(sequence_name)
unique = select_value("SELECT UNIQUE FROM #{sequence_name}","Next Sequence Value")
# The test cases cannot handle a zero primary key
unique.zero? ? select_value("SELECT UNIQUE FROM #{sequence_name}","Next Sequence Value") : unique
end
def default_sequence_name(table, column)
table
end
# Set the sequence to the max value of the table's column.
def reset_sequence!(table, column, sequence = nil)
klasses = classes_for_table_name(table)
klass = klasses.nil? ? nil : klasses.first
pk = klass.primary_key unless klass.nil?
if pk && klass.columns_hash[pk].type == :integer
execute("SET UNIQUE FOR #{klass.table_name}(#{pk})")
end
end
def classes_for_table_name(table)
ActiveRecord::Base.send(:subclasses).select {|klass| klass.table_name == table}
end
def reset_pk_sequence!(table, pk = nil, sequence = nil)
klasses = classes_for_table_name(table)
klass = klasses.nil? ? nil : klasses.first
pk = klass.primary_key unless klass.nil?
if pk && klass.columns_hash[pk].type == :integer
mpk = select_value("SELECT MAX(#{pk}) FROM #{table}")
execute("SET UNIQUE FOR #{klass.table_name}(#{pk})")
end
end
# SCHEMA STATEMENTS ========================================
def structure_dump #:nodoc:
select_all("SHOW TABLES").inject('') do |structure, table|
structure << select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] << ";\n\n"
end
end
def recreate_database(name) #:nodoc:
drop_database(name)
create_database(name)
end
def create_database(name) #:nodoc:
execute "CREATE DATABASE #{name}"
end
def drop_database(name) #:nodoc:
execute "DROP DATABASE #{name}"
end
def current_database
select_value('SELECT "CATALOG_NAME" FROM INFORMATION_SCHEMA.CATALOGS').downcase
end
def tables(name = nil) #:nodoc:
select_values(<<-SQL, nil)
SELECT "TABLE_NAME"
FROM INFORMATION_SCHEMA.TABLES AS T0,
INFORMATION_SCHEMA.SCHEMATA AS T1
WHERE T0.SCHEMA_PK = T1.SCHEMA_PK
AND "SCHEMA_NAME" = CURRENT_SCHEMA
SQL
end
def indexes(table_name, name = nil)#:nodoc:
indexes = []
current_index = nil
sql = <<-SQL
SELECT INDEX_NAME, T2.ORDINAL_POSITION, INDEX_COLUMN_COUNT, INDEX_TYPE,
"COLUMN_NAME", IS_NULLABLE
FROM INFORMATION_SCHEMA.TABLES AS T0,
INFORMATION_SCHEMA.INDEXES AS T1,
INFORMATION_SCHEMA.INDEX_COLUMN_USAGE AS T2,
INFORMATION_SCHEMA.COLUMNS AS T3
WHERE T0."TABLE_NAME" = '#{table_name}'
AND INDEX_TYPE <> 0
AND T0.TABLE_PK = T1.TABLE_PK
AND T0.TABLE_PK = T2.TABLE_PK
AND T0.TABLE_PK = T3.TABLE_PK
AND T1.INDEXES_PK = T2.INDEX_PK
AND T2.COLUMN_PK = T3.COLUMN_PK
ORDER BY INDEX_NAME, T2.ORDINAL_POSITION
SQL
columns = []
query(sql).each do |row|
index_name = row[0]
ord_position = row[1]
ndx_colcount = row[2]
index_type = row[3]
column_name = row[4]
is_unique = index_type == 1
columns << column_name
if ord_position == ndx_colcount
indexes << IndexDefinition.new(table_name, index_name, is_unique , columns)
columns = []
end
end
indexes
end
def columns(table_name, name = nil)#:nodoc:
sql = <<-SQL
SELECT "TABLE_NAME", "COLUMN_NAME", ORDINAL_POSITION, IS_NULLABLE, COLUMN_DEFAULT,
DATA_TYPE, DATA_TYPE_CODE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,
NUMERIC_PRECISION_RADIX, NUMERIC_SCALE, DATETIME_PRECISION, DATETIME_PRECISION_LEADING
FROM INFORMATION_SCHEMA.TABLES T0,
INFORMATION_SCHEMA.COLUMNS T1,
INFORMATION_SCHEMA.DATA_TYPE_DESCRIPTOR T3
WHERE "TABLE_NAME" = '#{table_name}'
AND T0.TABLE_PK = T1.TABLE_PK
AND T0.TABLE_PK = T3.TABLE_OR_DOMAIN_PK
AND T1.COLUMN_PK = T3.COLUMN_NAME_PK
ORDER BY T1.ORDINAL_POSITION
SQL
rawresults = query(sql,name)
columns = []
rawresults.each do |field|
args = [base = field[0],
name = field[1],
typecode = field[6],
typestring = field[5],
limit = field[7],
precision = field[8],
scale = field[9],
default = field[4],
nullable = field[3]]
columns << FrontBaseColumn.new(*args)
end
columns
end
def create_table(name, options = {})
table_definition = TableDefinition.new(self)
table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
yield table_definition
if options[:force]
drop_table(name) rescue nil
end
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
create_sql << "#{name} ("
create_sql << table_definition.to_sql
create_sql << ") #{options[:options]}"
begin_db_transaction
execute create_sql
commit_db_transaction
rescue ActiveRecord::StatementInvalid => e
raise e unless e.message.match(/Table name - \w* - exists/)
end
def rename_table(name, new_name)
columns = columns(name)
pkcol = columns.find {|c| c.fb_autogen}
execute "ALTER TABLE NAME #{name} TO #{new_name}"
if pkcol
change_column_default(new_name,pkcol.name,"UNIQUE")
begin_db_transaction
mpk = select_value("SELECT MAX(#{pkcol.name}) FROM #{new_name}")
mpk = 0 if mpk.nil?
execute "SET UNIQUE=#{mpk} FOR #{new_name}"
commit_db_transaction
end
end
# Drops a table from the database.
def drop_table(name, options = {})
execute "DROP TABLE #{name} RESTRICT"
rescue ActiveRecord::StatementInvalid => e
raise e unless e.message.match(/Referenced TABLE - \w* - does not exist/)
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
add_column_sql = "ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}"
options[:type] = type
add_column_options!(add_column_sql, options)
execute(add_column_sql)
end
def add_column_options!(sql, options) #:nodoc:
default_value = quote(options[:default], options[:column])
if options_include_default?(options)
if options[:type] == :boolean
default_value = options[:default] == 0 ? quoted_false : quoted_true
end
sql << " DEFAULT #{default_value}"
end
sql << " NOT NULL" if options[:null] == false
end
# Removes the column from the table definition.
# ===== Examples
# remove_column(:suppliers, :qualification)
def remove_column(table_name, column_name)
execute "ALTER TABLE #{table_name} DROP #{column_name} RESTRICT"
end
def remove_index(table_name, options = {}) #:nodoc:
if options[:unique]
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{quote_column_name(index_name(table_name, options))} RESTRICT"
else
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
end
end
def change_column_default(table_name, column_name, default) #:nodoc:
execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{default}" if default != "NULL"
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
change_column_sql = %( ALTER COLUMN "#{table_name}"."#{column_name}" TO #{type_to_sql(type, options[:limit])} )
execute(change_column_sql)
change_column_sql = %( ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" )
if options_include_default?(options)
default_value = quote(options[:default], options[:column])
if type == :boolean
default_value = options[:default] == 0 ? quoted_false : quoted_true
end
change_column_sql << " SET DEFAULT #{default_value}"
end
execute(change_column_sql)
# change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}"
# add_column_options!(change_column_sql, options)
# execute(change_column_sql)
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
execute %( ALTER COLUMN NAME "#{table_name}"."#{column_name}" TO "#{new_column_name}" )
end
private
# Clean up sql to make it something FrontBase can digest
def cleanup_fb_sql(sql) #:nodoc:
# Turn non-standard != into standard <>
cleansql = sql.gsub("!=", "<>")
# Strip blank lines and comments
cleansql.split("\n").reject { |line| line.match(/^(?:\s*|--.*)$/) } * "\n"
end
end
end
end

View file

@ -0,0 +1,106 @@
module ActiveRecord
module Locking
# Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
# record increments the lock_version column and the locking facilities ensure that records instantiated twice
# will let the last one saved raise a StaleObjectError if the first was also updated. Example:
#
# p1 = Person.find(1)
# p2 = Person.find(1)
#
# p1.first_name = "Michael"
# p1.save
#
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
# You must ensure that your database schema defaults the lock_version column to 0.
#
# This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
# To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
# This method uses the same syntax as <tt>set_table_name</tt>
module Optimistic
def self.included(base) #:nodoc:
super
base.extend ClassMethods
base.cattr_accessor :lock_optimistically, :instance_writer => false
base.lock_optimistically = true
base.alias_method_chain :update, :lock
base.alias_method_chain :attributes_from_column_definition, :lock
class << base
alias_method :locking_column=, :set_locking_column
end
end
def locking_enabled? #:nodoc:
lock_optimistically && respond_to?(self.class.locking_column)
end
def attributes_from_column_definition_with_lock
result = attributes_from_column_definition_without_lock
# If the locking column has no default value set,
# start the lock version at zero. Note we can't use
# locking_enabled? at this point as @attributes may
# not have been initialized yet
if lock_optimistically && result.include?(self.class.locking_column)
result[self.class.locking_column] ||= 0
end
return result
end
def update_with_lock #:nodoc:
return update_without_lock unless locking_enabled?
lock_col = self.class.locking_column
previous_value = send(lock_col)
send(lock_col + '=', previous_value + 1)
affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
UPDATE #{self.class.table_name}
SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))}
WHERE #{self.class.primary_key} = #{quote_value(id)}
AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}
end_sql
unless affected_rows == 1
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
end
return true
end
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
# Set the column to use for optimistic locking. Defaults to lock_version.
def set_locking_column(value = nil, &block)
define_attr_method :locking_column, value, &block
value
end
# The version column used for optimistic locking. Defaults to lock_version.
def locking_column
reset_locking_column
end
# Quote the column name used for optimistic locking.
def quoted_locking_column
connection.quote_column_name(locking_column)
end
# Reset the column used for optimistic locking back to the lock_version default.
def reset_locking_column
set_locking_column DEFAULT_LOCKING_COLUMN
end
end
end
end
end

View file

@ -0,0 +1,77 @@
# Copyright (c) 2006 Shugo Maeda <shugo@ruby-lang.org>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject
# to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module ActiveRecord
module Locking
# Locking::Pessimistic provides support for row-level locking using
# SELECT ... FOR UPDATE and other lock types.
#
# Pass :lock => true to ActiveRecord::Base.find to obtain an exclusive
# lock on the selected rows:
# # select * from accounts where id=1 for update
# Account.find(1, :lock => true)
#
# Pass :lock => 'some locking clause' to give a database-specific locking clause
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'.
#
# Example:
# Account.transaction do
# # select * from accounts where name = 'shugo' limit 1 for update
# shugo = Account.find(:first, :conditions => "name = 'shugo'", :lock => true)
# yuko = Account.find(:first, :conditions => "name = 'yuko'", :lock => true)
# shugo.balance -= 100
# shugo.save!
# yuko.balance += 100
# yuko.save!
# end
#
# You can also use ActiveRecord::Base#lock! method to lock one record by id.
# This may be better if you don't need to lock every row. Example:
# Account.transaction do
# # select * from accounts where ...
# accounts = Account.find(:all, :conditions => ...)
# account1 = accounts.detect { |account| ... }
# account2 = accounts.detect { |account| ... }
# # select * from accounts where id=? for update
# account1.lock!
# account2.lock!
# account1.balance -= 100
# account1.save!
# account2.balance += 100
# account2.save!
# end
#
# Database-specific information on row locking:
# MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
# PostgreSQL: http://www.postgresql.org/docs/8.1/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
module Pessimistic
# Obtain a row lock on this record. Reloads the record to obtain the requested
# lock. Pass an SQL locking clause to append the end of the SELECT statement
# or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
# the locked record.
def lock!(lock = true)
reload(:lock => lock) unless new_record?
self
end
end
end
end

View file

@ -0,0 +1,308 @@
module ActiveRecord #:nodoc:
module XmlSerialization
# Builds an XML document to represent the model. Some configuration is
# availble through +options+, however more complicated cases should use
# override ActiveRecord's to_xml.
#
# By default the generated XML document will include the processing
# instruction and all object's attributes. For example:
#
# <?xml version="1.0" encoding="UTF-8"?>
# <topic>
# <title>The First Topic</title>
# <author-name>David</author-name>
# <id type="integer">1</id>
# <approved type="boolean">false</approved>
# <replies-count type="integer">0</replies-count>
# <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
# <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
# <content>Have a nice day</content>
# <author-email-address>david@loudthinking.com</author-email-address>
# <parent-id></parent-id>
# <last-read type="date">2004-04-15</last-read>
# </topic>
#
# This behavior can be controlled with :only, :except,
# :skip_instruct, :skip_types and :dasherize. The :only and
# :except options are the same as for the #attributes method.
# The default is to dasherize all column names, to disable this,
# set :dasherize to false. To not have the column type included
# in the XML output, set :skip_types to false.
#
# For instance:
#
# topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
#
# <topic>
# <title>The First Topic</title>
# <author-name>David</author-name>
# <approved type="boolean">false</approved>
# <content>Have a nice day</content>
# <author-email-address>david@loudthinking.com</author-email-address>
# <parent-id></parent-id>
# <last-read type="date">2004-04-15</last-read>
# </topic>
#
# To include first level associations use :include
#
# firm.to_xml :include => [ :account, :clients ]
#
# <?xml version="1.0" encoding="UTF-8"?>
# <firm>
# <id type="integer">1</id>
# <rating type="integer">1</rating>
# <name>37signals</name>
# <clients>
# <client>
# <rating type="integer">1</rating>
# <name>Summit</name>
# </client>
# <client>
# <rating type="integer">1</rating>
# <name>Microsoft</name>
# </client>
# </clients>
# <account>
# <id type="integer">1</id>
# <credit-limit type="integer">50</credit-limit>
# </account>
# </firm>
#
# To include any methods on the object(s) being called use :methods
#
# firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
#
# <firm>
# # ... normal attributes as shown above ...
# <calculated-earnings>100000000000000000</calculated-earnings>
# <real-earnings>5</real-earnings>
# </firm>
#
# To call any Proc's on the object(s) use :procs. The Proc's
# are passed a modified version of the options hash that was
# given to #to_xml.
#
# proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
# firm.to_xml :procs => [ proc ]
#
# <firm>
# # ... normal attributes as shown above ...
# <abc>def</abc>
# </firm>
#
# You may override the to_xml method in your ActiveRecord::Base
# subclasses if you need to. The general form of doing this is
#
# class IHaveMyOwnXML < ActiveRecord::Base
# def to_xml(options = {})
# options[:indent] ||= 2
# xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
# xml.instruct! unless options[:skip_instruct]
# xml.level_one do
# xml.tag!(:second_level, 'content')
# end
# end
# end
def to_xml(options = {})
XmlSerializer.new(self, options).to_s
end
end
class XmlSerializer #:nodoc:
attr_reader :options
def initialize(record, options = {})
@record, @options = record, options.dup
end
def builder
@builder ||= begin
options[:indent] ||= 2
builder = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
unless options[:skip_instruct]
builder.instruct!
options[:skip_instruct] = true
end
builder
end
end
def root
root = (options[:root] || @record.class.to_s.underscore).to_s
dasherize? ? root.dasherize : root
end
def dasherize?
!options.has_key?(:dasherize) || options[:dasherize]
end
# To replicate the behavior in ActiveRecord#attributes,
# :except takes precedence over :only. If :only is not set
# for a N level model but is set for the N+1 level models,
# then because :except is set to a default value, the second
# level model can have both :except and :only set. So if
# :only is set, always delete :except.
def serializable_attributes
attribute_names = @record.attribute_names
if options[:only]
options.delete(:except)
attribute_names = attribute_names & Array(options[:only]).collect { |n| n.to_s }
else
options[:except] = Array(options[:except]) | Array(@record.class.inheritance_column)
attribute_names = attribute_names - options[:except].collect { |n| n.to_s }
end
attribute_names.collect { |name| Attribute.new(name, @record) }
end
def serializable_method_attributes
Array(options[:methods]).collect { |name| MethodAttribute.new(name.to_s, @record) }
end
def add_attributes
(serializable_attributes + serializable_method_attributes).each do |attribute|
add_tag(attribute)
end
end
def add_includes
if include_associations = options.delete(:include)
root_only_or_except = { :except => options[:except],
:only => options[:only] }
include_has_options = include_associations.is_a?(Hash)
for association in include_has_options ? include_associations.keys : Array(include_associations)
association_options = include_has_options ? include_associations[association] : root_only_or_except
opts = options.merge(association_options)
case @record.class.reflect_on_association(association).macro
when :has_many, :has_and_belongs_to_many
records = @record.send(association).to_a
unless records.empty?
tag = records.first.class.to_s.underscore.pluralize
tag = tag.dasherize if dasherize?
builder.tag!(tag) do
records.each { |r| r.to_xml(opts.merge(:root => association.to_s.singularize)) }
end
end
when :has_one, :belongs_to
if record = @record.send(association)
record.to_xml(opts.merge(:root => association))
end
end
end
options[:include] = include_associations
end
end
def add_procs
if procs = options.delete(:procs)
[ *procs ].each do |proc|
proc.call(options)
end
end
end
def add_tag(attribute)
builder.tag!(
dasherize? ? attribute.name.dasherize : attribute.name,
attribute.value.to_s,
attribute.decorations(!options[:skip_types])
)
end
def serialize
args = [root]
if options[:namespace]
args << {:xmlns=>options[:namespace]}
end
builder.tag!(*args) do
add_attributes
add_includes
add_procs
end
end
alias_method :to_s, :serialize
class Attribute #:nodoc:
attr_reader :name, :value, :type
def initialize(name, record)
@name, @record = name, record
@type = compute_type
@value = compute_value
end
# There is a significant speed improvement if the value
# does not need to be escaped, as #tag! escapes all values
# to ensure that valid XML is generated. For known binary
# values, it is at least an order of magnitude faster to
# Base64 encode binary values and directly put them in the
# output XML than to pass the original value or the Base64
# encoded value to the #tag! method. It definitely makes
# no sense to Base64 encode the value and then give it to
# #tag!, since that just adds additional overhead.
def needs_encoding?
![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
end
def decorations(include_types = true)
decorations = {}
if type == :binary
decorations[:encoding] = 'base64'
end
if include_types && type != :string
decorations[:type] = type
end
decorations
end
protected
def compute_type
type = @record.class.columns_hash[name].type
case type
when :text
:string
when :time
:datetime
else
type
end
end
def compute_value
value = @record.send(name)
if formatter = Hash::XML_FORMATTING[type.to_s]
value ? formatter.call(value) : nil
else
value
end
end
end
class MethodAttribute < Attribute #:nodoc:
protected
def compute_type
Hash::XML_TYPE_NAMES[@record.send(name).class.name] || :string
end
end
end
end

View file

@ -0,0 +1,31 @@
require 'abstract_unit'
class ActiveSchemaTest < Test::Unit::TestCase
def setup
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
alias_method :real_execute, :execute
def execute(sql, name = nil) return sql end
end
end
def teardown
ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:alias_method, :execute, :real_execute)
end
def test_drop_table
assert_equal "DROP TABLE people", drop_table(:people)
end
def test_add_column
assert_equal "ALTER TABLE people ADD `last_name` varchar(255)", add_column(:people, :last_name, :string)
end
def test_add_column_with_limit
assert_equal "ALTER TABLE people ADD `key` varchar(32)", add_column(:people, :key, :string, :limit => 32)
end
private
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
end

View file

@ -0,0 +1,81 @@
require 'abstract_unit'
require 'fixtures/default'
require 'fixtures/post'
require 'fixtures/task'
class SqlServerAdapterTest < Test::Unit::TestCase
fixtures :posts, :tasks
def setup
@connection = ActiveRecord::Base.connection
end
def teardown
@connection.execute("SET LANGUAGE us_english")
end
# SQL Server 2000 has a bug where some unambiguous date formats are not
# correctly identified if the session language is set to german
def test_date_insertion_when_language_is_german
@connection.execute("SET LANGUAGE deutsch")
assert_nothing_raised do
Task.create(:starting => Time.utc(2000, 1, 31, 5, 42, 0), :ending => Date.new(2006, 12, 31))
end
end
def test_execute_without_block_closes_statement
assert_all_statements_used_are_closed do
@connection.execute("SELECT 1")
end
end
def test_execute_with_block_closes_statement
assert_all_statements_used_are_closed do
@connection.execute("SELECT 1") do |sth|
assert !sth.finished?, "Statement should still be alive within block"
end
end
end
def test_insert_with_identity_closes_statement
assert_all_statements_used_are_closed do
@connection.insert("INSERT INTO accounts ([id], [firm_id],[credit_limit]) values (999, 1, 50)")
end
end
def test_insert_without_identity_closes_statement
assert_all_statements_used_are_closed do
@connection.insert("INSERT INTO accounts ([firm_id],[credit_limit]) values (1, 50)")
end
end
def test_active_closes_statement
assert_all_statements_used_are_closed do
@connection.active?
end
end
def assert_all_statements_used_are_closed(&block)
existing_handles = []
ObjectSpace.each_object(DBI::StatementHandle) {|handle| existing_handles << handle}
GC.disable
yield
used_handles = []
ObjectSpace.each_object(DBI::StatementHandle) {|handle| used_handles << handle unless existing_handles.include? handle}
assert_block "No statements were used within given block" do
used_handles.size > 0
end
ObjectSpace.each_object(DBI::StatementHandle) do |handle|
assert_block "Statement should have been closed within given block" do
handle.finished?
end
end
ensure
GC.enable
end
end

View file

@ -0,0 +1,126 @@
require 'abstract_unit'
require 'fixtures/post'
require 'fixtures/comment'
require 'fixtures/author'
require 'fixtures/category'
require 'fixtures/project'
require 'fixtures/developer'
class AssociationCallbacksTest < Test::Unit::TestCase
fixtures :posts, :authors, :projects, :developers
def setup
@david = authors(:david)
@thinking = posts(:thinking)
@authorless = posts(:authorless)
assert @david.post_log.empty?
end
def test_adding_macro_callbacks
@david.posts_with_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log
@david.posts_with_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}",
"after_adding#{@thinking.id}"], @david.post_log
end
def test_adding_with_proc_callbacks
@david.posts_with_proc_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log
@david.posts_with_proc_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}",
"after_adding#{@thinking.id}"], @david.post_log
end
def test_removing_with_macro_callbacks
first_post, second_post = @david.posts_with_callbacks[0, 2]
@david.posts_with_callbacks.delete(first_post)
assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log
@david.posts_with_callbacks.delete(second_post)
assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}",
"after_removing#{second_post.id}"], @david.post_log
end
def test_removing_with_proc_callbacks
first_post, second_post = @david.posts_with_callbacks[0, 2]
@david.posts_with_proc_callbacks.delete(first_post)
assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log
@david.posts_with_proc_callbacks.delete(second_post)
assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}",
"after_removing#{second_post.id}"], @david.post_log
end
def test_multiple_callbacks
@david.posts_with_multiple_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}",
"after_adding_proc#{@thinking.id}"], @david.post_log
@david.posts_with_multiple_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}",
"after_adding_proc#{@thinking.id}", "before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}",
"after_adding#{@thinking.id}", "after_adding_proc#{@thinking.id}"], @david.post_log
end
def test_has_and_belongs_to_many_add_callback
david = developers(:david)
ar = projects(:active_record)
assert ar.developers_log.empty?
ar.developers_with_callbacks << david
assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log
ar.developers_with_callbacks << david
assert_equal ["before_adding#{david.id}", "after_adding#{david.id}", "before_adding#{david.id}",
"after_adding#{david.id}"], ar.developers_log
end
def test_has_and_belongs_to_many_remove_callback
david = developers(:david)
jamis = developers(:jamis)
activerecord = projects(:active_record)
assert activerecord.developers_log.empty?
activerecord.developers_with_callbacks.delete(david)
assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log
activerecord.developers_with_callbacks.delete(jamis)
assert_equal ["before_removing#{david.id}", "after_removing#{david.id}", "before_removing#{jamis.id}",
"after_removing#{jamis.id}"], activerecord.developers_log
end
def test_has_and_belongs_to_many_remove_callback_on_clear
activerecord = projects(:active_record)
assert activerecord.developers_log.empty?
if activerecord.developers_with_callbacks.size == 0
activerecord.developers << developers(:david)
activerecord.developers << developers(:jamis)
activerecord.reload
assert activerecord.developers_with_callbacks.size == 2
end
log_array = activerecord.developers_with_callbacks.collect {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.flatten.sort
assert activerecord.developers_with_callbacks.clear
assert_equal log_array, activerecord.developers_log.sort
end
def test_dont_add_if_before_callback_raises_exception
assert !@david.unchangable_posts.include?(@authorless)
begin
@david.unchangable_posts << @authorless
rescue Exception => e
end
assert @david.post_log.empty?
assert !@david.unchangable_posts.include?(@authorless)
@david.reload
assert !@david.unchangable_posts.include?(@authorless)
end
def test_push_with_attributes
assert_deprecated 'push_with_attributes' do
david = developers(:david)
activerecord = projects(:active_record)
assert activerecord.developers_log.empty?
activerecord.developers_with_callbacks.push_with_attributes(david, {})
assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], activerecord.developers_log
activerecord.developers_with_callbacks.push_with_attributes(david, {})
assert_equal ["before_adding#{david.id}", "after_adding#{david.id}", "before_adding#{david.id}",
"after_adding#{david.id}"], activerecord.developers_log
end
end
end

View file

@ -0,0 +1,138 @@
require 'abstract_unit'
require 'active_record/acts/list'
require 'fixtures/post'
require 'fixtures/comment'
require 'fixtures/author'
require 'fixtures/category'
require 'fixtures/categorization'
require 'fixtures/mixin'
require 'fixtures/company'
require 'fixtures/topic'
require 'fixtures/reply'
class CascadedEagerLoadingTest < Test::Unit::TestCase
fixtures :authors, :mixins, :companies, :posts, :topics
def test_eager_association_loading_with_cascaded_two_levels
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 1, authors[1].posts.size
assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
end
def test_eager_association_loading_with_cascaded_two_levels_and_one_level
authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 1, authors[1].posts.size
assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
assert_equal 1, authors[0].categorizations.size
assert_equal 2, authors[1].categorizations.size
end
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 1, authors[1].posts.size
assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
end
def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference
authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 5, authors[0].posts.size
assert_equal authors(:david).name, authors[0].name
assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq
end
def test_eager_association_loading_with_cascaded_two_levels_with_condition
authors = Author.find(:all, :include=>{:posts=>:comments}, :conditions=>"authors.id=1", :order=>"authors.id")
assert_equal 1, authors.size
assert_equal 5, authors[0].posts.size
end
def test_eager_association_loading_with_acts_as_tree
roots = TreeMixin.find(:all, :include=>"children", :conditions=>"mixins.parent_id IS NULL", :order=>"mixins.id")
assert_equal [mixins(:tree_1), mixins(:tree2_1), mixins(:tree3_1)], roots
assert_no_queries do
assert_equal 2, roots[0].children.size
assert_equal 0, roots[1].children.size
assert_equal 0, roots[2].children.size
end
end
def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong
firms = Firm.find(:all, :include=>{:account=>{:firm=>:account}}, :order=>"companies.id")
assert_equal 2, firms.size
assert_equal firms.first.account, firms.first.account.firm.account
assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account }
assert_equal companies(:first_firm).account.firm.account, assert_no_queries { firms.first.account.firm.account }
end
def test_eager_association_loading_with_has_many_sti
topics = Topic.find(:all, :include => :replies, :order => 'topics.id')
assert_equal [topics(:first), topics(:second)], topics
assert_no_queries do
assert_equal 1, topics[0].replies.size
assert_equal 0, topics[1].replies.size
end
end
def test_eager_association_loading_with_belongs_to_sti
replies = Reply.find(:all, :include => :topic, :order => 'topics.id')
assert_equal [topics(:second)], replies
assert_equal topics(:first), assert_no_queries { replies.first.topic }
end
def test_eager_association_loading_with_multiple_stis_and_order
author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => 'authors.name, comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4')
assert_equal authors(:david), author
assert_no_queries do
author.posts.first.special_comments
author.posts.first.very_special_comment
end
end
def test_eager_association_loading_of_stis_with_multiple_references
authors = Author.find(:all, :include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4')
assert_equal [authors(:david)], authors
assert_no_queries do
authors.first.posts.first.special_comments.first.post.special_comments
authors.first.posts.first.special_comments.first.post.very_special_comment
end
end
def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
root_node = RecursivelyCascadedTreeMixin.find(:first, :include=>{:children=>{:children=>:children}}, :order => 'mixins.id')
assert_equal mixins(:recursively_cascaded_tree_4), assert_no_queries { root_node.children.first.children.first.children.first }
end
def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
root_node = RecursivelyCascadedTreeMixin.find(:first, :include=>{:first_child=>{:first_child=>:first_child}}, :order => 'mixins.id')
assert_equal mixins(:recursively_cascaded_tree_4), assert_no_queries { root_node.first_child.first_child.first_child }
end
def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include=>{:parent=>{:parent=>:parent}}, :order => 'mixins.id DESC')
assert_equal mixins(:recursively_cascaded_tree_1), assert_no_queries { leaf_node.parent.parent.parent }
end
end
require 'fixtures/vertex'
require 'fixtures/edge'
class CascadedEagerLoadingTest < Test::Unit::TestCase
fixtures :edges, :vertices
def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through
source = Vertex.find(:first, :include=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id')
assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first }
end
def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many
sink = Vertex.find(:first, :include=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC')
assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first }
end
end

View file

@ -0,0 +1,393 @@
require 'abstract_unit'
require 'fixtures/post'
require 'fixtures/comment'
require 'fixtures/author'
require 'fixtures/category'
require 'fixtures/company'
require 'fixtures/person'
require 'fixtures/reader'
class EagerAssociationTest < Test::Unit::TestCase
fixtures :posts, :comments, :authors, :categories, :categories_posts,
:companies, :accounts, :tags, :people, :readers
def test_loading_with_one_association
posts = Post.find(:all, :include => :comments)
post = posts.find { |p| p.id == 1 }
assert_equal 2, post.comments.size
assert post.comments.include?(comments(:greetings))
post = Post.find(:first, :include => :comments, :conditions => "posts.title = 'Welcome to the weblog'")
assert_equal 2, post.comments.size
assert post.comments.include?(comments(:greetings))
end
def test_loading_conditions_with_or
posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'")
assert_nil posts.detect { |p| p.author_id != authors(:david).id },
"expected to find only david's posts"
end
def test_with_ordering
list = Post.find(:all, :include => :comments, :order => "posts.id DESC")
[:eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments,
:authorless, :thinking, :welcome
].each_with_index do |post, index|
assert_equal posts(post), list[index]
end
end
def test_loading_with_multiple_associations
posts = Post.find(:all, :include => [ :comments, :author, :categories ], :order => "posts.id")
assert_equal 2, posts.first.comments.size
assert_equal 2, posts.first.categories.size
assert posts.first.comments.include?(comments(:greetings))
end
def test_loading_from_an_association
posts = authors(:david).posts.find(:all, :include => :comments, :order => "posts.id")
assert_equal 2, posts.first.comments.size
end
def test_loading_with_no_associations
assert_nil Post.find(posts(:authorless).id, :include => :author).author
end
def test_eager_association_loading_with_belongs_to
comments = Comment.find(:all, :include => :post)
assert_equal 10, comments.length
titles = comments.map { |c| c.post.title }
assert titles.include?(posts(:welcome).title)
assert titles.include?(posts(:sti_post_and_comments).title)
end
def test_eager_association_loading_with_belongs_to_and_limit
comments = Comment.find(:all, :include => :post, :limit => 5, :order => 'comments.id')
assert_equal 5, comments.length
assert_equal [1,2,3,5,6], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_conditions
comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :order => 'comments.id')
assert_equal 3, comments.length
assert_equal [5,6,7], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset
comments = Comment.find(:all, :include => :post, :limit => 3, :offset => 2, :order => 'comments.id')
assert_equal 3, comments.length
assert_equal [3,5,6], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions
comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id')
assert_equal 3, comments.length
assert_equal [6,7,8], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array
comments = Comment.find(:all, :include => :post, :conditions => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id')
assert_equal 3, comments.length
assert_equal [6,7,8], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations
posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :order => 'posts.id')
assert_equal 1, posts.length
assert_equal [1], posts.collect { |p| p.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations
posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id')
assert_equal 1, posts.length
assert_equal [2], posts.collect { |p| p.id }
end
def test_eager_with_has_many_through
posts_with_comments = people(:michael).posts.find(:all, :include => :comments)
posts_with_author = people(:michael).posts.find(:all, :include => :author )
posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ])
assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
end
def test_eager_with_has_many_through_an_sti_join_model
author = Author.find(:first, :include => :special_post_comments, :order => 'authors.id')
assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments }
end
def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both
author = Author.find(:first, :include => :special_nonexistant_post_comments, :order => 'authors.id')
assert_equal [], author.special_nonexistant_post_comments
end
def test_eager_with_has_many_through_join_model_with_conditions
assert_equal Author.find(:first, :include => :hello_post_comments,
:order => 'authors.id').hello_post_comments.sort_by(&:id),
Author.find(:first, :order => 'authors.id').hello_post_comments.sort_by(&:id)
end
def test_eager_with_has_many_and_limit
posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2)
assert_equal 2, posts.size
assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size }
end
def test_eager_with_has_many_and_limit_and_conditions
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.body = 'hello'", :order => "posts.id")
assert_equal 2, posts.size
assert_equal [4,5], posts.collect { |p| p.id }
end
def test_eager_with_has_many_and_limit_and_conditions_array
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "posts.body = ?", 'hello' ], :order => "posts.id")
assert_equal 2, posts.size
assert_equal [4,5], posts.collect { |p| p.id }
end
def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
assert_equal 2, posts.size
count = Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
assert_equal count, posts.size
end
def test_eager_with_has_many_and_limit_ond_high_offset
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
assert_equal 0, posts.size
end
def test_count_eager_with_has_many_and_limit_ond_high_offset
posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
assert_equal 0, posts
end
def test_eager_with_has_many_and_limit_with_no_results
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.title = 'magic forest'")
assert_equal 0, posts.size
end
def test_eager_with_has_and_belongs_to_many_and_limit
posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3)
assert_equal 3, posts.size
assert_equal 2, posts[0].categories.size
assert_equal 1, posts[1].categories.size
assert_equal 0, posts[2].categories.size
assert posts[0].categories.include?(categories(:technology))
assert posts[1].categories.include?(categories(:general))
end
def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers
posts = authors(:david).posts.find(:all,
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
:limit => 2
)
assert_equal 2, posts.size
count = Post.count(
:include => [ :comments, :author ],
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
:limit => 2
)
assert_equal count, posts.size
end
def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers
posts = nil
Post.with_scope(:find => {
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'"
}) do
posts = authors(:david).posts.find(:all, :limit => 2)
assert_equal 2, posts.size
end
Post.with_scope(:find => {
:include => [ :comments, :author ],
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')"
}) do
count = Post.count(:limit => 2)
assert_equal count, posts.size
end
end
def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the_eagers
Post.with_scope(:find => { :conditions => "1=1" }) do
posts = authors(:david).posts.find(:all,
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
:limit => 2
)
assert_equal 2, posts.size
count = Post.count(
:include => [ :comments, :author ],
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
:limit => 2
)
assert_equal count, posts.size
end
end
def test_eager_association_loading_with_habtm
posts = Post.find(:all, :include => :categories, :order => "posts.id")
assert_equal 2, posts[0].categories.size
assert_equal 1, posts[1].categories.size
assert_equal 0, posts[2].categories.size
assert posts[0].categories.include?(categories(:technology))
assert posts[1].categories.include?(categories(:general))
end
def test_eager_with_inheritance
posts = SpecialPost.find(:all, :include => [ :comments ])
end
def test_eager_has_one_with_association_inheritance
post = Post.find(4, :include => [ :very_special_comment ])
assert_equal "VerySpecialComment", post.very_special_comment.class.to_s
end
def test_eager_has_many_with_association_inheritance
post = Post.find(4, :include => [ :special_comments ])
post.special_comments.each do |special_comment|
assert_equal "SpecialComment", special_comment.class.to_s
end
end
def test_eager_habtm_with_association_inheritance
post = Post.find(6, :include => [ :special_categories ])
assert_equal 1, post.special_categories.size
post.special_categories.each do |special_category|
assert_equal "SpecialCategory", special_category.class.to_s
end
end
def test_eager_with_has_one_dependent_does_not_destroy_dependent
assert_not_nil companies(:first_firm).account
f = Firm.find(:first, :include => :account,
:conditions => ["companies.name = ?", "37signals"])
assert_not_nil f.account
assert_equal companies(:first_firm, :reload).account, f.account
end
def test_eager_with_invalid_association_reference
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=> :monkeys )
}
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ :monkeys ])
}
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ 'monkeys' ])
}
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
post = Post.find(6, :include=>[ :monkeys, :elephants ])
}
end
def find_all_ordered(className, include=nil)
className.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include)
end
def test_limited_eager_with_order
assert_equal [posts(:thinking), posts(:sti_comments)], Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title)', :limit => 2, :offset => 1)
assert_equal [posts(:sti_post_and_comments), posts(:sti_comments)], Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1)
end
def test_limited_eager_with_multiple_order_columns
assert_equal [posts(:thinking), posts(:sti_comments)], Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title), posts.id', :limit => 2, :offset => 1)
assert_equal [posts(:sti_post_and_comments), posts(:sti_comments)], Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC, posts.id', :limit => 2, :offset => 1)
end
def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm
# Eager includes of has many and habtm associations aren't necessarily sorted in the same way
def assert_equal_after_sort(item1, item2, item3 = nil)
assert_equal(item1.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id})
assert_equal(item3.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id}) if item3
end
# Test regular association, association with conditions, association with
# STI, and association with conditions assured not to be true
post_types = [:posts, :other_posts, :special_posts]
# test both has_many and has_and_belongs_to_many
[Author, Category].each do |className|
d1 = find_all_ordered(className)
# test including all post types at once
d2 = find_all_ordered(className, post_types)
d1.each_index do |i|
assert_equal(d1[i], d2[i])
assert_equal_after_sort(d1[i].posts, d2[i].posts)
post_types[1..-1].each do |post_type|
# test including post_types together
d3 = find_all_ordered(className, [:posts, post_type])
assert_equal(d1[i], d3[i])
assert_equal_after_sort(d1[i].posts, d3[i].posts)
assert_equal_after_sort(d1[i].send(post_type), d2[i].send(post_type), d3[i].send(post_type))
end
end
end
end
def test_eager_with_multiple_associations_with_same_table_has_one
d1 = find_all_ordered(Firm)
d2 = find_all_ordered(Firm, :account)
d1.each_index do |i|
assert_equal(d1[i], d2[i])
assert_equal(d1[i].account, d2[i].account)
end
end
def test_eager_with_multiple_associations_with_same_table_belongs_to
firm_types = [:firm, :firm_with_basic_id, :firm_with_other_name, :firm_with_condition]
d1 = find_all_ordered(Client)
d2 = find_all_ordered(Client, firm_types)
d1.each_index do |i|
assert_equal(d1[i], d2[i])
firm_types.each { |type| assert_equal(d1[i].send(type), d2[i].send(type)) }
end
end
def test_eager_with_valid_association_as_string_not_symbol
assert_nothing_raised { Post.find(:all, :include => 'comments') }
end
def test_preconfigured_includes_with_belongs_to
author = posts(:welcome).author_with_posts
assert_equal 5, author.posts.size
end
def test_preconfigured_includes_with_has_one
comment = posts(:sti_comments).very_special_comment_with_post
assert_equal posts(:sti_comments), comment.post
end
def test_preconfigured_includes_with_has_many
posts = authors(:david).posts_with_comments
one = posts.detect { |p| p.id == 1 }
assert_equal 5, posts.size
assert_equal 2, one.comments.size
end
def test_preconfigured_includes_with_habtm
posts = authors(:david).posts_with_categories
one = posts.detect { |p| p.id == 1 }
assert_equal 5, posts.size
assert_equal 2, one.categories.size
end
def test_preconfigured_includes_with_has_many_and_habtm
posts = authors(:david).posts_with_comments_and_categories
one = posts.detect { |p| p.id == 1 }
assert_equal 5, posts.size
assert_equal 2, one.comments.size
assert_equal 2, one.categories.size
end
def test_count_with_include
if current_adapter?(:SQLServerAdapter, :SybaseAdapter)
assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15")
else
assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15")
end
end
end

View file

@ -0,0 +1,42 @@
require 'abstract_unit'
require 'fixtures/post'
require 'fixtures/comment'
require 'fixtures/project'
require 'fixtures/developer'
class AssociationsExtensionsTest < Test::Unit::TestCase
fixtures :projects, :developers, :developers_projects, :comments, :posts
def test_extension_on_has_many
assert_equal comments(:more_greetings), posts(:welcome).comments.find_most_recent
end
def test_extension_on_habtm
assert_equal projects(:action_controller), developers(:david).projects.find_most_recent
end
def test_named_extension_on_habtm
assert_equal projects(:action_controller), developers(:david).projects_extended_by_name.find_most_recent
end
def test_named_two_extensions_on_habtm
assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_twice.find_most_recent
assert_equal projects(:active_record), developers(:david).projects_extended_by_name_twice.find_least_recent
end
def test_marshalling_extensions
david = developers(:david)
assert_equal projects(:action_controller), david.projects.find_most_recent
david = Marshal.load(Marshal.dump(david))
assert_equal projects(:action_controller), david.projects.find_most_recent
end
def test_marshalling_named_extensions
david = developers(:david)
assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
david = Marshal.load(Marshal.dump(david))
assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
end
end

View file

@ -0,0 +1,480 @@
require 'abstract_unit'
require 'fixtures/tag'
require 'fixtures/tagging'
require 'fixtures/post'
require 'fixtures/comment'
require 'fixtures/author'
require 'fixtures/category'
require 'fixtures/categorization'
class AssociationsJoinModelTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites
def test_has_many
assert authors(:david).categories.include?(categories(:general))
end
def test_has_many_inherited
assert authors(:mary).categories.include?(categories(:sti_test))
end
def test_inherited_has_many
assert categories(:sti_test).authors.include?(authors(:mary))
end
def test_has_many_uniq_through_join_model
assert_equal 2, authors(:mary).categorized_posts.size
assert_equal 1, authors(:mary).unique_categorized_posts.size
end
def test_polymorphic_has_many
assert posts(:welcome).taggings.include?(taggings(:welcome_general))
end
def test_polymorphic_has_one
assert_equal taggings(:welcome_general), posts(:welcome).tagging
end
def test_polymorphic_belongs_to
assert_equal posts(:welcome), posts(:welcome).taggings.first.taggable
end
def test_polymorphic_has_many_going_through_join_model
assert_equal tags(:general), tag = posts(:welcome).tags.first
assert_no_queries do
tag.tagging
end
end
def test_count_polymorphic_has_many
assert_equal 1, posts(:welcome).taggings.count
assert_equal 1, posts(:welcome).tags.count
end
def test_polymorphic_has_many_going_through_join_model_with_find
assert_equal tags(:general), tag = posts(:welcome).tags.find(:first)
assert_no_queries do
tag.tagging
end
end
def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection
assert_equal tags(:general), tag = posts(:welcome).funky_tags.first
assert_no_queries do
tag.tagging
end
end
def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection_with_find
assert_equal tags(:general), tag = posts(:welcome).funky_tags.find(:first)
assert_no_queries do
tag.tagging
end
end
def test_polymorphic_has_many_going_through_join_model_with_disabled_include
assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first
assert_queries 1 do
tag.tagging
end
end
def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins
assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first
tag.author_id
end
def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key
assert_equal tags(:misc), taggings(:welcome_general).super_tag
assert_equal tags(:misc), posts(:welcome).super_tags.first
end
def test_polymorphic_has_many_create_model_with_inheritance_and_custom_base_class
post = SubStiPost.create :title => 'SubStiPost', :body => 'SubStiPost body'
assert_instance_of SubStiPost, post
tagging = tags(:misc).taggings.create(:taggable => post)
assert_equal "SubStiPost", tagging.taggable_type
end
def test_polymorphic_has_many_going_through_join_model_with_inheritance
assert_equal tags(:general), posts(:thinking).tags.first
end
def test_polymorphic_has_many_going_through_join_model_with_inheritance_with_custom_class_name
assert_equal tags(:general), posts(:thinking).funky_tags.first
end
def test_polymorphic_has_many_create_model_with_inheritance
post = posts(:thinking)
assert_instance_of SpecialPost, post
tagging = tags(:misc).taggings.create(:taggable => post)
assert_equal "Post", tagging.taggable_type
end
def test_polymorphic_has_one_create_model_with_inheritance
tagging = tags(:misc).create_tagging(:taggable => posts(:thinking))
assert_equal "Post", tagging.taggable_type
end
def test_set_polymorphic_has_many
tagging = tags(:misc).taggings.create
posts(:thinking).taggings << tagging
assert_equal "Post", tagging.taggable_type
end
def test_set_polymorphic_has_one
tagging = tags(:misc).taggings.create
posts(:thinking).tagging = tagging
assert_equal "Post", tagging.taggable_type
end
def test_create_polymorphic_has_many_with_scope
old_count = posts(:welcome).taggings.count
tagging = posts(:welcome).taggings.create(:tag => tags(:misc))
assert_equal "Post", tagging.taggable_type
assert_equal old_count+1, posts(:welcome).taggings.count
end
def test_create_bang_polymorphic_with_has_many_scope
old_count = posts(:welcome).taggings.count
tagging = posts(:welcome).taggings.create!(:tag => tags(:misc))
assert_equal "Post", tagging.taggable_type
assert_equal old_count+1, posts(:welcome).taggings.count
end
def test_create_polymorphic_has_one_with_scope
old_count = Tagging.count
tagging = posts(:welcome).tagging.create(:tag => tags(:misc))
assert_equal "Post", tagging.taggable_type
assert_equal old_count+1, Tagging.count
end
def test_delete_polymorphic_has_many_with_delete_all
assert_equal 1, posts(:welcome).taggings.count
posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDeleteAll'
post = find_post_with_dependency(1, :has_many, :taggings, :delete_all)
old_count = Tagging.count
post.destroy
assert_equal old_count-1, Tagging.count
assert_equal 0, posts(:welcome).taggings.count
end
def test_delete_polymorphic_has_many_with_destroy
assert_equal 1, posts(:welcome).taggings.count
posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDestroy'
post = find_post_with_dependency(1, :has_many, :taggings, :destroy)
old_count = Tagging.count
post.destroy
assert_equal old_count-1, Tagging.count
assert_equal 0, posts(:welcome).taggings.count
end
def test_delete_polymorphic_has_many_with_nullify
assert_equal 1, posts(:welcome).taggings.count
posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyNullify'
post = find_post_with_dependency(1, :has_many, :taggings, :nullify)
old_count = Tagging.count
post.destroy
assert_equal old_count, Tagging.count
assert_equal 0, posts(:welcome).taggings.count
end
def test_delete_polymorphic_has_one_with_destroy
assert posts(:welcome).tagging
posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneDestroy'
post = find_post_with_dependency(1, :has_one, :tagging, :destroy)
old_count = Tagging.count
post.destroy
assert_equal old_count-1, Tagging.count
assert_nil posts(:welcome).tagging(true)
end
def test_delete_polymorphic_has_one_with_nullify
assert posts(:welcome).tagging
posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneNullify'
post = find_post_with_dependency(1, :has_one, :tagging, :nullify)
old_count = Tagging.count
post.destroy
assert_equal old_count, Tagging.count
assert_nil posts(:welcome).tagging(true)
end
def test_has_many_with_piggyback
assert_equal "2", categories(:sti_test).authors.first.post_id.to_s
end
def test_include_has_many_through
posts = Post.find(:all, :order => 'posts.id')
posts_with_authors = Post.find(:all, :include => :authors, :order => 'posts.id')
assert_equal posts.length, posts_with_authors.length
posts.length.times do |i|
assert_equal posts[i].authors.length, assert_no_queries { posts_with_authors[i].authors.length }
end
end
def test_include_polymorphic_has_one
post = Post.find_by_id(posts(:welcome).id, :include => :tagging)
tagging = taggings(:welcome_general)
assert_no_queries do
assert_equal tagging, post.tagging
end
end
def test_include_polymorphic_has_many_through
posts = Post.find(:all, :order => 'posts.id')
posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id')
assert_equal posts.length, posts_with_tags.length
posts.length.times do |i|
assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length }
end
end
def test_include_polymorphic_has_many
posts = Post.find(:all, :order => 'posts.id')
posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id')
assert_equal posts.length, posts_with_taggings.length
posts.length.times do |i|
assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length }
end
end
def test_has_many_find_all
assert_equal [categories(:general)], authors(:david).categories.find(:all)
end
def test_has_many_find_first
assert_equal categories(:general), authors(:david).categories.find(:first)
end
def test_has_many_with_hash_conditions
assert_equal categories(:general), authors(:david).categories_like_general.find(:first)
end
def test_has_many_find_conditions
assert_equal categories(:general), authors(:david).categories.find(:first, :conditions => "categories.name = 'General'")
assert_equal nil, authors(:david).categories.find(:first, :conditions => "categories.name = 'Technology'")
end
def test_has_many_class_methods_called_by_method_missing
assert_equal categories(:general), authors(:david).categories.find_all_by_name('General').first
# assert_equal nil, authors(:david).categories.find_by_name('Technology')
end
def test_has_many_going_through_join_model_with_custom_foreign_key
assert_equal [], posts(:thinking).authors
assert_equal [authors(:mary)], posts(:authorless).authors
end
def test_belongs_to_polymorphic_with_counter_cache
assert_equal 0, posts(:welcome)[:taggings_count]
tagging = posts(:welcome).taggings.create(:tag => tags(:general))
assert_equal 1, posts(:welcome, :reload)[:taggings_count]
tagging.destroy
assert posts(:welcome, :reload)[:taggings_count].zero?
end
def test_unavailable_through_reflection
assert_raises (ActiveRecord::HasManyThroughAssociationNotFoundError) { authors(:david).nothings }
end
def test_has_many_through_join_model_with_conditions
assert_equal [], posts(:welcome).invalid_taggings
assert_equal [], posts(:welcome).invalid_tags
end
def test_has_many_polymorphic
assert_raises ActiveRecord::HasManyThroughAssociationPolymorphicError do
assert_equal [posts(:welcome), posts(:thinking)], tags(:general).taggables
end
assert_raises ActiveRecord::EagerLoadPolymorphicError do
assert_equal [posts(:welcome), posts(:thinking)], tags(:general).taggings.find(:all, :include => :taggable)
end
end
def test_has_many_through_has_many_find_all
assert_equal comments(:greetings), authors(:david).comments.find(:all, :order => 'comments.id').first
end
def test_has_many_through_has_many_find_all_with_custom_class
assert_equal comments(:greetings), authors(:david).funky_comments.find(:all, :order => 'comments.id').first
end
def test_has_many_through_has_many_find_first
assert_equal comments(:greetings), authors(:david).comments.find(:first, :order => 'comments.id')
end
def test_has_many_through_has_many_find_conditions
options = { :conditions => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' }
assert_equal comments(:does_it_hurt), authors(:david).comments.find(:first, options)
end
def test_has_many_through_has_many_find_by_id
assert_equal comments(:more_greetings), authors(:david).comments.find(2)
end
def test_has_many_through_polymorphic_has_one
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tagging }
end
def test_has_many_through_polymorphic_has_many
assert_equal [taggings(:welcome_general), taggings(:thinking_general)], authors(:david).taggings.uniq.sort_by { |t| t.id }
end
def test_include_has_many_through_polymorphic_has_many
author = Author.find_by_id(authors(:david).id, :include => :taggings)
expected_taggings = [taggings(:welcome_general), taggings(:thinking_general)]
assert_no_queries do
assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
end
end
def test_has_many_through_has_many_through
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags }
end
def test_has_many_through_habtm
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories }
end
def test_eager_load_has_many_through_has_many
author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id'
SpecialComment.new; VerySpecialComment.new
assert_no_queries do
assert_equal [1,2,3,5,6,7,8,9,10], author.comments.collect(&:id)
end
end
def test_eager_load_has_many_through_has_many_with_conditions
post = Post.find(:first, :include => :invalid_tags)
assert_no_queries do
post.invalid_tags
end
end
def test_eager_belongs_to_and_has_one_not_singularized
assert_nothing_raised do
Author.find(:first, :include => :author_address)
AuthorAddress.find(:first, :include => :author)
end
end
def test_self_referential_has_many_through
assert_equal [authors(:mary)], authors(:david).favorite_authors
assert_equal [], authors(:mary).favorite_authors
end
def test_add_to_self_referential_has_many_through
new_author = Author.create(:name => "Bob")
authors(:david).author_favorites.create :favorite_author => new_author
assert_equal new_author, authors(:david).reload.favorite_authors.first
end
def test_has_many_through_uses_correct_attributes
assert_nil posts(:thinking).tags.find_by_name("General").attributes["tag_id"]
end
def test_raise_error_when_adding_new_record_to_has_many_through
assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags << tags(:general).clone }
assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).clone.tags << tags(:general) }
assert_raise(ActiveRecord::HasManyThroughCantAssociateNewRecords) { posts(:thinking).tags.build }
end
def test_create_associate_when_adding_to_has_many_through
count = posts(:thinking).tags.count
push = Tag.create!(:name => 'pushme')
post_thinking = posts(:thinking)
assert_nothing_raised { post_thinking.tags << push }
assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 1, post_thinking.tags.size)
assert_equal(count + 1, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.create!(:name => 'foo') }
assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 2, post_thinking.tags.size)
assert_equal(count + 2, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 4, post_thinking.tags.size)
assert_equal(count + 4, post_thinking.tags(true).size)
end
def test_adding_junk_to_has_many_through_should_raise_type_mismatch
assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags << "Uhh what now?" }
end
def test_adding_to_has_many_through_should_return_self
tags = posts(:thinking).tags
assert_equal tags, posts(:thinking).tags.push(tags(:general))
end
def test_delete_associate_when_deleting_from_has_many_through
count = posts(:thinking).tags.count
tags_before = posts(:thinking).tags
tag = Tag.create!(:name => 'doomed')
post_thinking = posts(:thinking)
post_thinking.tags << tag
assert_equal(count + 1, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.delete(tag) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
assert_equal(tags_before.sort, post_thinking.tags.sort)
end
def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
count = posts(:thinking).tags.count
tags_before = posts(:thinking).tags
doomed = Tag.create!(:name => 'doomed')
doomed2 = Tag.create!(:name => 'doomed2')
quaked = Tag.create!(:name => 'quaked')
post_thinking = posts(:thinking)
post_thinking.tags << doomed << doomed2
assert_equal(count + 2, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
assert_equal(tags_before.sort, post_thinking.tags.sort)
end
def test_deleting_junk_from_has_many_through_should_raise_type_mismatch
assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete("Uhh what now?") }
end
def test_has_many_through_sum_uses_calculations
assert_nothing_raised { authors(:david).comments.sum(:post_id) }
end
def test_has_many_through_has_many_with_sti
assert_equal [comments(:does_it_hurt)], authors(:david).special_post_comments
end
private
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}"
Post.find(post_id).update_attribute :type, class_name
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
klass.set_table_name 'posts'
klass.send(association, association_name, :as => :taggable, :dependent => dependency)
klass.find(post_id)
end
end

View file

@ -0,0 +1,49 @@
require 'abstract_unit'
class AttributeMethodsTest < Test::Unit::TestCase
def setup
@old_suffixes = ActiveRecord::Base.send(:attribute_method_suffixes).dup
@target = Class.new(ActiveRecord::Base)
@target.table_name = 'topics'
end
def teardown
ActiveRecord::Base.send(:attribute_method_suffixes).clear
ActiveRecord::Base.attribute_method_suffix *@old_suffixes
end
def test_match_attribute_method_query_returns_match_data
assert_not_nil md = @target.match_attribute_method?('title=')
assert_equal 'title', md.pre_match
assert_equal ['='], md.captures
%w(_hello_world ist! _maybe?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
assert_not_nil md = @target.match_attribute_method?("title#{suffix}")
assert_equal 'title', md.pre_match
assert_equal [suffix], md.captures
end
end
def test_declared_attribute_method_affects_respond_to_and_method_missing
topic = @target.new(:title => 'Budget')
assert topic.respond_to?('title')
assert_equal 'Budget', topic.title
assert !topic.respond_to?('title_hello_world')
assert_raise(NoMethodError) { topic.title_hello_world }
%w(_hello_world _it! _candidate= able?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
meth = "title#{suffix}"
assert topic.respond_to?(meth)
assert_equal ['title'], topic.send(meth)
assert_equal ['title', 'a'], topic.send(meth, 'a')
assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3)
end
end
end

View file

@ -0,0 +1,8 @@
require 'abstract_unit'
class ConnectionTest < Test::Unit::TestCase
def test_charset_properly_set
fb_conn = ActiveRecord::Base.connection.instance_variable_get(:@connection)
assert_equal 'UTF8', fb_conn.database.character_set
end
end

View file

@ -0,0 +1,27 @@
puts 'Using native Frontbase'
require_dependency 'fixtures/course'
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
ActiveRecord::Base.configurations = {
'arunit' => {
:adapter => 'frontbase',
:host => 'localhost',
:username => 'rails',
:password => '',
:database => 'activerecord_unittest',
:session_name => "unittest-#{$$}"
},
'arunit2' => {
:adapter => 'frontbase',
:host => 'localhost',
:username => 'rails',
:password => '',
:database => 'activerecord_unittest2',
:session_name => "unittest-#{$$}"
}
}
ActiveRecord::Base.establish_connection 'arunit'
Course.establish_connection 'arunit2'

View file

@ -0,0 +1,52 @@
require 'abstract_unit'
class PostgresqlDatatype < ActiveRecord::Base
end
class PGDataTypeTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
TABLE_NAME = 'postgresql_datatypes'
COLUMNS = [
'id SERIAL PRIMARY KEY',
'commission_by_quarter INTEGER[]',
'nicknames TEXT[]'
]
def setup
@connection = ActiveRecord::Base.connection
@connection.execute "CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
@connection.execute "INSERT INTO #{TABLE_NAME} (commission_by_quarter, nicknames) VALUES ( '{35000,21000,18000,17000}', '{foo,bar,baz}' )"
@first = PostgresqlDatatype.find( 1 )
end
def teardown
@connection.execute "DROP TABLE #{TABLE_NAME}"
end
def test_data_type_of_array_types
assert_equal :string, @first.column_for_attribute("commission_by_quarter").type
assert_equal :string, @first.column_for_attribute("nicknames").type
end
def test_array_values
assert_equal '{35000,21000,18000,17000}', @first.commission_by_quarter
assert_equal '{foo,bar,baz}', @first.nicknames
end
def test_update_integer_array
new_value = '{32800,95000,29350,17000}'
assert @first.commission_by_quarter = new_value
assert @first.save
assert @first.reload
assert_equal @first.commission_by_quarter, new_value
end
def test_update_text_array
new_value = '{robby,robert,rob,robbie}'
assert @first.nicknames = new_value
assert @first.save
assert @first.reload
assert_equal @first.nicknames, new_value
end
end

View file

@ -0,0 +1,25 @@
require 'abstract_unit'
require 'fixtures/topic'
require 'fixtures/task'
class EmptyDateTimeTest < Test::Unit::TestCase
def test_assign_empty_date_time
task = Task.new
task.starting = ''
task.ending = nil
assert_nil task.starting
assert_nil task.ending
end
def test_assign_empty_date
topic = Topic.new
topic.last_read = ''
assert_nil topic.last_read
end
def test_assign_empty_time
topic = Topic.new
topic.bonus_time = ''
assert_nil topic.bonus_time
end
end

View file

@ -0,0 +1,31 @@
DROP TABLE accounts CASCADE;
DROP TABLE funny_jokes CASCADE;
DROP TABLE companies CASCADE;
DROP TABLE topics CASCADE;
DROP TABLE developers CASCADE;
DROP TABLE projects CASCADE;
DROP TABLE developers_projects CASCADE;
DROP TABLE orders CASCADE;
DROP TABLE customers CASCADE;
DROP TABLE movies CASCADE;
DROP TABLE subscribers CASCADE;
DROP TABLE booleantests CASCADE;
DROP TABLE auto_id_tests CASCADE;
DROP TABLE entrants CASCADE;
DROP TABLE colnametests CASCADE;
DROP TABLE mixins CASCADE;
DROP TABLE people CASCADE;
DROP TABLE readers CASCADE;
DROP TABLE binaries CASCADE;
DROP TABLE computers CASCADE;
DROP TABLE posts CASCADE;
DROP TABLE comments CASCADE;
DROP TABLE authors CASCADE;
DROP TABLE tasks CASCADE;
DROP TABLE categories CASCADE;
DROP TABLE categories_posts CASCADE;
DROP TABLE fk_test_has_fk CASCADE;
DROP TABLE fk_test_has_pk CASCADE;
DROP TABLE keyboards CASCADE;
DROP TABLE legacy_things CASCADE;
DROP TABLE numeric_data CASCADE;

View file

@ -0,0 +1,262 @@
CREATE TABLE accounts (
id integer DEFAULT unique,
firm_id integer,
credit_limit integer,
PRIMARY KEY (id)
);
SET UNIQUE FOR accounts(id);
CREATE TABLE funny_jokes (
id integer DEFAULT unique,
firm_id integer default NULL,
name character varying(50),
PRIMARY KEY (id)
);
SET UNIQUE FOR funny_jokes(id);
CREATE TABLE companies (
id integer DEFAULT unique,
"type" character varying(50),
"ruby_type" character varying(50),
firm_id integer,
name character varying(50),
client_of integer,
rating integer default 1,
PRIMARY KEY (id)
);
SET UNIQUE FOR companies(id);
CREATE TABLE topics (
id integer DEFAULT unique,
title character varying(255),
author_name character varying(255),
author_email_address character varying(255),
written_on timestamp,
bonus_time time,
last_read date,
content varchar(65536),
approved boolean default true,
replies_count integer default 0,
parent_id integer,
"type" character varying(50),
PRIMARY KEY (id)
);
SET UNIQUE FOR topics(id);
CREATE TABLE developers (
id integer DEFAULT unique,
name character varying(100),
salary integer DEFAULT 70000,
created_at timestamp,
updated_at timestamp,
PRIMARY KEY (id)
);
SET UNIQUE FOR developers(id);
CREATE TABLE projects (
id integer DEFAULT unique,
name character varying(100),
type varchar(255),
PRIMARY KEY (id)
);
SET UNIQUE FOR projects(id);
CREATE TABLE developers_projects (
developer_id integer NOT NULL,
project_id integer NOT NULL,
joined_on date,
access_level integer default 1
);
CREATE TABLE orders (
id integer DEFAULT unique,
name character varying(100),
billing_customer_id integer,
shipping_customer_id integer,
PRIMARY KEY (id)
);
SET UNIQUE FOR orders(id);
CREATE TABLE customers (
id integer DEFAULT unique,
name character varying(100),
balance integer default 0,
address_street character varying(100),
address_city character varying(100),
address_country character varying(100),
gps_location character varying(100),
PRIMARY KEY (id)
);
SET UNIQUE FOR customers(id);
CREATE TABLE movies (
movieid integer DEFAULT unique,
name varchar(65536),
PRIMARY KEY (movieid)
);
SET UNIQUE FOR movies(movieid);
CREATE TABLE subscribers (
nick varchar(65536) NOT NULL,
name varchar(65536),
PRIMARY KEY (nick)
);
CREATE TABLE booleantests (
id integer DEFAULT unique,
value boolean,
PRIMARY KEY (id)
);
SET UNIQUE FOR booleantests(id);
CREATE TABLE auto_id_tests (
auto_id integer DEFAULT unique,
value integer,
PRIMARY KEY (auto_id)
);
SET UNIQUE FOR auto_id_tests(auto_id);
CREATE TABLE entrants (
id integer DEFAULT unique,
name varchar(65536),
course_id integer,
PRIMARY KEY (id)
);
SET UNIQUE FOR entrants(id);
CREATE TABLE colnametests (
id integer DEFAULT unique,
"references" integer NOT NULL,
PRIMARY KEY (id)
);
SET UNIQUE FOR colnametests(id);
CREATE TABLE mixins (
id integer DEFAULT unique,
parent_id integer,
type character varying(100),
pos integer,
lft integer,
rgt integer,
root_id integer,
created_at timestamp,
updated_at timestamp,
PRIMARY KEY (id)
);
SET UNIQUE FOR mixins(id);
CREATE TABLE people (
id integer DEFAULT unique,
first_name varchar(65536),
lock_version integer default 0,
PRIMARY KEY (id)
);
SET UNIQUE FOR people(id);
CREATE TABLE readers (
id integer DEFAULT unique,
post_id INTEGER NOT NULL,
person_id INTEGER NOT NULL,
PRIMARY KEY (id)
);
SET UNIQUE FOR readers(id);
CREATE TABLE binaries (
id integer DEFAULT unique,
data BLOB,
PRIMARY KEY (id)
);
SET UNIQUE FOR binaries(id);
CREATE TABLE computers (
id integer DEFAULT unique,
developer integer NOT NULL,
"extendedWarranty" integer NOT NULL,
PRIMARY KEY (id)
);
SET UNIQUE FOR computers(id);
CREATE TABLE posts (
id integer DEFAULT unique,
author_id integer,
title varchar(255),
type varchar(255),
body varchar(65536),
PRIMARY KEY (id)
);
SET UNIQUE FOR posts(id);
CREATE TABLE comments (
id integer DEFAULT unique,
post_id integer,
type varchar(255),
body varchar(65536),
PRIMARY KEY (id)
);
SET UNIQUE FOR comments(id);
CREATE TABLE authors (
id integer DEFAULT unique,
name varchar(255) default NULL,
PRIMARY KEY (id)
);
SET UNIQUE FOR authors(id);
CREATE TABLE tasks (
id integer DEFAULT unique,
starting timestamp,
ending timestamp,
PRIMARY KEY (id)
);
SET UNIQUE FOR tasks(id);
CREATE TABLE categories (
id integer DEFAULT unique,
name varchar(255),
type varchar(255),
PRIMARY KEY (id)
);
SET UNIQUE FOR categories(id);
CREATE TABLE categories_posts (
category_id integer NOT NULL,
post_id integer NOT NULL
);
CREATE TABLE fk_test_has_pk (
id INTEGER NOT NULL PRIMARY KEY
);
SET UNIQUE FOR fk_test_has_pk(id);
CREATE TABLE fk_test_has_fk (
id INTEGER NOT NULL PRIMARY KEY,
fk_id INTEGER NOT NULL REFERENCES fk_test_has_fk(id)
);
SET UNIQUE FOR fk_test_has_fk(id);
CREATE TABLE keyboards (
key_number integer DEFAULT unique,
"name" character varying(50),
PRIMARY KEY (key_number)
);
SET UNIQUE FOR keyboards(key_number);
create table "legacy_things"
(
"id" int,
"tps_report_number" int default NULL,
"version" int default 0 not null,
primary key ("id")
);
SET UNIQUE FOR legacy_things(id);
CREATE TABLE "numeric_data" (
"id" integer NOT NULL
"bank_balance" DECIMAL(10,2),
"big_bank_balance" DECIMAL(15,2),
"world_population" DECIMAL(10),
"my_house_population" DECIMAL(2),
"decimal_number_with_default" DECIMAL(3,2) DEFAULT 2.78,
primary key ("id")
);
SET UNIQUE FOR numeric_data(id);

View file

@ -0,0 +1 @@
DROP TABLE courses CASCADE;

View file

@ -0,0 +1,4 @@
CREATE TABLE courses (
id integer DEFAULT unique,
name varchar(100)
);

View file

@ -0,0 +1,5 @@
# This class models an edge in a directed graph.
class Edge < ActiveRecord::Base
belongs_to :source, :class_name => 'Vertex', :foreign_key => 'source_id'
belongs_to :sink, :class_name => 'Vertex', :foreign_key => 'sink_id'
end

View file

@ -0,0 +1,6 @@
<% (1..4).each do |id| %>
edge_<%= id %>:
id: <%= id %>
source_id: <%= id %>
sink_id: <%= id + 1 %>
<% end %>

View file

@ -0,0 +1,15 @@
class GiveMeBigNumbers < ActiveRecord::Migration
def self.up
create_table :big_numbers do |table|
table.column :bank_balance, :decimal, :precision => 10, :scale => 2
table.column :big_bank_balance, :decimal, :precision => 15, :scale => 2
table.column :world_population, :decimal, :precision => 10
table.column :my_house_population, :decimal, :precision => 2
table.column :value_of_e, :decimal
end
end
def self.down
drop_table :big_numbers
end
end

View file

@ -0,0 +1,9 @@
class PeopleHaveMiddleNames < ActiveRecord::Migration
def self.up
add_column "people", "middle_name", :string
end
def self.down
remove_column "people", "middle_name"
end
end

View file

@ -0,0 +1,9 @@
class PeopleHaveLastNames < ActiveRecord::Migration
def self.up
add_column "people", "last_name", :string
end
def self.down
remove_column "people", "last_name"
end
end

View file

@ -0,0 +1,12 @@
class WeNeedReminders < ActiveRecord::Migration
def self.up
create_table("reminders") do |t|
t.column :content, :text
t.column :remind_at, :datetime
end
end
def self.down
drop_table "reminders"
end
end

View file

@ -0,0 +1,12 @@
class InnocentJointable < ActiveRecord::Migration
def self.up
create_table("people_reminders", :id => false) do |t|
t.column :reminder_id, :integer
t.column :person_id, :integer
end
end
def self.down
drop_table "people_reminders"
end
end

View file

@ -0,0 +1,9 @@
# This class models a vertex in a directed graph.
class Vertex < ActiveRecord::Base
has_many :sink_edges, :class_name => 'Edge', :foreign_key => 'source_id'
has_many :sinks, :through => :sink_edges, :source => :sink
has_and_belongs_to_many :sources,
:class_name => 'Vertex', :join_table => 'edges',
:foreign_key => 'sink_id', :association_foreign_key => 'source_id'
end

View file

@ -0,0 +1,4 @@
<% (1..5).each do |id| %>
vertex_<%= id %>:
id: <%= id %>
<% end %>

View file

@ -0,0 +1,124 @@
require 'abstract_unit'
require 'fixtures/course'
class FirebirdMigrationTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
def setup
# using Course connection for tests -- need a db that doesn't already have a BOOLEAN domain
@connection = Course.connection
@fireruby_connection = @connection.instance_variable_get(:@connection)
end
def teardown
@connection.drop_table :foo rescue nil
@connection.execute("DROP DOMAIN D_BOOLEAN") rescue nil
end
def test_create_table_with_custom_sequence_name
assert_nothing_raised do
@connection.create_table(:foo, :sequence => 'foo_custom_seq') do |f|
f.column :bar, :string
end
end
assert !sequence_exists?('foo_seq')
assert sequence_exists?('foo_custom_seq')
assert_nothing_raised { @connection.drop_table(:foo, :sequence => 'foo_custom_seq') }
assert !sequence_exists?('foo_custom_seq')
ensure
FireRuby::Generator.new('foo_custom_seq', @fireruby_connection).drop rescue nil
end
def test_create_table_without_sequence
assert_nothing_raised do
@connection.create_table(:foo, :sequence => false) do |f|
f.column :bar, :string
end
end
assert !sequence_exists?('foo_seq')
assert_nothing_raised { @connection.drop_table :foo }
assert_nothing_raised do
@connection.create_table(:foo, :id => false) do |f|
f.column :bar, :string
end
end
assert !sequence_exists?('foo_seq')
assert_nothing_raised { @connection.drop_table :foo }
end
def test_create_table_with_boolean_column
assert !boolean_domain_exists?
assert_nothing_raised do
@connection.create_table :foo do |f|
f.column :bar, :string
f.column :baz, :boolean
end
end
assert boolean_domain_exists?
end
def test_add_boolean_column
assert !boolean_domain_exists?
@connection.create_table :foo do |f|
f.column :bar, :string
end
assert_nothing_raised { @connection.add_column :foo, :baz, :boolean }
assert boolean_domain_exists?
assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "baz" }.type
end
def test_change_column_to_boolean
assert !boolean_domain_exists?
# Manually create table with a SMALLINT column, which can be changed to a BOOLEAN
@connection.execute "CREATE TABLE foo (bar SMALLINT)"
assert_equal :integer, @connection.columns(:foo).find { |c| c.name == "bar" }.type
assert_nothing_raised { @connection.change_column :foo, :bar, :boolean }
assert boolean_domain_exists?
assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "bar" }.type
end
def test_rename_table_with_data_and_index
@connection.create_table :foo do |f|
f.column :baz, :string, :limit => 50
end
100.times { |i| @connection.execute "INSERT INTO foo VALUES (GEN_ID(foo_seq, 1), 'record #{i+1}')" }
@connection.add_index :foo, :baz
assert_nothing_raised { @connection.rename_table :foo, :bar }
assert !@connection.tables.include?("foo")
assert @connection.tables.include?("bar")
assert_equal "index_bar_on_baz", @connection.indexes("bar").first.name
assert_equal 100, FireRuby::Generator.new("bar_seq", @fireruby_connection).last
assert_equal 100, @connection.select_one("SELECT COUNT(*) FROM bar")["count"]
ensure
@connection.drop_table :bar rescue nil
end
def test_renaming_table_with_fk_constraint_raises_error
@connection.create_table :parent do |p|
p.column :name, :string
end
@connection.create_table :child do |c|
c.column :parent_id, :integer
end
@connection.execute "ALTER TABLE child ADD CONSTRAINT fk_child_parent FOREIGN KEY(parent_id) REFERENCES parent(id)"
assert_raise(ActiveRecord::ActiveRecordError) { @connection.rename_table :child, :descendant }
ensure
@connection.drop_table :child rescue nil
@connection.drop_table :descendant rescue nil
@connection.drop_table :parent rescue nil
end
private
def boolean_domain_exists?
!@connection.select_one("SELECT 1 FROM rdb$fields WHERE rdb$field_name = 'D_BOOLEAN'").nil?
end
def sequence_exists?(sequence_name)
FireRuby::Generator.exists?(sequence_name, @fireruby_connection)
end
end

View file

@ -0,0 +1,75 @@
require 'abstract_unit'
class SchemaThing < ActiveRecord::Base
end
class SchemaAuthorizationTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
TABLE_NAME = 'schema_things'
COLUMNS = [
'id serial primary key',
'name character varying(50)'
]
USERS = ['rails_pg_schema_user1', 'rails_pg_schema_user2']
def setup
@connection = ActiveRecord::Base.connection
@connection.execute "SET search_path TO '$user',public"
set_session_auth
USERS.each do |u|
@connection.execute "CREATE ROLE #{u}"
@connection.execute "CREATE SCHEMA AUTHORIZATION #{u}"
set_session_auth u
@connection.execute "CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
@connection.execute "INSERT INTO #{TABLE_NAME} (name) VALUES ('#{u}')"
set_session_auth
end
end
def teardown
set_session_auth
@connection.execute "RESET search_path"
USERS.each do |u|
@connection.execute "DROP SCHEMA #{u} CASCADE"
@connection.execute "DROP ROLE #{u}"
end
end
def test_schema_invisible
assert_raise(ActiveRecord::StatementInvalid) do
set_session_auth
@connection.execute "SELECT * FROM #{TABLE_NAME}"
end
end
def test_schema_uniqueness
assert_nothing_raised do
set_session_auth
USERS.each do |u|
set_session_auth u
assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1")
set_session_auth
end
end
end
def test_sequence_schema_caching
assert_nothing_raised do
USERS.each do |u|
set_session_auth u
st = SchemaThing.new :name => 'TEST1'
st.save!
st = SchemaThing.new :id => 5, :name => 'TEST2'
st.save!
set_session_auth
end
end
end
private
def set_session_auth auth = nil
@connection.execute "SET SESSION AUTHORIZATION #{auth || 'default'}"
end
end

View file

@ -0,0 +1,23 @@
require 'abstract_unit'
require "#{File.dirname(__FILE__)}/../lib/active_record/schema"
if ActiveRecord::Base.connection.supports_migrations?
class Order < ActiveRecord::Base
self.table_name = '[order]'
end
class TableNameTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
# Ensures Model.columns works when using SQLServer escape characters.
# Enables legacy schemas using SQL reserved words as table names.
# Should work with table names with spaces as well ('table name').
def test_escaped_table_name
assert_nothing_raised do
ActiveRecord::Base.connection.select_all 'SELECT * FROM [order]'
end
assert_equal '[order]', Order.table_name
assert_equal 5, Order.columns.length
end
end
end

View file

@ -0,0 +1,125 @@
require 'abstract_unit'
require 'fixtures/post'
require 'fixtures/author'
class Contact < ActiveRecord::Base
# mock out self.columns so no pesky db is needed for these tests
def self.columns() @columns ||= []; end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
column :name, :string
column :age, :integer
column :avatar, :binary
column :created_at, :datetime
column :awesome, :boolean
end
class XmlSerializationTest < Test::Unit::TestCase
def test_should_serialize_default_root
@xml = Contact.new.to_xml
assert_match %r{^<contact>}, @xml
assert_match %r{</contact>$}, @xml
end
def test_should_serialize_default_root_with_namespace
@xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact"
assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, @xml
assert_match %r{</contact>$}, @xml
end
def test_should_serialize_custom_root
@xml = Contact.new.to_xml :root => 'xml_contact'
assert_match %r{^<xml-contact>}, @xml
assert_match %r{</xml-contact>$}, @xml
end
def test_should_allow_undasherized_tags
@xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false
assert_match %r{^<xml_contact>}, @xml
assert_match %r{</xml_contact>$}, @xml
assert_match %r{<created_at}, @xml
end
def test_should_allow_attribute_filtering
@xml = Contact.new.to_xml :only => [:age, :name]
assert_match %r{<name}, @xml
assert_match %r{<age}, @xml
assert_no_match %r{<created-at}, @xml
@xml = Contact.new.to_xml :except => [:age, :name]
assert_no_match %r{<name}, @xml
assert_no_match %r{<age}, @xml
assert_match %r{<created-at}, @xml
end
end
class DefaultXmlSerializationTest < Test::Unit::TestCase
def setup
@xml = Contact.new(:name => 'aaron stack', :age => 25, :avatar => 'binarydata', :created_at => Time.utc(2006, 8, 1), :awesome => false).to_xml
end
def test_should_serialize_string
assert_match %r{<name>aaron stack</name>}, @xml
end
def test_should_serialize_integer
assert_match %r{<age type="integer">25</age>}, @xml
end
def test_should_serialize_binary
assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, @xml
assert_match %r{<avatar(.*)(type="binary")}, @xml
assert_match %r{<avatar(.*)(encoding="base64")}, @xml
end
def test_should_serialize_datetime
assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @xml
end
def test_should_serialize_boolean
assert_match %r{<awesome type=\"boolean\">false</awesome>}, @xml
end
end
class NilXmlSerializationTest < Test::Unit::TestCase
def setup
@xml = Contact.new.to_xml(:root => 'xml_contact')
end
def test_should_serialize_string
assert_match %r{<name></name>}, @xml
end
def test_should_serialize_integer
assert_match %r{<age type="integer"></age>}, @xml
end
def test_should_serialize_binary
assert_match %r{></avatar>}, @xml
assert_match %r{<avatar(.*)(type="binary")}, @xml
assert_match %r{<avatar(.*)(encoding="base64")}, @xml
end
def test_should_serialize_datetime
assert_match %r{<created-at type=\"datetime\"></created-at>}, @xml
end
def test_should_serialize_boolean
assert_match %r{<awesome type=\"boolean\"></awesome>}, @xml
end
end
class DatabaseConnectedXmlSerializationTest < Test::Unit::TestCase
fixtures :authors, :posts
# to_xml used to mess with the hash the user provided which
# caused the builder to be reused
def test_passing_hash_shouldnt_reuse_builder
options = {:include=>:posts}
david = authors(:david)
first_xml_size = david.to_xml(options).size
second_xml_size = david.to_xml(options).size
assert_equal first_xml_size, second_xml_size
end
end

20
vendor/rails/activesupport/MIT-LICENSE vendored Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2005-2006 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,55 @@
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module Array #:nodoc:
module Grouping
# Iterate over an array in groups of a certain size, padding any remaining
# slots with specified value (<tt>nil</tt> by default) unless it is
# <tt>false</tt>.
#
# E.g.
#
# %w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g}
# ["1", "2", "3"]
# ["4", "5", "6"]
# ["7", nil, nil]
#
# %w(1 2 3).in_groups_of(2, '&nbsp;') {|g| p g}
# ["1", "2"]
# ["3", "&nbsp;"]
#
# %w(1 2 3).in_groups_of(2, false) {|g| p g}
# ["1", "2"]
# ["3"]
def in_groups_of(number, fill_with = nil, &block)
require 'enumerator'
collection = dup
collection << fill_with until collection.size.modulo(number).zero? unless fill_with == false
grouped_collection = [] unless block_given?
collection.each_slice(number) do |group|
block_given? ? yield(group) : grouped_collection << group
end
grouped_collection unless block_given?
end
# Divide the array into one or more subarrays based on a delimiting +value+
# or the result of an optional block.
#
# ex.
#
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
def split(value = nil, &block)
block ||= Proc.new { |e| e == value }
inject([[]]) do |results, element|
if block.call(element)
results << []
else
results.last << element
end
results
end
end
end
end
end
end

View file

@ -0,0 +1,3 @@
require 'bigdecimal'
require File.dirname(__FILE__) + '/bigdecimal/formatting.rb'

View file

@ -0,0 +1,7 @@
class BigDecimal #:nodoc:
alias :_original_to_s :to_s
def to_s(format="F")
_original_to_s(format)
end
end

View file

@ -0,0 +1,58 @@
class Module
# Encapsulates the common pattern of:
#
# alias_method :foo_without_feature, :foo
# alias_method :foo, :foo_with_feature
#
# With this, you simply do:
#
# alias_method_chain :foo, :feature
#
# And both aliases are set up for you.
#
# Query and bang methods (foo?, foo!) keep the same punctuation:
#
# alias_method_chain :foo?, :feature
#
# is equivalent to
#
# alias_method :foo_without_feature?, :foo?
# alias_method :foo?, :foo_with_feature?
#
# so you can safely chain foo, foo?, and foo! with the same feature.
def alias_method_chain(target, feature)
# Strip out punctuation on predicates or bang methods since
# e.g. target?_without_feature is not a valid method name.
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
yield(aliased_target, punctuation) if block_given?
alias_method "#{aliased_target}_without_#{feature}#{punctuation}", target
alias_method target, "#{aliased_target}_with_#{feature}#{punctuation}"
end
# Allows you to make aliases for attributes, which includes
# getter, setter, and query methods.
#
# Example:
#
# class Content < ActiveRecord::Base
# # has a title attribute
# end
#
# class Email < ActiveRecord::Base
# alias_attribute :subject, :title
# end
#
# e = Email.find(1)
# e.title # => "Superstars"
# e.subject # => "Superstars"
# e.subject? # => true
# e.subject = "Megastars"
# e.title # => "Megastars"
def alias_attribute(new_name, old_name)
module_eval <<-STR, __FILE__, __LINE__+1
def #{new_name}; #{old_name}; end
def #{new_name}?; #{old_name}?; end
def #{new_name}=(v); self.#{old_name} = v; end
STR
end
end

View file

@ -0,0 +1,31 @@
class Module
# Declare an attribute reader backed by an internally-named instance variable.
def attr_internal_reader(*attrs)
attrs.each do |attr|
module_eval "def #{attr}() #{attr_internal_ivar_name(attr)} end"
end
end
# Declare an attribute writer backed by an internally-named instance variable.
def attr_internal_writer(*attrs)
attrs.each do |attr|
module_eval "def #{attr}=(v) #{attr_internal_ivar_name(attr)} = v end"
end
end
# Declare attributes backed by 'internal' instance variables names.
def attr_internal_accessor(*attrs)
attr_internal_reader(*attrs)
attr_internal_writer(*attrs)
end
alias_method :attr_internal, :attr_internal_accessor
private
mattr_accessor :attr_internal_naming_format
self.attr_internal_naming_format = '@_%s'
def attr_internal_ivar_name(attr)
attr_internal_naming_format % attr
end
end

View file

@ -0,0 +1,17 @@
# Add a +missing_name+ method to NameError instances.
class NameError < StandardError #:nodoc:
# Add a method to obtain the missing name from a NameError.
def missing_name
$1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message
end
# Was this exception raised because the given name was missing?
def missing_name?(name)
if name.is_a? Symbol
last_name = (missing_name || '').split('::').last
last_name == name.to_s
else
missing_name == name.to_s
end
end
end

View file

@ -0,0 +1,42 @@
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module String #:nodoc:
# Define methods for handeling unicode data.
module Unicode
# +chars+ is a Unicode safe proxy for string methods. It creates and returns an instance of the
# ActiveSupport::Multibyte::Chars class which encapsulates the original string. A Unicode safe version of all
# the String methods are defined on this proxy class. Undefined methods are forwarded to String, so all of the
# string overrides can also be called through the +chars+ proxy.
#
# name = 'Claus Müller'
# name.reverse #=> "rell??M sualC"
# name.length #=> 13
#
# name.chars.reverse.to_s #=> "rellüM sualC"
# name.chars.length #=> 12
#
#
# All the methods on the chars proxy which normally return a string will return a Chars object. This allows
# method chaining on the result of any of these methods.
#
# name.chars.reverse.length #=> 12
#
# The Char object tries to be as interchangeable with String objects as possible: sorting and comparing between
# String and Char work like expected. The bang! methods change the internal string representation in the Chars
# object. Interoperability problems can be resolved easily with a +to_s+ call.
#
# For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars and
# ActiveSupport::Multibyte::Handlers::UTF8Handler
def chars
ActiveSupport::Multibyte::Chars.new(self)
end
# Returns true if the string has UTF-8 semantics (a String used for purely byte resources is unlikely to have
# them), returns false otherwise.
def is_utf8?
ActiveSupport::Multibyte::Handlers::UTF8Handler.consumes?(self)
end
end
end
end
end

View file

@ -0,0 +1,201 @@
require 'yaml'
module ActiveSupport
module Deprecation #:nodoc:
mattr_accessor :debug
self.debug = false
# Choose the default warn behavior according to RAILS_ENV.
# Ignore deprecation warnings in production.
DEFAULT_BEHAVIORS = {
'test' => Proc.new { |message, callstack|
$stderr.puts(message)
$stderr.puts callstack.join("\n ") if debug
},
'development' => Proc.new { |message, callstack|
RAILS_DEFAULT_LOGGER.warn message
RAILS_DEFAULT_LOGGER.debug callstack.join("\n ") if debug
}
}
class << self
def warn(message = nil, callstack = caller)
behavior.call(deprecation_message(callstack, message), callstack) if behavior && !silenced?
end
def default_behavior
if defined?(RAILS_ENV)
DEFAULT_BEHAVIORS[RAILS_ENV.to_s]
else
DEFAULT_BEHAVIORS['test']
end
end
# Have deprecations been silenced?
def silenced?
@silenced = false unless defined?(@silenced)
@silenced
end
# Silence deprecation warnings within the block.
def silence
old_silenced, @silenced = @silenced, true
yield
ensure
@silenced = old_silenced
end
attr_writer :silenced
private
def deprecation_message(callstack, message = nil)
message ||= "You are using deprecated behavior which will be removed from Rails 2.0."
"DEPRECATION WARNING: #{message} See http://www.rubyonrails.org/deprecation for details. #{deprecation_caller_message(callstack)}"
end
def deprecation_caller_message(callstack)
file, line, method = extract_callstack(callstack)
if file
if line && method
"(called from #{method} at #{file}:#{line})"
else
"(called from #{file}:#{line})"
end
end
end
def extract_callstack(callstack)
if md = callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/)
md.captures
else
callstack.first
end
end
end
# Behavior is a block that takes a message argument.
mattr_accessor :behavior
self.behavior = default_behavior
# Warnings are not silenced by default.
self.silenced = false
module ClassMethods #:nodoc:
# Declare that a method has been deprecated.
def deprecate(*method_names)
options = method_names.last.is_a?(Hash) ? method_names.pop : {}
method_names = method_names + options.keys
method_names.each do |method_name|
alias_method_chain(method_name, :deprecation) do |target, punctuation|
class_eval(<<-EOS, __FILE__, __LINE__)
def #{target}_with_deprecation#{punctuation}(*args, &block)
::ActiveSupport::Deprecation.warn(self.class.deprecated_method_warning(:#{method_name}, #{options[method_name].inspect}), caller)
#{target}_without_deprecation#{punctuation}(*args, &block)
end
EOS
end
end
end
def deprecated_method_warning(method_name, message=nil)
warning = "#{method_name} is deprecated and will be removed from Rails #{deprecation_horizon}"
case message
when Symbol then "#{warning} (use #{message} instead)"
when String then "#{warning} (#{message})"
else warning
end
end
def deprecation_horizon
'2.0'
end
end
module Assertions #:nodoc:
def assert_deprecated(match = nil, &block)
result, warnings = collect_deprecations(&block)
assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
if match
match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp)
assert warnings.any? { |w| w =~ match }, "No deprecation warning matched #{match}: #{warnings.join(', ')}"
end
result
end
def assert_not_deprecated(&block)
result, deprecations = collect_deprecations(&block)
assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
result
end
private
def collect_deprecations
old_behavior = ActiveSupport::Deprecation.behavior
deprecations = []
ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack|
deprecations << message
end
result = yield
[result, deprecations]
ensure
ActiveSupport::Deprecation.behavior = old_behavior
end
end
# Stand-in for @request, @attributes, @params, etc which emits deprecation
# warnings on any method call (except #inspect).
class DeprecatedInstanceVariableProxy #:nodoc:
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
def initialize(instance, method, var = "@#{method}")
@instance, @method, @var = instance, method, var
end
# Don't give a deprecation warning on inspect since test/unit and error
# logs rely on it for diagnostics.
def inspect
target.inspect
end
private
def method_missing(called, *args, &block)
warn caller, called, args
target.__send__(called, *args, &block)
end
def target
@instance.__send__(@method)
end
def warn(callstack, called, args)
ActiveSupport::Deprecation.warn("#{@var} is deprecated! Call #{@method}.#{called} instead of #{@var}.#{called}. Args: #{args.inspect}", callstack)
end
end
end
end
class Module
include ActiveSupport::Deprecation::ClassMethods
end
require 'test/unit/error'
module Test
module Unit
class TestCase
include ActiveSupport::Deprecation::Assertions
end
class Error # :nodoc:
# Silence warnings when reporting test errors.
def message_with_silenced_deprecation
ActiveSupport::Deprecation.silence do
message_without_silenced_deprecation
end
end
alias_method_chain :message, :silenced_deprecation
end
end
end

View file

@ -0,0 +1,7 @@
module ActiveSupport::Multibyte #:nodoc:
DEFAULT_NORMALIZATION_FORM = :kc
NORMALIZATIONS_FORMS = [:c, :kc, :d, :kd]
UNICODE_VERSION = '5.0.0'
end
require 'active_support/multibyte/chars'

View file

@ -0,0 +1,129 @@
require 'active_support/multibyte/handlers/utf8_handler'
require 'active_support/multibyte/handlers/passthru_handler'
# Encapsulates all the functionality related to the Chars proxy.
module ActiveSupport::Multibyte #:nodoc:
# Chars enables you to work transparently with multibyte encodings in the Ruby String class without having extensive
# knowledge about the encoding. A Chars object accepts a string upon initialization and proxies String methods in an
# encoding safe manner. All the normal String methods are also implemented on the proxy.
#
# String methods are proxied through the Chars object, and can be accessed through the +chars+ method. Methods
# which would normally return a String object now return a Chars object so methods can be chained.
#
# "The Perfect String ".chars.downcase.strip.normalize #=> "the perfect string"
#
# Chars objects are perfectly interchangeable with String objects as long as no explicit class checks are made.
# If certain methods do explicitly check the class, call +to_s+ before you pass chars objects to them.
#
# bad.explicit_checking_method "T".chars.downcase.to_s
#
# The actual operations on the string are delegated to handlers. Theoretically handlers can be implemented for
# any encoding, but the default handler handles UTF-8. This handler is set during initialization, if you want to
# use you own handler, you can set it on the Chars class. Look at the UTF8Handler source for an example how to
# implement your own handler. If you your own handler to work on anything but UTF-8 you probably also
# want to override Chars#handler.
#
# ActiveSupport::Multibyte::Chars.handler = MyHandler
#
# Note that a few methods are defined on Chars instead of the handler because they are defined on Object or Kernel
# and method_missing can't catch them.
class Chars
attr_reader :string # The contained string
alias_method :to_s, :string
include Comparable
# The magic method to make String and Chars comparable
def to_str
# Using any other ways of overriding the String itself will lead you all the way from infinite loops to
# core dumps. Don't go there.
@string
end
# Create a new Chars instance.
def initialize(str)
@string = (str.string rescue str)
end
# Returns -1, 0 or +1 depending on whether the Chars object is to be sorted before, equal or after the
# object on the right side of the operation. It accepts any object that implements +to_s+. See String.<=>
# for more details.
def <=>(other); @string <=> other.to_s; end
# Works just like String#split, with the exception that the items in the resulting list are Chars
# instances instead of String. This makes chaining methods easier.
def split(*args)
@string.split(*args).map { |i| i.chars }
end
# Gsub works exactly the same as gsub on a normal string.
def gsub(*a, &b); @string.gsub(*a, &b).chars; end
# Like String.=~ only it returns the character offset (in codepoints) instead of the byte offset.
def =~(other)
handler.translate_offset(@string, @string =~ other)
end
# Try to forward all undefined methods to the handler, when a method is not defined on the handler, send it to
# the contained string. Method_missing is also responsible for making the bang! methods destructive.
def method_missing(m, *a, &b)
begin
# Simulate methods with a ! at the end because we can't touch the enclosed string from the handlers.
if m.to_s =~ /^(.*)\!$/
result = handler.send($1, @string, *a, &b)
if result == @string
result = nil
else
@string.replace result
end
else
result = handler.send(m, @string, *a, &b)
end
rescue NoMethodError
result = @string.send(m, *a, &b)
rescue Handlers::EncodingError
@string.replace handler.tidy_bytes(@string)
retry
end
if result.kind_of?(String)
result.chars
else
result
end
end
# Set the handler class for the Char objects.
def self.handler=(klass)
@@handler = klass
end
# Returns the proper handler for the contained string depending on $KCODE and the encoding of the string. This
# method is used internally to always redirect messages to the proper classes depending on the context.
def handler
if utf8_pragma?
@@handler
else
ActiveSupport::Multibyte::Handlers::PassthruHandler
end
end
private
# +utf8_pragma+ checks if it can send this string to the handlers. It makes sure @string isn't nil and $KCODE is
# set to 'UTF8'.
def utf8_pragma?
!@string.nil? && ($KCODE == 'UTF8')
end
end
end
# When we can load the utf8proc library, override normalization with the faster methods
begin
require 'utf8proc_native'
require 'active_support/multibyte/handlers/utf8_handler_proc'
ActiveSupport::Multibyte::Chars.handler = ActiveSupport::Multibyte::Handlers::UTF8HandlerProc
rescue LoadError
ActiveSupport::Multibyte::Chars.handler = ActiveSupport::Multibyte::Handlers::UTF8Handler
end

Some files were not shown because too many files have changed in this diff Show more