Add part7

This commit is contained in:
Richard Feldman
2018-05-05 08:04:50 -04:00
parent 8ae366a175
commit c34810f421
576 changed files with 79147 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,46 @@
sudo: false
cache:
directories:
- test/elm-stuff/build-artifacts
- sysconfcpus
os:
- osx
- linux
env:
matrix:
- ELM_VERSION=0.18.0 TARGET_NODE_VERSION=node
- ELM_VERSION=0.18.0 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-2017 the Elm-test contributors
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,152 @@
# 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/elm-community/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/Example.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/master/.travis.yml)
* [`appveyor.yml`](https://github.com/rtfeldman/elm-css/blob/master/appveyor.yml)
### Not running tests
During development, you'll often want to focus on specific tests, silence failing tests, or jot down many ideas for tests that you can't implement all at once. We've got you covered with `skip`, `only`, and `todo`:
```elm
wipSuite : Test
wipSuite =
describe "skip, only, and todo"
[ only <| describe "Marking this test as `only` means no other tests will be run!"
[ test "This test will be run" <|
\_ -> 1 + 1 |> Expect.equal 2
, skip <| test "This test will be skipped, even though it's in an only!" <|
\_ -> 2 + 3 |> Expect.equal 4
]
, test "This test will be skipped because it has no only" <|
\_ -> "left" |> Expect.equal "right"
, todo "Make sure all splines are reticulated"
]
```
If you run this example, or any suite that uses one of these three functions, it will result in an _incomplete_ test run. No tests failed, but you also didn't run your entire suite, so we can't call it a success either. Incomplete test runs are reported to CI systems as indistinguishable from failed test runs, to safeguard against accidentally committing a gutted test suite!
## Strategies for effective testing
1. [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.
1. 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!
1. If your API is difficult for you to test, it will be difficult for someone else to use. You are your API's first client.
1. Prefer fuzz tests to unit tests, where possible. If you have a union type with a small number of values, list them all and map over the list with a unit test for each. Unit tests are also great for when you know the edge cases, and for regression tests.
1. If you're writing a library that wraps an existing standard or protocol, use examples from the specification or docs as unit tests.
1. 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.
1. Tests should be small and focused: call the code under test and set an expectation about the result. Setup code should be moved into reusable functions, or custom fuzzers. For example, a test to remove an element from a data structure should be given a nonempty data structure; it should not have to create one itself.
1. If you find yourself inspecting the fuzzed input and making different expectations based on it, split each code path into its own test with a fuzzer that makes only the right kind of values.
1. Consider using [elm-verify-examples](https://github.com/stoeffel/elm-verify-examples) to extract examples in your docs into unit tests.
1. 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!)
1. 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.
### Application-specific techniques
There are a few extra ideas that apply to testing webapps and reusable view packages:
1. Avoid importing your `Main` module. Most of your code belongs in other modules, so import those instead.
1. Test your views using [elm-html-test](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest).
1. To test effects, consider using [elm-testable](http://package.elm-lang.org/packages/rogeriochaves/elm-testable/latest).
1. There is currently no Elm solution for integration or end-to-end testing. Use your favorite PhantomJS or Selenium webdriver, such as Capybara.
## Upgrading
### From 3.1.0
Make sure you grab the latest versions of the test runner that you are using:
* `npm update -g elm-test`
* `elm package install rtfeldman/html-test-runner`
`Fuzz.frequency` now fails the test if the frequency is invalid, rather than return a `Result`. If you are using this function, you can remove your `Err` handling code. More likely you are using `Fuzz.frequencyOrCrash`, which you can replace with `Fuzz.frequency`.
Instead of using `Test.filter` to avoid running tests, use `skip` and `only` (see above for documentation).
We now forbid tests and suites to have descriptions that are blank, or that are identical across siblings or parents and children. If you get failures from this, rename your tests to be clearer about what they're testing.
### 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 install the latest version (`npm update -g elm-test`) and pull down the new `Main.elm`: `curl -o tests/Main.elm https://raw.githubusercontent.com/rtfeldman/node-test-runner/3.0.1/templates/Main.elm`
### From 1.x and elm-check
[`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 |
| ------- | ----- |
| [**4.0.0**](https://github.com/elm-community/elm-test/tree/4.0.0) | Add `only`, `skip`, `todo`; change `Fuzz.frequency` to fail rather than crash on bad input, disallow tests with blank or duplicate descriptions.
| [**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,88 @@
module Main exposing (..)
import Benchmark exposing (..)
import Benchmark.Runner as Runner
import Expect exposing (Expectation)
import Random.Pcg
import Snippets
import Test.Internal exposing (Test(Labeled, Test))
main : Runner.BenchmarkProgram
main =
Runner.program suite
suite : Benchmark
suite =
describe "Fuzz"
[ describe "int"
[ benchmark "generating" (benchTest Snippets.intPass)
, benchmark "shrinking" (benchTest Snippets.intFail)
]
, describe "intRange"
[ benchmark "generating" (benchTest Snippets.intRangePass)
, benchmark "shrinking" (benchTest Snippets.intRangeFail)
]
, describe "string"
[ benchmark "generating" (benchTest Snippets.stringPass)
, benchmark "shrinking" (benchTest Snippets.stringFail)
]
, describe "float"
[ benchmark "generating" (benchTest Snippets.floatPass)
, benchmark "shrinking" (benchTest Snippets.floatFail)
]
, describe "bool"
[ benchmark "generating" (benchTest Snippets.boolPass)
, benchmark "shrinking" (benchTest Snippets.boolFail)
]
, describe "char"
[ benchmark "generating" (benchTest Snippets.charPass)
, benchmark "shrinking" (benchTest Snippets.charFail)
]
, describe "list of int"
[ benchmark "generating" (benchTest Snippets.listIntPass)
, benchmark "shrinking" (benchTest Snippets.listIntFail)
]
, describe "maybe of int"
[ benchmark "generating" (benchTest Snippets.maybeIntPass)
, benchmark "shrinking" (benchTest Snippets.maybeIntFail)
]
, describe "result of string and int"
[ benchmark "generating" (benchTest Snippets.resultPass)
, benchmark "shrinking" (benchTest Snippets.resultFail)
]
, describe "map"
[ benchmark "generating" (benchTest Snippets.mapPass)
, benchmark "shrinking" (benchTest Snippets.mapFail)
]
, describe "andMap"
[ benchmark "generating" (benchTest Snippets.andMapPass)
, benchmark "shrinking" (benchTest Snippets.andMapFail)
]
, describe "map5"
[ benchmark "generating" (benchTest Snippets.map5Pass)
, benchmark "shrinking" (benchTest Snippets.map5Fail)
]
, describe "andThen"
[ benchmark "generating" (benchTest Snippets.andThenPass)
, benchmark "shrinking" (benchTest Snippets.andThenFail)
]
, describe "conditional"
[ benchmark "generating" (benchTest Snippets.conditionalPass)
, benchmark "shrinking" (benchTest Snippets.conditionalFail)
]
]
benchTest : Test -> (() -> List Expectation)
benchTest test =
case test of
Test fn ->
\_ -> fn (Random.Pcg.initialSeed 0) 10
Labeled _ test ->
benchTest test
test ->
Debug.crash <| "No support for benchmarking this type of test: " ++ toString test

View File

@@ -0,0 +1,34 @@
# Benchmarks for elm-test
These are some benchmarks of the elm-test library built using the excellent [elm-benchmark](https://github.com/BrianHicks/elm-benchmark).
## How to run
```sh
cd ./benchmarks
elm-make Main.elm
open index.html
```
## How to use
These benchmarks can help get an idea of the performance impact of a change in the elm-test code.
Beware however that a fifty percent performance increase in these benchmarks will most likely not translate to a fifty percent faster tests for users.
In real word scenario's the execution of the test body will have a significant impact on the running time of the test suite, an aspect we're not testing here because it's different for every test suite.
To get a feeling for the impact your change has on actual test run times try running some real test suites with and without your changes.
## Benchmarking complete test suites
These are some examples of test suites that contain a lot of fuzzer tests:
- [elm-benchmark](https://github.com/BrianHicks/elm-benchmark)
- [elm-nonempty-list](https://github.com/mgold/elm-nonempty-list)
- [json-elm-schema](https://github.com/NoRedInk/json-elm-schema)
A tool you can use for benchmarking the suite is [bench](https://github.com/Gabriel439/bench).
To run the tests using your modified code (this only works if your modified version is backwards compatible with the version of elm-test currenlty in use by the test suite):
- In your test suite directories `elm-package.json`:
- Remove the dependency on `elm-test`.
- Add dependecies of `elm-test` as dependencies of the test suite itself.
- Add the path to your changed elm-test src directory to your `source-directories`.
It will be something like `/<projects-dir>/elm-test/src`.
- Run `elm-test` once to trigger compilation.
- Now run `elm-test` with your benchmarking tool.

View File

@@ -0,0 +1,250 @@
module Snippets exposing (..)
import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import Test exposing (Test, fuzz)
intPass : Test
intPass =
fuzz Fuzz.int "(passes) int" <|
\_ ->
Expect.pass
intFail : Test
intFail =
fuzz Fuzz.int "(fails) int" <|
\numbers ->
Expect.fail "Failed"
intRangePass : Test
intRangePass =
fuzz (Fuzz.intRange 10 100) "(passes) intRange" <|
\_ ->
Expect.pass
intRangeFail : Test
intRangeFail =
fuzz (Fuzz.intRange 10 100) "(fails) intRange" <|
\numbers ->
Expect.fail "Failed"
stringPass : Test
stringPass =
fuzz Fuzz.string "(passes) string" <|
\_ ->
Expect.pass
stringFail : Test
stringFail =
fuzz Fuzz.string "(fails) string" <|
\numbers ->
Expect.fail "Failed"
floatPass : Test
floatPass =
fuzz Fuzz.float "(passes) float" <|
\_ ->
Expect.pass
floatFail : Test
floatFail =
fuzz Fuzz.float "(fails) float" <|
\numbers ->
Expect.fail "Failed"
boolPass : Test
boolPass =
fuzz Fuzz.bool "(passes) bool" <|
\_ ->
Expect.pass
boolFail : Test
boolFail =
fuzz Fuzz.bool "(fails) bool" <|
\numbers ->
Expect.fail "Failed"
charPass : Test
charPass =
fuzz Fuzz.char "(passes) char" <|
\_ ->
Expect.pass
charFail : Test
charFail =
fuzz Fuzz.char "(fails) char" <|
\numbers ->
Expect.fail "Failed"
listIntPass : Test
listIntPass =
fuzz (Fuzz.list Fuzz.int) "(passes) list of int" <|
\_ ->
Expect.pass
listIntFail : Test
listIntFail =
fuzz (Fuzz.list Fuzz.int) "(fails) list of int" <|
{- The empty list is the first value the list shrinker will try.
If we immediately fail on that example than we're not doing a lot of shrinking.
-}
Expect.notEqual []
maybeIntPass : Test
maybeIntPass =
fuzz (Fuzz.maybe Fuzz.int) "(passes) maybe of int" <|
\_ ->
Expect.pass
maybeIntFail : Test
maybeIntFail =
fuzz (Fuzz.maybe Fuzz.int) "(fails) maybe of int" <|
\numbers ->
Expect.fail "Failed"
resultPass : Test
resultPass =
fuzz (Fuzz.result Fuzz.string Fuzz.int) "(passes) result of string and int" <|
\_ ->
Expect.pass
resultFail : Test
resultFail =
fuzz (Fuzz.result Fuzz.string Fuzz.int) "(fails) result of string and int" <|
\numbers ->
Expect.fail "Failed"
mapPass : Test
mapPass =
fuzz even "(passes) map" <|
\_ -> Expect.pass
mapFail : Test
mapFail =
fuzz even "(fails) map" <|
\_ -> Expect.fail "Failed"
andMapPass : Test
andMapPass =
fuzz person "(passes) andMap" <|
\_ -> Expect.pass
andMapFail : Test
andMapFail =
fuzz person "(fails) andMap" <|
\_ -> Expect.fail "Failed"
map5Pass : Test
map5Pass =
fuzz person2 "(passes) map5" <|
\_ -> Expect.pass
map5Fail : Test
map5Fail =
fuzz person2 "(fails) map5" <|
\_ -> Expect.fail "Failed"
andThenPass : Test
andThenPass =
fuzz (variableList 2 5 Fuzz.int) "(passes) andThen" <|
\_ -> Expect.pass
andThenFail : Test
andThenFail =
fuzz (variableList 2 5 Fuzz.int) "(fails) andThen" <|
\_ -> Expect.fail "Failed"
conditionalPass : Test
conditionalPass =
fuzz evenWithConditional "(passes) conditional" <|
\_ -> Expect.pass
conditionalFail : Test
conditionalFail =
fuzz evenWithConditional "(fails) conditional" <|
\_ -> Expect.fail "Failed"
type alias Person =
{ firstName : String
, lastName : String
, age : Int
, nationality : String
, height : Float
}
person : Fuzzer Person
person =
Fuzz.map Person Fuzz.string
|> Fuzz.andMap Fuzz.string
|> Fuzz.andMap Fuzz.int
|> Fuzz.andMap Fuzz.string
|> Fuzz.andMap Fuzz.float
person2 : Fuzzer Person
person2 =
Fuzz.map5 Person
Fuzz.string
Fuzz.string
Fuzz.int
Fuzz.string
Fuzz.float
even : Fuzzer Int
even =
Fuzz.map ((*) 2) Fuzz.int
variableList : Int -> Int -> Fuzzer a -> Fuzzer (List a)
variableList min max item =
Fuzz.intRange min max
|> Fuzz.andThen (\length -> List.repeat length item |> sequence)
sequence : List (Fuzzer a) -> Fuzzer (List a)
sequence fuzzers =
List.foldl
(Fuzz.map2 (::))
(Fuzz.constant [])
fuzzers
evenWithConditional : Fuzzer Int
evenWithConditional =
Fuzz.intRange 1 6
|> Fuzz.conditional
{ retries = 3
, fallback = (+) 1
, condition = \n -> (n % 2) == 0
}

View File

@@ -0,0 +1,21 @@
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
".",
"../src"
],
"exposed-modules": [],
"dependencies": {
"BrianHicks/elm-benchmark": "1.0.2 <= v < 2.0.0",
"eeue56/elm-lazy-list": "1.0.0 <= v < 2.0.0",
"eeue56/elm-shrink": "1.0.0 <= v < 2.0.0",
"elm-lang/core": "5.1.1 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0",
"eeue56/elm-lazy": "1.0.0 <= v < 2.0.0",
"mgold/elm-random-pcg": "5.0.0 <= v < 6.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

View File

@@ -0,0 +1,24 @@
{
"version": "4.1.1",
"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",
"Test.Runner.Failure",
"Expect",
"Fuzz"
],
"dependencies": {
"eeue56/elm-lazy-list": "1.0.0 <= v < 2.0.0",
"eeue56/elm-shrink": "1.0.0 <= v < 2.0.0",
"elm-lang/core": "5.0.0 <= v < 6.0.0",
"eeue56/elm-lazy": "1.0.0 <= v < 2.0.0",
"mgold/elm-random-pcg": "4.0.2 <= v < 6.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

View File

@@ -0,0 +1,819 @@
module Expect
exposing
( Expectation
, FloatingPointTolerance(Absolute, AbsoluteOrRelative, Relative)
, all
, atLeast
, atMost
, equal
, equalDicts
, equalLists
, equalSets
, err
, fail
, false
, greaterThan
, lessThan
, notEqual
, notWithin
, onFail
, pass
, true
, within
)
{-| 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)`
- [Floating Point Comparisons](#floating-point-comparisons)
## Basic Expectations
@docs Expectation, equal, notEqual, all
## Numeric Comparisons
@docs lessThan, atMost, greaterThan, atLeast
## Floating Point Comparisons
These functions allow you to compare `Float` values up to a specified rounding error, which may be relative, absolute,
or both. For an in-depth look, see our [Guide to Floating Point Comparison](#guide-to-floating-point-comparison).
@docs FloatingPointTolerance, within, notWithin
## Booleans
@docs true, false
## Collections
@docs err, equalLists, equalDicts, equalSets
## Customizing
These functions will let you build your own expectations.
@docs pass, fail, onFail
## Guide to Floating Point Comparison
In general, if you are multiplying, you want relative tolerance, and if you're adding,
you want absolute tolerance. If you are doing both, you want both kinds of tolerance,
or to split the calculation into smaller parts for testing.
### Absolute Tolerance
Let's say we want to figure out if our estimation of pi is precise enough.
Is `3.14` within `0.01` of `pi`? Yes, because `3.13 < pi < 3.15`.
test "3.14 approximates pi with absolute precision" <| \_ ->
3.14 |> Expect.within (Absolute 0.01) pi
### Relative Tolerance
What if we also want to know if our circle circumference estimation is close enough?
Let's say our circle has a radius of `r` meters. The formula for circle circumference is `C=2*r*pi`.
To make the calculations a bit easier ([ahem](https://tauday.com/tau-manifesto)), we'll look at half the circumference; `C/2=r*pi`.
Is `r * 3.14` within `0.01` of `r * pi`?
That depends, what does `r` equal? If `r` is `0.01`mm, or `0.00001` meters, we're comparing
`0.00001 * 3.14 - 0.01 < r * pi < 0.00001 * 3.14 + 0.01` or `-0.0099686 < 0.0000314159 < 0.0100314`.
That's a huge tolerance! A circumference that is _a thousand times longer_ than we expected would pass that test!
On the other hand, if `r` is very large, we're going to need many more digits of pi.
For an absolute tolerance of `0.01` and a pi estimation of `3.14`, this expectation only passes if `r < 2*pi`.
If we use a relative tolerance of `0.01` instead, the circle area comparison becomes much better. Is `r * 3.14` within
`1%` of `r * pi`? Yes! In fact, three digits of pi approximation is always good enough for a 0.1% relative tolerance,
as long as `r` isn't [too close to zero](https://en.wikipedia.org/wiki/Denormal_number).
fuzz
(floatRange 0.000001 100000)
"Circle half-circumference with relative tolerance"
(\r -> r * 3.14 |> Expect.within (Relative 0.001) (r * pi))
### Trouble with Numbers Near Zero
If you are adding things near zero, you probably want absolute tolerance. If you're comparing values between `-1` and `1`, you should consider using absolute tolerance.
For example: Is `1 + 2 - 3` within `1%` of `0`? Well, if `1`, `2` and `3` have any amount of rounding error, you might not get exactly zero. What is `1%` above and below `0`? Zero. We just lost all tolerance. Even if we hard-code the numbers, we might not get exactly zero; `0.1 + 0.2` rounds to a value just above `0.3`, since computers, counting in binary, cannot write down any of those three numbers using a finite number of digits, just like we cannot write `0.333...` exactly in base 10.
Another example is comparing values that are on either side of zero. `0.0001` is more than `100%` away from `-0.0001`. In fact, `infinity` is closer to `0.0001` than `0.0001` is to `-0.0001`, if you are using a relative tolerance. Twice as close, actually. So even though both `0.0001` and `-0.0001` could be considered very close to zero, they are very far apart relative to each other. The same argument applies for any number of zeroes.
-}
import Dict exposing (Dict)
import Set exposing (Set)
import Test.Expectation
import Test.Runner.Failure exposing (InvalidReason(..), Reason(..))
{-| 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 =
equateWith "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 =
equateWith "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" (>=)
{-| A type to describe how close a floating point number must be to the expected value for the test to pass. This may be
specified as absolute or relative.
`AbsoluteOrRelative` tolerance uses a logical OR between the absolute (specified first) and relative tolerance. If you
want a logical AND, use [`Expect.all`](#all).
-}
type FloatingPointTolerance
= Absolute Float
| Relative Float
| AbsoluteOrRelative Float Float
{-| Passes if the second and third arguments are equal within a tolerance
specified by the first argument. This is intended to avoid failing because of
minor inaccuracies introduced by floating point arithmetic.
-- Fails because 0.1 + 0.2 == 0.30000000000000004 (0.1 is non-terminating in base 2)
0.1 + 0.2 |> Expect.equal 0.3
-- So instead write this test, which passes
0.1 + 0.2 |> Expect.within (Absolute 0.000000001) 0.3
Failures resemble code written in pipeline style, so you can tell
which argument is which:
-- Fails because 3.14 is not close enough to pi
3.14 |> Expect.within (Absolute 0.0001) pi
{-
3.14
Expect.within Absolute 0.0001
3.141592653589793
-}
-}
within : FloatingPointTolerance -> Float -> Float -> Expectation
within tolerance a b =
nonNegativeToleranceError tolerance "within" <|
compareWith ("Expect.within " ++ toString tolerance)
(withinCompare tolerance)
a
b
{-| Passes if (and only if) a call to `within` with the same arguments would have failed.
-}
notWithin : FloatingPointTolerance -> Float -> Float -> Expectation
notWithin tolerance a b =
nonNegativeToleranceError tolerance "notWithin" <|
compareWith ("Expect.notWithin " ++ toString tolerance)
(\a b -> not <| withinCompare tolerance a b)
a
b
{-| 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
[`Result`](http://package.elm-lang.org/packages/elm-lang/core/latest/Result) is
an `Err` rather than `Ok`. This is useful for tests where you expect to get an
error but you don't care about what the actual error is. If your possibly
erroring function returns a `Maybe`, simply use `Expect.equal Nothing`.
-- Passes
String.toInt "not an int"
|> Expect.err
Test failures will be printed with the unexpected `Ok` value contrasting with
any `Err`.
-- Fails
String.toInt "20"
|> Expect.err
{-
Ok 20
Expect.err
Err _
-}
-}
err : Result a b -> Expectation
err result =
case result of
Ok _ ->
{ description = "Expect.err"
, reason = Comparison "Err _" (toString result)
}
|> Test.Expectation.fail
Err _ ->
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
{ description = "Expect.equalLists"
, reason = ListDiff (List.map toString expected) (List.map toString actual)
}
|> Test.Expectation.fail
{-| 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
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
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 str =
Test.Expectation.fail { description = str, reason = Custom }
{-| 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 failure ->
Test.Expectation.Fail { failure | description = str, reason = Custom }
{-| Passes if each of the given functions passes when applied to the subject.
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
Test.Expectation.fail
{ reason = Invalid EmptyList
, description = "Expect.all was given an empty list. You must make at least one expectation to have a valid 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
{---- Private helper functions ----}
reportFailure : String -> String -> String -> Expectation
reportFailure comparison expected actual =
{ description = comparison
, reason = Comparison (toString expected) (toString actual)
}
|> Test.Expectation.fail
reportCollectionFailure : String -> a -> b -> List c -> List d -> Expectation
reportCollectionFailure comparison expected actual missingKeys extraKeys =
{ description = comparison
, reason =
{ expected = toString expected
, actual = toString actual
, extra = List.map toString extraKeys
, missing = List.map toString missingKeys
}
|> CollectionDiff
}
|> Test.Expectation.fail
{-| String arg is label, e.g. "Expect.equal".
-}
equateWith : String -> (a -> b -> Bool) -> b -> a -> Expectation
equateWith =
testWith Equality
compareWith : String -> (a -> b -> Bool) -> b -> a -> Expectation
compareWith =
testWith Comparison
testWith : (String -> String -> Reason) -> String -> (a -> b -> Bool) -> b -> a -> Expectation
testWith makeReason label runTest expected actual =
if runTest actual expected then
pass
else
{ description = label
, reason = makeReason (toString expected) (toString actual)
}
|> Test.Expectation.fail
{---- Private *floating point* helper functions ----}
absolute : FloatingPointTolerance -> Float
absolute tolerance =
case tolerance of
Absolute absolute ->
absolute
AbsoluteOrRelative absolute _ ->
absolute
_ ->
0
relative : FloatingPointTolerance -> Float
relative tolerance =
case tolerance of
Relative relative ->
relative
AbsoluteOrRelative _ relative ->
relative
_ ->
0
nonNegativeToleranceError : FloatingPointTolerance -> String -> Expectation -> Expectation
nonNegativeToleranceError tolerance name result =
if absolute tolerance < 0 && relative tolerance < 0 then
Test.Expectation.fail { description = "Expect." ++ name ++ " was given negative absolute and relative tolerances", reason = Custom }
else if absolute tolerance < 0 then
Test.Expectation.fail { description = "Expect." ++ name ++ " was given a negative absolute tolerance", reason = Custom }
else if relative tolerance < 0 then
Test.Expectation.fail { description = "Expect." ++ name ++ " was given a negative relative tolerance", reason = Custom }
else
result
withinCompare : FloatingPointTolerance -> Float -> Float -> Bool
withinCompare tolerance a b =
let
withinAbsoluteTolerance =
a - absolute tolerance <= b && b <= a + absolute tolerance
withinRelativeTolerance =
(a * (1 - relative tolerance) <= b && b <= a * (1 + relative tolerance))
|| (b * (1 - relative tolerance) <= a && a <= b * (1 + relative tolerance))
in
(a == b) || withinAbsoluteTolerance || withinRelativeTolerance

View File

@@ -0,0 +1,78 @@
module Float
exposing
( epsilon
, infinity
, maxAbsValue
, minAbsNormal
, minAbsValue
, nan
)
{-| Float contains useful constants related to 64 bit floating point numbers,
as specified in IEEE 754-2008.
@docs epsilon, infinity, nan, minAbsNormal, minAbsValue, maxAbsValue
-}
{-| Largest possible rounding error in a single 64 bit floating point
calculation on an x86-x64 CPU. Also known as the Machine Epsilon.
If you do not know what tolerance you should use, use this number, multiplied
by the number of floating point operations you're doing in your calculation.
According to the [MSDN system.double.epsilon documentation]
(<https://msdn.microsoft.com/en-us/library/system.double.epsilon.aspx#Remarks>),
ARM has a machine epsilon that is too small to represent in a 64 bit float,
so we're simply ignoring that. On phones, tablets, raspberry pi's and other
devices with ARM chips, you might get slightly better precision than we assume
here.
-}
epsilon : Float
epsilon =
2.0 ^ -52
{-| Positive infinity. Negative infinity is just -infinity.
-}
infinity : Float
infinity =
1.0 / 0.0
{-| Not a Number. NaN does not compare equal to anything, including itself.
Any operation including NaN will result in NaN.
-}
nan : Float
nan =
0.0 / 0.0
{-| Smallest possible value which still has full precision.
Values closer to zero are denormalized, which means that they are
using some of the significant bits to emulate a slightly larger mantissa.
The number of significant binary digits is proportional to the binary
logarithm of the denormalized number; halving a denormalized number also
halves the precision of that number.
-}
minAbsNormal : Float
minAbsNormal =
2.0 ^ -1022
{-| Smallest absolute value representable in a 64 bit float.
-}
minAbsValue : Float
minAbsValue =
2.0 ^ -1074
{-| Largest finite absolute value representable in a 64 bit float.
-}
maxAbsValue : Float
maxAbsValue =
(2.0 - epsilon) * 2.0 ^ 1023

View File

@@ -0,0 +1,748 @@
module Fuzz exposing (Fuzzer, andMap, andThen, array, bool, char, conditional, constant, custom, float, floatRange, frequency, int, intRange, invalid, list, map, map2, map3, map4, map5, maybe, oneOf, order, percentage, result, string, tuple, tuple3, tuple4, tuple5, unit)
{-| 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, conditional
@docs Fuzzer, oneOf, constant, map, map2, map3, map4, map5, andMap, andThen, frequency, conditional
## Tuple Fuzzers
Instead of using a tuple, consider using `fuzzN`.
@docs tuple, tuple3, tuple4, tuple5
## Uncommon Fuzzers
@docs custom, char, unit, order, invalid
-}
import Array exposing (Array)
import Char
import Fuzz.Internal as Internal
exposing
( Fuzzer
, Valid
, ValidFuzzer
, combineValid
, invalidReason
)
import Lazy
import Lazy.List exposing ((+++), LazyList)
import Random.Pcg as Random exposing (Generator)
import RoseTree exposing (RoseTree(..))
import Shrink exposing (Shrinker)
import Util exposing (..)
{-| 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.lazy <| \_ -> Lazy.force <| Lazy.List.map shrinkTree (shrinker a))
in
Ok <|
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 =
RoseTree.singleton ()
|> Random.constant
|> Ok
{-| 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 =
if hi < lo then
Err <| "Fuzz.intRange was given a lower bound of " ++ toString lo ++ " which is greater than the upper bound, " ++ toString hi ++ "."
else
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 =
if hi < lo then
Err <| "Fuzz.floatRange was given a lower bound of " ++ toString lo ++ " which is greater than the upper bound, " ++ toString hi ++ "."
else
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 and the extended character set.
-}
char : Fuzzer Char
char =
custom asciiCharGenerator Shrink.character
asciiCharGenerator : Generator Char
asciiCharGenerator =
Random.map Char.fromCode (Random.int 32 126)
whitespaceCharGenerator : Generator Char
whitespaceCharGenerator =
Random.sample [ ' ', '\t', '\n' ] |> Random.map (Maybe.withDefault ' ')
{-| Generates random printable ASCII strings of up to 1000 characters.
Shorter strings are more common, especially the empty string.
-}
string : Fuzzer String
string =
let
asciiGenerator : Generator String
asciiGenerator =
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 asciiCharGenerator)
whitespaceGenerator : Generator String
whitespaceGenerator =
Random.int 1 10
|> Random.andThen (lengthString whitespaceCharGenerator)
in
custom
(Random.frequency
[ ( 9, asciiGenerator )
, ( 1, whitespaceGenerator )
]
)
Shrink.string
{-| Given a fuzzer of a type, create a fuzzer of a maybe for that type.
-}
maybe : Fuzzer a -> Fuzzer (Maybe a)
maybe fuzzer =
let
toMaybe : Bool -> RoseTree a -> RoseTree (Maybe a)
toMaybe useNothing tree =
if useNothing then
RoseTree.singleton Nothing
else
RoseTree.map Just tree |> RoseTree.addChild (RoseTree.singleton Nothing)
in
(Result.map << Random.map2 toMaybe) (Random.oneIn 4) fuzzer
{-| 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 fuzzerError fuzzerValue =
let
toResult : Bool -> RoseTree error -> RoseTree value -> RoseTree (Result error value)
toResult useError errorTree valueTree =
if useError then
RoseTree.map Err errorTree
else
RoseTree.map Ok valueTree
in
(Result.map2 <| Random.map3 toResult (Random.oneIn 4)) fuzzerError fuzzerValue
{-| 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 fuzzer =
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
fuzzer
|> Result.map
(\validFuzzer ->
genLength
|> Random.andThen (flip Random.list validFuzzer)
|> Random.map listShrinkHelp
)
listShrinkHelp : List (RoseTree a) -> RoseTree (List a)
listShrinkHelp listOfTrees =
{- This extends listShrinkRecurse algorithm with an attempt to shrink directly to the empty list. -}
listShrinkRecurse listOfTrees
|> mapChildren (Lazy.List.cons <| RoseTree.singleton [])
mapChildren : (LazyList (RoseTree a) -> LazyList (RoseTree a)) -> RoseTree a -> RoseTree a
mapChildren fn (Rose root children) =
Rose root (fn children)
listShrinkRecurse : List (RoseTree a) -> RoseTree (List a)
listShrinkRecurse 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, remove elements at various positions in the list.
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
dropFirstHalf : List (RoseTree a) -> RoseTree (List a)
dropFirstHalf list_ =
List.drop (List.length list_ // 2) list_
|> listShrinkRecurse
dropSecondHalf : List (RoseTree a) -> RoseTree (List a)
dropSecondHalf list_ =
List.take (List.length list_ // 2) list_
|> listShrinkRecurse
halved : LazyList (RoseTree (List a))
halved =
-- The list halving shortcut is useful only for large lists.
-- For small lists attempting to remove elements one by one is good enough.
if n >= 8 then
Lazy.lazy <|
\_ ->
Lazy.List.fromList [ dropFirstHalf listOfTrees, dropSecondHalf listOfTrees ]
|> Lazy.force
else
Lazy.List.empty
shrinkOne prefix list =
case list of
[] ->
Lazy.List.empty
(Rose x shrunkenXs) :: more ->
Lazy.List.map (\childTree -> prefix ++ (childTree :: more) |> listShrinkRecurse) shrunkenXs
shrunkenVals =
Lazy.lazy <|
\_ ->
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))
|> Lazy.force
shortened =
Lazy.lazy <|
\_ ->
List.range 0 (n - 1)
|> Lazy.List.fromList
|> Lazy.List.map (\index -> removeOne index listOfTrees)
|> Lazy.List.map listShrinkRecurse
|> Lazy.force
removeOne index list =
List.append
(List.take index list)
(List.drop (index + 1) list)
in
Rose root (halved +++ shortened +++ shrunkenVals)
{-| 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 ( fuzzerA, fuzzerB ) =
map2 (,) fuzzerA fuzzerB
{-| Turn a 3-tuple of fuzzers into a fuzzer of 3-tuples.
-}
tuple3 : ( Fuzzer a, Fuzzer b, Fuzzer c ) -> Fuzzer ( a, b, c )
tuple3 ( fuzzerA, fuzzerB, fuzzerC ) =
map3 (,,) fuzzerA fuzzerB fuzzerC
{-| 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 ( fuzzerA, fuzzerB, fuzzerC, fuzzerD ) =
map4 (,,,) fuzzerA fuzzerB fuzzerC fuzzerD
{-| 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 ( fuzzerA, fuzzerB, fuzzerC, fuzzerD, fuzzerE ) =
map5 (,,,,) fuzzerA fuzzerB fuzzerC fuzzerD fuzzerE
{-| 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 =
Ok <| Random.constant (RoseTree.singleton x)
{-| Map a function over a fuzzer. This applies to both the generated and the shrunken values.
-}
map : (a -> b) -> Fuzzer a -> Fuzzer b
map =
Internal.map
{-| Map over two fuzzers.
-}
map2 : (a -> b -> c) -> Fuzzer a -> Fuzzer b -> Fuzzer c
map2 transform fuzzA fuzzB =
(Result.map2 << Random.map2 << map2RoseTree) transform fuzzA fuzzB
{-| Map over three fuzzers.
-}
map3 : (a -> b -> c -> d) -> Fuzzer a -> Fuzzer b -> Fuzzer c -> Fuzzer d
map3 transform fuzzA fuzzB fuzzC =
(Result.map3 << Random.map3 << map3RoseTree) transform 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 =
(Result.map4 << Random.map4 << map4RoseTree) transform 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 =
(Result.map5 << Random.map5 << map5RoseTree) transform fuzzA fuzzB fuzzC fuzzD fuzzE
{-| Map over many fuzzers. This can act as mapN for N > 5.
The argument order is meant to accommodate 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 =
Internal.andThen
{-| Conditionally filter a fuzzer to remove occasional undesirable
input. Takes a limit for how many retries to attempt, and a fallback
function to, if no acceptable input can be found, create one from an
unacceptable one. Also takes a condition to determine if the input is
acceptable or not, and finally the fuzzer itself.
A good number of max retries is ten. A large number of retries might
blow the stack.
-}
conditional : { retries : Int, fallback : a -> a, condition : a -> Bool } -> Fuzzer a -> Fuzzer a
conditional opts fuzzer =
Result.map (conditionalHelper opts) fuzzer
conditionalHelper : { retries : Int, fallback : a -> a, condition : a -> Bool } -> ValidFuzzer a -> ValidFuzzer a
conditionalHelper opts validFuzzer =
if opts.retries <= 0 then
Random.map
(RoseTree.map opts.fallback >> RoseTree.filterBranches opts.condition)
validFuzzer
else
validFuzzer
|> Random.andThen
(\tree ->
case RoseTree.filter opts.condition tree of
Just tree ->
Random.constant tree
Nothing ->
conditionalHelper { opts | retries = opts.retries - 1 } validFuzzer
)
{-| 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 )
]
There are a few circumstances in which this function will return an invalid
fuzzer, which causes it to fail any test that uses it:
- If you provide an empty list of frequencies
- If any of the weights are less than 0
- If the weights sum to 0
Be careful recursively using this fuzzer in its arguments. Often using `map`
is a better way to do what you want. If you are fuzzing a tree-like data
structure, you should include a depth limit so to avoid infinite recursion, like
so:
type Tree
= Leaf
| Branch Tree Tree
tree : Int -> Fuzzer Tree
tree i =
if i <= 0 then
Fuzz.constant Leaf
else
Fuzz.frequency
[ ( 1, Fuzz.constant Leaf )
, ( 2, Fuzz.map2 Branch (tree (i - 1)) (tree (i - 1)) )
]
-}
frequency : List ( Float, Fuzzer a ) -> Fuzzer a
frequency list =
if List.isEmpty list then
invalid "You must provide at least one frequency pair."
else if List.any (\( weight, _ ) -> weight < 0) list then
invalid "No frequency weights can be less than 0."
else if List.sum (List.map Tuple.first list) <= 0 then
invalid "Frequency weights must sum to more than 0."
else
list
|> List.map extractValid
|> combineValid
|> Result.map Random.frequency
extractValid : ( a, Valid b ) -> Valid ( a, b )
extractValid ( a, valid ) =
Result.map ((,) a) valid
{-| Choose one of the given fuzzers at random. Each fuzzer has an equal chance
of being chosen; to customize the probabilities, use [`frequency`](#frequency).
Fuzz.oneOf
[ Fuzz.intRange 0 3
, Fuzz.intRange 7 9
]
-}
oneOf : List (Fuzzer a) -> Fuzzer a
oneOf list =
if List.isEmpty list then
invalid "You must pass at least one Fuzzer to Fuzz.oneOf."
else
list
|> List.map (\fuzzer -> ( 1, fuzzer ))
|> frequency
{-| A fuzzer that is invalid for the provided reason. Any fuzzers built with it
are also invalid. Any tests using an invalid fuzzer fail.
-}
invalid : String -> Fuzzer a
invalid reason =
Err reason
map2RoseTree : (a -> b -> c) -> RoseTree a -> RoseTree b -> RoseTree c
map2RoseTree transform ((Rose root1 children1) as rose1) ((Rose root2 children2) as rose2) =
{- Shrinking a pair of RoseTrees
Recurse on all pairs 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 =
transform root1 root2
shrink1 =
Lazy.List.map (\subtree -> map2RoseTree transform subtree rose2) children1
shrink2 =
Lazy.List.map (\subtree -> map2RoseTree transform rose1 subtree) children2
in
Rose root (shrink1 +++ shrink2)
-- The RoseTree 'mapN, n > 2' functions below follow the same strategy as map2RoseTree.
-- They're implemented separately instead of in terms of `andMap` because this has significant perfomance benefits.
map3RoseTree : (a -> b -> c -> d) -> RoseTree a -> RoseTree b -> RoseTree c -> RoseTree d
map3RoseTree transform ((Rose root1 children1) as rose1) ((Rose root2 children2) as rose2) ((Rose root3 children3) as rose3) =
let
root =
transform root1 root2 root3
shrink1 =
Lazy.List.map (\childOf1 -> map3RoseTree transform childOf1 rose2 rose3) children1
shrink2 =
Lazy.List.map (\childOf2 -> map3RoseTree transform rose1 childOf2 rose3) children2
shrink3 =
Lazy.List.map (\childOf3 -> map3RoseTree transform rose1 rose2 childOf3) children3
in
Rose root (shrink1 +++ shrink2 +++ shrink3)
map4RoseTree : (a -> b -> c -> d -> e) -> RoseTree a -> RoseTree b -> RoseTree c -> RoseTree d -> RoseTree e
map4RoseTree transform ((Rose root1 children1) as rose1) ((Rose root2 children2) as rose2) ((Rose root3 children3) as rose3) ((Rose root4 children4) as rose4) =
let
root =
transform root1 root2 root3 root4
shrink1 =
Lazy.List.map (\childOf1 -> map4RoseTree transform childOf1 rose2 rose3 rose4) children1
shrink2 =
Lazy.List.map (\childOf2 -> map4RoseTree transform rose1 childOf2 rose3 rose4) children2
shrink3 =
Lazy.List.map (\childOf3 -> map4RoseTree transform rose1 rose2 childOf3 rose4) children3
shrink4 =
Lazy.List.map (\childOf4 -> map4RoseTree transform rose1 rose2 rose3 childOf4) children4
in
Rose root (shrink1 +++ shrink2 +++ shrink3 +++ shrink4)
map5RoseTree : (a -> b -> c -> d -> e -> f) -> RoseTree a -> RoseTree b -> RoseTree c -> RoseTree d -> RoseTree e -> RoseTree f
map5RoseTree transform ((Rose root1 children1) as rose1) ((Rose root2 children2) as rose2) ((Rose root3 children3) as rose3) ((Rose root4 children4) as rose4) ((Rose root5 children5) as rose5) =
let
root =
transform root1 root2 root3 root4 root5
shrink1 =
Lazy.List.map (\childOf1 -> map5RoseTree transform childOf1 rose2 rose3 rose4 rose5) children1
shrink2 =
Lazy.List.map (\childOf2 -> map5RoseTree transform rose1 childOf2 rose3 rose4 rose5) children2
shrink3 =
Lazy.List.map (\childOf3 -> map5RoseTree transform rose1 rose2 childOf3 rose4 rose5) children3
shrink4 =
Lazy.List.map (\childOf4 -> map5RoseTree transform rose1 rose2 rose3 childOf4 rose5) children4
shrink5 =
Lazy.List.map (\childOf5 -> map5RoseTree transform rose1 rose2 rose3 rose4 childOf5) children5
in
Rose root (shrink1 +++ shrink2 +++ shrink3 +++ shrink4 +++ shrink5)

View File

@@ -0,0 +1,109 @@
module Fuzz.Internal exposing (Fuzzer, Valid, ValidFuzzer, andThen, combineValid, invalidReason, map)
import Lazy
import Lazy.List exposing ((:::), LazyList)
import Random.Pcg as Random exposing (Generator)
import RoseTree exposing (RoseTree(Rose))
type alias Fuzzer a =
Valid (ValidFuzzer a)
type alias Valid a =
Result String a
type alias ValidFuzzer a =
Generator (RoseTree a)
combineValid : List (Valid a) -> Valid (List a)
combineValid valids =
case valids of
[] ->
Ok []
(Ok x) :: rest ->
Result.map ((::) x) (combineValid rest)
(Err reason) :: _ ->
Err reason
map : (a -> b) -> Fuzzer a -> Fuzzer b
map fn fuzzer =
(Result.map << Random.map << RoseTree.map) fn fuzzer
andThen : (a -> Fuzzer b) -> Fuzzer a -> Fuzzer b
andThen fn fuzzer =
let
helper : (a -> Fuzzer b) -> RoseTree a -> ValidFuzzer b
helper fn xs =
RoseTree.map fn xs
|> removeInvalid
|> sequenceRoseTree
|> Random.map RoseTree.flatten
in
Result.map (Random.andThen (helper fn)) fuzzer
removeInvalid : RoseTree (Valid a) -> RoseTree a
removeInvalid tree =
case RoseTree.filterMap getValid tree of
Just newTree ->
newTree
Nothing ->
Debug.crash "Returning an invalid fuzzer from `andThen` is currently unsupported"
sequenceRoseTree : RoseTree (Generator a) -> Generator (RoseTree a)
sequenceRoseTree (Rose root branches) =
Random.map2
Rose
root
(Lazy.List.map sequenceRoseTree branches |> sequenceLazyList)
sequenceLazyList : LazyList (Generator a) -> Generator (LazyList a)
sequenceLazyList xs =
Random.independentSeed
|> Random.map (runAll xs)
runAll : LazyList (Generator a) -> Random.Seed -> LazyList a
runAll xs seed =
Lazy.lazy <|
\_ ->
case Lazy.force xs of
Lazy.List.Nil ->
Lazy.List.Nil
Lazy.List.Cons firstGenerator rest ->
let
( x, newSeed ) =
Random.step firstGenerator seed
in
Lazy.List.Cons x (runAll rest newSeed)
getValid : Valid a -> Maybe a
getValid valid =
case valid of
Ok x ->
Just x
Err _ ->
Nothing
invalidReason : Valid a -> Maybe String
invalidReason valid =
case valid of
Ok _ ->
Nothing
Err reason ->
Just reason

View File

@@ -0,0 +1,91 @@
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)
filter : (a -> Bool) -> RoseTree a -> Maybe (RoseTree a)
filter predicate tree =
let
maybeKeep x =
if predicate x then
Just x
else
Nothing
in
filterMap maybeKeep tree
{-| filterMap a function over a rosetree
-}
filterMap : (a -> Maybe b) -> RoseTree a -> Maybe (RoseTree b)
filterMap f (Rose a c) =
case f a of
Just newA ->
Just <| Rose newA (LazyList.filterMap (filterMap f) c)
Nothing ->
Nothing
filterBranches : (a -> Bool) -> RoseTree a -> RoseTree a
filterBranches predicate (Rose root branches) =
Rose
root
(LazyList.filterMap (filter predicate) branches)
{-| 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,474 @@
module Test exposing (FuzzOptions, Test, concat, describe, fuzz, fuzz2, fuzz3, fuzz4, fuzz5, fuzzWith, only, skip, test, todo)
{-| A module containing functions for creating and managing tests.
@docs Test, test
## Organizing Tests
@docs describe, concat, todo, skip, only
## Fuzz Testing
@docs fuzz, fuzz2, fuzz3, fuzz4, fuzz5, fuzzWith, FuzzOptions
-}
import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import Set
import Test.Fuzz
import Test.Internal as Internal
import Test.Runner.Failure exposing (InvalidReason(..), Reason(..))
{-| 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 tests =
if List.isEmpty tests then
Internal.failNow
{ description = "This `concat` has no tests in it. Let's give it some!"
, reason = Invalid EmptyList
}
else
case Internal.duplicatedName tests of
Err duped ->
Internal.failNow
{ description = "A test group contains multiple tests named '" ++ duped ++ "'. Do some renaming so that tests have unique names."
, reason = Invalid DuplicatedName
}
Ok _ ->
Internal.Batch tests
{-| 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.equal []
, fuzz int "has no effect on a one-item list" <|
\num ->
List.reverse [ num ]
|> Expect.equal [ num ]
]
]
Passing an empty list will result in a failing test, because you either made a
mistake or are creating a placeholder.
-}
describe : String -> List Test -> Test
describe untrimmedDesc tests =
let
desc =
String.trim untrimmedDesc
in
if String.isEmpty desc then
Internal.failNow
{ description = "This `describe` has a blank description. Let's give it a useful one!"
, reason = Invalid BadDescription
}
else if List.isEmpty tests then
Internal.failNow
{ description = "This `describe " ++ toString desc ++ "` has no tests in it. Let's give it some!"
, reason = Invalid EmptyList
}
else
case Internal.duplicatedName tests of
Err duped ->
Internal.failNow
{ description = "The tests '" ++ desc ++ "' contain multiple tests named '" ++ duped ++ "'. Let's rename them so we know which is which."
, reason = Invalid DuplicatedName
}
Ok childrenNames ->
if Set.member desc childrenNames then
Internal.failNow
{ description = "The test '" ++ desc ++ "' contains a child test of the same name. Let's rename them so we know which is which."
, reason = Invalid DuplicatedName
}
else
Internal.Labeled desc (Internal.Batch tests)
{-| 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.equal 0
-}
test : String -> (() -> Expectation) -> Test
test untrimmedDesc thunk =
let
desc =
String.trim untrimmedDesc
in
if String.isEmpty desc then
Internal.blankDescriptionFailure
else
Internal.Labeled desc (Internal.UnitTest (\() -> [ thunk () ]))
{-| Returns a [`Test`](#Test) that is "TODO" (not yet implemented). These tests
always fail, but test runners will only include them in their output if there
are no other failures.
These tests aren't meant to be committed to version control. Instead, use them
when you're brainstorming lots of tests you'd like to write, but you can't
implement them all at once. When you replace `todo` with a real test, you'll be
able to see if it fails without clutter from tests still not implemented. But,
unlike leaving yourself comments, you'll be prompted to implement these tests
because your suite will fail.
describe "a new thing"
[ todo "does what is expected in the common case"
, todo "correctly handles an edge case I just thought of"
]
This functionality is similar to "pending" tests in other frameworks, except
that a TODO test is considered failing but a pending test often is not.
-}
todo : String -> Test
todo desc =
Internal.failNow
{ description = desc
, reason = TODO
}
{-| Returns a [`Test`](#Test) that causes other tests to be skipped, and
only runs the given one.
Calls to `only` aren't meant to be committed to version control. Instead, use
them when you want to focus on getting a particular subset of your tests to pass.
If you use `only`, your entire test suite will fail, even if
each of the individual tests pass. This is to help avoid accidentally
committing a `only` to version control.
If you you use `only` on multiple tests, only those tests will run. If you
put a `only` inside another `only`, only the outermost `only`
will affect which tests gets run.
See also [`skip`](#skip). Note that `skip` takes precedence over `only`;
if you use a `skip` inside an `only`, it will still get skipped, and if you use
an `only` inside a `skip`, it will also get skipped.
describe "List"
[ only <| describe "reverse"
[ test "has no effect on an empty list" <|
\_ ->
List.reverse []
|> Expect.equal []
, fuzz int "has no effect on a one-item list" <|
\num ->
List.reverse [ num ]
|> Expect.equal [ num ]
]
, test "This will not get run, because of the `only` above!" <|
\_ ->
List.length []
|> Expect.equal 0
]
-}
only : Test -> Test
only =
Internal.Only
{-| Returns a [`Test`](#Test) that gets skipped.
Calls to `skip` aren't meant to be committed to version control. Instead, use
it when you want to focus on getting a particular subset of your tests to
pass. If you use `skip`, your entire test suite will fail, even if
each of the individual tests pass. This is to help avoid accidentally
committing a `skip` to version control.
See also [`only`](#only). Note that `skip` takes precedence over `only`;
if you use a `skip` inside an `only`, it will still get skipped, and if you use
an `only` inside a `skip`, it will also get skipped.
describe "List"
[ skip <| describe "reverse"
[ test "has no effect on an empty list" <|
\_ ->
List.reverse []
|> Expect.equal []
, fuzz int "has no effect on a one-item list" <|
\num ->
List.reverse [ num ]
|> Expect.equal [ num ]
]
, test "This is the only test that will get run; the other was skipped!" <|
\_ ->
List.length []
|> Expect.equal 0
]
-}
skip : Test -> Test
skip =
Internal.Skipped
{-| 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.equal (List.member target nums)
-}
fuzzWith : FuzzOptions -> Fuzzer a -> String -> (a -> Expectation) -> Test
fuzzWith options fuzzer desc getTest =
if options.runs < 1 then
Internal.failNow
{ description = "Fuzz tests must have a run count of at least 1, not " ++ toString options.runs ++ "."
, reason = Invalid NonpositiveFuzzCount
}
else
fuzzWithHelp options (fuzz fuzzer desc getTest)
fuzzWithHelp : FuzzOptions -> Test -> Test
fuzzWithHelp options test =
case test of
Internal.UnitTest _ ->
test
Internal.FuzzTest run ->
Internal.FuzzTest (\seed _ -> run seed options.runs)
Internal.Labeled label subTest ->
Internal.Labeled label (fuzzWithHelp options subTest)
Internal.Skipped subTest ->
-- It's important to treat skipped tests exactly the same as normal,
-- until after seed distribution has completed.
fuzzWithHelp options subTest
|> Internal.Only
Internal.Only subTest ->
fuzzWithHelp options subTest
|> Internal.Only
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 =
Test.Fuzz.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.equal (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,27 @@
module Test.Expectation exposing (Expectation(..), fail, withGiven)
import Test.Runner.Failure exposing (Reason)
type Expectation
= Pass
| Fail { given : Maybe String, description : String, reason : Reason }
{-| Create a failure without specifying the given.
-}
fail : { description : String, reason : Reason } -> Expectation
fail { description, reason } =
Fail { given = Nothing, description = description, reason = reason }
{-| Set the given (fuzz test input) of an expectation.
-}
withGiven : String -> Expectation -> Expectation
withGiven newGiven expectation =
case expectation of
Fail failure ->
Fail { failure | given = Just newGiven }
Pass ->
expectation

View File

@@ -0,0 +1,164 @@
module Test.Fuzz exposing (fuzzTest)
import Dict exposing (Dict)
import Fuzz exposing (Fuzzer)
import Fuzz.Internal exposing (ValidFuzzer)
import Lazy.List
import Random.Pcg as Random exposing (Generator)
import RoseTree exposing (RoseTree(..))
import Test.Expectation exposing (Expectation(..))
import Test.Internal exposing (Test(..), blankDescriptionFailure, failNow)
import Test.Runner.Failure exposing (InvalidReason(..), Reason(..))
{-| Reject always-failing tests because of bad names or invalid fuzzers.
-}
fuzzTest : Fuzzer a -> String -> (a -> Expectation) -> Test
fuzzTest fuzzer untrimmedDesc getExpectation =
let
desc =
String.trim untrimmedDesc
in
if String.isEmpty desc then
blankDescriptionFailure
else
case fuzzer of
Err reason ->
failNow
{ description = reason
, reason = Invalid InvalidFuzzer
}
Ok validFuzzer ->
-- Preliminary checks passed; run the fuzz test
validatedFuzzTest validFuzzer desc getExpectation
{-| Knowing that the fuzz test isn't obviously invalid, run the test and package up the results.
-}
validatedFuzzTest : ValidFuzzer a -> String -> (a -> Expectation) -> Test
validatedFuzzTest fuzzer desc getExpectation =
let
run seed runs =
let
failures =
getFailures fuzzer getExpectation 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 (FuzzTest run)
type alias Failures =
Dict String Expectation
getFailures : ValidFuzzer a -> (a -> Expectation) -> Random.Seed -> Int -> Dict String Expectation
getFailures fuzzer getExpectation initialSeed totalRuns =
{- Fuzz test algorithm with memoization and opt-in RoseTrees:
Generate a single value from the fuzzer's genVal random generator
Determine if the value is memoized. If so, skip. Otherwise continue.
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
genVal =
Random.map RoseTree.root fuzzer
initialFailures =
Dict.empty
helper currentSeed remainingRuns failures =
let
( value, nextSeed ) =
Random.step genVal currentSeed
newFailures =
findNewFailure fuzzer getExpectation failures currentSeed value
in
if remainingRuns <= 1 then
newFailures
else
helper nextSeed (remainingRuns - 1) newFailures
in
helper initialSeed totalRuns initialFailures
{-| Knowing that a value in not in the cache, determine if it causes the test to pass or fail.
-}
findNewFailure :
ValidFuzzer a
-> (a -> Expectation)
-> Failures
-> Random.Seed
-> a
-> Failures
findNewFailure fuzzer getExpectation failures currentSeed value =
case getExpectation value of
Pass ->
failures
failedExpectation ->
let
( rosetree, nextSeed ) =
-- nextSeed is not used here because caller function has currentSeed
Random.step fuzzer currentSeed
in
shrinkAndAdd rosetree getExpectation failedExpectation failures
{-| Knowing that the rosetree's root already failed, finds the shrunken failure.
Returns the updated failures dictionary.
-}
shrinkAndAdd :
RoseTree a
-> (a -> Expectation)
-> Expectation
-> Failures
-> Failures
shrinkAndAdd rootTree getExpectation rootsExpectation failures =
let
shrink : Expectation -> RoseTree a -> ( a, Expectation )
shrink oldExpectation (Rose failingValue branches) =
case Lazy.List.headAndTail branches of
Just ( (Rose possiblyFailingValue _) as rosetree, moreLazyRoseTrees ) ->
-- either way, recurse with the most recent failing expectation, and failing input with its list of shrunken values
case getExpectation possiblyFailingValue of
Pass ->
shrink oldExpectation
(Rose failingValue moreLazyRoseTrees)
newExpectation ->
let
( minimalValue, finalExpectation ) =
shrink newExpectation rosetree
in
( minimalValue
, finalExpectation
)
Nothing ->
( failingValue, oldExpectation )
(Rose failingValue _) =
rootTree
( minimalValue, finalExpectation ) =
shrink rootsExpectation rootTree
in
Dict.insert (toString minimalValue) finalExpectation failures
formatExpectation : ( String, Expectation ) -> Expectation
formatExpectation ( given, expectation ) =
Test.Expectation.withGiven given expectation

View File

@@ -0,0 +1,69 @@
module Test.Internal exposing (Test(..), blankDescriptionFailure, duplicatedName, failNow)
import Random.Pcg as Random exposing (Generator)
import Set exposing (Set)
import Test.Expectation exposing (Expectation(..))
import Test.Runner.Failure exposing (InvalidReason(..), Reason(..))
type Test
= UnitTest (() -> List Expectation)
| FuzzTest (Random.Seed -> Int -> List Expectation)
| Labeled String Test
| Skipped Test
| Only Test
| Batch (List Test)
{-| Create a test that always fails for the given reason and description.
-}
failNow : { description : String, reason : Reason } -> Test
failNow record =
UnitTest
(\() -> [ Test.Expectation.fail record ])
blankDescriptionFailure : Test
blankDescriptionFailure =
failNow
{ description = "This test has a blank description. Let's give it a useful one!"
, reason = Invalid BadDescription
}
duplicatedName : List Test -> Result String (Set String)
duplicatedName =
let
names : Test -> List String
names test =
case test of
Labeled str _ ->
[ str ]
Batch subtests ->
List.concatMap names subtests
UnitTest _ ->
[]
FuzzTest _ ->
[]
Skipped subTest ->
names subTest
Only subTest ->
names subTest
insertOrFail : String -> Result String (Set String) -> Result String (Set String)
insertOrFail newName =
Result.andThen
(\oldNames ->
if Set.member newName oldNames then
Err newName
else
Ok <| Set.insert newName oldNames
)
in
List.concatMap names
>> List.foldl insertOrFail (Ok Set.empty)

View File

@@ -0,0 +1,532 @@
module Test.Runner
exposing
( Runner
, SeededRunners(..)
, Shrinkable
, formatLabels
, fromTest
, fuzz
, getFailure
, getFailureReason
, isTodo
, shrink
)
{-| This is an "experts only" module that exposes functions needed to run and
display tests. A typical user will use an existing runner library for Node or
the browser, which is implemented using this interface. A list of these runners
can be found in the `README`.
## Runner
@docs Runner, SeededRunners, fromTest
## Expectations
@docs getFailure, getFailureReason, isTodo
## Formatting
@docs formatLabels
## Fuzzers
These functions give you the ability to run fuzzers separate of running fuzz tests.
@docs Shrinkable, fuzz, shrink
-}
import Bitwise
import Char
import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import Lazy.List as LazyList exposing (LazyList)
import Random.Pcg as Random
import RoseTree exposing (RoseTree(Rose))
import String
import Test exposing (Test)
import Test.Expectation
import Test.Internal as Internal
import Test.Runner.Failure exposing (Reason(..))
{-| An unevaluated test. Run it with [`run`](#run) to evaluate it into a
list of `Expectation`s.
-}
type Runnable
= Thunk (() -> List Expectation)
{-| A function which, when evaluated, produces a list of expectations. Also a
list of labels which apply to this outcome.
-}
type alias Runner =
{ run : () -> List Expectation
, labels : List String
}
{-| A structured test runner, incorporating:
- The expectations to run
- The hierarchy of description strings that describe the results
-}
type RunnableTree
= Runnable Runnable
| Labeled String RunnableTree
| Batch (List RunnableTree)
{-| Convert a `Test` into `SeededRunners`.
In order to run any fuzz tests that the `Test` may have, it requires a default run count as well
as an initial `Random.Seed`. `100` is a good run count. To obtain a good random seed, pass a
random 32-bit integer to `Random.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.Seed -> Test -> SeededRunners
fromTest runs seed test =
if runs < 1 then
Invalid ("Test runner run count must be at least 1, not " ++ toString runs)
else
let
distribution =
distributeSeeds runs seed test
in
if List.isEmpty distribution.only then
if countAllRunnables distribution.skipped == 0 then
distribution.all
|> List.concatMap fromRunnableTree
|> Plain
else
distribution.all
|> List.concatMap fromRunnableTree
|> Skipping
else
distribution.only
|> List.concatMap fromRunnableTree
|> Only
countAllRunnables : List RunnableTree -> Int
countAllRunnables =
List.foldl (countRunnables >> (+)) 0
countRunnables : RunnableTree -> Int
countRunnables runnable =
case runnable of
Runnable _ ->
1
Labeled _ runner ->
countRunnables runner
Batch runners ->
countAllRunnables runners
run : Runnable -> List Expectation
run (Thunk fn) =
fn ()
fromRunnableTree : RunnableTree -> List Runner
fromRunnableTree =
fromRunnableTreeHelp []
fromRunnableTreeHelp : List String -> RunnableTree -> List Runner
fromRunnableTreeHelp labels runner =
case runner of
Runnable runnable ->
[ { labels = labels
, run = \_ -> run runnable
}
]
Labeled label subRunner ->
fromRunnableTreeHelp (label :: labels) subRunner
Batch runners ->
List.concatMap (fromRunnableTreeHelp labels) runners
type alias Distribution =
{ seed : Random.Seed
, only : List RunnableTree
, all : List RunnableTree
, skipped : List RunnableTree
}
{-| Test Runners which have had seeds distributed to them, and which are now
either invalid or are ready to run. Seeded runners include some metadata:
- `Invalid` runners had a problem (e.g. two sibling tests had the same description) making them un-runnable.
- `Only` runners can be run, but `Test.only` was used somewhere, so ultimately they will lead to a failed test run even if each test that gets run passes.
- `Skipping` runners can be run, but `Test.skip` was used somewhere, so ultimately they will lead to a failed test run even if each test that gets run passes.
- `Plain` runners are ready to run, and have none of these issues.
-}
type SeededRunners
= Plain (List Runner)
| Only (List Runner)
| Skipping (List Runner)
| Invalid String
emptyDistribution : Random.Seed -> Distribution
emptyDistribution seed =
{ seed = seed
, all = []
, only = []
, skipped = []
}
{-| This breaks down a test into individual Runners, while assigning different
random number seeds to them. Along the way it also does a few other things:
1. Collect any tests created with `Test.only` so later we can run only those.
2. Collect any tests created with `Test.todo` so later we can fail the run.
3. Validate that the run count is at least 1.
Some design notes:
1. `only` tests and `skip` tests do not affect seed distribution. This is
important for the case where a user runs tests, sees one failure, and decides
to isolate it by using both `only` and providing the same seed as before. If
`only` changes seed distribution, then that test result might not reproduce!
This would be very frustrating, as it would mean you could reproduce the
failure when not using `only`, but it magically disappeared as soon as you
tried to isolate it. The same logic applies to `skip`.
2. Theoretically this could become tail-recursive. However, the Labeled and Batch
cases would presumably become very gnarly, and it's unclear whether there would
be a performance benefit or penalty in the end. If some brave soul wants to
attempt it for kicks, beware that this is not a performance optimization for
the faint of heart. Practically speaking, it seems unlikely to be worthwhile
unless somehow people start seeing stack overflows during seed distribution -
which would presumably require some absurdly deeply nested `describe` calls.
-}
distributeSeeds : Int -> Random.Seed -> Test -> Distribution
distributeSeeds =
distributeSeedsHelp False
distributeSeedsHelp : Bool -> Int -> Random.Seed -> Test -> Distribution
distributeSeedsHelp hashed runs seed test =
case test of
Internal.UnitTest run ->
{ seed = seed
, all = [ Runnable (Thunk (\_ -> run ())) ]
, only = []
, skipped = []
}
Internal.FuzzTest run ->
let
( firstSeed, nextSeed ) =
Random.step Random.independentSeed seed
in
{ seed = nextSeed
, all = [ Runnable (Thunk (\_ -> run firstSeed runs)) ]
, only = []
, skipped = []
}
Internal.Labeled description subTest ->
-- This fixes https://github.com/elm-community/elm-test/issues/192
-- The first time we hit a Labeled, we want to use the hash of
-- that label, along with the original seed, as our starting
-- point for distribution. Repeating this process more than
-- once would be a waste.
if hashed then
let
next =
distributeSeedsHelp True runs seed subTest
in
{ seed = next.seed
, all = List.map (Labeled description) next.all
, only = List.map (Labeled description) next.only
, skipped = List.map (Labeled description) next.skipped
}
else
let
intFromSeed =
-- At this point, this seed will be the original
-- one passed into distributeSeeds. We know this
-- because the only other branch that does a
-- Random.step on that seed is the Internal.Test
-- branch, and you can't have a Labeled inside a
-- Test, so that couldn't have come up yet.
seed
-- Convert the Seed back to an Int
|> Random.step (Random.int 0 Random.maxInt)
|> Tuple.first
hashedSeed =
description
-- Hash from String to Int
|> fnvHashString fnvInit
-- Incorporate the originally passed-in seed
|> fnvHash intFromSeed
-- Convert Int back to Seed
|> Random.initialSeed
next =
distributeSeedsHelp True runs hashedSeed subTest
in
-- Using seed instead of next.seed fixes https://github.com/elm-community/elm-test/issues/192
-- by making it so that all the tests underneath this Label begin
-- with the hashed seed, but subsequent sibling tests in this Batch
-- get the same seed as before.
{ seed = seed
, all = List.map (Labeled description) next.all
, only = List.map (Labeled description) next.only
, skipped = List.map (Labeled description) next.skipped
}
Internal.Skipped subTest ->
let
-- Go through the motions in order to obtain the seed, but then
-- move everything to skipped.
next =
distributeSeedsHelp hashed runs seed subTest
in
{ seed = next.seed
, all = []
, only = []
, skipped = next.all
}
Internal.Only subTest ->
let
next =
distributeSeedsHelp hashed runs seed subTest
in
-- `only` all the things!
{ next | only = next.all }
Internal.Batch tests ->
List.foldl (batchDistribute hashed runs) (emptyDistribution seed) tests
batchDistribute : Bool -> Int -> Test -> Distribution -> Distribution
batchDistribute hashed runs test prev =
let
next =
distributeSeedsHelp hashed runs prev.seed test
in
{ seed = next.seed
, all = prev.all ++ next.all
, only = prev.only ++ next.only
, skipped = prev.skipped ++ next.skipped
}
{-| FNV-1a initial hash value
-}
fnvInit : Int
fnvInit =
2166136261
{-| FNV-1a helper for strings, using Char.toCode
-}
fnvHashString : Int -> String -> Int
fnvHashString hash str =
str |> String.toList |> List.map Char.toCode |> List.foldl fnvHash hash
{-| FNV-1a implementation.
-}
fnvHash : Int -> Int -> Int
fnvHash a b =
Bitwise.xor a b * 16777619 |> Bitwise.shiftRightZfBy 0
{-| **DEPRECATED.** Please use [`getFailureReason`](#getFailureReason) instead.
This function will be removed in the next major version.
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 `Nothing`).
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 : Maybe String, message : String }
getFailure expectation =
case expectation of
Test.Expectation.Pass ->
Nothing
Test.Expectation.Fail { given, description, reason } ->
Just
{ given = given
, message = Test.Runner.Failure.format description reason
}
{-| Return `Nothing` if the given [`Expectation`](#Expectation) is a [`pass`](#pass).
If it is a [`fail`](#fail), return a record containing the expectation
description, the [`Reason`](#Reason) the test failed, and the given inputs if
it was a fuzz test. (If it was not a fuzz test, the record's `given` field
will be `Nothing`).
For example:
getFailureReason (Expect.equal 1 2)
-- Just { reason = Equal 1 2, description = "Expect.equal", given = Nothing }
getFailureReason (Expect.equal 1 1)
-- Nothing
-}
getFailureReason :
Expectation
->
Maybe
{ given : Maybe String
, description : String
, reason : Reason
}
getFailureReason expectation =
case expectation of
Test.Expectation.Pass ->
Nothing
Test.Expectation.Fail record ->
Just record
{-| Determine if an expectation was created by a call to `Test.todo`. Runners
may treat these tests differently in their output.
-}
isTodo : Expectation -> Bool
isTodo expectation =
case expectation of
Test.Expectation.Pass ->
False
Test.Expectation.Fail { reason } ->
reason == TODO
{-| A standard way to format descriptions 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
Example:
[ "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
type alias Shrunken a =
{ down : LazyList (RoseTree a)
, over : LazyList (RoseTree a)
}
{-| A `Shrinkable a` is an opaque type that allows you to obtain a value of type
`a` that is smaller than the one you've previously obtained.
-}
type Shrinkable a
= Shrinkable (Shrunken a)
{-| Given a fuzzer, return a random generator to produce a value and a
Shrinkable. The value is what a fuzz test would have received as input.
-}
fuzz : Fuzzer a -> Random.Generator ( a, Shrinkable a )
fuzz fuzzer =
case fuzzer of
Ok validFuzzer ->
validFuzzer
|> Random.map
(\(Rose root children) ->
( root, Shrinkable { down = children, over = LazyList.empty } )
)
Err reason ->
Debug.crash <| "Cannot call `fuzz` with an invalid fuzzer: " ++ reason
{-| Given a Shrinkable, attempt to shrink the value further. Pass `False` to
indicate that the last value you've seen (from either `fuzz` or this function)
caused the test to **fail**. This will attempt to find a smaller value. Pass
`True` if the test passed. If you have already seen a failure, this will attempt
to shrink that failure in another way. In both cases, it may be impossible to
shrink the value, represented by `Nothing`.
-}
shrink : Bool -> Shrinkable a -> Maybe ( a, Shrinkable a )
shrink causedPass (Shrinkable { down, over }) =
let
tryNext =
if causedPass then
over
else
down
in
case LazyList.headAndTail tryNext of
Just ( Rose root children, tl ) ->
Just ( root, Shrinkable { down = children, over = tl } )
Nothing ->
Nothing

View File

@@ -0,0 +1,174 @@
module Test.Runner.Failure exposing (InvalidReason(..), Reason(..), format)
{-| The reason a test failed.
@docs Reason, InvalidReason, format
-}
{-| The reason a test failed.
Test runners can use this to provide nice output, e.g. by doing diffs on the
two parts of an `Expect.equal` failure.
-}
type Reason
= Custom
| Equality String String
| Comparison String String
-- Expected, actual, (index of problem, expected element, actual element)
| ListDiff (List String) (List String)
{- I don't think we need to show the diff twice with + and - reversed. Just show it after the main vertical bar.
"Extra" and "missing" are relative to the actual value.
-}
| CollectionDiff
{ expected : String
, actual : String
, extra : List String
, missing : List String
}
| TODO
| Invalid InvalidReason
{-| The reason a test run was invalid.
Test runners should report these to the user in whatever format is appropriate.
-}
type InvalidReason
= EmptyList
| NonpositiveFuzzCount
| InvalidFuzzer
| BadDescription
| DuplicatedName
verticalBar : String -> String -> String -> String
verticalBar comparison expected actual =
[ actual
, ""
, " " ++ comparison
, ""
, expected
]
|> String.join "\n"
{-| DEPRECATED. In the future, test runners should implement versions of this
that make sense for their own environments.
Format test run failures in a reasonable way.
-}
format : String -> Reason -> String
format description reason =
case reason of
Custom ->
description
Equality e a ->
verticalBar description e a
Comparison e a ->
verticalBar description e a
TODO ->
description
Invalid BadDescription ->
if description == "" then
"The empty string is not a valid test description."
else
"This is an invalid test description: " ++ description
Invalid _ ->
description
ListDiff expected actual ->
listDiffToString 0
description
{ expected = expected
, actual = actual
}
{ originalExpected = expected
, originalActual = actual
}
CollectionDiff { expected, actual, extra, missing } ->
let
extraStr =
if List.isEmpty extra then
""
else
"\nThese keys are extra: "
++ (extra |> String.join ", " |> (\d -> "[ " ++ d ++ " ]"))
missingStr =
if List.isEmpty missing then
""
else
"\nThese keys are missing: "
++ (missing |> String.join ", " |> (\d -> "[ " ++ d ++ " ]"))
in
String.join ""
[ verticalBar description expected actual
, "\n"
, extraStr
, missingStr
]
listDiffToString :
Int
-> String
-> { expected : List String, actual : List String }
-> { originalExpected : List String, originalActual : List String }
-> String
listDiffToString index description { expected, actual } originals =
case ( expected, actual ) of
( [], [] ) ->
[ "Two lists were unequal previously, yet ended up equal later."
, "This should never happen!"
, "Please report this bug to https://github.com/elm-community/elm-test/issues - and include these lists: "
, "\n"
, toString originals.originalExpected
, "\n"
, toString originals.originalActual
]
|> String.join ""
( first :: _, [] ) ->
verticalBar (description ++ " was shorter than")
(toString originals.originalExpected)
(toString originals.originalActual)
( [], first :: _ ) ->
verticalBar (description ++ " was longer than")
(toString originals.originalExpected)
(toString originals.originalActual)
( firstExpected :: restExpected, firstActual :: restActual ) ->
if firstExpected == firstActual then
-- They're still the same so far; keep going.
listDiffToString (index + 1)
description
{ expected = restExpected
, actual = restActual
}
originals
else
-- We found elements that differ; fail!
String.join ""
[ verticalBar description
(toString originals.originalExpected)
(toString originals.originalActual)
, "\n\nThe first diff is at index "
, toString index
, ": it was `"
, firstActual
, "`, but `"
, firstExpected
, "` was expected."
]

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 Array exposing (Array)
import Random.Pcg exposing (..)
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,128 @@
module FloatWithinTests exposing (floatWithinTests)
import Expect exposing (FloatingPointTolerance(Absolute, AbsoluteOrRelative, Relative))
import Fuzz exposing (..)
import Helpers exposing (..)
import Test exposing (..)
floatWithinTests : Test
floatWithinTests =
describe "Expect.within"
[ describe "use-cases"
[ fuzz float "pythagorean identity" <|
\x ->
sin x ^ 2 + cos x ^ 2 |> Expect.within (AbsoluteOrRelative 0.000001 0.00001) 1.0
, test "floats known to not add exactly" <|
\_ -> 0.1 + 0.2 |> Expect.within (Absolute 0.000000001) 0.3
, test "approximation of pi" <|
\_ -> 3.14 |> Expect.within (Absolute 0.01) pi
, fuzz (floatRange 0.000001 100000) "relative tolerance of circle circumference using pi approximation" <|
\radius ->
(radius * pi)
|> Expect.within (Relative 0.001) (radius * 3.14)
, expectToFail <|
test "approximation of pi is not considered too accurate" <|
\_ -> 3.14 |> Expect.within (Absolute 0.001) pi
, expectToFail <|
fuzz (floatRange 0.000001 100000) "too high absolute tolerance of circle circumference using pi approximation" <|
\radius ->
(radius * pi)
|> Expect.within (Absolute 0.001) (radius * 3.14)
, expectToFail <|
fuzz (floatRange 0.000001 100000) "too high relative tolerance of circle circumference using pi approximation" <|
\radius ->
(radius * pi)
|> Expect.within (Relative 0.0001) (radius * 3.14)
]
, describe "edge-cases"
[ fuzz2 float float "self equality" <|
\epsilon value ->
let
eps =
if epsilon /= 0 then
epsilon
else
1
in
value |> Expect.within (Relative (abs eps)) value
, fuzz float "NaN inequality" <|
\epsilon ->
let
nan =
0.0 / 0.0
in
nan |> Expect.notWithin (Relative (abs epsilon)) nan
, fuzz2 float float "NaN does not equal anything" <|
\epsilon a ->
let
nan =
0.0 / 0.0
in
nan |> Expect.notWithin (Relative (abs epsilon)) a
, fuzz float "Infinity equality" <|
\epsilon ->
let
infinity =
1.0 / 0.0
in
infinity |> Expect.within (Relative (abs epsilon)) infinity
, fuzz float "Negative infinity equality" <|
\epsilon ->
let
negativeInfinity =
-1.0 / 0.0
in
negativeInfinity |> Expect.within (Relative (abs epsilon)) negativeInfinity
, fuzz3 float float float "within and notWithin should never agree on relative tolerance" <|
\epsilon a b ->
let
withinTest =
a |> Expect.within (Relative (abs epsilon)) b
notWithinTest =
a |> Expect.notWithin (Relative (abs epsilon)) b
in
different withinTest notWithinTest
, fuzz3 float float float "within and notWithin should never agree on absolute tolerance" <|
\epsilon a b ->
let
withinTest =
a |> Expect.within (Absolute (abs epsilon)) b
notWithinTest =
a |> Expect.notWithin (Absolute (abs epsilon)) b
in
different withinTest notWithinTest
, fuzz4 float float float float "within and notWithin should never agree on absolute or relative tolerance" <|
\absoluteEpsilon relativeEpsilon a b ->
let
withinTest =
a |> Expect.within (AbsoluteOrRelative (abs absoluteEpsilon) (abs relativeEpsilon)) b
notWithinTest =
a |> Expect.notWithin (AbsoluteOrRelative (abs absoluteEpsilon) (abs relativeEpsilon)) b
in
different withinTest notWithinTest
, fuzz float "Zero equality" <|
\epsilon -> 0.0 |> Expect.within (Relative (abs epsilon)) 0.0
, fuzz3 float float float "within absolute commutativity" <|
\epsilon a b ->
same (Expect.within (Absolute (abs epsilon)) a b) (Expect.within (Absolute (abs epsilon)) b a)
, fuzz3 float float float "notWithin absolute commutativity" <|
\epsilon a b ->
same (Expect.notWithin (Absolute (abs epsilon)) a b) (Expect.notWithin (Absolute (abs epsilon)) b a)
, fuzz2 float float "within absolute reflexive" <|
\epsilon a ->
Expect.within (Absolute (abs epsilon)) a a
, fuzz3 float float float "within relative commutativity" <|
\epsilon a b ->
same (Expect.within (Relative (abs epsilon)) a b) (Expect.within (Relative (abs epsilon)) b a)
, fuzz3 float float float "notWithin relative commutativity" <|
\epsilon a b ->
same (Expect.notWithin (Relative (abs epsilon)) a b) (Expect.notWithin (Relative (abs epsilon)) b a)
, fuzz2 float float "within relative reflexive" <|
\epsilon a ->
Expect.within (Relative (abs epsilon)) a a
]
]

View File

@@ -0,0 +1,304 @@
module FuzzerTests exposing (fuzzerTests)
import Expect
import Fuzz exposing (..)
import Helpers exposing (..)
import Lazy.List
import Random.Pcg as Random
import RoseTree
import Test exposing (..)
import Test.Runner
die : Fuzzer Int
die =
Fuzz.intRange 1 6
seed : Fuzzer Random.Seed
seed =
Fuzz.custom
(Random.int Random.minInt Random.maxInt |> Random.map Random.initialSeed)
(always Lazy.List.empty)
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
(frequency [ ( 1, intRange 1 6 ), ( 1, intRange 1 20 ) ])
"Fuzz.frequency"
(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)
, fuzz
(map2 (,) die die
|> conditional
{ retries = 10
, fallback = \( a, b ) -> ( a, (b + 1) % 6 )
, condition = \( a, b ) -> a /= b
}
)
"conditional: reroll dice until they are not equal"
<|
\( roll1, roll2 ) ->
roll1 |> Expect.notEqual roll2
, fuzz seed "conditional: shrunken values all pass condition" <|
\seed ->
let
evenInt : Fuzzer Int
evenInt =
Fuzz.intRange 0 10
|> Fuzz.conditional
{ retries = 3
, fallback = (+) 1
, condition = even
}
even : Int -> Bool
even n =
(n % 2) == 0
shrinkable : Test.Runner.Shrinkable Int
shrinkable =
Test.Runner.fuzz evenInt
|> flip Random.step seed
|> Tuple.first
|> Tuple.second
testShrinkable : Test.Runner.Shrinkable Int -> Expect.Expectation
testShrinkable shrinkable =
case Test.Runner.shrink False shrinkable of
Nothing ->
Expect.pass
Just ( value, next ) ->
if even value then
testShrinkable next
else
Expect.fail <| "Shrunken value does not pass conditional: " ++ toString value
in
testShrinkable shrinkable
, describe "Whitebox testing using Fuzz.Internal"
[ fuzz randomSeedFuzzer "the same value is generated with and without shrinking" <|
\seed ->
let
step gen =
Random.step gen seed
aFuzzer =
tuple5
( tuple ( list int, array float )
, maybe bool
, result unit char
, tuple3
( percentage
, map2 (+) int int
, frequency [ ( 1, constant True ), ( 3, constant False ) ]
)
, tuple3 ( intRange 0 100, floatRange -51 pi, map abs int )
)
valNoShrink =
aFuzzer |> Result.map (Random.map RoseTree.root >> step >> Tuple.first)
valWithShrink =
aFuzzer |> Result.map (step >> Tuple.first >> RoseTree.root)
in
Expect.equal valNoShrink valWithShrink
, shrinkingTests
, manualFuzzerTests
]
]
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]"
, fuzz (intRange 1 8 |> andThen (\i -> intRange 0 (2 ^ i))) "Fuzz.andThen shrinks a number" <|
\i ->
i <= 2 |> Expect.true "3"
]
type alias ShrinkResult a =
Maybe ( a, Test.Runner.Shrinkable a )
manualFuzzerTests : Test
manualFuzzerTests =
describe "Test.Runner.{fuzz, shrink}"
[ fuzz randomSeedFuzzer "Claim there are no even numbers" <|
\seed ->
let
-- fuzzer is guaranteed to produce an even number
fuzzer =
Fuzz.intRange 2 10000
|> Fuzz.map
(\n ->
if failsTest n then
n
else
n + 1
)
failsTest n =
n % 2 == 0
pair =
Random.step (Test.Runner.fuzz fuzzer) seed
|> Tuple.first
|> Just
unfold acc maybePair =
case maybePair of
Just ( valN, shrinkN ) ->
if failsTest valN then
unfold (valN :: acc) (Test.Runner.shrink False shrinkN)
else
unfold acc (Test.Runner.shrink True shrinkN)
Nothing ->
acc
in
unfold [] pair
|> Expect.all
[ List.all failsTest >> Expect.true "Not all elements were even"
, List.head
>> Maybe.map (Expect.all [ Expect.lessThan 5, Expect.atLeast 0 ])
>> Maybe.withDefault (Expect.fail "Did not cause failure")
, List.reverse >> List.head >> Expect.equal (Maybe.map Tuple.first pair)
]
, fuzz randomSeedFuzzer "No strings contain the letter e" <|
\seed ->
let
-- fuzzer is guaranteed to produce a string with the letter e
fuzzer =
map2 (\pre suf -> pre ++ "e" ++ suf) string string
failsTest =
String.contains "e"
pair =
Random.step (Test.Runner.fuzz fuzzer) seed
|> Tuple.first
|> Just
unfold acc maybePair =
case maybePair of
Just ( valN, shrinkN ) ->
if failsTest valN then
unfold (valN :: acc) (Test.Runner.shrink False shrinkN)
else
unfold acc (Test.Runner.shrink True shrinkN)
Nothing ->
acc
in
unfold [] pair
|> Expect.all
[ List.all failsTest >> Expect.true "Not all contained the letter e"
, List.head >> Expect.equal (Just "e")
, List.reverse >> List.head >> Expect.equal (Maybe.map Tuple.first pair)
]
, fuzz randomSeedFuzzer "List shrinker finds the smallest counter example" <|
\seed ->
let
fuzzer : Fuzzer (List Int)
fuzzer =
Fuzz.list Fuzz.int
allEven : List Int -> Bool
allEven xs =
List.all (\x -> x % 2 == 0) xs
initialShrink : ShrinkResult (List Int)
initialShrink =
Random.step (Test.Runner.fuzz fuzzer) seed
|> Tuple.first
|> Just
shrink : Maybe (List Int) -> ShrinkResult (List Int) -> Maybe (List Int)
shrink shrunken lastShrink =
case lastShrink of
Just ( valN, shrinkN ) ->
shrink
(if allEven valN then
shrunken
else
Just valN
)
(Test.Runner.shrink (allEven valN) shrinkN)
Nothing ->
shrunken
in
case shrink Nothing initialShrink of
Just shrunken ->
Expect.equal [ 1 ] shrunken
Nothing ->
Expect.pass
]

View File

@@ -0,0 +1,159 @@
module Helpers exposing (different, expectPass, expectToFail, randomSeedFuzzer, same, succeeded, testShrinking, testStringLengthIsPreserved)
import Expect
import Fuzz exposing (Fuzzer)
import Random.Pcg as Random
import Shrink
import Test exposing (Test)
import Test.Expectation exposing (Expectation(..))
import Test.Internal as Internal
import Test.Runner.Failure exposing (Reason(..))
expectPass : a -> Expectation
expectPass _ =
Expect.pass
testStringLengthIsPreserved : List String -> Expectation
testStringLengthIsPreserved strings =
strings
|> List.map String.length
|> List.sum
|> Expect.equal (String.length (List.foldl (++) "" strings))
expectToFail : Test -> Test
expectToFail =
expectFailureHelper (always Nothing)
succeeded : Expectation -> Bool
succeeded expectation =
case expectation of
Pass ->
True
Fail _ ->
False
passesToFails :
({ reason : Reason
, description : String
, given : Maybe String
}
-> Maybe String
)
-> List Expectation
-> List Expectation
passesToFails f expectations =
expectations
|> List.filterMap (passToFail f)
|> List.map Expect.fail
|> (\list ->
if List.isEmpty list then
[ Expect.pass ]
else
list
)
passToFail :
({ reason : Reason
, description : String
, given : Maybe String
}
-> Maybe String
)
-> Expectation
-> Maybe String
passToFail f expectation =
case expectation of
Pass ->
Just "Expected this test to fail, but it passed!"
Fail record ->
f record
expectFailureHelper : ({ description : String, given : Maybe String, reason : Reason } -> Maybe String) -> Test -> Test
expectFailureHelper f test =
case test of
Internal.UnitTest runTest ->
Internal.UnitTest <|
\() ->
passesToFails f (runTest ())
Internal.FuzzTest runTest ->
Internal.FuzzTest <|
\seed runs ->
passesToFails f (runTest seed runs)
Internal.Labeled desc labeledTest ->
Internal.Labeled desc (expectFailureHelper f labeledTest)
Internal.Batch tests ->
Internal.Batch (List.map (expectFailureHelper f) tests)
Internal.Skipped subTest ->
expectFailureHelper f subTest
|> Internal.Skipped
Internal.Only subTest ->
expectFailureHelper f subTest
|> Internal.Only
testShrinking : Test -> Test
testShrinking =
let
handleFailure { given, description } =
let
acceptable =
String.split "|" description
in
case given of
Nothing ->
Just "Expected this test to have a given value!"
Just g ->
if List.member g acceptable then
Nothing
else
Just <| "Got shrunken value " ++ g ++ " but expected " ++ String.join " or " acceptable
in
expectFailureHelper handleFailure
{-| get a good distribution of random seeds, and don't shrink our seeds!
-}
randomSeedFuzzer : Fuzzer Random.Seed
randomSeedFuzzer =
Fuzz.custom (Random.int 0 0xFFFFFFFF) Shrink.noShrink |> Fuzz.map Random.initialSeed
same : Expectation -> Expectation -> Expectation
same a b =
case ( a, b ) of
( Test.Expectation.Pass, Test.Expectation.Pass ) ->
Test.Expectation.Pass
( Test.Expectation.Fail _, Test.Expectation.Fail _ ) ->
Test.Expectation.Pass
( a, b ) ->
Test.Expectation.fail { description = "expected both arguments to fail, or both to succeed", reason = Equality (toString a) (toString b) }
different : Expectation -> Expectation -> Expectation
different a b =
case ( a, b ) of
( Test.Expectation.Pass, Test.Expectation.Fail _ ) ->
Test.Expectation.Pass
( Test.Expectation.Fail _, Test.Expectation.Pass ) ->
Test.Expectation.Pass
( a, b ) ->
Test.Expectation.fail { description = "expected one argument to fail", reason = Equality (toString a) (toString b) }

View File

@@ -0,0 +1,82 @@
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 Platform
import Runner.Log
import Runner.String exposing (Summary)
import SeedTests
import Tests
main : Program Never () msg
main =
let
program =
Platform.program
{ init = ( (), Cmd.none )
, update = \_ _ -> ( (), Cmd.none )
, subscriptions = \_ -> Sub.none
}
in
runAllTests program
runAllTests : a -> a
runAllTests a =
let
runSeedTest =
Runner.String.runWithOptions 1 SeedTests.fixedSeed
_ =
[ [ Runner.String.run Tests.all ]
, List.map runSeedTest SeedTests.tests
, List.map (runSeedTest >> removeAutoFail) SeedTests.noAutoFail
]
|> List.concat
|> List.foldl combineSummaries emptySummary
|> Runner.Log.logOutput
in
a
emptySummary : Summary
emptySummary =
{ output = "", passed = 0, failed = 0, autoFail = Nothing }
{-| Considers autoFail as pass so we can actually write tests about Test.skip
and Test.only which do not automatically fail.
-}
removeAutoFail : Summary -> Summary
removeAutoFail summary =
{ summary | autoFail = Nothing }
combineSummaries : Summary -> Summary -> Summary
combineSummaries first second =
{ output = first.output ++ second.output
, passed = first.passed + second.passed
, failed = first.failed + second.failed
, autoFail =
case ( first.autoFail, second.autoFail ) of
( Nothing, Nothing ) ->
Nothing
( Nothing, second ) ->
second
( first, Nothing ) ->
first
( Just first, Just second ) ->
[ first, second ]
|> String.join "\n"
|> Just
}

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,82 @@
module Runner.Log exposing (logOutput, 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 Runner.String exposing (Summary)
import String
import Test exposing (Test)
{-| Run the test using the default `Test.Runner.String` options.
-}
run : Test -> ()
run test =
Runner.String.run test
|> logOutput
{-| Run the test using the provided options.
-}
runWithOptions : Int -> Random.Seed -> Test -> ()
runWithOptions runs seed test =
Runner.String.runWithOptions runs seed test
|> logOutput
summarize : Summary -> String
summarize { output, passed, failed, autoFail } =
let
headline =
if failed > 0 then
output ++ "\n\nTEST RUN FAILED"
else
case autoFail of
Nothing ->
"TEST RUN PASSED"
Just reason ->
"TEST RUN FAILED because " ++ reason
in
String.join "\n"
[ output
, headline ++ "\n"
, "Passed: " ++ toString passed
, "Failed: " ++ toString failed
]
logOutput : Summary -> ()
logOutput summary =
let
output =
summarize summary ++ "\n\nExit code"
_ =
if summary.failed > 0 || summary.autoFail /= Nothing then
output
|> flip Debug.log 1
|> (\_ -> Debug.crash "FAILED TEST RUN")
|> (\_ -> ())
else
output
|> flip Debug.log 0
|> (\_ -> ())
in
()

View File

@@ -0,0 +1,134 @@
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 Expect exposing (Expectation)
import Random.Pcg as Random
import Runner.String.Format
import Test exposing (Test)
import Test.Runner exposing (Runner, SeededRunners(..))
{-| The output string, the number of passed tests,
and the number of failed tests.
-}
type alias Summary =
{ output : String, passed : Int, failed : Int, autoFail : Maybe String }
toOutput : Summary -> SeededRunners -> Summary
toOutput summary seededRunners =
let
render =
List.foldl (toOutputHelp [])
in
case seededRunners of
Plain runners ->
render { summary | autoFail = Nothing } runners
Only runners ->
render { summary | autoFail = Just "Test.only was used" } runners
Skipping runners ->
render { summary | autoFail = Just "Test.skip was used" } runners
Invalid message ->
{ output = message, passed = 0, failed = 0, autoFail = Nothing }
toOutputHelp : List String -> Runner -> Summary -> Summary
toOutputHelp labels runner summary =
runner.run ()
|> List.foldl fromExpectation summary
fromExpectation : Expectation -> Summary -> Summary
fromExpectation expectation summary =
case Test.Runner.getFailureReason expectation of
Nothing ->
{ summary | passed = summary.passed + 1 }
Just { given, description, reason } ->
let
message =
Runner.String.Format.format description reason
prefix =
case given of
Nothing ->
""
Just g ->
"Given " ++ g ++ "\n\n"
newOutput =
"\n\n" ++ (prefix ++ indentLines message) ++ "\n"
in
{ summary
| 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 =
let
seededRunners =
Test.Runner.fromTest runs seed test
in
toOutput
{ output = ""
, passed = 0
, failed = 0
, autoFail = Just "no tests were run"
}
seededRunners

View File

@@ -0,0 +1,182 @@
module Runner.String.Format exposing (format)
import Diff exposing (Change(..))
import Test.Runner.Failure exposing (InvalidReason(BadDescription), Reason(..))
format : String -> Reason -> String
format description reason =
case reason of
Custom ->
description
Equality expected actual ->
equalityToString { operation = description, expected = expected, actual = actual }
Comparison first second ->
verticalBar description first second
TODO ->
description
Invalid BadDescription ->
if description == "" then
"The empty string is not a valid test description."
else
"This is an invalid test description: " ++ description
Invalid _ ->
description
ListDiff expected actual ->
listDiffToString 0
description
{ expected = expected
, actual = actual
}
{ originalExpected = expected
, originalActual = actual
}
CollectionDiff { expected, actual, extra, missing } ->
let
extraStr =
if List.isEmpty extra then
""
else
"\nThese keys are extra: "
++ (extra |> String.join ", " |> (\d -> "[ " ++ d ++ " ]"))
missingStr =
if List.isEmpty missing then
""
else
"\nThese keys are missing: "
++ (missing |> String.join ", " |> (\d -> "[ " ++ d ++ " ]"))
in
String.join ""
[ verticalBar description expected actual
, "\n"
, extraStr
, missingStr
]
verticalBar : String -> String -> String -> String
verticalBar comparison expected actual =
[ actual
, ""
, " " ++ comparison
, ""
, expected
]
|> String.join "\n"
listDiffToString :
Int
-> String
-> { expected : List String, actual : List String }
-> { originalExpected : List String, originalActual : List String }
-> String
listDiffToString index description { expected, actual } originals =
case ( expected, actual ) of
( [], [] ) ->
[ "Two lists were unequal previously, yet ended up equal later."
, "This should never happen!"
, "Please report this bug to https://github.com/elm-community/elm-test/issues - and include these lists: "
, "\n"
, toString originals.originalExpected
, "\n"
, toString originals.originalActual
]
|> String.join ""
( first :: _, [] ) ->
verticalBar (description ++ " was shorter than")
(toString originals.originalExpected)
(toString originals.originalActual)
( [], first :: _ ) ->
verticalBar (description ++ " was longer than")
(toString originals.originalExpected)
(toString originals.originalActual)
( firstExpected :: restExpected, firstActual :: restActual ) ->
if firstExpected == firstActual then
-- They're still the same so far; keep going.
listDiffToString (index + 1)
description
{ expected = restExpected
, actual = restActual
}
originals
else
-- We found elements that differ; fail!
String.join ""
[ verticalBar description
(toString originals.originalExpected)
(toString originals.originalActual)
, "\n\nThe first diff is at index "
, toString index
, ": it was `"
, firstActual
, "`, but `"
, firstExpected
, "` was expected."
]
equalityToString : { operation : String, expected : String, actual : String } -> String
equalityToString { operation, expected, actual } =
-- TODO make sure this looks reasonable for multiline strings
let
( formattedExpected, belowFormattedExpected ) =
Diff.diff (String.toList expected) (String.toList actual)
|> List.map formatExpectedChange
|> List.unzip
( formattedActual, belowFormattedActual ) =
Diff.diff (String.toList actual) (String.toList expected)
|> List.map formatActualChange
|> List.unzip
combinedExpected =
String.join "\n"
[ String.join "" formattedExpected
, String.join "" belowFormattedExpected
]
combinedActual =
String.join "\n"
[ String.join "" formattedActual
, String.join "" belowFormattedActual
]
in
verticalBar operation combinedExpected combinedActual
formatExpectedChange : Change Char -> ( String, String )
formatExpectedChange diff =
case diff of
Added char ->
( "", "" )
Removed char ->
( String.fromChar char, "" )
NoChange char ->
( String.fromChar char, " " )
formatActualChange : Change Char -> ( String, String )
formatActualChange diff =
case diff of
Added char ->
( "", "" )
Removed char ->
( "", String.fromChar char )
NoChange char ->
( " ", String.fromChar char )

View File

@@ -0,0 +1,196 @@
module RunnerTests exposing (all)
import Expect
import Fuzz exposing (..)
import Helpers exposing (expectPass)
import Random.Pcg as Random
import Test exposing (..)
import Test.Runner exposing (SeededRunners(..))
all : Test
all =
Test.concat
[ fromTest ]
toSeededRunners : Test -> SeededRunners
toSeededRunners =
Test.Runner.fromTest 5 (Random.initialSeed 42)
fromTest : Test
fromTest =
describe "TestRunner.fromTest"
[ describe "test length"
[ fuzz2 int int "only positive tests runs are valid" <|
\runs intSeed ->
case Test.Runner.fromTest runs (Random.initialSeed intSeed) passing of
Invalid str ->
if runs > 0 then
Expect.fail ("Expected a run count of " ++ toString runs ++ " to be valid, but was invalid with this message: " ++ toString str)
else
Expect.pass
val ->
if runs > 0 then
Expect.pass
else
Expect.fail ("Expected a run count of " ++ toString runs ++ " to be invalid, but was valid with this value: " ++ toString val)
, test "an only inside another only has no effect" <|
\_ ->
let
runners =
toSeededRunners <|
describe "three tests"
[ test "passes" expectPass
, Test.only <|
describe "two tests"
[ test "fails" <|
\_ -> Expect.fail "failed on purpose"
, Test.only <|
test "is an only" <|
\_ -> Expect.fail "failed on purpose"
]
]
in
case runners of
Only runners ->
runners
|> List.length
|> Expect.equal 2
val ->
Expect.fail ("Expected SeededRunner to be Only, but was " ++ toString val)
, test "a skip inside an only takes effect" <|
\_ ->
let
runners =
toSeededRunners <|
describe "three tests"
[ test "passes" expectPass
, Test.only <|
describe "two tests"
[ test "fails" <|
\_ -> Expect.fail "failed on purpose"
, Test.skip <|
test "is skipped" <|
\_ -> Expect.fail "failed on purpose"
]
]
in
case runners of
Only runners ->
runners
|> List.length
|> Expect.equal 1
val ->
Expect.fail ("Expected SeededRunner to be Only, but was " ++ toString val)
, test "an only inside a skip has no effect" <|
\_ ->
let
runners =
toSeededRunners <|
describe "three tests"
[ test "passes" expectPass
, Test.skip <|
describe "two tests"
[ test "fails" <|
\_ -> Expect.fail "failed on purpose"
, Test.only <|
test "is skipped" <|
\_ -> Expect.fail "failed on purpose"
]
]
in
case runners of
Skipping runners ->
runners
|> List.length
|> Expect.equal 1
val ->
Expect.fail ("Expected SeededRunner to be Skipping, but was " ++ toString val)
, test "a test that uses only is an Only summary" <|
\_ ->
case toSeededRunners (Test.only <| test "passes" expectPass) of
Only runners ->
runners
|> List.length
|> Expect.equal 1
val ->
Expect.fail ("Expected SeededRunner to be Only, but was " ++ toString val)
, test "a skip inside another skip has no effect" <|
\_ ->
let
runners =
toSeededRunners <|
describe "three tests"
[ test "passes" expectPass
, Test.skip <|
describe "two tests"
[ test "fails" <|
\_ -> Expect.fail "failed on purpose"
, Test.skip <|
test "is skipped" <|
\_ -> Expect.fail "failed on purpose"
]
]
in
case runners of
Skipping runners ->
runners
|> List.length
|> Expect.equal 1
val ->
Expect.fail ("Expected SeededRunner to be Skipping, but was " ++ toString val)
, test "a pair of tests where one uses skip is a Skipping summary" <|
\_ ->
let
runners =
toSeededRunners <|
describe "two tests"
[ test "passes" expectPass
, Test.skip <|
test "fails" <|
\_ -> Expect.fail "failed on purpose"
]
in
case runners of
Skipping runners ->
runners
|> List.length
|> Expect.equal 1
val ->
Expect.fail ("Expected SeededRunner to be Skipping, but was " ++ toString val)
, test "when all tests are skipped, we get an empty Skipping summary" <|
\_ ->
case toSeededRunners (Test.skip <| test "passes" expectPass) of
Skipping runners ->
runners
|> List.length
|> Expect.equal 0
val ->
Expect.fail ("Expected SeededRunner to be Skipping, but was " ++ toString val)
, test "a test that does not use only or skip is a Plain summary" <|
\_ ->
case toSeededRunners (test "passes" expectPass) of
Plain runners ->
runners
|> List.length
|> Expect.equal 1
val ->
Expect.fail ("Expected SeededRunner to be Plain, but was " ++ toString val)
]
]
passing : Test
passing =
test "A passing test" expectPass

View File

@@ -0,0 +1,190 @@
module SeedTests exposing (fixedSeed, noAutoFail, tests)
import Expect exposing (FloatingPointTolerance(Absolute, AbsoluteOrRelative, Relative))
import Fuzz exposing (..)
import Random.Pcg as Random
import Test exposing (..)
-- NOTE: These tests are only here so that we can watch out for regressions. All constants in this file are what the implementation happened to output, not what we expected the implementation to output.
expectedNum : Int
expectedNum =
-3954212174
oneSeedAlreadyDistributed : Int
oneSeedAlreadyDistributed =
198384431
fixedSeed : Random.Seed
fixedSeed =
Random.initialSeed 133742
{-| Most of the tests will use this, but we won't run it directly.
When these tests are run using fixedSeed and a run count of 1, this is the
exact number they will get when the description around this fuzz test is
exactly the string "Seed test".
-}
fuzzTest : Test
fuzzTest =
fuzz int "It receives the expected number" <|
\num ->
Expect.equal num expectedNum
fuzzTestAfterOneDistributed : Test
fuzzTestAfterOneDistributed =
fuzz int "This should be different than expectedNum, because there is a fuzz test before it." <|
\num ->
Expect.equal num oneSeedAlreadyDistributed
tests : List Test
tests =
[ describe "Seed test"
[ fuzzTest ]
, describe "Seed test"
[ fuzz int "It receives the expected number even though this text is different" <|
\num ->
Expect.equal num expectedNum
]
, describe "Seed test"
[ describe "Nested describes shouldn't affect seed distribution"
[ fuzzTest ]
]
, describe "Seed test"
[ test "Unit tests before should not affect seed distribution" <|
\_ ->
Expect.pass
, fuzzTest
, test "Unit tests after should not affect seed distribution" <|
\_ ->
Expect.pass
]
, -- Wrapping in a Test.concat shouldn't change anything
Test.concat
[ describe "Seed test"
[ fuzzTest ]
]
, -- Wrapping in a Test.concat wth unit tests shouldn't change anything
Test.concat
[ describe "Seed test"
[ test "Unit tests before should not affect seed distribution" <|
\_ ->
Expect.pass
, fuzzTest
, test "Unit tests after should not affect seed distribution" <|
\_ ->
Expect.pass
]
]
, -- Putting a fuzz test before it, within a second label, *should* change things
Test.concat
[ describe "Seed test"
[ fuzzTest
, fuzzTestAfterOneDistributed
]
]
, Test.concat
[ fuzz int "top-level fuzz tests don't affect subsequent top-level fuzz tests, since they use their labels to get different seeds" <|
\num ->
Expect.equal num 409469537
, describe "Seed test"
[ fuzzTest ]
, describe "another top-level fuzz test"
[ fuzz int "it still gets different values, due to computing the seed as a hash of the label, and these labels must be unique" <|
\num ->
Expect.equal num 0
]
]
, describe "Fuzz tests with different outer describe texts get different seeds"
[ fuzz int "It receives the expected number" <|
\num ->
Expect.equal num 2049737128
]
]
noAutoFail : List Test
noAutoFail =
[ -- Test.skip does not affect seed distribution
Test.concat
[ describe "Seed test"
[ skip fuzzTest
, fuzzTestAfterOneDistributed
]
]
, -- Test.only does not affect seed distribution
Test.concat
[ describe "Seed test"
[ only fuzzTest ]
]
, -- Test.only skips the other tests in question
Test.concat
[ describe "Seed test"
[ skip <|
test "Autofail" <|
\_ ->
Expect.fail "Test.skip is broken! This should not have been run."
, fuzzTest
]
]
, -- Test.only skips the other tests.
Test.concat
[ describe "Seed test"
[ only <|
fuzz int "No Autofail here" <|
\num ->
Expect.equal num expectedNum
, test "This should never get run" <|
\() ->
Expect.fail "Test.only is broken! This should not have been run."
]
]
, -- Test.skip skips the test in question
describe "Seed test"
[ skip <|
fuzz int "Skip test sanity check" <|
\_ ->
Expect.fail "Test.skip is broken! This should not have been run."
, fuzzTestAfterOneDistributed
]
, -- the previous test gets the same answer if Test.skip is removed
describe "Seed test"
[ fuzz int "Skip test sanity check" <|
\_ ->
Expect.pass
, fuzzTestAfterOneDistributed
]
, -- Test.only skips the other tests.
describe "Seed test"
[ only <|
fuzz int "No Autofail here" <|
\num ->
Expect.equal num expectedNum
, test "this should never get run" <|
\() ->
Expect.fail "Test.only is broken! This should not have been run."
]
, -- Test.only does not affect seed distribution
describe "Seed test"
[ test "Autofail" <|
\_ -> Expect.fail "Test.only is broken! This should not have been run."
, fuzzTest
, only <|
fuzzTestAfterOneDistributed
]
, -- the previous test gets the same answer if Test.only is removed
describe "Seed test"
[ test "Autofail" <|
\_ -> Expect.pass
, fuzzTest
, fuzzTestAfterOneDistributed
]
]

View File

@@ -0,0 +1,175 @@
module Tests exposing (all)
import Expect exposing (FloatingPointTolerance(Absolute, AbsoluteOrRelative, Relative))
import FloatWithinTests exposing (floatWithinTests)
import Fuzz exposing (..)
import FuzzerTests exposing (fuzzerTests)
import Helpers exposing (..)
import Random.Pcg as Random
import RunnerTests
import Shrink
import Test exposing (..)
import Test.Expectation exposing (Expectation(..))
import Test.Runner
import Test.Runner.Failure exposing (Reason(..))
all : Test
all =
Test.concat
[ readmeExample
, regressions
, testTests
, expectationTests
, fuzzerTests
, floatWithinTests
, RunnerTests.all
]
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"
, fuzz string "restores the original string if you run it again" <|
\randomlyGeneratedString ->
randomlyGeneratedString
|> String.reverse
|> String.reverse
|> Expect.equal randomlyGeneratedString
]
]
expectationTests : Test
expectationTests =
describe "Expectations"
[ describe "Expect.err"
[ test "passes on Err _" <|
\_ ->
Err 12 |> Expect.err
, expectToFail <|
test "passes on Ok _" <|
\_ ->
Ok 12 |> Expect.err
]
, describe "Expect.all"
[ expectToFail <|
test "fails with empty list" <|
\_ -> "dummy subject" |> Expect.all []
]
]
regressions : Test
regressions =
describe "regression tests"
[ fuzz (intRange 1 32) "for #39" <|
\positiveInt ->
positiveInt
|> Expect.greaterThan 0
, fuzz
(custom (Random.int 1 8) Shrink.noShrink)
"fuzz tests run 100 times"
(Expect.notEqual 5)
|> expectToFail
{- If fuzz tests actually run 100 times, then asserting that no number
in 1..8 equals 5 fails with 0.999998 probability. If they only run
once, or stop after a duplicate due to #127, then it's much more
likely (but not guaranteed) that the 5 won't turn up. See #128.
-}
]
testTests : Test
testTests =
describe "functions that create tests"
[ describe "describe"
[ expectToFail <| describe "fails with empty list" []
, expectToFail <| describe "" [ test "describe with empty description fail" expectPass ]
]
, describe "test"
[ expectToFail <| test "" expectPass
]
, describe "fuzz"
[ expectToFail <| fuzz Fuzz.bool "" expectPass
]
, describe "fuzzWith"
[ expectToFail <| fuzzWith { runs = 0 } Fuzz.bool "nonpositive" expectPass
, expectToFail <| fuzzWith { runs = 1 } Fuzz.bool "" expectPass
]
, describe "Test.todo"
[ expectToFail <| todo "a TODO test fails"
, test "Passes are not TODO"
(\_ -> Expect.pass |> Test.Runner.isTodo |> Expect.false "was true")
, test "Simple failures are not TODO" <|
\_ ->
Expect.fail "reason" |> Test.Runner.isTodo |> Expect.false "was true"
, test "Failures with TODO reason are TODO" <|
\_ ->
Test.Expectation.fail { description = "", reason = TODO }
|> Test.Runner.isTodo
|> Expect.true "was false"
]
, identicalNamesAreRejectedTests
]
identicalNamesAreRejectedTests : Test
identicalNamesAreRejectedTests =
describe "Identically-named sibling and parent/child tests fail"
[ expectToFail <|
describe "a describe with two identically named children fails"
[ test "foo" expectPass
, test "foo" expectPass
]
, expectToFail <|
describe "a describe with the same name as a child test fails"
[ test "a describe with the same name as a child test fails" expectPass
]
, expectToFail <|
describe "a describe with the same name as a child describe fails"
[ describe "a describe with the same name as a child describe fails"
[ test "a test" expectPass ]
]
, expectToFail <|
Test.concat
[ describe "a describe with the same name as a sibling describe fails"
[ test "a test" expectPass ]
, describe "a describe with the same name as a sibling describe fails"
[ test "another test" expectPass ]
]
, expectToFail <|
Test.concat
[ Test.concat
[ describe "a describe with the same name as a de facto sibling describe fails"
[ test "a test" expectPass ]
]
, describe "a describe with the same name as a de facto sibling describe fails"
[ test "another test" expectPass ]
]
, expectToFail <|
Test.concat
[ Test.concat
[ describe "a describe with the same name as a de facto sibling describe fails"
[ test "a test" expectPass ]
]
, Test.concat
[ describe "a describe with the same name as a de facto sibling describe fails"
[ test "another test" expectPass ]
]
]
]

View File

@@ -0,0 +1,20 @@
{
"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": {
"eeue56/elm-lazy-list": "1.0.0 <= v < 2.0.0",
"eeue56/elm-shrink": "1.0.0 <= v < 2.0.0",
"elm-lang/core": "5.0.0 <= v < 6.0.0",
"jinjor/elm-diff": "1.0.0 <= v < 2.0.0",
"eeue56/elm-lazy": "1.0.0 <= v < 2.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,22 @@
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,4 @@
# Ignore build or dist files
elm-stuff
node_modules
tests/Doc

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,68 @@
### 2.6.0
**Additions:**
- `keys` allows extracting _only_ the keys from a JSON object
### 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,21 @@
The MIT License (MIT)
Copyright (c) 2016 CircuitHub Inc., Elm Community members
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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,17 @@
{
"version": "2.6.0",
"summary": "Convenience functions for working with Json",
"repository": "https://github.com/elm-community/json-extra.git",
"license": "MIT",
"source-directories": [
"src"
],
"exposed-modules": [
"Json.Decode.Extra",
"Json.Encode.Extra"
],
"dependencies": {
"elm-lang/core": "5.0.0 <= v < 6.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
{
"devDependencies": {
"elm-test": "^0.18.9",
"elm-verify-examples": "^1.0.2"
},
"scripts": {
"test": "elm-verify-examples && elm-test"
}
}

View File

@@ -0,0 +1,434 @@
module Json.Decode.Extra
exposing
( (|:)
, andMap
, collection
, combine
, date
, dict2
, doubleEncoded
, fromResult
, indexedList
, keys
, optionalField
, parseFloat
, parseInt
, sequence
, set
, withDefault
)
{-| Convenience functions for working with Json
Examples assume the following imports:
import Json.Decode exposing (..)
import Date
import Set
import Dict
# Date
@docs date
# Incremental Decoding
@docs andMap, (|:)
# List
@docs collection, sequence, combine, indexedList, keys
# Set
@docs set
# Dict
@docs dict2
# Maybe
@docs withDefault, optionalField
# Result
@docs fromResult
# Encoded strings
@docs parseInt, parseFloat, doubleEncoded
-}
import Date
import Dict exposing (Dict)
import Json.Decode exposing (..)
import Set exposing (Set)
import String
{-| Can be helpful when decoding large objects incrementally.
See [the `andMap` docs](https://github.com/elm-community/json-extra/blob/2.0.0/docs/andMap.md)
for an explanation of how `andMap` works and how to use it.
-}
andMap : Decoder a -> Decoder (a -> b) -> Decoder b
andMap =
map2 (|>)
{-| Infix version of `andMap` that makes for a nice DSL when decoding objects.
See [the `(|:)` docs](https://github.com/elm-community/json-extra/blob/2.0.0/docs/infixAndMap.md)
for an explanation of how `(|:)` works and how to use it.
-}
(|:) : Decoder (a -> b) -> Decoder a -> Decoder b
(|:) =
flip andMap
{-| Extract a date using [`Date.fromString`](http://package.elm-lang.org/packages/elm-lang/core/latest/Date#fromString)
""" "2012-04-23T18:25:43.511Z" """
|> decodeString date
--> Date.fromString "2012-04-23T18:25:43.511Z"
""" "foo" """
|> decodeString date
--> Err "I ran into a `fail` decoder: Unable to parse 'foo' as a date. Dates must be in the ISO 8601 format."
-}
date : Decoder Date.Date
date =
string
|> andThen (Date.fromString >> fromResult)
{-| Extract a set.
"[ 1, 1, 5, 2 ]"
|> decodeString (set int)
--> Ok <| Set.fromList [ 1, 2, 5 ]
-}
set : Decoder comparable -> Decoder (Set comparable)
set decoder =
list decoder
|> map Set.fromList
{-| Extract a dict using separate decoders for keys and values.
""" { "1": "foo", "2": "bar" } """
|> decodeString (dict2 int string)
--> Ok <| Dict.fromList [ ( 1, "foo" ), ( 2, "bar" ) ]
-}
dict2 : Decoder comparable -> Decoder v -> Decoder (Dict comparable v)
dict2 keyDecoder valueDecoder =
keyValuePairs valueDecoder
|> andThen (decodeDictFromTuples keyDecoder)
{-| Helper function for dict
-}
decodeDictFromTuples : Decoder comparable -> List ( String, v ) -> Decoder (Dict comparable v)
decodeDictFromTuples keyDecoder tuples =
case tuples of
[] ->
succeed Dict.empty
( strKey, value ) :: rest ->
case decodeString keyDecoder strKey of
Ok key ->
decodeDictFromTuples keyDecoder rest
|> andThen (Dict.insert key value >> succeed)
Err error ->
fail error
{-| Try running the given decoder; if that fails, then succeed with the given
fallback value.
""" { "children": "oops" } """
|> decodeString (field "children" (list string) |> withDefault [])
--> Ok []
""" null """
|> decodeString (field "children" (list string) |> withDefault [])
--> Ok []
""" 30 """
|> decodeString (int |> withDefault 42)
--> Ok 30
-}
withDefault : a -> Decoder a -> Decoder a
withDefault fallback decoder =
maybe decoder
|> map (Maybe.withDefault fallback)
{-| If a field is missing, succeed with `Nothing`. If it is present, decode it
as normal and wrap successes in a `Just`.
When decoding with
[`maybe`](http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#maybe),
if a field is present but malformed, you get a success and Nothing.
`optionalField` gives you a failed decoding in that case, so you know
you received malformed data.
Examples:
Let's define a `stuffDecoder` that extracts the `"stuff"` field, if it exists.
stuffDecoder : Decoder (Maybe String)
stuffDecoder =
optionalField "stuff" string
If the "stuff" field is missing, decode to Nothing.
""" { } """
|> decodeString stuffDecoder
--> Ok Nothing
If the "stuff" field is present but not a String, fail decoding.
""" { "stuff": [] } """
|> decodeString stuffDecoder
--> Err "Expecting a String at _.stuff but instead got: []"
If the "stuff" field is present and valid, decode to Just String.
""" { "stuff": "yay!" } """
|> decodeString stuffDecoder
--> Ok <| Just "yay!"
-}
optionalField : String -> Decoder a -> Decoder (Maybe a)
optionalField fieldName decoder =
let
finishDecoding json =
case decodeValue (field fieldName value) json of
Ok val ->
-- The field is present, so run the decoder on it.
map Just (field fieldName decoder)
Err _ ->
-- The field was missing, which is fine!
succeed Nothing
in
value
|> andThen finishDecoding
{-| This function turns a list of decoders into a decoder that returns a list.
The returned decoder will zip the list of decoders with a list of values,
matching each decoder with exactly one value at the same position. This is most
often useful in cases when you find yourself needing to dynamically generate a
list of decoders based on some data, and decode some other data with this list
of decoders.
Note that this function, unlike `List.map2`'s behaviour, expects the list of
decoders to have the same length as the list of values in the JSON.
sequence
[ map Just string
, succeed Nothing
, map Just string
]
|> flip decodeString """ [ "pick me", "ignore me", "and pick me" ] """
--> Ok [ Just "pick me", Nothing, Just "and pick me" ]
-}
sequence : List (Decoder a) -> Decoder (List a)
sequence decoders =
list value |> andThen (sequenceHelp decoders)
{-| Helper function for sequence
-}
sequenceHelp : List (Decoder a) -> List Value -> Decoder (List a)
sequenceHelp decoders jsonValues =
if List.length jsonValues /= List.length decoders then
fail "Number of decoders does not match number of values"
else
List.map2 decodeValue decoders jsonValues
|> List.foldr (Result.map2 (::)) (Ok [])
|> fromResult
{-| Get access to the current index while decoding a list element.
repeatedStringDecoder : Int -> Decoder String
repeatedStringDecoder times =
string |> map (String.repeat times)
""" [ "a", "b", "c", "d" ] """
|> decodeString (indexedList repeatedStringDecoder)
--> Ok [ "", "b", "cc", "ddd" ]
-}
indexedList : (Int -> Decoder a) -> Decoder (List a)
indexedList indexedDecoder =
list value
|> andThen
(\values ->
List.range 0 (List.length values - 1)
|> List.map indexedDecoder
|> sequence
)
{-| Get a list of the keys of a JSON object
""" { "alice": 42, "bob": 99 } """
|> decodeString keys
--> Ok [ "alice", "bob" ]
-}
keys : Decoder (List String)
keys =
keyValuePairs (succeed ())
|> map (List.foldl (\( key, _ ) acc -> key :: acc) [])
{-| Transform a result into a decoder
Sometimes it can be useful to use functions that primarily operate on
`Result` in decoders. An example of this is `Json.Decode.Extra.date`. It
uses the built-in `Date.fromString` to parse a `String` as a `Date`, and
then converts the `Result` from that conversion into a decoder which has
either already succeeded or failed based on the outcome.
validateString : String -> Result String String
validateString input =
case input of
"" ->
Err "Empty string is not allowed"
_ ->
Ok input
""" "something" """
|> decodeString (string |> andThen (fromResult << validateString))
--> Ok "something"
""" "" """
|> decodeString (string |> andThen (fromResult << validateString))
--> Err "I ran into a `fail` decoder: Empty string is not allowed"
-}
fromResult : Result String a -> Decoder a
fromResult result =
case result of
Ok successValue ->
succeed successValue
Err errorMessage ->
fail errorMessage
{-| Extract an int using [`String.toInt`](http://package.elm-lang.org/packages/elm-lang/core/latest/String#toInt)
""" { "field": "123" } """
|> decodeString (field "field" parseInt)
--> Ok 123
-}
parseInt : Decoder Int
parseInt =
string |> andThen (String.toInt >> fromResult)
{-| Extract a float using [`String.toFloat`](http://package.elm-lang.org/packages/elm-lang/core/latest/String#toFloat)
""" { "field": "50.5" } """
|> decodeString (field "field" parseFloat)
--> Ok 50.5
-}
parseFloat : Decoder Float
parseFloat =
string |> andThen (String.toFloat >> fromResult)
{-| Extract a JSON-encoded string field
"Yo dawg, I heard you like JSON..."
If someone has put JSON in your JSON (perhaps a JSON log entry, encoded
as a string) this is the function you're looking for. Give it a decoder
and it will return a new decoder that applies your decoder to a string
field and yields the result (or fails if your decoder fails).
logEntriesDecoder : Decoder (List String)
logEntriesDecoder =
doubleEncoded (list string)
logsDecoder : Decoder (List String)
logsDecoder =
field "logs" logEntriesDecoder
""" { "logs": "[\\"log1\\", \\"log2\\"]"} """
|> decodeString logsDecoder
--> Ok [ "log1", "log2" ]
-}
doubleEncoded : Decoder a -> Decoder a
doubleEncoded decoder =
string |> andThen (fromResult << decodeString decoder)
{-| Helps convering a list of decoders into a decoder for a list of that type.
decoders : List (Decoder String)
decoders =
[ field "foo" string
, field "bar" string
, field "another" string
]
""" { "foo": "hello", "another": "!", "bar": "world" } """
|> decodeString (combine decoders)
--> Ok [ "hello", "world", "!" ]
-}
combine : List (Decoder a) -> Decoder (List a)
combine =
List.foldr (map2 (::)) (succeed [])
{-| Some JavaScript structures look like arrays, but aren't really. Examples
include `HTMLCollection`, `NodeList` and everything else that has a `length`
property, has values indexed by an integer key between 0 and `length`, but yet
_is not_ a JavaScript Array.
This decoder can come to the rescue.
""" { "length": 3, "0": "foo", "1": "bar", "2": "baz" } """
|> decodeString (collection string)
--> Ok [ "foo", "bar", "baz" ]
-}
collection : Decoder a -> Decoder (List a)
collection decoder =
field "length" int
|> andThen
(\length ->
List.range 0 (length - 1)
|> List.map (\index -> field (toString index) decoder)
|> combine
)

View File

@@ -0,0 +1,43 @@
module Json.Encode.Extra exposing (dict, maybe)
{-| Convenience functions for turning Elm values into Json values.
@docs dict, maybe
-}
import Dict exposing (Dict)
import Json.Encode exposing (Value, encode, int, null, object)
{-| Encode a Maybe value. If the value is `Nothing` it will be encoded as `null`
import Json.Encode exposing (int, null, encode)
maybe int (Just 50)
--> int 50
maybe int Nothing
--> null
-}
maybe : (a -> Value) -> Maybe a -> Value
maybe encoder =
Maybe.map encoder >> Maybe.withDefault null
{-| Turn a `Dict` into a JSON object.
import Dict
Dict.fromList [ ( "Sue", 38 ), ( "Tom", 42 ) ]
|> dict identity int
|> encode 0
--> """{"Sue":38,"Tom":42}"""
-}
dict : (comparable -> String) -> (v -> Value) -> Dict comparable v -> Value
dict toKey toValue dict =
Dict.toList dict
|> List.map (\( key, value ) -> ( toKey key, toValue value ))
|> object

View File

@@ -0,0 +1,16 @@
{
"version": "1.0.0",
"summary": "Test Suites",
"repository": "https://github.com/elm-community/json-extra.git",
"license": "MIT",
"source-directories": [
"../src",
"."
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "5.0.0 <= v < 6.0.0",
"elm-community/elm-test": "4.0.0 <= v < 5.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

View File

@@ -0,0 +1,7 @@
{
"root": "../src",
"tests": [
"Json.Encode.Extra",
"Json.Decode.Extra"
]
}