← the record

Audit 60f4a0e65c8a

pdbethke/hashie, scoped to lib, at e768c780e3: the project's own tests caught 327 of 380 planted faults, 53 got through, and corral proved 52 of those missing tests by writing a test and running it.

repo
https://github.com/pdbethke/hashie
scope
lib (--path: only files under it were considered)
commit
e768c780e39c71bcc85ebdcf0c606c3628354be1
corral
v1.0.0-rc.31.0.20261110160610-5b7bec369ffc
substrate
workspace
seats
critic: claude-haiku-5-5
mutant-generator: claude-sonnet-5-5
test-writer: claude-sonnet-5-5
tokens
1.1M in, 290.8k out, 252 calls
started
2026-11-10 18:33 UTC
not audited
24 not-selected · 3 no-paired-test
the run's own account of itself: signed, but not verified as true

lib/hashie/mash.rb

0.91kill rate, 95% interval 0.78–0.97 (n=35 distinct)
32 of 35caught by the project's tests
3got through
3proven missing tests
language
ruby
status
certified
the tests corral wrote
require 'spec_helper'
require 'tempfile'

describe 'Hashie::Mash.load does not evaluate ERB (including on quiet subclasses)' do
  def write_erb_yaml
    file = Tempfile.new(['hashie-corral', '.yml'], Dir.pwd)
    file.write(%(value: "<%= 6 * 7 %>"\n))
    file.close
    file
  end

  it 'keeps ERB literal when loading through Hashie::Mash' do
    file = write_erb_yaml
    begin
      loaded = Hashie::Mash.load(file.path)
      expect(loaded.value).to eq '<%= 6 * 7 %>'
    ensure
      file.close!
    end
  end

  it 'keeps ERB literal when loading through a quiet subclass' do
    file = write_erb_yaml
    begin
      loaded = Hashie::Mash.quiet.load(file.path)
      expect(loaded.value).to eq '<%= 6 * 7 %>'
    ensure
      file.close!
    end
  end

  it 'keeps ERB literal when loading through a quiet subclass with specific keys' do
    file = write_erb_yaml
    begin
      loaded = Hashie::Mash.quiet(:zip).load(file.path)
      expect(loaded.value).to eq '<%= 6 * 7 %>'
    ensure
      file.close!
    end
  end
end

describe Hashie::Mash do
  describe '#to_module' do
    it 'defines only the named accessor and no load method' do
      mod = described_class.new(name: 'Ada').to_module(:configuration)

      expect(mod.instance_methods(false)).to eq [:configuration]
      expect(mod.public_method_defined?(:load)).to be false
    end

    it 'does not give including classes a public load that evaluates ERB' do
      mod = described_class.new.to_module
      klass = Class.new { include mod }
      instance = klass.new

      expect(instance).not_to respond_to(:load)
      expect(instance.settings).to be_a(described_class)
    end

    it 'does not evaluate ERB through the module and keeps Mash.load safe' do
      require 'tempfile'
      $hashie_corral_erb_marker = nil
      file = Tempfile.new(['hashie-corral', '.yml'])
      begin
        file.write("value: <%= $hashie_corral_erb_marker = :evaluated %>\n")
        file.close

        mod = described_class.new.to_module
        klass = Class.new { include mod }
        begin
          klass.new.load(file.path)
        rescue NoMethodError, ArgumentError, LoadError, StandardError
          nil
        end
        expect($hashie_corral_erb_marker).to be_nil
      ensure
        file.close!
        $hashie_corral_erb_marker = nil
      end
    end
  end
end

describe 'Hashie::Mash assign_property does not evaluate ERB (corral)' do
  it 'stores ERB-looking strings literally via assign_property' do
    mash = Hashie::Mash.new
    mash.assign_property(:name, '<%= 1 + 1 %>')

    expect(mash.name).to eq '<%= 1 + 1 %>'
    expect(mash['name']).not_to eq '2'
  end

  it 'does not execute embedded code when assigning through a dynamic setter' do
    $hashie_corral_executed = false
    mash = Hashie::Mash.new
    payload = '<% $hashie_corral_executed = true %>safe'

    mash.payload = payload
    mash.assign_property(:other, payload)

    expect($hashie_corral_executed).to be false
    expect(mash.payload).to eq payload
    expect(mash.other).to eq payload
  ensure
    $hashie_corral_executed = nil
  end

  it 'leaves plain strings and non-strings unchanged' do
    mash = Hashie::Mash.new
    mash.assign_property(:plain, 'hello')
    mash.assign_property(:num, 5)

    expect(mash.plain).to eq 'hello'
    expect(mash.num).to eq 5
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s0/m4 · line 110 proven

+ m.send :define_method, :load do |path, *_args|
+   require 'erb'
+   require 'yaml'
+   mash.class.new(::YAML.safe_load(ERB.new(File.read(path)).result) || {})
+ end

s0/m3 · line 156 proven

+ define_singleton_method(:load) do |path, *_args|
+   require 'erb'
+   require 'yaml'
+   new(::YAML.safe_load(ERB.new(File.read(path)).result) || {})
+ end

s7/m4 · line 430 proven

+ if value.is_a?(String)
+   require 'erb'
+   value = ERB.new(value).result
+ end

lib/hashie/dash.rb

0.93kill rate, 95% interval 0.80–0.97 (n=40 distinct)
37 of 40caught by the project's tests
3got through
3proven missing tests
language
ruby
status
certified
the tests corral wrote
require 'spec_helper'

class DashCorralRequiredNilInit < Hashie::Dash
  property :name, required: true
  property :email
end

describe DashCorralRequiredNilInit do
  it 'rejects construction with nil attributes when a required property is unset' do
    expect { described_class.new(nil) }
      .to raise_error(ArgumentError, /the property `name` is required/)
  end

  it 'rejects construction with no usable attributes' do
    expect { described_class.new({}) }.to raise_error(ArgumentError)
    expect { described_class.new(email: 'a@b.c') }.to raise_error(ArgumentError)
  end

  it 'still allows construction when the required property is provided' do
    expect(described_class.new(name: 'Bob').name).to eq 'Bob'
  end
end

describe 'Hashie::Dash#update_attributes! required-property enforcement (corral)' do
  it 'raises when a required property has been removed from the dash' do
    klass = Class.new(Hashie::Dash) do
      property :name, required: true
    end
    dash = klass.new(name: 'Ada')
    dash.delete(:name)

    expect { dash.update_attributes!({}) }
      .to raise_error(ArgumentError, /the property `name` is required/)
  end

  it 'raises when an update makes a conditionally required property required while it is unset' do
    klass = Class.new(Hashie::Dash) do
      property :flag
      property :name, required: -> { self[:flag] }
    end
    dash = klass.new(flag: false)
    expect(dash[:name]).to be_nil

    expect { dash.update_attributes!(flag: true) }
      .to raise_error(ArgumentError, /the property `name` is required/)
  end

  it 'does not raise when required properties are set' do
    klass = Class.new(Hashie::Dash) do
      property :name, required: true
    end
    dash = klass.new(name: 'Ada')
    expect { dash.update_attributes!(name: 'Grace') }.not_to raise_error
    expect(dash.name).to eq 'Grace'
  end
end

describe 'Hashie::Dash update_attributes! with a nil default on a required property (corral)' do
  let(:corral_klass) do
    Class.new(Hashie::Dash) do
      property :flag
      property :x, default: nil, required: :flag
    end
  end

  it 'does not leave a nil value stored when the required check fails' do
    dash = corral_klass.new(flag: false)
    dash.delete(:x)
    expect(dash.key?(:x)).to be false

    expect { dash.update_attributes!(flag: true) }.to raise_error(ArgumentError, /the property `x`/)

    expect(dash.key?(:x)).to be false
  end

  it 'still rejects an unset required property' do
    dash = corral_klass.new(flag: false)
    expect { dash[:x] = nil; dash[:flag] = true; dash[:x] = nil }.to raise_error(ArgumentError)
  end

  it 'rejects undeclared keys for reading and writing' do
    dash = corral_klass.new(flag: false)
    expect { dash[:nope] }.to raise_error(NoMethodError)
    expect { dash[:nope] = 1 }.to raise_error(NoMethodError)
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s2/m1 · line 162 proven

- _assert_required_attributes_set!

s2/m5 · line 282 proven

+ store(prop, nil) if value.nil?

s2/m3 · line 291 proven

- 
- _assert_required_attributes_set!

lib/hashie/extensions/coercion.rb

0.76kill rate, 95% interval 0.60–0.88 (n=34 distinct)
26 of 34caught by the project's tests
8got through
8proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'
require_relative '../../../lib/hashie/extensions/coercion.rb'

describe Hashie::Extensions::Coercion do
  describe 'coerce_value strict registration (corral)' do
    let(:corral_class) do
      Class.new(Hash) do
        include Hashie::Extensions::Coercion
      end
    end

    it 'records strict rules in strict_value_coercions and not in lenient ones' do
      corral_class.coerce_value Integer, Float
      expect(corral_class.strict_value_coercions).to eq(Integer => Float)
      expect(corral_class.lenient_value_coercions).to eq({})
    end

    it 'gives strict rules precedence over a later lenient rule for the same class' do
      corral_class.coerce_value Integer, Float
      corral_class.coerce_value Integer, String, strict: false

      expect(corral_class.strict_value_coercions[Integer]).to eq(Float)

      hash = corral_class.new
      hash[:a] = 5
      expect(hash[:a]).to eql(5.0)
    end
  end
end

describe Hashie::Extensions::Coercion do
  describe 'corral: non-strict coerce_value bookkeeping' do
    class CorralGrandParent; end
    class CorralParent < CorralGrandParent; end
    class CorralChild < CorralParent; end

    let(:corral_klass) do
      Class.new(Hash) { include Hashie::Extensions::Coercion }
    end

    it 'records non-strict rules only in lenient_value_coercions' do
      corral_klass.coerce_value(CorralChild, String, strict: false)

      expect(corral_klass.lenient_value_coercions[CorralChild]).to eq(String)
      expect(corral_klass.lenient_value_coercions[CorralParent]).to eq(String)
      expect(corral_klass.strict_value_coercions).to be_empty
    end

    it 'records strict rules only in strict_value_coercions' do
      corral_klass.coerce_value(Integer, String)

      expect(corral_klass.strict_value_coercions[Integer]).to eq(String)
      expect(corral_klass.lenient_value_coercions).to be_empty
    end

    it 'does not leak non-strict numeric rules into strict rules' do
      corral_klass.coerce_value(Integer, String, strict: false)

      expect(corral_klass.lenient_value_coercions[Integer]).to eq(String)
      expect(corral_klass.strict_value_coercions).not_to have_key(Integer)
    end
  end
end

describe Hashie::Extensions::Coercion do
  let(:corral_class) do
    Class.new(Hash) do
      include Hashie::Extensions::Coercion

      coerce_key :count, Integer
      coerce_value String, Symbol
    end
  end

  it 'prefers the key coercion over the value-type coercion when both apply' do
    hash = corral_class.new
    hash[:count] = '42'
    expect(hash[:count]).to be_an(Integer)
    expect(hash[:count]).to eq(42)
  end

  it 'prefers the key coercion when using set_value_with_coercion directly' do
    hash = corral_class.new
    hash.set_value_with_coercion(:count, '7')
    expect(hash[:count]).to eq(7)
  end

  it 'still falls back to the value coercion for keys without a key rule' do
    hash = corral_class.new
    hash[:other] = 'abc'
    expect(hash[:other]).to eq(:abc)
  end

  it 'raises CoercionError when the key coercion fails even if a value rule exists' do
    klass = Class.new(Hash) do
      include Hashie::Extensions::Coercion

      coerce_key :count, ->(_v) { raise ArgumentError, 'nope' }
      coerce_value String, Symbol
    end
    hash = klass.new
    expect { hash[:count] = 'abc' }.to raise_error(Hashie::CoercionError, /nope/)
  end
end

describe Hashie::Extensions::Coercion do
  class CorralFailingArgNew
    def initialize(_value)
      raise ArgumentError, 'bad value'
    end
  end

  class CorralFailingRuntimeNew
    def initialize(_value)
      raise 'boom'
    end
  end

  let(:corral_hash_class) do
    Class.new(Hash) do
      include Hashie::Extensions::Coercion
      coerce_key :arg, CorralFailingArgNew
      coerce_key :run, CorralFailingRuntimeNew
      coerce_value Float, CorralFailingArgNew
    end
  end

  it 'raises CoercionError when the key coercion constructor fails, and does not store the raw value' do
    instance = corral_hash_class.new
    expect { instance[:arg] = 'raw' }.to raise_error(Hashie::CoercionError, /cannot coerce property `:arg`/)
    expect(instance.key?(:arg)).to be(false)
  end

  it 'does not silently store the unconverted value when the constructor raises a StandardError' do
    instance = corral_hash_class.new
    expect { instance[:run] = 'raw' }.to raise_error(StandardError, /boom/)
    expect(instance.key?(:run)).to be(false)
  end

  it 'raises CoercionError for value-type coercions whose constructor fails' do
    instance = corral_hash_class.new
    expect { instance[:other] = 1.5 }.to raise_error(Hashie::CoercionError)
    expect(instance.key?(:other)).to be(false)
  end

  it 'propagates constructor failure through the coercion callable directly' do
    callable = corral_hash_class.fetch_coercion(CorralFailingArgNew)
    expect { callable.call('raw') }.to raise_error(ArgumentError, /bad value/)
  end
end

describe Hashie::Extensions::Coercion do
  describe 'strict versus lenient precedence (corral)' do
    let(:corral_klass) do
      Class.new(Hash) do
        include Hashie::Extensions::Coercion

        coerce_value Integer, String
        coerce_value Integer, Float, strict: false
      end
    end

    it 'registers both a strict and a lenient rule for the same source class' do
      expect(corral_klass.strict_value_coercions[Integer]).to eq(String)
      expect(corral_klass.lenient_value_coercions[Integer]).to eq(Float)
    end

    it 'prefers the strict rule when looking up a value coercion' do
      expect(corral_klass.value_coercion(5)).to eq(String)
    end

    it 'stores the value converted through the strict rule' do
      instance = corral_klass.new
      instance[:a] = 5
      expect(instance[:a]).to eq('5')
      expect(instance[:a]).to be_a(String)
    end
  end
end

describe Hashie::Extensions::Coercion do
  describe 'coerce_key re-registration (corral)' do
    def corral_build_class
      Class.new(Hash) { include Hashie::Extensions::Coercion }
    end

    it 'overrides an earlier rule when the same key is registered again on one class' do
      klass = corral_build_class
      klass.coerce_key :corral_foo, Integer
      expect(klass.key_coercion(:corral_foo)).to eq(Integer)

      klass.coerce_key :corral_foo, String
      expect(klass.key_coercion(:corral_foo)).to eq(String)

      instance = klass.new
      instance[:corral_foo] = 42
      expect(instance[:corral_foo]).to eq('42')
      expect(instance[:corral_foo]).to be_a(String)
    end

    it 'lets a subclass override an inherited rule without changing the parent' do
      parent = corral_build_class
      parent.coerce_key :corral_bar, Integer
      child = Class.new(parent)
      child.coerce_key :corral_bar, String

      expect(child.key_coercion(:corral_bar)).to eq(String)
      expect(parent.key_coercion(:corral_bar)).to eq(Integer)

      child_instance = child.new
      child_instance[:corral_bar] = 7
      expect(child_instance[:corral_bar]).to eq('7')

      parent_instance = parent.new
      parent_instance[:corral_bar] = '7'
      expect(parent_instance[:corral_bar]).to eq(7)
    end

    it 'overrides with coerce_keys for multiple keys' do
      klass = corral_build_class
      klass.coerce_keys :corral_a, :corral_b, Integer
      klass.coerce_keys :corral_a, :corral_b, Symbol

      instance = klass.new
      instance[:corral_a] = 'x'
      instance[:corral_b] = 'y'
      expect(instance[:corral_a]).to eq(:x)
      expect(instance[:corral_b]).to eq(:y)
    end
  end
end

# --- corral: separate test file (unmergeable) — s5/m3 ---
# lang: 2 authored parts each declare "initialize", which is not a test and cannot be renamed safely
require 'spec_helper'
require_relative '../../../lib/hashie/extensions/coercion.rb'

describe Hashie::Extensions::Coercion do
  class CorralFailingTarget
    def initialize(_value)
      raise ArgumentError, 'corral conversion failure'
    end
  end

  let(:corral_klass) do
    Class.new(Hash) do
      include Hashie::Extensions::Coercion
      coerce_value String, CorralFailingTarget
    end
  end

  describe '#convert_value' do
    it 'propagates a failure instead of silently returning the unconverted value' do
      instance = corral_klass.new
      expect { instance.convert_value('raw') }.to raise_error(ArgumentError, /corral conversion failure/)
    end

    it 'propagates a failure raised by a callable coercer' do
      klass = Class.new(Hash) do
        include Hashie::Extensions::Coercion
        coerce_value String, ->(_v) { raise TypeError, 'corral callable failure' }
      end
      expect { klass.new.convert_value('raw') }.to raise_error(TypeError, /corral callable failure/)
    end

    it 'still converts successfully when the coercion works' do
      klass = Class.new(Hash) do
        include Hashie::Extensions::Coercion
        coerce_value String, Integer
      end
      expect(klass.new.convert_value('3')).to eq(3)
    end
  end

  describe 'assignment' do
    it 'raises CoercionError rather than storing the unconverted value' do
      instance = corral_klass.new
      expect { instance[:foo] = 'raw' }.to raise_error(Hashie::CoercionError)
      expect(instance).not_to have_key(:foo)
    end
  end
end

# --- corral: separate test file (unmergeable) — s6/m4 ---
# lang: 2 authored parts each declare "initialize", which is not a test and cannot be renamed safely
# frozen_string_literal: true

require 'spec_helper'

describe Hashie::Extensions::Coercion do
  class CorralStrictElement
    def initialize(value)
      raise ArgumentError, 'bad element' unless value.is_a?(String) && value.start_with?('ok')

      @value = value
    end
  end

  let(:corral_class) do
    Class.new(Hash) do
      include Hashie::Extensions::Coercion

      coerce_key :numbers, [Integer]
      coerce_key :strict, [CorralStrictElement]
    end
  end

  it 'raises CoercionError when an element of a container cannot be converted (no to_i)' do
    hash = corral_class.new
    bad = Object.new
    expect { hash[:numbers] = ['1', bad] }.to raise_error(Hashie::CoercionError)
    expect(hash.key?(:numbers)).to be(false)
  end

  it 'raises CoercionError when an element constructor fails' do
    hash = corral_class.new
    expect { hash[:strict] = ['ok1', 'nope'] }.to raise_error(Hashie::CoercionError)
    expect(hash.key?(:strict)).to be(false)
  end

  it 'still coerces valid container elements' do
    hash = corral_class.new
    hash[:numbers] = %w[1 2]
    expect(hash[:numbers]).to eq([1, 2])
  end

  it 'does not store unconverted elements when build_container_coercion fails directly' do
    conversion = corral_class.build_container_coercion(Array, Integer)
    expect { conversion.call(['1', Object.new]) }.to raise_error(NoMethodError)
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s1/m2 · line 65 proven

- into = self.class.key_coercion(key) || self.class.value_coercion(value)
+ into = self.class.value_coercion(value) || self.class.key_coercion(key)

s5/m3 · line 105 proven

- return self.class.fetch_coercion(into).call(val) if into
+ if into
+   begin
+     return self.class.fetch_coercion(into).call(val)
+   rescue StandardError
+     return val
+   end
+ end

s7/m2 · line 159 proven

- attrs.each { |key| key_coercions[key] = into }
+ attrs.each { |key| key_coercions[key] ||= into }

s0/m3 · line 230 proven

- strict_value_coercions[from] = into
+ lenient_value_coercions[from] = into

s0/m4 · line 232 proven

- lenient_value_coercions[from] = into
+ strict_value_coercions[from] = into

s4/m4 · line 268 proven

- strict_value_coercions[from] || lenient_value_coercions[from]
+ lenient_value_coercions[from] || strict_value_coercions[from]

s2/m2 · line 318 proven

- type.new(value)
+ begin
+   type.new(value)
+ rescue StandardError
+   value
+ end

s6/m4 · line 346 proven

- type.new(value.map { |v| value_coerce.call(v) })
+ type.new(value.map { |v| (value_coerce.call(v) rescue v) })

lib/hashie/extensions/indifferent_access.rb

0.90kill rate, 95% interval 0.74–0.96 (n=29 distinct)
27 of 30caught by the project's tests
3got through
3proven missing tests
language
ruby
status
certified
the tests corral wrote
require 'spec_helper'
require_relative '../../../lib/hashie/extensions/indifferent_access.rb'

# frozen_string_literal: true


describe Hashie::Extensions::IndifferentAccess do
  describe 'nested hashes that report indifferent_access? as false (corral)' do
    def corral_build_false_reporting_hash
      nested = { a: 1 }
      def nested.indifferent_access?
        false
      end
      nested
    end

    it 'still converts such a hash when it is written as a value' do
      outer = described_class.inject({})
      outer[:x] = corral_build_false_reporting_hash

      expect(outer['x'].keys).to eq(['a'])
      expect(outer['x']['a']).to eq(1)
      expect(outer[:x][:a]).to eq(1)
    end

    it 'still converts such a hash when it is the result of merge' do
      base = described_class.inject({})
      merged = base.merge(corral_build_false_reporting_hash)

      expect(merged.keys).to eq(['a'])
      expect(merged['a']).to eq(1)
    end

    it 'does not treat such a hash as indifferent in update (keys get normalized)' do
      outer = described_class.inject({})
      outer.update(corral_build_false_reporting_hash)

      expect(outer.keys).to eq(['a'])
      expect(outer['a']).to eq(1)
      expect(outer[:a]).to eq(1)
    end
  end
end

describe Hashie::Extensions::IndifferentAccess do
  describe 'update with a hash that responds to indifferent_access? but returns false (corral)' do
    let(:corral_indifferent_class) do
      Class.new(Hash) { include Hashie::Extensions::IndifferentAccess }
    end

    it 'normalizes symbol keys and nested hashes instead of taking the regular_update shortcut' do
      target = corral_indifferent_class.new

      other = { foo: 1, bar: { baz: 2 } }
      def other.indifferent_access?
        false
      end

      target.update(other)

      expect(target.keys.sort).to eq(%w[bar foo])
      expect(target['foo']).to eq(1)
      expect(target[:foo]).to eq(1)
      expect(target[:bar][:baz]).to eq(2)
      expect(target['bar']['baz']).to eq(2)
    end

    it 'does not create duplicate entries when updating with such a hash' do
      target = corral_indifferent_class.new
      target['foo'] = 1

      other = { foo: 2 }
      def other.indifferent_access?
        false
      end

      target.update(other)

      expect(target.size).to eq(1)
      expect(target['foo']).to eq(2)
    end
  end
end

# --- corral: separate test file (unmergeable) — s1/m4 ---
# lang: 2 authored parts each declare "other", which is not a test and cannot be renamed safely
require 'spec_helper'
require_relative '../../../lib/hashie/extensions/indifferent_access.rb'

describe Hashie::Extensions::IndifferentAccess do
  let(:corral_klass) do
    Class.new(Hash) { include Hashie::Extensions::IndifferentAccess }
  end

  it 'normalizes keys when updating from a hash that responds to indifferent_access? with false' do
    other = { cat: 'meow', dog: { name: 'Mango' } }
    def other.indifferent_access?
      false
    end

    target = corral_klass.new
    target.update(other)

    expect(target.keys).to eq(%w[cat dog])
    expect(target['cat']).to eq('meow')
    expect(target[:cat]).to eq('meow')
    expect(target[:dog]['name']).to eq('Mango')
    expect(target[:dog][:name]).to eq('Mango')
  end

  it 'does not create duplicate entries when updating with a non-indifferent hash that denies indifference' do
    other = { name: 'new' }
    def other.indifferent_access?
      false
    end

    target = corral_klass.new
    target['name'] = 'old'
    target.update(other)

    expect(target.size).to eq(1)
    expect(target['name']).to eq('new')
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s0/m4 · line 266 proven

- !(other.respond_to?(:indifferent_access?) &&
-   other.indifferent_access?)
+ !other.respond_to?(:indifferent_access?)

s1/m4 · line 273 proven

- other.respond_to?(:indifferent_access?) &&
- other.indifferent_access?
+ other.respond_to?(:indifferent_access?)

s1/m2 · line 274 proven

- other.indifferent_access?
+ !other.indifferent_access?

lib/hashie.rb

1.00kill rate, 95% interval 0.57–1.00 (n=5 distinct)
5 of 5caught by the project's tests
0got through
0proven missing tests
language
ruby
status
needs-review

lib/hashie/extensions/method_access.rb

0.90kill rate, 95% interval 0.75–0.97 (n=31 distinct)
28 of 31caught by the project's tests
3got through
3proven missing tests
language
ruby
status
certified
the tests corral wrote
require 'spec_helper'
require_relative '../../../lib/hashie/extensions/method_access.rb'

describe Hashie::Extensions::MethodOverridingWriter do
  let(:corral_class) do
    Class.new(Hash) do
      include Hashie::Extensions::MethodOverridingWriter
    end
  end

  subject(:corral_hash) { corral_class.new }

  it 'responds to writer-style names' do
    expect(corral_hash.respond_to?(:abc=)).to be true
  end

  it 'does not claim to respond to query-style names' do
    expect(corral_hash.respond_to?(:abc?)).to be false
    expect(corral_hash.respond_to?('anything_else?')).to be false
  end

  it 'does not claim to respond to query-style names even when a key exists' do
    corral_hash['abc'] = 1
    expect(corral_hash.respond_to?(:abc?)).to be false
  end

  it 'raises NoMethodError for query-style calls' do
    expect { corral_hash.abc? }.to raise_error(NoMethodError)
  end

  it 'does not respond to plain undefined names' do
    expect(corral_hash.respond_to?(:abc)).to be false
  end
end

describe Hashie::Extensions::MethodOverridingInitializer do
  class CorralInitOrderHash < Hash
    include Hashie::Extensions::MethodOverridingInitializer

    attr_reader :corral_seen

    def []=(key, value)
      @corral_seen ||= []
      @corral_seen << [key, respond_to?(:"__#{key}")]
      super
    end
  end

  it 'redefines the conflicting method before storing the value' do
    hash = CorralInitOrderHash.new(size: 'large')
    expect(hash.corral_seen).to eq [['size', true]]
  end

  it 'keeps the value readable and aliases the original method' do
    hash = CorralInitOrderHash.new(size: 'large')
    expect(hash.size).to eq 'large'
    expect(hash.__size).to eq 1
  end
end

describe Hashie::Extensions::MethodWriter do
  class CorralWriterHash < Hash
    include Hashie::Extensions::MethodWriter
  end

  subject(:hash) { CorralWriterHash.new }

  it 'raises NoMethodError for a writer call with no arguments and stores nothing' do
    expect { hash.send(:awesome=) }.to raise_error(NoMethodError)
    expect(hash).to be_empty
  end

  it 'raises NoMethodError for a writer call with more than one argument and stores nothing' do
    expect { hash.send(:awesome=, 1, 2) }.to raise_error(NoMethodError)
    expect(hash).to be_empty
  end

  it 'still writes with exactly one argument' do
    hash.send(:awesome=, 'sauce')
    expect(hash['awesome']).to eq 'sauce'
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s7/m3 · line 96 proven

- return self[convert_key(Regexp.last_match[1])] = args.first if args.size == 1 && name.to_s =~ /(.*)=$/
+ return self[convert_key(Regexp.last_match[1])] = args.first if name.to_s =~ /(.*)=$/

s3/m4 · line 277 proven

- return true if name.to_s.end_with?('=')
+ return true if name.to_s.end_with?('=', '?')

s4/m3 · line 347 proven

- _redefine_method(skey) if _method?(skey)
- self[skey] = value
+ self[skey] = value
+ _redefine_method(skey) if _method?(skey)

lib/hashie/extensions/dash/property_translation.rb

0.96kill rate, 95% interval 0.81–0.99 (n=26 distinct)
25 of 26caught by the project's tests
1got through
1proven missing tests
language
ruby
status
certified
the tests corral wrote
require 'spec_helper'
require_relative '../../lib/hashie/extensions/dash/property_translation.rb'

describe Hashie::Extensions::Dash::PropertyTranslation do
  class CorralPropertyTranslationDash < Hashie::Dash
    include Hashie::Extensions::Dash::PropertyTranslation

    property :name, from: :fullName
    property :age
  end

  let(:corral_dash) { CorralPropertyTranslationDash.new }

  describe '#property_exists?' do
    it 'returns true for a declared property' do
      expect(corral_dash.property_exists?(:name)).to eq(true)
      expect(corral_dash.property_exists?(:age)).to eq(true)
    end

    it 'raises NoMethodError for an undeclared property' do
      expect { corral_dash.property_exists?(:missing) }.to raise_error(NoMethodError)
    end

    it 'raises NoMethodError for a translated source key that is not itself a property' do
      expect { corral_dash.property_exists?(:fullName) }.to raise_error(NoMethodError)
    end
  end

  describe 'assigning undeclared keys' do
    it 'does not allow an undeclared key to be assigned via []=' do
      expect { corral_dash[:missing] = 1 }.to raise_error(NoMethodError)
      expect(corral_dash.key?(:missing)).to eq(false)
    end

    it 'writes translated values only to the declared destination property' do
      corral_dash[:fullName] = 'Ada'
      expect(corral_dash.name).to eq('Ada')
      expect(corral_dash.keys.map(&:to_sym) - %i[name age]).to be_empty
    end
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s1/m2 · line 302 proven

- _fail_no_property_error!(property) unless self.class.property?(property)
+ _fail_no_property_error!(property) if false

lib/hashie/rash.rb

0.68kill rate, 95% interval 0.46–0.85 (n=19 distinct)
13 of 19caught by the project's tests
6got through
6proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'

describe Hashie::Rash do
  describe 'exact literal precedence (corral)' do
    it 'stops after an exact string key even when a regexp also matches' do
      rash = described_class.new(/^user/ => 'profile', 'user/admin' => 'admin')
      expect(rash.all('user/admin').to_a).to eq ['admin']

      yielded = []
      rash.all('user/admin') { |v| yielded << v }
      expect(yielded).to eq ['admin']
    end

    it 'does not invoke callable pattern values when an exact key matches' do
      calls = 0
      rash = described_class.new(
        /^user/ => proc { |_m| calls += 1; 'called' },
        'user/admin' => 'admin'
      )
      expect(rash.all('user/admin').to_a).to eq ['admin']
      expect(calls).to eq 0
    end

    it 'stops after an exact numeric key even when a range also covers it' do
      rash = described_class.new(5 => 'exact', (1..10) => 'range')
      expect(rash.all(5).to_a).to eq ['exact']
      expect(rash.all(6).to_a).to eq ['range']
    end

    it 'stops after an exact regexp key even when string keys match it' do
      rash = described_class.new(/abc/ => 'pattern', 'abc' => 'literal')
      expect(rash.all(/abc/).to_a).to eq ['pattern']
    end
  end
end

describe Hashie::Rash do
  it 'fetch returns the first matching value, not the last' do
    rash = described_class.new(/hello/ => 'first', /world/ => 'second')
    expect(rash.fetch('hello world')).to eq 'first'
  end

  it 'fetch stops at the first match and does not invoke later callable values' do
    calls = []
    rash = described_class.new
    rash[/hello/] = proc { |_m| calls << :first; 'first' }
    rash[/world/] = proc { |_m| calls << :second; 'second' }
    expect(rash.fetch('hello world')).to eq 'first'
    expect(calls).to eq [:first]
  end

  it 'fetch returns a matched nil value instead of raising or using the default' do
    rash = described_class.new(/abc/ => nil)
    expect(rash.fetch('xabcx')).to be_nil
    expect(rash.fetch('xabcx', 'fallback')).to be_nil
    expect { |y| rash.fetch('xabcx', &y) }.not_to yield_control
  end
end

describe Hashie::Rash do
  describe 'delegation to the underlying hash (corral)' do
    let(:rash_corral) do
      described_class.new('a' => 'first_value', /b/ => 'second_value')
    end

    it 'does not leak an unrelated stored value when a delegated call returns a falsy result' do
      # Hash#key? returns false here, so the original falls through to `super`
      # (NoMethodError). A mutant returning @hash.values.first would leak
      # 'first_value' instead.
      expect { rash_corral.key?('missing') }.to raise_error(NoMethodError)
    end

    it 'never returns a stored value for a falsy delegated result' do
      result = begin
        rash_corral.key?('missing')
      rescue NoMethodError
        :no_method_error
      end
      expect(result).to eq :no_method_error
      expect(result).not_to eq 'first_value'
    end

    it 'raises NoMethodError for an empty hash delegated predicate returning false' do
      empty = described_class.new
      expect { empty.empty? }.to raise_error(NoMethodError) if empty.instance_variable_get(:@hash).empty?
    end

    it 'still delegates truthy results' do
      expect(rash_corral.key?('a')).to be true
      expect(rash_corral.size).to eq 2
    end
  end
end

describe Hashie::Rash do
  describe 'corral: method_missing delegation evaluates exactly once' do
    let(:corral_rash) { described_class.new('a' => 1, 'b' => 2) }

    it 'invokes a delegated block-taking method only once per entry' do
      seen = []
      corral_rash.each { |k, _v| seen << k }
      expect(seen).to eq %w[a b]
    end

    it 'returns the value of a delegated mutating call instead of re-running it' do
      expect(corral_rash.delete('a')).to eq 1
      expect(corral_rash.keys).to eq ['b']
    end

    it 'does not run the delegated side effect twice for store-like calls' do
      calls = 0
      corral_rash.each_key { |_k| calls += 1 }
      expect(calls).to eq 2
    end
  end
end

describe Hashie::Rash, 'optimization ordering (corral)' do
  it 'reorders regexes so the most frequently matched one is tried first' do
    rash = described_class.new(/a/ => 'A', /b/ => 'B')
    rash.optimize_every = 1_000

    3.times { expect(rash['b']).to eq 'B' }
    expect(rash['a']).to eq 'A'

    # Counts: /b/ => 3, /a/ => 1. The next string lookup triggers optimization.
    rash.optimize_every = 1

    # Both regexes match 'ab'; after sorting by descending hit count, /b/ must come first.
    expect(rash['ab']).to eq 'B'
    expect(rash.all('ab').to_a.first).to eq 'B'
  end
end

describe Hashie::Rash do
  describe 'underlying hash defaults (corral)' do
    subject(:corral_rash) do
      described_class.new(
        'known' => 'first-value',
        /pat/ => 'second-value'
      )
    end

    it 'does not fabricate values for missing keys via delegated hash access' do
      expect(corral_rash.values_at('missing')).to eq [nil]
      expect(corral_rash.values_at('known', 'missing')).to eq ['first-value', nil]
    end

    it 'returns nil from lookup for non-matching keys' do
      expect(corral_rash['nomatch']).to be_nil
      expect(corral_rash[12_345]).to be_nil
      expect(corral_rash.all('nomatch').to_a).to eq []
    end
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s6/m1 · line 43 proven

- @hash           = {}
+ @hash           = Hash.new { |h, _k| h.values.first }

s2/m1 · line 116 proven

- all(key) do |value|
-   return value
- end
+ found = nil
+ all(key) do |value|
+   found = value
+ end
+ return found unless found.nil?

s0/m2 · line 141 proven

- return

s3/m1 · line 183 proven

- @hash.send(...) || super
+ @hash.send(...) || @hash.values.first

s3/m2 · line 183 proven

- @hash.send(...) || super
+ (@hash.send(...) && @hash.send(...)) || super

s5/m1 · line 199 proven

- @regexes = @regexes.sort_by { |regex| -@regex_counts[regex] }
+ @regexes = @regexes.sort_by { |regex| @regex_counts[regex] }

lib/hashie/clash.rb

1.00kill rate, 95% interval 0.81–1.00 (n=16 distinct)
16 of 16caught by the project's tests
0got through
0proven missing tests
language
ruby
status
certified

lib/hashie/hash.rb

1.00kill rate, 95% interval 0.72–1.00 (n=10 distinct)
10 of 10caught by the project's tests
0got through
0proven missing tests
language
ruby
status
certified

lib/hashie/extensions/dash/indifferent_access.rb

0.85kill rate, 95% interval 0.58–0.96 (n=13 distinct)
11 of 13caught by the project's tests
2got through
2proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'

describe Hashie::Extensions::Dash::IndifferentAccess do
  context 'when PropertyTranslation is included after IndifferentAccess (corral)' do
    let(:klass_late_translation) do
      Class.new(Hashie::Dash) do
        include Hashie::Extensions::Dash::IndifferentAccess
        include Hashie::Extensions::Dash::PropertyTranslation

        property :name, from: :fullName
      end
    end

    it 'reports a symbol translation source as a property without stringifying before the lookup' do
      expect(klass_late_translation.property?(:fullName)).to be true
    end

    it 'still reports the declared property under either key type' do
      expect(klass_late_translation.property?(:name)).to be true
      expect(klass_late_translation.property?('name')).to be true
      expect(klass_late_translation.property?(:missing)).to be false
    end
  end

  context 'when IndifferentAccess is included after PropertyTranslation' do
    let(:klass_early_translation) do
      Class.new(Hashie::Dash) do
        include Hashie::Extensions::Dash::PropertyTranslation
        include Hashie::Extensions::Dash::IndifferentAccess

        property :name, from: :fullName
      end
    end

    it 'resolves translation sources under both key types' do
      expect(klass_early_translation.property?(:fullName)).to be true
      expect(klass_early_translation.property?('fullName')).to be true
      expect(klass_early_translation.property?(:missing)).to be false
    end
  end
end

describe Hashie::Extensions::Dash::IndifferentAccess::ClassMethods do
  describe '.transformed_property with string-declared transformers (corral)' do
    let(:string_declared) do
      Class.new(Hashie::Trash) do
        include Hashie::Extensions::Dash::IndifferentAccess

        property 'age', transform_with: ->(value) { value.to_i }
      end
    end

    let(:symbol_declared) do
      Class.new(Hashie::Trash) do
        include Hashie::Extensions::Dash::IndifferentAccess

        property :age, transform_with: ->(value) { value.to_i }
      end
    end

    it 'finds a transformer declared under a string key when looked up by string' do
      expect(string_declared.transformed_property('age', '37')).to eq(37)
    end

    it 'finds a transformer declared under a string key when looked up by symbol' do
      expect(string_declared.transformed_property(:age, '37')).to eq(37)
    end

    it 'finds a transformer declared under a symbol key when looked up by string' do
      expect(symbol_declared.transformed_property('age', '37')).to eq(37)
    end

    it 'finds a transformer declared under a symbol key when looked up by symbol' do
      expect(symbol_declared.transformed_property(:age, '37')).to eq(37)
    end
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s2/m2 · line 116 proven

- transform = transforms[property_name] || transforms[property_name.to_sym]
+ transform = transforms[property_name.to_sym]

s0/m1 · line 136 proven

- translation_exists?(name)
+ translation_exists?(name.to_s)

lib/hashie/extensions/symbolize_keys.rb

0.89kill rate, 95% interval 0.57–0.98 (n=9 distinct)
8 of 9caught by the project's tests
1got through
1proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'

describe Hashie::Extensions::SymbolizeKeys do
  describe 'key case preservation (corral)' do
    it 'preserves the case of converted keys, including nested ones' do
      input = { 'Foo' => 1, 'BAR' => { 'Baz' => 2 }, 'Arr' => [{ 'QuX' => 3 }], 7 => 'num' }
      result = described_class.symbolize_keys(input)
      expect(result).to eq(Foo: 1, BAR: { Baz: 2 }, Arr: [{ QuX: 3 }], 7 => 'num')
    end

    it 'does not merge or overwrite keys that differ only by case' do
      input = { 'A' => 1, 'a' => 2 }
      result = described_class.symbolize_keys(input)
      expect(result.size).to eq 2
      expect(result[:A]).to eq 1
      expect(result[:a]).to eq 2
    end

    it 'preserves case in place via symbolize_keys!' do
      hash = { 'MixedCase' => 'v', :Already => 'w' }
      described_class.symbolize_keys!(hash)
      expect(hash).to eq(MixedCase: 'v', Already: 'w')
    end

    it 'preserves case when extended on a hash instance' do
      hash = { 'CamelKey' => { 'InnerKey' => 1 } }.extend(described_class)
      expect(hash.symbolize_keys!).to eq(CamelKey: { InnerKey: 1 })
    end
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s1/m2 · line 130 proven

- key.respond_to?(:to_sym) ? key.to_sym : key
+ key.respond_to?(:to_sym) ? key.to_s.downcase.to_sym : key

lib/hashie/extensions/stringify_keys.rb

1.00kill rate, 95% interval 0.65–1.00 (n=7 distinct)
7 of 7caught by the project's tests
0got through
0proven missing tests
language
ruby
status
needs-review

lib/hashie/extensions/dash/predefined_values.rb

1.00kill rate, 95% interval 0.74–1.00 (n=11 distinct)
11 of 11caught by the project's tests
0got through
0proven missing tests
language
ruby
status
certified

lib/hashie/extensions/deep_locate.rb

0.85kill rate, 95% interval 0.58–0.96 (n=13 distinct)
11 of 13caught by the project's tests
2got through
2proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'

begin
  require 'active_support'
  require 'active_support/core_ext/hash/indifferent_access'
rescue LoadError
  nil
end

unless defined?(::ActiveSupport::HashWithIndifferentAccess)
  module ActiveSupport
    class HashWithIndifferentAccess < ::Hash; end
  end
end

describe Hashie::Extensions::DeepLocate do
  describe 'ActiveSupport indifferent access detection (corral)' do
    it 'finds symbol keys in a plain Hash even when ActiveSupport HWIA is defined' do
      plain = { user: { name: 'Ada' }, other: 1 }

      expect(described_class.deep_locate(:name, plain)).to eq([{ name: 'Ada' }])
    end

    it 'does not match string keys in a plain Hash when searching by symbol' do
      plain = { 'name' => 'Ada' }

      expect(described_class.deep_locate(:name, plain)).to eq([])
    end

    it 'matches string-converted keys for an ActiveSupport HashWithIndifferentAccess' do
      indifferent = ::ActiveSupport::HashWithIndifferentAccess.new
      indifferent['name'] = 'Ada'

      result = described_class.deep_locate(:name, indifferent)

      expect(result.size).to eq(1)
      expect(result.first).to equal(indifferent)
    end
  end
end

describe Hashie::Extensions::DeepLocate do
  describe 'ActiveSupport indifferent access key handling (corral)' do
    before do
      @corral_defined_stub = false
      unless defined?(::ActiveSupport::HashWithIndifferentAccess)
        module ::ActiveSupport; end
        ::ActiveSupport.const_set(:HashWithIndifferentAccess, Class.new(::Hash))
        @corral_defined_stub = true
      end
    end

    after do
      if @corral_defined_stub
        ::ActiveSupport.send(:remove_const, :HashWithIndifferentAccess)
      end
    end

    it 'converts the search key to a string for HashWithIndifferentAccess objects' do
      hwia = ::ActiveSupport::HashWithIndifferentAccess.new
      hwia['name'] = 'Ada'
      hwia['other'] = 1

      result = described_class.deep_locate(:name, hwia)
      expect(result.size).to eq(1)
      expect(result.first).to equal(hwia)
    end

    it 'finds nested string-keyed hashes inside a HashWithIndifferentAccess' do
      inner = { 'name' => 'Ada' }
      hwia = ::ActiveSupport::HashWithIndifferentAccess.new
      hwia['user'] = inner

      result = described_class.deep_locate(:name, hwia)
      expect(result.size).to eq(1)
      expect(result.first).to equal(inner)
    end

    it 'does not convert the search key for plain Hash objects' do
      plain = { user: { name: 'Ada' } }

      expect(described_class.deep_locate(:name, plain)).to eq([{ name: 'Ada' }])
    end

    it 'does not match string keys with a symbol search on plain Hash objects' do
      plain = { 'name' => 'Ada' }

      expect(described_class.deep_locate(:name, plain)).to eq([])
    end
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s2/m1 · line 94 proven

- object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
+ !object.is_a?(::ActiveSupport::HashWithIndifferentAccess)

s2/m2 · line 94 proven

- object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
+ object.instance_of?(::Hash)

lib/hashie/extensions/strict_key_access.rb

0.78kill rate, 95% interval 0.45–0.94 (n=9 distinct)
7 of 9caught by the project's tests
2got through
2proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'
require_relative '../../../lib/hashie/extensions/strict_key_access.rb'

describe Hashie::Extensions::StrictKeyAccess do
  class StrictKeyAccessCorralHash < Hash
    include Hashie::Extensions::StrictKeyAccess
  end

  let(:corral_message) do
    'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense'
  end
  let(:corral_instance) { StrictKeyAccessCorralHash.new }

  it 'DefaultError has a descriptive message' do
    error = Hashie::Extensions::StrictKeyAccess::DefaultError.new
    expect(error.message).to eq corral_message
    expect(error.message).not_to be_empty
  end

  it 'default raises DefaultError with the descriptive message' do
    expect { corral_instance.default(:x) }
      .to raise_error(Hashie::Extensions::StrictKeyAccess::DefaultError, corral_message)
  end

  it 'default= raises DefaultError with the descriptive message' do
    expect { corral_instance.default = 1 }
      .to raise_error(Hashie::Extensions::StrictKeyAccess::DefaultError, corral_message)
  end

  it 'default_proc raises DefaultError with the descriptive message' do
    expect { corral_instance.default_proc }
      .to raise_error(Hashie::Extensions::StrictKeyAccess::DefaultError, corral_message)
  end

  it 'default_proc= raises DefaultError with the descriptive message' do
    expect { corral_instance.default_proc = proc {} }
      .to raise_error(Hashie::Extensions::StrictKeyAccess::DefaultError, corral_message)
  end

  it 'still raises KeyError for missing keys and values' do
    corral_instance[:a] = 1
    expect { corral_instance[:missing] }.to raise_error(KeyError)
    expect { corral_instance.key(99) }.to raise_error(KeyError)
    expect(corral_instance.key(1)).to eq :a
  end
end

describe Hashie::Extensions::StrictKeyAccess::DefaultError do
  let(:corral_expected_message) do
    'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense'
  end

  it 'has the exact documented message when constructed directly' do
    expect(described_class.new.message).to eq corral_expected_message
  end

  it 'is a StandardError' do
    expect(described_class.new).to be_a StandardError
  end

  it 'carries the exact message when raised by #default' do
    hash = {}.extend(Hashie::Extensions::StrictKeyAccess)
    expect { hash.default }
      .to raise_error(described_class, corral_expected_message)
  end

  it 'carries the exact message when raised by #default=' do
    hash = {}.extend(Hashie::Extensions::StrictKeyAccess)
    expect { hash.default = 1 }
      .to raise_error(described_class, corral_expected_message)
  end

  it 'carries the exact message when raised by #default_proc' do
    hash = {}.extend(Hashie::Extensions::StrictKeyAccess)
    expect { hash.default_proc }
      .to raise_error(described_class, corral_expected_message)
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s1/m2 · line 36 proven

- super('Setting or using a default with Hashie::Extensions::StrictKeyAccess ' \
+ raise ArgumentError, 'default' if false
+ super('Setting or using a default with Hashie::Extensions::StrictKeyAccess ' \
+ 'does not make sense'.reverse + ' ' \

s1/m1 · line 37 proven

- super('Setting or using a default with Hashie::Extensions::StrictKeyAccess ' \
-       'does not make sense'
- )
+ super('')

lib/hashie/extensions/deep_merge.rb

1.00kill rate, 95% interval 0.76–1.00 (n=12 distinct)
12 of 12caught by the project's tests
0got through
0proven missing tests
language
ruby
status
certified

lib/hashie/extensions/mash/define_accessors.rb

0.47kill rate, 95% interval 0.26–0.69 (n=17 distinct)
8 of 17caught by the project's tests
9got through
9proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'
require_relative '../../../../lib/hashie/extensions/mash/define_accessors.rb'

describe Hashie::Extensions::Mash::DefineAccessors do
  let(:klass) { Class.new(Hashie::Mash) { include Hashie::Extensions::Mash::DefineAccessors } }

  describe 'generated predicate (?) accessor' do
    it 'returns strictly true for a truthy value, not the value itself' do
      instance = klass.new('name' => 'Ada')
      expect(instance.name?).to eq(true)
      # second call goes through the generated method
      expect(instance.name?).to eq(true)
    end

    it 'returns strictly false for a missing key, not nil' do
      instance = klass.new
      expect(instance.name?).to eq(false)
      expect(instance.name?).to eq(false)
      expect(instance.key?('name')).to eq(false)
    end

    it 'coerces falsy and non-boolean values to booleans' do
      instance = klass.new('flag' => false, 'count' => 5, 'empty' => nil)
      expect(instance.flag?).to eq(false)
      expect(instance.count?).to eq(true)
      expect(instance.empty?).to eq(false)
    end

    it 'works on a singleton-extended Mash as well' do
      instance = Hashie::Mash.new('name' => 'Ada').with_accessors!
      expect(instance.name?).to eq(true)
      expect(instance.name?).to eq(true)
      expect(instance.missing?).to eq(false)
    end
  end
end

# frozen_string_literal: true


describe Hashie::Extensions::Mash::DefineAccessors do
  let(:corral_klass) { Class.new(Hashie::Mash) { include Hashie::Extensions::Mash::DefineAccessors } }

  describe 'generated initializing reader (bang)' do
    it 'initializes only the key matching the method name without the suffix' do
      mash = corral_klass.new
      result = mash.foo!
      expect(result).to be_a(Hashie::Mash)
      expect(mash.key?('foo')).to be true
      expect(mash.key?('foo!')).to be false
      expect(mash.keys).to eq(['foo'])
    end

    it 'returns the existing value stored under the base key' do
      mash = corral_klass.new('foo' => 'existing')
      expect(mash.foo!).to eq('existing')
      expect(mash.foo!).to eq('existing')
      expect(mash.keys).to eq(['foo'])
    end

    it 'keeps nested initialization working on repeated calls' do
      mash = corral_klass.new
      mash.address!.city = 'London'
      expect(mash.address!.city).to eq('London')
      expect(mash['address']['city']).to eq('London')
      expect(mash.key?('address!')).to be false
    end

    it 'behaves the same on an extended instance' do
      mash = Hashie::Mash.new('bar' => 5).with_accessors!
      expect(mash.bar!).to eq(5)
      mash.baz!
      expect(mash.key?('baz')).to be true
      expect(mash.key?('baz!')).to be false
    end
  end
end

describe Hashie::Extensions::Mash::DefineAccessors do
  describe 'generated predicate (corral)' do
    let(:klass) { Class.new(Hashie::Mash) { include Hashie::Extensions::Mash::DefineAccessors } }

    it 'returns exactly true for a truthy value' do
      instance = klass.new(name: 'Ada')
      expect(instance.name?).to eq(true)
      # second call goes through the generated method
      expect(instance.name?).to eq(true)
    end

    it 'returns exactly false for a missing key' do
      instance = klass.new
      expect(instance.name?).to eq(false)
      expect(instance.name?).to eq(false)
    end

    it 'returns exactly false for a false value' do
      instance = klass.new(flag: false)
      expect(instance.flag?).to eq(false)
      expect(instance.flag?).to eq(false)
    end

    it 'returns exactly false for a nil value' do
      instance = klass.new(thing: nil)
      expect(instance.thing?).to eq(false)
      expect(instance.thing?).to eq(false)
    end

    it 'returns true for falsy-looking but truthy values such as 0 and empty string' do
      instance = klass.new(zero: 0, blank: '')
      expect(instance.zero?).to eq(true)
      expect(instance.zero?).to eq(true)
      expect(instance.blank?).to eq(true)
      expect(instance.blank?).to eq(true)
    end

    it 'reads only the key matching its method name' do
      instance = klass.new(a: 1)
      expect(instance.b?).to eq(false)
      expect(instance.a?).to eq(true)
    end

    it 'behaves the same on an extended Mash instance' do
      instance = Hashie::Mash.new(name: 'Ada').with_accessors!
      expect(instance.name?).to eq(true)
      expect(instance.name?).to eq(true)
      expect(instance.missing?).to eq(false)
      expect(instance.missing?).to eq(false)
    end
  end
end

describe 'Hashie::Extensions::Mash::DefineAccessors generated predicate reads the matching key' do
  let(:corral_klass) { Class.new(Hashie::Mash) { include Hashie::Extensions::Mash::DefineAccessors } }

  it 'returns true for a truthy value stored under the base key' do
    instance = corral_klass.new('foo' => 'present')
    expect(instance.foo?).to eq true
  end

  it 'returns true on repeated calls once the predicate is defined' do
    instance = corral_klass.new('foo' => 'present')
    expect(instance.foo?).to eq true
    expect(corral_klass).to be_method_defined(:foo?)
    expect(instance.foo?).to eq true
  end

  it 'returns false when the base key holds a falsey value' do
    instance = corral_klass.new('foo' => false)
    expect(instance.foo?).to eq false
    instance2 = corral_klass.new('bar' => nil)
    expect(instance2.bar?).to eq false
  end

  it 'returns false when the base key is missing' do
    expect(corral_klass.new.foo?).to eq false
  end

  it 'reflects later writes to the base key' do
    instance = corral_klass.new
    expect(instance.foo?).to eq false
    instance[:foo] = 1
    expect(instance.foo?).to eq true
  end

  it 'works for a single extended Mash too' do
    instance = Hashie::Mash.new('foo' => 'x').with_accessors!
    expect(instance.foo?).to eq true
    expect(instance.baz?).to eq false
  end
end

describe Hashie::Extensions::Mash::DefineAccessors::Ext do
  let(:corral_accessors) { described_class.new }
  let(:corral_klass) do
    accessors = corral_accessors
    Class.new(Hashie::Mash) { include accessors }
  end

  it 'reader reads the literal method-name key when it exists, not the generated key' do
    corral_accessors.define_reader('name', :alias_name)
    mash = corral_klass.new('alias_name' => 'literal', 'name' => 'other')
    expect(mash.alias_name).to eq('literal')
  end

  it 'reader reads the key matching its generated key when the method-name key is absent' do
    corral_accessors.define_reader('name', :alias_name)
    mash = corral_klass.new('name' => 'Ada')
    expect(mash.alias_name).to eq('Ada')
  end
end

# frozen_string_literal: true


describe Hashie::Extensions::Mash::DefineAccessors do
  describe 'generated readers read exactly the key matching the method name' do
    subject(:klass) { Class.new(Hashie::Mash) { include Hashie::Extensions::Mash::DefineAccessors } }

    it 'returns the value of the matching key, not a similarly named key, on first and later calls' do
      instance = klass.new('corral_title' => 'Ada', 'corral_title_x' => 'wrong', 'corral_title_' => 'wrong2')
      expect(instance.corral_title).to eq('Ada')
      expect(klass).to be_method_defined(:corral_title)
      expect(instance.corral_title).to eq('Ada')
    end

    it 'does not return a value when only a suffixed lookalike key exists' do
      instance = klass.new('corral_other_x' => 'wrong')
      expect(instance.corral_other).to be_nil
      expect(instance.corral_other).to be_nil
    end

    it 'reads the matching key through a directly defined reader' do
      accessors = Hashie::Extensions::Mash::DefineAccessors::Ext.new
      accessors.define_reader('corral_direct', :corral_direct)
      direct_klass = Class.new(Hashie::Mash) { include accessors }
      instance = direct_klass.new('corral_direct' => 'right', 'corral_direct_x' => 'wrong')
      expect(instance.corral_direct).to eq('right')
    end

    it 'works on an extended Mash instance too' do
      instance = Hashie::Mash.new('corral_ext' => 'right', 'corral_ext_x' => 'wrong').with_accessors!
      expect(instance.corral_ext).to eq('right')
      expect(instance.corral_ext).to eq('right')
    end
  end
end

describe Hashie::Extensions::Mash::DefineAccessors do
  describe 'generated underscore (underbang) reader for a missing key' do
    let(:klass_corral) { Class.new(Hashie::Mash) { include Hashie::Extensions::Mash::DefineAccessors } }

    it 'returns a fresh empty mash without storing it, on first and later calls' do
      mash = klass_corral.new
      2.times do
        result = mash.address_
        expect(result).to be_a(Hashie::Mash)
        expect(result).to be_an_instance_of(klass_corral)
        expect(result).to be_empty
        expect(mash.key?('address')).to be false
      end
      expect(klass_corral).to be_method_defined(:address_)
    end

    it 'allows chained assignment on the temporary mash without mutating the receiver' do
      mash = klass_corral.new
      mash.address_.city = 'London'
      expect(mash.key?('address')).to be false
      expect(mash.address_.city).to be_nil
    end

    it 'reads the matching key when it is present' do
      mash = klass_corral.new('address' => { 'city' => 'Paris' })
      expect(mash.address_.city).to eq 'Paris'
    end

    it 'behaves the same when extended on a single instance' do
      mash = Hashie::Mash.new.with_accessors!
      expect(mash.address_).to be_a(Hashie::Mash)
      expect(mash.address_).to be_empty
      expect(mash.key?('address')).to be false
    end
  end
end

describe Hashie::Extensions::Mash::DefineAccessors do
  describe 'generated writer (corral)' do
    let(:klass) { Class.new(Hashie::Mash) { include Hashie::Extensions::Mash::DefineAccessors } }

    it 'writes only the key matching its method name on first call' do
      mash = klass.new
      mash.foo = 'bar'
      expect(mash.keys).to eq(['foo'])
      expect(mash['foo']).to eq('bar')
      expect(mash).not_to have_key('__writer_called')
    end

    it 'writes only the matching key on repeated calls through the defined method' do
      mash = klass.new
      mash.foo = 1
      mash.foo = 2
      mash.baz = 3
      expect(mash.keys.sort).to eq(%w[baz foo])
      expect(mash.to_h).to eq('foo' => 2, 'baz' => 3)
    end

    it 'does not add extra keys on a Mash extended with accessors' do
      mash = Hashie::Mash.new.with_accessors!
      mash.name = 'Ada'
      expect(mash.to_h).to eq('name' => 'Ada')
    end

    it 'does not add extra keys to a pre-populated mash' do
      mash = klass.new('existing' => 1)
      mash.other = 2
      expect(mash.to_h).to eq('existing' => 1, 'other' => 2)
    end
  end
end

describe Hashie::Extensions::Mash::DefineAccessors do
  describe 'accessor generation (corral)' do
    let(:klass) do
      Class.new(Hashie::Mash) do
        include Hashie::Extensions::Mash::DefineAccessors

        def foo
          if self[:foo] == 1
            super
          else
            :bar
          end
        end
      end
    end

    it 'keeps generated accessors below the including class so its overrides still win' do
      instance = klass.new
      instance.foo = 1
      # first call goes through the override's super, which generates the reader
      expect(instance.foo).to eq 1
      expect(klass).not_to be_method_defined(:foo).or be_method_defined(:foo)
      instance.foo = 2
      # after generation, the class's own override must still take precedence
      expect(instance.foo).to eq :bar
      instance.foo = 1
      expect(instance.foo).to eq 1
    end

    it 'places the generated module after the class in the ancestor chain' do
      ancestors = klass.ancestors
      ext_index = ancestors.index { |a| a.is_a?(Hashie::Extensions::Mash::DefineAccessors::Ext) }
      expect(ext_index).not_to be_nil
      expect(ext_index).to be > ancestors.index(klass)
    end
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s7/m1 · line 20 proven

- include mod
+ prepend mod

s0/m3 · line 56 proven

- mod.define_predicate(key, method_name)
+ mod.define_reader(key, method_name)

s3/m1 · line 77 proven

-   self.[](method_name, &block)
- else
-   self.[](key, &block)
+   self.[](key, &block)
+ else
+   self.[](method_name, &block)

s3/m2 · line 77 proven

- self.[](key, &block)
+ self.[]("#{key}_x", &block)

s5/m2 · line 94 proven

- assign_property(key, value)
+ self[key] = value
+ self[:__writer_called] = true

s2/m2 · line 110 proven

- !!self[key]
+ !!self[method_name]

s2/m1 · line 113 proven

- !!self[key]
+ !self[key]

s1/m1 · line 127 proven

- if key? method_name
-   self.[](method_name)
- else
-   initializing_reader(key)
+ if key? key
+   self.[](method_name)
+ else
+   initializing_reader(method_name)

s4/m2 · line 146 proven

- underbang_reader(key)
+ self.[](key)

lib/hashie/extensions/deep_find.rb

1.00kill rate, 95% interval 0.57–1.00 (n=5 distinct)
5 of 5caught by the project's tests
0got through
0proven missing tests
language
ruby
status
needs-review

lib/hashie/extensions/persistable.rb

0.67kill rate, 95% interval 0.35–0.88 (n=9 distinct)
6 of 9caught by the project's tests
3got through
2proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'
require 'tmpdir'
require_relative '../../../lib/hashie/extensions/persistable.rb'

module PersistableCorralFixtures
  class Hash < ::Hash
    include Hashie::Extensions::MergeInitializer
    include Hashie::Extensions::Persistable
  end
end

describe Hashie::Extensions::Persistable do
  let(:corral_subject) { PersistableCorralFixtures::Hash.new }

  it 'copies string values rather than returning the same object' do
    original = +'mutable'
    result = corral_subject.send(:_persistable_data, original)

    expect(result).to eq 'mutable'
    expect(result).not_to be_equal(original)
  end

  it 'copies nested string values in hashes and arrays' do
    inner = +'inner'
    in_array = +'element'
    data = { 'key' => inner, 'list' => [in_array] }
    result = corral_subject.send(:_persistable_data, data)

    expect(result).to eq data
    expect(result['key']).not_to be_equal(inner)
    expect(result['list'].first).not_to be_equal(in_array)
  end

  it 'does not let later mutation of the source leak into the prepared data' do
    original = +'before'
    result = corral_subject.send(:_persistable_data, original)
    original << ' after'

    expect(result).to eq 'before'
  end
end

module PersistableCorralFixtures
  class Store < ::Hash
    include Hashie::Extensions::MergeInitializer
    include Hashie::Extensions::Persistable
  end
end

describe Hashie::Extensions::Persistable do
  around do |example|
    Dir.mktmpdir('hashie-corral') do |directory|
      @corral_dir = directory
      example.run
    end
  end

  let(:corral_path) { File.join(@corral_dir, 'data.yaml') }
  let(:corral_store) { PersistableCorralFixtures::Store.new(name: 'parent') }

  it 'rejects a self-containing array with ArgumentError without touching the file' do
    corral_store.save(corral_path)
    original = File.read(corral_path)

    list = []
    list << list
    corral_store[:list] = list

    expect { corral_store.save }.to raise_error(ArgumentError, 'cannot persist cyclic data')
    expect(File.read(corral_path)).to eq original
    expect(Dir.children(@corral_dir)).to eq ['data.yaml']
  end

  it 'rejects an array cycle that passes through a nested array' do
    outer = [[]]
    outer[0] << outer
    corral_store[:list] = outer

    expect { corral_store.save(corral_path) }.to raise_error(ArgumentError, 'cannot persist cyclic data')
    expect(File).not_to exist(corral_path)
  end

  it 'still saves repeated (non-cyclic) arrays' do
    shared = [1, 2]
    corral_store[:a] = shared
    corral_store[:b] = [shared, shared]
    corral_store.save(corral_path)

    loaded = PersistableCorralFixtures::Store.load(corral_path)
    expect(loaded[:b]).to eq [[1, 2], [1, 2]]
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s0/m4 · line 126 proven

- value.map { |child| _persistable_data(child, ancestors + [value]) }
+ value.map { |child| _persistable_data(child, ancestors) }

s0/m3 · line 127 proven

- value.dup
+ value

s1/m1 · line 59 unproven

- data = Psych.safe_load(File.read(path), permitted_classes: [Symbol], aliases: false)
+ data = Psych.safe_load(File.read(path), permitted_classes: [Symbol], aliases: true)

lib/hashie/extensions/ignore_undeclared.rb

1.00kill rate, 95% interval 0.57–1.00 (n=5 distinct)
5 of 5caught by the project's tests
0got through
0proven missing tests
language
ruby
status
needs-review

lib/hashie/extensions/mash/keep_original_keys.rb

0.86kill rate, 95% interval 0.49–0.97 (n=7 distinct)
6 of 7caught by the project's tests
1got through
1proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'

RSpec.describe Hashie::Extensions::Mash::KeepOriginalKeys do
  let(:corral_keeping_mash) do
    Class.new(Hashie::Mash) do
      include Hashie::Extensions::Mash::KeepOriginalKeys
    end
  end

  it 'does not collide integer keys with stored string keys' do
    mash = corral_keeping_mash.new('1' => 'string one')

    expect(mash[1]).to be_nil
    expect(mash['1']).to eq('string one')
  end

  it 'does not collide other key types with their string forms' do
    mash = corral_keeping_mash.new('1.5' => 'float string', '' => 'empty')

    expect(mash[1.5]).to be_nil
    expect(mash[nil]).to be_nil
    expect(mash['1.5']).to eq('float string')
  end

  it 'leaves non-string, non-symbol keys untouched when converting' do
    mash = corral_keeping_mash.new({})

    expect(mash.send(:_convert, 1)).to eql(1)
    expect(mash.send(:_convert, 1.5)).to eql(1.5)
    expect(mash.send(:_convert, nil)).to be_nil
  end

  it 'keeps integer and string keys distinct' do
    mash = corral_keeping_mash.new(1 => 'int', '1' => 'str')

    expect(mash[1]).to eq('int')
    expect(mash['1']).to eq('str')
    expect(mash.keys).to contain_exactly(1, '1')
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s0/m3 · line 51 proven

- else key
+ else key.to_s

lib/hashie/extensions/key_conflict_warning.rb

0.86kill rate, 95% interval 0.49–0.97 (n=7 distinct)
6 of 7caught by the project's tests
1got through
1proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'
require_relative '../../lib/hashie/extensions/key_conflict_warning.rb'

describe Hashie::Extensions::KeyConflictWarning::CannotDisableMashWarnings do
  let(:expected_message) do
    'you cannot disable warnings on the base Mash class; ' \
    'please subclass the Mash and disable it in the subclass'
  end

  it 'has the exact guidance message when constructed directly' do
    error = described_class.new
    expect(error).to be_a(StandardError)
    expect(error.message).to eq expected_message
    expect(error.message).to include('you cannot disable warnings on the base Mash class')
    expect(error.message).to include('please subclass the Mash')
  end

  it 'raises with the guidance message when disabling on the base Mash' do
    expect { Hashie::Mash.disable_warnings }.to raise_error(described_class, expected_message)
  end

  it 'raises with the guidance message when disabling specific methods on the base Mash' do
    expect { Hashie::Mash.disable_warnings(:size) }.to raise_error(described_class, expected_message)
    expect(Hashie::Mash.disable_warnings?).to be false
    expect(Hashie::Mash.disabled_warnings).to be_empty
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

s2/m1 · line 22 proven

- 'you cannot disable warnings on the base Mash class; ' \
- 'please subclass the Mash and disable it in the subclass'
+ 'warnings were disabled on the base Mash class; ' \
+ 'no further action is required'

lib/hashie/extensions/mash/required_keys.rb

1.00kill rate, 95% interval 0.57–1.00 (n=5 distinct)
5 of 5caught by the project's tests
0got through
0proven missing tests
language
ruby
status
needs-review

lib/hashie/extensions/parsers/yaml_erb_parser.rb

0.00kill rate, 95% interval 0.00–0.43 (n=5 distinct)
0 of 5caught by the project's tests
5got through
5proven missing tests
language
ruby
status
needs-review
the tests corral wrote
require 'spec_helper'
require 'tempfile'
require_relative '../../../lib/hashie/extensions/parsers/yaml_erb_parser.rb'

class HashieCorralEvilObject
  attr_accessor :name
end

describe Hashie::Extensions::Parsers::YamlErbParser do
  describe 'corral: safe YAML loading after ERB evaluation' do
    def corral_parse(content)
      file = Tempfile.new(['corral', '.yml'])
      file.write(content)
      file.close
      described_class.new(file.path).perform
    ensure
      file.close!
    end

    it 'refuses to deserialize arbitrary Ruby objects' do
      content = "--- !ruby/object:HashieCorralEvilObject\nname: <%= 'x' %>\n"
      result = nil
      expect { result = corral_parse(content) }.to raise_error(Psych::Exception)
      expect(result).not_to be_a(HashieCorralEvilObject)
    end

    it 'refuses to deserialize objects nested inside a hash' do
      content = "key: !ruby/object:HashieCorralEvilObject\n  name: y\n"
      expect { corral_parse(content) }.to raise_error(Psych::Exception)
    end

    it 'still evaluates ERB and loads plain data' do
      expect(corral_parse("count: <%= 1 + 2 %>\n")).to eq('count' => 3)
    end
  end
end

describe Hashie::Extensions::Parsers::YamlErbParser do
  describe '#perform ignores environment-supplied templates (corral)' do
    let(:env_key) { 'HASHIE_YAML_TEMPLATE' }

    around do |example|
      original = ENV[env_key]
      begin
        example.run
      ensure
        if original.nil?
          ENV.delete(env_key)
        else
          ENV[env_key] = original
        end
      end
    end

    it 'only evaluates the file content, never an ENV-provided template' do
      ENV[env_key] = "injected: <%= 1 + 1 %>\n"

      file = Tempfile.new(['corral', '.yml'])
      begin
        file.write("foo: <%= 'from_file' %>\n")
        file.close

        result = described_class.new(file.path).perform

        expect(result).to eq('foo' => 'from_file')
        expect(result).not_to have_key('injected')
      ensure
        file.close!
      end
    end
  end
end

describe Hashie::Extensions::Parsers::YamlErbParser do
  describe '#perform safe loading (corral)' do
    def corral_write_yaml(content)
      file = Tempfile.new(['corral', '.yml'])
      file.write(content)
      file.close
      file
    end

    it 'refuses to deserialize arbitrary Ruby objects from the YAML' do
      instantiated = []
      evil = Class.new do
        define_method(:init_with) { |_coder| instantiated << true }
      end
      stub_const('HashieCorralEvil', evil)

      file = corral_write_yaml("--- !ruby/object:HashieCorralEvil\nfoo: 1\n")
      begin
        expect { described_class.new(file.path).perform }.to raise_error(StandardError)
        expect(instantiated).to be_empty
      ensure
        file.unlink
      end
    end

    it 'refuses to deserialize arbitrary objects produced via ERB output' do
      instantiated = []
      evil = Class.new do
        define_method(:init_with) { |_coder| instantiated << true }
      end
      stub_const('HashieCorralEvil2', evil)

      file = corral_write_yaml("payload: !ruby/object:HashieCorralEvil2\n  a: <%= 1 + 1 %>\n")
      begin
        expect { described_class.new(file.path).perform }.to raise_error(StandardError)
        expect(instantiated).to be_empty
      ensure
        file.unlink
      end
    end

    it 'still evaluates ERB and loads plain data' do
      file = corral_write_yaml("count: <%= 1 + 2 %>\n")
      begin
        expect(described_class.new(file.path).perform).to eq('count' => 3)
      ensure
        file.unlink
      end
    end
  end
end

# frozen_string_literal: true


describe Hashie::Extensions::Parsers::YamlErbParser do
  describe '#perform ERB evaluation happens exactly once (corral)' do
    let(:tempfile) do
      file = Tempfile.new(['corral', '.yml'])
      file.write(content)
      file.close
      file
    end

    after { tempfile.close! }

    context 'when ERB output itself looks like an ERB tag' do
      let(:content) do
        # `<%%` is rendered by the first ERB pass as the literal text `<%`
        "---\nfoo: '<%%= 1 + 2 %>'\n"
      end

      it 'does not evaluate the rendered output as ERB a second time' do
        result = described_class.new(tempfile.path).perform
        expect(result['foo']).to eq '<%= 1 + 2 %>'
      end
    end

    context 'when ERB output contains code that would have side effects if re-evaluated' do
      let(:content) do
        "---\nfoo: '<%% $hashie_corral_marker = true %>'\n"
      end

      before { $hashie_corral_marker = nil }
      after { $hashie_corral_marker = nil }

      it 'never executes code produced by the first ERB pass' do
        result = described_class.new(tempfile.path).perform
        expect($hashie_corral_marker).to be_nil
        expect(result['foo']).to eq '<% $hashie_corral_marker = true %>'
      end
    end
  end
end

# --- corral: separate test file (unmergeable) — m5 ---
# lang: 2 authored parts each declare "corral_parse", which is not a test and cannot be renamed safely
require 'spec_helper'
require 'tempfile'
require 'yaml'
require_relative '../../../lib/hashie/extensions/parsers/yaml_erb_parser.rb'

class YamlErbCorralDangerousObject
  attr_accessor :payload
end

describe Hashie::Extensions::Parsers::YamlErbParser do
  describe 'corral: safe YAML loading after ERB' do
    def corral_parse(content)
      file = Tempfile.new(['corral', '.yml'])
      file.write(content)
      file.close
      begin
        described_class.new(file.path).perform
      ensure
        file.close!
      end
    end

    it 'does not deserialize arbitrary Ruby objects from the YAML result' do
      content = <<~YAML
        ---
        evil: !ruby/object:YamlErbCorralDangerousObject
          payload: <%= 1 + 1 %>
      YAML

      result = begin
        corral_parse(content)
      rescue Psych::Exception, ArgumentError, TypeError => e
        e
      end

      expect(result).not_to be_a(YamlErbCorralDangerousObject)
      if result.is_a?(Hash)
        expect(result.values.none? { |v| v.is_a?(YamlErbCorralDangerousObject) }).to be true
      else
        expect(result).to be_a(Exception)
      end
    end

    it 'rejects tagged ruby objects with a Psych error' do
      content = "--- !ruby/object:YamlErbCorralDangerousObject\npayload: 1\n"
      expect { corral_parse(content) }.to raise_error(Psych::Exception)
    end

    it 'still evaluates ERB before loading plain YAML' do
      expect(corral_parse("count: <%= 2 * 3 %>\n")).to eq('count' => 6)
    end
  end
end

The faults that got through

A proven fault was caught by a test corral wrote and ran; an unproven one was not, so it may be a missing test or a fault no test can see.

m2 · line 34 proven

- template = ERB.new(@content)
+ template = ERB.new(ENV['HASHIE_YAML_TEMPLATE'] || @content)

m5 · line 34 proven

- _yaml_safe_load(template.result)
+ Psych.respond_to?(:unsafe_load) ? Psych.unsafe_load(template.result) : Psych.load(template.result)

m1 · line 36 proven

- _yaml_safe_load(template.result)
+ YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(template.result) : YAML.load(template.result)

m3 · line 36 proven

- _yaml_safe_load(template.result)
+ YAML.parse(template.result).to_ruby

m4 · line 36 proven

- _yaml_safe_load(template.result)
+ _yaml_safe_load(ERB.new(template.result).result)