221 lines
10 KiB
Plaintext
221 lines
10 KiB
Plaintext
/*
|
||
* wota_test.cm – self‑contained test‑suite for the Wota encode/decode module
|
||
* *** rewritten to run in an environment that ONLY supports
|
||
* Blobs; TypedArrays / ArrayBuffers / DataView are GONE. ***
|
||
*
|
||
* Exit status 0 → all tests passed, non‑zero otherwise.
|
||
*/
|
||
|
||
'use strict'
|
||
|
||
var wota = use('wota')
|
||
var os = use('os')
|
||
var Blob = use('blob')
|
||
|
||
/*──────────────────────────────────────────────────────────────────────────*/
|
||
/* Helper utilities */
|
||
/*──────────────────────────────────────────────────────────────────────────*/
|
||
|
||
def EPSILON = 1e-12
|
||
|
||
function stone_if_needed(b) { if (!stone.p(b)) stone(b) }
|
||
|
||
/* Convert an array of octets to a stone Blob */
|
||
function bytes_to_blob(bytes) {
|
||
var b = new Blob()
|
||
for (var i = 0; i < bytes.length; i++) {
|
||
var byte = bytes[i]
|
||
for (var bit = 7; bit >= 0; bit--) b.write_bit((byte >> bit) & 1)
|
||
}
|
||
stone(b)
|
||
return b
|
||
}
|
||
|
||
/* Parse hex → Blob */
|
||
function hex_to_blob(hex) {
|
||
if (hex.length % 2) hex = '0' + hex // odd nibble safety
|
||
var bytes = []
|
||
for (var i = 0; i < hex.length; i += 2)
|
||
bytes.push(parseInt(hex.substr(i, 2), 16))
|
||
return bytes_to_blob(bytes)
|
||
}
|
||
|
||
/* Blob → lower‑case hex */
|
||
function blob_to_hex(blob) {
|
||
stone_if_needed(blob)
|
||
var bytes = []
|
||
for (var i = 0; i < blob.length; i += 8) {
|
||
var byte = 0
|
||
for (var bit = 0; bit < 8; bit++) byte = (byte << 1) | (blob.read_logical(i + bit) ? 1 : 0)
|
||
bytes.push(byte)
|
||
}
|
||
return bytes.map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase()
|
||
}
|
||
|
||
function is_blob(x) { return x && typeof x == 'object' && typeof x.length == 'number' && typeof x.read_logical == 'function' }
|
||
|
||
/* Deep comparison capable of Blobs + tolerance for floating diff */
|
||
function deep_compare(expected, actual, path = '') {
|
||
if (expected == actual) return { passed: true, messages: [] }
|
||
|
||
if (typeof expected == 'number' && typeof actual == 'number') {
|
||
if (isNaN(expected) && isNaN(actual)) return { passed: true, messages: [] }
|
||
var diff = Math.abs(expected - actual)
|
||
if (diff <= EPSILON) return { passed: true, messages: [] }
|
||
return { passed: false, messages: [`Value mismatch at ${path}: ${expected} vs ${actual} (diff ${diff})`] }
|
||
}
|
||
|
||
if (is_blob(expected) && is_blob(actual)) {
|
||
stone_if_needed(expected); stone_if_needed(actual)
|
||
if (expected.length != actual.length)
|
||
return { passed: false, messages: [`Blob length mismatch at ${path}: ${expected.length} vs ${actual.length}`] }
|
||
for (var i = 0; i < expected.length; i++) {
|
||
if (expected.read_logical(i) != actual.read_logical(i))
|
||
return { passed: false, messages: [`Blob bit mismatch at ${path}[${i}]`] }
|
||
}
|
||
return { passed: true, messages: [] }
|
||
}
|
||
|
||
if (Array.isArray(expected) && Array.isArray(actual)) {
|
||
if (expected.length != actual.length)
|
||
return { passed: false, messages: [`Array length mismatch at ${path}: ${expected.length} vs ${actual.length}`] }
|
||
var msgs = []
|
||
for (var i = 0; i < expected.length; i++) {
|
||
var res = deep_compare(expected[i], actual[i], `${path}[${i}]`)
|
||
if (!res.passed) msgs.push(...res.messages)
|
||
}
|
||
return { passed: msgs.length == 0, messages: msgs }
|
||
}
|
||
|
||
if (typeof expected == 'object' && expected && typeof actual == 'object' && actual) {
|
||
var expKeys = Object.keys(expected).sort()
|
||
var actKeys = Object.keys(actual).sort()
|
||
if (JSON.stringify(expKeys) != JSON.stringify(actKeys))
|
||
return { passed: false, messages: [`Object keys mismatch at ${path}: ${expKeys} vs ${actKeys}`] }
|
||
var msgs = []
|
||
for (var k of expKeys) {
|
||
var res = deep_compare(expected[k], actual[k], `${path}.${k}`)
|
||
if (!res.passed) msgs.push(...res.messages)
|
||
}
|
||
return { passed: msgs.length == 0, messages: msgs }
|
||
}
|
||
|
||
return { passed: false, messages: [`Value mismatch at ${path}: ${JSON.stringify(expected)} vs ${JSON.stringify(actual)}`] }
|
||
}
|
||
|
||
/*──────────────────────────────────────────────────────────────────────────*/
|
||
/* Test matrix */
|
||
/*──────────────────────────────────────────────────────────────────────────*/
|
||
|
||
var testarr = []
|
||
var hex = 'a374'
|
||
for (var i = 0; i < 500; i++) { testarr.push(1); hex += '61' }
|
||
|
||
function bb() { return bytes_to_blob.apply(null, arguments) } // shorthand
|
||
|
||
var testCases = [
|
||
{ input: 0, expectedHex: '60' },
|
||
{ input: 2023, expectedHex: 'e08f67' },
|
||
{ input: -1, expectedHex: '69' },
|
||
{ input: 7, expectedHex: '67' },
|
||
{ input: -7, expectedHex: '6f' },
|
||
{ input: 1023, expectedHex: 'e07f' },
|
||
{ input: -1023, expectedHex: 'ef7f' },
|
||
{ input: Math.pow(2, 55) - 1, expectedHex: 'e0ffffffffffffff' },
|
||
{ input: -Math.pow(2, 55), expectedHex: 'e000000000000000' },
|
||
|
||
{ input: undefined, expectedHex: '70' },
|
||
{ input: false, expectedHex: '72' },
|
||
{ input: true, expectedHex: '73' },
|
||
|
||
{ input: -1.01, expectedHex: '5a65' },
|
||
{ input: 98.6, expectedHex: '51875a' },
|
||
{ input: -0.5772156649, expectedHex: 'd80a95c0b0bd69' },
|
||
{ input: -1.00000000000001, expectedHex: 'd80e96deb183e98001' },
|
||
{ input: -10000000000000, expectedHex: 'c80d01' },
|
||
{ input: Math.pow(2, 55), expectedHex: 'd80e01' },
|
||
|
||
{ input: '', expectedHex: '10' },
|
||
{ input: 'cat', expectedHex: '13636174' },
|
||
{ input: 'U+1F4A9 「うんち絵文字」 «💩»', expectedHex: '9014552b314634413920e00ce046e113e06181fa7581cb0781b657e00d20812b87e929813b' },
|
||
|
||
{ input: bytes_to_blob([0xff, 0xaa]), expectedHex: '8010ffaa' },
|
||
{ input: bytes_to_blob([0xf0, 0xe3, 0x20, 0x80]), expectedHex: '8019f0e32080' },
|
||
|
||
{ input: testarr, expectedHex: hex },
|
||
|
||
{ input: [], expectedHex: '20' },
|
||
{ input: [1, 2, 3], expectedHex: '23616263' },
|
||
{ input: [-1, 0, 1.5], expectedHex: '2369605043' },
|
||
|
||
{ input: {}, expectedHex: '30' },
|
||
{ input: { a: 1, b: 2 }, expectedHex: '32116161116262' },
|
||
|
||
{ input: { num: 42, arr: [1, -1, 2.5], str: 'test', obj: { x: true } }, expectedHex: '34216e756d622a2173747214746573742161727223616965235840216f626a21117873' },
|
||
|
||
{ input: new Blob(), expectedHex: '00' },
|
||
{ input: [[]], expectedHex: '2120' },
|
||
{ input: { '': '' }, expectedHex: '311010' },
|
||
{ input: 1e-10, expectedHex: 'd00a01' },
|
||
|
||
{ input: { a: 1, b: 2 }, replacer: (k, v) => typeof v == 'number' ? v * 2 : v, expected: { a: 2, b: 4 }, testType: 'replacer' },
|
||
{ input: { x: 'test', y: 5 }, replacer: (k, v) => k == 'x' ? v + '!' : v, expected: { x: 'test!', y: 5 }, testType: 'replacer' },
|
||
|
||
{ input: { a: 1, b: 2 }, reviver: (k, v) => typeof v == 'number' ? v * 3 : v, expected: { a: 3, b: 6 }, testType: 'reviver' },
|
||
{ input: { x: 'test', y: 10 }, reviver: (k, v) => k == 'y' ? v + 1 : v, expected: { x: 'test', y: 11 }, testType: 'reviver' }
|
||
]
|
||
|
||
/*──────────────────────────────────────────────────────────────────────────*/
|
||
/* Execution */
|
||
/*──────────────────────────────────────────────────────────────────────────*/
|
||
|
||
var results = []
|
||
var testCount = 0
|
||
|
||
for (var t of testCases) {
|
||
testCount++
|
||
var name = `Test ${testCount}: ${JSON.stringify(t.input)}${t.testType ? ' (' + t.testType + ')' : ''}`
|
||
var passed = true
|
||
var msgs = []
|
||
|
||
try {
|
||
var enc = t.replacer ? wota.encode(t.input, t.replacer) : wota.encode(t.input)
|
||
if (!is_blob(enc)) { passed = false; msgs.push('encode() should return a Blob') }
|
||
else {
|
||
if (t.expectedHex) {
|
||
var gotHex = blob_to_hex(enc)
|
||
if (gotHex != t.expectedHex.toLowerCase())
|
||
msgs.push(`Hex encoding differs (info): exp ${t.expectedHex}, got ${gotHex}`)
|
||
}
|
||
|
||
var dec = t.reviver ? wota.decode(enc, t.reviver) : wota.decode(enc)
|
||
var exp = t.expected != undefined ? t.expected : t.input
|
||
|
||
var cmp = deep_compare(exp, dec)
|
||
if (!cmp.passed) { passed = false; msgs.push(...cmp.messages) }
|
||
}
|
||
} catch (e) { passed = false; msgs.push('Exception: ' + e) }
|
||
|
||
results.push({ name, passed, msgs })
|
||
if (!passed) {
|
||
log.console('\nFailure detail for ' + name + '\n' + msgs.join('\n') + '\n')
|
||
}
|
||
}
|
||
|
||
/*──────────────────────────────────────────────────────────────────────────*/
|
||
/* Summary */
|
||
/*──────────────────────────────────────────────────────────────────────────*/
|
||
|
||
log.console('\nTest Summary:')
|
||
var passCount = 0
|
||
for (var r of results) {
|
||
log.console(`${r.name} – ${r.passed ? 'Passed' : 'Failed'}`)
|
||
if (r.msgs.length && r.passed) log.console(' ' + r.msgs.join('\n '))
|
||
if (r.passed) passCount++
|
||
}
|
||
|
||
log.console(`\nResult: ${passCount}/${testCount} tests passed`)
|
||
if (passCount == testCount) { log.console('Overall: PASSED'); os.exit(0) }
|
||
log.console('Overall: FAILED')
|
||
|
||
$_.stop() |