diff options
150 files changed, 3608 insertions, 1694 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 7327f1e631..3e3b963a47 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -52,6 +52,9 @@ Layout/EndAlignment: Layout/EmptyLineAfterMagicComment: Enabled: true +Layout/EmptyLinesAroundBlockBody: + Enabled: true + # In a regular class definition, no empty lines around the body. Layout/EmptyLinesAroundClassBody: Enabled: true diff --git a/Gemfile.lock b/Gemfile.lock index 0e9def0aa5..9c28735a0d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -454,7 +454,7 @@ GEM concurrent-ruby (~> 1.0) serverengine (~> 2.0.5) thor - sprockets (3.7.1) + sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-export (1.0.0) diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb index 4873026b71..e658948a55 100644 --- a/actioncable/lib/action_cable/connection/stream.rb +++ b/actioncable/lib/action_cable/connection/stream.rb @@ -98,8 +98,10 @@ module ActionCable def hijack_rack_socket return unless @socket_object.env["rack.hijack"] - @socket_object.env["rack.hijack"].call - @rack_hijack_io = @socket_object.env["rack.hijack_io"] + # This should return the underlying io according to the SPEC: + @rack_hijack_io = @socket_object.env["rack.hijack"].call + # Retain existing behaviour if required: + @rack_hijack_io ||= @socket_object.env["rack.hijack_io"] @event_loop.attach(@rack_hijack_io, self) end diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb index 9176c7ac8b..a7db32c3e4 100644 --- a/actioncable/test/connection/client_socket_test.rb +++ b/actioncable/test/connection/client_socket_test.rb @@ -41,7 +41,6 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase # Internal hax = :( client = connection.websocket.send(:websocket) client.instance_variable_get("@stream").stub(:write, proc { raise "foo" }) do - assert_not_called(client, :client_gone) do client.write("boo") end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 3bbc9a9cfd..7645b2b0e7 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,13 @@ +* Pass along arguments to underlying `get` method in `follow_redirect!` + + Now all arguments passed to `follow_redirect!` are passed to the underlying + `get` method. This for example allows to set custom headers for the + redirection request to the server. + + follow_redirect!(params: { foo: :bar }) + + *Remo Fritzsche* + * Introduce a new error page to when the implicit render page is accessed in the browser. Now instead of showing an error page that with exception and backtraces we now show only diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 01676f3237..a871ccd533 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -56,8 +56,9 @@ module ActionController # In your integration tests, you can do something like this: # # def test_access_granted_from_xml - # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) - # get "/notes/1.xml" + # authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) + # + # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization } # # assert_equal 200, status # end @@ -389,10 +390,9 @@ module ActionController # In your integration tests, you can do something like this: # # def test_access_granted_from_xml - # get( - # "/notes/1.xml", nil, - # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token) - # ) + # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token) + # + # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization } # # assert_equal 200, status # end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 33b0bcbefe..5d784ceb31 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -109,7 +109,7 @@ module ActionController when :xml data = non_path_parameters.to_xml when :url_encoded_form - data = Rack::Utils.build_nested_query(non_path_parameters) + data = non_path_parameters.to_query else @custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters } data = non_path_parameters.to_query diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb index 3ba8361d77..c0377459d5 100644 --- a/actionpack/lib/action_dispatch/journey/routes.rb +++ b/actionpack/lib/action_dispatch/journey/routes.rb @@ -56,6 +56,7 @@ module ActionDispatch end def simulator + return if ast.nil? @simulator ||= begin gtg = GTG::Builder.new(ast).transition_table GTG::Simulator.new(gtg) diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 33edad8bd9..077a83b112 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -152,23 +152,13 @@ module ActionDispatch end def create_template(request, wrapper) - traces = wrapper.traces - - trace_to_show = "Application Trace" - if traces[trace_to_show].empty? && wrapper.rescue_template != "routing_error" - trace_to_show = "Full Trace" - end - - if source_to_show = traces[trace_to_show].first - source_to_show_id = source_to_show[:id] - end - DebugView.new([RESCUES_TEMPLATE_PATH], request: request, + exception_wrapper: wrapper, exception: wrapper.exception, - traces: traces, - show_source_idx: source_to_show_id, - trace_to_show: trace_to_show, + traces: wrapper.traces, + show_source_idx: wrapper.source_to_show_id, + trace_to_show: wrapper.trace_to_show, routes_inspector: routes_inspector(wrapper.exception), source_extracts: wrapper.source_extracts, line_number: wrapper.line_number, diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index f05c69137b..fb2b2bd3b0 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -31,11 +31,12 @@ module ActionDispatch "ActionController::MissingExactTemplate" => "missing_exact_template", ) - attr_reader :backtrace_cleaner, :exception, :line_number, :file + attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file def initialize(backtrace_cleaner, exception) @backtrace_cleaner = backtrace_cleaner @exception = original_exception(exception) + @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner) expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError) end @@ -66,7 +67,11 @@ module ActionDispatch full_trace_with_ids = [] full_trace.each_with_index do |trace, idx| - trace_with_id = { id: idx, trace: trace } + trace_with_id = { + exception_object_id: @exception.object_id, + id: idx, + trace: trace + } if application_trace.include?(trace) application_trace_with_ids << trace_with_id @@ -99,6 +104,18 @@ module ActionDispatch end end + def trace_to_show + if traces["Application Trace"].empty? && rescue_template != "routing_error" + "Full Trace" + else + "Application Trace" + end + end + + def source_to_show_id + (traces[trace_to_show].first || {})[:id] + end + private def backtrace @@ -113,6 +130,16 @@ module ActionDispatch end end + def causes_for(exception) + return enum_for(__method__, exception) unless block_given? + + yield exception while exception = exception.cause + end + + def wrapped_causes_for(exception, backtrace_cleaner) + causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) } + end + def clean_backtrace(*args) if backtrace_cleaner backtrace_cleaner.clean(backtrace, *args) diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 8130bfe2e7..277074f216 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -41,7 +41,6 @@ module ActionDispatch rescue SystemCallError false end - } return ::Rack::Utils.escape_path(match).b end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb index e7b913bbe4..88a8e6ad83 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb @@ -1,6 +1,8 @@ -<% @source_extracts.each_with_index do |source_extract, index| %> +<% error_index = local_assigns[:error_index] || 0 %> + +<% source_extracts.each_with_index do |source_extract, index| %> <% if source_extract[:code] %> - <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>"> + <div class="source <%= "hidden" if show_source_idx != index %>" id="frame-source-<%= error_index %>-<%= index %>"> <div class="info"> Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>): </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb index ab57b11c7d..835ca8d260 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb @@ -1,52 +1,62 @@ -<% names = @traces.keys %> +<% names = traces.keys %> +<% error_index = local_assigns[:error_index] || 0 %> <p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p> -<div id="traces"> +<div id="traces-<%= error_index %>"> <% names.each do |name| %> <% - show = "show('#{name.gsub(/\s/, '-')}');" - hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"} + show = "show('#{name.gsub(/\s/, '-')}-#{error_index}');" + hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}-#{error_index}');"} %> <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %> <% end %> - <% @traces.each do |name, trace| %> - <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;"> - <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre> + <% traces.each do |name, trace| %> + <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;"> + <code style="font-size: 11px;"> + <% trace.each do |frame| %> + <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#"> + <%= frame[:trace] %> + </a> + <br> + <% end %> + </code> </div> <% end %> <script type="text/javascript"> - var traceFrames = document.getElementsByClassName('trace-frames'); - var selectedFrame, currentSource = document.getElementById('frame-source-0'); - - // Add click listeners for all stack frames - for (var i = 0; i < traceFrames.length; i++) { - traceFrames[i].addEventListener('click', function(e) { - e.preventDefault(); - var target = e.target; - var frame_id = target.dataset.frameId; - - if (selectedFrame) { - selectedFrame.className = selectedFrame.className.replace("selected", ""); - } - - target.className += " selected"; - selectedFrame = target; - - // Change the extracted source code - changeSourceExtract(frame_id); - }); - - function changeSourceExtract(frame_id) { - var el = document.getElementById('frame-source-' + frame_id); - if (currentSource && el) { - currentSource.className += " hidden"; - el.className = el.className.replace(" hidden", ""); - currentSource = el; + (function() { + var traceFrames = document.getElementsByClassName('trace-frames-<%= error_index %>'); + var selectedFrame, currentSource = document.getElementById('frame-source-<%= error_index %>-0'); + + // Add click listeners for all stack frames + for (var i = 0; i < traceFrames.length; i++) { + traceFrames[i].addEventListener('click', function(e) { + e.preventDefault(); + var target = e.target; + var frame_id = target.dataset.frameId; + + if (selectedFrame) { + selectedFrame.className = selectedFrame.className.replace("selected", ""); + } + + target.className += " selected"; + selectedFrame = target; + + // Change the extracted source code + changeSourceExtract(frame_id); + }); + + function changeSourceExtract(frame_id) { + var el = document.getElementById('frame-source-<%= error_index %>-' + frame_id); + if (currentSource && el) { + currentSource.className += " hidden"; + el.className = el.className.replace(" hidden", ""); + currentSource = el; + } } } - } + })(); </script> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb index f154021ae6..bde26f46c2 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb @@ -10,7 +10,25 @@ <div id="container"> <h2><%= h @exception.message %></h2> - <%= render template: "rescues/_source" %> - <%= render template: "rescues/_trace" %> + <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx, error_index: 0 %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show, error_index: 0 %> + + <% if @exception.cause %> + <h2>Exception Causes</h2> + <% end %> + + <% @exception_wrapper.wrapped_causes.each.with_index(1) do |wrapper, index| %> + <div class="details"> + <a class="summary" href="#" style="color: #F0F0F0; text-decoration: none; background: #C52F24; border-bottom: none;" onclick="return toggle(<%= wrapper.exception.object_id %>)"> + <%= wrapper.exception.class.name %>: <%= h wrapper.exception.message %> + </a> + </div> + + <div id="<%= wrapper.exception.object_id %>" style="display: none;"> + <%= render "rescues/source", source_extracts: wrapper.source_extracts, show_source_idx: wrapper.source_to_show_id, error_index: index %> + <%= render "rescues/trace", traces: wrapper.traces, trace_to_show: wrapper.trace_to_show, error_index: index %> + </div> + <% end %> + <%= render template: "rescues/_request_and_response" %> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb index e1b129ccc5..056d99b03f 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb @@ -15,7 +15,7 @@ <% end %> </h2> - <%= render template: "rescues/_source" %> - <%= render template: "rescues/_trace" %> + <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %> <%= render template: "rescues/_request_and_response" %> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb index 2a65fd06ad..22eb6e9b4e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb @@ -5,7 +5,7 @@ <div id="container"> <h2><%= h @exception.message %></h2> - <%= render template: "rescues/_source" %> - <%= render template: "rescues/_trace" %> + <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %> <%= render template: "rescues/_request_and_response" %> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb index 55dd5ddc7b..2b8f3f2a5e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb @@ -14,7 +14,7 @@ </p> <% end %> - <%= render template: "rescues/_trace" %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %> <% if @routes_inspector %> <h2> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb index 5060da9369..324ef1567a 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb @@ -11,10 +11,10 @@ </p> <pre><code><%= h @exception.message %></code></pre> - <%= render template: "rescues/_source" %> + <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %> <p><%= @exception.sub_template_message %></p> - <%= render template: "rescues/_trace" %> + <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %> <%= render template: "rescues/_request_and_response" %> </div> diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index f0398dc7b1..7637febd1c 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -50,10 +50,11 @@ module ActionDispatch # Follow a single redirect response. If the last response was not a # redirect, an exception will be raised. Otherwise, the redirect is - # performed on the location header. - def follow_redirect! + # performed on the location header. Any arguments are passed to the + # underlying call to `get`. + def follow_redirect!(**args) raise "not a redirect! #{status} #{status_message}" unless redirect? - get(response.location) + get(response.location, **args) status end end diff --git a/actionpack/lib/action_dispatch/testing/request_encoder.rb b/actionpack/lib/action_dispatch/testing/request_encoder.rb index 01246b7a2e..9889f61951 100644 --- a/actionpack/lib/action_dispatch/testing/request_encoder.rb +++ b/actionpack/lib/action_dispatch/testing/request_encoder.rb @@ -34,7 +34,7 @@ module ActionDispatch end def encode_params(params) - @param_encoder.call(params) + @param_encoder.call(params) if params end def self.parser(content_type) diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 9cdf04b886..39ede1442a 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -349,6 +349,16 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + def test_redirect_with_arguments + with_test_route_set do + get "/redirect" + follow_redirect! params: { foo: :bar } + + assert_response :ok + assert_equal "bar", request.parameters["foo"] + end + end + def test_xml_http_request_get with_test_route_set do get "/get", xhr: true @@ -1069,6 +1079,20 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest end end + def test_get_request_with_json_excludes_null_query_string + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + get ":action" => FooController + end + end + + get "/foos_json", as: :json + + assert_equal "http://www.example.com/foos_json", request.url + end + end + private def post_to_foos(as:) with_routing do |routes| diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 30bea64c55..3688fdbeee 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -66,7 +66,6 @@ class ResourcesTest < ActionController::TestCase member_methods.each_key do |action| assert_named_route "/messages/1/#{path_names[action] || action}", "#{action}_message_path", action: action, id: "1" end - end end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 9d0a8b4f00..a7033b2d30 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -937,7 +937,6 @@ class RouteSetTest < ActiveSupport::TestCase @default_route_set ||= begin set = ActionDispatch::Routing::RouteSet.new set.draw do - ActiveSupport::Deprecation.silence do get "/:controller(/:action(/:id))" end @@ -1342,11 +1341,9 @@ class RouteSetTest < ActiveSupport::TestCase def test_namespace set.draw do - namespace "api" do get "inventory" => "products#inventory" end - end params = request_path_params("/api/inventory", method: :get) diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 663832e9bb..dda2686a9b 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -220,7 +220,7 @@ XML params = Hash[:page, { name: "page name" }, "some key", 123] post :render_raw_post, params: params.dup - assert_equal Rack::Utils.build_nested_query(params), @response.body + assert_equal params.to_query, @response.body end def test_params_round_trip @@ -231,12 +231,25 @@ XML assert_equal params.merge(controller_info), JSON.parse(@response.body) end + def test_handle_to_params + klass = Class.new do + def to_param + "bar" + end + end + + post :test_params, params: { foo: klass.new } + + assert_equal JSON.parse(@response.body)["foo"], "bar" + end + + def test_body_stream params = Hash[:page, { name: "page name" }, "some key", 123] post :render_body, params: params.dup - assert_equal Rack::Utils.build_nested_query(params), @response.body + assert_equal params.to_query, @response.body end def test_document_body_and_params_with_post @@ -388,7 +401,13 @@ XML process :test_xml_output, params: { response_as: "text/html" } # <area> auto-closes, so the <p> becomes a sibling - assert_select "root > area + p" + if defined?(JRUBY_VERSION) + # https://github.com/sparklemotion/nokogiri/issues/1653 + # HTML parser "fixes" "broken" markup in slightly different ways + assert_select "root > map > area + p" + else + assert_select "root > area + p" + end end def test_should_not_impose_childless_html_tags_in_xml diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 045567ff83..44b79c0e5d 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -26,6 +26,18 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest raise StandardError.new "error in framework" end + def raise_nested_exceptions + begin + raise "First error" + rescue + begin + raise "Second error" + rescue + raise "Third error" + end + end + end + def call(env) env["action_dispatch.show_detailed_exceptions"] = @detailed req = ActionDispatch::Request.new(env) @@ -74,6 +86,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end when %r{/framework_raises} method_that_raises + when %r{/nested_exceptions} + raise_nested_exceptions else raise "puke!" end @@ -440,8 +454,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest get "/original_syntax_error", headers: { "action_dispatch.backtrace_cleaner" => ActiveSupport::BacktraceCleaner.new } assert_response 500 - assert_select "#Application-Trace" do - assert_select "pre code", /syntax error, unexpected/ + assert_select "#Application-Trace-0" do + assert_select "code", /syntax error, unexpected/ end end @@ -454,9 +468,9 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_select "#container h2", /^Missing template/ - assert_select "#Application-Trace" - assert_select "#Framework-Trace" - assert_select "#Full-Trace" + assert_select "#Application-Trace-0" + assert_select "#Framework-Trace-0" + assert_select "#Full-Trace-0" assert_select "h2", /Request/ end @@ -467,8 +481,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest get "/syntax_error_into_view", headers: { "action_dispatch.backtrace_cleaner" => ActiveSupport::BacktraceCleaner.new } assert_response 500 - assert_select "#Application-Trace" do - assert_select "pre code", /syntax error, unexpected/ + assert_select "#Application-Trace-0" do + assert_select "code", /syntax error, unexpected/ end end @@ -497,13 +511,13 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end # assert application trace refers to line that calls method_that_raises is first - assert_select "#Application-Trace" do - assert_select "pre code a:first", %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call} + assert_select "#Application-Trace-0" do + assert_select "code a:first", %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call} end # assert framework trace that threw the error is first - assert_select "#Framework-Trace" do - assert_select "pre code a:first", /method_that_raises/ + assert_select "#Framework-Trace-0" do + assert_select "code a:first", /method_that_raises/ end end end @@ -523,4 +537,39 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_response 500 assert_match(/puke/, body) end + + test "debug exceptions app shows all the nested exceptions in source view" do + @app = DevelopmentApp + Rails.stub :root, Pathname.new(".") do + cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc| + bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} } + end + + get "/nested_exceptions", headers: { "action_dispatch.backtrace_cleaner" => cleaner } + + # Assert correct error + assert_response 500 + assert_select "h2", /Third error/ + + # assert source view line shows the last error + assert_select "div.source:not(.hidden)" do + assert_select "pre .line.active", /raise "Third error"/ + end + + # assert application trace refers to line that raises the last exception + assert_select "#Application-Trace-0" do + assert_select "code a:first", %r{in `rescue in rescue in raise_nested_exceptions'} + end + + # assert the second application trace refers to the line that raises the second exception + assert_select "#Application-Trace-1" do + assert_select "code a:first", %r{in `rescue in raise_nested_exceptions'} + end + + # assert the third application trace refers to the line that raises the first exception + assert_select "#Application-Trace-2" do + assert_select "code a:first", %r{in `raise_nested_exceptions'} + end + end + end end diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb index f6e70382a8..600280d6b3 100644 --- a/actionpack/test/dispatch/exception_wrapper_test.rb +++ b/actionpack/test/dispatch/exception_wrapper_test.rb @@ -108,11 +108,27 @@ module ActionDispatch wrapper = ExceptionWrapper.new(@cleaner, exception) assert_equal({ - "Application Trace" => [ id: 0, trace: "lib/file.rb:42:in `index'" ], - "Framework Trace" => [ id: 1, trace: "/gems/rack.rb:43:in `index'" ], + "Application Trace" => [ + exception_object_id: exception.object_id, + id: 0, + trace: "lib/file.rb:42:in `index'" + ], + "Framework Trace" => [ + exception_object_id: exception.object_id, + id: 1, + trace: "/gems/rack.rb:43:in `index'" + ], "Full Trace" => [ - { id: 0, trace: "lib/file.rb:42:in `index'" }, - { id: 1, trace: "/gems/rack.rb:43:in `index'" } + { + exception_object_id: exception.object_id, + id: 0, + trace: "lib/file.rb:42:in `index'" + }, + { + exception_object_id: exception.object_id, + id: 1, + trace: "/gems/rack.rb:43:in `index'" + } ] }, wrapper.traces) end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 26170dfa77..6d45cc1d8a 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,26 @@ +* Fix issue with `button_to`'s `to_form_params` + + `button_to` was throwing exception when invoked with `params` hash that + contains symbol and string keys. The reason for the exception was that + `to_form_params` was comparing the given symbol and string keys. + + The issue is fixed by turning all keys to strings inside + `to_form_params` before comparing them. + + *Georgi Georgiev* + +* Mark arrays of translations as trusted safe by using the `_html` suffix. + + Example: + + en: + foo_html: + - "One" + - "<strong>Two</strong>" + - "Three 👋 🙂" + + *Juan Broullon* + * Add `year_format` option to date_select tag. This option makes it possible to customize year names. Lambda should be passed to use this option. @@ -5,7 +28,7 @@ date_select('user_birthday', '', start_year: 1998, end_year: 2000, year_format: ->year { "Heisei #{year - 1988}" }) - The HTML produced: + The HTML produced: <select id="user_birthday__1i" name="user_birthday[(1i)]"> <option value="1998">Heisei 10</option> diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 2d5c5684c1..07f3d98322 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1658,6 +1658,7 @@ module ActionView @nested_child_index = {} @object_name, @object, @template, @options = object_name, object, template, options @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {} + @default_html_options = @default_options.except(:skip_default_ids, :allow_method_names_outside_object) convert_to_legacy_options(@options) diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index d02f641867..7884a8d997 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -820,7 +820,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def select(method, choices = nil, options = {}, html_options = {}, &block) - @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options), &block) + @template.select(@object_name, method, choices, objectify_options(options), @default_html_options.merge(html_options), &block) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders: @@ -832,7 +832,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) - @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options)) end # Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders: @@ -844,7 +844,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) - @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options)) + @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_html_options.merge(html_options)) end # Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders: @@ -856,7 +856,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) - @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options)) + @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_html_options.merge(html_options)) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders: @@ -868,7 +868,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block) - @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) + @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders: @@ -880,7 +880,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block) - @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) + @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block) end end end diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 275a2dffb4..cb0c99c4cf 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -126,7 +126,7 @@ module ActionView attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer # Vendors the full, link and white list sanitizers. - # Provided strictly for compatibility and can be removed in Rails 5.1. + # Provided strictly for compatibility and can be removed in Rails 6. def sanitizer_vendor Rails::Html::Sanitizer end diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index 345484ba92..790721a0b7 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -8,7 +8,7 @@ module ActionView @choices = block_given? ? template_object.capture { yield || "" } : choices @choices = @choices.to_a if @choices.is_a?(Range) - @html_options = html_options.except(:skip_default_ids, :allow_method_names_outside_object) + @html_options = html_options super(object_name, method_name, template_object, options) end diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index d3cdab0d2f..ba82dcab3e 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -83,8 +83,11 @@ module ActionView end end translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise)) - - translation.respond_to?(:html_safe) ? translation.html_safe : translation + if translation.respond_to?(:map) + translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element } + else + translation.respond_to?(:html_safe) ? translation.html_safe : translation + end else I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise)) end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index cae62f2312..52bffaab84 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -634,7 +634,7 @@ module ActionView # suitable for use as the names and values of form input fields: # # to_form_params(name: 'David', nationality: 'Danish') - # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}] + # # => [{name: 'name', value: 'David'}, {name: 'nationality', value: 'Danish'}] # # to_form_params(country: { name: 'Denmark' }) # # => [{name: 'country[name]', value: 'Denmark'}] @@ -666,7 +666,7 @@ module ActionView params.push(*to_form_params(value, array_prefix)) end else - params << { name: namespace, value: attribute.to_param } + params << { name: namespace.to_s, value: attribute.to_param } end params.sort_by { |pair| pair[:name] } diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index ed1683bad2..f07e494ee3 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -464,6 +464,23 @@ class FormWithActsLikeFormForTest < FormWithTest end end + def test_form_with_with_collection_select + post = Post.new + def post.active; false; end + form_with(model: post) do |f| + concat f.collection_select(:active, [true, false], :to_s, :to_s) + end + + expected = whole_form("/posts") do + "<select name='post[active]' id='post_active'>" \ + "<option value='true'>true</option>\n" \ + "<option selected='selected' value='false'>false</option>" \ + "</select>" + end + + assert_dom_equal expected, output_buffer + end + def test_form_with_with_collection_radio_buttons post = Post.new def post.active; false; end diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index f40595bf4d..e756348938 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -164,8 +164,11 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal "<a>Other <One></a>", translate(:'translations.count_html', count: "<One>") end - def test_translation_returning_an_array_ignores_html_suffix - assert_equal ["foo", "bar"], translate(:'translations.array_html') + def test_translate_marks_array_of_translations_with_a_html_safe_suffix_as_safe_html + translate(:'translations.array_html').tap do |translated| + assert_equal %w( foo bar ), translated + assert translated.all?(&:html_safe?) + end end def test_translate_with_default_named_html diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 08cb5dfea7..9d91dbb72b 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -77,11 +77,18 @@ class UrlHelperTest < ActiveSupport::TestCase def test_to_form_params_with_hash assert_equal( - [{ name: :name, value: "David" }, { name: :nationality, value: "Danish" }], + [{ name: "name", value: "David" }, { name: "nationality", value: "Danish" }], to_form_params(name: "David", nationality: "Danish") ) end + def test_to_form_params_with_hash_having_symbol_and_string_keys + assert_equal( + [{ name: "name", value: "David" }, { name: "nationality", value: "Danish" }], + to_form_params("name" => "David", :nationality => "Danish") + ) + end + def test_to_form_params_with_nested_hash assert_equal( [{ name: "country[name]", value: "Denmark" }], diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 3ecbc2ff38..8526741383 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,10 @@ +* Move `enqueue`/`enqueue_at` notifications to an around callback. + + Improves timing accuracy over the old after callback by including + time spent writing to the adapter's IO implementation. + + *Zach Kemp* + * Allow `queue` option to `assert_no_enqueued_jobs`. Example: diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index 2b2a59e969..95b1062701 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -60,7 +60,6 @@ module ActiveJob #:nodoc: # * SerializationError - Error class for serialization errors. class Base include Core - include Serializers include QueueAdapter include QueueName include QueuePriority diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index 3312857ac7..9ffd60ad53 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -27,13 +27,13 @@ module ActiveJob end end - after_enqueue do |job| + around_enqueue do |job, block| if job.scheduled_at - ActiveSupport::Notifications.instrument "enqueue_at.active_job", - adapter: job.class.queue_adapter, job: job + ActiveSupport::Notifications.instrument("enqueue_at.active_job", + adapter: job.class.queue_adapter, job: job, &block) else - ActiveSupport::Notifications.instrument "enqueue.active_job", - adapter: job.class.queue_adapter, job: job + ActiveSupport::Notifications.instrument("enqueue.active_job", + adapter: job.class.queue_adapter, job: job, &block) end end end diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb index df66e66659..a5d90f48b8 100644 --- a/activejob/lib/active_job/serializers.rb +++ b/activejob/lib/active_job/serializers.rb @@ -7,7 +7,6 @@ module ActiveJob # and to add new ones. It also has helpers to serialize/deserialize objects. module Serializers # :nodoc: extend ActiveSupport::Autoload - extend ActiveSupport::Concern autoload :ObjectSerializer autoload :SymbolSerializer diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index 1f8c4a5573..a1107a07fd 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -45,6 +45,14 @@ class LoggingTest < ActiveSupport::TestCase ActiveJob::Base.logger = logger end + def subscribed + [].tap do |events| + ActiveSupport::Notifications.subscribed(-> (*args) { events << args }, /enqueue.*\.active_job/) do + yield + end + end + end + def test_uses_active_job_as_tag HelloJob.perform_later "Cristian" assert_match(/\[ActiveJob\]/, @logger.messages) @@ -86,8 +94,11 @@ class LoggingTest < ActiveSupport::TestCase end def test_enqueue_job_logging - HelloJob.perform_later "Cristian" + events = subscribed { HelloJob.perform_later "Cristian" } assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages) + assert_equal(events.count, 1) + key, * = events.first + assert_equal(key, "enqueue.active_job") end def test_perform_job_logging @@ -110,15 +121,21 @@ class LoggingTest < ActiveSupport::TestCase end def test_enqueue_at_job_logging - HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian" + events = subscribed { HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian" } assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages) + assert_equal(events.count, 1) + key, * = events.first + assert_equal(key, "enqueue_at.active_job") rescue NotImplementedError skip end def test_enqueue_in_job_logging - HelloJob.set(wait: 2.seconds).perform_later "Cristian" + events = subscribed { HelloJob.set(wait: 2.seconds).perform_later "Cristian" } assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages) + assert_equal(events.count, 1) + key, * = events.first + assert_equal(key, "enqueue_at.active_job") rescue NotImplementedError skip end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 1a464c2ffd..8f80838a33 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -6,13 +6,13 @@ Example: class User < ActiveRecord::Base - has_secure_password :activation_token, validations: false + has_secure_password :recovery_password, validations: false end user = User.new() - user.activation_token = "a_new_token" - user.activation_token_digest # => "$2a$10$0Budk0Fi/k2CDm2PEwa3Be..." - user.authenticate_activation_token('a_new_token') # => user + user.recovery_password = "42password" + user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uX..." + user.authenticate_recovery_password('42password') # => user *Unathi Chonco* diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index eaf8dfb223..093052a70c 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -304,7 +304,7 @@ module ActiveModel # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - previous_changes[attr] if attribute_previously_changed?(attr) + previous_changes[attr] end # Handles <tt>*_will_change!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 56404a036c..edc30ee64d 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -328,7 +328,7 @@ module ActiveModel # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) if message.is_a? Symbol - self.details[attribute].map { |e| e[:error] }.include? message + self.details[attribute.to_sym].map { |e| e[:error] }.include? message else message = message.call if message.respond_to?(:call) message = normalize_message(attribute, message, options) diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 7f3763fa56..51d54f34f3 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -17,7 +17,7 @@ module ActiveModel module ClassMethods # Adds methods to set and authenticate against a BCrypt password. # This mechanism requires you to have a +XXX_digest+ attribute. - # Where +XXX+ is the attribute name of your desired password/token or defaults to +password+ + # Where +XXX+ is the attribute name of your desired password. # # The following validations are added automatically: # * Password must be present on creation @@ -38,10 +38,10 @@ module ActiveModel # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # - # # Schema: User(name:string, password_digest:string, activation_token_digest:string) + # # Schema: User(name:string, password_digest:string, recovery_password_digest:string) # class User < ActiveRecord::Base # has_secure_password - # has_secure_password :activation_token, validations: false + # has_secure_password :recovery_password, validations: false # end # # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch') @@ -50,12 +50,12 @@ module ActiveModel # user.save # => false, confirmation doesn't match # user.password_confirmation = 'mUc3m00RsqyRe' # user.save # => true - # user.activation_token = "a_new_token" - # user.activation_token_digest # => "$2a$10$0Budk0Fi/k2CDm2PEwa3BeXO5tPOA85b6xazE9rp8nF2MIJlsUik." + # user.recovery_password = "42password" + # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC" # user.save # => true # user.authenticate('notright') # => false # user.authenticate('mUc3m00RsqyRe') # => user - # user.authenticate_activation_token('a_new_token') # => user + # user.authenticate_recovery_password('42password') # => user # User.find_by(name: 'david').try(:authenticate, 'notright') # => false # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user def has_secure_password(attribute = :password, validations: true) diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 6ff3be1308..41ff6443fe 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -185,6 +185,12 @@ class ErrorsTest < ActiveModel::TestCase assert person.errors.added?(:name, :blank) end + test "added? returns true when string attribute is used with a symbol message" do + person = Person.new + person.errors.add(:name, :blank) + assert person.errors.added?("name", :blank) + end + test "added? handles proc messages" do person = Person.new message = Proc.new { "cannot be blank" } diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index bc23316ad5..9ef1148be8 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -186,13 +186,16 @@ class SecurePasswordTest < ActiveModel::TestCase test "authenticate" do @user.password = "secret" - @user.activation_token = "new_token" + @user.recovery_password = "42password" - assert_not @user.authenticate("wrong") - assert @user.authenticate("secret") + assert_equal false, @user.authenticate("wrong") + assert_equal @user, @user.authenticate("secret") - assert !@user.authenticate_activation_token("wrong") - assert @user.authenticate_activation_token("new_token") + assert_equal false, @user.authenticate_password("wrong") + assert_equal @user, @user.authenticate_password("secret") + + assert_equal false, @user.authenticate_recovery_password("wrong") + assert_equal @user, @user.authenticate_recovery_password("42password") end test "Password digest cost defaults to bcrypt default cost when min_cost is false" do diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb index 1ff3379153..bb1b187694 100644 --- a/activemodel/test/models/user.rb +++ b/activemodel/test/models/user.rb @@ -7,7 +7,7 @@ class User define_model_callbacks :create has_secure_password - has_secure_password :activation_token, validations: false + has_secure_password :recovery_password, validations: false - attr_accessor :password_digest, :activation_token_digest + attr_accessor :password_digest, :recovery_password_digest end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index fad09182c9..456f569718 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,18 @@ +* Don't impose primary key order if limit() has already been supplied. + + Fixes #23607 + + *Brian Christian* + +* Add environment & load_config dependency to `bin/rake db:seed` to enable + seed load in environments without Rails and custom DB configuration + + *Tobias Bielohlawek* + +* Fix default value for mysql time types with specified precision. + + *Nikolay Kondratyev* + * Fix `touch` option to behave consistently with `Persistence#touch` method. *Ryuta Kamizono* diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 8cf1b5d25a..b76005b587 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -14,10 +14,8 @@ module ActiveRecord i[column.name] = column.alias } } - @name_and_alias_cache = tables.each_with_object({}) { |table, h| - h[table.node] = table.columns.map { |column| - [column.name, column.alias] - } + @columns_cache = tables.each_with_object({}) { |table, h| + h[table.node] = table.columns } end @@ -25,9 +23,8 @@ module ActiveRecord @tables.flat_map(&:column_aliases) end - # An array of [column_name, alias] pairs for the table def column_aliases(node) - @name_and_alias_cache[node] + @columns_cache[node] end def column_alias(node, column) @@ -225,19 +222,14 @@ module ActiveRecord next end - model = seen[ar_parent.object_id][node.base_klass][id] + model = seen[ar_parent.object_id][node][id] if model construct(model, node, row, seen, model_cache) else model = construct_model(ar_parent, node, row, model_cache, id) - if node.reflection.scope && - node.reflection.scope_for(node.base_klass.unscoped).readonly_value - model.readonly! - end - - seen[ar_parent.object_id][node.base_klass][id] = model + seen[ar_parent.object_id][node][id] = model construct(model, node, row, seen, model_cache) end end @@ -257,6 +249,7 @@ module ActiveRecord other.target = model end + model.readonly! if node.readonly? model end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 6e5e950e90..4583d89cba 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -54,6 +54,12 @@ module ActiveRecord @tables = tables @table = tables.first end + + def readonly? + return @readonly if defined?(@readonly) + + @readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value + end end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 3cabb21983..3ad72a3646 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -54,8 +54,8 @@ module ActiveRecord length = column_names_with_alias.length while index < length - column_name, alias_name = column_names_with_alias[index] - hash[column_name] = row[alias_name] + column = column_names_with_alias[index] + hash[column.name] = row[column.alias] index += 1 end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index aec5fa6ba1..98b1348135 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -130,6 +130,7 @@ module ActiveRecord end def quoted_time(value) # :nodoc: + value = value.change(year: 2000, month: 1, day: 1) quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "") end 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 07acb5425e..284b38ed7b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -558,17 +558,6 @@ module ActiveRecord @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin) end - def with_multi_statements - previous_flags = @config[:flags] - @config[:flags] = Mysql2::Client::MULTI_STATEMENTS - reconnect! - - yield - ensure - @config[:flags] = previous_flags - reconnect! - end - def initialize_type_map(m = type_map) super diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index 4106ce01be..d89eeb7f54 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -62,6 +62,42 @@ module ActiveRecord @connection.abandon_results! end + def supports_set_server_option? + @connection.respond_to?(:set_server_option) + end + + def multi_statements_enabled?(flags) + if flags.is_a?(Array) + flags.include?("MULTI_STATEMENTS") + else + (flags & Mysql2::Client::MULTI_STATEMENTS) != 0 + end + end + + def with_multi_statements + previous_flags = @config[:flags] + + unless multi_statements_enabled?(previous_flags) + if supports_set_server_option? + @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON) + else + @config[:flags] = Mysql2::Client::MULTI_STATEMENTS + reconnect! + end + end + + yield + ensure + unless multi_statements_enabled?(previous_flags) + if supports_set_server_option? + @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF) + else + @config[:flags] = previous_flags + reconnect! + end + end + end + def exec_stmt_and_free(sql, name, binds, cache_stmt: false) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index ce50590651..2087938d7c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -80,8 +80,8 @@ module ActiveRecord def new_column_from_field(table_name, field) type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\(\))?\z/i.match?(field[:Default]) - default, default_function = nil, "CURRENT_TIMESTAMP" + if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(field[:Default]) + default, default_function = nil, field[:Default] else default, default_function = field[:Default], nil end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index 58e5138e02..24e7bc65fa 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -7,6 +7,10 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name) exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| + # Indexes SQLite creates implicitly for internal use start with "sqlite_". + # See https://www.sqlite.org/fileformat2.html#intschema + next if row["name"].starts_with?("sqlite_") + index_sql = query_value(<<-SQL, "SCHEMA") SELECT sql FROM sqlite_master @@ -40,7 +44,7 @@ module ActiveRecord where: where, orders: orders ) - end + end.compact end def create_schema_dumper(options) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 544374586c..bee74dc33d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -15,6 +15,8 @@ require "sqlite3" module ActiveRecord module ConnectionHandling # :nodoc: def sqlite3_connection(config) + config = config.symbolize_keys + # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" @@ -31,7 +33,7 @@ module ActiveRecord db = SQLite3::Database.new( config[:database].to_s, - results_as_hash: true + config.merge(results_as_hash: true) ) db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] @@ -455,9 +457,6 @@ module ActiveRecord def copy_table_indexes(from, to, rename = {}) indexes(from).each do |index| name = index.name - # indexes sqlite creates for internal use start with `sqlite_` and - # don't need to be copied - next if name.starts_with?("sqlite_") if to == "a#{from}" name = "t#{name}" elsif from == "a#{to}" diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 24449e8df3..8b7d18fb3d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -217,7 +217,7 @@ db_namespace = namespace :db do task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed] desc "Loads the seed data from db/seeds.rb" - task :seed do + task seed: :load_config do db_namespace["abort_if_pending_migrations"].invoke ActiveRecord::Tasks::DatabaseTasks.load_seed end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 7ab9bb2d2d..fce5b66719 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -309,10 +309,10 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - previous, klass.current_scope = klass.current_scope(true), self + previous, klass.current_scope = klass.current_scope(true), self unless @delegate_to_klass yield ensure - klass.current_scope = previous + klass.current_scope = previous unless @delegate_to_klass end def _exec_scope(*args, &block) # :nodoc: diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index b9e7c52e88..93f3b67686 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -371,15 +371,14 @@ module ActiveRecord relation end - def construct_join_dependency - including = eager_load_values + includes_values + def construct_join_dependency(associations) ActiveRecord::Associations::JoinDependency.new( - klass, table, including + klass, table, associations ) end def apply_join_dependency(eager_loading: group_values.empty?) - join_dependency = construct_join_dependency + join_dependency = construct_join_dependency(eager_load_values + includes_values) relation = except(:includes, :eager_load, :preload).joins!(join_dependency) if eager_loading && !using_limitable_reflections?(join_dependency.reflections) @@ -551,7 +550,7 @@ module ActiveRecord end def ordered_relation - if order_values.empty? && primary_key + if order_values.empty? && primary_key && limit_value.blank? order(arel_attribute(primary_key).asc) else self diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 9cbcf61b3e..10e4779ca4 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -120,9 +120,7 @@ module ActiveRecord joins_dependency = other.joins_values.map do |join| case join when Hash, Symbol, Array - ActiveRecord::Associations::JoinDependency.new( - other.klass, other.table, join - ) + other.send(:construct_join_dependency, join) else join end @@ -141,9 +139,7 @@ module ActiveRecord joins_dependency = other.left_outer_joins_values.map do |join| case join when Hash, Symbol, Array - ActiveRecord::Associations::JoinDependency.new( - other.klass, other.table, join - ) + other.send(:construct_join_dependency, join) else join end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 5b4ba85316..52405f21a1 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1026,9 +1026,7 @@ module ActiveRecord join_list = join_nodes + convert_join_strings_to_ast(string_joins) alias_tracker = alias_tracker(join_list, aliases) - join_dependency = ActiveRecord::Associations::JoinDependency.new( - klass, table, association_joins - ) + join_dependency = construct_join_dependency(association_joins) joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker) joins.each { |join| manager.from(join) } diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index b092399657..562e04194c 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -10,7 +10,6 @@ module ActiveRecord def spawn #:nodoc: clone end - alias :all :spawn # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 976c5dde58..58fa39d18c 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -158,7 +158,6 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def test_indexes_in_create ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false) - ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) AS SELECT id, name, zip FROM a_really_complicated_query" actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb index fa54f39992..00a075e063 100644 --- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -45,9 +45,10 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase end def stub_version(full_version_string) - @connection.stubs(:full_version).returns(full_version_string) - @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) - yield + @connection.stub(:full_version, full_version_string) do + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + yield + end ensure @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index ad155c7492..d1d4d545a3 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -504,6 +504,39 @@ module ActiveRecord assert_deprecated { @conn.valid_alter_table_type?(:string) } end + def test_db_is_not_readonly_when_readonly_option_is_false + conn = Base.sqlite3_connection database: ":memory:", + adapter: "sqlite3", + readonly: false + + assert_not_predicate conn.raw_connection, :readonly? + end + + def test_db_is_not_readonly_when_readonly_option_is_unspecified + conn = Base.sqlite3_connection database: ":memory:", + adapter: "sqlite3" + + assert_not_predicate conn.raw_connection, :readonly? + end + + def test_db_is_readonly_when_readonly_option_is_true + conn = Base.sqlite3_connection database: ":memory:", + adapter: "sqlite3", + readonly: true + + assert_predicate conn.raw_connection, :readonly? + end + + def test_writes_are_not_permitted_to_readonly_databases + conn = Base.sqlite3_connection database: ":memory:", + adapter: "sqlite3", + readonly: true + + assert_raises(ActiveRecord::StatementInvalid, /SQLite3::ReadOnlyException/) do + conn.execute("CREATE TABLE test(id integer)") + end + end + private def assert_logged(logs) diff --git a/activerecord/test/cases/arel/insert_manager_test.rb b/activerecord/test/cases/arel/insert_manager_test.rb index ae10ccf56c..2376ad8d37 100644 --- a/activerecord/test/cases/arel/insert_manager_test.rb +++ b/activerecord/test/cases/arel/insert_manager_test.rb @@ -220,7 +220,6 @@ module Arel end describe "select" do - it "accepts a select query in place of a VALUES clause" do table = Table.new :users @@ -238,7 +237,6 @@ module Arel INSERT INTO "users" ("id", "name") (SELECT 1, "aaron") } end - end end end diff --git a/activerecord/test/cases/arel/select_manager_test.rb b/activerecord/test/cases/arel/select_manager_test.rb index f318577b94..6b10d0b612 100644 --- a/activerecord/test/cases/arel/select_manager_test.rb +++ b/activerecord/test/cases/arel/select_manager_test.rb @@ -244,8 +244,6 @@ module Arel @m2 = Arel::SelectManager.new table @m2.project Arel.star @m2.where(table[:age].gt(99)) - - end it "should union two managers" do @@ -266,7 +264,6 @@ module Arel ( SELECT * FROM "users" WHERE "users"."age" < 18 UNION ALL SELECT * FROM "users" WHERE "users"."age" > 99 ) } end - end describe "intersect" do @@ -279,8 +276,6 @@ module Arel @m2 = Arel::SelectManager.new table @m2.project Arel.star @m2.where(table[:age].lt(99)) - - end it "should interect two managers" do @@ -293,7 +288,6 @@ module Arel ( SELECT * FROM "users" WHERE "users"."age" > 18 INTERSECT SELECT * FROM "users" WHERE "users"."age" < 99 ) } end - end describe "except" do @@ -318,7 +312,6 @@ module Arel ( SELECT * FROM "users" WHERE "users"."age" BETWEEN 18 AND 60 EXCEPT SELECT * FROM "users" WHERE "users"."age" BETWEEN 40 AND 99 ) } end - end describe "with" do @@ -647,7 +640,6 @@ module Arel end describe "joins" do - it "returns inner join sql" do table = Table.new :users aliaz = table.alias @@ -1002,7 +994,6 @@ module Arel end describe "update" do - it "creates an update statement" do table = Table.new :users manager = Arel::SelectManager.new @@ -1075,7 +1066,6 @@ module Arel UPDATE "users" SET "id" = 1 WHERE "users"."id" IN (SELECT "users"."id" FROM "users" WHERE "users"."foo" = 10 LIMIT 42) } end - end describe "project" do @@ -1097,7 +1087,6 @@ module Arel manager.project "*" manager.to_sql.must_be_like %{ SELECT * } end - end describe "projections" do diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 8be663e3dc..5b8d4722af 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -123,6 +123,24 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal memberships(:membership_of_boring_club), member.current_membership end + def test_loading_associations_dont_leak_instance_state + assertions = ->(firm) { + assert_equal companies(:first_firm), firm + + assert_predicate firm.association(:readonly_account), :loaded? + assert_predicate firm.association(:accounts), :loaded? + + assert_equal accounts(:signals37), firm.readonly_account + assert_equal [accounts(:signals37)], firm.accounts + + assert_predicate firm.readonly_account, :readonly? + assert firm.accounts.none?(&:readonly?) + } + + assertions.call(Firm.preload(:readonly_account, :accounts).first) + assertions.call(Firm.eager_load(:readonly_account, :accounts).first) + end + def test_with_ordering list = Post.all.merge!(includes: :comments, order: "posts.id DESC").to_a [:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other, diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 1c96aaabe2..0f957d41cf 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -106,21 +106,31 @@ if current_adapter?(:Mysql2Adapter) class MysqlDefaultExpressionTest < ActiveRecord::TestCase include SchemaDumpingHelper - if ActiveRecord::Base.connection.version >= "5.6.0" + if subsecond_precision_supported? test "schema dump datetime includes default expression" do output = dump_table_schema("datetime_defaults") - assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP(?:\(\))?" }/i, output end - end - test "schema dump timestamp includes default expression" do - output = dump_table_schema("timestamp_defaults") - assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP" }/, output - end + test "schema dump datetime includes precise default expression" do + output = dump_table_schema("datetime_defaults") + assert_match %r/t\.datetime\s+"precise_datetime",.+default: -> { "CURRENT_TIMESTAMP\(6\)" }/i, output + end - test "schema dump timestamp without default expression" do - output = dump_table_schema("timestamp_defaults") - assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output + test "schema dump timestamp includes default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP(?:\(\))?" }/i, output + end + + test "schema dump timestamp includes precise default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"precise_timestamp",.+default: -> { "CURRENT_TIMESTAMP\(6\)" }/i, output + end + + test "schema dump timestamp without default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output + end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 39ffe12ec6..2f256dd36a 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1265,6 +1265,21 @@ class FinderTest < ActiveRecord::TestCase end end + def test_first_and_last_with_limit_for_order_without_primary_key + # While Topic.first should impose an ordering by primary key, + # Topic.limit(n).first should not + + Topic.first.touch # PostgreSQL changes the default order if no order clause is used + + assert_equal Topic.limit(1).to_a.first, Topic.limit(1).first + assert_equal Topic.limit(2).to_a.first, Topic.limit(2).first + assert_equal Topic.limit(2).to_a.first(2), Topic.limit(2).first(2) + + assert_equal Topic.limit(1).to_a.last, Topic.limit(1).last + assert_equal Topic.limit(2).to_a.last, Topic.limit(2).last + assert_equal Topic.limit(2).to_a.last(2), Topic.limit(2).last(2) + end + def test_finder_with_offset_string assert_nothing_raised { Topic.offset("3").to_a } end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index ee88bd8144..c65523d8c1 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "cases/helper" +require "support/connection_helper" require "models/admin" require "models/admin/account" require "models/admin/randomly_named_c1" @@ -32,6 +33,8 @@ require "models/treasure" require "tempfile" class FixturesTest < ActiveRecord::TestCase + include ConnectionHelper + self.use_instantiated_fixtures = true self.use_transactional_tests = false @@ -114,9 +117,96 @@ class FixturesTest < ActiveRecord::TestCase end end end + + def test_bulk_insert_with_a_multi_statement_query_in_a_nested_transaction + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a"] }, + ] + } + + assert_difference "TrafficLight.count" do + ActiveRecord::Base.transaction do + conn = ActiveRecord::Base.connection + assert_equal 1, conn.open_transactions + conn.insert_fixtures_set(fixtures) + assert_equal 1, conn.open_transactions + end + end + end end if current_adapter?(:Mysql2Adapter) + def test_bulk_insert_with_multi_statements_enabled + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection( + orig_connection.merge(flags: %w[MULTI_STATEMENTS]) + ) + + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a"] }, + ] + } + + ActiveRecord::Base.connection.stub(:supports_set_server_option?, false) do + assert_nothing_raised do + conn = ActiveRecord::Base.connection + conn.execute("SELECT 1; SELECT 2;") + conn.raw_connection.abandon_results! + end + + assert_difference "TrafficLight.count" do + ActiveRecord::Base.transaction do + conn = ActiveRecord::Base.connection + assert_equal 1, conn.open_transactions + conn.insert_fixtures_set(fixtures) + assert_equal 1, conn.open_transactions + end + end + + assert_nothing_raised do + conn = ActiveRecord::Base.connection + conn.execute("SELECT 1; SELECT 2;") + conn.raw_connection.abandon_results! + end + end + end + end + + def test_bulk_insert_with_multi_statements_disabled + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection( + orig_connection.merge(flags: []) + ) + + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a"] }, + ] + } + + ActiveRecord::Base.connection.stub(:supports_set_server_option?, false) do + assert_raises(ActiveRecord::StatementInvalid) do + conn = ActiveRecord::Base.connection + conn.execute("SELECT 1; SELECT 2;") + conn.raw_connection.abandon_results! + end + + assert_difference "TrafficLight.count" do + conn = ActiveRecord::Base.connection + conn.insert_fixtures_set(fixtures) + end + + assert_raises(ActiveRecord::StatementInvalid) do + conn = ActiveRecord::Base.connection + conn.execute("SELECT 1; SELECT 2;") + conn.raw_connection.abandon_results! + end + end + end + end + def test_insert_fixtures_set_raises_an_error_when_max_allowed_packet_is_smaller_than_fixtures_set_size conn = ActiveRecord::Base.connection mysql_margin = 2 @@ -128,10 +218,10 @@ class FixturesTest < ActiveRecord::TestCase ] } - conn.stubs(:max_allowed_packet).returns(packet_size - mysql_margin) - - error = assert_raises(ActiveRecord::ActiveRecordError) { conn.insert_fixtures_set(fixtures) } - assert_match(/Fixtures set is too large #{packet_size}\./, error.message) + conn.stub(:max_allowed_packet, packet_size - mysql_margin) do + error = assert_raises(ActiveRecord::ActiveRecordError) { conn.insert_fixtures_set(fixtures) } + assert_match(/Fixtures set is too large #{packet_size}\./, error.message) + end end def test_insert_fixture_set_when_max_allowed_packet_is_bigger_than_fixtures_set_size @@ -143,10 +233,10 @@ class FixturesTest < ActiveRecord::TestCase ] } - conn.stubs(:max_allowed_packet).returns(packet_size) - - assert_difference "TrafficLight.count" do - conn.insert_fixtures_set(fixtures) + conn.stub(:max_allowed_packet, packet_size) do + assert_difference "TrafficLight.count" do + conn.insert_fixtures_set(fixtures) + end end end @@ -164,12 +254,13 @@ class FixturesTest < ActiveRecord::TestCase ] } - conn.stubs(:max_allowed_packet).returns(packet_size) + conn.stub(:max_allowed_packet, packet_size) do + conn.insert_fixtures_set(fixtures) - conn.insert_fixtures_set(fixtures) - assert_equal 2, subscriber.events.size - assert_operator subscriber.events.first.bytesize, :<, packet_size - assert_operator subscriber.events.second.bytesize, :<, packet_size + assert_equal 2, subscriber.events.size + assert_operator subscriber.events.first.bytesize, :<, packet_size + assert_operator subscriber.events.second.bytesize, :<, packet_size + end ensure ActiveSupport::Notifications.unsubscribe(subscription) end @@ -188,10 +279,10 @@ class FixturesTest < ActiveRecord::TestCase ] } - conn.stubs(:max_allowed_packet).returns(packet_size) - - assert_difference ["TrafficLight.count", "Comment.count"], +1 do - conn.insert_fixtures_set(fixtures) + conn.stub(:max_allowed_packet, packet_size) do + assert_difference ["TrafficLight.count", "Comment.count"], +1 do + conn.insert_fixtures_set(fixtures) + end end assert_equal 1, subscriber.events.size ensure @@ -833,44 +924,58 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase self.use_instantiated_fixtures = false def test_transaction_created_on_connection_notification - connection = stub(transaction_open?: false) - connection.expects(:begin_transaction).with(joinable: false) - pool = connection.stubs(:pool).returns(ActiveRecord::ConnectionAdapters::ConnectionPool.new(ActiveRecord::Base.connection_pool.spec)) - pool.stubs(:lock_thread=).with(false) - fire_connection_notification(connection) + connection = Class.new do + attr_accessor :pool + + def transaction_open?; end + def begin_transaction(*args); end + def rollback_transaction(*args); end + end.new + + connection.pool = Class.new do + def lock_thread=(lock_thread); end + end.new + + assert_called_with(connection, :begin_transaction, [joinable: false]) do + fire_connection_notification(connection) + end end def test_notification_established_transactions_are_rolled_back - # Mocha is not thread-safe so define our own stub to test connection = Class.new do attr_accessor :rollback_transaction_called attr_accessor :pool + def transaction_open?; true; end def begin_transaction(*args); end def rollback_transaction(*args) @rollback_transaction_called = true end end.new + connection.pool = Class.new do - def lock_thread=(lock_thread); false; end + def lock_thread=(lock_thread); end end.new + fire_connection_notification(connection) teardown_fixtures + assert(connection.rollback_transaction_called, "Expected <mock connection>#rollback_transaction to be called but was not") end private def fire_connection_notification(connection) - ActiveRecord::Base.connection_handler.stubs(:retrieve_connection).with("book").returns(connection) - message_bus = ActiveSupport::Notifications.instrumenter - payload = { - spec_name: "book", - config: nil, - connection_id: connection.object_id - } + assert_called_with(ActiveRecord::Base.connection_handler, :retrieve_connection, ["book"], returns: connection) do + message_bus = ActiveSupport::Notifications.instrumenter + payload = { + spec_name: "book", + config: nil, + connection_id: connection.object_id + } - message_bus.instrument("!connection.active_record", payload) {} + message_bus.instrument("!connection.active_record", payload) {} + end end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 4a0ad0442a..3d3189900f 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -91,7 +91,6 @@ class InheritanceTest < ActiveRecord::TestCase end ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise e }) do - exception = assert_raises NameError do Company.send :compute_type, "InvalidModel" end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 32af90caef..ec01a2965d 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -669,7 +669,6 @@ module NestedAttributesOnACollectionAssociationTests def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models @child_1.stub(:id, "ABC1X") do @child_2.stub(:id, "ABC2X") do - @pirate.attributes = { association_getter => [ { id: @child_1.id, name: "Grace OMalley" }, diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 60dac91ec9..4ed7469039 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -305,6 +305,7 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase test "schema dump primary key includes type and options" do schema = dump_table_schema "barcodes" assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema + assert_no_match %r{t\.index \["code"\]}, schema end if current_adapter?(:Mysql2Adapter) && subsecond_precision_supported? diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 1c05571f1b..69be091869 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -482,7 +482,6 @@ class QueryCacheTest < ActiveRecord::TestCase assert_not ActiveRecord::Base.connection.query_cache_enabled }.join }.call({}) - end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 92eb0c814f..f875dc96f7 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -71,8 +71,8 @@ module ActiveRecord with_timezone_config default: :utc do t = Time.now.change(usec: 0) - expected = t.getutc.change(year: 2000, month: 1, day: 1) - expected = expected.to_s(:db).sub("2000-01-01 ", "") + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getutc.to_s(:db).slice(11..-1) assert_equal expected, @quoter.quoted_time(t) end @@ -89,6 +89,28 @@ module ActiveRecord end end + def test_quoted_time_dst_utc + with_timezone_config default: :utc do + t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30") + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getutc.to_s(:db).slice(11..-1) + + assert_equal expected, @quoter.quoted_time(t) + end + end + + def test_quoted_time_dst_local + with_timezone_config default: :local do + t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30") + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getlocal.to_s(:db).slice(11..-1) + + assert_equal expected, @quoter.quoted_time(t) + end + end + def test_quoted_time_crazy with_timezone_config default: :asdfasdf do t = Time.now.change(usec: 0) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 15f4cea1a6..75b68b521c 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -297,29 +297,33 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_extensions connection = ActiveRecord::Base.connection - connection.stubs(:extensions).returns(["hstore"]) - output = perform_schema_dump - assert_match "# These are extensions that must be enabled", output - assert_match %r{enable_extension "hstore"}, output - - connection.stubs(:extensions).returns([]) - output = perform_schema_dump - assert_no_match "# These are extensions that must be enabled", output - assert_no_match %r{enable_extension}, output + connection.stub(:extensions, ["hstore"]) do + output = perform_schema_dump + assert_match "# These are extensions that must be enabled", output + assert_match %r{enable_extension "hstore"}, output + end + + connection.stub(:extensions, []) do + output = perform_schema_dump + assert_no_match "# These are extensions that must be enabled", output + assert_no_match %r{enable_extension}, output + end end def test_schema_dump_includes_extensions_in_alphabetic_order connection = ActiveRecord::Base.connection - connection.stubs(:extensions).returns(["hstore", "uuid-ossp", "xml2"]) - output = perform_schema_dump - enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten - assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions + connection.stub(:extensions, ["hstore", "uuid-ossp", "xml2"]) do + output = perform_schema_dump + enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten + assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions + end - connection.stubs(:extensions).returns(["uuid-ossp", "xml2", "hstore"]) - output = perform_schema_dump - enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten - assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions + connection.stub(:extensions, ["uuid-ossp", "xml2", "hstore"]) do + output = perform_schema_dump + enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten + assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions + end end end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index f18f1ed981..544adc9b39 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -254,6 +254,11 @@ class RelationScopingTest < ActiveRecord::TestCase end end + def test_scoping_works_in_the_scope_block + expected = SpecialPostWithDefaultScope.unscoped.to_a + assert_equal expected, SpecialPostWithDefaultScope.unscoped_all + end + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do Post.first(10) diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 60c7cb1bb4..1f79f1a630 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -6,10 +6,18 @@ require "active_record/tasks/database_tasks" module ActiveRecord module DatabaseTasksSetupper def setup - @mysql_tasks, @postgresql_tasks, @sqlite_tasks = stub, stub, stub - ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks - ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks - ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks + @mysql_tasks, @postgresql_tasks, @sqlite_tasks = Array.new( + 3, + Class.new do + def create; end + def drop; end + def purge; end + def charset; end + def collation; end + def structure_dump(*); end + def structure_load(*); end + end.new + ) $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr @@ -18,6 +26,16 @@ module ActiveRecord def teardown $stdout, $stderr = @original_stdout, @original_stderr end + + def with_stubbed_new + ActiveRecord::Tasks::MySQLDatabaseTasks.stub(:new, @mysql_tasks) do + ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stub(:new, @postgresql_tasks) do + ActiveRecord::Tasks::SQLiteDatabaseTasks.stub(:new, @sqlite_tasks) do + yield + end + end + end + end end ADAPTERS_TASKS = { @@ -37,6 +55,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! ActiveRecord::Base.protected_environments = [current_env] + assert_raise(ActiveRecord::ProtectedEnvironmentError) do ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! end @@ -62,11 +81,12 @@ module ActiveRecord end def test_raises_an_error_if_no_migrations_have_been_made - ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false) - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) + ActiveRecord::InternalMetadata.stub(:table_exists?, false) do + ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end end end @@ -79,11 +99,11 @@ module ActiveRecord end instance = klazz.new - klazz.stubs(:new).returns instance - - assert_called_with(instance, :structure_dump, ["awesome-file.sql", nil]) do - ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz) - ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql") + klazz.stub(:new, instance) do + assert_called_with(instance, :structure_dump, ["awesome-file.sql", nil]) do + ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz) + ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql") + end end end @@ -99,8 +119,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_create") do - eval("@#{v}").expects(:create) - ActiveRecord::Tasks::DatabaseTasks.create "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :create) do + ActiveRecord::Tasks::DatabaseTasks.create "adapter" => k + end + end end end end @@ -120,59 +143,85 @@ module ActiveRecord def setup @configurations = { "development" => { "database" => "my-db" } } - ActiveRecord::Base.stubs(:configurations).returns(@configurations) # To refrain from connecting to a newly created empty DB in sqlite3_mem tests ActiveRecord::Base.connection_handler.stubs(:establish_connection) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_ignores_configurations_without_databases @configurations["development"].merge!("database" => nil) - assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end end end def test_ignores_remote_databases @configurations["development"].merge!("host" => "my.server.tld") - $stderr.stubs(:puts).returns(nil) - assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end end end def test_warning_for_remote_databases @configurations["development"].merge!("host" => "my.server.tld") - assert_called_with($stderr, :puts, ["This task only modifies local databases. my-db is on a remote host."]) do + with_stubbed_configurations do ActiveRecord::Tasks::DatabaseTasks.create_all + + assert_match "This task only modifies local databases. my-db is on a remote host.", + $stderr.string end end def test_creates_configurations_with_local_ip @configurations["development"].merge!("host" => "127.0.0.1") - assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end end end def test_creates_configurations_with_local_host @configurations["development"].merge!("host" => "localhost") - assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end end end def test_creates_configurations_with_blank_hosts @configurations["development"].merge!("host" => nil) - assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do - ActiveRecord::Tasks::DatabaseTasks.create_all + with_stubbed_configurations do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do + ActiveRecord::Tasks::DatabaseTasks.create_all + end end end + + private + + def with_stubbed_configurations + ActiveRecord::Base.stub(:configurations, @configurations) do + yield + end + end end class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase @@ -182,70 +231,94 @@ module ActiveRecord "test" => { "database" => "test-db" }, "production" => { "url" => "prod-db-url" } } - - ActiveRecord::Base.stubs(:configurations).returns(@configurations) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_creates_current_environment_database - assert_called_with( - ActiveRecord::Tasks::DatabaseTasks, - :create, - ["database" => "test-db"], - ) do - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("test") - ) + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + ["database" => "test-db"], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end end end def test_creates_current_environment_database_with_url - assert_called_with( - ActiveRecord::Tasks::DatabaseTasks, - :create, - ["url" => "prod-db-url"], - ) do - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("production") - ) + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + ["url" => "prod-db-url"], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("production") + ) + end end end def test_creates_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "test-db") - - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("development") - ) + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end end def test_creates_test_and_development_databases_when_rails_env_is_development old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "test-db") - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("development") - ) + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ], + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end ensure ENV["RAILS_ENV"] = old_env end def test_establishes_connection_for_the_given_environments - ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true + ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do + assert_called_with(ActiveRecord::Base, :establish_connection, [:development]) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end - ActiveRecord::Base.expects(:establish_connection).with(:development) + private - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("development") - ) - end + def with_stubbed_configurations_establish_connection + ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.stub(:establish_connection, nil) do + yield + end + end + end end class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase @@ -255,78 +328,108 @@ module ActiveRecord "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } } - - ActiveRecord::Base.stubs(:configurations).returns(@configurations) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_creates_current_environment_database - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "test-db") - - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "secondary-test-db") - - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("test") - ) + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end end def test_creates_current_environment_database_with_url - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("url" => "prod-db-url") - - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("url" => "secondary-prod-db-url") - - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("production") - ) + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["url" => "prod-db-url"], + ["url" => "secondary-prod-db-url"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end end def test_creates_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "secondary-dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "test-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "secondary-test-db") - - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("development") - ) + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end end def test_creates_test_and_development_databases_when_rails_env_is_development old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "secondary-dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "test-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "secondary-test-db") - - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("development") - ) + + with_stubbed_configurations_establish_connection do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :create, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end ensure ENV["RAILS_ENV"] = old_env end def test_establishes_connection_for_the_given_environments_config - ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true + ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [:development] + ) do + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end - ActiveRecord::Base.expects(:establish_connection).with(:development) + private - ActiveRecord::Tasks::DatabaseTasks.create_current( - ActiveSupport::StringInquirer.new("development") - ) - end + def with_stubbed_configurations_establish_connection + ActiveRecord::Base.stub(:configurations, @configurations) do + ActiveRecord::Base.stub(:establish_connection, nil) do + yield + end + end + end end class DatabaseTasksDropTest < ActiveRecord::TestCase @@ -334,8 +437,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_drop") do - eval("@#{v}").expects(:drop) - ActiveRecord::Tasks::DatabaseTasks.drop "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop "adapter" => k + end + end end end end @@ -344,59 +450,72 @@ module ActiveRecord def setup @configurations = { development: { "database" => "my-db" } } - ActiveRecord::Base.stubs(:configurations).returns(@configurations) + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_ignores_configurations_without_databases @configurations[:development].merge!("database" => nil) - assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end end end def test_ignores_remote_databases @configurations[:development].merge!("host" => "my.server.tld") - $stderr.stubs(:puts).returns(nil) - assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end end end def test_warning_for_remote_databases @configurations[:development].merge!("host" => "my.server.tld") - assert_called_with( - $stderr, - :puts, - ["This task only modifies local databases. my-db is on a remote host."], - ) do + ActiveRecord::Base.stub(:configurations, @configurations) do ActiveRecord::Tasks::DatabaseTasks.drop_all + + assert_match "This task only modifies local databases. my-db is on a remote host.", + $stderr.string end end def test_drops_configurations_with_local_ip @configurations[:development].merge!("host" => "127.0.0.1") - assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end end end def test_drops_configurations_with_local_host @configurations[:development].merge!("host" => "localhost") - assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end end end def test_drops_configurations_with_blank_hosts @configurations[:development].merge!("host" => nil) - assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do - ActiveRecord::Tasks::DatabaseTasks.drop_all + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do + ActiveRecord::Tasks::DatabaseTasks.drop_all + end end end end @@ -408,50 +527,65 @@ module ActiveRecord "test" => { "database" => "test-db" }, "production" => { "url" => "prod-db-url" } } - - ActiveRecord::Base.stubs(:configurations).returns(@configurations) end def test_drops_current_environment_database - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "test-db") - - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("test") - ) + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, + ["database" => "test-db"]) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end end def test_drops_current_environment_database_with_url - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("url" => "prod-db-url") - - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("production") - ) + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, + ["url" => "prod-db-url"]) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "test-db") - - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("development") - ) + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end end def test_drops_testand_development_databases_when_rails_env_is_development old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "test-db") - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("development") - ) + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end ensure ENV["RAILS_ENV"] = old_env end @@ -464,64 +598,81 @@ module ActiveRecord "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } } - - ActiveRecord::Base.stubs(:configurations).returns(@configurations) end def test_drops_current_environment_database - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "test-db") - - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "secondary-test-db") - - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("test") - ) + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + end end def test_drops_current_environment_database_with_url - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("url" => "prod-db-url") - - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("url" => "secondary-prod-db-url") - - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("production") - ) + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["url" => "prod-db-url"], + ["url" => "secondary-prod-db-url"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("production") + ) + end + end end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "secondary-dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "test-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "secondary-test-db") - - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("development") - ) + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end end def test_drops_testand_development_databases_when_rails_env_is_development old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "secondary-dev-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "test-db") - ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "secondary-test-db") - - ActiveRecord::Tasks::DatabaseTasks.drop_current( - ActiveSupport::StringInquirer.new("development") - ) + + ActiveRecord::Base.stub(:configurations, @configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :drop, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"], + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end ensure ENV["RAILS_ENV"] = old_env end @@ -649,10 +800,10 @@ module ActiveRecord end def test_migrate_raise_error_on_failed_check_target_version - ActiveRecord::Tasks::DatabaseTasks.stubs(:check_target_version).raises("foo") - - e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } - assert_equal "foo", e.message + ActiveRecord::Tasks::DatabaseTasks.stub(:check_target_version, -> { raise "foo" }) do + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_equal "foo", e.message + end end def test_migrate_clears_schema_cache_afterward @@ -667,8 +818,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_purge") do - eval("@#{v}").expects(:purge) - ActiveRecord::Tasks::DatabaseTasks.purge "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :purge) do + ActiveRecord::Tasks::DatabaseTasks.purge "adapter" => k + end + end end end end @@ -680,13 +834,16 @@ module ActiveRecord "test" => { "database" => "test-db" }, "production" => { "database" => "prod-db" } } - ActiveRecord::Base.stubs(:configurations).returns(configurations) - - ActiveRecord::Tasks::DatabaseTasks.expects(:purge). - with("database" => "prod-db") - - assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do - ActiveRecord::Tasks::DatabaseTasks.purge_current("production") + ActiveRecord::Base.stub(:configurations, configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "prod-db"] + ) do + assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do + ActiveRecord::Tasks::DatabaseTasks.purge_current("production") + end + end end end end @@ -694,12 +851,15 @@ module ActiveRecord class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase def test_purge_all_local_configurations configurations = { development: { "database" => "my-db" } } - ActiveRecord::Base.stubs(:configurations).returns(configurations) - - ActiveRecord::Tasks::DatabaseTasks.expects(:purge). - with("database" => "my-db") - - ActiveRecord::Tasks::DatabaseTasks.purge_all + ActiveRecord::Base.stub(:configurations, configurations) do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "my-db"] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end end end @@ -708,8 +868,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_charset") do - eval("@#{v}").expects(:charset) - ActiveRecord::Tasks::DatabaseTasks.charset "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :charset) do + ActiveRecord::Tasks::DatabaseTasks.charset "adapter" => k + end + end end end end @@ -719,8 +882,11 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_collation") do - eval("@#{v}").expects(:collation) - ActiveRecord::Tasks::DatabaseTasks.collation "adapter" => k + with_stubbed_new do + assert_called(eval("@#{v}"), :collation) do + ActiveRecord::Tasks::DatabaseTasks.collation "adapter" => k + end + end end end end @@ -832,8 +998,14 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_dump") do - eval("@#{v}").expects(:structure_dump).with("awesome-file.sql", nil) - ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql") + with_stubbed_new do + assert_called_with( + eval("@#{v}"), :structure_dump, + ["awesome-file.sql", nil] + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql") + end + end end end end @@ -843,8 +1015,15 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_load") do - eval("@#{v}").expects(:structure_load).with("awesome-file.sql", nil) - ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql") + with_stubbed_new do + assert_called_with( + eval("@#{v}"), + :structure_load, + ["awesome-file.sql", nil] + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql") + end + end end end end @@ -859,16 +1038,18 @@ module ActiveRecord class DatabaseTasksCheckSchemaFileDefaultsTest < ActiveRecord::TestCase def test_check_schema_file_defaults - ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns("/tmp") - assert_equal "/tmp/schema.rb", ActiveRecord::Tasks::DatabaseTasks.schema_file + ActiveRecord::Tasks::DatabaseTasks.stub(:db_dir, "/tmp") do + assert_equal "/tmp/schema.rb", ActiveRecord::Tasks::DatabaseTasks.schema_file + end end end class DatabaseTasksCheckSchemaFileSpecifiedFormatsTest < ActiveRecord::TestCase { ruby: "schema.rb", sql: "structure.sql" }.each_pair do |fmt, filename| define_method("test_check_schema_file_for_#{fmt}_format") do - ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns("/tmp") - assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt) + ActiveRecord::Tasks::DatabaseTasks.stub(:db_dir, "/tmp") do + assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt) + end end end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 6cddfaefeb..a86ddd3c16 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -7,15 +7,11 @@ if current_adapter?(:Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def create_database(*); end }.new @configuration = { "adapter" => "mysql2", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -25,59 +21,86 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_without_database - ActiveRecord::Base.expects(:establish_connection). - with("adapter" => "mysql2", "database" => nil) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.stubs(:establish_connection) + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.expects(:establish_connection). + with("adapter" => "mysql2", "database" => nil) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end end def test_creates_database_with_no_default_options - @connection.expects(:create_database). - with("my-app-db", {}) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + with_stubbed_connection_establish_connection do + assert_called_with(@connection, :create_database, ["my-app-db", {}]) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end end def test_creates_database_with_given_encoding - @connection.expects(:create_database). - with("my-app-db", charset: "latin1") - - ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("encoding" => "latin1") + with_stubbed_connection_establish_connection do + assert_called_with(@connection, :create_database, ["my-app-db", charset: "latin1"]) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("encoding" => "latin1") + end + end end def test_creates_database_with_given_collation - @connection.expects(:create_database). - with("my-app-db", collation: "latin1_swedish_ci") - - ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("collation" => "latin1_swedish_ci") + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :create_database, + ["my-app-db", collation: "latin1_swedish_ci"] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("collation" => "latin1_swedish_ci") + end + end end def test_establishes_connection_to_database - ActiveRecord::Base.expects(:establish_connection).with(@configuration) + ActiveRecord::Base.stubs(:establish_connection) - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end end def test_when_database_created_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.create @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal "Created database 'my-app-db'\n", $stdout.string + assert_equal "Created database 'my-app-db'\n", $stdout.string + end end def test_create_when_database_exists_outputs_info_to_stderr - ActiveRecord::Base.connection.stubs(:create_database).raises( - ActiveRecord::Tasks::DatabaseAlreadyExists - ) + with_stubbed_connection_establish_connection do + ActiveRecord::Base.connection.stub( + :create_database, + proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists } + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + + assert_equal "Database 'my-app-db' already exists\n", $stderr.string + end + end + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration + private - assert_equal "Database 'my-app-db' already exists\n", $stderr.string - end + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:establish_connection, nil) do + ActiveRecord::Base.stub(:connection, @connection) do + yield + end + end + end end class MysqlDBCreateWithInvalidPermissionsTest < ActiveRecord::TestCase def setup - @connection = stub("Connection", create_database: true) @error = Mysql2::Error.new("Invalid permissions") @configuration = { "adapter" => "mysql2", @@ -85,10 +108,6 @@ if current_adapter?(:Mysql2Adapter) "username" => "pat", "password" => "wossname" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).raises(@error) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -98,23 +117,21 @@ if current_adapter?(:Mysql2Adapter) end def test_raises_error - assert_raises(Mysql2::Error) do - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.stub(:establish_connection, -> * { raise @error }) do + assert_raises(Mysql2::Error, "Invalid permissions") do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end end end end class MySQLDBDropTest < ActiveRecord::TestCase def setup - @connection = stub(drop_database: true) + @connection = Class.new { def drop_database(name); end }.new @configuration = { "adapter" => "mysql2", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -124,91 +141,122 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_to_mysql_database - ActiveRecord::Base.expects(:establish_connection).with @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Base.expects(:establish_connection).with @configuration - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end def test_drops_database - @connection.expects(:drop_database).with("my-app-db") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + with_stubbed_connection_establish_connection do + assert_called_with(@connection, :drop_database, ["my-app-db"]) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end end def test_when_database_dropped_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration - assert_equal "Dropped database 'my-app-db'\n", $stdout.string + assert_equal "Dropped database 'my-app-db'\n", $stdout.string + end end + + private + + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:establish_connection, nil) do + ActiveRecord::Base.stub(:connection, @connection) do + yield + end + end + end end class MySQLPurgeTest < ActiveRecord::TestCase def setup - @connection = stub(recreate_database: true) + @connection = Class.new { def recreate_database(*); end }.new @configuration = { "adapter" => "mysql2", "database" => "test-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_establishes_connection_to_the_appropriate_database - ActiveRecord::Base.expects(:establish_connection).with(@configuration) + with_stubbed_connection_establish_connection do + ActiveRecord::Base.expects(:establish_connection).with(@configuration) - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end end def test_recreates_database_with_no_default_options - @connection.expects(:recreate_database). - with("test-db", {}) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection_establish_connection do + assert_called_with(@connection, :recreate_database, ["test-db", {}]) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end end def test_recreates_database_with_the_given_options - @connection.expects(:recreate_database). - with("test-db", charset: "latin", collation: "latin1_swedish_ci") - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge( - "encoding" => "latin", "collation" => "latin1_swedish_ci") + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :recreate_database, + ["test-db", charset: "latin", collation: "latin1_swedish_ci"] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge( + "encoding" => "latin", "collation" => "latin1_swedish_ci") + end + end end + + private + + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:establish_connection, nil) do + ActiveRecord::Base.stub(:connection, @connection) do + yield + end + end + end end class MysqlDBCharsetTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def charset; end }.new @configuration = { "adapter" => "mysql2", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_charset - @connection.expects(:charset) - ActiveRecord::Tasks::DatabaseTasks.charset @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called(@connection, :charset) do + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end end end class MysqlDBCollationTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def collation; end }.new @configuration = { "adapter" => "mysql2", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_collation - @connection.expects(:collation) - ActiveRecord::Tasks::DatabaseTasks.collation @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with(@connection, :collation) do + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end end end @@ -245,15 +293,15 @@ if current_adapter?(:Mysql2Adapter) def test_structure_dump_with_ignore_tables filename = "awesome-file.sql" - ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) - - assert_called_with( - Kernel, - :system, - ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db"], - returns: true - ) do - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + ActiveRecord::SchemaDumper.stub(:ignore_tables, ["foo", "bar"]) do + assert_called_with( + Kernel, + :system, + ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db"], + returns: true + ) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index a1a3700f07..289847e514 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -7,15 +7,11 @@ if current_adapter?(:PostgreSQLAdapter) module ActiveRecord class PostgreSQLDBCreateTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def create_database(*); end }.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -25,82 +21,119 @@ if current_adapter?(:PostgreSQLAdapter) end def test_establishes_connection_to_postgresql_database - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "postgresql", - "database" => "postgres", - "schema_search_path" => "public" - ) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Base.stubs(:establish_connection) + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.expects(:establish_connection).with( + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end end def test_creates_database_with_default_encoding - @connection.expects(:create_database). - with("my-app-db", @configuration.merge("encoding" => "utf8")) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :create_database, + ["my-app-db", @configuration.merge("encoding" => "utf8")] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end end def test_creates_database_with_given_encoding - @connection.expects(:create_database). - with("my-app-db", @configuration.merge("encoding" => "latin")) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration. - merge("encoding" => "latin") + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :create_database, + ["my-app-db", @configuration.merge("encoding" => "latin")] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge("encoding" => "latin") + end + end end def test_creates_database_with_given_collation_and_ctype - @connection.expects(:create_database). - with("my-app-db", @configuration.merge("encoding" => "utf8", "collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8")) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration. - merge("collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8") + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :create_database, + [ + "my-app-db", + @configuration.merge( + "encoding" => "utf8", + "collation" => "ja_JP.UTF8", + "ctype" => "ja_JP.UTF8" + ) + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge("collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8") + end + end end def test_establishes_connection_to_new_database - ActiveRecord::Base.expects(:establish_connection).with(@configuration) + ActiveRecord::Base.stubs(:establish_connection) + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.expects(:establish_connection).with(@configuration) - ActiveRecord::Tasks::DatabaseTasks.create @configuration + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end end def test_db_create_with_error_prints_message - ActiveRecord::Base.stubs(:establish_connection).raises(Exception) - - $stderr.stubs(:puts).returns(true) - $stderr.expects(:puts). - with("Couldn't create database for #{@configuration.inspect}") - - assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.stub(:establish_connection, -> * { raise Exception }) do + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } + assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + end + end end def test_when_database_created_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.create @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal "Created database 'my-app-db'\n", $stdout.string + assert_equal "Created database 'my-app-db'\n", $stdout.string + end end def test_create_when_database_exists_outputs_info_to_stderr - ActiveRecord::Base.connection.stubs(:create_database).raises( - ActiveRecord::Tasks::DatabaseAlreadyExists - ) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Base.connection.stub( + :create_database, + proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists } + ) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal "Database 'my-app-db' already exists\n", $stderr.string + assert_equal "Database 'my-app-db' already exists\n", $stderr.string + end + end end + + private + + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.stub(:establish_connection, nil) do + yield + end + end + end end class PostgreSQLDBDropTest < ActiveRecord::TestCase def setup - @connection = stub(drop_database: true) + @connection = Class.new { def drop_database(*); end }.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -110,125 +143,171 @@ if current_adapter?(:PostgreSQLAdapter) end def test_establishes_connection_to_postgresql_database - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "postgresql", - "database" => "postgres", - "schema_search_path" => "public" - ) - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.expects(:establish_connection).with( + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ) + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end def test_drops_database - @connection.expects(:drop_database).with("my-app-db") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + with_stubbed_connection_establish_connection do + assert_called_with( + @connection, + :drop_database, + ["my-app-db"] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end end def test_when_database_dropped_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + with_stubbed_connection_establish_connection do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration - assert_equal "Dropped database 'my-app-db'\n", $stdout.string + assert_equal "Dropped database 'my-app-db'\n", $stdout.string + end end + + private + + def with_stubbed_connection_establish_connection + ActiveRecord::Base.stub(:connection, @connection) do + ActiveRecord::Base.stub(:establish_connection, nil) do + yield + end + end + end end class PostgreSQLPurgeTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true, drop_database: true) + @connection = Class.new do + def create_database(*); end + def drop_database(*); end + end.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:clear_active_connections!).returns(true) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_clears_active_connections - ActiveRecord::Base.expects(:clear_active_connections!) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection do + ActiveRecord::Base.stub(:establish_connection, nil) do + assert_called(ActiveRecord::Base, :clear_active_connections!) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end + end end def test_establishes_connection_to_postgresql_database - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "postgresql", - "database" => "postgres", - "schema_search_path" => "public" - ) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + ActiveRecord::Base.stubs(:establish_connection) + with_stubbed_connection do + ActiveRecord::Base.expects(:establish_connection).with( + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end end def test_drops_database - @connection.expects(:drop_database).with("my-app-db") - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection do + ActiveRecord::Base.stub(:establish_connection, nil) do + assert_called_with(@connection, :drop_database, ["my-app-db"]) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end + end end def test_creates_database - @connection.expects(:create_database). - with("my-app-db", @configuration.merge("encoding" => "utf8")) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + with_stubbed_connection do + ActiveRecord::Base.stub(:establish_connection, nil) do + assert_called_with( + @connection, + :create_database, + ["my-app-db", @configuration.merge("encoding" => "utf8")] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end + end end def test_establishes_connection - ActiveRecord::Base.expects(:establish_connection).with(@configuration) + ActiveRecord::Base.stubs(:establish_connection) + with_stubbed_connection do + ActiveRecord::Base.expects(:establish_connection).with(@configuration) - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end end + + private + + def with_stubbed_connection + ActiveRecord::Base.stub(:connection, @connection) do + yield + end + end end class PostgreSQLDBCharsetTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new do + def create_database(*); end + def encoding; end + end.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_charset - @connection.expects(:encoding) - ActiveRecord::Tasks::DatabaseTasks.charset @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called(@connection, :encoding) do + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end end end class PostgreSQLDBCollationTest < ActiveRecord::TestCase def setup - @connection = stub(create_database: true) + @connection = Class.new { def collation; end }.new @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_collation - @connection.expects(:collation) - ActiveRecord::Tasks::DatabaseTasks.collation @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called(@connection, :collation) do + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end end end class PostgreSQLStructureDumpTest < ActiveRecord::TestCase def setup - @connection = stub(schema_search_path: nil, structure_dump: true) @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } @filename = "/tmp/awesome-file.sql" FileUtils.touch(@filename) - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def teardown @@ -247,12 +326,12 @@ if current_adapter?(:PostgreSQLAdapter) end def test_structure_dump_header_comments_removed - Kernel.stubs(:system).returns(true) - File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n") - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + Kernel.stub(:system, true) do + File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n") + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) - assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2) + assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2) + end end def test_structure_dump_with_extra_flags @@ -358,14 +437,10 @@ if current_adapter?(:PostgreSQLAdapter) class PostgreSQLStructureLoadTest < ActiveRecord::TestCase def setup - @connection = stub @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_structure_load diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index d368a7a6ee..7eb062b456 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -9,16 +9,10 @@ if current_adapter?(:SQLite3Adapter) class SqliteDBCreateTest < ActiveRecord::TestCase def setup @database = "db_create.sqlite3" - @connection = stub :connection @configuration = { "adapter" => "sqlite3", "database" => @database } - - File.stubs(:exist?).returns(false) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr end @@ -28,63 +22,62 @@ if current_adapter?(:SQLite3Adapter) end def test_db_checks_database_exists - File.expects(:exist?).with(@database).returns(false) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + ActiveRecord::Base.stub(:establish_connection, nil) do + assert_called_with(File, :exist?, [@database], returns: false) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + end + end end def test_when_db_created_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + ActiveRecord::Base.stub(:establish_connection, nil) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" - assert_equal "Created database '#{@database}'\n", $stdout.string + assert_equal "Created database '#{@database}'\n", $stdout.string + end end def test_db_create_when_file_exists - File.stubs(:exist?).returns(true) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + File.stub(:exist?, true) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" - assert_equal "Database '#{@database}' already exists\n", $stderr.string + assert_equal "Database '#{@database}' already exists\n", $stderr.string + end end def test_db_create_with_file_does_nothing - File.stubs(:exist?).returns(true) - $stderr.stubs(:puts).returns(nil) + File.stub(:exist?, true) do + ActiveRecord::Base.expects(:establish_connection).never - ActiveRecord::Base.expects(:establish_connection).never - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + end end def test_db_create_establishes_a_connection - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + assert_called_with(ActiveRecord::Base, :establish_connection, [@configuration]) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + end end def test_db_create_with_error_prints_message - ActiveRecord::Base.stubs(:establish_connection).raises(Exception) - - $stderr.stubs(:puts).returns(true) - $stderr.expects(:puts). - with("Couldn't create database for #{@configuration.inspect}") - - assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" } + ActiveRecord::Base.stub(:establish_connection, proc { raise Exception }) do + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" } + assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + end end end class SqliteDBDropTest < ActiveRecord::TestCase def setup @database = "db_create.sqlite3" - @path = stub(to_s: "/absolute/path", absolute?: true) @configuration = { "adapter" => "sqlite3", "database" => @database } - - Pathname.stubs(:new).returns(@path) - File.stubs(:join).returns("/former/relative/path") - FileUtils.stubs(:rm).returns(true) + @path = Class.new do + def to_s; "/absolute/path" end + def absolute?; true end + end.new $stdout, @original_stdout = StringIO.new, $stdout $stderr, @original_stderr = StringIO.new, $stderr @@ -95,77 +88,76 @@ if current_adapter?(:SQLite3Adapter) end def test_creates_path_from_database - Pathname.expects(:new).with(@database).returns(@path) - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + assert_called_with(Pathname, :new, [@database], returns: @path) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + end end def test_removes_file_with_absolute_path - File.stubs(:exist?).returns(true) - @path.stubs(:absolute?).returns(true) - - FileUtils.expects(:rm).with("/absolute/path") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + Pathname.stub(:new, @path) do + assert_called_with(FileUtils, :rm, ["/absolute/path"]) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + end + end end def test_generates_absolute_path_with_given_root - @path.stubs(:absolute?).returns(false) - - File.expects(:join).with("/rails/root", @path). - returns("/former/relative/path") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + Pathname.stub(:new, @path) do + @path.stub(:absolute?, false) do + assert_called_with(File, :join, ["/rails/root", @path], + returns: "/former/relative/path" + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + end + end + end end def test_removes_file_with_relative_path - File.stubs(:exist?).returns(true) - @path.stubs(:absolute?).returns(false) - - FileUtils.expects(:rm).with("/former/relative/path") - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + File.stub(:join, "/former/relative/path") do + @path.stub(:absolute?, false) do + assert_called_with(FileUtils, :rm, ["/former/relative/path"]) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + end + end + end end def test_when_db_dropped_successfully_outputs_info_to_stdout - ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" + FileUtils.stub(:rm, nil) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" - assert_equal "Dropped database '#{@database}'\n", $stdout.string + assert_equal "Dropped database '#{@database}'\n", $stdout.string + end end end class SqliteDBCharsetTest < ActiveRecord::TestCase def setup @database = "db_create.sqlite3" - @connection = stub :connection + @connection = Class.new { def encoding; end }.new @configuration = { "adapter" => "sqlite3", "database" => @database } - - File.stubs(:exist?).returns(false) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_charset - @connection.expects(:encoding) - ActiveRecord::Tasks::DatabaseTasks.charset @configuration, "/rails/root" + ActiveRecord::Base.stub(:connection, @connection) do + assert_called(@connection, :encoding) do + ActiveRecord::Tasks::DatabaseTasks.charset @configuration, "/rails/root" + end + end end end class SqliteDBCollationTest < ActiveRecord::TestCase def setup @database = "db_create.sqlite3" - @connection = stub :connection @configuration = { "adapter" => "sqlite3", "database" => @database } - - File.stubs(:exist?).returns(false) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_db_retrieves_collation diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 5b685ca564..46463ac414 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -573,7 +573,6 @@ class TransactionTest < ActiveRecord::TestCase assert_called(Topic.connection, :begin_db_transaction) do Topic.connection.stub(:commit_db_transaction, -> { raise("OH NOES") }) do assert_called(Topic.connection, :rollback_db_transaction) do - e = assert_raise RuntimeError do Topic.transaction do # do nothing diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 54eb5e6783..640cdb33b4 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -253,6 +253,7 @@ class SpecialPostWithDefaultScope < ActiveRecord::Base self.inheritance_column = :disabled self.table_name = "posts" default_scope { where(id: [1, 5, 6]) } + scope :unscoped_all, -> { unscoped { all } } end class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index e634e9e6b1..8371ba9528 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true ActiveRecord::Schema.define do - - if ActiveRecord::Base.connection.version >= "5.6.0" + if subsecond_precision_supported? create_table :datetime_defaults, force: true do |t| t.datetime :modified_datetime, default: -> { "CURRENT_TIMESTAMP" } + t.datetime :precise_datetime, precision: 6, default: -> { "CURRENT_TIMESTAMP(6)" } end - end - create_table :timestamp_defaults, force: true do |t| - t.timestamp :nullable_timestamp - t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" } + create_table :timestamp_defaults, force: true do |t| + t.timestamp :nullable_timestamp + t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" } + t.timestamp :precise_timestamp, precision: 6, default: -> { "CURRENT_TIMESTAMP(6)" } + end end create_table :binary_fields, force: true do |t| diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb index e236571caa..bc1e45ca80 100644 --- a/activerecord/test/schema/oracle_specific_schema.rb +++ b/activerecord/test/schema/oracle_specific_schema.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true ActiveRecord::Schema.define do - execute "drop table test_oracle_defaults" rescue nil execute "drop sequence test_oracle_defaults_seq" rescue nil execute "drop sequence companies_nonstd_seq" rescue nil @@ -38,5 +37,4 @@ create sequence test_oracle_defaults_seq minvalue 10000 ) SQL execute "create sequence defaults_seq minvalue 10000" - end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index f15178d695..975824ed51 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true ActiveRecord::Schema.define do - enable_extension!("uuid-ossp", ActiveRecord::Base.connection) enable_extension!("pgcrypto", ActiveRecord::Base.connection) if ActiveRecord::Base.connection.supports_pgcrypto_uuid? diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 4aa551781b..0e9c2b5619 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,17 @@ +* Uploaded files assigned to a record are persisted to storage when the record + is saved instead of immediately. + + In Rails 5.2, the following causes an uploaded file in `params[:avatar]` to + be stored: + + ```ruby + @user.avatar = params[:avatar] + ``` + + In Rails 6, the uploaded file is stored when `@user` is successfully saved. + + *George Claghorn* + * Add the ability to reflect on defined attachments using the existing ActiveRecord reflection mechanism. diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index 63918eb6f4..75cc11d6ff 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -5,30 +5,20 @@ # Always go through the BlobsController, or your own authenticated controller, rather than directly # to the service url. class ActiveStorage::DiskController < ActiveStorage::BaseController - include ActionController::Live - skip_forgery_protection def show if key = decode_verified_key - response.headers["Content-Type"] = params[:content_type] || DEFAULT_SEND_FILE_TYPE - response.headers["Content-Disposition"] = params[:disposition] || DEFAULT_SEND_FILE_DISPOSITION - - disk_service.download key do |chunk| - response.stream.write chunk - end + serve_file disk_service.path_for(key), content_type: params[:content_type], disposition: params[:disposition] else head :not_found end - ensure - response.stream.close end def update if token = decode_verified_token if acceptable_content?(token) disk_service.upload token[:key], request.body, checksum: token[:checksum] - head :no_content else head :unprocessable_entity end @@ -37,8 +27,6 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController end rescue ActiveStorage::IntegrityError head :unprocessable_entity - ensure - response.stream.close end private @@ -51,6 +39,20 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key) end + def serve_file(path, content_type:, disposition:) + Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)| + self.status = status + self.response_body = body + + headers.each do |name, value| + response.headers[name] = value + end + + response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE + response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION + end + end + def decode_verified_token ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token) diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb index 98874d2250..b021b5f2d0 100644 --- a/activestorage/app/jobs/active_storage/purge_job.rb +++ b/activestorage/app/jobs/active_storage/purge_job.rb @@ -2,8 +2,7 @@ # Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later. class ActiveStorage::PurgeJob < ActiveStorage::BaseJob - # FIXME: Limit this to a custom ActiveStorage error - retry_on StandardError + discard_on ActiveRecord::RecordNotFound, ActiveRecord::InvalidForeignKey def perform(blob) blob.purge diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index c59877a9a5..1c4dc25094 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -15,17 +15,18 @@ class ActiveStorage::Attachment < ActiveRecord::Base delegate_missing_to :blob after_create_commit :analyze_blob_later, :identify_blob + after_destroy_commit :purge_dependent_blob_later - # Synchronously purges the blob (deletes it from the configured service) and destroys the attachment. + # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge]. def purge + delete blob.purge - destroy end - # Destroys the attachment and asynchronously purges the blob (deletes it from the configured service). + # Deletes the attachment and {enqueues a background job}[rdoc-ref:ActiveStorage::Blob#purge_later] to purge the blob. def purge_later + delete blob.purge_later - destroy end private @@ -36,4 +37,13 @@ class ActiveStorage::Attachment < ActiveRecord::Base def analyze_blob_later blob.analyze_later unless blob.analyzed? end + + def purge_dependent_blob_later + blob.purge_later if dependent == :purge_later + end + + + def dependent + record.attachment_reflections[name]&.options[:dependent] + end end diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 73029f21a1..bf87598a66 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -48,15 +48,17 @@ class ActiveStorage::Blob < ActiveRecord::Base # Returns a new, unsaved blob instance after the +io+ has been uploaded to the service. # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference. def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true) - new.tap do |blob| - blob.filename = filename - blob.content_type = content_type - blob.metadata = metadata - + new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob| blob.upload(io, identify: identify) end end + def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc: + new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob| + blob.unfurl(io, identify: identify) + end + end + # Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built, # then the +io+ is uploaded, then the blob is saved. This is done this way to avoid uploading (which may take # time), while having an open database transaction. @@ -152,12 +154,19 @@ class ActiveStorage::Blob < ActiveRecord::Base # Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+ # and +create_after_upload!+. def upload(io, identify: true) + unfurl io, identify: identify + upload_without_unfurling io + end + + def unfurl(io, identify: true) #:nodoc: self.checksum = compute_checksum_in_chunks(io) self.content_type = extract_content_type(io) if content_type.nil? || identify self.byte_size = io.size self.identified = true + end - service.upload(key, io, checksum: checksum) + def upload_without_unfurling(io) #:nodoc: + service.upload key, io, checksum: checksum end # Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned. @@ -184,8 +193,8 @@ class ActiveStorage::Blob < ActiveRecord::Base end - # Deletes the file on the service that's associated with this blob. This should only be done if the blob is going to be - # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+ + # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be + # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later # methods in most circumstances. def delete service.delete(key) @@ -194,14 +203,14 @@ class ActiveStorage::Blob < ActiveRecord::Base # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted # blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may - # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use +#purge_later+ instead. + # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead. def purge - delete destroy + delete end - # Enqueues an ActiveStorage::PurgeJob job that'll call +purge+. This is the recommended way to purge blobs when the call - # needs to be made from a transaction, a callback, or any other real-time scenario. + # Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction, + # an Active Record callback, or in any other real-time scenario. def purge_later ActiveStorage::PurgeJob.perform_later(self) end diff --git a/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb index 9e31e3966a..cfaf01cd5e 100644 --- a/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb +++ b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb @@ -20,6 +20,7 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2] t.datetime :created_at, null: false t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id end end end diff --git a/activestorage/lib/active_storage/attached.rb b/activestorage/lib/active_storage/attached.rb index c08fd56652..b540f85fbe 100644 --- a/activestorage/lib/active_storage/attached.rb +++ b/activestorage/lib/active_storage/attached.rb @@ -1,40 +1,25 @@ # frozen_string_literal: true -require "action_dispatch" -require "action_dispatch/http/upload" require "active_support/core_ext/module/delegation" module ActiveStorage # Abstract base class for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many # classes that both provide proxy access to the blob association for a record. class Attached - attr_reader :name, :record, :dependent + attr_reader :name, :record - def initialize(name, record, dependent:) - @name, @record, @dependent = name, record, dependent + def initialize(name, record) + @name, @record = name, record end private - def create_blob_from(attachable) - case attachable - when ActiveStorage::Blob - attachable - when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile - ActiveStorage::Blob.create_after_upload! \ - io: attachable.open, - filename: attachable.original_filename, - content_type: attachable.content_type - when Hash - ActiveStorage::Blob.create_after_upload!(attachable) - when String - ActiveStorage::Blob.find_signed(attachable) - else - nil - end + def change + record.attachment_changes[name] end end end +require "active_storage/attached/model" require "active_storage/attached/one" require "active_storage/attached/many" -require "active_storage/attached/macros" +require "active_storage/attached/changes" diff --git a/activestorage/lib/active_storage/attached/changes.rb b/activestorage/lib/active_storage/attached/changes.rb new file mode 100644 index 0000000000..1db3906a63 --- /dev/null +++ b/activestorage/lib/active_storage/attached/changes.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module ActiveStorage + module Attached::Changes #:nodoc: + extend ActiveSupport::Autoload + + eager_autoload do + autoload :CreateOne + autoload :CreateMany + autoload :CreateOneOfMany + + autoload :DeleteOne + autoload :DeleteMany + end + end +end diff --git a/activestorage/lib/active_storage/attached/changes/create_many.rb b/activestorage/lib/active_storage/attached/changes/create_many.rb new file mode 100644 index 0000000000..a7a8553e0f --- /dev/null +++ b/activestorage/lib/active_storage/attached/changes/create_many.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module ActiveStorage + class Attached::Changes::CreateMany #:nodoc: + attr_reader :name, :record, :attachables + + def initialize(name, record, attachables) + @name, @record, @attachables = name, record, Array(attachables) + end + + def attachments + @attachments ||= subchanges.collect(&:attachment) + end + + def blobs + @blobs ||= subchanges.collect(&:blob) + end + + def upload + subchanges.each(&:upload) + end + + def save + assign_associated_attachments + reset_associated_blobs + end + + private + def subchanges + @subchanges ||= attachables.collect { |attachable| build_subchange_from(attachable) } + end + + def build_subchange_from(attachable) + ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable) + end + + + def assign_associated_attachments + record.public_send("#{name}_attachments=", attachments) + end + + def reset_associated_blobs + record.public_send("#{name}_blobs").reset + end + end +end diff --git a/activestorage/lib/active_storage/attached/changes/create_one.rb b/activestorage/lib/active_storage/attached/changes/create_one.rb new file mode 100644 index 0000000000..5812fd2b08 --- /dev/null +++ b/activestorage/lib/active_storage/attached/changes/create_one.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require "action_dispatch" +require "action_dispatch/http/upload" + +module ActiveStorage + class Attached::Changes::CreateOne #:nodoc: + attr_reader :name, :record, :attachable + + def initialize(name, record, attachable) + @name, @record, @attachable = name, record, attachable + end + + def attachment + @attachment ||= find_or_build_attachment + end + + def blob + @blob ||= find_or_build_blob + end + + def upload + case attachable + when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile + blob.upload_without_unfurling(attachable.open) + when Hash + blob.upload_without_unfurling(attachable.fetch(:io)) + end + end + + def save + record.public_send("#{name}_attachment=", attachment) + end + + private + def find_or_build_attachment + find_attachment || build_attachment + end + + def find_attachment + if record.public_send("#{name}_blob") == blob + record.public_send("#{name}_attachment") + end + end + + def build_attachment + ActiveStorage::Attachment.new(record: record, name: name, blob: blob) + end + + def find_or_build_blob + case attachable + when ActiveStorage::Blob + attachable + when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile + ActiveStorage::Blob.build_after_unfurling \ + io: attachable.open, + filename: attachable.original_filename, + content_type: attachable.content_type + when Hash + ActiveStorage::Blob.build_after_unfurling(attachable) + when String + ActiveStorage::Blob.find_signed(attachable) + else + raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}" + end + end + end +end diff --git a/activestorage/lib/active_storage/attached/changes/create_one_of_many.rb b/activestorage/lib/active_storage/attached/changes/create_one_of_many.rb new file mode 100644 index 0000000000..7268e87316 --- /dev/null +++ b/activestorage/lib/active_storage/attached/changes/create_one_of_many.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module ActiveStorage + class Attached::Changes::CreateOneOfMany < Attached::Changes::CreateOne #:nodoc: + private + def find_attachment + record.public_send("#{name}_attachments").detect { |attachment| attachment.blob_id == blob.id } + end + end +end diff --git a/activestorage/lib/active_storage/attached/changes/delete_many.rb b/activestorage/lib/active_storage/attached/changes/delete_many.rb new file mode 100644 index 0000000000..6cbd1158dc --- /dev/null +++ b/activestorage/lib/active_storage/attached/changes/delete_many.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ActiveStorage + class Attached::Changes::DeleteMany #:nodoc: + attr_reader :name, :record + + def initialize(name, record) + @name, @record = name, record + end + + def attachments + ActiveStorage::Attachment.none + end + + def blobs + ActiveStorage::Blob.none + end + + def save + record.public_send("#{name}_attachments=", []) + end + end +end diff --git a/activestorage/lib/active_storage/attached/changes/delete_one.rb b/activestorage/lib/active_storage/attached/changes/delete_one.rb new file mode 100644 index 0000000000..2f7d356613 --- /dev/null +++ b/activestorage/lib/active_storage/attached/changes/delete_one.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ActiveStorage + class Attached::Changes::DeleteOne #:nodoc: + attr_reader :name, :record + + def initialize(name, record) + @name, @record = name, record + end + + def attachment + nil + end + + def save + record.public_send("#{name}_attachment=", nil) + end + end +end diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb deleted file mode 100644 index 6ad9fc43d7..0000000000 --- a/activestorage/lib/active_storage/attached/macros.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true - -module ActiveStorage - # Provides the class-level DSL for declaring that an Active Record model has attached blobs. - module Attached::Macros - # Specifies the relation between a single attachment and the model. - # - # class User < ActiveRecord::Base - # has_one_attached :avatar - # end - # - # There is no column defined on the model side, Active Storage takes - # care of the mapping between your records and the attachment. - # - # To avoid N+1 queries, you can include the attached blobs in your query like so: - # - # User.with_attached_avatar - # - # Under the covers, this relationship is implemented as a +has_one+ association to a - # ActiveStorage::Attachment record and a +has_one-through+ association to a - # ActiveStorage::Blob record. These associations are available as +avatar_attachment+ - # and +avatar_blob+. But you shouldn't need to work with these associations directly in - # most circumstances. - # - # The system has been designed to having you go through the ActiveStorage::Attached::One - # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+. - # - # If the +:dependent+ option isn't set, the attachment will be purged - # (i.e. destroyed) whenever the record is destroyed. - def has_one_attached(name, dependent: :purge_later) - generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name} - @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"}) - end - - def #{name}=(attachable) - #{name}.attach(attachable) - end - CODE - - has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: false - has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob - - scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) } - - if dependent == :purge_later - after_destroy_commit { public_send(name).purge_later } - else - before_destroy { public_send(name).detach } - end - - ActiveRecord::Reflection.add_attachment_reflection( - self, - name, - ActiveRecord::Reflection.create(:has_one_attached, name, nil, { dependent: dependent }, self) - ) - end - - # Specifies the relation between multiple attachments and the model. - # - # class Gallery < ActiveRecord::Base - # has_many_attached :photos - # end - # - # There are no columns defined on the model side, Active Storage takes - # care of the mapping between your records and the attachments. - # - # To avoid N+1 queries, you can include the attached blobs in your query like so: - # - # Gallery.where(user: Current.user).with_attached_photos - # - # Under the covers, this relationship is implemented as a +has_many+ association to a - # ActiveStorage::Attachment record and a +has_many-through+ association to a - # ActiveStorage::Blob record. These associations are available as +photos_attachments+ - # and +photos_blobs+. But you shouldn't need to work with these associations directly in - # most circumstances. - # - # The system has been designed to having you go through the ActiveStorage::Attached::Many - # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+. - # - # If the +:dependent+ option isn't set, all the attachments will be purged - # (i.e. destroyed) whenever the record is destroyed. - def has_many_attached(name, dependent: :purge_later) - generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name} - @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"}) - end - - def #{name}=(attachables) - #{name}.attach(attachables) - end - CODE - - has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: false do - def purge - each(&:purge) - reset - end - - def purge_later - each(&:purge_later) - reset - end - end - has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob - - scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) } - - if dependent == :purge_later - after_destroy_commit { public_send(name).purge_later } - else - before_destroy { public_send(name).detach } - end - - ActiveRecord::Reflection.add_attachment_reflection( - self, - name, - ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self) - ) - end - end -end diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb index d61acb6fad..25f88284df 100644 --- a/activestorage/lib/active_storage/attached/many.rb +++ b/activestorage/lib/active_storage/attached/many.rb @@ -9,22 +9,29 @@ module ActiveStorage # # All methods called on this proxy object that aren't listed here will automatically be delegated to +attachments+. def attachments - record.public_send("#{name}_attachments") + change.present? ? change.attachments : record.public_send("#{name}_attachments") end - # Associates one or several attachments with the current record, saving them to the database. + # Returns all attached blobs. + def blobs + change.present? ? change.blobs : record.public_send("#{name}_blobs") + end + + # Attaches one or more +attachables+ to the record. + # + # If the record is persisted and unchanged, the attachments are saved to + # the database immediately. Otherwise, they'll be saved to the DB when the + # record is next saved. # # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload # document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg") # document.images.attach([ first_blob, second_blob ]) def attach(*attachables) - attachables.flatten.collect do |attachable| - if record.new_record? - attachments.build(record: record, blob: create_blob_from(attachable)) - else - attachments.create!(record: record, blob: create_blob_from(attachable)) - end + if record.persisted? && !record.changed? + record.update(name => blobs + attachables.flatten) + else + record.public_send("#{name}=", blobs + attachables.flatten) end end @@ -41,7 +48,7 @@ module ActiveStorage # Deletes associated attachments without purging them, leaving their respective blobs in place. def detach - attachments.destroy_all if attached? + attachments.delete_all if attached? end ## @@ -50,7 +57,6 @@ module ActiveStorage # Directly purges each associated attachment (i.e. destroys the blobs and # attachments and deletes the files on the service). - ## # :method: purge_later # diff --git a/activestorage/lib/active_storage/attached/model.rb b/activestorage/lib/active_storage/attached/model.rb new file mode 100644 index 0000000000..ae7f0685f2 --- /dev/null +++ b/activestorage/lib/active_storage/attached/model.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +module ActiveStorage + # Provides the class-level DSL for declaring an Active Record model's attachments. + module Attached::Model + extend ActiveSupport::Concern + + class_methods do + # Specifies the relation between a single attachment and the model. + # + # class User < ActiveRecord::Base + # has_one_attached :avatar + # end + # + # There is no column defined on the model side, Active Storage takes + # care of the mapping between your records and the attachment. + # + # To avoid N+1 queries, you can include the attached blobs in your query like so: + # + # User.with_attached_avatar + # + # Under the covers, this relationship is implemented as a +has_one+ association to a + # ActiveStorage::Attachment record and a +has_one-through+ association to a + # ActiveStorage::Blob record. These associations are available as +avatar_attachment+ + # and +avatar_blob+. But you shouldn't need to work with these associations directly in + # most circumstances. + # + # The system has been designed to having you go through the ActiveStorage::Attached::One + # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+. + # + # If the +:dependent+ option isn't set, the attachment will be purged + # (i.e. destroyed) whenever the record is destroyed. + def has_one_attached(name, dependent: :purge_later) + generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name} + @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self) + end + + def #{name}=(attachable) + attachment_changes["#{name}"] = + if attachable.nil? + ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self) + else + ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable) + end + end + CODE + + has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy + has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob + + scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) } + + after_save { attachment_changes[name.to_s]&.save } + + after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) } + + ActiveRecord::Reflection.add_attachment_reflection( + self, + name, + ActiveRecord::Reflection.create(:has_one_attached, name, nil, { dependent: dependent }, self) + ) + end + + # Specifies the relation between multiple attachments and the model. + # + # class Gallery < ActiveRecord::Base + # has_many_attached :photos + # end + # + # There are no columns defined on the model side, Active Storage takes + # care of the mapping between your records and the attachments. + # + # To avoid N+1 queries, you can include the attached blobs in your query like so: + # + # Gallery.where(user: Current.user).with_attached_photos + # + # Under the covers, this relationship is implemented as a +has_many+ association to a + # ActiveStorage::Attachment record and a +has_many-through+ association to a + # ActiveStorage::Blob record. These associations are available as +photos_attachments+ + # and +photos_blobs+. But you shouldn't need to work with these associations directly in + # most circumstances. + # + # The system has been designed to having you go through the ActiveStorage::Attached::Many + # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+. + # + # If the +:dependent+ option isn't set, all the attachments will be purged + # (i.e. destroyed) whenever the record is destroyed. + def has_many_attached(name, dependent: :purge_later) + generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name} + @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self) + end + + def #{name}=(attachables) + attachment_changes["#{name}"] = + if attachables.nil? || Array(attachables).none? + ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self) + else + ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables) + end + end + CODE + + has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: :destroy do + def purge + each(&:purge) + reset + end + + def purge_later + each(&:purge_later) + reset + end + end + has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob + + scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) } + + after_save { attachment_changes[name.to_s]&.save } + + after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) } + + ActiveRecord::Reflection.add_attachment_reflection( + self, + name, + ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self) + ) + end + end + + def attachment_changes #:nodoc: + @attachment_changes ||= {} + end + + def reload(*) #:nodoc: + super.tap { @attachment_changes = nil } + end + end +end diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb index f992cb5f84..c039226fcd 100644 --- a/activestorage/lib/active_storage/attached/one.rb +++ b/activestorage/lib/active_storage/attached/one.rb @@ -10,30 +10,28 @@ module ActiveStorage # You don't have to call this method to access the attachment's methods as # they are all available at the model level. def attachment - record.public_send("#{name}_attachment") + change.present? ? change.attachment : record.public_send("#{name}_attachment") end def blank? - attachment.blank? + !attached? end - # Associates a given attachment with the current record, saving it to the database. + # Attaches an +attachable+ to the record. + # + # If the record is persisted and unchanged, the attachment is saved to + # the database immediately. Otherwise, it'll be saved to the DB when the + # record is next saved. # # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg") # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object def attach(attachable) - blob_was = blob if attached? - blob = create_blob_from(attachable) - - unless blob == blob_was - transaction do - detach - write_attachment build_attachment(blob: blob) - end - - blob_was.purge_later if blob_was && dependent == :purge_later + if record.persisted? && !record.changed? + record.update(name => attachable) + else + record.public_send("#{name}=", attachable) end end @@ -51,7 +49,7 @@ module ActiveStorage # Deletes the attachment without purging it, leaving its blob in place. def detach if attached? - attachment.destroy + attachment.delete write_attachment nil end end @@ -69,16 +67,11 @@ module ActiveStorage def purge_later if attached? attachment.purge_later + write_attachment nil end end private - delegate :transaction, to: :record - - def build_attachment(blob:) - ActiveStorage::Attachment.new(record: record, name: name, blob: blob) - end - def write_attachment(attachment) record.public_send("#{name}_attachment=", attachment) end diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index 93c5c55b95..9d6a27eabe 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -62,7 +62,7 @@ module ActiveStorage require "active_storage/attached" ActiveSupport.on_load(:active_record) do - extend ActiveStorage::Attached::Macros + include ActiveStorage::Attached::Model end end diff --git a/activestorage/lib/active_storage/reflection.rb b/activestorage/lib/active_storage/reflection.rb index 04a1b20882..ce248c88b5 100644 --- a/activestorage/lib/active_storage/reflection.rb +++ b/activestorage/lib/active_storage/reflection.rb @@ -19,8 +19,8 @@ module ActiveStorage end module ReflectionExtension # :nodoc: - def add_attachment_reflection(ar, name, reflection) - ar.attachment_reflections.merge!(name.to_s => reflection) + def add_attachment_reflection(model, name, reflection) + model.attachment_reflections = model.attachment_reflections.merge(name.to_s => reflection) end private diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index b1b6f1ddcf..9f304b7e01 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -117,11 +117,11 @@ module ActiveStorage { "Content-Type" => content_type } end - private - def path_for(key) - File.join root, folder_for(key), key - end + def path_for(key) #:nodoc: + File.join root, folder_for(key), key + end + private def folder_for(key) [ key[0..1], key[2..3] ].join("/") end diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index 38acef81f4..fdde01d4a3 100644 --- a/activestorage/lib/active_storage/service/gcs_service.rb +++ b/activestorage/lib/active_storage/service/gcs_service.rb @@ -6,7 +6,6 @@ require "google/cloud/storage" require "net/http" require "active_support/core_ext/object/to_query" -require "active_storage/filename" module ActiveStorage # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API diff --git a/activestorage/test/controllers/disk_controller_test.rb b/activestorage/test/controllers/disk_controller_test.rb index 9af7c83bdf..c053052f6f 100644 --- a/activestorage/test/controllers/disk_controller_test.rb +++ b/activestorage/test/controllers/disk_controller_test.rb @@ -6,8 +6,8 @@ require "database/setup" class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest test "showing blob inline" do blob = create_blob - get blob.service_url + assert_response :ok assert_equal "inline; filename=\"hello.txt\"; filename*=UTF-8''hello.txt", response.headers["Content-Disposition"] assert_equal "text/plain", response.headers["Content-Type"] assert_equal "Hello world!", response.body @@ -15,13 +15,22 @@ class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest test "showing blob as attachment" do blob = create_blob - get blob.service_url(disposition: :attachment) + assert_response :ok assert_equal "attachment; filename=\"hello.txt\"; filename*=UTF-8''hello.txt", response.headers["Content-Disposition"] assert_equal "text/plain", response.headers["Content-Type"] assert_equal "Hello world!", response.body end + test "showing blob range inline" do + blob = create_blob + get blob.service_url, headers: { "Range" => "bytes=5-9" } + assert_response :partial_content + assert_equal "inline; filename=\"hello.txt\"; filename*=UTF-8''hello.txt", response.headers["Content-Disposition"] + assert_equal "text/plain", response.headers["Content-Type"] + assert_equal " worl", response.body + end + test "directly uploading blob with integrity" do data = "Something else entirely!" @@ -58,4 +67,10 @@ class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest assert_response :unprocessable_entity assert_not blob.service.exist?(blob.key) end + + test "directly uploading blob with invalid token" do + put update_rails_disk_service_url(encoded_token: "invalid"), + params: "Something else entirely!", headers: { "Content-Type" => "text/plain" } + assert_response :not_found + end end diff --git a/activestorage/test/jobs/purge_job_test.rb b/activestorage/test/jobs/purge_job_test.rb new file mode 100644 index 0000000000..ed4100b78d --- /dev/null +++ b/activestorage/test/jobs/purge_job_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "test_helper" +require "database/setup" + +class ActiveStorage::PurgeJobTest < ActiveJob::TestCase + setup { @blob = create_blob } + + test "purges" do + assert_difference -> { ActiveStorage::Blob.count }, -1 do + ActiveStorage::PurgeJob.perform_now @blob + end + + assert_not ActiveStorage::Blob.exists?(@blob.id) + assert_not ActiveStorage::Blob.service.exist?(@blob.key) + end + + test "ignores missing blob" do + @blob.purge + + perform_enqueued_jobs do + assert_nothing_raised do + ActiveStorage::PurgeJob.perform_later @blob + end + end + end + + test "ignores attached blob" do + User.create! name: "DHH", avatar: @blob + + perform_enqueued_jobs do + assert_nothing_raised do + ActiveStorage::PurgeJob.perform_later @blob + end + end + end +end diff --git a/activestorage/test/models/attached/many_test.rb b/activestorage/test/models/attached/many_test.rb new file mode 100644 index 0000000000..334254d099 --- /dev/null +++ b/activestorage/test/models/attached/many_test.rb @@ -0,0 +1,540 @@ +# frozen_string_literal: true + +require "test_helper" +require "database/setup" + +class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + setup do + @user = User.create!(name: "Josh") + end + + teardown { ActiveStorage::Blob.all.each(&:delete) } + + test "attaching existing blobs to an existing record" do + @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + end + + test "attaching existing blobs from signed IDs to an existing record" do + @user.highlights.attach create_blob(filename: "funky.jpg").signed_id, create_blob(filename: "town.jpg").signed_id + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + end + + test "attaching new blobs from Hashes to an existing record" do + @user.highlights.attach( + { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" }, + { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" }) + + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + end + + test "attaching new blobs from uploaded files to an existing record" do + @user.highlights.attach fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") + assert_equal "racecar.jpg", @user.highlights.first.filename.to_s + assert_equal "video.mp4", @user.highlights.second.filename.to_s + end + + test "attaching existing blobs to an existing, changed record" do + @user.name = "Tina" + assert @user.changed? + + @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + assert_not @user.highlights.first.persisted? + assert_not @user.highlights.second.persisted? + assert @user.will_save_change_to_name? + + @user.save! + assert_equal "funky.jpg", @user.highlights.reload.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + end + + test "attaching existing blobs from signed IDs to an existing, changed record" do + @user.name = "Tina" + assert @user.changed? + + @user.highlights.attach create_blob(filename: "funky.jpg").signed_id, create_blob(filename: "town.jpg").signed_id + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + assert_not @user.highlights.first.persisted? + assert_not @user.highlights.second.persisted? + assert @user.will_save_change_to_name? + + @user.save! + assert_equal "funky.jpg", @user.highlights.reload.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + end + + test "attaching new blobs from Hashes to an existing, changed record" do + @user.name = "Tina" + assert @user.changed? + + @user.highlights.attach( + { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" }, + { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" }) + + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + assert_not @user.highlights.first.persisted? + assert_not @user.highlights.second.persisted? + assert @user.will_save_change_to_name? + + @user.save! + assert_equal "funky.jpg", @user.highlights.reload.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + end + + test "attaching new blobs from uploaded files to an existing, changed record" do + @user.name = "Tina" + assert @user.changed? + + @user.highlights.attach fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") + assert_equal "racecar.jpg", @user.highlights.first.filename.to_s + assert_equal "video.mp4", @user.highlights.second.filename.to_s + assert_not @user.highlights.first.persisted? + assert_not @user.highlights.second.persisted? + assert @user.will_save_change_to_name? + + @user.save! + assert_equal "racecar.jpg", @user.highlights.reload.first.filename.to_s + assert_equal "video.mp4", @user.highlights.second.filename.to_s + end + + test "attaching existing blobs to an existing record one at a time" do + @user.highlights.attach create_blob(filename: "funky.jpg") + @user.highlights.attach create_blob(filename: "town.jpg") + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + + @user.reload + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + end + + test "updating an existing record to attach existing blobs" do + @user.update! highlights: [ create_file_blob(filename: "racecar.jpg"), create_file_blob(filename: "video.mp4") ] + assert_equal "racecar.jpg", @user.highlights.first.filename.to_s + assert_equal "video.mp4", @user.highlights.second.filename.to_s + end + + test "updating an existing record to attach existing blobs from signed IDs" do + @user.update! highlights: [ create_blob(filename: "funky.jpg").signed_id, create_blob(filename: "town.jpg").signed_id ] + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + end + + test "successfully updating an existing record to attach new blobs from uploaded files" do + @user.highlights = [ fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") ] + assert_equal "racecar.jpg", @user.highlights.first.filename.to_s + assert_equal "video.mp4", @user.highlights.second.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(@user.highlights.first.key) + assert_not ActiveStorage::Blob.service.exist?(@user.highlights.second.key) + + @user.save! + assert ActiveStorage::Blob.service.exist?(@user.highlights.first.key) + assert ActiveStorage::Blob.service.exist?(@user.highlights.second.key) + end + + test "unsuccessfully updating an existing record to attach new blobs from uploaded files" do + assert_not @user.update(name: "", highlights: [ fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") ]) + assert_equal "racecar.jpg", @user.highlights.first.filename.to_s + assert_equal "video.mp4", @user.highlights.second.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(@user.highlights.first.key) + assert_not ActiveStorage::Blob.service.exist?(@user.highlights.second.key) + end + + test "replacing existing, dependent attachments on an existing record via assign and attach" do + [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |old_blobs| + @user.highlights.attach old_blobs + + @user.highlights = [] + assert_not @user.highlights.attached? + + perform_enqueued_jobs do + @user.highlights.attach create_blob(filename: "whenever.jpg"), create_blob(filename: "wherever.jpg") + end + + assert_equal "whenever.jpg", @user.highlights.first.filename.to_s + assert_equal "wherever.jpg", @user.highlights.second.filename.to_s + assert_not ActiveStorage::Blob.exists?(old_blobs.first.id) + assert_not ActiveStorage::Blob.exists?(old_blobs.second.id) + assert_not ActiveStorage::Blob.service.exist?(old_blobs.first.key) + assert_not ActiveStorage::Blob.service.exist?(old_blobs.second.key) + end + end + + test "replacing existing, independent attachments on an existing record via assign and attach" do + @user.vlogs.attach create_blob(filename: "funky.mp4"), create_blob(filename: "town.mp4") + + @user.vlogs = [] + assert_not @user.vlogs.attached? + + assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do + @user.vlogs.attach create_blob(filename: "whenever.mp4"), create_blob(filename: "wherever.mp4") + end + + assert_equal "whenever.mp4", @user.vlogs.first.filename.to_s + assert_equal "wherever.mp4", @user.vlogs.second.filename.to_s + end + + test "successfully updating an existing record to replace existing, dependent attachments" do + [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |old_blobs| + @user.highlights.attach old_blobs + + perform_enqueued_jobs do + @user.update! highlights: [ create_blob(filename: "whenever.jpg"), create_blob(filename: "wherever.jpg") ] + end + + assert_equal "whenever.jpg", @user.highlights.first.filename.to_s + assert_equal "wherever.jpg", @user.highlights.second.filename.to_s + assert_not ActiveStorage::Blob.exists?(old_blobs.first.id) + assert_not ActiveStorage::Blob.exists?(old_blobs.second.id) + assert_not ActiveStorage::Blob.service.exist?(old_blobs.first.key) + assert_not ActiveStorage::Blob.service.exist?(old_blobs.second.key) + end + end + + test "successfully updating an existing record to replace existing, independent attachments" do + @user.vlogs.attach create_blob(filename: "funky.mp4"), create_blob(filename: "town.mp4") + + assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do + @user.update! vlogs: [ create_blob(filename: "whenever.mp4"), create_blob(filename: "wherever.mp4") ] + end + + assert_equal "whenever.mp4", @user.vlogs.first.filename.to_s + assert_equal "wherever.mp4", @user.vlogs.second.filename.to_s + end + + test "unsuccessfully updating an existing record to replace existing attachments" do + @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") + + assert_no_enqueued_jobs do + assert_not @user.update(name: "", highlights: [ fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") ]) + end + + assert_equal "racecar.jpg", @user.highlights.first.filename.to_s + assert_equal "video.mp4", @user.highlights.second.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(@user.highlights.first.key) + assert_not ActiveStorage::Blob.service.exist?(@user.highlights.second.key) + end + + test "updating an existing record to attach one new blob and one previously-attached blob" do + [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs| + @user.highlights.attach blobs.first + + perform_enqueued_jobs do + assert_no_changes -> { @user.highlights_attachments.first.id } do + @user.update! highlights: blobs + end + end + + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + assert ActiveStorage::Blob.service.exist?(@user.highlights.first.key) + end + end + + test "updating an existing record to remove dependent attachments" do + [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs| + @user.highlights.attach blobs + + assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blobs.first ] do + assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blobs.second ] do + @user.update! highlights: [] + end + end + + assert_not @user.highlights.attached? + end + end + + test "updating an existing record to remove independent attachments" do + [ create_blob(filename: "funky.mp4"), create_blob(filename: "town.mp4") ].tap do |blobs| + @user.vlogs.attach blobs + + assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do + @user.update! vlogs: [] + end + + assert_not @user.vlogs.attached? + end + end + + test "analyzing a new blob from an uploaded file after attaching it to an existing record" do + perform_enqueued_jobs do + @user.highlights.attach fixture_file_upload("racecar.jpg") + end + + assert @user.highlights.reload.first.analyzed? + assert_equal 4104, @user.highlights.first.metadata[:width] + assert_equal 2736, @user.highlights.first.metadata[:height] + end + + test "analyzing a new blob from an uploaded file after attaching it to an existing record via update" do + perform_enqueued_jobs do + @user.update! highlights: [ fixture_file_upload("racecar.jpg") ] + end + + assert @user.highlights.reload.first.analyzed? + assert_equal 4104, @user.highlights.first.metadata[:width] + assert_equal 2736, @user.highlights.first.metadata[:height] + end + + test "analyzing a directly-uploaded blob after attaching it to an existing record" do + perform_enqueued_jobs do + @user.highlights.attach directly_upload_file_blob(filename: "racecar.jpg") + end + + assert @user.highlights.reload.first.analyzed? + assert_equal 4104, @user.highlights.first.metadata[:width] + assert_equal 2736, @user.highlights.first.metadata[:height] + end + + test "analyzing a directly-uploaded blob after attaching it to an existing record via update" do + perform_enqueued_jobs do + @user.update! highlights: [ directly_upload_file_blob(filename: "racecar.jpg") ] + end + + assert @user.highlights.reload.first.analyzed? + assert_equal 4104, @user.highlights.first.metadata[:width] + assert_equal 2736, @user.highlights.first.metadata[:height] + end + + test "attaching existing blobs to a new record" do + User.new(name: "Jason").tap do |user| + user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") + assert user.new_record? + assert_equal "funky.jpg", user.highlights.first.filename.to_s + assert_equal "town.jpg", user.highlights.second.filename.to_s + + user.save! + assert_equal "funky.jpg", user.highlights.first.filename.to_s + assert_equal "town.jpg", user.highlights.second.filename.to_s + end + end + + test "attaching an existing blob from a signed ID to a new record" do + User.new(name: "Jason").tap do |user| + user.avatar.attach create_blob(filename: "funky.jpg").signed_id + assert user.new_record? + assert_equal "funky.jpg", user.avatar.filename.to_s + + user.save! + assert_equal "funky.jpg", user.reload.avatar.filename.to_s + end + end + + test "attaching new blobs from Hashes to a new record" do + User.new(name: "Jason").tap do |user| + user.highlights.attach( + { io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" }, + { io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpg" }) + + assert user.new_record? + assert user.highlights.first.new_record? + assert user.highlights.second.new_record? + assert user.highlights.first.blob.new_record? + assert user.highlights.second.blob.new_record? + assert_equal "funky.jpg", user.highlights.first.filename.to_s + assert_equal "town.jpg", user.highlights.second.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(user.highlights.first.key) + assert_not ActiveStorage::Blob.service.exist?(user.highlights.second.key) + + user.save! + assert user.highlights.first.persisted? + assert user.highlights.second.persisted? + assert user.highlights.first.blob.persisted? + assert user.highlights.second.blob.persisted? + assert_equal "funky.jpg", user.reload.highlights.first.filename.to_s + assert_equal "town.jpg", user.highlights.second.filename.to_s + assert ActiveStorage::Blob.service.exist?(user.highlights.first.key) + assert ActiveStorage::Blob.service.exist?(user.highlights.second.key) + end + end + + test "attaching new blobs from uploaded files to a new record" do + User.new(name: "Jason").tap do |user| + user.highlights.attach fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") + assert user.new_record? + assert user.highlights.first.new_record? + assert user.highlights.second.new_record? + assert user.highlights.first.blob.new_record? + assert user.highlights.second.blob.new_record? + assert_equal "racecar.jpg", user.highlights.first.filename.to_s + assert_equal "video.mp4", user.highlights.second.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(user.highlights.first.key) + assert_not ActiveStorage::Blob.service.exist?(user.highlights.second.key) + + user.save! + assert user.highlights.first.persisted? + assert user.highlights.second.persisted? + assert user.highlights.first.blob.persisted? + assert user.highlights.second.blob.persisted? + assert_equal "racecar.jpg", user.reload.highlights.first.filename.to_s + assert_equal "video.mp4", user.highlights.second.filename.to_s + assert ActiveStorage::Blob.service.exist?(user.highlights.first.key) + assert ActiveStorage::Blob.service.exist?(user.highlights.second.key) + end + end + + test "creating a record with existing blobs attached" do + user = User.create!(name: "Jason", highlights: [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ]) + assert_equal "funky.jpg", user.reload.highlights.first.filename.to_s + assert_equal "town.jpg", user.reload.highlights.second.filename.to_s + end + + test "creating a record with an existing blob from signed IDs attached" do + user = User.create!(name: "Jason", highlights: [ + create_blob(filename: "funky.jpg").signed_id, create_blob(filename: "town.jpg").signed_id ]) + assert_equal "funky.jpg", user.reload.highlights.first.filename.to_s + assert_equal "town.jpg", user.reload.highlights.second.filename.to_s + end + + test "creating a record with new blobs from uploaded files attached" do + User.new(name: "Jason", highlights: [ fixture_file_upload("racecar.jpg"), fixture_file_upload("video.mp4") ]).tap do |user| + assert user.new_record? + assert user.highlights.first.new_record? + assert user.highlights.second.new_record? + assert user.highlights.first.blob.new_record? + assert user.highlights.second.blob.new_record? + assert_equal "racecar.jpg", user.highlights.first.filename.to_s + assert_equal "video.mp4", user.highlights.second.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(user.highlights.first.key) + assert_not ActiveStorage::Blob.service.exist?(user.highlights.second.key) + + user.save! + assert_equal "racecar.jpg", user.highlights.first.filename.to_s + assert_equal "video.mp4", user.highlights.second.filename.to_s + end + end + + test "creating a record with an unexpected object attached" do + error = assert_raises(ArgumentError) { User.create!(name: "Jason", highlights: :foo) } + assert_equal "Could not find or build blob: expected attachable, got :foo", error.message + end + + test "analyzing a new blob from an uploaded file after attaching it to a new record" do + perform_enqueued_jobs do + user = User.create!(name: "Jason", highlights: [ fixture_file_upload("racecar.jpg") ]) + assert user.highlights.reload.first.analyzed? + assert_equal 4104, user.highlights.first.metadata[:width] + assert_equal 2736, user.highlights.first.metadata[:height] + end + end + + test "analyzing a directly-uploaded blob after attaching it to a new record" do + perform_enqueued_jobs do + user = User.create!(name: "Jason", highlights: [ directly_upload_file_blob(filename: "racecar.jpg") ]) + assert user.highlights.reload.first.analyzed? + assert_equal 4104, user.highlights.first.metadata[:width] + assert_equal 2736, user.highlights.first.metadata[:height] + end + end + + test "detaching" do + [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs| + @user.highlights.attach blobs + assert @user.highlights.attached? + + perform_enqueued_jobs do + @user.highlights.detach + end + + assert_not @user.highlights.attached? + assert ActiveStorage::Blob.exists?(blobs.first.id) + assert ActiveStorage::Blob.exists?(blobs.second.id) + assert ActiveStorage::Blob.service.exist?(blobs.first.key) + assert ActiveStorage::Blob.service.exist?(blobs.second.key) + end + end + + test "purging" do + [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs| + @user.highlights.attach blobs + assert @user.highlights.attached? + + @user.highlights.purge + assert_not @user.highlights.attached? + assert_not ActiveStorage::Blob.exists?(blobs.first.id) + assert_not ActiveStorage::Blob.exists?(blobs.second.id) + assert_not ActiveStorage::Blob.service.exist?(blobs.first.key) + assert_not ActiveStorage::Blob.service.exist?(blobs.second.key) + end + end + + test "purging later" do + [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs| + @user.highlights.attach blobs + assert @user.highlights.attached? + + perform_enqueued_jobs do + @user.highlights.purge_later + end + + assert_not @user.highlights.attached? + assert_not ActiveStorage::Blob.exists?(blobs.first.id) + assert_not ActiveStorage::Blob.exists?(blobs.second.id) + assert_not ActiveStorage::Blob.service.exist?(blobs.first.key) + assert_not ActiveStorage::Blob.service.exist?(blobs.second.key) + end + end + + test "purging dependent attachment later on destroy" do + [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs| + @user.highlights.attach blobs + + perform_enqueued_jobs do + @user.destroy! + end + + assert_not ActiveStorage::Blob.exists?(blobs.first.id) + assert_not ActiveStorage::Blob.exists?(blobs.second.id) + assert_not ActiveStorage::Blob.service.exist?(blobs.first.key) + assert_not ActiveStorage::Blob.service.exist?(blobs.second.key) + end + end + + test "not purging independent attachment on destroy" do + [ create_blob(filename: "funky.mp4"), create_blob(filename: "town.mp4") ].tap do |blobs| + @user.vlogs.attach blobs + + assert_no_enqueued_jobs do + @user.destroy! + end + end + end + + test "clearing change on reload" do + @user.highlights = [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ] + assert @user.highlights.attached? + + @user.reload + assert_not @user.highlights.attached? + end + + test "overriding attached reader" do + @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") + + assert_equal "funky.jpg", @user.highlights.first.filename.to_s + assert_equal "town.jpg", @user.highlights.second.filename.to_s + + begin + User.class_eval do + def highlights + super.reverse + end + end + + assert_equal "town.jpg", @user.highlights.first.filename.to_s + assert_equal "funky.jpg", @user.highlights.second.filename.to_s + ensure + User.send(:remove_method, :highlights) + end + end +end diff --git a/activestorage/test/models/attached/one_test.rb b/activestorage/test/models/attached/one_test.rb new file mode 100644 index 0000000000..3333fd9323 --- /dev/null +++ b/activestorage/test/models/attached/one_test.rb @@ -0,0 +1,478 @@ +# frozen_string_literal: true + +require "test_helper" +require "database/setup" + +class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + setup do + @user = User.create!(name: "Josh") + end + + teardown { ActiveStorage::Blob.all.each(&:delete) } + + test "attaching an existing blob to an existing record" do + @user.avatar.attach create_blob(filename: "funky.jpg") + assert_equal "funky.jpg", @user.avatar.filename.to_s + end + + test "attaching an existing blob from a signed ID to an existing record" do + @user.avatar.attach create_blob(filename: "funky.jpg").signed_id + assert_equal "funky.jpg", @user.avatar.filename.to_s + end + + test "attaching a new blob from a Hash to an existing record" do + @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + assert_equal "town.jpg", @user.avatar.filename.to_s + end + + test "attaching a new blob from an uploaded file to an existing record" do + @user.avatar.attach fixture_file_upload("racecar.jpg") + assert_equal "racecar.jpg", @user.avatar.filename.to_s + end + + test "attaching an existing blob to an existing, changed record" do + @user.name = "Tina" + assert @user.changed? + + @user.avatar.attach create_blob(filename: "funky.jpg") + assert_equal "funky.jpg", @user.avatar.filename.to_s + assert_not @user.avatar.persisted? + assert @user.will_save_change_to_name? + + @user.save! + assert_equal "funky.jpg", @user.reload.avatar.filename.to_s + end + + test "attaching an existing blob from a signed ID to an existing, changed record" do + @user.name = "Tina" + assert @user.changed? + + @user.avatar.attach create_blob(filename: "funky.jpg").signed_id + assert_equal "funky.jpg", @user.avatar.filename.to_s + assert_not @user.avatar.persisted? + assert @user.will_save_change_to_name? + + @user.save! + assert_equal "funky.jpg", @user.reload.avatar.filename.to_s + end + + test "attaching a new blob from a Hash to an existing, changed record" do + @user.name = "Tina" + assert @user.changed? + + @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + assert_equal "town.jpg", @user.avatar.filename.to_s + assert_not @user.avatar.persisted? + assert @user.will_save_change_to_name? + + @user.save! + assert_equal "town.jpg", @user.reload.avatar.filename.to_s + end + + test "attaching a new blob from an uploaded file to an existing, changed record" do + @user.name = "Tina" + assert @user.changed? + + @user.avatar.attach fixture_file_upload("racecar.jpg") + assert_equal "racecar.jpg", @user.avatar.filename.to_s + assert_not @user.avatar.persisted? + assert @user.will_save_change_to_name? + + @user.save! + assert_equal "racecar.jpg", @user.reload.avatar.filename.to_s + end + + test "updating an existing record to attach an existing blob" do + @user.update! avatar: create_blob(filename: "funky.jpg") + assert_equal "funky.jpg", @user.avatar.filename.to_s + end + + test "updating an existing record to attach an existing blob from a signed ID" do + @user.update! avatar: create_blob(filename: "funky.jpg").signed_id + assert_equal "funky.jpg", @user.avatar.filename.to_s + end + + test "successfully updating an existing record to attach a new blob from an uploaded file" do + @user.avatar = fixture_file_upload("racecar.jpg") + assert_equal "racecar.jpg", @user.avatar.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(@user.avatar.key) + + @user.save! + assert ActiveStorage::Blob.service.exist?(@user.avatar.key) + end + + test "unsuccessfully updating an existing record to attach a new blob from an uploaded file" do + assert_not @user.update(name: "", avatar: fixture_file_upload("racecar.jpg")) + assert_equal "racecar.jpg", @user.avatar.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(@user.avatar.key) + end + + test "successfully replacing an existing, dependent attachment on an existing record" do + create_blob(filename: "funky.jpg").tap do |old_blob| + @user.avatar.attach old_blob + + perform_enqueued_jobs do + @user.avatar.attach create_blob(filename: "town.jpg") + end + + assert_equal "town.jpg", @user.avatar.filename.to_s + assert_not ActiveStorage::Blob.exists?(old_blob.id) + assert_not ActiveStorage::Blob.service.exist?(old_blob.key) + end + end + + test "replacing an existing, independent attachment on an existing record" do + @user.cover_photo.attach create_blob(filename: "funky.jpg") + + assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do + @user.cover_photo.attach create_blob(filename: "town.jpg") + end + + assert_equal "town.jpg", @user.cover_photo.filename.to_s + end + + test "replacing an attached blob on an existing record with itself" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.avatar.attach blob + + assert_no_changes -> { @user.reload.avatar_attachment.id } do + assert_no_enqueued_jobs do + @user.avatar.attach blob + end + end + + assert_equal "funky.jpg", @user.avatar.filename.to_s + assert ActiveStorage::Blob.service.exist?(@user.avatar.key) + end + end + + test "successfully updating an existing record to replace an existing, dependent attachment" do + create_blob(filename: "funky.jpg").tap do |old_blob| + @user.avatar.attach old_blob + + perform_enqueued_jobs do + @user.update! avatar: create_blob(filename: "town.jpg") + end + + assert_equal "town.jpg", @user.avatar.filename.to_s + assert_not ActiveStorage::Blob.exists?(old_blob.id) + assert_not ActiveStorage::Blob.service.exist?(old_blob.key) + end + end + + test "successfully updating an existing record to replace an existing, independent attachment" do + @user.cover_photo.attach create_blob(filename: "funky.jpg") + + assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do + @user.update! cover_photo: create_blob(filename: "town.jpg") + end + + assert_equal "town.jpg", @user.cover_photo.filename.to_s + end + + test "unsuccessfully updating an existing record to replace an existing attachment" do + @user.avatar.attach create_blob(filename: "funky.jpg") + + assert_no_enqueued_jobs do + assert_not @user.update(name: "", avatar: fixture_file_upload("racecar.jpg")) + end + + assert_equal "racecar.jpg", @user.avatar.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(@user.avatar.key) + end + + test "updating an existing record to replace an attached blob with itself" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.avatar.attach blob + + assert_no_enqueued_jobs do + assert_no_changes -> { @user.reload.avatar_attachment.id } do + @user.update! avatar: blob + end + end + end + end + + test "removing a dependent attachment from an existing record" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.avatar.attach blob + + assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blob ] do + @user.avatar.attach nil + end + + assert_not @user.avatar.attached? + end + end + + test "removing an independent attachment from an existing record" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.cover_photo.attach blob + + assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do + @user.cover_photo.attach nil + end + + assert_not @user.cover_photo.attached? + end + end + + test "updating an existing record to remove a dependent attachment" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.avatar.attach blob + + assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blob ] do + @user.update! avatar: nil + end + + assert_not @user.avatar.attached? + end + end + + test "updating an existing record to remove an independent attachment" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.cover_photo.attach blob + + assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do + @user.update! cover_photo: nil + end + + assert_not @user.cover_photo.attached? + end + end + + test "analyzing a new blob from an uploaded file after attaching it to an existing record" do + perform_enqueued_jobs do + @user.avatar.attach fixture_file_upload("racecar.jpg") + end + + assert @user.avatar.reload.analyzed? + assert_equal 4104, @user.avatar.metadata[:width] + assert_equal 2736, @user.avatar.metadata[:height] + end + + test "analyzing a new blob from an uploaded file after attaching it to an existing record via update" do + perform_enqueued_jobs do + @user.update! avatar: fixture_file_upload("racecar.jpg") + end + + assert @user.avatar.reload.analyzed? + assert_equal 4104, @user.avatar.metadata[:width] + assert_equal 2736, @user.avatar.metadata[:height] + end + + test "analyzing a directly-uploaded blob after attaching it to an existing record" do + perform_enqueued_jobs do + @user.avatar.attach directly_upload_file_blob(filename: "racecar.jpg") + end + + assert @user.avatar.reload.analyzed? + assert_equal 4104, @user.avatar.metadata[:width] + assert_equal 2736, @user.avatar.metadata[:height] + end + + test "analyzing a directly-uploaded blob after attaching it to an existing record via updates" do + perform_enqueued_jobs do + @user.update! avatar: directly_upload_file_blob(filename: "racecar.jpg") + end + + assert @user.avatar.reload.analyzed? + assert_equal 4104, @user.avatar.metadata[:width] + assert_equal 2736, @user.avatar.metadata[:height] + end + + test "attaching an existing blob to a new record" do + User.new(name: "Jason").tap do |user| + user.avatar.attach create_blob(filename: "funky.jpg") + assert user.new_record? + assert_equal "funky.jpg", user.avatar.filename.to_s + + user.save! + assert_equal "funky.jpg", user.reload.avatar.filename.to_s + end + end + + test "attaching an existing blob from a signed ID to a new record" do + User.new(name: "Jason").tap do |user| + user.avatar.attach create_blob(filename: "funky.jpg").signed_id + assert user.new_record? + assert_equal "funky.jpg", user.avatar.filename.to_s + + user.save! + assert_equal "funky.jpg", user.reload.avatar.filename.to_s + end + end + + test "attaching a new blob from a Hash to a new record" do + User.new(name: "Jason").tap do |user| + user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + assert user.new_record? + assert user.avatar.attachment.new_record? + assert user.avatar.blob.new_record? + assert_equal "town.jpg", user.avatar.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(user.avatar.key) + + user.save! + assert user.avatar.attachment.persisted? + assert user.avatar.blob.persisted? + assert_equal "town.jpg", user.reload.avatar.filename.to_s + assert ActiveStorage::Blob.service.exist?(user.avatar.key) + end + end + + test "attaching a new blob from an uploaded file to a new record" do + User.new(name: "Jason").tap do |user| + user.avatar.attach fixture_file_upload("racecar.jpg") + assert user.new_record? + assert user.avatar.attachment.new_record? + assert user.avatar.blob.new_record? + assert_equal "racecar.jpg", user.avatar.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(user.avatar.key) + + user.save! + assert user.avatar.attachment.persisted? + assert user.avatar.blob.persisted? + assert_equal "racecar.jpg", user.reload.avatar.filename.to_s + assert ActiveStorage::Blob.service.exist?(user.avatar.key) + end + end + + test "creating a record with an existing blob attached" do + user = User.create!(name: "Jason", avatar: create_blob(filename: "funky.jpg")) + assert_equal "funky.jpg", user.reload.avatar.filename.to_s + end + + test "creating a record with an existing blob from a signed ID attached" do + user = User.create!(name: "Jason", avatar: create_blob(filename: "funky.jpg").signed_id) + assert_equal "funky.jpg", user.reload.avatar.filename.to_s + end + + test "creating a record with a new blob from an uploaded file attached" do + User.new(name: "Jason", avatar: fixture_file_upload("racecar.jpg")).tap do |user| + assert user.new_record? + assert user.avatar.attachment.new_record? + assert user.avatar.blob.new_record? + assert_equal "racecar.jpg", user.avatar.filename.to_s + assert_not ActiveStorage::Blob.service.exist?(user.avatar.key) + + user.save! + assert_equal "racecar.jpg", user.reload.avatar.filename.to_s + end + end + + test "creating a record with an unexpected object attached" do + error = assert_raises(ArgumentError) { User.create!(name: "Jason", avatar: :foo) } + assert_equal "Could not find or build blob: expected attachable, got :foo", error.message + end + + test "analyzing a new blob from an uploaded file after attaching it to a new record" do + perform_enqueued_jobs do + user = User.create!(name: "Jason", avatar: fixture_file_upload("racecar.jpg")) + assert user.avatar.reload.analyzed? + assert_equal 4104, user.avatar.metadata[:width] + assert_equal 2736, user.avatar.metadata[:height] + end + end + + test "analyzing a directly-uploaded blob after attaching it to a new record" do + perform_enqueued_jobs do + user = User.create!(name: "Jason", avatar: directly_upload_file_blob(filename: "racecar.jpg")) + assert user.avatar.reload.analyzed? + assert_equal 4104, user.avatar.metadata[:width] + assert_equal 2736, user.avatar.metadata[:height] + end + end + + test "detaching" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.avatar.attach blob + assert @user.avatar.attached? + + perform_enqueued_jobs do + @user.avatar.detach + end + + assert_not @user.avatar.attached? + assert ActiveStorage::Blob.exists?(blob.id) + assert ActiveStorage::Blob.service.exist?(blob.key) + end + end + + test "purging" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.avatar.attach blob + assert @user.avatar.attached? + + @user.avatar.purge + assert_not @user.avatar.attached? + assert_not ActiveStorage::Blob.exists?(blob.id) + assert_not ActiveStorage::Blob.service.exist?(blob.key) + end + end + + test "purging later" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.avatar.attach blob + assert @user.avatar.attached? + + perform_enqueued_jobs do + @user.avatar.purge_later + end + + assert_not @user.avatar.attached? + assert_not ActiveStorage::Blob.exists?(blob.id) + assert_not ActiveStorage::Blob.service.exist?(blob.key) + end + end + + test "purging dependent attachment later on destroy" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.avatar.attach blob + + perform_enqueued_jobs do + @user.destroy! + end + + assert_not ActiveStorage::Blob.exists?(blob.id) + assert_not ActiveStorage::Blob.service.exist?(blob.key) + end + end + + test "not purging independent attachment on destroy" do + create_blob(filename: "funky.jpg").tap do |blob| + @user.cover_photo.attach blob + + assert_no_enqueued_jobs do + @user.destroy! + end + end + end + + test "clearing change on reload" do + @user.avatar = create_blob(filename: "funky.jpg") + assert @user.avatar.attached? + + @user.reload + assert_not @user.avatar.attached? + end + + test "overriding attached reader" do + @user.avatar.attach create_blob(filename: "funky.jpg") + + assert_equal "funky.jpg", @user.avatar.filename.to_s + + begin + User.class_eval do + def avatar + super.filename.to_s.reverse + end + end + + assert_equal "gpj.yknuf", @user.avatar + ensure + User.send(:remove_method, :avatar) + end + end +end diff --git a/activestorage/test/models/attached_test.rb b/activestorage/test/models/attached_test.rb deleted file mode 100644 index b10d2bebe3..0000000000 --- a/activestorage/test/models/attached_test.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "database/setup" - -class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase - setup do - @user = User.create!(name: "Josh") - end - - teardown { ActiveStorage::Blob.all.each(&:purge) } - - test "overriding has_one_attached methods works" do - # attach blob before messing with getter, which breaks `#attach` - @user.avatar.attach create_blob(filename: "funky.jpg") - - # inherited only - assert_equal "funky.jpg", @user.avatar.filename.to_s - - begin - User.class_eval do - def avatar - super.filename.to_s.reverse - end - end - - # override with super - assert_equal "funky.jpg".reverse, @user.avatar - ensure - User.send(:remove_method, :avatar) - end - end - - test "overriding has_many_attached methods works" do - # attach blobs before messing with getter, which breaks `#attach` - @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg") - - # inherited only - assert_equal "funky.jpg", @user.highlights.first.filename.to_s - assert_equal "wonky.jpg", @user.highlights.second.filename.to_s - - begin - User.class_eval do - def highlights - super.reverse - end - end - - # override with super - assert_equal "wonky.jpg", @user.highlights.first.filename.to_s - assert_equal "funky.jpg", @user.highlights.second.filename.to_s - ensure - User.send(:remove_method, :highlights) - end - end -end diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb deleted file mode 100644 index ce83ec27d2..0000000000 --- a/activestorage/test/models/attachments_test.rb +++ /dev/null @@ -1,459 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "database/setup" - -class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase - include ActiveJob::TestHelper - - setup { @user = User.create!(name: "DHH") } - - teardown { ActiveStorage::Blob.all.each(&:purge) } - - test "attach existing blob" do - @user.avatar.attach create_blob(filename: "funky.jpg") - assert_equal "funky.jpg", @user.avatar.filename.to_s - end - - test "attach existing blob from a signed ID" do - @user.avatar.attach create_blob(filename: "funky.jpg").signed_id - assert_equal "funky.jpg", @user.avatar.filename.to_s - end - - test "attach new blob from a Hash" do - @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" - assert_equal "town.jpg", @user.avatar.filename.to_s - end - - test "attach new blob from an UploadedFile" do - file = file_fixture "racecar.jpg" - @user.avatar.attach Rack::Test::UploadedFile.new file.to_s - assert_equal "racecar.jpg", @user.avatar.filename.to_s - end - - test "replace attached blob" do - @user.avatar.attach create_blob(filename: "funky.jpg") - - perform_enqueued_jobs do - assert_no_difference -> { ActiveStorage::Blob.count } do - @user.avatar.attach create_blob(filename: "town.jpg") - end - end - - assert_equal "town.jpg", @user.avatar.filename.to_s - end - - test "replace attached blob unsuccessfully" do - @user.avatar.attach create_blob(filename: "funky.jpg") - - perform_enqueued_jobs do - assert_raises do - @user.avatar.attach nil - end - end - - assert_equal "funky.jpg", @user.reload.avatar.filename.to_s - assert ActiveStorage::Blob.service.exist?(@user.avatar.key) - end - - test "replace attached blob with itself" do - @user.avatar.attach create_blob(filename: "funky.jpg") - - assert_no_changes -> { @user.reload.avatar.blob } do - assert_no_changes -> { @user.reload.avatar.attachment } do - assert_no_enqueued_jobs do - @user.avatar.attach @user.avatar.blob - end - end - end - end - - test "replaced attached blob with itself by signed ID" do - @user.avatar.attach create_blob(filename: "funky.jpg") - - assert_no_changes -> { @user.reload.avatar.blob } do - assert_no_changes -> { @user.reload.avatar.attachment } do - assert_no_enqueued_jobs do - @user.avatar.attach @user.avatar.blob.signed_id - end - end - end - end - - test "replace independent attached blob" do - @user.cover_photo.attach create_blob(filename: "funky.jpg") - - perform_enqueued_jobs do - assert_difference -> { ActiveStorage::Blob.count }, +1 do - assert_no_difference -> { ActiveStorage::Attachment.count } do - @user.cover_photo.attach create_blob(filename: "town.jpg") - end - end - end - - assert_equal "town.jpg", @user.cover_photo.filename.to_s - end - - test "attach blob to new record" do - user = User.new(name: "Jason") - - assert_no_changes -> { user.new_record? } do - assert_no_difference -> { ActiveStorage::Attachment.count } do - user.avatar.attach create_blob(filename: "funky.jpg") - end - end - - assert_predicate user.avatar, :attached? - assert_equal "funky.jpg", user.avatar.filename.to_s - - assert_difference -> { ActiveStorage::Attachment.count }, +1 do - user.save! - end - - assert_predicate user.reload.avatar, :attached? - assert_equal "funky.jpg", user.avatar.filename.to_s - end - - test "build new record with attached blob" do - assert_no_difference -> { ActiveStorage::Attachment.count } do - @user = User.new(name: "Jason", avatar: { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }) - end - - assert_predicate @user, :new_record? - assert_predicate @user.avatar, :attached? - assert_equal "town.jpg", @user.avatar.filename.to_s - - @user.save! - assert_predicate @user.reload.avatar, :attached? - assert_equal "town.jpg", @user.avatar.filename.to_s - end - - test "access underlying associations of new blob" do - @user.avatar.attach create_blob(filename: "funky.jpg") - assert_equal @user, @user.avatar_attachment.record - assert_equal @user.avatar_attachment.blob, @user.avatar_blob - assert_equal "funky.jpg", @user.avatar_attachment.blob.filename.to_s - end - - test "identify newly-attached, directly-uploaded blob" do - blob = directly_upload_file_blob(content_type: "application/octet-stream") - - @user.avatar.attach(blob) - - assert_equal "image/jpeg", @user.avatar.reload.content_type - assert_predicate @user.avatar, :identified? - end - - test "identify and analyze newly-attached, directly-uploaded blob" do - blob = directly_upload_file_blob(content_type: "application/octet-stream") - - perform_enqueued_jobs do - @user.avatar.attach blob - end - - assert_equal true, @user.avatar.reload.metadata[:identified] - assert_equal 4104, @user.avatar.metadata[:width] - assert_equal 2736, @user.avatar.metadata[:height] - end - - test "identify newly-attached blob only once" do - blob = create_file_blob - assert_predicate blob, :identified? - - # The blob's backing file is a PNG image. Fudge its content type so we can tell if it's identified when we attach it. - blob.update! content_type: "application/octet-stream" - - @user.avatar.attach blob - assert_equal "application/octet-stream", blob.content_type - end - - test "analyze newly-attached blob" do - perform_enqueued_jobs do - @user.avatar.attach create_file_blob - end - - assert_equal 4104, @user.avatar.reload.metadata[:width] - assert_equal 2736, @user.avatar.metadata[:height] - end - - test "analyze attached blob only once" do - blob = create_file_blob - - perform_enqueued_jobs do - @user.avatar.attach blob - end - - assert_predicate blob.reload, :analyzed? - - @user.avatar.detach - - assert_no_enqueued_jobs do - @user.reload.avatar.attach blob - end - end - - test "preserve existing metadata when analyzing a newly-attached blob" do - blob = create_file_blob(metadata: { foo: "bar" }) - - perform_enqueued_jobs do - @user.avatar.attach blob - end - - assert_equal "bar", blob.reload.metadata[:foo] - end - - test "detach blob" do - @user.avatar.attach create_blob(filename: "funky.jpg") - avatar_blob_id = @user.avatar.blob.id - avatar_key = @user.avatar.key - - @user.avatar.detach - assert_not_predicate @user.avatar, :attached? - assert ActiveStorage::Blob.exists?(avatar_blob_id) - assert ActiveStorage::Blob.service.exist?(avatar_key) - end - - test "purge attached blob" do - @user.avatar.attach create_blob(filename: "funky.jpg") - avatar_key = @user.avatar.key - - @user.avatar.purge - assert_not_predicate @user.avatar, :attached? - assert_not ActiveStorage::Blob.service.exist?(avatar_key) - end - - test "purge attached blob later when the record is destroyed" do - @user.avatar.attach create_blob(filename: "funky.jpg") - avatar_key = @user.avatar.key - - perform_enqueued_jobs do - @user.reload.destroy - - assert_nil ActiveStorage::Blob.find_by(key: avatar_key) - assert_not ActiveStorage::Blob.service.exist?(avatar_key) - end - end - - test "delete attachment for independent blob when record is destroyed" do - @user.cover_photo.attach create_blob(filename: "funky.jpg") - - @user.destroy - assert_not ActiveStorage::Attachment.exists?(record: @user, name: "cover_photo") - end - - test "find with attached blob" do - records = %w[alice bob].map do |name| - User.create!(name: name).tap do |user| - user.avatar.attach create_blob(filename: "#{name}.jpg") - end - end - - users = User.where(id: records.map(&:id)).with_attached_avatar.all - - assert_equal "alice.jpg", users.first.avatar.filename.to_s - assert_equal "bob.jpg", users.second.avatar.filename.to_s - end - - - test "attach existing blobs" do - @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg") - - assert_equal "funky.jpg", @user.highlights.first.filename.to_s - assert_equal "wonky.jpg", @user.highlights.second.filename.to_s - end - - test "attach new blobs" do - @user.highlights.attach( - { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }, - { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }) - - assert_equal "town.jpg", @user.highlights.first.filename.to_s - assert_equal "country.jpg", @user.highlights.second.filename.to_s - end - - test "attach blobs to new record" do - user = User.new(name: "Jason") - - assert_no_changes -> { user.new_record? } do - assert_no_difference -> { ActiveStorage::Attachment.count } do - user.highlights.attach( - { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }, - { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }) - end - end - - assert_predicate user.highlights, :attached? - assert_equal "town.jpg", user.highlights.first.filename.to_s - assert_equal "country.jpg", user.highlights.second.filename.to_s - - assert_difference -> { ActiveStorage::Attachment.count }, +2 do - user.save! - end - - assert_predicate user.reload.highlights, :attached? - assert_equal "town.jpg", user.highlights.first.filename.to_s - assert_equal "country.jpg", user.highlights.second.filename.to_s - end - - test "build new record with attached blobs" do - assert_no_difference -> { ActiveStorage::Attachment.count } do - @user = User.new(name: "Jason", highlights: [ - { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }, - { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }]) - end - - assert_predicate @user, :new_record? - assert_predicate @user.highlights, :attached? - assert_equal "town.jpg", @user.highlights.first.filename.to_s - assert_equal "country.jpg", @user.highlights.second.filename.to_s - - @user.save! - assert_predicate @user.reload.highlights, :attached? - assert_equal "town.jpg", @user.highlights.first.filename.to_s - assert_equal "country.jpg", @user.highlights.second.filename.to_s - end - - test "find attached blobs" do - @user.highlights.attach( - { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }, - { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }) - - highlights = User.where(id: @user.id).with_attached_highlights.first.highlights - - assert_equal "town.jpg", highlights.first.filename.to_s - assert_equal "country.jpg", highlights.second.filename.to_s - end - - test "access underlying associations of new blobs" do - @user.highlights.attach( - { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }, - { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }) - - assert_equal @user, @user.highlights_attachments.first.record - assert_equal @user.highlights_attachments.collect(&:blob).sort, @user.highlights_blobs.sort - assert_equal "town.jpg", @user.highlights_attachments.first.blob.filename.to_s - end - - test "analyze newly-attached blobs" do - perform_enqueued_jobs do - @user.highlights.attach( - create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg"), - create_file_blob(filename: "video.mp4", content_type: "video/mp4")) - end - - assert_equal 4104, @user.highlights.first.metadata[:width] - assert_equal 2736, @user.highlights.first.metadata[:height] - - assert_equal 640, @user.highlights.second.metadata[:width] - assert_equal 480, @user.highlights.second.metadata[:height] - end - - test "analyze attached blobs only once" do - blobs = [ - create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg"), - create_file_blob(filename: "video.mp4", content_type: "video/mp4") - ] - - perform_enqueued_jobs do - @user.highlights.attach(blobs) - end - - assert blobs.each(&:reload).all?(&:analyzed?) - - @user.highlights.attachments.destroy_all - - assert_no_enqueued_jobs do - @user.highlights.attach(blobs) - end - end - - test "preserve existing metadata when analyzing newly-attached blobs" do - blobs = [ - create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: { foo: "bar" }), - create_file_blob(filename: "video.mp4", content_type: "video/mp4", metadata: { foo: "bar" }) - ] - - perform_enqueued_jobs do - @user.highlights.attach(blobs) - end - - blobs.each do |blob| - assert_equal "bar", blob.reload.metadata[:foo] - end - end - - test "detach blobs" do - @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg") - highlight_blob_ids = @user.highlights.collect { |highlight| highlight.blob.id } - highlight_keys = @user.highlights.collect(&:key) - - @user.highlights.detach - assert_not_predicate @user.highlights, :attached? - - assert ActiveStorage::Blob.exists?(highlight_blob_ids.first) - assert ActiveStorage::Blob.exists?(highlight_blob_ids.second) - - assert ActiveStorage::Blob.service.exist?(highlight_keys.first) - assert ActiveStorage::Blob.service.exist?(highlight_keys.second) - end - - test "purge attached blobs" do - @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg") - highlight_keys = @user.highlights.collect(&:key) - - @user.highlights.purge - assert_not_predicate @user.highlights, :attached? - assert_not ActiveStorage::Blob.service.exist?(highlight_keys.first) - assert_not ActiveStorage::Blob.service.exist?(highlight_keys.second) - end - - test "purge attached blobs later when the record is destroyed" do - @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg") - highlight_keys = @user.highlights.collect(&:key) - - perform_enqueued_jobs do - @user.reload.destroy - - assert_nil ActiveStorage::Blob.find_by(key: highlight_keys.first) - assert_not ActiveStorage::Blob.service.exist?(highlight_keys.first) - - assert_nil ActiveStorage::Blob.find_by(key: highlight_keys.second) - assert_not ActiveStorage::Blob.service.exist?(highlight_keys.second) - end - end - - test "delete attachments for independent blobs when the record is destroyed" do - @user.vlogs.attach create_blob(filename: "funky.mp4"), create_blob(filename: "wonky.mp4") - - @user.destroy - assert_not ActiveStorage::Attachment.exists?(record: @user, name: "vlogs") - end - - test "selectively purge one attached blob of many" do - first_blob = create_blob(filename: "funky.jpg") - second_blob = create_blob(filename: "wonky.jpg") - attachments = @user.highlights.attach(first_blob, second_blob) - - assert_difference -> { ActiveStorage::Blob.count }, -1 do - @user.highlights.where(id: attachments.first.id).purge - end - - assert_not ActiveStorage::Blob.exists?(key: first_blob.key) - assert ActiveStorage::Blob.exists?(key: second_blob.key) - end - - test "selectively purge one attached blob of many later" do - first_blob = create_blob(filename: "funky.jpg") - second_blob = create_blob(filename: "wonky.jpg") - attachments = @user.highlights.attach(first_blob, second_blob) - - perform_enqueued_jobs do - assert_difference -> { ActiveStorage::Blob.count }, -1 do - @user.highlights.where(id: attachments.first.id).purge_later - end - end - - assert_not ActiveStorage::Blob.exists?(key: first_blob.key) - assert ActiveStorage::Blob.exists?(key: second_blob.key) - end -end diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index 40b30acd3e..c2e7aae13a 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -7,21 +7,15 @@ require "active_support/testing/method_call_assertions" class ActiveStorage::BlobTest < ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions - test ".unattached scope returns not attached blobs" do - class UserWithHasOneAttachedDependentFalse < User - has_one_attached :avatar, dependent: false + test "unattached scope" do + [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs| + User.create! name: "DHH", avatar: blobs.first + assert_includes ActiveStorage::Blob.unattached, blobs.second + assert_not_includes ActiveStorage::Blob.unattached, blobs.first + + User.create! name: "Jason", avatar: blobs.second + assert_not_includes ActiveStorage::Blob.unattached, blobs.second end - - ActiveStorage::Blob.delete_all - blob_1 = create_blob filename: "funky.jpg" - blob_2 = create_blob filename: "town.jpg" - - user = UserWithHasOneAttachedDependentFalse.create! - user.avatar.attach blob_1 - - assert_equal [blob_2], ActiveStorage::Blob.unattached - user.destroy - assert_equal [blob_1, blob_2].map(&:id).sort, ActiveStorage::Blob.unattached.pluck(:id).sort end test "create after upload sets byte size and checksum" do @@ -180,6 +174,14 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase assert_not ActiveStorage::Blob.service.exist?(variant.key) end + test "purge fails when attachments exist" do + create_blob.tap do |blob| + User.create! name: "DHH", avatar: blob + assert_raises(ActiveRecord::InvalidForeignKey) { blob.purge } + assert ActiveStorage::Blob.service.exist?(blob.key) + end + end + private def expected_url_for(blob, disposition: :inline, filename: nil) filename ||= blob.filename diff --git a/activestorage/test/models/presence_validation_test.rb b/activestorage/test/models/presence_validation_test.rb index aa804506dd..13ba3c900d 100644 --- a/activestorage/test/models/presence_validation_test.rb +++ b/activestorage/test/models/presence_validation_test.rb @@ -12,7 +12,7 @@ class ActiveStorage::PresenceValidationTest < ActiveSupport::TestCase test "validates_presence_of has_one_attached" do Admin.validates_presence_of :avatar - a = Admin.new + a = Admin.new(name: "DHH") assert_predicate a, :invalid? a.avatar.attach create_blob(filename: "funky.jpg") @@ -21,7 +21,7 @@ class ActiveStorage::PresenceValidationTest < ActiveSupport::TestCase test "validates_presence_of has_many_attached" do Admin.validates_presence_of :highlights - a = Admin.new + a = Admin.new(name: "DHH") assert_predicate a, :invalid? a.highlights.attach create_blob(filename: "funky.jpg") diff --git a/activestorage/test/models/reflection_test.rb b/activestorage/test/models/reflection_test.rb index da866ca996..98606b0617 100644 --- a/activestorage/test/models/reflection_test.rb +++ b/activestorage/test/models/reflection_test.rb @@ -3,27 +3,32 @@ require "test_helper" class ActiveStorage::ReflectionTest < ActiveSupport::TestCase - test "allows reflecting for all attachment" do - expected_classes = - User.reflect_on_all_attachments.all? do |reflection| - reflection.is_a?(ActiveStorage::Reflection::HasOneAttachedReflection) || - reflection.is_a?(ActiveStorage::Reflection::HasManyAttachedReflection) - end - - assert expected_classes - end - - test "allows reflecting on a singular has_one_attached attachment" do + test "reflecting on a singular attachment" do reflection = User.reflect_on_attachment(:avatar) - + assert_equal User, reflection.active_record assert_equal :avatar, reflection.name assert_equal :has_one_attached, reflection.macro + assert_equal :purge_later, reflection.options[:dependent] end - test "allows reflecting on a singular has_many_attached attachment" do - reflection = User.reflect_on_attachment(:highlights) + test "reflection on a singular attachment with the same name as an attachment on another model" do + reflection = Group.reflect_on_attachment(:avatar) + assert_equal Group, reflection.active_record + end + test "reflecting on a collection attachment" do + reflection = User.reflect_on_attachment(:highlights) + assert_equal User, reflection.active_record assert_equal :highlights, reflection.name assert_equal :has_many_attached, reflection.macro + assert_equal :purge_later, reflection.options[:dependent] + end + + test "reflecting on all attachments" do + reflections = User.reflect_on_all_attachments.sort_by(&:name) + assert_equal [ User ], reflections.collect(&:active_record).uniq + assert_equal %i[ avatar cover_photo highlights vlogs ], reflections.collect(&:name) + assert_equal %i[ has_one_attached has_one_attached has_many_attached has_many_attached ], reflections.collect(&:macro) + assert_equal [ :purge_later, false, :purge_later, false ], reflections.collect { |reflection| reflection.options[:dependent] } end end diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index 573a8e0b0b..7b7926ac79 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -79,6 +79,10 @@ class ActiveSupport::TestCase def extract_metadata_from(blob) blob.tap(&:analyze).metadata end + + def fixture_file_upload(filename) + Rack::Test::UploadedFile.new file_fixture(filename).to_s + end end require "global_id" @@ -86,9 +90,15 @@ GlobalID.app = "ActiveStorageExampleApp" ActiveRecord::Base.send :include, GlobalID::Identification class User < ActiveRecord::Base + validates :name, presence: true + has_one_attached :avatar has_one_attached :cover_photo, dependent: false has_many_attached :highlights has_many_attached :vlogs, dependent: false end + +class Group < ActiveRecord::Base + has_one_attached :avatar +end diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index abb461966a..bac6ff9c97 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -75,11 +75,14 @@ class Hash # # This method is also aliased as +to_param+. def to_query(namespace = nil) - collect do |key, value| + query = collect do |key, value| unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? value.to_query(namespace ? "#{namespace}[#{key}]" : key) end - end.compact.sort! * "&" + end.compact + + query.sort! unless namespace.to_s.include?("[]") + query.join("&") end alias_method :to_param, :to_query diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb index dc8080c469..a6b096a973 100644 --- a/activesupport/lib/active_support/lazy_load_hooks.rb +++ b/activesupport/lib/active_support/lazy_load_hooks.rb @@ -68,7 +68,11 @@ module ActiveSupport if options[:yield] block.call(base) else - base.instance_eval(&block) + if base.is_a?(Module) + base.class_eval(&block) + else + base.instance_eval(&block) + end end end end diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 6a56da384f..b27ac7ce99 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -113,11 +113,23 @@ module ActiveSupport # post :create, params: { article: invalid_attributes } # end # + # A lambda can be passed in and evaluated. + # + # assert_no_difference -> { Article.count } do + # post :create, params: { article: invalid_attributes } + # end + # # An error message can be specified. # # assert_no_difference 'Article.count', 'An Article should not be created' do # post :create, params: { article: invalid_attributes } # end + # + # An array of expressions can also be passed in and evaluated. + # + # assert_no_difference [ 'Article.count', -> { Post.count } ] do + # post :create, params: { article: invalid_attributes } + # end def assert_no_difference(expression, message = nil, &block) assert_difference expression, 0, message, &block end diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 7593bcfa4d..b0b7ef0913 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -77,6 +77,20 @@ class ToQueryTest < ActiveSupport::TestCase assert_equal "name=Nakshay&type=human", hash.to_query end + def test_hash_not_sorted_lexicographically_for_nested_structure + params = { + "foo" => { + "contents" => [ + { "name" => "gorby", "id" => "123" }, + { "name" => "puff", "d" => "true" } + ] + } + } + expected = "foo[contents][][name]=gorby&foo[contents][][id]=123&foo[contents][][name]=puff&foo[contents][][d]=true" + + assert_equal expected, URI.decode(params.to_query) + end + private def assert_query_equal(expected, actual) assert_equal expected.split("&"), actual.to_query.split("&") diff --git a/activesupport/test/lazy_load_hooks_test.rb b/activesupport/test/lazy_load_hooks_test.rb index 721d44d0c1..50a703e49f 100644 --- a/activesupport/test/lazy_load_hooks_test.rb +++ b/activesupport/test/lazy_load_hooks_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "abstract_unit" +require "active_support/core_ext/module/remove_method" class LazyLoadHooksTest < ActiveSupport::TestCase def test_basic_hook @@ -125,6 +126,54 @@ class LazyLoadHooksTest < ActiveSupport::TestCase assert_equal 7, i end + def test_hook_uses_class_eval_when_base_is_a_class + ActiveSupport.on_load(:uses_class_eval) do + def first_wrestler + "John Cena" + end + end + + ActiveSupport.run_load_hooks(:uses_class_eval, FakeContext) + assert_equal "John Cena", FakeContext.new(0).first_wrestler + ensure + FakeContext.remove_possible_method(:first_wrestler) + end + + def test_hook_uses_class_eval_when_base_is_a_module + mod = Module.new + ActiveSupport.on_load(:uses_class_eval2) do + def last_wrestler + "Dwayne Johnson" + end + end + ActiveSupport.run_load_hooks(:uses_class_eval2, mod) + + klass = Class.new do + include mod + end + + assert_equal "Dwayne Johnson", klass.new.last_wrestler + end + + def test_hook_uses_instance_eval_when_base_is_an_instance + ActiveSupport.on_load(:uses_instance_eval) do + def second_wrestler + "Hulk Hogan" + end + end + + context = FakeContext.new(1) + ActiveSupport.run_load_hooks(:uses_instance_eval, context) + + assert_raises NoMethodError do + FakeContext.new(2).second_wrestler + end + assert_raises NoMethodError do + FakeContext.second_wrestler + end + assert_equal "Hulk Hogan", context.second_wrestler + end + private def incr_amt diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index 19901fad99..8698c66e6d 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -52,6 +52,22 @@ class AssertionsTest < ActiveSupport::TestCase assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message end + def test_assert_no_difference_with_multiple_expressions_pass + another_object = @object.dup + assert_no_difference ["@object.num", -> { another_object.num }] do + # ... + end + end + + def test_assert_no_difference_with_multiple_expressions_fail + another_object = @object.dup + assert_raises(Minitest::Assertion) do + assert_no_difference ["@object.num", -> { another_object.num }], "Another Object Changed" do + another_object.increment + end + end + end + def test_assert_difference assert_difference "@object.num", +1 do @object.increment diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index 4b0ea32d7c..09b4782eca 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -459,17 +459,18 @@ features out of the box. `ActiveModel::SecurePassword` provides a way to securely store any password in an encrypted form. When you include this module, a `has_secure_password` class method is provided which defines -a `password` accessor with certain validations on it. +a `password` accessor with certain validations on it by default. #### Requirements `ActiveModel::SecurePassword` depends on [`bcrypt`](https://github.com/codahale/bcrypt-ruby 'BCrypt'), so include this gem in your `Gemfile` to use `ActiveModel::SecurePassword` correctly. -In order to make this work, the model must have an accessor named `password_digest`. -The `has_secure_password` will add the following validations on the `password` accessor: +In order to make this work, the model must have an accessor named `XXX_digest`. +Where `XXX` is the attribute name of your desired password. +The following validations are added automatically: 1. Password should be present. -2. Password should be equal to its confirmation (provided `password_confirmation` is passed along). +2. Password should be equal to its confirmation (provided `XXX_confirmation` is passed along). 3. The maximum length of a password is 72 (required by `bcrypt` on which ActiveModel::SecurePassword depends) #### Examples @@ -478,7 +479,9 @@ The `has_secure_password` will add the following validations on the `password` a class Person include ActiveModel::SecurePassword has_secure_password - attr_accessor :password_digest + has_secure_password :recovery_password, validations: false + + attr_accessor :password_digest, :recovery_password_digest end person = Person.new @@ -502,4 +505,17 @@ person.valid? # => true # When all validations are passed. person.password = person.password_confirmation = 'aditya' person.valid? # => true + +person.recovery_password = "42password" + +person.authenticate('aditya') # => person +person.authenticate('notright') # => false +person.authenticate_password('aditya') # => person +person.authenticate_password('notright') # => false + +person.authenticate_recovery_password('42password') # => person +person.authenticate_recovery_password('notright') # => false + +person.password_digest # => "$2a$04$gF8RfZdoXHvyTjHhiU4ZsO.kQqV9oonYZu31PRE4hLQn3xM2qkpIy" +person.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC" ``` diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 0f74daace6..379c0925b5 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -184,9 +184,9 @@ class Company < ApplicationRecord after_touch :log_when_employees_or_company_touched private - def log_when_employees_or_company_touched - puts 'Employee/Company was touched' - end + def log_when_employees_or_company_touched + puts 'Employee/Company was touched' + end end >> @employee = Employee.last @@ -194,8 +194,8 @@ end # triggers @employee.company.touch >> @employee.touch -Employee/Company was touched An Employee was touched +Employee/Company was touched => true ``` diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 944cee8a23..6c57cd8b8e 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1777,6 +1777,12 @@ Client.pluck(:name) # => ["David", "Jeremy", "Jose"] ``` +You are not limited to querying fields from a single table, you can query multiple tables as well. + +``` +Client.joins(:comments, :categories).pluck("clients.email, comments.title, categories.name") +``` + Furthermore, unlike `select` and other `Relation` scopes, `pluck` triggers an immediate query, and thus cannot be chained with any further scopes, although it can work with scopes already constructed earlier: diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index c7846a0283..3c51313906 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -1133,24 +1133,6 @@ person.errors.full_messages # => ["Name cannot contain the characters !@#%*()_-+="] ``` -An equivalent to `errors#add` is to use `<<` to append a message to the `errors.messages` array for an attribute: - -```ruby - class Person < ApplicationRecord - def a_method_used_for_validation_purposes - errors.messages[:name] << "cannot contain the characters !@#%*()_-+=" - end - end - - person = Person.create(name: "!@#") - - person.errors[:name] - # => ["cannot contain the characters !@#%*()_-+="] - - person.errors.to_a - # => ["Name cannot contain the characters !@#%*()_-+="] -``` - ### `errors.details` You can specify a validator type to the returned error details hash using the diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index e7408b5a7f..67844ae7a4 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -96,7 +96,7 @@ end ![belongs_to Association Diagram](images/association_basics/belongs_to.png) -NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `author` association in the `Book` model, you would be told that there was an "uninitialized constant Book::Authors". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too. +NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `author` association in the `Book` model and tried to create the instance by `Book.create(authors: @author)`, you would be told that there was an "uninitialized constant Book::Authors". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too. The corresponding migration might look like this: diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 7c629f74c8..0c2b66da57 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -457,65 +457,90 @@ More information about migrations can be found in the [Migrations](active_record ### `notes` -`bin/rails notes` will search through your code for comments beginning with FIXME, OPTIMIZE, or TODO. The search is done in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js`, and `.erb` for both default and custom annotations. +`bin/rails notes` searches through your code for comments beginning with a specific keyword. You can refer to `bin/rails notes --help` for information about usage. + +By default, it will search in `app`, `config`, `db`, `lib`, and `test` directories for FIXME, OPTIMIZE, and TODO annotations in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js`, and `.erb`. ```bash $ bin/rails notes -(in /home/foobar/commandsapp) app/controllers/admin/users_controller.rb: * [ 20] [TODO] any other way to do this? * [132] [FIXME] high priority for next deploy -app/models/school.rb: +lib/school.rb: * [ 13] [OPTIMIZE] refactor this code to make it faster * [ 17] [FIXME] ``` -You can add support for new file extensions using `config.annotations.register_extensions` option, which receives a list of the extensions with its corresponding regex to match it up. - -```ruby -config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } -``` +#### Annotations -If you are looking for a specific annotation, say FIXME, you can use `bin/rails notes:fixme`. Note that you have to lower case the annotation's name. +You can pass specific annotations by using the `--annotations` argument. By default, it will search for FIXME, OPTIMIZE, and TODO. +Note that annotations are case sensitive. ```bash -$ bin/rails notes:fixme -(in /home/foobar/commandsapp) +$ bin/rails notes --annotations FIXME RELEASE app/controllers/admin/users_controller.rb: - * [132] high priority for next deploy + * [101] [RELEASE] We need to look at this before next release + * [132] [FIXME] high priority for next deploy -app/models/school.rb: - * [ 17] +lib/school.rb: + * [ 17] [FIXME] ``` -You can also use custom annotations in your code and list them using `bin/rails notes:custom` by specifying the annotation using an environment variable `ANNOTATION`. +#### Directories + +You can add more default directories to search from by using `config.annotations.register_directories`. It receives a list of directory names. + +```ruby +config.annotations.register_directories("spec", "vendor") +``` ```bash -$ bin/rails notes:custom ANNOTATION=BUG -(in /home/foobar/commandsapp) -app/models/article.rb: - * [ 23] Have to fix this one before pushing! +$ bin/rails notes +app/controllers/admin/users_controller.rb: + * [ 20] [TODO] any other way to do this? + * [132] [FIXME] high priority for next deploy + +lib/school.rb: + * [ 13] [OPTIMIZE] Refactor this code to make it faster + * [ 17] [FIXME] + +spec/models/user_spec.rb: + * [122] [TODO] Verify the user that has a subscription works + +vendor/tools.rb: + * [ 56] [TODO] Get rid of this dependency ``` -NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. +#### Extensions -By default, `rails notes` will look in the `app`, `config`, `db`, `lib`, and `test` directories. If you would like to search other directories, you can configure them using `config.annotations.register_directories` option. +You can add more default file extensions to search from by using `config.annotations.register_extensions`. It receives a list of extensions with its corresponding regex to match it up. ```ruby -config.annotations.register_directories("spec", "vendor") +config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } ``` -You can also provide them as a comma separated list in the environment variable `SOURCE_ANNOTATION_DIRECTORIES`. - ```bash -$ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor' $ bin/rails notes -(in /home/foobar/commandsapp) -app/models/user.rb: - * [ 35] [FIXME] User should have a subscription at this point +app/controllers/admin/users_controller.rb: + * [ 20] [TODO] any other way to do this? + * [132] [FIXME] high priority for next deploy + +app/assets/stylesheets/application.css.sass: + * [ 34] [TODO] Use pseudo element for this class + +app/assets/stylesheets/application.css.scss: + * [ 1] [TODO] Split into multiple components + +lib/school.rb: + * [ 13] [OPTIMIZE] Refactor this code to make it faster + * [ 17] [FIXME] + spec/models/user_spec.rb: * [122] [TODO] Verify the user that has a subscription works + +vendor/tools.rb: + * [ 56] [TODO] Get rid of this dependency ``` ### `routes` diff --git a/guides/source/engines.md b/guides/source/engines.md index 9dbce5d09b..470bb50ba7 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -313,6 +313,9 @@ The engine that this guide covers provides submitting articles and commenting functionality and follows a similar thread to the [Getting Started Guide](getting_started.html), with some new twists. +NOTE: For this section, make sure to run the commands in the root of the +`blorgh` engine's directory. + ### Generating an Article Resource The first thing to generate for a blog engine is the `Article` model and related @@ -695,7 +698,7 @@ pre-defined path which may be customizable. The engine contains migrations for the `blorgh_articles` and `blorgh_comments` table which need to be created in the application's database so that the engine's models can query them correctly. To copy these migrations into the -application run the following command from the `test/dummy` directory of your Rails engine: +application run the following command from the application's root: ```bash $ bin/rails blorgh:install:migrations diff --git a/guides/source/initialization.md b/guides/source/initialization.md index d3b122c7fe..65222aabda 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -45,7 +45,7 @@ load Gem.bin_path('railties', 'rails', version) ``` If you try out this command in a Rails console, you would see that this loads -`railties/exe/rails`. A part of the file `railties/exe/rails.rb` has the +`railties/exe/rails`. A part of the file `railties/exe/rails` has the following code: ```ruby diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 83a57a8c6a..81cb5a6c9f 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,22 @@ +* Deprecate `rails notes` subcommands in favor of passing an `annotations` argument to `rails notes`. + + The following subcommands are replaced by passing `--annotations` or `-a` to `rails notes`: + - `rails notes:custom ANNOTATION=custom` is deprecated in favor of using `rails notes -a custom`. + - `rails notes:optimize` is deprecated in favor of using `rails notes -a OPTIMIZE`. + - `rails notes:todo` is deprecated in favor of using`rails notes -a TODO`. + - `rails notes:fixme` is deprecated in favor of using `rails notes -a FIXME`. + + *Annie-Claude Côté* + +* Deprecate `SOURCE_ANNOTATION_DIRECTORIES` environment variable used by `rails notes` + through `Rails::SourceAnnotationExtractor::Annotation` in favor of using `config.annotations.register_directories`. + + *Annie-Claude Côté* + +* Deprecate `rake notes` in favor of `rails notes`. + + *Annie-Claude Côté* + * Don't generate unused files in `app:update` task Skip the assets' initializer when sprockets isn't loaded. diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index c4b188aeee..04aaf6dd9a 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -127,7 +127,7 @@ module Rails initializer :set_routes_reloader_hook do |app| reloader = routes_reloader reloader.eager_load = app.config.eager_load - reloader.execute_if_updated + reloader.execute reloaders << reloader app.reloader.to_run do # We configure #execute rather than #execute_if_updated because if diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index 2ef09b4162..3ecb8e264e 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -7,7 +7,7 @@ module Rails class RoutesReloader attr_reader :route_sets, :paths attr_accessor :eager_load - delegate :updated?, to: :updater + delegate :execute_if_updated, :execute, :updated?, to: :updater def initialize @paths = [] @@ -19,31 +19,15 @@ module Rails clear! load_paths finalize! + route_sets.each(&:eager_load!) if eager_load ensure revert end - def execute - ret = updater.execute - route_sets.each(&:eager_load!) if eager_load - ret - end - - def execute_if_updated - if updated = updater.execute_if_updated - route_sets.each(&:eager_load!) if eager_load - end - updated - end - private def updater - @updater ||= begin - updater = ActiveSupport::FileUpdateChecker.new(paths) { reload! } - updater.execute - updater - end + @updater ||= ActiveSupport::FileUpdateChecker.new(paths) { reload! } end def clear! diff --git a/railties/lib/rails/commands/notes/notes_command.rb b/railties/lib/rails/commands/notes/notes_command.rb new file mode 100644 index 0000000000..64b339b3cd --- /dev/null +++ b/railties/lib/rails/commands/notes/notes_command.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails/source_annotation_extractor" + +module Rails + module Command + class NotesCommand < Base # :nodoc: + class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: %w(OPTIMIZE FIXME TODO) + + def perform(*) + require_application_and_environment! + + deprecation_warning + display_annotations + end + + private + def display_annotations + annotations = options[:annotations] + tag = (annotations.length > 1) + + Rails::SourceAnnotationExtractor.enumerate annotations.join("|"), tag: tag, dirs: directories + end + + def directories + Rails::SourceAnnotationExtractor::Annotation.directories + source_annotation_directories + end + + def deprecation_warning + return if source_annotation_directories.empty? + ActiveSupport::Deprecation.warn("`SOURCE_ANNOTATION_DIRECTORIES` is deprecated and will be removed in Rails 6.1. You can add default directories by using config.annotations.register_directories instead.") + end + + def source_annotation_directories + ENV["SOURCE_ANNOTATION_DIRECTORIES"].to_s.split(",") + end + end + end +end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index cf17f0ef12..2c5440d9ec 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -97,10 +97,6 @@ module Rails end end - def restart_command - "bin/rails server #{ARGV.join(' ')}" - end - def use_puma? server.to_s == "Rack::Handler::Puma" end @@ -132,16 +128,18 @@ module Rails desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name class_option :pid, aliases: "-P", type: :string, default: DEFAULT_PID_PATH, desc: "Specifies the PID file." - class_option "dev-caching", aliases: "-C", type: :boolean, default: nil, + class_option :dev_caching, aliases: "-C", type: :boolean, default: nil, desc: "Specifies whether to perform caching in development." - class_option "restart", type: :boolean, default: nil, hide: true - class_option "early_hints", type: :boolean, default: nil, desc: "Enables HTTP/2 early hints." + class_option :restart, type: :boolean, default: nil, hide: true + class_option :early_hints, type: :boolean, default: nil, desc: "Enables HTTP/2 early hints." + class_option :log_to_stdout, type: :boolean, default: nil, optional: true, + desc: "Whether to log to stdout. Enabled by default in development when not daemonized." - def initialize(args = [], local_options = {}, config = {}) - @original_options = local_options + def initialize(args, local_options, *) super - @using = deprecated_positional_rack_server(using) || options[:using] - @log_stdout = options[:daemon].blank? && (options[:environment] || Rails.env) == "development" + + @original_options = local_options - %w( --restart ) + deprecate_positional_rack_server_and_rewrite_to_option(@original_options) end def perform @@ -169,7 +167,7 @@ module Rails { user_supplied_options: user_supplied_options, server: using, - log_stdout: @log_stdout, + log_stdout: log_to_stdout?, Port: port, Host: host, DoNotReverseLookup: true, @@ -177,7 +175,7 @@ module Rails environment: environment, daemonize: options[:daemon], pid: pid, - caching: options["dev-caching"], + caching: options[:dev_caching], restart_cmd: restart_command, early_hints: early_hints } @@ -210,7 +208,7 @@ module Rails name = :Port when :binding name = :Host - when :"dev-caching" + when :dev_caching name = :caching when :daemonize name = :daemon @@ -252,13 +250,19 @@ module Rails end def restart_command - "bin/rails server #{using} #{@original_options.join(" ")} --restart" + "bin/rails server #{@original_options.join(" ")} --restart" end def early_hints options[:early_hints] end + def log_to_stdout? + options.fetch(:log_to_stdout) do + options[:daemon].blank? && environment == "development" + end + end + def pid File.expand_path(options[:pid]) end @@ -271,14 +275,19 @@ module Rails FileUtils.rm_f(options[:pid]) if options[:restart] end - def deprecated_positional_rack_server(value) - if value - ActiveSupport::Deprecation.warn(<<-MSG.squish) + def deprecate_positional_rack_server_and_rewrite_to_option(original_options) + if using + ActiveSupport::Deprecation.warn(<<~MSG) Passing the Rack server name as a regular argument is deprecated and will be removed in the next Rails version. Please, use the -u option instead. MSG - value + + original_options.concat [ "-u", using ] + else + # Use positional internally to get around Thor's immutable options. + # TODO: Replace `using` occurences with `options[:using]` after deprecation removal. + @using = options[:using] end end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index f8460bd4ee..2a41403557 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -258,7 +258,6 @@ module Rails namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }] lookups.each do |namespace| - klass = namespaces[namespace] return klass if klass end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 34067240d7..76dbfadc0e 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -21,7 +21,6 @@ module Rails RUBY end - # TODO: Remove once this is fully in place def method_missing(meth, *args, &block) @generator.send(meth, *args, &block) end @@ -488,7 +487,11 @@ module Rails end def app_name - @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_") + @app_name ||= original_app_name.tr("-", "_") + end + + def original_app_name + @original_app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_") end def defined_app_name @@ -513,13 +516,13 @@ module Rails def valid_const? if app_const =~ /^\d/ - raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers." - elsif RESERVED_NAMES.include?(app_name) - raise Error, "Invalid application name #{app_name}. Please give a " \ + raise Error, "Invalid application name #{original_app_name}. Please give a name which does not start with numbers." + elsif RESERVED_NAMES.include?(original_app_name) + raise Error, "Invalid application name #{original_app_name}. Please give a " \ "name which does not match one of the reserved rails " \ "words: #{RESERVED_NAMES.join(", ")}" elsif Object.const_defined?(app_const_base) - raise Error, "Invalid application name #{app_name}, constant #{app_const_base} is already in use. Please choose another application name." + raise Error, "Invalid application name #{original_app_name}, constant #{app_const_base} is already in use. Please choose another application name." end end diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 7257aaeaae..2d66a4dc7d 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -8,12 +8,7 @@ SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy. new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor") module Rails - # Implements the logic behind the rake tasks for annotations like - # - # rails notes - # rails notes:optimize - # - # and friends. See <tt>rails -T notes</tt> and <tt>railties/lib/rails/tasks/annotations.rake</tt>. + # Implements the logic behind <tt>Rails::Command::NotesCommand</tt>. See <tt>rails notes --help</tt> for usage information. # # Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that # represent the line where the annotation lives, its tag, and its text. Note @@ -25,7 +20,7 @@ module Rails class SourceAnnotationExtractor class Annotation < Struct.new(:line, :tag, :text) def self.directories - @@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",") + @@directories ||= %w(app config db lib test) end # Registers additional directories to be included @@ -59,15 +54,19 @@ module Rails s << "[#{tag}] " if options[:tag] s << text end + + # Used in annotations.rake + #:nodoc: + def self.notes_task_deprecation_warning + ActiveSupport::Deprecation.warn("This rake task is deprecated and will be removed in Rails 6.1. \nRefer to `rails notes --help` for more information.\n") + puts "\n" + end end # Prints all annotations with tag +tag+ under the root directories +app+, # +config+, +db+, +lib+, and +test+ (recursively). # - # Additional directories may be added using a comma-delimited list set using - # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>. - # - # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+. + # Specific directories can be explicitly set using the <tt>:dirs</tt> key in +options+. # # Rails::SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true # @@ -75,7 +74,7 @@ module Rails # # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. # - # This class method is the single entry point for the rake tasks. + # This class method is the single entry point for the `rails notes` command. def self.enumerate(tag, options = {}) extractor = new(tag) dirs = options.delete(:dirs) || Annotation.directories diff --git a/railties/lib/rails/tasks/annotations.rake b/railties/lib/rails/tasks/annotations.rake index 60bcdc5e1b..65af778a15 100644 --- a/railties/lib/rails/tasks/annotations.rake +++ b/railties/lib/rails/tasks/annotations.rake @@ -2,21 +2,21 @@ require "rails/source_annotation_extractor" -desc "Enumerate all annotations (use notes:optimize, :fixme, :todo for focus)" task :notes do - Rails::SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", tag: true + Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning + Rails::Command.invoke :notes end namespace :notes do ["OPTIMIZE", "FIXME", "TODO"].each do |annotation| - # desc "Enumerate all #{annotation} annotations" task annotation.downcase.intern do - Rails::SourceAnnotationExtractor.enumerate annotation + Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning + Rails::Command.invoke :notes, ["--annotations", annotation] end end - desc "Enumerate a custom annotation, specify with ANNOTATION=CUSTOM" task :custom do - Rails::SourceAnnotationExtractor.enumerate ENV["ANNOTATION"] + Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning + Rails::Command.invoke :notes, ["--annotations", ENV["ANNOTATION"]] end end diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake index e219277d23..ec56957204 100644 --- a/railties/lib/rails/tasks/log.rake +++ b/railties/lib/rails/tasks/log.rake @@ -1,7 +1,6 @@ # frozen_string_literal: true namespace :log do - ## # Truncates all/specified log files # ENV['LOGS'] diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 4bd7d74b04..4e3ec184be 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -20,28 +20,29 @@ if defined?(ActiveRecord::Base) exit 1 end - module ActiveSupport - class TestCase - include ActiveRecord::TestDatabases - include ActiveRecord::TestFixtures - self.fixture_path = "#{Rails.root}/test/fixtures/" - self.file_fixture_path = fixture_path + "files" - end + ActiveSupport.on_load(:active_support_test_case) do + include ActiveRecord::TestDatabases + include ActiveRecord::TestFixtures + + self.fixture_path = "#{Rails.root}/test/fixtures/" + self.file_fixture_path = fixture_path + "files" end - ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path + ActiveSupport.on_load(:action_dispatch_integration_test) do + self.fixture_path = ActiveSupport::TestCase.fixture_path + end end # :enddoc: -class ActionController::TestCase +ActiveSupport.on_load(:action_controller_test_case) do def before_setup # :nodoc: @routes = Rails.application.routes super end end -class ActionDispatch::IntegrationTest +ActiveSupport.on_load(:action_dispatch_integration_test) do def before_setup # :nodoc: @routes = Rails.application.routes super diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index d73e5cdfa3..999155fa93 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -87,6 +87,9 @@ module ApplicationTests assert_equal 6, lines.size assert_equal [4], lines.map(&:size).uniq + + log = File.read(Rails.application.config.paths["log"].first) + assert_match(/`SOURCE_ANNOTATION_DIRECTORIES` is deprecated/, log) end end diff --git a/railties/test/commands/notes_test.rb b/railties/test/commands/notes_test.rb new file mode 100644 index 0000000000..147019e299 --- /dev/null +++ b/railties/test/commands/notes_test.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/command" +require "rails/commands/notes/notes_command" + +class Rails::Command::NotesTest < ActiveSupport::TestCase + setup :build_app + teardown :teardown_app + + test "`rails notes` displays results for default directories and default annotations with aligned line number and annotation tag" do + app_file "app/controllers/some_controller.rb", "# OPTIMIZE: note in app directory" + app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" + app_file "db/some_seeds.rb", "# FIXME: note in db directory" + app_file "lib/some_file.rb", "# TODO: note in lib directory" + app_file "test/some_test.rb", "\n" * 100 + "# FIXME: note in test directory" + + app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory" + + assert_equal <<~OUTPUT, run_notes_command + app/controllers/some_controller.rb: + * [ 1] [OPTIMIZE] note in app directory + + config/initializers/some_initializer.rb: + * [ 1] [TODO] note in config directory + + db/some_seeds.rb: + * [ 1] [FIXME] note in db directory + + lib/some_file.rb: + * [ 1] [TODO] note in lib directory + + test/some_test.rb: + * [101] [FIXME] note in test directory + + OUTPUT + end + + test "`rails notes` displays an empty string when no results were found" do + assert_equal "", run_notes_command + end + + test "`rails notes --annotations` displays results for a single annotation without being prefixed by a tag" do + app_file "db/some_seeds.rb", "# FIXME: note in db directory" + app_file "test/some_test.rb", "# FIXME: note in test directory" + + app_file "app/controllers/some_controller.rb", "# OPTIMIZE: note in app directory" + app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" + + assert_equal <<~OUTPUT, run_notes_command(["--annotations", "FIXME"]) + db/some_seeds.rb: + * [1] note in db directory + + test/some_test.rb: + * [1] note in test directory + + OUTPUT + end + + test "`rails notes --annotations` displays results for multiple annotations being prefixed by a tag" do + app_file "app/controllers/some_controller.rb", "# FOOBAR: note in app directory" + app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" + app_file "lib/some_file.rb", "# TODO: note in lib directory" + + app_file "test/some_test.rb", "# FIXME: note in test directory" + + assert_equal <<~OUTPUT, run_notes_command(["--annotations", "FOOBAR", "TODO"]) + app/controllers/some_controller.rb: + * [1] [FOOBAR] note in app directory + + config/initializers/some_initializer.rb: + * [1] [TODO] note in config directory + + lib/some_file.rb: + * [1] [TODO] note in lib directory + + OUTPUT + end + + test "displays results from additional directories added to the default directories from a config file" do + app_file "db/some_seeds.rb", "# FIXME: note in db directory" + app_file "lib/some_file.rb", "# TODO: note in lib directory" + app_file "spec/spec_helper.rb", "# TODO: note in spec" + app_file "spec/models/user_spec.rb", "# TODO: note in model spec" + + add_to_config "config.annotations.register_directories \"spec\"" + + assert_equal <<~OUTPUT, run_notes_command + db/some_seeds.rb: + * [1] [FIXME] note in db directory + + lib/some_file.rb: + * [1] [TODO] note in lib directory + + spec/models/user_spec.rb: + * [1] [TODO] note in model spec + + spec/spec_helper.rb: + * [1] [TODO] note in spec + + OUTPUT + end + + test "displays results from additional file extensions added to the default extensions from a config file" do + add_to_config "config.assets.precompile = []" + add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } } + app_file "db/some_seeds.rb", "# FIXME: note in db directory" + app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss" + app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass" + + assert_equal <<~OUTPUT, run_notes_command + app/assets/stylesheets/application.css.sass: + * [1] [TODO] note in sass + + app/assets/stylesheets/application.css.scss: + * [1] [TODO] note in scss + + db/some_seeds.rb: + * [1] [FIXME] note in db directory + + OUTPUT + end + + private + def run_notes_command(args = []) + rails "notes", args + end +end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index e7a56b3e6d..e5b1da6ea4 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -143,10 +143,22 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase options = parse_arguments(args) assert_equal true, options[:log_stdout] + args = ["-e", "development", "-d"] + options = parse_arguments(args) + assert_equal false, options[:log_stdout] + args = ["-e", "production"] options = parse_arguments(args) assert_equal false, options[:log_stdout] + args = ["-e", "development", "--no-log-to-stdout"] + options = parse_arguments(args) + assert_equal false, options[:log_stdout] + + args = ["-e", "production", "--log-to-stdout"] + options = parse_arguments(args) + assert_equal true, options[:log_stdout] + with_rack_env "development" do args = [] options = parse_arguments(args) @@ -245,10 +257,9 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase args = %w(-p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C) ARGV.replace args - options = parse_arguments(args) - expected = "bin/rails server -p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C --restart" + expected = "bin/rails server -p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C --restart" - assert_equal expected, options[:restart_cmd] + assert_equal expected, parse_arguments(args)[:restart_cmd] ensure ARGV.replace original_args end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index d8e9ae3369..b0f958091c 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -438,6 +438,30 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "config/application.rb", /\s+config\.load_defaults 5\.1/ end + def test_app_update_does_not_change_app_name_when_app_name_is_hyphenated_name + app_root = File.join(destination_root, "hyphenated-app") + run_generator [app_root, "-d", "postgresql"] + + assert_file "#{app_root}/config/database.yml" do |content| + assert_match(/hyphenated_app_development/, content) + assert_no_match(/hyphenated-app_development/, content) + end + + assert_file "#{app_root}/config/cable.yml" do |content| + assert_match(/hyphenated_app/, content) + assert_no_match(/hyphenated-app/, content) + end + + FileUtils.cd(app_root) do + quietly { system("bin/rails app:update") } + end + + assert_file "#{app_root}/config/cable.yml" do |content| + assert_match(/hyphenated_app/, content) + assert_no_match(/hyphenated-app/, content) + end + end + def test_application_names_are_not_singularized run_generator [File.join(destination_root, "hats")] assert_file "hats/config/environment.rb", /Rails\.application\.initialize!/ diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 9a3ddc8d5e..4ac8f8d741 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -570,7 +570,6 @@ YAML get("/arunagw") assert_equal "arunagw", last_response.body - end test "it provides routes as default endpoint" do |