1%% -------------------------------------------------------------------
2%%
3%% trunc_io_eqc: QuickCheck test for trunc_io:format with maxlen
4%%
5%% Copyright (c) 2011-2012 Basho Technologies, Inc.  All Rights Reserved.
6%%
7%% This file is provided to you under the Apache License,
8%% Version 2.0 (the "License"); you may not use this file
9%% except in compliance with the License.  You may obtain
10%% a copy of the License at
11%%
12%%   http://www.apache.org/licenses/LICENSE-2.0
13%%
14%% Unless required by applicable law or agreed to in writing,
15%% software distributed under the License is distributed on an
16%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17%% KIND, either express or implied.  See the License for the
18%% specific language governing permissions and limitations
19%% under the License.
20%%
21%% -------------------------------------------------------------------
22-module(trunc_io_eqc).
23
24-ifdef(TEST).
25-ifdef(EQC).
26-export([test/0, test/1, check/0, prop_format/0, prop_equivalence/0]).
27
28-include_lib("eqc/include/eqc.hrl").
29-include_lib("eunit/include/eunit.hrl").
30
31-define(QC_OUT(P),
32        eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)).
33
34%%====================================================================
35%% eunit test
36%%====================================================================
37
38eqc_test_() ->
39    {timeout, 60,
40     {spawn,
41      [
42                {timeout, 30, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(14, ?QC_OUT(prop_format()))))},
43                {timeout, 30, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(14, ?QC_OUT(prop_equivalence()))))}
44                ]
45     }}.
46
47%%====================================================================
48%% Shell helpers
49%%====================================================================
50
51test() ->
52    test(100).
53
54test(N) ->
55    quickcheck(numtests(N, prop_format())).
56
57check() ->
58    check(prop_format(), current_counterexample()).
59
60%%====================================================================
61%% Generators
62%%====================================================================
63
64gen_fmt_args() ->
65    list(oneof([gen_print_str(),
66                "~~",
67                {"~10000000.p", gen_any(5)},
68                {"~w", gen_any(5)},
69                {"~s", oneof([gen_print_str(), gen_atom(), gen_quoted_atom(), gen_print_bin(), gen_iolist(5)])},
70                {"~1000000.P", gen_any(5), 4},
71                {"~W", gen_any(5), 4},
72                {"~i", gen_any(5)},
73                {"~B", nat()},
74                {"~b", nat()},
75                {"~X", nat(), "0x"},
76                {"~x", nat(), "0x"},
77                {"~.10#", nat()},
78                {"~.10+", nat()},
79                {"~.36B", nat()},
80                {"~1000000.62P", gen_any(5), 4},
81                {"~c", gen_char()},
82                {"~tc", gen_char()},
83                {"~f", real()},
84                {"~10.f", real()},
85                {"~g", real()},
86                {"~10.g", real()},
87                {"~e", real()},
88                {"~10.e", real()}
89               ])).
90
91
92%% Generates a printable string
93gen_print_str() ->
94    ?LET(Xs, list(char()), [X || X <- Xs, io_lib:printable_list([X]), X /= $~, X < 256]).
95
96gen_print_bin() ->
97    ?LET(Xs, gen_print_str(), list_to_binary(Xs)).
98
99gen_any(MaxDepth) ->
100    oneof([largeint(),
101           gen_atom(),
102           gen_quoted_atom(),
103           nat(),
104           %real(),
105           binary(),
106           gen_bitstring(),
107           gen_pid(),
108           gen_port(),
109           gen_ref(),
110           gen_fun()] ++
111           [?LAZY(list(gen_any(MaxDepth - 1))) || MaxDepth /= 0] ++
112           [?LAZY(gen_tuple(gen_any(MaxDepth - 1))) || MaxDepth /= 0]).
113
114gen_iolist(0) ->
115    [];
116gen_iolist(Depth) ->
117    list(oneof([gen_char(), gen_print_str(), gen_print_bin(), gen_iolist(Depth-1)])).
118
119gen_atom() ->
120    elements([abc, def, ghi]).
121
122gen_quoted_atom() ->
123    elements(['abc@bar', '@bar', '10gen']).
124
125gen_bitstring() ->
126    ?LET(XS, binary(), <<XS/binary, 1:7>>).
127
128gen_tuple(Gen) ->
129    ?LET(Xs, list(Gen), list_to_tuple(Xs)).
130
131gen_max_len() -> %% Generate length from 3 to whatever.  Needs space for ... in output
132    ?LET(Xs, int(), 3 + abs(Xs)).
133
134gen_pid() ->
135    ?LAZY(spawn(fun() -> ok end)).
136
137gen_port() ->
138    ?LAZY(begin
139              Port = erlang:open_port({spawn, "true"}, []),
140              catch(erlang:port_close(Port)),
141              Port
142          end).
143
144gen_ref() ->
145    ?LAZY(make_ref()).
146
147gen_fun() ->
148    ?LAZY(fun() -> ok end).
149
150gen_char() ->
151    oneof(lists:seq($A, $z)).
152
153%%====================================================================
154%% Property
155%%====================================================================
156
157%% Checks that trunc_io:format produces output less than or equal to MaxLen
158prop_format() ->
159    ?FORALL({FmtArgs, MaxLen}, {gen_fmt_args(), gen_max_len()},
160            begin
161                %% Because trunc_io will print '...' when its running out of
162                %% space, even if the remaining space is less than 3, it
163                %% doesn't *exactly* stick to the specified limit.
164
165                %% Also, since we don't truncate terms not printed with
166                %% ~p/~P/~w/~W/~s, we also need to calculate the wiggle room
167                %% for those. Hence the fudge factor calculated below.
168                FudgeLen = calculate_fudge(FmtArgs, 50),
169                {FmtStr, Args} = build_fmt_args(FmtArgs),
170                try
171                    Str = lists:flatten(lager_trunc_io:format(FmtStr, Args, MaxLen)),
172                    ?WHENFAIL(begin
173                                  io:format(user, "FmtStr:   ~p\n", [FmtStr]),
174                                  io:format(user, "Args:     ~p\n", [Args]),
175                                  io:format(user, "FudgeLen: ~p\n", [FudgeLen]),
176                                  io:format(user, "MaxLen:   ~p\n", [MaxLen]),
177                                  io:format(user, "ActLen:   ~p\n", [length(Str)]),
178                                  io:format(user, "Str:      ~p\n", [Str])
179                              end,
180                              %% Make sure the result is a printable list
181                              %% and if the format string is less than the length,
182                              %% the result string is less than the length.
183                              conjunction([{printable, Str == "" orelse
184                                                       io_lib:printable_list(Str)},
185                                           {length, length(FmtStr) > MaxLen orelse
186                                                    length(Str) =< MaxLen + FudgeLen}]))
187                catch
188                    _:Err ->
189                        io:format(user, "\nException: ~p\n", [Err]),
190                        io:format(user, "FmtStr: ~p\n", [FmtStr]),
191                        io:format(user, "Args:   ~p\n", [Args]),
192                        false
193                end
194            end).
195
196%% Checks for equivalent formatting to io_lib
197prop_equivalence() ->
198    ?FORALL(FmtArgs, gen_fmt_args(),
199            begin
200            {FmtStr, Args} = build_fmt_args(FmtArgs),
201            Expected = lists:flatten(io_lib:format(FmtStr, Args)),
202            Actual = lists:flatten(lager_trunc_io:format(FmtStr, Args, 10485760)),
203            ?WHENFAIL(begin
204                io:format(user, "FmtStr:   ~p\n", [FmtStr]),
205                io:format(user, "Args:     ~p\n", [Args]),
206                io:format(user, "Expected: ~p\n", [Expected]),
207                io:format(user, "Actual:   ~p\n", [Actual])
208            end,
209                      Expected == Actual)
210        end).
211
212
213%%====================================================================
214%% Internal helpers
215%%====================================================================
216
217%% Build a tuple of {Fmt, Args} from a gen_fmt_args() return
218build_fmt_args(FmtArgs) ->
219    F = fun({Fmt, Arg}, {FmtStr0, Args0}) ->
220                {FmtStr0 ++ Fmt, Args0 ++ [Arg]};
221           ({Fmt, Arg1, Arg2}, {FmtStr0, Args0}) ->
222                {FmtStr0 ++ Fmt, Args0 ++ [Arg1, Arg2]};
223           (Str, {FmtStr0, Args0}) ->
224                {FmtStr0 ++ Str, Args0}
225        end,
226    lists:foldl(F, {"", []}, FmtArgs).
227
228calculate_fudge([], Acc) ->
229    Acc;
230calculate_fudge([{"~62P", _Arg, _Depth}|T], Acc) ->
231    calculate_fudge(T, Acc+62);
232calculate_fudge([{Fmt, Arg}|T], Acc) when
233        Fmt == "~f"; Fmt == "~10.f";
234        Fmt == "~g"; Fmt == "~10.g";
235        Fmt == "~e"; Fmt == "~10.e";
236        Fmt == "~x"; Fmt == "~X";
237        Fmt == "~B"; Fmt == "~b"; Fmt == "~36B";
238        Fmt == "~.10#"; Fmt == "~10+" ->
239    calculate_fudge(T, Acc + length(lists:flatten(io_lib:format(Fmt, [Arg]))));
240calculate_fudge([_|T], Acc) ->
241    calculate_fudge(T, Acc).
242
243-endif. % (EQC).
244-endif. % (TEST).
245