|
| 1 | +local module = {} |
| 2 | + |
| 3 | +--- |
| 4 | +-- Matcher verifying that distance between given value and expected is not greater than maxDistance. |
| 5 | +-- @function elementsAreArray |
| 6 | +-- @param expected#vector. |
| 7 | +-- @usage |
| 8 | +-- expectThat(util.vector2(0, 0), closeToVector(util.vector2(0, 1), 1)) |
| 9 | +function module.closeToVector(expected, maxDistance) |
| 10 | + return function(actual) |
| 11 | + local distance = (expected - actual):length() |
| 12 | + if distance <= maxDistance then |
| 13 | + return '' |
| 14 | + end |
| 15 | + return string.format('%s is too far from expected %s: %s > %s', actual, expected, distance, maxDistance) |
| 16 | + end |
| 17 | +end |
| 18 | + |
| 19 | +--- |
| 20 | +-- Matcher verifying that given value is an array each element of which matches elements of expected. |
| 21 | +-- @function elementsAreArray |
| 22 | +-- @param expected#array of values or matcher functions. |
| 23 | +-- @usage |
| 24 | +-- local t = {42, 13} |
| 25 | +-- local matcher = function(actual) |
| 26 | +-- if actual ~= 42 then |
| 27 | +-- return string.format('%s is not 42', actual) |
| 28 | +-- end |
| 29 | +-- return '' |
| 30 | +-- end |
| 31 | +-- expectThat({42, 13}, elementsAreArray({matcher, 13})) |
| 32 | +function module.elementsAreArray(expected) |
| 33 | + local expected_matchers = {} |
| 34 | + for i, v in ipairs(expected) do |
| 35 | + if type(v) == 'function' then |
| 36 | + expected_matchers[i] = v |
| 37 | + else |
| 38 | + expected_matchers[i] = function (other) |
| 39 | + if expected[i].__eq(expected[i], other) then |
| 40 | + return '' |
| 41 | + end |
| 42 | + return string.format('%s element %s does no match expected: %s', i, other, expected[i]) |
| 43 | + end |
| 44 | + end |
| 45 | + end |
| 46 | + return function(actual) |
| 47 | + if #actual < #expected_matchers then |
| 48 | + return string.format('number of elements is less than expected: %s < %s', #actual, #expected_matchers) |
| 49 | + end |
| 50 | + local message = '' |
| 51 | + for i, v in ipairs(actual) do |
| 52 | + if i > #expected_matchers then |
| 53 | + message = string.format('%s\n%s element is out of expected range: %s', message, i, #expected_matchers) |
| 54 | + break |
| 55 | + end |
| 56 | + local match_message = expected_matchers[i](v) |
| 57 | + if match_message ~= '' then |
| 58 | + message = string.format('%s\n%s', message, match_message) |
| 59 | + end |
| 60 | + end |
| 61 | + return message |
| 62 | + end |
| 63 | +end |
| 64 | + |
| 65 | +--- |
| 66 | +-- Matcher verifying that given number is not a nan. |
| 67 | +-- @function isNotNan |
| 68 | +-- @usage |
| 69 | +-- expectThat(value, isNotNan()) |
| 70 | +function module.isNotNan() |
| 71 | + return function(actual) |
| 72 | + if actual ~= actual then |
| 73 | + return 'actual value is nan, expected to be not nan' |
| 74 | + end |
| 75 | + return '' |
| 76 | + end |
| 77 | +end |
| 78 | + |
| 79 | +--- |
| 80 | +-- Matcher accepting any value. |
| 81 | +-- @function isAny |
| 82 | +-- @usage |
| 83 | +-- expectThat(value, isAny()) |
| 84 | +function module.isAny() |
| 85 | + return function(actual) |
| 86 | + return '' |
| 87 | + end |
| 88 | +end |
| 89 | + |
| 90 | +local function serializeArray(a) |
| 91 | + local result = nil |
| 92 | + for _, v in ipairs(a) do |
| 93 | + if result == nil then |
| 94 | + result = string.format('{%s', serialize(v)) |
| 95 | + else |
| 96 | + result = string.format('%s, %s', result, serialize(v)) |
| 97 | + end |
| 98 | + end |
| 99 | + if result == nil then |
| 100 | + return '{}' |
| 101 | + end |
| 102 | + return string.format('%s}', result) |
| 103 | +end |
| 104 | + |
| 105 | +local function serializeTable(t) |
| 106 | + local result = nil |
| 107 | + for k, v in pairs(t) do |
| 108 | + if result == nil then |
| 109 | + result = string.format('{%q = %s', k, serialize(v)) |
| 110 | + else |
| 111 | + result = string.format('%s, %q = %s', result, k, serialize(v)) |
| 112 | + end |
| 113 | + end |
| 114 | + if result == nil then |
| 115 | + return '{}' |
| 116 | + end |
| 117 | + return string.format('%s}', result) |
| 118 | +end |
| 119 | + |
| 120 | +local function isArray(t) |
| 121 | + local i = 1 |
| 122 | + for _ in pairs(t) do |
| 123 | + if t[i] == nil then |
| 124 | + return false |
| 125 | + end |
| 126 | + i = i + 1 |
| 127 | + end |
| 128 | + return true |
| 129 | +end |
| 130 | + |
| 131 | +function serialize(v) |
| 132 | + local t = type(v) |
| 133 | + if t == 'string' then |
| 134 | + return string.format('%q', v) |
| 135 | + elseif t == 'table' then |
| 136 | + if isArray(v) then |
| 137 | + return serializeArray(v) |
| 138 | + end |
| 139 | + return serializeTable(v) |
| 140 | + end |
| 141 | + return string.format('%s', v) |
| 142 | +end |
| 143 | + |
| 144 | +local function compareScalars(v1, v2) |
| 145 | + if v1 == v2 then |
| 146 | + return '' |
| 147 | + end |
| 148 | + if type(v1) == 'string' then |
| 149 | + return string.format('%q ~= %q', v1, v2) |
| 150 | + end |
| 151 | + return string.format('%s ~= %s', v1, v2) |
| 152 | +end |
| 153 | + |
| 154 | +local function collectKeys(t) |
| 155 | + local result = {} |
| 156 | + for key in pairs(t) do |
| 157 | + table.insert(result, key) |
| 158 | + end |
| 159 | + table.sort(result) |
| 160 | + return result |
| 161 | +end |
| 162 | + |
| 163 | +local function compareTables(t1, t2) |
| 164 | + local keys1 = collectKeys(t1) |
| 165 | + local keys2 = collectKeys(t2) |
| 166 | + if #keys1 ~= #keys2 then |
| 167 | + return string.format('table size mismatch: %d ~= %d', #keys1, #keys2) |
| 168 | + end |
| 169 | + for i = 1, #keys1 do |
| 170 | + local key1 = keys1[i] |
| 171 | + local key2 = keys2[i] |
| 172 | + if key1 ~= key2 then |
| 173 | + return string.format('table keys mismatch: %q ~= %q', key1, key2) |
| 174 | + end |
| 175 | + local d = compare(t1[key1], t2[key2]) |
| 176 | + if d ~= '' then |
| 177 | + return string.format('table values mismatch at key %s: %s', serialize(key1), d) |
| 178 | + end |
| 179 | + end |
| 180 | + return '' |
| 181 | +end |
| 182 | + |
| 183 | +function compare(v1, v2) |
| 184 | + local type1 = type(v1) |
| 185 | + local type2 = type(v2) |
| 186 | + if type2 == 'function' then |
| 187 | + return v2(v1) |
| 188 | + end |
| 189 | + if type1 ~= type2 then |
| 190 | + return string.format('types mismatch: %s ~= %s', type1, type2) |
| 191 | + end |
| 192 | + if type1 == 'nil' then |
| 193 | + return '' |
| 194 | + elseif type1 == 'table' then |
| 195 | + return compareTables(v1, v2) |
| 196 | + elseif type1 == 'nil' or type1 == 'boolean' or type1 == 'number' or type1 == 'string' then |
| 197 | + return compareScalars(v1, v2) |
| 198 | + end |
| 199 | + error('unsupported type: %s', type1) |
| 200 | +end |
| 201 | + |
| 202 | +--- |
| 203 | +-- Matcher verifying that given value is equal to expected. Accepts nil, boolean, number, string and table or matcher |
| 204 | +-- function. |
| 205 | +-- @function equalTo |
| 206 | +-- @usage |
| 207 | +-- expectThat({a = {42, 'foo', {b = true}}}, equalTo({a = {42, 'foo', {b = true}}})) |
| 208 | +function module.equalTo(expected) |
| 209 | + return function(actual) |
| 210 | + local diff = compare(actual, expected) |
| 211 | + if diff == '' then |
| 212 | + return '' |
| 213 | + end |
| 214 | + return string.format('%s; actual: %s; expected: %s', diff, serialize(actual, ''), serialize(expected, '')) |
| 215 | + end |
| 216 | +end |
| 217 | + |
| 218 | +return module |
0 commit comments