add nota replacer and reviver options; don't serialize functions
This commit is contained in:
@@ -5,329 +5,345 @@
|
||||
#include "nota.h"
|
||||
|
||||
typedef struct NotaEncodeContext {
|
||||
JSContext *ctx;
|
||||
JSValue visitedStack;
|
||||
NotaBuffer nb; // use the dynamic NotaBuffer
|
||||
int cycle;
|
||||
JSContext *ctx;
|
||||
JSValue visitedStack;
|
||||
NotaBuffer nb;
|
||||
int cycle;
|
||||
JSValue replacer;
|
||||
} NotaEncodeContext;
|
||||
|
||||
static void nota_stack_push(NotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
JS_SetPropertyInt64(ctx, enc->visitedStack, len, JS_DupValue(ctx, val));
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
JS_SetPropertyInt64(ctx, enc->visitedStack, len, JS_DupValue(ctx, val));
|
||||
}
|
||||
|
||||
static void nota_stack_pop(NotaEncodeContext *enc)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
JS_SetPropertyStr(ctx, enc->visitedStack, "length", JS_NewUint32(ctx, len - 1));
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
JS_SetPropertyStr(ctx, enc->visitedStack, "length", JS_NewUint32(ctx, len - 1));
|
||||
}
|
||||
|
||||
static int nota_stack_has(NotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
for (int i = 0; i < len; i++) {
|
||||
JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i);
|
||||
if (JS_IsObject(elem) && JS_IsObject(val)) {
|
||||
if (JS_VALUE_GET_OBJ(elem) == JS_VALUE_GET_OBJ(val)) {
|
||||
JS_FreeValue(ctx, elem);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
for (int i = 0; i < len; i++) {
|
||||
JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i);
|
||||
if (JS_IsObject(elem) && JS_IsObject(val)) {
|
||||
if (JS_VALUE_GET_OBJ(elem) == JS_VALUE_GET_OBJ(val)) {
|
||||
JS_FreeValue(ctx, elem);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
JS_FreeValue(ctx, elem);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static JSValue apply_replacer(NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
|
||||
if (JS_IsUndefined(enc->replacer)) return JS_DupValue(enc->ctx, val);
|
||||
|
||||
JSValue args[2] = { JS_DupValue(enc->ctx, key), JS_DupValue(enc->ctx, val) };
|
||||
JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args);
|
||||
JS_FreeValue(enc->ctx, args[0]);
|
||||
JS_FreeValue(enc->ctx, args[1]);
|
||||
|
||||
if (JS_IsException(result)) return JS_DupValue(enc->ctx, val);
|
||||
return result;
|
||||
}
|
||||
|
||||
JSValue number;
|
||||
|
||||
char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota)
|
||||
{
|
||||
int type = nota_type(nota);
|
||||
JSValue ret2;
|
||||
long long n;
|
||||
double d;
|
||||
int b;
|
||||
char *str;
|
||||
uint8_t *blob;
|
||||
char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) {
|
||||
int type = nota_type(nota);
|
||||
JSValue ret2;
|
||||
long long n;
|
||||
double d;
|
||||
int b;
|
||||
char *str;
|
||||
uint8_t *blob;
|
||||
|
||||
switch(type) {
|
||||
case NOTA_BLOB:
|
||||
nota = nota_read_blob(&n, (char**)&blob, nota);
|
||||
*tmp = JS_NewArrayBufferCopy(js, blob, n);
|
||||
free(blob);
|
||||
break;
|
||||
case NOTA_TEXT:
|
||||
switch(type) {
|
||||
case NOTA_BLOB:
|
||||
nota = nota_read_blob(&n, (char**)&blob, nota);
|
||||
*tmp = JS_NewArrayBufferCopy(js, blob, n);
|
||||
free(blob);
|
||||
break;
|
||||
case NOTA_TEXT:
|
||||
nota = nota_read_text(&str, nota);
|
||||
*tmp = JS_NewString(js, str);
|
||||
free(str);
|
||||
break;
|
||||
case NOTA_ARR:
|
||||
nota = nota_read_array(&n, nota);
|
||||
*tmp = JS_NewArray(js);
|
||||
for (int i = 0; i < n; i++) {
|
||||
nota = js_do_nota_decode(js, &ret2, nota, *tmp, JS_NewInt32(js, i), reviver);
|
||||
JS_SetPropertyInt64(js, *tmp, i, ret2);
|
||||
}
|
||||
break;
|
||||
case NOTA_REC:
|
||||
nota = nota_read_record(&n, nota);
|
||||
*tmp = JS_NewObject(js);
|
||||
for (int i = 0; i < n; i++) {
|
||||
nota = nota_read_text(&str, nota);
|
||||
*tmp = JS_NewString(js, str);
|
||||
JSValue prop_key = JS_NewString(js, str);
|
||||
nota = js_do_nota_decode(js, &ret2, nota, *tmp, prop_key, reviver);
|
||||
JS_SetPropertyStr(js, *tmp, str, ret2);
|
||||
JS_FreeValue(js, prop_key);
|
||||
free(str);
|
||||
break;
|
||||
case NOTA_ARR:
|
||||
nota = nota_read_array(&n, nota);
|
||||
*tmp = JS_NewArray(js);
|
||||
for (int i = 0; i < n; i++) {
|
||||
nota = js_do_nota_decode(js, &ret2, nota);
|
||||
JS_SetPropertyInt64(js, *tmp, i, ret2);
|
||||
}
|
||||
break;
|
||||
case NOTA_REC:
|
||||
nota = nota_read_record(&n, nota);
|
||||
*tmp = JS_NewObject(js);
|
||||
for (int i = 0; i < n; i++) {
|
||||
nota = nota_read_text(&str, nota);
|
||||
nota = js_do_nota_decode(js, &ret2, nota);
|
||||
JS_SetPropertyStr(js, *tmp, str, ret2);
|
||||
free(str);
|
||||
}
|
||||
break;
|
||||
case NOTA_INT:
|
||||
nota = nota_read_int(&n, nota);
|
||||
*tmp = JS_NewInt64(js,n);
|
||||
break;
|
||||
case NOTA_SYM:
|
||||
nota = nota_read_sym(&b, nota);
|
||||
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:
|
||||
nota = nota_read_float(&d, nota);
|
||||
*tmp = JS_NewFloat64(js,d);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NOTA_INT:
|
||||
nota = nota_read_int(&n, nota);
|
||||
*tmp = JS_NewInt64(js, n);
|
||||
break;
|
||||
case NOTA_SYM:
|
||||
nota = nota_read_sym(&b, nota);
|
||||
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:
|
||||
nota = nota_read_float(&d, nota);
|
||||
*tmp = JS_NewFloat64(js, d);
|
||||
break;
|
||||
}
|
||||
|
||||
return nota;
|
||||
if (!JS_IsUndefined(reviver)) {
|
||||
JSValue args[2] = { JS_DupValue(js, key), JS_DupValue(js, *tmp) };
|
||||
JSValue revived = JS_Call(js, reviver, holder, 2, args);
|
||||
JS_FreeValue(js, args[0]);
|
||||
JS_FreeValue(js, args[1]);
|
||||
if (!JS_IsException(revived)) {
|
||||
JS_FreeValue(js, *tmp);
|
||||
*tmp = revived;
|
||||
} else {
|
||||
JS_FreeValue(js, revived);
|
||||
}
|
||||
}
|
||||
|
||||
return nota;
|
||||
}
|
||||
|
||||
static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val);
|
||||
static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) {
|
||||
JSContext *ctx = enc->ctx;
|
||||
JSValue replaced = apply_replacer(enc, holder, key, val);
|
||||
int tag = JS_VALUE_GET_TAG(replaced);
|
||||
|
||||
static void encode_object_properties(NotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
switch (tag) {
|
||||
case JS_TAG_INT: {
|
||||
double d;
|
||||
JS_ToFloat64(ctx, &d, replaced);
|
||||
nota_write_number(&enc->nb, d);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_BIG_INT:
|
||||
case JS_TAG_FLOAT64:
|
||||
case JS_TAG_BIG_DECIMAL:
|
||||
case JS_TAG_BIG_FLOAT: {
|
||||
const char *str = JS_ToCString(ctx, replaced);
|
||||
nota_write_number_str(&enc->nb, str);
|
||||
JS_FreeCString(ctx, str);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_STRING: {
|
||||
const char *str = JS_ToCString(ctx, replaced);
|
||||
nota_write_text(&enc->nb, str);
|
||||
JS_FreeCString(ctx, str);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_BOOL:
|
||||
if (JS_VALUE_GET_BOOL(replaced)) nota_write_sym(&enc->nb, NOTA_TRUE);
|
||||
else nota_write_sym(&enc->nb, NOTA_FALSE);
|
||||
break;
|
||||
case JS_TAG_NULL:
|
||||
case JS_TAG_UNDEFINED:
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
break;
|
||||
case JS_TAG_OBJECT: {
|
||||
if (JS_IsArrayBuffer(ctx, replaced)) {
|
||||
size_t buf_len;
|
||||
void *buf_data = JS_GetArrayBuffer(ctx, &buf_len, replaced);
|
||||
nota_write_blob(&enc->nb, (unsigned long long)buf_len * 8, (const char*)buf_data);
|
||||
break;
|
||||
}
|
||||
|
||||
JSPropertyEnum *ptab;
|
||||
uint32_t plen;
|
||||
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
|
||||
if (JS_IsArray(ctx, replaced)) {
|
||||
if (nota_stack_has(enc, replaced)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
}
|
||||
nota_stack_push(enc, replaced);
|
||||
int arr_len = JS_ArrayLength(ctx, replaced);
|
||||
nota_write_array(&enc->nb, arr_len);
|
||||
for (int i = 0; i < arr_len; i++) {
|
||||
JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i);
|
||||
JSValue elem_key = JS_NewInt32(ctx, i);
|
||||
nota_encode_value(enc, elem_val, replaced, elem_key);
|
||||
JS_FreeValue(ctx, elem_val);
|
||||
JS_FreeValue(ctx, elem_key);
|
||||
}
|
||||
nota_stack_pop(enc);
|
||||
break;
|
||||
}
|
||||
|
||||
if (nota_stack_has(enc, replaced)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
}
|
||||
nota_stack_push(enc, replaced);
|
||||
|
||||
JSValue to_json = JS_GetPropertyStr(ctx, replaced, "toJSON");
|
||||
if (JS_IsFunction(ctx, to_json)) {
|
||||
JSValue result = JS_Call(ctx, to_json, replaced, 0, NULL);
|
||||
JS_FreeValue(ctx, to_json);
|
||||
if (!JS_IsException(result)) {
|
||||
nota_encode_value(enc, result, holder, key);
|
||||
JS_FreeValue(ctx, result);
|
||||
} else {
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
}
|
||||
nota_stack_pop(enc);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue(ctx, to_json);
|
||||
|
||||
JSPropertyEnum *ptab;
|
||||
uint32_t plen;
|
||||
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, replaced, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
return;
|
||||
}
|
||||
nota_stack_pop(enc);
|
||||
break;
|
||||
}
|
||||
|
||||
nota_write_record(&enc->nb, plen);
|
||||
uint32_t non_function_count = 0;
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
|
||||
if (!JS_IsFunction(ctx, prop_val)) non_function_count++;
|
||||
JS_FreeValue(ctx, prop_val);
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
// property name
|
||||
const char *propName = JS_AtomToCString(ctx, ptab[i].atom);
|
||||
nota_write_text(&enc->nb, propName);
|
||||
JS_FreeCString(ctx, propName);
|
||||
|
||||
// property value
|
||||
JSValue propVal = JS_GetProperty(ctx, val, ptab[i].atom);
|
||||
nota_encode_value(enc, propVal);
|
||||
JS_FreeValue(ctx, propVal);
|
||||
|
||||
// free the atom
|
||||
nota_write_record(&enc->nb, non_function_count);
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
|
||||
if (!JS_IsFunction(ctx, prop_val)) {
|
||||
const char *prop_name = JS_AtomToCString(ctx, ptab[i].atom);
|
||||
JSValue prop_key = JS_AtomToValue(ctx, ptab[i].atom);
|
||||
nota_write_text(&enc->nb, prop_name);
|
||||
nota_encode_value(enc, prop_val, replaced, prop_key);
|
||||
JS_FreeCString(ctx, prop_name);
|
||||
JS_FreeValue(ctx, prop_key);
|
||||
}
|
||||
JS_FreeValue(ctx, prop_val);
|
||||
JS_FreeAtom(ctx, ptab[i].atom);
|
||||
}
|
||||
js_free(ctx, ptab);
|
||||
nota_stack_pop(enc);
|
||||
break;
|
||||
}
|
||||
js_free(ctx, ptab);
|
||||
default:
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue(ctx, replaced);
|
||||
}
|
||||
|
||||
static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int tag = JS_VALUE_GET_TAG(val);
|
||||
void *value2nota(JSContext *ctx, JSValue v) {
|
||||
NotaEncodeContext enc_s, *enc = &enc_s;
|
||||
enc->ctx = ctx;
|
||||
enc->visitedStack = JS_NewArray(ctx);
|
||||
enc->cycle = 0;
|
||||
enc->replacer = JS_UNDEFINED;
|
||||
|
||||
switch (tag) {
|
||||
case JS_TAG_INT: {
|
||||
double d;
|
||||
JS_ToFloat64(ctx, &d, val);
|
||||
nota_write_number(&enc->nb, d);
|
||||
return;
|
||||
}
|
||||
case JS_TAG_BIG_INT:
|
||||
case JS_TAG_FLOAT64:
|
||||
case JS_TAG_BIG_DECIMAL:
|
||||
case JS_TAG_BIG_FLOAT: {
|
||||
const char *str = JS_ToCString(ctx, val);
|
||||
nota_write_number_str(&enc->nb, str);
|
||||
JS_FreeCString(ctx, str);
|
||||
return;
|
||||
}
|
||||
|
||||
case JS_TAG_STRING: {
|
||||
const char *str = JS_ToCString(ctx, val);
|
||||
nota_write_text(&enc->nb, str);
|
||||
JS_FreeCString(ctx, str);
|
||||
return;
|
||||
}
|
||||
|
||||
case JS_TAG_BOOL: {
|
||||
if (JS_VALUE_GET_BOOL(val))
|
||||
nota_write_sym(&enc->nb, NOTA_TRUE);
|
||||
else
|
||||
nota_write_sym(&enc->nb, NOTA_FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
case JS_TAG_NULL:
|
||||
case JS_TAG_UNDEFINED:
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
return;
|
||||
|
||||
case JS_TAG_OBJECT: {
|
||||
if (JS_IsArrayBuffer(ctx, val)) {
|
||||
size_t bufLen;
|
||||
void *bufData = JS_GetArrayBuffer(ctx, &bufLen, val);
|
||||
/* Write as a blob of bits (bufLen * 8). */
|
||||
nota_write_blob(&enc->nb, (unsigned long long)bufLen * 8, (const char*)bufData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (JS_IsArray(ctx, val)) {
|
||||
if (nota_stack_has(enc, val)) {
|
||||
enc->cycle = 1;
|
||||
return; // bail out
|
||||
}
|
||||
nota_stack_push(enc, val);
|
||||
|
||||
int arrLen = JS_ArrayLength(ctx, val);
|
||||
nota_write_array(&enc->nb, arrLen);
|
||||
for (int i = 0; i < arrLen; i++) {
|
||||
JSValue elemVal = JS_GetPropertyUint32(ctx, val, i);
|
||||
nota_encode_value(enc, elemVal);
|
||||
JS_FreeValue(ctx, elemVal);
|
||||
}
|
||||
|
||||
nota_stack_pop(enc);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nota_stack_has(enc, val)) {
|
||||
enc->cycle = 1;
|
||||
return; // bail out
|
||||
}
|
||||
|
||||
nota_stack_push(enc, val);
|
||||
encode_object_properties(enc, val);
|
||||
nota_stack_pop(enc);
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void *value2nota(JSContext *ctx, JSValue v)
|
||||
{
|
||||
NotaEncodeContext enc_s, *enc = &enc_s;
|
||||
enc->ctx = ctx;
|
||||
enc->visitedStack = JS_NewArray(ctx); // empty array initially
|
||||
enc->cycle = 0;
|
||||
|
||||
nota_buffer_init(&enc->nb, 128);
|
||||
|
||||
nota_encode_value(enc, v);
|
||||
|
||||
if (enc->cycle) {
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
nota_buffer_free(&enc->nb);
|
||||
return NULL;
|
||||
}
|
||||
nota_buffer_init(&enc->nb, 128);
|
||||
nota_encode_value(enc, v, JS_UNDEFINED, JS_NewString(ctx, ""));
|
||||
|
||||
if (enc->cycle) {
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
|
||||
void* dataPtr = enc->nb.data; // pointer to the raw data
|
||||
enc->nb.data = NULL;
|
||||
nota_buffer_free(&enc->nb);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return dataPtr;
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
void *data_ptr = enc->nb.data;
|
||||
enc->nb.data = NULL;
|
||||
nota_buffer_free(&enc->nb);
|
||||
return data_ptr;
|
||||
}
|
||||
|
||||
JSValue nota2value(JSContext *js, char *nota)
|
||||
{
|
||||
JSValue nota2value(JSContext *js, char *nota) {
|
||||
if (!nota) return JS_UNDEFINED;
|
||||
JSValue ret;
|
||||
js_do_nota_decode(js, &ret, nota);
|
||||
JSValue holder = JS_NewObject(js);
|
||||
js_do_nota_decode(js, &ret, nota, holder, JS_NewString(js, ""), JS_UNDEFINED);
|
||||
JS_FreeValue(js, holder);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
|
||||
{
|
||||
if (argc < 1)
|
||||
return JS_ThrowTypeError(ctx, "nota.encode requires 1 argument");
|
||||
static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_ThrowTypeError(ctx, "nota.encode requires at least 1 argument");
|
||||
|
||||
NotaEncodeContext enc_s, *enc = &enc_s;
|
||||
enc->ctx = ctx;
|
||||
enc->visitedStack = JS_NewArray(ctx); // empty array initially
|
||||
enc->cycle = 0;
|
||||
NotaEncodeContext enc_s, *enc = &enc_s;
|
||||
enc->ctx = ctx;
|
||||
enc->visitedStack = JS_NewArray(ctx);
|
||||
enc->cycle = 0;
|
||||
enc->replacer = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_UNDEFINED;
|
||||
|
||||
nota_buffer_init(&enc->nb, 128);
|
||||
|
||||
nota_encode_value(enc, argv[0]);
|
||||
|
||||
if (enc->cycle) {
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
nota_buffer_free(&enc->nb);
|
||||
return JS_ThrowReferenceError(ctx, "Tried to encode something to nota with a cycle.");
|
||||
}
|
||||
nota_buffer_init(&enc->nb, 128);
|
||||
nota_encode_value(enc, argv[0], JS_UNDEFINED, JS_NewString(ctx, ""));
|
||||
|
||||
if (enc->cycle) {
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
|
||||
size_t totalLen = enc->nb.size; // how many bytes used
|
||||
void* dataPtr = enc->nb.data; // pointer to the raw data
|
||||
JSValue ret = JS_NewArrayBufferCopy(ctx, (uint8_t*)dataPtr, totalLen);
|
||||
|
||||
nota_buffer_free(&enc->nb);
|
||||
return JS_ThrowReferenceError(ctx, "Tried to encode something to nota with a cycle.");
|
||||
}
|
||||
|
||||
return ret;
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
size_t total_len = enc->nb.size;
|
||||
void *data_ptr = enc->nb.data;
|
||||
JSValue ret = JS_NewArrayBufferCopy(ctx, (uint8_t*)data_ptr, total_len);
|
||||
|
||||
nota_buffer_free(&enc->nb);
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSValue js_nota_decode(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
if (argc < 1) return JS_UNDEFINED;
|
||||
static JSValue js_nota_decode(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_UNDEFINED;
|
||||
|
||||
size_t len;
|
||||
unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]);
|
||||
if (!nota) return JS_UNDEFINED;
|
||||
size_t len;
|
||||
unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]);
|
||||
if (!nota) return JS_UNDEFINED;
|
||||
|
||||
JSValue ret;
|
||||
js_do_nota_decode(js, &ret, (char*)nota);
|
||||
return ret;
|
||||
JSValue reviver = (argc > 1 && JS_IsFunction(js, argv[1])) ? argv[1] : JS_UNDEFINED;
|
||||
JSValue ret;
|
||||
JSValue holder = JS_NewObject(js);
|
||||
js_do_nota_decode(js, &ret, (char*)nota, holder, JS_NewString(js, ""), reviver);
|
||||
JS_FreeValue(js, holder);
|
||||
return ret;
|
||||
}
|
||||
|
||||
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("encode", 1, js_nota_encode),
|
||||
JS_CFUNC_DEF("decode", 1, js_nota_decode),
|
||||
};
|
||||
|
||||
static int js_nota_init(JSContext *ctx, JSModuleDef *m) {
|
||||
JS_SetModuleExportList(ctx, m, js_nota_funcs,
|
||||
sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
|
||||
return 0;
|
||||
JS_SetModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
#ifdef JS_SHARED_LIBRARY
|
||||
@@ -337,9 +353,8 @@ JSValue js_nota_use(JSContext *js)
|
||||
#endif
|
||||
|
||||
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) {
|
||||
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_nota_init);
|
||||
if (!m) return NULL;
|
||||
JS_AddModuleExportList(ctx, m, js_nota_funcs,
|
||||
sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
|
||||
return m;
|
||||
}
|
||||
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_nota_init);
|
||||
if (!m) return NULL;
|
||||
JS_AddModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
|
||||
return m;
|
||||
}
|
||||
410
tests/nota.js
410
tests/nota.js
@@ -3,96 +3,87 @@ var os = use('os');
|
||||
|
||||
// Helper function to convert hex string to ArrayBuffer
|
||||
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);
|
||||
}
|
||||
return bytes.buffer;
|
||||
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);
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
// Helper function to convert ArrayBuffer to hex string
|
||||
function bufferToHex(buffer) {
|
||||
return Array.from(new Uint8Array(buffer))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
.toLowerCase();
|
||||
return Array.from(new Uint8Array(buffer))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
if (expected === actual) return { passed: true, messages: [] };
|
||||
|
||||
if (typeof expected === 'number' && typeof actual === 'number') {
|
||||
if (isNaN(expected) && isNaN(actual))
|
||||
return { passed: true, messages: [] };
|
||||
|
||||
const diff = Math.abs(expected - actual);
|
||||
if (diff <= EPSILON)
|
||||
return { passed: true, messages: [] };
|
||||
|
||||
return {
|
||||
passed: false,
|
||||
messages: [`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`]
|
||||
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
|
||||
@@ -104,79 +95,100 @@ for (var i = 0; i < 500; i++) {
|
||||
}
|
||||
|
||||
var testCases = [
|
||||
// Integer tests
|
||||
{ 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" },
|
||||
// Integer tests
|
||||
{ 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" },
|
||||
|
||||
// Symbol tests
|
||||
{ input: undefined, expectedHex: "70" },
|
||||
{ input: false, expectedHex: "72" },
|
||||
{ input: true, expectedHex: "73" },
|
||||
// Note: private (78) and system (79) require following records, tested below
|
||||
// Symbol tests
|
||||
{ input: undefined, expectedHex: "70" },
|
||||
{ input: false, expectedHex: "72" },
|
||||
{ input: true, expectedHex: "73" },
|
||||
|
||||
// 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" },
|
||||
// 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" },
|
||||
// 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
|
||||
// Blob tests
|
||||
{ input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "8010ffaa" },
|
||||
{ input: new Uint8Array([0b11110000, 0b11100011, 0b00100000, 0b10000000]).buffer,
|
||||
expectedHex: "8019f0e32080" },
|
||||
|
||||
{ input: testarr, expectedHex: hex },
|
||||
{ input: testarr, expectedHex: hex },
|
||||
|
||||
// Array tests
|
||||
{ input: [], expectedHex: "20" },
|
||||
{ input: [1, 2, 3], expectedHex: "23616263" },
|
||||
{ input: [-1, 0, 1.5], expectedHex: "2369605043" },
|
||||
// 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" },
|
||||
|
||||
// Complex nested structures
|
||||
{ input: {
|
||||
num: 42,
|
||||
arr: [1, -1, 2.5],
|
||||
str: "test",
|
||||
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" },
|
||||
// Record tests
|
||||
{ input: {}, expectedHex: "30" },
|
||||
{ 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" },
|
||||
|
||||
// Private prefix test
|
||||
{ input: { private: { address: "test" } },
|
||||
expectedHex: "317821616464726573731474657374" },
|
||||
|
||||
// System prefix test
|
||||
{ input: { system: { msg: "hello" } },
|
||||
expectedHex: "3179216d73671568656c6c6f" },
|
||||
|
||||
{ input: [ { system: {msg: "hello" } }, {
|
||||
num: 42,
|
||||
arr: [1, -1, 2.5],
|
||||
str: "test",
|
||||
obj: { x: true }
|
||||
} ], expectedHex: "223179216d73671568656c6c6f34216e756d622a2173747214746573742161727223616965235840216f626a21117873" },
|
||||
{ input: [{ system: {msg: "hello" } }, {
|
||||
num: 42,
|
||||
arr: [1, -1, 2.5],
|
||||
str: "test",
|
||||
obj: { x: true }
|
||||
}], expectedHex: "223179216d73671568656c6c6f34216e756d622a2173747214746573742161727223616965235840216f626a21117873" },
|
||||
|
||||
// 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
|
||||
// Additional edge cases
|
||||
{ input: new Uint8Array([]).buffer, expectedHex: "00" },
|
||||
{ input: [[]], expectedHex: "2120" },
|
||||
{ input: { "": "" }, expectedHex: "311010" },
|
||||
{ input: 1e-10, expectedHex: "d00a01" },
|
||||
|
||||
// Replacer tests
|
||||
{ input: { a: 1, b: 2 },
|
||||
replacer: (key, value) => typeof value === 'number' ? value * 2 : value,
|
||||
expected: { a: 2, b: 4 },
|
||||
testType: 'replacer' },
|
||||
|
||||
{ input: { x: "test", y: 5 },
|
||||
replacer: (key, value) => key === 'x' ? value + "!" : value,
|
||||
expected: { x: "test!", y: 5 },
|
||||
testType: 'replacer' },
|
||||
|
||||
// Reviver tests
|
||||
{ input: { a: 1, b: 2 },
|
||||
reviver: (key, value) => typeof value === 'number' ? value * 3 : value,
|
||||
expected: { a: 3, b: 6 },
|
||||
testType: 'reviver' },
|
||||
|
||||
{ input: { x: "test", y: 10 },
|
||||
reviver: (key, value) => key === 'y' ? value + 1 : value,
|
||||
expected: { x: "test", y: 11 },
|
||||
testType: 'reviver' }
|
||||
];
|
||||
|
||||
// Run tests and collect results
|
||||
@@ -184,79 +196,77 @@ let results = [];
|
||||
let testCount = 0;
|
||||
|
||||
for (let test of testCases) {
|
||||
testCount++;
|
||||
let testName = `Test ${testCount}: ${JSON.stringify(test.input)}`;
|
||||
let passed = true;
|
||||
let messages = [];
|
||||
testCount++;
|
||||
let testName = `Test ${testCount}: ${JSON.stringify(test.input)}${test.testType ? ` (${test.testType})` : ''}`;
|
||||
let passed = true;
|
||||
let messages = [];
|
||||
|
||||
// Test encoding
|
||||
let encoded = nota.encode(test.input);
|
||||
if (!(encoded instanceof ArrayBuffer)) {
|
||||
passed = false;
|
||||
messages.push("Encode should return ArrayBuffer");
|
||||
} else {
|
||||
let encodedHex = bufferToHex(encoded);
|
||||
if (encodedHex !== test.expectedHex.toLowerCase()) {
|
||||
messages.push(
|
||||
`Hex encoding differs (informational):
|
||||
Expected: ${test.expectedHex}
|
||||
Got: ${encodedHex}`
|
||||
);
|
||||
}
|
||||
|
||||
// Test decoding
|
||||
let decoded = nota.decode(encoded);
|
||||
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:");
|
||||
messages.push(...compareResult.messages);
|
||||
}
|
||||
// Test encoding
|
||||
let encoded = test.replacer ? nota.encode(test.input, test.replacer) : nota.encode(test.input);
|
||||
if (!(encoded instanceof ArrayBuffer)) {
|
||||
passed = false;
|
||||
messages.push("Encode should return ArrayBuffer");
|
||||
} else {
|
||||
if (test.expectedHex) {
|
||||
let encodedHex = bufferToHex(encoded);
|
||||
if (encodedHex !== test.expectedHex.toLowerCase()) {
|
||||
messages.push(
|
||||
`Hex encoding differs (informational):
|
||||
Expected: ${test.expectedHex}
|
||||
Got: ${encodedHex}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Record result
|
||||
results.push({ testName, passed, messages });
|
||||
// Test decoding
|
||||
let decoded = test.reviver ? nota.decode(encoded, test.reviver) : nota.decode(encoded);
|
||||
let expected = test.expected || test.input;
|
||||
|
||||
// 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("");
|
||||
// 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));
|
||||
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:");
|
||||
messages.push(...compareResult.messages);
|
||||
}
|
||||
}
|
||||
|
||||
results.push({ testName, passed, messages });
|
||||
|
||||
if (!passed) {
|
||||
console.log(`\nDetailed Failure Report for ${testName}:`);
|
||||
console.log(`Input: ${JSON.stringify(test.input)}`);
|
||||
if (test.replacer) console.log(`Replacer: ${test.replacer.toString()}`);
|
||||
if (test.reviver) console.log(`Reviver: ${test.reviver.toString()}`);
|
||||
console.log(messages.join("\n"));
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log("\nTest Summary:");
|
||||
results.forEach(result => {
|
||||
console.log(`${result.testName} - ${result.passed ? "Passed" : "Failed"}`);
|
||||
if (!result.passed)
|
||||
console.log(result.messages)
|
||||
console.log(`${result.testName} - ${result.passed ? "Passed" : "Failed"}`);
|
||||
if (!result.passed)
|
||||
console.log(result.messages)
|
||||
});
|
||||
|
||||
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);
|
||||
console.log("Overall: FAILED");
|
||||
os.exit(1);
|
||||
} else {
|
||||
console.log("Overall: PASSED");
|
||||
os.exit(0);
|
||||
}
|
||||
|
||||
console.log("Overall: PASSED");
|
||||
os.exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user