🎨 elm-format

This commit is contained in:
Richard Feldman
2018-02-22 17:00:38 -05:00
parent 40c66a8a10
commit c8c89adc63
60 changed files with 4253 additions and 124 deletions

View File

@@ -0,0 +1,5 @@
*~
node_modules/
elm-stuff/
docs/
*.html

View File

@@ -0,0 +1,46 @@
sudo: false
cache:
directories:
- test/elm-stuff/build-artifacts
- sysconfcpus
os:
- osx
- linux
env:
matrix:
- ELM_VERSION=0.18.0-beta TARGET_NODE_VERSION=node
- ELM_VERSION=0.18.0-beta TARGET_NODE_VERSION=4.0
before_install:
- if [ ${TRAVIS_OS_NAME} == "osx" ];
then brew update; brew install nvm; mkdir ~/.nvm; export NVM_DIR=~/.nvm; source $(brew --prefix nvm)/nvm.sh;
fi
- echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
- | # epic build time improvement - see https://github.com/elm-lang/elm-compiler/issues/1473#issuecomment-245704142
if [ ! -d sysconfcpus/bin ];
then
git clone https://github.com/obmarg/libsysconfcpus.git;
cd libsysconfcpus;
./configure --prefix=$TRAVIS_BUILD_DIR/sysconfcpus;
make && make install;
cd ..;
fi
install:
- nvm install $TARGET_NODE_VERSION
- nvm use $TARGET_NODE_VERSION
- node --version
- npm --version
- cd tests
- npm install -g elm@$ELM_VERSION elm-test
- mv $(npm config get prefix)/bin/elm-make $(npm config get prefix)/bin/elm-make-old
- printf '%s\n\n' '#!/bin/bash' 'echo "Running elm-make with sysconfcpus -n 2"' '$TRAVIS_BUILD_DIR/sysconfcpus/bin/sysconfcpus -n 2 elm-make-old "$@"' > $(npm config get prefix)/bin/elm-make
- chmod +x $(npm config get prefix)/bin/elm-make
- npm install
- elm package install --yes
script:
- npm test

View File

@@ -0,0 +1,27 @@
Copyright (c) 2016 Richard Feldman and Max Goldstein
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of elm-test nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,107 @@
# elm-test [![Travis build Status](https://travis-ci.org/elm-community/elm-test.svg?branch=master)](http://travis-ci.org/elm-community/elm-test)
Write unit and fuzz tests for your Elm code, in Elm.
## Quick Start
Here are three example tests:
```elm
suite : Test
suite =
describe "The String module"
[ describe "String.reverse" -- Nest as many descriptions as you like.
[ test "has no effect on a palindrome" <|
\() ->
let
palindrome =
"hannah"
in
Expect.equal palindrome (String.reverse palindrome)
-- Expect.equal is designed to be used in pipeline style, like this.
, test "reverses a known string" <|
\() ->
"ABCDEFG"
|> String.reverse
|> Expect.equal "GFEDCBA"
-- fuzz runs the test 100 times with randomly-generated inputs!
, fuzz string "restores the original string if you run it again" <|
\randomlyGeneratedString ->
randomlyGeneratedString
|> String.reverse
|> String.reverse
|> Expect.equal randomlyGeneratedString
]
]
```
This code uses a few common functions:
* [`describe`](http://package.elm-lang.org/packages/elm-community/elm-test/latest/Test#test) to add a description string to a list of tests
* [`test`](http://package.elm-lang.org/packages/elm-community/elm-test/latest/Test#test) to write a unit test
* [`Expect`](http://package.elm-lang.org/packages/elm-community/elm-test/latest/Expect) to determine if a test should pass or fail
* [`fuzz`](http://package.elm-lang.org/packages/elm-community/elm-test/latest/Test#fuzz) to run a function that produces a test several times with randomly-generated inputs
Check out [a large real-world test suite](https://github.com/rtfeldman/elm-css/tree/master/tests) for more.
### Running tests locally
There are several ways you can run tests locally:
* [from your terminal](https://github.com/rtfeldman/node-test-runner) via `npm install -g elm-test`
* [from your browser](https://github.com/rtfeldman/html-test-runner)
Here's how to set up and run your tests using the CLI test runner:
1. Run `npm install -g elm-test` if you haven't already.
2. `cd` into the project's root directory that has your `elm-package.json`.
3. Run `elm-test init`. It will create a `tests` directory inside this one,
with some files in it.
4. Copy all the dependencies from `elm-package.json` into
`tests/elm-package.json`. These dependencies need to stay in sync, so make
sure whenever you change your dependencies in your current
`elm-package.json`, you make the same change to `tests/elm-package.json`.
5. Run `elm-test`.
6. Edit `tests/Tests.elm` to introduce new tests.
### Running tests on CI
Here are some examples of running tests on CI servers:
* [`travis.yml`](https://github.com/rtfeldman/elm-css/blob/6ba8404f53269bc110c2e08ab24c9caf850da515/.travis.yml)
* [`appveyor.yml`](https://github.com/rtfeldman/elm-css/blob/6ba8404f53269bc110c2e08ab24c9caf850da515/appveyor.yml)
## Strategies for effective testing
* [Make impossible states unrepresentable](https://www.youtube.com/watch?v=IcgmSRJHu_8) so that you don't have to test that they can't occur.
* When doing TDD, treat compiler errors as a red test. So feel free to write the test you wish you had even if it means calling functions that don't exist yet!
* How do you know when to stop testing? This is an engineering tradeoff without a perfect answer. If you don't feel confident in the correctness of your code, write more tests. If you feel you are wasting time testing better spent writing your application, stop writing tests for now.
* Prefer fuzz tests to unit tests, when possible. But don't be afraid to supplement them with unit tests for tricky cases and regressions.
* For simple functions, it's okay to copy the implementation to the test; this is a useful regression check. But if the implementation isn't obviously right, try to write tests that don't duplicate the suspect logic. The great thing about fuzz tests is that you don't have to arrive at the exact same value as the code under test, just state something that will be true of that value.
* If you're writing a library that wraps an existing standard or protocol, use examples from the specification or docs as unit tests. Anything in your README should be backed by a unit test (sadly there's no easy way to keep them in sync).
* Not even your test modules can import unexposed functions, so test them only as the exposed interface uses them. Don't expose a function just to test it. Every exposed function should have tests. (If you practice TDD, this happens automatically!)
* `elm-test` is designed to test functions, not effects. To test them, use [elm-testable](http://package.elm-lang.org/packages/avh4/elm-testable/latest). For integration or end-to-end testing, use your favorite PhantomJS or Selenium webdriver, such as Capybara.
## Upgrading
### From 0.17
You will need to delete `elm-stuff` and `tests/elm-stuff`.
If you are using the Node runner, you will need to pull down the new `Main.elm`: `curl -o tests/Main.elm https://raw.githubusercontent.com/rtfeldman/node-test-runner/master/templates/Main.elm`
### From the old elm-test
[`legacy-elm-test`](http://package.elm-lang.org/packages/rtfeldman/legacy-elm-test/latest) provides a
drop-in replacement for the `ElmTest 1.0` API, except implemented in terms of
the current `elm-test`. It also includes support for `elm-check` tests.
This lets you use the latest test runners right now, and upgrade incrementally.
## Releases
| Version | Notes |
| ------- | ----- |
| [**3.1.0**](https://github.com/elm-community/elm-test/tree/3.1.0) | Add Expect.all
| [**3.0.0**](https://github.com/elm-community/elm-test/tree/3.0.0) | Update for Elm 0.18; switch the argument order of `Fuzz.andMap`.
| [**2.1.0**](https://github.com/elm-community/elm-test/tree/2.1.0) | Switch to rose trees for `Fuzz.andThen`, other API additions.
| [**2.0.0**](https://github.com/elm-community/elm-test/tree/2.0.0) | Scratch-rewrite to project-fuzzball
| [**1.0.0**](https://github.com/elm-community/elm-test/tree/1.0.0) | ElmTest initial release

View File

@@ -0,0 +1,22 @@
{
"version": "3.1.0",
"summary": "Unit and Fuzz testing support with Console/Html/String outputs.",
"repository": "https://github.com/elm-community/elm-test.git",
"license": "BSD-3-Clause",
"source-directories": [
"src"
],
"exposed-modules": [
"Test",
"Test.Runner",
"Expect",
"Fuzz"
],
"dependencies": {
"elm-community/lazy-list": "1.0.0 <= v < 2.0.0",
"elm-community/shrink": "2.0.0 <= v < 3.0.0",
"elm-lang/core": "5.0.0 <= v < 6.0.0",
"mgold/elm-random-pcg": "4.0.2 <= v < 5.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

View File

@@ -0,0 +1,649 @@
module Expect
exposing
( Expectation
, pass
, fail
, getFailure
, equal
, notEqual
, atMost
, lessThan
, greaterThan
, atLeast
, true
, false
, equalLists
, equalDicts
, equalSets
, onFail
, all
)
{-| A library to create `Expectation`s, which describe a claim to be tested.
## Quick Reference
* [`equal`](#equal) `(arg2 == arg1)`
* [`notEqual`](#notEqual) `(arg2 /= arg1)`
* [`lessThan`](#lessThan) `(arg2 < arg1)`
* [`atMost`](#atMost) `(arg2 <= arg1)`
* [`greaterThan`](#greaterThan) `(arg2 > arg1)`
* [`atLeast`](#atLeast) `(arg2 >= arg1)`
* [`true`](#true) `(arg == True)`
* [`false`](#false) `(arg == False)`
## Basic Expectations
@docs Expectation, equal, notEqual, all
## Comparisons
@docs lessThan, atMost, greaterThan, atLeast
## Booleans
@docs true, false
## Collections
@docs equalLists, equalDicts, equalSets
## Customizing
@docs pass, fail, onFail, getFailure
-}
import Test.Expectation
import Dict exposing (Dict)
import Set exposing (Set)
import String
{-| The result of a single test run: either a [`pass`](#pass) or a
[`fail`](#fail).
-}
type alias Expectation =
Test.Expectation.Expectation
{-| Passes if the arguments are equal.
Expect.equal 0 (List.length [])
-- Passes because (0 == 0) is True
Failures resemble code written in pipeline style, so you can tell
which argument is which:
-- Fails because the expected value didn't split the space in "Betty Botter"
String.split " " "Betty Botter bought some butter"
|> Expect.equal [ "Betty Botter", "bought", "some", "butter" ]
{-
[ "Betty", "Botter", "bought", "some", "butter" ]
Expect.equal
[ "Betty Botter", "bought", "some", "butter" ]
-}
-}
equal : a -> a -> Expectation
equal =
compareWith "Expect.equal" (==)
{-| Passes if the arguments are not equal.
-- Passes because (11 /= 100) is True
90 + 10
|> Expect.notEqual 11
-- Fails because (100 /= 100) is False
90 + 10
|> Expect.notEqual 100
{-
100
Expect.notEqual
100
-}
-}
notEqual : a -> a -> Expectation
notEqual =
compareWith "Expect.notEqual" (/=)
{-| Passes if the second argument is less than the first.
Expect.lessThan 1 (List.length [])
-- Passes because (0 < 1) is True
Failures resemble code written in pipeline style, so you can tell
which argument is which:
-- Fails because (0 < -1) is False
List.length []
|> Expect.lessThan -1
{-
0
Expect.lessThan
-1
-}
-}
lessThan : comparable -> comparable -> Expectation
lessThan =
compareWith "Expect.lessThan" (<)
{-| Passes if the second argument is less than or equal to the first.
Expect.atMost 1 (List.length [])
-- Passes because (0 <= 1) is True
Failures resemble code written in pipeline style, so you can tell
which argument is which:
-- Fails because (0 <= -3) is False
List.length []
|> Expect.atMost -3
{-
0
Expect.atMost
-3
-}
-}
atMost : comparable -> comparable -> Expectation
atMost =
compareWith "Expect.atMost" (<=)
{-| Passes if the second argument is greater than the first.
Expect.greaterThan -2 List.length []
-- Passes because (0 > -2) is True
Failures resemble code written in pipeline style, so you can tell
which argument is which:
-- Fails because (0 > 1) is False
List.length []
|> Expect.greaterThan 1
{-
0
Expect.greaterThan
1
-}
-}
greaterThan : comparable -> comparable -> Expectation
greaterThan =
compareWith "Expect.greaterThan" (>)
{-| Passes if the second argument is greater than or equal to the first.
Expect.atLeast -2 (List.length [])
-- Passes because (0 >= -2) is True
Failures resemble code written in pipeline style, so you can tell
which argument is which:
-- Fails because (0 >= 3) is False
List.length []
|> Expect.atLeast 3
{-
0
Expect.atLeast
3
-}
-}
atLeast : comparable -> comparable -> Expectation
atLeast =
compareWith "Expect.atLeast" (>=)
{-| Passes if the argument is 'True', and otherwise fails with the given message.
Expect.true "Expected the list to be empty." (List.isEmpty [])
-- Passes because (List.isEmpty []) is True
Failures resemble code written in pipeline style, so you can tell
which argument is which:
-- Fails because List.isEmpty returns False, but we expect True.
List.isEmpty [ 42 ]
|> Expect.true "Expected the list to be empty."
{-
Expected the list to be empty.
-}
-}
true : String -> Bool -> Expectation
true message bool =
if bool then
pass
else
fail message
{-| Passes if the argument is 'False', and otherwise fails with the given message.
Expect.false "Expected the list not to be empty." (List.isEmpty [ 42 ])
-- Passes because (List.isEmpty [ 42 ]) is False
Failures resemble code written in pipeline style, so you can tell
which argument is which:
-- Fails because (List.isEmpty []) is True
List.isEmpty []
|> Expect.false "Expected the list not to be empty."
{-
Expected the list not to be empty.
-}
-}
false : String -> Bool -> Expectation
false message bool =
if bool then
fail message
else
pass
{-| Passes if the arguments are equal lists.
-- Passes
[1, 2, 3]
|> Expect.equalLists [1, 2, 3]
Failures resemble code written in pipeline style, so you can tell
which argument is which, and reports which index the lists first
differed at or which list was longer:
-- Fails
[ 1, 2, 4, 6 ]
|> Expect.equalLists [ 1, 2, 5 ]
{-
[1,2,4,6]
first diff at index index 2: +`4`, -`5`
Expect.equalLists
first diff at index index 2: +`5`, -`4`
[1,2,5]
-}
-}
equalLists : List a -> List a -> Expectation
equalLists expected actual =
if expected == actual then
pass
else
let
result =
List.map2 (,) actual expected
|> List.indexedMap (,)
|> List.filterMap
(\( index, ( a, e ) ) ->
if e == a then
Nothing
else
Just ( index, a, e )
)
|> List.head
|> Maybe.map
(\( index, a, e ) ->
[ toString actual
, "first diff at index index " ++ toString index ++ ": +`" ++ toString a ++ "`, -`" ++ toString e ++ "`"
, ""
, " Expect.equalLists"
, ""
, "first diff at index index " ++ toString index ++ ": +`" ++ toString e ++ "`, -`" ++ toString a ++ "`"
, toString expected
]
|> String.join "\n"
|> fail
)
in
case result of
Just failure ->
failure
Nothing ->
case compare (List.length actual) (List.length expected) of
GT ->
reportFailure "Expect.equalLists was longer than" (toString expected) (toString actual)
|> fail
LT ->
reportFailure "Expect.equalLists was shorter than" (toString expected) (toString actual)
|> fail
_ ->
pass
{-| Passes if the arguments are equal dicts.
-- Passes
(Dict.fromList [ ( 1, "one" ), ( 2, "two" ) ])
|> Expect.equalDicts (Dict.fromList [ ( 1, "one" ), ( 2, "two" ) ])
Failures resemble code written in pipeline style, so you can tell
which argument is which, and reports which keys were missing from
or added to each dict:
-- Fails
(Dict.fromList [ ( 1, "one" ), ( 2, "too" ) ])
|> Expect.equalDicts (Dict.fromList [ ( 1, "one" ), ( 2, "two" ), ( 3, "three" ) ])
{-
Dict.fromList [(1,"one"),(2,"too")]
diff: -[ (2,"two"), (3,"three") ] +[ (2,"too") ]
Expect.equalDicts
diff: +[ (2,"two"), (3,"three") ] -[ (2,"too") ]
Dict.fromList [(1,"one"),(2,"two"),(3,"three")]
-}
-}
equalDicts : Dict comparable a -> Dict comparable a -> Expectation
equalDicts expected actual =
if Dict.toList expected == Dict.toList actual then
pass
else
let
differ dict k v diffs =
if Dict.get k dict == Just v then
diffs
else
( k, v ) :: diffs
missingKeys =
Dict.foldr (differ actual) [] expected
extraKeys =
Dict.foldr (differ expected) [] actual
in
fail (reportCollectionFailure "Expect.equalDicts" expected actual missingKeys extraKeys)
{-| Passes if the arguments are equal sets.
-- Passes
(Set.fromList [1, 2])
|> Expect.equalSets (Set.fromList [1, 2])
Failures resemble code written in pipeline style, so you can tell
which argument is which, and reports which keys were missing from
or added to each set:
-- Fails
(Set.fromList [ 1, 2, 4, 6 ])
|> Expect.equalSets (Set.fromList [ 1, 2, 5 ])
{-
Set.fromList [1,2,4,6]
diff: -[ 5 ] +[ 4, 6 ]
Expect.equalSets
diff: +[ 5 ] -[ 4, 6 ]
Set.fromList [1,2,5]
-}
-}
equalSets : Set comparable -> Set comparable -> Expectation
equalSets expected actual =
if Set.toList expected == Set.toList actual then
pass
else
let
missingKeys =
Set.diff expected actual
|> Set.toList
extraKeys =
Set.diff actual expected
|> Set.toList
in
fail (reportCollectionFailure "Expect.equalSets" expected actual missingKeys extraKeys)
{-| Always passes.
import Json.Decode exposing (decodeString, int)
import Test exposing (test)
import Expect
test "Json.Decode.int can decode the number 42." <|
\() ->
case decodeString int "42" of
Ok _ ->
Expect.pass
Err err ->
Expect.fail err
-}
pass : Expectation
pass =
Test.Expectation.Pass
{-| Fails with the given message.
import Json.Decode exposing (decodeString, int)
import Test exposing (test)
import Expect
test "Json.Decode.int can decode the number 42." <|
\() ->
case decodeString int "42" of
Ok _ ->
Expect.pass
Err err ->
Expect.fail err
-}
fail : String -> Expectation
fail =
Test.Expectation.Fail ""
{-| Return `Nothing` if the given [`Expectation`](#Expectation) is a [`pass`](#pass).
If it is a [`fail`](#fail), return a record containing the failure message,
along with the given inputs if it was a fuzz test. (If no inputs were involved,
the record's `given` field will be `""`).
For example, if a fuzz test generates random integers, this might return
`{ message = "it was supposed to be positive", given = "-1" }`
getFailure (Expect.fail "this failed")
-- Just { message = "this failed", given = "" }
getFailure (Expect.pass)
-- Nothing
-}
getFailure : Expectation -> Maybe { given : String, message : String }
getFailure expectation =
case expectation of
Test.Expectation.Pass ->
Nothing
Test.Expectation.Fail given message ->
Just { given = given, message = message }
{-| If the given expectation fails, replace its failure message with a custom one.
"something"
|> Expect.equal "something else"
|> Expect.onFail "thought those two strings would be the same"
-}
onFail : String -> Expectation -> Expectation
onFail str expectation =
case expectation of
Test.Expectation.Pass ->
expectation
Test.Expectation.Fail given _ ->
Test.Expectation.Fail given str
reportFailure : String -> String -> String -> String
reportFailure comparison expected actual =
[ actual
, ""
, " " ++ comparison
, ""
, expected
]
|> String.join "\n"
reportCollectionFailure : String -> a -> b -> List c -> List d -> String
reportCollectionFailure comparison expected actual missingKeys extraKeys =
[ toString actual
, "diff:" ++ formatDiffs Missing missingKeys ++ formatDiffs Extra extraKeys
, ""
, " " ++ comparison
, ""
, "diff:" ++ formatDiffs Extra missingKeys ++ formatDiffs Missing extraKeys
, toString expected
]
|> String.join "\n"
type Diff
= Extra
| Missing
formatDiffs : Diff -> List a -> String
formatDiffs diffType diffs =
if List.isEmpty diffs then
""
else
let
modifier =
case diffType of
Extra ->
"+"
Missing ->
"-"
in
diffs
|> List.map toString
|> String.join ", "
|> (\d -> " " ++ modifier ++ "[ " ++ d ++ " ]")
compareWith : String -> (a -> b -> Bool) -> b -> a -> Expectation
compareWith label compare expected actual =
if compare actual expected then
pass
else
fail (reportFailure label (toString expected) (toString actual))
{-| Passes if each of the given functions passes when applied to the subject.
**NOTE:** Passing an empty list is assumed to be a mistake, so `Expect.all []`
will always return a failed expectation no matter what else it is passed.
Expect.all
[ Expect.greaterThan -2
, Expect.lessThan 5
]
(List.length [])
-- Passes because (0 > -2) is True and (0 < 5) is also True
Failures resemble code written in pipeline style, so you can tell
which argument is which:
-- Fails because (0 > -10) is False
List.length []
|> Expect.all
[ Expect.greaterThan -2
, Expect.lessThan -10
, Expect.equal 0
]
{-
0
Expect.lessThan
-10
-}
-}
all : List (subject -> Expectation) -> subject -> Expectation
all list query =
if List.isEmpty list then
fail "Expect.all received an empty list. I assume this was due to a mistake somewhere, so I'm failing this test!"
else
allHelp list query
allHelp : List (subject -> Expectation) -> subject -> Expectation
allHelp list query =
case list of
[] ->
pass
check :: rest ->
case check query of
Test.Expectation.Pass ->
allHelp rest query
outcome ->
outcome

View File

@@ -0,0 +1,802 @@
module Fuzz exposing (Fuzzer, custom, constant, unit, bool, order, char, float, floatRange, int, tuple, tuple3, tuple4, tuple5, result, string, percentage, map, map2, map3, map4, map5, andMap, andThen, maybe, intRange, list, array, frequency, frequencyOrCrash)
{-| This is a library of *fuzzers* you can use to supply values to your fuzz
tests. You can typically pick out which ones you need according to their types.
A `Fuzzer a` knows how to create values of type `a` in two different ways. It
can create them randomly, so that your test's expectations are run against many
values. Fuzzers will often generate edge cases likely to find bugs. If the
fuzzer can make your test fail, it also knows how to "shrink" that failing input
into more minimal examples, some of which might also cause the tests to fail. In
this way, fuzzers can usually find the smallest or simplest input that
reproduces a bug.
## Common Fuzzers
@docs bool, int, intRange, float, floatRange, percentage, string, maybe, result, list, array
## Working with Fuzzers
@docs Fuzzer, constant, map, map2, map3,map4, map5, andMap, andThen, frequency, frequencyOrCrash
## Tuple Fuzzers
Instead of using a tuple, consider using `fuzzN`.
@docs tuple, tuple3, tuple4, tuple5
## Uncommon Fuzzers
@docs custom, char, unit, order
-}
import Array exposing (Array)
import Char
import Util exposing (..)
import Lazy.List exposing (LazyList)
import Shrink exposing (Shrinker)
import RoseTree exposing (RoseTree(..))
import Random.Pcg as Random exposing (Generator)
import Fuzz.Internal as Internal exposing (Fuzz(..))
{-| The representation of fuzzers is opaque. Conceptually, a `Fuzzer a`
consists of a way to randomly generate values of type `a`, and a way to shrink
those values.
-}
type alias Fuzzer a =
Internal.Fuzzer a
{-| Build a custom `Fuzzer a` by providing a `Generator a` and a `Shrinker a`.
Generators are defined in [`mgold/elm-random-pcg`](http://package.elm-lang.org/packages/mgold/elm-random-pcg/latest),
which is not core's Random module but has a compatible interface. Shrinkers are
defined in [`elm-community/shrink`](http://package.elm-lang.org/packages/elm-community/shrink/latest/).
Here is an example for a record:
import Random.Pcg as Random
import Shrink
type alias Position =
{ x : Int, y : Int }
position : Fuzzer Position
position =
Fuzz.custom
(Random.map2 Position (Random.int -100 100) (Random.int -100 100))
(\{ x, y } -> Shrink.map Position (Shrink.int x) |> Shrink.andMap (Shrink.int y))
Here is an example for a custom union type, assuming there is already a `genName : Generator String` defined:
type Question
= Name String
| Age Int
question =
let
generator =
Random.bool |> Random.andThen (\b ->
if b then
Random.map Name genName
else
Random.map Age (Random.int 0 120)
)
shrinker question =
case question of
Name n ->
Shrink.string n |> Shrink.map Name
Age i ->
Shrink.int i |> Shrink.map Age
in
Fuzz.custom generator shrinker
It is not possible to extract the generator and shrinker from an existing fuzzer.
-}
custom : Generator a -> Shrinker a -> Fuzzer a
custom generator shrinker =
let
shrinkTree a =
Rose a (Lazy.List.map shrinkTree (shrinker a))
in
Internal.Fuzzer
(\noShrink ->
if noShrink then
Gen generator
else
Shrink <| Random.map shrinkTree generator
)
{-| A fuzzer for the unit value. Unit is a type with only one value, commonly
used as a placeholder.
-}
unit : Fuzzer ()
unit =
Internal.Fuzzer
(\noShrink ->
if noShrink then
Gen <| Random.constant ()
else
Shrink <| Random.constant (RoseTree.singleton ())
)
{-| A fuzzer for bool values.
-}
bool : Fuzzer Bool
bool =
custom Random.bool Shrink.bool
{-| A fuzzer for order values.
-}
order : Fuzzer Order
order =
let
intToOrder i =
if i == 0 then
LT
else if i == 1 then
EQ
else
GT
in
custom (Random.map intToOrder (Random.int 0 2)) Shrink.order
{-| A fuzzer for int values. It will never produce `NaN`, `Infinity`, or `-Infinity`.
It's possible for this fuzzer to generate any 32-bit integer, but it favors
numbers between -50 and 50 and especially zero.
-}
int : Fuzzer Int
int =
let
generator =
Random.frequency
[ ( 3, Random.int -50 50 )
, ( 0.2, Random.constant 0 )
, ( 1, Random.int 0 (Random.maxInt - Random.minInt) )
, ( 1, Random.int (Random.minInt - Random.maxInt) 0 )
]
in
custom generator Shrink.int
{-| A fuzzer for int values within between a given minimum and maximum value,
inclusive. Shrunken values will also be within the range.
Remember that [Random.maxInt](http://package.elm-lang.org/packages/elm-lang/core/latest/Random#maxInt)
is the maximum possible int value, so you can do `intRange x Random.maxInt` to get all
the ints x or bigger.
-}
intRange : Int -> Int -> Fuzzer Int
intRange lo hi =
custom
(Random.frequency
[ ( 8, Random.int lo hi )
, ( 1, Random.constant lo )
, ( 1, Random.constant hi )
]
)
(Shrink.keepIf (\i -> i >= lo && i <= hi) Shrink.int)
{-| A fuzzer for float values. It will never produce `NaN`, `Infinity`, or `-Infinity`.
It's possible for this fuzzer to generate any other floating-point value, but it
favors numbers between -50 and 50, numbers between -1 and 1, and especially zero.
-}
float : Fuzzer Float
float =
let
generator =
Random.frequency
[ ( 3, Random.float -50 50 )
, ( 0.5, Random.constant 0 )
, ( 1, Random.float -1 1 )
, ( 1, Random.float 0 (toFloat <| Random.maxInt - Random.minInt) )
, ( 1, Random.float (toFloat <| Random.minInt - Random.maxInt) 0 )
]
in
custom generator Shrink.float
{-| A fuzzer for float values within between a given minimum and maximum
value, inclusive. Shrunken values will also be within the range.
-}
floatRange : Float -> Float -> Fuzzer Float
floatRange lo hi =
custom
(Random.frequency
[ ( 8, Random.float lo hi )
, ( 1, Random.constant lo )
, ( 1, Random.constant hi )
]
)
(Shrink.keepIf (\i -> i >= lo && i <= hi) Shrink.float)
{-| A fuzzer for percentage values. Generates random floats between `0.0` and
`1.0`. It will test zero and one about 10% of the time each.
-}
percentage : Fuzzer Float
percentage =
let
generator =
Random.frequency
[ ( 8, Random.float 0 1 )
, ( 1, Random.constant 0 )
, ( 1, Random.constant 1 )
]
in
custom generator Shrink.float
{-| A fuzzer for char values. Generates random ascii chars disregarding the control
characters.
-}
char : Fuzzer Char
char =
custom charGenerator Shrink.character
charGenerator : Generator Char
charGenerator =
(Random.map Char.fromCode (Random.int 32 126))
{-| Generates random printable ASCII strings of up to 1000 characters.
Shorter strings are more common, especially the empty string.
-}
string : Fuzzer String
string =
let
generator : Generator String
generator =
Random.frequency
[ ( 3, Random.int 1 10 )
, ( 0.2, Random.constant 0 )
, ( 1, Random.int 11 50 )
, ( 1, Random.int 50 1000 )
]
|> Random.andThen (lengthString charGenerator)
in
custom generator Shrink.string
{-| Given a fuzzer of a type, create a fuzzer of a maybe for that type.
-}
maybe : Fuzzer a -> Fuzzer (Maybe a)
maybe (Internal.Fuzzer baseFuzzer) =
Internal.Fuzzer <|
\noShrink ->
case baseFuzzer noShrink of
Gen gen ->
Gen <|
Random.map2
(\useNothing val ->
if useNothing then
Nothing
else
Just val
)
(Random.oneIn 4)
gen
Shrink genTree ->
Shrink <|
Random.map2
(\useNothing tree ->
if useNothing then
RoseTree.singleton Nothing
else
RoseTree.map Just tree |> RoseTree.addChild (RoseTree.singleton Nothing)
)
(Random.oneIn 4)
genTree
{-| Given fuzzers for an error type and a success type, create a fuzzer for
a result.
-}
result : Fuzzer error -> Fuzzer value -> Fuzzer (Result error value)
result (Internal.Fuzzer baseFuzzerError) (Internal.Fuzzer baseFuzzerValue) =
Internal.Fuzzer <|
\noShrink ->
case ( baseFuzzerError noShrink, baseFuzzerValue noShrink ) of
( Gen genErr, Gen genVal ) ->
Gen <|
Random.map3
(\useError err val ->
if useError then
Err err
else
Ok val
)
(Random.oneIn 4)
genErr
genVal
( Shrink genTreeErr, Shrink genTreeVal ) ->
Shrink <|
Random.map3
(\useError errorTree valueTree ->
if useError then
RoseTree.map Err errorTree
else
RoseTree.map Ok valueTree
)
(Random.oneIn 4)
genTreeErr
genTreeVal
err ->
Debug.crash "This shouldn't happen: Fuzz.result" err
{-| Given a fuzzer of a type, create a fuzzer of a list of that type.
Generates random lists of varying length, favoring shorter lists.
-}
list : Fuzzer a -> Fuzzer (List a)
list (Internal.Fuzzer baseFuzzer) =
let
genLength =
Random.frequency
[ ( 1, Random.constant 0 )
, ( 1, Random.constant 1 )
, ( 3, Random.int 2 10 )
, ( 2, Random.int 10 100 )
, ( 0.5, Random.int 100 400 )
]
in
Internal.Fuzzer
(\noShrink ->
case baseFuzzer noShrink of
Gen genVal ->
genLength
|> Random.andThen (\i -> (Random.list i genVal))
|> Gen
Shrink genTree ->
genLength
|> Random.andThen (\i -> (Random.list i genTree))
|> Random.map listShrinkHelp
|> Shrink
)
listShrinkHelp : List (RoseTree a) -> RoseTree (List a)
listShrinkHelp listOfTrees =
{- Shrinking a list of RoseTrees
We need to do two things. First, shrink individual values. Second, shorten the list.
To shrink individual values, we create every list copy of the input list where any
one value is replaced by a shrunken form.
To shorten the length of the list, slide windows of various lengths over it.
In all cases, recurse! The goal is to make a little forward progress and then recurse.
-}
let
n =
List.length listOfTrees
root =
List.map RoseTree.root listOfTrees
shrinkOne prefix list =
case list of
[] ->
Lazy.List.empty
(Rose x shrunkenXs) :: more ->
Lazy.List.map (\childTree -> prefix ++ (childTree :: more) |> listShrinkHelp) shrunkenXs
shrunkenVals =
Lazy.List.numbers
|> Lazy.List.map (\i -> i - 1)
|> Lazy.List.take n
|> Lazy.List.andThen
(\i -> shrinkOne (List.take i listOfTrees) (List.drop i listOfTrees))
shortened =
(if n > 6 then
Lazy.List.iterate (\n -> n // 2) n
|> Lazy.List.takeWhile (\x -> x > 0)
else
Lazy.List.fromList (List.range 1 n)
)
|> Lazy.List.andThen (\len -> shorter len listOfTrees False)
|> Lazy.List.map listShrinkHelp
shorter windowSize aList recursing =
-- Tricky: take the whole list if we've recursed down here, but don't let a list shrink to itself
if windowSize > List.length aList || (windowSize == List.length aList && not recursing) then
Lazy.List.empty
else
case aList of
[] ->
Lazy.List.empty
head :: tail ->
Lazy.List.cons (List.take windowSize aList) (shorter windowSize tail True)
in
Lazy.List.append shortened shrunkenVals
|> Lazy.List.cons (RoseTree.singleton [])
|> Rose root
{-| Given a fuzzer of a type, create a fuzzer of an array of that type.
Generates random arrays of varying length, favoring shorter arrays.
-}
array : Fuzzer a -> Fuzzer (Array a)
array fuzzer =
map Array.fromList (list fuzzer)
{-| Turn a tuple of fuzzers into a fuzzer of tuples.
-}
tuple : ( Fuzzer a, Fuzzer b ) -> Fuzzer ( a, b )
tuple ( Internal.Fuzzer baseFuzzerA, Internal.Fuzzer baseFuzzerB ) =
Internal.Fuzzer
(\noShrink ->
case ( baseFuzzerA noShrink, baseFuzzerB noShrink ) of
( Gen genA, Gen genB ) ->
Gen <| Random.map2 (,) genA genB
( Shrink genTreeA, Shrink genTreeB ) ->
Shrink <| Random.map2 tupleShrinkHelp genTreeA genTreeB
err ->
Debug.crash "This shouldn't happen: Fuzz.tuple" err
)
tupleShrinkHelp : RoseTree a -> RoseTree b -> RoseTree ( a, b )
tupleShrinkHelp ((Rose root1 children1) as rose1) ((Rose root2 children2) as rose2) =
{- Shrinking a tuple of RoseTrees
Recurse on all tuples created by substituting one element for any of its shrunken values.
A weakness of this algorithm is that it expects that values can be shrunken independently.
That is, to shrink from (a,b) to (a',b'), we must go through (a',b) or (a,b').
"No pairs sum to zero" is a pathological predicate that cannot be shrunken this way.
-}
let
root =
( root1, root2 )
shrink1 =
Lazy.List.map (\subtree -> tupleShrinkHelp subtree rose2) children1
shrink2 =
Lazy.List.map (\subtree -> tupleShrinkHelp rose1 subtree) children2
in
shrink2
|> Lazy.List.append shrink1
|> Rose root
{-| Turn a 3-tuple of fuzzers into a fuzzer of 3-tuples.
-}
tuple3 : ( Fuzzer a, Fuzzer b, Fuzzer c ) -> Fuzzer ( a, b, c )
tuple3 ( Internal.Fuzzer baseFuzzerA, Internal.Fuzzer baseFuzzerB, Internal.Fuzzer baseFuzzerC ) =
Internal.Fuzzer
(\noShrink ->
case ( baseFuzzerA noShrink, baseFuzzerB noShrink, baseFuzzerC noShrink ) of
( Gen genA, Gen genB, Gen genC ) ->
Gen <| Random.map3 (,,) genA genB genC
( Shrink genTreeA, Shrink genTreeB, Shrink genTreeC ) ->
Shrink <| Random.map3 tupleShrinkHelp3 genTreeA genTreeB genTreeC
err ->
Debug.crash "This shouldn't happen: Fuzz.tuple3" err
)
tupleShrinkHelp3 : RoseTree a -> RoseTree b -> RoseTree c -> RoseTree ( a, b, c )
tupleShrinkHelp3 ((Rose root1 children1) as rose1) ((Rose root2 children2) as rose2) ((Rose root3 children3) as rose3) =
let
root =
( root1, root2, root3 )
shrink1 =
Lazy.List.map (\subtree -> tupleShrinkHelp3 subtree rose2 rose3) children1
shrink2 =
Lazy.List.map (\subtree -> tupleShrinkHelp3 rose1 subtree rose3) children2
shrink3 =
Lazy.List.map (\subtree -> tupleShrinkHelp3 rose1 rose2 subtree) children3
in
shrink3
|> Lazy.List.append shrink2
|> Lazy.List.append shrink1
|> Rose root
{-| Turn a 4-tuple of fuzzers into a fuzzer of 4-tuples.
-}
tuple4 : ( Fuzzer a, Fuzzer b, Fuzzer c, Fuzzer d ) -> Fuzzer ( a, b, c, d )
tuple4 ( Internal.Fuzzer baseFuzzerA, Internal.Fuzzer baseFuzzerB, Internal.Fuzzer baseFuzzerC, Internal.Fuzzer baseFuzzerD ) =
Internal.Fuzzer
(\noShrink ->
case ( baseFuzzerA noShrink, baseFuzzerB noShrink, baseFuzzerC noShrink, baseFuzzerD noShrink ) of
( Gen genA, Gen genB, Gen genC, Gen genD ) ->
Gen <| Random.map4 (,,,) genA genB genC genD
( Shrink genTreeA, Shrink genTreeB, Shrink genTreeC, Shrink genTreeD ) ->
Shrink <| Random.map4 tupleShrinkHelp4 genTreeA genTreeB genTreeC genTreeD
err ->
Debug.crash "This shouldn't happen: Fuzz.tuple4" err
)
tupleShrinkHelp4 : RoseTree a -> RoseTree b -> RoseTree c -> RoseTree d -> RoseTree ( a, b, c, d )
tupleShrinkHelp4 rose1 rose2 rose3 rose4 =
let
root =
( RoseTree.root rose1, RoseTree.root rose2, RoseTree.root rose3, RoseTree.root rose4 )
shrink1 =
Lazy.List.map (\subtree -> tupleShrinkHelp4 subtree rose2 rose3 rose4) (RoseTree.children rose1)
shrink2 =
Lazy.List.map (\subtree -> tupleShrinkHelp4 rose1 subtree rose3 rose4) (RoseTree.children rose2)
shrink3 =
Lazy.List.map (\subtree -> tupleShrinkHelp4 rose1 rose2 subtree rose4) (RoseTree.children rose3)
shrink4 =
Lazy.List.map (\subtree -> tupleShrinkHelp4 rose1 rose2 rose3 subtree) (RoseTree.children rose4)
in
shrink4
|> Lazy.List.append shrink3
|> Lazy.List.append shrink2
|> Lazy.List.append shrink1
|> Rose root
{-| Turn a 5-tuple of fuzzers into a fuzzer of 5-tuples.
-}
tuple5 : ( Fuzzer a, Fuzzer b, Fuzzer c, Fuzzer d, Fuzzer e ) -> Fuzzer ( a, b, c, d, e )
tuple5 ( Internal.Fuzzer baseFuzzerA, Internal.Fuzzer baseFuzzerB, Internal.Fuzzer baseFuzzerC, Internal.Fuzzer baseFuzzerD, Internal.Fuzzer baseFuzzerE ) =
Internal.Fuzzer
(\noShrink ->
case ( baseFuzzerA noShrink, baseFuzzerB noShrink, baseFuzzerC noShrink, baseFuzzerD noShrink, baseFuzzerE noShrink ) of
( Gen genA, Gen genB, Gen genC, Gen genD, Gen genE ) ->
Gen <| Random.map5 (,,,,) genA genB genC genD genE
( Shrink genTreeA, Shrink genTreeB, Shrink genTreeC, Shrink genTreeD, Shrink genTreeE ) ->
Shrink <| Random.map5 tupleShrinkHelp5 genTreeA genTreeB genTreeC genTreeD genTreeE
err ->
Debug.crash "This shouldn't happen: Fuzz.tuple5" err
)
tupleShrinkHelp5 : RoseTree a -> RoseTree b -> RoseTree c -> RoseTree d -> RoseTree e -> RoseTree ( a, b, c, d, e )
tupleShrinkHelp5 rose1 rose2 rose3 rose4 rose5 =
let
root =
( RoseTree.root rose1, RoseTree.root rose2, RoseTree.root rose3, RoseTree.root rose4, RoseTree.root rose5 )
shrink1 =
Lazy.List.map (\subtree -> tupleShrinkHelp5 subtree rose2 rose3 rose4 rose5) (RoseTree.children rose1)
shrink2 =
Lazy.List.map (\subtree -> tupleShrinkHelp5 rose1 subtree rose3 rose4 rose5) (RoseTree.children rose2)
shrink3 =
Lazy.List.map (\subtree -> tupleShrinkHelp5 rose1 rose2 subtree rose4 rose5) (RoseTree.children rose3)
shrink4 =
Lazy.List.map (\subtree -> tupleShrinkHelp5 rose1 rose2 rose3 subtree rose5) (RoseTree.children rose4)
shrink5 =
Lazy.List.map (\subtree -> tupleShrinkHelp5 rose1 rose2 rose3 rose4 subtree) (RoseTree.children rose5)
in
shrink5
|> Lazy.List.append shrink4
|> Lazy.List.append shrink3
|> Lazy.List.append shrink2
|> Lazy.List.append shrink1
|> Rose root
{-| Create a fuzzer that only and always returns the value provided, and performs no shrinking. This is hardly random,
and so this function is best used as a helper when creating more complicated fuzzers.
-}
constant : a -> Fuzzer a
constant x =
Internal.Fuzzer
(\noShrink ->
if noShrink then
Gen (Random.constant x)
else
Shrink (Random.constant (RoseTree.singleton x))
)
{-| Map a function over a fuzzer. This applies to both the generated and the shruken values.
-}
map : (a -> b) -> Fuzzer a -> Fuzzer b
map transform (Internal.Fuzzer baseFuzzer) =
Internal.Fuzzer
(\noShrink ->
case baseFuzzer noShrink of
Gen genVal ->
Gen <| Random.map transform genVal
Shrink genTree ->
Shrink <| Random.map (RoseTree.map transform) genTree
)
{-| Map over two fuzzers.
-}
map2 : (a -> b -> c) -> Fuzzer a -> Fuzzer b -> Fuzzer c
map2 transform fuzzA fuzzB =
map (\( a, b ) -> transform a b) (tuple ( fuzzA, fuzzB ))
{-| Map over three fuzzers.
-}
map3 : (a -> b -> c -> d) -> Fuzzer a -> Fuzzer b -> Fuzzer c -> Fuzzer d
map3 transform fuzzA fuzzB fuzzC =
map (\( a, b, c ) -> transform a b c) (tuple3 ( fuzzA, fuzzB, fuzzC ))
{-| Map over four fuzzers.
-}
map4 : (a -> b -> c -> d -> e) -> Fuzzer a -> Fuzzer b -> Fuzzer c -> Fuzzer d -> Fuzzer e
map4 transform fuzzA fuzzB fuzzC fuzzD =
map (\( a, b, c, d ) -> transform a b c d) (tuple4 ( fuzzA, fuzzB, fuzzC, fuzzD ))
{-| Map over five fuzzers.
-}
map5 : (a -> b -> c -> d -> e -> f) -> Fuzzer a -> Fuzzer b -> Fuzzer c -> Fuzzer d -> Fuzzer e -> Fuzzer f
map5 transform fuzzA fuzzB fuzzC fuzzD fuzzE =
map (\( a, b, c, d, e ) -> transform a b c d e) (tuple5 ( fuzzA, fuzzB, fuzzC, fuzzD, fuzzE ))
{-| Map over many fuzzers. This can act as mapN for N > 5.
The argument order is meant to accomodate chaining:
map f aFuzzer
|> andMap anotherFuzzer
|> andMap aThirdFuzzer
Note that shrinking may be better using mapN.
-}
andMap : Fuzzer a -> Fuzzer (a -> b) -> Fuzzer b
andMap =
map2 (|>)
{-| Create a fuzzer based on the result of another fuzzer.
-}
andThen : (a -> Fuzzer b) -> Fuzzer a -> Fuzzer b
andThen transform (Internal.Fuzzer baseFuzzer) =
Internal.Fuzzer
(\noShrink ->
case baseFuzzer noShrink of
Gen genVal ->
Gen <| Random.andThen (transform >> Internal.unpackGenVal) genVal
Shrink genTree ->
Shrink <| andThenRoseTrees transform genTree
)
andThenRoseTrees : (a -> Fuzzer b) -> Generator (RoseTree a) -> Generator (RoseTree b)
andThenRoseTrees transform genTree =
genTree
|> Random.andThen
(\(Rose root branches) ->
let
genOtherChildren : Generator (LazyList (RoseTree b))
genOtherChildren =
branches
|> Lazy.List.map (\rt -> RoseTree.map (transform >> Internal.unpackGenTree) rt |> unwindRoseTree)
|> unwindLazyList
|> Random.map (Lazy.List.map RoseTree.flatten)
in
Random.map2
(\(Rose trueRoot rootsChildren) otherChildren ->
Rose trueRoot (Lazy.List.append rootsChildren otherChildren)
)
(Internal.unpackGenTree (transform root))
genOtherChildren
)
unwindRoseTree : RoseTree (Generator a) -> Generator (RoseTree a)
unwindRoseTree (Rose genRoot lazyListOfRoseTreesOfGenerators) =
case Lazy.List.headAndTail lazyListOfRoseTreesOfGenerators of
Nothing ->
Random.map RoseTree.singleton genRoot
Just ( Rose gen children, moreList ) ->
Random.map4 (\a b c d -> Rose a (Lazy.List.cons (Rose b c) d))
genRoot
gen
(Lazy.List.map unwindRoseTree children |> unwindLazyList)
(Lazy.List.map unwindRoseTree moreList |> unwindLazyList)
unwindLazyList : LazyList (Generator a) -> Generator (LazyList a)
unwindLazyList lazyListOfGenerators =
case Lazy.List.headAndTail lazyListOfGenerators of
Nothing ->
Random.constant Lazy.List.empty
Just ( head, tail ) ->
Random.map2 Lazy.List.cons head (unwindLazyList tail)
{-| Create a new `Fuzzer` by providing a list of probabilistic weights to use
with other fuzzers.
For example, to create a `Fuzzer` that has a 1/4 chance of generating an int
between -1 and -100, and a 3/4 chance of generating one between 1 and 100,
you could do this:
Fuzz.frequency
[ ( 1, Fuzz.intRange -100 -1 )
, ( 3, Fuzz.intRange 1 100 )
]
This returns a `Result` because it can fail in a few ways:
* If you provide an empy list of frequencies
* If any of the weights are less than 0
* If the weights sum to 0
Any of these will lead to a result of `Err`, with a `String` explaining what
went wrong.
-}
frequency : List ( Float, Fuzzer a ) -> Result String (Fuzzer a)
frequency list =
if List.isEmpty list then
Err "You must provide at least one frequency pair."
else if List.any (\( weight, _ ) -> weight < 0) list then
Err "No frequency weights can be less than 0."
else if List.sum (List.map Tuple.first list) <= 0 then
Err "Frequency weights must sum to more than 0."
else
Ok <|
Internal.Fuzzer <|
\noShrink ->
if noShrink then
list
|> List.map (\( weight, fuzzer ) -> ( weight, Internal.unpackGenVal fuzzer ))
|> Random.frequency
|> Gen
else
list
|> List.map (\( weight, fuzzer ) -> ( weight, Internal.unpackGenTree fuzzer ))
|> Random.frequency
|> Shrink
{-| Calls `frequency` and handles `Err` results by crashing with the given
error message.
This is useful in tests, where a crash will simply cause the test run to fail.
There is no danger to a production system there.
-}
frequencyOrCrash : List ( Float, Fuzzer a ) -> Fuzzer a
frequencyOrCrash =
frequency >> okOrCrash
okOrCrash : Result String a -> a
okOrCrash result =
case result of
Ok a ->
a
Err str ->
Debug.crash str

View File

@@ -0,0 +1,58 @@
module Fuzz.Internal exposing (Fuzzer(Fuzzer), Fuzz(..), unpackGenVal, unpackGenTree)
import RoseTree exposing (RoseTree)
import Random.Pcg exposing (Generator)
{- Fuzzers as opt-in RoseTrees
In the beginning, a Fuzzer was a record of a random generator and a shrinker.
And it was bad, because that makes it impossible to shrink any value created by
mapping over other values. But at least it was fast, and shrinking worked well.
On the second branch, we created RoseTrees, where every randomly-generated value
also kept a lazy list of shrunken values, which also keep shrunken forms of
themselves. This allows for advanced maps to be implemented, but it was slow.
On the third branch, we realized that we shouldn't have to pay for shrinking in
the common case of a passing test. So Fuzzers became a function from a boolean
to either another union type. If the function is passed True, it returns a
Generator of a single value; if False, a Generator of a RoseTree of values.
(This is almost certainly dependent types leaning on Debug.crash.) The root of
the RoseTree must equal the single value. Thus the testing harness "opts-in" to
producing a rosetree, doing so only after the single-value generator has caused
a test to fail.
These two optimizations make the Fuzzer code rather hard to understand, but
allow it to offer a full mapping API, be fast for passing tests, and provide
shrunken values for failing tests.
-}
type Fuzzer a
= Fuzzer (Bool -> Fuzz a)
type Fuzz a
= Gen (Generator a)
| Shrink (Generator (RoseTree a))
unpackGenVal : Fuzzer a -> Generator a
unpackGenVal (Fuzzer g) =
case g True of
Gen genVal ->
genVal
err ->
Debug.crash "This shouldn't happen: Fuzz.Internal.unpackGenVal" err
unpackGenTree : Fuzzer a -> Generator (RoseTree a)
unpackGenTree (Fuzzer g) =
case g False of
Shrink genTree ->
genTree
err ->
Debug.crash "This shouldn't happen: Fuzz.Internal.unpackGenTree" err

View File

@@ -0,0 +1,59 @@
module RoseTree exposing (..)
{-| RoseTree implementation in Elm using Lazy Lists.
This implementation is private to elm-test and has non-essential functions removed.
If you need a complete RoseTree implementation, one can be found on elm-package.
-}
import Lazy.List as LazyList exposing (LazyList, (:::), (+++))
{-| RoseTree type.
A rosetree is a tree with a root whose children are themselves
rosetrees.
-}
type RoseTree a
= Rose a (LazyList (RoseTree a))
{-| Make a singleton rosetree
-}
singleton : a -> RoseTree a
singleton a =
Rose a LazyList.empty
{-| Get the root of a rosetree
-}
root : RoseTree a -> a
root (Rose a _) =
a
{-| Get the children of a rosetree
-}
children : RoseTree a -> LazyList (RoseTree a)
children (Rose _ c) =
c
{-| Add a child to the rosetree.
-}
addChild : RoseTree a -> RoseTree a -> RoseTree a
addChild child (Rose a c) =
Rose a (child ::: c)
{-| Map a function over a rosetree
-}
map : (a -> b) -> RoseTree a -> RoseTree b
map f (Rose a c) =
Rose (f a) (LazyList.map (map f) c)
{-| Flatten a rosetree of rosetrees.
-}
flatten : RoseTree (RoseTree a) -> RoseTree a
flatten (Rose (Rose a c) cs) =
Rose a (c +++ LazyList.map flatten cs)

View File

@@ -0,0 +1,307 @@
module Test exposing (Test, FuzzOptions, describe, test, filter, concat, fuzz, fuzz2, fuzz3, fuzz4, fuzz5, fuzzWith)
{-| A module containing functions for creating and managing tests.
@docs Test, test
## Organizing Tests
@docs describe, concat, filter
## Fuzz Testing
@docs fuzz, fuzz2, fuzz3, fuzz4, fuzz5, fuzzWith, FuzzOptions
-}
import Test.Internal as Internal
import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
{-| A test which has yet to be evaluated. When evaluated, it produces one
or more [`Expectation`](../Expect#Expectation)s.
See [`test`](#test) and [`fuzz`](#fuzz) for some ways to create a `Test`.
-}
type alias Test =
Internal.Test
{-| Run each of the given tests.
concat [ testDecoder, testSorting ]
-}
concat : List Test -> Test
concat =
Internal.Batch
{-| Remove any test unless its description satisfies the given predicate
function. Nested descriptions added with [`describe`](#describe) are not considered.
describe "String.reverse"
[ test "has no effect on a palindrome" testGoesHere
, test "reverses a known string" anotherTest
, fuzz string "restores the original string if you run it again" oneMore
]
|> Test.filter (String.contains "original")
-- only runs the final test
You can use this to focus on a specific test or two, silencing the failures of
tests you don't want to work on yet, and then remove the call to `Test.filter`
after you're done working on the tests.
-}
filter : (String -> Bool) -> Test -> Test
filter =
Internal.filter
{-| Apply a description to a list of tests.
import Test exposing (describe, test, fuzz)
import Fuzz exposing (int)
import Expect
describe "List"
[ describe "reverse"
[ test "has no effect on an empty list" <|
\() ->
List.reverse []
|> Expect.toEqual []
, fuzz int "has no effect on a one-item list" <|
\num ->
List.reverse [ num ]
|> Expect.toEqual [ num ]
]
]
-}
describe : String -> List Test -> Test
describe desc =
Internal.Batch >> Internal.Labeled desc
{-| Return a [`Test`](#Test) that evaluates a single
[`Expectation`](../Expect#Expectation).
import Test exposing (fuzz)
import Expect
test "the empty list has 0 length" <|
\() ->
List.length []
|> Expect.toEqual 0
-}
test : String -> (() -> Expectation) -> Test
test desc thunk =
Internal.Labeled desc (Internal.Test (\_ _ -> [ thunk () ]))
{-| Options [`fuzzWith`](#fuzzWith) accepts. Currently there is only one but this
API is designed so that it can accept more in the future.
### `runs`
The number of times to run each fuzz test. (Default is 100.)
import Test exposing (fuzzWith)
import Fuzz exposing (list, int)
import Expect
fuzzWith { runs = 350 } (list int) "List.length should always be positive" <|
-- This anonymous function will be run 350 times, each time with a
-- randomly-generated fuzzList value. (It will always be a list of ints
-- because of (list int) above.)
\fuzzList ->
fuzzList
|> List.length
|> Expect.atLeast 0
-}
type alias FuzzOptions =
{ runs : Int }
{-| Run a [`fuzz`](#fuzz) test with the given [`FuzzOptions`](#FuzzOptions).
Note that there is no `fuzzWith2`, but you can always pass more fuzz values in
using [`Fuzz.tuple`](../Fuzz#tuple), [`Fuzz.tuple3`](../Fuzz#tuple3),
for example like this:
import Test exposing (fuzzWith)
import Fuzz exposing (tuple, list, int)
import Expect
fuzzWith { runs = 4200 }
(tuple ( list int, int ))
"List.reverse never influences List.member" <|
\(nums, target) ->
List.member target (List.reverse nums)
|> Expect.toEqual (List.member target nums)
-}
fuzzWith : FuzzOptions -> Fuzzer a -> String -> (a -> Expectation) -> Test
fuzzWith options fuzzer desc getTest =
if options.runs < 1 then
test desc <|
\() ->
Expect.fail ("Fuzz test run count must be at least 1, not " ++ toString options.runs)
else
fuzzWithHelp options (fuzz fuzzer desc getTest)
fuzzWithHelp : FuzzOptions -> Test -> Test
fuzzWithHelp options test =
case test of
Internal.Test run ->
Internal.Test (\seed _ -> run seed options.runs)
Internal.Labeled label subTest ->
Internal.Labeled label (fuzzWithHelp options subTest)
Internal.Batch tests ->
tests
|> List.map (fuzzWithHelp options)
|> Internal.Batch
{-| Take a function that produces a test, and calls it several (usually 100) times, using a randomly-generated input
from a [`Fuzzer`](http://package.elm-lang.org/packages/elm-community/elm-test/latest/Fuzz) each time. This allows you to
test that a property that should always be true is indeed true under a wide variety of conditions. The function also
takes a string describing the test.
These are called "[fuzz tests](https://en.wikipedia.org/wiki/Fuzz_testing)" because of the randomness.
You may find them elsewhere called [property-based tests](http://blog.jessitron.com/2013/04/property-based-testing-what-is-it.html),
[generative tests](http://www.pivotaltracker.com/community/tracker-blog/generative-testing), or
[QuickCheck-style tests](https://en.wikipedia.org/wiki/QuickCheck).
import Test exposing (fuzz)
import Fuzz exposing (list, int)
import Expect
fuzz (list int) "List.length should always be positive" <|
-- This anonymous function will be run 100 times, each time with a
-- randomly-generated fuzzList value.
\fuzzList ->
fuzzList
|> List.length
|> Expect.atLeast 0
-}
fuzz :
Fuzzer a
-> String
-> (a -> Expectation)
-> Test
fuzz =
Internal.fuzzTest
{-| Run a [fuzz test](#fuzz) using two random inputs.
This is a convenience function that lets you skip calling [`Fuzz.tuple`](../Fuzz#tuple).
See [`fuzzWith`](#fuzzWith) for an example of writing this in tuple style.
import Test exposing (fuzz2)
import Fuzz exposing (list, int)
fuzz2 (list int) int "List.reverse never influences List.member" <|
\nums target ->
List.member target (List.reverse nums)
|> Expect.toEqual (List.member target nums)
-}
fuzz2 :
Fuzzer a
-> Fuzzer b
-> String
-> (a -> b -> Expectation)
-> Test
fuzz2 fuzzA fuzzB desc =
let
fuzzer =
Fuzz.tuple ( fuzzA, fuzzB )
in
uncurry >> fuzz fuzzer desc
{-| Run a [fuzz test](#fuzz) using three random inputs.
This is a convenience function that lets you skip calling [`Fuzz.tuple3`](../Fuzz#tuple3).
-}
fuzz3 :
Fuzzer a
-> Fuzzer b
-> Fuzzer c
-> String
-> (a -> b -> c -> Expectation)
-> Test
fuzz3 fuzzA fuzzB fuzzC desc =
let
fuzzer =
Fuzz.tuple3 ( fuzzA, fuzzB, fuzzC )
in
uncurry3 >> fuzz fuzzer desc
{-| Run a [fuzz test](#fuzz) using four random inputs.
This is a convenience function that lets you skip calling [`Fuzz.tuple4`](../Fuzz#tuple4).
-}
fuzz4 :
Fuzzer a
-> Fuzzer b
-> Fuzzer c
-> Fuzzer d
-> String
-> (a -> b -> c -> d -> Expectation)
-> Test
fuzz4 fuzzA fuzzB fuzzC fuzzD desc =
let
fuzzer =
Fuzz.tuple4 ( fuzzA, fuzzB, fuzzC, fuzzD )
in
uncurry4 >> fuzz fuzzer desc
{-| Run a [fuzz test](#fuzz) using five random inputs.
This is a convenience function that lets you skip calling [`Fuzz.tuple5`](../Fuzz#tuple5).
-}
fuzz5 :
Fuzzer a
-> Fuzzer b
-> Fuzzer c
-> Fuzzer d
-> Fuzzer e
-> String
-> (a -> b -> c -> d -> e -> Expectation)
-> Test
fuzz5 fuzzA fuzzB fuzzC fuzzD fuzzE desc =
let
fuzzer =
Fuzz.tuple5 ( fuzzA, fuzzB, fuzzC, fuzzD, fuzzE )
in
uncurry5 >> fuzz fuzzer desc
-- INTERNAL HELPERS --
uncurry3 : (a -> b -> c -> d) -> ( a, b, c ) -> d
uncurry3 fn ( a, b, c ) =
fn a b c
uncurry4 : (a -> b -> c -> d -> e) -> ( a, b, c, d ) -> e
uncurry4 fn ( a, b, c, d ) =
fn a b c d
uncurry5 : (a -> b -> c -> d -> e -> f) -> ( a, b, c, d, e ) -> f
uncurry5 fn ( a, b, c, d, e ) =
fn a b c d e

View File

@@ -0,0 +1,16 @@
module Test.Expectation exposing (Expectation(..), withGiven)
type Expectation
= Pass
| Fail String String
withGiven : String -> Expectation -> Expectation
withGiven given outcome =
case outcome of
Fail _ message ->
Fail given message
Pass ->
outcome

View File

@@ -0,0 +1,133 @@
module Test.Internal exposing (Test(..), fuzzTest, filter)
import Random.Pcg as Random exposing (Generator)
import Test.Expectation exposing (Expectation(..))
import Dict exposing (Dict)
import Shrink exposing (Shrinker)
import Fuzz exposing (Fuzzer)
import Fuzz.Internal exposing (unpackGenVal, unpackGenTree)
import RoseTree exposing (RoseTree(..))
import Lazy.List
type Test
= Test (Random.Seed -> Int -> List Expectation)
| Labeled String Test
| Batch (List Test)
filter : (String -> Bool) -> Test -> Test
filter =
filterHelp False
filterHelp : Bool -> (String -> Bool) -> Test -> Test
filterHelp lastCheckPassed isKeepable test =
case test of
Test _ ->
if lastCheckPassed then
test
else
Batch []
Labeled desc labeledTest ->
labeledTest
|> filterHelp (isKeepable desc) isKeepable
|> Labeled desc
Batch tests ->
tests
|> List.map (filterHelp lastCheckPassed isKeepable)
|> Batch
fuzzTest : Fuzzer a -> String -> (a -> Expectation) -> Test
fuzzTest fuzzer desc getExpectation =
{- Fuzz test algorithm with opt-in RoseTrees:
Generate a single value by passing the fuzzer True (indicates skip shrinking)
Run the test on that value. If it fails:
Generate the rosetree by passing the fuzzer False *and the same random seed*
Find the new failure by looking at the children for any shrunken values:
If a shrunken value causes a failure, recurse on its children
If no shrunken value replicates the failure, use the root
Whether it passes or fails, do this n times
-}
let
getFailures failures currentSeed remainingRuns =
let
genVal =
unpackGenVal fuzzer
( value, nextSeed ) =
Random.step genVal currentSeed
newFailures =
case getExpectation value of
Pass ->
failures
failedExpectation ->
let
genTree =
unpackGenTree fuzzer
( rosetree, nextSeedAgain ) =
Random.step genTree currentSeed
in
shrinkAndAdd rosetree getExpectation failedExpectation failures
in
if remainingRuns == 1 then
newFailures
else
getFailures newFailures nextSeed (remainingRuns - 1)
run seed runs =
let
-- Use a Dict so we don't report duplicate inputs.
failures : Dict String Expectation
failures =
getFailures Dict.empty seed runs
in
-- Make sure if we passed, we don't do any more work.
if Dict.isEmpty failures then
[ Pass ]
else
failures
|> Dict.toList
|> List.map formatExpectation
in
Labeled desc (Test run)
shrinkAndAdd : RoseTree a -> (a -> Expectation) -> Expectation -> Dict String Expectation -> Dict String Expectation
shrinkAndAdd rootTree getExpectation rootsExpectation dict =
-- Knowing that the root already failed, adds the shrunken failure to the dictionary
let
shrink oldExpectation (Rose root branches) =
case Lazy.List.headAndTail branches of
Just ( (Rose branch _) as rosetree, moreLazyRoseTrees ) ->
-- either way, recurse with the most recent failing expectation, and failing input with its list of shrunken values
case getExpectation branch of
Pass ->
shrink oldExpectation (Rose root moreLazyRoseTrees)
newExpectation ->
shrink newExpectation rosetree
Nothing ->
( root, oldExpectation )
( result, finalExpectation ) =
shrink rootsExpectation rootTree
in
Dict.insert (toString result) finalExpectation dict
formatExpectation : ( String, Expectation ) -> Expectation
formatExpectation ( given, expectation ) =
Test.Expectation.withGiven given expectation
isFail : Expectation -> Bool
isFail =
(/=) Pass

View File

@@ -0,0 +1,149 @@
module Test.Runner exposing (Runnable, Runner(..), run, fromTest, formatLabels)
{-| A collection of functions used by authors of test runners. To run your
own tests, you should use these runners; see the `README` for more information.
## Runner
@docs Runner, fromTest
## Runnable
@docs Runnable, run
## Formatting
@docs formatLabels
-}
import Test exposing (Test)
import Test.Internal as Internal
import Expect exposing (Expectation)
import Random.Pcg
import String
{-| An unevaluated test. Run it with [`run`](#run) to evaluate it into a
list of `Expectation`s.
-}
type Runnable
= Thunk (() -> List Expectation)
{-| A structured test runner, incorporating:
* The expectations to run
* The hierarchy of description strings that describe the results
-}
type Runner
= Runnable Runnable
| Labeled String Runner
| Batch (List Runner)
{-| Evaluate a [`Runnable`](#Runnable) to get a list of `Expectation`s.
-}
run : Runnable -> List Expectation
run (Thunk fn) =
fn ()
{-| Convert a `Test` into a `Runner`.
In order to run any fuzz tests that the `Test` may have, it requires a default run count as well
as an initial `Random.Pcg.Seed`. `100` is a good run count. To obtain a good random seed, pass a
random 32-bit integer to `Random.Pcg.initialSeed`. You can obtain such an integer by running
`Math.floor(Math.random()*0xFFFFFFFF)` in Node. It's typically fine to hard-code this value into
your Elm code; it's easy and makes your tests reproducible.
-}
fromTest : Int -> Random.Pcg.Seed -> Test -> Runner
fromTest runs seed test =
if runs < 1 then
Thunk (\() -> [ Expect.fail ("Test runner run count must be at least 1, not " ++ toString runs) ])
|> Runnable
else
case test of
Internal.Test run ->
Thunk (\() -> run seed runs)
|> Runnable
Internal.Labeled label subTest ->
subTest
|> fromTest runs seed
|> Labeled label
Internal.Batch subTests ->
subTests
|> List.foldl (distributeSeeds runs) ( seed, [] )
|> Tuple.second
|> Batch
distributeSeeds : Int -> Test -> ( Random.Pcg.Seed, List Runner ) -> ( Random.Pcg.Seed, List Runner )
distributeSeeds runs test ( startingSeed, runners ) =
case test of
Internal.Test run ->
let
( seed, nextSeed ) =
Random.Pcg.step Random.Pcg.independentSeed startingSeed
in
( nextSeed, runners ++ [ Runnable (Thunk (\() -> run seed runs)) ] )
Internal.Labeled label subTest ->
let
( nextSeed, nextRunners ) =
distributeSeeds runs subTest ( startingSeed, [] )
finalRunners =
List.map (Labeled label) nextRunners
in
( nextSeed, runners ++ finalRunners )
Internal.Batch tests ->
let
( nextSeed, nextRunners ) =
List.foldl (distributeSeeds runs) ( startingSeed, [] ) tests
in
( nextSeed, [ Batch (runners ++ nextRunners) ] )
{-| A standard way to format descriptiona and test labels, to keep things
consistent across test runner implementations.
The HTML, Node, String, and Log runners all use this.
What it does:
* drop any labels that are empty strings
* format the first label differently from the others
* reverse the resulting list
[ "the actual test that failed"
, "nested description failure"
, "top-level description failure"
]
|> formatLabels ((++) " ") ((++) " ")
{-
[ " top-level description failure"
, " nested description failure"
, " the actual test that failed"
]
-}
-}
formatLabels :
(String -> format)
-> (String -> format)
-> List String
-> List format
formatLabels formatDescription formatTest labels =
case List.filter (not << String.isEmpty) labels of
[] ->
[]
test :: descriptions ->
descriptions
|> List.map formatDescription
|> (::) (formatTest test)
|> List.reverse

View File

@@ -0,0 +1,32 @@
module Util exposing (..)
{-| This is where I'm sticking Random helper functions I don't want to add to Pcg.
-}
import Random.Pcg exposing (..)
import Array exposing (Array)
import String
rangeLengthList : Int -> Int -> Generator a -> Generator (List a)
rangeLengthList minLength maxLength generator =
(int minLength maxLength)
|> andThen (\len -> list len generator)
rangeLengthArray : Int -> Int -> Generator a -> Generator (Array a)
rangeLengthArray minLength maxLength generator =
rangeLengthList minLength maxLength generator
|> map Array.fromList
rangeLengthString : Int -> Int -> Generator Char -> Generator String
rangeLengthString minLength maxLength charGenerator =
(int minLength maxLength)
|> andThen (lengthString charGenerator)
lengthString : Generator Char -> Int -> Generator String
lengthString charGenerator stringLength =
list stringLength charGenerator
|> map String.fromList

View File

@@ -0,0 +1,22 @@
module Main exposing (..)
{-| HOW TO RUN THESE TESTS
$ npm test
Note that this always uses an initial seed of 902101337, since it can't do effects.
-}
import Runner.Log
import Html
import Tests
main : Program Never () msg
main =
Html.beginnerProgram
{ model = ()
, update = \_ _ -> ()
, view = \() -> Html.text "Check the console for useful output!"
}
|> Runner.Log.run Tests.all

View File

@@ -0,0 +1,6 @@
## Running the tests for elm-test itself
1. `cd` into this directory
2. `npm install`
3. `elm package install --yes`
4. `npm test`

View File

@@ -0,0 +1,76 @@
module Runner.Log exposing (run, runWithOptions)
{-| # Log Runner
Runs a test and outputs its results using `Debug.log`, then calls `Debug.crash`
if there are any failures.
This is not the prettiest runner, but it is simple and cross-platform. For
example, you can use it as a crude Node runner like so:
$ elm-make LogRunnerExample.elm --output=elm.js
$ node elm.js
This will log the test results to the console, then exit with exit code 0
if the tests all passed, and 1 if any failed.
@docs run, runWithOptions
-}
import Random.Pcg as Random
import Test exposing (Test)
import Runner.String exposing (Summary)
import String
{-| Run the test using the default `Test.Runner.String` options.
-}
run : Test -> a -> a
run test =
Runner.String.run test
|> logOutput
{-| Run the test using the provided options.
-}
runWithOptions : Int -> Random.Seed -> Test -> a -> a
runWithOptions runs seed test =
Runner.String.runWithOptions runs seed test
|> logOutput
summarize : Summary -> String
summarize { output, passed, failed } =
let
headline =
if failed > 0 then
output ++ "\n\nTEST RUN FAILED"
else
"TEST RUN PASSED"
in
String.join "\n"
[ output
, headline ++ "\n"
, "Passed: " ++ toString passed
, "Failed: " ++ toString failed
]
logOutput : Summary -> a -> a
logOutput summary arg =
let
output =
summarize summary ++ "\n\nExit code"
_ =
if summary.failed > 0 then
output
|> (flip Debug.log 1)
|> (\_ -> Debug.crash "FAILED TEST RUN")
|> (\_ -> ())
else
output
|> (flip Debug.log 0)
|> (\_ -> ())
in
arg

View File

@@ -0,0 +1,111 @@
module Runner.String exposing (Summary, run, runWithOptions)
{-| # String Runner
Run a test and present its results as a nicely-formatted String, along with
a count of how many tests passed and failed.
Note that this always uses an initial seed of 902101337, since it can't do effects.
@docs Summary, run, runWithOptions
-}
import Random.Pcg as Random
import Test exposing (Test)
import Expect exposing (Expectation)
import String
import Test.Runner exposing (Runner(..))
{-| The output string, the number of passed tests,
and the number of failed tests.
-}
type alias Summary =
{ output : String, passed : Int, failed : Int }
toOutput : Summary -> Runner -> Summary
toOutput =
flip (toOutputHelp [])
toOutputHelp : List String -> Runner -> Summary -> Summary
toOutputHelp labels runner summary =
case runner of
Runnable runnable ->
Test.Runner.run runnable
|> List.foldl fromExpectation summary
Labeled label subRunner ->
toOutputHelp (label :: labels) subRunner summary
Batch runners ->
List.foldl (toOutputHelp labels) summary runners
fromExpectation : Expectation -> Summary -> Summary
fromExpectation expectation summary =
case Expect.getFailure expectation of
Nothing ->
{ summary | passed = summary.passed + 1 }
Just { given, message } ->
let
prefix =
if String.isEmpty given then
""
else
"Given " ++ given ++ "\n\n"
newOutput =
"\n\n" ++ (prefix ++ indentLines message) ++ "\n"
in
{ output = summary.output ++ newOutput
, failed = summary.failed + 1
, passed = summary.passed
}
outputLabels : List String -> String
outputLabels labels =
labels
|> Test.Runner.formatLabels ((++) " ") ((++) " ")
|> String.join "\n"
defaultSeed : Random.Seed
defaultSeed =
Random.initialSeed 902101337
defaultRuns : Int
defaultRuns =
100
indentLines : String -> String
indentLines str =
str
|> String.split "\n"
|> List.map ((++) " ")
|> String.join "\n"
{-| Run a test and return a tuple of the output message and the number of
tests that failed.
Fuzz tests use a default run count of 100, and a fixed initial seed.
-}
run : Test -> Summary
run =
runWithOptions defaultRuns defaultSeed
{-| Run a test and return a tuple of the output message and the number of
tests that failed.
-}
runWithOptions : Int -> Random.Seed -> Test -> Summary
runWithOptions runs seed test =
test
|> Test.Runner.fromTest runs seed
|> toOutput { output = "", passed = 0, failed = 0 }

View File

@@ -0,0 +1,229 @@
module Tests exposing (all)
import Test exposing (..)
import Test.Expectation exposing (Expectation(..))
import Test.Internal as TI
import Fuzz exposing (..)
import Dict
import Set
import String
import Expect
import Fuzz.Internal
import RoseTree
import Random.Pcg as Random
all : Test
all =
Test.concat
[ readmeExample, bug39, fuzzerTests, shrinkingTests ]
{-| Regression test for https://github.com/elm-community/elm-test/issues/39
-}
bug39 : Test
bug39 =
fuzz (intRange 1 32) "small slice end" <|
\positiveInt ->
positiveInt
|> Expect.greaterThan 0
readmeExample : Test
readmeExample =
describe "The String module"
[ describe "String.reverse"
[ test "has no effect on a palindrome" <|
\() ->
let
palindrome =
"hannah"
in
Expect.equal palindrome (String.reverse palindrome)
, test "reverses a known string" <|
\() ->
"ABCDEFG"
|> String.reverse
|> Expect.equal "GFEDCBA"
, test "equal lists" <|
\() ->
[ 1, 2, 3 ]
|> Expect.equalLists [ 1, 2, 3 ]
, test "equal dicts" <|
\() ->
(Dict.fromList [ ( 1, "one" ), ( 2, "two" ) ])
|> Expect.equalDicts (Dict.fromList [ ( 1, "one" ), ( 2, "two" ) ])
, test "equal sets" <|
\() ->
(Set.fromList [ 1, 2, 3 ])
|> Expect.equalSets (Set.fromList [ 1, 2, 3 ])
, fuzz string "restores the original string if you run it again" <|
\randomlyGeneratedString ->
randomlyGeneratedString
|> String.reverse
|> String.reverse
|> Expect.equal randomlyGeneratedString
]
]
testStringLengthIsPreserved : List String -> Expectation
testStringLengthIsPreserved strings =
strings
|> List.map String.length
|> List.sum
|> Expect.equal (String.length (List.foldl (++) "" strings))
fuzzerTests : Test
fuzzerTests =
describe "Fuzzer methods that use Debug.crash don't call it"
[ describe "FuzzN (uses tupleN) testing string length properties"
[ fuzz2 string string "fuzz2" <|
\a b ->
testStringLengthIsPreserved [ a, b ]
, fuzz3 string string string "fuzz3" <|
\a b c ->
testStringLengthIsPreserved [ a, b, c ]
, fuzz4 string string string string "fuzz4" <|
\a b c d ->
testStringLengthIsPreserved [ a, b, c, d ]
, fuzz5 string string string string string "fuzz5" <|
\a b c d e ->
testStringLengthIsPreserved [ a, b, c, d, e ]
]
, fuzz
(intRange 1 6)
"intRange"
(Expect.greaterThan 0)
, fuzz
(frequencyOrCrash [ ( 1, intRange 1 6 ), ( 1, intRange 1 20 ) ])
"Fuzz.frequency(OrCrash)"
(Expect.greaterThan 0)
, fuzz (result string int) "Fuzz.result" <| \r -> Expect.pass
, fuzz (andThen (\i -> intRange 0 (2 ^ i)) (intRange 1 8))
"Fuzz.andThen"
(Expect.atMost 256)
, describe "Whitebox testing using Fuzz.Internal"
[ fuzz (intRange 0 0xFFFFFFFF) "the same value is generated with and without shrinking" <|
\i ->
let
seed =
Random.initialSeed i
step gen =
Random.step gen seed
aFuzzer =
tuple5
( tuple ( list int, array float )
, maybe bool
, result unit char
, tuple3
( percentage
, map2 (+) int int
, frequencyOrCrash [ ( 1, constant True ), ( 3, constant False ) ]
)
, tuple3 ( intRange 0 100, floatRange -51 pi, map abs int )
)
valNoShrink =
aFuzzer |> Fuzz.Internal.unpackGenVal |> step |> Tuple.first
valWithShrink =
aFuzzer |> Fuzz.Internal.unpackGenTree |> step |> Tuple.first |> RoseTree.root
in
Expect.equal valNoShrink valWithShrink
]
]
testShrinking : Test -> Test
testShrinking test =
case test of
TI.Test runTest ->
TI.Test
(\seed runs ->
let
expectations =
runTest seed runs
goodShrink expectation =
case expectation of
Pass ->
Just "Expected this test to fail, but it passed!"
Fail given outcome ->
let
acceptable =
String.split "|" outcome
in
if List.member given acceptable then
Nothing
else
Just <| "Got shrunken value " ++ given ++ " but expected " ++ String.join " or " acceptable
in
expectations
|> List.filterMap goodShrink
|> List.map Expect.fail
|> (\list ->
if List.isEmpty list then
[ Expect.pass ]
else
list
)
)
TI.Labeled desc labeledTest ->
TI.Labeled desc (testShrinking labeledTest)
TI.Batch tests ->
TI.Batch (List.map testShrinking tests)
shrinkingTests : Test
shrinkingTests =
testShrinking <|
describe "Tests that fail intentionally to test shrinking"
[ fuzz2 int int "Every pair of ints has a zero" <|
\i j ->
(i == 0)
|| (j == 0)
|> Expect.true "(1,1)"
, fuzz3 int int int "Every triple of ints has a zero" <|
\i j k ->
(i == 0)
|| (j == 0)
|| (k == 0)
|> Expect.true "(1,1,1)"
, fuzz4 int int int int "Every 4-tuple of ints has a zero" <|
\i j k l ->
(i == 0)
|| (j == 0)
|| (k == 0)
|| (l == 0)
|> Expect.true "(1,1,1,1)"
, fuzz5 int int int int int "Every 5-tuple of ints has a zero" <|
\i j k l m ->
(i == 0)
|| (j == 0)
|| (k == 0)
|| (l == 0)
|| (m == 0)
|> Expect.true "(1,1,1,1,1)"
, fuzz (list int) "All lists are sorted" <|
\aList ->
let
checkPair l =
case l of
a :: b :: more ->
if a > b then
False
else
checkPair (b :: more)
_ ->
True
in
checkPair aList |> Expect.true "[1,0]|[0,-1]"
]

View File

@@ -0,0 +1,19 @@
{
"version": "2.0.1",
"summary": "tests for elm-test, so you can elm-test while you elm-test",
"repository": "https://github.com/elm-community/elm-test.git",
"license": "BSD-3-Clause",
"source-directories": [
".",
"../src"
],
"exposed-modules": [],
"dependencies": {
"elm-community/lazy-list": "1.0.0 <= v < 2.0.0",
"elm-community/shrink": "2.0.0 <= v < 3.0.0",
"elm-lang/core": "5.0.0 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0",
"mgold/elm-random-pcg": "4.0.2 <= v < 5.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

View File

@@ -0,0 +1,22 @@
{
"name": "elm-test-tests",
"version": "0.0.0",
"description": "tests for elm-test, so you can elm-test while you elm-test",
"main": "elm.js",
"scripts": {
"test": "node run-tests.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/elm-community/elm-test.git"
},
"author": "Richard Feldman",
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/elm-community/elm-test/issues"
},
"homepage": "https://github.com/elm-community/elm-test#readme",
"devDependencies": {
"node-elm-compiler": "4.1.3"
}
}

View File

@@ -0,0 +1,19 @@
var compiler = require("node-elm-compiler")
testFile = "Main.elm"
compiler.compileToString([testFile], {}).then(function(str) {
try {
eval(str);
process.exit(0)
} catch (err) {
console.error(err);
process.exit(1)
}
}).catch(function(err) {
console.error(err);
process.exit(1)
});