Skip to content

Commit

Permalink
Merge pull request erlang-lager#77 from basho/adt-format-equivalence
Browse files Browse the repository at this point in the history
Use quickcheck to test for formatting equivalenve with io_lib
  • Loading branch information
Vagabond committed Sep 10, 2012
2 parents c78ecdc + 3587240 commit a582eaf
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 34 deletions.
169 changes: 142 additions & 27 deletions src/lager_trunc_io.erl
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,6 @@ print(Term, _Max, #print_options{force_strings=true}) when not is_list(Term), no
print(_, Max, _Options) when Max < 0 -> {"...", 3};
print(_, _, #print_options{depth=0}) -> {"...", 3};

print(Tuple, Max, Options) when is_tuple(Tuple) ->
{TC, Len} = tuple_contents(Tuple, Max-2, Options),
{[${, TC, $}], Len + 2};

%% @doc We assume atoms, floats, funs, integers, PIDs, ports and refs never need
%% to be truncated. This isn't strictly true, someone could make an
Expand All @@ -130,24 +127,73 @@ print(Atom, _Max, #print_options{force_strings=NoQuote}) when is_atom(Atom) ->
end,
{R, length(R)};

print(<<>>, _Max, _Options) ->
{"<<>>", 4};
print(Bin, _Max, O = #print_options{depth=1}) when is_binary(Bin) ->
case O#print_options.lists_as_strings of
true when Bin == <<>> ->
{"<<>>", 4};
_ ->
{"<<...>>", 7}
end;
print(<<>>, _Max, Options) ->
case Options#print_options.force_strings of
true ->
{"", 0};
false ->
{"<<>>", 4}
end;

print(Binary, 0, _Options) when is_bitstring(Binary) ->
{"<<..>>", 6};

print(Bin, Max, _Options) when is_binary(Bin), Max < 2 ->
{"<<...>>", 7};
print(Binary, Max, Options) when is_binary(Binary) ->
B = binary_to_list(Binary, 1, lists:min([Max, byte_size(Binary)])),
{L, Len} = case Options#print_options.lists_as_strings orelse
Options#print_options.force_strings of
true ->
alist_start(B, Max-4, Options);
Depth = Options#print_options.depth,
MaxSize = (Depth - 1) * 4,
%% check if we need to truncate based on depth
In = case Depth > -1 andalso MaxSize < length(B) andalso
not Options#print_options.force_strings of
true ->
string:substr(B, 1, MaxSize);
false -> B
end,
try alist(In, Max -1, Options) of
{L0, Len0} ->
case Options#print_options.force_strings of
false ->
case B /= In of
true ->
{[$", L0, "..."], Len0+4};
false ->
{[$"|L0], Len0+1}
end;
true ->
{L0, Len0}
end
catch
throw:{unprintable, C} ->
Index = string:chr(In, C),
case Index > 1 andalso Options#print_options.depth =< Index andalso
Options#print_options.depth > -1 andalso
not Options#print_options.force_strings of
true ->
%% print first Index-1 characters followed by ...
{L0, Len0} = alist_start(string:substr(In, 1, Index - 1), Max - 1, Options),
{L0++"...", Len0+3};
false ->
list_body(In, Max-4, dec_depth(Options), true)
end
end;
_ ->
list_body(B, Max-4, Options, false)
list_body(B, Max-4, dec_depth(Options), true)
end,
{Res, Length} = case L of
[91, X, 93] ->
{X, Len - 2};
{X, Len-2};
X ->
{X, Len}
end,
Expand All @@ -163,14 +209,24 @@ print(Binary, Max, Options) when is_binary(Binary) ->
%% some magic for dealing with the output of bitstring_to_list, which returns
%% a list of integers (as expected) but with a trailing binary that represents
%% the remaining bits.
print({inline_bitstring, B}, _Max, _Options) when is_bitstring(B) ->
Size = bit_size(B),
<<Value:Size>> = B,
ValueStr = integer_to_list(Value),
SizeStr = integer_to_list(Size),
{[ValueStr, $:, SizeStr], length(ValueStr) + length(SizeStr) +1};
print(BitString, Max, Options) when is_bitstring(BitString) ->
case byte_size(BitString) > Max of
true ->
BL = binary_to_list(BitString, 1, Max);
_ ->
BL = erlang:bitstring_to_list(BitString)
R = erlang:bitstring_to_list(BitString),
{Bytes, [Bits]} = lists:splitwith(fun erlang:is_integer/1, R),
%% tag the trailing bits with a special tuple we catch when
%% list_body calls print again
BL = Bytes ++ [{inline_bitstring, Bits}]
end,
{X, Len0} = list_body(BL, Max - 4, Options, false),
{X, Len0} = list_body(BL, Max - 4, dec_depth(Options), true),
{["<<", X, ">>"], Len0 + 4};

print(Float, _Max, _Options) when is_float(Float) ->
Expand Down Expand Up @@ -206,6 +262,10 @@ print(Port, _Max, _Options) when is_port(Port) ->
L = erlang:port_to_list(Port),
{L, length(L)};

print(Tuple, Max, Options) when is_tuple(Tuple) ->
{TC, Len} = tuple_contents(Tuple, Max-2, Options),
{[${, TC, $}], Len + 2};

print(List, Max, Options) when is_list(List) ->
case Options#print_options.lists_as_strings orelse
Options#print_options.force_strings of
Expand All @@ -226,12 +286,15 @@ tuple_contents(Tuple, Max, Options) ->
list_body([], _Max, _Options, _Tuple) -> {[], 0};
list_body(_, Max, _Options, _Tuple) when Max < 4 -> {"...", 3};
list_body(_, _Max, #print_options{depth=0}, _Tuple) -> {"...", 3};
list_body([B], _Max, _Options, _Tuple) when is_bitstring(B), not is_binary(B) ->
Size = bit_size(B),
<<Value:Size>> = B,
ValueStr = integer_to_list(Value),
SizeStr = integer_to_list(Size),
{[ValueStr, $:, SizeStr], length(ValueStr) + length(SizeStr) +1};
list_body([H], Max, Options=#print_options{depth=1}, _Tuple) ->
print(H, Max, Options);
list_body([H|_], Max, Options=#print_options{depth=1}, Tuple) ->
{List, Len} = print(H, Max-4, Options),
Sep = case Tuple of
true -> $,;
false -> $|
end,
{[List ++ [Sep | "..."]], Len + 4};
list_body([H|T], Max, Options, Tuple) ->
{List, Len} = print(H, Max, Options),
{Final, FLen} = list_bodyc(T, Max - Len, Options, Tuple),
Expand All @@ -242,15 +305,11 @@ list_body(X, Max, Options, _Tuple) -> %% improper list

list_bodyc([], _Max, _Options, _Tuple) -> {[], 0};
list_bodyc(_, Max, _Options, _Tuple) when Max < 5 -> {",...", 4};
list_bodyc([B], _Max, _Options, _Tuple) when is_bitstring(B), not is_binary(B) ->
Size = bit_size(B),
<<Value:Size>> = B,
ValueStr = integer_to_list(Value),
SizeStr = integer_to_list(Size),
{[$, , ValueStr, $:, SizeStr], length(ValueStr) + length(SizeStr) +2};
list_bodyc(_, _Max, #print_options{depth=1}, true) -> {",...", 4};
list_bodyc(_, _Max, #print_options{depth=1}, false) -> {"|...", 4};
list_bodyc([H|T], Max, #print_options{depth=Depth} = Options, Tuple) ->
{List, Len} = print(H, Max, dec_depth(Options)),
{Final, FLen} = list_bodyc(T, Max - Len - 1, Options, Tuple),
{Final, FLen} = list_bodyc(T, Max - Len - 1, dec_depth(Options), Tuple),
Sep = case Depth == 1 andalso not Tuple of
true -> $|;
_ -> $,
Expand All @@ -273,12 +332,22 @@ alist_start(_, Max, _Options) when Max < 4 -> {"...", 3};
alist_start(_, _Max, #print_options{depth=0}) -> {"[...]", 5};
alist_start(L, Max, #print_options{force_strings=true} = Options) ->
alist(L, Max, Options);
%alist_start([H|_T], _Max, #print_options{depth=1}) when is_integer(H) -> {[$[, H, $|, $., $., $., $]], 7};
alist_start([H|T], Max, Options) when is_integer(H), H >= 16#20, H =< 16#7e -> % definitely printable
try alist([H|T], Max -1, Options) of
{L, Len} ->
{[$"|L], Len + 1}
catch
throw:unprintable ->
throw:{unprintable, _} ->
{R, Len} = list_body([H|T], Max-2, Options, false),
{[$[, R, $]], Len + 2}
end;
alist_start([H|T], Max, Options) when is_integer(H), H >= 16#a0, H =< 16#ff -> % definitely printable
try alist([H|T], Max -1, Options) of
{L, Len} ->
{[$"|L], Len + 1}
catch
throw:{unprintable, _} ->
{R, Len} = list_body([H|T], Max-2, Options, false),
{[$[, R, $]], Len + 2}
end;
Expand All @@ -287,7 +356,7 @@ alist_start([H|T], Max, Options) when H =:= $\t; H =:= $\n; H =:= $\r; H =:= $\v
{L, Len} ->
{[$"|L], Len + 1}
catch
throw:unprintable ->
throw:{unprintable, _} ->
{R, Len} = list_body([H|T], Max-2, Options, false),
{[$[, R, $]], Len + 2}
end;
Expand All @@ -306,6 +375,9 @@ alist([H|T], Max, Options = #print_options{force_strings=false,lists_as_strings=
alist([H|T], Max, Options) when is_integer(H), H >= 16#20, H =< 16#7e -> % definitely printable
{L, Len} = alist(T, Max-1, Options),
{[H|L], Len + 1};
alist([H|T], Max, Options) when is_integer(H), H >= 16#a0, H =< 16#ff -> % definitely printable
{L, Len} = alist(T, Max-1, Options),
{[H|L], Len + 1};
alist([H|T], Max, Options) when H =:= $\t; H =:= $\n; H =:= $\r; H =:= $\v; H =:= $\e; H=:= $\f; H=:= $\b ->
{L, Len} = alist(T, Max-1, Options),
case Options#print_options.force_strings of
Expand All @@ -319,8 +391,8 @@ alist([H|T], Max, #print_options{force_strings=true} = Options) when is_integer(
{[H|L], Len + 1};
alist(_, _, #print_options{force_strings=true}) ->
erlang:error(badarg);
alist(_L, _Max, _Options) ->
throw(unprintable).
alist([H|_L], _Max, _Options) ->
throw({unprintable, H}).

%% is the first character in the atom alphabetic & lowercase?
atom_needs_quoting_start([H|T]) when H >= $a, H =< $z ->
Expand Down Expand Up @@ -473,6 +545,7 @@ quote_strip_test() ->

binary_printing_test() ->
?assertEqual("<<>>", lists:flatten(format("~p", [<<>>], 50))),
?assertEqual("", lists:flatten(format("~s", [<<>>], 50))),
?assertEqual("<<..>>", lists:flatten(format("~p", [<<"hi">>], 0))),
?assertEqual("<<...>>", lists:flatten(format("~p", [<<"hi">>], 1))),
?assertEqual("<<\"hello\">>", lists:flatten(format("~p", [<<$h, $e, $l, $l, $o>>], 50))),
Expand Down Expand Up @@ -512,6 +585,7 @@ bitstring_printing_test() ->
?assertEqual("<<...>>", lists:flatten(format("~p", [<<1:7>>], 1))),
?assertEqual("[<<1>>,<<2>>]", lists:flatten(format("~p", [[<<1>>, <<2>>]],
100))),
?assertEqual("{<<1:7>>}", lists:flatten(format("~p", [{<<1:7>>}], 50))),
ok.

list_printing_test() ->
Expand Down Expand Up @@ -545,6 +619,9 @@ list_printing_test() ->
lists:flatten(format("~p", [
[22835963083295358096932575511191922182123945984,
22835963083295358096932575511191922182123945984]], 53))),
%%improper list
?assertEqual("[1,2,3|4]", lists:flatten(format("~P", [[1|[2|[3|4]]], 5], 50))),
?assertEqual("[1|1]", lists:flatten(format("~P", [[1|1], 5], 50))),
ok.

tuple_printing_test() ->
Expand Down Expand Up @@ -600,6 +677,44 @@ depth_limit_test() ->
?assertEqual("{\"a\",[...]}", lists:flatten(format("~P", [{"a", ["b", ["c", ["d"]]]}, 3], 50))),
?assertEqual("{\"a\",[\"b\",[[...]|...]]}", lists:flatten(format("~P", [{"a", ["b", ["c", ["d"]]]}, 6], 50))),
?assertEqual("{\"a\",[\"b\",[\"c\",[\"d\"]]]}", lists:flatten(format("~P", [{"a", ["b", ["c", ["d"]]]}, 9], 50))),

?assertEqual("[...]", lists:flatten(format("~P", [[1, 2, 3], 1], 50))),
?assertEqual("[1|...]", lists:flatten(format("~P", [[1, 2, 3], 2], 50))),
?assertEqual("[1,2|...]", lists:flatten(format("~P", [[1, 2, 3], 3], 50))),
?assertEqual("[1,2,3]", lists:flatten(format("~P", [[1, 2, 3], 4], 50))),

?assertEqual("{1,...}", lists:flatten(format("~P", [{1, 2, 3}, 2], 50))),
?assertEqual("{1,2,...}", lists:flatten(format("~P", [{1, 2, 3}, 3], 50))),
?assertEqual("{1,2,3}", lists:flatten(format("~P", [{1, 2, 3}, 4], 50))),

?assertEqual("{1,...}", lists:flatten(format("~P", [{1, 2, 3}, 2], 50))),
?assertEqual("[1,2|...]", lists:flatten(format("~P", [[1, 2, <<3>>], 3], 50))),
?assertEqual("[1,2,<<...>>]", lists:flatten(format("~P", [[1, 2, <<3>>], 4], 50))),
?assertEqual("[1,2,<<3>>]", lists:flatten(format("~P", [[1, 2, <<3>>], 5], 50))),

?assertEqual("<<...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 1], 50))),
?assertEqual("<<0,...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 2], 50))),
?assertEqual("<<0,0,...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 3], 50))),
?assertEqual("<<0,0,0,...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 4], 50))),
?assertEqual("<<0,0,0,0>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 5], 50))),

%% this is a seriously weird edge case
?assertEqual("<<\" \"...>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 2], 50))),
?assertEqual("<<\" \"...>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 3], 50))),
?assertEqual("<<\" \"...>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 4], 50))),
?assertEqual("<<32,32,32,0>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 5], 50))),
?assertEqual("<<32,32,32,0>>", lists:flatten(format("~p", [<<32, 32, 32, 0>>], 50))),

%% depth limiting for some reason works in 4 byte chunks on printable binaries?
?assertEqual("<<\"hell\"...>>", lists:flatten(format("~P", [<<"hello world">>, 2], 50))),
?assertEqual("<<\"abcd\"...>>", lists:flatten(format("~P", [<<$a, $b, $c, $d, $e, 0>>, 2], 50))),

%% I don't even know...
?assertEqual("<<>>", lists:flatten(format("~P", [<<>>, 1], 50))),
?assertEqual("<<...>>", lists:flatten(format("~W", [<<>>, 1], 50))),

?assertEqual("{abc,<<\"abc\\\"\">>}", lists:flatten(format("~P", [{abc,<<"abc\"">>}, 4], 50))),

ok.

-endif.
45 changes: 38 additions & 7 deletions test/trunc_io_eqc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

-ifdef(TEST).
-ifdef(EQC).
-export([test/0, test/1, check/0, prop_format/0]).
-export([test/0, test/1, check/0, prop_format/0, prop_equivalence/0]).

-include_lib("eqc/include/eqc.hrl").
-include_lib("eunit/include/eunit.hrl").
Expand All @@ -36,9 +36,12 @@
%%====================================================================

eqc_test_() ->
{timeout, 300,
{timeout, 30,
{spawn,
[?_assertEqual(true, quickcheck(numtests(500, ?QC_OUT(prop_format()))))]
[
{timeout, 15, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(14, ?QC_OUT(prop_format()))))},
{timeout, 15, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(14, ?QC_OUT(prop_equivalence()))))}
]
}}.

%%====================================================================
Expand All @@ -61,10 +64,10 @@ check() ->
gen_fmt_args() ->
list(oneof([gen_print_str(),
"~~",
{"~p", gen_any(5)},
{"~10000000.p", gen_any(5)},
{"~w", gen_any(5)},
{"~s", gen_print_str()},
{"~P", gen_any(5), 4},
{"~s", oneof([gen_print_str(), gen_atom(), gen_quoted_atom(), gen_print_bin()])},
{"~1000000.P", gen_any(5), 4},
{"~W", gen_any(5), 4},
{"~i", gen_any(5)},
{"~B", nat()},
Expand All @@ -74,7 +77,7 @@ gen_fmt_args() ->
{"~.10#", nat()},
{"~.10+", nat()},
{"~.36B", nat()},
{"~62P", gen_any(5), 4},
{"~1000000.62P", gen_any(5), 4},
{"~c", gen_char()},
{"~tc", gen_char()},
{"~f", real()},
Expand All @@ -90,12 +93,17 @@ gen_fmt_args() ->
gen_print_str() ->
?LET(Xs, list(char()), [X || X <- Xs, io_lib:printable_list([X]), X /= $~]).

gen_print_bin() ->
?LET(Xs, gen_print_str(), list_to_binary(Xs)).

gen_any(MaxDepth) ->
oneof([largeint(),
gen_atom(),
gen_quoted_atom(),
nat(),
%real(),
binary(),
gen_bitstring(),
gen_pid(),
gen_port(),
gen_ref(),
Expand All @@ -106,6 +114,12 @@ gen_any(MaxDepth) ->
gen_atom() ->
elements([abc, def, ghi]).

gen_quoted_atom() ->
elements(['abc@bar', '@bar', '10gen']).

gen_bitstring() ->
?LET(XS, binary(), <<XS/binary, 1:7>>).

gen_tuple(Gen) ->
?LET(Xs, list(Gen), list_to_tuple(Xs)).

Expand Down Expand Up @@ -174,6 +188,23 @@ prop_format() ->
end
end).

%% Checks for equivalent formatting to io_lib
prop_equivalence() ->
?FORALL(FmtArgs, gen_fmt_args(),
begin
{FmtStr, Args} = build_fmt_args(FmtArgs),
Expected = lists:flatten(io_lib:format(FmtStr, Args)),
Actual = lists:flatten(lager_trunc_io:format(FmtStr, Args, 10485760)),
?WHENFAIL(begin
io:format(user, "FmtStr: ~p\n", [FmtStr]),
io:format(user, "Args: ~p\n", [Args]),
io:format(user, "Expected: ~p\n", [Expected]),
io:format(user, "Actual: ~p\n", [Actual])
end,
Expected == Actual)
end).


%%====================================================================
%% Internal helpers
%%====================================================================
Expand Down

0 comments on commit a582eaf

Please sign in to comment.