├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .haxerc ├── .travis.yml ├── .vscode ├── settings.json └── tasks.json ├── README.md ├── build.hxml ├── haxe_libraries ├── ansi.hxml ├── deep_equal.hxml ├── hxcpp.hxml ├── hxcs.hxml ├── hxjava.hxml ├── hxnodejs.hxml ├── tink_chunk.hxml ├── tink_cli.hxml ├── tink_core.hxml ├── tink_io.hxml ├── tink_macro.hxml ├── tink_priority.hxml ├── tink_streams.hxml ├── tink_stringly.hxml ├── tink_syntaxhub.hxml ├── tink_testrunner.hxml ├── tink_unittest.hxml └── travix.hxml ├── haxelib.json ├── src └── deepequal │ ├── CustomCompare.hx │ ├── DeepEqual.hx │ ├── Error.hx │ ├── Helper.hx │ ├── Noise.hx │ ├── Outcome.hx │ ├── Path.hx │ ├── Result.hx │ └── custom │ ├── Anything.hx │ ├── ArrayContains.hx │ ├── ArrayOfLength.hx │ ├── EnumByName.hx │ ├── ObjectContains.hx │ ├── ObjectContainsKeys.hx │ ├── Regex.hx │ └── StringStartsWith.hx ├── submit.sh ├── tests.hxml └── tests ├── FakeOutcome.hx └── RunTests.hx /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | haxe-version: 20 | - stable 21 | - nightly 22 | target: 23 | - interp 24 | - node 25 | - neko 26 | - python 27 | - php 28 | - cpp 29 | - js 30 | - jvm 31 | - java 32 | - cs 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | 37 | - name: Get yarn cache directory path 38 | id: yarn-cache-dir-path 39 | run: echo "::set-output name=dir::$(yarn cache dir)" 40 | 41 | - uses: actions/cache@v1 42 | with: 43 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 44 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 45 | restore-keys: | 46 | ${{ runner.os }}-yarn- 47 | 48 | - name: Cache Haxe 49 | uses: actions/cache@v1 50 | with: 51 | path: ~/haxe 52 | key: haxe 53 | 54 | - uses: lix-pm/setup-lix@master 55 | - run: lix install haxe ${{ matrix.haxe-version }} 56 | - run: lix download 57 | - run: lix run travix ${{ matrix.target }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | -------------------------------------------------------------------------------- /.haxerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.2.1", 3 | "resolveLibs": "scoped" 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: xenial 3 | 4 | language: node_js 5 | node_js: 8 6 | 7 | os: 8 | - linux 9 | # - osx 10 | 11 | env: 12 | - HAXE_VERSION=3.4.7 13 | - HAXE_VERSION=latest 14 | - HAXE_VERSION=nightly 15 | 16 | install: 17 | - npm i -g lix@15.3.13 18 | - lix install haxe $HAXE_VERSION 19 | - lix download 20 | 21 | script: 22 | - lix run travix interp 23 | - lix run travix neko 24 | - lix run travix python 25 | - lix run travix node 26 | - lix run travix js 27 | # - lix run travix flash 28 | - lix run travix java 29 | - if [[ "$(haxe -version)" =~ ^4.* ]]; then lix run travix java -D jvm; fi 30 | - lix run travix cpp 31 | - lix run travix cs 32 | - lix run travix php -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // These are configurations used for haxe completion. 3 | // 4 | // Each configuration is an array of arguments that will be passed to the Haxe completion server, 5 | // they should only contain arguments and/or hxml files that are needed for completion, 6 | // such as -cp, -lib, target output settings and defines. 7 | "haxe.displayConfigurations": [ 8 | ["build.hxml"] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "lix", 4 | "args": ["run","travix","node"], 5 | "problemMatcher": { 6 | "owner": "haxe", 7 | "pattern": { 8 | "regexp": "^(.+):(\\d+): (?:lines \\d+-(\\d+)|character(?:s (\\d+)-| )(\\d+)) : (?:(Warning) : )?(.*)$", 9 | "file": 1, 10 | "line": 2, 11 | "endLine": 3, 12 | "column": 4, 13 | "endColumn": 5, 14 | "severity": 6, 15 | "message": 7 16 | } 17 | }, 18 | "group": { 19 | "kind": "build", 20 | "isDefault": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deep Equal 2 | 3 | [![Build Status](https://github.com/kevinresol/deep_equal/actions/workflows/ci.yml/badge.svg)](https://github.com/kevinresol/deep_equal/actions) 4 | 5 | Every programmer needs to compare values. 6 | 7 | 8 | 9 | ## Basic Usage 10 | 11 | ```haxe 12 | 13 | var expected = // any value, array, objects, etc 14 | var actual = // some other value 15 | 16 | switch DeepEqual.compare(expected, actual) { 17 | case Success(_): // they are value-identical 18 | case Failure(f): trace(f.message, f.data); // they are different! 19 | } 20 | 21 | ``` 22 | 23 | ## Advanced Usage 24 | 25 | By default, string, bool, Int, float, date, bytes, int64, array, map, objects, class instances, 26 | enum values, class/enum objects (`Class/Enum`) are recursively compared by value. 27 | In case more advanced comparison (such as partial equals, regex checks, etc) is needed, 28 | one can implement the `CustomCompare` interface and put it as the expected value. 29 | 30 | The following shows an example on checking if an array contains some required elements, 31 | while not necessarily the same length as the required elements. 32 | 33 | ```haxe 34 | 35 | var a = [1,2,3,4]; 36 | var e = new ArrayContains([1,2,3]); 37 | compare(e, a); // success, because the actual value contains all the required values 1, 2 and 3. 38 | 39 | var a = [1,2,3,4]; 40 | var e = new ArrayContains([3,5]); 41 | compare(e, a); // fail, because the actual value does not contain the required value 5. 42 | 43 | class ArrayContains implements deepequal.CustomCompare { 44 | var items:Array; 45 | public function new(items) { 46 | this.items = items; 47 | } 48 | public function check(other:Dynamic, compare:Dynamic->Dynamic->Outcome) { 49 | if(!Std.is(other, Array)) return Failure(new Error('Expected array but got $other')); 50 | for(i in items) { 51 | var matched = false; 52 | for(o in (other:Array)) switch compare(i, o) { 53 | case Success(_): matched = true; break; 54 | case Failure(_): 55 | } 56 | if(!matched) return Failure(new Error('Cannot find $i in $other')); 57 | } 58 | return Success(Noise); 59 | } 60 | } 61 | 62 | ``` 63 | 64 | ### Here are some more use case examples: 65 | 66 | #### Assert array of certain length without caring about the contents 67 | ```haxe 68 | var actual = [1,2,3]; 69 | compare(new ArrayOfLength(3), actual)` // or; 70 | compare([for(i in 0...3) new Anything()], actual); 71 | ``` 72 | 73 | #### Assert object with certain contents without doing a perfect match 74 | ```haxe 75 | var actual = {a:1, b:2} 76 | compare(new ObjectContains({a: 1}), actual); 77 | ``` 78 | 79 | #### Assert an enum value and matches its first parameter as regex 80 | ```haxe 81 | var actual = Bar("MyString", 1); // suppose defined `enum Foo {Bar(s:String, i:Int)}` 82 | compare(new EnumByName(Foo, 'Bar', [new Regex(~/MyS.*/), new Anything()]), actual); 83 | ``` 84 | -------------------------------------------------------------------------------- /build.hxml: -------------------------------------------------------------------------------- 1 | tests.hxml 2 | 3 | -js bin/node/tests.js 4 | -lib hxnodejs 5 | -lib travix 6 | -lib deep_equal -------------------------------------------------------------------------------- /haxe_libraries/ansi.hxml: -------------------------------------------------------------------------------- 1 | -D ansi=1.0.0 2 | # @install: lix --silent download "haxelib:/ansi#1.0.0" into ansi/1.0.0/haxelib 3 | -cp ${HAXE_LIBCACHE}/ansi/1.0.0/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/deep_equal.hxml: -------------------------------------------------------------------------------- 1 | -D deep_equal 2 | -cp src -------------------------------------------------------------------------------- /haxe_libraries/hxcpp.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:/hxcpp#4.1.15" into hxcpp/4.1.15/haxelib 2 | # @run: haxelib run-dir hxcpp ${HAXE_LIBCACHE}/hxcpp/4.1.15/haxelib 3 | -cp ${HAXE_LIBCACHE}/hxcpp/4.1.15/haxelib/ 4 | -D hxcpp=4.1.15 -------------------------------------------------------------------------------- /haxe_libraries/hxcs.hxml: -------------------------------------------------------------------------------- 1 | -D hxcs=3.4.0 2 | # @install: lix --silent download "haxelib:/hxcs#3.4.0" into hxcs/3.4.0/haxelib 3 | # @run: haxelib run-dir hxcs ${HAXE_LIBCACHE}/hxcs/3.4.0/haxelib 4 | -cp ${HAXE_LIBCACHE}/hxcs/3.4.0/haxelib/ 5 | -------------------------------------------------------------------------------- /haxe_libraries/hxjava.hxml: -------------------------------------------------------------------------------- 1 | -D hxjava=3.2.0 2 | # @install: lix --silent download "haxelib:/hxjava#3.2.0" into hxjava/3.2.0/haxelib 3 | # @run: haxelib run-dir hxjava ${HAXE_LIBCACHE}/hxjava/3.2.0/haxelib 4 | -cp ${HAXE_LIBCACHE}/hxjava/3.2.0/haxelib/ 5 | -java-lib lib/hxjava-std.jar 6 | -------------------------------------------------------------------------------- /haxe_libraries/hxnodejs.hxml: -------------------------------------------------------------------------------- 1 | -D hxnodejs=6.9.1 2 | # @install: lix --silent download "gh://github.com/haxefoundation/hxnodejs#666bc1fb9ebf9dcf77150dc4d0ed0be7d551197d" into hxnodejs/6.9.1/github/666bc1fb9ebf9dcf77150dc4d0ed0be7d551197d 3 | -cp ${HAXE_LIBCACHE}/hxnodejs/6.9.1/github/666bc1fb9ebf9dcf77150dc4d0ed0be7d551197d/src 4 | --macro allowPackage('sys') 5 | # should behave like other target defines and not be defined in macro context 6 | --macro define('nodejs') 7 | -------------------------------------------------------------------------------- /haxe_libraries/tink_chunk.hxml: -------------------------------------------------------------------------------- 1 | -D tink_chunk=0.2.0 2 | # @install: lix --silent download "haxelib:/tink_chunk#0.2.0" into tink_chunk/0.2.0/haxelib 3 | -cp ${HAXE_LIBCACHE}/tink_chunk/0.2.0/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_cli.hxml: -------------------------------------------------------------------------------- 1 | -D tink_cli=0.3.1 2 | # @install: lix --silent download "haxelib:/tink_cli#0.3.1" into tink_cli/0.3.1/haxelib 3 | -lib tink_io 4 | -lib tink_stringly 5 | -lib tink_macro 6 | -cp ${HAXE_LIBCACHE}/tink_cli/0.3.1/haxelib/src 7 | # Make sure docs are generated 8 | -D use-rtti-doc -------------------------------------------------------------------------------- /haxe_libraries/tink_core.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/haxetink/tink_core#abee932c4e724517090238b6527eac28874c0354" into tink_core/1.27.1/github/abee932c4e724517090238b6527eac28874c0354 2 | -cp ${HAXE_LIBCACHE}/tink_core/1.27.1/github/abee932c4e724517090238b6527eac28874c0354/src 3 | -D tink_core=1.27.1 -------------------------------------------------------------------------------- /haxe_libraries/tink_io.hxml: -------------------------------------------------------------------------------- 1 | -D tink_io=0.6.0 2 | # @install: lix --silent download "haxelib:/tink_io#0.6.0" into tink_io/0.6.0/haxelib 3 | -lib tink_chunk 4 | -lib tink_streams 5 | -cp ${HAXE_LIBCACHE}/tink_io/0.6.0/haxelib/src 6 | -------------------------------------------------------------------------------- /haxe_libraries/tink_macro.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/haxetink/tink_macro#e01bf6912a23cd49733df6c8b789c8e8b845995d" into tink_macro/0.23.0/github/e01bf6912a23cd49733df6c8b789c8e8b845995d 2 | -lib tink_core 3 | -cp ${HAXE_LIBCACHE}/tink_macro/0.23.0/github/e01bf6912a23cd49733df6c8b789c8e8b845995d/src 4 | -D tink_macro=0.23.0 -------------------------------------------------------------------------------- /haxe_libraries/tink_priority.hxml: -------------------------------------------------------------------------------- 1 | -D tink_priority=0.1.4 2 | # @install: lix --silent download "haxelib:/tink_priority#0.1.4" into tink_priority/0.1.4/haxelib 3 | -cp ${HAXE_LIBCACHE}/tink_priority/0.1.4/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_streams.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/haxetink/tink_streams#aab6ed2ce041f1d3960f1c992be2a6e754b2faff" into tink_streams/0.3.3/github/aab6ed2ce041f1d3960f1c992be2a6e754b2faff 2 | -lib tink_core 3 | -cp ${HAXE_LIBCACHE}/tink_streams/0.3.3/github/aab6ed2ce041f1d3960f1c992be2a6e754b2faff/src 4 | -D tink_streams=0.3.3 5 | # temp for development, delete this file when pure branch merged 6 | -D pure -------------------------------------------------------------------------------- /haxe_libraries/tink_stringly.hxml: -------------------------------------------------------------------------------- 1 | -D tink_stringly=0.2.0 2 | # @install: lix --silent download "haxelib:/tink_stringly#0.2.0" into tink_stringly/0.2.0/haxelib 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_stringly/0.2.0/haxelib/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_syntaxhub.hxml: -------------------------------------------------------------------------------- 1 | -D tink_syntaxhub=0.4.3 2 | # @install: lix --silent download "haxelib:/tink_syntaxhub#0.4.3" into tink_syntaxhub/0.4.3/haxelib 3 | -lib tink_priority 4 | -lib tink_macro 5 | -cp ${HAXE_LIBCACHE}/tink_syntaxhub/0.4.3/haxelib/src 6 | --macro tink.SyntaxHub.use() -------------------------------------------------------------------------------- /haxe_libraries/tink_testrunner.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/haxetink/tink_testrunner#abc1379b767d58051111543ac6dc3bd5d6b19227" into tink_testrunner/0.8.0/github/abc1379b767d58051111543ac6dc3bd5d6b19227 2 | -lib ansi 3 | -lib tink_macro 4 | -lib tink_streams 5 | -cp ${HAXE_LIBCACHE}/tink_testrunner/0.8.0/github/abc1379b767d58051111543ac6dc3bd5d6b19227/src 6 | -D tink_testrunner=0.8.0 -------------------------------------------------------------------------------- /haxe_libraries/tink_unittest.hxml: -------------------------------------------------------------------------------- 1 | -D tink_unittest=0.6.2 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_unittest#0b0c7de647e522ca42662e2cdfc59e21ed8d4eb4" into tink_unittest/0.6.2/github/0b0c7de647e522ca42662e2cdfc59e21ed8d4eb4 3 | -lib tink_syntaxhub 4 | -lib tink_testrunner 5 | -cp ${HAXE_LIBCACHE}/tink_unittest/0.6.2/github/0b0c7de647e522ca42662e2cdfc59e21ed8d4eb4/src 6 | --macro tink.unit.AssertionBufferInjector.use() -------------------------------------------------------------------------------- /haxe_libraries/travix.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "gh://github.com/back2dos/travix#d19b6bd0d71f991cefd64225b21bd63a5f3e0251" into travix/0.14.1/github/d19b6bd0d71f991cefd64225b21bd63a5f3e0251 2 | # @post-install: cd ${HAXE_LIBCACHE}/travix/0.14.1/github/d19b6bd0d71f991cefd64225b21bd63a5f3e0251 && haxe -cp src --run travix.PostDownload 3 | # @run: haxelib run-dir travix ${HAXE_LIBCACHE}/travix/0.14.1/github/d19b6bd0d71f991cefd64225b21bd63a5f3e0251 4 | -lib tink_cli 5 | -cp ${HAXE_LIBCACHE}/travix/0.14.1/github/d19b6bd0d71f991cefd64225b21bd63a5f3e0251/src 6 | -D travix=0.14.1 7 | --macro travix.Macro.setup() -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep_equal", 3 | "description": "Recursive comparison-by-value helper", 4 | "classPath": "src", 5 | "dependencies": {}, 6 | "contributors": [ 7 | "kevinresol" 8 | ], 9 | "version": "0.3.2", 10 | "releasenote": "Fix error message", 11 | "tags": [ 12 | "deep equal", 13 | "compare" 14 | ], 15 | "license": "MIT" 16 | } -------------------------------------------------------------------------------- /src/deepequal/CustomCompare.hx: -------------------------------------------------------------------------------- 1 | package deepequal; 2 | 3 | interface CustomCompare { 4 | function check(actual:Dynamic, compare:Dynamic->Dynamic->Result):Result; 5 | } -------------------------------------------------------------------------------- /src/deepequal/DeepEqual.hx: -------------------------------------------------------------------------------- 1 | package deepequal; 2 | 3 | import haxe.Int64; 4 | import haxe.PosInfos; 5 | import haxe.io.Bytes; 6 | import haxe.Constraints; 7 | import deepequal.Outcome; 8 | import deepequal.Noise; 9 | import deepequal.Error; 10 | import deepequal.Helper.*; 11 | 12 | using Lambda; 13 | 14 | class DeepEqual { 15 | public static function compare(e:Dynamic, a:Dynamic, ?pos:haxe.PosInfos) { 16 | return switch new Compare(e, a).compare() { 17 | case Failure(f): 18 | Failure(new Error(f.message + ' @ v' + reconstructPath(f.path), pos)); 19 | case Success(s): 20 | Success(s); 21 | } 22 | } 23 | 24 | static function reconstructPath(path:Array) { 25 | var buf = new StringBuf(); 26 | for(p in path) switch p { 27 | case EnumParam(i): buf.add('(enumParam:$i)'); 28 | case Index(i): buf.add('[$i]'); 29 | case Field(k): buf.add('.$k'); 30 | case Key(k): buf.add('[$k]'); 31 | } 32 | return buf.toString(); 33 | } 34 | } 35 | 36 | 37 | 38 | private class Compare { 39 | var path:Array; 40 | var e:Dynamic; 41 | var a:Dynamic; 42 | 43 | public function new(e, a) { 44 | path = []; 45 | this.e = e; 46 | this.a = a; 47 | } 48 | 49 | static function comparer(e:Dynamic, a:Dynamic):Result 50 | return new Compare(e, a).compare(); 51 | 52 | public function compare():Result { 53 | 54 | if(e == null) { 55 | 56 | return simple(e, a); 57 | 58 | } else if(isOfType(e, CustomCompare)) { 59 | 60 | return (e:CustomCompare).check(a, comparer); 61 | 62 | } else if(a == null) { 63 | 64 | return fail('Expected $e but got null'); 65 | 66 | } else if(isOfType(e, String)) { 67 | 68 | if(!isOfType(a, String)) return mismatch(e, a); 69 | return simple(e, a); 70 | 71 | } else if(isInt64(e)) { 72 | 73 | #if !java 74 | if(!isInt64(a)) return mismatch(e, a); 75 | #end 76 | return if((e:Int64) == (a:Int64)) Success(Noise) else mismatch(e, a); 77 | 78 | } else if(isOfType(e, Float)) { 79 | 80 | if(!isOfType(a, Float)) return mismatch(e, a); 81 | return simple(e, a); 82 | 83 | } else if(isOfType(e, Bool)) { 84 | 85 | if(!isOfType(a, Bool)) return mismatch(e, a); 86 | return simple(e, a); 87 | 88 | } else if(isOfType(e, Date)) { 89 | 90 | if(!isOfType(a, Date)) return mismatch(e, a); 91 | return date(e, a); 92 | 93 | } else if (isOfType(e, Array)) { 94 | 95 | if(!isOfType(a, Array)) return fail('Expected array but got $a'); 96 | if(a.length != e.length) return fail('Expected array of length ${e.length} but got ${a.length}'); 97 | for(i in 0...a.length) { 98 | path.push(Index(i)); 99 | switch comparer(e[i], a[i]) { 100 | case Success(_): path.pop(); 101 | case Failure({message: m, path: p}): 102 | path = path.concat(p); 103 | return fail(m); 104 | } 105 | } 106 | return success(); 107 | 108 | } else if(Reflect.isEnumValue(e)) { 109 | 110 | var ecls = Type.getEnum(e); 111 | var acls = try Type.getEnum(a) catch(e:Dynamic) null; 112 | if(acls == null || !isOfType(acls, Enum)) return fail('Expected enum ${Helper.getEnumName(ecls)} but got ${a}'); 113 | if(ecls != acls) return fail('Expected enum ${Helper.getEnumName(ecls)} but got ${Helper.getEnumName(acls)}'); 114 | var a:EnumValue = cast a; 115 | var e:EnumValue = cast e; 116 | switch [e.getName(), a.getName()] { 117 | case [en, an] if(en != an): return fail('Expected enum constructor $en but got $an'); 118 | default: 119 | } 120 | return switch comparer(e.getParameters(), a.getParameters()) { 121 | case Success(_): Success(Noise); 122 | case Failure(f): 123 | switch f.path.pop() { 124 | case Index(i): f.path.push(EnumParam(i)); 125 | default: 126 | } 127 | Failure(f); 128 | } 129 | 130 | } else if(isOfType(e, Bytes)) { 131 | 132 | var e:Bytes = e; 133 | var a:Bytes = a; 134 | if(e.length != a.length) return fail('Expected bytes of length ${e.length} but got ${a.length}'); 135 | for(i in 0...e.length) if(e.get(i) != a.get(i)) return mismatch(e, a); 136 | return success(); 137 | 138 | } else if(isOfType(e, IMap)) { 139 | 140 | if(!isOfType(a, IMap)) return fail('Expected map but got $a'); 141 | 142 | var emap:IMap = e; 143 | var amap:IMap = a; 144 | 145 | var ekeys = [for(k in emap.keys()) k]; 146 | var akeys = [for(k in amap.keys()) k]; 147 | switch akeys.length { 148 | case len if(len != ekeys.length): return fail('Expected ${ekeys.length} field(s) but got $len'); 149 | default: 150 | ekeys.sort(Reflect.compare); 151 | akeys.sort(Reflect.compare); 152 | switch comparer(ekeys, akeys) { 153 | case Success(_): 154 | case Failure({message: m, path: p}): 155 | path = path.concat(p); 156 | return fail('Map keys mismatch: $m'); 157 | } 158 | } 159 | for(key in ekeys) { 160 | 161 | path.push(Key(key)); 162 | switch comparer(emap.get(key), amap.get(key)) { 163 | case Success(_): path.pop(); 164 | case Failure({message: m, path: p}): 165 | path = path.concat(p); 166 | return fail(m); 167 | } 168 | } 169 | return success(); 170 | 171 | } else if(Reflect.isFunction(e)) { 172 | 173 | if(!Reflect.isFunction(a)) return fail('Expected function but got $a'); 174 | if(!Reflect.compareMethods(e, a)) return fail('The two functions are not equal'); 175 | return success(); 176 | 177 | } else if(Type.getClass(e) != null) { 178 | 179 | var ecls = Type.getClass(e); 180 | var acls = try Type.getClass(a) catch(e:Dynamic) null; 181 | if(acls == null || !isOfType(acls, Class)) return fail('Expected class instance of ${Type.getClassName(ecls)} but got ${a}'); 182 | if(ecls != acls) return fail('Expected class instance of ${Type.getClassName(ecls)} but got ${Type.getClassName(acls)}'); 183 | for(key in Type.getInstanceFields(ecls)) { 184 | if(Reflect.isFunction(Reflect.field(e, key))) continue; 185 | path.push(Field(key)); 186 | switch comparer(Reflect.getProperty(e, key), Reflect.getProperty(a, key)) { 187 | case Success(_): path.pop(); 188 | case Failure(f): 189 | path = path.concat(f.path); 190 | return fail(f.message); 191 | } 192 | } 193 | return success(); 194 | 195 | } else if(isOfType(e, Class)) { 196 | 197 | if(!isOfType(a, Class)) return mismatch(e, a); 198 | return simple(e, a); 199 | 200 | } else if(isOfType(e, Enum)) { 201 | 202 | if(!isOfType(a, Enum)) return mismatch(e, a); 203 | return simple(e, a); 204 | 205 | } else if(Reflect.isObject(e)) { 206 | 207 | if(!Reflect.isObject(a)) return fail('Expected object but got $a'); 208 | var keys = Reflect.fields(e); 209 | switch Reflect.fields(a).length { 210 | case len if(len != keys.length): return fail('Expected ${keys.length} field(s) but got $len'); 211 | default: 212 | } 213 | for(key in keys) { 214 | 215 | path.push(Field(key)); 216 | switch comparer(Reflect.field(e, key), Reflect.field(a, key)) { 217 | case Success(_): path.pop(); 218 | case Failure({message: m, path: p}): 219 | path = path.concat(p); 220 | return fail(m); 221 | } 222 | } 223 | return success(); 224 | 225 | } else { 226 | 227 | throw 'Unhandled type: ${Type.typeof(e)} ($e)'; // if anyone reaches this block, please file an issue 228 | 229 | } 230 | } 231 | 232 | function success() 233 | return Success(Noise); 234 | 235 | function fail(msg:String) 236 | return Failure({message: msg, path: path}); 237 | 238 | function mismatch(e:Dynamic, a:Dynamic) 239 | return fail('Expected ${stringify(e)} but got ${a == null ? null : stringify(a)}'); 240 | 241 | function bool(b:Bool) 242 | return b ? success() : fail('Expected true but got false'); 243 | 244 | function simple(e:T, a:T) 245 | return e == a ? success() : mismatch(e, a); 246 | 247 | function date(e:Date, a:Date) { 248 | return switch simple( 249 | #if (neko || cs) 250 | Std.int(e.getTime() / 1000), Std.int(a.getTime() / 1000) 251 | #else 252 | e.getTime(), a.getTime() 253 | #end 254 | ) { 255 | case Success(_): Success(Noise); 256 | case Failure(_): mismatch(e, a); 257 | } 258 | } 259 | 260 | } -------------------------------------------------------------------------------- /src/deepequal/Error.hx: -------------------------------------------------------------------------------- 1 | package deepequal; 2 | 3 | import haxe.PosInfos; 4 | 5 | #if tink_core 6 | typedef Error = tink.core.Error; 7 | #else 8 | class Error { 9 | public var message:String; 10 | public var data:Dynamic; 11 | public var pos:PosInfos; 12 | public function new(message:String, ?pos:PosInfos) { 13 | this.message = message; 14 | this.pos = pos; 15 | } 16 | public static function withData(message:String, data:Dynamic, ?pos:PosInfos) { 17 | var e = new Error(message, pos); 18 | e.data = data; 19 | return e; 20 | } 21 | } 22 | #end -------------------------------------------------------------------------------- /src/deepequal/Helper.hx: -------------------------------------------------------------------------------- 1 | package deepequal; 2 | 3 | 4 | 5 | 6 | class Helper { 7 | public static function stringify(v:Dynamic):String { 8 | return 9 | if(isInt64(v)) haxe.Int64.toStr(v); 10 | else if(isOfType(v, String) || isOfType(v, Float) || isOfType(v, Bool)) haxe.Json.stringify(v); 11 | else if(isOfType(v, Date)) DateTools.format(v, '%F %T'); 12 | else if(isOfType(v, haxe.io.Bytes)) 'bytes(hex):' + (v:haxe.io.Bytes).toHex(); 13 | else if(isOfType(v, Class)) Type.getClassName(v); 14 | else if(isOfType(v, Enum)) Helper.getEnumName(v); 15 | else Std.string(v); 16 | } 17 | 18 | public static inline function isOfType(v:Dynamic, type:Dynamic):Bool { 19 | return 20 | #if (haxe_ver >= 4.1) 21 | Std.isOfType(v, type); 22 | #else 23 | Std.is(v, type); 24 | #end 25 | } 26 | 27 | public static inline function isInt64(v:Dynamic):Bool { 28 | return 29 | #if (haxe_ver >= 4.1) 30 | haxe.Int64.isInt64(v); 31 | #else 32 | haxe.Int64.is(v); 33 | #end 34 | } 35 | 36 | // WORKAROUND: https://github.com/HaxeFoundation/haxe/issues/9759 37 | public static inline function getEnumName(e:Dynamic) { 38 | var v = Type.getEnumName(e); 39 | #if jvm 40 | if(StringTools.startsWith(v, 'haxe.root.')) v = v.substr(10); 41 | #end 42 | return v; 43 | } 44 | } -------------------------------------------------------------------------------- /src/deepequal/Noise.hx: -------------------------------------------------------------------------------- 1 | package deepequal; 2 | 3 | #if tink_core 4 | typedef Noise = tink.core.Noise; 5 | #else 6 | #if cs @:native('deepequal.DeepNoise') #end 7 | enum Noise { 8 | Noise; 9 | } 10 | #end -------------------------------------------------------------------------------- /src/deepequal/Outcome.hx: -------------------------------------------------------------------------------- 1 | package deepequal; 2 | 3 | #if tink_core 4 | typedef Outcome = tink.core.Outcome; 5 | #else 6 | enum Outcome { 7 | Success(s:S); 8 | Failure(f:F); 9 | } 10 | #end -------------------------------------------------------------------------------- /src/deepequal/Path.hx: -------------------------------------------------------------------------------- 1 | package deepequal; 2 | 3 | enum Path { 4 | EnumParam(i:Int); 5 | Index(i:Int); 6 | Field(n:String); 7 | Key(n:Dynamic); 8 | } -------------------------------------------------------------------------------- /src/deepequal/Result.hx: -------------------------------------------------------------------------------- 1 | package deepequal; 2 | 3 | typedef Result = Outcome}>; -------------------------------------------------------------------------------- /src/deepequal/custom/Anything.hx: -------------------------------------------------------------------------------- 1 | package deepequal.custom; 2 | 3 | import deepequal.Outcome; 4 | import deepequal.Noise; 5 | 6 | abstract Anything(Impl) { 7 | public inline function new() 8 | this = Impl.instance; 9 | } 10 | 11 | private class Impl implements deepequal.CustomCompare { 12 | public static var instance(default, null) = new Impl(); 13 | function new() {} 14 | public function check(other:Dynamic, compare) return Success(Noise); 15 | } -------------------------------------------------------------------------------- /src/deepequal/custom/ArrayContains.hx: -------------------------------------------------------------------------------- 1 | package deepequal.custom; 2 | 3 | import deepequal.Outcome; 4 | import deepequal.Noise; 5 | import deepequal.Helper.*; 6 | 7 | /** 8 | Checks if targets is an array and contains the required elements 9 | This is a simple and naive implementation with complexity O(n^2), 10 | don't use it with really huge arrays 11 | **/ 12 | class ArrayContains implements deepequal.CustomCompare { 13 | var items:Array; 14 | public function new(items) { 15 | this.items = items; 16 | } 17 | public function check(other:Dynamic, compare:Dynamic->Dynamic->Result) { 18 | if(!isOfType(other, Array)) return Failure({message: 'Expected array but got ${stringify(other)}', path: []}); 19 | for(i in items) { 20 | var matched = false; 21 | for(o in (other:Array)) switch compare(i, o) { 22 | case Success(_): matched = true; break; 23 | case Failure(_): 24 | } 25 | if(!matched) return Failure({message: 'Cannot find $i in ${stringify(other)}', path:[]}); 26 | } 27 | return Success(Noise); 28 | } 29 | @:keep 30 | public function toString() 31 | return 'ArrayContains($items)'; 32 | } 33 | -------------------------------------------------------------------------------- /src/deepequal/custom/ArrayOfLength.hx: -------------------------------------------------------------------------------- 1 | package deepequal.custom; 2 | 3 | import deepequal.Outcome; 4 | import deepequal.Noise; 5 | import deepequal.Helper.*; 6 | 7 | /** 8 | Checks if target is an array and of the required length 9 | **/ 10 | class ArrayOfLength implements deepequal.CustomCompare { 11 | var length:Int; 12 | public function new(length) { 13 | this.length = length; 14 | } 15 | public function check(other:Dynamic, compare:Dynamic->Dynamic->Result) { 16 | if(!isOfType(other, Array)) return Failure({message: 'Expected array but got ${stringify(other)}', path: []}); 17 | var len = (other:Array).length; 18 | return len == length ? Success(Noise) : Failure({message: 'Expected array of length $length but got $len', path: []}); 19 | } 20 | @:keep 21 | public function toString() 22 | return 'ArrayLength($length)'; 23 | } 24 | -------------------------------------------------------------------------------- /src/deepequal/custom/EnumByName.hx: -------------------------------------------------------------------------------- 1 | package deepequal.custom; 2 | 3 | import deepequal.Outcome; 4 | import deepequal.Noise; 5 | 6 | /** 7 | Checks if target is an enum specified by a string name 8 | **/ 9 | class EnumByName implements deepequal.CustomCompare { 10 | 11 | var enm:Enum; 12 | var name:Dynamic; 13 | var params:Dynamic; 14 | public function new(enm:Enum, name:Dynamic, ?params:Dynamic) { 15 | this.enm = enm; 16 | this.name = name; 17 | this.params = params; 18 | } 19 | public function check(other:Dynamic, compare:Dynamic->Dynamic->Result) { 20 | if(!Reflect.isEnumValue(other)) return return Failure({message: 'Expected $other to be an enum', path: []}); 21 | var oenm = Type.getEnum(other); 22 | if(enm != oenm) return Failure({message: 'Expected enum of ${Helper.getEnumName(enm)} but got $other', path: []}); 23 | switch compare(name, (other:EnumValue).getName()) { 24 | case Failure(f): return Failure({message: 'Expected enum named $name but got $other', path: []}); 25 | default: 26 | } 27 | if(params != null) switch compare(params, (other:EnumValue).getParameters()) { 28 | case Failure(f): return Failure({message: 'Unmatched enum parameters in $other', path: []}); 29 | default: 30 | } 31 | return Success(Noise); 32 | } 33 | @:keep 34 | public function toString() 35 | return 'EnumByName($enm, $name, $params)'; 36 | } 37 | -------------------------------------------------------------------------------- /src/deepequal/custom/ObjectContains.hx: -------------------------------------------------------------------------------- 1 | package deepequal.custom; 2 | 3 | import deepequal.Outcome; 4 | import deepequal.Noise; 5 | import deepequal.Path; 6 | import deepequal.Helper.*; 7 | 8 | /** 9 | Check if target include the required field and values 10 | **/ 11 | class ObjectContains implements deepequal.CustomCompare { 12 | var expected:{}; 13 | public function new(expected) { 14 | this.expected = expected; 15 | } 16 | public function check(other:Dynamic, compare:Dynamic->Dynamic->Result) { 17 | if(!Reflect.isObject(other)) return Failure({message: 'Expected object but got ${stringify(other)}', path: []}); 18 | var path = []; 19 | for(field in Reflect.fields(expected)) { 20 | if(!Reflect.hasField(other, field)) return Failure({message: 'Cannot find field $field', path: path}); 21 | var e = Reflect.field(expected, field); 22 | var a = Reflect.field(other, field); 23 | path.push(Field(field)); 24 | switch compare(e, a) { 25 | case Success(_): 26 | path.pop(); 27 | case Failure(f): 28 | path = path.concat(f.path); 29 | return Failure({message: f.message, path: path}); 30 | } 31 | } 32 | return Success(Noise); 33 | } 34 | @:keep 35 | public function toString() 36 | return 'ObjectContains($expected)'; 37 | } 38 | -------------------------------------------------------------------------------- /src/deepequal/custom/ObjectContainsKeys.hx: -------------------------------------------------------------------------------- 1 | package deepequal.custom; 2 | 3 | import deepequal.Outcome; 4 | import deepequal.Noise; 5 | import deepequal.Helper.*; 6 | 7 | /** 8 | Checks if target is an object (as defined by Reflect.isObject) and contains the required fields 9 | **/ 10 | class ObjectContainsKeys implements deepequal.CustomCompare { 11 | var keys:Array; 12 | public function new(keys) { 13 | this.keys = keys; 14 | } 15 | public function check(other:Dynamic, compare:Dynamic->Dynamic->Result) { 16 | if(!Reflect.isObject(other)) return Failure({message: 'Expected object but got ${stringify(other)}', path: []}); 17 | for(key in keys) if(!Reflect.hasField(other, key)) return Failure({message: 'Cannot find key $key', path: []}); 18 | return Success(Noise); 19 | } 20 | @:keep 21 | public function toString() 22 | return 'ObjectContainsKeys($keys)'; 23 | } 24 | -------------------------------------------------------------------------------- /src/deepequal/custom/Regex.hx: -------------------------------------------------------------------------------- 1 | package deepequal.custom; 2 | 3 | import deepequal.Outcome; 4 | import deepequal.Noise; 5 | import deepequal.Helper.*; 6 | 7 | /** 8 | Checks if target is a string and fulfills the required regex 9 | **/ 10 | class Regex implements deepequal.CustomCompare { 11 | var regex:EReg; 12 | public function new(regex) { 13 | this.regex = regex; 14 | } 15 | public function check(other:Dynamic, compare:Dynamic->Dynamic->Result) { 16 | if(!isOfType(other, String)) return Failure({message: 'Expected string but got $other', path: []}); 17 | if(!regex.match(other)) return Failure({message: 'Cannot match $other with the required regex: $regex', path: []}); 18 | return Success(Noise); 19 | } 20 | @:keep 21 | public function toString() 22 | return 'Regex($regex)'; 23 | } 24 | -------------------------------------------------------------------------------- /src/deepequal/custom/StringStartsWith.hx: -------------------------------------------------------------------------------- 1 | package deepequal.custom; 2 | 3 | import deepequal.Outcome; 4 | import deepequal.Noise; 5 | import deepequal.Helper.*; 6 | 7 | /** 8 | Checks if target is a string and starts with the required characters 9 | **/ 10 | class StringStartsWith implements deepequal.CustomCompare { 11 | var s:String; 12 | public function new(s) { 13 | this.s = s; 14 | } 15 | public function check(other:Dynamic, compare:Dynamic->Dynamic->Result) { 16 | if(!isOfType(other, String)) return Failure({message: 'Expected string but got $other', path: []}); 17 | if(!StringTools.startsWith(other, s)) return Failure({message: 'Expected string starting with $s but got $other', path: []}); 18 | return Success(Noise); 19 | } 20 | @:keep 21 | public function toString() 22 | return 'StringStartsWith($s)'; 23 | } 24 | -------------------------------------------------------------------------------- /submit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | zip -r temp.zip haxelib.json src README.md 4 | haxelib submit temp.zip 5 | rm temp.zip -------------------------------------------------------------------------------- /tests.hxml: -------------------------------------------------------------------------------- 1 | -cp tests 2 | -main RunTests 3 | -dce full 4 | -lib tink_unittest -------------------------------------------------------------------------------- /tests/FakeOutcome.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | enum FakeOutcome { 4 | Success(s:Int); 5 | } -------------------------------------------------------------------------------- /tests/RunTests.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | 3 | import tink.unit.*; 4 | import tink.testrunner.*; 5 | import haxe.Int64; 6 | import haxe.io.Bytes; 7 | import deepequal.custom.*; 8 | import deepequal.DeepEqual.compare; 9 | import deepequal.Outcome; 10 | import deepequal.Noise; 11 | import deepequal.Error; 12 | 13 | using StringTools; 14 | using RunTests; 15 | 16 | @:asserts 17 | class RunTests { 18 | 19 | static function main() { 20 | Runner.run(TestBatch.make([ 21 | new RunTests(), 22 | ])).handle(Runner.exit); 23 | } 24 | 25 | static function print(v:Dynamic) { 26 | trace('Value: $v'); 27 | } 28 | 29 | function new() {} 30 | 31 | public function nil() { 32 | var a = null; 33 | var e = null; 34 | asserts.assert(compare(e, a)); 35 | 36 | var a = null; 37 | var e = 1; 38 | asserts.assertFailure(compare(e, a), 'Expected 1 but got null @ v'); 39 | 40 | var a = null; 41 | var e = Success('foo'); 42 | asserts.assertFailure(compare(e, a), 'Expected Success(foo) but got null @ v'); 43 | 44 | var a = null; 45 | var e = new Foo(1); 46 | asserts.assertFailureRegex(compare(e, a), ~/Expected .* but got null @ v/); 47 | 48 | var a = null; 49 | var e = Foo; 50 | asserts.assertFailureRegex(compare(e, a), ~/Expected .* but got null @ v/); 51 | 52 | var a = 1; 53 | var e = null; 54 | asserts.assertFailure(compare(e, a), 'Expected null but got 1 @ v'); 55 | 56 | return asserts.done(); 57 | } 58 | 59 | #if !jvm // see: https://github.com/HaxeFoundation/haxe/issues/8286 60 | public function object() { 61 | var a = {a:1, b:2}; 62 | var e = {a:1, b:2}; 63 | asserts.assert(compare(e, a)); 64 | 65 | var a = {a:1, b:[2]}; 66 | var e = {a:1, b:[2]}; 67 | asserts.assert(compare(e, a)); 68 | 69 | var a = {a:1, b:2}; 70 | var e = {a:1, c:2}; 71 | asserts.assertFailure(compare(e, a), 'Expected 2 but got null @ v.c'); 72 | 73 | var a = {a:1, b:2}; 74 | var e = {a:1, b:3}; 75 | asserts.assertFailure(compare(e, a), 'Expected 3 but got 2 @ v.b'); 76 | 77 | var a = {a:1, b:2}; 78 | var e = {a:1, b:'2'}; 79 | asserts.assertFailure(compare(e, a), 'Expected "2" but got 2 @ v.b'); 80 | 81 | return asserts.done(); 82 | } 83 | 84 | public function arrayOfObjects() { 85 | var a = [{a:1, b:2}]; 86 | var e = [{a:1, b:2}]; 87 | asserts.assert(compare(e, a)); 88 | 89 | var a = [{a:1, b:2}]; 90 | var e = [{a:1, c:2}]; 91 | asserts.assertFailure(compare(e, a), 'Expected 2 but got null @ v[0].c'); 92 | 93 | var a = [{a:1, b:2}]; 94 | var e = [{a:1, b:3}]; 95 | asserts.assertFailure(compare(e, a), 'Expected 3 but got 2 @ v[0].b'); 96 | 97 | return asserts.done(); 98 | } 99 | #end 100 | 101 | public function array() { 102 | var a = [0.1]; 103 | var e = [0.1]; 104 | asserts.assert(compare(e, a)); 105 | 106 | var a = [0.1]; 107 | var e = [1.1]; 108 | asserts.assertFailure(compare(e, a), 'Expected 1.1 but got 0.1 @ v[0]'); 109 | 110 | var a = [0.1, 0.2]; 111 | var e = [0.1, 0.2, 0.3]; 112 | asserts.assertFailure(compare(e, a), 'Expected array of length 3 but got 2 @ v'); 113 | 114 | return asserts.done(); 115 | } 116 | 117 | public function float() { 118 | var a = 0.1; 119 | var e = 0.1; 120 | asserts.assert(compare(e, a)); 121 | 122 | var a = 0.1; 123 | var e = 1.1; 124 | asserts.assertFailure(compare(e, a), 'Expected 1.1 but got 0.1 @ v'); 125 | 126 | return asserts.done(); 127 | } 128 | 129 | public function int() { 130 | var a = 0; 131 | var e = 0; 132 | asserts.assert(compare(e, a)); 133 | 134 | var a = 0; 135 | var e = 1; 136 | asserts.assertFailure(compare(e, a), 'Expected 1 but got 0 @ v'); 137 | 138 | return asserts.done(); 139 | } 140 | 141 | public function string() { 142 | var a = 'actual'; 143 | var e = 'actual'; 144 | asserts.assert(compare(e, a)); 145 | 146 | var a = 'actual'; 147 | var e = 'expected'; 148 | asserts.assertFailure(compare(e, a), 'Expected "expected" but got "actual" @ v'); 149 | 150 | return asserts.done(); 151 | } 152 | 153 | public function date() { 154 | var a = new Date(2016, 1, 1, 1, 1, 1); 155 | var e = new Date(2016, 1, 1, 1, 1, 1); 156 | asserts.assert(compare(e, a)); 157 | 158 | var a = new Date(2016, 1, 1, 1, 1, 2); 159 | var e = new Date(2016, 1, 1, 1, 1, 1); 160 | asserts.assertFailure(compare(e, a), 'Expected 2016-02-01 01:01:01 but got 2016-02-01 01:01:02 @ v'); 161 | 162 | return asserts.done(); 163 | } 164 | 165 | public function int64() { 166 | var a = Int64.make(1, 2); 167 | var e = Int64.make(1, 2); 168 | asserts.assert(compare(e, a)); 169 | 170 | var a = Int64.make(1, 2); 171 | var e = Int64.make(1, 3); 172 | asserts.assertFailure(compare(e, a), 'Expected 4294967299 but got 4294967298 @ v'); 173 | 174 | return asserts.done(); 175 | } 176 | 177 | public function map() { 178 | var a = [1 => 'a', 2 => 'b']; 179 | var e = [1 => 'a', 2 => 'b']; 180 | asserts.assert(compare(e, a)); 181 | 182 | var a = [1 => 'a']; 183 | var e = [1 => 'a', 2 => 'b']; 184 | asserts.assertFailure(compare(e, a), 'Expected 2 field(s) but got 1 @ v'); 185 | 186 | var a = [1 => 'a', 3 => 'c']; 187 | var e = [1 => 'a', 2 => 'b']; 188 | asserts.assertFailure(compare(e, a), 'Map keys mismatch: Expected 2 but got 3 @ v[1]'); 189 | 190 | var a = [1 => 'a', 2 => 'b']; 191 | var e = ['1' => 'a', '2' => 'b']; 192 | asserts.assertFailure(compare(e, a), 'Map keys mismatch: Expected "1" but got 1 @ v[0]'); 193 | 194 | return asserts.done(); 195 | } 196 | 197 | public function func() { 198 | var a = main; 199 | var e = main; 200 | asserts.assert(compare(e, a)); 201 | 202 | var a = func; 203 | var e = func; 204 | asserts.assert(compare(e, a)); 205 | 206 | var a = function() {}; 207 | var e = function() {}; 208 | asserts.assertFailure(compare(e, a), 'The two functions are not equal @ v'); 209 | 210 | return asserts.done(); 211 | } 212 | 213 | public function enm() { 214 | var a = Success('foo'); 215 | var e = Success('foo'); 216 | asserts.assert(compare(e, a)); 217 | 218 | var a = Success('foo'); 219 | var e = Success('f'); 220 | asserts.assertFailure(compare(e, a), 'Expected "f" but got "foo" @ v(enumParam:0)'); 221 | 222 | var a = Success('foo'); 223 | var e = Failure('foo'); 224 | asserts.assertFailure(compare(e, a), 'Expected enum constructor Failure but got Success @ v'); 225 | 226 | var a = FakeOutcome.Success(1); 227 | var e = Outcome.Success(1); 228 | asserts.assertFailure(compare(e, a), 'Expected enum tink.core.Outcome but got FakeOutcome @ v'); 229 | 230 | return asserts.done(); 231 | } 232 | 233 | public function cls() { 234 | var a = new Foo(1); 235 | var e = new Foo(1); 236 | asserts.assert(compare(e, a)); 237 | 238 | var a = new Foo({a: 1}); 239 | var e = new Foo({a: 1}); 240 | asserts.assert(compare(e, a)); 241 | 242 | var a = new Foo(([1, 'a']:Array)); 243 | var e = new Foo(([1, 'a']:Array)); 244 | asserts.assert(compare(e, a)); 245 | 246 | var a = new Foo(1); 247 | var e = new Foo(2); 248 | asserts.assertFailure(compare(e, a), 'Expected 2 but got 1 @ v.value'); 249 | 250 | var a = new Foo(1); 251 | var e = new Bar(1); 252 | asserts.assertFailure(compare(e, a), 'Expected class instance of Bar but got Foo @ v'); 253 | 254 | var a = new Foo(1); 255 | var e = new Bar(2); 256 | asserts.assertFailure(compare(e, a), 'Expected class instance of Bar but got Foo @ v'); 257 | 258 | return asserts.done(); 259 | } 260 | 261 | public function bytes() { 262 | 263 | var a = Bytes.alloc(10); 264 | var e = Bytes.alloc(10); 265 | asserts.assert(compare(e, a)); 266 | 267 | var a = Bytes.ofString('abc'); 268 | var e = Bytes.ofString('abc'); 269 | asserts.assert(compare(e, a)); 270 | 271 | var a = Bytes.alloc(10); 272 | var e = Bytes.alloc(20); 273 | asserts.assertFailure(compare(e, a), 'Expected bytes of length 20 but got 10 @ v'); 274 | 275 | var a = Bytes.ofString('xyz'); 276 | var e = Bytes.ofString('abc'); 277 | asserts.assertFailure(compare(e, a), 'Expected bytes(hex):616263 but got bytes(hex):78797a @ v'); 278 | 279 | return asserts.done(); 280 | } 281 | 282 | public function classObj() { 283 | var a = Foo; 284 | var e = Foo; 285 | asserts.assert(compare(e, a)); 286 | 287 | var a = Foo; 288 | var e = Bar; 289 | asserts.assertFailureRegex(compare(e, a), ~/^Expected Bar but got Foo @ v$/); 290 | 291 | return asserts.done(); 292 | } 293 | 294 | public function enumObj() { 295 | // var a = Outcome; 296 | // var e = Outcome; 297 | // asserts.assert(compare(e, a)); 298 | 299 | var a = Outcome; 300 | var e = haxe.ds.Option; 301 | asserts.assertFailureRegex(compare(e, a), ~/^Expected haxe.ds.Option but got tink.core.Outcome @ v$/); 302 | 303 | return asserts.done(); 304 | } 305 | 306 | public function arrayContains() { 307 | var a = [1,2,3,4]; 308 | var e = new ArrayContains([1,2,3]); 309 | asserts.assert(compare(e, a)); 310 | 311 | var a = [1,2,3,4]; 312 | var e = new ArrayContains([3,5]); 313 | asserts.assertFailure(compare(e, a), 'Cannot find 5 in [1,2,3,4] @ v'); 314 | 315 | return asserts.done(); 316 | } 317 | 318 | public function arrayOfLength() { 319 | var a = [1,2,3]; 320 | var e = new ArrayOfLength(3); 321 | asserts.assert(compare(e, a)); 322 | 323 | var a = [1,2,3,4]; 324 | var e = new ArrayOfLength(3); 325 | asserts.assertFailure(compare(e, a), 'Expected array of length 3 but got 4 @ v'); 326 | 327 | return asserts.done(); 328 | } 329 | 330 | public function objectContains() { 331 | var a = {a: 1, b: '2'} 332 | var e = new ObjectContains({a: 1}); 333 | asserts.assert(compare(e, a)); 334 | 335 | var a = {a: 1, b: '2'} 336 | var e = new ObjectContains({b: 2}); 337 | asserts.assertFailure(compare(e, a), 'Expected 2 but got "2" @ v.b'); 338 | 339 | var a = {a: 1, b: '2'} 340 | var e = new ObjectContains({b: '2'}); 341 | asserts.assert(compare(e, a)); 342 | 343 | var a = {a: 1, c: '2'} 344 | var e = new ObjectContains({a:1, b:2}); 345 | asserts.assertFailure(compare(e, a), 'Cannot find field b @ v'); 346 | 347 | return asserts.done(); 348 | } 349 | 350 | public function objectContainsKeys() { 351 | var a = {a: 1, b: '2'} 352 | var e = new ObjectContainsKeys(['a', 'b']); 353 | asserts.assert(compare(e, a)); 354 | 355 | var a = {a: 1, c: '2'} 356 | var e = new ObjectContainsKeys(['a', 'b']); 357 | asserts.assertFailure(compare(e, a), 'Cannot find key b @ v'); 358 | 359 | return asserts.done(); 360 | 361 | } 362 | 363 | public function anything() { 364 | var a = null; 365 | var e = new Anything(); 366 | asserts.assert(compare(e, a)); 367 | 368 | var a = [1,2,3,4]; 369 | var e = new Anything(); 370 | asserts.assert(compare(e, a)); 371 | 372 | var a = [1,2,3,4]; 373 | var e = [for(i in 0...4) new Anything()]; // essentially same as ArrayLength 374 | asserts.assert(compare(e, a)); 375 | 376 | var a = [1,2,3]; 377 | var e = [for(i in 0...4) new Anything()]; // essentially same as ArrayLength 378 | asserts.assertFailure(compare(e, a), 'Expected array of length 4 but got 3 @ v'); 379 | 380 | return asserts.done(); 381 | } 382 | 383 | public function enumByName() { 384 | var a = Success(1); 385 | var e = new EnumByName(Outcome, 'Success'); 386 | asserts.assert(compare(e, a)); 387 | 388 | var a = Success(1); 389 | var e = new EnumByName(Outcome, 'Success', [1]); 390 | asserts.assert(compare(e, a)); 391 | 392 | var a = Success(1); 393 | var e = new EnumByName(Outcome, 'Success', [2]); 394 | asserts.assertFailure(compare(e, a), 'Unmatched enum parameters in Success(1) @ v'); 395 | 396 | var a = Success(1); 397 | var e = new EnumByName(FakeOutcome, 'Success'); 398 | asserts.assertFailure(compare(e, a), 'Expected enum of FakeOutcome but got Success(1) @ v'); 399 | 400 | var a = Success(1); 401 | var e = new EnumByName(Outcome, 'Failure'); 402 | asserts.assertFailure(compare(e, a), 'Expected enum named Failure but got Success(1) @ v'); 403 | 404 | var a = 'Success'; 405 | var e = new EnumByName(Outcome, 'Failure'); 406 | asserts.assertFailure(compare(e, a), 'Expected Success to be an enum @ v'); 407 | 408 | return asserts.done(); 409 | } 410 | 411 | public function regex() { 412 | var a = 'Success'; 413 | var e = new Regex(~/Suc/); 414 | asserts.assert(compare(e, a)); 415 | 416 | var a = 'Success'; 417 | var e = new Regex(~/suc/); 418 | asserts.assertFailureRegex(compare(e, a), ~/^Cannot match Success with the required regex:.*/); 419 | 420 | var a = 'Success'; 421 | var e = new Regex(~/suc/i); 422 | asserts.assert(compare(e, a)); 423 | 424 | var a = 'Success'; 425 | var e = new Regex(~/Suc.*/); 426 | asserts.assert(compare(e, a)); 427 | 428 | return asserts.done(); 429 | } 430 | 431 | public function stringStartsWith() { 432 | var a = 'Success'; 433 | var e = new StringStartsWith('Succ'); 434 | asserts.assert(compare(e, a)); 435 | 436 | var a = 'Success'; 437 | var e = new StringStartsWith('succ'); 438 | asserts.assertFailure(compare(e, a), 'Expected string starting with succ but got Success @ v'); 439 | 440 | var a = 'Success'; 441 | var e = new StringStartsWith('Success'); 442 | asserts.assert(compare(e, a)); 443 | 444 | var a = 'Success'; 445 | var e = new StringStartsWith('Failure'); 446 | asserts.assertFailure(compare(e, a), 'Expected string starting with Failure but got Success @ v'); 447 | 448 | return asserts.done(); 449 | } 450 | 451 | public function nonClass() { 452 | 453 | var a = {}; 454 | var e = new Foo(1); 455 | asserts.assert(compare(e, a).match(Failure(_))); 456 | 457 | var a = 1; 458 | var e = new Foo(1); 459 | asserts.assert(compare(e, a).match(Failure(_))); 460 | 461 | var a = 'foo'; 462 | var e = new Foo(1); 463 | asserts.assert(compare(e, a).match(Failure(_))); 464 | 465 | var a = true; 466 | var e = new Foo(1); 467 | asserts.assert(compare(e, a).match(Failure(_))); 468 | 469 | return asserts.done(); 470 | } 471 | 472 | public function nonEnum() { 473 | var a = {}; 474 | var e = Success(1); 475 | asserts.assert(compare(e, a).match(Failure(_))); 476 | 477 | var a = 1; 478 | var e = Success(1); 479 | asserts.assert(compare(e, a).match(Failure(_))); 480 | 481 | var a = 'foo'; 482 | var e = Success(1); 483 | asserts.assert(compare(e, a).match(Failure(_))); 484 | 485 | var a = true; 486 | var e = Success(1); 487 | asserts.assert(compare(e, a).match(Failure(_))); 488 | 489 | return asserts.done(); 490 | } 491 | 492 | static function assertFailure(asserts:AssertionBuffer, outcome:Outcome, message:String, ?pos:haxe.PosInfos) { 493 | switch outcome { 494 | case Failure(e): asserts.assert(e.message == message, pos); 495 | case Success(e): asserts.fail('Expected a Failure', pos); 496 | } 497 | } 498 | 499 | static function assertFailureRegex(asserts:AssertionBuffer, outcome:Outcome, regex:EReg, ?pos:haxe.PosInfos) { 500 | switch outcome { 501 | case Failure(e): asserts.assert(regex.match(e.message.replace('\n', '')), pos); 502 | case Success(_): asserts.fail('Expected a Failure', pos); 503 | } 504 | } 505 | } 506 | 507 | class Foo { 508 | var value:T; 509 | public function new(v:T) { 510 | value = v; 511 | } 512 | function foo() {} 513 | } 514 | class Bar { 515 | var value:T; 516 | public function new(v:T) { 517 | value = v; 518 | } 519 | function bar() {} 520 | } 521 | 522 | --------------------------------------------------------------------------------