blob: 532f047b5153fb9d3e6fbfae46b718fd8a5fe59c [file] [log] [blame]
%% -------------------------------------------------------------------
%%
%% trunc_io_eqc: QuickCheck test for trunc_io:format with maxlen
%%
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
-module(trunc_io_eqc).
-ifdef(TEST).
-ifdef(EQC).
-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").
-define(QC_OUT(P),
eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)).
%%====================================================================
%% eunit test
%%====================================================================
eqc_test_() ->
{timeout, 60,
{spawn,
[
{timeout, 30, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(14, ?QC_OUT(prop_format()))))},
{timeout, 30, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(14, ?QC_OUT(prop_equivalence()))))}
]
}}.
%%====================================================================
%% Shell helpers
%%====================================================================
test() ->
test(100).
test(N) ->
quickcheck(numtests(N, prop_format())).
check() ->
check(prop_format(), current_counterexample()).
%%====================================================================
%% Generators
%%====================================================================
gen_fmt_args() ->
list(oneof([gen_print_str(),
"~~",
{"~10000000.p", gen_any(5)},
{"~w", gen_any(5)},
{"~s", oneof([gen_print_str(), gen_atom(), gen_quoted_atom(), gen_print_bin(), gen_iolist(5)])},
{"~1000000.P", gen_any(5), 4},
{"~W", gen_any(5), 4},
{"~i", gen_any(5)},
{"~B", nat()},
{"~b", nat()},
{"~X", nat(), "0x"},
{"~x", nat(), "0x"},
{"~.10#", nat()},
{"~.10+", nat()},
{"~.36B", nat()},
{"~1000000.62P", gen_any(5), 4},
{"~c", gen_char()},
{"~tc", gen_char()},
{"~f", real()},
{"~10.f", real()},
{"~g", real()},
{"~10.g", real()},
{"~e", real()},
{"~10.e", real()}
])).
%% Generates a printable string
gen_print_str() ->
?LET(Xs, list(char()), [X || X <- Xs, io_lib:printable_list([X]), X /= $~, X < 255]).
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(),
gen_fun()] ++
[?LAZY(list(gen_any(MaxDepth - 1))) || MaxDepth /= 0] ++
[?LAZY(gen_tuple(gen_any(MaxDepth - 1))) || MaxDepth /= 0]).
gen_iolist(0) ->
[];
gen_iolist(Depth) ->
list(oneof([gen_char(), gen_print_str(), gen_print_bin(), gen_iolist(Depth-1)])).
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)).
gen_max_len() -> %% Generate length from 3 to whatever. Needs space for ... in output
?LET(Xs, int(), 3 + abs(Xs)).
gen_pid() ->
?LAZY(spawn(fun() -> ok end)).
gen_port() ->
?LAZY(begin
Port = erlang:open_port({spawn, "true"}, []),
erlang:port_close(Port),
Port
end).
gen_ref() ->
?LAZY(make_ref()).
gen_fun() ->
?LAZY(fun() -> ok end).
gen_char() ->
oneof(lists:seq($A, $z)).
%%====================================================================
%% Property
%%====================================================================
%% Checks that trunc_io:format produces output less than or equal to MaxLen
prop_format() ->
?FORALL({FmtArgs, MaxLen}, {gen_fmt_args(), gen_max_len()},
begin
%% Because trunc_io will print '...' when its running out of
%% space, even if the remaining space is less than 3, it
%% doesn't *exactly* stick to the specified limit.
%% Also, since we don't truncate terms not printed with
%% ~p/~P/~w/~W/~s, we also need to calculate the wiggle room
%% for those. Hence the fudge factor calculated below.
FudgeLen = calculate_fudge(FmtArgs, 50),
{FmtStr, Args} = build_fmt_args(FmtArgs),
try
Str = lists:flatten(lager_trunc_io:format(FmtStr, Args, MaxLen)),
?WHENFAIL(begin
io:format(user, "FmtStr: ~p\n", [FmtStr]),
io:format(user, "Args: ~p\n", [Args]),
io:format(user, "FudgeLen: ~p\n", [FudgeLen]),
io:format(user, "MaxLen: ~p\n", [MaxLen]),
io:format(user, "ActLen: ~p\n", [length(Str)]),
io:format(user, "Str: ~p\n", [Str])
end,
%% Make sure the result is a printable list
%% and if the format string is less than the length,
%% the result string is less than the length.
conjunction([{printable, Str == "" orelse
io_lib:printable_list(Str)},
{length, length(FmtStr) > MaxLen orelse
length(Str) =< MaxLen + FudgeLen}]))
catch
_:Err ->
io:format(user, "\nException: ~p\n", [Err]),
io:format(user, "FmtStr: ~p\n", [FmtStr]),
io:format(user, "Args: ~p\n", [Args]),
false
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
%%====================================================================
%% Build a tuple of {Fmt, Args} from a gen_fmt_args() return
build_fmt_args(FmtArgs) ->
F = fun({Fmt, Arg}, {FmtStr0, Args0}) ->
{FmtStr0 ++ Fmt, Args0 ++ [Arg]};
({Fmt, Arg1, Arg2}, {FmtStr0, Args0}) ->
{FmtStr0 ++ Fmt, Args0 ++ [Arg1, Arg2]};
(Str, {FmtStr0, Args0}) ->
{FmtStr0 ++ Str, Args0}
end,
lists:foldl(F, {"", []}, FmtArgs).
calculate_fudge([], Acc) ->
Acc;
calculate_fudge([{"~62P", _Arg, _Depth}|T], Acc) ->
calculate_fudge(T, Acc+62);
calculate_fudge([{Fmt, Arg}|T], Acc) when
Fmt == "~f"; Fmt == "~10.f";
Fmt == "~g"; Fmt == "~10.g";
Fmt == "~e"; Fmt == "~10.e";
Fmt == "~x"; Fmt == "~X";
Fmt == "~B"; Fmt == "~b"; Fmt == "~36B";
Fmt == "~.10#"; Fmt == "~10+" ->
calculate_fudge(T, Acc + length(lists:flatten(io_lib:format(Fmt, [Arg]))));
calculate_fudge([_|T], Acc) ->
calculate_fudge(T, Acc).
-endif. % (EQC).
-endif. % (TEST).