Skip to content

Commit 0a85410

Browse files
committed
Pretty-print contracts in error messages.
1 parent 8112ddc commit 0a85410

File tree

6 files changed

+341
-10
lines changed

6 files changed

+341
-10
lines changed

features/basics/pretty-print.feature

+241
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
Feature: Pretty printing Contract violations
2+
3+
Scenario: Big array argument being passed to big array method parameter
4+
Given a file named "example.rb" with:
5+
"""ruby
6+
require "contracts"
7+
C = Contracts
8+
9+
class Example
10+
include Contracts::Core
11+
12+
class << self
13+
Contract [
14+
C::Or[String, Symbol],
15+
C::Or[String, Symbol],
16+
C::Or[String, Symbol],
17+
C::Or[String, Symbol],
18+
C::Or[String, Symbol],
19+
C::Or[String, Symbol],
20+
C::Or[String, Symbol]
21+
] => nil
22+
def run(data)
23+
nil
24+
end
25+
end
26+
end
27+
28+
puts Example.run([
29+
["foo", "foo"],
30+
["foo", "foo"],
31+
["foo", "foo"],
32+
["foo", "foo"],
33+
["foo", "foo"],
34+
["foo", "foo"],
35+
["foo", "foo"],
36+
["foo", "foo"],
37+
["foo", "foo"]
38+
])
39+
"""
40+
When I run `ruby example.rb`
41+
Then the output should contain:
42+
"""
43+
: Contract violation for argument 1 of 1: (ParamContractError)
44+
Expected: [(String or Symbol),
45+
(String or Symbol),
46+
(String or Symbol),
47+
(String or Symbol),
48+
(String or Symbol),
49+
(String or Symbol),
50+
(String or Symbol)],
51+
Actual: [["foo", "foo"],
52+
["foo", "foo"],
53+
["foo", "foo"],
54+
["foo", "foo"],
55+
["foo", "foo"],
56+
["foo", "foo"],
57+
["foo", "foo"],
58+
["foo", "foo"],
59+
["foo", "foo"]]
60+
Value guarded in: Example::run
61+
With Contract: Array => NilClass
62+
At: example.rb:17
63+
"""
64+
65+
Scenario: Big array value being returned from method expecting different big array type
66+
Given a file named "example.rb" with:
67+
"""ruby
68+
require "contracts"
69+
C = Contracts
70+
71+
class Example
72+
include Contracts::Core
73+
74+
class << self
75+
Contract C::None => [
76+
C::Or[String, Symbol],
77+
C::Or[String, Symbol],
78+
C::Or[String, Symbol],
79+
C::Or[String, Symbol],
80+
C::Or[String, Symbol],
81+
C::Or[String, Symbol],
82+
C::Or[String, Symbol]
83+
]
84+
def run
85+
[
86+
["foo", "foo"],
87+
["foo", "foo"],
88+
["foo", "foo"],
89+
["foo", "foo"],
90+
["foo", "foo"],
91+
["foo", "foo"],
92+
["foo", "foo"],
93+
["foo", "foo"],
94+
["foo", "foo"]
95+
]
96+
end
97+
end
98+
end
99+
100+
puts Example.run
101+
"""
102+
When I run `ruby example.rb`
103+
Then the output should contain:
104+
"""
105+
: Contract violation for return value: (ReturnContractError)
106+
Expected: [(String or Symbol),
107+
(String or Symbol),
108+
(String or Symbol),
109+
(String or Symbol),
110+
(String or Symbol),
111+
(String or Symbol),
112+
(String or Symbol)],
113+
Actual: [["foo", "foo"],
114+
["foo", "foo"],
115+
["foo", "foo"],
116+
["foo", "foo"],
117+
["foo", "foo"],
118+
["foo", "foo"],
119+
["foo", "foo"],
120+
["foo", "foo"],
121+
["foo", "foo"]]
122+
Value guarded in: Example::run
123+
With Contract: None => Array
124+
At: example.rb:17
125+
"""
126+
127+
Scenario: Big hash argument being passed to big hash method parameter
128+
Given a file named "example.rb" with:
129+
"""ruby
130+
require "contracts"
131+
C = Contracts
132+
133+
class Example
134+
include Contracts::Core
135+
136+
class << self
137+
Contract ({
138+
a: C::Or[String, Symbol],
139+
b: C::Or[String, Symbol],
140+
c: C::Or[String, Symbol],
141+
d: C::Or[String, Symbol],
142+
e: C::Or[String, Symbol],
143+
f: C::Or[String, Symbol],
144+
g: C::Or[String, Symbol]
145+
}) => nil
146+
def run(data)
147+
nil
148+
end
149+
end
150+
end
151+
152+
puts Example.run({
153+
a: ["foo", "foo"],
154+
b: ["foo", "foo"],
155+
c: ["foo", "foo"],
156+
d: ["foo", "foo"],
157+
e: ["foo", "foo"],
158+
f: ["foo", "foo"],
159+
g: ["foo", "foo"]
160+
})
161+
"""
162+
When I run `ruby example.rb`
163+
Then the output should contain:
164+
"""
165+
: Contract violation for argument 1 of 1: (ParamContractError)
166+
Expected: {:a=>(String or Symbol),
167+
:b=>(String or Symbol),
168+
:c=>(String or Symbol),
169+
:d=>(String or Symbol),
170+
:e=>(String or Symbol),
171+
:f=>(String or Symbol),
172+
:g=>(String or Symbol)},
173+
Actual: {:a=>["foo", "foo"],
174+
:b=>["foo", "foo"],
175+
:c=>["foo", "foo"],
176+
:d=>["foo", "foo"],
177+
:e=>["foo", "foo"],
178+
:f=>["foo", "foo"],
179+
:g=>["foo", "foo"]}
180+
Value guarded in: Example::run
181+
With Contract: Hash => NilClass
182+
At: example.rb:17
183+
"""
184+
185+
Scenario: Big hash value being returned from method expecting different big hash type
186+
Given a file named "example.rb" with:
187+
"""ruby
188+
require "contracts"
189+
C = Contracts
190+
191+
class Example
192+
include Contracts::Core
193+
194+
class << self
195+
Contract C::None => ({
196+
a: C::Or[String, Symbol],
197+
b: C::Or[String, Symbol],
198+
c: C::Or[String, Symbol],
199+
d: C::Or[String, Symbol],
200+
e: C::Or[String, Symbol],
201+
f: C::Or[String, Symbol],
202+
g: C::Or[String, Symbol]
203+
})
204+
def run
205+
{
206+
a: ["foo", "foo"],
207+
b: ["foo", "foo"],
208+
c: ["foo", "foo"],
209+
d: ["foo", "foo"],
210+
e: ["foo", "foo"],
211+
f: ["foo", "foo"],
212+
g: ["foo", "foo"]
213+
}
214+
end
215+
end
216+
end
217+
218+
puts Example.run
219+
"""
220+
When I run `ruby example.rb`
221+
Then the output should contain:
222+
"""
223+
: Contract violation for return value: (ReturnContractError)
224+
Expected: {:a=>(String or Symbol),
225+
:b=>(String or Symbol),
226+
:c=>(String or Symbol),
227+
:d=>(String or Symbol),
228+
:e=>(String or Symbol),
229+
:f=>(String or Symbol),
230+
:g=>(String or Symbol)},
231+
Actual: {:a=>["foo", "foo"],
232+
:b=>["foo", "foo"],
233+
:c=>["foo", "foo"],
234+
:d=>["foo", "foo"],
235+
:e=>["foo", "foo"],
236+
:f=>["foo", "foo"],
237+
:g=>["foo", "foo"]}
238+
Value guarded in: Example::run
239+
With Contract: None => Hash
240+
At: example.rb:17
241+
"""

lib/contracts.rb

+43-8
Original file line numberDiff line numberDiff line change
@@ -116,22 +116,57 @@ def to_s
116116
# This function is used by the default #failure_callback method
117117
# and uses the hash passed into the failure_callback method.
118118
def self.failure_msg(data)
119-
expected = Contracts::Formatters::Expected.new(data[:contract]).contract
120-
position = Contracts::Support.method_position(data[:method])
119+
indent_amount = 8
121120
method_name = Contracts::Support.method_name(data[:method])
122121

122+
# Header
123123
header = if data[:return_value]
124124
"Contract violation for return value:"
125125
else
126126
"Contract violation for argument #{data[:arg_pos]} of #{data[:total_args]}:"
127127
end
128128

129-
%{#{header}
130-
Expected: #{expected},
131-
Actual: #{data[:arg].inspect}
132-
Value guarded in: #{data[:class]}::#{method_name}
133-
With Contract: #{data[:contracts]}
134-
At: #{position} }
129+
# Expected
130+
expected_prefix = "Expected: "
131+
expected_value = Contracts::Support.indent_string(
132+
Contracts::Formatters::Expected.new(data[:contract]).contract.pretty_inspect,
133+
expected_prefix.length
134+
).strip
135+
expected_line = expected_prefix + expected_value + ","
136+
137+
# Actual
138+
actual_prefix = "Actual: "
139+
actual_value = Contracts::Support.indent_string(
140+
data[:arg].pretty_inspect,
141+
actual_prefix.length
142+
).strip
143+
actual_line = actual_prefix + actual_value
144+
145+
# Value guarded in
146+
value_prefix = "Value guarded in: "
147+
value_value = "#{data[:class]}::#{method_name}"
148+
value_line = value_prefix + value_value
149+
150+
# Contract
151+
contract_prefix = "With Contract: "
152+
contract_value = data[:contracts].to_s
153+
contract_line = contract_prefix + contract_value
154+
155+
# Position
156+
position_prefix = "At: "
157+
position_value = Contracts::Support.method_position(data[:method])
158+
position_line = position_prefix + position_value
159+
160+
header +
161+
"\n" +
162+
Contracts::Support.indent_string(
163+
[expected_line,
164+
actual_line,
165+
value_line,
166+
contract_line,
167+
position_line].join("\n"),
168+
indent_amount
169+
)
135170
end
136171

137172
# Callback for when a contract fails. By default it raises

lib/contracts/formatters.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require "pp"
2+
13
module Contracts
24
# A namespace for classes related to formatting.
35
module Formatters
@@ -25,13 +27,13 @@ def hash_contract(hash)
2527
@full = true # Complex values output completely, overriding @full
2628
hash.inject({}) do |repr, (k, v)|
2729
repr.merge(k => InspectWrapper.create(contract(v), @full))
28-
end.inspect
30+
end
2931
end
3032

3133
# Formats Array contracts.
3234
def array_contract(array)
3335
@full = true
34-
array.map { |v| InspectWrapper.create(contract(v), @full) }.inspect
36+
array.map { |v| InspectWrapper.create(contract(v), @full) }
3537
end
3638
end
3739

lib/contracts/support.rb

+7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ def eigenclass?(target)
4242
target <= eigenclass_of(Object)
4343
end
4444

45+
def indent_string(string, amount)
46+
string.gsub(
47+
/^(?!$)/,
48+
(string[/^[ \t]/] || " ") * amount
49+
)
50+
end
51+
4552
private
4653

4754
# Module eigenclass can be detected by its ancestor chain

spec/contracts_spec.rb

+22
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,28 @@ def delim(match)
637637
end.to raise_error(ContractError, not_s(delim "String or Symbol"))
638638
end
639639

640+
it "should wrap and pretty print for long param contracts" do
641+
expect do
642+
@o.long_array_param_contracts(true)
643+
end.to(
644+
raise_error(
645+
ParamContractError,
646+
/\[\(String or Symbol\),\n \(String or Symbol\),/
647+
)
648+
)
649+
end
650+
651+
it "should wrap and pretty print for long return contracts" do
652+
expect do
653+
@o.long_array_return_contracts
654+
end.to(
655+
raise_error(
656+
ReturnContractError,
657+
/\[\(String or Symbol\),\n \(String or Symbol\),/
658+
)
659+
)
660+
end
661+
640662
it "should not contain Contracts:: module prefix" do
641663
expect do
642664
@o.double("bad")

0 commit comments

Comments
 (0)