diff --git a/features/basics/pretty-print.feature b/features/basics/pretty-print.feature new file mode 100644 index 0000000..af88f49 --- /dev/null +++ b/features/basics/pretty-print.feature @@ -0,0 +1,241 @@ +Feature: Pretty printing Contract violations + + Scenario: Big array argument being passed to big array method parameter + Given a file named "example.rb" with: + """ruby + require "contracts" + C = Contracts + + class Example + include Contracts::Core + + class << self + Contract [ + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol] + ] => nil + def run(data) + nil + end + end + end + + puts Example.run([ + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"] + ]) + """ + When I run `ruby example.rb` + Then the output should contain: + """ + : Contract violation for argument 1 of 1: (ParamContractError) + Expected: [(String or Symbol), + (String or Symbol), + (String or Symbol), + (String or Symbol), + (String or Symbol), + (String or Symbol), + (String or Symbol)], + Actual: [["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"]] + Value guarded in: Example::run + With Contract: Array => NilClass + At: example.rb:17 + """ + + Scenario: Big array value being returned from method expecting different big array type + Given a file named "example.rb" with: + """ruby + require "contracts" + C = Contracts + + class Example + include Contracts::Core + + class << self + Contract C::None => [ + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol] + ] + def run + [ + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"] + ] + end + end + end + + puts Example.run + """ + When I run `ruby example.rb` + Then the output should contain: + """ + : Contract violation for return value: (ReturnContractError) + Expected: [(String or Symbol), + (String or Symbol), + (String or Symbol), + (String or Symbol), + (String or Symbol), + (String or Symbol), + (String or Symbol)], + Actual: [["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"], + ["foo", "foo"]] + Value guarded in: Example::run + With Contract: None => Array + At: example.rb:17 + """ + + Scenario: Big hash argument being passed to big hash method parameter + Given a file named "example.rb" with: + """ruby + require "contracts" + C = Contracts + + class Example + include Contracts::Core + + class << self + Contract ({ + a: C::Or[String, Symbol], + b: C::Or[String, Symbol], + c: C::Or[String, Symbol], + d: C::Or[String, Symbol], + e: C::Or[String, Symbol], + f: C::Or[String, Symbol], + g: C::Or[String, Symbol] + }) => nil + def run(data) + nil + end + end + end + + puts Example.run({ + a: ["foo", "foo"], + b: ["foo", "foo"], + c: ["foo", "foo"], + d: ["foo", "foo"], + e: ["foo", "foo"], + f: ["foo", "foo"], + g: ["foo", "foo"] + }) + """ + When I run `ruby example.rb` + Then the output should contain: + """ + : Contract violation for argument 1 of 1: (ParamContractError) + Expected: {:a=>(String or Symbol), + :b=>(String or Symbol), + :c=>(String or Symbol), + :d=>(String or Symbol), + :e=>(String or Symbol), + :f=>(String or Symbol), + :g=>(String or Symbol)}, + Actual: {:a=>["foo", "foo"], + :b=>["foo", "foo"], + :c=>["foo", "foo"], + :d=>["foo", "foo"], + :e=>["foo", "foo"], + :f=>["foo", "foo"], + :g=>["foo", "foo"]} + Value guarded in: Example::run + With Contract: Hash => NilClass + At: example.rb:17 + """ + + Scenario: Big hash value being returned from method expecting different big hash type + Given a file named "example.rb" with: + """ruby + require "contracts" + C = Contracts + + class Example + include Contracts::Core + + class << self + Contract C::None => ({ + a: C::Or[String, Symbol], + b: C::Or[String, Symbol], + c: C::Or[String, Symbol], + d: C::Or[String, Symbol], + e: C::Or[String, Symbol], + f: C::Or[String, Symbol], + g: C::Or[String, Symbol] + }) + def run + { + a: ["foo", "foo"], + b: ["foo", "foo"], + c: ["foo", "foo"], + d: ["foo", "foo"], + e: ["foo", "foo"], + f: ["foo", "foo"], + g: ["foo", "foo"] + } + end + end + end + + puts Example.run + """ + When I run `ruby example.rb` + Then the output should contain: + """ + : Contract violation for return value: (ReturnContractError) + Expected: {:a=>(String or Symbol), + :b=>(String or Symbol), + :c=>(String or Symbol), + :d=>(String or Symbol), + :e=>(String or Symbol), + :f=>(String or Symbol), + :g=>(String or Symbol)}, + Actual: {:a=>["foo", "foo"], + :b=>["foo", "foo"], + :c=>["foo", "foo"], + :d=>["foo", "foo"], + :e=>["foo", "foo"], + :f=>["foo", "foo"], + :g=>["foo", "foo"]} + Value guarded in: Example::run + With Contract: None => Hash + At: example.rb:17 + """ diff --git a/lib/contracts.rb b/lib/contracts.rb index baf2acf..a4303d0 100644 --- a/lib/contracts.rb +++ b/lib/contracts.rb @@ -116,22 +116,57 @@ def to_s # This function is used by the default #failure_callback method # and uses the hash passed into the failure_callback method. def self.failure_msg(data) - expected = Contracts::Formatters::Expected.new(data[:contract]).contract - position = Contracts::Support.method_position(data[:method]) + indent_amount = 8 method_name = Contracts::Support.method_name(data[:method]) + # Header header = if data[:return_value] "Contract violation for return value:" else "Contract violation for argument #{data[:arg_pos]} of #{data[:total_args]}:" end - %{#{header} - Expected: #{expected}, - Actual: #{data[:arg].inspect} - Value guarded in: #{data[:class]}::#{method_name} - With Contract: #{data[:contracts]} - At: #{position} } + # Expected + expected_prefix = "Expected: " + expected_value = Contracts::Support.indent_string( + Contracts::Formatters::Expected.new(data[:contract]).contract.pretty_inspect, + expected_prefix.length + ).strip + expected_line = expected_prefix + expected_value + "," + + # Actual + actual_prefix = "Actual: " + actual_value = Contracts::Support.indent_string( + data[:arg].pretty_inspect, + actual_prefix.length + ).strip + actual_line = actual_prefix + actual_value + + # Value guarded in + value_prefix = "Value guarded in: " + value_value = "#{data[:class]}::#{method_name}" + value_line = value_prefix + value_value + + # Contract + contract_prefix = "With Contract: " + contract_value = data[:contracts].to_s + contract_line = contract_prefix + contract_value + + # Position + position_prefix = "At: " + position_value = Contracts::Support.method_position(data[:method]) + position_line = position_prefix + position_value + + header + + "\n" + + Contracts::Support.indent_string( + [expected_line, + actual_line, + value_line, + contract_line, + position_line].join("\n"), + indent_amount + ) end # Callback for when a contract fails. By default it raises diff --git a/lib/contracts/formatters.rb b/lib/contracts/formatters.rb index db9b107..2d4f70b 100644 --- a/lib/contracts/formatters.rb +++ b/lib/contracts/formatters.rb @@ -1,3 +1,5 @@ +require "pp" + module Contracts # A namespace for classes related to formatting. module Formatters @@ -25,13 +27,13 @@ def hash_contract(hash) @full = true # Complex values output completely, overriding @full hash.inject({}) do |repr, (k, v)| repr.merge(k => InspectWrapper.create(contract(v), @full)) - end.inspect + end end # Formats Array contracts. def array_contract(array) @full = true - array.map { |v| InspectWrapper.create(contract(v), @full) }.inspect + array.map { |v| InspectWrapper.create(contract(v), @full) } end end diff --git a/lib/contracts/support.rb b/lib/contracts/support.rb index f3efa20..cda3a55 100644 --- a/lib/contracts/support.rb +++ b/lib/contracts/support.rb @@ -42,6 +42,13 @@ def eigenclass?(target) target <= eigenclass_of(Object) end + def indent_string(string, amount) + string.gsub( + /^(?!$)/, + (string[/^[ \t]/] || " ") * amount + ) + end + private # Module eigenclass can be detected by its ancestor chain diff --git a/spec/contracts_spec.rb b/spec/contracts_spec.rb index d37c40e..56be470 100644 --- a/spec/contracts_spec.rb +++ b/spec/contracts_spec.rb @@ -637,6 +637,28 @@ def delim(match) end.to raise_error(ContractError, not_s(delim "String or Symbol")) end + it "should wrap and pretty print for long param contracts" do + expect do + @o.long_array_param_contracts(true) + end.to( + raise_error( + ParamContractError, + /\[\(String or Symbol\),\n \(String or Symbol\),/ + ) + ) + end + + it "should wrap and pretty print for long return contracts" do + expect do + @o.long_array_return_contracts + end.to( + raise_error( + ReturnContractError, + /\[\(String or Symbol\),\n \(String or Symbol\),/ + ) + ) + end + it "should not contain Contracts:: module prefix" do expect do @o.double("bad") diff --git a/spec/fixtures/fixtures.rb b/spec/fixtures/fixtures.rb index b6d2bea..67b5740 100644 --- a/spec/fixtures/fixtures.rb +++ b/spec/fixtures/fixtures.rb @@ -152,6 +152,30 @@ def array_complex_contracts(data) def nested_array_complex_contracts(data) end + Contract [ + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol] + ] => nil + def long_array_param_contracts(data) + end + + Contract C::None => [ + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol], + C::Or[String, Symbol] + ] + def long_array_return_contracts + end + Contract Proc => C::Any def do_call(&block) block.call