Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions spec/compiler/formatter/formatter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,43 @@ private def assert_format(input, output = input, strict = false, file = __FILE__
output = "#{output}\n" unless strict
result = Crystal.format(input)
unless result == output
fail "Expected\n\n~~~\n#{input}\n~~~\nto format to:\n\n~~~\n#{output}\n~~~\n\nbut got:\n\n~~~\n#{result}\n~~~\n\n assert_format #{input.inspect}, #{result.chomp.inspect}"
fail <<-MSG
Expected

~~
#{input}
~~~

to format to:

~~~
#{output}
~~~

but got:

~~~
#{result}
~~~

diff:

~~~
#{Spec.diff(output, result)}
~~~

assert_format #{input.inspect}, #{result.chomp.inspect}
MSG
end

# Check idempotency
result2 = Crystal.format(result)
unless result == result2
fail "Idempotency failed:\nBefore: #{result.inspect}\nAfter: #{result2.inspect}"
fail <<-MSG
Idempotency failed:
Before: #{result.inspect}
After: #{result2.inspect}
MSG
end
end
end
Expand Down
56 changes: 55 additions & 1 deletion spec/std/spec/expectations_spec.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
require "spec"
require "../spec_helper"

private def it_on_diff_and_no_diff(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block)
it(description, file, line, end_line, &block)

it("#{description} [no diff]", file, line, end_line) do
old_use_diff = Spec.use_diff
begin
Spec.use_diff = false
yield
ensure
Spec.use_diff = old_use_diff
end
end
end

describe "expectations" do
describe "be" do
Expand Down Expand Up @@ -141,4 +155,44 @@ describe "expectations" do
expect_raises(Exception, "Ops") { raise Exception.new("Ops") }
end
end

describe "Spec.diff" do
it { Spec.diff("", "").should be_nil }
it { Spec.diff("foo", "bar").should be_nil }
pending_diff { Spec.diff("foo\nbar", "foo\nbar").should eq("") }
pending_diff { Spec.diff("bar\nfoo", "foo\nbar").should eq(<<-DIFF) }
@@ -1,2 +1,2 @@
-bar
foo
+bar
DIFF
end

describe "Spec.diff_values" do
it { Spec.diff_values("", "").should be_nil }
it { Spec.diff_values("foo", "bar").should be_nil }

pending_diff "shows diff of two long arrays" do
xs = (1..100).to_a
ys = xs[0...50] + [-1] + xs[50...100]
Spec.diff_values(xs, ys).should eq(<<-DIFF)
@@ -48,6 +48,7 @@
48,
49,
50,
+ -1,
51,
52,
53,
DIFF
end

pending_diff "shows the message when the diff is empty" do
xs = (1..100).to_a
Spec.diff_values(xs, xs).should eq(<<-MSG)
No visible difference in the `Array(Int32)#pretty_inspect` output.
You should look at the implementation of `#==` on Array(Int32) or its members.
MSG
end
end
end
1 change: 1 addition & 0 deletions spec/std/spec_helper.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require "spec"
require "../support/tempfile"
require "../support/fibers"
require "../support/win32"
require "../support/diff"

def datapath(*components)
File.join("spec", "std", "data", *components)
Expand Down
30 changes: 29 additions & 1 deletion spec/std/spec_spec.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require "spec"
require "./spec_helper"

private class SpecException < Exception
getter value : Int32
Expand All @@ -15,6 +15,34 @@ private class NilMimicker
end

describe "Spec matchers" do
describe "should eq" do
it "passes for the same value" do
1.should eq(1)
end

pending_diff "should raise an exception with diff when the values are long" do
xs = (1..100).to_a
ys = xs[0...50] + [-1] + xs[50...100]
msg = <<-MSG
Expected: #{xs.inspect}
got: #{ys.inspect}

Difference:
@@ -48,6 +48,7 @@
48,
49,
50,
+ -1,
51,
52,
53,
MSG
expect_raises Spec::AssertionFailed, msg do
ys.should eq(xs)
end
end
end

describe "should be_truthy" do
it "passes for true" do
true.should be_truthy
Expand Down
7 changes: 7 additions & 0 deletions spec/support/diff.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def pending_diff(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block)
if Spec.use_diff?
it(description, file, line, end_line, &block)
else
pending("#{description} [no diff]", file, line, end_line)
end
end
107 changes: 106 additions & 1 deletion src/spec/expectations.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,97 @@
module Spec
# A list of diff command candidates.
DIFF_COMMANDS = [
%w(git diff --no-index -U3),
%w(gdiff -u),
%w(diff -u),
]

# A diff command path and options to use in diff computation.
class_property diff_command : Array(String)? do
DIFF_COMMANDS.each.compact_map { |cmd| check_diff_command(cmd) }.first?
end

# A flag whether it uses diff on generating a expectation message.
class_property? use_diff : Bool { diff_command != nil }

# Checks the given `diff` command works.
# It takes an array of strings as `diff` command and options,
# and it returns a new array with resolved path and options if it works,
# otherwise it returns `nil`.
private def self.check_diff_command(cmd)
name = Process.find_executable(cmd[0])
return unless name
opts = cmd[1..]

begin
tmp_file = File.tempfile { |f| f.puts "check_diff" }

# Try to invoke `diff` against a temporary file.
# When the `diff` exists with success status and its output is empty,
# we assume the `diff` command works.
output = String.build do |io|
status = Process.run(name, opts + [tmp_file.path, tmp_file.path], output: io)
return unless status.success?
end
return unless output.empty?
ensure
# Clean up temporary files!
tmp_file.try &.delete
end

[name] + opts
end

# Compute the difference between two values *expected_value* and *actual_value*
# by using `diff` command.
def self.diff_values(expected_value, actual_value)
expected = expected_value.pretty_inspect
actual = actual_value.pretty_inspect

result = diff expected, actual

# When the diff output is nothing even though the two values do not equal,
# it returns a fallback message so far.
if result && result.empty?
klass = expected_value.class
return <<-MSG
No visible difference in the `#{klass}#pretty_inspect` output.
You should look at the implementation of `#==` on #{klass} or its members.
MSG
end

result
end

# Compute the difference between two strings *expected* and *actual*
# by using `diff` command.
def self.diff(expected, actual)
return unless Spec.use_diff?

# If the diff command is available and outputs contain a newline,
# then it computes diff of them.
diff_command = Spec.diff_command
return unless diff_command && (expected.includes?('\n') || actual.includes?('\n'))
diff_command_name = diff_command[0]
diff_command_opts = diff_command[1..]

begin
expected_file = File.tempfile("expected") { |f| f.puts expected }
actual_file = File.tempfile("actual") { |f| f.puts actual }

# Invoke `diff` command and fix up its output.
output = String.build do |io|
Process.run(diff_command_name, diff_command_opts + [expected_file.path, actual_file.path], output: io)
end
# Remove `--- expected` and `+++ actual` lines.
output.chomp.gsub(/^(-{3}|\+{3}|diff --\w+|index) .+?\n/m, "")
ensure
# Clean up temporary files!
expected_file.try &.delete
actual_file.try &.delete
end
end

# :nodoc:
struct EqualExpectation(T)
def initialize(@expected_value : T)
Expand Down Expand Up @@ -46,7 +139,19 @@ module Spec
expected += " : #{@expected_value.class}"
got += " : #{actual_value.class}"
end
"Expected: #{expected}\n got: #{got}"
msg = <<-MSG
Expected: #{expected}
got: #{got}
MSG
if diff = Spec.diff_values(expected_value, actual_value)
msg = <<-MSG
#{msg}

Difference:
#{diff}
MSG
end
msg
end
end

Expand Down