├── .gitignore ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── eqc.ex ├── eqc │ ├── cluster.ex │ ├── component.ex │ ├── component │ │ └── callouts.ex │ ├── exunit.ex │ ├── mocking.ex │ ├── pulse.ex │ └── statem.ex └── mix │ └── tasks │ ├── eqc.ex │ ├── eqc_install.ex │ ├── eqc_registration.ex │ ├── eqc_template.ex │ ├── eqc_uninstall.ex │ └── mix_eqcci.ex ├── mix.exs ├── rebar.config ├── src └── eqc_ex.app.src └── test ├── comp_eqc.exs ├── option_parser_eqc.exs ├── props_eqc.exs ├── string_eqc.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | mix.lock 3 | _build 4 | doc 5 | deps 6 | erl_crash.dump 7 | *.ez 8 | .compile.* 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015, Quviq AB 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | EQC for Elixir 2 | ============== 3 | 4 | This package defines wrappers for using Quviq QuickCheck with Elixir. See 5 | [quviq.com](http://www.quviq.com) for information on QuickCheck. 6 | 7 | QuickCheck Mini is Quviq's free version of QuickCheck 8 | mix eqc.install --mini 9 | 10 | 11 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /lib/eqc.ex: -------------------------------------------------------------------------------- 1 | defmodule EQC do 2 | @copyright "Quviq AB, 2014-2016" 3 | 4 | @moduledoc """ 5 | This module contains macros to be used with [Quviq 6 | QuickCheck](http://www.quviq.com). It defines Elixir versions of the Erlang 7 | macros found in `eqc/include/eqc.hrl`. For detailed documentation of the 8 | macros, please refer to the QuickCheck documentation. 9 | 10 | `Copyright (C) Quviq AB, 2014-2016.` 11 | """ 12 | 13 | defmacro __using__(_opts) do 14 | quote do 15 | import EQC 16 | import :eqc_gen, except: [lazy: 1] 17 | 18 | end 19 | end 20 | 21 | defp syntax_error(err) do 22 | raise ArgumentError, message: "Usage: " <> err 23 | end 24 | 25 | defp eqc_forall(x, g, prop) do 26 | quote(do: :eqc.forall(unquote(g), fn unquote(x) -> unquote(prop) end)) 27 | end 28 | 29 | defp eqc_bind(x, g, body) do 30 | quote(do: :eqc_gen.bind(unquote(g), fn unquote(x) -> unquote(body) end)) 31 | end 32 | 33 | @doc """ 34 | A property that should hold for all values generated by a generator. 35 | 36 | Usage: 37 | 38 | forall pat <- gen do 39 | prop 40 | end 41 | 42 | The variables of `pat` are bound in `prop`. 43 | 44 | In Erlang: `?FORALL(Pat, Gen, Prop)`. 45 | """ 46 | defmacro forall({:<-, _, [x, g]}, do: prop) when prop != nil, do: eqc_forall(x, g, prop) 47 | defmacro forall(bind, prop) do 48 | _ = {bind, prop} 49 | syntax_error "forall PAT <- GEN, do: PROP" 50 | end 51 | 52 | @doc """ 53 | Bind a generated value for use by another generator. 54 | 55 | Usage: 56 | 57 | let pat <- gen1 do 58 | gen_a 59 | end 60 | 61 | let [pat1 <- gen1, pat2 <- gen2] do 62 | gen_b 63 | end 64 | 65 | In the first example, the variables of `pat` are bound in `gen_a`. 66 | In the second example, the variables of `pat1` scope over both `gen2` and `gen_b`. 67 | 68 | In Erlang: `?LET(Pat, Gen1, Gen2)`. 69 | """ 70 | defmacro let(bindings, do: body) when body != nil, do: do_let(bindings, body) 71 | 72 | defp do_let({:<-, _, [_, _]}=binding, body), do: do_let([binding], body) 73 | defp do_let([{:<-, _, [x, g]}|rest], body), do: eqc_bind(x, g, do_let(rest, body)) 74 | defp do_let([], body), do: body 75 | defp do_let(_, _) do 76 | syntax_error "let PAT <- GEN, do: GEN or let [PAT1 <- GEN1, PAT2 <- GEN2, ...], do: GEN" 77 | end 78 | 79 | @doc """ 80 | Generate a value that satisfies a given predicate. 81 | 82 | Throws an exception if no value is found after 100 attempts. 83 | Usage: 84 | 85 | such_that pat <- gen, do: pred 86 | 87 | The variables of `pat` are bound in `pred`. 88 | 89 | In Erlang: `?SUCHTHAT(Pat, Gen, Pred)`. 90 | """ 91 | defmacro such_that({:<-, _, [x, g]}, do: pred) when pred != nil do 92 | loc = {__CALLER__.file, __CALLER__.line} 93 | quote do 94 | :eqc_gen.suchthat(unquote(g), fn unquote(x) -> unquote(pred) end, unquote(loc)) 95 | end 96 | end 97 | defmacro such_that(bind, pred) do 98 | _ = {bind, pred} 99 | syntax_error "such_that PAT <- GEN, do: PRED" 100 | end 101 | 102 | @doc """ 103 | Generate a value that satisfies a given predicate, or `false` if no value is found. 104 | 105 | Usage: 106 | 107 | such_that_maybe pat <- gen, do: pred 108 | 109 | The variables of `pat` are bound in `pred`. 110 | 111 | In Erlang: `?SUCHTHATMAYBE(Pat, Gen, Pred)`. 112 | """ 113 | defmacro such_that_maybe({:<-, _, [x, g]}, do: pred) when pred != nil do 114 | quote do 115 | :eqc_gen.suchthatmaybe(unquote(g), fn unquote(x) -> unquote(pred) end) 116 | end 117 | end 118 | defmacro such_that_maybe(bind, pred) do 119 | _ = {bind, pred} 120 | syntax_error "such_that_maybe PAT <- GEN, do: PRED" 121 | end 122 | 123 | @doc """ 124 | Bind the current size parameter. 125 | 126 | Usage: 127 | 128 | sized n, do: prop 129 | 130 | In `prop`, `n` is bound to the current size. 131 | 132 | In Erlang: `?SIZED(N, Prop)` 133 | """ 134 | defmacro sized(n, do: prop) when prop != nil do 135 | quote(do: :eqc_gen.sized(fn unquote(n) -> unquote(prop) end)) 136 | end 137 | defmacro sized(_, prop) do 138 | _ = prop 139 | syntax_error "sized N, do: PROP" 140 | end 141 | 142 | @doc """ 143 | Add shrinking behaviour to a generator. 144 | 145 | Usage: 146 | 147 | shrink g, gs 148 | 149 | Generates a value from `g` that can shrink to a value generated by any of the 150 | generators in `gs`. 151 | 152 | In Erlang: `?SHRINK(G, Gs)`. 153 | """ 154 | defmacro shrink(g, gs) do 155 | quote(do: :eqc_gen.shrinkwith(unquote(g), fn -> unquote(gs) end)) 156 | end 157 | 158 | @doc """ 159 | Like `let/2` but adds shrinking behaviour. 160 | 161 | Usage: 162 | 163 | let_shrink pat <- gen1 do 164 | gen2 165 | end 166 | 167 | Here `gen1` must generate a list of values, each of which is added as a 168 | possible shrinking of the result. 169 | 170 | In Erlang: `?LETSHRINK(Pat, Gen1, Gen2)`. 171 | """ 172 | defmacro let_shrink({:<-, _, [es, gs]}, do: g) when g != nil do 173 | quote(do: :eqc_gen.letshrink(unquote(gs), fn unquote(es) -> unquote(g) end)) 174 | end 175 | defmacro let_shrink(bind, gen) do 176 | _ = {bind, gen} 177 | syntax_error "let_shrink PAT <- GEN, do: GEN" 178 | end 179 | 180 | @doc """ 181 | Perform an action when a test fails. 182 | 183 | Usage: 184 | 185 | when_fail(action) do 186 | prop 187 | end 188 | 189 | Typically the action will be printing some diagnostic information. 190 | 191 | In Erlang: `?WHENFAIL(Action, Prop)`. 192 | """ 193 | defmacro when_fail(action, do: prop) when prop != nil do 194 | quote do 195 | :eqc.whenfail(fn eqcResult -> 196 | :erlang.put :eqc_result, eqcResult 197 | unquote(action) 198 | end, EQC.lazy(do: unquote(prop))) 199 | end 200 | end 201 | defmacro when_fail(_, prop) do 202 | _ = prop 203 | syntax_error "when_fail ACTION, do: PROP" 204 | end 205 | 206 | @doc """ 207 | Make a generator lazy. 208 | 209 | Usage: 210 | 211 | lazy do: gen 212 | 213 | The generator is not evaluated until a value is generated from it. Crucial when 214 | building recursive generators. 215 | 216 | In Erlang: `?LAZY(Gen)`. 217 | """ 218 | defmacro lazy(do: g) when g != nil do 219 | quote(do: :eqc_gen.lazy(fn -> unquote(g) end)) 220 | end 221 | defmacro lazy(gen) do 222 | _ = gen 223 | syntax_error "lazy do: GEN" 224 | end 225 | 226 | @doc """ 227 | Add a precondition to a property. 228 | 229 | Usage: 230 | 231 | implies pre do 232 | prop 233 | end 234 | 235 | Any test case not satisfying the precondition will be discarded. 236 | 237 | In Erlang: `?IMPLIES(Pre, Prop)`. 238 | """ 239 | defmacro implies(pre, do: prop) when prop != nil do 240 | quote(do: :eqc.implies(unquote(pre), unquote(to_char_list(Macro.to_string(pre))), fn -> unquote(prop) end)) 241 | end 242 | defmacro implies(pre, prop) do 243 | _ = {pre, prop} 244 | syntax_error "implies COND, do: PROP" 245 | end 246 | 247 | @doc """ 248 | Run a property in a separate process and trap exits. 249 | 250 | Usage: 251 | 252 | trap_exit do 253 | prop 254 | end 255 | 256 | Prevents a property from crashing if a linked process exits. 257 | 258 | In Erlang: `?TRAPEXIT(Prop)`. 259 | """ 260 | defmacro trap_exit(do: prop) when prop != nil, do: quote(do: :eqc.trapexit(fn -> unquote(prop) end)) 261 | defmacro trap_exit(prop) do 262 | _ = prop 263 | syntax_error "trap_exit do: PROP" 264 | end 265 | 266 | @doc """ 267 | Set a time limit on a property. 268 | 269 | Usage: 270 | 271 | timeout limit do 272 | prop 273 | end 274 | 275 | Causes the property to fail if it doesn't complete within the time limit. 276 | 277 | In Erlang: `?TIMEOUT(Limit, Prop)`. 278 | """ 279 | defmacro timeout(limit, do: prop) when prop != nil do 280 | quote(do: :eqc.timeout_property(unquote(limit), EQC.lazy(do: unquote(prop)))) 281 | end 282 | defmacro timeout(_, prop) do 283 | _ = prop 284 | syntax_error "timeout TIME, do: PROP" 285 | end 286 | 287 | @doc """ 288 | Repeat a property several times. 289 | 290 | Usage: 291 | 292 | always n do 293 | prop 294 | end 295 | 296 | The property succeeds if all `n` tests of `prop` succeed. 297 | 298 | In Erlang: `?ALWAYS(N, Prop)`. 299 | """ 300 | defmacro always(n, do: prop) when prop != nil do 301 | quote(do: :eqc.always(unquote(n), fn -> unquote(prop) end)) 302 | end 303 | defmacro always(_, prop) do 304 | _ = prop 305 | syntax_error "always N, do: PROP" 306 | end 307 | 308 | @doc """ 309 | Repeat a property several times, failing only if the property fails every time. 310 | 311 | Usage: 312 | 313 | sometimes n do 314 | prop 315 | end 316 | 317 | The property succeeds if any of the `n` tests of `prop` succeed. 318 | 319 | In Erlang: `?SOMETIMES(N, Prop)`. 320 | """ 321 | defmacro sometimes(n, do: prop) when prop != nil do 322 | quote(do: :eqc.sometimes(unquote(n), fn -> unquote(prop) end)) 323 | end 324 | defmacro sometimes(_, prop) do 325 | _ = prop 326 | syntax_error "sometimes N, do: PROP" 327 | end 328 | 329 | @doc """ 330 | Setup and tear-down for a test run. 331 | 332 | Usage: 333 | 334 | setup_teardown(setup) do 335 | prop 336 | after 337 | x -> teardown 338 | end 339 | 340 | Performs `setup` before a test run (default 100 tests) and `teardown` after 341 | the test run. The result of `setup` is bound to `x` in `teardown`, allowing 342 | passing resources allocated in `setup` to `teardown`. The `after` argument is 343 | optional. 344 | 345 | In Erlang: `?SETUP(fun() -> X = Setup, fun() -> Teardown end, Prop)`. 346 | """ 347 | defmacro setup_teardown(setup, do: prop, after: teardown) when prop != nil do 348 | x = Macro.var :x, __MODULE__ 349 | td = cond do 350 | !teardown -> :ok 351 | true -> {:case, [], [x, [do: teardown]]} 352 | end 353 | quote do 354 | {:eqc_setup, fn -> 355 | unquote(x) = unquote(setup) 356 | fn -> unquote(td) end 357 | end, 358 | EQC.lazy(do: unquote(prop))} 359 | end 360 | end 361 | defmacro setup_teardown(_, opts) do 362 | _ = opts 363 | syntax_error "setup_teardown SETUP, do: PROP, after: (X -> TEARDOWN)" 364 | end 365 | 366 | @doc """ 367 | Setup for a test run. 368 | 369 | Usage: 370 | 371 | setup function do 372 | prop 373 | end 374 | 375 | Performs `setup` before a test run (default 100 tests) without `teardown` function 376 | after the test run. 377 | 378 | In Erlang: `?SETUP(fun() -> X = Setup, fun() -> ok end, Prop)`. 379 | """ 380 | defmacro setup(setup, do: prop) when prop != nil do 381 | quote do 382 | {:eqc_setup, fn -> 383 | unquote(setup) 384 | fn -> :ok end 385 | end, 386 | EQC.lazy(do: unquote(prop))} 387 | end 388 | end 389 | defmacro setup(_, opts) do 390 | _ = opts 391 | syntax_error "setup SETUP, do: PROP" 392 | end 393 | 394 | 395 | @doc """ 396 | A property that is only executed once for each test case. 397 | 398 | Usage: 399 | 400 | once_only do 401 | prop 402 | end 403 | 404 | Repeated tests are generated but not run, and shows up as `x`s in the test 405 | output. Useful if running tests is very expensive. 406 | 407 | In Erlang: `?ONCEONLY(Prop)`. 408 | """ 409 | defmacro once_only(do: prop) when prop != nil do 410 | quote(do: :eqc.onceonly(fn -> unquote(prop) end)) 411 | end 412 | defmacro once_only(prop) do 413 | _ = prop 414 | syntax_error "once_only do: PROP" 415 | end 416 | 417 | 418 | @doc """ 419 | A property combinator to obtain test statistics 420 | 421 | Usage: 422 | collect KeywordList do 423 | prop 424 | end 425 | 426 | Example: 427 | forall {m, n} <- {int, int} do 428 | collect m: m, n: n do 429 | length(Enum.to_list(m .. n)) == abs(n - m) + 1 430 | end 431 | end 432 | """ 433 | ## This is the collect ... do PROP end 434 | defmacro collect(xs, do: prop) when prop != nil do 435 | if Keyword.keyword?(xs) do 436 | do_nested(quote do (fn(wt, t, p) -> :eqc.collect(:eqc.with_title(wt), t, p) end) end, Enum.reverse(xs), prop) 437 | else 438 | syntax_error "collect KEYWORDLIST do PROP end" 439 | end 440 | end 441 | defmacro collect(_, prop) do 442 | _ = prop 443 | syntax_error "collect KEYWORDLIST do PROP end" 444 | end 445 | 446 | @doc false 447 | ## This takes care of collect(..., do: PROP) 448 | defmacro collect(xs) when is_list(xs) do 449 | case Enum.reverse(xs) do 450 | [ {:in, prop} | tail] -> 451 | # for backward compatibility 452 | quote do collect(unquote(Enum.reverse(tail)), do: unquote(prop)) end 453 | [ {:do, prop} | tail ] -> 454 | quote do collect(unquote(Enum.reverse(tail)), do: unquote(prop)) end 455 | _ -> 456 | syntax_error "collect(KEYWORDLIST, do: PROP)" 457 | end 458 | end 459 | 460 | 461 | @doc """ 462 | A property combinator to obtain test statistics for numbers 463 | 464 | Usage: 465 | measure KeywordList do 466 | prop 467 | end 468 | 469 | Example: 470 | forall {m, n} <- {int, int} do 471 | measure m: m, n: n do 472 | length(Enum.to_list(m .. n)) == abs(n - m) + 1 473 | end 474 | end 475 | """ 476 | defmacro measure(xs, do: prop) when prop != nil do 477 | if Keyword.keyword?(xs) do 478 | do_nested(quote do (fn(t, v, p) -> :eqc.measure(t, v, p) end) end, Enum.reverse(xs), prop) 479 | else 480 | syntax_error "measure KEYWORDLIST do PROP end" 481 | end 482 | end 483 | defmacro measure(_, prop) do 484 | _ = prop 485 | syntax_error "measure KEYWORDLIST do PROP end" 486 | end 487 | @doc false 488 | 489 | defmacro measure(xs) do 490 | try do 491 | [ {:do, prop} | tail ] = Enum.reverse(xs) 492 | quote do measure(unquote(Enum.reverse(tail)), do: unquote(prop)) end 493 | rescue 494 | _ -> syntax_error "measure KEYWORDLIST do PROP end" 495 | end 496 | end 497 | 498 | @doc """ 499 | A property combinator to obtain test statistics for sequences 500 | 501 | Usage: 502 | aggregate KeywordList do 503 | prop 504 | end 505 | 506 | """ 507 | defmacro aggregate(xs, do: prop) when prop != nil do 508 | if Keyword.keyword?(xs) do 509 | do_nested(quote do (fn(t, v, p) -> :eqc.aggregate(:eqc.with_title(t), v, p) end) end, Enum.reverse(xs), prop) 510 | else 511 | syntax_error "aggregate KEYWORDLIST do PROP end" 512 | end 513 | end 514 | defmacro aggregate(_, prop) do 515 | _ = prop 516 | syntax_error "aggregate KEYWORDLIST do PROP end" 517 | end 518 | 519 | @doc false 520 | defmacro aggregate(xs) when is_list(xs) do 521 | try do 522 | [ {:do, prop} | tail ] = Enum.reverse(xs) 523 | quote do aggregate(unquote(Enum.reverse(tail)), do: unquote(prop)) end 524 | rescue 525 | _ -> syntax_error "aggregate(KEYWORDLIST, do: PROP)" 526 | end 527 | end 528 | 529 | 530 | # defp do_nested(f, [{tag, {:in, _, [count,requirement]}} | t], acc) do 531 | # acc = quote do: unquote(f).( 532 | # fn res -> 533 | # case (unquote(requirement) -- Keyword.keys(res)) do 534 | # [] -> :ok 535 | # uncovered -> 536 | # :eqc.format("Warning: not all features covered! ~p\n",[uncovered]) 537 | # end 538 | # :eqc.with_title(unquote(tag)).(res) 539 | # end, unquote(count), unquote(acc)) 540 | # do_nested(f, t, acc) 541 | # end 542 | defp do_nested(f, [{tag, term} | t], acc) do 543 | ( acc = quote do: unquote(f).(unquote(tag), unquote(term), unquote(acc)) 544 | do_nested(f, t, acc) ) 545 | end 546 | defp do_nested(_, [], acc) do acc 547 | end 548 | 549 | 550 | ## probably put somewhere else EQC-Suite for example? 551 | def feature(term, prop) do 552 | :eqc.collect(term, :eqc.features([term], prop)) 553 | end 554 | 555 | @doc """ 556 | Property combinator that is true if all components are true. The tags are used to report the counterexample. 557 | 558 | ## Example 559 | property "Sort" do 560 | forall l <- list(int()) do 561 | sorted = sort(l) 562 | conjunction in_order: in_order?(sorted), 563 | same_element: same_elements(l, sorted), 564 | same_length: ensure length(l) == length(sorted) 565 | end 566 | end 567 | 568 | 569 | """ 570 | def conjunction(kvlist) do 571 | try do :eqc.conjunction(kvlist) 572 | rescue _ in UndefinedFunctionError -> 573 | raise UndefinedFunctionError, reason: "Your QuickCheck does not support conjunction" 574 | end 575 | end 576 | 577 | @doc """ 578 | Wraps an expression that may raise an exception such that if the exception 579 | is of the expected error kind, this error is returned as a value, not raised 580 | as an exception. 581 | 582 | It only catches the expected error, or all errors if none is specified. 583 | 584 | Usage: 585 | resist ArithmeticError, div(4, 0) 586 | > ArithmeticError 587 | 588 | resist ArgumentError, div(4,0) 589 | ** (ArithmeticError) bad argument in arithmetic expression 590 | 591 | resist div(4,0) 592 | > ArithmeticError 593 | 594 | 595 | 596 | """ 597 | defmacro resist(error, cmd) do 598 | quote do 599 | try do 600 | unquote(cmd) 601 | rescue 602 | e in unquote(error) -> unquote(error) 603 | end 604 | end 605 | end 606 | 607 | @doc false 608 | defmacro resist(cmd) do 609 | quote do 610 | try do 611 | unquote(cmd) 612 | rescue 613 | e -> if is_map(e), do: e.__struct__, else: e 614 | end 615 | end 616 | end 617 | 618 | 619 | 620 | 621 | @doc """ 622 | A property checking an operation and prints when relation is violated. 623 | In postconditions, one uses satisfy instead. 624 | 625 | Usage: 626 | 627 | ensure t1 == t2 628 | ensure t1 > t2 629 | 630 | In Erlang ?WHENFAILS(eqc:format("not ensured: ~p ~p ~p\n",[T1, Operator, T2]), T1 Operator T2). 631 | """ 632 | 633 | @operator [:==, :<, :>, :<=, :>=, :===, :=~, :!==, :!=, :in] 634 | defmacro ensure({operator, _, [left, right]}) when operator in @operator do 635 | quote do 636 | uleft = unquote(left) 637 | uright = unquote(right) 638 | message = "not ensured: #{inspect(uleft)} #{unquote operator} #{inspect(uright)}" 639 | when_fail :eqc.format("~ts\n", [message]) do 640 | unquote(operator)(uleft, uright) 641 | end 642 | end 643 | end 644 | 645 | @doc """ 646 | A condition modifier for operators to be used in postconditions. 647 | Instead of returning false, it returns a tuple representation of failing operator. 648 | 649 | Usage: 650 | 651 | satisfy t1 == t2 652 | satisfy t1 > t2 653 | 654 | In Erlang eq(t1, t2). 655 | """ 656 | 657 | defmacro satisfy({operator, _, [left, right]}) when operator in @operator do 658 | quote do 659 | uleft = unquote(left) 660 | uright = unquote(right) 661 | unquote(operator)(uleft, uright) || 'not satisfied #{inspect(uleft)} #{unquote(operator)} #{inspect(uright)}' 662 | end 663 | end 664 | defmacro satisfy(expr) do 665 | quote bind_quoted: [expr: expr, escaped: Macro.escape(expr)] do 666 | string = Macro.to_string(escaped) 667 | expr == true || 'not satisfied #{string} by #{inspect(expr)}' 668 | end 669 | end 670 | 671 | end 672 | -------------------------------------------------------------------------------- /lib/eqc/cluster.ex: -------------------------------------------------------------------------------- 1 | defmodule EQC.Cluster do 2 | @copyright "Quviq AB, 2014-2016" 3 | 4 | @moduledoc """ 5 | This module contains macros to be used with [Quviq 6 | QuickCheck](http://www.quviq.com). It defines Elixir versions of the Erlang 7 | macros found in `eqc/include/eqc_cluster.hrl`. For detailed documentation of the 8 | macros, please refer to the QuickCheck documentation. 9 | 10 | `Copyright (C) Quviq AB, 2014-2016.` 11 | """ 12 | 13 | defmacro __using__(_opts) do 14 | quote do 15 | import :eqc_cluster, only: [commands: 1, commands: 2, adapt_commands: 2, state_after: 2, 16 | api_spec: 1] 17 | import :eqc_statem, only: [eq: 2, command_names: 1, more_commands: 2] 18 | import :eqc_mocking, only: [start_mocking: 2, stop_mocking: 0] 19 | 20 | import EQC.Cluster 21 | @tag eqc_callback: :eqc_cluster 22 | 23 | end 24 | end 25 | 26 | # -- Wrapper functions ------------------------------------------------------ 27 | 28 | @doc """ 29 | Same as `:eqc_cluster.run_commands/2` but returns a keyword list with 30 | `:history`, `:state`, and `:result` instead of a tuple. 31 | """ 32 | def run_commands(mod, cmds) do 33 | run_commands(mod, cmds, []) end 34 | 35 | @doc """ 36 | Same as `:eqc_cluster.run_commands/3` but returns a keyword list with 37 | `:history`, `:state`, and `:result` instead of a tuple. 38 | """ 39 | def run_commands(mod, cmds, env) do 40 | {history, state, result} = :eqc_cluster.run_commands(mod, cmds, env) 41 | [history: history, state: state, result: result] 42 | end 43 | 44 | @doc """ 45 | Same as `:eqc_component.pretty_commands/4` but takes a keyword list with 46 | `:history`, `:state`, and `:result` instead of a tuple as the third argument. 47 | """ 48 | def pretty_commands(mod, cmds, res, bool) do 49 | :eqc_component.pretty_commands(mod, cmds, 50 | {res[:history], res[:state], res[:result]}, 51 | bool) 52 | end 53 | 54 | @doc """ 55 | Generate a weight function given a keyword list of component names and weights. 56 | 57 | Usage: 58 | 59 | weight component1: weight1, component2: weight2 60 | 61 | Components not in the list get weight 1. 62 | """ 63 | defmacro weight(cmds) do 64 | for {cmd, w} <- cmds do 65 | quote do 66 | def weight(unquote(cmd)) do unquote(w) end 67 | end 68 | end ++ 69 | [ quote do 70 | def weight(_) do 1 end 71 | end ] 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/eqc/component.ex: -------------------------------------------------------------------------------- 1 | defmodule EQC.Component do 2 | @copyright "Quviq AB, 2014-2016" 3 | 4 | @moduledoc """ 5 | This module contains macros to be used with [Quviq 6 | QuickCheck](http://www.quviq.com). It defines Elixir versions of the Erlang 7 | macros found in `eqc/include/eqc_component.hrl`. For detailed documentation of the 8 | macros, please refer to the QuickCheck documentation. 9 | 10 | `Copyright (C) Quviq AB, 2014-2016.` 11 | """ 12 | 13 | defmacro __using__(_opts) do 14 | quote do 15 | import :eqc_component, only: [commands: 1, commands: 2] 16 | import :eqc_statem, only: [eq: 2, command_names: 1] 17 | import EQC.Component 18 | import EQC.Component.Callouts 19 | 20 | @file "eqc_component.hrl" 21 | @compile {:parse_transform, :eqc_group_commands} 22 | @compile {:parse_transform, :eqc_transform_callouts} 23 | @tag eqc_callback: :eqc_component 24 | end 25 | end 26 | 27 | # -- Wrapper functions ------------------------------------------------------ 28 | 29 | @doc """ 30 | Same as `:eqc_component.run_commands/2` but returns a keyword list with 31 | `:history`, `:state`, and `:result` instead of a tuple. 32 | """ 33 | def run_commands(mod, cmds) do 34 | run_commands(mod, cmds, []) end 35 | 36 | @doc """ 37 | Same as `:eqc_component.run_commands/3` but returns a keyword list with 38 | `:history`, `:state`, and `:result` instead of a tuple. 39 | """ 40 | def run_commands(mod, cmds, env) do 41 | {history, state, result} = :eqc_component.run_commands(mod, cmds, env) 42 | [history: history, state: state, result: result] 43 | end 44 | 45 | @doc """ 46 | Same as `:eqc_component.pretty_commands/4` but takes a keyword list with 47 | `:history`, `:state`, and `:result` instead of a tuple as the third argument. 48 | """ 49 | def pretty_commands(mod, cmds, res, bool) do 50 | :eqc_component.pretty_commands(mod, cmds, 51 | {res[:history], res[:state], res[:result]}, 52 | bool) 53 | end 54 | 55 | @doc """ 56 | Generate a weight function given a keyword list of command names and weights. 57 | 58 | Usage: 59 | 60 | weight state, 61 | cmd1: weight1, 62 | cmd2: weight2 63 | 64 | Commands not in the list get weight 1. 65 | """ 66 | defmacro weight(state, cmds) do 67 | for {cmd, w} <- cmds do 68 | quote do 69 | def weight(unquote(state), unquote(cmd)) do unquote(w) end 70 | end 71 | end ++ 72 | [ quote do 73 | def weight(unquote(state), _) do 1 end 74 | end ] 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/eqc/component/callouts.ex: -------------------------------------------------------------------------------- 1 | defmodule EQC.Component.Callouts do 2 | @copyright "Quviq AB, 2016" 3 | 4 | @moduledoc """ 5 | This module contains functions to be used with [Quviq 6 | QuickCheck](http://www.quviq.com). It defines an Elixir version of the callout 7 | language found in `eqc/include/eqc_component.hrl`. For detailed documentation 8 | of the macros, please refer to the QuickCheck documentation. 9 | 10 | `Copyright (C) Quviq AB, 2014-2016.` 11 | """ 12 | 13 | @doc """ 14 | Call a command from a callout. 15 | 16 | In Erlang: `?APPLY(Mod, Fun, Args)`. 17 | """ 18 | def call(mod, fun, args), do: {:self_callout, mod, fun, args} 19 | 20 | @doc """ 21 | Call a local command from a callout. 22 | 23 | In Erlang: `?APPLY(Fun, Args)`. 24 | """ 25 | defmacro call(fun, args) do 26 | quote do 27 | call(__MODULE__, unquote(fun), unquote(args)) 28 | end 29 | end 30 | 31 | @doc """ 32 | Convenient syntax for `call`. 33 | 34 | call m.f(e1, .., en) 35 | call f(e1, .., en) 36 | 37 | is equivalent to 38 | 39 | call(m, f, [e1, .., en]) 40 | call(f, [e1, .., en]) 41 | """ 42 | defmacro call({{:., _, [mod, fun]}, _, args}) do 43 | quote do call(unquote(mod), unquote(fun), unquote(args)) end 44 | end 45 | defmacro call({fun, _, args}) when is_atom(fun) do 46 | quote do call(__MODULE__, unquote(fun), unquote(args)) end 47 | end 48 | defmacro call(c) do 49 | _ = c 50 | syntax_error "call F(E1, .., En)" 51 | end 52 | 53 | @doc """ 54 | Specify a callout. 55 | 56 | In Erlang: `?CALLOUT(Mod, Fun, Args, Res)`. 57 | """ 58 | def callout(mod, fun, args, res), do: :eqc_component.callout(mod, fun, args, res) 59 | 60 | @doc """ 61 | Convenient syntax for `callout`. 62 | 63 | callout m.f(e1, .., en), return: res 64 | 65 | is equivalent to 66 | 67 | callout(m, f, [e1, .., en], res) 68 | """ 69 | defmacro callout({{:., _, [mod, fun]}, _, args}, [return: res]) do 70 | quote do callout(unquote(mod), unquote(fun), unquote(args), unquote(res)) end 71 | end 72 | defmacro callout(call, opts) do 73 | _ = {call, opts} 74 | syntax_error "callout MOD.FUN(ARG1, .., ARGN), return: RES" 75 | end 76 | 77 | defp do_match(e) do 78 | quote do {:"$eqc_callout_match", unquote(e)} end 79 | end 80 | 81 | defp do_match_gen(e) do 82 | quote do {:"$eqc_callout_match_gen", unquote(e)} end 83 | end 84 | 85 | @doc """ 86 | Bind the result of a callout or generator. 87 | 88 | Usage: 89 | 90 | match pat = exp 91 | match pat <- gen 92 | 93 | In Erlang: `?MATCH(Pat, Exp)` or `?MATCH_GEN(Pat, Gen)`. 94 | """ 95 | defmacro match(e={:=, _, [_, _]}), do: do_match(e) 96 | defmacro match({:<-, c, [pat, gen]}), do: do_match_gen({:=, c, [pat, gen]}) 97 | defmacro match(e) do 98 | _ = e 99 | syntax_error "match PAT = EXP, or match PAT <- GEN" 100 | end 101 | 102 | # Hacky. Let's you write (for instance) match pat = case exp do ... end. 103 | @doc false 104 | defmacro match({:=, cxt1, [pat, {fun, cxt2, args}]}, opts) do 105 | do_match({:=, cxt1, [pat, {fun, cxt2, args ++ [opts]}]}) 106 | end 107 | defmacro match({:<-, cxt1, [pat, {fun, cxt2, args}]}, opts) do 108 | do_match({:<-, cxt1, [pat, {fun, cxt2, args ++ [opts]}]}) 109 | end 110 | defmacro match(_, _), do: syntax_error "match PAT = EXP, or match PAT <- GEN" 111 | 112 | @doc """ 113 | Model failure. 114 | 115 | In Erlang: `?FAIL(E)`. 116 | """ 117 | def fail(e), do: {:fail, e} 118 | 119 | @doc """ 120 | Exception return value. Can be used as the return value for a callout to make it throw an exception. 121 | 122 | In Erlang: `?EXCEPTION(e)`. 123 | """ 124 | defmacro exception(e) do 125 | quote do {:"$eqc_exception", unquote(e)} end 126 | end 127 | 128 | @doc """ 129 | Model sending a message. 130 | 131 | In Erlang: `?SEND(Pid, Msg)` 132 | """ 133 | def send(pid, msg), do: callout(:erlang, :send, [pid, msg], msg) 134 | 135 | @doc """ 136 | Specify the result of an operation. 137 | 138 | In Erlang: `?RET(X)` 139 | """ 140 | def ret(x), do: {:return, x} 141 | 142 | @doc """ 143 | Run-time assertion. 144 | 145 | In Erlang: `?ASSERT(Mod, Fun, Args)` 146 | """ 147 | defmacro assert(mod, fun, args) do 148 | loc = {__CALLER__.file, __CALLER__.line} 149 | quote do 150 | {:assert, unquote(mod), unquote(fun), unquote(args), 151 | {:assertion_failed, unquote(mod), unquote(fun), unquote(args), unquote(loc)}} 152 | end 153 | end 154 | 155 | @doc """ 156 | Convenient syntax for assert. 157 | 158 | Usage: 159 | 160 | assert mod.fun(e1, .., en) 161 | """ 162 | defmacro assert({{:., _, [mod, fun]}, _, args}) do 163 | quote do assert(unquote(mod), unquote(fun), unquote(args)) end 164 | end 165 | defmacro assert(call) do 166 | _ = call 167 | syntax_error "assert MOD.FUN(ARG1, .., ARGN)" 168 | end 169 | 170 | @doc """ 171 | Get access to (part of) an argument to a callout. For instance, 172 | 173 | match {val, :ok} = callout :mock.foo(some_arg, __VAR__), return: :ok 174 | ... 175 | 176 | Argument values are returned in a tuple with the return value. 177 | 178 | Use `:_` to ignore a callout argument. 179 | 180 | In Erlang: `?VAR` 181 | """ 182 | defmacro __VAR__, do: :"$var" 183 | 184 | @doc """ 185 | Access the pid of the process executing an operation. 186 | 187 | In Erlang: `?SELF` 188 | """ 189 | defmacro __SELF__, do: :"$self" 190 | 191 | @doc """ 192 | A list of callout specifications in sequence. 193 | 194 | In Erlang: `?SEQ` 195 | """ 196 | def seq(list), do: {:seq, list} 197 | 198 | @doc """ 199 | A list of callout specications arbitrarily interleaved. 200 | 201 | In Erlang: `?PAR` 202 | """ 203 | def par(list), do: {:par, list} 204 | 205 | @doc """ 206 | A choice between two different callout specifications. 207 | 208 | In Erlang: `?EITHER(Tag, C1, C2)` 209 | """ 210 | def either(c1, c2), do: {:xalt, c1, c2} 211 | 212 | @doc """ 213 | A choice between two different callout specifications where every choice with 214 | the same tag has to go the same way (left or right). 215 | 216 | In Erlang: `?EITHER(Tag, C1, C2)` 217 | """ 218 | def either(tag, c1, c2), do: {:xalt, tag, c1, c2} 219 | 220 | @doc """ 221 | An optional callout specification. Equivalent to `either(c, :empty)`. 222 | 223 | In Erlang: `?OPTIONAL(C)` 224 | """ 225 | def optional(c), do: either(c, :empty) 226 | 227 | @doc """ 228 | Specify a blocking operation. 229 | 230 | In Erlang: `?BLOCK(Tag)` 231 | """ 232 | def block(tag), do: {:"$eqc_block", tag} 233 | 234 | @doc """ 235 | Equivalent to block(__SELF__). 236 | 237 | In Erlang: `?BLOCK` 238 | """ 239 | def block(), do: block(__SELF__()) 240 | 241 | @doc """ 242 | Unblocking a blocked operation. 243 | 244 | In Erlang: `?UNBLOCK(Tag, Res)` 245 | """ 246 | def unblock(tag, res), do: {:unblock, tag, res} 247 | 248 | @doc """ 249 | Conditional callout specification. 250 | 251 | Usage: 252 | 253 | guard g, do: c 254 | 255 | Equivalent to: 256 | 257 | case g do 258 | true -> c 259 | false -> :empty 260 | end 261 | 262 | In Erlang: `?WHEN(G, C)` 263 | """ 264 | defmacro guard(g, do: c) do 265 | quote do 266 | case unquote(g) do 267 | true -> unquote(c) 268 | false -> :empty 269 | end 270 | end 271 | end 272 | defmacro guard(g, c) do 273 | _ = {g, c} 274 | syntax_error "guard GUARD, do: CALLOUTS" 275 | end 276 | 277 | @doc """ 278 | Indicate that the following code is using the callout specification language. 279 | 280 | This is default for the `_callouts` callback, but this information is lost in 281 | some constructions like list comprehensions or `par/1` calls. 282 | 283 | Usage: 284 | 285 | callouts do 286 | ... 287 | end 288 | 289 | In Erlang: `?CALLOUTS(C1, .., CN)` 290 | """ 291 | defmacro callouts(do: {:__block__, cxt, args}), do: {:"$eqc_callout_quote", cxt, args} 292 | defmacro callouts(do: c), do: {:"$eqc_callout_quote", [], [c]} 293 | defmacro callouts(c) do 294 | _ = c 295 | syntax_error "callouts do CALLOUTS end" 296 | end 297 | 298 | defp syntax_error(err), do: raise(ArgumentError, "Usage: " <> err) 299 | end 300 | -------------------------------------------------------------------------------- /lib/eqc/exunit.ex: -------------------------------------------------------------------------------- 1 | defmodule EQC.ExUnit do 2 | # import ExUnit.Case 3 | 4 | defmodule Pretty do 5 | @moduledoc false 6 | 7 | def print(true), do: "" 8 | def print([]), do: "\n" 9 | def print([term|tail]), do: pp(term) <> "\n " <> print(tail) 10 | 11 | def pp(term) do 12 | if :eqc.version() == 2.01 do 13 | ## QuickCheck Mini has less advanced printing 14 | Macro.to_string(term) 15 | else 16 | IO.iodata_to_binary(:prettypr.format(:eqc_symbolic.pretty_elixir_symbolic_doc(term), 80)) 17 | end 18 | end 19 | 20 | end 21 | 22 | defmacro __using__(_opts) do 23 | quote do 24 | import EQC.ExUnit 25 | use EQC 26 | ExUnit.Case.register_attribute __ENV__, :check 27 | 28 | ExUnit.plural_rule("property", "properties") 29 | end 30 | end 31 | 32 | @moduledoc """ 33 | Properties can be executed using the ExUnit framework using 'mix test'. 34 | 35 | A test module using properties writes 'property' instead or 'test' and uses 36 | `EQC.ExUnit` module for the macro definitions. 37 | 38 | ## Example 39 | 40 | defmodule SimpleTests do 41 | use ExUnit.Case 42 | use EQC.ExUnit 43 | 44 | property "naturals are >= 0" do 45 | forall n <- nat do 46 | ensure n >= 0 47 | end 48 | end 49 | end 50 | 51 | 52 | ## Tags 53 | 54 | Properties can be tagged similar to ExUnit test cases. 55 | 56 | * `numtests:` `n` - QuickCheck runs `n` test cases, default is `100`. This tag has priority over `min_time` and `max_time` tags. 57 | * `min_time:` `t` - QuickCheck runs for `t` milliseconds unless `numtests` is reached. 58 | * `max_time:` `t` - QuickCheck runs for at most `t` milliseconds unless `numtests` is reached. 59 | * `timeout:` `t` - Inherited from ExUnit and fails if property takes more than `t` milliseconds. 60 | * `erlang_counterexample:` `false` - Specify whether QuickCheck should output 61 | the Erlang term that it gets as a counterexample when a property fails. Default `true`. 62 | * `:morebugs` - Runs more_bugs 63 | * `:showstates` - For QuickCheck state machines, show intermediate states for failing tests 64 | 65 | 66 | ## Example 67 | In the example below, QuickCheck runs the first propery for max 1 second and the second 68 | property for at least 1 second. This results in 100 tests (the default) or less for the 69 | first property and e.g. 22000 tests for the second property. 70 | 71 | defmodule SimpleTests do 72 | use ExUnit.Case 73 | use EQC.ExUnit 74 | 75 | @tag max_time: 1000 76 | property "naturals are >= 0" do 77 | forall n <- nat do 78 | ensure n >= 0 79 | end 80 | end 81 | 82 | @tag min_time: 1000 83 | property "implies fine" do 84 | forall {n,m} <- {int, nat} do 85 | implies m > n, do: 86 | ensure m > n 87 | end 88 | end 89 | 90 | end 91 | 92 | ## Checks 93 | 94 | You may want to test a previously failing case. You can do this by annotating the property 95 | with `@check`followed by a list of labelled counter examples. 96 | 97 | defmodule SimpleTests do 98 | use ExUnit.Case 99 | use EQC.ExUnit 100 | 101 | @check minimum_error: [-1], other_error: [-3] 102 | property "integers are >= 0" do 103 | forall n <- int do 104 | ensure n >= 0 105 | end 106 | end 107 | end 108 | 109 | """ 110 | 111 | @doc """ 112 | Defines a property with a string similar to how tests are defined in 113 | `ExUnit.Case`. 114 | 115 | ## Examples 116 | property "naturals are >= 0" do 117 | forall n <- nat do 118 | ensure n >= 0 119 | end 120 | end 121 | """ 122 | defmacro property(message, var \\ quote(do: _), contents) do 123 | prop_ok = 124 | case contents do 125 | [do: block] -> 126 | quote do 127 | unquote(block) 128 | end 129 | _ -> 130 | quote do 131 | try(unquote(contents)) 132 | end 133 | end 134 | 135 | context = Macro.escape(var) 136 | prop = Macro.escape(prop_ok, unquote: true) 137 | 138 | quote bind_quoted: [prop: prop, message: message, context: context] do 139 | string = Macro.to_string(prop) 140 | 141 | ## `mix eqc` options overwrite module tags 142 | env_flags = 143 | Enum.reduce([:numtests, :morebugs, :showstates], 144 | [], 145 | fn(key, acc) -> 146 | value = Application.get_env(:eqc, key) 147 | if value == nil do 148 | acc 149 | else 150 | [[{key, value}]|acc] 151 | end 152 | end) 153 | 154 | property = ExUnit.Case.register_test(__ENV__, :property, message, 155 | [:check, :property] ++ env_flags) 156 | 157 | def unquote(property)(context = unquote(context)) do 158 | 159 | transformed_prop = transform(unquote(prop), context) 160 | 161 | case {Map.get(context, :morebugs), Map.get(context, :eqc_callback)} do 162 | {true, callback} when callback != nil -> 163 | suite = :eqc_statem.more_bugs(transformed_prop) 164 | ## possibly save suite at ENV determined location 165 | case suite do 166 | {:feature_based, []} -> true 167 | {:feature_based, fset} -> 168 | tests = 169 | for {_,ce} <- fset do 170 | Pretty.print(ce) 171 | end 172 | assert false, Enum.join(tests, "\n\n") 173 | _ -> 174 | assert false, "No feature based suite returned" 175 | end 176 | _ -> 177 | failures = 178 | if context.registered.check do 179 | Enum.reduce(context.registered.check, "", 180 | fn({label, ce}, acc) -> 181 | if :eqc.check(transformed_prop, ce) do 182 | acc 183 | else 184 | acc <> "#{label}: " <> Pretty.print(ce) 185 | end 186 | end) 187 | else 188 | "" 189 | end 190 | if :check in ExUnit.configuration()[:include] do 191 | assert "" == failures, unquote(string) <> "\nFailed for\n" <> failures 192 | else 193 | :eqc_random.seed(:os.timestamp) 194 | 195 | counterexample = :eqc.counterexample(transformed_prop) 196 | assert true == counterexample, unquote(string) <> "\nFailed for " <> Pretty.print(counterexample) <> failures 197 | assert "" == failures, unquote(string) <> "\nFailed for\n" <> failures 198 | end 199 | end 200 | end 201 | 202 | end 203 | end 204 | 205 | @doc false 206 | def transform(prop, opts), do: do_transform(prop, Enum.to_list(opts)) 207 | 208 | defp do_transform(prop, []) do 209 | prop 210 | end 211 | defp do_transform(prop, [{:numtests, nr}| opts]) do 212 | do_transform(:eqc.numtests(nr, prop), opts) 213 | end 214 | defp do_transform(prop, [{:min_time, ms} | opts]) do 215 | do_transform(:eqc.with_testing_time_unit(1, :eqc.testing_time({:min, ms}, prop)), opts) 216 | end 217 | defp do_transform(prop, [{:max_time, ms} | opts]) do 218 | do_transform(:eqc.with_testing_time_unit(1, :eqc.testing_time({:max, ms}, prop)), opts) 219 | end 220 | defp do_transform(prop, [{:erlang_counterexample, b} | opts]) do 221 | if !b do 222 | do_transform(:eqc.dont_print_counterexample(prop), opts) 223 | else 224 | do_transform(prop, opts) 225 | end 226 | end 227 | defp do_transform(prop, [{:showstates, b} | opts]) do 228 | if b do 229 | do_transform(:eqc_statem.show_states(prop), opts) 230 | else 231 | do_transform(prop, opts) 232 | end 233 | end 234 | defp do_transform(prop, [_ | opts]) do 235 | do_transform(prop, opts) 236 | end 237 | 238 | 239 | 240 | end 241 | -------------------------------------------------------------------------------- /lib/eqc/mocking.ex: -------------------------------------------------------------------------------- 1 | defmodule EQC.Mocking do 2 | @copyright "Quviq AB, 2014-2016" 3 | 4 | @moduledoc """ 5 | This module contains macros to be used with [Quviq 6 | QuickCheck](http://www.quviq.com). It defines Elixir versions of the Erlang 7 | macros found in `eqc/include/eqc_mocking.hrl`. For detailed documentation of the 8 | macros, please refer to the QuickCheck documentation. 9 | 10 | `Copyright (C) Quviq AB, 2014-2016.` 11 | 12 | ## Example 13 | 14 | Typical use in Component module definitions 15 | 16 | require EQC.Mocking 17 | 18 | def api_spec do 19 | EQC.Mocking.api_spec [ 20 | modules: [ 21 | EQC.Mocking.api_module name: :mock 22 | ] 23 | ] 24 | end 25 | 26 | ## QuickCheck dependency 27 | 28 | Note that mocking only works with the full version of QuickCheck, not with 29 | QuickCheck Mini. In order to use this module, one has to compile it with QuickCheck installed. 30 | This compilation will then generate the Elixir Records corresponding to 31 | the records defined in the Erlang header files of QuickCheck. 32 | 33 | """ 34 | require Record 35 | 36 | try do 37 | Record.defrecord :api_spec, Record.extract(:api_spec, from_lib: "eqc/include/eqc_mocking_api.hrl") 38 | Record.defrecord :api_module, Record.extract(:api_module, from_lib: "eqc/include/eqc_mocking_api.hrl") 39 | Record.defrecord :api_fun, Record.extract(:api_fun, from_lib: "eqc/include/eqc_mocking_api.hrl") 40 | rescue _ -> 41 | "Full Version of QuickCheck Needed" 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lib/eqc/pulse.ex: -------------------------------------------------------------------------------- 1 | defmodule EQC.Pulse do 2 | @copyright "Quviq AB, 2014-2016" 3 | 4 | @moduledoc """ 5 | This module defines macros for using Quviq PULSE with Elixir. For more 6 | information about the compiler options see the QuickCheck documentation. 7 | 8 | See also the [`pulse_libs`](http://hex.pm/packages/pulse_libs) package for 9 | instrumented versions of some of the Elixir standard libraries. 10 | 11 | `Copyright (C) Quviq AB, 2014-2016.` 12 | """ 13 | 14 | defmacro __using__([]) do 15 | quote(do: EQC.Pulse.instrument) 16 | end 17 | 18 | @doc """ 19 | Instrument the current file with PULSE. 20 | 21 | Equivalent to 22 | 23 | @compile {:parse_transform, :pulse_instrument} 24 | """ 25 | defmacro instrument do 26 | quote do 27 | @compile {:parse_transform, :pulse_instrument} 28 | end 29 | end 30 | 31 | @doc """ 32 | Replace a module when instrumenting. 33 | 34 | Usage: 35 | 36 | replace_module old, with: new 37 | 38 | This will replace calls `old.f(args)` by `new.f(args)`. Note: it will not 39 | replace instances of `old` used as an atom. For instance `spawn old, :f, 40 | args` will not be changed. 41 | 42 | Equivalent to 43 | 44 | @compile {:pulse_replace_module, [{old, new}]} 45 | """ 46 | defmacro replace_module(old, with: new) when new != nil do 47 | quote(do: @compile {:pulse_replace_module, [{unquote(old), unquote(new)}]}) 48 | end 49 | defmacro replace_module(old, opts) do 50 | _ = {old, opts} 51 | raise ArgumentError, "Usage: replace_module NEW, with: OLD" 52 | end 53 | 54 | defp skip_funs({f, a}) when is_atom(f) and is_integer(a), do: [{f, a}] 55 | defp skip_funs({{f, _, nil}, a}) when is_atom(f) and is_integer(a), do: [{f, a}] 56 | defp skip_funs({:/, _, [f, a]}), do: skip_funs({f, a}) 57 | defp skip_funs(xs) when is_list(xs), do: :lists.flatmap(&skip_funs/1, xs) 58 | defp skip_funs(_) do 59 | raise ArgumentError, "Expected list of FUN/ARITY." 60 | end 61 | 62 | @doc """ 63 | Skip instrumentation of the given functions. 64 | 65 | Example: 66 | 67 | skip_function [f/2, g/0] 68 | 69 | Equivalent to 70 | 71 | @compile {:pulse_skip, [{:f, 2}, {:g, 0}]} 72 | """ 73 | defmacro skip_function(funs) do 74 | quote(do: @compile {:pulse_skip, unquote(skip_funs(funs))}) 75 | end 76 | 77 | defp mk_blank({:_, _, _}), do: :_ 78 | defp mk_blank(x), do: x 79 | 80 | defp side_effects(es) when is_list(es), do: :lists.flatmap(&side_effects/1, es) 81 | defp side_effects({:/, _, [{{:., _, [m, f]}, _, []}, a]}), do: side_effects({m, f, a}) 82 | defp side_effects({m, f, a}), do: [{:{}, [], [m, mk_blank(f), mk_blank(a)]}] 83 | defp side_effects(_) do 84 | raise ArgumentError, "Expected list of MOD.FUN/ARITY." 85 | end 86 | 87 | @doc """ 88 | Declare side effects. 89 | 90 | Example: 91 | 92 | side_effect [Mod.fun/2, :ets._/_] 93 | 94 | Equivalent to 95 | 96 | @compile {:pulse_side_effect, [{Mod, :fun, 2}, {:ets, :_, :_}]} 97 | """ 98 | defmacro side_effect(es) do 99 | quote(do: @compile {:pulse_side_effect, unquote(side_effects(es))}) 100 | end 101 | 102 | @doc """ 103 | Declare functions to not be effectful. 104 | 105 | Useful to override `side_effect/1`. For instance, 106 | 107 | side_effect :ets._/_ 108 | no_side_effect :ets.is_compiled_ms/1 109 | 110 | The latter line is quivalent to 111 | 112 | @compile {:pulse_no_side_effect, [{:ets, :is_compiled_ms, 1}]} 113 | """ 114 | defmacro no_side_effect(es) do 115 | quote(do: @compile {:pulse_no_side_effect, unquote(side_effects(es))}) 116 | end 117 | 118 | @doc """ 119 | Define a QuickCheck property that uses PULSE. 120 | 121 | Usage: 122 | 123 | with_pulse do 124 | action 125 | after res -> 126 | prop 127 | end 128 | 129 | Equivalent to 130 | 131 | forall seed <- :pulse.seed do 132 | case :pulse.run_with_seed(fn -> action end, seed) do 133 | res -> prop 134 | end 135 | end 136 | """ 137 | defmacro with_pulse(do: action, after: clauses) when action != nil and clauses != nil do 138 | res = Macro.var :res, __MODULE__ 139 | quote do 140 | :eqc.forall(:pulse.seed(), 141 | fn seed -> 142 | unquote(res) = :pulse.run_with_seed(fn -> unquote(action) end, seed) 143 | unquote({:case, [], [res, [do: clauses]]}) 144 | end) 145 | end 146 | end 147 | defmacro with_pulse(opts) do 148 | _ = opts 149 | raise(ArgumentError, "Syntax: with_pulse do: ACTION, after: (RES -> PROP)") 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /lib/eqc/statem.ex: -------------------------------------------------------------------------------- 1 | defmodule EQC.StateM do 2 | @moduledoc """ 3 | This module contains macros to be used with [Quviq 4 | QuickCheck](http://www.quviq.com). It defines Elixir versions of Erlang 5 | functions found in `eqc/include/eqc_statem.hrl`. For detailed documentation of the 6 | functions, please refer to the QuickCheck documentation. 7 | 8 | `Copyright (C) Quviq AB, 2014-2016.` 9 | """ 10 | 11 | defmacro __using__(_opts) do 12 | quote do 13 | import :eqc_statem, only: [commands: 1, commands: 2, 14 | parallel_commands: 1, parallel_commands: 2, 15 | more_commands: 2, 16 | commands_length: 1] 17 | import EQC.StateM 18 | 19 | @file "eqc_statem.hrl" 20 | @compile {:parse_transform, :eqc_group_commands} 21 | @tag eqc_callback: :eqc_statem 22 | 23 | end 24 | end 25 | 26 | @doc """ 27 | Runs a state machine generated command sequence and returns a keyword list with 28 | `:history`, `:state`, and `:result` instead of a tuple. 29 | """ 30 | def run_commands(cmds) do 31 | run_commands(cmds, []) 32 | end 33 | 34 | @doc """ 35 | Runs a state machine generated command sequence where vairables in this 36 | sequence are substituted by a Keyword list defined context. 37 | Returns a keyword list with 38 | `:history`, `:state`, and `:result` instead of a tuple. 39 | """ 40 | def run_commands(cmds, env) do 41 | {history, state, result} = :eqc_statem.run_commands(cmds, env) 42 | [history: history, state: state, result: result] 43 | end 44 | 45 | @doc false 46 | # deprecated 47 | def run_commands(mod, cmds, env) do 48 | {history, state, result} = :eqc_statem.run_commands(mod, cmds, env) 49 | [history: history, state: state, result: result] 50 | end 51 | 52 | 53 | @doc """ 54 | Runs a state machine generated parallel command sequenceand returns a keyword list with 55 | `:history`, `:state`, and `:result` instead of a tuple. Note that there is no 56 | actual final state in this case. 57 | """ 58 | def run_parallel_commands(cmds) do 59 | {history, state, result} = :eqc_statem.run_parallel_commands(cmds) 60 | [history: history, state: state, result: result] 61 | end 62 | 63 | @doc """ 64 | Runs a state machine generated parallel command sequence where vairables in this 65 | sequence are substituted by a Keyword list defined context. 66 | Returns a keyword list with 67 | `:history`, `:state`, and `:result` instead of a tuple. Note that there is no 68 | actual final state in this case. 69 | """ 70 | def run_parallel_commands(cmds, env) do 71 | {history, state, result} = :eqc_statem.run_parallel_commands(cmds, env) 72 | [history: history, state: state, result: result] 73 | end 74 | 75 | @doc false 76 | # deprecated 77 | def run_parallel_commands(mod, cmds, env) do 78 | {history, state, result} = :eqc_statem.run_parallel_commands(mod, cmds, env) 79 | [history: history, state: state, result: result] 80 | end 81 | 82 | @doc """ 83 | When a test case fails, this pretty prints the failing test case. 84 | """ 85 | def pretty_commands(cmds, res, bool) 86 | def pretty_commands([{:model, m} | cmds], res, bool) do 87 | :eqc_gen.with_parameter(:elixir, :true, 88 | :eqc_statem.pretty_commands(m, [{:model, m} | cmds], 89 | {res[:history], res[:state], res[:result]}, 90 | bool)) 91 | end 92 | 93 | 94 | @doc false 95 | # deprecated 96 | def pretty_commands(mod, cmds, res, bool) do 97 | :eqc_gen.with_parameter(:elixir, :true, 98 | :eqc_statem.pretty_commands(mod, cmds, 99 | {res[:history], res[:state], res[:result]}, 100 | bool)) 101 | end 102 | 103 | @doc false 104 | # deprecated 105 | def check_commands(mod, cmds, run_result) do 106 | check_commands(mod, cmds, run_result, []) end 107 | 108 | @doc false 109 | # deprecated 110 | def check_commands(mod, cmds, res, env) do 111 | :eqc_gen.with_parameter(:elixir, :true, 112 | :eqc_statem.check_commands(mod, cmds, 113 | {res[:history], res[:state], res[:result]}, 114 | env)) 115 | end 116 | 117 | @doc """ 118 | Add weights to the commands in a statem specification 119 | 120 | ## Example 121 | 122 | weight _, take: 10, reset: 1 123 | # Choose 10 times more 'take' than 'reset' 124 | 125 | weight s, take: 10, reset: s 126 | # The more tickets taken, the more likely reset becomes 127 | """ 128 | defmacro weight(state, cmds) do 129 | for {cmd, w} <- cmds do 130 | quote do 131 | def weight(unquote(state), unquote(cmd)) do unquote(w) end 132 | end 133 | end ++ 134 | [ quote do 135 | def weight(_, _) do 1 end 136 | end ] 137 | end 138 | 139 | @doc """ 140 | Same as `:eqc_statem.command_names/1` but replaces the module name to Elixir style. 141 | """ 142 | def command_names(cmds) do 143 | for {m, f, as} <- :eqc_statem.command_names(cmds) do 144 | {String.to_atom(Enum.join(Module.split(m), ".")), f, as} 145 | end 146 | end 147 | 148 | 149 | @doc """ 150 | Converts the given call expression into a symbolic call. 151 | 152 | ## Examples 153 | 154 | symcall extract_pid(result) 155 | # {:call, __MODULE__, :extract_pid, [result]} 156 | 157 | symcall OtherModule.do_something(result, args) 158 | # {:call, OtherModule, :do_something, [result, args]} 159 | """ 160 | defmacro symcall({{:., _, [mod, fun]}, _, args}) do 161 | quote do 162 | {:call, unquote(mod), unquote(fun), unquote(args)} 163 | end 164 | end 165 | 166 | defmacro symcall({fun, _, args}) do 167 | quote do 168 | {:call, __MODULE__, unquote(fun), unquote(args)} 169 | end 170 | end 171 | 172 | defp replace_var([], binding, seq) do 173 | {Enum.reverse(seq), binding} 174 | end 175 | defp replace_var([{:=, _, [{var, _, _}, {{:., _, [mod, fun]}, _, args}]} | cmds], binding, seq) do 176 | freshvar = {:var, length(seq) + 1} 177 | {callargs, _} = Code.eval_quoted(args, binding, __ENV__) 178 | symbcmd = quote do {:set, unquote(freshvar), 179 | {:call, unquote(mod), unquote(fun), unquote(callargs)}} end 180 | replace_var(cmds, [{var, freshvar}|binding], [symbcmd|seq]) 181 | end 182 | defp replace_var([{{:., _, [mod, fun]}, _, args} | cmds], binding, seq) do 183 | freshvar = {:var, length(seq) + 1} 184 | {callargs, _} = Code.eval_quoted(args, binding, __ENV__) 185 | symbcmd = quote do {:set, unquote(freshvar), 186 | {:call, unquote(mod), unquote(fun), unquote(callargs)}} end 187 | replace_var(cmds, binding, [symbcmd|seq]) 188 | end 189 | defp replace_var([{:=, _, [{var, _, _}, {fun, _, args}]} | cmds], binding, seq) do 190 | freshvar = {:var, length(seq) + 1} 191 | {callargs, _} = Code.eval_quoted(args, binding, __ENV__) 192 | symbcmd = quote do {:set, unquote(freshvar), 193 | {:call, Macro.escape(__MODULE__), unquote(fun), unquote(callargs)}} end 194 | replace_var(cmds, [{var, freshvar}|binding], [symbcmd|seq]) 195 | end 196 | defp replace_var([{fun, _, args} | cmds], binding, seq) when is_atom(fun) do 197 | freshvar = {:var, length(seq) + 1} 198 | {callargs, _} = Code.eval_quoted(args, binding, __ENV__) 199 | symbcmd = quote do {:set, unquote(freshvar), 200 | {:call, Macro.escape(__MODULE__), unquote(fun), unquote(callargs)}} end 201 | replace_var(cmds, binding, [symbcmd|seq]) 202 | end 203 | 204 | 205 | 206 | @doc """ 207 | Translates test cases of a specific format into a list of commands that is compatible with 208 | `EQC.StateM`. 209 | 210 | ## Examples 211 | 212 | @check same_seat: [ 213 | eqc_test do 214 | v1 = book("business") 215 | book("economy") 216 | checkin(2, v1) 217 | bookings() 218 | end ] 219 | 220 | """ 221 | defmacro eqc_test([do: cmds]) do 222 | commands = case cmds do 223 | {:__block__, _, block} -> block 224 | nil -> [] 225 | cmd -> [cmd] 226 | end 227 | {new_commands, _binding} = 228 | replace_var(commands, [], []) 229 | quote do 230 | [ {:model, __MODULE__} | unquote(new_commands) ] 231 | end 232 | end 233 | 234 | 235 | 236 | 237 | 238 | end 239 | -------------------------------------------------------------------------------- /lib/mix/tasks/eqc.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Eqc do 2 | use Mix.Task 3 | 4 | @shortdoc "Test QuickCheck properties" 5 | 6 | @moduledoc """ 7 | A Mix task for running QuickCheck properties. At the moment, this basically calls `mix test` with the given options. 8 | 9 | ## Options 10 | 11 | * `--only property` - Default is to run all tests, also ExUnit tests, 12 | but this flag picks only the properties to run 13 | * `--only check` - Runs specific test cases annotated by @check and does not generate new QuickCheck values for properties. 14 | * `--numtests n` - Runs `n` tests for each property. 15 | * `--morebugs` - Activates more_bugs where appropriate. 16 | * `--showstates` - Show intermediate states in failing State Machine properties. 17 | 18 | 19 | ## Examples 20 | 21 | mix eqc 22 | 23 | """ 24 | @switches [ numtests: :integer, 25 | morebugs: :boolean, 26 | showstates: :boolean 27 | ] 28 | 29 | def run(argv) do 30 | {opts, files} = OptionParser.parse!(argv, switches: @switches) 31 | 32 | opts_to_env(opts) 33 | 34 | test_opts = Enum.filter(opts, fn({k,_}) -> not k in Keyword.keys(@switches) end) 35 | new_argv = OptionParser.to_argv([max_cases: 1] ++ test_opts) ++ files 36 | 37 | Mix.env(:test) 38 | Mix.Task.run(:test, new_argv) 39 | end 40 | 41 | defp opts_to_env(opts) do 42 | for key <- Keyword.keys(@switches) do 43 | if opts[key] != nil do 44 | Application.put_env(:eqc, key, opts[key], []) 45 | end 46 | end 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/mix/tasks/eqc_install.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Eqc.Install do 2 | use Mix.Task 3 | 4 | @shortdoc "Install Quviq QuickCheck as local archive" 5 | 6 | @moduledoc """ 7 | A Mix task for installing QuickCheck as a local archive. Note that you need a QuickCheck licence to be able to run the full version of QuickCheck (mailto: support@quviq.com to purchase one). 8 | QuickCheck Mini is Quviq's free version of QuickCheck. 9 | 10 | We do not follow the strict local archive rules and also create `include` and some other directories needed to make QuickCheck work well. But, one can uninstall with `mix archive.uninstall` for each created archive. 11 | Alternatively, one can uninstall QuickCheck with `mix eqc.uninstall`. 12 | 13 | ## Options 14 | 15 | * `--mini` - Install QuickCheck Mini. Default is to install full version. 16 | * `--version` - Provide version number for specific QuickCheck to install 17 | * `--force`- Overwrites already installed version of QuickCheck 18 | 19 | ## Examples 20 | 21 | mix eqc.install --mini 22 | mix eqc.install --version 1.38.1 23 | mix eqc.install --force --version 1.39.1 Downloads/programs/ 24 | 25 | """ 26 | @switches [mini: :boolean, version: :string, force: :boolean] 27 | 28 | @spec run(OptionParser.argv) :: boolean 29 | def run(argv) do 30 | {opts, uris, _} = OptionParser.parse(argv, switches: @switches) 31 | version = if opts[:version] do 32 | "-" <> opts[:version] 33 | else 34 | "" 35 | end 36 | 37 | {uri, dst} = if opts[:mini] do 38 | {uri("http://quviq.com/downloads/", uris), 39 | "eqcmini#{version}"} 40 | else 41 | {uri("http://quviq-licencer.com/downloads/", uris), 42 | "eqcR#{:erlang.system_info(:otp_release)}#{version}"} 43 | end 44 | src = Path.join(uri,"#{dst}.zip") 45 | 46 | # Mix.Local.name_for and Mix.Local.path_for hardcode that only :escript and :archive can be used. 47 | # Need to fix this in Elixir. 48 | 49 | Mix.shell.info [:green, "* fetching ", :reset, src] 50 | case Mix.Utils.read_path(src, unsafe_uri: true) do 51 | {:ok, binary} -> 52 | unpack(binary, dst, opts) 53 | 54 | :badpath -> 55 | case File.read(src) do 56 | {:ok, binary} -> 57 | unpack(binary, dst, opts) 58 | _ -> 59 | Mix.raise "Expected #{inspect src} to be a URL or a local file path" 60 | end 61 | 62 | {:local, message} -> 63 | Mix.raise message 64 | 65 | {kind, message} when kind in [:remote, :checksum] -> 66 | Mix.raise """ 67 | #{message} 68 | 69 | Could not fetch QuickCheck at: 70 | #{src} 71 | """ 72 | end 73 | end 74 | 75 | 76 | defp uri(default, []), do: default 77 | defp uri(_, [provided]), do: provided 78 | defp uri(_, uris) do 79 | Mix.raise "Error: Use only one valid location #{inspect uris}" 80 | end 81 | 82 | 83 | defp build_archives(archives, opts) do 84 | for {prefix, a}<-archives do 85 | Mix.shell.info [:green, "* installing archive ", :reset, a] 86 | dst = Path.join(Mix.Local.path_for(:archive), a) 87 | case File.mkdir(dst) do 88 | :ok -> 89 | File.cp_r!(Path.join(prefix, a), Path.join(dst, a)) 90 | {:error, :eexist} -> 91 | if opts[:force] != true do 92 | Mix.raise """ 93 | Could not overwrite existing directory #{dst} 94 | Uninstall older version of QuickCheck first 95 | """ 96 | else 97 | Mix.shell.info [:yellow, "* deleting previously installed version ", :reset] 98 | end 99 | {:error, posix} -> 100 | Mix.raise "Could not create directory #{dst} Error: #{posix}" 101 | end 102 | end 103 | end 104 | 105 | defp unpack(binary, dst, opts) do 106 | dir_dst = Path.join(Mix.Local.path_for(:archive), dst) 107 | File.mkdir_p!(dir_dst) 108 | {:ok, files} = :zip.extract(binary, [cwd: dir_dst]) 109 | Mix.shell.info( [:green, "* stored #{Enum.count(files)} files in ", :reset, dir_dst ]) 110 | eqc_version = 111 | Enum.reduce(files, nil, 112 | fn(f, acc) -> 113 | acc || Regex.named_captures(~r/(?.*)\/(?eqc)-(?[^\/]*)/, f) 114 | end) 115 | if eqc_version do 116 | archives = if opts[:mini] do 117 | [ {eqc_version["prefix"], "eqc-#{eqc_version["version"]}"} ] 118 | else 119 | [ {eqc_version["prefix"], "eqc-#{eqc_version["version"]}"}, 120 | {eqc_version["prefix"], "pulse-#{eqc_version["version"]}"}, 121 | {eqc_version["prefix"], "pulse_otp-#{eqc_version["version"]}"} ] 122 | end 123 | build_archives(archives, opts) 124 | 125 | Mix.shell.info( [:green, "* deleted downloaded ", :reset, dir_dst ]) 126 | File.rm_rf!(dir_dst) 127 | 128 | # touch eqc_ex part that depends on QuickCheck version to force recompilation 129 | File.touch(List.to_string((Elixir.EQC.Mocking.module_info())[:compile][:source])) 130 | else 131 | Mix.raise "Error! Failed to find eqc in downloaded zip" 132 | end 133 | end 134 | 135 | end 136 | -------------------------------------------------------------------------------- /lib/mix/tasks/eqc_registration.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Eqc.Registration do 2 | use Mix.Task 3 | 4 | @shortdoc "Register Quviq QuickCheck full version" 5 | 6 | @moduledoc """ 7 | A Mix task for registration of QuickCheck when registration key has been provided. Only needed for commercial version. 8 | 9 | ONLY register your QuickCheck licence once. After first registration you should install and uninstall versions without using registration again. 10 | 11 | ## Examples 12 | 13 | mix eqc.registration MyRegistrationID 14 | 15 | """ 16 | 17 | @spec run(OptionParser.argv) :: boolean 18 | def run(argv) do 19 | {_opts, args, _} = OptionParser.parse(argv) 20 | case args do 21 | [key|_] -> 22 | if :code.which(:eqc) == :non_existing do 23 | Mix.raise """ 24 | Error: QuickCheck not found 25 | Use mix eqc.install to install QuickCheck 26 | """ 27 | else 28 | :eqc.registration(to_char_list key) 29 | end 30 | [] -> 31 | Mix.raise """ 32 | Error: provide registration key 33 | """ 34 | end 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /lib/mix/tasks/eqc_template.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Eqc.Template do 2 | use Mix.Task 3 | 4 | @shortdoc "Create template for Quviq QuickCheck models" 5 | 6 | @moduledoc """ 7 | A Mix task for creating a template for a specific QuickCheck model. 8 | 9 | ## Options 10 | 11 | * `--model model` - creates a QuickCheck model (default eqc_statem). 12 | * `--dir directory` - puts the created file into directory (default first path in :test_paths project parameter). 13 | * `--api api_description` - uses @spec notiation type declarations to create a template. 14 | 15 | ## Examples 16 | 17 | mix eqc.template --model eqc_statem model_eqc.exs 18 | mix eqc.template --model eqc_component eqc/comp_eqc.exs 19 | mix eqc.template --dir test model_eqc.exs 20 | mix eqc.template --api "Process.register(pid(), name()) :: true" process_eqc.exs 21 | mix eqc.template --api "[ Process.register(pid(), name()) :: true, Process.unregister(name()) :: true ]" process_eqc.exs 22 | 23 | 24 | """ 25 | 26 | @switches [dir: :string, model: :atom, api: :string] 27 | 28 | @spec run(OptionParser.argv) :: boolean 29 | def run(argv) do 30 | {opts, filename, _} = OptionParser.parse(argv, switches: @switches) 31 | 32 | file = Path.basename(filename, ".exs") 33 | name = case file do 34 | "" -> "ModelEqc" 35 | other -> 36 | Macro.camelize(other) 37 | end 38 | 39 | dir = Path.expand(opts[:dir] || 40 | case Path.split(filename) do 41 | [_] -> 42 | case Mix.Project.config[:test_paths] do 43 | nil -> "test" 44 | paths -> hd(paths) 45 | end 46 | _ -> 47 | Path.dirname(filename) 48 | end) 49 | 50 | ## Analyze API if provided 51 | api = extract_api(opts[:api]) 52 | 53 | model = opts[:model] || :eqc_statem 54 | content = template(model, name, api) 55 | 56 | case file do 57 | "" -> 58 | ## on screen 59 | IO.write content 60 | _ -> 61 | dest = Path.join(dir, file <> ".exs") 62 | if File.exists?(dest) do 63 | Mix.raise "Error: file already exists #{dest}" 64 | end 65 | if not File.exists?(dir) do 66 | Mix.raise "Error: no directory #{dir}" 67 | end 68 | Mix.shell.info [:green, "creating #{model} model as ", :reset, dest ] 69 | File.write!(dest, content) 70 | end 71 | 72 | end 73 | 74 | defp extract_api(nil) do 75 | [cmd: {:cmd, quote do [int()] end, quote do int() end}] 76 | end 77 | defp extract_api(string) do 78 | quoted_api = 79 | try do Code.eval_string("quote do\n" <> string <> "\nend") 80 | rescue 81 | _ -> Mix.raise "Error in API description" 82 | end 83 | api_defs = 84 | case quoted_api do 85 | {list, _} when is_list(list) -> list 86 | {single, _} -> [single] 87 | end 88 | 89 | for {:::, _, [{f, _, args}, return]} <-api_defs do 90 | name = 91 | case f do 92 | _ when is_atom(f) -> f 93 | {:., _, [_, atom]} -> atom 94 | end 95 | {name, {f, args, return}} 96 | end 97 | end 98 | 99 | 100 | ## Use strings not Macro.to_string of quoted expression 101 | ## to have the comments in the template file. 102 | defp template(:eqc_statem, name, operators) do 103 | """ 104 | defmodule #{name} do 105 | use ExUnit.Case 106 | use EQC.ExUnit 107 | use EQC.StateM 108 | 109 | ## -- Data generators ------------------------------------------------------- 110 | 111 | 112 | ## -- State generation ------------------------------------------------------ 113 | 114 | def initial_state() do 115 | %{} 116 | end 117 | 118 | #{Enum.map(operators, fn(op) -> operator(:eqc_statem, op) end)} 119 | 120 | weight _state, #{Enum.join(for {op,_}<-operators do "#{op}: 1" end, ", ")} 121 | 122 | @tag :show_states 123 | property "Registery" do 124 | forall cmds <- commands(__MODULE__) do 125 | cleanup() 126 | res = run_commands(cmds) 127 | pretty_commands(cmds, res, 128 | collect len: commands_length(cmds) do 129 | aggregate commands: command_names(cmds) do 130 | res[:result] == :ok 131 | end 132 | end) 133 | end 134 | end 135 | 136 | defp cleanup() do 137 | :ok 138 | end 139 | end 140 | """ 141 | end 142 | defp template(other, _, _) do 143 | Mix.raise "Error: template for #{other} not yet available" 144 | end 145 | 146 | defp operator(_, {op, {f, args, return}}) do 147 | arity = length(args) 148 | argument_vars = for i<-:lists.seq(1,arity) do {String.to_atom("x#{i}"), [], Elixir} end 149 | argument_string = Macro.to_string(argument_vars) 150 | sutf = 151 | if f == op do 152 | ":ok" 153 | else 154 | Macro.to_string({f, [], argument_vars}) 155 | end 156 | ## special case for atoms to be sure parenthesis are used if needed 157 | post = 158 | if is_atom(return) do 159 | """ 160 | def #{op}_post(state, #{argument_string}, res) do 161 | satisfy res == #{return} 162 | end 163 | """ 164 | else 165 | """ 166 | def #{op}_post(state, #{argument_string}, _res) do 167 | true 168 | end 169 | """ 170 | end 171 | 172 | """ 173 | ## operation --- #{op} ------------------------------------------------------- 174 | 175 | ## precondition for including op in the test sequence for present state 176 | def #{op}_pre(_state) do 177 | true 178 | end 179 | 180 | ## argument generators 181 | def #{op}_args(_state) do 182 | #{Macro.to_string(args)} 183 | end 184 | 185 | ## precondition on generated and shrunk arguments 186 | def #{op}_pre(_state, #{argument_string}) do 187 | true 188 | end 189 | 190 | ## implementation of the command under test 191 | def #{op}(#{Enum.join(for i<-:lists.seq(1,arity) do "x#{i}" end, ", ")}) do 192 | #{sutf} 193 | end 194 | 195 | ## the state update after the command has been performed 196 | def #{op}_next(state, _res, #{argument_string}) do 197 | state 198 | end 199 | 200 | ## the postconditions that should hold if command is performed in state state 201 | #{post} 202 | 203 | """ 204 | end 205 | 206 | end 207 | -------------------------------------------------------------------------------- /lib/mix/tasks/eqc_uninstall.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Eqc.Uninstall do 2 | use Mix.Task 3 | 4 | @shortdoc "Uninstall Quviq QuickCheck from local archive" 5 | 6 | @moduledoc """ 7 | A Mix task for deleting QuickCheck as a local archive. 8 | 9 | ## Examples 10 | 11 | mix eqc.uninstall 12 | 13 | """ 14 | 15 | @spec run(OptionParser.argv) :: boolean 16 | def run(_argv) do 17 | case :code.which(:eqc) do 18 | :non_existing -> 19 | Mix.raise """ 20 | Error: no QuickCheck version found 21 | """ 22 | path -> 23 | eqc_version = Regex.named_captures(~r/(?.*)\/(?eqc)-(?[^\/]*)/, List.to_string(path)) 24 | eqc_dir = eqc_version["prefix"] 25 | 26 | delete_dirs = 27 | [ eqc_dir ] ++ 28 | if :code.which(:pulse) == :non_existing do 29 | [] 30 | else 31 | [ Path.join(Path.dirname(eqc_dir), "pulse-" <> eqc_version["version"]), 32 | Path.join(Path.dirname(eqc_dir), "pulse_otp-" <> eqc_version["version"]) ] 33 | end 34 | if Mix.shell.yes?("Are you sure you want to delete#{for d<-delete_dirs, do: "\n "<> d }?") do 35 | for d <- delete_dirs, do: File.rm_rf!(d) 36 | else 37 | Mix.shell.info( [:yellow, "Uninstall aborted", :reset]) 38 | end 39 | 40 | end 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/mix/tasks/mix_eqcci.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.EqcCI do 2 | use Mix.Task 3 | 4 | @shortdoc "Create a project's properties for QuickCheck-CI" 5 | @recursive true 6 | 7 | @moduledoc """ 8 | Create the properties for a project. 9 | 10 | This task mimics `mix test` but compiles the test files to beam instead of in 11 | memory and does not execute the tests or properties. 12 | 13 | Switches are ignored for the moment. 14 | 15 | """ 16 | 17 | @switches [force: :boolean, color: :boolean, cover: :boolean, 18 | max_cases: :integer, include: :keep, 19 | exclude: :keep, only: :keep, compile: :boolean, 20 | timeout: :integer] 21 | 22 | @spec run(OptionParser.argv) :: :ok 23 | def run(args) do 24 | Mix.shell.info "Building properties for QuickCheck-CI" 25 | {opts, files, _} = OptionParser.parse(args, switches: @switches) 26 | 27 | unless System.get_env("MIX_ENV") || Mix.env == :test do 28 | Mix.raise "mix eqcci is running on environment #{Mix.env}. Please set MIX_ENV=test explicitly" 29 | end 30 | 31 | Mix.Task.run "loadpaths", args 32 | 33 | if Keyword.get(opts, :compile, true) do 34 | Mix.Task.run "compile", args 35 | end 36 | 37 | project = Mix.Project.config 38 | 39 | # Start the app and configure exunit with command line options 40 | # before requiring test_helper.exs so that the configuration is 41 | # available in test_helper.exs. Then configure exunit again so 42 | # that command line options override test_helper.exs 43 | Mix.shell.print_app 44 | Mix.Task.run "app.start", args 45 | 46 | # Ensure ex_unit is loaded. 47 | case Application.load(:ex_unit) do 48 | :ok -> :ok 49 | {:error, {:already_loaded, :ex_unit}} -> :ok 50 | end 51 | 52 | opts = ex_unit_opts(opts) 53 | ExUnit.configure(opts) 54 | 55 | test_paths = project[:test_paths] || ["test"] 56 | Enum.each(test_paths, &require_test_helper(&1)) 57 | ExUnit.configure(opts) 58 | 59 | # Finally parse, require and load the files 60 | test_files = parse_files(files, test_paths) 61 | test_pattern = project[:test_pattern] || "*_test.exs" 62 | 63 | test_files = Mix.Utils.extract_files(test_files, test_pattern) 64 | _ = Kernel.ParallelCompiler.files_to_path(test_files, Mix.Project.compile_path(project)) 65 | 66 | end 67 | 68 | @doc false 69 | def ex_unit_opts(opts) do 70 | opts = opts 71 | |> filter_opts(:include) 72 | |> filter_opts(:exclude) 73 | |> filter_only_opts() 74 | 75 | default_opts(opts) ++ 76 | Keyword.take(opts, [:trace, :max_cases, :include, :exclude, :seed, :timeout]) 77 | end 78 | 79 | defp default_opts(opts) do 80 | # Set autorun to false because Mix 81 | # automatically runs the test suite for us. 82 | case Keyword.get(opts, :color) do 83 | nil -> [autorun: false] 84 | enabled? -> [autorun: false, colors: [enabled: enabled?]] 85 | end 86 | end 87 | 88 | defp parse_files([], test_paths) do 89 | test_paths 90 | end 91 | 92 | defp parse_files([single_file], _test_paths) do 93 | # Check if the single file path matches test/path/to_test.exs:123, if it does 94 | # apply `--only line:123` and trim the trailing :123 part. 95 | {single_file, opts} = ExUnit.Filters.parse_path(single_file) 96 | ExUnit.configure(opts) 97 | [single_file] 98 | end 99 | 100 | defp parse_files(files, _test_paths) do 101 | files 102 | end 103 | 104 | defp parse_filters(opts, key) do 105 | if Keyword.has_key?(opts, key) do 106 | ExUnit.Filters.parse(Keyword.get_values(opts, key)) 107 | end 108 | end 109 | 110 | defp filter_opts(opts, key) do 111 | if filters = parse_filters(opts, key) do 112 | Keyword.put(opts, key, filters) 113 | else 114 | opts 115 | end 116 | end 117 | 118 | defp filter_only_opts(opts) do 119 | if filters = parse_filters(opts, :only) do 120 | opts 121 | |> Keyword.put_new(:include, []) 122 | |> Keyword.put_new(:exclude, []) 123 | |> Keyword.update!(:include, &(filters ++ &1)) 124 | |> Keyword.update!(:exclude, &[:test|&1]) 125 | else 126 | opts 127 | end 128 | end 129 | 130 | defp require_test_helper(dir) do 131 | file = Path.join(dir, "test_helper.exs") 132 | 133 | if File.exists?(file) do 134 | Code.require_file file 135 | else 136 | Mix.raise "Cannot run tests because test helper file #{inspect file} does not exist" 137 | end 138 | end 139 | 140 | 141 | end 142 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule EQC.Mixfile do 2 | use Mix.Project 3 | 4 | @version "1.4.2" 5 | 6 | def project do 7 | [ app: :eqc_ex, 8 | version: @version, 9 | elixir: "~> 1.3", 10 | test_pattern: "*_{test,eqc}.exs", 11 | deps: deps(), 12 | docs: docs(), 13 | package: package(), 14 | description: "Wrappers to facilitate using Quviq QuickCheck with Elixir.", 15 | ] 16 | end 17 | 18 | def application do 19 | [] 20 | end 21 | 22 | defp docs do 23 | [ 24 | readme: "README.md", 25 | main: "EQC", 26 | source_ref: "release", #"v#{@version}", 27 | source_url: "https://github.com/Quviq/eqc_ex", 28 | ] 29 | end 30 | 31 | defp package do 32 | [ 33 | contributors: ["Quviq AB"], 34 | maintainers: ["Quviq AB"], 35 | licenses: ["BSD"], 36 | files: ["lib", "mix.exs", "LICENSE", "README.md"], 37 | links: %{ 38 | "quviq.com" => "http://www.quviq.com", 39 | "Github" => "https://github.com/Quviq/eqc_ex" 40 | } 41 | ] 42 | end 43 | 44 | defp deps do 45 | [{:ex_doc, "~> 0.14", only: :dev}] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [{rebar_elixir_plugin, ".*", {git, "https://github.com/yrashk/rebar_elixir_plugin.git"}}]}. 2 | {plugins, [rebar_elixir_compiler, rebar_exunit] }. 3 | 4 | %% Allow compilation of eqc_ex as rebar dependency 5 | {lib_dirs, [ 6 | "../../deps/elixir/lib", 7 | "deps/elixir/lib" 8 | ]}. 9 | -------------------------------------------------------------------------------- /src/eqc_ex.app.src: -------------------------------------------------------------------------------- 1 | {application, eqc_ex, 2 | [{description, "QuickCheck for Elixir"}, 3 | {vsn, "1.3.0"}, 4 | {modules, []}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib]} 7 | ]}. 8 | -------------------------------------------------------------------------------- /test/comp_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule Comp_eqc do 2 | use ExUnit.Case 3 | use EQC.ExUnit 4 | use EQC.Component 5 | require EQC.Mocking 6 | 7 | def initial_state, do: 0 8 | 9 | def add_args(_state), do: [int] 10 | 11 | def add_pre(_state, [i]), do: i > 0 12 | 13 | def add(i), do: :mock.add(i) 14 | 15 | def add_callouts(_state, [i]) do 16 | callout(:mock, :add, [i], :ok) 17 | end 18 | 19 | def add_next(state, _, [i]) do 20 | state+i 21 | end 22 | 23 | def add_post(state, [_], _) do 24 | state >= 0 25 | end 26 | 27 | property "Mock Add" do 28 | EQC.setup_teardown setup do 29 | forall cmds <- commands(__MODULE__) do 30 | res = run_commands(__MODULE__, cmds) 31 | pretty_commands(__MODULE__, cmds, res, 32 | :eqc.aggregate(command_names(cmds), 33 | res[:result] == :ok)) 34 | end 35 | after _ -> teardown 36 | end 37 | end 38 | 39 | def setup() do 40 | :eqc_mocking.start_mocking(api_spec) 41 | end 42 | 43 | def teardown() do 44 | :ok 45 | end 46 | 47 | def api_spec do 48 | EQC.Mocking.api_spec [ 49 | modules: [ 50 | EQC.Mocking.api_module(name: :mock, 51 | functions: 52 | [ EQC.Mocking.api_fun(name: :add, arity: 1) ]) 53 | ] 54 | ] 55 | end 56 | 57 | 58 | end 59 | -------------------------------------------------------------------------------- /test/option_parser_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule OptionParserEqc do 2 | use ExUnit.Case 3 | use EQC.ExUnit 4 | 5 | @switches [b1: :boolean, b2: :boolean, count1: :count, count2: :count, 6 | b3: [:boolean, :keep], b4: [:keep, :boolean], 7 | int1: :integer, int2: :integer, int3: [:integer, :keep], int4: [:keep, :integer], 8 | f1: :float, f2: :float, f3: [:float, :keep], f4: [:keep, :float], 9 | str1: :string, str2: :string, str3: [:string, :keep], str4: [:keep, :string], 10 | str5: :keep 11 | ] 12 | 13 | defp pos do 14 | let n <- nat(), do: n+1 15 | end 16 | 17 | defp option({key, type}) do 18 | case type do 19 | :boolean -> {key, bool()} 20 | :count -> {key, pos()} 21 | [:keep, t] -> option({key, t}) 22 | [t, :keep] -> option({key, t}) 23 | :keep -> option({key, :string}) 24 | :string -> {key, string()} 25 | :float -> {key, real()} 26 | :integer -> {key, int()} 27 | end 28 | end 29 | 30 | defp keep(t) do (t == :keep) || (is_list(t) && :keep in t) end 31 | 32 | defp options do 33 | let {base_opts, keep_opts} <- {sublist(Enum.filter(@switches, fn({k,t}) -> not keep(t) end)), 34 | list(elements(Enum.filter(@switches, fn({k,t}) -> keep(t) end)))} do 35 | let opts<- shuffle(base_opts ++ keep_opts) do 36 | for opt<-opts, do: option(opt) 37 | end 38 | end 39 | end 40 | 41 | defp string do 42 | such_that str <- non_empty(utf8()), do: String.first(str) != "-" 43 | end 44 | 45 | defp args do 46 | list(string()) 47 | end 48 | 49 | property "OptionParser.to_argv as I like to have it" do 50 | forall {opts, arguments} <- {options(), args()} do 51 | argv = to_argv(opts ++ arguments, [switches: @switches]) 52 | when_fail IO.puts "Argv #{inspect argv}" do 53 | {new_opts, new_arguments, errors} = OptionParser.parse(argv, [switches: @switches]) 54 | conjunction(errors: (ensure errors == []), 55 | extra_arguments: (ensure new_arguments -- arguments == []), 56 | missing_arguments: (ensure arguments -- new_arguments == []), 57 | extra_options: (ensure new_opts -- opts == []), 58 | missing_options: (ensure opts -- new_opts == [])) 59 | end 60 | end 61 | end 62 | 63 | def to_argv(elements, options) do 64 | switches = options[:switches] 65 | {opts, args} = 66 | Enum.reduce(elements, {[], []}, 67 | fn({k,v}, {os, as}) -> 68 | if {k, :count} in switches do 69 | { os ++ List.duplicate({k, true}, v), as} 70 | else 71 | {os ++ [{k,v}], as} 72 | end 73 | (arg, {os, as}) -> 74 | {os, as ++ [arg]} 75 | end) 76 | OptionParser.to_argv(opts) ++ args 77 | end 78 | 79 | test "Counts cannot be translated back" do 80 | original = ["--count1", "--count1"] 81 | {opts, [], []} = OptionParser.parse(original, [switches: @switches]) 82 | assert original == OptionParser.to_argv(opts) 83 | end 84 | 85 | test "No real symmetry in operations" do 86 | argv = ["--b1", "--no-b2", "filename"] 87 | {opts, args, []} = OptionParser.parse(argv, [switches: @switches]) 88 | #assert argv == OptionParser.to_argv(opts) ++ args 89 | # assert argv == to_argv(opts ++ args, [switches: @switches]) 90 | assert argv == OptionParser.to_argv(opts ++ args) 91 | end 92 | 93 | 94 | end 95 | -------------------------------------------------------------------------------- /test/props_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleTests do 2 | use ExUnit.Case 3 | use EQC.ExUnit 4 | 5 | #@moduletag numtests: 80 6 | 7 | 8 | @tag erlang_counterexample: false 9 | @tag zero: 0 10 | property "naturals are >= 0", context do 11 | forall n <- nat() do 12 | ensure n > context.zero 13 | end 14 | end 15 | 16 | @check minimum_error: [-1], other_error: [-3] 17 | property "integers are >= 0" do 18 | forall n <- int() do 19 | ensure n >= 0 20 | end 21 | end 22 | 23 | 24 | @tag numtests: 31 25 | property "implies fine" do 26 | forall {n,m} <- {int(), nat()} do 27 | implies m > n, do: 28 | ensure m > n 29 | end 30 | end 31 | 32 | property "measure" do 33 | forall {n,m} <- {int(), nat()} do 34 | measure m: m, n: n, do: 35 | true 36 | end 37 | end 38 | 39 | @tag min_time: 2000 40 | property "min testing_time" do 41 | forall _min <- int() do 42 | true 43 | end 44 | end 45 | 46 | @tag timeout: 5000 47 | @tag min_time: 4000 48 | property "min testing_time too long" do 49 | forall _min_long <- int() do 50 | true 51 | end 52 | end 53 | 54 | def string(), do: utf8() 55 | 56 | property "reverse strings", context do 57 | forall s <- string() do 58 | ensure String.reverse(String.reverse(s)) == s 59 | end 60 | end 61 | 62 | end 63 | -------------------------------------------------------------------------------- /test/string_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule StringTests do 2 | use ExUnit.Case 3 | use EQC.ExUnit 4 | 5 | def string, do: utf8() 6 | 7 | @tag numtests: 0 8 | property "reverse strings" do 9 | forall s <- string() do 10 | ensure String.reverse(String.reverse(s)) == s 11 | end 12 | end 13 | 14 | @tag numtests: 1000 15 | property "replace all leading occurrence of strings" do 16 | forall {ls, n, s, rs} <- {string(), nat(), string(), string()} do 17 | implies not String.starts_with?(s, ls) do 18 | string = String.duplicate(ls, n) <> s 19 | replaced = String.duplicate(rs, n) <> s 20 | collect ls: (ls == ""), string: (s == ""), in: 21 | ensure String.replace_leading(string, ls, rs) == replaced 22 | end 23 | end 24 | end 25 | 26 | 27 | @tag numtests: 100000 28 | property "replace all multiple occurrences of string" do 29 | forall {ls, n, rs} <- {string(), nat(), string()} do 30 | implies ls != "" do 31 | string = String.duplicate(ls, n) 32 | replaced = String.duplicate(rs, n) 33 | ensure String.replace_leading(string, ls, rs) == replaced 34 | end 35 | end 36 | end 37 | 38 | @tag numtests: 1000 39 | @check error1: [{"","","0"}], 40 | error2: [{"","12","0"}] 41 | property "replace one prefix occurrences of string" do 42 | forall {ls, rs, s} <- {string(), string(), string()} do 43 | string = ls <> s 44 | replaced = rs <> s 45 | ensure String.replace_prefix(string, ls, rs) == replaced 46 | end 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------