Bundler + Rails: realpath expand_path and symlinked vendor/bundle cause ‘already loaded’ errors

Symlinked vendor/bundle results in double load errors since:

  • rails adds the realpath of each engines lib (and various other folders) to the $LOAD_PATH
  • bundler adds the symlinked version to the $LOAD_PATH
  • require_relative uses the realpath

which looks like: `already initialized constant Arturo::Middleware::MISSING_FEATURE_ERROR`

Reproduce:

  • bundle install –path vendor/bundle
  • mv vendor/bundle tmp
  • cd vendor && ln -s ../tmp/bundle bundle
  • enable eager_load + preload_frameworks in config/environment/development.rb
  • rails runner 1

Ruby issue

Patch:

# Gemfile
require_relative 'lib/bundler_realpath'
 
# lib/bundler_realpath.rb
# https://grosser.it/2017/08/19/bundler-rails-realpath-expand_path-and-symlinked-vendorbundle-cause-already-loaded-errors
Bundler::Runtime.prepend(Module.new do
  def setup(*)
    super
  ensure
    linked_bundle = File.expand_path(Bundler.bundle_path)
    real_bundle = File.realpath(Bundler.bundle_path)
    if linked_bundle != real_bundle
      $LOAD_PATH.each_with_index do |path, i|
        $LOAD_PATH[i] = path.sub(linked_bundle, real_bundle)
      end
    end
  end
end)
Advertisements

Rails 5.1 do not compile asset in test vs asset is not present in the asset pipeline

We don’t want to compile assets during test runs, since that is slow, but we also don’t want the asset pipeline to fail because assets are missing.

This will not work if you plan on doing javascript integration tests, but everything else should work fine.

Rails 5.1 added a flag for this which prints deprecations and will be removed in rails 5.2 so that is not a elegant solution either.

config.assets.unknown_asset_fallback = true

So we are now using this fix to fake assets being available!:

# config/environments/test.rb
# make our tests fast by avoiding asset compilation
# but do not raise when assets are not compiled either
Rails.application.config.assets.compile = false
Sprockets::Rails::Helper.prepend(Module.new do
  def resolve_asset_path(path, *)
    super || path
  end
end)

Making Rails 4 and 3 share signed cookies

Rails 4 by default wants to upgrade all cookies, which makes┬árails 3 unable to read them. But we want that to work since we let rails 3 and 4 run in parallel to test performance (which is terrible on rails 4 … )

# While we run servers with rails 3 and rails 4 we don't want to encrypt our cookie
# once everything is on rails 4 we can by using the upgrade signed to encrypted strategy
# tested via test/integration/rails_compatibility_test.rb
if RAILS4
  ActionDispatch::Cookies::ChainedCookieJars.class_eval do
    def signed_or_encrypted
      signed
    end
  end

  # do not update ... compare to action_dispatch/middleware/cookies.rb:184
  ActionDispatch::Cookies::UpgradeLegacySignedCookieJar.class_eval do
    def initialize(*args)
      super
      @verifier = @legacy_verifier
    end

    def verify_and_upgrade_legacy_signed_message(name, signed_message)
      deserialize(name, @legacy_verifier.verify(signed_message))
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      nil
    end
  end
end

Render nested Rails object errors

  • No looping
  • Infinitely deep
  • Html safe

nested-errors-wordpress

  # application_helper.rb
  def render_nested_errors(object, seen=Set.new)
    return "" if seen.include?(object)
    seen << object
    return "" if object.errors.empty?

    content_tag :ul do
      lis = object.errors.map do |attribute, message|
        content_tag(:li) do
          content = "".html_safe
          content << object.errors.full_message(attribute, message)
          values = (object.respond_to?(attribute) ? Array.wrap(object.send(attribute)) : [])
          if values.first.is_a?(ActiveRecord::Base)
            values.each do |value|
              content << render_nested_errors(value, seen)
            end
          end
          content
        end
      end
      safe_join lis
    end
  end

  # application_helper_test.rb
  
  describe "#render_nested_errors" do
    # simulate what erb will do so we can see html_safe issues
    def render
      ERB::Util.html_escape(render_nested_errors(stage))
    end

    let(:stage) { stages(:test_staging) }

    it "renders nothing for valid" do
      render.must_equal ""
    end

    it "renders simple errors" do
      stage.errors.add(:base, "Kaboom")
      render.must_equal "<ul><li>Kaboom</li></ul>"
    end

    it "renders nested errors" do
      stage.errors.add(:deploy_groups, "Invalid") # happens on save normally .. not a helpful message for our users
      stage.errors.add(:base, "BASE") # other error to make sure nesting is correct
      stage.deploy_groups.to_a.first.errors.add(:base, "Kaboom")
      render.must_equal "<ul><li>Deploy groups Invalid<ul><li>Kaboom</li></ul></li><li>BASE</li></ul>"
    end

    it "does not loop" do
      stage.errors.add(:project, "Invalid")
      stage.project.stubs(stages: [stage])
      stage.project.errors.add(:stages, "Invalid")
      render.must_equal "<ul><li>Project Invalid<ul><li>Stages Invalid</li></ul></li></ul>"
    end

    it "cannot inject html" do
      stage.errors.add(:deploy_groups, "<foo>")
      stage.errors.add(:base, "<bar>")
      stage.deploy_groups.to_a.first.errors.add(:base, "<baz>")
      render.must_equal "<ul><li>Deploy groups &lt;foo&gt;<ul><li>&lt;baz&gt;</li></ul></li><li>&lt;bar&gt;</li></ul>"
    end
  end