module AssertValueAssertion

Public Instance Methods

assert_value(*args) { || ... } click to toggle source

assert_value: assert which checks that two strings (expected and actual) are same and which can “magically” replace expected value with the actual in case the new behavior (and new actual value) is correct

Usage ==

Write this in the test source:

assert_value something, <<-END
    foo
    bar
    zee
END

You can also use assert_value for blocks. Then you can assert block result or raised exception

assert_value(<<-END) do
    Exception NoMethodError: undefined method `+' for nil:NilClass
END
    # Code block starts here
    c = nil + 1
end

Then run tests as usual:

rake test:units
ruby test/unit/foo_test.rb
...

When assert_value fails, you'll be able to:

  • review diff

  • (optionally) accept new actual value (this modifies the test source file)

Additional options for test runs: –no-interactive skips all questions and just reports failures –autoaccept prints diffs and automatically accepts all new actual values –no-canonicalize turns off expected and actual value canonicalization (see below for details)

Additional options can be passed during both single test file run and rake test run:

In Ruby 1.8:
ruby test/unit/foo_test.rb -- --autoaccept
ruby test/unit/foo_test.rb -- --no-interactive

rake test TESTOPTS="-- --autoaccept"
rake test:units TESTOPTS="-- --no-canonicalize --autoaccept"

In Ruby 1.9:
ruby test/unit/foo_test.rb --autoaccept
ruby test/unit/foo_test.rb --no-interactive

rake test TESTOPTS="--autoaccept"
rake test:units TESTOPTS="--no-canonicalize --autoaccept"

Canonicalization ==

Before comparing expected and actual strings, assert_value canonicalizes both using these rules:

  • indentation is ignored (except for indentation relative to the first line of the expected/actual string)

  • ruby-style comments after “#” are ignored: both whole-line and end-of-line comments are supported

  • empty lines are ignored

  • trailing whitespaces are ignored

You can turn canonicalization off with –no-canonicalize option. This is useful when you need to regenerate expected test strings. To regenerate the whole test suite, run:

In Ruby 1.8:
rake test TESTOPTS="-- --no-canonicalize --autoaccept"
In Ruby 1.9:
rake test TESTOPTS="--no-canonicalize --autoaccept"

Example of assert_value with comments:

assert_value something, <<-END
    # some tree
    foo 1
      foo 1.1
      foo 1.2    # some node
    bar 2
      bar 2.1
END

Umportant Usage Rules ==

Restrictions:

  • only END and EOS are supported as end of string sequence

  • it's a requirement that you have <<-END at the same line as assert_value

  • assert_value can't be within a block

Storing expected output in files:

  • assert_value something, :log => <path_to_file>

  • path to file is relative to:

    • RAILS_ROOT (if that is defined)

    • current dir (if no RAILS_ROOT is defined)

  • file doesn't have to exist, it will be created if necessary

Misc:

  • it's ok to have several assert_value's in the same test method, assert_value. correctly updates all assert_value's in the test file

  • it's ok to omit expected string, like this:

    assert_value something
    

    in fact, this is the preferred way to create assert_value tests - you write empty assert_value, run tests and they will fill expected values for you automatically

# File lib/assert_value.rb, line 188
def assert_value(*args)
    if block_given?
        # rspec passes block to the expect() function, not to the matcher
        # so string substitution should work as if assert_value is called with a string
        mode = defined?(@rspec_matcher) ? :scalar : :block
        expected = args[0]
        actual = ""
        begin
            actual = yield.to_s
        rescue Exception => e
            actual = "Exception #{e.class}: #{e.message}"
        end
    else
        mode = :scalar
        expected = args[1]
        actual = args[0]
    end

    if expected.nil?
        expected = ""
        change = :create_expected_string
    elsif expected.class == String
        change = :update_expected_string
    elsif expected.class == Hash
        raise ":log key is missing" unless expected.has_key? :log
        log_file = expected[:log]
        if defined?(RAILS_ROOT)
            log_file = File.expand_path(log_file, RAILS_ROOT)
        else
            log_file = File.expand_path(log_file, Dir.pwd)
        end
        expected = File.exist?(log_file) ? File.read(log_file) : ""
        change = :update_file_with_expected_string
    else
        internal_error("Invalid expected class #{expected.class}")
    end

    # interactive mode is turned on by default, except when
    # - --no-interactive is given
    # - CIRCLECI is set (CircleCI captures test output, but doesn't interact with user)
    # - STDIN is not a terminal device (i.e. we can't ask any questions)
    interactive = !$assert_value_options.include?("--no-interactive") && !ENV["CIRCLECI"] && STDIN.tty?
    canonicalize = !$assert_value_options.include?("--no-canonicalize")
    autoaccept = $assert_value_options.include?("--autoaccept")

    is_same_canonicalized, is_same, diff_canonicalized, diff = compare_for_assert_value(expected, actual)

    if (canonicalize and !is_same_canonicalized) or (!canonicalize and !is_same)
        diff_to_report = canonicalize ? diff_canonicalized : diff
        if interactive
            # print method name and short backtrace
            soft_fail(diff_to_report)

            if autoaccept
                accept = true
            else
                print "Accept the new value: yes to all, no to all, yes, no? [Y/N/y/n] (y): "
                STDOUT.flush
                response = STDIN.gets.strip
                accept = ["", "y", "Y"].include? response
                $assert_value_options << "--autoaccept" if response == "Y"
                $assert_value_options << "--no-interactive" if response == "N"
            end

            if accept
                if [:create_expected_string, :update_expected_string].include? change
                    accept_string(actual, change, mode)
                elsif change == :update_file_with_expected_string
                    accept_file(actual, log_file)
                else
                    internal_error("Invalid change #{change}")
                end
            end
        end
        if accept
            # when change is accepted, we should not report it as a failure because
            # we want the test method to continue executing (in case there're more
            # assert_value's in the method)
            succeed
        else
            fail(diff)
        end
    else
        succeed
    end
end
be_same_value_as(expected = nil) click to toggle source
# File lib/assert_value.rb, line 275
def be_same_value_as(expected = nil)
  BeSameValueAs.new(expected)
end
file_offsets() click to toggle source
# File lib/assert_value.rb, line 82
def file_offsets
  @@file_offsets ||= Hash.new { |hash, key| hash[key] = {} }
end

Private Instance Methods

accept_file(actual, log_file) click to toggle source
# File lib/assert_value.rb, line 378
def accept_file(actual, log_file)
    log = File.open(log_file, "w+")
    log.write(actual)
    log.fsync
    log.close
end
accept_string(actual, change, mode) click to toggle source

actual - actual value of the scalar or result of the executed block change - what to do with expected value (:create_expected_string or :update_expected_string) mode - describes signature of assert_value call by type of main argument (:block or :scalar)

# File lib/assert_value.rb, line 318
def accept_string(actual, change, mode)
    depth = @rspec_matcher ? 8 : 3
    file, _, line = get_caller_location(:depth => depth)

    # read source file, construct the new source, replacing everything
    # between "do" and "end" in assert_value's block
    # using File::expand_path here because "file" can be either
    # absolute path (when test is run with "rake test" runs)
    # or relative path (when test is run via ruby <path_to_test_file>)
    source = File.readlines(File::expand_path(file))

    # file may be changed by previous accepted assert_value's, adjust line numbers
    offset = file_offsets[file].keys.inject(0) do |sum, i|
        line.to_i >= i ? sum + file_offsets[file][i] : sum
    end

    expected_text_end_line = expected_text_start_line = line.to_i + offset
    if change == :update_expected_string
        #search for the end of expected value in code
        expected_text_end_line += 1 while !["END", "EOS"].include?(source[expected_text_end_line].strip)
    elsif change == :create_expected_string
        # The is no expected value yet. expected_text_end_line is unknown
    else
        internal_error("Invalid change #{change}")
    end

    expected_length = expected_text_end_line - expected_text_start_line

    # indentation is the indentation of assert_value call + 4
    indentation = source[expected_text_start_line-1] =~ /^(\s+)/ ? $1.length : 0
    indentation += 4

    if change == :create_expected_string 
        if mode == :scalar
            # add second argument to assert_value if it's omitted
            comma = "," unless @rspec_matcher
            source[expected_text_start_line-1] = "#{source[expected_text_start_line-1].chop}#{comma} <<-END\n"
        elsif mode == :block
            # add expected value as argument to assert_value before block call
            source[expected_text_start_line-1] = source[expected_text_start_line-1].sub(/assert_value(\(.*?\))*/, "assert_value(<<-END)")
        else
            internal_error("Invalid mode #{mode}")
        end
    end
    source = source[0, expected_text_start_line] +
        actual.split("\n").map { |l| "#{" "*(indentation)}#{l}\n"} +
        (change == :create_expected_string ? ["#{" "*(indentation-4)}END\n"] : [])+
        source[expected_text_end_line, source.length]

    # recalculate line number adjustments
    actual_length = actual.split("\n").length
    actual_length += 1 if change == :create_expected_string # END marker after expected value
    file_offsets[file][line.to_i] = actual_length - expected_length

    source_file = File.open(file, "w+")
    source_file.write(source.join(''))
    source_file.fsync
    source_file.close
end
canonicalize_for_assert_value(text) click to toggle source
# File lib/assert_value.rb, line 393
def canonicalize_for_assert_value(text)
    # make array of lines out of the text
    result = text.split("\n")

    # ignore leading newlines if any (trailing will be automatically ignored by split())
    result.delete_at(0) if result[0] == ""

    indentation = $1.length if result[0] and result[0] =~ /^(\s+)/

    result.map! do |line|
        # ignore indentation: we assume that the first line defines indentation
        line.gsub!(/^\s{#{indentation}}/, '') if indentation
        # ignore trailing spaces
        line.gsub(/\s*$/, '')
    end

    # ignore comments
    result_canonicalized= result.map do |line|
        line.gsub(/\s*(#.*)?$/, '')
    end
    # ignore blank lines (usually they are lines with comments only)
    result_canonicalized.delete_if { |line| line.nil? or line.empty? }

    [result_canonicalized, result]
end
compare_for_assert_value(expected_verbatim, actual_verbatim) click to toggle source
# File lib/assert_value.rb, line 385
def compare_for_assert_value(expected_verbatim, actual_verbatim)
    expected_canonicalized, expected = canonicalize_for_assert_value(expected_verbatim)
    actual_canonicalized, actual = canonicalize_for_assert_value(actual_verbatim)
    diff_canonicalized = AssertValue::TextDiff.array_diff(expected_canonicalized, actual_canonicalized)
    diff = AssertValue::TextDiff.array_diff(expected, actual)
    [expected_canonicalized == actual_canonicalized, expected == actual, diff_canonicalized, diff]
end
fail(diff) click to toggle source
# File lib/assert_value.rb, line 306
def fail(diff)
    increment_assertion_count
    if [:new_minitest, :old_minitest].include?(ASSERT_VALUE_TEST_FRAMEWORK)
        raise MiniTest::Assertion.new(diff)
    else
        raise Test::Unit::AssertionFailedError.new(diff)
    end
end
get_caller_location(options = {:depth => 2}) click to toggle source
# File lib/assert_value.rb, line 419
def get_caller_location(options = {:depth => 2})
    caller_method = caller(options[:depth])[0]

    #Sample output is:
    #either full path when run as "rake test"
    #   /home/user/assert_value/test/unit/assure_test.rb:9:in `test_assure
    #or relative path when run as ruby test/unit/assure_test.rb
    #   test/unit/assure_test.rb:9:in `test_assure
    caller_method =~ /([^:]+):([0-9]+):in `(.+)'/
    file = $1
    line = $2
    method = $3
    [file, method, line]
end
increment_assertion_count() click to toggle source
# File lib/assert_value.rb, line 286
def increment_assertion_count
    return if defined?(@rspec_matcher) or self.class.name =~ /RSpec/
    case ASSERT_VALUE_TEST_FRAMEWORK
        when :new_minitest then self.assertions += 1
        when :old_minitest then self._assertions += 1
        when :test_unit    then add_assertion
    end
end
soft_fail(diff) click to toggle source
# File lib/assert_value.rb, line 295
def soft_fail(diff)
    if [:new_minitest, :old_minitest].include?(ASSERT_VALUE_TEST_FRAMEWORK)
        failure = MiniTest::Assertion.new(diff)
    elsif [:rspec_only].include?(ASSERT_VALUE_TEST_FRAMEWORK)
        failure = diff
    else
        failure = Test::Unit::Failure.new(name, filter_backtrace(caller(0)), diff)
    end
    puts "\n#{failure}"
end
succeed() click to toggle source
# File lib/assert_value.rb, line 281
def succeed
    increment_assertion_count
    true
end