diff options
197 files changed, 3009 insertions, 904 deletions
diff --git a/.travis.yml b/.travis.yml index ba5526c009..5214b989af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,4 +23,4 @@ notifications: on_failure: always rooms: - secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI=" -bundler_args: --path vendor/bundle +bundler_args: --path vendor/bundle --without test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adc5eb6914..6d3cf072c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,14 @@ Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the team](http://contributors.rubyonrails.org)! -Please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide before submitting any code. +* If you want to submit a bug report please make sure to follow our [reporting guidelines](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#reporting-an-issue). + +* If you want to submit a patch, please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide. *We only accept bug reports and pull requests in GitHub*. -If you have a question about how to use Ruby on Rails, please [ask the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk). +* If you have a question about how to use Ruby on Rails, please [ask the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk). -If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. +* If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. Thanks! :heart: :heart: :heart: <br /> Rails Team @@ -27,11 +27,16 @@ gem 'dalli', '>= 2.2.1' local_gemfile = File.dirname(__FILE__) + "/.Gemfile" instance_eval File.read local_gemfile if File.exists? local_gemfile -platforms :mri do - group :test do - gem 'ruby-prof', '~> 0.11.2' if RUBY_VERSION < '2.0' - gem 'debugger' if !ENV['TRAVIS'] && RUBY_VERSION < '2.0' +group :test do + platforms :mri_19 do + gem 'ruby-prof', '~> 0.11.2' end + + platforms :mri_19, :mri_20 do + gem 'debugger' + end + + gem 'benchmark-ips' end platforms :ruby do @@ -53,7 +58,7 @@ end platforms :jruby do gem 'json' - gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2.0' + gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2.7' # This is needed by now to let tests work on JRuby # TODO: When the JRuby guys merge jruby-openssl in @@ -61,8 +66,8 @@ platforms :jruby do gem 'jruby-openssl' group :db do - gem 'activerecord-jdbcmysql-adapter', '>= 1.2.0' - gem 'activerecord-jdbcpostgresql-adapter', '>= 1.2.0' + gem 'activerecord-jdbcmysql-adapter', '>= 1.2.7' + gem 'activerecord-jdbcpostgresql-adapter', '>= 1.2.7' end end @@ -76,5 +81,3 @@ end # A gem necessary for ActiveRecord tests with IBM DB gem 'ibm_db' if ENV['IBM_DB'] - -gem 'benchmark-ips' diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 8f74ac0928..487e57be7b 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -21,7 +21,7 @@ *Olek Janiszewski* * Eager loading made to use relation's `in_clause_length` instead of host's one. - Fix #8474 + Fixes #8474. *Boris Staal* @@ -29,7 +29,7 @@ *Nate Berkopec* * Do not render views when mail() isn't called. - Fix #7761 + Fixes #7761. *Yves Senn* diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 3fc3e06160..69096443fe 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,50 @@ ## Rails 4.0.0 (unreleased) ## +* Include I18n locale fallbacks in view lookup. + Fixes GH#3512. + + *Juan Barreneche* + +* Integration and functional tests allow headers and rack env + variables to be passed when performing requests. + Fixes #6513. + + Example: + + # integration test + get "/success", {}, "HTTP_REFERER" => "http://test.com/", + "Accepts" => "text/plain, text/html" + + # functional test + @request.headers["Accepts"] = "text/plain, text/html" + + *Yves Senn* + +* Http::Headers respects headers that are not prefixed with HTTP_ + + *Yves Senn* + +* Fix incorrectly appended square brackets to a multiple select box + if an explicit name has been given and it already ends with "[]" + + Before: + + select(:category, [], {}, multiple: true, name: "post[category][]") + # => <select name="post[category][][]" ...> + + After: + + select(:category, [], {}, multiple: true, name: "post[category][]") + # => <select name="post[category][]" ...> + + *Olek Janiszewski* + +* Fixed regression when using `assert_template` to verify files sent using + `render file: 'README.md'`. + Fixes #9464. + + *Justin Coyne* + * Fixed `ActionView::Helpers::CaptureHelper#content_for` regression when trying to use it in a boolean statement. Fixes #9360. @@ -231,12 +276,12 @@ Client-IP and Remote-Addr headers, in that order. Document the rationale for that decision, and describe the options that can be passed to the RemoteIp middleware to change it. - Fix #7979 + Fixes #7979. *André Arko*, *Steve Klabnik*, *Alexey Gaziev* * Do not append second slash to `root_url` when using `trailing_slash: true` - Fix #8700 + Fixes #8700. Before: @@ -264,7 +309,7 @@ * Do not append `charset=` parameter when `head` is called with a `:content_type` option. - Fix #8661. + Fixes #8661. *Yves Senn* @@ -422,7 +467,7 @@ * Render every partial with a new `ActionView::PartialRenderer`. This resolves issues when rendering nested partials. - Fix #8197. + Fixes #8197. *Yves Senn* @@ -430,7 +475,7 @@ of mime types where template text is not html escaped by default. It prevents `Jack & Joe` from rendering as `Jack & Joe` for the whitelisted mime types. The default whitelist contains `text/plain`. - Fix #7976. + Fixes #7976. *Joost Baaij* @@ -446,7 +491,7 @@ check_box("post", "comment_ids", { multiple: true, index: "foo" }, 1) # => <input name=\"post[foo][comment_ids][]\" type=\"hidden\" value=\"0\" /><input id=\"post_foo_comment_ids_1\" name=\"post[foo][comment_ids][]\" type=\"checkbox\" value=\"1\" /> - Fix #8108. + Fixes #8108. *Daniel Fox, Grant Hutchins & Trace Wax* @@ -469,7 +514,7 @@ *Josh Peek* * `assert_template` can be used to assert on the same template with different locals - Fix #3675. + Fixes #3675. *Yves Senn* @@ -480,7 +525,7 @@ * Accept `:remote` as symbolic option for `link_to` helper. *Riley Lynch* * Warn when the `:locals` option is passed to `assert_template` outside of a view test case - Fix #3415. + Fixes #3415. *Yves Senn* @@ -504,12 +549,12 @@ * Rename internal variables on `ActionController::TemplateAssertions` to prevent naming collisions. `@partials`, `@templates` and `@layouts` are now prefixed with an underscore. - Fix #7459. + Fixes #7459. *Yves Senn* * `resource` and `resources` don't modify the passed options hash. - Fix #7777. + Fixes #7777. *Yves Senn* @@ -573,7 +618,7 @@ *Guillermo Iguaran* * Log now displays the correct status code when an exception is raised. - Fix #7646. + Fixes #7646. *Yves Senn* diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 3d274e7dd7..7318c8b7ec 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -48,6 +48,11 @@ module ActionController info("Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)") end + def unpermitted_parameters(event) + unpermitted_keys = event.payload[:keys] + debug("Unpermitted parameters: #{unpermitted_keys.join(", ")}") + end + %w(write_fragment read_fragment exist_fragment? expire_fragment expire_page write_page).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 32e5afa335..fb664a69dd 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -14,6 +14,7 @@ module ActionController # response.stream.write "hello world\n" # sleep 1 # } + # ensure # response.stream.close # end # end @@ -97,6 +98,10 @@ module ActionController def merge_default_headers(original, default) Header.new self, super end + + def handle_conditional_get! + super unless committed? + end end def process(name) diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index e4dcd3213f..acad8a0799 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -339,7 +339,8 @@ module ActionController if unpermitted_keys.any? case self.class.action_on_unpermitted_parameters when :log - ActionController::Base.logger.debug "Unpermitted parameters: #{unpermitted_keys.join(", ")}" + name = "unpermitted_parameters.action_controller" + ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys) when :raise raise ActionController::UnpermittedParameters.new(unpermitted_keys) end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index bba1f1e201..41b5228872 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -16,6 +16,7 @@ module ActionController @_partials = Hash.new(0) @_templates = Hash.new(0) @_layouts = Hash.new(0) + @_files = Hash.new(0) ActiveSupport::Notifications.subscribe("render_template.action_view") do |name, start, finish, id, payload| path = payload[:layout] @@ -39,6 +40,16 @@ module ActionController @_templates[path] += 1 end + + ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload| + next if payload[:virtual_path] # files don't have virtual path + + path = payload[:identifier] + if path + @_files[path] += 1 + @_files[path.split("/").last] += 1 + end + end end def teardown_subscriptions @@ -106,7 +117,7 @@ module ActionController end assert matches_template, msg when Hash - options.assert_valid_keys(:layout, :partial, :locals, :count) + options.assert_valid_keys(:layout, :partial, :locals, :count, :file) if options.key?(:layout) expected_layout = options[:layout] @@ -123,6 +134,10 @@ module ActionController end end + if options[:file] + assert_includes @_files.keys, options[:file] + end + if expected_partial = options[:partial] if expected_locals = options[:locals] if defined?(@_rendered_views) @@ -436,37 +451,55 @@ module ActionController end - # Executes a request simulating GET HTTP method and set/volley the response + # Simulate a GET request with the given parameters. + # + # - +action+: The controller action to call. + # - +parameters+: The HTTP parameters that you want to pass. This may + # be +nil+, a Hash, or a String that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). + # - +session+: A Hash of parameters to store in the session. This may be +nil+. + # - +flash+: A Hash of parameters to store in the flash. This may be +nil+. + # + # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with + # +#post+, +#patch+, +#put+, +#delete+, and +#head+. + # Note that the request method is not verified. The different methods are + # available to make the tests more expressive. def get(action, *args) process(action, "GET", *args) end - # Executes a request simulating POST HTTP method and set/volley the response + # Simulate a POST request with the given parameters and set/volley the response. + # See +#get+ for more details. def post(action, *args) process(action, "POST", *args) end - # Executes a request simulating PATCH HTTP method and set/volley the response + # Simulate a PATCH request with the given parameters and set/volley the response. + # See +#get+ for more details. def patch(action, *args) process(action, "PATCH", *args) end - # Executes a request simulating PUT HTTP method and set/volley the response + # Simulate a PUT request with the given parameters and set/volley the response. + # See +#get+ for more details. def put(action, *args) process(action, "PUT", *args) end - # Executes a request simulating DELETE HTTP method and set/volley the response + # Simulate a DELETE request with the given parameters and set/volley the response. + # See +#get+ for more details. def delete(action, *args) process(action, "DELETE", *args) end - # Executes a request simulating HEAD HTTP method and set/volley the response + # Simulate a HEAD request with the given parameters and set/volley the response. + # See +#get+ for more details. def head(action, *args) process(action, "HEAD", *args) end - # Executes a request simulating OPTIONS HTTP method and set/volley the response + # Simulate a OPTIONS request with the given parameters and set/volley the response. + # See +#get+ for more details. def options(action, *args) process(action, "OPTIONS", *args) end diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index dc04d4577b..2666cd4b0a 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,38 +1,62 @@ module ActionDispatch module Http class Headers + CGI_VARIABLES = %w( + CONTENT_TYPE CONTENT_LENGTH + HTTPS AUTH_TYPE GATEWAY_INTERFACE + PATH_INFO PATH_TRANSLATED QUERY_STRING + REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER + REQUEST_METHOD SCRIPT_NAME + SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE + ) + HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ + include Enumerable + attr_reader :env def initialize(env = {}) - @headers = env + @env = env + end + + def [](key) + @env[env_name(key)] end - def [](header_name) - @headers[env_name(header_name)] + def []=(key, value) + @env[env_name(key)] = value end - def []=(k,v); @headers[k] = v; end - def key?(k); @headers.key? k; end + def key?(key); @env.key? key; end alias :include? :key? - def fetch(header_name, *args, &block) - @headers.fetch env_name(header_name), *args, &block + def fetch(key, *args, &block) + @env.fetch env_name(key), *args, &block end def each(&block) - @headers.each(&block) + @env.each(&block) end - private + def merge(headers_or_env) + headers = Http::Headers.new(env.dup) + headers.merge!(headers_or_env) + headers + end - # Converts a HTTP header name to an environment variable name if it is - # not contained within the headers hash. - def env_name(header_name) - @headers.include?(header_name) ? header_name : cgi_name(header_name) + def merge!(headers_or_env) + headers_or_env.each do |key, value| + self[env_name(key)] = value + end end - def cgi_name(k) - "HTTP_#{k.upcase.gsub(/-/, '_')}" + private + def env_name(key) + key = key.to_s + if key =~ HTTP_HEADER + key = key.upcase.tr('-', '_') + key = "HTTP_" + key unless CGI_VARIABLES.include?(key) + end + key end end end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 446862aad0..246d9c121a 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -18,7 +18,7 @@ module ActionDispatch query_parameters.dup end params.merge!(path_parameters) - encode_params(params).with_indifferent_access + params.with_indifferent_access end end alias :params :parameters @@ -50,40 +50,33 @@ module ActionDispatch private + # Convert nested Hash to HashWithIndifferentAccess + # and UTF-8 encode both keys and values in nested Hash. + # # TODO: Validate that the characters are UTF-8. If they aren't, # you'll get a weird error down the road, but our form handling # should really prevent that from happening - def encode_params(params) + def normalize_encode_params(params) if params.is_a?(String) return params.force_encoding(Encoding::UTF_8).encode! elsif !params.is_a?(Hash) return params end + new_hash = {} params.each do |k, v| - case v - when Hash - encode_params(v) - when Array - v.map! {|el| encode_params(el) } - else - encode_params(v) - end - end - end - - # Convert nested Hash to ActiveSupport::HashWithIndifferentAccess - def normalize_parameters(value) - case value - when Hash - h = {} - value.each { |k, v| h[k] = normalize_parameters(v) } - h.with_indifferent_access - when Array - value.map { |e| normalize_parameters(e) } - else - value + new_key = k.is_a?(String) ? k.dup.force_encoding("UTF-8").encode! : k + new_hash[new_key] = + case v + when Hash + normalize_encode_params(v) + when Array + v.map! {|el| normalize_encode_params(el) } + else + normalize_encode_params(v) + end end + new_hash.with_indifferent_access end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 7b04d6e851..aff2172788 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -156,14 +156,31 @@ module ActionDispatch @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) end + # Returns the +String+ full path including params of the last URL requested. + # + # app.get "/articles" + # app.request.fullpath # => "/articles" + # + # app.get "/articles?page=2" + # app.request.fullpath # => "/articles?page=2" def fullpath @fullpath ||= super end + # Returns the original request URL as a +String+ + # + # app.get "/articles?page=2" + # app.request.original_url + # # => "http://www.example.com/articles?page=2" def original_url base_url + original_fullpath end + # The +String+ MIME type of the request + # + # app.get "/articles" + # app.request.media_type + # # => "application/x-www-form-urlencoded" def media_type content_mime_type.to_s end @@ -256,7 +273,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {}) + @env["action_dispatch.request.query_parameters"] ||= (normalize_encode_params(super) || {}) rescue TypeError => e raise ActionController::BadRequest.new(:query, e) end @@ -264,7 +281,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {}) + @env["action_dispatch.request.request_parameters"] ||= (normalize_encode_params(super) || {}) rescue TypeError => e raise ActionController::BadRequest.new(:request, e) end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 67cb7fbcb5..319d0197d1 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -75,16 +75,16 @@ module ActionDispatch end module Upload # :nodoc: - # Convert nested Hash to ActiveSupport::HashWithIndifferentAccess and replace - # file upload hash with UploadedFile objects - def normalize_parameters(value) + # Replace file upload hash with UploadedFile objects + # when normalize and encode parameters. + def normalize_encode_params(value) if Hash === value && value.has_key?(:tempfile) UploadedFile.new(value) else super end end - private :normalize_parameters + private :normalize_encode_params end end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb index 6b1f233930..550f4dbd0d 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb @@ -10,7 +10,7 @@ clean_params.delete("action") clean_params.delete("controller") - request_dump = clean_params.empty? ? 'None' : clean_params.inspect.tr(',', ",\n") + request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") def debug_hash(object) object.to_hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") @@ -31,4 +31,4 @@ </div> <h2 style="margin-top: 30px">Response</h2> -<p><b>Headers</b>:</p> <pre><%= defined?(@response) ? @response.headers.inspect.tr(',', ",\n") : 'None' %></pre> +<p><b>Headers</b>:</p> <pre><%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre> diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index ed4e88aab6..56c31255f3 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -17,7 +17,7 @@ module ActionDispatch # a Hash, or a String that is appropriately encoded # (<tt>application/x-www-form-urlencoded</tt> or # <tt>multipart/form-data</tt>). - # - +headers+: Additional headers to pass, as a Hash. The headers will be + # - +headers_or_env+: Additional headers to pass, as a Hash. The headers will be # merged into the Rack env hash. # # This method returns a Response object, which one can use to @@ -28,44 +28,44 @@ module ActionDispatch # # You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with # +#post+, +#patch+, +#put+, +#delete+, and +#head+. - def get(path, parameters = nil, headers = nil) - process :get, path, parameters, headers + def get(path, parameters = nil, headers_or_env = nil) + process :get, path, parameters, headers_or_env end # Performs a POST request with the given parameters. See +#get+ for more # details. - def post(path, parameters = nil, headers = nil) - process :post, path, parameters, headers + def post(path, parameters = nil, headers_or_env = nil) + process :post, path, parameters, headers_or_env end # Performs a PATCH request with the given parameters. See +#get+ for more # details. - def patch(path, parameters = nil, headers = nil) - process :patch, path, parameters, headers + def patch(path, parameters = nil, headers_or_env = nil) + process :patch, path, parameters, headers_or_env end # Performs a PUT request with the given parameters. See +#get+ for more # details. - def put(path, parameters = nil, headers = nil) - process :put, path, parameters, headers + def put(path, parameters = nil, headers_or_env = nil) + process :put, path, parameters, headers_or_env end # Performs a DELETE request with the given parameters. See +#get+ for # more details. - def delete(path, parameters = nil, headers = nil) - process :delete, path, parameters, headers + def delete(path, parameters = nil, headers_or_env = nil) + process :delete, path, parameters, headers_or_env end # Performs a HEAD request with the given parameters. See +#get+ for more # details. - def head(path, parameters = nil, headers = nil) - process :head, path, parameters, headers + def head(path, parameters = nil, headers_or_env = nil) + process :head, path, parameters, headers_or_env end # Performs a OPTIONS request with the given parameters. See +#get+ for # more details. - def options(path, parameters = nil, headers = nil) - process :options, path, parameters, headers + def options(path, parameters = nil, headers_or_env = nil) + process :options, path, parameters, headers_or_env end # Performs an XMLHttpRequest request with the given parameters, mirroring @@ -74,11 +74,11 @@ module ActionDispatch # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart # string; the headers are a hash. - def xml_http_request(request_method, path, parameters = nil, headers = nil) - headers ||= {} - headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - process(request_method, path, parameters, headers) + def xml_http_request(request_method, path, parameters = nil, headers_or_env = nil) + headers_or_env ||= {} + headers_or_env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + headers_or_env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + process(request_method, path, parameters, headers_or_env) end alias xhr :xml_http_request @@ -95,40 +95,40 @@ module ActionDispatch # redirect. Note that the redirects are followed until the response is # not a redirect--this means you may run into an infinite loop if your # redirect loops back to itself. - def request_via_redirect(http_method, path, parameters = nil, headers = nil) - process(http_method, path, parameters, headers) + def request_via_redirect(http_method, path, parameters = nil, headers_or_env = nil) + process(http_method, path, parameters, headers_or_env) follow_redirect! while redirect? status end # Performs a GET request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def get_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:get, path, parameters, headers) + def get_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:get, path, parameters, headers_or_env) end # Performs a POST request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def post_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:post, path, parameters, headers) + def post_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:post, path, parameters, headers_or_env) end # Performs a PATCH request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def patch_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:patch, path, parameters, headers) + def patch_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:patch, path, parameters, headers_or_env) end # Performs a PUT request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def put_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:put, path, parameters, headers) + def put_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:put, path, parameters, headers_or_env) end # Performs a DELETE request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def delete_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:delete, path, parameters, headers) + def delete_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:delete, path, parameters, headers_or_env) end end @@ -268,8 +268,7 @@ module ActionDispatch end # Performs the actual request. - def process(method, path, parameters = nil, rack_env = nil) - rack_env ||= {} + def process(method, path, parameters = nil, headers_or_env = nil) if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -300,10 +299,12 @@ module ActionDispatch "CONTENT_TYPE" => "application/x-www-form-urlencoded", "HTTP_ACCEPT" => accept } + # this modifies the passed env directly + Http::Headers.new(env).merge!(headers_or_env || {}) session = Rack::Test::Session.new(_mock_session) - env.merge!(rack_env) + env.merge!(env) # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 1bad82159a..5afe435459 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -180,7 +180,7 @@ module ActionView # <title>My Website</title> # <%= yield :script %> # </head> - # <body class="<%= content_for?(:right_col) ? 'one-column' : 'two-column' %>"> + # <body class="<%= content_for?(:right_col) ? 'two-column' : 'one-column' %>"> # <%= yield %> # <%= yield :right_col %> # </body> diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb index db7f9f3494..c29c1b1eea 100644 --- a/actionpack/lib/action_view/helpers/debug_helper.rb +++ b/actionpack/lib/action_view/helpers/debug_helper.rb @@ -28,7 +28,7 @@ module ActionView # </pre> def debug(object) Marshal::dump(object) - object = ERB::Util.html_escape(object.to_yaml).tr(" ", " ").html_safe + object = ERB::Util.html_escape(object.to_yaml).gsub(" ", " ").html_safe content_tag(:pre, object, :class => "debug_dump") rescue Exception # errors from Marshal or YAML # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 3dae1fc87a..6b6a7edc1d 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -433,7 +433,7 @@ module ActionView builder = instantiate_builder(object_name, object, options) output = capture(builder, &block) - html_options[:multipart] = builder.multipart? + html_options[:multipart] ||= builder.multipart? form_tag(options[:url] || {}, html_options) { output } end diff --git a/actionpack/lib/action_view/helpers/tags/base.rb b/actionpack/lib/action_view/helpers/tags/base.rb index 3d597079c4..aef1572290 100644 --- a/actionpack/lib/action_view/helpers/tags/base.rb +++ b/actionpack/lib/action_view/helpers/tags/base.rb @@ -84,7 +84,7 @@ module ActionView options["id"] = options.fetch("id"){ tag_id } end - options["name"] += "[]" if options["multiple"] + options["name"] += "[]" if options["multiple"] && !options["name"].ends_with?("[]") options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence end diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index 4e4816d983..d61cc0f304 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -43,7 +43,13 @@ module ActionView module Accessors #:nodoc: end - register_detail(:locale) { [I18n.locale, I18n.default_locale].uniq } + register_detail(:locale) do + locales = [I18n.locale] + locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks + locales << I18n.default_locale + locales.uniq! + locales + end register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] } register_detail(:handlers){ Template::Handlers.extensions } diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index f73d14c79b..946db1df79 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -138,7 +138,7 @@ module ActionView # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. def render(view, locals, buffer=nil, &block) - ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do + ActiveSupport::Notifications.instrument("!render_template.action_view", virtual_path: @virtual_path, identifier: @identifier) do compile!(view) view.send(method_name, locals, buffer, &block) end @@ -324,7 +324,8 @@ module ActionView end def locals_code #:nodoc: - @locals.map { |key| "#{key} = local_assigns[:#{key}];" }.join + # Double assign to suppress the dreaded 'assigned but unused variable' warning + @locals.map { |key| "#{key} = #{key} = local_assigns[:#{key}];" }.join end def method_name #:nodoc: diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index 1a1083bf00..47bd011d5e 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -109,7 +109,7 @@ module ActionView @cache.clear end - # Normalizes the arguments and passes it on to find_template. + # Normalizes the arguments and passes it on to find_templates. def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) cached(key, [name, prefix, partial], details, locals) do find_templates(name, prefix, partial, details) diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 463f192d0c..10b487f37a 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -219,6 +219,7 @@ module ActionView :@_routes, :@controller, :@_layouts, + :@_files, :@_rendered_views, :@method_name, :@output_buffer, diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 5d727b3811..22a410db94 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -96,6 +96,14 @@ class ActionPackAssertionsController < ActionController::Base raise "post" if request.post? render :text => "request method: #{request.env['REQUEST_METHOD']}" end + + def render_file_absolute_path + render :file => File.expand_path('../../../README.rdoc', __FILE__) + end + + def render_file_relative_path + render :file => 'README.rdoc' + end end # Used to test that assert_response includes the exception message @@ -142,6 +150,16 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert_tag :content => "/action_pack_assertions/flash_me" end + def test_render_file_absolute_path + get :render_file_absolute_path + assert_match(/\A= Action Pack/, @response.body) + end + + def test_render_file_relative_path + get :render_file_relative_path + assert_match(/\A= Action Pack/, @response.body) + end + def test_get_request assert_raise(RuntimeError) { get :raise_exception_on_get } get :raise_exception_on_post @@ -441,6 +459,23 @@ class AssertTemplateTest < ActionController::TestCase assert_template :partial => '_partial' end + def test_file_with_absolute_path_success + get :render_file_absolute_path + assert_template :file => File.expand_path('../../../README.rdoc', __FILE__) + end + + def test_file_with_relative_path_success + get :render_file_relative_path + assert_template :file => 'README.rdoc' + end + + def test_with_file_failure + get :render_file_absolute_path + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template :file => 'test/hello_world' + end + end + def test_with_nil_passes_when_no_template_rendered get :nothing assert_template nil diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 72b882539c..c3bdf74d93 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -573,6 +573,21 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest def test_generate_url_without_controller assert_equal 'http://www.example.com/foo', url_for(:controller => "foo") end + + def test_pass_headers + get "/success", {}, "Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com" + + assert_equal "http://nohost.com", @request.env["HTTP_HOST"] + assert_equal "http://www.example.com/foo", @request.env["HTTP_REFERER"] + end + + def test_pass_env + get "/success", {}, "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" + + assert_equal "http://test.com", @request.env["HTTP_HOST"] + assert_equal "http://test.com/", @request.env["HTTP_REFERER"] + end + end class ApplicationIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 3b1a07d7af..5755444a65 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -48,6 +48,10 @@ module ActionController end response.stream.close end + + def with_stale + render :text => 'stale' if stale?(:etag => "123") + end end tests TestController @@ -117,5 +121,16 @@ module ActionController assert_equal 'zomg', response.body assert response.stream.closed?, 'stream should be closed' end + + def test_stale_without_etag + get :with_stale + assert_equal 200, @response.status.to_i + end + + def test_stale_with_etag + @request.if_none_match = Digest::MD5.hexdigest("123") + get :with_stale + assert_equal 304, @response.status.to_i + end end end diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb index bac1d02977..6b02eedaed 100644 --- a/actionpack/test/controller/localized_templates_test.rb +++ b/actionpack/test/controller/localized_templates_test.rb @@ -25,4 +25,13 @@ class LocalizedTemplatesTest < ActionController::TestCase ensure I18n.locale = old_locale end + + def test_use_fallback_locales + I18n.locale = :"de-AT" + I18n.backend.class.send(:include, I18n::Backend::Fallbacks) + I18n.fallbacks[:"de-AT"] = [:de] + + get :hello_world + assert_equal "Gutten Tag", @response.body + end end diff --git a/actionpack/test/controller/output_escaping_test.rb b/actionpack/test/controller/output_escaping_test.rb index 43a8c05cda..c3c549fbfc 100644 --- a/actionpack/test/controller/output_escaping_test.rb +++ b/actionpack/test/controller/output_escaping_test.rb @@ -11,8 +11,6 @@ class OutputEscapingTest < ActiveSupport::TestCase end test "escapeHTML shouldn't touch explicitly safe strings" do - # TODO this seems easier to compose and reason about, but - # this should be verified assert_equal "<", ERB::Util.h("<".html_safe) end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index df31338f09..38b9794b4d 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -57,6 +57,10 @@ class TestCaseTest < ActionController::TestCase render :text => request.protocol end + def test_headers + render text: request.headers.env.to_json + end + def test_html_output render :text => <<HTML <html> @@ -626,6 +630,24 @@ XML assert_equal 2004, page[:year] end + test "set additional HTTP headers" do + @request.headers['Referer'] = "http://nohost.com/home" + @request.headers['Content-Type'] = "application/rss+xml" + get :test_headers + parsed_env = JSON.parse(@response.body) + assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"] + assert_equal "application/rss+xml", parsed_env["CONTENT_TYPE"] + end + + test "set additional env variables" do + @request.headers['HTTP_REFERER'] = "http://example.com/about" + @request.headers['CONTENT_TYPE'] = "application/json" + get :test_headers + parsed_env = JSON.parse(@response.body) + assert_equal "http://example.com/about", parsed_env["HTTP_REFERER"] + assert_equal "application/json", parsed_env["CONTENT_TYPE"] + end + def test_id_converted_to_string get :test_params, :id => 20, :foo => Object.new assert_kind_of String, @request.path_parameters['id'] diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb index 42432510c3..9e37b96951 100644 --- a/actionpack/test/dispatch/header_test.rb +++ b/actionpack/test/dispatch/header_test.rb @@ -1,41 +1,137 @@ -require 'abstract_unit' +require "abstract_unit" class HeaderTest < ActiveSupport::TestCase - def setup + setup do @headers = ActionDispatch::Http::Headers.new( - "HTTP_CONTENT_TYPE" => "text/plain" + "CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page" ) end - def test_each + test "#new does not normalize the data" do + headers = ActionDispatch::Http::Headers.new( + "Content-Type" => "application/json", + "HTTP_REFERER" => "/some/page", + "Host" => "http://test.com") + + assert_equal({"Content-Type" => "application/json", + "HTTP_REFERER" => "/some/page", + "Host" => "http://test.com"}, headers.env) + end + + test "#env returns the headers as env variables" do + assert_equal({"CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "#each iterates through the env variables" do headers = [] @headers.each { |pair| headers << pair } - assert_equal [["HTTP_CONTENT_TYPE", "text/plain"]], headers + assert_equal [["CONTENT_TYPE", "text/plain"], + ["HTTP_REFERER", "/some/page"]], headers + end + + test "set new headers" do + @headers["Host"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] + end + + test "headers can contain numbers" do + @headers["Content-MD5"] = "Q2hlY2sgSW50ZWdyaXR5IQ==" + + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["Content-MD5"] + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["HTTP_CONTENT_MD5"] + end + + test "set new env variables" do + @headers["HTTP_HOST"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] end - def test_setter - @headers['foo'] = "bar" - assert_equal "bar", @headers['foo'] + test "key?" do + assert @headers.key?("CONTENT_TYPE") + assert @headers.include?("CONTENT_TYPE") end - def test_key? - assert @headers.key?('HTTP_CONTENT_TYPE') - assert @headers.include?('HTTP_CONTENT_TYPE') + test "fetch with block" do + assert_equal "omg", @headers.fetch("notthere") { "omg" } end - def test_fetch_with_block - assert_equal 'omg', @headers.fetch('notthere') { 'omg' } + test "accessing http header" do + assert_equal "/some/page", @headers["Referer"] + assert_equal "/some/page", @headers["referer"] + assert_equal "/some/page", @headers["HTTP_REFERER"] end - test "content type" do + test "accessing special header" do assert_equal "text/plain", @headers["Content-Type"] assert_equal "text/plain", @headers["content-type"] assert_equal "text/plain", @headers["CONTENT_TYPE"] - assert_equal "text/plain", @headers["HTTP_CONTENT_TYPE"] end test "fetch" do assert_equal "text/plain", @headers.fetch("content-type", nil) - assert_equal "not found", @headers.fetch('not-found', 'not found') + assert_equal "not found", @headers.fetch("not-found", "not found") + end + + test "#merge! headers with mutation" do + @headers.merge!("Host" => "http://example.test", + "Content-Type" => "text/html") + assert_equal({"HTTP_HOST" => "http://example.test", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "#merge! env with mutation" do + @headers.merge!("HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html") + assert_equal({"HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "merge without mutation" do + combined = @headers.merge("HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html") + assert_equal({"HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, combined.env) + + assert_equal({"CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "env variables with . are not modified" do + headers = ActionDispatch::Http::Headers.new + headers.merge! "rack.input" => "", + "rack.request.cookie_hash" => "", + "action_dispatch.logger" => "" + + assert_equal(["action_dispatch.logger", + "rack.input", + "rack.request.cookie_hash"], headers.env.keys.sort) + end + + test "symbols are treated as strings" do + headers = ActionDispatch::Http::Headers.new + headers.merge!(:SERVER_NAME => "example.com", + "HTTP_REFERER" => "/", + :Host => "test.com") + assert_equal "example.com", headers["SERVER_NAME"] + assert_equal "/", headers[:HTTP_REFERER] + assert_equal "test.com", headers["HTTP_HOST"] + end + + test "headers directly modifies the passed environment" do + env = {"HTTP_REFERER" => "/"} + headers = ActionDispatch::Http::Headers.new(env) + headers['Referer'] = "http://example.com/" + headers.merge! "CONTENT_TYPE" => "text/plain" + assert_equal({"HTTP_REFERER"=>"http://example.com/", + "CONTENT_TYPE"=>"text/plain"}, env) end end diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 399f15199c..3c30a705e9 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -1,13 +1,15 @@ +# encoding: utf-8 require 'abstract_unit' class MultipartParamsParsingTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base class << self - attr_accessor :last_request_parameters + attr_accessor :last_request_parameters, :last_parameters end def parse self.class.last_request_parameters = request.request_parameters + self.class.last_parameters = request.parameters head :ok end @@ -30,6 +32,23 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param')) end + test "parse single utf8 parameter" do + assert_equal({ 'Iñtërnâtiônà lizætiøn_name' => 'Iñtërnâtiônà lizætiøn_value'}, + parse_multipart('single_utf8_param'), "request.request_parameters") + assert_equal( + 'Iñtërnâtiônà lizætiøn_value', + TestController.last_parameters['Iñtërnâtiônà lizætiøn_name'], "request.parameters") + end + + test "parse bracketed utf8 parameter" do + assert_equal({ 'Iñtërnâtiônà lizætiøn_name' => { + 'Iñtërnâtiônà lizætiøn_nested_name' => 'Iñtërnâtiônà lizætiøn_value'} }, + parse_multipart('bracketed_utf8_param'), "request.request_parameters") + assert_equal( + {'Iñtërnâtiônà lizætiøn_nested_name' => 'Iñtërnâtiônà lizætiøn_value'}, + TestController.last_parameters['Iñtërnâtiônà lizætiøn_name'], "request.parameters") + end + test "parses text file" do params = parse_multipart('text_file') assert_equal %w(file foo), params.keys.sort diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index e9b59f55a7..9169658c22 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -164,7 +164,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest return end - object.each do |k,v| + object.each_value do |v| case v when Hash assert_utf8(v) diff --git a/actionpack/test/fixtures/multipart/bracketed_utf8_param b/actionpack/test/fixtures/multipart/bracketed_utf8_param new file mode 100644 index 0000000000..976ca44a45 --- /dev/null +++ b/actionpack/test/fixtures/multipart/bracketed_utf8_param @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônà lizætiøn_name[Iñtërnâtiônà lizætiøn_nested_name]"
+
+Iñtërnâtiônà lizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/single_utf8_param b/actionpack/test/fixtures/multipart/single_utf8_param new file mode 100644 index 0000000000..b86f62d1e1 --- /dev/null +++ b/actionpack/test/fixtures/multipart/single_utf8_param @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônà lizætiøn_name"
+
+Iñtërnâtiônà lizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/template/debug_helper_test.rb b/actionpack/test/template/debug_helper_test.rb new file mode 100644 index 0000000000..42d06bd9ff --- /dev/null +++ b/actionpack/test/template/debug_helper_test.rb @@ -0,0 +1,8 @@ +require 'active_record_unit' + +class DebugHelperTest < ActionView::TestCase + def test_debug + company = Company.new(name: "firebase") + assert_match " name: firebase", debug(company) + end +end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 268bab6ad2..dff0b8bdc2 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -2791,8 +2791,8 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_html_options_adds_options_to_form_tag - form_for(@post, html: { id: 'some_form', class: 'some_class' }) do |f| end - expected = whole_form("/posts/123", "some_form", "some_class", method: "patch") + form_for(@post, html: { id: 'some_form', class: 'some_class', multipart: true }) do |f| end + expected = whole_form("/posts/123", "some_form", "some_class", method: "patch", multipart: "multipart/form-data") assert_dom_equal expected, output_buffer end diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 04cdd068c8..29d63d9653 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -575,6 +575,14 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_multiple_and_with_explicit_name_ending_with_brackets + output_buffer = select(:post, :category, [], {include_hidden: false}, multiple: true, name: 'post[category][]') + assert_dom_equal( + "<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>", + output_buffer + ) + end + def test_select_with_multiple_and_disabled_to_add_disabled_hidden_input output_buffer = select(:post, :category, "", {}, :multiple => true, :disabled => true) assert_dom_equal( diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 5d87c96605..e359f47975 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -51,7 +51,6 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal 'javascript:history.back()', url_for(:back) end - # TODO: missing test cases def test_button_to_with_straight_url assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com") end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 1fe6dbd4d9..6ba0c7cd6b 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,5 +1,32 @@ ## Rails 4.0.0 (unreleased) ## +* `has_secure_password` does not fail the confirmation validation + when assigning empty String to `password` and `password_confirmation`. + + Example: + + # Given User has_secure_password. + @user.password = "" + @user.password_confirmation = "" + @user.valid?(:update) # used to be false + + *Yves Senn* + +* `validates_confirmation_of` does not override writer methods for + the confirmation attribute if no reader is defined. + + Example: + + class Blog + def title=(new_title) + @title = new_title.downcase + end + + # previously this would override the setter above. + validates_confirmation_of :title + end + + *Yves Senn* ## Rails 4.0.0.beta1 (February 25, 2013) ## @@ -84,7 +111,7 @@ * Changed `ActiveModel::Serializers::Xml::Serializer#add_associations` to by default propagate `:skip_types, :dasherize, :camelize` keys to included associations. - It can be overriden on each association by explicitly specifying the option on one + It can be overridden on each association by explicitly specifying the option on one or more associations *Anthony Alberto* diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 6644b60609..9324a1ad0a 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -48,6 +48,8 @@ module ActiveModel attr_reader :password + include InstanceMethodsOnActivation + if options.fetch(:validations, true) validates_confirmation_of :password validates_presence_of :password, :on => :create @@ -55,8 +57,6 @@ module ActiveModel before_create { raise "Password digest missing on new record" if password_digest.blank? } end - include InstanceMethodsOnActivation - if respond_to?(:attributes_protected_by_default) def self.attributes_protected_by_default #:nodoc: super + ['password_digest'] @@ -99,6 +99,12 @@ module ActiveModel self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost) end end + + def password_confirmation=(unencrypted_password) + unless unencrypted_password.blank? + @password_confirmation = unencrypted_password + end + end end end end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 3a3abce364..d14fb4dc53 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -10,9 +10,13 @@ module ActiveModel end def setup(klass) - klass.send(:attr_accessor, *attributes.map do |attribute| + klass.send(:attr_reader, *attributes.map do |attribute| :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation") end.compact) + + klass.send(:attr_writer, *attributes.map do |attribute| + :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=") + end.compact) end end diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index baaf842222..d3ec78157c 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -194,7 +194,7 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_raises(NoMethodError) { ModelWithAttributes.new.foo } end - test 'acessing a suffixed attribute' do + test 'accessing a suffixed attribute' do m = ModelWithAttributes2.new m.attributes = { 'foo' => 'bar' } diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb index 086e7266ff..c4c34b0be7 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -107,7 +107,7 @@ class CallbacksTest < ActiveModel::TestCase test "after_create callbacks with both callbacks declared in one line" do assert_equal ["callback1", "callback2"], Violin1.new.create.history end - test "after_create callbacks with both callbacks declared in differnt lines" do + test "after_create callbacks with both callbacks declared in different lines" do assert_equal ["callback1", "callback2"], Violin2.new.create.history end diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index 38ba3cc152..aa683f4152 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -245,7 +245,7 @@ class NamingHelpersTest < ActiveModel::TestCase end def test_uncountable - assert uncountable?(@uncountable), "Expected 'sheep' to be uncoutable" + assert uncountable?(@uncountable), "Expected 'sheep' to be uncountable" assert !uncountable?(@klass), "Expected 'contact' to be countable" end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 7783bb25d5..02cd3b8a93 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -88,4 +88,10 @@ class SecurePasswordTest < ActiveModel::TestCase @user.password = "secret" assert_equal BCrypt::Engine::MIN_COST, @user.password_digest.cost end + + test "blank password_confirmation does not result in a confirmation error" do + @user.password = "" + @user.password_confirmation = "" + assert @user.valid?(:update), "user should be valid" + end end diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb index f7556a249f..814eec3f59 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -71,4 +71,35 @@ class ConfirmationValidationTest < ActiveModel::TestCase I18n.backend = @old_backend end + test "does not override confirmation reader if present" do + klass = Class.new do + include ActiveModel::Validations + + def title_confirmation + "expected title" + end + + validates_confirmation_of :title + end + + assert_equal "expected title", klass.new.title_confirmation, + "confirmation validation should not override the reader" + end + + test "does not override confirmation writer if present" do + klass = Class.new do + include ActiveModel::Validations + + def title_confirmation=(value) + @title_confirmation = "expected title" + end + + validates_confirmation_of :title + end + + model = klass.new + model.title_confirmation = "new title" + assert_equal "expected title", model.title_confirmation, + "confirmation validation should not override the writer" + end end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 457f553661..daa7a8d5c4 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -50,7 +50,7 @@ class ValidatesWithTest < ActiveModel::TestCase end end - test "vaidation with class that adds errors" do + test "validation with class that adds errors" do Topic.validates_with(ValidatorThatAddsErrors) topic = Topic.new assert topic.invalid?, "A class that adds errors causes the record to be invalid" diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index a9d32808da..2934e70c2f 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -26,11 +26,11 @@ class ValidationsTest < ActiveModel::TestCase def test_single_field_validation r = Reply.new r.title = "There's no content!" - assert r.invalid?, "A reply without content shouldn't be saveable" + assert r.invalid?, "A reply without content shouldn't be savable" assert r.after_validation_performed, "after_validation callback should be called" r.content = "Messa content!" - assert r.valid?, "A reply with content should be saveable" + assert r.valid?, "A reply with content should be savable" assert r.after_validation_performed, "after_validation callback should be called" end @@ -213,7 +213,7 @@ class ValidationsTest < ActiveModel::TestCase assert_equal 'is too short (minimum is 2 characters)', t.errors[key][0] end - def test_validaton_with_if_and_on + def test_validation_with_if_and_on Topic.validates_presence_of :title, :if => Proc.new{|x| x.author_name = "bad"; true }, :on => :update t = Topic.new(:title => "") @@ -361,7 +361,7 @@ class ValidationsTest < ActiveModel::TestCase def test_dup_validity_is_independent Topic.validates_presence_of :title - topic = Topic.new("title" => "Litterature") + topic = Topic.new("title" => "Literature") topic.valid? duped = topic.dup diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 97616ffc58..e5ab6bac58 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,7 +1,237 @@ ## Rails 4.0.0 (unreleased) ## +* `rake db:create` does not change permissions of the MySQL root user. + Fixes #8079. + + *Yves Senn* + +* The length of the `version` column in the `schema_migrations` table + created by the `mysql2` adapter is 191 if the encoding is "utf8mb4". + + The "utf8" encoding in MySQL has support for a maximum of 3 bytes per character, + and only contains characters from the BMP. The recently added + [utf8mb4](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html) + encoding extends the support to four bytes. As of this writing, said encoding + is supported in the betas of the `mysql2` gem. + + Setting the encoding to "utf8mb4" has + [a few implications](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-upgrading.html). + This change addresses the max length for indexes, which is 191 instead of 255. + + *Xavier Noria* + +* Counter caches on associations will now stay valid when attributes are + updated (not just when records are created or destroyed), for example, + when calling `update_attributes`. The following code now works: + + class Comment < ActiveRecord::Base + belongs_to :post, counter_cache: true + end + + class Post < ActiveRecord::Base + has_many :comments + end + + post = Post.create + comment = Comment.create + + post.comments << comment + post.save.reload.comments_count # => 1 + comment.update_attributes(post_id: nil) + + post.save.reload.comments_count # => 0 + + Updating the id of a `belongs_to` object with the id of a new object will + also keep the count accurate. + + *John Wang* + +* Referencing join tables implicitly was deprecated. There is a + possibility that these deprecation warnings are shown even if you + don't make use of that feature. You can now disable the feature entirely. + Fixes #9712. + + Example: + + # in your configuration + config.active_record.disable_implicit_join_references = true + + # or directly + ActiveRecord::Base.disable_implicit_join_references = true + + *Yves Senn* + +* The `:distinct` option for `Relation#count` is deprecated. You + should use `Relation#distinct` instead. + + Example: + + # Before + Post.select(:author_name).count(distinct: true) + + # After + Post.select(:author_name).distinct.count + + *Yves Senn* + +* Rename `Relation#uniq` to `Relation#distinct`. `#uniq` is still + available as an alias but we encourage to use `#distinct` instead. + Also `Relation#uniq_value` is aliased to `Relation#distinct_value`, + this is a temporary solution and you should migrate to `distinct_value`. + + *Yves Senn* + +* Fix quoting for sqlite migrations using copy_table_contents() with binary + columns. + + These would fail with "SQLite3::SQLException: unrecognized token" because + the column was not being passed to quote() so the data was not quoted + correctly. + + *Matthew M. Boedicker* + +* Promotes `change_column_null` to the migrations API. This macro sets/removes + `NOT NULL` constraints, and accepts an optional argument to replace existing + `NULL`s if needed. The adapters for SQLite, MySQL, PostgreSQL, and (at least) + Oracle, already implement this method. + + *Xavier Noria* + +* Uniqueness validation allows you to pass `:conditions` to limit + the constraint lookup. + + Example: + + validates_uniqueness_of :title, conditions: -> { where('approved = ?', true) } + + *Mattias Pfeiffer + Yves Senn* + +* `connection` is deprecated as an instance method. + This allows end-users to have a `connection` method on their models + without clashing with Active Record internals. + + *Ben Moss* + +* When copying migrations, preserve their magic comments and content encoding. + + *OZAWA Sakuro* + +* Fix `subclass_from_attrs` when `eager_load` is false. It cannot find + subclass because all classes are loaded automatically when it needs. + + *Dmitry Vorotilin* + +* When `:name` option is provided to `remove_index`, use it if there is no + index by the conventional name. + + For example, previously if an index was removed like so + `remove_index :values, column: :value, name: 'a_different_name'` + the generated SQL would not contain the specified index name, + and hence the migration would fail. + Fixes #8858. + + *Ezekiel Smithburg* + +* Created block to by-pass the prepared statement bindings. + This will allow to compose fragments of large SQL statements to + avoid multiple round-trips between Ruby and the DB. + + Example: + + sql = Post.connection.unprepared_statement do + Post.first.comments.to_sql + end + + *Cédric Fabianski* + +* Change the semantics of combining scopes to be the same as combining + class methods which return scopes. For example: + + class User < ActiveRecord::Base + scope :active, -> { where state: 'active' } + scope :inactive, -> { where state: 'inactive' } + end + + class Post < ActiveRecord::Base + def self.active + where state: 'active' + end + + def self.inactive + where state: 'inactive' + end + end + + ### BEFORE ### + + User.where(state: 'active').where(state: 'inactive') + # => SELECT * FROM users WHERE state = 'active' AND state = 'inactive' + + User.active.inactive + # => SELECT * FROM users WHERE state = 'inactive' + + Post.active.inactive + # => SELECT * FROM posts WHERE state = 'active' AND state = 'inactive' + + ### AFTER ### + + User.active.inactive + # => SELECT * FROM posts WHERE state = 'active' AND state = 'inactive' + + Before this change, invoking a scope would merge it into the current + scope and return the result. `Relation#merge` applies "last where + wins" logic to de-duplicate the conditions, but this lead to + confusing and inconsistent behaviour. This fixes that. + + If you really do want the "last where wins" logic, you can opt-in to + it like so: + + User.active.merge(User.inactive) + + Fixes #7365. + + *Neeraj Singh* and *Jon Leighton* + +* Expand `#cache_key` to consult all relevant updated timestamps. + + Previously only `updated_at` column was checked, now it will + consult other columns that received updated timestamps on save, + such as `updated_on`. When multiple columns are present it will + use the most recent timestamp. + Fixes #9033. + + *Brendon Murphy* + +* Throw `NotImplementedError` when trying to instantiate `ActiveRecord::Base` or an abstract class. + + *Aaron Weiner* + +* Warn when `rake db:structure:dump` with a MySQL database and + `mysqldump` is not in the PATH or fails. + Fixes #9518. + + *Yves Senn* + +* Remove `connection#structure_dump`, which is no longer used. *Yves Senn* + +* Make it possible to execute migrations without a transaction even + if the database adapter supports DDL transactions. + Fixes #9483. + + Example: + + class ChangeEnum < ActiveRecord::Migration + disable_ddl_transaction! + + def up + execute "ALTER TYPE model_size ADD VALUE 'new_value'" + end + end + + *Yves Senn* + * Assigning "0.0" to a nullable numeric column does not make it dirty. - Fix #9034. + Fixes #9034. Example: @@ -436,17 +666,17 @@ *Marc-André Lafortune* * Serialized attributes can be serialized in integer columns. - Fix #8575. + Fixes #8575. *Rafael Mendonça França* * Keep index names when using `alter_table` with sqlite3. - Fix #3489. + Fixes #3489. *Yves Senn* * Add ability for postgresql adapter to disable user triggers in `disable_referential_integrity`. - Fix #5523. + Fixes #5523. *Gary S. Weaver* @@ -469,7 +699,7 @@ *Matthew Robertson* * Recognize migrations placed in directories containing numbers and 'rb'. - Fix #8492 + Fixes #8492. *Yves Senn* @@ -527,13 +757,13 @@ * Fix performance problem with `primary_key` method in PostgreSQL adapter when having many schemas. Uses `pg_constraint` table instead of `pg_depend` table which has many records in general. - Fix #8414 + Fixes #8414. *kennyj* * Do not instantiate intermediate Active Record objects when eager loading. These records caused `after_find` to run more than expected. - Fix #3313 + Fixes #3313. *Yves Senn* @@ -554,12 +784,13 @@ * Fix dirty attribute checks for `TimeZoneConversion` with nil and blank datetime attributes. Setting a nil datetime to a blank string should not - result in a change being flagged. Fix #8310 + result in a change being flagged. + Fixes #8310. *Alisdair McDiarmid* * Prevent mass assignment to the type column of polymorphic associations when using `build` - Fix #8265 + Fixes #8265. *Yves Senn* @@ -612,7 +843,7 @@ *Bogdan Gusiev* * `:counter_cache` option for `has_many` associations to support custom named counter caches. - Fix #7993 + Fixes #7993. *Yves Senn* @@ -636,7 +867,7 @@ *Nikita Afanasenko* * Use query cache/uncache when using `DATABASE_URL`. - Fix #6951. + Fixes #6951. *kennyj* @@ -645,7 +876,7 @@ *Henrik Nyh* * The `create_table` method raises an `ArgumentError` when the primary key column is redefined. - Fix #6378 + Fixes #6378. *Yves Senn* @@ -755,7 +986,7 @@ *Alexey Muranov* * The postgres adapter now supports tables with capital letters. - Fix #5920 + Fixes #5920. *Yves Senn* @@ -777,7 +1008,7 @@ *Francesco Rodriguez* * Fix `reset_counters` crashing on `has_many :through` associations. - Fix #7822. + Fixes #7822. *lulalala* @@ -852,7 +1083,7 @@ *Guillermo Iguaran* * Fix the return of querying with an empty hash. - Fix #6971. + Fixes #6971. User.where(token: {}) @@ -868,7 +1099,7 @@ * Fix creation of through association models when using `collection=[]` on a `has_many :through` association from an unsaved model. - Fix #7661. + Fixes #7661. *Ernie Miller* diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 89a62f0873..3e3475f709 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,6 +24,6 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 4.0.0.beta1' + s.add_dependency 'arel', '~> 4.0.0.beta2' s.add_dependency 'activerecord-deprecated_finders', '~> 0.0.3' end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 35e4eb19a4..0c670bdaa1 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -241,6 +241,7 @@ module ActiveRecord # others.destroy_all | X | X | X # others.find(*args) | X | X | X # others.exists? | X | X | X + # others.distinct | X | X | X # others.uniq | X | X | X # others.reset | X | X | X # @@ -1024,7 +1025,7 @@ module ActiveRecord # [collection<<(object, ...)] # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. # Note that this operation instantly fires update sql without waiting for the save or update call on the - # parent object. + # parent object, unless the parent object is a new record. # [collection.delete(object, ...)] # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>, @@ -1231,7 +1232,7 @@ module ActiveRecord # its owner is destroyed: # # * <tt>:destroy</tt> causes the associated object to also be destroyed - # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute) + # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute) # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object @@ -1407,6 +1408,8 @@ module ActiveRecord # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the # custom <tt>:join_table</tt> option if you need to. + # If your tables share a common prefix, it will only appear once at the beginning. For example, + # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products". # # The join table should not have a primary key or a model associated with it. You must manually generate the # join table with a migration such as this: @@ -1433,7 +1436,7 @@ module ActiveRecord # Adds one or more objects to the collection by creating associations in the join table # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). # Note that this operation instantly fires update sql without waiting for the save or update call on the - # parent object. + # parent object, unless the parent object is a new record. # [collection.delete(object, ...)] # Removes one or more objects from the collection by removing their associations from the join table. # This does not destroy the objects. diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index c5fb1fe2c7..a9525436fb 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -22,7 +22,7 @@ module ActiveRecord private def column_for(table_name, column_name) - columns = alias_tracker.connection.schema_cache.columns_hash[table_name] + columns = alias_tracker.connection.schema_cache.columns_hash(table_name) columns[column_name] end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 97b1ff18e2..fbcb21118d 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -21,11 +21,13 @@ module ActiveRecord::Associations::Builder def add_counter_cache_callbacks(reflection) cache_column = reflection.counter_cache_column + foreign_key = reflection.foreign_key mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def belongs_to_counter_cache_after_create_for_#{name} record = #{name} record.class.increment_counter(:#{cache_column}, record.id) unless record.nil? + @_after_create_counter_called = true end def belongs_to_counter_cache_before_destroy_for_#{name} @@ -34,10 +36,28 @@ module ActiveRecord::Associations::Builder record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? end end + + def belongs_to_counter_cache_after_update_for_#{name} + if (@_after_create_counter_called ||= false) + @_after_create_counter_called = false + elsif self.#{foreign_key}_changed? && !new_record? && defined?(#{name.to_s.camelize}) + model = #{name.to_s.camelize} + foreign_key_was = self.#{foreign_key}_was + foreign_key = self.#{foreign_key} + + if foreign_key && model.respond_to?(:increment_counter) + model.increment_counter(:#{cache_column}, foreign_key) + end + if foreign_key_was && model.respond_to?(:decrement_counter) + model.decrement_counter(:#{cache_column}, foreign_key_was) + end + end + end CODE model.after_create "belongs_to_counter_cache_after_create_for_#{name}" model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" + model.after_update "belongs_to_counter_cache_after_update_for_#{name}" klass = reflection.class_name.safe_constantize klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 5feb149946..906560bd44 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -34,7 +34,7 @@ module ActiveRecord reload end - CollectionProxy.new(klass, self) + @proxy ||= CollectionProxy.new(klass, self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -174,13 +174,14 @@ module ActiveRecord reflection.klass.count_by_sql(custom_counter_sql) else - if association_scope.uniq_value + relation = scope + if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name ||= reflection.klass.primary_key - count_options[:distinct] = true + relation = relation.distinct end - value = scope.count(column_name, count_options) + value = relation.count(column_name) limit = options[:limit] offset = options[:offset] @@ -204,6 +205,15 @@ module ActiveRecord dependent = options[:dependent] if records.first == :all + + if dependent && dependent == :destroy + message = 'In Rails 4.1 delete_all on associations would not fire callbacks. ' \ + 'It means if the :dependent option is :destroy then the associated ' \ + 'records would be deleted without loading and invoking callbacks.' + + ActiveRecord::Base.logger ? ActiveRecord::Base.logger.warn(message) : $stderr.puts(message) + end + if loaded? || dependent == :destroy delete_or_destroy(load_target, dependent) else @@ -237,14 +247,14 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if association_scope.uniq_value + if association_scope.distinct_value target.uniq.size else target.size end elsif !loaded? && !association_scope.group_values.empty? load_target.size - elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array) + elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) unsaved_records = target.select { |r| r.new_record? } unsaved_records.size + count_records else @@ -297,12 +307,13 @@ module ActiveRecord end end - def uniq + def distinct seen = {} load_target.find_all do |record| seen[record.id] = true unless seen.key?(record.id) end end + alias uniq distinct # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. @@ -343,7 +354,7 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? - if association_scope.uniq_value && index = @target.index(record) + if association_scope.distinct_value && index = @target.index(record) @target[index] = record else @target << record diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 543204abac..c2add32aa6 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -649,11 +649,12 @@ module ActiveRecord # # #<Pet name: "Fancy-Fancy"> # # ] # - # person.pets.select(:name).uniq + # person.pets.select(:name).distinct # # => [#<Pet name: "Fancy-Fancy">] - def uniq - @association.uniq + def distinct + @association.distinct end + alias uniq distinct # Count all records using SQL. # diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 93618721bb..bb3e3db379 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -26,7 +26,7 @@ module ActiveRecord join_table[reflection.association_foreign_key] => record.id ) - owner.connection.insert stmt + owner.class.connection.insert stmt end record @@ -41,7 +41,7 @@ module ActiveRecord def delete_records(records, method) if sql = options[:delete_sql] records = load_target if records == :all - records.each { |record| owner.connection.delete(interpolate(sql, record)) } + records.each { |record| owner.class.connection.delete(interpolate(sql, record)) } else relation = join_table condition = relation[reflection.foreign_key].eq(owner.id) @@ -53,7 +53,7 @@ module ActiveRecord ) end - owner.connection.delete(relation.where(condition).compile_delete) + owner.class.connection.delete(relation.where(condition).compile_delete) end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index f59565ae77..b7b4d7e3ae 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -23,9 +23,10 @@ module ActiveRecord if options[:dependent] == :destroy # No point in executing the counter update since we're going to destroy the parent anyway load_target.each(&:mark_for_destruction) + destroy_all + else + delete_all end - - delete_all end end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index 9a662d3f53..38bc7ce7da 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -6,7 +6,7 @@ module ActiveRecord def associated_records_by_owner super.each do |owner, records| - records.uniq! if reflection_scope.uniq_value + records.uniq! if reflection_scope.distinct_value end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 3e454b713a..931209b07b 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -90,7 +90,7 @@ module ActiveRecord base_name.foreign_key else if ActiveRecord::Base != self && table_exists? - connection.schema_cache.primary_keys[table_name] + connection.schema_cache.primary_keys(table_name) else 'id' end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 1754e424b8..9137504d15 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -238,7 +238,7 @@ module ActiveRecord @checkout_timeout = spec.config[:checkout_timeout] || 5 @dead_connection_timeout = spec.config[:dead_connection_timeout] || 5 - @reaper = Reaper.new self, spec.config[:reaping_frequency] + @reaper = Reaper.new(self, spec.config[:reaping_frequency] || 10) @reaper.run # default max pool size to 5 diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index c3d15ca929..c64b542286 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -125,7 +125,8 @@ module ActiveRecord # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html - # Savepoints are supported by MySQL and PostgreSQL, but not SQLite3. + # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' + # supports savepoints. # # It is safe to call this method if a database transaction is already open, # i.e. if #transaction is called within another #transaction block. In case diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 42206de8fc..902dbd148e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -16,7 +16,6 @@ module ActiveRecord # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc: - def string_to_binary(value) value end @@ -25,19 +24,24 @@ module ActiveRecord base.type_to_sql(type.to_sym, limit, precision, scale) end + def primary_key? + type.to_sym == :primary_key + end + def to_sql column_sql = "#{base.quote_column_name(name)} #{sql_type}" column_options = {} column_options[:null] = null unless null.nil? column_options[:default] = default unless default.nil? - add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key + column_options[:column] = self + add_column_options!(column_sql, column_options) unless primary_key? column_sql end private def add_column_options!(sql, options) - base.add_column_options!(sql, options.merge(:column => self)) + base.add_column_options!(sql, options) end end @@ -73,15 +77,6 @@ module ActiveRecord @base = base end - def xml(*args) - raise NotImplementedError unless %w{ - sqlite mysql mysql2 - }.include? @base.adapter_name.downcase - - options = args.extract_options! - column(args[0], :text, options) - end - # Appends a primary key definition to the table definition. # Can be called multiple times, but this is probably not a good idea. def primary_key(name) @@ -292,19 +287,23 @@ module ActiveRecord # concatenated together. This string can then be prepended and appended to # to generate the final SQL to create the table. def to_sql - @columns.map { |c| c.to_sql } * ', ' + columns.map { |c| c.to_sql } * ', ' end private + def create_column_definition(base, name, type) + ColumnDefinition.new base, name, type + end + def new_column_definition(base, name, type) - definition = ColumnDefinition.new base, name, type + definition = create_column_definition base, name, type @columns << definition @columns_hash[name] = definition definition end def primary_key_column_name - primary_key_column = columns.detect { |c| c.type == :primary_key } + primary_key_column = columns.detect { |c| c.primary_key? } primary_key_column && primary_key_column.name end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 0cce8c7596..cd4409295f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -5,7 +5,7 @@ module ActiveRecord module SchemaStatements include ActiveRecord::Migration::JoinTable - # Returns a Hash of mappings from the abstract data types to the native + # Returns a hash of mappings from the abstract data types to the native # database types. See TableDefinition#column for details on the recognized # abstract data types. def native_database_types @@ -20,6 +20,7 @@ module ActiveRecord # Checks to see if the table +table_name+ exists on the database. # # table_exists?(:developers) + # def table_exists?(table_name) tables.include?(table_name.to_s) end @@ -29,17 +30,18 @@ module ActiveRecord # Checks to see if an index exists on a table for a given index definition. # - # # Check an index exists - # index_exists?(:suppliers, :company_id) + # # Check an index exists + # index_exists?(:suppliers, :company_id) + # + # # Check an index on multiple columns exists + # index_exists?(:suppliers, [:company_id, :company_type]) # - # # Check an index on multiple columns exists - # index_exists?(:suppliers, [:company_id, :company_type]) + # # Check a unique index exists + # index_exists?(:suppliers, :company_id, unique: true) # - # # Check a unique index exists - # index_exists?(:suppliers, :company_id, unique: true) + # # Check an index with a custom name exists + # index_exists?(:suppliers, :company_id, name: "idx_company_id" # - # # Check an index with a custom name exists - # index_exists?(:suppliers, :company_id, name: "idx_company_id" def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name) index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) @@ -56,17 +58,18 @@ module ActiveRecord # Checks to see if a column exists in a given table. # - # # Check a column exists - # column_exists?(:suppliers, :name) + # # Check a column exists + # column_exists?(:suppliers, :name) # - # # Check a column exists of a particular type - # column_exists?(:suppliers, :name, :string) + # # Check a column exists of a particular type + # column_exists?(:suppliers, :name, :string) + # + # # Check a column exists with a specific definition + # column_exists?(:suppliers, :name, :string, limit: 100) + # column_exists?(:suppliers, :name, :string, default: 'default') + # column_exists?(:suppliers, :name, :string, null: false) + # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) # - # # Check a column exists with a specific definition - # column_exists?(:suppliers, :name, :string, limit: 100) - # column_exists?(:suppliers, :name, :string, default: 'default') - # column_exists?(:suppliers, :name, :string, null: false) - # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) def column_exists?(table_name, column_name, type = nil, options = {}) columns(table_name).any?{ |c| c.name == column_name.to_s && (!type || c.type == type) && @@ -84,27 +87,30 @@ module ActiveRecord # form or the regular form, like this: # # === Block form - # # create_table() passes a TableDefinition object to the block. - # # This form will not only create the table, but also columns for the - # # table. # - # create_table(:suppliers) do |t| - # t.column :name, :string, limit: 60 - # # Other fields here - # end + # # create_table() passes a TableDefinition object to the block. + # # This form will not only create the table, but also columns for the + # # table. + # + # create_table(:suppliers) do |t| + # t.column :name, :string, limit: 60 + # # Other fields here + # end # # === Block form, with shorthand - # # You can also use the column types as method calls, rather than calling the column method. - # create_table(:suppliers) do |t| - # t.string :name, limit: 60 - # # Other fields here - # end + # + # # You can also use the column types as method calls, rather than calling the column method. + # create_table(:suppliers) do |t| + # t.string :name, limit: 60 + # # Other fields here + # end # # === Regular form - # # Creates a table called 'suppliers' with no columns. - # create_table(:suppliers) - # # Add a column to 'suppliers'. - # add_column(:suppliers, :name, :string, {limit: 60}) + # + # # Creates a table called 'suppliers' with no columns. + # create_table(:suppliers) + # # Add a column to 'suppliers'. + # add_column(:suppliers, :name, :string, {limit: 60}) # # The +options+ hash can include the following keys: # [<tt>:id</tt>] @@ -127,37 +133,53 @@ module ActiveRecord # Defaults to false. # # ====== Add a backend specific option to the generated SQL (MySQL) - # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # + # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # # generates: - # CREATE TABLE suppliers ( - # id int(11) DEFAULT NULL auto_increment PRIMARY KEY - # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # + # CREATE TABLE suppliers ( + # id int(11) DEFAULT NULL auto_increment PRIMARY KEY + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column - # create_table(:objects, primary_key: 'guid') do |t| - # t.column :name, :string, limit: 80 - # end + # + # create_table(:objects, primary_key: 'guid') do |t| + # t.column :name, :string, limit: 80 + # end + # # generates: - # CREATE TABLE objects ( - # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, - # name varchar(80) - # ) + # + # CREATE TABLE objects ( + # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, + # name varchar(80) + # ) # # ====== Do not add a primary key column - # create_table(:categories_suppliers, id: false) do |t| - # t.column :category_id, :integer - # t.column :supplier_id, :integer - # end + # + # create_table(:categories_suppliers, id: false) do |t| + # t.column :category_id, :integer + # t.column :supplier_id, :integer + # end + # # generates: - # CREATE TABLE categories_suppliers ( - # category_id int, - # supplier_id int - # ) + # + # CREATE TABLE categories_suppliers ( + # category_id int, + # supplier_id int + # ) # # See also TableDefinition#column for details on how to create columns. def create_table(table_name, options = {}) td = create_table_definition - td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false + + unless options[:id] == false + pk = options.fetch(:primary_key) { + Base.get_primary_key table_name.to_s.singularize + } + + td.primary_key pk + end yield td if block_given? @@ -176,8 +198,8 @@ module ActiveRecord # Creates a new join table with the name created using the lexical order of the first two # arguments. These arguments can be a String or a Symbol. # - # # Creates a table called 'assemblies_parts' with no id. - # create_join_table(:assemblies, :parts) + # # Creates a table called 'assemblies_parts' with no id. + # create_join_table(:assemblies, :parts) # # You can pass a +options+ hash can include the following keys: # [<tt>:table_name</tt>] @@ -201,12 +223,16 @@ module ActiveRecord # end # # ====== Add a backend specific option to the generated SQL (MySQL) - # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # + # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # # generates: - # CREATE TABLE assemblies_parts ( - # assembly_id int NOT NULL, - # part_id int NOT NULL, - # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # + # CREATE TABLE assemblies_parts ( + # assembly_id int NOT NULL, + # part_id int NOT NULL, + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # def create_join_table(table_1, table_2, options = {}) join_table_name = find_join_table_name(table_1, table_2, options) @@ -223,7 +249,7 @@ module ActiveRecord end # Drops the join table specified by the given arguments. - # See create_join_table for details. + # See +create_join_table+ for details. # # Although this command ignores the block if one is given, it can be helpful # to provide one in a migration's +change+ method so it can be reverted. @@ -235,66 +261,74 @@ module ActiveRecord # A block for changing columns in +table+. # - # # change_table() yields a Table instance - # change_table(:suppliers) do |t| - # t.column :name, :string, limit: 60 - # # Other column alterations here - # end + # # change_table() yields a Table instance + # change_table(:suppliers) do |t| + # t.column :name, :string, limit: 60 + # # Other column alterations here + # end # # The +options+ hash can include the following keys: # [<tt>:bulk</tt>] # Set this to true to make this a bulk alter query, such as - # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # + # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... # # Defaults to false. # # ====== Add a column - # change_table(:suppliers) do |t| - # t.column :name, :string, limit: 60 - # end + # + # change_table(:suppliers) do |t| + # t.column :name, :string, limit: 60 + # end # # ====== Add 2 integer columns - # change_table(:suppliers) do |t| - # t.integer :width, :height, null: false, default: 0 - # end + # + # change_table(:suppliers) do |t| + # t.integer :width, :height, null: false, default: 0 + # end # # ====== Add created_at/updated_at columns - # change_table(:suppliers) do |t| - # t.timestamps - # end + # + # change_table(:suppliers) do |t| + # t.timestamps + # end # # ====== Add a foreign key column - # change_table(:suppliers) do |t| - # t.references :company - # end # - # Creates a <tt>company_id(integer)</tt> column + # change_table(:suppliers) do |t| + # t.references :company + # end + # + # Creates a <tt>company_id(integer)</tt> column. # # ====== Add a polymorphic foreign key column + # # change_table(:suppliers) do |t| # t.belongs_to :company, polymorphic: true # end # - # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns + # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns. # # ====== Remove a column + # # change_table(:suppliers) do |t| # t.remove :company # end # # ====== Remove several columns + # # change_table(:suppliers) do |t| # t.remove :company_id # t.remove :width, :height # end # # ====== Remove an index + # # change_table(:suppliers) do |t| # t.remove_index :company_id # end # - # See also Table for details on - # all of the various column transformation + # See also Table for details on all of the various column transformation. def change_table(table_name, options = {}) if supports_bulk_alter? && options[:bulk] recorder = ActiveRecord::Migration::CommandRecorder.new(self) @@ -307,7 +341,8 @@ module ActiveRecord # Renames a table. # - # rename_table('octopuses', 'octopi') + # rename_table('octopuses', 'octopi') + # def rename_table(table_name, new_name) raise NotImplementedError, "rename_table is not implemented" end @@ -331,7 +366,8 @@ module ActiveRecord # Removes the given columns from the table definition. # - # remove_columns(:suppliers, :qualification, :experience) + # remove_columns(:suppliers, :qualification, :experience) + # def remove_columns(table_name, *column_names) raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.empty? column_names.each do |column_name| @@ -341,7 +377,7 @@ module ActiveRecord # Removes the column from the table definition. # - # remove_column(:suppliers, :qualification) + # remove_column(:suppliers, :qualification) # # The +type+ and +options+ parameters will be ignored if present. It can be helpful # to provide these in a migration's +change+ method so it can be reverted. @@ -353,24 +389,50 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. # - # change_column(:suppliers, :name, :string, limit: 80) - # change_column(:accounts, :description, :text) + # change_column(:suppliers, :name, :string, limit: 80) + # change_column(:accounts, :description, :text) + # def change_column(table_name, column_name, type, options = {}) raise NotImplementedError, "change_column is not implemented" end - # Sets a new default value for a column. + # Sets a new default value for a column: + # + # change_column_default(:suppliers, :qualification, 'new') + # change_column_default(:accounts, :authorized, 1) + # + # Setting the default to +nil+ effectively drops the default: + # + # change_column_default(:users, :email, nil) # - # change_column_default(:suppliers, :qualification, 'new') - # change_column_default(:accounts, :authorized, 1) - # change_column_default(:users, :email, nil) def change_column_default(table_name, column_name, default) raise NotImplementedError, "change_column_default is not implemented" end + # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag + # indicates whether the value can be +NULL+. For example + # + # change_column_null(:users, :nickname, false) + # + # says nicknames cannot be +NULL+ (adds the constraint), whereas + # + # change_column_null(:users, :nickname, true) + # + # allows them to be +NULL+ (drops the constraint). + # + # The method accepts an optional fourth argument to replace existing + # +NULL+s with some other value. Use that one when enabling the + # constraint if needed, since otherwise those rows would not be valid. + # + # Please note the fourth argument does not set a column's default. + def change_column_null(table_name, column_name, null, default = nil) + raise NotImplementedError, "change_column_null is not implemented" + end + # Renames a column. # - # rename_column(:suppliers, :description, :name) + # rename_column(:suppliers, :description, :name) + # def rename_column(table_name, column_name, new_column_name) raise NotImplementedError, "rename_column is not implemented" end @@ -382,60 +444,87 @@ module ActiveRecord # you pass <tt>:name</tt> as an option. # # ====== Creating a simple index - # add_index(:suppliers, :name) + # + # add_index(:suppliers, :name) + # # generates - # CREATE INDEX suppliers_name_index ON suppliers(name) + # + # CREATE INDEX suppliers_name_index ON suppliers(name) # # ====== Creating a unique index - # add_index(:accounts, [:branch_id, :party_id], unique: true) + # + # add_index(:accounts, [:branch_id, :party_id], unique: true) + # # generates - # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id) + # + # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id) # # ====== Creating a named index - # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party') + # + # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party') + # # generates + # # CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id) # # ====== Creating an index with specific key length - # add_index(:accounts, :name, name: 'by_name', length: 10) + # + # add_index(:accounts, :name, name: 'by_name', length: 10) + # # generates - # CREATE INDEX by_name ON accounts(name(10)) # - # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) + # CREATE INDEX by_name ON accounts(name(10)) + # + # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) + # # generates - # CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) # - # Note: SQLite doesn't support index length + # CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) + # + # Note: SQLite doesn't support index length. # # ====== Creating an index with a sort order (desc or asc, asc is the default) - # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc}) + # + # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc}) + # # generates - # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # - # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it) + # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) + # + # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it). # # ====== Creating a partial index - # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active") + # + # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active") + # # generates - # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # - # Note: only supported by PostgreSQL + # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # + # Note: only supported by PostgreSQL. def add_index(table_name, column_name, options = {}) index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" end - # Remove the given index from the table. + # Removes the given index from the table. + # + # Removes the +index_accounts_on_column+ in the +accounts+ table. # - # Remove the index_accounts_on_column in the accounts table. # remove_index :accounts, :column - # Remove the index named index_accounts_on_branch_id in the accounts table. + # + # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. + # # remove_index :accounts, column: :branch_id - # Remove the index named index_accounts_on_branch_id_and_party_id in the accounts table. + # + # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table. + # # remove_index :accounts, column: [:branch_id, :party_id] - # Remove the index named by_branch_party in the accounts table. + # + # Removes the index named +by_branch_party+ in the +accounts+ table. + # # remove_index :accounts, name: :by_branch_party + # def remove_index(table_name, options = {}) remove_index!(table_name, index_name_for_remove(table_name, options)) end @@ -444,10 +533,12 @@ module ActiveRecord execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" end - # Rename an index. + # Renames an index. + # + # Rename the +index_people_on_last_name+ index to +index_users_on_last_name+: # - # Rename the index_people_on_last_name index to index_users_on_last_name # rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name' + # def rename_index(table_name, old_name, new_name) # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } @@ -470,7 +561,7 @@ module ActiveRecord end end - # Verify the existence of an index with a given name. + # Verifies the existence of an index with a given name. # # The default argument is returned if the underlying implementation does not define the indexes method, # as there's no way to determine the correct answer in that case. @@ -484,13 +575,16 @@ module ActiveRecord # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. # # ====== Create a user_id column - # add_reference(:products, :user) + # + # add_reference(:products, :user) # # ====== Create a supplier_id and supplier_type columns - # add_belongs_to(:products, :supplier, polymorphic: true) + # + # add_belongs_to(:products, :supplier, polymorphic: true) # # ====== Create a supplier_id, supplier_type columns and appropriate index - # add_reference(:products, :supplier, polymorphic: true, index: true) + # + # add_reference(:products, :supplier, polymorphic: true, index: true) # def add_reference(table_name, ref_name, options = {}) polymorphic = options.delete(:polymorphic) @@ -505,10 +599,12 @@ module ActiveRecord # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. # # ====== Remove the reference - # remove_reference(:products, :user, index: true) + # + # remove_reference(:products, :user, index: true) # # ====== Remove polymorphic reference - # remove_reference(:products, :supplier, polymorphic: true) + # + # remove_reference(:products, :supplier, polymorphic: true) # def remove_reference(table_name, ref_name, options = {}) remove_column(table_name, "#{ref_name}_id") @@ -516,11 +612,6 @@ module ActiveRecord end alias :remove_belongs_to :remove_reference - # Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the - # entire structure of the database. - def structure_dump - end - def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -600,21 +691,24 @@ module ActiveRecord # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax. # # distinct("posts.id", "posts.created_at desc") + # def distinct(columns, order_by) "DISTINCT #{columns}" end - # Adds timestamps (created_at and updated_at) columns to the named table. + # Adds timestamps (+created_at+ and +updated_at+) columns to the named table. + # + # add_timestamps(:suppliers) # - # add_timestamps(:suppliers) def add_timestamps(table_name) add_column table_name, :created_at, :datetime add_column table_name, :updated_at, :datetime end - # Removes the timestamp columns (created_at and updated_at) from the table definition. + # Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition. # # remove_timestamps(:suppliers) + # def remove_timestamps(table_name) remove_column table_name, :updated_at remove_column table_name, :created_at @@ -692,6 +786,14 @@ module ActiveRecord index_name = index_name(table_name, options) unless index_name_exists?(table_name, index_name, true) + if options.is_a?(Hash) && options.has_key?(:name) + options_without_column = options.dup + options_without_column.delete :column + index_name_without_column = index_name(table_name, options_without_column) + + return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + end + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ff9de712bc..7949bcb5ce 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -118,6 +118,17 @@ module ActiveRecord @in_use = false end + def unprepared_visitor + self.class::BindSubstitution.new self + end + + def unprepared_statement + old, @visitor = @visitor, unprepared_visitor + yield + ensure + @visitor = old + end + # Returns the human-readable name of the adapter. Use mixed case - one # can always use downcase if needed. def adapter_name diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 9826b18053..f88f5742a8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -143,7 +143,7 @@ module ActiveRecord if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::MySQL.new self else - @visitor = BindSubstitution.new self + @visitor = unprepared_visitor end end @@ -332,20 +332,6 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def structure_dump #:nodoc: - if supports_views? - sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" - else - sql = "SHOW TABLES" - end - - select_all(sql, 'SCHEMA').map { |table| - table.delete('Table_type') - sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" - exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n" - }.join - end - # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. def recreate_database(name, options = {}) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 20a5ca2baa..25b8aef617 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -38,6 +38,15 @@ module ActiveRecord configure_connection end + MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 + def initialize_schema_migrations_table + if @config[:encoding] == 'utf8mb4' + ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4) + else + ActiveRecord::SchemaMigration.create_table + end + end + def supports_explain? true end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 47e2e3928f..43f991b362 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -51,9 +51,12 @@ module ActiveRecord super end when Numeric - return super unless column.sql_type == 'money' - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" + if column.sql_type == 'money' || [:string, :text].include?(column.type) + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + else + super + end when String case column.sql_type when 'bytea' then "'#{escape_bytea(value)}'" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index c91e1b3fb9..dfa4c3967a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -338,13 +338,14 @@ module ActiveRecord self end + def xml(options = {}) + column(args[0], :text, options) + end + private - def new_column_definition(base, name, type) - definition = ColumnDefinition.new base, name, type - @columns << definition - @columns_hash[name] = definition - definition + def create_column_definition(base, name, type) + ColumnDefinition.new base, name, type end end @@ -489,7 +490,7 @@ module ActiveRecord if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::PostgreSQL.new self else - @visitor = BindSubstitution.new self + @visitor = unprepared_visitor end @connection_parameters, @config = connection_parameters, config @@ -594,13 +595,13 @@ module ActiveRecord end def enable_extension(name) - exec_query("CREATE EXTENSION IF NOT EXISTS #{name}").tap { + exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap { reload_type_map } end def disable_extension(name) - exec_query("DROP EXTENSION IF EXISTS #{name} CASCADE").tap { + exec_query("DROP EXTENSION IF EXISTS \"#{name}\" CASCADE").tap { reload_type_map } end @@ -631,7 +632,13 @@ module ActiveRecord if options[:array] || options[:column].try(:array) sql << '[]' end - super + + column = options.fetch(:column) { return super } + if column.type == :uuid && options[:default] =~ /\(\)/ + sql << " DEFAULT #{options[:default]}" + else + super + end end # Set the authorized user for this session diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 5839d1d3b4..1d7a22e831 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,7 +1,9 @@ +require 'active_support/deprecation/reporting' + module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :primary_keys, :tables, :version + attr_reader :version attr_accessor :connection def initialize(conn) @@ -14,6 +16,15 @@ module ActiveRecord prepare_default_proc end + def primary_keys(table_name = nil) + if table_name + @primary_keys[table_name] + else + ActiveSupport::Deprecation.warn('call primary_keys with a table name!') + @primary_keys.dup + end + end + # A cached lookup for table existence. def table_exists?(name) return @tables[name] if @tables.key? name @@ -30,12 +41,22 @@ module ActiveRecord end end + def tables(name = nil) + if name + @tables[name] + else + ActiveSupport::Deprecation.warn('call tables with a name!') + @tables.dup + end + end + # Get the columns for a table def columns(table = nil) if table @columns[table] else - @columns + ActiveSupport::Deprecation.warn('call columns with a table name!') + @columns.dup end end @@ -45,7 +66,8 @@ module ActiveRecord if table @columns_hash[table] else - @columns_hash + ActiveSupport::Deprecation.warn('call columns_hash with a table name!') + @columns_hash.dup end end @@ -58,6 +80,12 @@ module ActiveRecord @version = nil end + def size + [@columns, @columns_hash, @primary_keys, @tables].map { |x| + x.size + }.inject :+ + end + # Clear out internal caches for table with +table_name+. def clear_table_cache!(table_name) @columns.delete table_name @@ -69,9 +97,9 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val| - self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h } - end + [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val| + Hash[val] + } end def marshal_load(array) @@ -87,7 +115,7 @@ module ActiveRecord end @columns_hash.default_proc = Proc.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| + h[table_name] = Hash[columns(table_name).map { |col| [col.name, col] }] end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 981c4c96a0..d3ffee3a8b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -113,7 +113,7 @@ module ActiveRecord if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::SQLite.new self else - @visitor = BindSubstitution.new self + @visitor = unprepared_visitor end end @@ -583,9 +583,17 @@ module ActiveRecord quoted_columns = columns.map { |col| quote_column_name(col) } * ',' quoted_to = quote_table_name(to) + + raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }] + exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' + + column_values = columns.map do |col| + quote(row[column_mappings[col]], raw_column_mappings[col]) + end + + sql << column_values * ', ' sql << ')' exec_query sql end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 899fe7d7c7..aa56219755 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -69,6 +69,14 @@ module ActiveRecord mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true + ## + # :singleton-method: + # Disable implicit join references. This feature was deprecated with Rails 4. + # If you don't make use of implicit references but still see deprecation warnings + # you can disable the feature entirely. This will be the default with Rails 4.1. + mattr_accessor :disable_implicit_join_references, instance_writer: false + self.disable_implicit_join_references = false + class_attribute :connection_handler, instance_writer: false self.connection_handler = ConnectionAdapters::ConnectionHandler.new end @@ -324,6 +332,7 @@ module ActiveRecord # also be used to "borrow" the connection to do database work that isn't # easily done without going straight to SQL. def connection + ActiveSupport::Deprecation.warn("#connection is deprecated in favour of accessing it via the class") self.class.connection end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 81f92db271..81cca37939 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -11,7 +11,7 @@ module ActiveRecord # ==== Parameters # # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more counter names to reset + # * +counters+ - One or more association counters to reset # # ==== Examples # @@ -21,6 +21,7 @@ module ActiveRecord object = find(id) counters.each do |association| has_many_association = reflect_on_association(association.to_sym) + raise ArgumentError, "'#{self.name}' has no association called '#{association}'" unless has_many_association if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection has_many_association = has_many_association.through_reflection diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index b2a9a54af1..3135465dfe 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -6,7 +6,8 @@ module ActiveRecord def collecting_queries_for_explain # :nodoc: current = Thread.current original, current[:available_queries_for_explain] = current[:available_queries_for_explain], [] - return yield, current[:available_queries_for_explain] + yield + return current[:available_queries_for_explain] ensure # Note that the return value above does not depend on this assigment. current[:available_queries_for_explain] = original diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index e630897a4b..f54865c86e 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -15,6 +15,9 @@ module ActiveRecord # and if the inheritance column is attr accessible, it initializes an # instance of the given subclass instead of the base class def new(*args, &block) + if abstract_class? || self == Base + raise NotImplementedError, "#{self} is an abstract class and can not be instantiated." + end if (attrs = args.first).is_a?(Hash) if subclass = subclass_from_attrs(attrs) return subclass.new(*args, &block) @@ -167,11 +170,16 @@ module ActiveRecord # this will ignore the inheritance column and return nil def subclass_from_attrs(attrs) subclass_name = attrs.with_indifferent_access[inheritance_column] - return nil if subclass_name.blank? || subclass_name == self.name - unless subclass = subclasses.detect { |sub| sub.name == subclass_name } - raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}") + + if subclass_name.present? && subclass_name != self.name + subclass = subclass_name.safe_constantize + + unless subclasses.include?(subclass) + raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}") + end + + subclass end - subclass end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 32d35f0ec1..48c73d7781 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -49,7 +49,7 @@ module ActiveRecord case when new_record? "#{self.class.model_name.cache_key}/new" - when timestamp = self[:updated_at] + when timestamp = max_updated_column_timestamp timestamp = timestamp.utc.to_s(cache_timestamp_format) "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" else diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 701949e57b..209de78898 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -86,7 +86,7 @@ module ActiveRecord ) ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) - affected_rows = connection.update stmt + affected_rows = self.class.connection.update stmt unless affected_rows == 1 raise ActiveRecord::StaleObjectError.new(self, "update") @@ -117,7 +117,7 @@ module ActiveRecord if locking_enabled? column_name = self.class.locking_column column = self.class.columns_hash[column_name] - substitute = connection.substitute_at(column, relation.bind_values.length) + substitute = self.class.connection.substitute_at(column, relation.bind_values.length) relation = relation.where(self.class.arel_table[column_name].eq(substitute)) relation.bind_values << [column, self[column_name].to_i] diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 823595a128..5d7762ec3a 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -330,6 +330,24 @@ module ActiveRecord # # For a list of commands that are reversible, please see # <tt>ActiveRecord::Migration::CommandRecorder</tt>. + # + # == Transactional Migrations + # + # If the database adapter supports DDL transactions, all migrations will + # automatically be wrapped in a transaction. There are queries that you + # can't execute inside a transaction though, and for these situations + # you can turn the automatic transactions off. + # + # class ChangeEnum < ActiveRecord::Migration + # disable_ddl_transaction! + # + # def up + # execute "ALTER TYPE model_size ADD VALUE 'new_value'" + # end + # end + # + # Remember that you can still open your own transactions, even if you + # are in a Migration with <tt>self.disable_ddl_transaction!</tt>. class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' @@ -351,6 +369,7 @@ module ActiveRecord class << self attr_accessor :delegate # :nodoc: + attr_accessor :disable_ddl_transaction # :nodoc: end def self.check_pending! @@ -365,8 +384,16 @@ module ActiveRecord new.migrate direction end - cattr_accessor :verbose + # Disable DDL transactions for this migration. + def self.disable_ddl_transaction! + @disable_ddl_transaction = true + end + + def disable_ddl_transaction # :nodoc: + self.class.disable_ddl_transaction + end + cattr_accessor :verbose attr_accessor :name, :version def initialize(name = self.class.name, version = nil) @@ -375,8 +402,8 @@ module ActiveRecord @connection = nil end + self.verbose = true # instantiate the delegate object after initialize is defined - self.verbose = true self.delegate = new # Reverses the migration commands for the given block and @@ -607,8 +634,17 @@ module ActiveRecord source_migrations = ActiveRecord::Migrator.migrations(path) source_migrations.each do |migration| - source = File.read(migration.filename) - source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}" + source = File.binread(migration.filename) + inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n" + if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source + # If we have a magic comment in the original migration, + # insert our comment after the first newline(end of the magic comment line) + # so the magic keep working. + # Note that magic comments must be at the first line(except sh-bang). + source[/\n/] = "\n#{inserted_comment}" + else + source = "#{inserted_comment}#{source}" + end if duplicate = destination_migrations.detect { |m| m.name == migration.name } if options[:on_skip] && duplicate.scope != scope.to_s @@ -622,7 +658,7 @@ module ActiveRecord old_path, migration.filename = migration.filename, new_path last = migration - File.open(migration.filename, "w") { |f| f.write source } + File.binwrite(migration.filename, source) copied << migration options[:on_copy].call(scope, migration, old_path) if options[:on_copy] destination_migrations << migration @@ -663,7 +699,7 @@ module ActiveRecord File.basename(filename) end - delegate :migrate, :announce, :write, :to => :migration + delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration private @@ -856,12 +892,12 @@ module ActiveRecord Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger begin - ddl_transaction do + ddl_transaction(migration) do migration.migrate(@direction) record_version_state_after_migrating(migration.version) end rescue => e - canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : "" + canceled_msg = use_transaction?(migration) ? "this and " : "" raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace end end @@ -935,12 +971,16 @@ module ActiveRecord end # Wrap the migration in a transaction only if supported by the adapter. - def ddl_transaction - if Base.connection.supports_ddl_transactions? + def ddl_transaction(migration) + if use_transaction?(migration) Base.transaction { yield } else yield end end + + def use_transaction?(migration) + !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions? + end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 85fb4be992..ac2d2f2712 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -205,7 +205,7 @@ module ActiveRecord # Returns an array of column objects for the table associated with this class. def columns - @columns ||= connection.schema_cache.columns[table_name].map do |col| + @columns ||= connection.schema_cache.columns(table_name).map do |col| col = col.dup col.primary = (col.name == primary_key) col diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index c5bd11edbf..602ab9e2f4 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -269,23 +269,36 @@ module ActiveRecord self.nested_attributes_options = nested_attributes_options type = (reflection.collection? ? :collection : :one_to_one) - - # def pirate_attributes=(attributes) - # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options) - # end - generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 - if method_defined?(:#{association_name}_attributes=) - remove_method(:#{association_name}_attributes=) - end - def #{association_name}_attributes=(attributes) - assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) - end - eoruby + generate_association_writer(association_name, type) else raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?" end end end + + private + + # Generates a writer method for this association. Serves as a point for + # accessing the objects in the association. For example, this method + # could generate the following: + # + # def pirate_attributes=(attributes) + # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) + # end + # + # This redirects the attempts to write objects in an association through + # the helper methods defined below. Makes it seem like the nested + # associations are just regular associations. + def generate_association_writer(association_name, type) + generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 + if method_defined?(:#{association_name}_attributes=) + remove_method(:#{association_name}_attributes=) + end + def #{association_name}_attributes=(attributes) + assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) + end + eoruby + end end # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's @@ -371,20 +384,7 @@ module ActiveRecord raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end - if limit = options[:limit] - limit = case limit - when Symbol - send(limit) - when Proc - limit.call - else - limit - end - - if limit && attributes_collection.size > limit - raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." - end - end + check_record_limit!(options[:limit], attributes_collection) if attributes_collection.is_a? Hash keys = attributes_collection.keys @@ -433,6 +433,29 @@ module ActiveRecord end end + # Takes in a limit and checks if the attributes_collection has too many + # records. The method will take limits in the form of symbols, procs, and + # number-like objects (anything that can be compared with an integer). + # + # Will raise an TooManyRecords error if the attributes_collection is + # larger than the limit. + def check_record_limit!(limit, attributes_collection) + if limit + limit = case limit + when Symbol + send(limit) + when Proc + limit.call + else + limit + end + + if limit && attributes_collection.size > limit + raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." + end + end + end + # Updates a record with the +attributes+ or marks it for destruction if # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) @@ -452,6 +475,11 @@ module ActiveRecord has_destroy_flag?(attributes) || call_reject_if(association_name, attributes) end + # Determines if a record with the particular +attributes+ should be + # rejected by calling the reject_if Symbol or Proc (if defined). + # The reject_if option is defined by +accepts_nested_attributes_for+. + # + # Returns false if there is a +destroy_flag+ on the attributes. def call_reject_if(association_name, attributes) return false if has_destroy_flag?(attributes) case callback = self.nested_attributes_options[association_name][:reject_if] diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 347f023793..b25d0601cb 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -410,7 +410,7 @@ module ActiveRecord def relation_for_destroy pk = self.class.primary_key column = self.class.columns_hash[pk] - substitute = connection.substitute_at(column, 0) + substitute = self.class.connection.substitute_at(column, 0) relation = self.class.unscoped.where( self.class.arel_table[pk].eq(substitute)) diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index e04a3d0976..902fd90c54 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,7 +8,7 @@ module ActiveRecord delegate :find_each, :find_in_batches, :to => :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :references, :none, :to => :all + :having, :create_with, :uniq, :distinct, :references, :none, :to => :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all # Executes a custom SQL query against your database and returns all the results. The results will diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 13f3bf7085..99117b74c5 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -49,7 +49,7 @@ module ActiveRecord Rails.logger.extend ActiveSupport::Logger.broadcast console end - runner do |app| + runner do require "active_record/base" end @@ -64,7 +64,7 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end - initializer "active_record.migration_error" do |app| + initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::Migration::CheckPending" @@ -158,7 +158,7 @@ module ActiveRecord end # Expose database runtime to controller for logging. - initializer "active_record.log_runtime" do |app| + initializer "active_record.log_runtime" do require "active_record/railties/controller_runtime" ActiveSupport.on_load(:action_controller) do include ActiveRecord::Railties::ControllerRuntime diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb index 90b462fad6..604a220303 100644 --- a/activerecord/lib/active_record/railties/console_sandbox.rb +++ b/activerecord/lib/active_record/railties/console_sandbox.rb @@ -1,4 +1,5 @@ -ActiveRecord::Base.connection.begin_db_transaction +ActiveRecord::Base.connection.begin_transaction(joinable: false) + at_exit do - ActiveRecord::Base.connection.rollback_db_transaction + ActiveRecord::Base.connection.rollback_transaction end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index efbae108b9..037097d2dd 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,7 +10,7 @@ module ActiveRecord :extending] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, - :reverse_order, :uniq, :create_with] + :reverse_order, :distinct, :create_with] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS @@ -188,8 +188,7 @@ module ActiveRecord # Please see further details in the # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain - _, queries = collecting_queries_for_explain { exec_queries } - exec_explain(queries) + exec_explain(collecting_queries_for_explain { exec_queries }) end # Converts relation objects to Array. @@ -507,6 +506,12 @@ module ActiveRecord includes_values & joins_values end + # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+ + # to maintain backwards compatibility. Use +distinct_value+ instead. + def uniq_value + distinct_value + end + # Compares two relations for equality. def ==(other) case other @@ -590,7 +595,8 @@ module ActiveRecord if (references_values - joined_tables).any? true - elsif (string_tables - joined_tables).any? + elsif !ActiveRecord::Base.disable_implicit_join_references && + (string_tables - joined_tables).any? ActiveSupport::Deprecation.warn( "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \ "that are referenced in a string SQL snippet. For example: \n" \ @@ -604,7 +610,10 @@ module ActiveRecord "From now on, you must explicitly tell Active Record when you are referencing a table " \ "from a string:\n" \ "\n" \ - " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n" + " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n" \ + "\n" \ + "If you don't rely on implicit join references you can disable the feature entirely" \ + "by setting `config.active_record.disable_implicit_join_references = true`." ) true else diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 7f95181c67..be011b22af 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -11,7 +11,7 @@ module ActiveRecord # Person.count(:all) # # => performs a COUNT(*) (:all is an alias for '*') # - # Person.count(:age, distinct: true) + # Person.distinct.count(:age) # # => counts the number of different age values # # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column, @@ -198,8 +198,13 @@ module ActiveRecord def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase - # If #count is used in conjuction with #uniq it is considered distinct. (eg. relation.uniq.count) - distinct = options[:distinct] || self.uniq_value + # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) + distinct = self.distinct_value + if options.has_key?(:distinct) + ActiveSupport::Deprecation.warn "The :distinct option for `Relation#count` is deprecated. " \ + "Please use `Relation#distinct` instead. (eg. `relation.distinct.count`)" + distinct = options[:distinct] + end if operation == "count" column_name ||= (select_for_count || :all) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b7960936cf..10a31109d5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -710,20 +710,22 @@ module ActiveRecord # User.select(:name) # # => Might return two records with the same name # - # User.select(:name).uniq - # # => Returns 1 record per unique name + # User.select(:name).distinct + # # => Returns 1 record per distinct name # - # User.select(:name).uniq.uniq(false) + # User.select(:name).distinct.distinct(false) # # => You can also remove the uniqueness - def uniq(value = true) - spawn.uniq!(value) + def distinct(value = true) + spawn.distinct!(value) end + alias uniq distinct - # Like #uniq, but modifies relation in place. - def uniq!(value = true) # :nodoc: - self.uniq_value = value + # Like #distinct, but modifies relation in place. + def distinct!(value = true) # :nodoc: + self.distinct_value = value self end + alias uniq! distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -814,7 +816,7 @@ module ActiveRecord build_select(arel, select_values.uniq) - arel.distinct(uniq_value) + arel.distinct(distinct_value) arel.from(build_from) if from_value arel.lock(lock_value) if lock_value diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 9830abe7d8..6077144265 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -13,18 +13,21 @@ module ActiveRecord "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" end - def self.create_table + def self.create_table(limit=nil) unless connection.table_exists?(table_name) - connection.create_table(table_name, :id => false) do |t| - t.column :version, :string, :null => false + version_options = {null: false} + version_options[:limit] = limit if limit + + connection.create_table(table_name, id: false) do |t| + t.column :version, :string, version_options end - connection.add_index table_name, :version, :unique => true, :name => index_name + connection.add_index table_name, :version, unique: true, name: index_name end end def self.drop_table if connection.table_exists?(table_name) - connection.remove_index table_name, :name => index_name + connection.remove_index table_name, name: index_name connection.drop_table(table_name) end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 01fbb96b8e..12317601b6 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -159,10 +159,19 @@ module ActiveRecord end singleton_class.send(:define_method, name) do |*args| - options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body - relation = all.merge(options) + if body.respond_to?(:call) + scope = extension ? body.call(*args).extending(extension) : body.call(*args) - extension ? relation.extending(extension) : relation + if scope + default_scoped = scope.default_scoped + scope = relation.merge(scope) + scope.default_scoped = default_scoped + end + else + scope = body + end + + scope || all end end end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 17378969a5..50569d2462 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -26,7 +26,9 @@ module ActiveRecord $stdout.print error.error establish_connection root_configuration_without_database connection.create_database configuration['database'], creation_options - connection.execute grant_statement.gsub(/\s+/, ' ').strip + if configuration['username'] != 'root' + connection.execute grant_statement.gsub(/\s+/, ' ').strip + end establish_connection configuration else $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" @@ -57,7 +59,10 @@ module ActiveRecord args.concat(["--result-file", "#{filename}"]) args.concat(["--no-data"]) args.concat(["#{configuration['database']}"]) - Kernel.system(*args) + unless Kernel.system(*args) + $stderr.puts "Could not dump the database structure. "\ + "Make sure `mysqldump` is in your PATH and check the command output for warnings." + end end def structure_load(filename) diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 8ded6d4a86..ae99cff35e 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -98,6 +98,12 @@ module ActiveRecord timestamp_attributes_for_create + timestamp_attributes_for_update end + def max_updated_column_timestamp + if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present? + timestamps.map { |ts| ts.to_time }.max + end + end + def current_time_from_proper_timezone self.class.default_timezone == :utc ? Time.now.utc : Time.now end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 33718ef0e9..4dbd668fcf 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -160,7 +160,7 @@ module ActiveRecord # end # end # - # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.) + # only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it. # # Most databases don't support true nested transactions. At the time of # writing, the only database that we're aware of that supports true nested diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 1427189851..a705d8c2c4 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -2,6 +2,10 @@ module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator # :nodoc: def initialize(options) + if options[:conditions] && !options[:conditions].respond_to?(:call) + raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \ + "Pass a callable instead: `conditions: -> { where(approved: true) }`" + end super({ case_sensitive: true }.merge!(options)) end @@ -19,7 +23,7 @@ module ActiveRecord relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? relation = scope_relation(record, table, relation) relation = finder_class.unscoped.where(relation) - relation.merge!(options[:conditions]) if options[:conditions] + relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? error_options = options.except(:case_sensitive, :scope, :conditions) @@ -116,7 +120,7 @@ module ActiveRecord # of the title attribute: # # class Article < ActiveRecord::Base - # validates_uniqueness_of :title, conditions: where('status != ?', 'archived') + # validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') } # end # # When the record is created, a check is performed to make sure that no @@ -132,7 +136,7 @@ module ActiveRecord # the uniqueness constraint. # * <tt>:conditions</tt> - Specify the conditions to be included as a # <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup - # (e.g. <tt>conditions: where('status = ?', 'active')</tt>). + # (e.g. <tt>conditions: -> { where(status: 'active') }</tt>). # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by # non-text columns (+true+ by default). # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index 3a3cf86d73..fd94a2d038 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -2,8 +2,12 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change create_table :<%= table_name %> do |t| <% attributes.each do |attribute| -%> +<% if attribute.password_digest? -%> + t.string :password_digest<%= attribute.inject_options %> +<% else -%> t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> +<% end -%> <% if options[:timestamps] %> t.timestamps <% end -%> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 056f55470c..808598699b 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -1,7 +1,10 @@ <% module_namespacing do -%> class <%= class_name %> < <%= parent_class_name.classify %> -<% attributes.select {|attr| attr.reference? }.each do |attribute| -%> +<% attributes.select(&:reference?).each do |attribute| -%> belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> <% end -%> +<% if attributes.any?(&:password_digest?) -%> + has_secure_password +<% end -%> end <% end -%> diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 5164acf77f..4cf4bc4c61 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -63,11 +63,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { @connection.rename_column(:group, :order, :values) } end - # dump structure of table with reserved word name - def test_structure_dump - assert_nothing_raised { @connection.structure_dump } - end - # introspect table with reserved word name def test_introspect assert_nothing_raised { @connection.columns(:group) } diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 1265cb927e..fedd9f603c 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -70,12 +70,6 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_logs_name_structure_dump - @connection.structure_dump - assert_equal "SCHEMA", @connection.logged[0][1] - assert_equal "SCHEMA", @connection.logged[2][1] - end - def test_logs_name_show_variable @connection.show_variable 'foo' assert_equal "SCHEMA", @connection.logged[0][1] diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 1017b0758d..e76617b845 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -63,11 +63,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { @connection.rename_column(:group, :order, :values) } end - # dump structure of table with reserved word name - def test_structure_dump - assert_nothing_raised { @connection.structure_dump } - end - # introspect table with reserved word name def test_introspect assert_nothing_raised { @connection.columns(:group) } @@ -89,7 +84,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { x.save } assert_nothing_raised { Group.find_by_order('y') } assert_nothing_raised { Group.find(1) } - x = Group.find(1) end # has_one association with reserved-word table name diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb new file mode 100644 index 0000000000..9ecd901eac --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class Mysql2Adapter + class SchemaMigrationsTest < ActiveRecord::TestCase + def test_initializes_schema_migrations_for_encoding_utf8mb4 + conn = ActiveRecord::Base.connection + + smtn = ActiveRecord::Migrator.schema_migrations_table_name + conn.drop_table(smtn) if conn.table_exists?(smtn) + + config = conn.instance_variable_get(:@config) + original_encoding = config[:encoding] + + config[:encoding] = 'utf8mb4' + conn.initialize_schema_migrations_table + + assert conn.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4) + ensure + config[:encoding] = original_encoding + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 685f0ea74f..b3429648ee 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -44,6 +44,14 @@ module ActiveRecord c = Column.new(nil, 1, 'float') assert_equal "'Infinity'", @conn.quote(infinity, c) end + + def test_quote_cast_numeric + fixnum = 666 + c = Column.new(nil, nil, 'string') + assert_equal "'666'", @conn.quote(fixnum, c) + c = Column.new(nil, nil, 'text') + assert_equal "'666'", @conn.quote(fixnum, c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb new file mode 100644 index 0000000000..53002c5265 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -0,0 +1,43 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlUUIDTest < ActiveRecord::TestCase + class UUID < ActiveRecord::Base + self.table_name = 'pg_uuids' + end + + def setup + @connection = ActiveRecord::Base.connection + + unless @connection.supports_extensions? + return skip "do not test on PG without uuid-ossp" + end + + unless @connection.extension_enabled?('uuid-ossp') + @connection.enable_extension 'uuid-ossp' + @connection.commit_db_transaction + end + + @connection.reconnect! + + @connection.transaction do + @connection.create_table('pg_uuids', id: :uuid) do |t| + t.string 'name' + t.uuid 'other_uuid', default: 'uuid_generate_v4()' + end + end + end + + def teardown + @connection.execute 'drop table if exists pg_uuids' + end + + def test_auto_create_uuid + u = UUID.create + u.reload + assert_not_nil u.other_uuid + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index d03d1dd94c..21fb111c91 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class CopyTableTest < ActiveRecord::TestCase - fixtures :customers, :companies, :comments + fixtures :customers, :companies, :comments, :binaries def setup @connection = ActiveRecord::Base.connection @@ -72,6 +72,10 @@ class CopyTableTest < ActiveRecord::TestCase end end + def test_copy_table_with_binary_column + test_copy_table 'binaries', 'binaries2' + end + protected def copy_table(from, to, options = {}) @connection.copy_table(from, to, {:temporary => true}.merge(options)) diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 3a6da0e59f..f5316952b8 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -21,9 +21,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase :posts, :tags, :taggings, :comments, :sponsors, :members def test_belongs_to - Client.find(3).firm.name - assert_equal companies(:first_firm).name, Client.find(3).firm.name - assert_not_nil Client.find(3).firm, "Microsoft should have a firm" + firm = Client.find(3).firm + assert_not_nil firm + assert_equal companies(:first_firm).name, firm.name end def test_belongs_to_with_primary_key @@ -232,15 +232,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_belongs_to_counter_with_assigning_nil - p = Post.find(1) - c = Comment.find(1) + post = Post.find(1) + comment = Comment.find(1) - assert_equal p.id, c.post_id - assert_equal 2, Post.find(p.id).comments.size + assert_equal post.id, comment.post_id + assert_equal 2, Post.find(post.id).comments.size - c.post = nil + comment.post = nil - assert_equal 1, Post.find(p.id).comments.size + assert_equal 1, Post.find(post.id).comments.size end def test_belongs_to_with_primary_key_counter @@ -263,56 +263,56 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_belongs_to_counter_with_reassigning - t1 = Topic.create("title" => "t1") - t2 = Topic.create("title" => "t2") - r1 = Reply.new("title" => "r1", "content" => "r1") - r1.topic = t1 + topic1 = Topic.create("title" => "t1") + topic2 = Topic.create("title" => "t2") + reply1 = Reply.new("title" => "r1", "content" => "r1") + reply1.topic = topic1 - assert r1.save - assert_equal 1, Topic.find(t1.id).replies.size - assert_equal 0, Topic.find(t2.id).replies.size + assert reply1.save + assert_equal 1, Topic.find(topic1.id).replies.size + assert_equal 0, Topic.find(topic2.id).replies.size - r1.topic = Topic.find(t2.id) + reply1.topic = Topic.find(topic2.id) assert_no_queries do - r1.topic = t2 + reply1.topic = topic2 end - assert r1.save - assert_equal 0, Topic.find(t1.id).replies.size - assert_equal 1, Topic.find(t2.id).replies.size + assert reply1.save + assert_equal 0, Topic.find(topic1.id).replies.size + assert_equal 1, Topic.find(topic2.id).replies.size - r1.topic = nil + reply1.topic = nil - assert_equal 0, Topic.find(t1.id).replies.size - assert_equal 0, Topic.find(t2.id).replies.size + assert_equal 0, Topic.find(topic1.id).replies.size + assert_equal 0, Topic.find(topic2.id).replies.size - r1.topic = t1 + reply1.topic = topic1 - assert_equal 1, Topic.find(t1.id).replies.size - assert_equal 0, Topic.find(t2.id).replies.size + assert_equal 1, Topic.find(topic1.id).replies.size + assert_equal 0, Topic.find(topic2.id).replies.size - r1.destroy + reply1.destroy - assert_equal 0, Topic.find(t1.id).replies.size - assert_equal 0, Topic.find(t2.id).replies.size + assert_equal 0, Topic.find(topic1.id).replies.size + assert_equal 0, Topic.find(topic2.id).replies.size end def test_belongs_to_reassign_with_namespaced_models_and_counters - t1 = Web::Topic.create("title" => "t1") - t2 = Web::Topic.create("title" => "t2") - r1 = Web::Reply.new("title" => "r1", "content" => "r1") - r1.topic = t1 + topic1 = Web::Topic.create("title" => "t1") + topic2 = Web::Topic.create("title" => "t2") + reply1 = Web::Reply.new("title" => "r1", "content" => "r1") + reply1.topic = topic1 - assert r1.save - assert_equal 1, Web::Topic.find(t1.id).replies.size - assert_equal 0, Web::Topic.find(t2.id).replies.size + assert reply1.save + assert_equal 1, Web::Topic.find(topic1.id).replies.size + assert_equal 0, Web::Topic.find(topic2.id).replies.size - r1.topic = Web::Topic.find(t2.id) + reply1.topic = Web::Topic.find(topic2.id) - assert r1.save - assert_equal 0, Web::Topic.find(t1.id).replies.size - assert_equal 1, Web::Topic.find(t2.id).replies.size + assert reply1.save + assert_equal 0, Web::Topic.find(topic1.id).replies.size + assert_equal 1, Web::Topic.find(topic2.id).replies.size end def test_belongs_to_counter_after_save @@ -367,9 +367,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_new_record_with_foreign_key_but_no_object - c = Client.new("firm_id" => 1) + client = Client.new("firm_id" => 1) # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert_equal Firm.all.merge!(:order => "id").first, c.firm_with_basic_id + assert_equal Firm.all.merge!(:order => "id").first, client.firm_with_basic_id end def test_setting_foreign_key_after_nil_target_loaded diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 80bca7f63e..e693d34f99 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -55,7 +55,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_nothing_raised do assert_equal 4, categories.count assert_equal 4, categories.to_a.count - assert_equal 3, categories.count(:distinct => true) + assert_equal 3, categories.distinct.count assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 1b1b479f1a..84bdca3a97 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -316,7 +316,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase dev.projects << projects(:active_record) assert_equal 3, dev.projects.size - assert_equal 1, dev.projects.uniq.size + assert_equal 1, dev.projects.distinct.size end def test_uniq_before_the_fact diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 1ddd380f23..781b87741d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -789,6 +789,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_calling_update_attributes_on_id_changes_the_counter_cache + topic = Topic.order("id ASC").first + original_count = topic.replies.to_a.size + assert_equal original_count, topic.replies_count + + first_reply = topic.replies.first + first_reply.update_attributes(:parent_id => nil) + assert_equal original_count - 1, topic.reload.replies_count + + first_reply.update_attributes(:parent_id => topic.id) + assert_equal original_count, topic.reload.replies_count + end + + def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache + topic1 = Topic.find(1) + topic2 = Topic.find(3) + original_count1 = topic1.replies.to_a.size + original_count2 = topic2.replies.to_a.size + + reply1 = topic1.replies.first + reply2 = topic2.replies.first + + reply1.update_attributes(:parent_id => topic2.id) + assert_equal original_count1 - 1, topic1.reload.replies_count + assert_equal original_count2 + 1, topic2.reload.replies_count + + reply2.update_attributes(:parent_id => topic1.id) + assert_equal original_count1, topic1.reload.replies_count + assert_equal original_count2, topic2.reload.replies_count + end + def test_deleting_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 4f246f575e..918783e8f1 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -82,7 +82,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length - authors_with_welcoming_post_titles = Author.all.merge!(:joins => :posts, :where => "posts.title like 'Welcome%'").calculate(:count, 'authors.id', :distinct => true) + authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, 'authors.id') assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 10ec33be75..c05481dd91 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -397,14 +397,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_many - assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.uniq.sort_by { |t| t.id } + assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id } end def test_include_has_many_through_polymorphic_has_many author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } + assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } end end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index e355ed3495..e75d43bda8 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -410,7 +410,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # Mary and Bob both have posts in misc, but they are the only ones. authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id) - assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id) + assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id) # Check the polymorphism of taggings is being observed correctly (in both joins) authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel') diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 201fa5d5a9..a06bacafca 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -95,7 +95,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload firm = Firm.new("name" => "A New Firm, Inc") firm.save - firm.clients.each {|c|} # forcing to load all clients + firm.clients.each {} # forcing to load all clients assert firm.clients.empty?, "New firm shouldn't have client objects" assert_equal 0, firm.clients.size, "New firm should have 0 clients" @@ -237,6 +237,11 @@ class AssociationProxyTest < ActiveRecord::TestCase assert david.projects.scope.is_a?(ActiveRecord::Relation) assert_equal david.projects, david.projects.scope end + + test "proxy object is cached" do + david = developers(:david) + assert david.projects.equal?(david.projects) + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index af1845c937..445322486c 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1091,7 +1091,7 @@ class BasicsTest < ActiveRecord::TestCase res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res7 = nil assert_nothing_raised do - res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count(distinct: true) + res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count end assert_equal res6, res7 end @@ -1142,8 +1142,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_keeps_multiple_group_values - combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at').to_a - assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']).to_a + combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at, developers.created_on, developers.updated_on').to_a + assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at', 'developers.created_on', 'developers.updated_on']).to_a end def test_find_symbol_ordered_last @@ -1344,9 +1344,9 @@ class BasicsTest < ActiveRecord::TestCase def test_clear_cache! # preheat cache - c1 = Post.connection.schema_cache.columns['posts'] + c1 = Post.connection.schema_cache.columns('posts') ActiveRecord::Base.clear_cache! - c2 = Post.connection.schema_cache.columns['posts'] + c2 = Post.connection.schema_cache.columns('posts') assert_not_equal c1, c2 end @@ -1441,12 +1441,30 @@ class BasicsTest < ActiveRecord::TestCase assert_not_equal key, car.cache_key end - def test_cache_key_format_for_existing_record_with_nil_updated_at + def test_cache_key_format_for_existing_record_with_nil_updated_timestamps dev = Developer.first - dev.update_columns(updated_at: nil) + dev.update_columns(updated_at: nil, updated_on: nil) assert_match(/\/#{dev.id}$/, dev.cache_key) end + def test_cache_key_for_updated_on + dev = Developer.first + dev.updated_at = nil + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + end + + def test_cache_key_for_newer_updated_at + dev = Developer.first + dev.updated_at += 3600 + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key + end + + def test_cache_key_for_newer_updated_on + dev = Developer.first + dev.updated_on += 3600 + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + end + def test_touch_should_raise_error_on_a_new_object company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals") assert_raises(ActiveRecord::ActiveRecordError) do @@ -1467,6 +1485,12 @@ class BasicsTest < ActiveRecord::TestCase assert_equal scope, Bird.uniq end + def test_distinct_delegates_to_scoped + scope = stub + Bird.stubs(:all).returns(mock(:distinct => scope)) + assert_equal scope, Bird.distinct + end + def test_table_name_with_2_abstract_subclasses assert_equal "photos", Photo.table_name end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index be49e948fc..c645523905 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -305,8 +305,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_count_selected_field_with_include - assert_equal 6, Account.includes(:firm).count(:distinct => true) - assert_equal 4, Account.includes(:firm).select(:credit_limit).count(:distinct => true) + assert_equal 6, Account.includes(:firm).distinct.count + assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count end def test_should_not_perform_joined_include_by_default @@ -341,7 +341,18 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 5, Account.count(:firm_id) end - def test_count_with_uniq + def test_count_distinct_option_is_deprecated + assert_deprecated do + assert_equal 4, Account.select(:credit_limit).count(distinct: true) + end + + assert_deprecated do + assert_equal 6, Account.select(:credit_limit).count(distinct: false) + end + end + + def test_count_with_distinct + assert_equal 4, Account.select(:credit_limit).distinct.count assert_equal 4, Account.select(:credit_limit).uniq.count end @@ -351,7 +362,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_field_in_joined_table assert_equal 5, Account.joins(:firm).count('companies.id') - assert_equal 4, Account.joins(:firm).count('companies.id', :distinct => true) + assert_equal 4, Account.joins(:firm).distinct.count('companies.id') end def test_should_count_field_in_joined_table_with_group_by @@ -455,7 +466,7 @@ class CalculationsTest < ActiveRecord::TestCase approved_topics_count = Topic.group(:approved).count(:author_name)[true] assert_equal approved_topics_count, 3 # Count the number of distinct authors for approved Topics - distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true] + distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true] assert_equal distinct_authors_for_approved_count, 2 end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 541e983758..ecad7c942f 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -9,49 +9,46 @@ module ActiveRecord end def test_primary_key - assert_equal 'id', @cache.primary_keys['posts'] + assert_equal 'id', @cache.primary_keys('posts') end def test_primary_key_for_non_existent_table - assert_nil @cache.primary_keys['omgponies'] + assert_nil @cache.primary_keys('omgponies') end def test_caches_columns - columns = @cache.columns['posts'] - assert_equal columns, @cache.columns['posts'] + columns = @cache.columns('posts') + assert_equal columns, @cache.columns('posts') end def test_caches_columns_hash - columns_hash = @cache.columns_hash['posts'] - assert_equal columns_hash, @cache.columns_hash['posts'] + columns_hash = @cache.columns_hash('posts') + assert_equal columns_hash, @cache.columns_hash('posts') end def test_clearing - @cache.columns['posts'] - @cache.columns_hash['posts'] - @cache.tables['posts'] - @cache.primary_keys['posts'] + @cache.columns('posts') + @cache.columns_hash('posts') + @cache.tables('posts') + @cache.primary_keys('posts') @cache.clear! - assert_equal 0, @cache.columns.size - assert_equal 0, @cache.columns_hash.size - assert_equal 0, @cache.tables.size - assert_equal 0, @cache.primary_keys.size + assert_equal 0, @cache.size end def test_dump_and_load - @cache.columns['posts'] - @cache.columns_hash['posts'] - @cache.tables['posts'] - @cache.primary_keys['posts'] + @cache.columns('posts') + @cache.columns_hash('posts') + @cache.tables('posts') + @cache.primary_keys('posts') @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 12, @cache.columns['posts'].size - assert_equal 12, @cache.columns_hash['posts'].size - assert @cache.tables['posts'] - assert_equal 'id', @cache.primary_keys['posts'] + assert_equal 12, @cache.columns('posts').size + assert_equal 12, @cache.columns_hash('posts').size + assert @cache.tables('posts') + assert_equal 'id', @cache.primary_keys('posts') end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 23e64bee7e..e6af29282c 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -185,7 +185,7 @@ module ActiveRecord assert_not_nil connection threads = [] 4.times do |i| - threads << Thread.new(i) do |pool_count| + threads << Thread.new(i) do connection = pool.connection assert_not_nil connection connection.close diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index fc46a249c8..7d06fb5093 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -118,7 +118,7 @@ class CounterCacheTest < ActiveRecord::TestCase test "reset the right counter if two have the same foreign key" do michael = people(:michael) assert_nothing_raised(ActiveRecord::StatementInvalid) do - Person.reset_counters(michael.id, :followers) + Person.reset_counters(michael.id, :friends_too) end end @@ -131,4 +131,11 @@ class CounterCacheTest < ActiveRecord::TestCase Subscriber.reset_counters(subscriber.id, 'books') end end + + test "the passed symbol needs to be an association name" do + e = assert_raises(ArgumentError) do + Topic.reset_counters(@topic.id, :replies_count) + end + assert_equal "'Topic' has no association called 'replies_count'", e.message + end end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index b1d276f9eb..6dac5db111 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -20,7 +20,7 @@ if ActiveRecord::Base.connection.supports_explain? end def test_collecting_queries_for_explain - result, queries = ActiveRecord::Base.collecting_queries_for_explain do + queries = ActiveRecord::Base.collecting_queries_for_explain do Car.where(:name => 'honda').to_a end @@ -28,7 +28,6 @@ if ActiveRecord::Base.connection.supports_explain? assert_match "SELECT", sql assert_match "honda", sql assert_equal [], binds - assert_equal [cars(:honda)], result end def test_exec_explain_with_no_binds diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index a9fa107749..e505fe9f18 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -82,7 +82,7 @@ class FinderTest < ActiveRecord::TestCase # ensures +exists?+ runs valid SQL by excluding order value def test_exists_with_order - assert Topic.order(:id).uniq.exists? + assert Topic.order(:id).distinct.exists? end def test_exists_with_includes_limit_and_empty_result diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index b0b29f5f42..8ad40ec3f4 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -477,9 +477,8 @@ class CustomConnectionFixturesTest < ActiveRecord::TestCase fixtures :courses self.use_transactional_fixtures = false - def test_connection - assert_kind_of Course, courses(:ruby) - assert_equal Course.connection, courses(:ruby).connection + def test_connection_instance_method_deprecation + assert_deprecated { courses(:ruby).connection } end def test_leaky_destroy diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 189066eb41..99d54e7526 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -68,6 +68,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_company_descends_from_active_record + assert !ActiveRecord::Base.descends_from_active_record? assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base' assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base' assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base' @@ -171,6 +172,20 @@ class InheritanceTest < ActiveRecord::TestCase assert_equal Firm, firm.class end + def test_new_with_abstract_class + e = assert_raises(NotImplementedError) do + AbstractCompany.new + end + assert_equal("AbstractCompany is an abstract class and can not be instantiated.", e.message) + end + + def test_new_with_ar_base + e = assert_raises(NotImplementedError) do + ActiveRecord::Base.new + end + assert_equal("ActiveRecord::Base is an abstract class and can not be instantiated.", e.message) + end + def test_new_with_invalid_type assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'InvalidType') } end @@ -179,6 +194,17 @@ class InheritanceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') } end + def test_new_with_autoload_paths + path = File.expand_path('../../models/autoloadable', __FILE__) + ActiveSupport::Dependencies.autoload_paths << path + + firm = Company.new(:type => 'ExtraFirm') + assert_equal ExtraFirm, firm.class + ensure + ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path } + ActiveSupport::Dependencies.clear + end + def test_inheritance_condition assert_equal 10, Company.count assert_equal 2, Firm.count diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index a0a3e6cb0d..0c896beb1d 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -341,9 +341,6 @@ end # is so cumbersome. Will deadlock Ruby threads if the underlying db.execute # blocks, so separate script called by Kernel#system is needed. # (See exec vs. async_exec in the PostgreSQL adapter.) - -# TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking - unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase self.use_transactional_fixtures = false diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index ee0c20747e..97efb94b66 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -7,6 +7,7 @@ module ActiveRecord self.use_transactional_fixtures = false Migration = Struct.new(:name, :version) do + def disable_ddl_transaction; false end def migrate direction # do nothing end @@ -34,4 +35,3 @@ module ActiveRecord end end end - diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index fa8dec0e15..f8afb7c591 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -239,9 +239,13 @@ class MigrationTest < ActiveRecord::TestCase assert_not Person.column_methods_hash.include?(:last_name) - migration = Struct.new(:name, :version) { - def migrate(x); raise 'Something broke'; end - }.new('zomg', 100) + migration = Class.new(ActiveRecord::Migration) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new migrator = ActiveRecord::Migrator.new(:up, [migration], 100) @@ -250,7 +254,39 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message Person.reset_column_information + assert_not Person.column_methods_hash.include?(:last_name), + "On error, the Migrator should revert schema changes but it did not." + end + + def test_migration_without_transaction + unless ActiveRecord::Base.connection.supports_ddl_transactions? + skip "not supported on #{ActiveRecord::Base.connection.class}" + end + assert_not Person.column_methods_hash.include?(:last_name) + + migration = Class.new(ActiveRecord::Migration) { + self.disable_ddl_transaction! + + def version; 101 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new + + migrator = ActiveRecord::Migrator.new(:up, [migration], 101) + e = assert_raise(StandardError) { migrator.migrate } + assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message + + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name), + "without ddl transactions, the Migrator should not rollback on error but it did." + ensure + Person.reset_column_information + if Person.column_methods_hash.include?(:last_name) + Person.connection.remove_column('people', 'last_name') + end end def test_schema_migrations_table_name @@ -426,6 +462,22 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase end end +class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase + def test_drop_index_by_name + connection = Person.connection + connection.create_table :values, force: true do |t| + t.integer :value + end + + assert_nothing_raised ArgumentError do + connection.add_index :values, :value, name: 'a_different_name' + connection.remove_index :values, column: :value, name: 'a_different_name' + end + + connection.drop_table :values rescue nil + end +end + if ActiveRecord::Base.connection.supports_bulk_alter? class BulkAlterTableMigrationsTest < ActiveRecord::TestCase def setup @@ -686,6 +738,26 @@ class CopyMigrationsTest < ActiveRecord::TestCase clear end + def test_copying_migrations_preserving_magic_comments + ActiveRecord::Base.timestamped_migrations = false + @migrations_path = MIGRATIONS_ROOT + "/valid" + @existing_migrations = Dir[@migrations_path + "/*.rb"] + + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"}) + assert File.exists?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") + assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename) + + expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)" + assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..1].join.chomp + + files_count = Dir[@migrations_path + "/*.rb"].length + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"}) + assert_equal files_count, Dir[@migrations_path + "/*.rb"].length + assert copied.empty? + ensure + clear + end + def test_skipping_migrations @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index bd121126e7..b593270352 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -309,7 +309,7 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size end - def test_chaining_should_use_latest_conditions_when_creating + def test_chaining_applies_last_conditions_when_creating post = Topic.rejected.new assert !post.approved? @@ -323,13 +323,13 @@ class NamedScopeTest < ActiveRecord::TestCase assert post.approved? end - def test_chaining_should_use_latest_conditions_when_searching + def test_chaining_combines_conditions_when_searching # Normal hash conditions - assert_equal Topic.where(:approved => true).to_a, Topic.rejected.approved.to_a - assert_equal Topic.where(:approved => false).to_a, Topic.approved.rejected.to_a + assert_equal Topic.where(approved: false).where(approved: true).to_a, Topic.rejected.approved.to_a + assert_equal Topic.where(approved: true).where(approved: false).to_a, Topic.approved.rejected.to_a # Nested hash conditions with same keys - assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.to_a + assert_equal [], Post.with_special_comments.with_very_special_comments.to_a # Nested hash conditions with different keys assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 8156f99037..b936cca875 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -399,14 +399,6 @@ class PersistencesTest < ActiveRecord::TestCase assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } end - def test_string_ids - # FIXME: Fix this failing test - skip "Failing test. We need this fixed before 4.0.0" - mv = Minivan.where(:minivan_id => 1234).first_or_initialize - assert mv.new_record? - assert_equal '1234', mv.minivan_id - end - def test_update_attribute_with_one_updated t = Topic.first t.update_attribute(:title, 'super_title') diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index fd0b05cb77..9ca980fdf6 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -278,5 +278,17 @@ module ActiveRecord assert_equal [NullRelation], relation.extending_values assert relation.is_a?(NullRelation) end + + test "distinct!" do + relation.distinct! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end + + test "uniq! was replaced by distinct!" do + relation.uniq! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 8298d7534c..9008c2785e 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -492,6 +492,7 @@ class RelationTest < ActiveRecord::TestCase expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do + assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } end @@ -714,6 +715,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal [developers(:poor_jamis)], dev_with_count.to_a end + def test_relation_to_sql + sql = Post.connection.unprepared_statement do + Post.first.comments.to_sql + end + assert_no_match(/\?/, sql) + end + def test_relation_merging_with_arel_equalities_keeps_last_equality devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( Developer.where(Developer.arel_table[:salary].eq(9000)) @@ -782,11 +790,11 @@ class RelationTest < ActiveRecord::TestCase def test_count_with_distinct posts = Post.all - assert_equal 3, posts.count(:comments_count, :distinct => true) - assert_equal 11, posts.count(:comments_count, :distinct => false) + assert_equal 3, posts.distinct(true).count(:comments_count) + assert_equal 11, posts.distinct(false).count(:comments_count) - assert_equal 3, posts.select(:comments_count).count(:distinct => true) - assert_equal 11, posts.select(:comments_count).count(:distinct => false) + assert_equal 3, posts.distinct(true).select(:comments_count).count + assert_equal 11, posts.distinct(false).select(:comments_count).count end def test_count_explicit_columns @@ -1216,6 +1224,16 @@ class RelationTest < ActiveRecord::TestCase end end + def test_turn_off_eager_loading_with_conditions_on_joins + original_value = ActiveRecord::Base.disable_implicit_join_references + ActiveRecord::Base.disable_implicit_join_references = true + + scope = Topic.where(author_email_address: 'my.example@gmail.com').includes(:replies) + assert_not scope.eager_loading? + ensure + ActiveRecord::Base.disable_implicit_join_references = original_value + end + def test_ordering_with_extra_spaces assert_equal authors(:david), Author.order('id DESC , name DESC').last end @@ -1262,7 +1280,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:welcome), comments(:greetings).post end - def test_uniq + def test_distinct tag1 = Tag.create(:name => 'Foo') tag2 = Tag.create(:name => 'Foo') @@ -1270,11 +1288,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal ['Foo', 'Foo'], query.map(&:name) assert_sql(/DISTINCT/) do + assert_equal ['Foo'], query.distinct.map(&:name) assert_equal ['Foo'], query.uniq.map(&:name) end assert_sql(/DISTINCT/) do + assert_equal ['Foo'], query.distinct(true).map(&:name) assert_equal ['Foo'], query.uniq(true).map(&:name) end + assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name) assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 295c7e13fa..726338db14 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'models/topic' require 'models/person' +require 'models/traffic_light' require 'bcrypt' class SerializedAttributeTest < ActiveRecord::TestCase @@ -234,4 +235,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase person = person.reload assert_equal(insures, person.insures) end + + def test_regression_serialized_default_on_text_column_with_null_false + light = TrafficLight.new + assert_equal [], light.state + assert_equal [], light.long_state + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 38b9dd02f0..816bd62751 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -71,7 +71,7 @@ module ActiveRecord return skip("only tested on mysql") end - @connection = stub(:create_database => true, :execute => true) + @connection = stub("Connection", create_database: true) @error = Mysql::Error.new "Invalid permissions" @configuration = { 'adapter' => 'mysql', @@ -90,6 +90,7 @@ module ActiveRecord end def test_root_password_is_requested + assert_permissions_granted_for "pat" skip "only if mysql is available" unless defined?(::Mysql) $stdin.expects(:gets).returns("secret\n") @@ -97,6 +98,7 @@ module ActiveRecord end def test_connection_established_as_root + assert_permissions_granted_for "pat" ActiveRecord::Base.expects(:establish_connection).with( 'adapter' => 'mysql', 'database' => nil, @@ -108,6 +110,7 @@ module ActiveRecord end def test_database_created_by_root + assert_permissions_granted_for "pat" @connection.expects(:create_database). with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') @@ -115,12 +118,18 @@ module ActiveRecord end def test_grant_privileges_for_normal_user - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON my-app-db.* TO 'pat'@'localhost' IDENTIFIED BY 'wossname' WITH GRANT OPTION;") + assert_permissions_granted_for "pat" + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + def test_do_not_grant_privileges_for_root_user + @configuration['username'] = 'root' + @configuration['password'] = '' ActiveRecord::Tasks::DatabaseTasks.create @configuration end def test_connection_established_as_normal_user + assert_permissions_granted_for "pat" ActiveRecord::Base.expects(:establish_connection).returns do ActiveRecord::Base.expects(:establish_connection).with( 'adapter' => 'mysql', @@ -142,6 +151,13 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.create @configuration end + + private + def assert_permissions_granted_for(db_user) + db_name = @configuration['database'] + db_password = @configuration['password'] + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") + end end class MySQLDBDropTest < ActiveRecord::TestCase @@ -249,10 +265,21 @@ module ActiveRecord def test_structure_dump filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db") + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end + + def test_warn_when_external_structure_dump_fails + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(false) + + warnings = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + + assert_match(/Could not dump the database structure/, warnings) + end end class MySQLStructureLoadTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index eb4ffd4498..766a5c0c90 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -182,9 +182,9 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_call_after_rollback_when_commit_fails - @first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) + @first.class.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) begin - @first.connection.class.class_eval do + @first.class.connection.class.class_eval do def commit_db_transaction; raise "boom!"; end end @@ -194,8 +194,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert !@first.save rescue nil assert_equal [:after_rollback], @first.history ensure - @first.connection.class.send(:remove_method, :commit_db_transaction) - @first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) + @first.class.connection.class.send(:remove_method, :commit_db_transaction) + @first.class.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 46e767af1a..29b45944aa 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -348,7 +348,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase end def test_validate_uniqueness_with_conditions - Topic.validates_uniqueness_of(:title, :conditions => Topic.where('approved = ?', true)) + Topic.validates_uniqueness_of :title, conditions: -> { where(approved: true) } Topic.create("title" => "I'm a topic", "approved" => true) Topic.create("title" => "I'm an unapproved topic", "approved" => false) @@ -359,6 +359,12 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t4.valid?, "t4 should be valid" end + def test_validate_uniqueness_with_non_callable_conditions_is_not_supported + assert_raises(ArgumentError) { + Topic.validates_uniqueness_of :title, conditions: Topic.where(approved: true) + } + end + def test_validate_uniqueness_with_array_column return skip "Uniqueness on arrays has only been tested in PostgreSQL so far." if !current_adapter? :PostgreSQLAdapter diff --git a/activerecord/test/fixtures/friendships.yml b/activerecord/test/fixtures/friendships.yml index 1ee09175bf..ae0abe0162 100644 --- a/activerecord/test/fixtures/friendships.yml +++ b/activerecord/test/fixtures/friendships.yml @@ -1,4 +1,4 @@ Connection 1: id: 1 - person_id: 1 - friend_id: 2
\ No newline at end of file + friend_id: 1 + follower_id: 2 diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml index e640a38f1f..0ec05e8d56 100644 --- a/activerecord/test/fixtures/people.yml +++ b/activerecord/test/fixtures/people.yml @@ -5,6 +5,7 @@ michael: number1_fan_id: 3 gender: M followers_count: 1 + friends_too_count: 1 david: id: 2 first_name: David @@ -12,6 +13,7 @@ david: number1_fan_id: 1 gender: M followers_count: 1 + friends_too_count: 1 susan: id: 3 first_name: Susan @@ -19,3 +21,4 @@ susan: number1_fan_id: 1 gender: F followers_count: 1 + friends_too_count: 1 diff --git a/activerecord/test/fixtures/traffic_lights.yml b/activerecord/test/fixtures/traffic_lights.yml index 6dabd53474..81b4e47959 100644 --- a/activerecord/test/fixtures/traffic_lights.yml +++ b/activerecord/test/fixtures/traffic_lights.yml @@ -4,3 +4,7 @@ uk: - Green - Red - Orange + long_state: + - "Green, go ahead" + - "Red, wait" + - "Orange, caution light is about to switch"
\ No newline at end of file diff --git a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb new file mode 100644 index 0000000000..c066c068c2 --- /dev/null +++ b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb @@ -0,0 +1,12 @@ +# coding: ISO-8859-15 + +class CurrenciesHaveSymbols < ActiveRecord::Migration + def self.up + # We use ¤ for default currency symbol + add_column "currencies", "symbol", :string, :default => "¤" + end + + def self.down + remove_column "currencies", "symbol" + end +end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8423411474..a96899ae10 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -30,8 +30,8 @@ class Author < ActiveRecord::Base has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments has_many :funky_comments, :through => :posts, :source => :comments - has_many :ordered_uniq_comments, -> { uniq.order('comments.id') }, :through => :posts, :source => :comments - has_many :ordered_uniq_comments_desc, -> { uniq.order('comments.id DESC') }, :through => :posts, :source => :comments + has_many :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments + has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments has_many :readonly_comments, -> { readonly }, :through => :posts, :source => :comments has_many :special_posts @@ -78,7 +78,7 @@ class Author < ActiveRecord::Base has_many :categories_like_general, -> { where(:name => 'General') }, :through => :categorizations, :source => :category, :class_name => 'Category' has_many :categorized_posts, :through => :categorizations, :source => :post - has_many :unique_categorized_posts, -> { uniq }, :through => :categorizations, :source => :post + has_many :unique_categorized_posts, -> { distinct }, :through => :categorizations, :source => :post has_many :nothings, :through => :kateggorisatons, :class_name => 'Category' @@ -91,7 +91,7 @@ class Author < ActiveRecord::Base has_many :post_categories, :through => :posts, :source => :categories has_many :tagging_tags, :through => :taggings, :source => :tag - has_many :similar_posts, -> { uniq }, :through => :tags, :source => :tagged_posts + has_many :similar_posts, -> { distinct }, :through => :tags, :source => :tagged_posts has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, :through => :posts, :source => :tags has_many :tags_with_primary_key, :through => :posts diff --git a/activerecord/test/models/autoloadable/extra_firm.rb b/activerecord/test/models/autoloadable/extra_firm.rb new file mode 100644 index 0000000000..5578ba0d9b --- /dev/null +++ b/activerecord/test/models/autoloadable/extra_firm.rb @@ -0,0 +1,2 @@ +class ExtraFirm < Company +end diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index ce81a37966..5458a28cc9 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -2,7 +2,7 @@ class Book < ActiveRecord::Base has_many :authors has_many :citations, :foreign_key => 'book1_id' - has_many :references, -> { uniq }, :through => :citations, :source => :reference_of + has_many :references, -> { distinct }, :through => :citations, :source => :reference_of has_many :subscriptions has_many :subscribers, :through => :subscriptions diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb index 6b4f7acc38..4b411ca8e0 100644 --- a/activerecord/test/models/friendship.rb +++ b/activerecord/test/models/friendship.rb @@ -1,4 +1,6 @@ class Friendship < ActiveRecord::Base belongs_to :friend, class_name: 'Person' - belongs_to :follower, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :followers_count + # friend_too exists to test a bug, and probably shouldn't be used elsewhere + belongs_to :friend_too, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :friends_too_count + belongs_to :follower, class_name: 'Person' end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb index 6cfd443e75..69d4d7df1a 100644 --- a/activerecord/test/models/liquid.rb +++ b/activerecord/test/models/liquid.rb @@ -1,5 +1,4 @@ class Liquid < ActiveRecord::Base self.table_name = :liquid - has_many :molecules, -> { uniq } + has_many :molecules, -> { distinct } end - diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index fa717ef8d6..2985160d28 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -8,7 +8,10 @@ class Person < ActiveRecord::Base has_many :posts_with_no_comments, -> { includes(:comments).where('comments.id is null').references(:comments) }, :through => :readers, :source => :post - has_many :followers, foreign_key: 'friend_id', class_name: 'Friendship' + has_many :friendships, foreign_key: 'friend_id' + # friends_too exists to test a bug, and probably shouldn't be used elsewhere + has_many :friends_too, foreign_key: 'friend_id', class_name: 'Friendship' + has_many :followers, through: :friendships has_many :references has_many :bad_references diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 90273adafc..f893754b9f 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -1,11 +1,11 @@ class Project < ActiveRecord::Base - has_and_belongs_to_many :developers, -> { uniq.order 'developers.name desc, developers.id desc' } + has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' } has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer" - has_and_belongs_to_many :selected_developers, -> { uniq.select "developers.*" }, :class_name => "Developer" + has_and_belongs_to_many :selected_developers, -> { distinct.select "developers.*" }, :class_name => "Developer" has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer' has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer" - has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").uniq }, :class_name => "Developer" - has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').uniq }, :class_name => "Developer" + has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer" + has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').distinct }, :class_name => "Developer" has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer" ActiveSupport::Deprecation.silence do diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb index 228f3f7bd4..a6b7edb882 100644 --- a/activerecord/test/models/traffic_light.rb +++ b/activerecord/test/models/traffic_light.rb @@ -1,3 +1,4 @@ class TrafficLight < ActiveRecord::Base serialize :state, Array + serialize :long_state, Array end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index cd9835259a..a952738e84 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -219,6 +219,8 @@ ActiveRecord::Schema.define do t.integer :salary, :default => 70000 t.datetime :created_at t.datetime :updated_at + t.datetime :created_on + t.datetime :updated_on end create_table :developers_projects, :force => true, :id => false do |t| @@ -278,7 +280,7 @@ ActiveRecord::Schema.define do create_table :friendships, :force => true do |t| t.integer :friend_id - t.integer :person_id + t.integer :follower_id end create_table :goofy_string_id, :force => true, :id => false do |t| @@ -492,6 +494,7 @@ ActiveRecord::Schema.define do t.integer :lock_version, :null => false, :default => 0 t.string :comments t.integer :followers_count, :default => 0 + t.integer :friends_too_count, :default => 0 t.references :best_friend t.references :best_friend_of t.integer :insures, null: false, default: 0 @@ -685,6 +688,7 @@ ActiveRecord::Schema.define do create_table :traffic_lights, :force => true do |t| t.string :location t.string :state + t.text :long_state, :null => false t.datetime :created_at t.datetime :updated_at end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index c47cb75274..b629688591 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,11 +1,29 @@ ## Rails 4.0.0 (unreleased) ## -* Fix deletion of empty directories in ActiveSupport::Cache::FileStore. - +* `ActiveSupport::TimeWithZone` raises `NoMethodError` in proper context. + Fixes #9772. + + *Yves Senn* + +* Fix deletion of empty directories in `ActiveSupport::Cache::FileStore`. + *Charles Jones* ## Rails 4.0.0.beta1 (February 25, 2013) ## +* Improve singularizing a singular for multiple cases. + Fixes #2608 #1825 #2395. + + Example: + + # Before + 'address'.singularize # => 'addres' + + # After + 'address'.singularize # => 'address' + + *Mark McSpadden* + * Prevent `DateTime#change` from truncating the second fraction, when seconds do not need to be changed. diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 822c9d98ae..e3788ed54f 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -15,8 +15,6 @@ namespace :test do end end -# Create compressed packages -dist_dirs = [ "lib", "test"] spec = eval(File.read('activesupport.gemspec')) diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 5d8d09aa69..e51ab9ddbc 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -73,42 +73,44 @@ class Class instance_reader = instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) - # We use class_eval here rather than define_method because class_attribute - # may be used in a performance sensitive context therefore the overhead that - # define_method introduces may become significant. attrs.each do |name| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def self.#{name}() nil end - def self.#{name}?() !!#{name} end + define_singleton_method(name) { nil } + define_singleton_method("#{name}?") { !!public_send(name) } - def self.#{name}=(val) - singleton_class.class_eval do - remove_possible_method(:#{name}) - define_method(:#{name}) { val } - end + ivar = "@#{name}" + + define_singleton_method("#{name}=") do |val| + singleton_class.class_eval do + remove_possible_method(name) + define_method(name) { val } + end - if singleton_class? - class_eval do - remove_possible_method(:#{name}) - def #{name} - defined?(@#{name}) ? @#{name} : singleton_class.#{name} + if singleton_class? + class_eval do + remove_possible_method(name) + define_method(name) do + if instance_variable_defined? ivar + instance_variable_get ivar + else + singleton_class.send name end end end - val end + val + end - if instance_reader - remove_possible_method :#{name} - def #{name} - defined?(@#{name}) ? @#{name} : self.class.#{name} - end - - def #{name}? - !!#{name} + if instance_reader + remove_possible_method name + define_method(name) do + if instance_variable_defined?(ivar) + instance_variable_get ivar + else + self.class.public_send name end end - RUBY + define_method("#{name}?") { !!public_send(name) } + end attr_writer name if instance_writer end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 98c866ac43..4a032b0ad0 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -366,6 +366,8 @@ module ActiveSupport # TimeWithZone with the existing +time_zone+. def method_missing(sym, *args, &block) wrap_with_time_zone time.__send__(sym, *args, &block) + rescue NoMethodError => e + raise e, e.message.sub(time.inspect, self.inspect), e.backtrace end private diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index c5fbddcb5f..4b880cb5dc 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -62,6 +62,7 @@ module ActiveSupport "Newfoundland" => "America/St_Johns", "Brasilia" => "America/Sao_Paulo", "Buenos Aires" => "America/Argentina/Buenos_Aires", + "Montevideo" => "America/Montevideo", "Georgetown" => "America/Guyana", "Greenland" => "America/Godthab", "Mid-Atlantic" => "Atlantic/South_Georgia", @@ -238,7 +239,7 @@ module ActiveSupport # Compare #name and TZInfo identifier to a supplied regexp, returning +true+ # if a match is found. def =~(re) - return true if name =~ re || MAPPING[name] =~ re + re === name || re === MAPPING[name] end # Returns a textual representation of this time zone. diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index ede08e23d5..acd320dbe0 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -1,7 +1,7 @@ require 'logger' require 'abstract_unit' require 'active_support/cache' -require 'dependecies_test_helpers' +require 'dependencies_test_helpers' class CacheKeyTest < ActiveSupport::TestCase def test_entry_legacy_optional_ivars @@ -564,7 +564,7 @@ module LocalCacheBehavior end module AutoloadingCacheBehavior - include DependeciesTestHelpers + include DependenciesTestHelpers def test_simple_autoloading with_autoloading_fixtures do @cache.write('foo', E.new) diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb index ac79b15fa8..8f3f710dfd 100644 --- a/activesupport/test/core_ext/marshal_test.rb +++ b/activesupport/test/core_ext/marshal_test.rb @@ -1,10 +1,10 @@ require 'abstract_unit' require 'active_support/core_ext/marshal' -require 'dependecies_test_helpers' +require 'dependencies_test_helpers' class MarshalTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation - include DependeciesTestHelpers + include DependenciesTestHelpers def teardown ActiveSupport::Dependencies.clear diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 8c7d00cae1..3744d50864 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -153,22 +153,16 @@ end class NumericExtSizeTest < ActiveSupport::TestCase def test_unit_in_terms_of_another - relationships = { - 1024.bytes => 1.kilobyte, - 1024.kilobytes => 1.megabyte, - 3584.0.kilobytes => 3.5.megabytes, - 3584.0.megabytes => 3.5.gigabytes, - 1.kilobyte ** 4 => 1.terabyte, - 1024.kilobytes + 2.megabytes => 3.megabytes, - 2.gigabytes / 4 => 512.megabytes, - 256.megabytes * 20 + 5.gigabytes => 10.gigabytes, - 1.kilobyte ** 5 => 1.petabyte, - 1.kilobyte ** 6 => 1.exabyte - } - - relationships.each do |left, right| - assert_equal right, left - end + assert_equal 1024.bytes, 1.kilobyte + assert_equal 1024.kilobytes, 1.megabyte + assert_equal 3584.0.kilobytes, 3.5.megabytes + assert_equal 3584.0.megabytes, 3.5.gigabytes + assert_equal 1.kilobyte ** 4, 1.terabyte + assert_equal 1024.kilobytes + 2.megabytes, 3.megabytes + assert_equal 2.gigabytes / 4, 512.megabytes + assert_equal 256.megabytes * 20 + 5.gigabytes, 10.gigabytes + assert_equal 1.kilobyte ** 5, 1.petabyte + assert_equal 1.kilobyte ** 6, 1.exabyte end def test_units_as_bytes_independently diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 8d1a8c628c..92f996f9a4 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -47,7 +47,7 @@ class ToQueryTest < ActiveSupport::TestCase end private - def assert_query_equal(expected, actual, message = nil) + def assert_query_equal(expected, actual) assert_equal expected.split('&'), actual.to_query.split('&') end end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index bff155f045..62c5741ffb 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -293,16 +293,16 @@ end class StringConversionsTest < ActiveSupport::TestCase def test_string_to_time - with_env_tz "US/Eastern" do + with_env_tz "Europe/Moscow" do assert_equal Time.utc(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time(:utc) assert_equal Time.local(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time assert_equal Time.utc(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time(:utc) assert_equal Time.local(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time assert_equal Time.utc(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time(:utc) assert_equal Time.local(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time - assert_equal Time.local(2011, 2, 27, 18, 50), "2011-02-27 22:50 -0100".to_time + assert_equal Time.local(2011, 2, 27, 18, 50), "2011-02-27 13:50 -0100".to_time assert_equal Time.utc(2011, 2, 27, 23, 50), "2011-02-27 22:50 -0100".to_time(:utc) - assert_equal Time.local(2005, 2, 27, 23, 50), "2005-02-27 23:50 -0500".to_time + assert_equal Time.local(2005, 2, 27, 23, 50), "2005-02-27 14:50 -0500".to_time assert_nil "".to_time end end @@ -317,11 +317,11 @@ class StringConversionsTest < ActiveSupport::TestCase end def test_partial_string_to_time - with_env_tz "US/Eastern" do + with_env_tz "Europe/Moscow" do now = Time.now assert_equal Time.local(now.year, now.month, now.day, 23, 50), "23:50".to_time assert_equal Time.utc(now.year, now.month, now.day, 23, 50), "23:50".to_time(:utc) - assert_equal Time.local(now.year, now.month, now.day, 18, 50), "22:50 -0100".to_time + assert_equal Time.local(now.year, now.month, now.day, 18, 50), "13:50 -0100".to_time assert_equal Time.utc(now.year, now.month, now.day, 23, 50), "22:50 -0100".to_time(:utc) end end diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 0f5699fd63..98a87ab9e6 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -779,6 +779,14 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", (twz - 1.year).inspect end + def test_no_method_error_has_proper_context + e = assert_raises(NoMethodError) { + @twz.this_method_does_not_exist + } + assert_equal "undefined method `this_method_does_not_exist' for Fri, 31 Dec 1999 19:00:00 EST -05:00:Time", e.message + assert_no_match "rescue", e.backtrace.first + end + protected def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 615808090d..115a4e894d 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'pp' require 'active_support/dependencies' -require 'dependecies_test_helpers' +require 'dependencies_test_helpers' module ModuleWithMissing mattr_accessor :missing_count @@ -20,7 +20,7 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear end - include DependeciesTestHelpers + include DependenciesTestHelpers def test_depend_on_path skip "LoadError#path does not exist" if RUBY_VERSION < '2.0.0' @@ -878,7 +878,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_autoload_doesnt_shadow_name_error with_autoloading_fixtures do Object.send(:remove_const, :RaisesNameError) if defined?(::RaisesNameError) - 2.times do |i| + 2.times do begin ::RaisesNameError::FooBarBaz.object_id flunk 'should have raised NameError when autoloaded file referenced FooBarBaz' diff --git a/activesupport/test/dependecies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb index 4b46d32fb4..9268512a97 100644 --- a/activesupport/test/dependecies_test_helpers.rb +++ b/activesupport/test/dependencies_test_helpers.rb @@ -1,4 +1,4 @@ -module DependeciesTestHelpers +module DependenciesTestHelpers def with_loading(*from) old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load this_dir = File.dirname(__FILE__) diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index a1e5db6a2e..22cb61ffd6 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -61,9 +61,7 @@ class InflectorTest < ActiveSupport::TestCase assert_equal(plural, ActiveSupport::Inflector.pluralize(plural)) assert_equal(plural.capitalize, ActiveSupport::Inflector.pluralize(plural.capitalize)) end - end - SingularToPlural.each do |singular, plural| define_method "test_singularize_singular_#{singular}" do assert_equal(singular, ActiveSupport::Inflector.singularize(singular)) assert_equal(singular.capitalize, ActiveSupport::Inflector.singularize(singular.capitalize)) @@ -326,7 +324,7 @@ class InflectorTest < ActiveSupport::TestCase end def test_underscore_as_reverse_of_dasherize - UnderscoresToDashes.each do |underscored, dasherized| + UnderscoresToDashes.each_key do |underscored| assert_equal(underscored, ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.dasherize(underscored))) end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 9c3b5d0667..84c3154e53 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -232,6 +232,22 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Time.utc(2012, 5, 28, 7, 0, 0), twz.utc end + def test_parse_doesnt_use_local_dst + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['UTC'] + twz = zone.parse('2013-03-10 02:00:00') + assert_equal Time.utc(2013, 3, 10, 2, 0, 0), twz.time + end + end + + def test_parse_handles_dst_jump + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.parse('2013-03-10 02:00:00') + assert_equal Time.utc(2013, 3, 10, 3, 0, 0), twz.time + end + end + def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize tzinfo = TZInfo::Timezone.get('America/New_York') zone = ActiveSupport::TimeZone.create(tzinfo.name, nil, tzinfo) diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index b5d8142458..ce91c443e1 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -17,7 +17,7 @@ class TransliterateTest < ActiveSupport::TestCase # some reason or other are floating in the middle of all the letters. string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include?(c)}.pack("U*") string.each_char do |char| - assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(string) + assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(char) end end diff --git a/guides/assets/images/getting_started/unknown_action_create_for_posts.png b/guides/assets/images/getting_started/unknown_action_create_for_posts.png Binary files differindex c6750e1ae1..c0de53555d 100644 --- a/guides/assets/images/getting_started/unknown_action_create_for_posts.png +++ b/guides/assets/images/getting_started/unknown_action_create_for_posts.png diff --git a/guides/code/getting_started/app/controllers/comments_controller.rb b/guides/code/getting_started/app/controllers/comments_controller.rb index 0082e9c8ec..0e3d2a6dde 100644 --- a/guides/code/getting_started/app/controllers/comments_controller.rb +++ b/guides/code/getting_started/app/controllers/comments_controller.rb @@ -1,7 +1,7 @@ class CommentsController < ApplicationController -  http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy - + http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy + def create @post = Post.find(params[:post_id]) @comment = @post.comments.create(params[:comment].permit(:commenter, :body)) diff --git a/guides/code/getting_started/app/controllers/posts_controller.rb b/guides/code/getting_started/app/controllers/posts_controller.rb index 0398395200..6aa1409170 100644 --- a/guides/code/getting_started/app/controllers/posts_controller.rb +++ b/guides/code/getting_started/app/controllers/posts_controller.rb @@ -1,7 +1,7 @@ class PostsController < ApplicationController -  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show] - + http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show] + def index @posts = Post.all end diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index 463da488f2..37afb25181 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -178,12 +178,6 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activ If migrating down, the given migration / block is run normally. See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#reverting-previous-migrations) -* Adds some metadata columns to `schema_migrations` table. - - * `migrated_at` - * `fingerprint` - an md5 hash of the migration. - * `name` - the filename minus version and extension. - * Adds PostgreSQL array type support. Any datatype can be used to create an array column, with full migration and schema dumper support. * Add `Relation#load` to explicitly load the record and return `self`. diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 5861fc3d54..7e0a8a43d4 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -290,6 +290,20 @@ parameters: params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy]) ``` +Hashes with integer keys are treated differently and you can declare +the attributes as if they were direct children. You get this kind of +parameters when you use `accepts_nested_attributes_for` in combination +with a `has_many` association: + +```ruby +# To whitelist the following data: +# {"book" => {"title" => "Some Book", +# "chapters_attributes" => { "1" => {"title" => "First Chapter"}, +# "2" => {"title" => "Second Chapter"}}}} + +params.require(:book).permit(:title, chapters_attributes: [:title]) +``` + #### Outside the Scope of Strong Parameters The strong parameter API was designed with the most common use cases diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 8720aae169..31182e9aed 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -403,7 +403,7 @@ If you wish to override the default delivery options (e.g. SMTP credentials) whi ```ruby class UserMailer < ActionMailer::Base - def welcome_email(user,company) + def welcome_email(user, company) @user = user @url = user_url(@user) delivery_options = { user_name: company.smtp_user, password: company.smtp_password, address: company.smtp_host } @@ -412,6 +412,19 @@ class UserMailer < ActionMailer::Base end ``` +### Sending Emails without Template Rendering + +There may be cases in which you want to skip the template rendering step and supply the email body as a string. You can achieve this using the `:body` option. +In such cases don't forget to add the `:content_type` option. Rails will default to `text/plain` otherwise. + +```ruby +class UserMailer < ActionMailer::Base + def welcome_email(user, email_body) + mail(to: user.email, body: email_body, content_type: "text/html", subject: "Already rendered!") + end +end +``` + Receiving Emails ---------------- diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 0d0813c56a..7355f6816c 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -76,6 +76,7 @@ The methods are: * `reorder` * `reverse_order` * `select` +* `distinct` * `uniq` * `where` @@ -580,10 +581,10 @@ ActiveModel::MissingAttributeError: missing attribute: <attribute> Where `<attribute>` is the attribute you asked for. The `id` method will not raise the `ActiveRecord::MissingAttributeError`, so just be careful when working with associations because they need the `id` method to function properly. -If you would like to only grab a single record per unique value in a certain field, you can use `uniq`: +If you would like to only grab a single record per unique value in a certain field, you can use `distinct`: ```ruby -Client.select(:name).uniq +Client.select(:name).distinct ``` This would generate SQL like: @@ -595,10 +596,10 @@ SELECT DISTINCT name FROM clients You can also remove the uniqueness constraint: ```ruby -query = Client.select(:name).uniq +query = Client.select(:name).distinct # => Returns unique names -query.uniq(false) +query.distinct(false) # => Returns all names, even if there are duplicates ``` @@ -1196,6 +1197,61 @@ Using a class method is the preferred way to accept arguments for scopes. These category.posts.created_before(time) ``` +### Merging of scopes + +Just like `where` clauses scopes are merged using `AND` conditions. + +```ruby +class User < ActiveRecord::Base + scope :active, -> { where state: 'active' } + scope :inactive, -> { where state: 'inactive' } +end + +```ruby +User.active.inactive +# => SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive' +``` + +We can mix and match `scope` and `where` conditions and the final sql +will have all conditions joined with `AND` . + +```ruby +User.active.where(state: 'finished') +# => SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished' +``` + +If we do want the `last where clause` to win then `Relation#merge` can +be used . + +```ruby +User.active.merge(User.inactive) +# => SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive' +``` + +One important caveat is that `default_scope` will be overridden by +`scope` and `where` conditions. + +```ruby +class User < ActiveRecord::Base + default_scope { where state: 'pending' } + scope :active, -> { where state: 'active' } + scope :inactive, -> { where state: 'inactive' } +end + +User.all +# => SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' + +User.active +# => SELECT "users".* FROM "users" WHERE "users"."state" = 'active' + +User.where(state: 'inactive') +# => SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive' +``` + +As you can see above the `default_scope` is being overridden by both +`scope` and `where` conditions. + + ### Applying a default scope If we wish for a scope to be applied across all queries to the model we can use the @@ -1383,7 +1439,7 @@ Client.where(active: true).pluck(:id) # SELECT id FROM clients WHERE active = 1 # => [1, 2, 3] -Client.uniq.pluck(:role) +Client.distinct.pluck(:role) # SELECT DISTINCT role FROM clients # => ['admin', 'member', 'guest'] @@ -1399,7 +1455,7 @@ Client.select(:id).map { |c| c.id } # or Client.select(:id).map(&:id) # or -Client.select(:id).map { |c| [c.id, c.name] } +Client.select(:id, :name).map { |c| [c.id, c.name] } ``` with diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 6b3be69942..d08000eb69 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -450,7 +450,7 @@ ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*a data # { extra: :information } ``` -You may also subscribe to events matching a regular expresssion. This enables you to subscribe to +You may also subscribe to events matching a regular expression. This enables you to subscribe to multiple events at once. Here's you could subscribe to everything from `ActionController`. ```ruby diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index cb0a7c8026..65c8154064 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -1109,7 +1109,7 @@ end Controls what happens to the associated object when its owner is destroyed: * `:destroy` causes the associated object to also be destroyed -* `:delete` causes the asssociated object to be deleted directly from the database (so callbacks will not execute) +* `:delete` causes the associated object to be deleted directly from the database (so callbacks will not execute) * `:nullify` causes the foreign key to be set to `NULL`. Callbacks are not executed. * `:restrict_with_exception` causes an exception to be raised if there is an associated record * `:restrict_with_error` causes an error to be added to the owner if there is an associated object @@ -1463,7 +1463,7 @@ end Controls what happens to the associated objects when their owner is destroyed: * `:destroy` causes all the associated objects to also be destroyed -* `:delete_all` causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute) +* `:delete_all` causes all the associated objects to be deleted directly from the database (so callbacks will not execute) * `:nullify` causes the foreign keys to be set to `NULL`. Callbacks are not executed. * `:restrict_with_exception` causes an exception to be raised if there are any associated records * `:restrict_with_error` causes an error to be added to the owner if there are any associated objects diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index a1d7e955c8..cd23b5ee15 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -103,7 +103,7 @@ To verify that you have everything installed correctly, you should be able to ru $ rails --version ``` -If it says something like "Rails 3.2.9", you are ready to continue. +If it says something like "Rails 4.0.0", you are ready to continue. ### Creating the Blog Application @@ -568,7 +568,7 @@ interested in. We also use an instance variable (prefixed by `@`) to hold a reference to the post object. We do this because Rails will pass all instance variables to the view. -Now, create a new file `app/view/posts/show.html.erb` with the following +Now, create a new file `app/views/posts/show.html.erb` with the following content: ```html+erb diff --git a/guides/source/migrations.md b/guides/source/migrations.md index 89ae564c24..bd63970bea 100644 --- a/guides/source/migrations.md +++ b/guides/source/migrations.md @@ -61,6 +61,10 @@ migrations are wrapped in a transaction. If the database does not support this then when a migration fails the parts of it that succeeded will not be rolled back. You will have to rollback the changes that were made by hand. +NOTE: There are certain queries that can't run inside a transaction. If your +adapter supports DDL transactions you can use `disable_ddl_transaction!` to +disable them for a single migration. + If you wish for a migration to do something that Active Record doesn't know how to reverse, you can use `reversible`: @@ -180,7 +184,7 @@ end ``` If the migration name is of the form "CreateXXX" and is -followed by a list of column names and types then a migration creating the table +followed by a list of column names and types then a migration creating the table XXX with the columns listed will be generated. For example: ```bash diff --git a/guides/source/testing.md b/guides/source/testing.md index 540197e6e7..1937cbf17a 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1,8 +1,7 @@ A Guide to Testing Rails Applications ===================================== -This guide covers built-in mechanisms offered by Rails to test your -application. +This guide covers built-in mechanisms in Rails for testing your application. After reading this guide, you will know: @@ -38,11 +37,11 @@ Rails creates a `test` folder for you as soon as you create a Rails project usin ```bash $ ls -F test - -fixtures/ functional/ integration/ test_helper.rb unit/ +controllers/ helpers/ mailers/ test_helper.rb +fixtures/ integration/ models/ ``` -The `unit` directory is meant to hold tests for your models, the `functional` directory is meant to hold tests for your controllers and the `integration` directory is meant to hold tests that involve any number of controllers interacting. +The `models` directory is meant to hold tests for your models, the `controllers` directory is meant to hold tests for your controllers and the `integration` directory is meant to hold tests that involve any number of controllers interacting. Fixtures are a way of organizing test data; they reside in the `fixtures` folder. @@ -140,10 +139,9 @@ The default test stub in `test/models/post_test.rb` looks like this: require 'test_helper' class PostTest < ActiveSupport::TestCase - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end end ``` @@ -224,34 +222,30 @@ TIP: You can see all these rake tasks and their descriptions by running `rake -- ### Running Tests -Running a test is as simple as invoking the file containing the test cases through Ruby: +Running a test is as simple as invoking the file containing the test cases through `rails test` command. ```bash -$ ruby -Itest test/models/post_test.rb - -Loaded suite models/post_test -Started +$ rails test test/models/post_test.rb . -Finished in 0.023513 seconds. -1 tests, 1 assertions, 0 failures, 0 errors -``` +Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. -This will run all the test methods from the test case. Note that `test_helper.rb` is in the `test` directory, hence this directory needs to be added to the load path using the `-I` switch. +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips +``` -You can also run a particular test method from the test case by using the `-n` switch with the `test method name`. +You can also run a particular test method from the test case by running the test and using `-n` switch with the `test method name`. ```bash -$ ruby -Itest test/models/post_test.rb -n test_the_truth - -Loaded suite models/post_test -Started +$ rails test test/models/post_test.rb -n test_the_truth . -Finished in 0.023513 seconds. -1 tests, 1 assertions, 0 failures, 0 errors +Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. + +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips ``` +This will run all test methods from the test case. Note that `test_helper.rb` is in the `test` directory, hence this directory needs to be added to the load path using the `-I` switch. + The `.` (dot) above indicates a passing test. When a test fails you see an `F`; when a test throws an error you see an `E` in its place. The last line of the output is the summary. To see how a test failure is reported, you can add a failing test to the `post_test.rb` test case. @@ -266,17 +260,16 @@ end Let us run this newly added test. ```bash -$ ruby unit/post_test.rb -n test_should_not_save_post_without_title -Loaded suite -e -Started +$ rails test test/models/post_test.rb -n test_should_not_save_post_without_title F -Finished in 0.102072 seconds. + +Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s. 1) Failure: -test_should_not_save_post_without_title(PostTest) [/test/models/post_test.rb:6]: -<false> is not true. +test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]: +Failed assertion, no message given. -1 tests, 1 assertions, 1 failures, 0 errors +1 tests, 1 assertions, 1 failures, 0 errors, 0 skips ``` In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message which mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: @@ -292,9 +285,8 @@ Running this test shows the friendlier assertion message: ```bash 1) Failure: -test_should_not_save_post_without_title(PostTest) [/test/models/post_test.rb:6]: -Saved the post without a title. -<false> is not true. +test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]: +Saved the post without a title ``` Now to get this test to pass we can add a model level validation for the _title_ field. @@ -308,13 +300,12 @@ end Now the test should pass. Let us verify by running the test again: ```bash -$ ruby unit/post_test.rb -n test_should_not_save_post_without_title -Loaded suite unit/post_test -Started +$ rails test test/models/post_test.rb -n test_should_not_save_post_without_title . -Finished in 0.193608 seconds. -1 tests, 1 assertions, 0 failures, 0 errors +Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. + +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips ``` Now, if you noticed, we first wrote a test which fails for a desired functionality, then we wrote some code which adds the functionality and finally we ensured that our test passes. This approach to software development is referred to as _Test-Driven Development_ (TDD). @@ -334,18 +325,17 @@ end Now you can see even more output in the console from running the tests: ```bash -$ ruby unit/post_test.rb -n test_should_report_error -Loaded suite -e -Started +$ rails test test/models/post_test.rb -n test_should_report_error E -Finished in 0.082603 seconds. + +Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s. 1) Error: test_should_report_error(PostTest): -NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x249d354> - /test/models/post_test.rb:6:in `test_should_report_error' +NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x007fe32e24afe0> + test/models/post_test.rb:10:in `block in <class:PostTest>' -1 tests, 0 assertions, 0 failures, 1 errors +1 tests, 0 assertions, 0 failures, 1 errors, 0 skips ``` Notice the 'E' in the output. It denotes a test with error. @@ -511,6 +501,21 @@ You also have access to three instance variables in your functional tests: * `@request` - The request * `@response` - The response +### Setting Headers and CGI variables + +Headers and cgi variables can be set directly on the `@request` +instance variable: + +```ruby +# setting a HTTP Header +@request.headers["Accepts"] = "text/plain, text/html" +get :index # simulate the request with custom header + +# setting a CGI variable +@request.headers["HTTP_REFERER"] = "http://example.com/home" +post :create # simulate the request with custom env variable +``` + ### Testing Templates and Layouts If you want to make sure that the response rendered the correct template and layout, you can use the `assert_template` @@ -642,12 +647,9 @@ Here's what a freshly-generated integration test looks like: require 'test_helper' class UserFlowsTest < ActionDispatch::IntegrationTest - fixtures :all - - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end end ``` @@ -755,23 +757,28 @@ end Rake Tasks for Running your Tests --------------------------------- -You don't need to set up and run your tests by hand on a test-by-test basis. Rails comes with a number of rake tasks to help in testing. The table below lists all rake tasks that come along in the default Rakefile when you initiate a Rails project. +You don't need to set up and run your tests by hand on a test-by-test basis. Rails comes with a number of commands to help in testing. The table below lists all commands that come along in the default Rakefile when you initiate a Rails project. + +| Tasks | Description | +| ------------------------ | ----------- | +| `rails test` | Runs all unit, functional and integration tests. You can also simply run `rails test` as Rails will run all the tests by default| +| `rails test controllers` | Runs all the controller tests from `test/controllers`| +| `rails test functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional`| +| `rails test helpers` | Runs all the helper tests from `test/helpers`| +| `rails test integration` | Runs all the integration tests from `test/integration`| +| `rails test mailers` | Runs all the mailer tests from `test/mailers`| +| `rails test models` | Runs all the model tests from `test/models`| +| `rails test units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit`| -| Tasks | Description | -| ------------------------------- | ----------- | -| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake` as the _test_ target is the default.| -| `rake test:controllers` | Runs all the controller tests from `test/controllers`| -| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional`| -| `rake test:helpers` | Runs all the helper tests from `test/helpers`| -| `rake test:integration` | Runs all the integration tests from `test/integration`| -| `rake test:mailers` | Runs all the mailer tests from `test/mailers`| -| `rake test:models` | Runs all the model tests from `test/models`| -| `rake test:recent` | Tests recent changes| -| `rake test:uncommitted` | Runs all the tests which are uncommitted. Supports Subversion and Git| -| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit`| +There're also some test commands which you can initiate by running rake tasks: +| Tasks | Description | +| ------------------------ | ----------- | +| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake` as the _test_ target is the default.| +| `rake test:recent` | Tests recent changes| +| `rake test:uncommitted` | Runs all the tests which are uncommitted. Supports Subversion and Git| -Brief Note About `Test::Unit` +Brief Note About `MiniTest` ----------------------------- Ruby ships with a boat load of libraries. Ruby 1.8 provides `Test::Unit`, a framework for unit testing in Ruby. All the basic assertions discussed above are actually defined in `Test::Unit::Assertions`. The class `ActiveSupport::TestCase` which we have been using in our unit and functional tests extends `Test::Unit::TestCase`, allowing diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 57945a256b..0941bc7e58 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -43,7 +43,7 @@ Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must rep * Rails 4.0 has changed how orders get stacked in `ActiveRecord::Relation`. In previous versions of Rails, the new order was applied after the previously defined order. But this is no longer true. Check [Active Record Query guide](active_record_querying.html#ordering) for more information. -* Rails 4.0 has changed `serialized_attributes` and `attr_readonly` to class methods only. Now you shouldn't use instance methods, it's deprecated. You must change them, e.g. `self.serialized_attributes` to `self.class.serialized_attributes`. +* Rails 4.0 has changed `serialized_attributes` and `attr_readonly` to class methods only. You shouldn't use instance methods since it's now deprecated. You should change them to use class methods, e.g. `self.serialized_attributes` to `self.class.serialized_attributes`. * Rails 4.0 has removed `attr_accessible` and `attr_protected` feature in favor of Strong Parameters. You can use the [Protected Attributes gem](https://github.com/rails/protected_attributes) to a smoothly upgrade path. @@ -65,7 +65,7 @@ Rails 4.0 extracted Active Resource to its own gem. If you still need the featur ### Active Model -* Rails 4.0 has changed how errors attach with the `ActiveModel::Validations::ConfirmationValidator`. Now when confirmation validations fail the error will be attached to `:#{attribute}_confirmation` instead of `attribute`. +* Rails 4.0 has changed how errors attach with the `ActiveModel::Validations::ConfirmationValidator`. Now when confirmation validations fail, the error will be attached to `:#{attribute}_confirmation` instead of `attribute`. * Rails 4.0 has changed `ActiveModel::Serializers::JSON.include_root_in_json` default value to `false`. Now, Active Model Serializers and Active Record objects have the same default behaviour. This means that you can comment or remove the following option in the `config/initializers/wrap_parameters.rb` file: @@ -103,6 +103,32 @@ Rails 4.0 extracted Active Resource to its own gem. If you still need the featur * Rails 4.0 changed how `assert_generates`, `assert_recognizes`, and `assert_routing` work. Now all these assertions raise `Assertion` instead of `ActionController::RoutingError`. +* Rails 4.0 correctly prefers the first named route defined in `config/routes.rb` if a clashing route is found later. Check the output of `rake routes` before upgrading and remove unused named routes to avoid issues. + +```ruby + # config/routes.rb + get 'one' => 'test#example', as: :example + get 'two' => 'test#example', as: :example + + # Rails 3 + <%= example_path %> # => '/two' + + # Rails 4 + <%= example_path %> # => '/one' +``` + +```ruby + # config/routes.rb + resources :examples + get 'clashing/:id' => 'test#example', as: :example + + # Rails 3 + <%= example_path(1) %> # => '/clashing/1' + + # Rails 4 + <%= example_path(1) %> # => '/examples/1' +``` + * Rails 4.0 also changed the way unicode character routes are drawn. Now you can draw unicode character routes directly. If you already draw such routes, you must change them, for example: ```ruby @@ -128,7 +154,7 @@ get 'ã“ã‚“ã«ã¡ã¯', controller: 'welcome', action: 'index' get "/" => "root#index" ``` -* Rails 4.0 has removed ActionDispatch::BestStandardsSupport middleware, !DOCTYPE html already triggers standards mode per http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx and ChromeFrame header has been moved to `config.action_dispatch.default_headers` +* Rails 4.0 has removed `ActionDispatch::BestStandardsSupport` middleware, `<!DOCTYPE html>` already triggers standards mode per http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx and ChromeFrame header has been moved to `config.action_dispatch.default_headers`. Remember you must also remove any references to the middleware from your application code, for example: diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 4f7cb8254f..60a823de15 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,21 +1,66 @@ ## Rails 4.0.0 (unreleased) ## +* Add support for generate scaffold password:digest + + * adds password_digest attribute to the migration + * adds has_secure_password to the model + * adds password and password_confirmation password_fields to _form.html + * omits password from index.html and show.html + * adds password and password_confirmation to the controller + * adds unencrypted password and password_confirmation to the controller test + * adds encrypted password_digest to the fixture + + *Sam Ruby* + +* Rails now generates a `test/test_helper.rb` file with `fixtures :all` commented out by default, + since we don't want to force loading all fixtures for user when a single test is run. However, + fixtures are still going to be loaded automatically for test suites. + + To force all fixtures to be create in your database, use `rails test -f` to run your test. + + *Prem Sichanugrist* + +* Add `rails test` command for running tests + + To run all tests: + + $ rails test + + To run a test suite + + $ rails test [models,helpers,units,controllers,mailers,...] + + To run a selected test file(s): + + $ rails test test/unit/foo_test.rb [test/unit/bar_test.rb ...] + + To run a single test from a test file + + $ rails test test/unit/foo_test.rb -n test_the_truth + + For more information, see `rails test --help`. + + This command will eventually replace `rake test:*` and `rake test` tasks. + + *Prem Sichanugrist and Chris Toomey* + +* Improve service pages with new layout (404, etc). *Stanislav Sobolev* + ## Rails 4.0.0.beta1 (February 25, 2013) ## -* Change Service pages(404, etc). *Stanislav Sobolev* * Improve `rake stats` for JavaScript and CoffeeScript: ignore block comments and calculates number of functions. *Hendy Tanata* -* Ability to use a custom builder by passing `--builder` (or `-b`) has been removed. Consider - using application template instead. See this guide for more detail: +* Ability to use a custom builder by passing `--builder` (or `-b`) has been removed. + Consider using application template instead. See this guide for more detail: http://guides.rubyonrails.org/rails_application_templates.html *Prem Sichanugrist* -* fix rake db:* tasks to work with DATABASE_URL and without config/database.yml +* Fix `rake db:*` tasks to work with `DATABASE_URL` and without `config/database.yml`. *Terence Lee* @@ -39,13 +84,13 @@ *Amparo Luna* * Fixes database.yml when creating a new rails application with '.' - Fix #8304 + Fixes #8304. *Jeremy W. Rowe* * Restore Rails::Engine::Railties#engines with deprecation to ensure compatibility with gems such as Thinking Sphinx - Fix #8551 + Fixes #8551. *Tim Raymond* @@ -119,7 +164,7 @@ * Environment name can be a start substring of the default environment names (production, development, test). For example: tes, pro, prod, dev, devel. - Fix #8628. + Fixes #8628. *Mykola Kyryk* @@ -129,7 +174,7 @@ * Quote column names in generates fixture files. This prevents conflicts with reserved YAML keywords such as 'yes' and 'no' - Fix #8612. + Fixes #8612. *Yves Senn* @@ -162,19 +207,19 @@ * Add `db` to list of folders included by `rake notes` and `rake notes:custom`. *Antonio Cangiano* * Engines with a dummy app include the rake tasks of dependencies in the app namespace. - Fix #8229 + Fixes #8229. *Yves Senn* * Add `sqlserver.yml` template file to satisfy `-d sqlserver` being passed to `rails new`. - Fix #6882 + Fixes #6882. *Robert Nesius* * Rake test:uncommitted finds git directory in ancestors *Nicolas Despres* * Add dummy app Rake tasks when `--skip-test-unit` and `--dummy-path` is passed to the plugin generator. - Fix #8121 + Fixes #8121. *Yves Senn* diff --git a/railties/Rakefile b/railties/Rakefile index 8576275aea..eb068fc526 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -6,6 +6,8 @@ require 'rbconfig' task :default => :test + +desc "Run all unit tests" task :test => 'test:isolated' namespace :test do diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 25cc36ff5d..2417bdca21 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -46,10 +46,10 @@ module Rails # 6) Run config.before_initialize callbacks # 7) Run Railtie#initializer defined by railties, engines and application. # One by one, each engine sets up its load paths, routes and runs its config/initializers/* files. - # 9) Custom Railtie#initializers added by railties, engines and applications are executed - # 10) Build the middleware stack and run to_prepare callbacks - # 11) Run config.before_eager_load and eager_load! if eager_load is true - # 12) Run config.after_initialize callbacks + # 8) Custom Railtie#initializers added by railties, engines and applications are executed + # 9) Build the middleware stack and run to_prepare callbacks + # 10) Run config.before_eager_load and eager_load! if eager_load is true + # 11) Run config.after_initialize callbacks # class Application < Engine autoload :Bootstrap, 'rails/application/bootstrap' diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index aacde52cfc..41d3722c18 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -5,6 +5,7 @@ aliases = { "d" => "destroy", "c" => "console", "s" => "server", + "t" => "test", "db" => "dbconsole", "r" => "runner" } @@ -16,6 +17,7 @@ The most common rails commands are: generate Generate new code (short-cut alias: "g") console Start the Rails console (short-cut alias: "c") server Start the Rails server (short-cut alias: "s") + test Running the test file (short-cut alias: "t") dbconsole Start a console for the database specified in config/database.yml (short-cut alias: "db") new Create a new Rails application. "rails new my_app" creates a @@ -78,6 +80,15 @@ when 'server' server.start end +when 'test' + $LOAD_PATH.unshift("./test") + require 'rails/commands/test_runner' + options = Rails::TestRunner.parse_arguments(ARGV) + ENV['RAILS_ENV'] ||= options[:environment] || 'test' + + require APP_PATH + Rails::TestRunner.start(ARGV, options) + when 'dbconsole' require 'rails/commands/dbconsole' Rails::DBConsole.start diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb index 86ab1aabbf..96229bb4f6 100644 --- a/railties/lib/rails/commands/console.rb +++ b/railties/lib/rails/commands/console.rb @@ -46,7 +46,10 @@ module Rails def initialize(app, options={}) @app = app @options = options + + app.sandbox = sandbox? app.load_console + @console = app.config.console || IRB end @@ -71,7 +74,6 @@ module Rails end def start - app.sandbox = sandbox? require_debugger if debugger? set_environment! if environment? diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index cdb29a8156..876d457359 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -42,8 +42,12 @@ module Rails set_environment end + # TODO: this is no longer required but we keep it for the moment to support older config.ru files. def app - @app ||= super.respond_to?(:to_app) ? super.to_app : super + @app ||= begin + app = super + app.respond_to?(:to_app) ? app.to_app : app + end end def opt_parser diff --git a/railties/lib/rails/commands/test_runner.rb b/railties/lib/rails/commands/test_runner.rb new file mode 100644 index 0000000000..d8857bd183 --- /dev/null +++ b/railties/lib/rails/commands/test_runner.rb @@ -0,0 +1,146 @@ +require 'optparse' +require 'minitest/unit' + +module Rails + # Handles all logic behind +rails test+ command. + class TestRunner + class << self + # Creates a new +TestRunner+ object with an array of test files to run + # based on the arguments. When no arguments are provided, it runs all test + # files. When a suite argument is provided, it runs only the test files in + # that suite. Otherwise, it runs the specified test file(s). + def start(files, options = {}) + original_fixtures_options = options.delete(:fixtures) + options[:fixtures] = true + + case files.first + when nil + new(Dir['test/**/*_test.rb'], options).run + when 'models' + new(Dir['test/models/**/*_test.rb'], options).run + when 'helpers' + new(Dir['test/helpers/**/*_test.rb'], options).run + when 'units' + new(Dir['test/{models,helpers,unit}/**/*_test.rb'], options).run + when 'controllers' + new(Dir['test/controllers/**/*_test.rb'], options).run + when 'mailers' + new(Dir['test/mailers/**/*_test.rb'], options).run + when 'functionals' + new(Dir['test/{controllers,mailers,functional}/**/*_test.rb'], options).run + when 'integration' + new(Dir['test/integration/**/*_test.rb'], options).run + else + options[:fixtures] = original_fixtures_options + new(files, options).run + end + end + + # Parses arguments and sets them as option flags + def parse_arguments(arguments) + options = {} + orig_arguments = arguments.dup + + OptionParser.new do |opts| + opts.banner = "Usage: rails test [path to test file(s) or test suite]" + + opts.separator "" + opts.separator "Run a specific test file(s) or a test suite, under Rails'" + opts.separator "environment. If the file name(s) or suit name is omitted," + opts.separator "Rails will run all tests." + opts.separator "" + opts.separator "Specific options:" + + opts.on '-h', '--help', 'Display this help.' do + puts opts + exit + end + + opts.on '-e', '--environment NAME', String, 'Specifies the environment to run this test under' do |e| + options[:environment] = e + end + + opts.on '-f', '--fixtures', 'Load fixtures in test/fixtures/ before running the tests' do + options[:fixtures] = true + end + + opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m| + options[:seed] = m.to_i + end + + opts.on '-v', '--verbose', "Verbose. Show progress processing files." do + options[:verbose] = true + end + + opts.on '-n', '--name PATTERN', "Filter test names on pattern (e.g. /foo/)" do |n| + options[:filter] = n + end + + opts.separator "" + opts.separator "Support types of test suites:" + opts.separator "-------------------------------------------------------------" + opts.separator "* models (test/models/**/*)" + opts.separator "* helpers (test/helpers/**/*)" + opts.separator "* units (test/{models,helpers,unit}/**/*" + opts.separator "* controllers (test/controllers/**/*)" + opts.separator "* mailers (test/mailers/**/*)" + opts.separator "* functionals (test/{controllers,mailers,functional}/**/*)" + opts.separator "* integration (test/integration/**/*)" + opts.separator "-------------------------------------------------------------" + + opts.parse! arguments + orig_arguments -= arguments + end + options + end + end + + # Creates a new +TestRunner+ object with a list of test file paths. + def initialize(files, options) + @files = files + + Rails.application.load_tasks + Rake::Task['db:test:load'].invoke + + if options.delete(:fixtures) + if defined?(ActiveRecord::Base) + ActiveSupport::TestCase.send :include, ActiveRecord::TestFixtures + ActiveSupport::TestCase.fixture_path = "#{Rails.root}/test/fixtures/" + ActiveSupport::TestCase.fixtures :all + end + end + + MiniTest::Unit.runner.options = options + MiniTest::Unit.output = SilentUntilSyncStream.new(MiniTest::Unit.output) + end + + # Runs test files by evaluating each of them. + def run + @files.each { |filename| load(filename) } + end + + # A null stream object which ignores everything until +sync+ has been set + # to true. This is only used to silence unnecessary output from MiniTest, + # as MiniTest calls +output.sync = true+ right before it outputs the first + # test result. + class SilentUntilSyncStream < File + # Creates a +SilentUntilSyncStream+ object by giving it a target stream + # object that will be assigned to +MiniTest::Unit.output+ after +sync+ is + # set to true. + def initialize(target_stream) + @target_stream = target_stream + super(File::NULL, 'w') + end + + # Swaps +MiniTest::Unit.output+ to another stream when +sync+ is true. + def sync=(sync) + if sync + @target_stream.sync = true + MiniTest::Unit.output = @target_stream + end + + super + end + end + end +end diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index 32546936e3..85a1b01cc6 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -13,8 +13,17 @@ <% attributes.each do |attribute| -%> <div class="field"> +<% if attribute.password_digest? -%> + <%%= f.label :password %><br /> + <%%= f.password_field :password %> + </div> + <div> + <%%= f.label :password_confirmation %><br /> + <%%= f.password_field :password_confirmation %> +<% else -%> <%%= f.label :<%= attribute.name %> %><br /> <%%= f.<%= attribute.field_type %> :<%= attribute.name %> %> +<% end -%> </div> <% end -%> <div class="actions"> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index 90d8db1df5..d2fd99fdcb 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -3,7 +3,7 @@ <table> <thead> <tr> -<% attributes.each do |attribute| -%> +<% attributes.reject(&:password_digest?).each do |attribute| -%> <th><%= attribute.human_name %></th> <% end -%> <th></th> @@ -15,7 +15,7 @@ <tbody> <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %> <tr> -<% attributes.each do |attribute| -%> +<% attributes.reject(&:password_digest?).each do |attribute| -%> <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> <% end -%> <td><%%= link_to 'Show', <%= singular_table_name %> %></td> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb index daae72270f..5e634153be 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb @@ -1,6 +1,6 @@ <p id="notice"><%%= notice %></p> -<% attributes.each do |attribute| -%> +<% attributes.reject(&:password_digest?).each do |attribute| -%> <p> <strong><%= attribute.human_name %>:</strong> <%%= @<%= singular_table_name %>.<%= attribute.name %> %> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index 4ae8756ed0..5e2784c4b0 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -130,6 +130,10 @@ module Rails @has_uniq_index end + def password_digest? + name == 'password' && type == :digest + end + def inject_options "".tap { |s| @attr_options.each { |k,v| s << ", #{k}: #{v.inspect}" } } end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 9965db98de..8b4f52bb3b 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -163,6 +163,7 @@ module Rails def attributes_names @attributes_names ||= attributes.each_with_object([]) do |a, names| names << a.column_name + names << 'password_confirmation' if a.password_digest? names << "#{a.name}_type" if a.polymorphic? end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index b5db3d2187..f6bd107eba 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -23,5 +23,5 @@ gem 'jbuilder', '~> 1.0.1' <% unless defined?(JRUBY_VERSION) -%> # To use debugger -# gem 'debugger' +# gem 'debugger', group: [:development, :test] <% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru index fcfbc6b07a..5bc2a619e8 100644 --- a/railties/lib/rails/generators/rails/app/templates/config.ru +++ b/railties/lib/rails/generators/rails/app/templates/config.ru @@ -1,4 +1,4 @@ # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) -run <%= app_const %> +run Rails.application diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index d0e62d09cc..8b64881dbc 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -26,6 +26,8 @@ <%- unless options.skip_sprockets? -%> # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. config.assets.debug = true <%- end -%> end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 5669fe6d64..c40eef145f 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -24,10 +24,10 @@ <%- unless options.skip_sprockets? -%> # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass - # Whether to fallback to assets pipeline if a precompiled asset is missed. + # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Generate digests for assets URLs. diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb index 9afda2d0df..ca40914d3b 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb @@ -1,4 +1,4 @@ -ENV["RAILS_ENV"] = "test" +ENV["RAILS_ENV"] ||= "test" require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' @@ -6,11 +6,12 @@ class ActiveSupport::TestCase <% unless options[:skip_active_record] -%> ActiveRecord::Migration.check_pending! - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + # Uncomment the `fixtures :all` line below to setup all fixtures in test/fixtures/*.yml + # for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting - fixtures :all + # fixtures :all <% end -%> # Add more helper methods to be used by all tests here... diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml index c9d505c84a..90a92e6982 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml @@ -1,22 +1,20 @@ # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - <% unless attributes.empty? -%> -one: +<% %w(one two).each do |name| %> +<%= name %>: <% attributes.each do |attribute| -%> + <%- if attribute.password_digest? -%> + password_digest: <%%= BCrypt::Password.create('secret') %> + <%- else -%> <%= yaml_key_value(attribute.column_name, attribute.default) %> - <%- if attribute.polymorphic? -%> - <%= yaml_key_value("#{attribute.name}_type", attribute.human_name) %> <%- end -%> -<% end -%> - -two: -<% attributes.each do |attribute| -%> - <%= yaml_key_value(attribute.column_name, attribute.default) %> <%- if attribute.polymorphic? -%> <%= yaml_key_value("#{attribute.name}_type", attribute.human_name) %> <%- end -%> <% end -%> +<% end -%> <% else -%> + # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb index 8f3ecaadea..2e1f55f2a6 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -21,7 +21,11 @@ module TestUnit # :nodoc: return if attributes_names.empty? attributes_names.map do |name| - "#{name}: @#{singular_table_name}.#{name}" + if %w(password password_confirmation).include?(name) && attributes.any?(&:password_digest?) + "#{name}: 'secret'" + else + "#{name}: @#{singular_table_name}.#{name}" + end end.sort.join(', ') end end diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 44485d9b14..3c247f32c0 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -1,6 +1,7 @@ require 'rbconfig' require 'rake/testtask' require 'rails/test_unit/sub_test_task' +require 'active_support/deprecation' TEST_CHANGES_SINCE = Time.now - 600 @@ -47,7 +48,11 @@ task default: :test desc 'Runs test:units, test:functionals, test:integration together' task :test do - Rake::Task[ENV['TEST'] ? 'test:single' : 'test:run'].invoke + if ENV['TEST'] + exec "bundle exec rails test #{ENV['TEST'].inspect}" + else + exec 'bundle exec rails test' + end end namespace :test do @@ -56,19 +61,8 @@ namespace :test do end task :run do - errors = %w(test:units test:functionals test:integration).collect do |task| - begin - Rake::Task[task].invoke - nil - rescue => e - { task: task, exception: e } - end - end.compact - - if errors.any? - puts errors.map { |e| "Errors running #{e[:task]}! #{e[:exception].inspect}" }.join("\n") - abort - end + ActiveSupport::Deprecation.warn "`rake test:run` is deprecated. Please use `rails test`." + exec 'bundle exec rails test' end # Inspired by: http://ngauthier.com/2012/02/quick-tests-with-bash.html @@ -83,7 +77,12 @@ namespace :test do task :db => %w[db:test:prepare test:all] end - Rake::TestTask.new(recent: "test:prepare") do |t| + # Display deprecation message + task :deprecated do + ActiveSupport::Deprecation.warn "`rake #{ARGV.first}` is deprecated with no replacement." + end + + Rake::TestTask.new(recent: ["test:deprecated", "test:prepare"]) do |t| since = TEST_CHANGES_SINCE touched = FileList['test/**/*_test.rb'].select { |path| File.mtime(path) > since } + recent_tests('app/models/**/*.rb', 'test/models', since) + @@ -94,9 +93,9 @@ namespace :test do t.libs << 'test' t.test_files = touched.uniq end - Rake::Task['test:recent'].comment = "Test recent changes" + Rake::Task['test:recent'].comment = "Deprecated; Test recent changes" - Rake::TestTask.new(uncommitted: "test:prepare") do |t| + Rake::TestTask.new(uncommitted: ["test:deprecated", "test:prepare"]) do |t| def t.file_list if File.directory?(".svn") changed_since_checkin = silence_stderr { `svn status` }.split.map { |path| path.chomp[7 .. -1] } @@ -118,44 +117,20 @@ namespace :test do t.libs << 'test' end - Rake::Task['test:uncommitted'].comment = "Test changes since last checkin (only Subversion and Git)" - - Rake::TestTask.new(single: "test:prepare") do |t| - t.libs << "test" - end - - Rails::SubTestTask.new(models: "test:prepare") do |t| - t.libs << "test" - t.pattern = 'test/models/**/*_test.rb' - end - - Rails::SubTestTask.new(helpers: "test:prepare") do |t| - t.libs << "test" - t.pattern = 'test/helpers/**/*_test.rb' - end + Rake::Task['test:uncommitted'].comment = "Deprecated; Test changes since last checkin (only Subversion and Git)" - Rails::SubTestTask.new(units: "test:prepare") do |t| - t.libs << "test" - t.pattern = 'test/{models,helpers,unit}/**/*_test.rb' + desc "Deprecated; Please use `rails test \"#{ENV['TEST']}\"`" + task :single do + ActiveSupport::Deprecation.warn "`rake test:single` is deprecated. Please use `rails test \"#{ENV['TEST']}\"`." + exec "bundle exec rails test #{test_suit_name}" end - Rails::SubTestTask.new(controllers: "test:prepare") do |t| - t.libs << "test" - t.pattern = 'test/controllers/**/*_test.rb' - end + [:models, :helpers, :units, :controllers, :functionals, :integration].each do |test_suit_name| + desc "Deprecated; Please use `rails test #{test_suit_name}`" + task test_suit_name do + ActiveSupport::Deprecation.warn "`rake test:#{test_suit_name}` is deprecated. Please use `rails test #{test_suit_name}`." - Rails::SubTestTask.new(mailers: "test:prepare") do |t| - t.libs << "test" - t.pattern = 'test/mailers/**/*_test.rb' - end - - Rails::SubTestTask.new(functionals: "test:prepare") do |t| - t.libs << "test" - t.pattern = 'test/{controllers,mailers,functional}/**/*_test.rb' - end - - Rails::SubTestTask.new(integration: "test:prepare") do |t| - t.libs << "test" - t.pattern = 'test/integration/**/*_test.rb' + exec "bundle exec rails test #{test_suit_name}" + end end end diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 3cb3643e3a..af495bb450 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -81,18 +81,82 @@ class ConsoleTest < ActiveSupport::TestCase assert_equal 'Once upon a time in a world...', helper.truncate('Once upon a time in a world far far away') end +end + +begin + require "pty" +rescue LoadError +end + +class FullStackConsoleTest < ActiveSupport::TestCase + def setup + skip "PTY unavailable" unless defined?(PTY) && PTY.respond_to?(:open) + + build_app + app_file 'app/models/post.rb', <<-CODE + class Post < ActiveRecord::Base + end + CODE + system "#{app_path}/bin/rails runner 'Post.connection.create_table :posts'" + + @master, @slave = PTY.open + end + + def teardown + teardown_app + end - def test_with_sandbox - require 'rails/all' - value = false + def assert_output(expected, timeout = 1) + timeout = Time.now + timeout - Class.new(Rails::Railtie) do - console do |app| - value = app.sandbox? + output = "" + until output.include?(expected) || Time.now > timeout + if IO.select([@master], [], [], 0.1) + output << @master.read(1) end end - load_environment(true) - assert value + assert output.include?(expected), "#{expected.inspect} expected, but got:\n\n#{output}" + end + + def write_prompt(command, expected_output = nil) + @master.puts command + assert_output command + assert_output expected_output if expected_output + assert_output "> " + end + + def kill(pid) + Process.kill('QUIT', pid) + Process.wait(pid) + rescue Errno::ESRCH + end + + def spawn_console + pid = Process.spawn( + "#{app_path}/bin/rails console --sandbox", + in: @slave, out: @slave, err: @slave + ) + + assert_output "> ", 30 + pid + end + + def test_sandbox + pid = spawn_console + + write_prompt "Post.count", "=> 0" + write_prompt "Post.create" + write_prompt "Post.count", "=> 1" + + kill pid + + pid = spawn_console + + write_prompt "Post.count", "=> 0" + write_prompt "Post.transaction { Post.create; raise }" + write_prompt "Post.count", "=> 0" + ensure + kill pid if pid end end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 09f2ad1209..a9e0e1bcb7 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -91,19 +91,9 @@ module ApplicationTests raise 'models' RUBY - app_file "test/controllers/one_controller_test.rb", <<-RUBY - raise 'controllers' - RUBY - - app_file "test/integration/one_integration_test.rb", <<-RUBY - raise 'integration' - RUBY - silence_stderr do output = Dir.chdir(app_path) { `rake test 2>&1` } assert_match 'models', output - assert_match 'controllers', output - assert_match 'integration', output end end @@ -135,6 +125,19 @@ module ApplicationTests end end + def test_rake_test_deprecation_messages + Dir.chdir(app_path){ `rails generate scaffold user name:string` } + Dir.chdir(app_path){ `rake db:migrate` } + + %w(run recent uncommitted models helpers units controllers functionals integration).each do |test_suit_name| + output = Dir.chdir(app_path) { `rake test:#{test_suit_name} 2>&1` } + assert_match /DEPRECATION WARNING: `rake test:#{test_suit_name}` is deprecated/, output + end + + assert_match /DEPRECATION WARNING: `rake test:single` is deprecated/, + Dir.chdir(app_path) { `rake test:single TEST=test/models/user_test.rb 2>&1` } + end + def test_rake_routes_calls_the_route_inspector app_file "config/routes.rb", <<-RUBY AppTemplate::Application.routes.draw do diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb new file mode 100644 index 0000000000..56ca3bc1a9 --- /dev/null +++ b/railties/test/application/test_runner_test.rb @@ -0,0 +1,312 @@ +require 'isolation/abstract_unit' +require 'active_support/core_ext/string/strip' + +module ApplicationTests + class TestRunnerTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + ENV['RAILS_ENV'] = nil + create_schema + end + + def teardown + teardown_app + end + + def test_should_not_display_heading + create_test_file + run_test_command.tap do |output| + assert_no_match "Run options:", output + assert_no_match "Running tests:", output + end + end + + def test_run_in_test_environment + app_file 'test/unit/env_test.rb', <<-RUBY + require 'test_helper' + + class EnvTest < ActiveSupport::TestCase + def test_env + puts "Current Environment: \#{Rails.env}" + end + end + RUBY + + assert_match "Current Environment: test", run_test_command('test/unit/env_test.rb') + end + + def test_run_shortcut + create_test_file :models, 'foo' + output = Dir.chdir(app_path) { `bundle exec rails t test/models/foo_test.rb` } + assert_match "1 tests, 1 assertions, 0 failures", output + end + + def test_run_single_file + create_test_file :models, 'foo' + assert_match "1 tests, 1 assertions, 0 failures", run_test_command("test/models/foo_test.rb") + end + + def test_run_multiple_files + create_test_file :models, 'foo' + create_test_file :models, 'bar' + assert_match "2 tests, 2 assertions, 0 failures", run_test_command("test/models/foo_test.rb test/models/bar_test.rb") + end + + def test_run_file_with_syntax_error + app_file 'test/models/error_test.rb', <<-RUBY + require 'test_helper' + def; end + RUBY + + error_stream = Tempfile.new('error') + redirect_stderr(error_stream) { run_test_command('test/models/error_test.rb') } + assert_match "SyntaxError", error_stream.read + end + + def test_invoke_rake_db_test_load + app_file "lib/tasks/test.rake", <<-RUBY + task 'db:test:load' do + puts "Hello World" + end + RUBY + create_test_file + assert_match "Hello World", run_test_command + end + + def test_run_models + create_test_file :models, 'foo' + create_test_file :models, 'bar' + create_test_file :controllers, 'foobar_controller' + run_test_command("models").tap do |output| + assert_match "FooTest", output + assert_match "BarTest", output + assert_match "2 tests, 2 assertions, 0 failures", output + end + end + + def test_run_helpers + create_test_file :helpers, 'foo_helper' + create_test_file :helpers, 'bar_helper' + create_test_file :controllers, 'foobar_controller' + run_test_command('helpers').tap do |output| + assert_match "FooHelperTest", output + assert_match "BarHelperTest", output + assert_match "2 tests, 2 assertions, 0 failures", output + end + end + + def test_run_units + create_test_file :models, 'foo' + create_test_file :helpers, 'bar_helper' + create_test_file :unit, 'baz_unit' + create_test_file :controllers, 'foobar_controller' + run_test_command('units').tap do |output| + assert_match "FooTest", output + assert_match "BarHelperTest", output + assert_match "BazUnitTest", output + assert_match "3 tests, 3 assertions, 0 failures", output + end + end + + def test_run_controllers + create_test_file :controllers, 'foo_controller' + create_test_file :controllers, 'bar_controller' + create_test_file :models, 'foo' + run_test_command('controllers').tap do |output| + assert_match "FooControllerTest", output + assert_match "BarControllerTest", output + assert_match "2 tests, 2 assertions, 0 failures", output + end + end + + def test_run_mailers + create_test_file :mailers, 'foo_mailer' + create_test_file :mailers, 'bar_mailer' + create_test_file :models, 'foo' + run_test_command('mailers').tap do |output| + assert_match "FooMailerTest", output + assert_match "BarMailerTest", output + assert_match "2 tests, 2 assertions, 0 failures", output + end + end + + def test_run_functionals + create_test_file :mailers, 'foo_mailer' + create_test_file :controllers, 'bar_controller' + create_test_file :functional, 'baz_functional' + create_test_file :models, 'foo' + run_test_command('functionals').tap do |output| + assert_match "FooMailerTest", output + assert_match "BarControllerTest", output + assert_match "BazFunctionalTest", output + assert_match "3 tests, 3 assertions, 0 failures", output + end + end + + def test_run_integration + create_test_file :integration, 'foo_integration' + create_test_file :models, 'foo' + run_test_command('integration').tap do |output| + assert_match "FooIntegration", output + assert_match "1 tests, 1 assertions, 0 failures", output + end + end + + def test_run_all_suites + suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration] + suites.each { |suite| create_test_file suite, "foo_#{suite}" } + run_test_command('') .tap do |output| + suites.each { |suite| assert_match "Foo#{suite.to_s.camelize}Test", output } + assert_match "7 tests, 7 assertions, 0 failures", output + end + end + + def test_run_named_test + app_file 'test/unit/chu_2_koi_test.rb', <<-RUBY + require 'test_helper' + + class Chu2KoiTest < ActiveSupport::TestCase + def test_rikka + puts 'Rikka' + end + + def test_sanae + puts 'Sanae' + end + end + RUBY + + run_test_command('test/unit/chu_2_koi_test.rb -n test_rikka').tap do |output| + assert_match "Rikka", output + assert_no_match "Sanae", output + end + end + + def test_not_load_fixtures_when_running_single_test + create_model_with_fixture + create_fixture_test :models, 'user' + assert_match "0 users", run_test_command('test/models/user_test.rb') + assert_match "3 users", run_test_command('test/models/user_test.rb -f') + end + + def test_load_fixtures_when_running_test_suites + create_model_with_fixture + suites = [:models, :helpers, [:units, :unit], :controllers, :mailers, + [:functionals, :functional], :integration] + + suites.each do |suite, directory| + directory ||= suite + create_fixture_test directory + assert_match "3 users", run_test_command(suite) + Dir.chdir(app_path) { FileUtils.rm_f "test/#{directory}" } + end + end + + def test_run_different_environment_using_env_var + app_file 'test/unit/env_test.rb', <<-RUBY + require 'test_helper' + + class EnvTest < ActiveSupport::TestCase + def test_env + puts Rails.env + end + end + RUBY + + ENV['RAILS_ENV'] = 'development' + assert_match "development", run_test_command('test/unit/env_test.rb') + end + + def test_run_different_environment_using_e_tag + app_file 'test/unit/env_test.rb', <<-RUBY + require 'test_helper' + + class EnvTest < ActiveSupport::TestCase + def test_env + puts Rails.env + end + end + RUBY + + assert_match "development", run_test_command('-e development test/unit/env_test.rb') + end + + def test_generated_scaffold_works_with_rails_test + create_scaffold + assert_match "0 failures, 0 errors, 0 skips", run_test_command('') + end + + private + def run_test_command(arguments = 'test/unit/test_test.rb') + Dir.chdir(app_path) { `bundle exec rails test #{arguments}` } + end + + def create_model_with_fixture + script 'generate model user name:string' + + app_file 'test/fixtures/users.yml', <<-YAML.strip_heredoc + vampire: + id: 1 + name: Koyomi Araragi + crab: + id: 2 + name: Senjougahara Hitagi + cat: + id: 3 + name: Tsubasa Hanekawa + YAML + + run_migration + end + + def create_fixture_test(path = :unit, name = 'test') + app_file "test/#{path}/#{name}_test.rb", <<-RUBY + require 'test_helper' + + class #{name.camelize}Test < ActiveSupport::TestCase + def test_fixture + puts "\#{User.count} users (\#{__FILE__})" + end + end + RUBY + end + + def create_schema + app_file 'db/schema.rb', '' + end + + def redirect_stderr(target_stream) + previous_stderr = STDERR.dup + $stderr.reopen(target_stream) + yield + target_stream.rewind + ensure + $stderr = previous_stderr + end + + def create_test_file(path = :unit, name = 'test') + app_file "test/#{path}/#{name}_test.rb", <<-RUBY + require 'test_helper' + + class #{name.camelize}Test < ActiveSupport::TestCase + def test_truth + puts "#{name.camelize}Test" + assert true + end + end + RUBY + end + + def create_scaffold + script 'generate scaffold user name:string' + Dir.chdir(app_path) { File.exist?('app/models/user.rb') } + run_migration + end + + def run_migration + Dir.chdir(app_path) { `bundle exec rake db:migrate` } + end + end +end diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb index 6be4a5fe89..3c784b43be 100644 --- a/railties/test/commands/console_test.rb +++ b/railties/test/commands/console_test.rb @@ -58,10 +58,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_console_defaults_to_IRB - config = mock("config", console: nil) - app = mock("app", config: config) - app.expects(:load_console).returns(nil) - + app = build_app(console: nil) assert_equal IRB, Rails::Console.new(app).console end @@ -127,13 +124,15 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def app - @app ||= begin - config = mock("config", console: FakeConsole) - app = mock("app", config: config) - app.stubs(:sandbox=).returns(nil) - app.expects(:load_console) - app - end + @app ||= build_app(console: FakeConsole) + end + + def build_app(config) + config = mock("config", config) + app = mock("app", config: config) + app.stubs(:sandbox=).returns(nil) + app.expects(:load_console) + app end def parse_arguments(args) diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 357f472a3f..b29d1e018e 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -271,4 +271,45 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end end + + def test_scaffold_generator_password_digest + run_generator ["user", "name", "password:digest"] + + assert_file "app/models/user.rb", /has_secure_password/ + + assert_migration "db/migrate/create_users.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.string :name/, up) + assert_match(/t\.string :password_digest/, up) + end + end + + assert_file "app/controllers/users_controller.rb" do |content| + assert_instance_method :user_params, content do |m| + assert_match(/permit\(:name, :password, :password_confirmation\)/, m) + end + end + + assert_file "app/views/users/_form.html.erb" do |content| + assert_match(/<%= f\.password_field :password %>/, content) + assert_match(/<%= f\.password_field :password_confirmation %>/, content) + end + + assert_file "app/views/users/index.html.erb" do |content| + assert_no_match(/password/, content) + end + + assert_file "app/views/users/show.html.erb" do |content| + assert_no_match(/password/, content) + end + + assert_file "test/controllers/users_controller_test.rb" do |content| + assert_match(/password: 'secret'/, content) + assert_match(/password_confirmation: 'secret'/, content) + end + + assert_file "test/fixtures/users.yml" do |content| + assert_match(/password_digest: <%= BCrypt::Password.create\('secret'\) %>/, content) + end + end end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 37d0be107c..26b388b6f9 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -105,7 +105,7 @@ module RailtiesTest assert_not_nil bukkits_migration_order, "Expected migration to be skipped" migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length - output = `bundle exec rake railties:install:migrations` + `bundle exec rake railties:install:migrations` assert_equal migrations_count, Dir["#{app_path}/db/migrate/*.rb"].length end |