From 424a7182a93057730fada54b9d27d90b3cb7065c Mon Sep 17 00:00:00 2001 From: Aleksandr Kuzmenko Date: Thu, 30 Nov 2023 12:07:33 +0300 Subject: [PATCH] UTest 2 (#124) * Cleared out haxe_ver conditionals * enable analyzer * Require test cases to implement ITest * Drop support for non-ITest instances * [ci] setup github-actions with travix * [nodejs] exit gracefully * [ci] don't mklink python * [ci] fix script * Revert "[nodejs] exit gracefully" This reverts commit 44faf6e0c1af3dffd720616d68662e21216ea024. * try with travix * nope * [ci] disable development Haxe for now * [ci] don't fail early for now * [ci][js] disable non-nodejs test * [ci] how about this? * [ci] disable faulty steps for now * [ci] disable more steps * modernized esception handling * version change * got rid of Dynamic * fixes for Haxe 4.3.1 & 4.3.2 * modernized callback signature syntax * added test case name filter to `Runner.addCases` * added defines.json and meta.json * ci setup for minified js * moved closure-related code to js-minified-header * [ci] less verbose * typo * removed Misc.isOfType * ignoring tests with @:ignore meta * [js] fixes for advansed js minification * cleanup * UTEST_IGNORE_DEPENDS * cleanup * [ci] throw unhandled exception in minified js instead of exit code 1 * minor * [js][min] fix Assert.same for strings * [ci] run PHP tests without travix * [ci] don't test php with Haxe 4.1 * [ci] enable lua * removed reference to neko.Web module because it gets removed in Haxe 5 * [ci] enable hashlink * [ci] update Haxe 4.3 to 4.3.3 * [ci] enable python * Revert "[ci] enable python" because it's already enabled This reverts commit 1009b8220242f43df4a54bc521fc9a0161dc8680. * [ci] disable hl tests * [ci] disable lua for Haxe 4.3 * [ci] run lua on ubuntu only * [ci] try 'latest' haxe * [ci] fix * [ci] don't run lua on the latest haxe * Assert.similar (closes #126) * leftovers of php.Web and neko.Web * PosException does not exist in Haxe 4.1 * removed the last reference to neko.Web * fix Assert.raises for ValueException * TestAssert refactoring * uninline some things to overcome lua limitation of 200 local vars * test for Assert.similar * changelog * test Assert.similar --- .github/workflows/build.yml | 126 +++++ .sauce-browsers.json | 33 -- .travis.yml | 33 -- CHANGELOG.md | 11 + README.md | 29 + appveyor.yml.disabled | 56 -- defines.json | 19 + extraParams.hxml | 1 - haxelib.json | 10 +- hxml/appveyor.hxml | 2 - hxml/common.hxml | 1 + hxml/js-minified.hxml | 10 + hxml/travis.hxml | 2 - meta.json | 19 + src/utest/Assert.hx | 311 +++++++---- src/utest/Assertation.hx | 10 +- src/utest/Async.hx | 10 +- src/utest/Dispatcher.hx | 30 +- src/utest/ITest.hx | 4 - src/utest/ITestHandler.hx | 126 ----- src/utest/MacroRunner.hx | 9 +- src/utest/Runner.hx | 211 +++---- src/utest/TestData.hx | 19 +- src/utest/TestFixture.hx | 72 +-- src/utest/TestHandler.hx | 279 ++++------ src/utest/TestResult.hx | 28 +- src/utest/UTest.hx | 8 +- .../exceptions/AssertFailureException.hx | 15 + src/utest/exceptions/UTestException.hx | 10 + src/utest/ui/Report.hx | 14 +- src/utest/ui/common/IReport.hx | 4 +- src/utest/ui/common/ReportTools.hx | 6 +- src/utest/ui/common/ResultAggregator.hx | 24 - src/utest/ui/text/DiagnosticsReport.hx | 2 +- src/utest/ui/text/HtmlReport.hx | 35 +- src/utest/ui/text/PlainTextReport.hx | 26 +- src/utest/ui/text/PrintReport.hx | 2 +- src/utest/ui/text/TeamcityReport.hx | 2 +- src/utest/utils/AccessoriesUtils.hx | 8 +- src/utest/utils/Macro.hx | 7 - src/utest/utils/Misc.hx | 11 - src/utest/utils/Print.hx | 6 +- src/utest/utils/TestBuilder.hx | 57 +- submit.sh | 2 +- test/TestAll.hx | 42 +- test/js-minified-header.js | 6 + test/utest/TestAssert.hx | 527 +++++++++--------- test/utest/TestDispatcher.hx | 3 +- test/utest/TestIgnored.hx | 28 +- test/utest/TestRunner.hx | 19 +- test/utest/testrunner/TestDummy1.hx | 2 +- test/utest/testrunner/TestDummy2.hx | 2 +- test/utest/testrunner/UnusualTestDummy.hx | 10 + test/utest/testrunner/sub/TestDummy3.hx | 2 +- tests.hxml | 2 +- 55 files changed, 1119 insertions(+), 1224 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .sauce-browsers.json delete mode 100644 .travis.yml delete mode 100644 appveyor.yml.disabled create mode 100644 defines.json delete mode 100644 hxml/appveyor.hxml create mode 100644 hxml/js-minified.hxml delete mode 100644 hxml/travis.hxml create mode 100644 meta.json delete mode 100644 src/utest/ITestHandler.hx create mode 100644 src/utest/exceptions/AssertFailureException.hx create mode 100644 src/utest/exceptions/UTestException.hx delete mode 100644 src/utest/utils/Misc.hx create mode 100644 test/js-minified-header.js create mode 100644 test/utest/testrunner/UnusualTestDummy.hx diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ba8f887 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,126 @@ +# https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions +name: Build + +on: + push: + paths-ignore: + - '**/*.md' + pull_request: + workflow_dispatch: + # https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/ + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + haxe: + - latest + - 4.3.3 + - 4.2.5 + - 4.1.5 + + steps: + - name: Show environment variables + shell: bash + run: env | sort + + - name: Git Checkout + uses: actions/checkout@v2 #https://github.com/actions/checkout + + - name: "Cache Haxelib Repository" + uses: actions/cache@v2 + with: + path: $RUNNER_TOOL_CACHE/haxe/${{ matrix.haxe }}/x64/lib + key: ${{ runner.os }}-haxelib-${{ hashFiles('**/haxelib.json') }} + restore-keys: | + ${{ runner.os }}-haxelib- + + - name: Upgrade brew + if: runner.os == 'macOS' + env: + # https://docs.brew.sh/Manpage#environment + HOMEBREW_NO_ANALYTICS: 1 + HOMEBREW_NO_INSTALL_CLEANUP: 1 + run: | + echo "::group::brew update" && brew update && echo "::endgroup::" + echo "::group::brew config" && brew config && echo "::endgroup::" + + # workaround to prevent "/usr/local/... is not inside a keg" during "brew install mono" + rm /usr/local/bin/2to3 + rm /usr/local/share/man/man1/* + rm /usr/local/share/man/man5/* + + - name: Set up Python 3 + uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + python-version: 3.8 + + - name: Install Haxe ${{ matrix.haxe }} + uses: krdlab/setup-haxe@v1 # https://github.com/krdlab/setup-haxe + with: + haxe-version: ${{ matrix.haxe }} + + - name: Install haxe libs + shell: bash + id: prerequisites + run: | + haxelib install travix + haxelib run travix install + + + ################################################## + # Tests + ################################################## + - name: Test [interp] + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + run: haxelib run travix interp + - name: Test [neko] + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + run: haxelib run travix neko + - name: Test [python] + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + run: haxelib run travix python + - name: Test [node] + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + run: haxelib run travix node + - name: Test [node advanced minification] + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + run: | + haxelib install closure --quiet + haxe hxml/js-minified.hxml --js bin/test.js + node bin/test.js + # - name: Test [js] + # if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + # run: haxelib run travix js + # - name: Test [flash] + # if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + # run: haxelib run travix flash + - name: Test [java] + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + run: haxelib run travix java + - name: Test [cpp] + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + run: haxelib run travix cpp + - name: Test [cs] + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + run: haxelib run travix cs + - name: Test [php] + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' && matrix.haxe != '4.1.5' }} + #disabled php tests for Haxe 4.1 because it's not fully compatible with php 8 + run: | + # haxelib run travix php + haxe tests.hxml --php bin/php + php bin/php/index.php + - name: Test [lua] + #see https://github.com/back2dos/travix/pull/146 + if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' && matrix.haxe != '4.3.3' && matrix.haxe != 'latest' && matrix.os == 'ubuntu-latest' }} + run: haxelib run travix lua + # - name: Test [hashlink] + # if: ${{ !cancelled() && steps.prerequisites.conclusion == 'success' }} + # run: haxelib run travix hl diff --git a/.sauce-browsers.json b/.sauce-browsers.json deleted file mode 100644 index d36877d..0000000 --- a/.sauce-browsers.json +++ /dev/null @@ -1,33 +0,0 @@ -[ - { "browserName": "internet explorer", "version": "8", "platform": "Windows 7" }, - { "browserName": "internet explorer", "version": "9", "platform": "Windows 7" }, - { "browserName": "internet explorer", "version": "10", "platform": "Windows 8" }, - { "browserName": "internet explorer", "version": "11", "platform": "Windows 8.1" }, - - { "browserName": "firefox", "version": "31", "platform": "Windows 7" }, - { "browserName": "firefox", "version": "32", "platform": "Windows 7" }, - { "browserName": "firefox", "version": "beta", "platform": "Windows 7" }, - { "browserName": "firefox", "version": "dev", "platform": "Windows 7" }, - - { "browserName": "chrome", "version": "36", "platform": "Windows 7" }, - { "browserName": "chrome", "version": "37", "platform": "Windows 7" }, - { "browserName": "chrome", "version": "38", "platform": "Windows 7" }, - { "browserName": "chrome", "version": "beta", "platform": "Windows 7" }, - { "browserName": "chrome", "version": "dev", "platform": "Windows 7" }, - - { "browserName": "opera", "version": "11", "platform": "Windows 7" }, - { "browserName": "opera", "version": "12", "platform": "Windows 7" }, - - { "browserName": "safari", "version": "6", "platform": "OS X 10.8" }, - { "browserName": "safari", "version": "7", "platform": "OS X 10.9" }, - - { "browserName": "android", "version": "4.0", "platform": "Linux", "device-orientation": "portrait" }, - { "browserName": "android", "version": "4.1", "platform": "Linux", "device-orientation": "portrait" }, - { "browserName": "android", "version": "4.3", "platform": "Linux", "device-orientation": "portrait" }, - { "browserName": "android", "version": "4.4", "platform": "Linux", "device-orientation": "portrait" }, - - { "browserName": "iphone", "version": "6.1", "platform": "OS X 10.9" }, - { "browserName": "iphone", "version": "7", "platform": "OS X 10.9" }, - { "browserName": "iphone", "version": "7.1", "platform": "OS X 10.9" }, - { "browserName": "iphone", "version": "8", "platform": "OS X 10.9" } -] \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9cf9e08..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -sudo: required -dist: trusty - -language: haxe - -os: - - linux - - osx - -haxe: - - "3.4.7" - - "4.0.5" - - "4.1.0" - - development - -install: - - haxelib install travix - - haxelib run travix install - -script: - - haxelib run travix interp - - haxelib run travix neko - - haxelib run travix python - - haxelib run travix node - # - haxelib run travix flash # Failing - # - haxelib run travix js - - haxelib run travix java - - if ! [ "$TRAVIS_HAXE_VERSION" == "3.2.1" ]; then haxelib run travix cpp; fi - - haxelib run travix cs - #Fails to install php on both Linux & OSX: https://github.com/back2dos/travix/issues/114 - # - haxelib run travix php - - if [ `uname` = "Linux" ] && ! [ "$TRAVIS_HAXE_VERSION" == "3.2.1" ]; then haxelib run travix lua; fi; - diff --git a/CHANGELOG.md b/CHANGELOG.md index 286fe9b..abba194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# v2.0.0 +- Minimum supported Haxe version is 4.1 now. +- added `Assert.similar()` (see the doc to that method) +- changed various `Assert` methods to be type safe instead of accepting `Dynamic` arguments +- added UTEST_IGNORE_DEPENDS flag to ignore failed or missing dependencies (see README.md) +- added test case name filter to `Runner.addCases` +- support @:ignore meta (old @Ignored meta is still supported too) +- support advanced minification for JS target +- provided defines.json to get UTest compilation flags descriptions with `haxe -lib utest --help-user-defines` (requires Haxe 4.3) +- provided meta.json to get UTest metadata list with `haxe -lib utest --help-user-metas` (requires Haxe 4.3) + # v1.13.2 - Haxe 4.2.0 compatibility - Diagnostics reporter to show clickable messages in VSCode ([#106](https://github.com/haxe-utest/utest/issues/106) diff --git a/README.md b/README.md index a89017a..b0df5c5 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,31 @@ class TestCase extends utest.Test { } ``` +## Ignore a test + +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. +```haxe +class TestCase1 extends utest.Test { + @:ignore + function testSomethingNotReadyYet() { + //this test will not be executed + } +} + +@:ignore('The functionality under test is not ready yet') +class TestCase2 extends utest.Test { + function testSomething { + //this test will not be executed + } + + function testOtherThing { + //this test will not be executed + } +} +``` + + + ## Inter-test dependencies It is possible to define how tests depend on each other with `@:depends` meta: @@ -172,6 +197,10 @@ class TestCase2 extends utest.Test { ``` In this example tests from `some.pack.TestCase2` will be executed only if there were no failures in `some.pack.TestCase1`. +## Ignore dependencies + +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. + ## Running single test from a test suite. 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. diff --git a/appveyor.yml.disabled b/appveyor.yml.disabled deleted file mode 100644 index 9a8bb07..0000000 --- a/appveyor.yml.disabled +++ /dev/null @@ -1,56 +0,0 @@ -version: "{build}" - -environment: - global: - PYTHON_VERSION: "3.x.x" - -install: - - cinst haxe -y - - cinst curl -y - - cmd: mklink C:\Python34-x64\python3.exe C:\Python34-x64\python.exe - - set PATH=%PATH%;C:\Python34-x64 - - RefreshEnv - - haxelib setup %HOME%/haxelib - - haxelib install hxcpp - - haxelib install hxjava - - haxelib install hxcs - - haxelib list - -build_script: - - REM [Building NEKO] - - haxe hxml/appveyor.hxml -neko bin/test.n - - REM [Building JAVASCRIPT] - - haxe hxml/appveyor.hxml -js bin/test.js - - REM [Building PHP] - - haxe hxml/appveyor.hxml -php bin/php - - REM [Building PHP7] - - haxe hxml/appveyor.hxml -D php7 -php bin/php7 - - REM [Building PYTHON] - - haxe hxml/appveyor.hxml -python bin/test.py - - REM [Building JAVA] - - haxe hxml/appveyor.hxml -java bin/java - - REM [Building C#] - - haxe hxml/appveyor.hxml -cs bin/cs - - REM [Building C++] - - haxe hxml/appveyor.hxml -cpp bin/cpp - -test_script: - - REM [Running NEKO] - - neko bin\test.n - - REM [Running JAVASCRIPT] - - node bin\test.js - - REM [Running PHP 5.6] - - cinst php --version=5.6.32 -y - - C:\tools\php56\php.exe bin\php\index.php - - cuninst php - - REM [Running PHP 7.2] - - cinst php --version=7.2.0 -y - - C:\tools\php72\php.exe bin\php7\index.php - - REM [Running PYTHON 3.5] - - C:\Python35-x64\python.exe bin\test.py - - REM [Running JAVA] - - java -jar bin\java\TestAll.jar - - REM [Running C#] - - bin\cs\bin\TestAll.exe - - REM [Running C++] - - bin\cpp\TestAll.exe \ No newline at end of file diff --git a/defines.json b/defines.json new file mode 100644 index 0000000..db0245b --- /dev/null +++ b/defines.json @@ -0,0 +1,19 @@ +[ + { + "define": "UTEST_PATTERN", + "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.", + "params": ["pattern"] + }, + { + "define": "UTEST_PRINT_TESTS", + "doc": "Print test names in the process of tests execution." + }, + { + "define": "UTEST_FAILURE_THROW", + "doc": "Throw an unhandled exception instead of adding a failure to the report." + }, + { + "define": "UTEST_IGNORE_DEPENDS", + "doc": "Ignore @:depends meta and run annotated test regardless the outcome of the tests it depends on." + } +] \ No newline at end of file diff --git a/extraParams.hxml b/extraParams.hxml index 52a9a46..6796c80 100644 --- a/extraParams.hxml +++ b/extraParams.hxml @@ -1,2 +1 @@ ---macro utest.utils.Macro.checkHaxe() --macro utest.utils.Macro.importEnvSettings() diff --git a/haxelib.json b/haxelib.json index 4f47bd4..0ea84d0 100644 --- a/haxelib.json +++ b/haxelib.json @@ -4,9 +4,13 @@ "license": "MIT", "tags": ["cross","unittesting"], "description": "Unit Testing for Haxe", - "version": "1.13.2", + "version": "2.0.0-alpha", "classPath": "src", - "releasenote": "Haxe 4.2 compatibility; diagnostics reporter to show clickable messages in VSCode; exit code for Adobe AIR application in PlainTextReport; better position reporting on async.done() calls", + "releasenote": "See CHANGELOG.md", "contributors": ["fponticelli", "romanmikhailov", "RealyUniqueName"], - "dependencies": { } + "dependencies": { }, + "documentation": { + "metadata": "meta.json", + "defines": "defines.json" + } } diff --git a/hxml/appveyor.hxml b/hxml/appveyor.hxml deleted file mode 100644 index 0ebd9b3..0000000 --- a/hxml/appveyor.hxml +++ /dev/null @@ -1,2 +0,0 @@ -hxml/common.hxml --D travis diff --git a/hxml/common.hxml b/hxml/common.hxml index 6b9f913..47be404 100644 --- a/hxml/common.hxml +++ b/hxml/common.hxml @@ -3,5 +3,6 @@ -main TestAll -D utest -dce full +-D analyzer-optimize extraParams.hxml \ No newline at end of file diff --git a/hxml/js-minified.hxml b/hxml/js-minified.hxml new file mode 100644 index 0000000..a991d76 --- /dev/null +++ b/hxml/js-minified.hxml @@ -0,0 +1,10 @@ +hxml/common.hxml + +#Because of this (until the fix is released): https://github.com/HaxeFoundation/haxe/issues/11327 +-D js-enums-as-arrays + +-lib closure +-D closure_advanced +-D closure_overwrite +-D closure_warning_level=QUIET +--macro includeFile('test/js-minified-header.js') \ No newline at end of file diff --git a/hxml/travis.hxml b/hxml/travis.hxml deleted file mode 100644 index 0ebd9b3..0000000 --- a/hxml/travis.hxml +++ /dev/null @@ -1,2 +0,0 @@ -hxml/common.hxml --D travis diff --git a/meta.json b/meta.json new file mode 100644 index 0000000..b3b19e1 --- /dev/null +++ b/meta.json @@ -0,0 +1,19 @@ +[ + { + "metadata": ":timeout", + "doc": "Change timeout (milliseconds) for asynchronous test methods.", + "params": ["ms"], + "targets": ["TClass", "TClassField"] + }, + { + "metadata": ":depends", + "doc": "Prevents annotated test (or test case) from being executed if at least one of the tests in the list failed.", + "params": ["test1", "test2", "...", "testN"], + "targets": ["TClass", "TClassField"] + }, + { + "metadata": ":ignore", + "doc": "Skip annotated test.", + "targets": ["TClass", "TClassField"] + } +] \ No newline at end of file diff --git a/src/utest/Assert.hx b/src/utest/Assert.hx index 884b294..cdc7b87 100644 --- a/src/utest/Assert.hx +++ b/src/utest/Assert.hx @@ -1,12 +1,16 @@ package utest; -import utest.utils.Misc; +import haxe.display.Display.Package; +import haxe.ValueException; +import utest.exceptions.UTestException; +import utest.exceptions.AssertFailureException; import haxe.io.Bytes; import utest.Assertation; import haxe.PosInfos; -import Map; import haxe.Constraints; +typedef PosException = #if (haxe >= version("4.2.0")) haxe.exceptions.PosException #else haxe.Exception #end; + /** * This class contains only static members used to perform assertations inside a test method. * Its use is straight forward: @@ -26,7 +30,7 @@ class Assert { */ public static var results : List; - static inline function processResult(cond : Bool, getMessage : Void -> String, ?pos : PosInfos) : Bool { + static inline function processResult(cond : Bool, getMessage : () -> String, ?pos : PosInfos) : Bool { if (results == null) { throw 'Assert at ${pos.fileName}:${pos.lineNumber} out of context. Most likely you are trying to assert after a test timeout.'; } @@ -34,7 +38,7 @@ class Assert { results.add(Success(pos)); else { #if UTEST_FAILURE_THROW - throw '${pos.fileName}:${pos.lineNumber}: ${getMessage()}'; + throw new AssertFailureException('${pos.fileName}:${pos.lineNumber}: ${getMessage()}'); #else results.add(Failure(getMessage(), pos)); #end @@ -71,7 +75,7 @@ class Assert { * @param pos Code position where the Assert call has been executed. Don't fill it * unless you know what you are doing. */ - public static function isNull(value : Dynamic, ?msg : String, ?pos : PosInfos) : Bool { + public static function isNull(value : Null, ?msg : String, ?pos : PosInfos) : Bool { return processResult(value == null, function() return msg != null ? msg : "expected null but it is " + q(value), pos); } @@ -82,7 +86,7 @@ class Assert { * @param pos Code position where the Assert call has been executed. Don't fill it * unless you know what you are doing. */ - public static function notNull(value : Dynamic, ?msg : String, ?pos : PosInfos) : Bool { + public static function notNull(value : Null, ?msg : String, ?pos : PosInfos) : Bool { return processResult(value != null, function() return msg != null ? msg : "expected not null", pos); } @@ -95,7 +99,7 @@ class Assert { * unless you know what you are doing. */ @:deprecated("utest.Assert.is is deprecated. Use utest.Assert.isOfType instead.") - public static function is(value : Dynamic, type : Dynamic, ?msg : String , ?pos : PosInfos) : Bool { + public static function is(value : Null, type : Any, ?msg : String , ?pos : PosInfos) : Bool { return isOfType(value, type, msg, pos); } @@ -107,8 +111,8 @@ class Assert { * @param pos Code position where the Assert call has been executed. Don't fill it * unless you know what you are doing. */ - public static function isOfType(value : Dynamic, type : Dynamic, ?msg : String , ?pos : PosInfos) : Bool { - return processResult(Misc.isOfType(value, type), function() return msg != null ? msg : "expected type " + typeToString(type) + " but it is " + typeToString(value), pos); + public static function isOfType(value : Null, type : Any, ?msg : String , ?pos : PosInfos) : Bool { + return processResult(Std.isOfType(value, type), function() return msg != null ? msg : "expected type " + typeToString(type) + " but it is " + typeToString(value), pos); } /** @@ -122,7 +126,7 @@ class Assert { * @param pos Code position where the Assert call has been executed. Don't fill it * unless you know what you are doing. */ - public static function notEquals(expected : Dynamic, value : Dynamic, ?msg : String , ?pos : PosInfos) : Bool { + public static function notEquals(expected : Null, value : Null, ?msg : String , ?pos : PosInfos) : Bool { return processResult(expected != value, function() return msg != null ? msg : "expected " + q(expected) + " and test value " + q(value) + " should be different", pos); } @@ -137,7 +141,7 @@ class Assert { * @param pos Code position where the Assert call has been executed. Don't fill it * unless you know what you are doing. */ - public static function equals(expected : Dynamic, value : Dynamic, ?msg : String , ?pos : PosInfos) : Bool { + public static function equals(expected : Null, value : Null, ?msg : String , ?pos : PosInfos) : Bool { return processResult(expected == value, function() return msg != null ? msg : "expected " + q(expected) + " but it is " + q(value), pos); } @@ -152,7 +156,7 @@ class Assert { * @param pos Code position where the Assert call has been executed. Don't fill it * unless you know what you are doing. */ - public static function match(pattern : EReg, value : Dynamic, ?msg : String , ?pos : PosInfos) : Bool { + public static function match(pattern : EReg, value : String, ?msg : String , ?pos : PosInfos) : Bool { return processResult(pattern.match(value), function() return msg != null ? msg : "the value " + q(value) + " does not match the provided pattern", pos); } @@ -186,7 +190,7 @@ class Assert { return Math.abs(value-expected) <= approx; } - static function getTypeName(v : Dynamic) { + static function getTypeName(v : Any) { switch(Type.typeof(v)) { case TNull : return "`null`"; @@ -201,29 +205,69 @@ class Assert { } } - static function isIterable(v : Dynamic, isAnonym : Bool) { + static function isIterable(v : Any, isAnonym : Bool) { var fields = isAnonym ? Reflect.fields(v) : Type.getInstanceFields(Type.getClass(v)); if(!Lambda.has(fields, "iterator")) return false; return Reflect.isFunction(Reflect.field(v, "iterator")); } - static function isIterator(v : Dynamic, isAnonym : Bool) { + static function isIterator(v : Any, isAnonym : Bool) { var fields = isAnonym ? Reflect.fields(v) : Type.getInstanceFields(Type.getClass(v)); if(!Lambda.has(fields, "next") || !Lambda.has(fields, "hasNext")) return false; return Reflect.isFunction(Reflect.field(v, "next")) && Reflect.isFunction(Reflect.field(v, "hasNext")); } - static function sameAs(expected : Dynamic, value : Dynamic, status : LikeStatus, approx : Float) { + static function checkTypesCompatibility(expected : Any, value : Any, allowDifferentObjectTypes : Bool) : Bool { + var texpected = getTypeName(expected); + var tvalue = getTypeName(value); + + if(texpected == tvalue) { + return true; + } + //Int and Float are treated as same so that an int and float comaparison will use floatEquals + if((texpected == "Int" && tvalue == "Float") || (texpected == "Float" && tvalue == "Int")) { + return true; + } + + if(allowDifferentObjectTypes) { + var valueIsMap = Std.isOfType(value, IMap); + var valueIsArray = Std.isOfType(value, Array); + if(Std.isOfType(expected, IMap) && !valueIsMap) { + return false; + } + if(Std.isOfType(expected, Array) && !valueIsArray) { + return false; + } + if(valueIsArray || valueIsMap) { + return false; + } + + function isObject(v:Any) { + return switch Type.typeof(value) { + case TClass(String): false; + case TObject | TClass(_): true; + case _: false; + } + } + if(isObject(expected) && isObject(value)) { + return true; + } + } + + return false; + } + + static function sameAs(expected : Any, value : Any, status : LikeStatus, approx : Float, allowExtraFields : Bool, allowDifferentObjectTypes : Bool) { var texpected = getTypeName(expected); var tvalue = getTypeName(value); status.expectedValue = expected; status.actualValue = value; - if(texpected != tvalue && !((texpected == "Int" && tvalue == "Float") || (texpected == "Float" && tvalue == "Int"))) { //Int and Float are treated as same so that an int and float comaparison will use floatEquals - + if(!checkTypesCompatibility(expected, value, allowDifferentObjectTypes)) { status.error = "expected type " + texpected + " but it is " + tvalue + (status.path == '' ? '' : ' for field ' + status.path); return false; } + switch(Type.typeof(expected)) { case TFloat, TInt: @@ -247,21 +291,19 @@ class Assert { } return true; case TClass(c): - var cexpected = Type.getClassName(c); - var cvalue = Type.getClassName(Type.getClass(value)); #if cpp - if (cexpected == 'cpp::Pointer') { + if (texpected == 'cpp::Pointer') { return expected == value; } #end - if (cexpected != cvalue) + if (!allowExtraFields && texpected != tvalue) { - status.error = "expected instance of " + q(cexpected) + " but it is " + q(cvalue) + (status.path == '' ? '' : ' for field '+status.path); + status.error = "expected instance of " + q(texpected) + " but it is " + q(tvalue) + (status.path == '' ? '' : ' for field '+status.path); return false; } // string - if (Misc.isOfType(expected, String)) { + if (Std.isOfType(expected, String)) { if(expected == value) return true; else { @@ -271,16 +313,18 @@ class Assert { } // arrays - if(Misc.isOfType(expected, Array)) { + if(Std.isOfType(expected, Array)) { if(status.recursive || status.path == '') { - if(expected.length != value.length) { + var expected = (expected:Array); + var value = (value:Array); + if(!allowExtraFields && expected.length != value.length) { status.error = "expected "+expected.length+" elements but they are "+value.length + (status.path == '' ? '' : ' for field '+status.path); return false; } var path = status.path; for(i in 0...expected.length) { status.path = path == '' ? 'array['+i+']' : path + '['+i+']'; - if (!sameAs(expected[i], value[i], status, approx)) + if (!sameAs(expected[i], value[i], status, approx, allowExtraFields, allowDifferentObjectTypes)) { status.error = "expected array element at ["+i+"] to have " + q(status.expectedValue) + " but it is " + q(status.actualValue) + (status.path == '' ? '' : ' for field '+status.path); return false; @@ -291,7 +335,9 @@ class Assert { } // date - if(Misc.isOfType(expected, Date)) { + if(Std.isOfType(expected, Date)) { + var expected = (expected:Date); + var value = (value:Date); if(expected.getTime() != value.getTime()) { status.error = "expected " + q(expected) + " but it is " + q(value) + (status.path == '' ? '' : ' for field '+status.path); return false; @@ -300,7 +346,7 @@ class Assert { } // bytes - if(Misc.isOfType(expected, Bytes)) { + if(Std.isOfType(expected, Bytes)) { if(status.recursive || status.path == '') { var ebytes : Bytes = expected; var vbytes : Bytes = value; @@ -319,21 +365,21 @@ class Assert { } // hash, inthash - if (Misc.isOfType(expected, IMap)) { + if (Std.isOfType(expected, IMap)) { if(status.recursive || status.path == '') { var map = cast(expected, IMap); var vmap = cast(value, IMap); - var keys:Array = [for (k in map.keys()) k]; - var vkeys:Array = [for (k in vmap.keys()) k]; + var keys:Array = [for (k in map.keys()) k]; + var vkeys:Array = [for (k in vmap.keys()) k]; - if(keys.length != vkeys.length) { + if(!allowExtraFields && keys.length != vkeys.length) { status.error = "expected "+keys.length+" keys but they are "+vkeys.length + (status.path == '' ? '' : ' for field '+status.path); return false; } var path = status.path; for(key in keys) { status.path = path == '' ? 'hash['+key+']' : path + '['+key+']'; - if (!sameAs(map.get(key), vmap.get(key), status, approx)) + if (!sameAs(map.get(key), vmap.get(key), status, approx, allowExtraFields, allowDifferentObjectTypes)) { status.error = "expected " + q(status.expectedValue) + " but it is " + q(status.actualValue) + (status.path == '' ? '' : ' for field '+status.path); return false; @@ -348,14 +394,14 @@ class Assert { if(status.recursive || status.path == '') { var evalues = Lambda.array({ iterator : function() return expected }); var vvalues = Lambda.array({ iterator : function() return value }); - if(evalues.length != vvalues.length) { + if(!allowExtraFields && evalues.length != vvalues.length) { status.error = "expected "+evalues.length+" values in Iterator but they are "+vvalues.length + (status.path == '' ? '' : ' for field '+status.path); return false; } var path = status.path; for(i in 0...evalues.length) { status.path = path == '' ? 'iterator['+i+']' : path + '['+i+']'; - if (!sameAs(evalues[i], vvalues[i], status, approx)) + if (!sameAs(evalues[i], vvalues[i], status, approx, allowExtraFields, allowDifferentObjectTypes)) { status.error = "expected " + q(status.expectedValue) + " but it is " + q(status.actualValue) + (status.path == '' ? '' : ' for field '+status.path); return false; @@ -370,14 +416,14 @@ class Assert { if(status.recursive || status.path == '') { var evalues = Lambda.array(expected); var vvalues = Lambda.array(value); - if(evalues.length != vvalues.length) { + if(!allowExtraFields && evalues.length != vvalues.length) { status.error = "expected "+evalues.length+" values in Iterable but they are "+vvalues.length + (status.path == '' ? '' : ' for field '+status.path); return false; } var path = status.path; for(i in 0...evalues.length) { status.path = path == '' ? 'iterable['+i+']' : path + '['+i+']'; - if(!sameAs(evalues[i], vvalues[i], status, approx)) + if(!sameAs(evalues[i], vvalues[i], status, approx, allowExtraFields, allowDifferentObjectTypes)) return false; } } @@ -390,10 +436,10 @@ class Assert { var path = status.path; for(field in fields) { status.path = path == '' ? field : path+'.'+field; - var e = Reflect.field(expected, field); + var e = Reflect.getProperty(expected, field); if(Reflect.isFunction(e)) continue; - var v = Reflect.field(value, field); - if(!sameAs(e, v, status, approx)) + var v = Reflect.getProperty(value, field); + if(!sameAs(e, v, status, approx, allowExtraFields, allowDifferentObjectTypes)) return false; } } @@ -420,7 +466,7 @@ class Assert { for (i in 0...eparams.length) { status.path = path == '' ? 'enum[' + i + ']' : path + '[' + i + ']'; - if (!sameAs(eparams[i], vparams[i], status, approx)) + if (!sameAs(eparams[i], vparams[i], status, approx, allowExtraFields, allowDifferentObjectTypes)) { status.error = "expected enum param " + q(expected) + " but it is " + q(value) + (status.path == '' ? '' : ' for field ' + status.path) + ' with ' + status.error; return false; @@ -431,24 +477,28 @@ class Assert { case TObject : // anonymous object if(status.recursive || status.path == '') { - var tfields = Reflect.fields(value); + var tfields = switch Type.typeof(value) { + case TClass(cls): Type.getInstanceFields(cls); + case TObject: Reflect.fields(value); + case _: throw new PosException('Unexpected behavior'); + } var fields = Reflect.fields(expected); var path = status.path; for(field in fields) { - tfields.remove(field); status.path = path == '' ? field : path+'.'+field; - if(!Reflect.hasField(value, field)) { + if(!allowExtraFields && !tfields.contains(field)) { status.error = "expected field " + status.path + " does not exist in " + q(value); return false; } + tfields.remove(field); var e = Reflect.field(expected, field); if(Reflect.isFunction(e)) continue; - var v = Reflect.field(value, field); - if(!sameAs(e, v, status, approx)) + var v = Reflect.getProperty(value, field); + if(!sameAs(e, v, status, approx, allowExtraFields, allowDifferentObjectTypes)) return false; } - if(tfields.length > 0) + if(!allowExtraFields && tfields.length > 0) { status.error = "the tested object has extra field(s) (" + tfields.join(", ") + ") not included in the expected ones"; return false; @@ -464,14 +514,14 @@ class Assert { if(status.recursive || status.path == '') { var evalues = Lambda.array({ iterator : function() return expected }); var vvalues = Lambda.array({ iterator : function() return value }); - if(evalues.length != vvalues.length) { + if(!allowExtraFields && evalues.length != vvalues.length) { status.error = "expected "+evalues.length+" values in Iterator but they are "+vvalues.length + (status.path == '' ? '' : ' for field '+status.path); return false; } var path = status.path; for(i in 0...evalues.length) { status.path = path == '' ? 'iterator['+i+']' : path + '['+i+']'; - if (!sameAs(evalues[i], vvalues[i], status, approx)) + if (!sameAs(evalues[i], vvalues[i], status, approx, allowExtraFields, allowDifferentObjectTypes)) { status.error = "expected " + q(status.expectedValue) + " but it is " + q(status.actualValue) + (status.path == '' ? '' : ' for field '+status.path); return false; @@ -490,14 +540,14 @@ class Assert { if(status.recursive || status.path == '') { var evalues = Lambda.array(expected); var vvalues = Lambda.array(value); - if(evalues.length != vvalues.length) { + if(!allowExtraFields && evalues.length != vvalues.length) { status.error = "expected "+evalues.length+" values in Iterable but they are "+vvalues.length + (status.path == '' ? '' : ' for field '+status.path); return false; } var path = status.path; for(i in 0...evalues.length) { status.path = path == '' ? 'iterable['+i+']' : path + '['+i+']'; - if(!sameAs(evalues[i], vvalues[i], status, approx)) + if(!sameAs(evalues[i], vvalues[i], status, approx, allowExtraFields, allowDifferentObjectTypes)) return false; } } @@ -510,17 +560,20 @@ class Assert { return throw "Unable to compare values: " + q(expected) + " and " + q(value); } - static function q(v : Dynamic) + static function q(v : Any) { - if (Misc.isOfType(v, String)) + if (Std.isOfType(v, String)) return '"' + StringTools.replace(v, '"', '\\"') + '"'; else return Std.string(v); } /** - * Check that value is an object with the same fields and values found in expected. - * The default behavior is to check nested objects in fields recursively. + * Check the values are identical. + * Scalar values are checked for equality. + * Arrays are checked to have the same sets of values. + * If `value` is an object it is checked to have the same fields and values found in `expected`. + * The default behavior is to check nested arrays and objects in items and fields recursively. * ```haxe * Assert.same({ name : "utest"}, ob); * ``` @@ -533,7 +586,7 @@ class Assert { * @param pos Code position where the Assert call has been executed. Don't fill it * unless you know what you are doing. */ - public static function same(expected : Dynamic, value : Dynamic, ?recursive : Bool, ?msg : String, ?approx : Float, ?pos : PosInfos) : Bool { + public static function same(expected : Null, value : Null, ?recursive : Bool, ?msg : String, ?approx : Float, ?pos : PosInfos) : Bool { if (null == approx) approx = 1e-5; var status = { @@ -543,7 +596,53 @@ class Assert { expectedValue : expected, actualValue : value }; - return if(sameAs(expected, value, status, approx)) { + return if(sameAs(expected, value, status, approx, false, false)) { + pass(msg, pos); + } else { + fail(msg == null ? status.error : msg, pos); + } + } + + /** + * Check if `value` is similar to `expected`. + * That means: + * - For objects: the `value` object has at least the same set of fields and values the `expected` object has; + * - For arrays, iterables and iterators: the `value` collection has at least the same amount of items the `expected` collection has, and the items + * at corresponding positions are similar or equal (depending on the value of `recursive` argument); + * - For maps: the `value` map has at least the same set of keys and values the `expected` map has; + * It doesn't matter if the `value` object has additional fields/keys/items. Other than that `Assert.similar` replicates + * the behavior of `Assert.same`. + * ```haxe + * Assert.similar({foo:'bar'}, {foo:'bar', baz:0}); //pass + * ``` + * If the `value` object is a class instance then the properties get taken into account. + * ```haxe + * class Foo { + * public var foo(get,never):String; + * function get_foo():String return 'bar'; + * public function new() {} + * } + * + * Assert.similar({foo:'bar'}, new Foo()); //pass + * ``` + * @param expected The expected value to check against + * @param value The object to test. This may be an anonymous object as well as a class instance. + * @param recursive States whether or not the test will apply also to sub-objects. + * Defaults to true + * @param msg An optional error message. If not passed a default one will be used + * @param approx The approximation tollerance. Default is 1e-5 + * @param pos Code position where the Assert call has been executed. Don't fill it + * unless you know what you are doing. + */ + public static function similar(expected : Null, value : Null, recursive : Bool = true, ?msg : String, approx : Float = 1e-5, ?pos : PosInfos) : Bool { + var status = { + recursive : recursive, + path : '', + error : null, + expectedValue : expected, + actualValue : value + }; + return if(sameAs(expected, value, status, approx, true, true)) { pass(msg, pos); } else { fail(msg == null ? status.error : msg, pos); @@ -553,12 +652,12 @@ class Assert { /** * It is used to test an application that under certain circumstances must * react throwing an error. This assert guarantees that the error is of the - * correct type (or Dynamic if non is specified). + * correct type (or any type if non is specified). * ```haxe * Assert.raises(function() { throw "Error!"; }, String); * ``` * @param method A method that generates the exception. - * @param type The type of the expected error. Defaults to Dynamic (catch all). + * @param type The type of the expected error. Defaults to any type (catch all). * @param msgNotThrown An optional error message used when the function fails to raise the expected * exception. If not passed a default one will be used * @param msgWrongType An optional error message used when the function raises the exception but it is @@ -566,21 +665,30 @@ class Assert { * @param pos Code position where the Assert call has been executed. Don't fill it * unless you know what you are doing. */ - public static function raises(method:Void -> Void, ?type:Class, ?msgNotThrown : String , ?msgWrongType : String, ?pos : PosInfos) : Bool { - var name = type != null ? Type.getClassName(type) : "Dynamic"; - try { - method(); - } catch (ex : Dynamic) { + public static function raises(method:() -> Void, ?type:Any, ?msgNotThrown : String , ?msgWrongType : String, ?pos : PosInfos) : Bool { + var typeDescr = type == null ? "" : "of type " + Type.getClassName(type); + inline function handleCatch(ex:Any):Bool { return if(null == type) { pass(pos); } else { if (null == msgWrongType) - msgWrongType = "expected throw of type " + name + " but it is " + ex; - isTrue(Misc.isOfType(ex, type), msgWrongType, pos); + msgWrongType = "expected throw " + typeDescr + " but it is " + ex; + isTrue(Std.isOfType(ex, type), msgWrongType, pos); } } + try { + method(); + // Broken on eval in Haxe 4.3.2: https://github.com/HaxeFoundation/haxe/issues/11321 + // } catch(ex:ValueException) { + // return handleCatch(ex.value); + } catch (ex) { + if(Std.isOfType(ex, ValueException)) { + return handleCatch((cast ex:ValueException).value); + } + return handleCatch(ex); + } if (null == msgNotThrown) - msgNotThrown = "exception of type " + name + " not raised"; + msgNotThrown = "exception " + typeDescr + " not raised"; return fail(msgNotThrown, pos); } @@ -609,11 +717,7 @@ class Assert { * unless you know what you are doing. */ public static function contains(match : T, values : Array, ?msg : String , ?pos : PosInfos) : Bool { - return if(Lambda.has(values, match)) { - isTrue(true, msg, pos); - } else { - fail(msg == null ? "values " + q(values) + " do not contain "+match: msg, pos); - } + return isTrue(values.contains(match), msg == null ? "values " + q(values) + " do not contain "+match: msg, pos); } /** @@ -625,11 +729,7 @@ class Assert { * unless you know what you are doing. */ public static function notContains(match : T, values : Array, ?msg : String , ?pos : PosInfos) : Bool { - return if(!Lambda.has(values, match)) { - isTrue(true, msg, pos); - } else { - fail(msg == null ? "values " + q(values) + " do contain "+match: msg, pos); - } + return isFalse(values.contains(match), msg == null ? "values " + q(values) + " do contain "+match: msg, pos); } /** @@ -639,7 +739,7 @@ class Assert { * @param msg An optional error message. If not passed a default one will be used * @param pos Code position where the Assert call has been executed. */ - public static function stringContains(match : String, value : String, ?msg : String , ?pos : PosInfos) : Bool { + public static function stringContains(match : String, value : Null, ?msg : String , ?pos : PosInfos) : Bool { return if (value != null && value.indexOf(match) >= 0) { isTrue(true, msg, pos); } else { @@ -655,7 +755,7 @@ class Assert { * @param msg An optional error message. If not passed a default one is be used * @param pos Code position where the Assert call has been executed. */ - public static function stringSequence(sequence : Array, value : String, ?msg : String , ?pos : PosInfos) : Bool { + public static function stringSequence(sequence : Array, value : Null, ?msg : String , ?pos : PosInfos) : Bool { if (null == value) { return fail(msg == null ? "null argument value" : msg, pos); @@ -715,49 +815,34 @@ class Assert { results.add(Warning(msg)); } - /** - * Creates an asynchronous context for test execution. Assertions should be included - * in the passed function. - * ```haxe - * public function assertAsync() { - * var async = Assert.createAsync(function() Assert.isTrue(true)); - * haxe.Timer.delay(async, 50); - * } - * ``` - * @param f A function that contains other Assert tests - * @param timeout Optional timeout value in milliseconds. - */ - public static dynamic function createAsync(?f : Void -> Void, ?timeout : Int) { - return function(){}; + @:noCompletion + @:deprecated('Assert.createAsync is not supported since UTest 2.0. Add `async:utest.Async` argument to the test method instead.') + public static dynamic function createAsync(?f : () -> Void, ?timeout : Int):()->Void { + throw new UTestException('Assert.createAsync() is not supported since UTest 2.0. Add `async:utest.Async` argument to the test method instead.'); } - /** - * Creates an asynchronous context for test execution of an event like method. - * Assertions should be included in the passed function. - * It works the same way as Assert.createAsync() but accepts a function with one - * argument (usually some event data) instead of a function with no arguments - * @param f A function that contains other Assert tests - * @param timeout Optional timeout value in milliseconds. - */ - public static dynamic function createEvent(f : EventArg -> Void, ?timeout : Int) { - return function(e){}; + + @:noCompletion + @:deprecated('Assert.createEvent is not supported since UTest 2.0. Add `async:utest.Async` argument to the test method instead.') + public static dynamic function createEvent(f : (EventArg) -> Void, ?timeout : Int):(Dynamic) -> Void { + throw new UTestException('Assert.createEvent() is not supported since UTest 2.0. Add `async:utest.Async` argument to the test method instead.'); } - static function typeToString(t : Dynamic) { + static function typeToString(t : Any) { try { var _t = Type.getClass(t); if (_t != null) t = _t; - } catch(e : Dynamic) { } - try return Type.getClassName(t) catch (e : Dynamic) { } + } catch(_) { } + try return Type.getClassName(t) catch (_) { } try { var _t = Type.getEnum(t); if (_t != null) t = _t; - } catch(e : Dynamic) { } - try return Type.getEnumName(t) catch(e : Dynamic) {} - try return Std.string(Type.typeof(t)) catch (e : Dynamic) { } - try return Std.string(t) catch (e : Dynamic) { } + } catch(_) { } + try return Type.getEnumName(t) catch(_) {} + try return Std.string(Type.typeof(t)) catch (_) { } + try return Std.string(t) catch (_) { } return ''; } } @@ -766,6 +851,6 @@ private typedef LikeStatus = { recursive : Bool, path : String, error : String, - expectedValue:Dynamic, - actualValue:Dynamic + expectedValue:Any, + actualValue:Any }; diff --git a/src/utest/Assertation.hx b/src/utest/Assertation.hx index 2f9b50f..ce1a65e 100644 --- a/src/utest/Assertation.hx +++ b/src/utest/Assertation.hx @@ -28,33 +28,33 @@ enum Assertation { * futher assertion to be tested. * @param e The captured error/exception */ - Error(e:Dynamic, stack:Array); + Error(e:Any, stack:CallStack); /** * An error has occurred during the Setup phase of the test. It prevents * the test to be run. * @param e The captured error/exception */ - SetupError(e:Dynamic, stack:Array); + SetupError(e:Any, stack:CallStack); /** * An error has occurred during the Teardown phase of the test. * @param e The captured error/exception */ - TeardownError(e:Dynamic, stack:Array); + TeardownError(e:Any, stack:CallStack); /** * The asynchronous phase of a test has gone into timeout. * @param missedAsyncs The number of asynchronous calls that was expected * to be performed before the timeout. */ - TimeoutError(missedAsyncs:Int, stack:Array); + TimeoutError(missedAsyncs:Int, stack:CallStack); /** * An error has occurred during an asynchronous test. * @param e The captured error/exception */ - AsyncError(e:Dynamic, stack:Array); + AsyncError(e:Any, stack:CallStack); /** * A warning state. This can be declared explicitely by an Assert call diff --git a/src/utest/Async.hx b/src/utest/Async.hx index 9cde945..cb1fa10 100644 --- a/src/utest/Async.hx +++ b/src/utest/Async.hx @@ -1,9 +1,5 @@ package utest; -#if (haxe_ver < "3.4.0") - #error 'Haxe 3.4.0 or later is required for utest.Async' -#end - import haxe.PosInfos; import haxe.Timer; @@ -14,7 +10,7 @@ class Async { public var resolved(default,null):Bool = false; public var timedOut(default,null):Bool = false; - var callbacks:ArrayVoid> = []; + var callbacks:Array<() ->Void> = []; var timeoutMs:Int; var startTime:Float; var timer:Timer; @@ -72,7 +68,7 @@ class Async { /** Create a sub-async. Current `Async` instance will be resolved automatically once all sub-asyncs are resolved. **/ - public function branch(?fn:Async->Void, ?pos:PosInfos):Async { + public function branch(?fn:(Async)->Void, ?pos:PosInfos):Async { var branch = new Async(timeoutMs); branches.push(branch); branch.then(checkBranches.bind(pos)); @@ -101,7 +97,7 @@ class Async { ); } - function then(cb:Void->Void) { + function then(cb:()->Void) { if(resolved) { cb(); } else { diff --git a/src/utest/Dispatcher.hx b/src/utest/Dispatcher.hx index 908adea..fe0f412 100644 --- a/src/utest/Dispatcher.hx +++ b/src/utest/Dispatcher.hx @@ -1,22 +1,26 @@ package utest; -private enum EventException { - StopPropagation; +import haxe.Exception; + +private class StopPropagationException extends Exception { + public function new() { + super(''); + } } class Dispatcher { - private var handlers : Array Void>; + private var handlers : Array<(T) -> Void>; public function new() handlers = new Array(); - public function add(h : T -> Void) : T -> Void { + public function add(h : (T) -> Void) : (T) -> Void { handlers.push(h); return h; } - public function remove(h : T -> Void) : T -> Void { + public function remove(h : (T) -> Void) : (T) -> Void { for(i in 0...handlers.length) if(Reflect.compareMethods(handlers[i], h)) return handlers.splice(i, 1)[0]; @@ -26,14 +30,14 @@ class Dispatcher { public function clear() handlers = new Array(); - public function dispatch(e) { + public function dispatch(e:T) { try { // prevents problems with self removing events var list = handlers.copy(); for( l in list ) l(e); return true; - } catch( exc : EventException ) { + } catch( _ : StopPropagationException ) { return false; } } @@ -42,22 +46,22 @@ class Dispatcher { return handlers.length > 0; public static function stop() - throw StopPropagation; + throw new StopPropagationException(); } class Notifier { - private var handlers : Array Void>; + private var handlers : Array<() -> Void>; public function new() handlers = new Array(); - public function add(h : Void -> Void) : Void -> Void { + public function add(h : () -> Void) : () -> Void { handlers.push(h); return h; } - public function remove(h : Void -> Void) : Void -> Void { + public function remove(h : () -> Void) : () -> Void { for(i in 0...handlers.length) if(Reflect.compareMethods(handlers[i], h)) return handlers.splice(i, 1)[0]; @@ -74,7 +78,7 @@ class Notifier { for( l in list ) l(); return true; - } catch( exc : EventException ) { + } catch( _ : StopPropagationException ) { return false; } } @@ -83,5 +87,5 @@ class Notifier { return handlers.length > 0; public static function stop() - throw StopPropagation; + throw new StopPropagationException(); } \ No newline at end of file diff --git a/src/utest/ITest.hx b/src/utest/ITest.hx index 574833d..a718882 100644 --- a/src/utest/ITest.hx +++ b/src/utest/ITest.hx @@ -1,9 +1,5 @@ package utest; -#if (haxe_ver < "3.4.0") - #error 'Haxe 3.4.0 or later is required for utest.ITest' -#end - /** * Interface, which should be implemented by test cases. * diff --git a/src/utest/ITestHandler.hx b/src/utest/ITestHandler.hx deleted file mode 100644 index b5a037c..0000000 --- a/src/utest/ITestHandler.hx +++ /dev/null @@ -1,126 +0,0 @@ -package utest; - -import haxe.CallStack; -import haxe.Timer; -using utest.utils.AsyncUtils; - -class ITestHandler extends TestHandler { - var testCase:ITest; - var test:TestData; - var setupAsync:Null; - var testAsync:Null; - var teardownAsync:Null; - - public function new(fixture:TestFixture) { - super(fixture); - if(!fixture.isITest) { - throw 'Invalid fixture type for utest.ITestHandler'; - } - testCase = cast(fixture.target, ITest); - test = fixture.test; - if(test == null) { - throw 'Fixture is missing test data'; - } - } - - override public function execute() { - startTime = Timer.stamp(); - if (fixture.ignoringInfo.isIgnored) { - executeFinally(); - return; - } - bindHandler(); - runSetup(); - } - - function runSetup() { - try { - setupAsync = fixture.setupMethod(); - } - #if !UTEST_FAILURE_THROW - catch(e:Dynamic) { - results.add(SetupError(e, CallStack.exceptionStack())); - completedFinally(); - return; - } - #end - - setupAsync.then(checkSetup); - } - - function checkSetup() { - if(setupAsync.timedOut) { - results.add(SetupError('Setup timeout', [])); - completedFinally(); - } else { - runTest(); - } - } - - function runTest() { - try { - testAsync = test.execute(); - } - #if !UTEST_FAILURE_THROW - catch(e:Dynamic) { - results.add(Error(e, CallStack.exceptionStack())); - runTeardown(); - return; - } - #end - - testAsync.then(checkTest); - } - - function checkTest() { - onPrecheck.dispatch(this); - - if(testAsync.timedOut) { - results.add(TimeoutError(1, [])); - onTimeout.dispatch(this); - - } else if(testAsync.resolved) { - if(results.length == 0) { - results.add(Warning('no assertions')); - } - onTested.dispatch(this); - - } else { - throw 'Unexpected test state'; - } - - runTeardown(); - } - - function runTeardown() { - try { - teardownAsync = fixture.teardownMethod(); - } - #if !UTEST_FAILURE_THROW - catch(e:Dynamic) { - results.add(TeardownError(e, CallStack.exceptionStack())); - completedFinally(); - return; - } - #end - - teardownAsync.then(checkTeardown); - } - - function checkTeardown() { - if(teardownAsync.timedOut) { - results.add(TeardownError('Teardown timeout', [])); - } - completedFinally(); - } - - override function bindHandler() { - if (wasBound) return; - Assert.results = this.results; - var msg = ' is not allowed in tests extending utest.ITest. Add `async:utest.Async` argument to the test method instead.'; - Assert.createAsync = function(?f, ?t) throw 'Assert.createAsync() $msg'; - Assert.createEvent = function(f, ?t) throw 'Assert.createEvent() $msg'; - wasBound = true; - } - -} diff --git a/src/utest/MacroRunner.hx b/src/utest/MacroRunner.hx index 8bf3de5..586bc36 100755 --- a/src/utest/MacroRunner.hx +++ b/src/utest/MacroRunner.hx @@ -1,13 +1,8 @@ package utest; -import haxe.rtti.Meta; -import haxe.unit.TestRunner; - #if macro -import neko.io.File; import haxe.macro.Expr; import haxe.macro.Context; -import neko.Lib; #end import utest.ui.macro.MacroReport; @@ -19,7 +14,7 @@ class MacroRunner { Run the unit tests from a macro, displaying errors and a summary in the macro Context. @param testClass Class where the tests are located. */ - public static function run(testClass : Dynamic) { + public static function run(testClass : Any) { var runner = new Runner(); addClass(runner, testClass); @@ -55,7 +50,7 @@ Displays stub code for using MacroRunner. return { expr: EConst(CType("Void")), pos: Context.currentPos() }; } - static function addClass(runner : Runner, testClass : Class) { + static function addClass(runner : Runner, testClass : Class) { runner.addCase(testClass); var addTests = Reflect.field(testClass, "addTests"); diff --git a/src/utest/Runner.hx b/src/utest/Runner.hx index 3209c59..b26f43d 100644 --- a/src/utest/Runner.hx +++ b/src/utest/Runner.hx @@ -1,6 +1,6 @@ package utest; -import utest.utils.Misc; +import utest.exceptions.UTestException; import utest.utils.Print; import haxe.CallStack; import haxe.macro.Compiler; @@ -15,13 +15,15 @@ using StringTools; using haxe.macro.Tools; #end -#if (haxe_ver >= "3.4.0") using utest.utils.AsyncUtils; using utest.utils.AccessoriesUtils; + +#if (haxe_ver < "4.1.0") + #error 'Haxe 4.1.0 or later is required to run UTest' #end /** - * The Runner class performs a set of tests. The tests can be added using addCase or addFixtures. + * The Runner class performs a set of tests. The tests can be added using addCase. * Once all the tests are register they are axecuted on the run() call. * Note that Runner does not provide any visual output. To visualize the test results use one of * the classes in the utest.ui package. @@ -29,10 +31,7 @@ using utest.utils.AccessoriesUtils; * @todo AVOID CHAINING METHODS (long chains do not work properly on IE) */ class Runner { - var fixtures(default, null) : Array = []; - #if (haxe_ver >= "3.4.0") - var iTestFixtures:MapAsync, dependencies:Array, fixtures:Array, teardownClass:Void->Async}> = new Map(); - #end + var fixtures:MapAsync, dependencies:Array, fixtures:Array, teardownClass:()->Async}> = new Map(); /** * Event object that monitors the progress of the runner. @@ -102,79 +101,34 @@ class Runner { /** * Adds a new test case. - * @param test must be a not null object - * @param setup string name of the setup function (defaults to "setup") - * @param teardown string name of the teardown function (defaults to "teardown") - * @param prefix prefix for methods that are tests (defaults to "test") + * @param testCase must be a not null object * @param pattern a regular expression that discriminates the names of test - * functions; when set, the prefix parameter is meaningless - * @param setupAsync string name of the asynchronous setup function (defaults to "setupAsync") - * @param teardownAsync string name of the asynchronous teardown function (defaults to "teardownAsync") + * functions */ - public function addCase(test : Dynamic, setup = "setup", teardown = "teardown", prefix = "test", ?pattern : EReg, setupAsync = "setupAsync", teardownAsync = "teardownAsync") { - #if (haxe_ver >= "3.4.0") - if(Misc.isOfType(test, ITest)) { - addITest(test, pattern); - } else { - addCaseOld(test, setup, teardown, prefix, pattern, setupAsync, teardownAsync); - } - #else - addCaseOld(test, setup, teardown, prefix, pattern, setupAsync, teardownAsync); - #end - } - - #if (haxe_ver >= "3.4.0") - function addITest(testCase:ITest, pattern:Null) { + public function addCase(testCase : ITest, ?pattern : EReg) { var className = Type.getClassName(Type.getClass(testCase)); - if(iTestFixtures.exists(className)) { - throw 'Cannot add the same test twice.'; + if(fixtures.exists(className)) { + throw new UTestException('Cannot add the same test twice.'); } - var fixtures = []; - #if as3 - // AS3 can't handle the ECheckType cast. Let's dodge the issue. - var tmp:TestData.Initializer = cast testCase; - var init:TestData.InitializeUtest = tmp.__initializeUtest__(); - #else + var newFixtures = []; var init:TestData.InitializeUtest = (cast testCase:TestData.Initializer).__initializeUtest__(); - #end for(test in init.tests) { if(!isTestFixtureName(className, test.name, ['test', 'spec'], pattern, globalPattern)) { continue; } - var fixture = TestFixture.ofData(testCase, test, init.accessories); - addFixture(fixture); - fixtures.push(fixture); + newFixtures.push(new TestFixture(testCase, test, init.accessories)); } - if(fixtures.length > 0) { - iTestFixtures.set(className, { + if(newFixtures.length > 0) { + fixtures.set(className, { caseInstance:testCase, setupClass:init.accessories.getSetupClass(), - dependencies:init.dependencies, - fixtures:fixtures, + dependencies:#if UTEST_IGNORE_DEPENDS [] #else init.dependencies #end, + fixtures:newFixtures, teardownClass:init.accessories.getTeardownClass() }); + length += newFixtures.length; } } - #end - - function addCaseOld(test:Dynamic, setup = "setup", teardown = "teardown", prefix = "test", ?pattern : EReg, setupAsync = "setupAsync", teardownAsync = "teardownAsync") { - if(!Reflect.isObject(test)) throw "can't add a null object as a test case"; - if(!isMethod(test, setup)) - setup = null; - if(!isMethod(test, setupAsync)) - setupAsync = null; - if(!isMethod(test, teardown)) - teardown = null; - if(!isMethod(test, teardownAsync)) - teardownAsync = null; - var fields = Type.getInstanceFields(Type.getClass(test)); - var className = Type.getClassName(Type.getClass(test)); - for (field in fields) { - if(!isMethod(test, field)) continue; - if(!isTestFixtureName(className, field, [prefix], pattern, globalPattern)) continue; - addFixture(new TestFixture(test, field, setup, teardown, setupAsync, teardownAsync)); - } - } /** * Add all test cases located in specified package `path`. @@ -182,8 +136,10 @@ class Runner { * That means each module should contain a class with a constructor and with the same name as a module name. * @param path dot-separated path as a string or as an identifier/field expression. E.g. `"my.pack"` or `my.pack` * @param recursive recursively look for test cases in sub packages. + * @param nameFilterRegExp regular expression to check modules names against. If the module name does not + * match this argument, the module will not be added. */ - macro public function addCases(eThis:Expr, path:Expr, recursive:Bool = true):Expr { + macro public function addCases(eThis:Expr, path:Expr, recursive:Bool = true, nameFilterRegExp:String = '.*'):Expr { if(Context.defined('display')) { return macro {}; } @@ -195,6 +151,7 @@ class Runner { if(~/[^a-zA-Z0-9_.]/.match(path)) { Context.error('The first argument for utest.Runner.addCases() should be a valid package path.', pos); } + var nameFilter = new EReg(nameFilterRegExp, ''); var pack = path.split('.'); var relativePath = Path.join(pack); var exprs = []; @@ -212,7 +169,7 @@ class Runner { continue; } var className = file.substr(0, file.length - 3); - if(className == '') { + if(className == '' || !nameFilter.match(className)) { continue; } var testCase = Context.parse('new $path.$className()', pos); @@ -241,31 +198,10 @@ class Runner { return pattern.match('$caseName.$testName'); } - public function addFixture(fixture : TestFixture) { - fixtures.push(fixture); - length++; - } - - public function getFixture(index : Int) { - return fixtures[index]; - } - - function isMethod(test : Dynamic, name : String) { - try { - return Reflect.isFunction(Reflect.field(test, name)); - } catch(e : Dynamic) { - return false; - } - } - public function run() { onStart.dispatch(this); - #if (haxe_ver >= "3.4.0") var iTestRunner = new ITestRunner(this); iTestRunner.run(); - #else - runNext(); - #end waitForCompletion(); } @@ -275,50 +211,22 @@ class Runner { * Can't reproduce it on a separated sample. */ function waitForCompletion() { - #if (haxe_ver >= "3.4.0") if(!complete) { haxe.Timer.delay(waitForCompletion, 100); } - #end - } - - var pos:Int = 0; - var executedFixtures:Int = 0; - function runNext(?finishedHandler:TestHandler) { - var currentCase = null; - for(i in pos...fixtures.length) { - var fixture = fixtures[pos++]; - if(fixture.isITest) continue; - if(currentCase != fixture.target) { - currentCase = fixture.target; - Print.startCase(Type.getClassName(Type.getClass(currentCase))); - } - var handler = runFixture(fixture); - if(!handler.finished) { - handler.onComplete.add(runNext); - //wait till current test is finished - return; - } - } - complete = true; - onComplete.dispatch(this); } function runFixture(fixture : TestFixture):TestHandler { - // cast is required by C# - #if (haxe_ver >= "3.4.0") - var handler = (fixture.isITest ? new ITestHandler(fixture) : new TestHandler(fixture)); - #else - var handler = new TestHandler(cast fixture); - #end + var handler = new TestHandler(fixture); handler.onComplete.add(testComplete); handler.onPrecheck.add(this.onPrecheck.dispatch); - Print.startTest(fixture.method); + Print.startTest(fixture.name); onTestStart.dispatch(handler); handler.execute(); return handler; } + var executedFixtures:Int = 0; function testComplete(h : TestHandler) { ++executedFixtures; onTestComplete.dispatch(h); @@ -326,21 +234,22 @@ class Runner { } } -#if (haxe_ver >= "3.4.0") -@:access(utest.Runner.iTestFixtures) +@:access(utest.Runner.fixtures) @:access(utest.Runner.runNext) @:access(utest.Runner.runFixture) @:access(utest.Runner.executedFixtures) +@:access(utest.Runner.complete) private class ITestRunner { var runner:Runner; var cases:Iterator; var currentCaseName:String; var currentCase:ITest; var currentCaseFixtures:Array; - var teardownClass:Void->Async; + var teardownClass:()->Async; var setupAsync:Async; var teardownAsync:Async; var failedTestsInCurrentCase:Array = []; + var executedTestsInCurrentCase:Array = []; var failedCases:Array = []; public function new(runner:Runner) { @@ -350,10 +259,11 @@ private class ITestRunner { switch result { case Success(_): case _: - failedTestsInCurrentCase.push(handler.fixture.method); + failedTestsInCurrentCase.push(handler.fixture.name); failedCases.push(Type.getClassName(Type.getClass(handler.fixture.target))); } } + executedTestsInCurrentCase.push(handler.fixture.name); }); } @@ -375,7 +285,7 @@ private class ITestRunner { function addClass(cls:String, stack:Array) { if(added.exists(cls)) return; - var data = runner.iTestFixtures.get(cls); + var data = runner.fixtures.get(cls); if(stack.indexOf(cls) >= 0) { error(data.caseInstance, 'Circular dependencies among test classes detected: ' + stack.join(' -> ')); return; @@ -383,7 +293,7 @@ private class ITestRunner { stack.push(cls); var dependencies = data.dependencies; for(dependency in dependencies) { - if(runner.iTestFixtures.exists(dependency)) { + if(runner.fixtures.exists(dependency)) { addClass(dependency, stack); } else { error(data.caseInstance, 'This class depends on $dependency, but it cannot be found. Was it added to test runner?'); @@ -393,7 +303,7 @@ private class ITestRunner { result.push(cls); added.set(cls, true); } - for(cls in runner.iTestFixtures.keys()) { + for(cls in runner.fixtures.keys()) { addClass(cls, []); } return result.iterator(); @@ -410,9 +320,10 @@ private class ITestRunner { function runCases() { while(cases.hasNext()) { currentCaseName = cases.next(); - var data = runner.iTestFixtures.get(currentCaseName); + var data = runner.fixtures.get(currentCaseName); currentCase = data.caseInstance; failedTestsInCurrentCase = []; + executedTestsInCurrentCase = []; if(failedDependencies(data)) { failedCases.push(currentCaseName); continue; @@ -424,8 +335,8 @@ private class ITestRunner { setupAsync = data.setupClass(); } #if !UTEST_FAILURE_THROW - catch(e:Dynamic) { - setupFailed(SetupError('setupClass failed: $e', CallStack.exceptionStack())); + catch(e) { + setupFailed(SetupError('setupClass failed: ${e.message}', e.stack)); return; } #end @@ -436,8 +347,8 @@ private class ITestRunner { return; } } - //run old-fashioned tests - runner.runNext(); + runner.complete = true; + runner.onComplete.dispatch(runner); } function checkSetup() { @@ -464,12 +375,7 @@ private class ITestRunner { function runFixtures(?finishedHandler:TestHandler):Bool { while(currentCaseFixtures.length > 0) { var fixture = currentCaseFixtures.shift(); - for (dep in fixture.test.dependencies) { - if(failedTestsInCurrentCase.indexOf(dep) >= 0) { - @:privateAccess fixture.ignoringInfo = IgnoredFixture.Ignored('Failed dependencies'); - break; - } - } + checkFixtureDependencies(fixture); var handler = runner.runFixture(fixture); if(!handler.finished) { handler.onComplete.add(runFixtures); @@ -481,8 +387,8 @@ private class ITestRunner { teardownAsync = teardownClass(); } #if !UTEST_FAILURE_THROW - catch(e:Dynamic) { - teardownFailed(TeardownError('teardownClass failed: $e', CallStack.exceptionStack())); + catch(e) { + teardownFailed(TeardownError('teardownClass failed: ${e.message}', e.stack)); return true; } #end @@ -494,6 +400,34 @@ private class ITestRunner { return false; } + function checkFixtureDependencies(fixture:TestFixture) { + if(!fixture.ignoringInfo.isIgnored) { + #if !UTEST_IGNORE_DEPENDS + if(fixture.test.dependencies.length > 0) { + var failedDeps = []; + var ignoredDeps = []; + for (dep in fixture.test.dependencies) { + if(failedTestsInCurrentCase.contains(dep)) { + failedDeps.push(dep); + } + if(!executedTestsInCurrentCase.contains(dep)) { + ignoredDeps.push(dep); + } + } + var failedDepsMsg = failedDeps.length == 0 ? null : IgnoredFixture.Ignored('Failed dependencies: ${failedDeps.join(', ')}'); + var ignoredDepsMsg = ignoredDeps.length == 0 ? null : IgnoredFixture.Ignored('Skipped dependencies: ${ignoredDeps.join(', ')}'); + var ignoringInfo = switch [failedDepsMsg, ignoredDepsMsg] { + case [null, null]: IgnoredFixture.NotIgnored(); + case [_, null]: IgnoredFixture.Ignored(failedDepsMsg); + case [null, _]: IgnoredFixture.Ignored(ignoredDepsMsg); + case [_, _]: IgnoredFixture.Ignored('$failedDepsMsg. $ignoredDepsMsg'); + } + fixture.setIgnoringInfo(ignoringInfo); + } + #end + } + } + function checkTeardown() { if(teardownAsync.timedOut) { teardownFailed(TeardownError('teardownClass timeout', [])); @@ -509,4 +443,3 @@ private class ITestRunner { }); } } -#end diff --git a/src/utest/TestData.hx b/src/utest/TestData.hx index 03f9e8f..9c426a2 100644 --- a/src/utest/TestData.hx +++ b/src/utest/TestData.hx @@ -1,22 +1,31 @@ package utest; +import haxe.ds.Option; + /** * The data of a test as collected by utest.utils.TestBuilder at compile time. */ typedef TestData = { var name(default,null):String; var dependencies(default,null):Array; - var execute(default,null):Void->Async; + var execute(default,null):()->Async; + var ignore:Option; } +/** + * Describes the reason of a test being ignored. + * `null` means no reason provided with `@:ignore` meta. + */ +typedef IgnoreReason = Null; + /** * The data of accessory methods: setup, setupClass, teardown, teardownClass */ typedef Accessories = { - ?setup:Void->Async, - ?setupClass:Void->Async, - ?teardown:Void->Async, - ?teardownClass:Void->Async, + ?setup:()->Async, + ?setupClass:()->Async, + ?teardown:()->Async, + ?teardownClass:()->Async, } typedef InitializeUtest = { diff --git a/src/utest/TestFixture.hx b/src/utest/TestFixture.hx index e0fbd92..6933a9f 100644 --- a/src/utest/TestFixture.hx +++ b/src/utest/TestFixture.hx @@ -3,65 +3,35 @@ package utest; import haxe.rtti.Meta; import utest.IgnoredFixture; +using utest.utils.AccessoriesUtils; + class TestFixture { - public var target(default, null) : {}; - public var method(default, null) : String; - public var setup(default, null) : String; - public var setupAsync(default, null) : String; - public var teardown(default, null) : String; - public var teardownAsync(default, null) : String; - public var ignoringInfo(default, null) : IgnoredFixture; + public var target(default, null) : ITest; + public var ignoringInfo(default, null) : IgnoredFixture; + + public var name(get, never) : String; + function get_name():String return test.name; @:allow(utest) - var isITest:Bool = false; - #if (haxe_ver >= "3.4.0") - @:allow(utest) - var test:Null; + final test:TestData; @:allow(utest) - var setupMethod:Void->Async; + final setupMethod:()->Async; @:allow(utest) - var teardownMethod:Void->Async; - - static public function ofData(target:ITest, test:TestData, accessories:TestData.Accessories):TestFixture { - var fixture = new TestFixture(target, test.name); - fixture.isITest = true; - fixture.test = test; - fixture.setupMethod = utest.utils.AccessoriesUtils.getSetup(accessories); - fixture.teardownMethod = utest.utils.AccessoriesUtils.getTeardown(accessories); - return fixture; - } - #end - - public function new(target : {}, method : String, ?setup : String, ?teardown : String, ?setupAsync : String, ?teardownAsync : String) { - this.target = target; - this.method = method; - this.setup = setup; - this.setupAsync = setupAsync; - this.teardown = teardown; - this.teardownAsync = teardownAsync; - this.ignoringInfo = getIgnored(); - } - - function checkMethod(name : String, arg : String) { - var field = Reflect.field(target, name); - if(field == null) throw arg + " function " + name + " is not a field of target"; - if(!Reflect.isFunction(field)) throw arg + " function " + name + " is not a function"; - } + final teardownMethod:()->Async; - function getIgnored():IgnoredFixture { - var metas:Dynamic>> = Meta.getFields(Type.getClass(target)); - var metasForTestMetas = Reflect.getProperty(metas, method); + public function new(target:ITest, test:TestData, accessories:TestData.Accessories) { + this.target = target; + this.test = test; + this.setupMethod = accessories.getSetup(); + this.teardownMethod = accessories.getTeardown(); - if (metasForTestMetas == null || !Reflect.hasField(metasForTestMetas, "Ignored")) { - return IgnoredFixture.NotIgnored(); - } - - var ignoredArgs:Array = cast Reflect.getProperty(metasForTestMetas, "Ignored"); - if (ignoredArgs == null || ignoredArgs.length == 0 || ignoredArgs[0] == null) { - return IgnoredFixture.Ignored(); + ignoringInfo = switch test.ignore { + case None: IgnoredFixture.NotIgnored(); + case Some(reason): IgnoredFixture.Ignored(reason); } + } - var ignoredReason:String = Std.string(ignoredArgs[0]); - return IgnoredFixture.Ignored(ignoredReason); + public function setIgnoringInfo(info:IgnoredFixture) { + ignoringInfo = info; } } diff --git a/src/utest/TestHandler.hx b/src/utest/TestHandler.hx index 05a55b5..6327c9b 100644 --- a/src/utest/TestHandler.hx +++ b/src/utest/TestHandler.hx @@ -1,5 +1,8 @@ package utest; +import haxe.Exception; +import haxe.ValueException; +import utest.exceptions.UTestException; import haxe.CallStack; import haxe.Timer; import utest.Assertation; @@ -10,7 +13,7 @@ class TestHandler { public var fixture(default, null) : TestFixture; public var finished(default, null) : Bool = false; public var executionTime(default, null) : Float = 0; - var asyncStack : List; + var asyncStack : List; var startTime:Float = 0; public var onTested(default, null) : Dispatcher>; @@ -18,10 +21,16 @@ class TestHandler { public var onComplete(default, null) : Dispatcher>; public var onPrecheck(default, null) : Dispatcher>; - public var precheck(default, null) : Void->Void; + public var precheck(default, null) : ()->Void; private var wasBound:Bool = false; + var testCase:ITest; + var test:TestData; + var setupAsync:Null; + var testAsync:Null; + var teardownAsync:Null; + public function new(fixture : TestFixture) { if(fixture == null) throw "fixture argument is null"; this.fixture = fixture; @@ -35,6 +44,12 @@ class TestHandler { if (fixture.ignoringInfo.isIgnored) { results.add(Ignore(fixture.ignoringInfo.ignoreReason)); } + + testCase = fixture.target; + test = fixture.test; + if(test == null) { + throw 'Fixture is missing test data'; + } } public function execute() { @@ -43,227 +58,137 @@ class TestHandler { executeFinally(); return; } + bindHandler(); + runSetup(); + } - //ugly hack to call executeFinally() only once if asynchronous code is involved - var isSync = true; - var expectingAsync = true; - function run() { - if(isSync) { - expectingAsync = false; - return; - } - executeFixtureMethod(); - executeFinally(); + function runSetup() { + inline function handleCatch(e:Any, stack:CallStack) { + results.add(SetupError(e, stack)); + completedFinally(); } - try { - executeMethod(fixture.setup); - executeAsyncMethod(fixture.setupAsync, run); - if(!expectingAsync) { - executeFixtureMethod(); - } + setupAsync = fixture.setupMethod(); } #if !UTEST_FAILURE_THROW - catch(e : Dynamic) { - results.add(SetupError(e, exceptionStack())); + catch(e:ValueException) { + handleCatch(e.value, e.stack); + return; + } catch(e) { + handleCatch(e, e.stack); + return; } #end - isSync = false; - if(!expectingAsync) { - executeFinally(); + + setupAsync.then(checkSetup); + } + + function checkSetup() { + if(setupAsync.timedOut) { + results.add(SetupError('Setup timeout', [])); + completedFinally(); + } else { + runTest(); } } - function executeFixtureMethod() { + function runTest() { + inline function handleCatch(e:Any, stack:CallStack) { + results.add(Error(e, stack)); + runTeardown(); + } try { - executeMethod(fixture.method); + testAsync = test.execute(); } #if !UTEST_FAILURE_THROW - catch (e : Dynamic) { - results.add(Error(e, exceptionStack())); + catch(e:ValueException) { + handleCatch(e.value, e.stack); + return; + } catch(e) { + handleCatch(e, e.stack); + return; } #end + + testAsync.then(checkTest); } - function executeFinally() { + function checkTest() { onPrecheck.dispatch(this); - checkTested(); + + if(testAsync.timedOut) { + results.add(TimeoutError(1, [])); + onTimeout.dispatch(this); + + } else if(testAsync.resolved) { + if(results.length == 0) { + results.add(Warning('no assertions')); + } + onTested.dispatch(this); + + } else { + throw 'Unexpected test state'; + } + + runTeardown(); } - static function exceptionStack(pops = 2) - { - var stack = haxe.CallStack.exceptionStack(); - while (pops-- > 0) - stack.pop(); - return stack; + function runTeardown() { + inline function handleCatch(e:Any, stack:CallStack) { + results.add(TeardownError(e, CallStack.exceptionStack())); + completedFinally(); + } + try { + teardownAsync = fixture.teardownMethod(); + } + #if !UTEST_FAILURE_THROW + catch(e:ValueException) { + handleCatch(e.value, e.stack); + return; + } catch(e) { + handleCatch(e, e.stack); + return; + } + #end + + teardownAsync.then(checkTeardown); } - function checkTested() { -#if ((haxe_ver >= "3.4.0") || flash || js) - if(expiration == null || asyncStack.length == 0) { - tested(); - } else if(haxe.Timer.stamp() > expiration) { - timeout(); - } else { - haxe.Timer.delay(checkTested, POLLING_TIME); + function checkTeardown() { + if(teardownAsync.timedOut) { + results.add(TeardownError('Teardown timeout', [])); } -#else - if(asyncStack.length == 0) - tested(); - else - timeout(); -#end + completedFinally(); } - public var expiration(default, null) : Null; - public function setTimeout(timeout : Int) { - var newExpire = haxe.Timer.stamp() + timeout/1000; - expiration = (expiration == null) ? newExpire : (newExpire > expiration ? newExpire : expiration); + function executeFinally() { + onPrecheck.dispatch(this); + tested(); } function bindHandler() { if (wasBound) return; - Assert.results = this.results; - Assert.createAsync = this.addAsync; - Assert.createEvent = this.addEvent; + Assert.results = this.results; wasBound = true; - } function unbindHandler() { if (!wasBound) return; - Assert.results = null; - Assert.createAsync = function(?f, ?t){ return function(){}}; - Assert.createEvent = function(f, ?t){ return function(e){}}; + Assert.results = null; wasBound = false; } - /** - * Adds a function that is called asynchronously. - * - * Example: - *
-  * var fixture = new TestFixture(new TestClass(), "test");
-  * var handler = new TestHandler(fixture);
-  * var flag = false;
-  * var async = handler.addAsync(function() {
-  *   flag = true;
-  * }, 50);
-  * handler.onTimeout.add(function(h) {
-  *   trace("TIMEOUT");
-  * });
-  * handler.onTested.add(function(h) {
-  *   trace(flag ? "OK" : "FAILED");
-  * });
-  * haxe.Timer.delay(function() async(), 10);
-  * handler.execute();
-  * 
- * @param f, the function that is called asynchrnously - * @param timeout, the maximum time to wait for f() (default is 250) - * @return returns a function closure that must be executed asynchrnously - */ - public function addAsync(?f : Void->Void, timeout = 250) { - if (null == f) - f = function() { } - asyncStack.add(f); - var handler = this; - setTimeout(timeout); - return function() { - if(!handler.asyncStack.remove(f)) { - handler.results.add(AsyncError("async function already executed", [])); - return; - } - try { - handler.bindHandler(); - f(); - } - #if !UTEST_FAILURE_THROW - catch(e : Dynamic) { - handler.results.add(AsyncError(e, exceptionStack(0))); // TODO check the correct number of functions is popped from the stack - } - #end - }; - } - - public function addEvent(f : EventArg->Void, timeout = 250) { - asyncStack.add(f); - var handler = this; - setTimeout(timeout); - return function(e : EventArg) { - if(!handler.asyncStack.remove(f)) { - handler.results.add(AsyncError("event already executed", [])); - return; - } - try { - handler.bindHandler(); - f(e); - } - #if !UTEST_FAILURE_THROW - catch(e : Dynamic) { - handler.results.add(AsyncError(e, exceptionStack(0))); // TODO check the correct number of functions is popped from the stack - } - #end - }; - } - - function executeMethod(name : String) { - if(name == null) return; - bindHandler(); - Reflect.callMethod(fixture.target, Reflect.field(fixture.target, name), []); - } - - function executeAsyncMethod(name : String, done : Void->Void) : Void { - if(name == null) { - done(); - return; - } - bindHandler(); - Reflect.callMethod(fixture.target, Reflect.field(fixture.target, name), [done]); - } - function tested() { if(results.length == 0) results.add(Warning("no assertions")); onTested.dispatch(this); - completed(); + completedFinally(); } function timeout() { results.add(TimeoutError(asyncStack.length, [])); onTimeout.dispatch(this); - completed(); - } - - function completed() { - if (fixture.ignoringInfo.isIgnored) { - completedFinally(); - return; - } - - //ugly hack to call completedFinally() only once if asynchronous code is involved - var isSync = true; - var expectingAsync = true; - function complete() { - if(isSync) { - expectingAsync = false; - return; - } - completedFinally(); - } - - try { - executeMethod(fixture.teardown); - executeAsyncMethod(fixture.teardownAsync, complete); - } - #if !UTEST_FAILURE_THROW - catch(e : Dynamic) { - results.add(TeardownError(e, exceptionStack(2))); // TODO check the correct number of functions is popped from the stack - } - #end - isSync = false; - if(!expectingAsync) { - completedFinally(); - } + completedFinally(); } function completedFinally() { diff --git a/src/utest/TestResult.hx b/src/utest/TestResult.hx index 6e3b17f..ae1fe4f 100644 --- a/src/utest/TestResult.hx +++ b/src/utest/TestResult.hx @@ -15,22 +15,17 @@ class TestResult { public function new(){} - public static function ofHandler(handler : TestHandler) { + public static function ofHandler(handler : TestHandler):TestResult { var r = new TestResult(); var path = Type.getClassName(Type.getClass(handler.fixture.target)).split('.'); r.cls = path.pop(); r.pack = path.join('.'); - r.method = handler.fixture.method; - r.setup = handler.fixture.setup; - r.setupAsync = handler.fixture.setupAsync; - r.teardown = handler.fixture.teardown; - r.teardownAsync = handler.fixture.teardownAsync; + r.method = handler.fixture.name; r.assertations = handler.results; r.executionTime = handler.executionTime; return r; } - #if (haxe_ver >= "3.4.0") public static function ofFailedSetupClass(testCase:ITest, assertation:Assertation):TestResult { var r = new TestResult(); var path = Type.getClassName(Type.getClass(testCase)).split('.'); @@ -52,15 +47,14 @@ class TestResult { r.assertations.add(assertation); return r; } - #end -// public function allOk():Bool{ -// for(l in assertations) { -// switch (l){ -// case Success(_): break; -// default: return false; -// } -// } -// return true; -// } + public function allOk():Bool{ + for(l in assertations) { + switch l{ + case Success(_): + default: return false; + } + } + return true; + } } diff --git a/src/utest/UTest.hx b/src/utest/UTest.hx index 5c3d37f..4b1f93d 100644 --- a/src/utest/UTest.hx +++ b/src/utest/UTest.hx @@ -1,10 +1,14 @@ package utest; +#if (haxe_ver < "4.1.0") + #error 'Haxe 4.1.0 or later is required to run UTest' +#end + /** * Helper class to quickly generate test cases. */ -@:final class UTest { - public static function run(cases : Array, ?callback : Void->Void) { +final class UTest { + public static function run(cases : Array, ?callback : ()->Void) { var runner = new Runner(); for(eachCase in cases) runner.addCase(eachCase); diff --git a/src/utest/exceptions/AssertFailureException.hx b/src/utest/exceptions/AssertFailureException.hx new file mode 100644 index 0000000..9822625 --- /dev/null +++ b/src/utest/exceptions/AssertFailureException.hx @@ -0,0 +1,15 @@ +package utest.exceptions; + +/** + Thrown on a failed assert if `UTEST_FAILURE_THROW` is defined. + + UTest can throw an unhandled exception instead of adding a failure to the report. + + Enable this behavior with `-D UTEST_FAILURE_THROW`, or by adding `UTEST_FAILURE_THROW` to the environment variables at compile time. + + In this case any exception or failure in test or setup methods will lead to a crash. + Instead of a test report you will see an unhandled exception message with the exception + stack trace (depending on a target platform). +**/ +class AssertFailureException extends UTestException { +} \ No newline at end of file diff --git a/src/utest/exceptions/UTestException.hx b/src/utest/exceptions/UTestException.hx new file mode 100644 index 0000000..248c0a4 --- /dev/null +++ b/src/utest/exceptions/UTestException.hx @@ -0,0 +1,10 @@ +package utest.exceptions; + +import haxe.Exception; + +/** + * Exceptions thrown by UTest + */ +class UTestException extends Exception { + +} \ No newline at end of file diff --git a/src/utest/ui/Report.hx b/src/utest/ui/Report.hx index 4116c59..032418d 100644 --- a/src/utest/ui/Report.hx +++ b/src/utest/ui/Report.hx @@ -4,28 +4,22 @@ import utest.Runner; import utest.ui.common.IReport; import utest.ui.common.HeaderDisplayMode; -#if php -import php.Web; -#elseif neko -import neko.Web; -#end - class Report { public static function create(runner : Runner, ?displaySuccessResults : SuccessResultsDisplayMode, ?headerDisplayMode : HeaderDisplayMode) : IReport { - var report : IReport; + var report:IReport; #if teamcity report = new utest.ui.text.TeamcityReport(runner); #elseif travis report = new utest.ui.text.PrintReport(runner); -#elseif (php || neko) - if (!Web.isModNeko) +#elseif php + if (php.Lib.isCli()) report = new utest.ui.text.PrintReport(runner); else report = new utest.ui.text.HtmlReport(runner, true); #elseif nodejs report = new utest.ui.text.PrintReport(runner); #elseif js - if(#if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end("typeof window != 'undefined'")) { + if(js.Syntax.code("typeof window != 'undefined'")) { report = new utest.ui.text.HtmlReport(runner, true); } else report = new utest.ui.text.PrintReport(runner); diff --git a/src/utest/ui/common/IReport.hx b/src/utest/ui/common/IReport.hx index 5cbe79e..fdb58f0 100644 --- a/src/utest/ui/common/IReport.hx +++ b/src/utest/ui/common/IReport.hx @@ -2,9 +2,9 @@ package utest.ui.common; import utest.ui.common.HeaderDisplayMode; -interface IReport +interface IReport> { public var displaySuccessResults : SuccessResultsDisplayMode; public var displayHeader : HeaderDisplayMode; - public function setHandler(handler : T -> Void) : Void; + public function setHandler(handler : (T) -> Void) : Void; } \ No newline at end of file diff --git a/src/utest/ui/common/ReportTools.hx b/src/utest/ui/common/ReportTools.hx index b2c8f2f..25b97d9 100644 --- a/src/utest/ui/common/ReportTools.hx +++ b/src/utest/ui/common/ReportTools.hx @@ -2,7 +2,7 @@ package utest.ui.common; class ReportTools { - public static function hasHeader(report : IReport, stats : ResultStats) + public static function hasHeader>(report : IReport, stats : ResultStats) { switch(report.displayHeader) { @@ -23,7 +23,7 @@ class ReportTools }; } - public static function skipResult(report : IReport, stats : ResultStats, isOk) + public static function skipResult>(report : IReport, stats : ResultStats, isOk) { if (!stats.isOk) return false; return switch(report.displaySuccessResults) @@ -34,7 +34,7 @@ class ReportTools }; } - public static function hasOutput(report : IReport, stats : ResultStats) + public static function hasOutput>(report : IReport, stats : ResultStats) { if (!stats.isOk) return true; return hasHeader(report, stats); diff --git a/src/utest/ui/common/ResultAggregator.hx b/src/utest/ui/common/ResultAggregator.hx index 8735d5f..e305d0e 100644 --- a/src/utest/ui/common/ResultAggregator.hx +++ b/src/utest/ui/common/ResultAggregator.hx @@ -27,34 +27,10 @@ class ResultAggregator { } function start(runner : Runner) { - checkNonITest(); root = new PackageResult(null); onStart.dispatch(); } - function checkNonITest() { - var first = null; - var total = 0; - for(i in 0...runner.length) { - var fixture = runner.getFixture(i); - if(!fixture.isITest) { - total++; - if(first == null) { - first = Type.getClassName(Type.getClass(fixture.target)); - } - } - } - if(total > 0) { - var baseMsg = 'implement utest.ITest. Non-ITest tests are deprecated. Implement utest.ITest or extend utest.Test.'; - var msg = switch(total) { - case 1: '$first doesn\'t $baseMsg'; - case 2: '$first and 1 other don\'t $baseMsg'; - case _: '$first and $total others don\'t $baseMsg'; - } - trace(msg); - } - } - function getOrCreatePackage(pack : String, flat : Bool, ?ref : PackageResult) { if(ref == null) ref = root; if(pack == null || pack == '') return ref; diff --git a/src/utest/ui/text/DiagnosticsReport.hx b/src/utest/ui/text/DiagnosticsReport.hx index 1a7a95a..70dcaf5 100644 --- a/src/utest/ui/text/DiagnosticsReport.hx +++ b/src/utest/ui/text/DiagnosticsReport.hx @@ -27,7 +27,7 @@ class DiagnosticsReport extends PlainTextReport { } } - override function dumpStack(stack:Array) { + override function dumpStack(stack:CallStack) { if (stack.length == 0) { return ""; } diff --git a/src/utest/ui/text/HtmlReport.hx b/src/utest/ui/text/HtmlReport.hx index f442eae..c43775d 100644 --- a/src/utest/ui/text/HtmlReport.hx +++ b/src/utest/ui/text/HtmlReport.hx @@ -6,7 +6,6 @@ import utest.ui.common.ClassResult; import utest.ui.common.FixtureResult; import utest.ui.common.IReport; import utest.ui.common.HeaderDisplayMode; -import utest.utils.Misc; import utest.Runner; import utest.ui.common.ResultAggregator; import utest.ui.common.PackageResult; @@ -32,13 +31,13 @@ class HtmlReport implements IReport { public var traceRedirected(default, null) : Bool; public var displaySuccessResults : SuccessResultsDisplayMode; public var displayHeader : HeaderDisplayMode; - public var handler : HtmlReport -> Void; + public var handler : (HtmlReport) -> Void; var aggregator : ResultAggregator; - var oldTrace : Dynamic; - var _traces : Array<{ msg : String, infos : PosInfos, time : Float, delta : Float, stack : Array }>; + var oldTrace : Any; + var _traces : Array<{ msg : String, infos : PosInfos, time : Float, delta : Float, stack : CallStack }>; - public function new(runner : Runner, ?outputHandler : HtmlReport -> Void, traceRedirected = true) { + public function new(runner : Runner, ?outputHandler : (HtmlReport) -> Void, traceRedirected = true) { aggregator = new ResultAggregator(runner, true); runner.onStart.add(start); aggregator.onComplete.add(complete); @@ -52,7 +51,7 @@ class HtmlReport implements IReport { displayHeader = AlwaysShowHeader; } - public function setHandler(handler : HtmlReport -> Void) : Void + public function setHandler(handler : (HtmlReport) -> Void) : Void this.handler = handler; public function redirectTrace() { @@ -70,7 +69,7 @@ class HtmlReport implements IReport { } var _traceTime : Null; - function _trace(v : Dynamic, ?infos : PosInfos) { + function _trace(v : Any, ?infos : PosInfos) { var time = Timer.stamp(); var delta = _traceTime == null ? 0 : time - _traceTime; _traces.push({ @@ -137,7 +136,7 @@ class HtmlReport implements IReport { buf.add(''); } - function formatStack(stack : Array, addNL = true) { + function formatStack(stack : CallStack, addNL = true) { var parts = []; var nl = addNL ? '\n' : ''; var last = null; @@ -214,9 +213,9 @@ class HtmlReport implements IReport { buf.add('\n'); } - function getErrorDescription(e : Dynamic) { + function getErrorDescription(e : Any) { #if flash9 - if (Misc.isOfType(e, flash.errors.Error)) { + if (Std.isOfType(e, flash.errors.Error)) { var err = cast(e, flash.errors.Error); return err.name + ": " + err.message; } else { @@ -227,9 +226,9 @@ class HtmlReport implements IReport { #end } - function getErrorStack(s : Array, e : Dynamic) { + function getErrorStack(s : CallStack, e : Any) { #if flash9 - if (Misc.isOfType(e, flash.errors.Error)) { + if (Std.isOfType(e, flash.errors.Error)) { var stack = cast(e, flash.errors.Error).getStackTrace(); if (null != stack) { var parts = stack.split("\n"); @@ -290,7 +289,7 @@ class HtmlReport implements IReport { function indents(count : Int) { return [for(i in 0...count) " "].join(""); } - function dumpStack(stack : Array) { + function dumpStack(stack : CallStack) { if (stack.length == 0) return ""; var parts = CallStack.toString(stack).split("\n"), @@ -449,8 +448,8 @@ class HtmlReport implements IReport { }; #if js - if(#if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end("'undefined' != typeof window")) { - #if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end("window").utest_result = exposedResult; + if(js.Syntax.code("'undefined' != typeof window")) { + js.Syntax.code("window").utest_result = exposedResult; } #elseif flash flash.external.ExternalInterface.call('(function(result){ window.utest_result = result; })', exposedResult ); @@ -722,11 +721,11 @@ function utestRemoveTooltip() { return; } - var isDef = function(v) : Bool return #if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end("typeof v != 'undefined'"); - var hasProcess : Bool = #if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end("typeof process != 'undefined'"); + var isDef = function(v) : Bool return js.Syntax.code("typeof v != 'undefined'"); + var hasProcess : Bool = js.Syntax.code("typeof process != 'undefined'"); if(hasProcess) { - #if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end("process.stdout.write")(report.getHtml()); + js.Syntax.code("process.stdout.write")(report.getHtml()); return; } diff --git a/src/utest/ui/text/PlainTextReport.hx b/src/utest/ui/text/PlainTextReport.hx index d053e92..81c90c1 100644 --- a/src/utest/ui/text/PlainTextReport.hx +++ b/src/utest/ui/text/PlainTextReport.hx @@ -12,12 +12,13 @@ import haxe.CallStack; class PlainTextReport implements IReport { public var displaySuccessResults : SuccessResultsDisplayMode; public var displayHeader : HeaderDisplayMode; - public var handler : PlainTextReport -> Void; + public var handler : (PlainTextReport) -> Void; var aggregator : ResultAggregator; var newline : String; var indent : String; - public function new(runner : Runner, ?outputHandler : PlainTextReport -> Void) { + + public function new(runner : Runner, ?outputHandler : (PlainTextReport) -> Void) { aggregator = new ResultAggregator(runner, true); runner.onStart.add(start); aggregator.onComplete.add(complete); @@ -27,7 +28,7 @@ class PlainTextReport implements IReport { displayHeader = AlwaysShowHeader; } - public function setHandler(handler : PlainTextReport -> Void) : Void + public function setHandler(handler : (PlainTextReport) -> Void) : Void this.handler = handler; var startTime : Float; @@ -51,7 +52,7 @@ class PlainTextReport implements IReport { return s; } - function dumpStack(stack : Array) { + function dumpStack(stack : CallStack) { if (stack.length == 0) return ""; var parts = CallStack.toString(stack).split("\n"), @@ -157,22 +158,23 @@ class PlainTextReport implements IReport { function complete(result : PackageResult) { this.result = result; if (handler != null) handler(this); + var exitCode = result.stats.isOk ? 0 : 1; #if (php || neko || cpp || cs || java || python || lua || eval || hl) - Sys.exit(result.stats.isOk ? 0 : 1); + Sys.exit(exitCode); #elseif js - if(#if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end('typeof phantom != "undefined"')) - #if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end('phantom').exit(result.stats.isOk ? 0 : 1); - if(#if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end('typeof process != "undefined"')) - #if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end('process').exit(result.stats.isOk ? 0 : 1); + if(js.Syntax.code('typeof phantom != "undefined"')) + js.Syntax.code('phantom').exit(exitCode); + if(js.Syntax.code('typeof process != "undefined"')) + js.Syntax.code('process').exit(exitCode); #elseif air - flash.desktop.NativeApplication.nativeApplication.exit(result.stats.isOk ? 0 : 1); + flash.desktop.NativeApplication.nativeApplication.exit(exitCode); #elseif (flash && exit) if(flash.system.Security.sandboxType == "localTrusted") { var delay = 5; trace('all done, exiting in $delay seconds'); haxe.Timer.delay(function() try { - flash.system.System.exit(result.stats.isOk ? 0 : 1); - } catch(e : Dynamic) { + flash.system.System.exit(exitCode); + } catch(_) { // do nothing }, delay * 1000); } diff --git a/src/utest/ui/text/PrintReport.hx b/src/utest/ui/text/PrintReport.hx index c038f67..5461acf 100644 --- a/src/utest/ui/text/PrintReport.hx +++ b/src/utest/ui/text/PrintReport.hx @@ -19,7 +19,7 @@ class PrintReport extends PlainTextReport { #if php if (php.Lib.isCli()) { #elseif neko - if (!neko.Web.isModNeko) { + if (true) { #end newline = "\n"; indent = " "; diff --git a/src/utest/ui/text/TeamcityReport.hx b/src/utest/ui/text/TeamcityReport.hx index e5cba36..0546557 100644 --- a/src/utest/ui/text/TeamcityReport.hx +++ b/src/utest/ui/text/TeamcityReport.hx @@ -21,7 +21,7 @@ import utest.ui.common.PackageResult; * Otherwise `"Target: {TARGET_NAME}"` will be used. */ class TeamcityReport extends PlainTextReport { - public function new(runner:Runner, ?outputHandler:PlainTextReport -> Void) { + public function new(runner:Runner, ?outputHandler:(PlainTextReport) -> Void) { super(runner, outputHandler); newline = "\n"; indent = " "; diff --git a/src/utest/utils/AccessoriesUtils.hx b/src/utest/utils/AccessoriesUtils.hx index ea5bc2c..6aaa200 100644 --- a/src/utest/utils/AccessoriesUtils.hx +++ b/src/utest/utils/AccessoriesUtils.hx @@ -5,19 +5,19 @@ import utest.TestData; using utest.utils.AccessoriesUtils; class AccessoriesUtils { - static public function getSetupClass(accessories:Accessories):Void->Async { + static public function getSetupClass(accessories:Accessories):()->Async { return accessories.setupClass == null ? Async.getResolved : accessories.setupClass; } - static public function getSetup(accessories:Accessories):Void->Async { + static public function getSetup(accessories:Accessories):()->Async { return accessories.setup == null ? Async.getResolved : accessories.setup; } - static public function getTeardown(accessories:Accessories):Void->Async { + static public function getTeardown(accessories:Accessories):()->Async { return accessories.teardown == null ? Async.getResolved : accessories.teardown; } - static public function getTeardownClass(accessories:Accessories):Void->Async { + static public function getTeardownClass(accessories:Accessories):()->Async { return accessories.teardownClass == null ? Async.getResolved : accessories.teardownClass; } } \ No newline at end of file diff --git a/src/utest/utils/Macro.hx b/src/utest/utils/Macro.hx index 1acf98f..474bc09 100644 --- a/src/utest/utils/Macro.hx +++ b/src/utest/utils/Macro.hx @@ -4,13 +4,6 @@ import haxe.macro.Compiler; import haxe.macro.Context; class Macro { - macro static public function checkHaxe() { - #if (haxe_ver < '3.4.0') - Context.warning('UTest will stop supporting Haxe 3.3 and older in UTest 2.0.0', Context.currentPos()); - #end - return macro {}; - } - macro static public function importEnvSettings() { var env = Sys.environment(); for (name in env.keys()) { diff --git a/src/utest/utils/Misc.hx b/src/utest/utils/Misc.hx deleted file mode 100644 index fd81a3c..0000000 --- a/src/utest/utils/Misc.hx +++ /dev/null @@ -1,11 +0,0 @@ -package utest.utils; - -class Misc { - static public inline function isOfType(v:Dynamic, t:Dynamic):Bool { - #if (haxe_ver >= 4.1) - return Std.isOfType(v, t); - #else - return Std.is(v, t); - #end - } -} \ No newline at end of file diff --git a/src/utest/utils/Print.hx b/src/utest/utils/Print.hx index bcdc66d..d0190ae 100644 --- a/src/utest/utils/Print.hx +++ b/src/utest/utils/Print.hx @@ -5,19 +5,19 @@ class Print { #if sys Sys.print(msg); #elseif js - #if (haxe_ver >= 4.0) js.Syntax.code #else untyped __js__ #end('console.log({0})', msg); + js.Syntax.code('console.log({0})', msg); #else trace(msg); #end } - static public function startCase(caseName:Dynamic) { + static public function startCase(caseName:String) { #if UTEST_PRINT_TESTS immediately('Running $caseName...\n'); #end } - static public function startTest(name) { + static public function startTest(name:String) { #if UTEST_PRINT_TESTS immediately(' $name\n'); #end diff --git a/src/utest/utils/TestBuilder.hx b/src/utest/utils/TestBuilder.hx index 09c1f77..4572696 100644 --- a/src/utest/utils/TestBuilder.hx +++ b/src/utest/utils/TestBuilder.hx @@ -1,5 +1,6 @@ package utest.utils; +import haxe.ds.Option; import haxe.macro.Type.MetaAccess; import utest.TestData.AccessoryName; import haxe.macro.Type.ClassType; @@ -9,8 +10,7 @@ import haxe.macro.ExprTools; using Lambda; -@:enum -private abstract IsAsync(Int) { +private enum abstract IsAsync(Int) { var Yes = 1; var No = 0; var Unknown = -1; @@ -22,6 +22,10 @@ class TestBuilder { static inline var PROCESSED_META = ':utestProcessed'; static inline var TIMEOUT_META = ':timeout'; static inline var DEPENDS_META = ':depends'; + static inline var IGNORE_META = ':ignore'; + static inline var IGNORED_META = 'Ignored'; + + static inline var DEFAULT_TIMEOUT = 250; macro static public function build():Array { if(Context.defined('display') #if display || true #end) { @@ -189,6 +193,10 @@ class TestBuilder { static function processTest(cls:ClassType, field:Field, fn:Function, dependencies:Array, initExprs:Array) { var test = field.name; + var ignore = switch getIgnore(cls, field) { + case None: macro haxe.ds.Option.None; + case Some(reason): macro haxe.ds.Option.Some($v{reason}); + } switch(fn.args.length) { //synchronous test case 0: @@ -198,7 +206,8 @@ class TestBuilder { execute:function() { this.$test(); return @:privateAccess utest.Async.getResolved(); - } + }, + ignore:$ignore })); //asynchronous test case 1: @@ -209,7 +218,8 @@ class TestBuilder { var async = @:privateAccess new utest.Async(${getTimeoutExpr(cls, field)}); this.$test(async); return async; - } + }, + ignore:$ignore })); //wtf test case _: @@ -344,8 +354,8 @@ class TestBuilder { static function getTimeoutExpr(cls:ClassType, field:Field):Expr { function getValue(meta:MetadataEntry):Expr { if(meta.params == null || meta.params.length != 1) { - error('@:timeout meta should have one argument. E.g. @:timeout(250)', meta.pos); - return macro 250; + error('@$TIMEOUT_META meta should have one argument. E.g. @$TIMEOUT_META($DEFAULT_TIMEOUT)', meta.pos); + return macro $v{DEFAULT_TIMEOUT}; } else { return meta.params[0]; } @@ -363,7 +373,40 @@ class TestBuilder { return getValue(cls.meta.extract(TIMEOUT_META)[0]); } - return macro @:pos(field.pos) 250; + return macro @:pos(field.pos) $v{DEFAULT_TIMEOUT}; + } + + static function getIgnore(cls:ClassType, field:Field):Option> { + function getReason(meta:MetadataEntry):Option> { + if(meta.name == IGNORED_META) { + Context.warning('@$IGNORED_META is deprecated, use @$IGNORE_META instead', field.pos); + } + return Some(switch meta.params { + case null | []: + null; + case [{expr:EConst(CString(reason, _))}]: + reason; + case _: + error('@${meta.name} meta should have either no arguments or a single string argument', meta.pos); + null; + }); + } + + if(field.meta != null) { + for(meta in field.meta) { + if(meta.name == IGNORE_META || meta.name == IGNORED_META) { + return getReason(meta); + } + } + } + + if(cls.meta.has(IGNORE_META)) { + return getReason(cls.meta.extract(IGNORE_META)[0]); + } else if(cls.meta.has(IGNORED_META)) { + return getReason(cls.meta.extract(IGNORED_META)[0]); + } + + return None; } static function strBinop(op:Binop) { diff --git a/submit.sh b/submit.sh index 1b9d223..33547da 100755 --- a/submit.sh +++ b/submit.sh @@ -1,4 +1,4 @@ #!/bin/sh rm utest.zip -zip -r utest.zip hxml src test haxelib.json extraParams.hxml README.md CHANGELOG.md -x "*/\.*" +zip -r utest.zip hxml src test haxelib.json defines.json meta.json extraParams.hxml README.md CHANGELOG.md -x "*/\.*" haxelib submit utest.zip diff --git a/test/TestAll.hx b/test/TestAll.hx index 0738e3f..0824a33 100644 --- a/test/TestAll.hx +++ b/test/TestAll.hx @@ -1,19 +1,14 @@ -import utest.Runner; +import haxe.Exception; +import utest.Runner; import utest.ui.Report; -import utest.TestResult; -#if (haxe_ver >= "3.4.0") import utest.TestAsyncITest; -#end class TestAll { - #if (haxe_ver >= "3.4.0") static var testAsyncITest:TestAsyncITest = new TestAsyncITest(); - #end public static function addTests(runner : Runner) { runner.addCase(new utest.TestAssert()); runner.addCase(new utest.TestDispatcher()); - #if (haxe_ver >= "3.4.0") runner.addCase(new utest.TestAsync()); runner.addCase(new utest.TestAsync.TestClassTimeout()); runner.addCase(new utest.TestSyncITest()); @@ -25,7 +20,6 @@ class TestAll { runner.addCase(new utest.TestCaseDependencies.Case4()); runner.addCase(new utest.TestWithMacro()); runner.addCase(testAsyncITest); - #end runner.addCase(new utest.TestIgnored()); runner.addCase(new utest.TestRunner()); } @@ -35,12 +29,9 @@ class TestAll { addTests(runner); - // get test result to determine exit status - var r:TestResult = null; - runner.onProgress.add(function(o){ if (o.done == o.totals) r = o.result;}); - #if (haxe_ver >= "3.4.0") - runner.onComplete.add(function(runner) { - //check test case dependencies + #if !UTEST_PATTERN + //Check test case dependencies + runner.onComplete.add(_ -> { var expected = ['Case1', 'Case3', 'Case2', 'Case4']; for(i in 0...expected.length) { if(utest.TestCaseDependencies.caseExecutionOrder[i] != expected[i]) { @@ -54,8 +45,29 @@ class TestAll { }); #end - Report.create(runner); + var report = Report.create(runner); + report.displayHeader = AlwaysShowHeader; + report.displaySuccessResults = NeverShowSuccessResults; + + var failed = false; + runner.onProgress.add(r -> { + if(!r.result.allOk()) { + failed = true; + } + }); + runner.run(); + + #if closure + //Node's `process.exit` is not available in minified js because every attempt to access to it gets mangled. + //this is the simplest workaround I could invent. + runner.onComplete.add(_ -> { + if(failed) { + //delay to make sure the report is fully printed first. + haxe.Timer.delay(() -> throw new Exception('Failed. See UTest report above.'), 10); + } + }); + #end } public function new(){} diff --git a/test/js-minified-header.js b/test/js-minified-header.js new file mode 100644 index 0000000..683cf0e --- /dev/null +++ b/test/js-minified-header.js @@ -0,0 +1,6 @@ +/** @suppress {duplicate} */ +var global; +/** @suppress {duplicate} */ +var phantom; +/** @suppress {duplicate} */ +var process; \ No newline at end of file diff --git a/test/utest/TestAssert.hx b/test/utest/TestAssert.hx index 849a18c..9622bbc 100644 --- a/test/utest/TestAssert.hx +++ b/test/utest/TestAssert.hx @@ -1,258 +1,264 @@ package utest; +import haxe.PosInfos; +import haxe.Exception; import utest.Assert; +import utest.Assert.*; import utest.Assertation; @:keep -class TestAssert { - public function new(){} +class TestAssert extends Test { - var resultsbypass : List; - var results : List; - public function bypass() { - resultsbypass = Assert.results; + function _bypass(fn:()->Void):List { + var currentResults = Assert.results; Assert.results = new List(); + fn(); + var results = Assert.results; + Assert.results = currentResults; + return results; } - public function restore() { - results = Assert.results; - Assert.results = resultsbypass; + function success(assertion:()->Bool, ?msg:String, ?pos:PosInfos) { + var outcome = false; + _bypass(() -> outcome = assertion()); + Assert.isTrue(outcome, msg, pos); + } + + function failure(assertion:()->Bool, ?msg:String, ?pos:PosInfos) { + var outcome = true; + _bypass(() -> outcome = assertion()); + Assert.isFalse(outcome, msg, pos); + } + + function warning(expectedMessage:String, assertion:()->Void, ?pos:PosInfos) { + var results = Lambda.array(_bypass(assertion)); + if(results.length != 1) { + Assert.fail('warnings expects a single warning assertion', pos); + } else { + switch results[0] { + case Warning(msg): equals(expectedMessage, msg, pos); + case _: throw new Exception('Unexpected behavior'); + } + } + } + + function failMessages(expectedMessage:String, assertion:()->Bool, ?pos:PosInfos) { + var results = Lambda.array(_bypass(assertion)); + if(results.length != 1) { + Assert.fail('failMessage expects a single failing assertion', pos); + } else { + switch results[0] { + case Failure(msg, pos): equals(expectedMessage, msg, pos); + case _: throw new Exception('Unexpected behavior'); + } + } } public function testBooleans() { - bypass(); - Assert.isTrue(true); - Assert.isTrue(false); - Assert.isFalse(true); - Assert.isFalse(false); - restore(); - expect(2, 2); + success(() -> isTrue(true)); + failure(() -> isTrue(false)); + failure(() -> isFalse(true)); + success(() -> isFalse(false)); } public function testNullity() { - bypass(); - Assert.isNull(null); - Assert.isNull(0); - Assert.isNull(0.0); - Assert.isNull(0.1); - Assert.isNull(1); - Assert.isNull(""); - Assert.isNull("a"); - Assert.isNull(Math.NaN); - Assert.isNull(Math.POSITIVE_INFINITY); - Assert.isNull(true); - Assert.isNull(false); - restore(); - expect(1, 10); + success(() -> isNull(null)); + failure(() -> isNull(0)); + failure(() -> isNull(0.0)); + failure(() -> isNull(0.1)); + failure(() -> isNull(1)); + failure(() -> isNull("")); + failure(() -> isNull("a")); + failure(() -> isNull(Math.NaN)); + failure(() -> isNull(Math.POSITIVE_INFINITY)); + failure(() -> isNull(true)); + failure(() -> isNull(false)); } public function testNoNullity() { - bypass(); - Assert.notNull(null); - Assert.notNull(0); - Assert.notNull(0.0); - Assert.notNull(0.1); - Assert.notNull(1); - Assert.notNull(""); - Assert.notNull("a"); - Assert.notNull(Math.NaN); - Assert.notNull(Math.POSITIVE_INFINITY); - Assert.notNull(true); - Assert.notNull(false); - restore(); - expect(10, 1); + failure(() -> notNull(null)); + success(() -> notNull(0)); + success(() -> notNull(0.0)); + success(() -> notNull(0.1)); + success(() -> notNull(1)); + success(() -> notNull("")); + success(() -> notNull("a")); + success(() -> notNull(Math.NaN)); + success(() -> notNull(Math.POSITIVE_INFINITY)); + success(() -> notNull(true)); + success(() -> notNull(false)); } public function testRaises() { - bypass(); - var errors : Array = ["e", 1, 0.1, new TestAssert(), {}, [1]]; - var types : Array = [String, Int, Float, TestAssert, Dynamic, Array]; - var i = 0; - var expectedsuccess = 12; - for(error in errors) - for(type in types) { - i++; - Assert.raises(function() throw error, type); + //expect exception of any type + success(() -> raises(() -> throw 'error')); + + //expect specific exception type + var errors : Array = ["str", 1, 0.1, new TestAssert(), {}, [1], new SampleException('sample exception')]; + var types : Array = [String, Int, Float, TestAssert, Dynamic, Array, SampleException]; + var expectedsuccess = 14; + for(errorIndex => error in errors) + for(typeIndex => type in types) { + if(errorIndex == typeIndex || type == Dynamic || (Std.isOfType(error, Int) && type == Float)) { + success(() -> raises(() -> throw error, type), 'success expected: Assert.raises($error, $type)'); + } else { + failure(() -> raises(() -> throw error, type), 'failure expected: Assert.raises($error, $type)'); + } } - restore(); - expect(expectedsuccess, i-expectedsuccess); } public function testIs() { - bypass(); - var values : Array = ["e", 1, 0.1, new TestAssert(), {}, [1]]; - var types : Array = [String, Int, Float, TestAssert, Dynamic, Array]; - var i = 0; - var expectedsuccess = 12; - for(value in values) - for(type in types) { - i++; - Assert.isOfType(value, type); + var values : Array = ["str", 1, 0.1, new TestAssert(), {}, [1]]; + var types : Array = [String, Int, Float, TestAssert, Dynamic, Array]; + for(valueIndex => value in values) + for(typeIndex => type in types) { + if(valueIndex == typeIndex || type == Dynamic || (Std.isOfType(value, Int) && type == Float)) { + success(() -> isOfType(value, type), 'success expected: Assert.isOfType($value, $type)'); + } else { + failure(() -> isOfType(value, type), 'failure expected: Assert.isOfType($value, $type)'); + } } - restore(); - expect(expectedsuccess, i-expectedsuccess); + } + + public function testSimilar() { + similar({value:'hello'}, new Dummy('hello', new Dummy())); + return; + + success(() -> similar({a:'b'}, {a:'b', c:1})); + success(() -> similar(['a' => 'b'], ['a' => 'b', 'c' => 'd'])); + success(() -> similar(['a', 'b'], ['a', 'b', 'c'])); + success(() -> similar({value:'hello'}, new Dummy('hello', new Dummy()))); + success(() -> similar({value:'hello', sub:{value:'world'}}, new Dummy('hello', new Dummy('world')))); + success(() -> similar({sub:{value:'world'}}, new Dummy('hello', new Dummy('world')))); + success(() -> same(new Dummy(), new DummyLike())); + + failure(() -> similar({a:'b'}, {a:'', c:1})); + failure(() -> similar({a:'b'}, {c:1})); + failure(() -> similar(['a' => 'b', 'c' => 'd'], ['a' => 'b'])); + failure(() -> similar(['a' => 'b'], ['a' => 'c'])); + failure(() -> similar(['a', 'b', 'c'], ['a', 'b'])); + failure(() -> similar(['a', 'b', 'c'], ['a', 'b', 'd'])); + failure(() -> similar({value:'hello'}, new Dummy('world'))); + failure(() -> similar({value:'hello', sub:{value:'world'}}, new Dummy('hello', new Dummy('foo')))); } public function testSamePrimitive() { - bypass(); - Assert.same(null, 1); - Assert.same(1, 1); - Assert.same(1, "1"); - Assert.same("a", "a"); - Assert.same(null, ""); - Assert.same(new Date(2000, 0, 1, 0, 0, 0), null); - Assert.same([1 => "a", 2 => "b"], [1 => "a", 2 => "b"]); - Assert.same(["a" => 1], ["a" => 1]); - Assert.same(["a" => 1], [1 => 1]); - Assert.same([1 => "a"], [1 => "a", 2 => "b"]); + //same primitives + failure(() -> same(null, 1)); + success(() -> same(1, 1)); + failure(() -> same(1, "1")); + success(() -> same("a", "a")); + failure(() -> same(null, "")); + failure(() -> same(new Date(2000, 0, 1, 0, 0, 0), null)); + success(() -> same([1 => "a", 2 => "b"], [1 => "a", 2 => "b"])); + success(() -> same(["a" => 1], ["a" => 1])); + failure(() -> same(["a" => 1], [1 => 1])); + failure(() -> same([1 => "a"], [1 => "a", 2 => "b"])); // TODO doesn't work anymore // Assert.same(new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 0, 0, 0)); - restore(); - expect(5, 6); - } - - public function testSameType() { - bypass(); - Assert.same(null, {}); - Assert.same(null, null); - Assert.same({}, null); - Assert.same({}, 1); - Assert.same({}, []); - Assert.same(null, None); - Assert.same(None, null); - - restore(); - expect(1, 6); - } - - public function testSameArray() { - bypass(); - Assert.same([], []); - Assert.same([1], ["1"]); - Assert.same([1,2,3], [1,2,3]); - Assert.same([1,2,3], [1,2]); - Assert.same([1,2], [1,2,3]); - Assert.same([1,[1,2]], [1,[1,2]]); - Assert.same([1,[1,2]], [1,[]], false); - Assert.same([1,[1,2]], [1,[]], true); - - restore(); - expect(4, 4); - } - - public function testSameArray_message() { - bypass(); - Assert.same([{field:{sub:1}}], [{field:{sub:2}}]); - restore(); - - Assert.equals(1, results.length); + //same types + failure(() -> same(null, {})); + success(() -> same(null, null)); + failure(() -> same({}, null)); + failure(() -> same({}, 1)); + failure(() -> same({}, [])); + failure(() -> same(null, None)); + failure(() -> same(None, null)); + + //same array + success(() -> same([], [])); + failure(() -> same([1], ["1"])); + success(() -> same([1,2,3], [1,2,3])); + failure(() -> same([1,2,3], [1,2])); + failure(() -> same([1,2], [1,2,3])); + success(() -> same(([1,[1,2]]:Array), ([1,[1,2]]:Array))); + success(() -> same(([1,[1,2]]:Array), ([1,[]]:Array), false)); + failure(() -> same(([1,[1,2]]:Array), ([1,[]]:Array), true)); + + //check messages for arrays var expectedMessage = 'expected array element at [0] to have 1 but it is 2 for field array[0].field.sub'; - switch(results.first()) { - case Failure(msg, _): Assert.equals(expectedMessage, msg); - case _: Assert.fail(); - } - } - - public function testSameObject() { - bypass(); - Assert.same({}, {}); - Assert.same({a:1}, {a:"1"}); - Assert.same({a:1,b:"c"}, {a:1,b:"c"}); - Assert.same({a:1,b:"c"}, {a:1,c:"c"}); - Assert.same({a:1,b:"c"}, {a:1}); - Assert.same({a:1,b:{a:1,c:"c"}}, {a:1,b:{a:1,c:"c"}}); - Assert.same({a:1,b:{a:1,c:"c"}}, {a:1,b:{}}, false); - Assert.same({a:1,b:{a:1,c:"c"}}, {a:1,b:{}}, true); - - restore(); - expect(4, 4); - } - - public var value : String; - public var sub : TestAssert; - public function testSameInstance() { - var c1 = new TestAssert(); + //Use quoted fields names to avoid js minificator to cripple them because + //we rely on the fields names being intact in this test. + failMessages(expectedMessage, () -> same([{"field":{"sub":1}}], [{"field":{"sub":2}}])); + + //same objects + success(() -> same({}, {})); + failure(() -> same({a:1}, {a:"1"})); + success(() -> same({a:1,b:"c"}, {a:1,b:"c"})); + failure(() -> same({a:1,b:"c"}, {a:1,c:"c"})); + failure(() -> same({a:1,b:"c"}, {a:1})); + success(() -> same({a:1,b:{a:1,c:"c"}}, {a:1,b:{a:1,c:"c"}})); + success(() -> same({a:1,b:{a:1,c:"c"}}, {a:1,b:{}}, false)); + failure(() -> same({a:1,b:{a:1,c:"c"}}, {a:1,b:{}}, true)); + + //same class instances + var c1 = new Dummy(); c1.value = "a"; - var c2 = new TestAssert(); + var c2 = new Dummy(); c2.value = "a"; - var c3 = new TestAssert(); + var c3 = new Dummy(); - var r1 = new TestAssert(); + var r1 = new Dummy(); r1.sub = c1; - var r2 = new TestAssert(); + var r2 = new Dummy(); r2.sub = c2; - var r3 = new TestAssert(); + var r3 = new Dummy(); r3.sub = c3; + success(() -> same(c1, c1)); + success(() -> same(c1, c2)); + failure(() -> same(c1, c3)); - bypass(); - Assert.same(c1, c1); - Assert.same(c1, c2); - Assert.same(c1, c3); - - Assert.same(r1, r2); - Assert.same(r1, r3, false); - Assert.same(r1, r3, true); + success(() -> same(r1, r2)); + success(() -> same(r1, r3, false)); + failure(() -> same(r1, r3, true)); - restore(); - expect(4, 2); - } + failure(() -> same(new Dummy(), new DummyLike())); - public function testSameIterable() { - var list1 = new List(); + //same iterables + var list1 = new List(); list1.add("a"); list1.add(1); var s1 = new List(); s1.add(2); list1.add(s1); - var list2 = new List(); + var list2 = new List(); list2.add("a"); list2.add(1); list2.add(s1); - var list3 = new List(); + var list3 = new List(); list3.add("a"); list3.add(1); list3.add(new List()); - bypass(); - Assert.same(list1, list2); - Assert.same(list1, list3, false); - Assert.same(list1, list3, true); + success(() -> same(list1, list2)); + success(() -> same(list1, list3, false)); + failure(() -> same(list1, list3, true)); - Assert.same(0...3, 0...3); - Assert.same(0...3, 0...4); + success(() -> same(0...3, 0...3)); + failure(() -> same(0...3, 0...4)); - restore(); - expect(3, 2); - } + //check messages for iterables - public function testSameIterable_message() { + //Use quoted fields names to avoid js minificator to cripple them because + //we rely on the fields names being intact in this test. var list1 = new List(); - list1.add({field:{sub:1}}); + list1.add({"field":{"sub":1}}); var list2 = new List(); - list2.add({field:{sub:2}}); + list2.add({"field":{"sub":2}}); - bypass(); - Assert.same(list1, list2); - restore(); + failMessages('expected 1 but it is 2 for field iterable[0].field.sub', () -> same(list1, list2)); - Assert.equals(1, results.length); - var expectedMessage = 'expected 1 but it is 2 for field iterable[0].field.sub'; - switch(results.first()) { - case Failure(msg, _): Assert.equals(expectedMessage, msg); - case _: Assert.fail(); - } - } -/* - TODO Needs fixing - public function testSameMap() { + //same maps var h1 = new haxe.ds.StringMap(); h1.set('a', 'b'); h1.set('c', 'd'); var h2 = new haxe.ds.StringMap(); h2.set('a', 'b'); h2.set('c', 'd'); + var h1ExtraKeys = h1.copy(); + h1ExtraKeys.set('e', 'f'); var h3 = new haxe.ds.StringMap(); var h4 = new haxe.ds.StringMap(); h4.set('c', 'd'); @@ -262,116 +268,59 @@ class TestAssert { var i2 = new haxe.ds.IntMap(); i2.set(2, 'b'); - bypass(); - - Assert.same(h1, h2); - Assert.same(h1, h3); - Assert.same(h1, h4); - Assert.same(i1, i2); - - restore(); - expect(2, 2); - } -*/ - public function testSameEnums() { - bypass(); - - Assert.same(None, None); - Assert.same(Some("a"), Some("a")); - Assert.same(Some("a"), Some("b"), true); - Assert.same(Some("a"), Some("b"), false); - Assert.same(Some("a"), None); - Assert.same(Rec(Rec(Some("a"))), Rec(Rec(Some("a")))); - Assert.same(Rec(Rec(Some("a"))), Rec(None), true); -// TODO: something goes wrong here with flash6, haXe/Flash6 bug? -#if !flash6 - Assert.same(Rec(Rec(Some("a"))), Rec(Rec(None)), false); -#end - - restore(); -#if flash6 - expect(3, 4); -#else - expect(4, 4); -#end + success(() -> same(h1, h2)); + failure(() -> same(h1, h1ExtraKeys)); + failure(() -> same(h1, h3)); + failure(() -> same(h1, h4)); + success(() -> same(i1, i2)); + + //same enums + success(() -> same(None, None)); + success(() -> same(Some("a"), Some("a"))); + failure(() -> same(Some("a"), Some("b"), true)); //expected to fail + failure(() -> same(Some("a"), Some("b"), false)); //expected to fail + failure(() -> same(Some("a"), None)); //expected to fail + success(() -> same(Rec(Rec(Some("a"))), Rec(Rec(Some("a"))))); + failure(() -> same(Rec(Rec(Some("a"))), Rec(None), true)); //expected to fail + success(() -> same(Rec(Rec(Some("a"))), Rec(Rec(None)), false)); } public function testEquals() { - bypass(); - var values : Array = ["e", 1, 0.1, {}]; - var expecteds : Array = ["e", 1, 0.1, {}]; - var i = 0; - var expectedsuccess = 3; - for(expected in expecteds) - for(value in values) { - i++; - Assert.equals(expected, value); + var obj1 = {}; + var obj2 = {}; + var values : Array = ["e", 1, 0.1, obj1, obj2]; + var expecteds : Array = ["e", 1, 0.1, obj1, obj2]; + for(expectedIndex => expected in expecteds) + for(valueIndex => value in values) { + if(valueIndex == expectedIndex) + success(() -> equals(expected, value), 'success expected: equals($expected, $value)') + else + failure(() -> equals(expected, value), 'failure expected: equals($expected, $value)'); } - restore(); - expect(expectedsuccess, i-expectedsuccess); } public function testFloatEquals() { - bypass(); - var values : Array = [1, 0.1, 0.000000000000000000000000000011, Math.NaN, Math.NEGATIVE_INFINITY, Math.POSITIVE_INFINITY, Math.PI, 0.11]; - var expecteds : Array = [1, 0.1, 0.000000000000000000000000000012, Math.NaN, Math.NEGATIVE_INFINITY, Math.POSITIVE_INFINITY, Math.PI, 0.12]; - var i = 0; - var expectedsuccess = 7; - for(expected in expecteds) - for(value in values) { - i++; - Assert.floatEquals(expected, value); + var values : Array = [1, 0.1, 0.000000000000000000000000000011, Math.NaN, Math.NEGATIVE_INFINITY, Math.POSITIVE_INFINITY, Math.PI, 0.11, 0.12]; + var expecteds : Array = [1, 0.1, 0.000000000000000000000000000012, Math.NaN, Math.NEGATIVE_INFINITY, Math.POSITIVE_INFINITY, Math.PI, 0.11, 0.12]; + for(expectedIndex => expected in expecteds) + for(valueIndex => value in values) { + if(valueIndex == expectedIndex) + success(() -> floatEquals(expected, value), 'success expected: floatEquals($expected, $value)') + else + failure(() -> floatEquals(expected, value), 'failure expected: floatEquals($expected, $value)'); } - restore(); - expect(expectedsuccess, i-expectedsuccess); } public function testPass() { - bypass(); - Assert.pass(); - restore(); - expect(1, 0); + success(() -> pass()); } public function testFail() { - bypass(); - Assert.fail(); - restore(); - expect(0, 1); + failure(() -> fail()); } public function testWarn() { - bypass(); - Assert.warn(""); - restore(); - expect(0, 0, 1); - } - - #if (haxe_ver >= "3.4.0") - public function testCreateAsync() { - var assert = Assert.createAsync(function() Assert.pass(), 1000); - haxe.Timer.delay(assert, 50); - } - #end - - public function expect(esuccesses : Int, efailures : Int, eothers = 0) { - var successes = 0; - var failures = 0; - var others = 0; - for(result in results) { - switch(result) { - case Success(_): - successes++; - case Failure(_,_): - failures++; - default: - others++; - } - } - Assert.equals(eothers, others, "expected "+eothers+" other results but were "+others); -// TODO doesn't work anymore -// Assert.equals(esuccesses, successes, "expected "+esuccesses+" successes but were "+successes); - Assert.equals(efailures, failures, "expected "+efailures+" failures but were "+failures); + warning('Attention!', () -> warn('Attention!')); } } @@ -380,3 +329,23 @@ private enum Sample { Some(s : String); Rec(s : Sample); } + +private class SampleException extends Exception {} + +private class Dummy { + public var value : Null; + public var sub : Null; + public function new(?value:String, ?sub:Dummy) { + this.value = value; + this.sub = sub; + } +} + +private class DummyLike { + public var value : Null; + public var sub : Null; + public function new(?value:String, ?sub:DummyLike) { + this.value = value; + this.sub = sub; + } +} \ No newline at end of file diff --git a/test/utest/TestDispatcher.hx b/test/utest/TestDispatcher.hx index 77b0d40..d8d8bc5 100644 --- a/test/utest/TestDispatcher.hx +++ b/test/utest/TestDispatcher.hx @@ -4,8 +4,7 @@ import utest.Assert; import utest.Dispatcher; @:keep -class TestDispatcher { - public function new(){} +class TestDispatcher extends Test { public function testBase() { var dispatcher : Dispatcher = new Dispatcher(); diff --git a/test/utest/TestIgnored.hx b/test/utest/TestIgnored.hx index 2deea7a..dddc727 100644 --- a/test/utest/TestIgnored.hx +++ b/test/utest/TestIgnored.hx @@ -2,15 +2,12 @@ package utest; import utest.TestHandler; import utest.TestFixture; +import utest.Async; @:keep -class TestIgnored { - public function new() { - } - - public function testIgnoredWithoutReason():Void { - var async = Assert.createAsync(); +class TestIgnored extends Test { + public function testIgnoredWithoutReason(async:Async):Void { var runner:Runner = new Runner(); runner.addCase(new TestCaseWithIgnoredCaseWithoutReason()); @@ -31,15 +28,13 @@ class TestIgnored { default: Assert.fail('Expected Assertation.Ignore(""), Received: ${th.results.first()}'); } - async(); + async.done(); }); runner.run(); } - public function testIgnoredWithReason():Void { - var async = Assert.createAsync(); - + public function testIgnoredWithReason(async:Async):Void { var runner:Runner = new Runner(); runner.addCase(new TestCaseWithIgnoredCaseWithReason()); @@ -60,7 +55,7 @@ class TestIgnored { default: Assert.fail('Expected Assertation.Ignore("REASON"), Received: ${th.results.first()}'); } - async(); + async.done(); }); runner.run(); @@ -68,20 +63,17 @@ class TestIgnored { } @:keep -class TestCaseWithIgnoredCaseWithoutReason { - public function new() {} - - @Ignored +class TestCaseWithIgnoredCaseWithoutReason extends Test { + @:ignore public function testIgnoredWithoutReason():Void { Assert.fail(); } } @:keep -class TestCaseWithIgnoredCaseWithReason { - public function new() {} +class TestCaseWithIgnoredCaseWithReason extends Test { - @Ignored("REASON") + @:ignore("REASON") public function testIgnoredWithReason():Void { Assert.fail(); } diff --git a/test/utest/TestRunner.hx b/test/utest/TestRunner.hx index ad2a260..cad01fb 100644 --- a/test/utest/TestRunner.hx +++ b/test/utest/TestRunner.hx @@ -1,24 +1,33 @@ package utest; @:keep -class TestRunner { - public function new(){} +class TestRunner extends Test { public function testAddCases_notRecursive() { var runner = new Runner(); runner.addCases('utest.testrunner', false); - Assert.equals(2, runner.length); + Assert.equals(3, runner.length); } public function testAddCases_recursive() { var runner = new Runner(); runner.addCases('utest.testrunner', true); - Assert.equals(3, runner.length); + Assert.equals(4, runner.length); } - public function testAddCases_identifier() { + public function testAddCases_packageIdentifier() { var runner = new Runner(); runner.addCases(utest.testrunner, true); + Assert.equals(4, runner.length); + } + + public function testAddCases_nameFilter() { + var runner = new Runner(); + runner.addCases('utest.testrunner', true, 'Dummy3'); + Assert.equals(1, runner.length); + + var runner = new Runner(); + runner.addCases('utest.testrunner', true, '^Test'); Assert.equals(3, runner.length); } } \ No newline at end of file diff --git a/test/utest/testrunner/TestDummy1.hx b/test/utest/testrunner/TestDummy1.hx index d42915c..f9610e9 100644 --- a/test/utest/testrunner/TestDummy1.hx +++ b/test/utest/testrunner/TestDummy1.hx @@ -1,7 +1,7 @@ package utest.testrunner; @:keep -class TestDummy1 { +class TestDummy1 implements ITest { public function new() {} public function testDummy() { diff --git a/test/utest/testrunner/TestDummy2.hx b/test/utest/testrunner/TestDummy2.hx index d8658f9..c40ba06 100644 --- a/test/utest/testrunner/TestDummy2.hx +++ b/test/utest/testrunner/TestDummy2.hx @@ -1,7 +1,7 @@ package utest.testrunner; @:keep -class TestDummy2 { +class TestDummy2 implements ITest { public function new() {} public function testDummy() { diff --git a/test/utest/testrunner/UnusualTestDummy.hx b/test/utest/testrunner/UnusualTestDummy.hx new file mode 100644 index 0000000..a0f4f4e --- /dev/null +++ b/test/utest/testrunner/UnusualTestDummy.hx @@ -0,0 +1,10 @@ +package utest.testrunner; + +@:keep +class UnusualTestDummy implements ITest { + public function new() {} + + public function testDummy() { + Assert.pass(); + } +} \ No newline at end of file diff --git a/test/utest/testrunner/sub/TestDummy3.hx b/test/utest/testrunner/sub/TestDummy3.hx index 3056cd1..3d66072 100644 --- a/test/utest/testrunner/sub/TestDummy3.hx +++ b/test/utest/testrunner/sub/TestDummy3.hx @@ -1,7 +1,7 @@ package utest.testrunner.sub; @:keep -class TestDummy3 { +class TestDummy3 implements ITest { public function new() {} public function testDummy() { diff --git a/tests.hxml b/tests.hxml index 922a63b..3bdba16 100644 --- a/tests.hxml +++ b/tests.hxml @@ -1 +1 @@ -hxml/travis.hxml \ No newline at end of file +hxml/common.hxml \ No newline at end of file