WebBrick startup is slow if your machine name looks like a domain

Socket.gethostbyname is usually fast if your local machine has a normal name, because it crashes early, but if you have a name that looks like a real domain things take 5s.

Internally webbrick/config.rb does:

ruby -r socket -e 'Socket.gethostbyname(Socket.gethostname)'

Which is slow … wait for https://bugs.ruby-lang.org/issues/13007 to resolve … or rename your localhost to something that does not look like a domain to ruby.

Ruby Code Duplication Detection with Flay

Flay is terribly useful, but has terribly usability …

If the repo was not such a mess I’d make PRs to fix it, but tests are not even runnable and PRs to make the Readme readabe got rejected … so I’m not going to bother … a small excerpt from Samson
More config options can be found on it’s homepage
Just wanted to share this useful rake task which found a bunch of duplication and already prevented me from adding new duplication twice🙂

desc "Analyze for code duplication (large, identical syntax trees) with fuzzy matching."
task :flay do
  require 'flay' # do not require in production

  # FIXME: flay has a --mass option, but it ignores everything
  Flay.prepend(Module.new do
    def analyze(*)
      data = super
      data.select! { |i| i.mass > 50 }
      self.total = data.sum(&:mass)
      data
    end
  end)

  files = Dir["{config,lib,app/**/*.{rb,erb}"]
  files -= [
    # Things you want to ignore
  ]
  flay = Flay.run(files)
  abort "Code duplication found" if flay.report.any?
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
nested-errors-wordpress

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

Ad-hoc Rack Test Servers for Integration Tests

Boots up test servers so integration tests can connect to them (when running in the same process try webmock) … works for RSpec and Minitest (with minitest-around/maxitest or similar gem)

The server boot takes about 5 seconds, might be better with a different web-server, but WEBrick is simplest.

require 'rack'
require 'base64'
require 'json'
require 'open-uri'

describe 'CLI' do
  class FakeServer
    TEST_ENDPOINT = "/__ping__".freeze

    def self.open(port, replies)
      server = new(port, replies)
      server.boot
      yield server
    ensure
      server.shutdown
    end

    def initialize(port, replies)
      @port = port
      @replies = replies.merge(TEST_ENDPOINT => 'PONG')
    end

    def boot
      Thread.new do
        Rack::Handler::WEBrick.run(
          self,
          Port: @port,
          Logger: WEBrick::Log.new("/dev/null"),
          AccessLog: []
        ) { |s| @server = s }
      end
    end

    def wait
      loop do
        break if URI.parse("http://localhost:#{@port}#{TEST_ENDPOINT}").read rescue nil
      end
    end

    def call(env)
      path = env.fetch("PATH_INFO")
      unless reply = @replies[path]
        puts "Missing reply for path #{path}"
        raise
      end
      [200, {}, [reply.to_json]]
    end

    def shutdown
      @server.shutdown if @server
    end
  end

  let(:service_a_replies) {{'/v1/catalog/services' => {}}}
  let(:service_b_replies) {{'/api/v1/nodes' => {}}}

  around do |test|
    FakeServer.open(8500, service_a_replies) do |a|
      FakeServer.open(9000, service_b_replies) do |b|
        a.wait
        b.wait
        test.call
      end
    end
  end

  it "works" do
    ...
  end
end