├── .flowconfig ├── .github └── workflows │ ├── check.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── elm-test ├── elm-tooling.json ├── elm ├── .gitignore ├── elm.json ├── review │ ├── elm.json │ └── src │ │ └── ReviewConfig.elm ├── src │ ├── Console │ │ └── Text.elm │ └── Test │ │ ├── Reporter │ │ ├── Console.elm │ │ ├── Console │ │ │ ├── Format.elm │ │ │ └── Format │ │ │ │ ├── Color.elm │ │ │ │ └── Monochrome.elm │ │ ├── Highlightable.elm │ │ ├── JUnit.elm │ │ ├── Json.elm │ │ ├── Reporter.elm │ │ └── TestResults.elm │ │ └── Runner │ │ ├── JsMessage.elm │ │ ├── Node.elm │ │ └── Node │ │ └── Vendor │ │ ├── Console.elm │ │ └── Diff.elm └── tests │ └── Test │ └── Reporter │ └── Console │ └── FormatTest.elm ├── eslint.config.mjs ├── example-application-no-tests ├── .gitignore ├── elm.json ├── src │ └── Something.elm └── tests │ └── Example.elm ├── example-application-src ├── .gitignore ├── elm.json └── src │ └── Something.elm ├── example-application ├── .gitignore ├── elm.json ├── package.json ├── src │ └── Something.elm └── tests │ ├── TestsFailing.elm │ └── TestsPassing.elm ├── example-package-no-tests ├── .gitignore ├── elm.json ├── src │ └── Something.elm └── tests │ └── Example.elm ├── example-package ├── .gitignore ├── elm.json ├── src │ ├── Something.elm │ └── SomethingTest.elm ├── test.json └── tests │ ├── Example.elm │ ├── TestsFailing.elm │ └── TestsPassing.elm ├── lib ├── Compile.js ├── DependencyProvider.js ├── ElmCompiler.js ├── ElmHome.js ├── ElmJson.js ├── FindTests.js ├── Generate.js ├── Install.js ├── Parser.js ├── Project.js ├── Report.js ├── RunTests.js ├── Solve.js ├── Supervisor.js ├── SyncGet.js ├── SyncGetWorker.js └── elm-test.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── templates ├── after.js ├── before.js └── tests │ └── Example.elm └── tests ├── ElmJson.js ├── Parser.js ├── ci.js ├── fixtures ├── .gitignore ├── elm.json ├── install │ ├── elm.json │ └── test-elm.json ├── invalid-elm-json │ ├── application-with-package-style-test-dependencies │ │ ├── elm.json │ │ └── expected.txt │ ├── dependency-not-string │ │ ├── elm.json │ │ └── expected.txt │ ├── elm-test-package-too-old-application │ │ ├── elm.json │ │ └── expected.txt │ ├── elm-test-package-too-old-package │ │ ├── elm.json │ │ └── expected.txt │ ├── empty-source-directories │ │ ├── elm.json │ │ └── expected.txt │ ├── is-folder │ │ ├── elm.json │ │ │ └── .gitkeep │ │ └── expected.txt │ ├── is-null │ │ ├── elm.json │ │ └── expected.txt │ ├── json-syntax-error │ │ ├── elm.json │ │ └── expected.txt │ ├── missing-elm-test-package │ │ ├── elm.json │ │ └── expected.txt │ ├── null-type │ │ ├── elm.json │ │ └── expected.txt │ ├── package-with-application-style-dependencies │ │ ├── elm.json │ │ └── expected.txt │ ├── source-directories-not-array │ │ ├── elm.json │ │ └── expected.txt │ ├── source-directory-not-string │ │ ├── elm.json │ │ └── expected.txt │ └── unknown-type │ │ ├── elm.json │ │ └── expected.txt ├── src │ ├── Port1.elm │ └── Port2.elm ├── templates │ ├── application │ │ └── elm.json │ └── package │ │ └── elm.json ├── tests │ ├── CompileError │ │ ├── InvalidSyntax.elm │ │ └── NoTests.elm │ ├── Distribution │ │ ├── Everything.elm │ │ ├── ExpectDistributionFailingDistribution.elm │ │ ├── ExpectDistributionFailingTest.elm │ │ ├── ExpectDistributionPassing.elm │ │ ├── ReportDistributionFailing.elm │ │ └── ReportDistributionPassing.elm │ ├── Failing │ │ ├── Fuzz.elm │ │ ├── One.elm │ │ ├── OneRuntimeException.elm │ │ ├── OneTodo.elm │ │ ├── Several.elm │ │ ├── SeveralTodos.elm │ │ ├── SeveralWithComments.elm │ │ └── SplitSocketMessage.elm │ ├── InvalidXMLCharacter │ │ └── Test.elm │ ├── Nested │ │ └── Module │ │ │ └── Test.elm │ ├── Passing │ │ ├── .gitignore │ │ ├── Dedup │ │ │ └── One.elm │ │ ├── Dependency.elm │ │ ├── One.elm │ │ ├── Ports.elm │ │ ├── Several.elm │ │ ├── TrickyMultilines.elm │ │ └── Unexposed.elm │ └── RuntimeException │ │ └── OnePort.elm └── write-elm-json │ ├── .gitignore │ └── elm.input.json ├── flags.js ├── init-test ├── elm-package.json └── sources │ └── elm │ └── MagicConstant.elm_todo_rename_to_dot_elm ├── test-quality.js └── util.js /.flowconfig: -------------------------------------------------------------------------------- 1 | [options] 2 | include_warnings=true 3 | exact_by_default=true 4 | 5 | [lints] 6 | all=error 7 | untyped-import=off 8 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | 9 | jobs: 10 | main: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | node-version: [22.x] 17 | 18 | env: 19 | ELM_HOME: '${{ github.workspace }}/elm-stuff/elm-home' 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: '${{ matrix.node-version }}' 27 | 28 | - name: Cache node_modules 29 | id: cache-node_modules 30 | uses: actions/cache@v4 31 | with: 32 | path: node_modules 33 | key: node_modules-${{ matrix.os }}-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} 34 | 35 | - name: Cache ELM_HOME 36 | uses: actions/cache@v4 37 | with: 38 | path: '${{ env.ELM_HOME }}' 39 | key: elm-${{ matrix.os }}-${{ hashFiles('elm-tooling.json', 'elm/**/elm.json', 'example-*/**/elm.json', 'tests/**/elm.json') }} 40 | 41 | - name: npm ci 42 | if: steps.cache-node_modules.outputs.cache-hit != 'true' 43 | run: npm ci 44 | env: 45 | NO_ELM_TOOLING_INSTALL: 1 46 | 47 | - name: elm-tooling install 48 | run: npx --no-install elm-tooling install 49 | 50 | - name: Flow 51 | run: npx --no-install flow check 52 | 53 | - name: ESLint 54 | run: npx --no-install eslint --report-unused-disable-directives . 55 | 56 | - name: Prettier 57 | run: npx --no-install prettier --check . 58 | 59 | - name: elm-format 60 | run: npx --no-install elm-format --validate elm 61 | 62 | - name: elm-review 63 | run: npx --no-install elm-review 64 | working-directory: elm 65 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | 9 | jobs: 10 | main: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | # macOS-13 is the latest macOS version that is x86. 16 | # Node.js 12, 14 and 16 aren’t support on the arm64 runners. 17 | os: [ubuntu-latest, macOS-13, windows-latest] 18 | node-version: [12.x, 14.x, 16.x, 18.x, 20.x, 22.x] 19 | # Also have a test on macOS arm64. 20 | include: 21 | - os: macOS-latest 22 | node-version: 22.x 23 | 24 | env: 25 | ELM_HOME: '${{ github.workspace }}/elm-stuff/elm-home' 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version: '${{ matrix.node-version }}' 33 | 34 | - name: Cache node_modules 35 | id: cache-node_modules 36 | uses: actions/cache@v4 37 | with: 38 | path: node_modules 39 | key: node_modules-${{ matrix.os }}-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} 40 | 41 | - name: Cache ELM_HOME 42 | uses: actions/cache@v4 43 | with: 44 | path: '${{ env.ELM_HOME }}' 45 | key: elm-${{ matrix.os }}-${{ hashFiles('elm-tooling.json', 'elm/**/elm.json', 'example-*/**/elm.json', 'tests/**/elm.json') }} 46 | 47 | - name: npm ci 48 | if: steps.cache-node_modules.outputs.cache-hit != 'true' 49 | run: npm ci 50 | env: 51 | NO_ELM_TOOLING_INSTALL: 1 52 | 53 | - name: install mocha 9 54 | if: steps.cache-node_modules.outputs.cache-hit != 'true' && (matrix.node-version == '12.x' || matrix.node-version == '14.x' || matrix.node-version == '16.x') 55 | run: npm install mocha@9 56 | 57 | - name: elm-tooling install 58 | run: npx --no-install elm-tooling install 59 | 60 | - name: Mocha 61 | run: npx --no-install mocha tests 62 | 63 | - name: elm-test 64 | run: npm run elm-test 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | /coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | /elm-stuff 29 | tmp 30 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | elm-stuff 3 | fixtures 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Richard Feldman 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of node-test-runner nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-test-runner [![Version](https://img.shields.io/npm/v/elm-test.svg)](https://www.npmjs.com/package/elm-test) 2 | 3 | Runs [elm-explorations/test] suites in Node.js. 4 | 5 | When people say “elm-test” they usually refer to either: 6 | 7 | - This CLI tool for running tests. 8 | - [elm-explorations/test] – an Elm package for defining tests that this CLI tool can run. 9 | 10 | [elm-explorations/test]: https://package.elm-lang.org/packages/elm-explorations/test/latest 11 | 12 | ## Versions 13 | 14 | Not all versions of [elm-explorations/test] and this CLI tool work together! 15 | 16 | | elm-explorations/test | elm-test CLI | 17 | | --------------------- | -------------------- | 18 | | >= 2.0.0 | >= 0.19.1-revision10 | 19 | | <= 1.2.2 | <= 0.19.1-revision9 | 20 | 21 | > **Unfortunate behavior of 0.19.1-revision9 and older** 22 | > 23 | > - `elm-test init` always installs the latest [elm-explorations/test]. This means that if you run `elm-test init` on version 0.19.1-revision9 or older, you will get elm-explorations/test 2.0.0 or later, which don’t work 100 % together (see the next point). 24 | > - elm-test 0.19.1-revision9 or older do _not_ validate that [elm-explorations/test] in your elm.json has a compatible version. If you upgrade to elm-explorations/test 2.0.0 or later but forget to upgrade the elm-test CLI, most things will still work, but test distribution diagrams (new in elm-explorations/test 2.0.0) won’t show up. So if you use `Test.fuzzWith` and wonder why distribution diagrams never show up – check your elm-test CLI version! 25 | > - There exists an elm-test CLI version called just "0.19.1". It should have been called "0.19.1-revision1", but unfortunately isn’t. Don’t make the mistake thinking it’s the latest version! You always want "0.19.1-revisionX". 26 | 27 | ## Installation 28 | 29 | ``` 30 | npm install --save-dev elm-test 31 | ``` 32 | 33 | ## Quick start 34 | 35 | Install [elm-explorations/test] and create `tests/Example.elm`: 36 | 37 | npx elm-test init 38 | 39 | Run tests in the `tests/` folder: 40 | 41 | npx elm-test 42 | 43 | Run tests in one particular file: 44 | 45 | npx elm-test tests/Example.elm 46 | 47 | Run tests in files matching a [glob](https://github.com/isaacs/node-glob#glob-primer): 48 | 49 | npx elm-test "src/**/*Tests.elm" 50 | 51 | > Note: The double quotes are important! Without quotes, your shell might expand the globs for you. With quotes, elm-test expands the globs. This way the watcher can pick up new tests matching the globs, and it will work cross-platform. 52 | 53 | Run in watch mode: 54 | 55 | npx elm-test --watch 56 | 57 | ## Where to put tests 58 | 59 | ### Locating files containing tests 60 | 61 | There are 3 places you could put your tests: 62 | 63 | 1. In the `tests/` folder. 64 | 65 | This is the default and requires no extra setup. 66 | 67 | 2. In any source directory (`"source-directories"` in `elm.json` for applications, `src/` for packages) as separate files. 68 | 69 | A convention is to put test files next to the file it tests with a `Tests` suffix. For example, you could have `src/LoginForm.elm` and `src/LoginFormTests.elm`. 70 | 71 | This requires telling elm-test which folders/files to run. Examples: 72 | 73 | npx elm-test "src/**/*Tests.elm" 74 | npx elm-test test/frontend/elm 75 | 76 | You might also need to configure your editor to understand that the `"test-dependencies"` in your `elm.json` are available in these files. 77 | 78 | 3. In already existing source files. 79 | 80 | This allows testing internal functions without exposing them. (Be aware that testing implementation details can sometimes be counter-productive.) 81 | 82 | This requires moving everything in `"test-dependencies"` in your `elm.json` into regular `"dependencies"`, so your project still compiles. This also helps your editor. Note that this approach isn’t suitable for packages, since you don’t want your package to unnecessarily depend on [elm-explorations/test]. 83 | 84 | You can mix all three variants if you want: 85 | 86 | npx elm-test tests "src/**/*Tests.elm" app 87 | 88 | > In this example, `"src"` and `"app"` need to be in `"source-directories"` in `elm.json`. 89 | 90 | ### Locating tests within files 91 | 92 | For elm-test to find tests in your files you need to: 93 | 94 | 1. Create top-level values of the type [Test](https://package.elm-lang.org/packages/elm-explorations/test/latest/Test#Test). You can name the values anything – the only thing that matters is that their type is `Test`. 95 | 2. Expose them. 96 | 97 | Example: 98 | 99 | ```elm 100 | module LoginForm exposing (alreadyLoggedInTests, tests) 101 | 102 | import Test exposing (Test) 103 | 104 | 105 | tests : Test 106 | tests = 107 | -- ... 108 | 109 | 110 | alreadyLoggedInTests : Test 111 | alreadyLoggedInTests = 112 | -- ... 113 | ``` 114 | 115 | Some prefer to expose a single `Test` value and group everything using [describe](https://package.elm-lang.org/packages/elm-explorations/test/latest/Test#describe). Some prefer to expose several `Test` values. 116 | 117 | **Also check out the [elm-explorations/test quick-start](https://github.com/elm-explorations/test#quick-start) guide!** 118 | 119 | ## Command Line Arguments 120 | 121 | These are the most common commands and flags. Run `elm-test --help` for an exhaustive list. 122 | 123 | **Note:** Throughout this section, the `npx` prefix is omitted for brevity. 124 | 125 | ### install 126 | 127 | Like `elm install`, except elm-test will install to `"test-dependencies"` in your `elm.json` instead of to `"dependencies"`. 128 | 129 | elm-test install elm/regex 130 | 131 | ### init 132 | 133 | Runs `elm-test install elm-explorations/test` and then creates a `tests/Example.elm` example test to get you started. 134 | 135 | `elm-test init` requires an `elm.json` file up the directory tree, so you will need to run `elm init` first if you don’t already have one. 136 | 137 | After initializing elm-test in your project, try out the example by running `elm-test` with no arguments. 138 | 139 | elm init 140 | elm-test init 141 | elm-test 142 | 143 | ### --watch 144 | 145 | Start the runner in watch mode. Your tests will automatically rerun whenever your project changes. 146 | 147 | elm-test --watch 148 | 149 | ### --seed 150 | 151 | Run with a specific fuzzer seed, rather than a randomly generated seed. This allows reproducing a failing fuzz-test. The command needed to reproduce (including the `--seed` flag) is printed after each test run. Copy, paste and run it! 152 | 153 | elm-test --seed 336948560956134 154 | 155 | ### --fuzz 156 | 157 | Define how many times each fuzz-test should run. Defaults to `100`. 158 | 159 | elm-test --fuzz 500 160 | 161 | ### --report 162 | 163 | Specify which format to use for reporting test results. Valid options are: 164 | 165 | - `console` (default): pretty, human readable formatted output. 166 | - `json`: newline-delimited json with an object for each event. 167 | - `junit`: junit-compatible xml. 168 | 169 | ``` 170 | elm-test --report json 171 | ``` 172 | 173 | ### --no-color 174 | 175 | Disable colored console output. 176 | 177 | Colors are also disabled when you pipe the output of `elm-test` to another program. You can use `--color` to force the colors back. 178 | 179 | Alternatively, you can set the environment variable `FORCE_COLOR` to `0` to disable colors, or to any other value to force them. 180 | 181 | See [chalk.supportsColor](https://github.com/chalk/chalk#chalksupportscolor) for more information. 182 | 183 | ### --compiler 184 | 185 | If `elm` is _not_ in your `$PATH` when elm-test runs, or the Elm executable is called something other than `elm`, you can use this flag to point to your installation. 186 | 187 | elm-test --compiler /path/to/elm 188 | 189 | To run a tool installed locally using `npm` you can use `npx`: 190 | 191 | npx elm-test 192 | 193 | `npx` adds the local `node_modules/.bin/` folder to `$PATH` when it executes the command passed to it. This means that if you have installed `elm` locally, `elm-test` will automatically find that local installation. 194 | 195 | As mentioned in [Installation](#installation) we recommend installing elm-test locally in every project. This ensures all contributors and CI use the same version, to avoid nasty “works on my computer” issues. 196 | 197 | ## Travis CI 198 | 199 | If you want to run your tests on Travis CI, [here's a good starter `.travis.yml`](https://docs.travis-ci.com/user/languages/elm/): 200 | 201 | ```yml 202 | language: elm 203 | elm: 204 | - 0.19.1 205 | ``` 206 | 207 | Here is an example [`travis.yml`](https://github.com/rtfeldman/elm-css/blob/master/.travis.yml) configuration file for running tests in CI. 208 | -------------------------------------------------------------------------------- /bin/elm-test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../lib/elm-test.js"); 4 | -------------------------------------------------------------------------------- /elm-tooling.json: -------------------------------------------------------------------------------- 1 | { 2 | "tools": { 3 | "elm": "0.19.1", 4 | "elm-format": "0.8.5", 5 | "elm-json": "0.2.13" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /elm/.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff/ 2 | -------------------------------------------------------------------------------- /elm/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.5", 10 | "elm/json": "1.1.3", 11 | "elm/random": "1.0.0", 12 | "elm/time": "1.0.0", 13 | "elm-explorations/test": "2.0.0" 14 | }, 15 | "indirect": { 16 | "elm/bytes": "1.0.8", 17 | "elm/html": "1.0.0", 18 | "elm/virtual-dom": "1.0.3" 19 | } 20 | }, 21 | "test-dependencies": { 22 | "direct": {}, 23 | "indirect": {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /elm/review/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.5", 10 | "elm/json": "1.1.3", 11 | "elm/project-metadata-utils": "1.0.2", 12 | "jfmengels/elm-review": "2.15.1", 13 | "jfmengels/elm-review-unused": "1.2.4", 14 | "stil4m/elm-syntax": "7.3.8" 15 | }, 16 | "indirect": { 17 | "elm/bytes": "1.0.8", 18 | "elm/html": "1.0.0", 19 | "elm/parser": "1.1.0", 20 | "elm/random": "1.0.0", 21 | "elm/regex": "1.0.0", 22 | "elm/time": "1.0.0", 23 | "elm/virtual-dom": "1.0.3", 24 | "elm-explorations/test": "2.2.0", 25 | "rtfeldman/elm-hex": "1.0.0", 26 | "stil4m/structured-writer": "1.0.3" 27 | } 28 | }, 29 | "test-dependencies": { 30 | "direct": {}, 31 | "indirect": {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /elm/review/src/ReviewConfig.elm: -------------------------------------------------------------------------------- 1 | module ReviewConfig exposing (config) 2 | 3 | {-| Do not rename the ReviewConfig module or the config function, because 4 | `elm-review` will look for these. 5 | 6 | To add packages that contain rules, add them to this review project using 7 | 8 | `elm install author/packagename` 9 | 10 | when inside the directory containing this file. 11 | 12 | -} 13 | 14 | import NoUnused.CustomTypeConstructorArgs 15 | import NoUnused.CustomTypeConstructors 16 | import NoUnused.Dependencies 17 | import NoUnused.Exports 18 | import NoUnused.Modules 19 | import NoUnused.Parameters 20 | import NoUnused.Patterns 21 | import NoUnused.Variables 22 | import Review.Rule exposing (Rule) 23 | 24 | 25 | config : List Rule 26 | config = 27 | [ NoUnused.CustomTypeConstructors.rule [] 28 | |> Review.Rule.ignoreErrorsForFiles 29 | [ "src/Test/Reporter/Reporter.elm" --ConsoleReport, JsonReport, JunitReport are used externally 30 | , "src/Console/Text.elm" -- Monochrome, UseColor are used externally 31 | ] 32 | , NoUnused.Exports.rule 33 | |> Review.Rule.ignoreErrorsForFiles 34 | [ "x" --"src/Test/Runner/Node/Vendor/Diff.elm" 35 | , "src/Test/Runner/Node.elm" -- run, TestProgram are used externally 36 | ] 37 | , NoUnused.Modules.rule 38 | , NoUnused.CustomTypeConstructorArgs.rule 39 | |> Review.Rule.ignoreErrorsForFiles 40 | [ "src/Test/Runner/Node/Vendor/Diff.elm" -- UnexpectedPath is used for reporting errors 41 | , "src/Test/Runner/JsMessage.elm" -- Test is used for JSON decoding 42 | ] 43 | , NoUnused.Dependencies.rule 44 | , NoUnused.Parameters.rule 45 | , NoUnused.Patterns.rule 46 | , NoUnused.Variables.rule 47 | ] 48 | -------------------------------------------------------------------------------- /elm/src/Console/Text.elm: -------------------------------------------------------------------------------- 1 | module Console.Text exposing 2 | ( Color 3 | , ColorModifier 4 | , Style 5 | , Text 6 | , UseColor(..) 7 | , concat 8 | , dark 9 | , green 10 | , plain 11 | , red 12 | , render 13 | , underline 14 | , yellow 15 | ) 16 | 17 | import Test.Runner.Node.Vendor.Console as Console 18 | 19 | 20 | type Text 21 | = Text { background : Color, foreground : Color, style : Style, modifiers : List ColorModifier } String 22 | | Texts (List Text) 23 | 24 | 25 | type UseColor 26 | = UseColor 27 | | Monochrome 28 | 29 | 30 | type Color 31 | = Default 32 | | Red 33 | | Green 34 | | Yellow 35 | | Black 36 | | Blue 37 | | Magenta 38 | | Cyan 39 | | White 40 | 41 | 42 | type ColorModifier 43 | = Inverted 44 | | Dark 45 | 46 | 47 | type Style 48 | = Normal 49 | | Bold 50 | | Underline 51 | 52 | 53 | render : UseColor -> Text -> String 54 | render useColor txt = 55 | case txt of 56 | Text attrs str -> 57 | case useColor of 58 | UseColor -> 59 | str 60 | |> colorizeBackground attrs.background 61 | |> colorizeForeground attrs.foreground 62 | |> applyModifiers attrs.modifiers 63 | |> applyStyle attrs.style 64 | 65 | Monochrome -> 66 | str 67 | 68 | Texts texts -> 69 | List.map (render useColor) texts 70 | |> String.join "" 71 | 72 | 73 | concat : List Text -> Text 74 | concat = 75 | Texts 76 | 77 | 78 | plain : String -> Text 79 | plain = 80 | Text { foreground = Default, background = Default, style = Normal, modifiers = [] } 81 | 82 | 83 | 84 | -- FOREGROUND COLORS -- 85 | 86 | 87 | red : String -> Text 88 | red = 89 | Text { foreground = Red, background = Default, style = Normal, modifiers = [] } 90 | 91 | 92 | green : String -> Text 93 | green = 94 | Text { foreground = Green, background = Default, style = Normal, modifiers = [] } 95 | 96 | 97 | yellow : String -> Text 98 | yellow = 99 | Text { foreground = Yellow, background = Default, style = Normal, modifiers = [] } 100 | 101 | 102 | dark : Text -> Text 103 | dark txt = 104 | case txt of 105 | Text styles str -> 106 | Text { styles | modifiers = Dark :: styles.modifiers } str 107 | 108 | Texts texts -> 109 | Texts (List.map dark texts) 110 | 111 | 112 | 113 | -- STYLES -- 114 | 115 | 116 | underline : Text -> Text 117 | underline txt = 118 | case txt of 119 | Text styles str -> 120 | Text { styles | style = Underline } str 121 | 122 | Texts texts -> 123 | Texts (List.map dark texts) 124 | 125 | 126 | 127 | -- INTERNAL HELPERS -- 128 | 129 | 130 | colorizeForeground : Color -> String -> String 131 | colorizeForeground color str = 132 | case color of 133 | Default -> 134 | str 135 | 136 | Red -> 137 | Console.red str 138 | 139 | Green -> 140 | Console.green str 141 | 142 | Yellow -> 143 | Console.yellow str 144 | 145 | Black -> 146 | Console.black str 147 | 148 | Blue -> 149 | Console.blue str 150 | 151 | Magenta -> 152 | Console.magenta str 153 | 154 | Cyan -> 155 | Console.cyan str 156 | 157 | White -> 158 | Console.white str 159 | 160 | 161 | colorizeBackground : Color -> String -> String 162 | colorizeBackground color str = 163 | case color of 164 | Default -> 165 | str 166 | 167 | Red -> 168 | Console.bgRed str 169 | 170 | Green -> 171 | Console.bgGreen str 172 | 173 | Yellow -> 174 | Console.bgYellow str 175 | 176 | Black -> 177 | Console.bgBlack str 178 | 179 | Blue -> 180 | Console.bgBlue str 181 | 182 | Magenta -> 183 | Console.bgMagenta str 184 | 185 | Cyan -> 186 | Console.bgCyan str 187 | 188 | White -> 189 | Console.bgWhite str 190 | 191 | 192 | applyStyle : Style -> String -> String 193 | applyStyle style str = 194 | case style of 195 | Normal -> 196 | str 197 | 198 | Bold -> 199 | Console.bold str 200 | 201 | Underline -> 202 | Console.underline str 203 | 204 | 205 | applyModifiers : List ColorModifier -> String -> String 206 | applyModifiers modifiers str = 207 | List.foldl applyModifiersHelp str modifiers 208 | 209 | 210 | applyModifiersHelp : ColorModifier -> String -> String 211 | applyModifiersHelp modifier str = 212 | case modifier of 213 | Inverted -> 214 | Console.colorsInverted str 215 | 216 | Dark -> 217 | Console.dark str 218 | -------------------------------------------------------------------------------- /elm/src/Test/Reporter/Console.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.Console exposing (reportBegin, reportComplete, reportSummary) 2 | 3 | import Console.Text as Text exposing (..) 4 | import Json.Encode as Encode exposing (Value) 5 | import Test.Distribution exposing (DistributionReport) 6 | import Test.Reporter.Console.Format exposing (format) 7 | import Test.Reporter.Console.Format.Color as FormatColor 8 | import Test.Reporter.Console.Format.Monochrome as FormatMonochrome 9 | import Test.Reporter.TestResults as Results exposing (Failure, Outcome(..), SummaryInfo) 10 | import Test.Runner exposing (formatLabels) 11 | 12 | 13 | formatDuration : Float -> String 14 | formatDuration time = 15 | String.fromFloat time ++ " ms" 16 | 17 | 18 | indent : String -> String 19 | indent str = 20 | str 21 | |> String.split "\n" 22 | |> List.map ((++) " ") 23 | |> String.join "\n" 24 | 25 | 26 | pluralize : String -> String -> Int -> String 27 | pluralize singular plural count = 28 | let 29 | suffix = 30 | if count == 1 then 31 | singular 32 | 33 | else 34 | plural 35 | in 36 | String.join " " [ String.fromInt count, suffix ] 37 | 38 | 39 | passedToText : List String -> String -> Text 40 | passedToText labels distributionReport = 41 | Text.concat 42 | [ passedLabelsToText labels 43 | , dark <| plain <| "\n" ++ indent distributionReport ++ "\n\n" 44 | ] 45 | 46 | 47 | passedLabelsToText : List String -> Text 48 | passedLabelsToText = 49 | formatLabels (dark << plain << withChar '↓') (green << withChar '✓') >> Text.concat 50 | 51 | 52 | todosToText : ( List String, String ) -> Text 53 | todosToText ( labels, failure ) = 54 | Text.concat [ todoLabelsToText labels, todoToChalk failure ] 55 | 56 | 57 | todoLabelsToText : List String -> Text 58 | todoLabelsToText = 59 | formatLabels (dark << plain << withChar '↓') (dark << plain << withChar '↓') >> Text.concat 60 | 61 | 62 | todoToChalk : String -> Text 63 | todoToChalk message = 64 | plain ("◦ TODO: " ++ message ++ "\n\n") 65 | 66 | 67 | failuresToText : UseColor -> List String -> List ( Failure, DistributionReport ) -> Text 68 | failuresToText useColor labels failures = 69 | Text.concat (failureLabelsToText labels :: List.map (failureToText useColor) failures) 70 | 71 | 72 | failureLabelsToText : List String -> Text 73 | failureLabelsToText = 74 | formatLabels (dark << plain << withChar '↓') (red << withChar '✗') >> Text.concat 75 | 76 | 77 | failureToText : UseColor -> ( Failure, DistributionReport ) -> Text 78 | failureToText useColor ( { given, description, reason }, distributionReport ) = 79 | let 80 | formatEquality = 81 | case useColor of 82 | Monochrome -> 83 | FormatMonochrome.formatEquality 84 | 85 | UseColor -> 86 | FormatColor.formatEquality 87 | 88 | distributionText = 89 | distributionReportToString distributionReport 90 | |> Maybe.map (\str -> dark (plain ("\n" ++ indent str ++ "\n"))) 91 | 92 | givenText = 93 | given 94 | |> Maybe.map (\str -> dark (plain ("\nGiven " ++ str ++ "\n"))) 95 | 96 | messageText = 97 | plain <| "\n" ++ indent (format formatEquality description reason) ++ "\n\n" 98 | in 99 | [ distributionText 100 | , givenText 101 | , Just messageText 102 | ] 103 | |> List.filterMap identity 104 | |> Text.concat 105 | 106 | 107 | textToValue : UseColor -> Text -> Value 108 | textToValue useColor txt = 109 | txt 110 | |> Text.render useColor 111 | |> Encode.string 112 | 113 | 114 | reportBegin : UseColor -> { r | globs : List String, fuzzRuns : Int, testCount : Int, initialSeed : Int } -> Maybe Value 115 | reportBegin useColor { globs, fuzzRuns, testCount, initialSeed } = 116 | let 117 | prefix = 118 | "Running " 119 | ++ pluralize "test" "tests" testCount 120 | ++ ". To reproduce these results, run: elm-test --fuzz " 121 | ++ String.fromInt fuzzRuns 122 | ++ " --seed " 123 | ++ String.fromInt initialSeed 124 | in 125 | Encode.object 126 | [ ( "type", Encode.string "begin" ) 127 | , ( "output" 128 | , (String.join " " (prefix :: globs) ++ "\n") 129 | |> plain 130 | |> textToValue useColor 131 | ) 132 | ] 133 | |> Just 134 | 135 | 136 | getStatus : Outcome -> String 137 | getStatus outcome = 138 | case outcome of 139 | Failed _ -> 140 | "fail" 141 | 142 | Todo _ -> 143 | "todo" 144 | 145 | Passed _ -> 146 | "pass" 147 | 148 | 149 | reportComplete : UseColor -> Results.TestResult -> Value 150 | reportComplete useColor { labels, outcome } = 151 | Encode.object <| 152 | ( "type", Encode.string "complete" ) 153 | :: ( "status", Encode.string (getStatus outcome) ) 154 | :: (case outcome of 155 | Passed distributionReport -> 156 | -- No failures of any kind. 157 | case distributionReportToString distributionReport of 158 | Nothing -> 159 | [] 160 | 161 | Just report -> 162 | [ ( "distributionReport" 163 | , report 164 | |> passedToText labels 165 | |> textToValue useColor 166 | ) 167 | ] 168 | 169 | Failed failures -> 170 | [ ( "failure" 171 | , -- We have non-TODOs still failing; report them, not the TODOs. 172 | failures 173 | |> failuresToText useColor labels 174 | |> textToValue useColor 175 | ) 176 | ] 177 | 178 | Todo str -> 179 | [ ( "todo", Encode.string str ) 180 | , ( "labels", Encode.list Encode.string labels ) 181 | ] 182 | ) 183 | 184 | 185 | summarizeTodos : List ( List String, String ) -> Text 186 | summarizeTodos = 187 | List.map todosToText >> Text.concat 188 | 189 | 190 | reportSummary : UseColor -> SummaryInfo -> Maybe String -> Value 191 | reportSummary useColor { todos, passed, failed, duration } autoFail = 192 | let 193 | headlineResult = 194 | case ( autoFail, failed, List.length todos ) of 195 | ( Nothing, 0, 0 ) -> 196 | Ok "TEST RUN PASSED" 197 | 198 | ( Nothing, 0, 1 ) -> 199 | Err ( yellow, "TEST RUN INCOMPLETE", " because there is 1 TODO remaining" ) 200 | 201 | ( Nothing, 0, numTodos ) -> 202 | Err ( yellow, "TEST RUN INCOMPLETE", " because there are " ++ String.fromInt numTodos ++ " TODOs remaining" ) 203 | 204 | ( Just failure, 0, _ ) -> 205 | Err ( yellow, "TEST RUN INCOMPLETE", " because " ++ failure ) 206 | 207 | _ -> 208 | Err ( red, "TEST RUN FAILED", "" ) 209 | 210 | headline = 211 | case headlineResult of 212 | Ok str -> 213 | underline (green ("\n" ++ str ++ "\n\n")) 214 | 215 | Err ( colorize, str, suffix ) -> 216 | [ underline (colorize ("\n" ++ str)) 217 | , colorize (suffix ++ "\n\n") 218 | ] 219 | |> Text.concat 220 | 221 | todoStats = 222 | -- Print stats for Todos if there are any, 223 | --but don't print details unless only Todos remain 224 | case List.length todos of 225 | 0 -> 226 | plain "" 227 | 228 | numTodos -> 229 | stat "Todo: " (String.fromInt numTodos) 230 | 231 | individualTodos = 232 | if failed > 0 then 233 | plain "" 234 | 235 | else 236 | summarizeTodos (List.reverse todos) 237 | in 238 | Encode.object 239 | [ ( "type", Encode.string "summary" ) 240 | , ( "summary" 241 | , [ headline 242 | , stat "Duration: " (formatDuration duration) 243 | , stat "Passed: " (String.fromInt passed) 244 | , stat "Failed: " (String.fromInt failed) 245 | , todoStats 246 | , individualTodos 247 | ] 248 | |> Text.concat 249 | |> Text.render useColor 250 | |> Encode.string 251 | ) 252 | ] 253 | 254 | 255 | stat : String -> String -> Text 256 | stat label value = 257 | Text.concat 258 | [ dark (plain label) 259 | , plain (value ++ "\n") 260 | ] 261 | 262 | 263 | withChar : Char -> String -> String 264 | withChar icon str = 265 | String.fromChar icon ++ " " ++ str ++ "\n" 266 | 267 | 268 | distributionReportToString : DistributionReport -> Maybe String 269 | distributionReportToString distributionReport = 270 | case distributionReport of 271 | Test.Distribution.NoDistribution -> 272 | Nothing 273 | 274 | Test.Distribution.DistributionToReport r -> 275 | Just (Test.Distribution.distributionReportTable r) 276 | 277 | Test.Distribution.DistributionCheckSucceeded _ -> 278 | {- Not reporting the table although the data is technically there. 279 | We keep the full data dump for the JSON reporter. 280 | -} 281 | Nothing 282 | 283 | Test.Distribution.DistributionCheckFailed r -> 284 | Just (Test.Distribution.distributionReportTable r) 285 | -------------------------------------------------------------------------------- /elm/src/Test/Reporter/Console/Format.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.Console.Format exposing (format, highlightEqual) 2 | 3 | import Test.Reporter.Highlightable as Highlightable exposing (Highlightable) 4 | import Test.Runner.Failure exposing (InvalidReason(..), Reason(..)) 5 | 6 | 7 | format : 8 | (List (Highlightable String) -> List (Highlightable String) -> ( String, String )) 9 | -> String 10 | -> Reason 11 | -> String 12 | format formatEquality description reason = 13 | case reason of 14 | Custom -> 15 | description 16 | 17 | Equality expected actual -> 18 | case highlightEqual expected actual of 19 | Nothing -> 20 | verticalBar description expected actual 21 | 22 | Just ( highlightedExpected, highlightedActual ) -> 23 | let 24 | ( formattedExpected, formattedActual ) = 25 | formatEquality highlightedExpected highlightedActual 26 | in 27 | verticalBar description formattedExpected formattedActual 28 | 29 | Comparison first second -> 30 | verticalBar description first second 31 | 32 | TODO -> 33 | description 34 | 35 | Invalid BadDescription -> 36 | if description == "" then 37 | "The empty string is not a valid test description." 38 | 39 | else 40 | "This is an invalid test description: " ++ description 41 | 42 | Invalid _ -> 43 | description 44 | 45 | ListDiff expected actual -> 46 | listDiffToString 0 47 | description 48 | { expected = expected 49 | , actual = actual 50 | } 51 | { originalExpected = expected 52 | , originalActual = actual 53 | } 54 | 55 | CollectionDiff { expected, actual, extra, missing } -> 56 | let 57 | extraStr = 58 | if List.isEmpty extra then 59 | "" 60 | 61 | else 62 | "\nThese keys are extra: " 63 | ++ (extra |> String.join ", " |> (\d -> "[ " ++ d ++ " ]")) 64 | 65 | missingStr = 66 | if List.isEmpty missing then 67 | "" 68 | 69 | else 70 | "\nThese keys are missing: " 71 | ++ (missing |> String.join ", " |> (\d -> "[ " ++ d ++ " ]")) 72 | in 73 | String.join "" 74 | [ verticalBar description expected actual 75 | , "\n" 76 | , extraStr 77 | , missingStr 78 | ] 79 | 80 | 81 | highlightEqual : String -> String -> Maybe ( List (Highlightable String), List (Highlightable String) ) 82 | highlightEqual expected actual = 83 | if expected == "\"\"" || actual == "\"\"" then 84 | -- Diffing when one is the empty string looks silly. Don't bother. 85 | Nothing 86 | 87 | else if isFloat expected && isFloat actual then 88 | -- Diffing numbers looks silly. Don't bother. 89 | Nothing 90 | 91 | else 92 | let 93 | isHighlighted = 94 | Highlightable.resolve 95 | { fromHighlighted = always True 96 | , fromPlain = always False 97 | } 98 | 99 | edgeCount highlightedString = 100 | let 101 | highlights = 102 | List.map isHighlighted highlightedString 103 | in 104 | highlights 105 | |> List.map2 Tuple.pair (List.drop 1 highlights) 106 | |> List.filter (\( lhs, rhs ) -> lhs /= rhs) 107 | |> List.length 108 | 109 | expectedChars = 110 | String.toList expected 111 | 112 | actualChars = 113 | String.toList actual 114 | 115 | highlightedExpected = 116 | Highlightable.diffLists expectedChars actualChars 117 | |> List.map (Highlightable.map String.fromChar) 118 | 119 | highlightedActual = 120 | Highlightable.diffLists actualChars expectedChars 121 | |> List.map (Highlightable.map String.fromChar) 122 | 123 | plainCharCount = 124 | highlightedExpected 125 | |> List.filter (not << isHighlighted) 126 | |> List.length 127 | in 128 | if edgeCount highlightedActual > plainCharCount || edgeCount highlightedExpected > plainCharCount then 129 | -- Large number of small highlighted blocks. Diff is too messy to be useful. 130 | Nothing 131 | 132 | else 133 | Just 134 | ( highlightedExpected 135 | , highlightedActual 136 | ) 137 | 138 | 139 | isFloat : String -> Bool 140 | isFloat str = 141 | case String.toFloat str of 142 | Just _ -> 143 | True 144 | 145 | Nothing -> 146 | False 147 | 148 | 149 | listDiffToString : 150 | Int 151 | -> String 152 | -> { expected : List String, actual : List String } 153 | -> { originalExpected : List String, originalActual : List String } 154 | -> String 155 | listDiffToString index description { expected, actual } originals = 156 | case ( expected, actual ) of 157 | ( [], [] ) -> 158 | [ "Two lists were unequal previously, yet ended up equal later." 159 | , "This should never happen!" 160 | , "Please report this bug to https://github.com/elm-community/elm-test/issues - and include these lists: " 161 | , "\n" 162 | , String.join ", " originals.originalExpected 163 | , "\n" 164 | , String.join ", " originals.originalActual 165 | ] 166 | |> String.join "" 167 | 168 | ( _ :: _, [] ) -> 169 | verticalBar (description ++ " was shorter than") 170 | (String.join ", " originals.originalExpected) 171 | (String.join ", " originals.originalActual) 172 | 173 | ( [], _ :: _ ) -> 174 | verticalBar (description ++ " was longer than") 175 | (String.join ", " originals.originalExpected) 176 | (String.join ", " originals.originalActual) 177 | 178 | ( firstExpected :: restExpected, firstActual :: restActual ) -> 179 | if firstExpected == firstActual then 180 | -- They're still the same so far; keep going. 181 | listDiffToString (index + 1) 182 | description 183 | { expected = restExpected 184 | , actual = restActual 185 | } 186 | originals 187 | 188 | else 189 | -- We found elements that differ; fail! 190 | String.join "" 191 | [ verticalBar description 192 | (String.join ", " originals.originalExpected) 193 | (String.join ", " originals.originalActual) 194 | , "\n\nThe first diff is at index " 195 | , String.fromInt index 196 | , ": it was `" 197 | , firstActual 198 | , "`, but `" 199 | , firstExpected 200 | , "` was expected." 201 | ] 202 | 203 | 204 | verticalBar : String -> String -> String -> String 205 | verticalBar comparison expected actual = 206 | [ actual 207 | , "╷" 208 | , "│ " ++ comparison 209 | , "╵" 210 | , expected 211 | ] 212 | |> String.join "\n" 213 | -------------------------------------------------------------------------------- /elm/src/Test/Reporter/Console/Format/Color.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.Console.Format.Color exposing (formatEquality) 2 | 3 | import Test.Reporter.Highlightable as Highlightable exposing (Highlightable) 4 | import Test.Runner.Node.Vendor.Console as Console 5 | 6 | 7 | formatEquality : List (Highlightable String) -> List (Highlightable String) -> ( String, String ) 8 | formatEquality highlightedExpected highlightedActual = 9 | let 10 | formattedExpected = 11 | highlightedExpected 12 | |> List.map fromHighlightable 13 | |> String.join "" 14 | 15 | formattedActual = 16 | highlightedActual 17 | |> List.map fromHighlightable 18 | |> String.join "" 19 | in 20 | ( formattedExpected, formattedActual ) 21 | 22 | 23 | fromHighlightable : Highlightable String -> String 24 | fromHighlightable = 25 | Highlightable.resolve 26 | -- Cyan seems to look readable with both white and black text on top, 27 | -- so it should work with both dark and light console themes 28 | { fromHighlighted = Console.colorsInverted 29 | , fromPlain = identity 30 | } 31 | -------------------------------------------------------------------------------- /elm/src/Test/Reporter/Console/Format/Monochrome.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.Console.Format.Monochrome exposing (formatEquality) 2 | 3 | import Test.Reporter.Highlightable as Highlightable exposing (Highlightable) 4 | 5 | 6 | formatEquality : List (Highlightable String) -> List (Highlightable String) -> ( String, String ) 7 | formatEquality highlightedExpected highlightedActual = 8 | let 9 | ( formattedExpected, expectedIndicators ) = 10 | highlightedExpected 11 | |> List.map (fromHighlightable "▲") 12 | |> List.unzip 13 | 14 | ( formattedActual, actualIndicators ) = 15 | highlightedActual 16 | |> List.map (fromHighlightable "▼") 17 | |> List.unzip 18 | 19 | combinedExpected = 20 | String.join "\n" 21 | [ String.join "" formattedExpected 22 | , String.join "" expectedIndicators 23 | ] 24 | 25 | combinedActual = 26 | String.join "\n" 27 | [ String.join "" actualIndicators 28 | , String.join "" formattedActual 29 | ] 30 | in 31 | ( combinedExpected, combinedActual ) 32 | 33 | 34 | fromHighlightable : String -> Highlightable String -> ( String, String ) 35 | fromHighlightable indicator = 36 | Highlightable.resolve 37 | { fromHighlighted = \char -> ( char, indicator ) 38 | , fromPlain = \char -> ( char, " " ) 39 | } 40 | -------------------------------------------------------------------------------- /elm/src/Test/Reporter/Highlightable.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.Highlightable exposing (Highlightable, diffLists, map, resolve) 2 | 3 | import Test.Runner.Node.Vendor.Diff as Diff exposing (Change(..)) 4 | 5 | 6 | type Highlightable a 7 | = Highlighted a 8 | | Plain a 9 | 10 | 11 | resolve : { fromHighlighted : a -> b, fromPlain : a -> b } -> Highlightable a -> b 12 | resolve { fromHighlighted, fromPlain } highlightable = 13 | case highlightable of 14 | Highlighted val -> 15 | fromHighlighted val 16 | 17 | Plain val -> 18 | fromPlain val 19 | 20 | 21 | diffLists : List a -> List a -> List (Highlightable a) 22 | diffLists expected actual = 23 | -- TODO make sure this looks reasonable for multiline strings 24 | Diff.diff expected actual 25 | |> List.concatMap fromDiff 26 | 27 | 28 | map : (a -> b) -> Highlightable a -> Highlightable b 29 | map transform highlightable = 30 | case highlightable of 31 | Highlighted val -> 32 | Highlighted (transform val) 33 | 34 | Plain val -> 35 | Plain (transform val) 36 | 37 | 38 | fromDiff : Change a -> List (Highlightable a) 39 | fromDiff diff = 40 | case diff of 41 | Added _ -> 42 | [] 43 | 44 | Removed char -> 45 | [ Highlighted char ] 46 | 47 | NoChange char -> 48 | [ Plain char ] 49 | -------------------------------------------------------------------------------- /elm/src/Test/Reporter/JUnit.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.JUnit exposing (reportBegin, reportComplete, reportSummary) 2 | 3 | import Json.Encode as Encode exposing (Value) 4 | import Test.Distribution exposing (DistributionReport) 5 | import Test.Reporter.TestResults exposing (Failure, Outcome(..), SummaryInfo, TestResult) 6 | import Test.Runner.Failure exposing (InvalidReason(..), Reason(..)) 7 | 8 | 9 | reportBegin : runInfo -> Maybe Value 10 | reportBegin _ = 11 | Nothing 12 | 13 | 14 | encodeDistributionReport : String -> ( String, Value ) 15 | encodeDistributionReport reportText = 16 | ( "system-out", Encode.string reportText ) 17 | 18 | 19 | distributionReportToString : DistributionReport -> Maybe String 20 | distributionReportToString distributionReport = 21 | case distributionReport of 22 | Test.Distribution.NoDistribution -> 23 | Nothing 24 | 25 | Test.Distribution.DistributionToReport r -> 26 | Just (Test.Distribution.distributionReportTable r) 27 | 28 | Test.Distribution.DistributionCheckSucceeded _ -> 29 | {- Not reporting the table to the JUnit stdout (similarly to the 30 | Console reporter) although the data is technically there. 31 | We keep the full data dump for the JSON reporter. 32 | -} 33 | Nothing 34 | 35 | Test.Distribution.DistributionCheckFailed r -> 36 | Just (Test.Distribution.distributionReportTable r) 37 | 38 | 39 | encodeOutcome : Outcome -> List ( String, Value ) 40 | encodeOutcome outcome = 41 | case outcome of 42 | Passed distributionReport -> 43 | distributionReport 44 | |> distributionReportToString 45 | |> Maybe.map (encodeDistributionReport >> List.singleton) 46 | |> Maybe.withDefault [] 47 | 48 | Failed failures -> 49 | let 50 | message = 51 | failures 52 | |> List.map (Tuple.first >> formatFailure) 53 | |> String.join "\n\n\n" 54 | 55 | distributionReports : String 56 | distributionReports = 57 | failures 58 | |> List.filterMap (Tuple.second >> distributionReportToString) 59 | |> String.join "\n\n\n" 60 | 61 | nonemptyDistributionReports : Maybe String 62 | nonemptyDistributionReports = 63 | if String.isEmpty distributionReports then 64 | Nothing 65 | 66 | else 67 | Just distributionReports 68 | in 69 | List.filterMap identity 70 | [ Just (encodeFailureTuple message) 71 | , Maybe.map encodeDistributionReport nonemptyDistributionReports 72 | ] 73 | 74 | Todo message -> 75 | [ encodeFailureTuple ("TODO: " ++ message) ] 76 | 77 | 78 | encodeFailureTuple : String -> ( String, Value ) 79 | encodeFailureTuple message = 80 | ( "failure", Encode.string message ) 81 | 82 | 83 | formatFailure : Failure -> String 84 | formatFailure { given, description, reason } = 85 | let 86 | message = 87 | reasonToString description reason 88 | in 89 | case given of 90 | Just str -> 91 | "Given " ++ str ++ "\n\n" ++ message 92 | 93 | Nothing -> 94 | message 95 | 96 | 97 | formatClassAndName : List String -> ( String, String ) 98 | formatClassAndName labels = 99 | case labels of 100 | head :: rest -> 101 | ( String.join " " (List.reverse rest), head ) 102 | 103 | _ -> 104 | ( "", "" ) 105 | 106 | 107 | encodeDuration : Int -> Value 108 | encodeDuration time = 109 | (toFloat time / 1000) 110 | |> String.fromFloat 111 | |> Encode.string 112 | 113 | 114 | reportComplete : TestResult -> Value 115 | reportComplete { labels, duration, outcome } = 116 | let 117 | ( classname, name ) = 118 | formatClassAndName labels 119 | in 120 | Encode.object 121 | ([ ( "@classname", Encode.string classname ) 122 | , ( "@name", Encode.string name ) 123 | , ( "@time", encodeDuration duration ) 124 | ] 125 | ++ encodeOutcome outcome 126 | ) 127 | 128 | 129 | encodeExtraFailure : String -> Value 130 | encodeExtraFailure _ = 131 | reportComplete { labels = [], duration = 0, outcome = Failed [] } 132 | 133 | 134 | reportSummary : SummaryInfo -> Maybe String -> Value 135 | reportSummary { testCount, duration, failed } autoFail = 136 | let 137 | -- JUnit doesn't have a notion of "everything passed, but you left 138 | -- a Test.only in there, so it's a failure overall." In that case 139 | -- we'll tack on an extra failed test, so the overall suite fails. 140 | -- Another option would be to report it as an Error, but that would 141 | -- make JUnit have different semantics from the other reporters. 142 | -- Also, there wasn't really an error. Nothing broke. 143 | extraFailures = 144 | case ( failed, autoFail ) of 145 | ( 0, Just failure ) -> 146 | [ encodeExtraFailure failure ] 147 | 148 | _ -> 149 | [] 150 | in 151 | Encode.object 152 | [ ( "testsuite" 153 | , Encode.object 154 | [ ( "@name", Encode.string "elm-test" ) 155 | , ( "@package", Encode.string "elm-test" ) 156 | 157 | -- Would be nice to have this provided from elm-package.json of tests 158 | , ( "@tests", Encode.int testCount ) 159 | , ( "@failures", Encode.int failed ) 160 | , ( "@errors", Encode.int 0 ) 161 | , ( "@time", Encode.float duration ) 162 | , ( "testcase", Encode.list identity extraFailures ) 163 | ] 164 | ) 165 | ] 166 | 167 | 168 | reasonToString : String -> Reason -> String 169 | reasonToString description reason = 170 | case reason of 171 | Custom -> 172 | description 173 | 174 | Equality expected actual -> 175 | expected ++ "\n\nwas not equal to\n\n" ++ actual 176 | 177 | Comparison first second -> 178 | first ++ "\n\nfailed when compared with " ++ description ++ " on\n\n" ++ second 179 | 180 | TODO -> 181 | "TODO: " ++ description 182 | 183 | Invalid BadDescription -> 184 | let 185 | explanation = 186 | if description == "" then 187 | "The empty string is not a valid test description." 188 | 189 | else 190 | "This is an invalid test description: " ++ description 191 | in 192 | "Invalid test: " ++ explanation 193 | 194 | Invalid _ -> 195 | "Invalid test: " ++ description 196 | 197 | ListDiff expected actual -> 198 | String.join ", " expected ++ "\n\nhad different elements than\n\n" ++ String.join ", " actual 199 | 200 | CollectionDiff { expected, actual, extra, missing } -> 201 | expected 202 | ++ "\n\nhad different contents than\n\n" 203 | ++ actual 204 | ++ "\n\nthese were extra:\n\n" 205 | ++ String.join "\n" extra 206 | ++ "\n\nthese were missing:\n\n" 207 | ++ String.join "\n" missing 208 | -------------------------------------------------------------------------------- /elm/src/Test/Reporter/Json.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.Json exposing (reportBegin, reportComplete, reportSummary) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Encode as Encode exposing (Value) 5 | import Test.Distribution exposing (DistributionReport) 6 | import Test.Reporter.TestResults as TestResults exposing (Failure, Outcome(..), SummaryInfo) 7 | import Test.Runner.Failure exposing (InvalidReason(..), Reason(..)) 8 | 9 | 10 | reportBegin : { globs : List String, paths : List String, fuzzRuns : Int, testCount : Int, initialSeed : Int } -> Maybe Value 11 | reportBegin { globs, paths, fuzzRuns, testCount, initialSeed } = 12 | Encode.object 13 | [ ( "event", Encode.string "runStart" ) 14 | , ( "testCount", Encode.string <| String.fromInt testCount ) 15 | , ( "fuzzRuns", Encode.string <| String.fromInt fuzzRuns ) 16 | , ( "globs", Encode.list Encode.string globs ) 17 | , ( "paths", Encode.list Encode.string paths ) 18 | , ( "initialSeed", Encode.string <| String.fromInt initialSeed ) 19 | ] 20 | |> Just 21 | 22 | 23 | reportComplete : TestResults.TestResult -> Value 24 | reportComplete { duration, labels, outcome } = 25 | Encode.object 26 | [ ( "event", Encode.string "testCompleted" ) 27 | , ( "status", Encode.string (getStatus outcome) ) 28 | , ( "labels", encodeLabels labels ) 29 | , ( "failures", Encode.list identity (encodeFailures outcome) ) 30 | , ( "distributionReports", Encode.list identity (encodeDistributionReports outcome) ) 31 | , ( "duration", Encode.string <| String.fromInt duration ) 32 | ] 33 | 34 | 35 | encodeFailures : Outcome -> List Value 36 | encodeFailures outcome = 37 | case outcome of 38 | Failed failures -> 39 | List.map (Tuple.first >> encodeFailure) failures 40 | 41 | Todo str -> 42 | [ Encode.string str ] 43 | 44 | Passed _ -> 45 | [] 46 | 47 | 48 | encodeDistributionReports : Outcome -> List Value 49 | encodeDistributionReports outcome = 50 | case outcome of 51 | Failed failures -> 52 | List.map (Tuple.second >> encodeDistributionReport) failures 53 | 54 | Todo _ -> 55 | [] 56 | 57 | Passed distributionReport -> 58 | [ encodeDistributionReport distributionReport ] 59 | 60 | 61 | encodeDistributionReport : DistributionReport -> Value 62 | encodeDistributionReport distributionReport = 63 | case distributionReport of 64 | Test.Distribution.NoDistribution -> 65 | Encode.null 66 | |> encodeSumType "NoDistribution" 67 | 68 | Test.Distribution.DistributionToReport r -> 69 | [ ( "distributionCount", encodeDistributionCount r.distributionCount ) 70 | , ( "runsElapsed", Encode.int r.runsElapsed ) 71 | ] 72 | |> Encode.object 73 | |> encodeSumType "DistributionToReport" 74 | 75 | Test.Distribution.DistributionCheckSucceeded r -> 76 | [ ( "distributionCount", encodeDistributionCount r.distributionCount ) 77 | , ( "runsElapsed", Encode.int r.runsElapsed ) 78 | ] 79 | |> Encode.object 80 | |> encodeSumType "DistributionCheckSucceeded" 81 | 82 | Test.Distribution.DistributionCheckFailed r -> 83 | [ ( "distributionCount", encodeDistributionCount r.distributionCount ) 84 | , ( "runsElapsed", Encode.int r.runsElapsed ) 85 | , ( "badLabel", Encode.string r.badLabel ) 86 | , ( "badLabelPercentage", Encode.float r.badLabelPercentage ) 87 | , ( "expectedDistribution", Encode.string r.expectedDistribution ) 88 | ] 89 | |> Encode.object 90 | |> encodeSumType "DistributionCheckFailed" 91 | 92 | 93 | encodeDistributionCount : Dict (List String) Int -> Value 94 | encodeDistributionCount dict = 95 | dict 96 | |> Dict.toList 97 | |> Encode.list 98 | (\( labels, count ) -> 99 | Encode.object 100 | [ ( "labels", Encode.list Encode.string labels ) 101 | , ( "count", Encode.int count ) 102 | ] 103 | ) 104 | 105 | 106 | {-| Algorithm: 107 | 108 | - If any fail, return "fail" 109 | - Otherwise, if any are todo, return "todo" 110 | - Otherwise, return "pass" 111 | 112 | -} 113 | getStatus : Outcome -> String 114 | getStatus outcome = 115 | case outcome of 116 | Failed _ -> 117 | "fail" 118 | 119 | Todo _ -> 120 | "todo" 121 | 122 | Passed _ -> 123 | "pass" 124 | 125 | 126 | encodeLabels : List String -> Value 127 | encodeLabels labels = 128 | List.reverse labels 129 | |> Encode.list Encode.string 130 | 131 | 132 | reportSummary : SummaryInfo -> Maybe String -> Value 133 | reportSummary { duration, passed, failed } autoFail = 134 | Encode.object 135 | [ ( "event", Encode.string "runComplete" ) 136 | , ( "passed", Encode.string <| String.fromInt passed ) 137 | , ( "failed", Encode.string <| String.fromInt failed ) 138 | , ( "duration", Encode.string <| String.fromFloat duration ) 139 | , ( "autoFail" 140 | , autoFail 141 | |> Maybe.map Encode.string 142 | |> Maybe.withDefault Encode.null 143 | ) 144 | ] 145 | 146 | 147 | encodeFailure : Failure -> Value 148 | encodeFailure { given, description, reason } = 149 | Encode.object 150 | [ ( "given", Maybe.withDefault Encode.null (Maybe.map Encode.string given) ) 151 | , ( "message", Encode.string description ) 152 | , ( "reason", encodeReason description reason ) 153 | ] 154 | 155 | 156 | encodeSumType : String -> Value -> Value 157 | encodeSumType sumType data = 158 | Encode.object 159 | [ ( "type", Encode.string sumType ) 160 | , ( "data", data ) 161 | ] 162 | 163 | 164 | encodeReason : String -> Reason -> Value 165 | encodeReason description reason = 166 | case reason of 167 | Custom -> 168 | Encode.string description 169 | |> encodeSumType "Custom" 170 | 171 | Equality expected actual -> 172 | [ ( "expected", Encode.string expected ) 173 | , ( "actual", Encode.string actual ) 174 | , ( "comparison", Encode.string description ) 175 | ] 176 | |> Encode.object 177 | |> encodeSumType "Equality" 178 | 179 | Comparison first second -> 180 | [ ( "first", Encode.string first ) 181 | , ( "second", Encode.string second ) 182 | , ( "comparison", Encode.string description ) 183 | ] 184 | |> Encode.object 185 | |> encodeSumType "Comparison" 186 | 187 | TODO -> 188 | Encode.string description 189 | |> encodeSumType "TODO" 190 | 191 | Invalid BadDescription -> 192 | let 193 | explanation = 194 | if description == "" then 195 | "The empty string is not a valid test description." 196 | 197 | else 198 | "This is an invalid test description: " ++ description 199 | in 200 | Encode.string explanation 201 | |> encodeSumType "Invalid" 202 | 203 | Invalid _ -> 204 | Encode.string description 205 | |> encodeSumType "Invalid" 206 | 207 | ListDiff expected actual -> 208 | [ ( "expected", Encode.list Encode.string expected ) 209 | , ( "actual", Encode.list Encode.string actual ) 210 | ] 211 | |> Encode.object 212 | |> encodeSumType "ListDiff" 213 | 214 | CollectionDiff { expected, actual, extra, missing } -> 215 | [ ( "expected", Encode.string expected ) 216 | , ( "actual", Encode.string actual ) 217 | , ( "extra", Encode.list Encode.string extra ) 218 | , ( "missing", Encode.list Encode.string missing ) 219 | ] 220 | |> Encode.object 221 | |> encodeSumType "CollectionDiff" 222 | -------------------------------------------------------------------------------- /elm/src/Test/Reporter/Reporter.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.Reporter exposing (Report(..), RunInfo, TestReporter, createReporter) 2 | 3 | import Console.Text exposing (UseColor) 4 | import Json.Encode exposing (Value) 5 | import Test.Reporter.Console as ConsoleReporter 6 | import Test.Reporter.JUnit as JUnitReporter 7 | import Test.Reporter.Json as JsonReporter 8 | import Test.Reporter.TestResults exposing (SummaryInfo, TestResult) 9 | 10 | 11 | type Report 12 | = ConsoleReport UseColor 13 | | JsonReport 14 | | JUnitReport 15 | 16 | 17 | type alias TestReporter = 18 | { format : String 19 | , reportBegin : RunInfo -> Maybe Value 20 | , reportComplete : TestResult -> Value 21 | , reportSummary : SummaryInfo -> Maybe String -> Value 22 | } 23 | 24 | 25 | type alias RunInfo = 26 | { globs : List String 27 | , paths : List String 28 | , fuzzRuns : Int 29 | , testCount : Int 30 | , initialSeed : Int 31 | } 32 | 33 | 34 | createReporter : Report -> TestReporter 35 | createReporter report = 36 | case report of 37 | JsonReport -> 38 | TestReporter "JSON" 39 | JsonReporter.reportBegin 40 | JsonReporter.reportComplete 41 | JsonReporter.reportSummary 42 | 43 | ConsoleReport useColor -> 44 | TestReporter "CHALK" 45 | (ConsoleReporter.reportBegin useColor) 46 | (ConsoleReporter.reportComplete useColor) 47 | (ConsoleReporter.reportSummary useColor) 48 | 49 | JUnitReport -> 50 | TestReporter "JUNIT" 51 | JUnitReporter.reportBegin 52 | JUnitReporter.reportComplete 53 | JUnitReporter.reportSummary 54 | -------------------------------------------------------------------------------- /elm/src/Test/Reporter/TestResults.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.TestResults exposing 2 | ( Failure 3 | , Outcome(..) 4 | , SummaryInfo 5 | , TestResult 6 | , isFailure 7 | , outcomesFromExpectations 8 | ) 9 | 10 | import Expect exposing (Expectation) 11 | import Test.Distribution exposing (DistributionReport) 12 | import Test.Runner 13 | import Test.Runner.Failure exposing (Reason) 14 | 15 | 16 | type Outcome 17 | = Passed DistributionReport 18 | | Todo String 19 | | Failed (List ( Failure, DistributionReport )) 20 | 21 | 22 | type alias TestResult = 23 | { labels : List String 24 | , outcome : Outcome 25 | , duration : Int -- in milliseconds 26 | } 27 | 28 | 29 | type alias SummaryInfo = 30 | { testCount : Int 31 | , passed : Int 32 | , failed : Int 33 | , todos : List ( List String, String ) 34 | , duration : Float 35 | } 36 | 37 | 38 | type alias Failure = 39 | { given : Maybe String 40 | , description : String 41 | , reason : Reason 42 | } 43 | 44 | 45 | isFailure : Outcome -> Bool 46 | isFailure outcome = 47 | case outcome of 48 | Failed _ -> 49 | True 50 | 51 | _ -> 52 | False 53 | 54 | 55 | outcomesFromExpectations : List Expectation -> List Outcome 56 | outcomesFromExpectations expectations = 57 | case expectations of 58 | expectation :: [] -> 59 | -- Most often we'll get exactly 1 pass, so try that case first! 60 | case Test.Runner.getFailureReason expectation of 61 | Nothing -> 62 | [ Passed (Test.Runner.getDistributionReport expectation) ] 63 | 64 | Just failure -> 65 | if Test.Runner.isTodo expectation then 66 | [ Todo failure.description ] 67 | 68 | else 69 | [ Failed 70 | [ ( failure, Test.Runner.getDistributionReport expectation ) ] 71 | ] 72 | 73 | _ :: _ -> 74 | let 75 | builder = 76 | List.foldl outcomesFromExpectationsHelp 77 | { passes = [], todos = [], failures = [] } 78 | expectations 79 | 80 | failuresList = 81 | case builder.failures of 82 | [] -> 83 | [] 84 | 85 | failures -> 86 | [ Failed failures ] 87 | in 88 | List.concat 89 | [ List.map Passed builder.passes 90 | , List.map Todo builder.todos 91 | , failuresList 92 | ] 93 | 94 | [] -> 95 | [] 96 | 97 | 98 | type alias OutcomeBuilder = 99 | { passes : List DistributionReport 100 | , todos : List String 101 | , failures : List ( Failure, DistributionReport ) 102 | } 103 | 104 | 105 | outcomesFromExpectationsHelp : Expectation -> OutcomeBuilder -> OutcomeBuilder 106 | outcomesFromExpectationsHelp expectation builder = 107 | case Test.Runner.getFailureReason expectation of 108 | Just failure -> 109 | if Test.Runner.isTodo expectation then 110 | { builder | todos = failure.description :: builder.todos } 111 | 112 | else 113 | { builder 114 | | failures = 115 | ( failure 116 | , Test.Runner.getDistributionReport expectation 117 | ) 118 | :: builder.failures 119 | } 120 | 121 | Nothing -> 122 | { builder 123 | | passes = 124 | Test.Runner.getDistributionReport expectation 125 | :: builder.passes 126 | } 127 | -------------------------------------------------------------------------------- /elm/src/Test/Runner/JsMessage.elm: -------------------------------------------------------------------------------- 1 | module Test.Runner.JsMessage exposing (JsMessage(..), decoder) 2 | 3 | import Json.Decode as Decode exposing (Decoder) 4 | 5 | 6 | type JsMessage 7 | = Test Int 8 | | Summary Float Int (List ( List String, String )) 9 | 10 | 11 | decoder : Decoder JsMessage 12 | decoder = 13 | Decode.field "type" Decode.string 14 | |> Decode.andThen decodeMessageFromType 15 | 16 | 17 | decodeMessageFromType : String -> Decoder JsMessage 18 | decodeMessageFromType messageType = 19 | case messageType of 20 | "TEST" -> 21 | Decode.field "index" Decode.int 22 | |> Decode.map Test 23 | 24 | "SUMMARY" -> 25 | Decode.map3 Summary 26 | (Decode.field "duration" Decode.float) 27 | (Decode.field "failures" Decode.int) 28 | (Decode.field "todos" (Decode.list todoDecoder)) 29 | 30 | _ -> 31 | Decode.fail ("Unrecognized message type: " ++ messageType) 32 | 33 | 34 | todoDecoder : Decoder ( List String, String ) 35 | todoDecoder = 36 | Decode.map2 (\a b -> ( a, b )) 37 | (Decode.field "labels" (Decode.list Decode.string)) 38 | (Decode.field "todo" Decode.string) 39 | -------------------------------------------------------------------------------- /elm/src/Test/Runner/Node/Vendor/Console.elm: -------------------------------------------------------------------------------- 1 | module Test.Runner.Node.Vendor.Console exposing (bgBlack, bgBlue, bgCyan, bgGreen, bgMagenta, bgRed, bgWhite, bgYellow, black, blue, bold, colorsInverted, cyan, dark, green, magenta, red, underline, white, yellow) 2 | 3 | {-| -} 4 | 5 | -- NOTE: This is copy/pasted from https://github.com/rtfeldman/console-print 6 | -- It's inlined to avoid having to call elm-package install on the end user's 7 | -- system - the approach this library took prior to 8 | -- commit 19047f01d460739bfe7f16466bc60b41430a8f09 - because it assumes 9 | -- the end user has the correct elm-package on their PATH, which is not a 10 | -- safe assumption. 11 | -- 12 | -- License: 13 | {- 14 | BSD 3-Clause License 15 | 16 | Copyright (c) 2017, Richard Feldman 17 | All rights reserved. 18 | 19 | Redistribution and use in source and binary forms, with or without 20 | modification, are permitted provided that the following conditions are met: 21 | 22 | * Redistributions of source code must retain the above copyright notice, this 23 | list of conditions and the following disclaimer. 24 | 25 | * Redistributions in binary form must reproduce the above copyright notice, 26 | this list of conditions and the following disclaimer in the documentation 27 | and/or other materials provided with the distribution. 28 | 29 | * Neither the name of the copyright holder nor the names of its 30 | contributors may be used to endorse or promote products derived from 31 | this software without specific prior written permission. 32 | 33 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 34 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 35 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 36 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 37 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 38 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 39 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 40 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 41 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 42 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 43 | -} 44 | 45 | 46 | {-| Make the text darker. 47 | 48 | This can be used with other text modifiers, such as color. 49 | 50 | import Console exposing (dark, green) 51 | 52 | 53 | -- "Hello, dark green world!" with "dark green" in dark green 54 | greeting : String 55 | greeting = 56 | "Hello, " ++ (dark << green) "dark green" ++ " world!" 57 | 58 | Not all terminals support this. 59 | 60 | -} 61 | dark : String -> String 62 | dark str = 63 | String.join "" [ "\u{001B}[2m", str, "\u{001B}[22m" ] 64 | 65 | 66 | {-| Make the text bold. 67 | 68 | This can be used with other text modifiers, such as color. 69 | 70 | import Console exposing (blue, bold) 71 | 72 | 73 | -- "Hello, bold blue world!" with "bold blue" in bold and blue 74 | greeting : String 75 | greeting = 76 | "Hello, " ++ (bold << blue) "bold blue" ++ " world!" 77 | 78 | Some terminals implement this as a color change rather than a boldness change. 79 | 80 | -} 81 | bold : String -> String 82 | bold str = 83 | String.join "" [ "\u{001B}[1m", str, "\u{001B}[22m" ] 84 | 85 | 86 | {-| Make the text underlined. 87 | 88 | This can be used with other text modifiers, such as color. 89 | 90 | import Console exposing (underline) 91 | 92 | 93 | -- "This will look like a hyperlink" with "hyperlink" underlined 94 | example : String 95 | example = 96 | "This will look like a " ++ underline "hyperlink" 97 | 98 | Not all terminals support this. 99 | 100 | -} 101 | underline : String -> String 102 | underline str = 103 | String.join "" [ "\u{001B}[4m", str, "\u{001B}[24m" ] 104 | 105 | 106 | {-| Invert the foreground and background colors from what they would otherwise be. 107 | -} 108 | colorsInverted : String -> String 109 | colorsInverted str = 110 | String.join "" [ "\u{001B}[7m", str, "\u{001B}[27m" ] 111 | 112 | 113 | 114 | -- Foreground Colors 115 | 116 | 117 | {-| Make the foreground text black. 118 | -} 119 | black : String -> String 120 | black str = 121 | String.join "" [ "\u{001B}[30m", str, "\u{001B}[39m" ] 122 | 123 | 124 | {-| Make the foreground text red. 125 | -} 126 | red : String -> String 127 | red str = 128 | String.join "" [ "\u{001B}[31m", str, "\u{001B}[39m" ] 129 | 130 | 131 | {-| Make the foreground text green. 132 | -} 133 | green : String -> String 134 | green str = 135 | String.join "" [ "\u{001B}[32m", str, "\u{001B}[39m" ] 136 | 137 | 138 | {-| Make the foreground text yellow. 139 | -} 140 | yellow : String -> String 141 | yellow str = 142 | String.join "" [ "\u{001B}[33m", str, "\u{001B}[39m" ] 143 | 144 | 145 | {-| Make the foreground text blue. 146 | -} 147 | blue : String -> String 148 | blue str = 149 | String.join "" [ "\u{001B}[34m", str, "\u{001B}[39m" ] 150 | 151 | 152 | {-| Make the foreground text magenta. 153 | -} 154 | magenta : String -> String 155 | magenta str = 156 | String.join "" [ "\u{001B}[35m", str, "\u{001B}[39m" ] 157 | 158 | 159 | {-| Make the foreground text cyan. 160 | -} 161 | cyan : String -> String 162 | cyan str = 163 | String.join "" [ "\u{001B}[36m", str, "\u{001B}[39m" ] 164 | 165 | 166 | {-| Make the foreground text white. 167 | -} 168 | white : String -> String 169 | white str = 170 | String.join "" [ "\u{001B}[37m", str, "\u{001B}[39m" ] 171 | 172 | 173 | 174 | -- Background Colors 175 | 176 | 177 | {-| Make the background black. 178 | -} 179 | bgBlack : String -> String 180 | bgBlack str = 181 | String.join "" [ "\u{001B}[40m", str, "\u{001B}[49m" ] 182 | 183 | 184 | {-| Make the background red. 185 | -} 186 | bgRed : String -> String 187 | bgRed str = 188 | String.join "" [ "\u{001B}[41m", str, "\u{001B}[49m" ] 189 | 190 | 191 | {-| Make the background green. 192 | -} 193 | bgGreen : String -> String 194 | bgGreen str = 195 | String.join "" [ "\u{001B}[42m", str, "\u{001B}[49m" ] 196 | 197 | 198 | {-| Make the background yellow. 199 | -} 200 | bgYellow : String -> String 201 | bgYellow str = 202 | String.join "" [ "\u{001B}[43m", str, "\u{001B}[49m" ] 203 | 204 | 205 | {-| Make the background blue. 206 | -} 207 | bgBlue : String -> String 208 | bgBlue str = 209 | String.join "" [ "\u{001B}[44m", str, "\u{001B}[49m" ] 210 | 211 | 212 | {-| Make the background magenta. 213 | -} 214 | bgMagenta : String -> String 215 | bgMagenta str = 216 | String.join "" [ "\u{001B}[45m", str, "\u{001B}[49m" ] 217 | 218 | 219 | {-| Make the background cyan. 220 | -} 221 | bgCyan : String -> String 222 | bgCyan str = 223 | String.join "" [ "\u{001B}[46m", str, "\u{001B}[49m" ] 224 | 225 | 226 | {-| Make the background white. 227 | -} 228 | bgWhite : String -> String 229 | bgWhite str = 230 | String.join "" [ "\u{001B}[47m", str, "\u{001B}[49m" ] 231 | -------------------------------------------------------------------------------- /elm/src/Test/Runner/Node/Vendor/Diff.elm: -------------------------------------------------------------------------------- 1 | module Test.Runner.Node.Vendor.Diff exposing 2 | ( Change(..) 3 | , diff 4 | ) 5 | 6 | {-| Compares two list and returns how they have changed. 7 | Each function internally uses Wu's [O(NP) algorithm](http://myerslab.mpi-cbg.de/wp-content/uploads/2014/06/np_diff.pdf). 8 | 9 | 10 | # Types 11 | 12 | @docs Change 13 | 14 | 15 | # Diffing 16 | 17 | @docs diff, diffLines 18 | 19 | -} 20 | 21 | -- NOTE: This is copy/pasted from https://github.com/jinjor/elm-diff 22 | -- It's inlined to avoid having to call elm-package install on the end user's 23 | -- system - the approach this library took prior to 24 | -- commit 19047f01d460739bfe7f16466bc60b41430a8f09 - because it assumes 25 | -- the end user has the correct elm-package on their PATH, which is not a 26 | -- safe assumption. 27 | -- 28 | -- License: 29 | {- 30 | Copyright (c) 2016, Yosuke Torii 31 | All rights reserved. 32 | 33 | Redistribution and use in source and binary forms, with or without 34 | modification, are permitted provided that the following conditions are met: 35 | 36 | * Redistributions of source code must retain the above copyright notice, this 37 | list of conditions and the following disclaimer. 38 | 39 | * Redistributions in binary form must reproduce the above copyright notice, 40 | this list of conditions and the following disclaimer in the documentation 41 | and/or other materials provided with the distribution. 42 | 43 | * Neither the name of elm-diff nor the names of its 44 | contributors may be used to endorse or promote products derived from 45 | this software without specific prior written permission. 46 | 47 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 48 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 49 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 50 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 51 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 52 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 53 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 54 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 55 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 56 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 57 | 58 | -} 59 | 60 | import Array exposing (Array) 61 | 62 | 63 | {-| This describes how each line has changed and also contains its value. 64 | -} 65 | type Change a 66 | = Added a 67 | | Removed a 68 | | NoChange a 69 | 70 | 71 | type StepResult 72 | = Continue (Array (List ( Int, Int ))) 73 | | Found (List ( Int, Int )) 74 | 75 | 76 | type BugReport 77 | = CannotGetA Int 78 | | CannotGetB Int 79 | | UnexpectedPath ( Int, Int ) (List ( Int, Int )) 80 | 81 | 82 | {-| Compares general lists. 83 | 84 | diff [ 1, 3 ] [ 2, 3 ] == [ Removed 1, Added 2, NoChange 3 ] -- True 85 | 86 | -} 87 | diff : List a -> List a -> List (Change a) 88 | diff a b = 89 | case testDiff a b of 90 | Ok changes -> 91 | changes 92 | 93 | Err _ -> 94 | [] 95 | 96 | 97 | {-| Test the algolithm itself. 98 | If it returns Err, it should be a bug. 99 | -} 100 | testDiff : List a -> List a -> Result BugReport (List (Change a)) 101 | testDiff a b = 102 | let 103 | arrA = 104 | Array.fromList a 105 | 106 | arrB = 107 | Array.fromList b 108 | 109 | m = 110 | Array.length arrA 111 | 112 | n = 113 | Array.length arrB 114 | 115 | -- Elm's Array doesn't allow null element, 116 | -- so we'll use shifted index to access source. 117 | getA = 118 | \x -> Array.get (x - 1) arrA 119 | 120 | getB = 121 | \y -> Array.get (y - 1) arrB 122 | 123 | path = 124 | -- Is there any case ond is needed? 125 | -- ond getA getB m n 126 | onp getA getB m n 127 | in 128 | makeChanges getA getB path 129 | 130 | 131 | makeChanges : 132 | (Int -> Maybe a) 133 | -> (Int -> Maybe a) 134 | -> List ( Int, Int ) 135 | -> Result BugReport (List (Change a)) 136 | makeChanges getA getB path = 137 | case path of 138 | [] -> 139 | Ok [] 140 | 141 | latest :: tail -> 142 | makeChangesHelp [] getA getB latest tail 143 | 144 | 145 | makeChangesHelp : 146 | List (Change a) 147 | -> (Int -> Maybe a) 148 | -> (Int -> Maybe a) 149 | -> ( Int, Int ) 150 | -> List ( Int, Int ) 151 | -> Result BugReport (List (Change a)) 152 | makeChangesHelp changes getA getB ( x, y ) path = 153 | case path of 154 | [] -> 155 | Ok changes 156 | 157 | ( prevX, prevY ) :: tail -> 158 | let 159 | change = 160 | if x - 1 == prevX && y - 1 == prevY then 161 | case getA x of 162 | Just a -> 163 | Ok (NoChange a) 164 | 165 | Nothing -> 166 | Err (CannotGetA x) 167 | 168 | else if x == prevX then 169 | case getB y of 170 | Just b -> 171 | Ok (Added b) 172 | 173 | Nothing -> 174 | Err (CannotGetB y) 175 | 176 | else if y == prevY then 177 | case getA x of 178 | Just a -> 179 | Ok (Removed a) 180 | 181 | Nothing -> 182 | Err (CannotGetA x) 183 | 184 | else 185 | Err (UnexpectedPath ( x, y ) path) 186 | in 187 | case change of 188 | Err err -> 189 | Err err 190 | 191 | Ok c -> 192 | makeChangesHelp (c :: changes) getA getB ( prevX, prevY ) tail 193 | 194 | 195 | 196 | -- Wu's O(NP) algorithm (http://myerslab.mpi-cbg.de/wp-content/uploads/2014/06/np_diff.pdf) 197 | 198 | 199 | onp : (Int -> Maybe a) -> (Int -> Maybe a) -> Int -> Int -> List ( Int, Int ) 200 | onp getA getB m n = 201 | let 202 | v = 203 | Array.initialize (m + n + 1) (always []) 204 | 205 | delta = 206 | n - m 207 | in 208 | onpLoopP (snake getA getB) delta m 0 v 209 | 210 | 211 | onpLoopP : 212 | (Int -> Int -> List ( Int, Int ) -> ( List ( Int, Int ), Bool )) 213 | -> Int 214 | -> Int 215 | -> Int 216 | -> Array (List ( Int, Int )) 217 | -> List ( Int, Int ) 218 | onpLoopP snake_ delta offset p v = 219 | let 220 | ks = 221 | if delta > 0 then 222 | List.reverse (List.range (delta + 1) (delta + p)) 223 | ++ List.range -p delta 224 | 225 | else 226 | List.reverse (List.range (delta + 1) p) 227 | ++ List.range (-p + delta) delta 228 | in 229 | case onpLoopK snake_ offset ks v of 230 | Found path -> 231 | path 232 | 233 | Continue v_ -> 234 | onpLoopP snake_ delta offset (p + 1) v_ 235 | 236 | 237 | onpLoopK : 238 | (Int -> Int -> List ( Int, Int ) -> ( List ( Int, Int ), Bool )) 239 | -> Int 240 | -> List Int 241 | -> Array (List ( Int, Int )) 242 | -> StepResult 243 | onpLoopK snake_ offset ks v = 244 | case ks of 245 | [] -> 246 | Continue v 247 | 248 | k :: ks_ -> 249 | case step snake_ offset k v of 250 | Found path -> 251 | Found path 252 | 253 | Continue v_ -> 254 | onpLoopK snake_ offset ks_ v_ 255 | 256 | 257 | step : 258 | (Int -> Int -> List ( Int, Int ) -> ( List ( Int, Int ), Bool )) 259 | -> Int 260 | -> Int 261 | -> Array (List ( Int, Int )) 262 | -> StepResult 263 | step snake_ offset k v = 264 | let 265 | fromLeft = 266 | Maybe.withDefault [] (Array.get (k - 1 + offset) v) 267 | 268 | fromTop = 269 | Maybe.withDefault [] (Array.get (k + 1 + offset) v) 270 | 271 | ( path, ( x, y ) ) = 272 | case ( fromLeft, fromTop ) of 273 | ( [], [] ) -> 274 | ( [], ( 0, 0 ) ) 275 | 276 | ( [], ( topX, topY ) :: _ ) -> 277 | ( fromTop, ( topX + 1, topY ) ) 278 | 279 | ( ( leftX, leftY ) :: _, [] ) -> 280 | ( fromLeft, ( leftX, leftY + 1 ) ) 281 | 282 | ( ( leftX, leftY ) :: _, ( topX, topY ) :: _ ) -> 283 | -- this implies "remove" comes always earlier than "add" 284 | if leftY + 1 >= topY then 285 | ( fromLeft, ( leftX, leftY + 1 ) ) 286 | 287 | else 288 | ( fromTop, ( topX + 1, topY ) ) 289 | 290 | ( newPath, goal ) = 291 | snake_ (x + 1) (y + 1) (( x, y ) :: path) 292 | in 293 | if goal then 294 | Found newPath 295 | 296 | else 297 | Continue (Array.set (k + offset) newPath v) 298 | 299 | 300 | snake : 301 | (Int -> Maybe a) 302 | -> (Int -> Maybe a) 303 | -> Int 304 | -> Int 305 | -> List ( Int, Int ) 306 | -> ( List ( Int, Int ), Bool ) 307 | snake getA getB nextX nextY path = 308 | case ( getA nextX, getB nextY ) of 309 | ( Just a, Just b ) -> 310 | if a == b then 311 | snake 312 | getA 313 | getB 314 | (nextX + 1) 315 | (nextY + 1) 316 | (( nextX, nextY ) :: path) 317 | 318 | else 319 | ( path, False ) 320 | 321 | -- reached bottom-right corner 322 | ( Nothing, Nothing ) -> 323 | ( path, True ) 324 | 325 | _ -> 326 | ( path, False ) 327 | -------------------------------------------------------------------------------- /elm/tests/Test/Reporter/Console/FormatTest.elm: -------------------------------------------------------------------------------- 1 | module Test.Reporter.Console.FormatTest exposing (suite) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | import Test.Reporter.Console.Format exposing (highlightEqual) 6 | 7 | 8 | suite : Test 9 | suite = 10 | describe "highlightEqual" 11 | [ describe "strings that should *not* be highlighted" 12 | [ describe "very different strings" 13 | [ test "Two strings which are *just* too different for high" <| 14 | \() -> 15 | let 16 | expected = 17 | "Err { context = \"Explanation of what went so wrong\", description = \"An error\" }" 18 | 19 | actual = 20 | "Ok \"Success.\"" 21 | in 22 | highlightEqual expected actual 23 | |> Expect.equal Nothing 24 | , test "A string containing another string interpersed with other characters" <| 25 | \() -> 26 | let 27 | expected = 28 | "OhK3 S-u5c6c4e2s2s 4" 29 | 30 | actual = 31 | "Ok (Success.>" 32 | in 33 | highlightEqual expected actual 34 | |> Expect.equal Nothing 35 | ] 36 | , test "strings containing floating point numbers" <| 37 | \() -> 38 | let 39 | expected = 40 | "1.6" 41 | 42 | actual = 43 | "16" 44 | in 45 | highlightEqual expected actual 46 | |> Expect.equal Nothing 47 | ] 48 | , describe "strings that should be highlighted" 49 | [ test "similar strings" <| 50 | \() -> 51 | let 52 | expected = 53 | "Err { context = \"Success\" }" 54 | 55 | actual = 56 | "(Ok \"Success\"" 57 | in 58 | highlightEqual expected actual 59 | |> Expect.notEqual Nothing 60 | ] 61 | ] 62 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import mocha from 'eslint-plugin-mocha'; 3 | import globals from 'globals'; 4 | 5 | export default [ 6 | { 7 | ignores: [ 8 | // 9 | '**/elm-stuff', 10 | '**/fixtures', 11 | '**/templates', 12 | '**/flow-typed', 13 | ], 14 | }, 15 | { 16 | rules: { 17 | ...js.configs.recommended.rules, 18 | 'no-inner-declarations': 'off', 19 | 'no-prototype-builtins': 'off', 20 | 'no-unused-vars': ['error', { caughtErrorsIgnorePattern: '^_' }], 21 | }, 22 | languageOptions: { 23 | globals: { 24 | ...globals.node, 25 | }, 26 | }, 27 | }, 28 | { 29 | files: ['tests/*'], 30 | plugins: { 31 | mocha, 32 | }, 33 | languageOptions: { 34 | globals: { 35 | ...globals.mocha, 36 | }, 37 | }, 38 | rules: { 39 | 'mocha/handle-done-callback': 'error', 40 | 'mocha/no-exclusive-tests': 'error', 41 | 'mocha/no-exports': 'error', 42 | 'mocha/no-identical-title': 'error', 43 | 'mocha/no-nested-tests': 'error', 44 | 'mocha/no-skipped-tests': 'error', 45 | }, 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /example-application-no-tests/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff 2 | -------------------------------------------------------------------------------- /example-application-no-tests/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0" 12 | }, 13 | "indirect": { 14 | "elm/json": "1.1.3", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.3" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": { 22 | "elm-explorations/test": "2.0.0" 23 | }, 24 | "indirect": { 25 | "elm/bytes": "1.0.8", 26 | "elm/random": "1.0.0" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example-application-no-tests/src/Something.elm: -------------------------------------------------------------------------------- 1 | module Something exposing (ultimateAnswer) 2 | 3 | 4 | ultimateAnswer : Int 5 | ultimateAnswer = 6 | 42 7 | -------------------------------------------------------------------------------- /example-application-no-tests/tests/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (..) 2 | 3 | import Expect exposing (Expectation) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import Test exposing (..) 6 | 7 | 8 | suite : Test 9 | suite = 10 | todo "Implement our first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!" 11 | -------------------------------------------------------------------------------- /example-application-src/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff 2 | -------------------------------------------------------------------------------- /example-application-src/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0", 12 | "elm-explorations/test": "2.0.0" 13 | }, 14 | "indirect": { 15 | "elm/bytes": "1.0.8", 16 | "elm/json": "1.1.3", 17 | "elm/random": "1.0.0", 18 | "elm/time": "1.0.0", 19 | "elm/url": "1.0.0", 20 | "elm/virtual-dom": "1.0.3" 21 | } 22 | }, 23 | "test-dependencies": { 24 | "direct": {}, 25 | "indirect": {} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example-application-src/src/Something.elm: -------------------------------------------------------------------------------- 1 | module Something exposing (..) 2 | 3 | import Expect 4 | import Test 5 | 6 | 7 | ultimateAnswer : Int 8 | ultimateAnswer = 9 | 42 10 | 11 | 12 | test = 13 | Test.test "ultimateAnswer" <| 14 | \_ -> 15 | ultimateAnswer |> Expect.equal 42 16 | -------------------------------------------------------------------------------- /example-application/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff 2 | -------------------------------------------------------------------------------- /example-application/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src", 5 | "tests" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "elm/browser": "1.0.2", 11 | "elm/core": "1.0.5", 12 | "elm/html": "1.0.0" 13 | }, 14 | "indirect": { 15 | "elm/json": "1.1.3", 16 | "elm/time": "1.0.0", 17 | "elm/url": "1.0.0", 18 | "elm/virtual-dom": "1.0.3" 19 | } 20 | }, 21 | "test-dependencies": { 22 | "direct": { 23 | "elm-explorations/test": "2.0.0" 24 | }, 25 | "indirect": { 26 | "elm/bytes": "1.0.8", 27 | "elm/random": "1.0.0" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example-application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module" 4 | } 5 | -------------------------------------------------------------------------------- /example-application/src/Something.elm: -------------------------------------------------------------------------------- 1 | module Something exposing (ultimateAnswer) 2 | 3 | 4 | ultimateAnswer : Int 5 | ultimateAnswer = 6 | 42 7 | -------------------------------------------------------------------------------- /example-application/tests/TestsFailing.elm: -------------------------------------------------------------------------------- 1 | module TestsFailing exposing (oxfordify, someTodos, testExpectations, testFuzz, testWithoutNums, ultimateTest, withoutNums) 2 | 3 | import Char 4 | import Expect 5 | import Fuzz exposing (..) 6 | import Something 7 | import String 8 | import Test exposing (..) 9 | 10 | 11 | ultimateTest : Test 12 | ultimateTest = 13 | test "the ultimate answer is 41" <| 14 | \() -> 15 | Something.ultimateAnswer 16 | |> Expect.equal 41 17 | 18 | 19 | someTodos : Test 20 | someTodos = 21 | Test.describe "you should not see these in normal output, because there are non-Todo failures" 22 | [ Test.todo "write a test here" 23 | , Test.todo "write a second test here" 24 | , Test.todo "write a third test here" 25 | ] 26 | 27 | 28 | withoutNums : String -> String 29 | withoutNums = 30 | String.filter (\ch -> not (Char.isDigit ch || ch == '.')) 31 | 32 | 33 | testWithoutNums : Test 34 | testWithoutNums = 35 | describe "withoutNums" 36 | [ fuzzWith { runs = 100, distribution = Test.noDistribution } (triple string int string) "adding numbers to strings has no effect" <| 37 | \( prefix, num, suffix ) -> 38 | withoutNums (prefix ++ String.fromInt num ++ suffix) 39 | |> Expect.equal (withoutNums (prefix ++ suffix)) 40 | ] 41 | 42 | 43 | testExpectations : Test 44 | testExpectations = 45 | describe "basic expectations" 46 | [ test "this should succeed" <| 47 | \() -> 48 | "blah" 49 | |> Expect.equal " blah" 50 | , test "this should fail" <| 51 | \() -> 52 | "something" 53 | |> Expect.equal "someting else" 54 | , test "another failure" <| 55 | \() -> 56 | "forty-two" 57 | |> Expect.equal "forty-three" 58 | ] 59 | 60 | 61 | testFuzz : Test 62 | testFuzz = 63 | describe "fuzzing" 64 | [ fuzz2 string string "empty list etc" <| 65 | \name punctuation -> 66 | oxfordify "This sentence is empty" "." [] 67 | |> Expect.equal "" 68 | |> Expect.onFail "given an empty list, did not return an empty string" 69 | , fuzz2 string string "further testing" <| 70 | \name punctuation -> 71 | oxfordify "This sentence contains " "." [ "one item" ] 72 | |> Expect.equal "This sentence contains one item." 73 | , fuzz2 string string "custom onFail here" <| 74 | \name punctuation -> 75 | oxfordify "This sentence contains " "." [ "one item", "two item" ] 76 | |> Expect.equal "This sentence contains one item and two item." 77 | |> Expect.onFail "given an empty list, did not return an empty string" 78 | , fuzz2 string string "This is a test." <| 79 | \name punctuation -> 80 | oxfordify "This sentence contains " "." [ "one item", "two item", "three item" ] 81 | |> Expect.equal "This sentence contains one item, two item, and three item." 82 | |> Expect.onFail "given a list of length 3, did not return an oxford-style sentence" 83 | ] 84 | 85 | 86 | oxfordify : a -> b -> c -> String 87 | oxfordify _ _ _ = 88 | "Alice, Bob, and Claire" 89 | -------------------------------------------------------------------------------- /example-application/tests/TestsPassing.elm: -------------------------------------------------------------------------------- 1 | module TestsPassing exposing (testEqual, testFalse, testTrue) 2 | 3 | import Expect 4 | import Something 5 | import Test exposing (Test, test) 6 | 7 | 8 | testEqual : Test 9 | testEqual = 10 | test "Expect.equal works" <| 11 | \() -> 12 | Something.ultimateAnswer 13 | |> Expect.equal 42 14 | 15 | 16 | testTrue : Test 17 | testTrue = 18 | test "Expect.equal True works" <| 19 | \() -> 20 | True 21 | |> Expect.equal True 22 | |> Expect.onFail "this should never fail!" 23 | 24 | 25 | testFalse : Test 26 | testFalse = 27 | test "Expect.equal False works" <| 28 | \() -> 29 | False 30 | |> Expect.equal False 31 | |> Expect.onFail "this should never fail!" 32 | -------------------------------------------------------------------------------- /example-package-no-tests/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff 2 | -------------------------------------------------------------------------------- /example-package-no-tests/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "elm-explorations/node-test-runner-example-package", 4 | "summary": "Example package with tests", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.0", 7 | "exposed-modules": [ 8 | "Something" 9 | ], 10 | "elm-version": "0.19.1 <= v < 0.20.0", 11 | "dependencies": { 12 | "elm/core": "1.0.0 <= v < 2.0.0", 13 | "elm/project-metadata-utils": "1.0.0 <= v < 2.0.0" 14 | }, 15 | "test-dependencies": { 16 | "elm-explorations/test": "2.0.0 <= v < 3.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example-package-no-tests/src/Something.elm: -------------------------------------------------------------------------------- 1 | module Something exposing (ultimateAnswer) 2 | 3 | -- Elm.Docs from elm/project-metadata-utils is imported because 4 | -- it has a dependency on elm/parser, which is not a dependency of 5 | -- node-test-runner. This is done to test that the generated elm.json 6 | -- for package projects correctly includes any necessary transitive 7 | -- dependencies of the package being tested 8 | 9 | import Elm.Docs 10 | 11 | 12 | ultimateAnswer : Int 13 | ultimateAnswer = 14 | 42 15 | -------------------------------------------------------------------------------- /example-package-no-tests/tests/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (..) 2 | 3 | import Expect exposing (Expectation) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import Test exposing (..) 6 | 7 | 8 | suite : Test 9 | suite = 10 | todo "Implement our first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!" 11 | -------------------------------------------------------------------------------- /example-package/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff 2 | -------------------------------------------------------------------------------- /example-package/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "elm-explorations/node-test-runner-example-package", 4 | "summary": "Example package with tests", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.0", 7 | "exposed-modules": [ 8 | "Something" 9 | ], 10 | "elm-version": "0.19.0 <= v < 0.20.0", 11 | "dependencies": { 12 | "elm/core": "1.0.0 <= v < 2.0.0", 13 | "elm/project-metadata-utils": "1.0.0 <= v < 2.0.0" 14 | }, 15 | "test-dependencies": { 16 | "elm-explorations/test": "2.0.0 <= v < 3.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example-package/src/Something.elm: -------------------------------------------------------------------------------- 1 | module Something exposing (ultimateAnswer) 2 | 3 | -- Elm.Docs from elm/project-metadata-utils is imported because 4 | -- it has a dependency on elm/parser, which is not a dependency of 5 | -- node-test-runner. This is done to test that the generated elm.json 6 | -- for package projects correctly includes any necessary transitive 7 | -- dependencies of the package being tested 8 | 9 | import Elm.Docs 10 | 11 | 12 | ultimateAnswer : Int 13 | ultimateAnswer = 14 | 42 15 | -------------------------------------------------------------------------------- /example-package/src/SomethingTest.elm: -------------------------------------------------------------------------------- 1 | module SomethingTest exposing (..) 2 | 3 | import Expect 4 | import Something 5 | import Test 6 | 7 | 8 | test = 9 | Test.test "ultimateAnswer" <| 10 | \_ -> Something.ultimateAnswer |> Expect.equal 42 11 | -------------------------------------------------------------------------------- /example-package/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "exposedList": [ 3 | "Something" 4 | ], 5 | "exactDeps": { 6 | "elm/json": "1.1.3", 7 | "elm/html": "1.0.0", 8 | "elm/project-metadata-utils": "1.0.0", 9 | "elm/parser": "1.1.0", 10 | "elm-explorations/test": "2.0.0", 11 | "elm/core": "1.0.2", 12 | "elm/random": "1.0.0", 13 | "elm/time": "1.0.0", 14 | "elm/virtual-dom": "1.0.2" 15 | }, 16 | "pkg": "elm-explorations/node-test-runner-example-package", 17 | "type": "ValidPkg" 18 | } 19 | -------------------------------------------------------------------------------- /example-package/tests/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (..) 2 | 3 | import Expect exposing (Expectation) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import Test exposing (..) 6 | 7 | 8 | suite : Test 9 | suite = 10 | todo "Implement our first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!" 11 | -------------------------------------------------------------------------------- /example-package/tests/TestsFailing.elm: -------------------------------------------------------------------------------- 1 | module TestsFailing exposing (oxfordify, someTodos, testExpectations, testFuzz, testWithoutNums, ultimateTest, withoutNums) 2 | 3 | import Char 4 | import Expect 5 | import Fuzz exposing (..) 6 | import Something 7 | import String 8 | import Test exposing (..) 9 | 10 | 11 | ultimateTest : Test 12 | ultimateTest = 13 | test "the ultimate answer is 41" <| 14 | \() -> 15 | Something.ultimateAnswer 16 | |> Expect.equal 41 17 | 18 | 19 | someTodos : Test 20 | someTodos = 21 | Test.describe "you should not see these in normal output, because there are non-Todo failures" 22 | [ Test.todo "write a test here" 23 | , Test.todo "write a second test here" 24 | , Test.todo "write a third test here" 25 | ] 26 | 27 | 28 | withoutNums : String -> String 29 | withoutNums = 30 | String.filter (\ch -> not (Char.isDigit ch || ch == '.')) 31 | 32 | 33 | testWithoutNums : Test 34 | testWithoutNums = 35 | describe "withoutNums" 36 | [ fuzzWith { runs = 100, distribution = Test.noDistribution } (triple string int string) "adding numbers to strings has no effect" <| 37 | \( prefix, num, suffix ) -> 38 | withoutNums (prefix ++ String.fromInt num ++ suffix) 39 | |> Expect.equal (withoutNums (prefix ++ suffix)) 40 | ] 41 | 42 | 43 | testExpectations : Test 44 | testExpectations = 45 | describe "basic expectations" 46 | [ test "this should succeed" <| 47 | \() -> 48 | "blah" 49 | |> Expect.equal " blah" 50 | , test "this should fail" <| 51 | \() -> 52 | "something" 53 | |> Expect.equal "someting else" 54 | , test "another failure" <| 55 | \() -> 56 | "forty-two" 57 | |> Expect.equal "forty-three" 58 | ] 59 | 60 | 61 | testFuzz : Test 62 | testFuzz = 63 | describe "fuzzing" 64 | [ fuzz2 string string "empty list etc" <| 65 | \name punctuation -> 66 | oxfordify "This sentence is empty" "." [] 67 | |> Expect.equal "" 68 | |> Expect.onFail "given an empty list, did not return an empty string" 69 | , fuzz2 string string "further testing" <| 70 | \name punctuation -> 71 | oxfordify "This sentence contains " "." [ "one item" ] 72 | |> Expect.equal "This sentence contains one item." 73 | , fuzz2 string string "custom onFail here" <| 74 | \name punctuation -> 75 | oxfordify "This sentence contains " "." [ "one item", "two item" ] 76 | |> Expect.equal "This sentence contains one item and two item." 77 | |> Expect.onFail "given an empty list, did not return an empty string" 78 | , fuzz2 string string "This is a test." <| 79 | \name punctuation -> 80 | oxfordify "This sentence contains " "." [ "one item", "two item", "three item" ] 81 | |> Expect.equal "This sentence contains one item, two item, and three item." 82 | |> Expect.onFail "given a list of length 3, did not return an oxford-style sentence" 83 | ] 84 | 85 | 86 | oxfordify : a -> b -> c -> String 87 | oxfordify _ _ _ = 88 | "Alice, Bob, and Claire" 89 | -------------------------------------------------------------------------------- /example-package/tests/TestsPassing.elm: -------------------------------------------------------------------------------- 1 | module TestsPassing exposing (testEqual, testFalse, testTrue) 2 | 3 | import Expect 4 | import Something 5 | import Test exposing (Test, test) 6 | 7 | 8 | testEqual : Test 9 | testEqual = 10 | test "Expect.equal works" <| 11 | \() -> 12 | Something.ultimateAnswer 13 | |> Expect.equal 42 14 | 15 | 16 | testTrue : Test 17 | testTrue = 18 | test "Expect.equal True works" <| 19 | \() -> 20 | True 21 | |> Expect.equal True 22 | |> Expect.onFail "this should never fail!" 23 | 24 | 25 | testFalse : Test 26 | testFalse = 27 | test "Expect.equal False works" <| 28 | \() -> 29 | False 30 | |> Expect.equal False 31 | |> Expect.onFail "this should never fail!" 32 | -------------------------------------------------------------------------------- /lib/Compile.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | const { supportsColor } = require('chalk'); 4 | const spawn = require('cross-spawn'); 5 | const ElmCompiler = require('./ElmCompiler'); 6 | const Report = require('./Report'); 7 | 8 | function compile( 9 | cwd /*: string */, 10 | testFile /*: string */, 11 | dest /*: string */, 12 | pathToElmBinary /*: string */, 13 | report /*: typeof Report.Report */ 14 | ) /*: Promise */ { 15 | return new Promise((resolve, reject) => { 16 | const compileProcess = ElmCompiler.compile([testFile], { 17 | output: dest, 18 | spawn: spawnCompiler({ ignoreStdout: true, cwd }), 19 | pathToElm: pathToElmBinary, 20 | processOpts: processOptsForReporter(report), 21 | }); 22 | 23 | compileProcess.on('close', function (exitCode) { 24 | if (exitCode !== 0) { 25 | reject(new Error(`\`elm make\` failed with exit code ${exitCode}.`)); 26 | } else { 27 | resolve(); 28 | } 29 | }); 30 | }); 31 | } 32 | 33 | function compileSources( 34 | testFilePaths /*: Array */, 35 | projectRootDir /*: string */, 36 | pathToElmBinary /*: string */, 37 | report /*: typeof Report.Report */ 38 | ) /*: Promise */ { 39 | return new Promise((resolve, reject) => { 40 | const compilerReport = report === 'json' ? report : undefined; 41 | 42 | const compileProcess = ElmCompiler.compile(testFilePaths, { 43 | output: '/dev/null', 44 | cwd: projectRootDir, 45 | spawn: spawnCompiler({ ignoreStdout: false, cwd: projectRootDir }), 46 | pathToElm: pathToElmBinary, 47 | report: compilerReport, 48 | processOpts: processOptsForReporter(report), 49 | }); 50 | 51 | compileProcess.on('close', function (exitCode) { 52 | if (exitCode === 0) { 53 | resolve(); 54 | } else { 55 | reject(new Error(`\`elm make\` failed with exit code ${exitCode}.`)); 56 | } 57 | }); 58 | }); 59 | } 60 | 61 | function spawnCompiler({ ignoreStdout, cwd }) { 62 | return ( 63 | pathToElm /*: string */, 64 | processArgs /*: Array */, 65 | processOpts /*: child_process$spawnOpts */ 66 | ) => { 67 | // It might seem useless to specify 'pipe' and then just write all data to 68 | // `process.stdout`/`process.stderr`, but it does make a difference: The Elm 69 | // compiler turns off colors when it’s used in a pipe. In summary: 70 | // 71 | // 'inherit' -> Colors, automatically written to stdout/stderr 72 | // 'pipe' -> No colors, we need to explicitly write to stdout/stderr 73 | const stdout = ignoreStdout ? 'ignore' : supportsColor ? 'inherit' : 'pipe'; 74 | const stderr = supportsColor ? 'inherit' : 'pipe'; 75 | 76 | const finalOpts = { 77 | env: process.env, 78 | ...processOpts, 79 | cwd, 80 | stdio: ['inherit', stdout, stderr], 81 | }; 82 | 83 | const child = spawn(pathToElm, processArgs, finalOpts); 84 | 85 | if (stdout === 'pipe') { 86 | child.stdout.on('data', (data) => process.stdout.write(data)); 87 | } 88 | 89 | if (stderr === 'pipe') { 90 | child.stderr.on('data', (data) => process.stderr.write(data)); 91 | } 92 | 93 | return child; 94 | }; 95 | } 96 | 97 | function processOptsForReporter( 98 | report /*: typeof Report.Report */ 99 | ) /*: child_process$spawnOpts */ { 100 | if (Report.isMachineReadable(report)) { 101 | return { env: process.env, stdio: ['ignore', 'ignore', process.stderr] }; 102 | } else { 103 | return { env: process.env }; 104 | } 105 | } 106 | 107 | module.exports = { 108 | compile, 109 | compileSources, 110 | }; 111 | -------------------------------------------------------------------------------- /lib/ElmCompiler.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | 'use strict'; 4 | 5 | var spawn = require('cross-spawn'); 6 | 7 | var defaultOptions = { 8 | spawn: spawn, 9 | cwd: undefined, 10 | pathToElm: 'elm', 11 | output: undefined, 12 | report: undefined, 13 | processOpts: undefined, 14 | }; 15 | 16 | // Converts an object of key/value pairs to an array of arguments suitable 17 | // to be passed to child_process.spawn for elm-make. 18 | function compilerArgsFromOptions(options) { 19 | var args = []; 20 | 21 | if (options.output != null) { 22 | args.push('--output', options.output); 23 | } 24 | 25 | if (options.report != null) { 26 | args.push('--report', options.report); 27 | } 28 | 29 | return args; 30 | } 31 | 32 | function runCompiler(sources, options) { 33 | var pathToElm = options.pathToElm; 34 | var processArgs = ['make'].concat(sources, compilerArgsFromOptions(options)); 35 | 36 | var processOpts = Object.assign( 37 | {}, 38 | { 39 | env: Object.assign({ LANG: 'en_US.UTF-8' }, process.env), 40 | stdio: 'inherit', 41 | cwd: options.cwd, 42 | }, 43 | options.processOpts 44 | ); 45 | 46 | return options.spawn(pathToElm, processArgs, processOpts); 47 | } 48 | 49 | function compilerErrorToString(err, pathToElm) { 50 | if (typeof err === 'object' && typeof err.code === 'string') { 51 | switch (err.code) { 52 | case 'ENOENT': 53 | return ( 54 | 'Could not find Elm compiler "' + pathToElm + '". Is it installed?' 55 | ); 56 | 57 | case 'EACCES': 58 | return ( 59 | 'Elm compiler "' + 60 | pathToElm + 61 | '" did not have permission to run. Do you need to give it executable permissions?' 62 | ); 63 | 64 | default: 65 | return ( 66 | 'Error attempting to run Elm compiler "' + pathToElm + '":\n' + err 67 | ); 68 | } 69 | } else if (typeof err === 'object' && typeof err.message === 'string') { 70 | return JSON.stringify(err.message); 71 | } else { 72 | return ( 73 | 'Exception thrown when attempting to run Elm compiler ' + 74 | JSON.stringify(pathToElm) 75 | ); 76 | } 77 | } 78 | 79 | function compile( 80 | sources /*: Array */, 81 | options /*: {| 82 | spawn?: ( 83 | string, 84 | Array, 85 | child_process$spawnOpts 86 | ) => child_process$ChildProcess, 87 | cwd?: string, 88 | pathToElm?: string, 89 | output?: string, 90 | report?: 'json', 91 | processOpts?: child_process$spawnOpts, 92 | |} */ 93 | ) /*: child_process$ChildProcess */ { 94 | var optionsWithDefaults = Object.assign({}, defaultOptions, options); 95 | try { 96 | return runCompiler(sources, optionsWithDefaults).on( 97 | 'error', 98 | function (err) { 99 | throw err; 100 | } 101 | ); 102 | } catch (err) { 103 | throw new Error(compilerErrorToString(err, optionsWithDefaults.pathToElm)); 104 | } 105 | } 106 | 107 | module.exports = { 108 | compile: compile, 109 | }; 110 | -------------------------------------------------------------------------------- /lib/ElmHome.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const path = require('path'); 4 | const os = require('os'); 5 | 6 | function elmHome() /*: string */ { 7 | const elmHomeEnv = process.env['ELM_HOME']; 8 | return elmHomeEnv === undefined ? defaultElmHome() : elmHomeEnv; 9 | } 10 | 11 | function defaultElmHome() /*: string */ { 12 | return process.platform === 'win32' 13 | ? defaultWindowsElmHome() 14 | : defaultUnixElmHome(); 15 | } 16 | 17 | function defaultUnixElmHome() /*: string */ { 18 | return path.join(os.homedir(), '.elm'); 19 | } 20 | 21 | function defaultWindowsElmHome() /*: string */ { 22 | const appData = process.env.APPDATA; 23 | const dir = 24 | appData === undefined 25 | ? path.join(os.homedir(), 'AppData', 'Roaming') 26 | : appData; 27 | return path.join(dir, 'elm'); 28 | } 29 | 30 | function packagePath(pkg /*: string */) /*: string */ { 31 | const parts = splitAuthorPkg(pkg); 32 | return path.join(elmHome(), '0.19.1', 'packages', parts.author, parts.pkg); 33 | } 34 | 35 | function splitAuthorPkg(pkgIdentifier /*: string */) /*: { 36 | author: string, 37 | pkg: string, 38 | } */ { 39 | const parts = pkgIdentifier.split('/'); 40 | return { author: parts[0], pkg: parts[1] }; 41 | } 42 | 43 | module.exports = { 44 | elmHome, 45 | packagePath, 46 | splitAuthorPkg, 47 | }; 48 | -------------------------------------------------------------------------------- /lib/ElmJson.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | // Poor man’s type alias. We can’t use /*:: type Dependencies = ... */ because of: 7 | // https://github.com/prettier/prettier/issues/2597 8 | const Dependencies /*: { [string]: string } */ = {}; 9 | 10 | const DirectAndIndirectDependencies /*: { 11 | direct: typeof Dependencies, 12 | indirect: typeof Dependencies, 13 | } */ = { direct: {}, indirect: {} }; 14 | 15 | const ElmJson /*: 16 | | { 17 | type: 'application', 18 | 'source-directories': Array, 19 | dependencies: typeof DirectAndIndirectDependencies, 20 | 'test-dependencies': typeof DirectAndIndirectDependencies, 21 | [string]: mixed, 22 | } 23 | | { 24 | type: 'package', 25 | dependencies: typeof Dependencies, 26 | 'test-dependencies': typeof Dependencies, 27 | [string]: mixed, 28 | } */ = { 29 | type: 'package', 30 | dependencies: Dependencies, 31 | 'test-dependencies': Dependencies, 32 | }; 33 | 34 | function getPath(dir /*: string */) /*: string */ { 35 | return path.join(dir, 'elm.json'); 36 | } 37 | 38 | function write(dir /*: string */, elmJson /*: typeof ElmJson */) /*: void */ { 39 | const elmJsonPath = getPath(dir); 40 | 41 | try { 42 | fs.writeFileSync(elmJsonPath, JSON.stringify(elmJson, null, 4) + '\n'); 43 | } catch (error) { 44 | throw new Error( 45 | `${elmJsonPath}\nFailed to write elm.json:\n${error.message}` 46 | ); 47 | } 48 | } 49 | 50 | function read(dir /*: string */) /*: typeof ElmJson */ { 51 | const elmJsonPath = getPath(dir); 52 | 53 | try { 54 | return readHelper(elmJsonPath); 55 | } catch (error) { 56 | throw new Error( 57 | `${elmJsonPath}\nFailed to read elm.json:\n${error.message}` 58 | ); 59 | } 60 | } 61 | 62 | function readHelper(elmJsonPath /*: string */) /*: typeof ElmJson */ { 63 | const json = parseObject( 64 | JSON.parse(fs.readFileSync(elmJsonPath, 'utf8')), 65 | 'the file' 66 | ); 67 | 68 | switch (json.type) { 69 | case 'application': 70 | return { 71 | ...json, 72 | type: 'application', 73 | 'source-directories': parseSourceDirectories( 74 | json['source-directories'] 75 | ), 76 | dependencies: parseDirectAndIndirectDependencies( 77 | json.dependencies, 78 | 'dependencies' 79 | ), 80 | 'test-dependencies': parseDirectAndIndirectDependencies( 81 | json['test-dependencies'], 82 | 'test-dependencies' 83 | ), 84 | }; 85 | 86 | case 'package': 87 | return { 88 | ...json, 89 | type: 'package', 90 | dependencies: parseDependencies(json.dependencies, 'dependencies'), 91 | 'test-dependencies': parseDependencies( 92 | json['test-dependencies'], 93 | 'test-dependencies' 94 | ), 95 | }; 96 | 97 | default: 98 | throw new Error( 99 | `Expected "type" to be "application" or "package", but got: ${stringify( 100 | json.type 101 | )}` 102 | ); 103 | } 104 | } 105 | 106 | function parseSourceDirectories(json /*: mixed */) /*: Array */ { 107 | if (!Array.isArray(json)) { 108 | throw new Error( 109 | `Expected "source-directories" to be an array, but got: ${stringify( 110 | json 111 | )}` 112 | ); 113 | } 114 | 115 | const result = []; 116 | 117 | for (const [index, item] of json.entries()) { 118 | if (typeof item !== 'string') { 119 | throw new Error( 120 | `Expected "source-directories"->${index} to be a string, but got: ${stringify( 121 | item 122 | )}` 123 | ); 124 | } 125 | result.push(item); 126 | } 127 | 128 | if (result.length === 0) { 129 | throw new Error( 130 | 'Expected "source-directories" to contain at least one item, but it is empty.' 131 | ); 132 | } 133 | 134 | return result; 135 | } 136 | 137 | function parseDirectAndIndirectDependencies( 138 | json /*: mixed */, 139 | what /*: string */ 140 | ) /*: typeof DirectAndIndirectDependencies */ { 141 | const jsonObject = parseObject(json, what); 142 | return { 143 | direct: parseDependencies(jsonObject.direct, `${what}->"direct"`), 144 | indirect: parseDependencies(jsonObject.indirect, `${what}->"indirect"`), 145 | }; 146 | } 147 | 148 | function parseDependencies( 149 | json /*: mixed */, 150 | what /*: string */ 151 | ) /*: typeof Dependencies */ { 152 | const jsonObject = parseObject(json, what); 153 | const result = {}; 154 | 155 | for (const [key, value] of Object.entries(jsonObject)) { 156 | if (typeof value !== 'string') { 157 | throw new Error( 158 | `Expected ${what}->${stringify( 159 | key 160 | )} to be a string, but got: ${stringify(value)}` 161 | ); 162 | } 163 | result[key] = value; 164 | } 165 | 166 | return result; 167 | } 168 | 169 | function parseObject( 170 | json /*: mixed */, 171 | what /*: string */ 172 | ) /*: { +[string]: mixed } */ { 173 | if (json == null || typeof json !== 'object' || Array.isArray(json)) { 174 | throw new Error( 175 | `Expected ${what} to be an object, but got: ${stringify(json)}` 176 | ); 177 | } 178 | return json; 179 | } 180 | 181 | function stringify(json /*: mixed */) /*: string */ { 182 | const maybeString = JSON.stringify(json); 183 | return maybeString === undefined ? 'undefined' : maybeString; 184 | } 185 | 186 | const ELM_TEST_PACKAGE = 'elm-explorations/test'; 187 | 188 | function requireElmTestPackage( 189 | dir /*: string */, 190 | elmJson /*: typeof ElmJson */ 191 | ) /*: void */ { 192 | const elmJsonPath = getPath(dir); 193 | const versionOrRange = getElmExplorationsTestPackageVersionOrRange(elmJson); 194 | 195 | if (versionOrRange === undefined) { 196 | throw new Error( 197 | `${elmJsonPath}\nYou must have "${ELM_TEST_PACKAGE}" in your "test-dependencies" or "dependencies" to run elm-test.` 198 | ); 199 | } else if (!versionOrRange.trimStart().startsWith('2.')) { 200 | throw new Error( 201 | `${elmJsonPath}\nThis version of elm-test only supports ${ELM_TEST_PACKAGE} 2.x, but you have ${stringify( 202 | versionOrRange 203 | )}.` 204 | ); 205 | } 206 | } 207 | 208 | function getElmExplorationsTestPackageVersionOrRange( 209 | elmJson /*: typeof ElmJson */ 210 | ) /*: string | void */ { 211 | switch (elmJson.type) { 212 | case 'application': 213 | return ( 214 | elmJson['test-dependencies'].direct[ELM_TEST_PACKAGE] || 215 | elmJson.dependencies.direct[ELM_TEST_PACKAGE] 216 | ); 217 | case 'package': 218 | return ( 219 | elmJson['test-dependencies'][ELM_TEST_PACKAGE] || 220 | elmJson.dependencies[ELM_TEST_PACKAGE] 221 | ); 222 | } 223 | } 224 | 225 | module.exports = { 226 | ELM_TEST_PACKAGE, 227 | Dependencies, 228 | DirectAndIndirectDependencies, 229 | ElmJson, 230 | getPath, 231 | parseDirectAndIndirectDependencies, 232 | read, 233 | requireElmTestPackage, 234 | write, 235 | }; 236 | -------------------------------------------------------------------------------- /lib/FindTests.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const gracefulFs = require('graceful-fs'); 4 | const fs = require('fs'); 5 | const { globSync } = require('tinyglobby'); 6 | const path = require('path'); 7 | const Parser = require('./Parser'); 8 | const Project = require('./Project'); 9 | 10 | void Project; 11 | 12 | // Double stars at the start and end is the correct way to ignore directories in 13 | // the `glob` package. 14 | // https://github.com/isaacs/node-glob/issues/270#issuecomment-273949982 15 | // https://github.com/isaacs/node-glob/blob/f5a57d3d6e19b324522a3fa5bdd5075fd1aa79d1/common.js#L222-L231 16 | const ignoredDirsGlobs = ['**/elm-stuff/**', '**/node_modules/**']; 17 | 18 | function resolveGlobs( 19 | fileGlobs /*: Array */, 20 | projectRootDir /*: string */ 21 | ) /*: Array */ { 22 | return Array.from( 23 | new Set( 24 | fileGlobs.flatMap((fileGlob) => { 25 | const absolutePath = path.resolve(fileGlob); 26 | try { 27 | const stat = fs.statSync(absolutePath); 28 | // If the CLI arg exists… 29 | return stat.isDirectory() 30 | ? // …and it’s a directory, find all .elm files in there… 31 | findAllElmFilesInDir(absolutePath) 32 | : // …otherwise use it as-is. 33 | [absolutePath]; 34 | } catch (error) { 35 | // If the CLI arg does not exist… 36 | return error.code === 'ENOENT' 37 | ? // …resolve it as a glob for shells that don’t support globs. 38 | resolveCliArgGlob(absolutePath, projectRootDir) 39 | : // The glob package ignores other types of stat errors. 40 | []; 41 | } 42 | }) 43 | ), 44 | // The `glob` package returns absolute paths with slashes always, even on 45 | // Windows. All other paths in elm-test use the native directory separator 46 | // so normalize here. 47 | (filePath) => path.normalize(filePath) 48 | ); 49 | } 50 | 51 | function resolveCliArgGlob( 52 | fileGlob /*: string */, 53 | projectRootDir /*: string */ 54 | ) /*: Array */ { 55 | // Globs passed as CLI arguments are relative to CWD, while elm-test 56 | // operates from the project root dir. 57 | const globRelativeToProjectRoot = path.relative( 58 | projectRootDir, 59 | path.resolve(fileGlob) 60 | ); 61 | 62 | // The globs _have_ to use forwards slash as path separator, regardless of 63 | // platform, making it unambiguous which characters are separators and which 64 | // are escapes. 65 | // Note: As far I can tell, escaping glob syntax has _never_ worked on 66 | // Windows. In Elm, needing to escape glob syntax should be very rare, since 67 | // Elm file paths must match the module name (letters only). So it’s probably 68 | // more worth supporting `some\folder\*Test.elm` rather than escaping. 69 | const pattern = 70 | process.platform === 'win32' 71 | ? globRelativeToProjectRoot.replace(/\\/g, '/') 72 | : globRelativeToProjectRoot; 73 | 74 | return globSync(pattern, { 75 | cwd: projectRootDir, 76 | caseSensitiveMatch: false, 77 | absolute: true, 78 | ignore: ignoredDirsGlobs, 79 | // Match directories as well 80 | onlyFiles: false, 81 | }).flatMap((filePath) => 82 | // Directories have their path end with `/` 83 | filePath.endsWith('/') ? findAllElmFilesInDir(filePath) : filePath 84 | ); 85 | } 86 | 87 | // Recursively search for *.elm files. 88 | function findAllElmFilesInDir(dir /*: string */) /*: Array */ { 89 | return globSync('**/*.elm', { 90 | cwd: dir, 91 | caseSensitiveMatch: false, 92 | absolute: true, 93 | ignore: ignoredDirsGlobs, 94 | onlyFiles: true, 95 | }); 96 | } 97 | 98 | function findTests( 99 | testFilePaths /*: Array */, 100 | project /*: typeof Project.Project */ 101 | ) /*: Promise }>> */ { 102 | return Promise.all( 103 | testFilePaths.map((filePath) => { 104 | const matchingSourceDirs = project.testsSourceDirs.filter((dir) => 105 | filePath.startsWith(`${dir}${path.sep}`) 106 | ); 107 | 108 | // Tests must be in tests/ or in source-directories – otherwise they won’t 109 | // compile. Elm won’t be able to find imports. 110 | switch (matchingSourceDirs.length) { 111 | case 0: 112 | return Promise.reject( 113 | Error( 114 | missingSourceDirectoryError( 115 | filePath, 116 | project.elmJson.type === 'package' 117 | ) 118 | ) 119 | ); 120 | 121 | case 1: 122 | // Keep going. 123 | break; 124 | 125 | default: 126 | // This shouldn’t be possible for package projects. 127 | return Promise.reject( 128 | new Error( 129 | multipleSourceDirectoriesError( 130 | filePath, 131 | matchingSourceDirs, 132 | project.testsDir 133 | ) 134 | ) 135 | ); 136 | } 137 | 138 | // By finding the module name from the file path we can import it even if 139 | // the file is full of errors. Elm will then report what’s wrong. 140 | const moduleNameParts = path 141 | .relative(matchingSourceDirs[0], filePath) 142 | .replace(/\.elm$/, '') 143 | .split(path.sep); 144 | const moduleName = moduleNameParts.join('.'); 145 | 146 | if (!moduleNameParts.every(Parser.isUpperName)) { 147 | return Promise.reject( 148 | new Error( 149 | badModuleNameError(filePath, matchingSourceDirs[0], moduleName) 150 | ) 151 | ); 152 | } 153 | 154 | return Parser.extractExposedPossiblyTests( 155 | filePath, 156 | // We’re reading files asynchronously in a loop here, so it makes sense 157 | // to use graceful-fs to avoid “too many open files” errors. 158 | gracefulFs.createReadStream 159 | ).then((possiblyTests) => ({ 160 | moduleName, 161 | possiblyTests, 162 | })); 163 | }) 164 | ); 165 | } 166 | 167 | function missingSourceDirectoryError(filePath, isPackageProject) { 168 | return ` 169 | This file: 170 | 171 | ${filePath} 172 | 173 | …matches no source directory! Imports won't work then. 174 | 175 | ${ 176 | isPackageProject 177 | ? 'Move it to tests/ or src/ in your project root.' 178 | : 'Move it to tests/ in your project root, or make sure it is covered by "source-directories" in your elm.json.' 179 | } 180 | `.trim(); 181 | } 182 | 183 | function multipleSourceDirectoriesError( 184 | filePath, 185 | matchingSourceDirs, 186 | testsDir 187 | ) { 188 | const note = matchingSourceDirs.includes(testsDir) 189 | ? "Note: The tests/ folder counts as a source directory too (even if it isn't listed in your elm.json)!" 190 | : ''; 191 | 192 | return ` 193 | This file: 194 | 195 | ${filePath} 196 | 197 | …matches more than one source directory: 198 | 199 | ${matchingSourceDirs.join('\n')} 200 | 201 | Edit "source-directories" in your elm.json and try to make it so no source directory contains another source directory! 202 | 203 | ${note} 204 | `.trim(); 205 | } 206 | 207 | function badModuleNameError(filePath, sourceDir, moduleName) { 208 | return ` 209 | This file: 210 | 211 | ${filePath} 212 | 213 | …located in this directory: 214 | 215 | ${sourceDir} 216 | 217 | …is problematic. Trying to construct a module name from the parts after the directory gives: 218 | 219 | ${moduleName} 220 | 221 | …but module names need to look like for example: 222 | 223 | Main 224 | Http.Helpers 225 | 226 | Make sure that all parts start with an uppercase letter and don't contain any spaces or anything like that. 227 | `.trim(); 228 | } 229 | 230 | function noFilesFoundError( 231 | projectRootDir /*: string */, 232 | testFileGlobs /*: Array */ 233 | ) /*: string */ { 234 | return testFileGlobs.length === 0 235 | ? ` 236 | ${noFilesFoundInTestsDir(projectRootDir)} 237 | 238 | To generate some initial tests to get things going: elm-test init 239 | 240 | Alternatively, if your project has tests in a different directory, 241 | try calling elm-test with a glob such as: elm-test "src/**/*Tests.elm" 242 | `.trim() 243 | : ` 244 | No files found matching: 245 | 246 | ${testFileGlobs.join('\n')} 247 | 248 | Are the above patterns correct? Maybe try running elm-test with no arguments? 249 | `.trim(); 250 | } 251 | 252 | function noFilesFoundInTestsDir(projectRootDir) { 253 | const testsDir = path.join(projectRootDir, 'tests'); 254 | try { 255 | const stats = fs.statSync(testsDir); 256 | return stats.isDirectory() 257 | ? 'No .elm files found in the tests/ directory.' 258 | : `Expected a directory but found something else at: ${testsDir}\nCheck it out! Could you remove it?`; 259 | } catch (error) { 260 | return error.code === 'ENOENT' 261 | ? 'The tests/ directory does not exist.' 262 | : `Failed to read the tests/ directory: ${error.message}`; 263 | } 264 | } 265 | 266 | module.exports = { 267 | findTests, 268 | ignoredDirsGlobs, 269 | noFilesFoundError, 270 | resolveGlobs, 271 | }; 272 | -------------------------------------------------------------------------------- /lib/Generate.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const { supportsColor } = require('chalk'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const DependencyProvider = require('./DependencyProvider.js'); 7 | const ElmJson = require('./ElmJson'); 8 | const Project = require('./Project'); 9 | const Report = require('./Report'); 10 | const Solve = require('./Solve'); 11 | 12 | // These values are used _only_ in flow types. 'use' them with the javascript 13 | // void operator to keep eslint happy. 14 | void DependencyProvider; 15 | void Project; 16 | void Report; 17 | 18 | const before = fs.readFileSync( 19 | path.join(__dirname, '..', 'templates', 'before.js'), 20 | 'utf8' 21 | ); 22 | 23 | const after = fs.readFileSync( 24 | path.join(__dirname, '..', 'templates', 'after.js'), 25 | 'utf8' 26 | ); 27 | 28 | function prepareCompiledJsFile( 29 | pipeFilename /*: string */, 30 | dest /*: string */ 31 | ) /*: void */ { 32 | const content = fs.readFileSync(dest, 'utf8'); 33 | const finalContent = ` 34 | ${before} 35 | var Elm = (function(module) { 36 | ${addKernelTestChecking(content)} 37 | return this.Elm; 38 | })({}); 39 | var pipeFilename = ${JSON.stringify(pipeFilename)}; 40 | ${after} 41 | `.trim(); 42 | fs.writeFileSync(dest, finalContent); 43 | 44 | // Needed when the user has `"type": "module"` in their package.json. 45 | // Our output is CommonJS. 46 | fs.writeFileSync( 47 | path.join(path.dirname(dest), 'package.json'), 48 | JSON.stringify({ type: 'commonjs' }) 49 | ); 50 | } 51 | 52 | // For older versions of elm-explorations/test we need to list every single 53 | // variant of the `Test` type. To avoid having to update this regex if a new 54 | // variant is added, newer versions of elm-explorations/test have prefixed all 55 | // variants with `ElmTestVariant__` so we can match just on that. 56 | const testVariantDefinition = 57 | /^var\s+\$elm_explorations\$test\$Test\$Internal\$(?:ElmTestVariant__\w+|UnitTest|FuzzTest|Labeled|Skipped|Only|Batch)\s*=\s*(?:\w+\(\s*)?function\s*\([\w, ]*\)\s*\{\s*return *\{/gm; 58 | 59 | const checkDefinition = 60 | /^(var\s+\$author\$project\$Test\$Runner\$Node\$check)\s*=\s*\$author\$project\$Test\$Runner\$Node\$checkHelperReplaceMe___;?$/m; 61 | 62 | // Create a symbol, tag all `Test` constructors with it and make the `check` 63 | // function look for it. 64 | function addKernelTestChecking(content) { 65 | return ( 66 | 'var __elmTestSymbol = Symbol("elmTestSymbol");\n' + 67 | content 68 | .replace(testVariantDefinition, '$&__elmTestSymbol: __elmTestSymbol, ') 69 | .replace( 70 | checkDefinition, 71 | '$1 = value => value && value.__elmTestSymbol === __elmTestSymbol ? $elm$core$Maybe$Just(value) : $elm$core$Maybe$Nothing;' 72 | ) 73 | ); 74 | } 75 | 76 | function getGeneratedSrcDir(generatedCodeDir /*: string */) /*: string */ { 77 | return path.join(generatedCodeDir, 'src'); 78 | } 79 | 80 | function generateElmJson( 81 | dependencyProvider /*: DependencyProvider */, 82 | project /*: typeof Project.Project */ 83 | ) /*: void */ { 84 | const generatedSrc = getGeneratedSrcDir(project.generatedCodeDir); 85 | 86 | fs.mkdirSync(generatedSrc, { recursive: true }); 87 | 88 | const sourceDirs = [ 89 | // Include the generated test application. 90 | generatedSrc, 91 | 92 | // NOTE: we must include node-test-runner's Elm source as a source-directory 93 | // instead of adding it as a dependency so that it can include port modules 94 | path.join(__dirname, '..', 'elm', 'src'), 95 | ] 96 | .concat(project.testsSourceDirs) 97 | .filter( 98 | // When running node-test-runner's own test suite, the node-test-runner/src folder 99 | // will get added twice: once because it's the source-directory of the packge being tested, 100 | // and once because elm-test will always add it. 101 | // To prevent elm from being confused, we need to remove the duplicate when this happens. 102 | (value, index, self) => self.indexOf(value) === index 103 | ) 104 | .map((absolutePath) => 105 | // Relative paths have the nice benefit that if the user moves their 106 | // directory, this doesn't break. 107 | path.relative(project.generatedCodeDir, absolutePath) 108 | ); 109 | 110 | const testElmJson = { 111 | type: 'application', 112 | 'source-directories': sourceDirs, 113 | 'elm-version': '0.19.1', 114 | dependencies: Solve.getDependenciesCached(dependencyProvider, project), 115 | 'test-dependencies': { 116 | direct: {}, 117 | indirect: {}, 118 | }, 119 | }; 120 | 121 | // Generate the new elm.json, if necessary. 122 | const generatedContents = JSON.stringify(testElmJson, null, 4); 123 | const generatedPath = ElmJson.getPath(project.generatedCodeDir); 124 | 125 | // Don't write a fresh elm.json if it's going to be the same. If we do, 126 | // it will update the timestamp on the file, which will cause `elm make` 127 | // to do a bunch of unnecessary work. 128 | if ( 129 | !fs.existsSync(generatedPath) || 130 | generatedContents !== fs.readFileSync(generatedPath, 'utf8') 131 | ) { 132 | // package projects don't explicitly list their transitive dependencies, 133 | // to we have to figure out what they are. We write the elm.json that 134 | // we have so far, and run elm to see what it thinks is missing. 135 | fs.writeFileSync(generatedPath, generatedContents); 136 | } 137 | } 138 | 139 | function getMainModule(generatedCodeDir /*: string */) /*: { 140 | moduleName: string, 141 | path: string, 142 | } */ { 143 | const moduleName = ['Test', 'Generated', 'Main']; 144 | return { 145 | moduleName: moduleName.join('.'), 146 | path: 147 | // We'll be putting the generated Main in something like this: 148 | // 149 | // my-project-name/elm-stuff/generated-code/elm-community/elm-test/0.19.1-revisionX/src/Test/Generated/Main.elm 150 | path.join(getGeneratedSrcDir(generatedCodeDir), ...moduleName) + '.elm', 151 | }; 152 | } 153 | 154 | function generateMainModule( 155 | fuzz /*: number */, 156 | seed /*: number */, 157 | report /*: typeof Report.Report */, 158 | testFileGlobs /*: Array */, 159 | testFilePaths /*: Array */, 160 | testModules /*: Array<{ 161 | moduleName: string, 162 | possiblyTests: Array, 163 | }> */, 164 | mainModule /*: { moduleName: string, path: string } */, 165 | processes /*: number */ 166 | ) /*: void */ { 167 | const testFileBody = makeTestFileBody( 168 | testModules, 169 | makeOptsCode(fuzz, seed, report, testFileGlobs, testFilePaths, processes) 170 | ); 171 | 172 | const testFileContents = `module ${mainModule.moduleName} exposing (main)\n\n${testFileBody}`; 173 | 174 | fs.mkdirSync(path.dirname(mainModule.path), { recursive: true }); 175 | 176 | fs.writeFileSync(mainModule.path, testFileContents); 177 | } 178 | 179 | function makeTestFileBody( 180 | testModules /*: Array<{ 181 | moduleName: string, 182 | possiblyTests: Array, 183 | }> */, 184 | optsCode /*: string */ 185 | ) /*: string */ { 186 | const imports = testModules.map((mod) => `import ${mod.moduleName}`); 187 | 188 | const possiblyTestsList = makeList(testModules.map(makeModuleTuple)); 189 | 190 | return ` 191 | ${imports.join('\n')} 192 | 193 | import Test.Reporter.Reporter exposing (Report(..)) 194 | import Console.Text exposing (UseColor(..)) 195 | import Test.Runner.Node 196 | import Test 197 | 198 | main : Test.Runner.Node.TestProgram 199 | main = 200 | Test.Runner.Node.run 201 | ${indentAllButFirstLine(' ', optsCode)} 202 | ${indentAllButFirstLine(' ', possiblyTestsList)} 203 | `.trim(); 204 | } 205 | 206 | function makeModuleTuple(mod /*: { 207 | moduleName: string, 208 | possiblyTests: Array, 209 | } */) /*: string */ { 210 | const list = mod.possiblyTests.map( 211 | (test) => `Test.Runner.Node.check ${mod.moduleName}.${test}` 212 | ); 213 | 214 | return ` 215 | ( "${mod.moduleName}" 216 | , ${indentAllButFirstLine(' ', makeList(list))} 217 | ) 218 | `.trim(); 219 | } 220 | 221 | function makeList(parts /*: Array */) /*: string */ { 222 | if (parts.length === 0) { 223 | return '[]'; 224 | } 225 | 226 | const list = parts.map( 227 | (part, index) => 228 | `${index === 0 ? '' : ', '}${indentAllButFirstLine(' ', part)}` 229 | ); 230 | 231 | return ` 232 | [ ${list.join('\n')} 233 | ] 234 | `.trim(); 235 | } 236 | 237 | function indentAllButFirstLine(indent, string) { 238 | return string 239 | .split('\n') 240 | .map((line, index) => (index === 0 ? line : indent + line)) 241 | .join('\n'); 242 | } 243 | 244 | function makeOptsCode( 245 | fuzz /*: number */, 246 | seed /*: number */, 247 | report /*: typeof Report.Report */, 248 | testFileGlobs /*: Array */, 249 | testFilePaths /*: Array */, 250 | processes /*: number */ 251 | ) /*: string */ { 252 | return ` 253 | { runs = ${fuzz} 254 | , report = ${generateElmReportVariant(report)} 255 | , seed = ${seed} 256 | , processes = ${processes} 257 | , globs = 258 | ${indentAllButFirstLine(' ', makeList(testFileGlobs.map(makeElmString)))} 259 | , paths = 260 | ${indentAllButFirstLine(' ', makeList(testFilePaths.map(makeElmString)))} 261 | } 262 | `.trim(); 263 | } 264 | 265 | function generateElmReportVariant( 266 | report /*: typeof Report.Report */ 267 | ) /*: string */ { 268 | switch (report) { 269 | case 'json': 270 | return 'JsonReport'; 271 | case 'junit': 272 | return 'JUnitReport'; 273 | case 'console': 274 | if (supportsColor) { 275 | return 'ConsoleReport UseColor'; 276 | } else { 277 | return 'ConsoleReport Monochrome'; 278 | } 279 | } 280 | } 281 | 282 | function makeElmString(string) { 283 | return `"${string 284 | .replace(/[\\"]/g, '\\$&') 285 | .replace(/\n/g, '\\n') 286 | .replace(/\r/g, '\\r')}"`; 287 | } 288 | 289 | module.exports = { 290 | generateElmJson, 291 | generateMainModule, 292 | getMainModule, 293 | prepareCompiledJsFile, 294 | }; 295 | -------------------------------------------------------------------------------- /lib/Install.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const spawn = require('cross-spawn'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const ElmJson = require('./ElmJson'); 7 | const Project = require('./Project'); 8 | 9 | void Project; 10 | 11 | function rmDirSync(dir /*: string */) /*: void */ { 12 | // We can replace this with just `fs.rmSync(dir, { recursive: true, force: true })` 13 | // when Node.js 12 is EOL 2022-04-30 and support for Node.js 12 is dropped. 14 | // `fs.rmSync` was added in Node.js 14.14.0, which is also when the 15 | // `recursive` option of `fs.rmdirSync` was deprecated. The `if` avoids 16 | // printing a deprecation message. 17 | if (fs.rmSync !== undefined) { 18 | fs.rmSync(dir, { recursive: true, force: true }); 19 | } else if (fs.existsSync(dir)) { 20 | fs.rmdirSync(dir, { recursive: true }); 21 | } 22 | } 23 | 24 | function install( 25 | project /*: typeof Project.Project */, 26 | pathToElmBinary /*: string */, 27 | packageName /*: string */ 28 | ) /*: 'SuccessfullyInstalled' | 'AlreadyInstalled' */ { 29 | const installationScratchDir = path.join(project.generatedCodeDir, 'install'); 30 | 31 | try { 32 | // Recreate the directory to remove any artifacts from the last time 33 | // someone ran `elm-test install`. We do not delete this directory after 34 | // the installation finishes in case the user needs to debug the test run. 35 | rmDirSync(installationScratchDir); 36 | fs.mkdirSync(installationScratchDir, { recursive: true }); 37 | } catch (error) { 38 | throw new Error( 39 | `Unable to create temporary directory for elm-test install: ${error.message}` 40 | ); 41 | } 42 | 43 | const { elmJson } = project; 44 | 45 | if ( 46 | elmJson.type === 'package' 47 | ? elmJson['test-dependencies'].hasOwnProperty(packageName) 48 | : elmJson['test-dependencies'].direct.hasOwnProperty(packageName) 49 | ) { 50 | return 'AlreadyInstalled'; 51 | } 52 | 53 | const tmpElmJson = 54 | elmJson.type === 'package' 55 | ? elmJson 56 | : { 57 | ...elmJson, 58 | // Without this, `elm install` will complain about missing source dirs 59 | // in the temp dir. This way we don't have to create them! 60 | 'source-directories': ['.'], 61 | }; 62 | 63 | ElmJson.write(installationScratchDir, tmpElmJson); 64 | 65 | const result = spawn.sync(pathToElmBinary, ['install', packageName], { 66 | stdio: 'inherit', 67 | cwd: installationScratchDir, 68 | }); 69 | 70 | if (result.status !== 0) { 71 | process.exit(result.status); 72 | } 73 | 74 | const newElmJson = ElmJson.read(installationScratchDir); 75 | 76 | if (newElmJson.type === 'package') { 77 | Object.keys(newElmJson['dependencies']).forEach(function (key) { 78 | if (!elmJson['dependencies'].hasOwnProperty(key)) { 79 | // If we didn't have this dep before, move it to test-dependencies. 80 | newElmJson['test-dependencies'][key] = newElmJson['dependencies'][key]; 81 | 82 | delete newElmJson['dependencies'][key]; 83 | } 84 | }); 85 | } else { 86 | function moveToTestDeps(directness) { 87 | Object.keys(newElmJson['dependencies'][directness]).forEach(function ( 88 | key 89 | ) { 90 | // If we didn't have this dep before, move it to test-dependencies. 91 | if (!elmJson['dependencies'][directness].hasOwnProperty(key)) { 92 | // Don't put things in indirect test-dependencies if they 93 | // are already present in direct test-dependencies! See this issue: 94 | // https://github.com/rtfeldman/node-test-runner/issues/282 95 | if ( 96 | directness === 'direct' || 97 | !newElmJson['test-dependencies']['direct'].hasOwnProperty(key) 98 | ) { 99 | newElmJson['test-dependencies'][directness][key] = 100 | newElmJson['dependencies'][directness][key]; 101 | } 102 | 103 | delete newElmJson['dependencies'][directness][key]; 104 | } 105 | }); 106 | } 107 | 108 | moveToTestDeps('direct'); 109 | moveToTestDeps('indirect'); 110 | 111 | if (elmJson.type === 'application') { 112 | // Restore the old source-directories value. 113 | newElmJson['source-directories'] = elmJson['source-directories']; 114 | } 115 | } 116 | 117 | ElmJson.write(project.rootDir, newElmJson); 118 | 119 | return 'SuccessfullyInstalled'; 120 | } 121 | 122 | module.exports = { 123 | install, 124 | }; 125 | -------------------------------------------------------------------------------- /lib/Project.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const ElmJson = require('./ElmJson'); 6 | 7 | // Poor man’s type alias. We can’t use /*:: type Project = ... */ because of: 8 | // https://github.com/prettier/prettier/issues/2597 9 | const Project /*: { 10 | rootDir: string, 11 | testsDir: string, 12 | generatedCodeDir: string, 13 | testsSourceDirs: Array, 14 | elmJson: typeof ElmJson.ElmJson, 15 | } */ = { 16 | rootDir: '', 17 | testsDir: '', 18 | generatedCodeDir: '', 19 | testsSourceDirs: [], 20 | elmJson: ElmJson.ElmJson, 21 | }; 22 | 23 | function getTestsDir(rootDir /*: string */) /*: string */ { 24 | return path.join(rootDir, 'tests'); 25 | } 26 | 27 | function init( 28 | rootDir /*: string */, 29 | version /*: string */ 30 | ) /*: typeof Project */ { 31 | const testsDir = getTestsDir(rootDir); 32 | 33 | // The tests/ directory is not required. You can also co-locate tests with 34 | // their source files. 35 | const shouldAddTestsDirAsSource = fs.existsSync(testsDir); 36 | 37 | const elmJson = ElmJson.read(rootDir); 38 | 39 | const projectSourceDirs = 40 | elmJson.type === 'package' ? ['src'] : elmJson['source-directories']; 41 | 42 | const resolvedSourceDirs = projectSourceDirs.map((src) => 43 | path.resolve(rootDir, src) 44 | ); 45 | 46 | const testsSourceDirs = resolvedSourceDirs.includes(testsDir) 47 | ? resolvedSourceDirs 48 | : shouldAddTestsDirAsSource 49 | ? resolvedSourceDirs.concat([testsDir]) 50 | : resolvedSourceDirs; 51 | 52 | const generatedCodeDir = path.join( 53 | rootDir, 54 | 'elm-stuff', 55 | 'generated-code', 56 | 'elm-community', 57 | 'elm-test', 58 | version 59 | ); 60 | 61 | return { 62 | rootDir, 63 | testsDir, 64 | generatedCodeDir, 65 | testsSourceDirs, 66 | elmJson, 67 | }; 68 | } 69 | 70 | /* We do this validation ourselves to avoid the ../../../../../ in Elm’s error message: 71 | 72 | -- MISSING SOURCE DIRECTORY ------------------------------------------- elm.json 73 | 74 | I need a valid elm.json file, but the "source-directories" field lists the 75 | following directory: 76 | 77 | ../../../../../app 78 | 79 | I cannot find it though. Is it missing? Is there a typo? 80 | */ 81 | function validateTestsSourceDirs(project /*: typeof Project */) /*: void */ { 82 | for (const dir of project.testsSourceDirs) { 83 | const fullDir = path.resolve(project.rootDir, dir); 84 | let stats; 85 | try { 86 | stats = fs.statSync(fullDir); 87 | } catch (error) { 88 | throw new Error( 89 | validateTestsSourceDirsError( 90 | fullDir, 91 | error.code === 'ENOENT' 92 | ? "It doesn't exist though. Is it missing? Is there a typo?" 93 | : `Failed to read that directory: ${error.message}` 94 | ) 95 | ); 96 | } 97 | if (!stats.isDirectory()) { 98 | throw new Error( 99 | validateTestsSourceDirsError( 100 | fullDir, 101 | `It exists but isn't a directory!` 102 | ) 103 | ); 104 | } 105 | } 106 | } 107 | 108 | function validateTestsSourceDirsError(dir, message) { 109 | return ` 110 | The "source-directories" field in your elm.json lists the following directory: 111 | 112 | ${dir} 113 | 114 | ${message} 115 | `.trim(); 116 | } 117 | 118 | module.exports = { 119 | Project, 120 | getTestsDir, 121 | init, 122 | validateTestsSourceDirs, 123 | }; 124 | -------------------------------------------------------------------------------- /lib/Report.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // Poor man’s type alias. We can’t use /*:: type Report = ... */ because of: 4 | // https://github.com/prettier/prettier/issues/2597 5 | const Report /*: 'console' | 'json' | 'junit' */ = 'console'; 6 | 7 | const all = ['json', 'junit', 'console']; 8 | 9 | function isMachineReadable(report /*: typeof Report */) /*: boolean */ { 10 | switch (report) { 11 | case 'json': 12 | case 'junit': 13 | return true; 14 | case 'console': 15 | return false; 16 | } 17 | } 18 | 19 | module.exports = { 20 | Report, 21 | all, 22 | isMachineReadable, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/Solve.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const crypto = require('crypto'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const ElmJson = require('./ElmJson'); 7 | const Project = require('./Project'); 8 | const DependencyProvider = require('./DependencyProvider.js'); 9 | 10 | // These value are used _only_ in flow types. 'use' them with the javascript 11 | // void operator to keep eslint happy. 12 | void Project; 13 | void DependencyProvider; 14 | 15 | function sha256(string) { 16 | return crypto.createHash('sha256').update(string).digest('hex'); 17 | } 18 | 19 | function getDependenciesCached( 20 | dependencyProvider /*: DependencyProvider */, 21 | project /*: typeof Project.Project */ 22 | ) /*: typeof ElmJson.DirectAndIndirectDependencies */ { 23 | const hash = sha256( 24 | JSON.stringify({ 25 | dependencies: project.elmJson.dependencies, 26 | 'test-dependencies': project.elmJson['test-dependencies'], 27 | }) 28 | ); 29 | 30 | const cacheFile = path.join( 31 | project.generatedCodeDir, 32 | `dependencies.${hash}.json` 33 | ); 34 | 35 | try { 36 | return JSON.parse(fs.readFileSync(cacheFile, 'utf8')); 37 | } catch (error) { 38 | if (error.code !== 'ENOENT') { 39 | console.warn( 40 | `Ignoring bad dependencies cache file:\n\n${error.message}\n\nPlease report this issue: https://github.com/rtfeldman/node-test-runner/issues/new` 41 | ); 42 | } 43 | } 44 | 45 | const dependencies = getDependencies(dependencyProvider, project.elmJson); 46 | 47 | fs.writeFileSync(cacheFile, dependencies); 48 | 49 | return ElmJson.parseDirectAndIndirectDependencies( 50 | JSON.parse(dependencies), 51 | 'cached solved dependencies' 52 | ); 53 | } 54 | 55 | function getDependencies( 56 | dependencyProvider /*: DependencyProvider */, 57 | elmJson /*: typeof ElmJson.ElmJson */ 58 | ) /*: string */ { 59 | const useTest = true; 60 | // Note: These are the dependencies listed in `elm/elm.json`, except 61 | // `elm-explorations/test`. `elm/elm.json` is only used during development of 62 | // this CLI (for editor integrations and unit tests). When running `elm-test` 63 | // we add the `elm/` folder in the npm package as a source directory. The 64 | // dependencies listed here and the ones in `elm/elm.json` need to be in sync. 65 | const extra = { 66 | 'elm/core': '1.0.0 <= v < 2.0.0', 67 | 'elm/json': '1.0.0 <= v < 2.0.0', 68 | 'elm/time': '1.0.0 <= v < 2.0.0', 69 | 'elm/random': '1.0.0 <= v < 2.0.0', 70 | }; 71 | const elmJsonStr = JSON.stringify(elmJson); 72 | try { 73 | return dependencyProvider.solveOffline(elmJsonStr, useTest, extra); 74 | } catch (_) { 75 | return dependencyProvider.solveOnline(elmJsonStr, useTest, extra); 76 | } 77 | } 78 | 79 | module.exports = { 80 | getDependenciesCached, 81 | }; 82 | -------------------------------------------------------------------------------- /lib/SyncGet.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const path = require('path'); 4 | const { 5 | Worker, 6 | MessageChannel, 7 | receiveMessageOnPort, 8 | // $FlowFixMe[cannot-resolve-module]: Flow doesn’t seem to know about the `worker_threads` module yet. 9 | } = require('worker_threads'); 10 | 11 | // Start a worker thread and return a `syncGetWorker` 12 | // capable of making sync requests until shut down. 13 | function startWorker() /*: { 14 | get: (string) => string, 15 | shutDown: () => void, 16 | } */ { 17 | const { port1: localPort, port2: workerPort } = new MessageChannel(); 18 | const sharedLock = new SharedArrayBuffer(4); 19 | // $FlowFixMe[incompatible-call]: Flow is wrong and says `sharedLock` is not an accepted parameter here. 20 | const sharedLockArray = new Int32Array(sharedLock); 21 | const workerPath = path.resolve(__dirname, 'SyncGetWorker.js'); 22 | const worker = new Worker(workerPath, { 23 | workerData: { sharedLock, requestPort: workerPort }, 24 | transferList: [workerPort], 25 | }); 26 | function get(url) { 27 | worker.postMessage(url); 28 | Atomics.wait(sharedLockArray, 0, 0); // blocks until notified at index 0. 29 | const response = receiveMessageOnPort(localPort); 30 | if (response.message.error) { 31 | throw response.message.error; 32 | } else { 33 | return response.message; 34 | } 35 | } 36 | function shutDown() { 37 | localPort.close(); 38 | worker.terminate(); 39 | } 40 | return { get, shutDown }; 41 | } 42 | 43 | module.exports = { 44 | startWorker, 45 | }; 46 | -------------------------------------------------------------------------------- /lib/SyncGetWorker.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // $FlowFixMe[cannot-resolve-module]: Flow doesn’t seem to know about the `worker_threads` module yet. 4 | const { parentPort, workerData } = require('worker_threads'); 5 | const https = require('https'); 6 | 7 | const { sharedLock, requestPort } = workerData; 8 | const sharedLockArray = new Int32Array(sharedLock); 9 | 10 | parentPort.on('message', async (url) => { 11 | try { 12 | const response = await getBody(url); 13 | requestPort.postMessage(response); 14 | } catch (error) { 15 | requestPort.postMessage({ error }); 16 | } 17 | Atomics.notify(sharedLockArray, 0, Infinity); 18 | }); 19 | 20 | async function getBody(url /*: string */) /*: Promise */ { 21 | return new Promise(function (resolve, reject) { 22 | https 23 | .get(url, function (res) { 24 | let body = ''; 25 | res.on('data', function (chunk) { 26 | body += chunk; 27 | }); 28 | res.on('end', function () { 29 | resolve(body); 30 | }); 31 | }) 32 | .on('error', function (err) { 33 | reject(err); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-test", 3 | "version": "0.19.1-revision15", 4 | "description": "Run elm-test suites.", 5 | "main": "elm-test.js", 6 | "engines": { 7 | "node": ">=12.20.0" 8 | }, 9 | "scripts": { 10 | "prepare": "elm-tooling install", 11 | "test": "npm run check && npm run test-only", 12 | "flow": "flow", 13 | "lint": "eslint --report-unused-disable-directives .", 14 | "review": "cd elm && elm-review", 15 | "elm-test": "cd elm && node ../bin/elm-test", 16 | "test-only": "mocha tests && npm run elm-test", 17 | "check": "flow check && npm run lint && npm run format:check && npm run review", 18 | "format:check": "prettier --check . && elm-format elm --validate", 19 | "format:write": "prettier --write . && elm-format elm --yes" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/rtfeldman/node-test-runner.git" 24 | }, 25 | "bin": { 26 | "elm-test": "bin/elm-test" 27 | }, 28 | "files": [ 29 | "bin", 30 | "lib", 31 | "templates", 32 | "elm/src" 33 | ], 34 | "keywords": [ 35 | "elm", 36 | "elm-test", 37 | "cli" 38 | ], 39 | "author": "Richard Feldman", 40 | "license": "BSD-3-Clause", 41 | "bugs": { 42 | "url": "https://github.com/rtfeldman/node-test-runner/issues" 43 | }, 44 | "homepage": "https://github.com/rtfeldman/node-test-runner#readme", 45 | "dependencies": { 46 | "chalk": "^4.1.2", 47 | "chokidar": "^3.5.3", 48 | "commander": "^9.4.1", 49 | "cross-spawn": "^7.0.6", 50 | "elm-solve-deps-wasm": "^1.0.2 || ^2.0.0", 51 | "graceful-fs": "^4.2.10", 52 | "split": "^1.0.1", 53 | "tinyglobby": "^0.2.10", 54 | "which": "^2.0.2", 55 | "xmlbuilder": "^15.1.1" 56 | }, 57 | "devDependencies": { 58 | "@eslint/js": "9.20.0", 59 | "elm-review": "2.13.1", 60 | "elm-tooling": "1.15.1", 61 | "eslint": "9.20.1", 62 | "eslint-plugin-mocha": "10.5.0", 63 | "flow-bin": "0.180.0", 64 | "globals": "15.15.0", 65 | "mocha": "11.1.0", 66 | "prettier": "2.8.1", 67 | "strip-ansi": "6.0.0", 68 | "xml2js": "0.5.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | proseWrap: 'never', 4 | overrides: [ 5 | { 6 | files: '*.js', 7 | options: { 8 | parser: 'flow', 9 | }, 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /templates/after.js: -------------------------------------------------------------------------------- 1 | var net = require('net'), 2 | client = net.createConnection(pipeFilename); 3 | 4 | client.on('error', function (error) { 5 | console.error(error); 6 | client.end(); 7 | process.exit(1); 8 | }); 9 | 10 | client.setEncoding('utf8'); 11 | client.setNoDelay(true); 12 | 13 | // Run the Elm app. 14 | var app = Elm.Test.Generated.Main.init({ flags: Date.now() }); 15 | 16 | client.on('data', function (msg) { 17 | app.ports.elmTestPort__receive.send(JSON.parse(msg)); 18 | }); 19 | 20 | // Use ports for inter-process communication. 21 | app.ports.elmTestPort__send.subscribe(function (msg) { 22 | // We split incoming messages on the socket on newlines. The gist is that node 23 | // is rather unpredictable in whether or not a single `write` will result in a 24 | // single `on('data')` callback. Sometimes it does, sometimes multiple writes 25 | // result in a single callback and - worst of all - sometimes a single read 26 | // results in multiple callbacks, each receiving a piece of the data. The 27 | // horror. 28 | client.write(msg + '\n'); 29 | }); 30 | -------------------------------------------------------------------------------- /templates/before.js: -------------------------------------------------------------------------------- 1 | // Apply Node polyfills as necessary. 2 | var window = { 3 | Date: Date, 4 | addEventListener: function () {}, 5 | removeEventListener: function () {}, 6 | }; 7 | 8 | var location = { 9 | href: '', 10 | host: '', 11 | hostname: '', 12 | protocol: '', 13 | origin: '', 14 | port: '', 15 | pathname: '', 16 | search: '', 17 | hash: '', 18 | username: '', 19 | password: '', 20 | }; 21 | var document = { body: {}, createTextNode: function () {}, location: location }; 22 | 23 | if (typeof FileList === 'undefined') { 24 | FileList = function () {}; 25 | } 26 | 27 | if (typeof File === 'undefined') { 28 | File = function () {}; 29 | } 30 | 31 | if (typeof XMLHttpRequest === 'undefined') { 32 | XMLHttpRequest = function () { 33 | return { 34 | addEventListener: function () {}, 35 | open: function () {}, 36 | send: function () {}, 37 | }; 38 | }; 39 | 40 | var oldConsoleWarn = console.warn; 41 | console.warn = function () { 42 | if ( 43 | arguments.length === 1 && 44 | arguments[0].indexOf('Compiled in DEV mode') === 0 45 | ) 46 | return; 47 | return oldConsoleWarn.apply(console, arguments); 48 | }; 49 | } 50 | 51 | if (typeof FormData === 'undefined') { 52 | FormData = function () { 53 | this._data = []; 54 | }; 55 | FormData.prototype.append = function () { 56 | this._data.push(Array.prototype.slice.call(arguments)); 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /templates/tests/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (..) 2 | 3 | import Expect exposing (Expectation) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import Test exposing (..) 6 | 7 | 8 | suite : Test 9 | suite = 10 | todo "Implement our first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!" 11 | -------------------------------------------------------------------------------- /tests/ElmJson.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const ElmJson = require('../lib/ElmJson'); 7 | const { fixturesDir } = require('./util'); 8 | 9 | const invalidElmJsonContainerDir = path.join(fixturesDir, 'invalid-elm-json'); 10 | 11 | const invalidElmJsonDirs = [ 12 | 'application-with-package-style-test-dependencies', 13 | 'dependency-not-string', 14 | 'elm-test-package-too-old-application', 15 | 'elm-test-package-too-old-package', 16 | 'empty-source-directories', 17 | 'is-folder', 18 | 'is-null', 19 | 'json-syntax-error', 20 | 'missing-elm-test-package', 21 | 'null-type', 22 | 'package-with-application-style-dependencies', 23 | 'source-directories-not-array', 24 | 'source-directory-not-string', 25 | 'unknown-type', 26 | ]; 27 | 28 | describe('handling invalid elm.json', () => { 29 | it('Should run every directory in invalid-elm-json/', () => { 30 | const filesFound = fs.readdirSync(invalidElmJsonContainerDir).sort(); 31 | assert.deepStrictEqual(filesFound, invalidElmJsonDirs); 32 | }); 33 | 34 | for (const dir of invalidElmJsonDirs) { 35 | it(`Should handle error for: ${dir}`, () => { 36 | const fullPath = path.join(invalidElmJsonContainerDir, dir); 37 | const expected = fs 38 | .readFileSync(path.join(fullPath, 'expected.txt'), 'utf8') 39 | .trim() 40 | .replace('/full/path/to/elm.json', path.join(fullPath, 'elm.json')) 41 | .replace(/\r\n/g, '\n'); 42 | assert.throws( 43 | () => { 44 | const elmJson = ElmJson.read(fullPath); 45 | ElmJson.requireElmTestPackage(fullPath, elmJson); 46 | }, 47 | (error) => { 48 | assert.strictEqual( 49 | error.message.replace( 50 | // Handle slightly different JSON.parse error messages on different Node.js versions. 51 | /^.+ in JSON at position .+$/gm, 52 | '(the JSON parse error)' 53 | ), 54 | expected 55 | ); 56 | return true; 57 | } 58 | ); 59 | }); 60 | } 61 | }); 62 | 63 | // Note: 64 | // - The fields should be in the same order as the input file. 65 | // - The changed fields should be updated. 66 | // - The non-standard fields should be preserved. 67 | // - The file should use 4 spaces of indentation. 68 | const expectedWrittenElmJson = `{ 69 | "nonStandardFieldStart": 1, 70 | "type": "application", 71 | "source-directories": [ 72 | "other/directory" 73 | ], 74 | "elm-version": "0.19.0", 75 | "dependencies": { 76 | "direct": { 77 | "elm/core": "1.0.0" 78 | }, 79 | "indirect": {} 80 | }, 81 | "nonStandardFieldMiddle": [ 82 | 1, 83 | 2, 84 | 3 85 | ], 86 | "test-dependencies": { 87 | "direct": { 88 | "elm/regex": "1.0.0", 89 | "elm-explorations/test": "2.0.0" 90 | }, 91 | "indirect": { 92 | "elm/html": "1.0.0", 93 | "elm/virtual-dom": "1.0.2" 94 | } 95 | }, 96 | "nonStandardFieldEnd": { 97 | "a": 1, 98 | "b": 2 99 | } 100 | } 101 | `; 102 | 103 | describe('Writing an elm.json', () => { 104 | it('Should have a correct output', () => { 105 | const dir = path.join(fixturesDir, 'write-elm-json'); 106 | fs.copyFileSync( 107 | path.join(dir, 'elm.input.json'), 108 | path.join(dir, 'elm.json') 109 | ); 110 | const elmJson = ElmJson.read(dir); 111 | const newElmJson = { 112 | ...elmJson, 113 | 'elm-version': '0.19.0', 114 | 'source-directories': ['other/directory'], 115 | }; 116 | ElmJson.write(dir, newElmJson); 117 | const actual = fs.readFileSync(path.join(dir, 'elm.json'), 'utf8'); 118 | assert.strictEqual(actual, expectedWrittenElmJson); 119 | assert(actual.endsWith('\n'), 'elm.json should end with a newline'); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /tests/Parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const stream = require('stream'); 5 | const Parser = require('../lib/Parser'); 6 | 7 | async function testParser(elmCode, expectedExposedNames) { 8 | const exposed = await Parser.extractExposedPossiblyTests( 9 | 'SomeFile.elm', 10 | (_, options) => { 11 | const readable = stream.Readable.from([elmCode], { 12 | ...options, 13 | autoDestroy: true, 14 | }); 15 | readable.close = readable.destroy; 16 | return readable; 17 | } 18 | ); 19 | assert.deepStrictEqual(exposed, expectedExposedNames); 20 | } 21 | 22 | describe('Parser', () => { 23 | describe('valid Elm code', () => { 24 | it('handles a basic module definition', () => 25 | testParser('module Main exposing (one, two)', ['one', 'two'])); 26 | 27 | it('handles unicode', () => 28 | testParser('module Main exposing (åäö, Åä, π, ᾀ_5Ϡ)', [ 29 | 'åäö', 30 | 'π', 31 | 'ᾀ_5Ϡ', 32 | ])); 33 | 34 | it('handles a module definition with comments', () => 35 | testParser( 36 | ` 37 | module{--}Main {- 38 | {{-}-}- 39 | -}exposing--{- 40 | 41 | ({--}one{--} 42 | , 43 | -- notExport 44 | two{-{-{-{--}-}{--}-}{-{--}-}-},Type{--}({--}..{--}){--} 45 | , three 46 | )-- 47 | `, 48 | ['one', 'two', 'three'] 49 | )); 50 | 51 | it('is not fooled by strings, chars and comments', () => 52 | testParser( 53 | ` 54 | module Main exposing ( ..) 55 | 56 | one="\\"{-" 57 | two="""-} 58 | notAThing = something 59 | \\""" 60 | notAThing2 = something 61 | """ 62 | three = '"' {- " 63 | notAThing3 = something 64 | -} 65 | four{--}=--{- 66 | 1 67 | five = something 68 | --} 69 | 70 | `, 71 | ['one', 'two', 'three', 'four', 'five'] 72 | )); 73 | 74 | it('is not fooled by imports, ports, types and let-in', () => 75 | testParser( 76 | ` 77 | port module Main exposing (..-- 78 | ){- 79 | -} 80 | import Dict exposing (get) 81 | 82 | port sendMessage : String -> Cmd msg 83 | port messageReceiver : (String -> msg) -> Sub msg 84 | 85 | type alias Model = 86 | { one : String 87 | , two : Int } 88 | 89 | init flags = 90 | let 91 | notATest = 1 92 | in 93 | Model "" 0 94 | 95 | type User 96 | = Regular String 97 | | Visitor String 98 | 99 | user 100 | = Regular "Joe" 101 | `, 102 | ['user'] 103 | )); 104 | 105 | it('handles escapes in string literals and char literals', () => 106 | testParser( 107 | ` 108 | module Main exposing ( ..) 109 | 110 | string = "\\n\\r\\t\\"\\'\\\\\\u{00A0}" 111 | 112 | chars = [ '\\n', '\\r', '\\t', '\\"', '\\'', '\\\\', '\\u{00A0}' ] 113 | 114 | test = something 115 | --} 116 | 117 | `, 118 | ['string', 'chars', 'test'] 119 | )); 120 | 121 | it('handles tokens that look like the start of some other token at the end of a line', () => 122 | testParser( 123 | ` 124 | module Main exposing (..) 125 | 126 | testFuzz : Test 127 | testFuzz = 128 | fuzz2 string string "empty list etc" <| 129 | \name punctuation -> 130 | oxfordify "This sentence is empty" "." [] 131 | |> Expect.equal "" 132 | |> Expect.onFail "given an empty list, did not return an empty string" 133 | testRecord = 134 | helper 135 | { 136 | } 137 | testSubtraction = 138 | helper2 <| 139 | 2 - 140 | 1 141 | `, 142 | ['testFuzz', 'testRecord', 'testSubtraction'] 143 | )); 144 | 145 | it('handles a module definition with CRLF', () => 146 | testParser( 147 | `module Main exposing 148 | (one 149 | , two 150 | ) 151 | `.replace(/\n/g, '\r\n'), 152 | ['one', 'two'] 153 | )); 154 | 155 | it('handles finds test in a file with `exposing (..)` and CRLF', () => 156 | testParser( 157 | `module Main exposing (..) 158 | 159 | import Test exposing (Test, test) 160 | 161 | one = 162 | test "one" something 163 | 164 | two : Test 165 | two = 166 | test "two" somethingElse 167 | `.replace(/\n/g, '\r\n'), 168 | ['one', 'two'] 169 | )); 170 | }); 171 | 172 | // Note: It doesn’t matter much what the actual return array looks like. The 173 | // important part is that the function doesn’t crash. It’s still nice to get 174 | // test failures if the output changes, to help evaluate what a change in the 175 | // parser might cause. 176 | describe('invalid Elm code', () => { 177 | it('handles the empty string', () => testParser('', [])); 178 | 179 | it('handles a malformed module declaration', () => 180 | testParser( 181 | ` 182 | module Main 183 | 184 | import X exposing (one, two) 185 | `, 186 | [] 187 | )); 188 | 189 | it('handles lowercase type', () => 190 | testParser('module A.BBB.Circle exposing (one, circle (..))', [])); 191 | 192 | it('handles uppercase declaration', () => 193 | testParser( 194 | ` 195 | module Main exposing (..) 196 | 197 | One = 1 198 | `, 199 | [] 200 | )); 201 | 202 | it('does not treat strings as comments', () => 203 | testParser('module "string" Main exposing (one)', [])); 204 | 205 | it('treats `effect module` as a critical error', () => 206 | assert.rejects( 207 | testParser( 208 | 'effect module Example where { subscription = MySub } exposing (..)', 209 | ['should', 'not', 'succeed'] 210 | ), 211 | { 212 | message: 213 | 'This file is problematic:\n\nSomeFile.elm\n\nIt starts with `effect module`. Effect modules can only exist inside src/ in elm and elm-explorations packages. They cannot contain tests.', 214 | } 215 | )); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /tests/fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/ 2 | /scratch/ 3 | /dummy-bin/different-elm* 4 | -------------------------------------------------------------------------------- /tests/fixtures/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.5" 10 | }, 11 | "indirect": {} 12 | }, 13 | "test-dependencies": { 14 | "direct": { 15 | "elm/regex": "1.0.0", 16 | "elm-explorations/test": "2.0.0" 17 | }, 18 | "indirect": { 19 | "elm/bytes": "1.0.8", 20 | "elm/html": "1.0.0", 21 | "elm/json": "1.1.3", 22 | "elm/random": "1.0.0", 23 | "elm/time": "1.0.0", 24 | "elm/virtual-dom": "1.0.3" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/fixtures/install/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0", 10 | "this-is-not-real-but-we-are-testing-its-existence": "1.0.1" 11 | }, 12 | "indirect": {} 13 | }, 14 | "test-dependencies": { 15 | "direct": { 16 | "elm-explorations/test": "1.2.0" 17 | }, 18 | "indirect": { 19 | "elm/html": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/install/test-elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0", 10 | "this-is-not-real-but-we-are-testing-its-existence": "1.0.1" 11 | }, 12 | "indirect": {} 13 | }, 14 | "test-dependencies": { 15 | "direct": { 16 | "elm-explorations/test": "1.2.0" 17 | }, 18 | "indirect": { 19 | "elm/html": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0" 10 | }, 11 | "indirect": {} 12 | }, 13 | "test-dependencies": { 14 | "elm-explorations/test": "2.0.0 <= v < 3.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/application-with-package-style-test-dependencies/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | Expected test-dependencies->"direct" to be an object, but got: undefined 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/dependency-not-string/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0" 10 | }, 11 | "indirect": {} 12 | }, 13 | "test-dependencies": { 14 | "direct": { 15 | "elm/regex": "1.0.0", 16 | "elm-explorations/test": 1.2 17 | }, 18 | "indirect": { 19 | "elm/html": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/dependency-not-string/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | Expected test-dependencies->"direct"->"elm-explorations/test" to be a string, but got: 1.2 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/elm-test-package-too-old-application/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0" 10 | }, 11 | "indirect": {} 12 | }, 13 | "test-dependencies": { 14 | "direct": { 15 | "elm/regex": "1.0.0", 16 | "elm-explorations/test": "1.2.2" 17 | }, 18 | "indirect": { 19 | "elm/html": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/elm-test-package-too-old-application/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | This version of elm-test only supports elm-explorations/test 2.x, but you have "1.2.2". 3 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/elm-test-package-too-old-package/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "elm-explorations/node-test-runner-example-package", 4 | "summary": "Example package with tests", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.0", 7 | "exposed-modules": [], 8 | "elm-version": "0.19.0 <= v < 0.20.0", 9 | "dependencies": { 10 | "elm/core": "1.0.0 <= v < 2.0.0" 11 | }, 12 | "test-dependencies": { 13 | "elm-explorations/test": "1.2.0 <= v < 2.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/elm-test-package-too-old-package/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | This version of elm-test only supports elm-explorations/test 2.x, but you have "1.2.0 <= v < 2.0.0". 3 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/empty-source-directories/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | ], 5 | "elm-version": "0.19.1", 6 | "dependencies": { 7 | "direct": { 8 | "elm/core": "1.0.0" 9 | }, 10 | "indirect": {} 11 | }, 12 | "test-dependencies": { 13 | "direct": { 14 | "elm/regex": "1.0.0", 15 | "elm-explorations/test": "2.0.0" 16 | }, 17 | "indirect": { 18 | "elm/html": "1.0.0", 19 | "elm/virtual-dom": "1.0.2" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/empty-source-directories/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | Expected "source-directories" to contain at least one item, but it is empty. 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/is-folder/elm.json/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtfeldman/node-test-runner/acbd947da0282ffaaf54bc2146e45c4d20ea6705/tests/fixtures/invalid-elm-json/is-folder/elm.json/.gitkeep -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/is-folder/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | EISDIR: illegal operation on a directory, read 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/is-null/elm.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/is-null/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | Expected the file to be an object, but got: null 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/json-syntax-error/elm.json: -------------------------------------------------------------------------------- 1 | { "type" "application", 2 | "source-directories": [ 3 | "src" 4 | ], 5 | "elm-version": "0.19.1", 6 | "dependencies": { 7 | "direct": { 8 | "elm/core": "1.0.0" 9 | }, 10 | "indirect": {} 11 | }, 12 | "test-dependencies": { 13 | "direct": { 14 | "elm/regex": "1.0.0", 15 | "elm-explorations/test": "2.0.0" 16 | }, 17 | "indirect": { 18 | "elm/html": "1.0.0", 19 | "elm/virtual-dom": "1.0.2" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/json-syntax-error/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | (the JSON parse error) 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/missing-elm-test-package/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0" 10 | }, 11 | "indirect": {} 12 | }, 13 | "test-dependencies": { 14 | "direct": {}, 15 | "indirect": {} 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/missing-elm-test-package/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | You must have "elm-explorations/test" in your "test-dependencies" or "dependencies" to run elm-test. 3 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/null-type/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": null, 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0" 10 | }, 11 | "indirect": {} 12 | }, 13 | "test-dependencies": { 14 | "direct": { 15 | "elm/regex": "1.0.0", 16 | "elm-explorations/test": "2.0.0" 17 | }, 18 | "indirect": { 19 | "elm/html": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/null-type/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | Expected "type" to be "application" or "package", but got: null 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "elm-explorations/node-test-runner-example-package", 4 | "summary": "Example package with tests", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.0", 7 | "exposed-modules": [ 8 | "Something" 9 | ], 10 | "elm-version": "0.19.0 <= v < 0.20.0", 11 | "dependencies": { 12 | "direct": { 13 | "elm/core": "1.0.0" 14 | }, 15 | "indirect": {} 16 | }, 17 | "test-dependencies": { 18 | "elm-explorations/test": "2.0.0 <= v < 3.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/package-with-application-style-dependencies/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | Expected dependencies->"direct" to be a string, but got: {"elm/core":"1.0.0"} 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/source-directories-not-array/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": { 4 | "src": true 5 | }, 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0" 10 | }, 11 | "indirect": {} 12 | }, 13 | "test-dependencies": { 14 | "direct": { 15 | "elm/regex": "1.0.0", 16 | "elm-explorations/test": "2.0.0" 17 | }, 18 | "indirect": { 19 | "elm/html": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/source-directories-not-array/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | Expected "source-directories" to be an array, but got: {"src":true} 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/source-directory-not-string/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": ["src", 1, "app"], 4 | "elm-version": "0.19.1", 5 | "dependencies": { 6 | "direct": { 7 | "elm/core": "1.0.0" 8 | }, 9 | "indirect": {} 10 | }, 11 | "test-dependencies": { 12 | "direct": { 13 | "elm/regex": "1.0.0", 14 | "elm-explorations/test": "2.0.0" 15 | }, 16 | "indirect": { 17 | "elm/html": "1.0.0", 18 | "elm/virtual-dom": "1.0.2" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/source-directory-not-string/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | Expected "source-directories"->1 to be a string, but got: 1 4 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/unknown-type/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.0" 10 | }, 11 | "indirect": {} 12 | }, 13 | "test-dependencies": { 14 | "direct": { 15 | "elm/regex": "1.0.0", 16 | "elm-explorations/test": "2.0.0" 17 | }, 18 | "indirect": { 19 | "elm/html": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/invalid-elm-json/unknown-type/expected.txt: -------------------------------------------------------------------------------- 1 | /full/path/to/elm.json 2 | Failed to read elm.json: 3 | Expected "type" to be "application" or "package", but got: "Application" 4 | -------------------------------------------------------------------------------- /tests/fixtures/src/Port1.elm: -------------------------------------------------------------------------------- 1 | port module Port1 exposing (check) 2 | 3 | 4 | port check : String -> Cmd msg 5 | -------------------------------------------------------------------------------- /tests/fixtures/src/Port2.elm: -------------------------------------------------------------------------------- 1 | port module Port2 exposing (check) 2 | 3 | 4 | port check : String -> Cmd msg 5 | -------------------------------------------------------------------------------- /tests/fixtures/templates/application/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.0", 10 | "elm/core": "1.0.0", 11 | "elm/html": "1.0.0" 12 | }, 13 | "indirect": { 14 | "elm/json": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.2" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": {}, 22 | "indirect": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/templates/package/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "rtfeldman/elm-validate", 4 | "summary": "Validate data", 5 | "license": "BSD-3-Clause", 6 | "version": "4.0.1", 7 | "exposed-modules": [], 8 | "elm-version": "0.19.1 <= v < 0.20.0", 9 | "dependencies": { 10 | "elm/core": "1.0.0 <= v < 2.0.0" 11 | }, 12 | "test-dependencies": {} 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/tests/CompileError/InvalidSyntax.elm: -------------------------------------------------------------------------------- 1 | module CompileError.InvalidSyntax exposing(..) 2 | 3 | {- 4 | -------------------------------------------------------------------------------- /tests/fixtures/tests/CompileError/NoTests.elm: -------------------------------------------------------------------------------- 1 | module CompileError.NoTests exposing (somethingElse) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | somethingElse : Int 8 | somethingElse = 9 | 6 10 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Distribution/Everything.elm: -------------------------------------------------------------------------------- 1 | module Distribution.Everything exposing (test) 2 | 3 | import Distribution.ExpectDistributionFailingDistribution 4 | import Distribution.ExpectDistributionFailingTest 5 | import Distribution.ExpectDistributionPassing 6 | import Distribution.ReportDistributionFailing 7 | import Distribution.ReportDistributionPassing 8 | import Expect 9 | import Fuzz 10 | import Test exposing (Test) 11 | 12 | 13 | test : Test 14 | test = 15 | Test.concat 16 | [ Distribution.ExpectDistributionFailingDistribution.test 17 | , Distribution.ExpectDistributionFailingTest.test 18 | , Distribution.ExpectDistributionPassing.test 19 | , Distribution.ReportDistributionFailing.test 20 | , Distribution.ReportDistributionPassing.test 21 | ] 22 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Distribution/ExpectDistributionFailingDistribution.elm: -------------------------------------------------------------------------------- 1 | module Distribution.ExpectDistributionFailingDistribution exposing (test) 2 | 3 | import Expect 4 | import Fuzz 5 | import Test exposing (Test) 6 | import Test.Distribution 7 | 8 | 9 | test : Test 10 | test = 11 | Test.fuzzWith 12 | { runs = 10000 13 | , distribution = 14 | Test.expectDistribution 15 | [ ( Test.Distribution.atLeast 4, "low", \n -> n == 1 ) 16 | , ( Test.Distribution.atLeast 4, "high", \n -> n == 20 ) 17 | , ( Test.Distribution.atLeast 80, "in between", \n -> n > 1 && n < 20 ) 18 | , ( Test.Distribution.zero, "outside", \n -> n < 1 || n > 20 ) 19 | , ( Test.Distribution.zero, "one", \n -> n == 1 ) 20 | ] 21 | } 22 | (Fuzz.intRange 1 20) 23 | "expectDistribution: failing because of distribution" 24 | (\_ -> Expect.pass) 25 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Distribution/ExpectDistributionFailingTest.elm: -------------------------------------------------------------------------------- 1 | module Distribution.ExpectDistributionFailingTest exposing (test) 2 | 3 | import Expect 4 | import Fuzz 5 | import Test exposing (Test) 6 | import Test.Distribution 7 | 8 | 9 | test : Test 10 | test = 11 | Test.fuzzWith 12 | { runs = 10000 13 | , distribution = 14 | Test.expectDistribution 15 | [ ( Test.Distribution.atLeast 4, "low", \n -> n == 1 ) 16 | , ( Test.Distribution.atLeast 4, "high", \n -> n == 20 ) 17 | , ( Test.Distribution.atLeast 80, "in between", \n -> n > 1 && n < 20 ) 18 | , ( Test.Distribution.zero, "outside", \n -> n < 1 || n > 20 ) 19 | , ( Test.Distribution.moreThanZero, "one", \n -> n == 1 ) 20 | ] 21 | } 22 | (Fuzz.intRange 1 20) 23 | "expectDistribution: failing because of test" 24 | (\_ -> Expect.fail "This test is supposed to fail") 25 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Distribution/ExpectDistributionPassing.elm: -------------------------------------------------------------------------------- 1 | module Distribution.ExpectDistributionPassing exposing (test) 2 | 3 | import Expect 4 | import Fuzz 5 | import Test exposing (Test) 6 | import Test.Distribution 7 | 8 | 9 | test : Test 10 | test = 11 | Test.fuzzWith 12 | { runs = 10000 13 | , distribution = 14 | Test.expectDistribution 15 | [ ( Test.Distribution.atLeast 4, "low", \n -> n == 1 ) 16 | , ( Test.Distribution.atLeast 4, "high", \n -> n == 20 ) 17 | , ( Test.Distribution.atLeast 80, "in between", \n -> n > 1 && n < 20 ) 18 | , ( Test.Distribution.zero, "outside", \n -> n < 1 || n > 20 ) 19 | , ( Test.Distribution.moreThanZero, "one", \n -> n == 1 ) 20 | ] 21 | } 22 | (Fuzz.intRange 1 20) 23 | "expectDistribution: passing" 24 | (\_ -> Expect.pass) 25 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Distribution/ReportDistributionFailing.elm: -------------------------------------------------------------------------------- 1 | module Distribution.ReportDistributionFailing exposing (test) 2 | 3 | import Expect 4 | import Fuzz 5 | import Test exposing (Test) 6 | 7 | 8 | test : Test 9 | test = 10 | Test.fuzzWith 11 | { runs = 10000 12 | , distribution = 13 | Test.reportDistribution 14 | [ ( "low", \n -> n == 1 ) 15 | , ( "high", \n -> n == 20 ) 16 | , ( "in between", \n -> n > 1 && n < 20 ) 17 | , ( "outside", \n -> n < 1 || n > 20 ) 18 | ] 19 | } 20 | (Fuzz.intRange 1 20) 21 | "reportDistribution: failing" 22 | (\_ -> Expect.fail "The test is supposed to fail") 23 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Distribution/ReportDistributionPassing.elm: -------------------------------------------------------------------------------- 1 | module Distribution.ReportDistributionPassing exposing (test) 2 | 3 | import Expect 4 | import Fuzz 5 | import Test exposing (Test) 6 | 7 | 8 | test : Test 9 | test = 10 | Test.fuzzWith 11 | { runs = 10000 12 | , distribution = 13 | Test.reportDistribution 14 | [ ( "low", \n -> n == 1 ) 15 | , ( "high", \n -> n == 20 ) 16 | , ( "in between", \n -> n > 1 && n < 20 ) 17 | , ( "outside", \n -> n < 1 || n > 20 ) 18 | ] 19 | } 20 | (Fuzz.intRange 1 20) 21 | "reportDistribution: passing" 22 | (\_ -> Expect.pass) 23 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Failing/Fuzz.elm: -------------------------------------------------------------------------------- 1 | module Failing.Fuzz exposing (..) 2 | 3 | import Char 4 | import Expect 5 | import Fuzz exposing (..) 6 | import String 7 | import Test exposing (..) 8 | 9 | 10 | withoutNums : String -> String 11 | withoutNums = 12 | String.filter (\ch -> not (Char.isDigit ch || ch == '.')) 13 | 14 | 15 | testWithoutNums : Test 16 | testWithoutNums = 17 | describe "withoutNums" 18 | [ fuzzWith { runs = 100, distribution = Test.noDistribution } (triple string float string) "adding numbers to strings has no effect" <| 19 | \( prefix, num, suffix ) -> 20 | withoutNums (prefix ++ String.fromFloat num ++ suffix) 21 | |> Expect.equal (withoutNums (prefix ++ suffix)) 22 | ] 23 | 24 | 25 | testExpectations : Test 26 | testExpectations = 27 | describe "basic expectations" 28 | [ test "this should succeed" <| 29 | \() -> 30 | "blah" 31 | |> Expect.equal " blah" 32 | , test "this should fail" <| 33 | \() -> 34 | "something" 35 | |> Expect.equal "someting else" 36 | , test "another failure" <| 37 | \() -> 38 | "forty-two" 39 | |> Expect.equal "forty-three" 40 | ] 41 | 42 | 43 | oxfordify : a -> b -> c -> String 44 | oxfordify _ _ _ = 45 | "Alice, Bob, and Claire" 46 | 47 | 48 | testFuzz : Test 49 | testFuzz = 50 | describe "fuzzing" 51 | [ fuzz2 string string "empty list etc" <| 52 | \name punctuation -> 53 | oxfordify "This sentence is empty" "." [] 54 | |> Expect.equal "" 55 | |> Expect.onFail "given an empty list, did not return an empty string" 56 | , fuzz2 string string "further testing" <| 57 | \name punctuation -> 58 | oxfordify "This sentence contains " "." [ "one item" ] 59 | |> Expect.equal "This sentence contains one item." 60 | , fuzz2 string string "custom onFail here" <| 61 | \name punctuation -> 62 | oxfordify "This sentence contains " "." [ "one item", "two item" ] 63 | |> Expect.equal "This sentence contains one item and two item." 64 | |> Expect.onFail "given an empty list, did not return an empty string" 65 | , fuzz2 string string "This is a test." <| 66 | \name punctuation -> 67 | oxfordify "This sentence contains " "." [ "one item", "two item", "three item" ] 68 | |> Expect.equal "This sentence contains one item, two item, and three item." 69 | |> Expect.onFail "given a list of length 3, did not return an oxford-style sentence" 70 | ] 71 | 72 | 73 | testFailingFuzzTests : Test 74 | testFailingFuzzTests = 75 | describe "the first element in this fuzz tuple" 76 | [ fuzz2 string string "is always \"foo\"" <| 77 | \str1 str2 -> 78 | str1 79 | |> Expect.equal "foo" 80 | ] 81 | 82 | 83 | testOxfordify : Test 84 | testOxfordify = 85 | describe "oxfordify" 86 | [ describe "given an empty sentence" 87 | [ test "returns an empty string" <| 88 | \() -> 89 | oxfordify "This sentence is empty" "." [] 90 | |> Expect.equal "" 91 | ] 92 | , describe "given a sentence with one item" 93 | [ test "still contains one item" <| 94 | \() -> 95 | oxfordify "This sentence contains " "." [ "one item" ] 96 | |> Expect.equal "This sentence contains one item." 97 | ] 98 | , describe "given a sentence with multiple items" 99 | [ test "returns an oxford-style sentence" <| 100 | \() -> 101 | oxfordify "This sentence contains " "." [ "one item", "two item" ] 102 | |> Expect.equal "This sentence contains one item and two item." 103 | , test "returns an oxford-style sentence" <| 104 | \() -> 105 | oxfordify "This sentence contains " "." [ "one item", "two item", "three item" ] 106 | |> Expect.equal "This sentence contains one item, two item, and three item." 107 | , test "runs a Debug.todo on purpose" <| 108 | \() -> 109 | oxfordify "Everything is normal" 110 | |> Debug.todo "this test runs a Debug.todo on purpose!" 111 | ] 112 | ] 113 | 114 | 115 | testShrinkables : Test 116 | testShrinkables = 117 | describe "Some tests that should fail and produce shrunken values" 118 | [ describe "a randomly generated integer" 119 | [ fuzz int "is for sure exactly 0" <| Expect.equal 0 120 | , fuzz int "is <42" <| Expect.lessThan 42 121 | , fuzz int "is also >42" <| Expect.greaterThan 42 122 | ] 123 | , describe "a randomly generated string" 124 | [ fuzz string "equals its reverse" <| 125 | \str -> 126 | Expect.equal str (String.reverse str) 127 | ] 128 | ] 129 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Failing/One.elm: -------------------------------------------------------------------------------- 1 | module Failing.One exposing (suite) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | suite : Test 8 | suite = 9 | test "intentional failure" <| 10 | \() -> 11 | Expect.fail "This should fail!" 12 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Failing/OneRuntimeException.elm: -------------------------------------------------------------------------------- 1 | module Failing.OneRuntimeException exposing (suite) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | suite : Test 8 | suite = 9 | test "intentional failure" <| 10 | \() -> 11 | Debug.todo "This is intentionally failing with a runtime exception!" 12 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Failing/OneTodo.elm: -------------------------------------------------------------------------------- 1 | module Failing.OneTodo exposing (..) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | suite : Test 8 | suite = 9 | Test.describe "Todo tests" 10 | [ Test.todo "write a test here" 11 | ] 12 | 13 | 14 | aPassingTest : Test 15 | aPassingTest = 16 | test "this should pass" <| 17 | \() -> 18 | Expect.equal "success" "success" 19 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Failing/Several.elm: -------------------------------------------------------------------------------- 1 | module Failing.Several exposing (..) 2 | 3 | import Char 4 | import Expect 5 | import Fuzz exposing (..) 6 | import String 7 | import Test exposing (..) 8 | 9 | 10 | withoutNums : String -> String 11 | withoutNums = 12 | String.filter (\ch -> not (Char.isDigit ch || ch == '.')) 13 | 14 | 15 | testWithoutNums : Test 16 | testWithoutNums = 17 | describe "withoutNums" 18 | [ fuzzWith { runs = 100, distribution = Test.noDistribution } (triple string float string) "adding numbers to strings has no effect" <| 19 | \( prefix, num, suffix ) -> 20 | withoutNums (prefix ++ String.fromFloat num ++ suffix) 21 | |> Expect.equal (withoutNums (prefix ++ suffix)) 22 | ] 23 | 24 | 25 | testExpectations : Test 26 | testExpectations = 27 | describe "basic expectations" 28 | [ test "this should succeed" <| 29 | \() -> 30 | "blah" 31 | |> Expect.equal " blah" 32 | , test "this should fail" <| 33 | \() -> 34 | "something" 35 | |> Expect.equal "someting else" 36 | , test "another failure" <| 37 | \() -> 38 | "forty-two" 39 | |> Expect.equal "forty-three" 40 | ] 41 | 42 | 43 | testFailingFuzzTests : Test 44 | testFailingFuzzTests = 45 | describe "the first element in this fuzz tuple" 46 | [ fuzz2 string string "is always \"foo\"" <| 47 | \str1 str2 -> 48 | str1 49 | |> Expect.equal "foo" 50 | ] 51 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Failing/SeveralTodos.elm: -------------------------------------------------------------------------------- 1 | module Failing.SeveralTodos exposing (..) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | someTodos : Test 8 | someTodos = 9 | Test.describe "three Todo tests" 10 | [ Test.todo "write a test here" 11 | , Test.todo "write a second test here" 12 | , Test.todo "write a third test here" 13 | ] 14 | 15 | 16 | aPassingTest : Test 17 | aPassingTest = 18 | test "this should pass" <| 19 | \() -> 20 | Expect.equal "success" "success" 21 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Failing/SeveralWithComments.elm: -------------------------------------------------------------------------------- 1 | module Failing.SeveralWithComments exposing (testExpectations, testFailingFuzzTests, {- hello -} testWithoutNums, withoutNums) 2 | 3 | import Char 4 | import Expect 5 | import Fuzz exposing (..) 6 | import String 7 | import Test exposing (..) 8 | 9 | 10 | withoutNums : String -> String 11 | withoutNums = 12 | String.filter (\ch -> not (Char.isDigit ch || ch == '.')) 13 | 14 | 15 | testWithoutNums : Test 16 | testWithoutNums = 17 | describe "withoutNums" 18 | [ fuzzWith { runs = 100, distribution = Test.noDistribution } (triple string float string) "adding numbers to strings has no effect" <| 19 | \( prefix, num, suffix ) -> 20 | withoutNums (prefix ++ String.fromFloat num ++ suffix) 21 | |> Expect.equal (withoutNums (prefix ++ suffix)) 22 | ] 23 | 24 | 25 | testExpectations : Test 26 | testExpectations = 27 | describe "basic expectations" 28 | [ test "this should succeed" <| 29 | \() -> 30 | "blah" 31 | |> Expect.equal " blah" 32 | , test "this should fail" <| 33 | \() -> 34 | "something" 35 | |> Expect.equal "someting else" 36 | , test "another failure" <| 37 | \() -> 38 | "forty-two" 39 | |> Expect.equal "forty-three" 40 | ] 41 | 42 | 43 | testFailingFuzzTests : Test 44 | testFailingFuzzTests = 45 | describe "the first element in this fuzz tuple" 46 | [ fuzz2 string string "is always \"foo\"" <| 47 | \str1 str2 -> 48 | str1 49 | |> Expect.equal "foo" 50 | ] 51 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Failing/SplitSocketMessage.elm: -------------------------------------------------------------------------------- 1 | module Failing.SplitSocketMessage exposing (..) 2 | 3 | import Array 4 | import Expect exposing (Expectation) 5 | import Test exposing (..) 6 | 7 | 8 | 9 | {- This is a regression test. 10 | 11 | For some reason, the failure output of this specific test ends up being read 12 | from the socket in 2 separate chunks. This breaks our assumption and leads to 13 | a crash. 14 | 15 | I'm not sure to what degree this can be reproduced on platforms _other_ than 16 | OSX. 17 | -} 18 | 19 | 20 | suite : Test 21 | suite = 22 | test "scce" <| 23 | \_ -> 24 | Expect.equal 25 | (Array.repeat 1 0) 26 | (Array.repeat 49 0) 27 | -------------------------------------------------------------------------------- /tests/fixtures/tests/InvalidXMLCharacter/Test.elm: -------------------------------------------------------------------------------- 1 | module InvalidXMLCharacter.Test exposing (invalidCharacter) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | invalidCharacter : Test 8 | invalidCharacter = 9 | describe "The junit reporter should not crash due to invalid (for XML) characters in the output" 10 | [ test "backspace: \u{0008}" <| 11 | \() -> Expect.pass 12 | , test "escape: \u{001B}" <| 13 | \() -> Expect.pass 14 | ] 15 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Nested/Module/Test.elm: -------------------------------------------------------------------------------- 1 | module Nested.Module.Test exposing (..) 2 | 3 | import Char 4 | import Expect 5 | import Fuzz exposing (..) 6 | import String 7 | import Test exposing (..) 8 | 9 | 10 | withoutNums : String -> String 11 | withoutNums = 12 | String.filter (\ch -> not (Char.isDigit ch || ch == '.')) 13 | 14 | 15 | testWithoutNums : Test 16 | testWithoutNums = 17 | describe "withoutNums" 18 | [ fuzzWith { runs = 100, distribution = Test.noDistribution } (triple string int string) "adding numbers to strings has no effect" <| 19 | \( prefix, num, suffix ) -> 20 | withoutNums (prefix ++ String.fromInt num ++ suffix) 21 | |> Expect.equal (withoutNums (prefix ++ suffix)) 22 | ] 23 | 24 | 25 | testEqual : Test 26 | testEqual = 27 | test "Expect.equal works" <| 28 | \() -> 29 | 42 30 | |> Expect.equal 42 31 | 32 | 33 | testTrue : Test 34 | testTrue = 35 | test "Expect.equal True works" <| 36 | \() -> 37 | True 38 | |> Expect.equal True 39 | |> Expect.onFail "this should never fail!" 40 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Passing/.gitignore: -------------------------------------------------------------------------------- 1 | Generated.elm 2 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Passing/Dedup/One.elm: -------------------------------------------------------------------------------- 1 | module Passing.Dedup.One exposing (..) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | plainExpectation : Test 8 | plainExpectation = 9 | test "this should pass" <| 10 | \() -> 11 | Expect.equal "success" "success" 12 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Passing/Dependency.elm: -------------------------------------------------------------------------------- 1 | module Passing.Dependency exposing (..) 2 | 3 | import Expect 4 | import Regex 5 | import Test exposing (..) 6 | 7 | 8 | testDependency : Test 9 | testDependency = 10 | describe "tests that use a third-party dependency (but only for the tests!)" 11 | [ test "Regex.replace is available" <| 12 | \() -> 13 | "this totally works" 14 | |> Regex.replace (Regex.fromString "\\w+" |> Maybe.withDefault Regex.never) (always "word") 15 | |> Expect.equal "word word word" 16 | ] 17 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Passing/One.elm: -------------------------------------------------------------------------------- 1 | module Passing.One exposing (..) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | plainExpectation : Test 8 | plainExpectation = 9 | test "this should pass" <| 10 | \() -> 11 | Expect.equal "success" "success" 12 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Passing/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Passing.Ports exposing (..) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | 8 | -- Reasonably common port names: 9 | 10 | 11 | port send : String -> Cmd msg 12 | 13 | 14 | port receive : (String -> msg) -> Sub msg 15 | 16 | 17 | testWithPorts : Test 18 | testWithPorts = 19 | test "test with ports should pass" <| 20 | \() -> 21 | ( "success", ( send "out", receive always ) ) 22 | |> Tuple.first 23 | |> Expect.equal "success" 24 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Passing/Several.elm: -------------------------------------------------------------------------------- 1 | module Passing.Several exposing (..) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | testEqual : Test 8 | testEqual = 9 | test "Expect.equal works" <| 10 | \() -> 11 | "success" 12 | |> Expect.equal "success" 13 | 14 | 15 | testTrue : Test 16 | testTrue = 17 | test "Expect.equal True works" <| 18 | \() -> 19 | True 20 | |> Expect.equal True 21 | |> Expect.onFail "this should never fail!" 22 | 23 | 24 | testFalse : Test 25 | testFalse = 26 | test "Expect.equal False works" <| 27 | \() -> 28 | False 29 | |> Expect.equal False 30 | |> Expect.onFail "this should never fail!" 31 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Passing/TrickyMultilines.elm: -------------------------------------------------------------------------------- 1 | module Passing.TrickyMultilines exposing (..) 2 | 3 | import Expect 4 | import Test exposing (Test, test) 5 | 6 | 7 | {-| This _looks_ like the code that we replace in Generate.js, 8 | but it shouldn’t be touched. It won’t be because the compiled JS 9 | uses a single-line strings with `\n` escapes. 10 | 11 | notATest = testTrue 12 | 13 | -} 14 | code : String 15 | code = 16 | """ 17 | var $author$project$Test$Runner$Node$check = $author$project$Test$Runner$Node$checkHelperReplaceMe___; 18 | 19 | var $elm_explorations$test$Test$Internal$Batch = function (a) { 20 | return {$: 'Batch', a: a}; 21 | }; 22 | """ 23 | -- Remove CR on Windows to match manually escaped newlines in the 24 | -- testCode expectation below. 25 | |> String.replace "\u{000D}" "" 26 | 27 | 28 | testCode : Test 29 | testCode = 30 | test "Multiline string is not changed by mistake" <| 31 | \() -> 32 | code 33 | |> Expect.equal 34 | "\nvar $author$project$Test$Runner$Node$check = $author$project$Test$Runner$Node$checkHelperReplaceMe___;\n\nvar $elm_explorations$test$Test$Internal$Batch = function (a) {\n return {$: 'Batch', a: a};\n};\n" 35 | -------------------------------------------------------------------------------- /tests/fixtures/tests/Passing/Unexposed.elm: -------------------------------------------------------------------------------- 1 | module Passing.Unexposed exposing (all) 2 | 3 | {- Ideally we would detect unexposed tests and warn users about them. In elm 0.18.0 we did this, but 4 | internal compiler changes mean we no longer can. This test passes if unexposed tests are ignored to 5 | keep the behavior consistent. Ideas about how we can detect unexposed tests (and thus cause this 6 | test to fail) are very welcome! 7 | 8 | See https://github.com/rtfeldman/node-test-runner/pull/425#issuecomment-637028958 (and following 9 | comments) for more info. 10 | -} 11 | 12 | import Expect 13 | import Test exposing (..) 14 | 15 | 16 | all : Test 17 | all = 18 | describe "all" 19 | [ test "Expect.equal works" <| 20 | \() -> 21 | "success" 22 | |> Expect.equal "success" 23 | , testTrue 24 | ] 25 | 26 | 27 | testTrue : Test 28 | testTrue = 29 | test "Expect.equal True works" <| 30 | \() -> 31 | True 32 | |> Expect.equal True 33 | |> Expect.onFail "this should never fail!" 34 | 35 | 36 | testUnexposed : Test 37 | testUnexposed = 38 | test "This test is unexposed and will not run!" <| 39 | \() -> 40 | Expect.fail "This should fail if run!" 41 | -------------------------------------------------------------------------------- /tests/fixtures/tests/RuntimeException/OnePort.elm: -------------------------------------------------------------------------------- 1 | module RuntimeException.OnePort exposing (testRuntimeException) 2 | 3 | import Expect 4 | import Port1 5 | import Port2 6 | import Test exposing (..) 7 | 8 | 9 | testRuntimeException : Test 10 | testRuntimeException = 11 | test "This should error because the module imports two ports with the same name." <| 12 | \() -> 13 | -- To induce a crash, we need to reference Port1.check and Port2.check. 14 | -- Otherwise they will get DCE'd and there won't be a runtime exception! 15 | [ Port1.check "foo", Port2.check "bar" ] 16 | |> List.drop 2 17 | |> List.length 18 | |> Expect.equal 1234 19 | -------------------------------------------------------------------------------- /tests/fixtures/write-elm-json/.gitignore: -------------------------------------------------------------------------------- 1 | elm.json 2 | -------------------------------------------------------------------------------- /tests/fixtures/write-elm-json/elm.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "nonStandardFieldStart": 1, 3 | "type": "application", 4 | "source-directories": [ "src" ], 5 | "elm-version": "0.19.1", 6 | "dependencies": { 7 | "direct": { "elm/core": "1.0.0" }, 8 | "indirect": {} 9 | }, 10 | "nonStandardFieldMiddle": [1, 2, 3], 11 | "test-dependencies": { 12 | "direct": { 13 | "elm/regex": "1.0.0", 14 | "elm-explorations/test": "2.0.0" 15 | }, 16 | "indirect": { 17 | "elm/html": "1.0.0", 18 | "elm/virtual-dom": "1.0.2" 19 | } 20 | }, 21 | "nonStandardFieldEnd": { 22 | "a": 1, 23 | "b": 2 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/init-test/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.0.0", 3 | "summary": "Run elm-test suites in a Node.js CLI. Get it with npm install -g elm-test", 4 | "repository": "https://github.com/elm-community/elm-test.git", 5 | "license": "BSD-3-Clause", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [], 10 | "native-modules": true, 11 | "dependencies": { 12 | "elm-community/list-extra": "6.0.0 <= v < 7.0.0", 13 | "elm-lang/core": "5.0.0 <= v < 6.0.0" 14 | }, 15 | "elm-version": "0.19.1 <= v < 0.20.0" 16 | } 17 | -------------------------------------------------------------------------------- /tests/init-test/sources/elm/MagicConstant.elm_todo_rename_to_dot_elm: -------------------------------------------------------------------------------- 1 | module MagicConstant exposing (..) 2 | 3 | 4 | magicConstant : Int 5 | magicConstant = 6 | 42 7 | -------------------------------------------------------------------------------- /tests/test-quality.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const path = require('path'); 5 | const { globSync } = require('tinyglobby'); 6 | const spawn = require('cross-spawn'); 7 | 8 | const { spawnOpts } = require('./util'); 9 | 10 | function execElmJson(args, cwd) { 11 | return spawn.sync( 12 | 'elm-json', 13 | args, 14 | Object.assign({ encoding: 'utf-8', cwd: cwd }, spawnOpts) 15 | ); 16 | } 17 | 18 | function execElm(args, cwd) { 19 | return spawn.sync( 20 | 'elm', 21 | args, 22 | Object.assign({ encoding: 'utf-8', cwd: cwd }, spawnOpts) 23 | ); 24 | } 25 | 26 | let examples = globSync(path.join(__dirname, '..', 'example*')); 27 | 28 | describe('examples quality', () => { 29 | describe('Each example has valid json', () => { 30 | for (const example of examples) { 31 | it(`${path.basename(example)}`, () => { 32 | assert.strictEqual(execElmJson(['tree'], example).status, 0); 33 | if (require(path.join(example, 'elm.json')).type === 'package') { 34 | assert.strictEqual(execElm(['make'], example).status, 0); 35 | } 36 | }).timeout(5000); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/util.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const fixturesDir = path.join(__dirname, 'fixtures'); 4 | 5 | const dummyBinPath = path.join(fixturesDir, 'dummy-bin'); 6 | const newPath = process.env.PATH + path.delimiter + dummyBinPath; 7 | const spawnOpts = { 8 | silent: true, 9 | env: Object.assign({}, process.env, { 10 | PATH: newPath, 11 | Path: newPath, 12 | }), 13 | }; 14 | 15 | module.exports = { 16 | fixturesDir, 17 | spawnOpts, 18 | dummyBinPath, 19 | }; 20 | --------------------------------------------------------------------------------