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