🎨 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

@@ -1,11 +1,11 @@
module Tests exposing (..)
import Test exposing (..)
import Fuzz exposing (..)
import Expect exposing (Expectation)
import ElmHub exposing (responseDecoder)
import Json.Decode exposing (decodeString, Value)
import Expect exposing (Expectation)
import Fuzz exposing (..)
import Json.Decode exposing (Value, decodeString)
import String
import Test exposing (..)
all : Test
@@ -24,10 +24,10 @@ all =
-- Result docs: http://package.elm-lang.org/packages/elm-lang/core/latest/Result
False
in
json
|> decodeString responseDecoder
|> isErrorResult
|> Expect.true "Expected decoding an invalid response to return an Err."
json
|> decodeString responseDecoder
|> isErrorResult
|> Expect.true "Expected decoding an invalid response to return an Err."
, test "it successfully decodes a valid response" <|
\() ->
"""{ "items": [
@@ -60,11 +60,11 @@ all =
json =
"""{ "items": [""" ++ jsonItems ++ """] }"""
in
case decodeString responseDecoder json of
Ok results ->
List.length results
|> Expect.equal (List.length ids)
case decodeString responseDecoder json of
Ok results ->
List.length results
|> Expect.equal (List.length ids)
Err err ->
Expect.fail ("JSON decoding failed unexpectedly: " ++ err)
Err err ->
Expect.fail ("JSON decoding failed unexpectedly: " ++ err)
]

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)
});

View File

@@ -0,0 +1,36 @@
sudo: false
language: node_js
node_js: "node"
os: linux
env: ELM_VERSION=0.18.0
cache:
directories:
- test/elm-stuff/build-artifacts
- sysconfcpus
before_install:
- 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:
- node --version
- npm --version
- cd tests
- npm install -g elm@$ELM_VERSION
- 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,63 @@
### 2.5.0
**Additions:**
- `dict` helps encoding `Dict`
### 2.4.0
**Additions:**
- `collection` helps with decoding array-like JavaScript structures such as `HTMLCollection`
- `combine` helps combining a `List` of decoders into a single `Decoder` for a `List` of such things
### 2.3.0
**Additions:**
- `indexedList` to get access to the current js array index while decoding
**Other Stuff:**
- `elm-doc-test` is now `elm-verify-examples`!
### 2.2.0
**Additions:**
- `parseInt` and `parseFloat` for weird api's that return numbers as strings
- `doubleEncoded` for a more generic _json as a string in json_ issues
**Fixes:**
- `optionalField` decodes the field, rather than the surrounding object now.
**Other Stuff:**
- Code Style conforms to elm-format@exp
- Doc tests!
- Travis integration
### 2.1.0
**Additions:**
- `optionalField : String -> Json.Decode.Decoder a -> Json.Decode.Decoder (Maybe.Maybe a)` - Decode an optional field, succeeding with `Nothing` if it is missing, but still giving an error if it is malformed.
### 2.0.0
**Breaking Changes:**
- Upgrade for Elm 0.18
- Removed `maybeNull` in favor of `Json.Decode.nullable`
- Removed `lazy` in favor of `Json.Decode.lazy`
- Renamed `apply` to `andMap` and reversed arguments to `Decoder a -> Decoder (a -> b) -> Decoder b` to make it work nicely with `(|>)`
**Additions:**
- `fromResult : Result String a -> Decoder a` - convert a `Result` to a `Decoder`, helpful in `andThen` callbacks following the removal of `Json.Decode.customDecoder`
- `Json.Encode.Extra.maybe : (a -> Value) -> Maybe a -> Value` - encode a `Maybe a` given an encoder for `a`. Thanks to @hendore for this addition.
**Other Stuff:**
- Code style conforms to elm-format
#### 1.1.0
**Additions:**
- `Json.Decode.Extra.sequence` - lets you generate a list of `Decoder a` and attempt to apply them to a JSON list. _Authored by @cobalamin_
#### 1.0.0
**Breaking Changes:**
- Upgrade for Elm 0.17

View File

@@ -0,0 +1,9 @@
[![Build Status](https://travis-ci.org/elm-community/json-extra.svg?branch=master)](https://travis-ci.org/elm-community/json-extra)
# json-extra
```
elm-package install elm-community/json-extra
```
Convenience functions for working with JSON

View File

@@ -0,0 +1,61 @@
## Json.Decode.Extra.andMap
Imagine you have a data type for a user
```elm
import Date (Date)
type alias User =
{ id : Int
, createdAt : Date
, updatedAt : Date
, deletedAt : Maybe Date
, username : Maybe String
, email : Maybe String
, isAdmin : Bool
}
```
You can use `andMap` to incrementally apply decoders to your `User` type alias
by using that type alias as a function. Recall that record type aliases are
also functions which accept arguments in the order their fields are declared. In
this case, `User` looks like
```elm
User : Int -> Date -> Date -> Maybe Date -> Maybe String -> Maybe String -> Bool -> User
```
And also recall that Elm functions can be partially applied. We can use these
properties to apply each field of our JSON object to each field in our user one
field at a time. All we need to do is also wrap `User` in a decoder and step
through using `andMap`.
```elm
userDecoder : Decoder User
userDecoder =
succeed User
|> andMap (field "id" int)
|> andMap (field "createdAt" date)
|> andMap (field "updatedAt" date)
|> andMap (field "deletedAt" (maybe date))
|> andMap (field "username" (maybe string))
|> andMap (field "email" (maybe string))
|> andMap (field "isAdmin" bool)
```
This is a shortened form of
```elm
userDecoder : Decoder User
userDecoder =
succeed User
|> andThen (\f -> map f (field "id" int))
|> andThen (\f -> map f (field "createdAt" date))
|> andThen (\f -> map f (field "updatedAt" date))
|> andThen (\f -> map f (field "deletedAt" (maybe date)))
|> andThen (\f -> map f (field "username" (maybe string)))
|> andThen (\f -> map f (field "email" (maybe string)))
|> andThen (\f -> map f (field "isAdmin" bool))
```
See also: The [docs for `(|:)`](https://github.com/elm-community/json-extra/blob/master/docs/infixAndMap.md)

View File

@@ -0,0 +1,131 @@
## Json.Decode.Extra.(|:)
Infix version of `andMap` that makes for a nice DSL when decoding objects.
Consider the following type alias for a `Location`:
```elm
type alias Location =
{ id : Int
, name : String
, address : String
}
```
We can use `(|:)` to build up a decoder for `Location`:
```elm
locationDecoder : Decoder Location
locationDecoder =
succeed Location
|: (field "id" int)
|: (field "name" string)
|: (field "address" string)
```
If you're curious, here's how this works behind the scenes, read on.
`Location` is a type alias, and type aliases give you a convenience function
that returns an instance of the record in question. Try this out in `elm-repl`:
```elm
> type alias Location = { id : Int, name: String, address: String }
> Location
<function> : Int -> String -> String -> Repl.Location
> Location 1 "The White House" "1600 Pennsylvania Ave"
{ id = 1, name = "The White House", address = "1600 Pennsylvania Ave" }
```
In other words, if you call the `Location` function, passing three arguments,
it will return a new `Location` record by filling in each of its fields. (The
argument order is based on the order in which we listed the fields in the
type alias; the first argument sets `id`, the second argument sets `name`, etc.)
Now try running this through `elm-repl`:
```elm
> import Json.Decode exposing (succeed, int, string, field)
> succeed Location
<function>
: Json.Decode.Decoder
(Int -> String -> String -> Repl.Location)
```
So `succeed Location` gives us a `Decoder (Int -> String -> String -> Location)`.
That's not what we want! What we want is a `Decoder Location`. All we have so
far is a `Decoder` that wraps not a `Location`, but rather a function that
returns a `Location`.
What `|: (field "id" int)` does is to take that wrapped function and pass an
argument to it.
```elm
> import Json.Decode exposing (succeed, int, string, field)
> (field "id" int)
<function> : Json.Decode.Decoder Int
> succeed Location |: (field "id" int)
<function>
: Json.Decode.Decoder
(String -> String -> Repl.Location)
```
Notice how the wrapped function no longer takes an `Int` as its first argument.
That's because `(|:)` went ahead and supplied one: the `Int` wrapped by the decoder
`(field "id" int)` (which returns a `Decoder Int`).
Compare:
```elm
> succeed Location
Decoder (Int -> String -> String -> Location)
> succeed Location |: (field "id" int)
Decoder (String -> String -> Location)
```
We still want a `Decoder Location` and we still don't have it yet. Our decoder
still wraps a function instead of a plain `Location`. However, that function is
now smaller by one argument!
Let's repeat this pattern to provide the first `String` argument next.
```elm
> succeed Location
Decoder (Int -> String -> String -> Location)
> succeed Location |: (field "id" int)
Decoder (String -> String -> Location)
> succeed Location |: (field "id" int) |: (field "name" string)
Decoder (String -> Location)
```
Smaller and smaller! Now we're down from `(Int -> String -> String -> Location)`
to `(String -> Location)`. What happens if we repeat the pattern one more time?
```elm
> succeed Location
Decoder (Int -> String -> String -> Location)
> succeed Location |: (field "id" int)
Decoder (String -> String -> Location)
> succeed Location |: (field "id" int) |: (field "name" string)
Decoder (String -> Location)
> succeed Location |: (field "id" int) |: (field "name" string) |: (field "address" string)
Decoder Location
```
Having now supplied all three arguments to the wrapped function, it has ceased
to be a function. It's now just a plain old `Location`, like we wanted all along.
We win!

View File

@@ -0,0 +1,5 @@
# Lazy list implementation in Elm
Lazy lists are useful for large trees where you will only need one path. They power shrinking in elm-test.
Prior to 0.18, this repo was elm-lazy-list. Starting with the 0.18 release, the repo is renamed lazy-list and versioning resets to 1.0.0.

View File

@@ -0,0 +1,241 @@
# Shrinking strategies with elm-community/shrink
`shrink` is a library for using and creating shrinking strategies. A
shrinking strategy, or shrinker, is a way of taking a value and producing a
list of values which are, some sense, more minimal than the original value.
Shrinking is heavily used in property-based testing as a way to shrink
failing test cases into a more minimal test case. This is key for being
able to debug code swiftly and easily. Therefore, the main intended use
of this library is to support testing frameworks. You might also use it to
define shrinkers for types you define, in order to test them better.
Note that `shrink` uses lazy lists instead of lists. This means that `shrink` has a direct dependency on
[elm-community/elm-lazy-list](https://github.com/elm-community/elm-lazy-list).
```elm
type alias Shrinker a = a -> LazyList a
```
That is, a shrinker takes a value to shrink, and produces, *lazily*, a list
of shrunken values.
### Basic Examples
The following examples show how to use the basic shrinkers and the kinds of
results they produce. Note that we're glossing over the lazy part of this;
pretend there's a `Lazy.List.toList` on the left of each example.
**Shrink an Int**
```elm
int 10 == [0,5,7,8,9]
```
**Shrink a String**
```elm
string "Hello World" ==
[""," World","Hellod","llo World","Heo World","HellWorld","Hello rld","Hello Wod","ello World","Hllo World","Helo World","Helo World","Hell World","HelloWorld","Hello orld","Hello Wrld","Hello Wold","Hello Word","Hello Worl","\0ello World","$ello World","6ello World","?ello World","Cello World","Eello World","Fello World","Gello World","H\0llo World","H2llo World","HKllo World","HXllo World","H^llo World","Hallo World","Hcllo World","Hdllo World","He\0lo World","He6lo World","HeQlo World","He^lo World","Heelo World","Hehlo World","Hejlo World","Heklo World","Hel\0o World","Hel6o World","HelQo World","Hel^o World","Heleo World","Helho World","Heljo World","Helko World","Hell\0 World","Hell7 World","HellS World","Hella World","Hellh World","Hellk World","Hellm World","Helln World","Hello\0World","HelloWorld","HelloWorld","HelloWorld","HelloWorld","HelloWorld","Hello \0orld","Hello +orld","Hello Aorld","Hello Lorld","Hello Qorld","Hello Torld","Hello Uorld","Hello Vorld","Hello W\0rld","Hello W7rld","Hello WSrld","Hello Warld","Hello Whrld","Hello Wkrld","Hello Wmrld","Hello Wnrld","Hello Wo\0ld","Hello Wo9ld","Hello WoUld","Hello Wocld","Hello Wojld","Hello Wonld","Hello Wopld","Hello Woqld","Hello Wor\0d","Hello Wor6d","Hello WorQd","Hello Wor^d","Hello Wored","Hello Worhd","Hello Worjd","Hello Workd","Hello Worl\0","Hello Worl2","Hello WorlK","Hello WorlW","Hello Worl]","Hello Worl`","Hello Worlb","Hello Worlc"]
```
**Shrink a Maybe Float**
```elm
maybe float (Just 3.14) ==
[Nothing,Just 0,Just 1.57,Just 2.355,Just 2.7475,Just 2.94375,Just 3.041875,Just 3.0909375,Just 3.11546875,Just 3.127734375,Just 3.1338671875,Just 3.1369335937500002,Just 3.138466796875,Just 3.1392333984375,Just 3.1396166992187498,Just 3.1398083496093747]
```
**Shrink a List of Bools**
```elm
list bool [True, False, False, True, False] ==
[[],[False,True,False],[True,False,False],[False,False,True,False],[True,False,True,False],[True,False,True,False],[True,False,False,False],[True,False,False,True],[False,False,False,True,False],[True,False,False,False,False]]
```
## Make your own shrinkers
With `shrink`, it is very easy to make your own shrinkers for your own data
types.
First of all, let's look at one of the basic shrinkers available in `shrink`
and how it is implemented.
**Shrinker Bool**
To shrink a `Bool`, you have to consider the possible values of `Bool`: `True`
and `False`. Intuitively, we understand that `False` is more "minimal"
than `True`. As, such, we would shrink `True` to `False`. As for `False`,
there is no value that is more "minimal" than `False`. As such, we simply
shrink it to the empty list.
```elm
bool : Shrinker Bool
bool b = case b of
True -> False ::: empty
False -> empty
```
*Note that there is no "exact" rule to deciding on whether something is more
"minimal" than another.* The idea is that you want to have one case return
the empty list if possible while other cases move towards the more "minimal"
cases. In this example, we decided that `False` was the more "minimal" case and,
in a sense, moved `True` towards `False` since `False` then returns the empty
list. Obviously, this choice could have been reversed and you would be
justified in doing so. Just remember that *a value should never shrink to itself,
or shrink to something that (through any number of steps) shrinks back to itself.*
This is a recipe for an infinite loop.
Now that we understand how to make a simple shrinker, let's see how we can use
these simple shrinkers together to make something that can shrink a more
complex data structure.
**Shrinker Vector**
Consider the following `Vector` type:
```elm
type alias Vector =
{ x : Float
, y : Float
, z : Float
}
```
Our goal is to produce a vector shrinker:
```elm
vector : Shrinker Vector
```
`shrink` provides a basic `Float` shrinker and we can use it in combination
with `map` and `andMap` to make the `Vector` shrinker.
```elm
vector : Shrinker Vector
vector {x, y, z} =
Vector
`map` float x
`andMap` float y
`andMap` float z
```
And voila! Super simple. Let's try this on an even larger structure.
**Shrinker Mario**
Consider the following types:
```elm
type alias Mario =
{ position : Vector
, velocity : Vector
, direction : Direction
}
type alias Vector =
{ x : Float
, y : Float
}
type Direction
= Left
| Right
```
And our goal is to produce a shrinker of Marios.
```elm
mario : Shrinker Mario
```
To do this, we will split the steps. We can notice that we have two distinct
data types we need to shrink: `Vector` and `Direction`.
For `Vector`, we can use the approach from the previous example:
```elm
vector : Shrinker Vector
vector {x, y} =
Vector
`map` float x
`andMap` float y
```
And for `Direction`, we can apply a similar approach to our `Bool` example:
```elm
direction : Shrinker Direction
direction dir = case dir of
Left -> empty
Right -> Left ::: empty
```
Where `Left` here is considered the "minimal" case.
Now, let's put these together:
```elm
mario : Shrinker Mario
mario m =
Mario
`map` vector m.position
`andMap` vector m.velocity
`andMap` direction m.direction
```
And, yay! We now can shrink `Mario`! No mushrooms needed!
### One more technique
Sometimes, you want to shrink a data structure but you know intuitively that
it should shrink in a similar fashion to some other data structure. It would
be nice if you could just convert back and from that other data structure and
use its already existing shrinker.
For example `List` and `Array`.
In `shrink`, there exists a `List` shrinker:
```elm
list : Shrinker a -> Shrinker (List a)
```
This shrinker is quite involved and does a number of things to shuffle elements,
shrink some elements, preserve others, etc...
It would be nice if that can be re-used for arrays, because in a high-level
sense, arrays and lists are equivalent.
This is exactly what `shrink` does and it uses a function called `convert`.
```elm
convert : (a -> b) -> (b -> a) -> Shrinker a -> Shrinker b
```
`convert` converts a shrinker of a's into a shrinker of b's by taking a
two functions to convert to and from b's.
**IMPORTANT NOTE: Both functions must be perfectly invertible or else this
process may create garbage!**
By invertible, I mean that `f` and `g` are invertible **if and only if**
```elm
f (g x) == g (f x) == x
```
**for all `x`.**
Now we can very simply implement a shrinker of arrays as follows:
```elm
array : Shrinker a -> Shrinker (Array a)
array shrinker =
convert (Array.fromList) (Array.toList) (list shrinker)
```
And, ta-da... 0 brain cells were used to get a shrinker on arrays.

View File

@@ -0,0 +1,69 @@
# Laziness in Elm
This package provides the basic primitives for working with laziness in Elm.
# Motivating Example
Maybe you have 100 different graphs you want to show at various times, each
requiring a decent amount of computation. Here are a couple ways to handle
this:
1. Compute everything up front. This will introduce a delay on startup, but
it should be quite fast after that. Depending on how much memory is needed
to store each graph, you may be paying a lot there as well.
2. Compute each graph whenever you need it. This minimizes startup cost and
uses a minimal amount of memory, but when you are flipping between two
graphs you may be running the same computations again and again.
3. Compute each graph whenever you need it and save the result. Again, this
makes startup as fast as possible fast, but since we save the result,
flipping between graphs becomes much quicker. As we look at more graphs
we will need to use more and more memory though.
All of these strategies are useful in general, but the details of your
particular problem will mean that one of these ways provides the best
experience. This library makes it super easy to use strategy #3.
# Pitfalls
**Laziness + Time** &mdash;
Over time, laziness can become a bad strategy. As a very simple example, think
of a timer that counts down from 10 minutes, decrementing every second. Each
step is very cheap to compute. You subtract one from the current time and store
the new time in memory, so each step has a constant cost and memory usage is
constant. Great! If you are lazy, you say &ldquo;here is how you would subtract
one&rdquo; and store that *entire computation* in memory. This means our memory
usage grows linearly as each second passes. When we finally need the result, we
might have 10 minutes of computation to run all at once. In the best case, this
introduces a delay that no one *really* notices. In the worst case, this
computation is actually too big to run all at once and crashes. Just like with
dishes or homework, being lazy over time can be quite destructive.
**Laziness + Concurrency** &mdash;
When you add concurrency into the mix, you need to be even more careful with
laziness. As an example, say we are running expensive computations on three
worker threads, and the results are sent to a fourth thread just for rendering.
If our three worker threads are doing their work lazily, they
&ldquo;finish&rdquo; super quick and pass the entire workload onto the render
thread. All the work we put into designing this concurrent system is wasted,
everything is run sequentially on the render thread! It is just like working on
a team with lazy people. You have to pay the cost of coordinating with them,
but you end up doing all the work anyway. You are better off making things
single threaded!
## Learn More
One of the most delightful uses of laziness is to create infinite streams of
values. Hopefully we can get a set of interesting challenges together so
you can run through them and get comfortable.
For a deeper dive, Chris Okasaki's book *Purely Functional Data Structures*
and [thesis](http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf)
have interesting examples of data structures that get great
benefits from laziness, and hopefully it will provide some inspiration for the
problems you face in practice.

View File

@@ -0,0 +1,47 @@
# Random.Pcg for Elm
> "The generation of random numbers is too important to be left to chance." Robert R. Coveyou
An alternate random number generator built around four principles:
* **Statistical Quality.** If you use any seed less than 53,668 and generate one bool, it will be `True` if you're
using core's `Random` module. More sophisticated statistical tests spot patterns in the "random" numbers almost
immediately. Would you want to trust the accuracy of your [fuzz
tests](http://package.elm-lang.org/packages/elm-community/elm-test/latest/) to such a flawed algorithm? This library
produces far less predictable and biased output, especially if you use thousands of random numbers. See
`test/dieharder` for more details.
* **Useful features.** This library exports `constant` and `andMap`, which are conspicuously absent from core, along
with other helpful functions for composing generators. Particularly interesting is `independentSeed`, which allows for
lazy lists and isolated components to generate as much randomness as they need, when they need it.
* **Performace.** This library will generate floats about 3.5 times faster than core, and ints do not regress. These
figures stand to improve pending some optimizations to the compiler. You can see the [full
benchmark results](https://github.com/mgold/elm-random-pcg/issues/5#issuecomment-236398261).
* **Compatibility.** This library is a drop-in replacement for core's Random module. Specifically, you
can replace `import Random` with `import Random.Pcg as Random` and everything will continue to work. (The one exception is third party
libraries like [elm-random-extra](http://package.elm-lang.org/packages/NoRedInk/elm-random-extra/latest/Random-Extra).)
This is an implementation of [PCG](http://www.pcg-random.org/) by M. E. O'Neil. The generator is **not cryptographically
secure**.
Please report bugs, feature requests, and other issues [on GitHub](https://github.com/mgold/elm-random-pcg/issues/new).
## Changelog (major versions only)
### 4.0.0
* Upgraded for 0.18.
* Argument order of `andThen` flipped.
### 3.0.0
* Change implementation to use the RXS-M-SH variant of PCG. Now much faster and not much worse statistically.
* Remove `initialSeed2`, since there are now only 32 bits of state.
* `Random.Pcg.Interop.fission` has been changed to a (core) generator of (PCG) seeds.
* Add `generate` to match core 4.x API. Implemented by Richard Feldman.
### 2.0.0
* Upgraded for 0.17.
* `generate` renamed `step` to match core 4.x API.
* Module renamed `Random.Pcg` from `Random.PCG`.
* `split` has been removed; use `independentSeed`.
* `minInt` and `maxInt` values changed to match core.

View File

@@ -0,0 +1,4 @@
# Random.Pcg Tests
The tests in this directory are one-off simple programs to confirm that certain functions aren't obviously wrong.
Heavyweight statistical tests are under `dieharder`. Benchmarks (using a 0.16 benchmarking lib) are in `benchmark`.

View File

@@ -0,0 +1,14 @@
# Benchmarks for Random.Pcg
These benchmarks are possible thanks to Robin Heggelund Hansen's work benchmarking [collections-ng](https://github.com/Skinney/collections-ng).
To run them yourself, first download the vendor files (you only need to do this once):
```
mkdir vendor
cd vendor
wget https://cdn.jsdelivr.net/lodash/4.13.1/lodash.min.js
wget https://cdn.jsdelivr.net/benchmarkjs/2.1.0/benchmark.js
```
Then adjust the import of `Random` in `Bencher.elm` to control which library is being benchmarked. Finally run `sh prep-bench.sh` then
`elm-reactor` in this directory and open `run-benchmarks.html`.

View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -e
elm-make --yes --output bench.js Bencher.elm

View File

@@ -0,0 +1,27 @@
# Dieharder tests for Random.Pcg
**These tests rely on a library that has not been update for 0.17.**
The purpose of testing the random number generator is twofold: one, show the deficiencies in the core implementation;
two, show the correctness of the Pcg implementation *in Elm*. Because we are testing the Elm implementation, not the Pcg
algorithm, we must feed dieharder a file of already-generated random numbers. I've seen sources recommending 10 million
random numbers; these tests use 24 million, but even so the files are "rewound", as many as 2500 times on later tests.
For the original 19 diehard tests, the core fails 9 tests while Pcg fails none. (One test resulted in "weak" but further
investigation resulted in passing; we expect this on one test in 100). On the entire battery, core passes 29, fails 75,
and is weak in 10. Pcg passes 82, fails 24, and is weak in 8. Some of Pcg's strength may be attributed to its 64 bits of
state, compared to core's 32, but this does not excuse failing the less-comprehensive diehard tests. Conversely, many of
the failures can be attributed to reusing pre-generated random numbers.
The source Elm is `Dieharder.elm`. Because the tests require more random numbers than can be stored in memory, they
output chunks of 2 million at a time. In order to reuse the code, `compile.sh` rewrites the file to change the Random
implementation. It also writes out the `.txt` data files, and then runs dieharder, logging the results.
The result log files have been committed to version control; I would have liked to commit the data files but they're
huge, and only take a few minutes to generate. You are free to alter the random seed in the Elm code, and rerun the
tests with `sh compile` (which takes several hours to complete). That said, I encourage you to run it long enough to see
core fail the birthday test, after only five or six seconds of scrutiny. (The `dieharder` tool is available through most
package managers. Once the data files are generated, try `time dieharder -g 202 -f elm-core-random.txt -d 0`.)
The Pcg paper uses the TestU01 (e.g. BigCrush) suite; I'm using dieharder since it was easier to get to work
reading from a file.

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -e
if [ ! -f elm-core-random.txt ]; then
sed -i.bak 's/import Random.Pcg as Random/import Random/g' Dieharder.elm
sed -i.bak 's/elm-random-pcg/elm-core-random/g' Dieharder.elm
elm make Dieharder.elm --output=raw_out.js
sh elm-io.sh raw_out.js generate_files.js
echo "Generating elm-core-random.txt..."
node generate_files.js > elm-core-random.txt
dieharder -g 202 -f elm-core-random.txt -a | tee dieharder-core.log
fi
if [ ! -f elm-random-pcg.txt ]; then
sed -i.bak 's/import Random$/import Random.Pcg as Random/g' Dieharder.elm
sed -i.bak 's/elm-core-random/elm-random-pcg/g' Dieharder.elm
elm make Dieharder.elm --output=raw_out.js
sh elm-io.sh raw_out.js generate_files.js
echo "Generating elm-random-pcg.txt..."
node generate_files.js > elm-random-pcg.txt
dieharder -g 202 -f elm-random-pcg.txt -a | tee dieharder-pcg.log
fi

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Script for use with the Console library to allow Elm to run on Node.
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <generated-js-file> <output-file>"
exit 1
fi
read -d '' handler <<- EOF
(function(){
if (typeof Elm === "undefined") { throw "elm-io config error: Elm is not defined. Make sure you call elm-io with a real Elm output file"}
if (typeof Elm.Main === "undefined" ) { throw "Elm.Main is not defined, make sure your module is named Main." };
var worker = Elm.worker(Elm.Main);
})();
EOF
cat $1 > $2
echo "$handler" >> $2

View File

@@ -0,0 +1,7 @@
# Port test
```
elm make Test.elm --output=elm.js
```
This shows how to hook up ports to the random number generator to produce different values each time.

View File

@@ -0,0 +1,8 @@
# html-test-runner
Run elm-test suites in the browser
## Try it
1. `cd examples`
2. `elm-reactor`
3. Visit [http://localhost:8000/HtmlRunnerExample.elm](http://localhost:8000/HtmlRunnerExample.elm)

View File

@@ -0,0 +1,43 @@
sudo: false
cache:
directories:
- test/elm-stuff/build-artifacts
- sysconfcpus
os:
- linux
- osx
env:
matrix:
- ELM_VERSION=0.18.0 TARGET_NODE_VERSION=node
- ELM_VERSION=0.18.0 TARGET_NODE_VERSION=0.12
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
- | # 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
- npm install -g elm@$ELM_VERSION
- mv $(npm config get prefix)/bin/elm-make $(npm config get prefix)/bin/elm-make-old
- echo "#\!/bin/bash\\n\\necho \"Running elm-make with sysconfcpus -n 2\"\\n\\n$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
script:
- npm test

View File

@@ -0,0 +1,47 @@
# node-test-runner [![Version](https://img.shields.io/npm/v/elm-test.svg)](https://www.npmjs.com/package/elm-test) [![Travis build Status](https://travis-ci.org/rtfeldman/node-test-runner.svg?branch=master)](http://travis-ci.org/rtfeldman/node-test-runner) [![AppVeyor build status](https://ci.appveyor.com/api/projects/status/fixcy4ko78di0l31/branch/master?svg=true)](https://ci.appveyor.com/project/rtfeldman/node-test-runner/branch/master)
Runs [elm-test](https://github.com/elm-community/elm-test) suites from Node.js
## Installation
```bash
npm install -g elm-test
```
## Usage
```bash
elm-test init # Adds the elm-test dependency and creates Main.elm and Tests.elm
elm-test # Runs the tests
```
Then add your tests to Tests.elm.
### Configuration
The `--compiler` flag can be used to use a version of the Elm compiler that
has not been install globally.
```
npm install elm
elm-test --compiler ./node_modules/.bin/elm-make
```
### Travis CI
If you want to run your tests on Travis CI, here's a good starter `.travis.yml`:
```yml
language: node_js
node_js:
- "5"
install:
- npm install -g elm
- npm install -g elm-test
- elm-package install -y
- pushd tests && elm-package install -y && popd
script:
- elm-test
```

View File

@@ -0,0 +1,25 @@
environment:
ELM_VERSION: "0.18.0"
matrix:
- nodejs_version: "5.0"
- nodejs_version: "0.12"
- nodejs_version: "0.11.13"
platform:
- x86
- x64
matrix:
fast_finish: true
install:
- ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:Platform
- node --version
- npm --version
- npm install -g elm@%ELM_VERSION%
- npm install
test_script:
- npm test
build: off

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env node
var processTitle = "elm-test";
process.title = processTitle;
var compile = require("node-elm-compiler").compile,
fs = require("fs-extra"),
chalk = require("chalk"),
path = require("path"),
temp = require("temp").track(), // Automatically cleans up temp files.
util = require("util"),
_ = require("lodash"),
spawn = require("cross-spawn"),
minimist = require("minimist"),
firstline = require("firstline"),
findUp = require("find-up"),
chokidar = require("chokidar");
var elm = {
'elm-package': 'elm-package'
};
var args = minimist(process.argv.slice(2), {
alias: {
'help': 'h',
'yes': 'y',
'seed': 's',
'compiler': 'c',
'report': 'r',
'watch': 'w'
},
boolean: [ 'yes', 'warn', 'version', 'help', 'watch' ],
string: [ 'compiler', 'seed', 'report' ]
});
if (args.help) {
console.log("Usage: elm-test init [--yes] # Create example tests\n");
console.log("Usage: elm-test TESTFILE [--compiler /path/to/compiler] # Run TESTFILE\n");
console.log("Usage: elm-test [--compiler /path/to/compiler] # Run tests/Main.elm\n");
console.log("Usage: elm-test [--seed integer] # Run with initial fuzzer seed\n");
console.log("Usage: elm-test [--report json or chalk (default)] # Print results to stdout in given format\n");
console.log("Usage: elm-test [--watch] # Run tests on file changes\n");
process.exit(1);
}
if (args.version) {
console.log(require(path.join(__dirname, "..", "package.json")).version);
process.exit(0);
}
var report = "chalk";
if (args.report !== undefined) {
report = args.report;
}
checkNodeVersion();
function checkNodeVersion() {
var nodeVersionString = process.versions.node;
var nodeVersion = _.map(_.split(nodeVersionString, '.'), _.parseInt);
if((nodeVersion[0] === 0 && nodeVersion[1] < 11) ||
(nodeVersion[0] === 0 && nodeVersion[1] === 11 && nodeVersion[2] < 13)) {
console.log("using node v" + nodeVersionString);
console.error("elm-test requires node v0.11.13 or greater - upgrade the installed version of node and try again");
process.exit(1);
}
}
if (args._[0] == "init") {
var copyTemplate = function(templateName, destName) {
if (arguments.length == 1) {
destName = templateName;
}
var source = path.resolve(__dirname, "../templates/" + templateName);
var destination = path.resolve("tests", destName);
if (fs.existsSync(destination)) {
console.log(destination + " already exists");
} else {
fs.copySync(source, destination);
console.log("Created " + destination);
}
};
var ensureDirectory = function(dirName) {
var destination = path.resolve(".", dirName);
if (fs.existsSync(destination)) {
console.log(destination + " already exists");
} else {
fs.mkdirSync(destination);
console.log("Created " + destination);
}
};
var elmOptions = "";
if (args.yes) {
elmOptions += " --yes";
}
ensureDirectory("src");
ensureDirectory("tests");
copyTemplate("elm-package.json");
copyTemplate("Main.elm");
copyTemplate("Tests.elm");
copyTemplate("gitignore", ".gitignore");
process.exit(0);
}
function evalElmCode (compiledCode, testModuleName) {
var Elm = function(module) { eval(compiledCode); return module.exports; }({});
// Make sure necessary things are defined.
if (typeof Elm === 'undefined') { throw 'elm-io config error: Elm is not defined. Make sure you provide a file compiled by Elm!'; }
function getTestModule() {
var module = Elm;
var moduleParts = testModuleName.split('.');
while (moduleParts.length) {
var modulePart = moduleParts.shift();
if (module[modulePart] === undefined) {
return undefined;
}
module = module[modulePart];
}
return module;
}
var testModule = getTestModule();
if (testModule === undefined) { throw 'Elm.' + testModuleName + ' is not defined. Make sure you provide a file compiled by Elm!'; }
var initialSeed = null;
if (args.seed !== undefined) {
initialSeed = args.seed;
}
// Apply Node polyfills as necessary.
var window = {Date: Date, addEventListener: function() {}, removeEventListener: function() {}};
var document = {body: {}, createTextNode: function() {}};
pathToMake = args.compiler;
if (typeof XMLHttpRequest === 'undefined') { XMLHttpRequest = function() { return { addEventListener: function() {}, open: function() {}, send: function() {} }; }; }
if (typeof FormData === 'undefined') { FormData = function () { this._data = []; }; FormData.prototype.append = function () { this._data.push(Array.prototype.slice.call(arguments)); }; }
// Fix Windows Unicode problems. Credit to https://github.com/sindresorhus/figures for the Windows compat idea!
var windowsSubstitutions = [[/[↓✗►]/g, '>'], [/╵│╷╹┃╻/g, '|'], [/═/g, '='],, [/▔/g, '-'], [/✔/g, '√']];
function windowsify(str) {
return windowsSubstitutions.reduce(
function(result, sub) { return result.replace(sub[0], sub[1]); }, str);
}
function chalkify(messages) {
return messages.map(function(msg) {
var path = msg.styles;
var text = process.platform === 'win32' ? windowsify(msg.text) : msg.text;
if (path.length === 0) {
return text;
} else {
var fn = chalk;
path.forEach(function(nextPath) { fn = fn[nextPath]; });
return fn(text);
}
}).join('');
}
// Run the Elm app.
var app = testModule.worker({seed: initialSeed, report: report});
// Receive messages from ports and translate them into appropriate JS calls.
app.ports.emit.subscribe(function(msg) {
var msgType = msg[0];
var data = msg[1];
if (msgType === 'FINISHED') {
if (data.format === "CHALK") {
console.log(chalkify(data.message));
} else {
console.log(JSON.stringify(data.message));
}
if (!args.watch) {
process.exit(data.exitCode);
}
} else if (msgType === "STARTED" || msgType === "TEST_COMPLETED") {
if (data.format === "CHALK") {
console.log(chalkify(data.message));
} else {
console.log(JSON.stringify(data.message));
}
}
});
}
var testFile = args._[0],
cwd = __dirname,
pathToMake = undefined;
function spawnCompiler(cmd, args, opts) {
var compilerOpts =
_.defaults({stdio: [process.stdin, report === "chalk" ? process.stdout : 'ignore', process.stderr] }, opts);
return spawn(cmd, args, compilerOpts);
}
if (typeof testFile == "undefined") {
testFile = "tests/Main.elm";
}
if (args.compiler !== undefined) {
pathToMake = args.compiler;
if (!pathToMake) {
console.error("The --compiler option must be given a path to an elm-make executable.");
process.exit(1);
}
}
if (!fs.existsSync(testFile)) {
console.error("Could not find file " + testFile);
process.exit(1);
}
var testModulePromise = firstline(testFile).then(function (testModuleLine) {
var moduleMatches = testModuleLine.match(/^(?:port\s+)?module\s+([^\s]+)/);
if (moduleMatches) {
return moduleMatches[1];
}
return 'Main';
});
function infoLog(msg) {
if (report === "chalk") {
console.log(msg);
}
}
findUp('elm-package.json', { cwd: path.dirname(testFile) })
.then(function (elmPackagePath) {
var elmRootDir = path.dirname(elmPackagePath);
if (fs.realpathSync(elmRootDir) !== fs.realpathSync(process.cwd())) {
testFile = path.relative(elmRootDir, testFile);
process.chdir(elmRootDir);
}
if (args.watch) {
infoLog('Running in watch mode');
var watcher = chokidar.watch('**/*.elm', { ignoreInitial: true });
var eventNameMap = {
add: 'added',
addDir: 'added',
change: 'changed',
unlink: 'removed',
unlinkDir: 'removed',
};
watcher.on('all', function (event, filePath) {
var relativePath = path.relative(elmRootDir, filePath);
var eventName = eventNameMap[event] || event;
infoLog('\n' + relativePath + ' ' + eventName + '. Rebuilding!');
runTests();
});
}
function runTests () {
temp.open({ prefix:'elm_test_', suffix:'.js' }, function(err, info) {
var dest = info.path;
var compileProcess = compile( [testFile], {
output: dest,
verbose: args.verbose,
yes: true,
spawn: spawnCompiler,
pathToMake: pathToMake,
warn:args.warn
});
compileProcess.on('close', function(exitCode) {
if (exitCode !== 0) {
console.error("Compilation failed for", testFile);
if (!args.watch) {
process.exit(exitCode);
}
} else {
testModulePromise.then(function (testModuleName) {
evalElmCode(fs.readFileSync(dest, {encoding: "utf8"}), testModuleName);
});
}
});
});
}
runTests();
});
process.on('uncaughtException', function(error) {
if (/ an argument in Javascript/.test(error)) {
// Handle arg mismatch between js and elm code. Expected message from Elm:
// "You are giving module `Main` an argument in JavaScript.
// This module does not take arguments though! You probably need to change the
// initialization code to something like `Elm.Main.fullscreen()`]"
console.error("Error starting the node-test-runner.");
console.error("Please check your Javascript 'elm-test' and Elm 'node-test-runner' package versions are compatible");
} else {
console.error("Unhandled exception while running the tests:", error);
}
});