├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── bin └── index.html ├── defines.json ├── doc ├── ImportUTest.hx ├── build_doc.hxml └── doc.hxml ├── extraParams.hxml ├── haxelib.json ├── hxml ├── common.hxml ├── js-minified.hxml └── test-all-linux.hxml ├── meta.json ├── src └── utest │ ├── Assert.hx │ ├── Assertation.hx │ ├── Async.hx │ ├── Dispatcher.hx │ ├── ITest.hx │ ├── IgnoredFixture.hx │ ├── MacroRunner.hx │ ├── Runner.hx │ ├── Test.hx │ ├── TestData.hx │ ├── TestFixture.hx │ ├── TestHandler.hx │ ├── TestResult.hx │ ├── UTest.hx │ ├── exceptions │ ├── AssertFailureException.hx │ └── UTestException.hx │ ├── ui │ ├── Report.hx │ ├── common │ │ ├── ClassResult.hx │ │ ├── FixtureResult.hx │ │ ├── HeaderDisplayMode.hx │ │ ├── IReport.hx │ │ ├── PackageResult.hx │ │ ├── ReportTools.hx │ │ ├── ResultAggregator.hx │ │ └── ResultStats.hx │ ├── macro │ │ └── MacroReport.hx │ └── text │ │ ├── DiagnosticsReport.hx │ │ ├── HtmlReport.hx │ │ ├── PlainTextReport.hx │ │ ├── PrintReport.hx │ │ └── TeamcityReport.hx │ └── utils │ ├── AccessoriesUtils.hx │ ├── AsyncUtils.hx │ ├── Macro.hx │ ├── Print.hx │ └── TestBuilder.hx ├── submit.sh ├── test ├── TestAll.hx ├── js-minified-header.js └── utest │ ├── TestAssert.hx │ ├── TestAsync.hx │ ├── TestAsyncITest.hx │ ├── TestCaseDependencies.hx │ ├── TestDependencies.hx │ ├── TestDispatcher.hx │ ├── TestIgnored.hx │ ├── TestRunner.hx │ ├── TestSpec.hx │ ├── TestSyncITest.hx │ ├── TestWithMacro.hx │ └── testrunner │ ├── TestDummy1.hx │ ├── TestDummy2.hx │ ├── UnusualTestDummy.hx │ └── sub │ └── TestDummy3.hx └── tests.hxml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*.hx] 3 | indent_style = space 4 | end_of_line = lf 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions 2 | name: Build 3 | 4 | on: 5 | push: 6 | paths-ignore: 7 | - '**/*.md' 8 | pull_request: 9 | workflow_dispatch: 10 | # https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/ 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: 19 | - ubuntu-latest 20 | - macos-latest 21 | - windows-latest 22 | haxe: 23 | - latest 24 | - 4.3.3 25 | - 4.2.5 26 | - 4.1.5 27 | 28 | steps: 29 | - name: Show environment variables 30 | shell: bash 31 | run: env | sort 32 | 33 | - name: Git Checkout 34 | uses: actions/checkout@v2 #https://github.com/actions/checkout 35 | 36 | - name: "Cache Haxelib Repository" 37 | uses: actions/cache@v2 38 | with: 39 | path: $RUNNER_TOOL_CACHE/haxe/${{ matrix.haxe }}/x64/lib 40 | key: ${{ runner.os }}-haxelib-${{ hashFiles('**/haxelib.json') }} 41 | restore-keys: | 42 | ${{ runner.os }}-haxelib- 43 | 44 | - name: Upgrade brew 45 | if: runner.os == 'macOS' 46 | env: 47 | # https://docs.brew.sh/Manpage#environment 48 | HOMEBREW_NO_ANALYTICS: 1 49 | HOMEBREW_NO_INSTALL_CLEANUP: 1 50 | run: | 51 | echo "::group::brew update" && brew update && echo "::endgroup::" 52 | echo "::group::brew config" && brew config && echo "::endgroup::" 53 | 54 | # workaround to prevent "/usr/local/... is not inside a keg" during "brew install mono" 55 | rm /usr/local/bin/2to3 56 | rm /usr/local/share/man/man1/* 57 | rm /usr/local/share/man/man5/* 58 | 59 | - name: Set up Python 3 60 | uses: actions/setup-python@v2 # https://github.com/actions/setup-python 61 | with: 62 | python-version: 3.8 63 | 64 | - name: Install Haxe ${{ matrix.haxe }} 65 | uses: krdlab/setup-haxe@v1 # https://github.com/krdlab/setup-haxe 66 | with: 67 | haxe-version: ${{ matrix.haxe }} 68 | 69 | - name: Install haxe libs 70 | shell: bash 71 | id: prerequisites 72 | run: | 73 | haxelib install travix 74 | haxelib run travix install 75 | 76 | 77 | ################################################## 78 | # Tests 79 | ################################################## 80 | - name: Test [interp] 81 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 82 | run: haxelib run travix interp 83 | - name: Test [neko] 84 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 85 | run: haxelib run travix neko 86 | - name: Test [python] 87 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 88 | run: haxelib run travix python 89 | - name: Test [node] 90 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 91 | run: haxelib run travix node 92 | - name: Test [node advanced minification] 93 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 94 | run: | 95 | haxelib install closure --quiet 96 | haxe hxml/js-minified.hxml --js bin/test.js 97 | node bin/test.js 98 | # - name: Test [js] 99 | # if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 100 | # run: haxelib run travix js 101 | # - name: Test [flash] 102 | # if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 103 | # run: haxelib run travix flash 104 | - name: Test [java] 105 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 106 | run: haxelib run travix java 107 | - name: Test [cpp] 108 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 109 | run: haxelib run travix cpp 110 | - name: Test [cs] 111 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 112 | run: haxelib run travix cs 113 | - name: Test [php] 114 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' && matrix.haxe != '4.1.5' }} 115 | #disabled php tests for Haxe 4.1 because it's not fully compatible with php 8 116 | run: | 117 | # haxelib run travix php 118 | haxe tests.hxml --php bin/php 119 | php bin/php/index.php 120 | - name: Test [lua] 121 | #see https://github.com/back2dos/travix/pull/146 122 | if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' && matrix.haxe != '4.3.3' && matrix.haxe != 'latest' && matrix.os == 'ubuntu-latest' }} 123 | run: haxelib run travix lua 124 | # - name: Test [hashlink] 125 | # if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} 126 | # run: haxelib run travix hl 127 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | !bin/index.html 3 | doc/pages 4 | doc/xml 5 | doc/dox 6 | .DS_Store 7 | *.sublime-* 8 | display.hxml 9 | .vscode 10 | utest.zip 11 | .idea 12 | .haxelib 13 | dump/* 14 | .unittest -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.0.0 2 | - Minimum supported Haxe version is 4.1 now. 3 | - added `Assert.similar()` (see the doc to that method) 4 | - changed various `Assert` methods to be type safe instead of accepting `Dynamic` arguments 5 | - added `Assert.raisesCondition` 6 | - added UTEST_IGNORE_DEPENDS flag to ignore failed or missing dependencies (see README.md) 7 | - added test case name filter to `Runner.addCases` 8 | - support @:ignore meta (old @Ignored meta is still supported too) 9 | - support advanced minification for JS target 10 | - provided defines.json to get UTest compilation flags descriptions with `haxe -lib utest --help-user-defines` (requires Haxe 4.3) 11 | - provided meta.json to get UTest metadata list with `haxe -lib utest --help-user-metas` (requires Haxe 4.3) 12 | 13 | # v1.13.2 14 | - Haxe 4.2.0 compatibility 15 | - Diagnostics reporter to show clickable messages in VSCode ([#106](https://github.com/haxe-utest/utest/issues/106) 16 | - Better position reporting upon `async.done()` calls for branched async methods 17 | - Exit code for Adobe AIR application in PlainTextReport ([#108](https://github.com/haxe-utest/utest/issues/108) 18 | 19 | # v1.13.1 20 | - improved failure messages for `haxe.io.Bytes` checks 21 | - don't run test classes if their dependencies were not run because of their own dependencies failures 22 | - fixed error reporting upon inter-class dependency resolution failures 23 | 24 | # v1.13.0 25 | - added support for `@:depends(some.pack.AnotherTestCase)` to define dependencies among test cases 26 | 27 | # v1.12.1 28 | - fixed compatibility with Haxe 3.4 and 4.0 ([#104](https://github.com/haxe-utest/utest/issues/104) 29 | 30 | # v1.12.0 31 | - added support for `@:depends(testName1, testName2)` to define test dependencies ([#25](https://github.com/haxe-utest/utest/issues/25) 32 | 33 | # v1.11.0 34 | - `utest.Assert` methods return true on success and false on fail. 35 | - 36 | # v1.10.5 37 | - `utest.Assert.is` is deprecated. `Use utest.Assert.isOfType` instead. 38 | 39 | # v1.10.4 40 | - Fixed execution of `callback` parameter in `UTest.run(cases, calback)` ([#100](https://github.com/haxe-utest/utest/issues/100) 41 | - Use `Std.isOfType` instead of deprecated `Std.is` when compiled with Haxe 4.1+ 42 | 43 | # v1.10.3 44 | - Fixed compatibility with Haxe 3.4 45 | 46 | # v1.10.2 47 | - Get rid of deprecation messages about `untyped __js__` with Haxe 4.1.0 48 | - Accept `UTEST_PRINT_TESTS` and `UTEST_FAILURE_THROW` via env vars at compile time ([#97](https://github.com/haxe-utest/utest/issues/97)) 49 | 50 | # v.1.10.1 51 | - java: make `async.branch(a -> ...)` more stable for immediate `a.done()` 52 | 53 | # v.1.10.0 54 | - `async.branch()` to create multiple branches of asynchronous tests ([#94](https://github.com/haxe-utest/utest/issues/94)) 55 | - `UTEST_PATTERN` and `Runner.globalPattern` also check test class name now ([#93](https://github.com/haxe-utest/utest/issues/93)) 56 | - `-D UTEST_PRINT_TESTS` to print test names in the process of execution ([#95](https://github.com/haxe-utest/utest/issues/95)) 57 | - `-D UTEST_FAILURE_THROW` to throw an unhandled exceptions on failed assertions instead of collecting them for a report ([#84](https://github.com/haxe-utest/utest/issues/84)) 58 | - Added a compile-time error if a package passed to `runner.addCases(pack)` does not exist ([#73](https://github.com/haxe-utest/utest/issues/73)) 59 | - Fixed compatibility with Haxe 3 (was broken since 1.9.6) 60 | 61 | # v.1.9.6 62 | - Better failure messages for collections ([#81](https://github.com/haxe-utest/utest/issues/81)) 63 | - Fixed for as3 target ([#78](https://github.com/haxe-utest/utest/pull/78)) 64 | - Fixed test app shutdown before all tests are finished in some rare case on Java (see https://github.com/HaxeFoundation/haxe/issues/8131) 65 | 66 | # v.1.9.5 67 | - Fixed UTest trying to execute static method whose name starts with 'test' ([#71](https://github.com/haxe-utest/utest/issues/71)) 68 | 69 | # v.1.9.4 70 | - Fixed signature of UTest.run(), which led to variance problems when the array isn't declared directly against the call argument. ([#70](https://github.com/haxe-utest/utest/pull/70)) 71 | 72 | # v.1.9.3 73 | - Added `utest.Async.setTimeout(ms)` to change async test timeout from within the test code ([#67](https://github.com/haxe-utest/utest/pull/67)) 74 | - Added `@:timeout(999)` support at class level to change default timeout for all tests in a class ([#67](https://github.com/haxe-utest/utest/pull/67)) 75 | 76 | # v.1.9.2 77 | - Fixed `ITest requires __initializeUtest__` error for test cases with macros 78 | 79 | # v.1.9.1 80 | - Fixed compatibility with `-dce full` flag ([#62](https://github.com/haxe-utest/utest/pull/62)) 81 | 82 | # v.1.9.0 83 | - Introduced `utest.ITest` and `utest.Test`. Test cases should implement or extend them. See README.md for details. 84 | - Implemented `.setupClass()`/`.teardownClass()` to setup once before the first test in a test case and teardown once after the last one. 85 | - Added a deprecation notice for test cases which don't implement `utest.ITest`. 86 | - Use the compile-time environment variable or the compiler define "UTEST_PATTERN" to skip tests, which don't match its value. 87 | - Add a failure to the result if no tests were executed. 88 | 89 | # v.1.8.4 90 | - Fixed exit code value for `--interp` target. 91 | 92 | # v.1.8.3 93 | - Avoid recursion while running synchronous tests (could deplete stack on big test suites) 94 | 95 | # v.1.8.2 96 | - Fixed waiting for completion of asynchronous tests on php, neko, python, java, lua 97 | - Check for phantomjs before nodejs on shutting down the tests ([#55](https://github.com/haxe-utest/utest/issues/55)) 98 | 99 | # v.1.8.1 100 | - Fixed "duplicated fixture" error caused by other utest bugs ([#52](https://github.com/haxe-utest/utest/issues/52)) 101 | - Fixed HtmlReporter exception if a script tag is added to a head tag ([#54](https://github.com/haxe-utest/utest/issues/54)) 102 | 103 | # v1.8.0 104 | - Added `Runner.addCases(my.pack)`, which adds all test cases located in `my.pack` package. 105 | - Reverted async tests for Haxe < 3.4.0 (https://github.com/haxe-utest/utest/pull/39#issuecomment-353660192) 106 | 107 | # v1.7.2 108 | - Fix broken compilation for js target (Caused by `@Ignored` functionality in `HtmlReport`). 109 | 110 | # v1.7.1 111 | - Fix exiting from tests in case TeamcityReport. 112 | - Add functionality of ignoring tests withing `@Ignored` meta. 113 | - Force `Runner#globalPattern` overriding withing `pattern` argument from `Runner#addCase` (https://github.com/fponticelli/utest/issues/42) 114 | 115 | # v1.7.0 116 | - Fix Assert.raises for unspecified exception types 117 | - Add PlainTextReport#getTime implementation for C# 118 | - Fix null pointer on clear `PlainTextReport.hx` reported. 119 | - Add Teamcity reporter (enables with flag `-Dteamcity`). 120 | - Enabled async tests for all platforms 121 | 122 | # v1.6.0 123 | - HL support fixed 124 | - Compatibility with Lua target. 125 | 126 | # v1.5.0 127 | - Added async setup/teardown handling 128 | - Add executing of java tests synchronously 129 | - Add C++ pointers comparison 130 | 131 | # v1.4.0 132 | - Initial support for phantomjs. 133 | - Added handlers to catch test start/complete. 134 | - Assert.same supports an optional parameter to set float precision comparison. 135 | - Added `globalPattern` to filter only the desired results. 136 | - Fixes. 137 | 138 | # v1.3.10 139 | - Fixed Assert.raises. 140 | 141 | # v1.3.9 142 | - Fixed issue with PHP. 143 | 144 | # v1.3.8 145 | - Minor fix for HTML output. 146 | 147 | # v1.3.7 148 | - Improved Java experience and simplified API. 149 | 150 | # v1.3.6 151 | - Message improvements and fixed recursion issue with Python. 152 | 153 | # v1.3.5 154 | - Minor message improvement. 155 | 156 | # v1.3.4 157 | - Added `Assert.pass()` (thanks Andy White) and other minor improvements. 158 | 159 | # v1.3.3 160 | - Added support for phantomjs 161 | 162 | # v1.3.2 163 | - Future proof IMap check. 164 | 165 | # v1.3.1 166 | - Fixed issues with Map/IMap in Assert.same() 167 | 168 | # v1.3.0 169 | - library is now Travis/travis-hx friendly 170 | 171 | # v1.2.0 172 | - added async tests to Java 173 | 174 | # v1.1.3 175 | - minor improvements 176 | 177 | # v1.1.2 178 | - Haxe3 release 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | utest 2 | --- 3 | 4 | utest is an easy to use unit testing library for Haxe. It works on all the supported platforms including nodejs. 5 | 6 | - [utest](#utest) 7 | - [Installation](#installation) 8 | - [Basic usage](#basic-usage) 9 | - [Inter-test dependencies](#inter-test-dependencies) 10 | - [Dependencies between test classes](#dependencies-between-test-classes) 11 | - [Running single test from a test suite.](#running-single-test-from-a-test-suite) 12 | - [Async tests](#async-tests) 13 | - [Print test names being executed](#print-test-names-being-executed) 14 | - [Convert failures into exceptions](#convert-failures-into-exceptions) 15 | - [Assert](#assert) 16 | - [Ignoring tests](#ignoring-tests) 17 | 18 | ## Installation 19 | 20 | Install is as easy as: 21 | 22 | ```bash 23 | haxelib install utest 24 | ``` 25 | 26 | ## Basic usage 27 | 28 | In your main method define the minimal instances needed to run your tests. 29 | 30 | ```haxe 31 | import utest.Runner; 32 | import utest.ui.Report; 33 | 34 | class TestAll { 35 | public static function main() { 36 | //the long way 37 | var runner = new Runner(); 38 | runner.addCase(new TestCase1()); 39 | runner.addCase(new TestCase2()); 40 | Report.create(runner); 41 | runner.run(); 42 | 43 | //the short way in case you don't need to handle any specifics 44 | utest.UTest.run([new TestCase1(), new TestCase2()]); 45 | } 46 | } 47 | ``` 48 | 49 | `TestCase` must extend `utest.Test` or implement `utest.ITest`. 50 | 51 | `TestCase` needs to follow some conventions: 52 | 53 | * Every test case method name must be prefixed with `test` or `spec`; 54 | * If a method is prefixed with `spec` it is treated as the specification test. Every boolean binary operation will be wrapped in `Assert.isTrue()` 55 | 56 | Following methods could be implemented to setup or teardown: 57 | ```haxe 58 | /** 59 | * This method is executed once before running the first test in the current class. 60 | * If it accepts an argument, it is treated as an asynchronous method. 61 | */ 62 | function setupClass():Void; 63 | function setupClass(async:Async):Void; 64 | /** 65 | * This method is executed before each test. 66 | * If it accepts an argument, it is treated as an asynchronous method. 67 | */ 68 | function setup():Void; 69 | function setup(async:Async):Void; 70 | /** 71 | * This method is executed after each test. 72 | * If it accepts an argument, it is treated as an asynchronous method. 73 | */ 74 | function teardown():Void; 75 | function teardown(async:Async):Void; 76 | /** 77 | * This method is executed once after the last test in the current class is finished. 78 | * If it accepts an argument, it is treated as an asynchronous method. 79 | */ 80 | function teardownClass():Void; 81 | function teardownClass(async:Async):Void; 82 | ``` 83 | 84 | Default timeout for asynchronous methods is 250ms. You can change it by adding `@:timeout(500)` meta. 85 | 86 | To add all test cases from `my.pack` package use `runner.addCases(my.pack)`. Any module found in `my.pack` is treated as a test case. That means each module should contain a class implementing `utest.ITest` and that class should have the same name as the module name. 87 | 88 | ```haxe 89 | import utest.Assert; 90 | import utest.Async; 91 | 92 | class TestCase extends utest.Test { 93 | var field:String; 94 | 95 | //synchronous setup 96 | public function setup() { 97 | field = "some"; 98 | } 99 | 100 | function testFieldIsSome() { 101 | Assert.equals("some", field); 102 | } 103 | 104 | function specField() { 105 | field.charAt(0) == 's'; 106 | field.length > 3; 107 | } 108 | 109 | //asynchronous teardown 110 | @:timeout(700) //default timeout is 250ms 111 | public function teardown(async:Async) { 112 | field = null; // not really needed 113 | 114 | //simulate asynchronous teardown 115 | haxe.Timer.delay( 116 | function() { 117 | //resolve asynchronous action 118 | async.done(); 119 | }, 120 | 500 121 | ); 122 | } 123 | } 124 | ``` 125 | 126 | ## Ignore a test 127 | 128 | To skip certain test or test case one can annotate them with `@:ignore` meta. The meta accpets an optional string argument to indicate the reason ignoring the test. 129 | ```haxe 130 | class TestCase1 extends utest.Test { 131 | @:ignore 132 | function testSomethingNotReadyYet() { 133 | //this test will not be executed 134 | } 135 | } 136 | 137 | @:ignore('The functionality under test is not ready yet') 138 | class TestCase2 extends utest.Test { 139 | function testSomething { 140 | //this test will not be executed 141 | } 142 | 143 | function testOtherThing { 144 | //this test will not be executed 145 | } 146 | } 147 | ``` 148 | 149 | 150 | 151 | ## Inter-test dependencies 152 | 153 | It is possible to define how tests depend on each other with `@:depends` meta: 154 | ```haxe 155 | class TestCase extends utest.Test { 156 | 157 | function testBasicThing1() { 158 | //... 159 | } 160 | 161 | function testBasicThing2() { 162 | //... 163 | } 164 | 165 | 166 | @:depends(testBasicThing, testBasicThing2) 167 | function testComplexThing() { 168 | //... 169 | } 170 | } 171 | ``` 172 | In this example `testComplexThing` will be executed only if `testBasicThing1` and `testBasicThing2` pass. 173 | 174 | ## Dependencies between test classes 175 | 176 | `@:depends` meta could also be used to define dependencies of one class with tests on other classes with tests. 177 | ```haxe 178 | package some.pack; 179 | 180 | class TestCase1 extends utest.Test { 181 | function test1() { 182 | //... 183 | } 184 | } 185 | 186 | @:depends(some.pack.TestCase1) 187 | class TestCase2 extends utest.Test { 188 | function test2() { 189 | //... 190 | } 191 | } 192 | ``` 193 | In this example tests from `some.pack.TestCase2` will be executed only if there were no failures in `some.pack.TestCase1`. 194 | 195 | ## Ignore dependencies 196 | 197 | If for some reason you want to run a test regardless the outcome of the tests it depends on, you can use `-D UTEST_IGNORE_DEPENDS` define or set an environment variable with the same name. 198 | 199 | ## Running single test from a test suite. 200 | 201 | Adding `-D UTEST_PATTERN=pattern` to the compilation flags makes UTest to run only tests which have names matching the `pattern`. The pattern could be a plain string or a regular expression without delimiters. 202 | 203 | Another option is to add `UTEST_PATTERN` to the environment variables at compile time. 204 | 205 | ## Async tests 206 | 207 | If a test case accepts an argument, that test case is treated as an asynchronous test. 208 | 209 | ```haxe 210 | @:timeout(500) //change timeout (default: 250ms) 211 | function testSomething(async:utest.Async) { 212 | // do your async goodness and remember to call `done()` at the end. 213 | haxe.Timer.delay(function() { 214 | Assert.isTrue(true); // put a sensible test here 215 | async.done(); 216 | }, 50); 217 | } 218 | ``` 219 | 220 | It's also possible to "branch" asynchronous tests. In this case a test will be considered completed when all branches are finished. 221 | 222 | ```haxe 223 | function testSomething(async:utest.Async) { 224 | var branch = async.branch(); 225 | haxe.Timer.delay(function() { 226 | Assert.isTrue(true); // put a sensible test here 227 | branch.done(); 228 | }, 50); 229 | 230 | // or create an asynchronous branch with a callback 231 | async.branch(function(branch) { 232 | haxe.Timer.delay(function() { 233 | Assert.isTrue(true); // put a sensible test here 234 | branch.done(); 235 | }, 50); 236 | }); 237 | } 238 | ``` 239 | 240 | ## Print test names being executed 241 | 242 | `-D UTEST_PRINT_TESTS` makes UTest print test names in the process of tests execution. 243 | The output will look like this: 244 | ``` 245 | Running my.tests.TestAsync... 246 | testSetTimeout 247 | testTimeout 248 | Running my.tests.TestAnother... 249 | testThis 250 | testThat 251 | ``` 252 | And after finishing all the tests UTest will print usual report. 253 | 254 | Another option is to add `UTEST_PRINT_TESTS` to the environment variables at compile time. 255 | 256 | ## Convert failures into exceptions 257 | 258 | It is possible to make UTest throw an unhandled exception instead of adding a failure to the report. 259 | 260 | Enable this behavior with `-D UTEST_FAILURE_THROW`, or by adding `UTEST_FAILURE_THROW` to the environment variables at compile time. 261 | 262 | In this case any exception or failure in test or setup methods will lead to a crash. 263 | Instead of a test report you will see an unhandled exception message with the exception 264 | stack trace (depending on a target platform). 265 | 266 | ## Assert 267 | 268 | [Assert](src/utest/Assert.hx) contains a plethora of methods to perform your tests: 269 | 270 | > *`isTrue(cond:Bool, ?msg:String, ?pos:PosInfos)`* 271 | 272 | Asserts successfully when the condition is true. 273 | 274 | `cond` The condition to test 275 | 276 | `msg` An optional error message. If not passed a default one will be used 277 | 278 | `pos` Code position where the Assert call has been executed. Don't fill it 279 | unless you know what you are doing. 280 | 281 | > *`isFalse(value:Bool, ?msg:String, ?pos:PosInfos)`* 282 | 283 | Asserts successfully when the condition is false. 284 | 285 | `cond` The condition to test 286 | 287 | `msg` An optional error message. If not passed a default one will be used 288 | 289 | `pos` Code position where the Assert call has been executed. Don't fill it 290 | unless you know what you are doing. 291 | 292 | > *`isNull(value:Dynamic, ?msg:String, ?pos:PosInfos)`* 293 | 294 | Asserts successfully when the value is null. 295 | 296 | `value` The value to test 297 | 298 | `msg` An optional error message. If not passed a default one will be used 299 | 300 | `pos` Code position where the Assert call has been executed. Don't fill it 301 | unless you know what you are doing. 302 | 303 | > *`notNull(value:Dynamic, ?msg:String, ?pos:PosInfos)`* 304 | 305 | Asserts successfully when the value is not null. 306 | 307 | `value` The value to test 308 | 309 | `msg` An optional error message. If not passed a default one will be used 310 | 311 | `pos` Code position where the Assert call has been executed. Don't fill it 312 | unless you know what you are doing. 313 | 314 | > *`is(value:Dynamic, type:Dynamic, ?msg:String , ?pos:PosInfos)`* 315 | 316 | Asserts successfully when the 'value' parameter is of the of the passed type 'type'. 317 | 318 | `value` The value to test 319 | 320 | `type` The type to test against 321 | 322 | `msg` An optional error message. If not passed a default one will be used 323 | 324 | `pos` Code position where the Assert call has been executed. Don't fill it 325 | unless you know what you are doing. 326 | 327 | > *`notEquals(expected:Dynamic, value:Dynamic, ?msg:String , ?pos:PosInfos)`* 328 | 329 | Asserts successfully when the value parameter is not the same as the expected one. 330 | ```haxe 331 | Assert.notEquals(10, age); 332 | ``` 333 | 334 | `expected` The expected value to check against 335 | 336 | `value` The value to test 337 | 338 | `msg` An optional error message. If not passed a default one will be used 339 | 340 | `pos` Code position where the Assert call has been executed. Don't fill it 341 | unless you know what you are doing. 342 | 343 | > *`equals(expected:Dynamic, value:Dynamic, ?msg:String , ?pos:PosInfos)`* 344 | 345 | Asserts successfully when the value parameter is equal to the expected one. 346 | ```haxe 347 | Assert.equals(10, age); 348 | ``` 349 | 350 | `expected` The expected value to check against 351 | 352 | `value` The value to test 353 | 354 | `msg` An optional error message. If not passed a default one will be used 355 | 356 | `pos` Code position where the Assert call has been executed. Don't fill it 357 | unless you know what you are doing. 358 | 359 | 360 | > *`match(pattern:EReg, value:Dynamic, ?msg:String , ?pos:PosInfos)`* 361 | 362 | Asserts successfully when the value parameter does match against the passed EReg instance. 363 | ```haxe 364 | Assert.match(~/x/i, "Haxe"); 365 | ``` 366 | 367 | `pattern` The pattern to match against 368 | 369 | `value` The value to test 370 | 371 | `msg` An optional error message. If not passed a default one will be used 372 | 373 | `pos` Code position where the Assert call has been executed. Don't fill it 374 | unless you know what you are doing. 375 | 376 | > *`floatEquals(expected:Float, value:Float, ?approx:Float, ?msg:String , ?pos:PosInfos)`* 377 | 378 | Same as Assert.equals but considering an approximation error. 379 | ```haxe 380 | Assert.floatEquals(Math.PI, value); 381 | ``` 382 | 383 | `expected` The expected value to check against 384 | 385 | `value` The value to test 386 | 387 | `approx` The approximation tollerance. Default is 1e-5 388 | 389 | `msg` An optional error message. If not passed a default one will be used 390 | 391 | `pos` Code position where the Assert call has been executed. Don't fill it 392 | unless you know what you are doing. 393 | 394 | > *`same(expected:Dynamic, value:Dynamic, ?recursive:Bool, ?msg:String, ?pos:PosInfos)`* 395 | 396 | Check that value is an object with the same fields and values found in expected. 397 | The default behavior is to check nested objects in fields recursively. 398 | ```haxe 399 | Assert.same({ name:"utest"}, ob); 400 | ``` 401 | 402 | `expected` The expected value to check against 403 | 404 | `value` The value to test 405 | 406 | `recursive` States whether or not the test will apply also to sub-objects. 407 | Defaults to true 408 | 409 | `msg` An optional error message. If not passed a default one will be used 410 | 411 | `pos` Code position where the Assert call has been executed. Don't fill it 412 | unless you know what you are doing. 413 | 414 | > *`raises(method:Void -> Void, ?type:Class, ?msgNotThrown:String , ?msgWrongType:String, ?pos:PosInfos)`* 415 | 416 | It is used to test an application that under certain circumstances must 417 | react throwing an error. This assert guarantees that the error is of the 418 | correct type (or Dynamic if non is specified). 419 | ```haxe 420 | Assert.raises(function() { throw "Error!"; }, String); 421 | ``` 422 | 423 | `method` A method that generates the exception. 424 | 425 | `type` The type of the expected error. Defaults to Dynamic (catch all). 426 | 427 | `msgNotThrown` An optional error message used when the function fails to raise the expected 428 | exception. If not passed a default one will be used 429 | 430 | `msgWrongType` An optional error message used when the function raises the exception but it is 431 | of a different type than the one expected. If not passed a default one will be used 432 | 433 | `pos` Code position where the Assert call has been executed. Don't fill it 434 | unless you know what you are doing. 435 | @todo test the optional type parameter 436 | 437 | > *`allows(possibilities:Array, value:T, ?msg:String , ?pos:PosInfos)`* 438 | 439 | Checks that the test value matches at least one of the possibilities. 440 | 441 | `possibility` An array of possible matches 442 | 443 | `value` The value to test 444 | 445 | `msg` An optional error message. If not passed a default one will be used 446 | 447 | `pos` Code position where the Assert call has been executed. Don't fill it 448 | unless you know what you are doing. 449 | 450 | > *`contains(match:T, values:Array, ?msg:String , ?pos:PosInfos)`* 451 | 452 | Checks that the test array contains the match parameter. 453 | 454 | `match` The element that must be included in the tested array 455 | 456 | `values` The values to test 457 | 458 | `msg` An optional error message. If not passed a default one will be used 459 | 460 | `pos` Code position where the Assert call has been executed. Don't fill it 461 | unless you know what you are doing. 462 | 463 | > *`notContains(match:T, values:Array, ?msg:String , ?pos:PosInfos)`* 464 | 465 | Checks that the test array does not contain the match parameter. 466 | 467 | `match` The element that must NOT be included in the tested array 468 | 469 | `values` The values to test 470 | 471 | `msg` An optional error message. If not passed a default one will be used 472 | 473 | `pos` Code position where the Assert call has been executed. Don't fill it 474 | unless you know what you are doing. 475 | 476 | > *`stringContains(match:String, value:String, ?msg:String , ?pos:PosInfos)`* 477 | 478 | Checks that the expected values is contained in value. 479 | 480 | `match` the string value that must be contained in value 481 | 482 | `value` the value to test 483 | 484 | `msg` An optional error message. If not passed a default one will be used 485 | 486 | `pos` Code position where the Assert call has been executed. Don't fill it 487 | 488 | > *`fail(msg = "failure expected", ?pos:PosInfos)`* 489 | 490 | Forces a failure. 491 | 492 | `msg` An optional error message. If not passed a default one will be used 493 | 494 | `pos` Code position where the Assert call has been executed. Don't fill it 495 | unless you know what you are doing. 496 | 497 | 498 | > *`warn(msg)`* 499 | 500 | Creates a warning message. 501 | 502 | `msg` A mandatory message that justifies the warning. 503 | 504 | `pos` Code position where the Assert call has been executed. Don't fill it 505 | -------------------------------------------------------------------------------- /bin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /defines.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "define": "UTEST_PATTERN", 4 | "doc": "Only run tests which have names matching the `UTEST_PATTERN` value. The pattern could be a plain string or a regular expression without delimiters.", 5 | "params": ["pattern"] 6 | }, 7 | { 8 | "define": "UTEST_PRINT_TESTS", 9 | "doc": "Print test names in the process of tests execution." 10 | }, 11 | { 12 | "define": "UTEST_FAILURE_THROW", 13 | "doc": "Throw an unhandled exception instead of adding a failure to the report." 14 | }, 15 | { 16 | "define": "UTEST_IGNORE_DEPENDS", 17 | "doc": "Ignore @:depends meta and run annotated test regardless the outcome of the tests it depends on." 18 | } 19 | ] -------------------------------------------------------------------------------- /doc/ImportUTest.hx: -------------------------------------------------------------------------------- 1 | import utest.Assert; 2 | import utest.Assertation; 3 | import utest.Dispatcher; 4 | import utest.MacroRunner; 5 | import utest.Runner; 6 | import utest.TestFixture; 7 | import utest.TestHandler; 8 | import utest.TestResult; 9 | import utest.ui.Report; 10 | import utest.ui.common.ClassResult; 11 | import utest.ui.common.FixtureResult; 12 | import utest.ui.common.HeaderDisplayMode; 13 | import utest.ui.common.IReport; 14 | import utest.ui.common.PackageResult; 15 | import utest.ui.common.ReportTools; 16 | import utest.ui.common.ResultAggregator; 17 | import utest.ui.common.ResultStats; 18 | import utest.ui.macro.MacroReport; 19 | import utest.ui.text.HtmlReport; 20 | import utest.ui.text.PlainTextReport; 21 | import utest.ui.text.PrintReport; -------------------------------------------------------------------------------- /doc/build_doc.hxml: -------------------------------------------------------------------------------- 1 | -cp ../src 2 | --no-output 3 | -D doc-gen 4 | -D display 5 | -D dox 6 | -dce no 7 | -lib utest 8 | ImportUTest 9 | 10 | --each 11 | -xml xml/js.xml 12 | -js _ 13 | 14 | --next 15 | -xml xml/swf.xml 16 | -swf-version 11.4 17 | -swf _ 18 | 19 | --next 20 | -xml xml/neko.xml 21 | -neko _ 22 | 23 | --next 24 | -xml xml/php.xml 25 | -php _ 26 | 27 | --next 28 | -xml xml/cpp.xml 29 | -cpp _ 30 | 31 | --next 32 | -xml xml/java.xml 33 | -java _ 34 | 35 | --next 36 | -xml xml/cs.xml 37 | -cs _ 38 | 39 | #--next 40 | #-xml xml/python.xml 41 | #-python _ 42 | 43 | #--next 44 | #-xml xml/cross.xml 45 | -------------------------------------------------------------------------------- /doc/doc.hxml: -------------------------------------------------------------------------------- 1 | -cmd rm -rf xml/* 2 | -cmd mkdir -p xml 3 | -cmd haxe build_doc.hxml 4 | -cmd rm -rf pages 5 | -cmd haxelib run dox -r / -o pages -i xml --title "utest" -in utest 6 | -cmd static pages 7 | -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | --macro utest.utils.Macro.importEnvSettings() 2 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utest", 3 | "url": "https://github.com/haxe-utest/utest", 4 | "license": "MIT", 5 | "tags": ["cross","unittesting"], 6 | "description": "Unit Testing for Haxe", 7 | "version": "2.0.0-alpha", 8 | "classPath": "src", 9 | "releasenote": "See CHANGELOG.md", 10 | "contributors": ["fponticelli", "romanmikhailov", "RealyUniqueName"], 11 | "dependencies": { }, 12 | "documentation": { 13 | "metadata": "meta.json", 14 | "defines": "defines.json" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /hxml/common.hxml: -------------------------------------------------------------------------------- 1 | -cp src 2 | -cp test 3 | -main TestAll 4 | -D utest 5 | -dce full 6 | -D analyzer-optimize 7 | 8 | extraParams.hxml -------------------------------------------------------------------------------- /hxml/js-minified.hxml: -------------------------------------------------------------------------------- 1 | hxml/common.hxml 2 | 3 | #Because of this (until the fix is released): https://github.com/HaxeFoundation/haxe/issues/11327 4 | -D js-enums-as-arrays 5 | 6 | -lib closure 7 | -D closure_advanced 8 | -D closure_overwrite 9 | -D closure_warning_level=QUIET 10 | --macro includeFile('test/js-minified-header.js') -------------------------------------------------------------------------------- /hxml/test-all-linux.hxml: -------------------------------------------------------------------------------- 1 | ## 2 | # Build 3 | ## 4 | 5 | -cmd echo "[Building NEKO]" 6 | --next 7 | hxml/common.hxml 8 | -neko bin/test.n 9 | 10 | -cmd echo "[Building JAVASCRIPT]" 11 | --next 12 | hxml/common.hxml 13 | -js bin/test.js 14 | 15 | -cmd echo "[Building PHP]" 16 | --next 17 | hxml/common.hxml 18 | -php bin/php 19 | 20 | -cmd echo "[Building PYTHON]" 21 | --next 22 | hxml/common.hxml 23 | -python bin/test.py 24 | 25 | -cmd echo "[Building JAVA]" 26 | --next 27 | hxml/common.hxml 28 | -java bin/java 29 | 30 | -cmd echo "[Building C#]" 31 | --next 32 | hxml/common.hxml 33 | -cs bin/cs 34 | 35 | -cmd echo "[Building CPP]" 36 | --next 37 | hxml/common.hxml 38 | -cpp bin/cpp 39 | 40 | ## 41 | # Run 42 | ## 43 | -cmd echo "\n" 44 | --next 45 | 46 | -cmd echo "\n[Running INTERP]" 47 | -cmd haxe hxml/common.hxml --interp 48 | 49 | --next 50 | 51 | -cmd echo "\n[Running CPP]" 52 | -cmd ./bin/cpp/TestAll 53 | 54 | -cmd echo "\n[Running JAVASCRIPT]" 55 | -cmd nodejs ./bin/test.js 56 | 57 | -cmd echo "\n[Running JAVA]" 58 | -cmd java -jar ./bin/java/TestAll.jar 59 | 60 | -cmd echo "\n[Running C#]" 61 | -cmd mono ./bin/cs/bin/TestAll.exe 62 | 63 | -cmd echo "\n[Running PYTHON]" 64 | -cmd python3 ./bin/test.py 65 | 66 | -cmd echo "\n[Running NEKO]" 67 | -cmd neko ./bin/test.n 68 | 69 | -cmd echo "\n[Running PHP]" 70 | -cmd php bin/php/index.php -------------------------------------------------------------------------------- /meta.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "metadata": ":timeout", 4 | "doc": "Change timeout (milliseconds) for asynchronous test methods.", 5 | "params": ["ms"], 6 | "targets": ["TClass", "TClassField"] 7 | }, 8 | { 9 | "metadata": ":depends", 10 | "doc": "Prevents annotated test (or test case) from being executed if at least one of the tests in the list failed.", 11 | "params": ["test1", "test2", "...", "testN"], 12 | "targets": ["TClass", "TClassField"] 13 | }, 14 | { 15 | "metadata": ":ignore", 16 | "doc": "Skip annotated test.", 17 | "targets": ["TClass", "TClassField"] 18 | } 19 | ] -------------------------------------------------------------------------------- /src/utest/Assertation.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | import haxe.CallStack; 4 | import haxe.PosInfos; 5 | 6 | /** 7 | * Enumerates the states available as a result of 8 | * invoking one of the static methods of @see {@link utest.Assert}. 9 | */ 10 | enum Assertation { 11 | /** 12 | * Assertion is succesful 13 | * @param pos Code position where the Assert call has been executed 14 | */ 15 | Success(pos:PosInfos); 16 | 17 | /** 18 | * Assertion is a falure. This does not denote an error in the assertion 19 | * code but that the testing condition has failed for some reason. 20 | * Ei.: Assert.isTrue(1 == 0); 21 | * @param msg An error message containing the reasons for the failure. 22 | * @param pos Code position where the Assert call has been executed 23 | */ 24 | Failure(msg:String, pos:PosInfos); 25 | 26 | /** 27 | * An error has occurred during the execution of the test that prevents 28 | * futher assertion to be tested. 29 | * @param e The captured error/exception 30 | */ 31 | Error(e:Any, stack:CallStack); 32 | 33 | /** 34 | * An error has occurred during the Setup phase of the test. It prevents 35 | * the test to be run. 36 | * @param e The captured error/exception 37 | */ 38 | SetupError(e:Any, stack:CallStack); 39 | 40 | /** 41 | * An error has occurred during the Teardown phase of the test. 42 | * @param e The captured error/exception 43 | */ 44 | TeardownError(e:Any, stack:CallStack); 45 | 46 | /** 47 | * The asynchronous phase of a test has gone into timeout. 48 | * @param missedAsyncs The number of asynchronous calls that was expected 49 | * to be performed before the timeout. 50 | */ 51 | TimeoutError(missedAsyncs:Int, stack:CallStack); 52 | 53 | /** 54 | * An error has occurred during an asynchronous test. 55 | * @param e The captured error/exception 56 | */ 57 | AsyncError(e:Any, stack:CallStack); 58 | 59 | /** 60 | * A warning state. This can be declared explicitely by an Assert call 61 | * or can denote a test method that contains no assertions at all. 62 | * @param msg The reason behind the warning 63 | */ 64 | Warning(msg:String); 65 | 66 | /** 67 | * Test is ignored. 68 | * @param reason Reason of test ignoring. 69 | */ 70 | Ignore(reason:String); 71 | } -------------------------------------------------------------------------------- /src/utest/Async.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | import haxe.PosInfos; 4 | import haxe.Timer; 5 | 6 | @:allow(utest) 7 | class Async { 8 | static var resolvedInstance:Async; 9 | 10 | public var resolved(default,null):Bool = false; 11 | public var timedOut(default,null):Bool = false; 12 | 13 | var callbacks:Array<() ->Void> = []; 14 | var timeoutMs:Int; 15 | var startTime:Float; 16 | var timer:Timer; 17 | var branches:Array = []; 18 | 19 | /** 20 | * Returns an instance of `Async` which is already resolved. 21 | * Any actions handling this instance will be executed synchronously. 22 | */ 23 | static function getResolved():Async { 24 | if(resolvedInstance == null) { 25 | resolvedInstance = new Async(); 26 | resolvedInstance.done(); 27 | } 28 | return resolvedInstance; 29 | } 30 | 31 | static inline function strPos(pos:PosInfos):String { 32 | return pos.fileName + ':' + pos.lineNumber; 33 | } 34 | 35 | function new(timeoutMs:Int = 250) { 36 | this.timeoutMs = timeoutMs; 37 | startTime = Timer.stamp(); 38 | timer = Timer.delay(setTimedOutState, timeoutMs); 39 | } 40 | 41 | public function done(?pos:PosInfos) { 42 | if(resolved) { 43 | if(timedOut) { 44 | throw 'Cannot done() at ${strPos(pos)} because async is timed out.'; 45 | } else { 46 | throw 'Cannot done() at ${strPos(pos)} because async is done already.'; 47 | } 48 | } 49 | resolved = true; 50 | timer.stop(); 51 | for (cb in callbacks) cb(); 52 | } 53 | 54 | /** 55 | * Change timeout for this async. 56 | */ 57 | public function setTimeout(timeoutMs:Int, ?pos:PosInfos) { 58 | if(resolved) { 59 | throw 'Cannot setTimeout($timeoutMs) at ${strPos(pos)} because async is done.'; 60 | } 61 | if(timedOut) { 62 | throw 'Cannot setTimeout($timeoutMs) at ${strPos(pos)} because async is timed out.'; 63 | } 64 | 65 | timer.stop(); 66 | 67 | this.timeoutMs = timeoutMs; 68 | var delay = timeoutMs - Math.round(1000 * (Timer.stamp() - startTime)); 69 | timer = Timer.delay(setTimedOutState, delay); 70 | } 71 | 72 | /** 73 | Create a sub-async. Current `Async` instance will be resolved automatically once all sub-asyncs are resolved. 74 | **/ 75 | public function branch(?fn:(Async)->Void, ?pos:PosInfos):Async { 76 | var branch = new Async(timeoutMs); 77 | branches.push(branch); 78 | branch.then(checkBranches.bind(pos)); 79 | if(fn != null) fn(branch); 80 | return branch; 81 | } 82 | 83 | function checkBranches(pos:PosInfos) { 84 | if(resolved) return; 85 | for(branch in branches) { 86 | if(!branch.resolved) return; 87 | if(branch.timedOut) { 88 | setTimedOutState(); 89 | return; 90 | } 91 | } 92 | //wait, maybe other branches are about to be created 93 | var branchCount = branches.length; 94 | Timer.delay( 95 | function() { 96 | if(branchCount == branches.length) { // no new branches have been spawned 97 | done(pos); 98 | } 99 | }, 100 | 5 101 | ); 102 | } 103 | 104 | function then(cb:()->Void) { 105 | if(resolved) { 106 | cb(); 107 | } else { 108 | callbacks.push(cb); 109 | } 110 | } 111 | 112 | function setTimedOutState() { 113 | if(resolved) return; 114 | timedOut = true; 115 | done(); 116 | } 117 | } -------------------------------------------------------------------------------- /src/utest/Dispatcher.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | import haxe.Exception; 4 | 5 | private class StopPropagationException extends Exception { 6 | public function new() { 7 | super(''); 8 | } 9 | } 10 | 11 | class Dispatcher { 12 | 13 | private var handlers : Array<(T) -> Void>; 14 | 15 | public function new() 16 | handlers = new Array(); 17 | 18 | public function add(h : (T) -> Void) : (T) -> Void { 19 | handlers.push(h); 20 | return h; 21 | } 22 | 23 | public function remove(h : (T) -> Void) : (T) -> Void { 24 | for(i in 0...handlers.length) 25 | if(Reflect.compareMethods(handlers[i], h)) 26 | return handlers.splice(i, 1)[0]; 27 | return null; 28 | } 29 | 30 | public function clear() 31 | handlers = new Array(); 32 | 33 | public function dispatch(e:T) { 34 | try { 35 | // prevents problems with self removing events 36 | var list = handlers.copy(); 37 | for( l in list ) 38 | l(e); 39 | return true; 40 | } catch( _ : StopPropagationException ) { 41 | return false; 42 | } 43 | } 44 | 45 | public function has() 46 | return handlers.length > 0; 47 | 48 | public static function stop() 49 | throw new StopPropagationException(); 50 | } 51 | 52 | class Notifier { 53 | 54 | private var handlers : Array<() -> Void>; 55 | 56 | public function new() 57 | handlers = new Array(); 58 | 59 | public function add(h : () -> Void) : () -> Void { 60 | handlers.push(h); 61 | return h; 62 | } 63 | 64 | public function remove(h : () -> Void) : () -> Void { 65 | for(i in 0...handlers.length) 66 | if(Reflect.compareMethods(handlers[i], h)) 67 | return handlers.splice(i, 1)[0]; 68 | return null; 69 | } 70 | 71 | public function clear() 72 | handlers = new Array(); 73 | 74 | public function dispatch() { 75 | try { 76 | // prevents problems with self removing events 77 | var list = handlers.copy(); 78 | for( l in list ) 79 | l(); 80 | return true; 81 | } catch( _ : StopPropagationException ) { 82 | return false; 83 | } 84 | } 85 | 86 | public function has() 87 | return handlers.length > 0; 88 | 89 | public static function stop() 90 | throw new StopPropagationException(); 91 | } -------------------------------------------------------------------------------- /src/utest/ITest.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | /** 4 | * Interface, which should be implemented by test cases. 5 | * 6 | */ 7 | #if !macro 8 | @:autoBuild(utest.utils.TestBuilder.build()) 9 | #end 10 | interface ITest { 11 | // /** 12 | // * This method is executed once before running the first test in the current class. 13 | // * If it accepts an argument, it is treated as an asynchronous method. 14 | // */ 15 | // function setupClass():Void; 16 | // function setupClass(async:Async):Void; 17 | 18 | // /** 19 | // * This method is executed before each test. 20 | // * If it accepts an argument, it is treated as an asynchronous method. 21 | // */ 22 | // function setup():Void; 23 | // function setup(async:Async):Void; 24 | 25 | // /** 26 | // * This method is executed after each test. 27 | // * If it accepts an argument, it is treated as an asynchronous method. 28 | // */ 29 | // function teardown():Void; 30 | // function teardown(async:Async):Void; 31 | 32 | // /** 33 | // * This method is executed once after the last test in the current class is finished. 34 | // * If it accepts an argument, it is treated as an asynchronous method. 35 | // */ 36 | // function teardownClass():Void; 37 | // function teardownClass(async:Async):Void; 38 | } 39 | -------------------------------------------------------------------------------- /src/utest/IgnoredFixture.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | abstract IgnoredFixture(String) to String { 4 | public static function NotIgnored():IgnoredFixture { 5 | return new IgnoredFixture(null); 6 | } 7 | 8 | public static function Ignored(reason:String = null):IgnoredFixture { 9 | return new IgnoredFixture(reason != null ? reason : ""); 10 | } 11 | 12 | public var isIgnored(get, never):Bool; 13 | public var ignoreReason(get, never):String; 14 | 15 | public inline function new(reason:String) { 16 | this = reason; 17 | } 18 | 19 | private inline function get_isIgnored():Bool { 20 | return this != null; 21 | } 22 | 23 | private inline function get_ignoreReason():String { 24 | return this; 25 | } 26 | } -------------------------------------------------------------------------------- /src/utest/MacroRunner.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | #if macro 4 | import haxe.macro.Expr; 5 | import haxe.macro.Context; 6 | #end 7 | 8 | import utest.ui.macro.MacroReport; 9 | import utest.Runner; 10 | 11 | class MacroRunner { 12 | #if macro 13 | /** 14 | Run the unit tests from a macro, displaying errors and a summary in the macro Context. 15 | @param testClass Class where the tests are located. 16 | */ 17 | public static function run(testClass : Any) { 18 | var runner = new Runner(); 19 | addClass(runner, testClass); 20 | 21 | new MacroReport(runner); 22 | runner.run(); 23 | 24 | return { expr: EConst(CType("Void")), pos: Context.currentPos() }; 25 | } 26 | #end 27 | 28 | /** 29 | Displays stub code for using MacroRunner. 30 | @param n String of test class to use, "package.ClassName" for example. 31 | @todo Parse real package/class references instead of just a string. 32 | */ 33 | macro public static function generateMainCode(n : Expr) { 34 | var className = "YOURTESTCLASS"; 35 | 36 | switch n.expr { 37 | case EConst(c): 38 | switch c { 39 | case CString(s): 40 | className = s; 41 | default: 42 | } 43 | default: 44 | } 45 | 46 | trace("MacroRunner.run() can only be executed from a macro context.\nUse this code as a template in the main class:\n\n" + 47 | "class Main\n{\n\tstatic function main()\n\t{\n\t\tMain.runTests();\n\t}\n\n" + 48 | "\tmacro static function runTests()\n\t{\n\t\treturn MacroRunner.run(new " + className + "());\n\t}\n}\n"); 49 | 50 | return { expr: EConst(CType("Void")), pos: Context.currentPos() }; 51 | } 52 | 53 | static function addClass(runner : Runner, testClass : Class) { 54 | runner.addCase(testClass); 55 | 56 | var addTests = Reflect.field(testClass, "addTests"); 57 | 58 | if (addTests != null && Reflect.isFunction(addTests)) { 59 | Reflect.callMethod(testClass, addTests, [runner]); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/utest/Runner.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | import utest.exceptions.UTestException; 4 | import utest.utils.Print; 5 | import haxe.CallStack; 6 | import haxe.macro.Compiler; 7 | import utest.Dispatcher; 8 | #if macro 9 | import haxe.macro.Context; 10 | import haxe.macro.Expr; 11 | import haxe.io.Path; 12 | 13 | using sys.FileSystem; 14 | using StringTools; 15 | using haxe.macro.Tools; 16 | #end 17 | 18 | using utest.utils.AsyncUtils; 19 | using utest.utils.AccessoriesUtils; 20 | 21 | #if (haxe_ver < "4.1.0") 22 | #error 'Haxe 4.1.0 or later is required to run UTest' 23 | #end 24 | 25 | /** 26 | * The Runner class performs a set of tests. The tests can be added using addCase. 27 | * Once all the tests are register they are axecuted on the run() call. 28 | * Note that Runner does not provide any visual output. To visualize the test results use one of 29 | * the classes in the utest.ui package. 30 | * @todo complete documentation 31 | * @todo AVOID CHAINING METHODS (long chains do not work properly on IE) 32 | */ 33 | class Runner { 34 | var fixtures:MapAsync, dependencies:Array, fixtures:Array, teardownClass:()->Async}> = new Map(); 35 | 36 | /** 37 | * Event object that monitors the progress of the runner. 38 | */ 39 | public var onProgress(default, null) : Dispatcher<{ result : TestResult, done : Int, totals : Int }>; 40 | 41 | /** 42 | * Event object that monitors when the runner starts. 43 | */ 44 | public var onStart(default, null) : Dispatcher; 45 | 46 | /** 47 | * Event object that monitors when the runner ends. This event takes into account async calls 48 | * performed during the tests. 49 | */ 50 | public var onComplete(default, null) : Dispatcher; 51 | 52 | /** 53 | * Event object that monitors when a handler has executed a test case, and 54 | * is about to evaluate the results. Useful for mocking certain 55 | * custom asynchronouse behavior in order for certain tests to pass. 56 | */ 57 | public var onPrecheck(default, null) : Dispatcher>; 58 | 59 | /** 60 | * Event object that notifies when a handler is about to start executing. 61 | */ 62 | public var onTestStart(default, null) : Dispatcher>; 63 | 64 | /** 65 | * Event object that notifies when a handler has completed executing. 66 | */ 67 | public var onTestComplete(default, null) : Dispatcher>; 68 | 69 | /** 70 | * The number of fixtures registered. 71 | */ 72 | public var length(default, null) : Int; 73 | 74 | /** 75 | * Global pattern to override the test pattern specified with `addCase` 76 | */ 77 | public var globalPattern(default, default) : Null = null; 78 | 79 | /** 80 | * Indicates if all tests are finished. 81 | */ 82 | var complete:Bool = false; 83 | 84 | /** 85 | * Instantiates a Runner onject. 86 | */ 87 | public function new() { 88 | onProgress = new Dispatcher(); 89 | onStart = new Dispatcher(); 90 | onComplete = new Dispatcher(); 91 | onPrecheck = new Dispatcher(); 92 | onTestStart = new Dispatcher(); 93 | onTestComplete = new Dispatcher(); 94 | length = 0; 95 | 96 | var envPattern = Compiler.getDefine('UTEST_PATTERN'); 97 | if(envPattern != null) { 98 | globalPattern = new EReg(envPattern, ''); 99 | } 100 | } 101 | 102 | /** 103 | * Adds a new test case. 104 | * @param testCase must be a not null object 105 | * @param pattern a regular expression that discriminates the names of test 106 | * functions 107 | */ 108 | public function addCase(testCase : ITest, ?pattern : EReg) { 109 | var className = Type.getClassName(Type.getClass(testCase)); 110 | if(fixtures.exists(className)) { 111 | throw new UTestException('Cannot add the same test twice.'); 112 | } 113 | var newFixtures = []; 114 | var init:TestData.InitializeUtest = (cast testCase:TestData.Initializer).__initializeUtest__(); 115 | for(test in init.tests) { 116 | if(!isTestFixtureName(className, test.name, ['test', 'spec'], pattern, globalPattern)) { 117 | continue; 118 | } 119 | newFixtures.push(new TestFixture(testCase, test, init.accessories)); 120 | } 121 | if(newFixtures.length > 0) { 122 | fixtures.set(className, { 123 | caseInstance:testCase, 124 | setupClass:init.accessories.getSetupClass(), 125 | dependencies:#if UTEST_IGNORE_DEPENDS [] #else init.dependencies #end, 126 | fixtures:newFixtures, 127 | teardownClass:init.accessories.getTeardownClass() 128 | }); 129 | length += newFixtures.length; 130 | } 131 | } 132 | 133 | /** 134 | * Add all test cases located in specified package `path`. 135 | * Any module found in `path` is treated as a test case. 136 | * That means each module should contain a class with a constructor and with the same name as a module name. 137 | * @param path dot-separated path as a string or as an identifier/field expression. E.g. `"my.pack"` or `my.pack` 138 | * @param recursive recursively look for test cases in sub packages. 139 | * @param nameFilterRegExp regular expression to check modules names against. If the module name does not 140 | * match this argument, the module will not be added. 141 | */ 142 | macro public function addCases(eThis:Expr, path:Expr, recursive:Bool = true, nameFilterRegExp:String = '.*'):Expr { 143 | if(Context.defined('display')) { 144 | return macro {}; 145 | } 146 | var path = switch(path.expr) { 147 | case EConst(CString(s)): s; 148 | case _: path.toString(); 149 | } 150 | var pos = Context.currentPos(); 151 | if(~/[^a-zA-Z0-9_.]/.match(path)) { 152 | Context.error('The first argument for utest.Runner.addCases() should be a valid package path.', pos); 153 | } 154 | var nameFilter = new EReg(nameFilterRegExp, ''); 155 | var pack = path.split('.'); 156 | var relativePath = Path.join(pack); 157 | var exprs = []; 158 | var packageExists = false; 159 | function traverse(dir:String, path:String) { 160 | if(!dir.exists()) return; 161 | packageExists = true; 162 | for(file in dir.readDirectory()) { 163 | var fullPath = Path.join([dir, file]); 164 | if(fullPath.isDirectory() && recursive){ 165 | traverse(fullPath, '$path.$file'); 166 | continue; 167 | } 168 | if(file.substr(-3) != '.hx' || file == 'import.hx') { 169 | continue; 170 | } 171 | var className = file.substr(0, file.length - 3); 172 | if(className == '' || !nameFilter.match(className)) { 173 | continue; 174 | } 175 | var testCase = Context.parse('new $path.$className()', pos); 176 | exprs.push(macro @:pos(pos) $eThis.addCase($testCase)); 177 | } 178 | } 179 | for(classPath in Context.getClassPath()) { 180 | traverse(Path.join([classPath, relativePath]), path); 181 | } 182 | if(!packageExists) { 183 | Context.error('Package $path does not exist', pos); 184 | } 185 | return macro @:pos(pos) $b{exprs}; 186 | } 187 | 188 | private function isTestFixtureName(caseName:String, testName:String, prefixes:Array, ?pattern:EReg, ?globalPattern:EReg):Bool { 189 | if (pattern == null && globalPattern == null) { 190 | for(prefix in prefixes) { 191 | if(StringTools.startsWith(testName, prefix)) { 192 | return true; 193 | } 194 | } 195 | return false; 196 | } 197 | if (pattern == null) pattern = globalPattern; 198 | return pattern.match('$caseName.$testName'); 199 | } 200 | 201 | public function run() { 202 | onStart.dispatch(this); 203 | var iTestRunner = new ITestRunner(this); 204 | iTestRunner.run(); 205 | waitForCompletion(); 206 | } 207 | 208 | /** 209 | * Don't let the app to shutdown until all tests are finished. 210 | * @see https://github.com/HaxeFoundation/haxe/issues/8131 211 | * Can't reproduce it on a separated sample. 212 | */ 213 | function waitForCompletion() { 214 | if(!complete) { 215 | haxe.Timer.delay(waitForCompletion, 100); 216 | } 217 | } 218 | 219 | function runFixture(fixture : TestFixture):TestHandler { 220 | var handler = new TestHandler(fixture); 221 | handler.onComplete.add(testComplete); 222 | handler.onPrecheck.add(this.onPrecheck.dispatch); 223 | Print.startTest(fixture.name); 224 | onTestStart.dispatch(handler); 225 | handler.execute(); 226 | return handler; 227 | } 228 | 229 | var executedFixtures:Int = 0; 230 | function testComplete(h : TestHandler) { 231 | ++executedFixtures; 232 | onTestComplete.dispatch(h); 233 | onProgress.dispatch({ result : TestResult.ofHandler(h), done : executedFixtures, totals : length }); 234 | } 235 | } 236 | 237 | @:access(utest.Runner.fixtures) 238 | @:access(utest.Runner.runNext) 239 | @:access(utest.Runner.runFixture) 240 | @:access(utest.Runner.executedFixtures) 241 | @:access(utest.Runner.complete) 242 | private class ITestRunner { 243 | var runner:Runner; 244 | var cases:Iterator; 245 | var currentCaseName:String; 246 | var currentCase:ITest; 247 | var currentCaseFixtures:Array; 248 | var teardownClass:()->Async; 249 | var setupAsync:Async; 250 | var teardownAsync:Async; 251 | var failedTestsInCurrentCase:Array = []; 252 | var executedTestsInCurrentCase:Array = []; 253 | var failedCases:Array = []; 254 | 255 | public function new(runner:Runner) { 256 | this.runner = runner; 257 | runner.onTestComplete.add(function(handler) { 258 | for (result in handler.results) { 259 | switch result { 260 | case Success(_): 261 | case _: 262 | failedTestsInCurrentCase.push(handler.fixture.name); 263 | failedCases.push(Type.getClassName(Type.getClass(handler.fixture.target))); 264 | } 265 | } 266 | executedTestsInCurrentCase.push(handler.fixture.name); 267 | }); 268 | } 269 | 270 | public function run() { 271 | cases = orderClassesByDependencies(); 272 | runCases(); 273 | } 274 | 275 | function orderClassesByDependencies():Iterator { 276 | var result = []; 277 | function error(testCase:ITest, msg:String) { 278 | runner.onProgress.dispatch({ 279 | totals: runner.length, 280 | result: TestResult.ofFailedSetupClass(testCase, SetupError(msg, [])), 281 | done: runner.executedFixtures 282 | }); 283 | } 284 | var added = new Map(); 285 | function addClass(cls:String, stack:Array) { 286 | if(added.exists(cls)) 287 | return; 288 | var data = runner.fixtures.get(cls); 289 | if(stack.indexOf(cls) >= 0) { 290 | error(data.caseInstance, 'Circular dependencies among test classes detected: ' + stack.join(' -> ')); 291 | return; 292 | } 293 | stack.push(cls); 294 | var dependencies = data.dependencies; 295 | for(dependency in dependencies) { 296 | if(runner.fixtures.exists(dependency)) { 297 | addClass(dependency, stack); 298 | } else { 299 | error(data.caseInstance, 'This class depends on $dependency, but it cannot be found. Was it added to test runner?'); 300 | return; 301 | } 302 | } 303 | result.push(cls); 304 | added.set(cls, true); 305 | } 306 | for(cls in runner.fixtures.keys()) { 307 | addClass(cls, []); 308 | } 309 | return result.iterator(); 310 | } 311 | 312 | function failedDependencies(data:{dependencies:Array}):Bool { 313 | for(dependency in data.dependencies) { 314 | if(failedCases.indexOf(dependency) >= 0) 315 | return true; 316 | } 317 | return false; 318 | } 319 | 320 | function runCases() { 321 | while(cases.hasNext()) { 322 | currentCaseName = cases.next(); 323 | var data = runner.fixtures.get(currentCaseName); 324 | currentCase = data.caseInstance; 325 | failedTestsInCurrentCase = []; 326 | executedTestsInCurrentCase = []; 327 | if(failedDependencies(data)) { 328 | failedCases.push(currentCaseName); 329 | continue; 330 | } 331 | Print.startCase(currentCaseName); 332 | currentCaseFixtures = data.fixtures; 333 | teardownClass = data.teardownClass; 334 | try { 335 | setupAsync = data.setupClass(); 336 | } 337 | #if !UTEST_FAILURE_THROW 338 | catch(e) { 339 | setupFailed(SetupError('setupClass failed: ${e.message}', e.stack)); 340 | return; 341 | } 342 | #end 343 | if(setupAsync.resolved) { 344 | if(!runFixtures()) return; 345 | } else { 346 | setupAsync.then(checkSetup); 347 | return; 348 | } 349 | } 350 | runner.complete = true; 351 | runner.onComplete.dispatch(runner); 352 | } 353 | 354 | function checkSetup() { 355 | if(setupAsync.timedOut) { 356 | setupFailed(SetupError('setupClass timeout', [])); 357 | } else { 358 | if(runFixtures()) 359 | runCases(); 360 | } 361 | } 362 | 363 | function setupFailed(assertation:Assertation) { 364 | runner.executedFixtures += currentCaseFixtures.length; 365 | runner.onProgress.dispatch({ 366 | totals: runner.length, 367 | result: TestResult.ofFailedSetupClass(currentCase, assertation), 368 | done: runner.executedFixtures 369 | }); 370 | runCases(); 371 | } 372 | 373 | /** 374 | * Returns `true` if all fixtures were executed synchronously. 375 | */ 376 | function runFixtures(?finishedHandler:TestHandler):Bool { 377 | while(currentCaseFixtures.length > 0) { 378 | var fixture = currentCaseFixtures.shift(); 379 | checkFixtureDependencies(fixture); 380 | var handler = runner.runFixture(fixture); 381 | if(!handler.finished) { 382 | handler.onComplete.add(runFixtures); 383 | return false; 384 | } 385 | } 386 | //no fixtures left in the current case 387 | try { 388 | teardownAsync = teardownClass(); 389 | } 390 | #if !UTEST_FAILURE_THROW 391 | catch(e) { 392 | teardownFailed(TeardownError('teardownClass failed: ${e.message}', e.stack)); 393 | return true; 394 | } 395 | #end 396 | //case was executed synchronously from `runCases()` 397 | if(teardownAsync.resolved && finishedHandler == null) { 398 | return true; 399 | } 400 | teardownAsync.then(checkTeardown); 401 | return false; 402 | } 403 | 404 | function checkFixtureDependencies(fixture:TestFixture) { 405 | if(!fixture.ignoringInfo.isIgnored) { 406 | #if !UTEST_IGNORE_DEPENDS 407 | if(fixture.test.dependencies.length > 0) { 408 | var failedDeps = []; 409 | var ignoredDeps = []; 410 | for (dep in fixture.test.dependencies) { 411 | if(failedTestsInCurrentCase.contains(dep)) { 412 | failedDeps.push(dep); 413 | } 414 | if(!executedTestsInCurrentCase.contains(dep)) { 415 | ignoredDeps.push(dep); 416 | } 417 | } 418 | var failedDepsMsg = failedDeps.length == 0 ? null : IgnoredFixture.Ignored('Failed dependencies: ${failedDeps.join(', ')}'); 419 | var ignoredDepsMsg = ignoredDeps.length == 0 ? null : IgnoredFixture.Ignored('Skipped dependencies: ${ignoredDeps.join(', ')}'); 420 | var ignoringInfo = switch [failedDepsMsg, ignoredDepsMsg] { 421 | case [null, null]: IgnoredFixture.NotIgnored(); 422 | case [_, null]: IgnoredFixture.Ignored(failedDepsMsg); 423 | case [null, _]: IgnoredFixture.Ignored(ignoredDepsMsg); 424 | case [_, _]: IgnoredFixture.Ignored('$failedDepsMsg. $ignoredDepsMsg'); 425 | } 426 | fixture.setIgnoringInfo(ignoringInfo); 427 | } 428 | #end 429 | } 430 | } 431 | 432 | function checkTeardown() { 433 | if(teardownAsync.timedOut) { 434 | teardownFailed(TeardownError('teardownClass timeout', [])); 435 | } 436 | runCases(); 437 | } 438 | 439 | function teardownFailed(assertation:Assertation) { 440 | runner.onProgress.dispatch({ 441 | totals: runner.length, 442 | result: TestResult.ofFailedTeardownClass(currentCase, assertation), 443 | done: runner.executedFixtures 444 | }); 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /src/utest/Test.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | /** 4 | * Base test class for convenience. 5 | */ 6 | class Test implements ITest { 7 | public function new() {} 8 | } -------------------------------------------------------------------------------- /src/utest/TestData.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | import haxe.ds.Option; 4 | 5 | /** 6 | * The data of a test as collected by utest.utils.TestBuilder at compile time. 7 | */ 8 | typedef TestData = { 9 | var name(default,null):String; 10 | var dependencies(default,null):Array; 11 | var execute(default,null):()->Async; 12 | var ignore:Option; 13 | } 14 | 15 | /** 16 | * Describes the reason of a test being ignored. 17 | * `null` means no reason provided with `@:ignore` meta. 18 | */ 19 | typedef IgnoreReason = Null; 20 | 21 | /** 22 | * The data of accessory methods: setup, setupClass, teardown, teardownClass 23 | */ 24 | typedef Accessories = { 25 | ?setup:()->Async, 26 | ?setupClass:()->Async, 27 | ?teardown:()->Async, 28 | ?teardownClass:()->Async, 29 | } 30 | 31 | typedef InitializeUtest = { 32 | accessories:Accessories, 33 | dependencies:Array, 34 | tests:Array 35 | } 36 | 37 | typedef Initializer = { 38 | function __initializeUtest__():InitializeUtest; 39 | } 40 | 41 | class AccessoryName { 42 | /** 43 | * This method is executed once before running the first test in the current class 44 | */ 45 | static public inline var SETUP_NAME = 'setup'; 46 | /** 47 | * This method is executed before each test. 48 | */ 49 | static public inline var SETUP_CLASS_NAME = 'setupClass'; 50 | /** 51 | * This method is executed after each test. 52 | */ 53 | static public inline var TEARDOWN_NAME = 'teardown'; 54 | /** 55 | * This method is executed once after the last test in the current class is finished. 56 | */ 57 | static public inline var TEARDOWN_CLASS_NAME = 'teardownClass'; 58 | } -------------------------------------------------------------------------------- /src/utest/TestFixture.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | import haxe.rtti.Meta; 4 | import utest.IgnoredFixture; 5 | 6 | using utest.utils.AccessoriesUtils; 7 | 8 | class TestFixture { 9 | public var target(default, null) : ITest; 10 | public var ignoringInfo(default, null) : IgnoredFixture; 11 | 12 | public var name(get, never) : String; 13 | function get_name():String return test.name; 14 | 15 | @:allow(utest) 16 | final test:TestData; 17 | @:allow(utest) 18 | final setupMethod:()->Async; 19 | @:allow(utest) 20 | final teardownMethod:()->Async; 21 | 22 | public function new(target:ITest, test:TestData, accessories:TestData.Accessories) { 23 | this.target = target; 24 | this.test = test; 25 | this.setupMethod = accessories.getSetup(); 26 | this.teardownMethod = accessories.getTeardown(); 27 | 28 | ignoringInfo = switch test.ignore { 29 | case None: IgnoredFixture.NotIgnored(); 30 | case Some(reason): IgnoredFixture.Ignored(reason); 31 | } 32 | } 33 | 34 | public function setIgnoringInfo(info:IgnoredFixture) { 35 | ignoringInfo = info; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/utest/TestHandler.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | import haxe.Exception; 4 | import haxe.ValueException; 5 | import utest.exceptions.UTestException; 6 | import haxe.CallStack; 7 | import haxe.Timer; 8 | import utest.Assertation; 9 | 10 | class TestHandler { 11 | private static inline var POLLING_TIME = 10; 12 | public var results(default, null) : List; 13 | public var fixture(default, null) : TestFixture; 14 | public var finished(default, null) : Bool = false; 15 | public var executionTime(default, null) : Float = 0; 16 | var asyncStack : List; 17 | var startTime:Float = 0; 18 | 19 | public var onTested(default, null) : Dispatcher>; 20 | public var onTimeout(default, null) : Dispatcher>; 21 | public var onComplete(default, null) : Dispatcher>; 22 | public var onPrecheck(default, null) : Dispatcher>; 23 | 24 | public var precheck(default, null) : ()->Void; 25 | 26 | private var wasBound:Bool = false; 27 | 28 | var testCase:ITest; 29 | var test:TestData; 30 | var setupAsync:Null; 31 | var testAsync:Null; 32 | var teardownAsync:Null; 33 | 34 | public function new(fixture : TestFixture) { 35 | if(fixture == null) throw "fixture argument is null"; 36 | this.fixture = fixture; 37 | results = new List(); 38 | asyncStack = new List(); 39 | onTested = new Dispatcher(); 40 | onTimeout = new Dispatcher(); 41 | onComplete = new Dispatcher(); 42 | onPrecheck = new Dispatcher(); 43 | 44 | if (fixture.ignoringInfo.isIgnored) { 45 | results.add(Ignore(fixture.ignoringInfo.ignoreReason)); 46 | } 47 | 48 | testCase = fixture.target; 49 | test = fixture.test; 50 | if(test == null) { 51 | throw 'Fixture is missing test data'; 52 | } 53 | } 54 | 55 | public function execute() { 56 | startTime = Timer.stamp(); 57 | if (fixture.ignoringInfo.isIgnored) { 58 | executeFinally(); 59 | return; 60 | } 61 | bindHandler(); 62 | runSetup(); 63 | } 64 | 65 | function runSetup() { 66 | inline function handleCatch(e:Any, stack:CallStack) { 67 | results.add(SetupError(e, stack)); 68 | completedFinally(); 69 | } 70 | try { 71 | setupAsync = fixture.setupMethod(); 72 | } 73 | #if !UTEST_FAILURE_THROW 74 | catch(e:ValueException) { 75 | handleCatch(e.value, e.stack); 76 | return; 77 | } catch(e) { 78 | handleCatch(e, e.stack); 79 | return; 80 | } 81 | #end 82 | 83 | setupAsync.then(checkSetup); 84 | } 85 | 86 | function checkSetup() { 87 | if(setupAsync.timedOut) { 88 | results.add(SetupError('Setup timeout', [])); 89 | completedFinally(); 90 | } else { 91 | runTest(); 92 | } 93 | } 94 | 95 | function runTest() { 96 | inline function handleCatch(e:Any, stack:CallStack) { 97 | results.add(Error(e, stack)); 98 | runTeardown(); 99 | } 100 | try { 101 | testAsync = test.execute(); 102 | } 103 | #if !UTEST_FAILURE_THROW 104 | catch(e:ValueException) { 105 | handleCatch(e.value, e.stack); 106 | return; 107 | } catch(e) { 108 | handleCatch(e, e.stack); 109 | return; 110 | } 111 | #end 112 | 113 | testAsync.then(checkTest); 114 | } 115 | 116 | function checkTest() { 117 | onPrecheck.dispatch(this); 118 | 119 | if(testAsync.timedOut) { 120 | results.add(TimeoutError(1, [])); 121 | onTimeout.dispatch(this); 122 | 123 | } else if(testAsync.resolved) { 124 | if(results.length == 0) { 125 | results.add(Warning('no assertions')); 126 | } 127 | onTested.dispatch(this); 128 | 129 | } else { 130 | throw 'Unexpected test state'; 131 | } 132 | 133 | runTeardown(); 134 | } 135 | 136 | function runTeardown() { 137 | inline function handleCatch(e:Any, stack:CallStack) { 138 | results.add(TeardownError(e, CallStack.exceptionStack())); 139 | completedFinally(); 140 | } 141 | try { 142 | teardownAsync = fixture.teardownMethod(); 143 | } 144 | #if !UTEST_FAILURE_THROW 145 | catch(e:ValueException) { 146 | handleCatch(e.value, e.stack); 147 | return; 148 | } catch(e) { 149 | handleCatch(e, e.stack); 150 | return; 151 | } 152 | #end 153 | 154 | teardownAsync.then(checkTeardown); 155 | } 156 | 157 | function checkTeardown() { 158 | if(teardownAsync.timedOut) { 159 | results.add(TeardownError('Teardown timeout', [])); 160 | } 161 | completedFinally(); 162 | } 163 | 164 | function executeFinally() { 165 | onPrecheck.dispatch(this); 166 | tested(); 167 | } 168 | 169 | function bindHandler() { 170 | if (wasBound) return; 171 | Assert.results = this.results; 172 | wasBound = true; 173 | } 174 | 175 | function unbindHandler() { 176 | if (!wasBound) return; 177 | Assert.results = null; 178 | wasBound = false; 179 | } 180 | 181 | function tested() { 182 | if(results.length == 0) 183 | results.add(Warning("no assertions")); 184 | onTested.dispatch(this); 185 | completedFinally(); 186 | } 187 | 188 | function timeout() { 189 | results.add(TimeoutError(asyncStack.length, [])); 190 | onTimeout.dispatch(this); 191 | completedFinally(); 192 | } 193 | 194 | function completedFinally() { 195 | finished = true; 196 | unbindHandler(); 197 | executionTime = (Timer.stamp() - startTime) * 1000; 198 | onComplete.dispatch(this); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/utest/TestResult.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | import utest.Assertation; 4 | 5 | class TestResult { 6 | public var pack : String; 7 | public var cls : String; 8 | public var method : String; 9 | public var setup : String; 10 | public var setupAsync : String; 11 | public var teardown : String; 12 | public var teardownAsync : String; 13 | public var assertations : List; 14 | public var executionTime : Float; 15 | 16 | public function new(){} 17 | 18 | public static function ofHandler(handler : TestHandler):TestResult { 19 | var r = new TestResult(); 20 | var path = Type.getClassName(Type.getClass(handler.fixture.target)).split('.'); 21 | r.cls = path.pop(); 22 | r.pack = path.join('.'); 23 | r.method = handler.fixture.name; 24 | r.assertations = handler.results; 25 | r.executionTime = handler.executionTime; 26 | return r; 27 | } 28 | 29 | public static function ofFailedSetupClass(testCase:ITest, assertation:Assertation):TestResult { 30 | var r = new TestResult(); 31 | var path = Type.getClassName(Type.getClass(testCase)).split('.'); 32 | r.cls = path.pop(); 33 | r.pack = path.join('.'); 34 | r.method = 'setup'; 35 | r.assertations = new List(); 36 | r.assertations.add(assertation); 37 | return r; 38 | } 39 | 40 | public static function ofFailedTeardownClass(testCase:ITest, assertation:Assertation):TestResult { 41 | var r = new TestResult(); 42 | var path = Type.getClassName(Type.getClass(testCase)).split('.'); 43 | r.cls = path.pop(); 44 | r.pack = path.join('.'); 45 | r.method = 'setup'; 46 | r.assertations = new List(); 47 | r.assertations.add(assertation); 48 | return r; 49 | } 50 | 51 | public function allOk():Bool{ 52 | for(l in assertations) { 53 | switch l{ 54 | case Success(_): 55 | default: return false; 56 | } 57 | } 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/utest/UTest.hx: -------------------------------------------------------------------------------- 1 | package utest; 2 | 3 | #if (haxe_ver < "4.1.0") 4 | #error 'Haxe 4.1.0 or later is required to run UTest' 5 | #end 6 | 7 | /** 8 | * Helper class to quickly generate test cases. 9 | */ 10 | final class UTest { 11 | public static function run(cases : Array, ?callback : ()->Void) { 12 | var runner = new Runner(); 13 | for(eachCase in cases) 14 | runner.addCase(eachCase); 15 | if(null != callback) 16 | runner.onComplete.add(function(_) callback()); 17 | utest.ui.Report.create(runner); 18 | runner.run(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utest/exceptions/AssertFailureException.hx: -------------------------------------------------------------------------------- 1 | package utest.exceptions; 2 | 3 | /** 4 | Thrown on a failed assert if `UTEST_FAILURE_THROW` is defined. 5 | 6 | UTest can throw an unhandled exception instead of adding a failure to the report. 7 | 8 | Enable this behavior with `-D UTEST_FAILURE_THROW`, or by adding `UTEST_FAILURE_THROW` to the environment variables at compile time. 9 | 10 | In this case any exception or failure in test or setup methods will lead to a crash. 11 | Instead of a test report you will see an unhandled exception message with the exception 12 | stack trace (depending on a target platform). 13 | **/ 14 | class AssertFailureException extends UTestException { 15 | } -------------------------------------------------------------------------------- /src/utest/exceptions/UTestException.hx: -------------------------------------------------------------------------------- 1 | package utest.exceptions; 2 | 3 | import haxe.Exception; 4 | 5 | /** 6 | * Exceptions thrown by UTest 7 | */ 8 | class UTestException extends Exception { 9 | 10 | } -------------------------------------------------------------------------------- /src/utest/ui/Report.hx: -------------------------------------------------------------------------------- 1 | package utest.ui; 2 | 3 | import utest.Runner; 4 | import utest.ui.common.IReport; 5 | import utest.ui.common.HeaderDisplayMode; 6 | 7 | class Report { 8 | public static function create(runner : Runner, ?displaySuccessResults : SuccessResultsDisplayMode, ?headerDisplayMode : HeaderDisplayMode) : IReport { 9 | var report:IReport; 10 | #if teamcity 11 | report = new utest.ui.text.TeamcityReport(runner); 12 | #elseif travis 13 | report = new utest.ui.text.PrintReport(runner); 14 | #elseif php 15 | if (php.Lib.isCli()) 16 | report = new utest.ui.text.PrintReport(runner); 17 | else 18 | report = new utest.ui.text.HtmlReport(runner, true); 19 | #elseif nodejs 20 | report = new utest.ui.text.PrintReport(runner); 21 | #elseif js 22 | if(js.Syntax.code("typeof window != 'undefined'")) { 23 | report = new utest.ui.text.HtmlReport(runner, true); 24 | } else 25 | report = new utest.ui.text.PrintReport(runner); 26 | #elseif flash 27 | if(flash.external.ExternalInterface.available) 28 | report = new utest.ui.text.HtmlReport(runner, true); 29 | else 30 | report = new utest.ui.text.PrintReport(runner); 31 | #else 32 | report = new utest.ui.text.PrintReport(runner); 33 | #end 34 | if (null == displaySuccessResults) 35 | report.displaySuccessResults = #if (travis || hidesuccess) NeverShowSuccessResults #else ShowSuccessResultsWithNoErrors #end; 36 | else 37 | report.displaySuccessResults = displaySuccessResults; 38 | 39 | if (null == headerDisplayMode) 40 | report.displayHeader = #if (travis || showheader) AlwaysShowHeader #else ShowHeaderWithResults #end; 41 | else 42 | report.displayHeader = headerDisplayMode; 43 | 44 | return report; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/utest/ui/common/ClassResult.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.common; 2 | 3 | class ClassResult { 4 | var fixtures : Map; 5 | public var className(default, null) : String; 6 | public var setupName(default, null) : String; 7 | public var teardownName(default, null) : String; 8 | public var hasSetup(default, null) : Bool; 9 | public var hasTeardown(default, null) : Bool; 10 | 11 | public var methods(default, null) : Int; 12 | public var stats(default, null) : ResultStats; 13 | 14 | public function new(className : String, setupName : String, teardownName : String) { 15 | fixtures = new Map(); 16 | this.className = className; 17 | this.setupName = setupName; 18 | hasSetup = setupName != null; 19 | this.teardownName = teardownName; 20 | hasTeardown = teardownName != null; 21 | 22 | methods = 0; 23 | stats = new ResultStats(); 24 | } 25 | 26 | public function add(result : FixtureResult) { 27 | if(fixtures.exists(result.methodName)) throw 'invalid duplicated fixture: ${className}.${result.methodName}'; 28 | 29 | stats.wire(result.stats); 30 | 31 | methods++; 32 | fixtures.set(result.methodName, result); 33 | } 34 | 35 | public function get(method : String) 36 | return fixtures.get(method); 37 | 38 | public function exists(method : String) 39 | return fixtures.exists(method); 40 | 41 | public function methodNames(errorsHavePriority = true) : Array { 42 | var names = []; 43 | for(name in fixtures.keys()) 44 | names.push(name); 45 | if(errorsHavePriority) { 46 | var me = this; 47 | names.sort(function(a, b) { 48 | var as = me.get(a).stats; 49 | var bs = me.get(b).stats; 50 | if(as.hasErrors) { 51 | return (!bs.hasErrors) ? -1 : (as.errors == bs.errors ? Reflect.compare(a, b) : Reflect.compare(as.errors, bs.errors)); 52 | } else if(bs.hasErrors) { 53 | return 1; 54 | } else if(as.hasFailures) { 55 | return (!bs.hasFailures) ? -1 : (as.failures == bs.failures ? Reflect.compare(a, b) : Reflect.compare(as.failures, bs.failures)); 56 | } else if(bs.hasFailures) { 57 | return 1; 58 | } else if(as.hasWarnings) { 59 | return (!bs.hasWarnings) ? -1 : (as.warnings == bs.warnings ? Reflect.compare(a, b) : Reflect.compare(as.warnings, bs.warnings)); 60 | } else if(bs.hasWarnings) { 61 | return 1; 62 | } else { 63 | return Reflect.compare(a, b); 64 | } 65 | }); 66 | } else { 67 | names.sort(function(a, b) { 68 | return Reflect.compare(a, b); 69 | }); 70 | } 71 | return names; 72 | } 73 | 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/utest/ui/common/FixtureResult.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.common; 2 | 3 | import utest.Assertation; 4 | 5 | class FixtureResult { 6 | public var methodName(default, null) : String; 7 | public var hasTestError(default, null) : Bool; 8 | public var hasSetupError(default, null) : Bool; 9 | public var hasTeardownError(default, null) : Bool; 10 | public var hasTimeoutError(default, null) : Bool; 11 | public var hasAsyncError(default, null) : Bool; 12 | public var executionTime(default, null) : Float; 13 | 14 | public var stats(default, null) : ResultStats; 15 | 16 | var list(default, null) : List; 17 | public function new(methodName : String, executionTime:Float) { 18 | this.methodName = methodName; 19 | this.executionTime = executionTime; 20 | this.list = new List(); 21 | hasTestError = false; 22 | hasSetupError = false; 23 | hasTeardownError = false; 24 | hasTimeoutError = false; 25 | hasAsyncError = false; 26 | 27 | stats = new ResultStats(); 28 | } 29 | 30 | public function iterator() 31 | return list.iterator(); 32 | 33 | public function add(assertation : Assertation) { 34 | list.add(assertation); 35 | switch(assertation) { 36 | case Assertation.Success(_): 37 | stats.addSuccesses(1); 38 | case Assertation.Failure(_, _): 39 | stats.addFailures(1); 40 | case Assertation.Error(_, _): 41 | stats.addErrors(1); 42 | case Assertation.SetupError(_, _): 43 | stats.addErrors(1); 44 | hasSetupError = true; 45 | case Assertation.TeardownError(_, _): 46 | stats.addErrors(1); 47 | hasTeardownError = true; 48 | case Assertation.TimeoutError(_, _): 49 | stats.addErrors(1); 50 | hasTimeoutError = true; 51 | case Assertation.AsyncError(_, _): 52 | stats.addErrors(1); 53 | hasAsyncError = true; 54 | case Assertation.Warning(_): 55 | stats.addWarnings(1); 56 | case Assertation.Ignore(_): 57 | stats.addIgnores(1); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/utest/ui/common/HeaderDisplayMode.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.common; 2 | 3 | enum HeaderDisplayMode { 4 | AlwaysShowHeader; 5 | NeverShowHeader; 6 | ShowHeaderWithResults; 7 | } 8 | 9 | enum SuccessResultsDisplayMode { 10 | AlwaysShowSuccessResults; 11 | NeverShowSuccessResults; 12 | ShowSuccessResultsWithNoErrors; 13 | } -------------------------------------------------------------------------------- /src/utest/ui/common/IReport.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.common; 2 | 3 | import utest.ui.common.HeaderDisplayMode; 4 | 5 | interface IReport> 6 | { 7 | public var displaySuccessResults : SuccessResultsDisplayMode; 8 | public var displayHeader : HeaderDisplayMode; 9 | public function setHandler(handler : (T) -> Void) : Void; 10 | } -------------------------------------------------------------------------------- /src/utest/ui/common/PackageResult.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.common; 2 | 3 | import utest.Assertation; 4 | import utest.TestResult; 5 | 6 | class PackageResult { 7 | public var packageName(default, null) : String; 8 | public var isEmpty(default,null):Bool = true; 9 | var classes : Map; 10 | var packages : Map; 11 | 12 | public var stats(default, null) : ResultStats; 13 | 14 | public function new(packageName : String) { 15 | this.packageName = packageName; 16 | classes = new Map(); 17 | packages = new Map(); 18 | stats = new ResultStats(); 19 | } 20 | 21 | public function addResult(result : TestResult, flattenPackage : Bool) { 22 | isEmpty = false; 23 | var pack = getOrCreatePackage(result.pack, flattenPackage, this); 24 | var cls = getOrCreateClass(pack, result.cls, result.setup, result.teardown); 25 | var fix = createFixture(result.method, result.assertations, result.executionTime); 26 | cls.add(fix); 27 | } 28 | 29 | public function addClass(result : ClassResult) { 30 | isEmpty = false; 31 | classes.set(result.className, result); 32 | stats.wire(result.stats); 33 | } 34 | 35 | public function addPackage(result : PackageResult) { 36 | isEmpty = false; 37 | packages.set(result.packageName, result); 38 | stats.wire(result.stats); 39 | } 40 | 41 | public function existsPackage(name : String) { 42 | return packages.exists(name); 43 | } 44 | 45 | public function existsClass(name : String) { 46 | return classes.exists(name); 47 | } 48 | 49 | public function getPackage(name : String) { 50 | if (packageName == null && name == "") return this; 51 | return packages.get(name); 52 | } 53 | 54 | public function getClass(name : String) { 55 | return classes.get(name); 56 | } 57 | 58 | public function classNames(errorsHavePriority = true) : Array { 59 | var names = []; 60 | for(name in classes.keys()) 61 | names.push(name); 62 | if(errorsHavePriority) { 63 | var me = this; 64 | names.sort(function(a, b) { 65 | var as = me.getClass(a).stats; 66 | var bs = me.getClass(b).stats; 67 | if(as.hasErrors) { 68 | return (!bs.hasErrors) ? -1 : (as.errors == bs.errors ? Reflect.compare(a, b) : Reflect.compare(as.errors, bs.errors)); 69 | } else if(bs.hasErrors) { 70 | return 1; 71 | } else if(as.hasFailures) { 72 | return (!bs.hasFailures) ? -1 : (as.failures == bs.failures ? Reflect.compare(a, b) : Reflect.compare(as.failures, bs.failures)); 73 | } else if(bs.hasFailures) { 74 | return 1; 75 | } else if(as.hasWarnings) { 76 | return (!bs.hasWarnings) ? -1 : (as.warnings == bs.warnings ? Reflect.compare(a, b) : Reflect.compare(as.warnings, bs.warnings)); 77 | } else if(bs.hasWarnings) { 78 | return 1; 79 | } else { 80 | return Reflect.compare(a, b); 81 | } 82 | }); 83 | } else { 84 | names.sort(function(a, b) { 85 | return Reflect.compare(a, b); 86 | }); 87 | } 88 | return names; 89 | } 90 | 91 | public function packageNames(errorsHavePriority = true) : Array { 92 | var names = []; 93 | if (packageName == null) names.push(""); 94 | for(name in packages.keys()) 95 | names.push(name); 96 | if(errorsHavePriority) { 97 | var me = this; 98 | names.sort(function(a, b) { 99 | var as = me.getPackage(a).stats; 100 | var bs = me.getPackage(b).stats; 101 | if(as.hasErrors) { 102 | return (!bs.hasErrors) ? -1 : (as.errors == bs.errors ? Reflect.compare(a, b) : Reflect.compare(as.errors, bs.errors)); 103 | } else if(bs.hasErrors) { 104 | return 1; 105 | } else if(as.hasFailures) { 106 | return (!bs.hasFailures) ? -1 : (as.failures == bs.failures ? Reflect.compare(a, b) : Reflect.compare(as.failures, bs.failures)); 107 | } else if(bs.hasFailures) { 108 | return 1; 109 | } else if(as.hasWarnings) { 110 | return (!bs.hasWarnings) ? -1 : (as.warnings == bs.warnings ? Reflect.compare(a, b) : Reflect.compare(as.warnings, bs.warnings)); 111 | } else if(bs.hasWarnings) { 112 | return 1; 113 | } else { 114 | return Reflect.compare(a, b); 115 | } 116 | }); 117 | } else { 118 | names.sort(function(a, b) { 119 | return Reflect.compare(a, b); 120 | }); 121 | } 122 | return names; 123 | } 124 | 125 | function createFixture(method : String, assertations : Iterable, executionTime:Float) { 126 | var f = new FixtureResult(method, executionTime); 127 | for(assertation in assertations) 128 | f.add(assertation); 129 | return f; 130 | } 131 | 132 | function getOrCreateClass(pack : PackageResult, cls : String, setup : String, teardown : String) { 133 | if(pack.existsClass(cls)) return pack.getClass(cls); 134 | var c = new ClassResult(cls, setup, teardown); 135 | pack.addClass(c); 136 | return c; 137 | } 138 | 139 | function getOrCreatePackage(pack : String, flat : Bool, ref : PackageResult) { 140 | if(pack == null || pack == '') return ref; 141 | if(flat) { 142 | if(ref.existsPackage(pack)) 143 | return ref.getPackage(pack); 144 | var p = new PackageResult(pack); 145 | ref.addPackage(p); 146 | return p; 147 | } else { 148 | var parts = pack.split('.'); 149 | for(part in parts) { 150 | ref = getOrCreatePackage(part, true, ref); 151 | } 152 | return ref; 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /src/utest/ui/common/ReportTools.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.common; 2 | 3 | class ReportTools 4 | { 5 | public static function hasHeader>(report : IReport, stats : ResultStats) 6 | { 7 | switch(report.displayHeader) 8 | { 9 | case NeverShowHeader: 10 | return false; 11 | case ShowHeaderWithResults: 12 | if (!stats.isOk) 13 | return true; 14 | switch(report.displaySuccessResults) 15 | { 16 | case NeverShowSuccessResults: 17 | return false; 18 | case AlwaysShowSuccessResults, ShowSuccessResultsWithNoErrors: 19 | return true; 20 | } 21 | case AlwaysShowHeader: 22 | return true; 23 | }; 24 | } 25 | 26 | public static function skipResult>(report : IReport, stats : ResultStats, isOk) 27 | { 28 | if (!stats.isOk) return false; 29 | return switch(report.displaySuccessResults) 30 | { 31 | case NeverShowSuccessResults: true; 32 | case AlwaysShowSuccessResults: false; 33 | case ShowSuccessResultsWithNoErrors: !isOk; 34 | }; 35 | } 36 | 37 | public static function hasOutput>(report : IReport, stats : ResultStats) 38 | { 39 | if (!stats.isOk) return true; 40 | return hasHeader(report, stats); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utest/ui/common/ResultAggregator.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.common; 2 | 3 | import utest.Dispatcher; 4 | import utest.Runner; 5 | import utest.TestResult; 6 | 7 | class ResultAggregator { 8 | var runner : Runner; 9 | var flattenPackage : Bool; 10 | public var root(default, null) : PackageResult; 11 | 12 | public var onStart(default, null) : Notifier; 13 | public var onComplete(default, null) : Dispatcher; 14 | public var onProgress(default, null) : Dispatcher<{ done : Int, totals : Int }>; 15 | 16 | public function new(runner : Runner, flattenPackage = false) { 17 | if(runner == null) throw "runner argument is null"; 18 | this.flattenPackage = flattenPackage; 19 | this.runner = runner; 20 | runner.onStart.add(start); 21 | runner.onProgress.add(progress); 22 | runner.onComplete.add(complete); 23 | 24 | onStart = new Notifier(); 25 | onComplete = new Dispatcher(); 26 | onProgress = new Dispatcher(); 27 | } 28 | 29 | function start(runner : Runner) { 30 | root = new PackageResult(null); 31 | onStart.dispatch(); 32 | } 33 | 34 | function getOrCreatePackage(pack : String, flat : Bool, ?ref : PackageResult) { 35 | if(ref == null) ref = root; 36 | if(pack == null || pack == '') return ref; 37 | if(flat) { 38 | if(ref.existsPackage(pack)) 39 | return ref.getPackage(pack); 40 | var p = new PackageResult(pack); 41 | ref.addPackage(p); 42 | return p; 43 | } else { 44 | var parts = pack.split('.'); 45 | for(part in parts) { 46 | ref = getOrCreatePackage(part, true, ref); 47 | } 48 | return ref; 49 | } 50 | } 51 | 52 | function getOrCreateClass(pack : PackageResult, cls : String, setup : String, teardown : String) { 53 | if(pack.existsClass(cls)) return pack.getClass(cls); 54 | var c = new ClassResult(cls, setup, teardown); 55 | pack.addClass(c); 56 | return c; 57 | } 58 | 59 | function createFixture(result : TestResult) { 60 | var f = new FixtureResult(result.method, result.executionTime); 61 | for(assertation in result.assertations) 62 | f.add(assertation); 63 | return f; 64 | } 65 | 66 | function progress(e) { 67 | root.addResult(e.result, flattenPackage); 68 | onProgress.dispatch(e); 69 | } 70 | 71 | function complete(runner : Runner) { 72 | if(root.isEmpty) { 73 | root.addResult(createNoTestsResult(), false); 74 | } 75 | onComplete.dispatch(root); 76 | } 77 | 78 | function createNoTestsResult():TestResult { 79 | var result = new TestResult(); 80 | result.pack = ''; 81 | result.cls = ''; 82 | result.method = ''; 83 | result.assertations = new List(); 84 | var pos = {fileName:'', lineNumber:1, className:'utest.Runner', methodName:'run'}; 85 | result.assertations.add(Failure('No tests executed.', pos)); 86 | return result; 87 | } 88 | } -------------------------------------------------------------------------------- /src/utest/ui/common/ResultStats.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.common; 2 | 3 | import utest.Dispatcher; 4 | 5 | class ResultStats { 6 | public var assertations(default, null) : Int; 7 | public var successes(default, null) : Int; 8 | public var failures(default, null) : Int; 9 | public var errors(default, null) : Int; 10 | public var warnings(default, null) : Int; 11 | public var ignores(default,null) : Int; 12 | 13 | public var onAddSuccesses(default, null) : Dispatcher; 14 | public var onAddFailures(default, null) : Dispatcher; 15 | public var onAddErrors(default, null) : Dispatcher; 16 | public var onAddWarnings(default, null) : Dispatcher; 17 | public var onAddIgnores(default, null) : Dispatcher; 18 | 19 | public var isOk(default, null) : Bool; 20 | public var hasFailures(default, null) : Bool; 21 | public var hasErrors(default, null) : Bool; 22 | public var hasWarnings(default, null) : Bool; 23 | public var hasIgnores(default, null) : Bool; 24 | 25 | public function new() { 26 | assertations = 0; 27 | successes = 0; 28 | failures = 0; 29 | errors = 0; 30 | warnings = 0; 31 | ignores = 0; 32 | 33 | isOk = true; 34 | hasFailures = false; 35 | hasErrors = false; 36 | hasWarnings = false; 37 | hasIgnores = false; 38 | 39 | onAddSuccesses = new Dispatcher(); 40 | onAddFailures = new Dispatcher(); 41 | onAddErrors = new Dispatcher(); 42 | onAddWarnings = new Dispatcher(); 43 | onAddIgnores = new Dispatcher(); 44 | } 45 | 46 | public function addSuccesses(v : Int) { 47 | if(v == 0) return; 48 | assertations += v; 49 | successes += v; 50 | onAddSuccesses.dispatch(v); 51 | } 52 | 53 | public function addFailures(v : Int) { 54 | if(v == 0) return; 55 | assertations += v; 56 | failures += v; 57 | hasFailures = failures > 0; 58 | isOk = !(hasFailures || hasErrors || hasWarnings); 59 | onAddFailures.dispatch(v); 60 | } 61 | 62 | public function addErrors(v : Int) { 63 | if(v == 0) return; 64 | assertations += v; 65 | errors += v; 66 | hasErrors = errors > 0; 67 | isOk = !(hasFailures || hasErrors || hasWarnings); 68 | onAddErrors.dispatch(v); 69 | } 70 | 71 | public function addIgnores(v:Int) { 72 | if (v == 0) return; 73 | assertations += v; 74 | ignores += v; 75 | hasIgnores = ignores > 0; 76 | onAddIgnores.dispatch(v); 77 | } 78 | 79 | public function addWarnings(v : Int) { 80 | if(v == 0) return; 81 | assertations += v; 82 | warnings += v; 83 | hasWarnings = warnings > 0; 84 | isOk = !(hasFailures || hasErrors || hasWarnings); 85 | onAddWarnings.dispatch(v); 86 | } 87 | 88 | public function sum(other : ResultStats) { 89 | addSuccesses(other.successes); 90 | addFailures(other.failures); 91 | addErrors(other.errors); 92 | addWarnings(other.warnings); 93 | addIgnores(other.ignores); 94 | } 95 | 96 | public function subtract(other : ResultStats) { 97 | addSuccesses(-other.successes); 98 | addFailures(-other.failures); 99 | addErrors(-other.errors); 100 | addWarnings(-other.warnings); 101 | addIgnores(-other.ignores); 102 | } 103 | 104 | public function wire(dependant : ResultStats) { 105 | dependant.onAddSuccesses.add(addSuccesses); 106 | dependant.onAddFailures.add(addFailures); 107 | dependant.onAddErrors.add(addErrors); 108 | dependant.onAddWarnings.add(addWarnings); 109 | dependant.onAddIgnores.add(addIgnores); 110 | sum(dependant); 111 | } 112 | 113 | public function unwire(dependant : ResultStats) { 114 | dependant.onAddSuccesses.remove(addSuccesses); 115 | dependant.onAddFailures.remove(addFailures); 116 | dependant.onAddErrors.remove(addErrors); 117 | dependant.onAddWarnings.remove(addWarnings); 118 | dependant.onAddIgnores.remove(addIgnores); 119 | subtract(dependant); 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /src/utest/ui/macro/MacroReport.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.macro; 2 | 3 | #if macro 4 | import haxe.macro.Context; 5 | import neko.Lib; 6 | 7 | #if haxe_211 8 | import haxe.CallStack; 9 | #else 10 | import haxe.Stack; 11 | #end 12 | 13 | import utest.Runner; 14 | import utest.TestResult; 15 | 16 | using StringTools; 17 | 18 | class MacroReport { 19 | public function new(runner : Runner) { 20 | runner.onStart.add(start); 21 | runner.onProgress.add(testDone); 22 | runner.onComplete.add(complete); 23 | } 24 | 25 | var startTime : Float; 26 | var totalTests : Int; 27 | var failedTests : Int; 28 | var testWarnings : Int; 29 | 30 | function start(e) { 31 | totalTests = 0; 32 | failedTests = 0; 33 | testWarnings = 0; 34 | 35 | startTime = haxe.Timer.stamp(); 36 | } 37 | 38 | function dumpStack(stack : Array) { 39 | if (stack.length == 0) 40 | return ""; 41 | 42 | var parts = Stack.toString(stack).split("\n"); 43 | var r = []; 44 | for (part in parts) { 45 | if (part.indexOf(" utest.") >= 0) continue; 46 | r.push(part); 47 | } 48 | return r.join("\n"); 49 | } 50 | 51 | /** 52 | * @todo When macro warnings work, use Context.warning() on Warning assertation. 53 | */ 54 | function testDone(test : { result : TestResult, done : Int, totals : Int } ) { 55 | for (assertation in test.result.assertations) { 56 | totalTests++; 57 | failedTests++; 58 | 59 | switch(assertation) { 60 | case Success(pos): 61 | failedTests--; 62 | case Failure(msg, pos): 63 | trace(pos.fileName + ":" + pos.lineNumber + ": " + msg, Context.currentPos()); 64 | case Error(e, s): 65 | trace(Std.string(e) + ", see output for stack.", Context.currentPos()); 66 | Lib.print(dumpStack(s)); 67 | case SetupError(e, s): 68 | trace(Std.string(e) + ", see output for stack.", Context.currentPos()); 69 | Lib.print(dumpStack(s)); 70 | case TeardownError(e, s): 71 | trace(Std.string(e) + ", see output for stack.", Context.currentPos()); 72 | Lib.print(dumpStack(s)); 73 | case TimeoutError(missedAsyncs, s): 74 | trace("missed async calls: " + missedAsyncs + ", see output for stack.", Context.currentPos()); 75 | Lib.print(dumpStack(s)); 76 | case AsyncError(e, s): 77 | trace(Std.string(e) + ", see output for stack.", Context.currentPos()); 78 | Lib.print(dumpStack(s)); 79 | case Warning(msg): 80 | failedTests--; 81 | testWarnings++; 82 | trace(msg); 83 | } 84 | } 85 | } 86 | 87 | function complete(runner : Runner) { 88 | var end = haxe.Timer.stamp(); 89 | var time = Std.string(Std.int((end - startTime) * 1000) / 1000); 90 | 91 | if (time.endsWith(".")) 92 | time = time.substr(0, -1); 93 | 94 | trace("uTest results: " + totalTests + " tests run, " + failedTests + " failed, " + testWarnings + " warnings. Execution time: " + time + "ms."); 95 | } 96 | } 97 | #end -------------------------------------------------------------------------------- /src/utest/ui/text/DiagnosticsReport.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.text; 2 | 3 | import haxe.CallStack; 4 | import utest.Runner; 5 | import utest.ui.common.PackageResult; 6 | 7 | using utest.ui.common.ReportTools; 8 | 9 | #if (sys || nodejs) 10 | class DiagnosticsReport extends PlainTextReport { 11 | public function new(runner:Runner) { 12 | super(runner, handleResults); 13 | newline = "\n"; 14 | indent = " "; 15 | } 16 | 17 | function printStats(result:PackageResult) { 18 | if (!this.hasHeader(result.stats)) 19 | return; 20 | 21 | var end = getTime(); 22 | var time = Std.int((end - startTime) * 1000) / 1000; 23 | 24 | Sys.println('assertions: ${result.stats.successes}/${result.stats.assertations} (${time}s)'); 25 | if (!result.stats.isOk) { 26 | Sys.println('errors: ${result.stats.errors} - failures: ${result.stats.failures} - warnings: ${result.stats.warnings}'); 27 | } 28 | } 29 | 30 | override function dumpStack(stack:CallStack) { 31 | if (stack.length == 0) { 32 | return ""; 33 | } 34 | 35 | var buf:StringBuf = new StringBuf(); 36 | buf.add(newline); 37 | for (item in stack) { 38 | buf.add(indents(1) + "Called from "); 39 | buf.add(stackItemToString(item)); 40 | buf.add(newline); 41 | } 42 | return buf.toString(); 43 | } 44 | 45 | function stackItemToString(item:Null):String { 46 | return switch (item) { 47 | case null: 48 | ""; 49 | case CFunction: 50 | "a C function"; 51 | case Module(m): 52 | 'module $m'; 53 | case FilePos(s, file, line, column): 54 | '${stackItemToString(s)} ($file:$line:$column:)'; 55 | case Method(null, method): 56 | '.$method'; 57 | case Method(classname, method): 58 | '$classname.$method'; 59 | case LocalFunction(v): 60 | 'local function #$v'; 61 | } 62 | } 63 | 64 | function handleResults(report:PlainTextReport) { 65 | var messages:Array = []; 66 | for (pname in result.packageNames()) { 67 | var pack = result.getPackage(pname); 68 | if (this.skipResult(pack.stats, result.stats.isOk)) 69 | continue; 70 | for (cname in pack.classNames()) { 71 | var cls = pack.getClass(cname); 72 | if (this.skipResult(cls.stats, result.stats.isOk)) 73 | continue; 74 | Sys.println((pname == '' ? '' : pname + ".") + cname); 75 | for (mname in cls.methodNames()) { 76 | var fix = cls.get(mname); 77 | if (this.skipResult(fix.stats, result.stats.isOk)) 78 | continue; 79 | Sys.print(indents(1) + mname + ": "); 80 | if (fix.stats.isOk) { 81 | Sys.print("OK "); 82 | } else if (fix.stats.hasErrors) { 83 | Sys.print("ERROR "); 84 | } else if (fix.stats.hasFailures) { 85 | Sys.print("FAILURE "); 86 | } else if (fix.stats.hasWarnings) { 87 | Sys.print("WARNING "); 88 | } 89 | for (assertation in fix.iterator()) { 90 | switch (assertation) { 91 | case Success(_): 92 | Sys.print('.'); 93 | case Failure(msg, pos): 94 | Sys.print('F'); 95 | messages.push('${pos.fileName}:${pos.lineNumber}: Test failed: $msg'); 96 | case Error(e, s): 97 | Sys.print('E'); 98 | messages.push(Std.string(e) + dumpStack(s)); 99 | case SetupError(e, s): 100 | Sys.print('S'); 101 | messages.push(Std.string(e) + dumpStack(s)); 102 | case TeardownError(e, s): 103 | Sys.print('T'); 104 | messages.push(Std.string(e) + dumpStack(s)); 105 | case TimeoutError(missedAsyncs, s): 106 | Sys.print('O'); 107 | messages.push("missed async calls: " + missedAsyncs + dumpStack(s)); 108 | case AsyncError(e, s): 109 | Sys.print('A'); 110 | messages.push(Std.string(e) + dumpStack(s)); 111 | case Warning(msg): 112 | Sys.print('W'); 113 | messages.push(msg); 114 | case Ignore(reason): 115 | Sys.print('I'); 116 | if (reason != null && reason != "") { 117 | messages.push('With reason: ${reason}'); 118 | } 119 | } 120 | } 121 | Sys.println(""); 122 | } 123 | } 124 | } 125 | Sys.println(""); 126 | printStats(result); 127 | if (messages.length > 0) { 128 | Sys.println(""); 129 | for (message in messages) { 130 | Sys.println(message); 131 | } 132 | Sys.println(""); 133 | } 134 | } 135 | } 136 | #end 137 | -------------------------------------------------------------------------------- /src/utest/ui/text/HtmlReport.hx: -------------------------------------------------------------------------------- 1 | package utest.ui.text; 2 | 3 | import haxe.PosInfos; 4 | import haxe.Timer; 5 | import utest.ui.common.ClassResult; 6 | import utest.ui.common.FixtureResult; 7 | import utest.ui.common.IReport; 8 | import utest.ui.common.HeaderDisplayMode; 9 | import utest.Runner; 10 | import utest.ui.common.ResultAggregator; 11 | import utest.ui.common.PackageResult; 12 | import utest.ui.common.ResultStats; 13 | 14 | import haxe.CallStack; 15 | 16 | using utest.ui.common.ReportTools; 17 | 18 | #if php 19 | import php.Lib; 20 | #elseif neko 21 | import neko.Lib; 22 | #elseif cpp 23 | import cpp.Lib; 24 | #elseif js 25 | import js.Browser; 26 | #end 27 | 28 | class HtmlReport implements IReport { 29 | static var platform = #if neko 'neko' #elseif php 'php' #elseif cpp 'cpp' #elseif js 'javascript' #elseif flash 'flash' #else 'unknown' #end; 30 | 31 | public var traceRedirected(default, null) : Bool; 32 | public var displaySuccessResults : SuccessResultsDisplayMode; 33 | public var displayHeader : HeaderDisplayMode; 34 | public var handler : (HtmlReport) -> Void; 35 | 36 | var aggregator : ResultAggregator; 37 | var oldTrace : Any; 38 | var _traces : Array<{ msg : String, infos : PosInfos, time : Float, delta : Float, stack : CallStack }>; 39 | 40 | public function new(runner : Runner, ?outputHandler : (HtmlReport) -> Void, traceRedirected = true) { 41 | aggregator = new ResultAggregator(runner, true); 42 | runner.onStart.add(start); 43 | aggregator.onComplete.add(complete); 44 | if (null == outputHandler) 45 | setHandler(_handler); 46 | else 47 | setHandler(outputHandler); 48 | if (traceRedirected) 49 | redirectTrace(); 50 | displaySuccessResults = AlwaysShowSuccessResults; 51 | displayHeader = AlwaysShowHeader; 52 | } 53 | 54 | public function setHandler(handler : (HtmlReport) -> Void) : Void 55 | this.handler = handler; 56 | 57 | public function redirectTrace() { 58 | if (traceRedirected) 59 | return; 60 | _traces = []; 61 | oldTrace = haxe.Log.trace; 62 | haxe.Log.trace = _trace; 63 | } 64 | 65 | public function restoreTrace() { 66 | if (!traceRedirected) 67 | return; 68 | haxe.Log.trace = oldTrace; 69 | } 70 | 71 | var _traceTime : Null; 72 | function _trace(v : Any, ?infos : PosInfos) { 73 | var time = Timer.stamp(); 74 | var delta = _traceTime == null ? 0 : time - _traceTime; 75 | _traces.push({ 76 | msg : StringTools.htmlEscape(Std.string(v)), 77 | infos : infos, 78 | time : time - startTime, 79 | delta : delta, 80 | stack : CallStack.callStack() 81 | }); 82 | _traceTime = Timer.stamp(); 83 | } 84 | 85 | var startTime : Float; 86 | function start(e) 87 | startTime = Timer.stamp(); 88 | 89 | function cls(stats : ResultStats) { 90 | if (stats.hasErrors) 91 | return 'error'; 92 | else if (stats.hasFailures) 93 | return 'failure'; 94 | else if (stats.hasWarnings) 95 | return 'warn'; 96 | else 97 | return 'ok'; 98 | } 99 | 100 | function resultNumbers(buf : StringBuf, stats : ResultStats) { 101 | var numbers = []; 102 | if (stats.assertations == 1) 103 | numbers.push('1 test'); 104 | else 105 | numbers.push('' + stats.assertations + ' tests'); 106 | 107 | if (stats.successes != stats.assertations) 108 | { 109 | if (stats.successes == 1) 110 | numbers.push('1 pass'); 111 | else if (stats.successes > 0) 112 | numbers.push('' + stats.successes + ' passes'); 113 | } 114 | 115 | if (stats.errors == 1) 116 | numbers.push('1 error'); 117 | else if (stats.errors > 0) 118 | numbers.push('' + stats.errors + ' errors'); 119 | 120 | if (stats.failures == 1) 121 | numbers.push('1 failure'); 122 | else if (stats.failures > 0) 123 | numbers.push('' + stats.failures + ' failures'); 124 | 125 | if (stats.warnings == 1) 126 | numbers.push('1 warning'); 127 | else if (stats.warnings > 0) 128 | numbers.push('' + stats.warnings + ' warnings'); 129 | 130 | buf.add(numbers.join(', ')); 131 | } 132 | 133 | function blockNumbers(buf : StringBuf, stats : ResultStats) { 134 | buf.add('
'); 135 | resultNumbers(buf, stats); 136 | buf.add('
'); 137 | } 138 | 139 | function formatStack(stack : CallStack, addNL = true) { 140 | var parts = []; 141 | var nl = addNL ? '\n' : ''; 142 | var last = null; 143 | var count = 1; 144 | for (part in CallStack.toString(stack).split('\n')) { 145 | if (StringTools.trim(part) == '') 146 | continue; 147 | if ( -1 < part.indexOf('Called from utest.')) 148 | continue; 149 | #if neko 150 | if ( -1 < part.indexOf('Called from a C function')) 151 | continue; 152 | #end 153 | if (part == last) { 154 | parts[parts.length - 1] = part + " (#" + (++count) + ")"; 155 | } else { 156 | count = 1; 157 | parts.push(last = part); 158 | } 159 | } 160 | 161 | var s = '
  • ' + parts.join('
  • '+nl+'
  • ') + '
'+nl; 162 | return "
" + s + "
"+nl; 163 | } 164 | 165 | function addFixture(buf : StringBuf, result : FixtureResult, name : String, isOk : Bool) { 166 | if (this.skipResult(result.stats, isOk)) return; 167 | buf.add('
  • '); 168 | buf.add(''); 169 | if(result.stats.isOk) { 170 | buf.add("OK "); 171 | } else if(result.stats.hasErrors) { 172 | buf.add("ERROR "); 173 | } else if(result.stats.hasFailures) { 174 | buf.add("FAILURE "); 175 | } else if(result.stats.hasWarnings) { 176 | buf.add("WARNING "); 177 | } 178 | buf.add(''); 179 | buf.add('
    '); 180 | buf.add('' + name + ''); 181 | buf.add(': '); 182 | resultNumbers(buf, result.stats); 183 | var messages = []; 184 | for(assertation in result.iterator()) { 185 | switch(assertation) { 186 | case Success(_): 187 | case Failure(msg, pos): 188 | messages.push("line " + pos.lineNumber + ": " + StringTools.htmlEscape(msg) + ""); 189 | case Error(e, s): 190 | messages.push("error: " + getErrorDescription(e) + "\n
    stack:" + getErrorStack(s, e)); 191 | case SetupError(e, s): 192 | messages.push("setup error: " + getErrorDescription(e) + "\n
    stack:" + getErrorStack(s, e)); 193 | case TeardownError(e, s): 194 | messages.push("tear-down error: " + getErrorDescription(e) + "\n
    stack:" + getErrorStack(s, e)); 195 | case TimeoutError(missedAsyncs, _): 196 | messages.push("missed async call(s): " + missedAsyncs); 197 | case AsyncError(e, s): 198 | messages.push("async error: " + getErrorDescription(e) + "\n
    stack:" + getErrorStack(s, e)); 199 | case Warning(msg): 200 | messages.push(StringTools.htmlEscape(msg)); 201 | case Ignore(reason): 202 | messages.push(StringTools.htmlEscape(reason)); 203 | 204 | } 205 | } 206 | if (messages.length > 0) 207 | { 208 | buf.add('
    '); 209 | buf.add(messages.join('
    ')); 210 | buf.add('
    \n'); 211 | } 212 | buf.add('
    \n'); 213 | buf.add('
  • \n'); 214 | } 215 | 216 | function getErrorDescription(e : Any) { 217 | #if flash9 218 | if (Std.isOfType(e, flash.errors.Error)) { 219 | var err = cast(e, flash.errors.Error); 220 | return err.name + ": " + err.message; 221 | } else { 222 | return Std.string(e); 223 | } 224 | #else 225 | return Std.string(e); 226 | #end 227 | } 228 | 229 | function getErrorStack(s : CallStack, e : Any) { 230 | #if flash9 231 | if (Std.isOfType(e, flash.errors.Error)) { 232 | var stack = cast(e, flash.errors.Error).getStackTrace(); 233 | if (null != stack) { 234 | var parts = stack.split("\n"); 235 | // cleanup utest calls 236 | var result = []; 237 | for (p in parts) 238 | if (p.indexOf("at utest::") < 0) 239 | result.push(p); 240 | // pops the last 2 calls 241 | result.pop(); 242 | result.pop(); 243 | return result.join("
    "); 244 | } 245 | } 246 | return formatStack(s); 247 | #else 248 | return formatStack(s); 249 | #end 250 | } 251 | 252 | function addClass(buf : StringBuf, result : ClassResult, name : String, isOk : Bool) { 253 | if (this.skipResult(result.stats, isOk)) return; 254 | buf.add('
  • '); 255 | buf.add('

    ' + name + '

    '); 256 | blockNumbers(buf, result.stats); 257 | buf.add('
      \n'); 258 | for (mname in result.methodNames()) { 259 | addFixture(buf, result.get(mname), mname, isOk); 260 | } 261 | buf.add('
    \n'); 262 | buf.add('
  • \n'); 263 | } 264 | 265 | function addPackages(buf : StringBuf, result : PackageResult, isOk : Bool) { 266 | if (this.skipResult(result.stats, isOk)) return; 267 | buf.add('
      \n'); 268 | for (name in result.packageNames(false)) { 269 | addPackage(buf, result.getPackage(name), name, isOk); 270 | } 271 | buf.add('
    \n'); 272 | } 273 | 274 | function addPackage(buf : StringBuf, result : PackageResult, name : String, isOk : Bool) { 275 | if (this.skipResult(result.stats, isOk)) return; 276 | if (name == '' && result.classNames().length == 0) return; 277 | buf.add('
  • '); 278 | buf.add('

    ' + name + '

    '); 279 | blockNumbers(buf, result.stats); 280 | buf.add('
      \n'); 281 | for (cname in result.classNames()) 282 | addClass(buf, result.getClass(cname), cname, isOk); 283 | buf.add('
    \n'); 284 | buf.add('
  • \n'); 285 | } 286 | 287 | public function getTextResults() : String { 288 | var newline = "\n"; 289 | function indents(count : Int) { 290 | return [for(i in 0...count) " "].join(""); 291 | } 292 | function dumpStack(stack : CallStack) { 293 | if (stack.length == 0) 294 | return ""; 295 | var parts = CallStack.toString(stack).split("\n"), 296 | r = []; 297 | for (part in parts) { 298 | if (part.indexOf(" utest.") >= 0) continue; 299 | r.push(part); 300 | } 301 | return r.join(newline); 302 | } 303 | var buf = new StringBuf(); 304 | for(pname in result.packageNames()) { 305 | var pack = result.getPackage(pname); 306 | if (this.skipResult(pack.stats, result.stats.isOk)) continue; 307 | for(cname in pack.classNames()) { 308 | var cls = pack.getClass(cname); 309 | if (this.skipResult(cls.stats, result.stats.isOk)) continue; 310 | buf.add((pname == '' ? '' : pname+".")+cname+newline); 311 | for(mname in cls.methodNames()) { 312 | var fix = cls.get(mname); 313 | if (this.skipResult(fix.stats, result.stats.isOk)) continue; 314 | buf.add(indents(1)+mname+": "); 315 | if(fix.stats.isOk) { 316 | buf.add("OK "); 317 | } else if(fix.stats.hasErrors) { 318 | buf.add("ERROR "); 319 | } else if(fix.stats.hasFailures) { 320 | buf.add("FAILURE "); 321 | } else if(fix.stats.hasWarnings) { 322 | buf.add("WARNING "); 323 | } 324 | var messages = ''; 325 | for(assertation in fix.iterator()) { 326 | switch(assertation) { 327 | case Success(_): 328 | buf.add('.'); 329 | case Failure(msg, pos): 330 | buf.add('F'); 331 | messages += indents(2)+"line: " + pos.lineNumber + ", " + msg + newline; 332 | case Error(e, s): 333 | buf.add('E'); 334 | messages += indents(2)+ Std.string(e) + dumpStack(s) + newline ; 335 | case SetupError(e, s): 336 | buf.add('S'); 337 | messages += indents(2)+ Std.string(e) + dumpStack(s) + newline; 338 | case TeardownError(e, s): 339 | buf.add('T'); 340 | messages += indents(2)+ Std.string(e) + dumpStack(s) + newline; 341 | case TimeoutError(missedAsyncs, s): 342 | buf.add('O'); 343 | messages += indents(2)+ "missed async calls: " + missedAsyncs + dumpStack(s) + newline; 344 | case AsyncError(e, s): 345 | buf.add('A'); 346 | messages += indents(2)+ Std.string(e) + dumpStack(s) + newline; 347 | case Warning(msg): 348 | buf.add('W'); 349 | messages += indents(2)+ msg + newline; 350 | case Ignore(reason): 351 | buf.add('I'); 352 | if (reason != null && reason != "") { 353 | messages += indents(2) + 'With reason: ${reason}' + newline; 354 | } 355 | } 356 | } 357 | buf.add(newline); 358 | buf.add(messages); 359 | } 360 | } 361 | } 362 | return buf.toString(); 363 | } 364 | 365 | 366 | public function getHeader() : String { 367 | var buf = new StringBuf(); 368 | if (!this.hasHeader(result.stats)) 369 | return ""; 370 | 371 | var end = haxe.Timer.stamp(); 372 | var time = Std.int((end-startTime)*1000)/1000; 373 | var msg = 'TEST OK'; 374 | if (result.stats.hasErrors) 375 | msg = 'TEST ERRORS'; 376 | else if (result.stats.hasFailures) 377 | msg = 'TEST FAILED'; 378 | else if (result.stats.hasWarnings) 379 | msg = 'WARNING REPORTED'; 380 | 381 | buf.add('

    ' + msg + "

    \n"); 382 | buf.add('
    '); 383 | 384 | resultNumbers(buf, result.stats); 385 | buf.add(' performed on ' + platform + ', executed in ' + time + ' sec.
    \n '); 386 | return buf.toString(); 387 | } 388 | 389 | public function getTrace() : String { 390 | var buf = new StringBuf(); 391 | if (_traces == null || _traces.length == 0) 392 | return ""; 393 | buf.add('

    traces

      '); 394 | for (t in _traces) { 395 | buf.add('
    1. '); 396 | var stack = StringTools.replace(formatStack(t.stack, false), "'", "\\'"); 397 | var method = '' + t.infos.className + "
      " + t.infos.methodName + "(" + t.infos.lineNumber + ")"; 398 | buf.add(''); 399 | buf.add(method); 400 | 401 | // time 402 | buf.add(''); 403 | buf.add("@ " + formatTime(t.time)); 404 | if(Math.round(t.delta * 1000) > 0) 405 | buf.add(", ~" + formatTime(t.delta)); 406 | 407 | buf.add(''); 408 | buf.add(StringTools.replace(StringTools.trim(t.msg), "\n", "
      \n")); 409 | 410 | buf.add('
    2. '); 411 | } 412 | buf.add('
    '); 413 | return buf.toString(); 414 | } 415 | 416 | public function getResults() : String { 417 | var buf = new StringBuf(); 418 | addPackages(buf, result, result.stats.isOk); 419 | return buf.toString(); 420 | } 421 | 422 | public function getAll() : String { 423 | if (!this.hasOutput(result.stats)) 424 | return ""; 425 | else 426 | return getHeader() + getTrace() + getResults(); 427 | } 428 | 429 | public function getHtml(?title : String) : String { 430 | if(null == title) 431 | title = "utest: " + platform; 432 | var s = getAll(); 433 | if ('' == s) 434 | return ''; 435 | else 436 | return wrapHtml(title, s); 437 | } 438 | 439 | var result : PackageResult; 440 | function complete(result : PackageResult) { 441 | this.result = result; 442 | handler(this); 443 | restoreTrace(); 444 | 445 | var exposedResult = { 446 | isOk : result.stats.isOk, 447 | message : getTextResults() 448 | }; 449 | 450 | #if js 451 | if(js.Syntax.code("'undefined' != typeof window")) { 452 | js.Syntax.code("window").utest_result = exposedResult; 453 | } 454 | #elseif flash 455 | flash.external.ExternalInterface.call('(function(result){ window.utest_result = result; })', exposedResult ); 456 | #end 457 | } 458 | 459 | function formatTime(t : Float) 460 | return Math.round(t * 1000) + " ms"; 461 | 462 | function cssStyle() 463 | return 'body, dd, dt { 464 | font-family: Verdana, Arial, Sans-serif; 465 | font-size: 12px; 466 | } 467 | dl { 468 | width: 180px; 469 | } 470 | dd, dt { 471 | margin : 0; 472 | padding : 2px 5px; 473 | border-top: 1px solid #f0f0f0; 474 | border-left: 1px solid #f0f0f0; 475 | border-right: 1px solid #CCCCCC; 476 | border-bottom: 1px solid #CCCCCC; 477 | } 478 | dd.value { 479 | text-align: center; 480 | background-color: #eeeeee; 481 | } 482 | dt { 483 | text-align: left; 484 | background-color: #e6e6e6; 485 | float: left; 486 | width: 100px; 487 | } 488 | 489 | h1, h2, h3, h4, h5, h6 { 490 | margin: 0; 491 | padding: 0; 492 | } 493 | 494 | h1 { 495 | text-align: center; 496 | font-weight: bold; 497 | padding: 5px 0 4px 0; 498 | font-family: Arial, Sans-serif; 499 | font-size: 18px; 500 | border-top: 1px solid #f0f0f0; 501 | border-left: 1px solid #f0f0f0; 502 | border-right: 1px solid #CCCCCC; 503 | border-bottom: 1px solid #CCCCCC; 504 | margin: 0 2px 0px 2px; 505 | } 506 | 507 | h2 { 508 | font-weight: bold; 509 | padding: 2px 0 2px 8px; 510 | font-family: Arial, Sans-serif; 511 | font-size: 13px; 512 | border-top: 1px solid #f0f0f0; 513 | border-left: 1px solid #f0f0f0; 514 | border-right: 1px solid #CCCCCC; 515 | border-bottom: 1px solid #CCCCCC; 516 | margin: 0 0 0px 0; 517 | background-color: #FFFFFF; 518 | color: #777777; 519 | } 520 | 521 | h2.classname { 522 | color: #000000; 523 | } 524 | 525 | .okbg { 526 | background-color: #66FF55; 527 | } 528 | .errorbg { 529 | background-color: #CC1100; 530 | } 531 | .failurebg { 532 | background-color: #EE3322; 533 | } 534 | .warnbg { 535 | background-color: #FFCC99; 536 | } 537 | .headerinfo { 538 | text-align: right; 539 | font-size: 11px; 540 | font - color: 0xCCCCCC; 541 | margin: 0 2px 5px 2px; 542 | border-left: 1px solid #f0f0f0; 543 | border-right: 1px solid #CCCCCC; 544 | border-bottom: 1px solid #CCCCCC; 545 | padding: 2px; 546 | } 547 | 548 | li { 549 | padding: 4px; 550 | margin: 2px; 551 | border-top: 1px solid #f0f0f0; 552 | border-left: 1px solid #f0f0f0; 553 | border-right: 1px solid #CCCCCC; 554 | border-bottom: 1px solid #CCCCCC; 555 | background-color: #e6e6e6; 556 | } 557 | 558 | li.fixture { 559 | background-color: #f6f6f6; 560 | padding-bottom: 6px; 561 | } 562 | 563 | div.fixturedetails { 564 | padding-left: 108px; 565 | } 566 | 567 | ul { 568 | padding: 0; 569 | margin: 6px 0 0 0; 570 | list-style-type: none; 571 | } 572 | 573 | ol { 574 | padding: 0 0 0 28px; 575 | margin: 0px 0 0 0; 576 | } 577 | 578 | .statnumbers { 579 | padding: 2px 8px; 580 | } 581 | 582 | .fixtureresult { 583 | width: 100px; 584 | text-align: center; 585 | display: block; 586 | float: left; 587 | font-weight: bold; 588 | padding: 1px; 589 | margin: 0 0 0 0; 590 | } 591 | 592 | .testoutput { 593 | border: 1px dashed #CCCCCC; 594 | margin: 4px 0 0 0; 595 | padding: 4px 8px; 596 | background-color: #eeeeee; 597 | } 598 | 599 | span.tracepos, span.traceposempty { 600 | display: block; 601 | float: left; 602 | font-weight: bold; 603 | font-size: 9px; 604 | width: 170px; 605 | margin: 2px 0 0 2px; 606 | } 607 | 608 | span.tracepos:hover { 609 | cursor : pointer; 610 | background-color: #ffff99; 611 | } 612 | 613 | span.tracemsg { 614 | display: block; 615 | margin-left: 180px; 616 | background-color: #eeeeee; 617 | padding: 7px; 618 | } 619 | 620 | span.tracetime { 621 | display: block; 622 | float: right; 623 | margin: 2px; 624 | font-size: 9px; 625 | color: #777777; 626 | } 627 | 628 | 629 | div.trace ol { 630 | padding: 0 0 0 40px; 631 | color: #777777; 632 | } 633 | 634 | div.trace li { 635 | padding: 0; 636 | } 637 | 638 | div.trace li div.li { 639 | color: #000000; 640 | } 641 | 642 | div.trace h2 { 643 | margin: 0 2px 0px 2px; 644 | padding-left: 4px; 645 | } 646 | 647 | .tracepackage { 648 | color: #777777; 649 | font-weight: normal; 650 | } 651 | 652 | .clr { 653 | clear: both; 654 | } 655 | 656 | #utesttip { 657 | margin-top: -3px; 658 | margin-left: 170px; 659 | font-size: 9px; 660 | } 661 | 662 | #utesttip li { 663 | margin: 0; 664 | background-color: #ffff99; 665 | padding: 2px 4px; 666 | border: 0; 667 | border-bottom: 1px dashed #ffff33; 668 | }'; 669 | 670 | function jsScript() 671 | return 672 | 'function utestTooltip(ref, text) { 673 | var el = document.getElementById("utesttip"); 674 | if(!el) { 675 | var el = document.createElement("div") 676 | el.id = "utesttip"; 677 | el.style.position = "absolute"; 678 | document.body.appendChild(el) 679 | } 680 | var p = utestFindPos(ref); 681 | el.style.left = (4 + p[0]) + "px"; 682 | el.style.top = (p[1] - 1) + "px"; 683 | el.innerHTML = text; 684 | } 685 | 686 | function utestFindPos(el) { 687 | var left = 0; 688 | var top = 0; 689 | do { 690 | left += el.offsetLeft; 691 | top += el.offsetTop; 692 | } while(el = el.offsetParent) 693 | return [left, top]; 694 | } 695 | 696 | function utestRemoveTooltip() { 697 | var el = document.getElementById("utesttip") 698 | if(el) 699 | document.body.removeChild(el) 700 | }'; 701 | 702 | function wrapHtml(title : String, s : String) 703 | return 704 | '\n\n' + title + ' 705 | 706 |