add http get
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m3s
Build and Deploy / build-windows (CLANG64) (push) Successful in 9m45s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped

This commit is contained in:
2025-03-03 08:07:16 -06:00
parent 30c5da879b
commit 6b4062eee6
12 changed files with 675 additions and 1 deletions

View File

@@ -132,8 +132,79 @@ deps += dependency('chipmunk', static:true)
deps += dependency('enet', static:true)
deps += dependency('soloud', static:true)
curl_opts = [
'http=enabled',
'ssl=enabled',
'openssl=enabled',
'schannel=disabled',
'secure-transport=disabled',
'dict=disabled',
'file=disabled',
'ftp=disabled',
'gopher=disabled',
'imap=disabled',
'ldap=disabled',
'ldaps=disabled',
'mqtt=disabled',
'pop3=disabled',
'rtmp=disabled',
'rtsp=disabled',
'smb=disabled',
'smtp=disabled',
'telnet=disabled',
'tftp=disabled',
'alt-svc=disabled',
'asynchdns=disabled',
'aws=disabled',
'basic-auth=disabled',
'bearer-auth=disabled',
'bindlocal=disabled',
'brotli=disabled',
'cookies=disabled',
'digest-auth=disabled',
'doh=disabled',
'form-api=disabled',
'getoptions=disabled',
'gsasl=disabled',
'gss-api=disabled',
'headers-api=disabled',
'hsts=disabled',
'http2=disabled',
'idn=disabled',
'kerberos-auth=disabled',
'libcurl-option=disabled',
'libz=disabled',
'mime=disabled',
'negotiate-auth=disabled',
'netrc=disabled',
'ntlm=disabled',
'parsedate=disabled',
'progress-meter=disabled',
'proxy=disabled',
'psl=disabled',
'sha512_256=disabled',
'shuffle-dns=disabled',
'socketpair=disabled',
'tls-srp=disabled',
'unixsockets=disabled',
'verbose-strings=disabled',
'zstd=disabled',
'debug=disabled',
'curldebug=false',
'libuv=disabled',
'tests=disabled',
'unittests=disabled',
'default_library=static'
]
curl_proj = subproject('curl', default_options: curl_opts)
deps += dependency('libcurl')
deps += dependency('zlib', static: true)
deps += dependency('openssl', static:true)
sources = []
src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c']
src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c', 'qjs_http.c']
imsrc = ['GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp','imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp','implot_items.cpp','implot.cpp', 'imgui_impl_sdlrenderer3.cpp', 'imgui_impl_sdl3.cpp', 'imgui_impl_sdlgpu3.cpp']

View File

@@ -519,6 +519,8 @@ var fnname = "doc"
script = `(function ${fnname}() { ${script}; })`
js.eval(DOCPATH, script)()
console.log(`BOTTOM OF ENGINE ${os.now()}`)
use('cmd')(prosperon.argv)
})()

26
scripts/modules/http.js Normal file
View File

@@ -0,0 +1,26 @@
var http = this
http.fetch[prosperon.DOC] = `Initiate an asynchronous HTTP GET request.
This function enqueues an HTTP GET request for the specified URL. It supports both buffered responses (full response collected in memory) and streaming data (processed as it arrives). The request is executed asynchronously, and its completion or streaming data is handled via callbacks provided in the options. Use 'poll' to process the results.
:param url: A string representing the URL to fetch.
:param options: Either a callback function or an object with optional properties:
- 'callback': A function invoked upon request completion, receiving an object with 'data' (string or null) and 'error' (string or null) properties.
- 'on_data': A function invoked for each chunk of streaming data, receiving a string chunk as its argument. If supplied, 'callback.data' will be null.
:return: undefined
:throws:
- An error if the URL is not a string or is invalid.
- An error if the options argument is neither a function nor an object.
- An error if memory allocation or CURL initialization fails.
`
http.poll[prosperon.DOC] = `Process pending HTTP requests and invoke callbacks.
This function checks for I/O activity on all enqueued HTTP requests and processes any that have completed or received data. For completed requests, it invokes the 'callback' function (if provided) with the result. For streaming requests, it invokes the 'data' function (if provided) as data arrives. This function must be called repeatedly to drive the asynchronous request system.
:return: undefined
:throws: An error if CURL multi-handle processing fails.
`
return http

View File

@@ -7585,6 +7585,7 @@ JSValue js_imgui_use(JSContext *js);
#include "qjs_nota.h"
#include "qjs_enet.h"
#include "qjs_soloud.h"
#include "qjs_http.h"
#define MISTLINE(NAME) (ModuleEntry){#NAME, js_##NAME##_use}
@@ -7610,6 +7611,7 @@ void ffi_load(JSContext *js, int argc, char **argv) {
arrput(module_registry, MISTLINE(dmon));
arrput(module_registry, MISTLINE(nota));
arrput(module_registry, MISTLINE(enet));
arrput(module_registry, MISTLINE(http));
#ifdef TRACY_ENABLE
arrput(module_registry, MISTLINE(tracy));

View File

@@ -5,6 +5,8 @@
#include <string.h>
#include <stdint.h>
#include <SDL3/SDL.h>
static unsigned char *zip_buffer_global;
static char *prosperon;
@@ -93,6 +95,8 @@ if (!ret) {
}
int main(int argc, char **argv) {
SDL_Init(SDL_INIT_EVENTS);
printf("INIT TIME %g\n", (double)SDL_GetTicksNS()/1000000000.0);
prosperon = argv[0];
PHYSFS_init(argv[0]);
@@ -107,6 +111,8 @@ int main(int argc, char **argv) {
printf("Could not mount core. Reason: %s\n", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
return 1;
}
printf("MOUNT TIME %g\n", (double)SDL_GetTicksNS()/1000000000.0);
script_startup(argc, argv); // runs engine.js
return 0;

265
source/qjs_http.c Normal file
View File

@@ -0,0 +1,265 @@
#include "quickjs.h"
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
// -----------------------------------------------------------------------------
// HTTP request structure
// -----------------------------------------------------------------------------
typedef struct {
char url[512]; // URL for the request
JSContext *ctx; // JS context for callbacks
JSValue callback; // Completion callback (optional)
JSValue on_data; // Streaming data callback (optional)
char *response; // Buffer for non-streaming mode
size_t size; // Size of response buffer
CURL *curl; // CURL easy handle
int done; // Request completion flag
int curl_result; // CURL result code
} HttpRequest;
// -----------------------------------------------------------------------------
// Global data
// -----------------------------------------------------------------------------
static CURLM *g_curl_multi = NULL; // CURL multi-handle for async I/O
// -----------------------------------------------------------------------------
// Libcurl write callback for streaming or buffering
// -----------------------------------------------------------------------------
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t realsize = size * nmemb;
HttpRequest *req = (HttpRequest *)userp;
// Streaming mode: call onData if provided
if (JS_IsFunction(req->ctx, req->on_data)) {
JSValue chunk = JS_NewStringLen(req->ctx, contents, realsize);
JSValue ret = JS_Call(req->ctx, req->on_data, JS_UNDEFINED, 1, &chunk);
JS_FreeValue(req->ctx, chunk);
JS_FreeValue(req->ctx, ret); // Ignore return value
return realsize;
}
// Non-streaming mode: buffer the response
char *ptr = realloc(req->response, req->size + realsize + 1);
if (!ptr) {
return 0; // Out of memory, tell CURL to abort
}
req->response = ptr;
memcpy(&(req->response[req->size]), contents, realsize);
req->size += realsize;
req->response[req->size] = '\0'; // Null-terminate
return realsize;
}
// -----------------------------------------------------------------------------
// JS function: http.fetch(url, options)
// - Enqueues an async HTTP request with optional streaming
// -----------------------------------------------------------------------------
static JSValue js_http_fetch(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
if (argc < 2 || !JS_IsString(argv[0])) {
return JS_ThrowTypeError(ctx, "fetch expects a URL string and an options object or callback");
}
// Get URL
const char *url = JS_ToCString(ctx, argv[0]);
if (!url) {
return JS_ThrowTypeError(ctx, "Invalid URL");
}
// Allocate request object
HttpRequest *req = calloc(1, sizeof(*req));
if (!req) {
JS_FreeCString(ctx, url);
return JS_ThrowInternalError(ctx, "Failed to allocate memory");
}
strncpy(req->url, url, sizeof(req->url) - 1);
req->ctx = ctx;
req->callback = JS_NULL;
req->on_data = JS_NULL;
// Parse second argument: callback or options object
if (JS_IsFunction(ctx, argv[1])) {
req->callback = JS_DupValue(ctx, argv[1]);
} else if (JS_IsObject(argv[1])) {
JSValue callback = JS_GetPropertyStr(ctx, argv[1], "callback");
JSValue on_data = JS_GetPropertyStr(ctx, argv[1], "on_data");
if (JS_IsFunction(ctx, callback)) {
req->callback = JS_DupValue(ctx, callback);
}
if (JS_IsFunction(ctx, on_data)) {
req->on_data = JS_DupValue(ctx, on_data);
}
JS_FreeValue(ctx, callback);
JS_FreeValue(ctx, on_data);
} else {
JS_FreeCString(ctx, url);
free(req);
return JS_ThrowTypeError(ctx, "Second argument must be a callback or options object");
}
JS_FreeCString(ctx, url);
// Initialize CURL easy handle
req->curl = curl_easy_init();
if (!req->curl) {
JS_FreeValue(ctx, req->callback);
JS_FreeValue(ctx, req->on_data);
free(req);
return JS_ThrowInternalError(ctx, "Failed to create CURL handle");
}
// Set CURL options
curl_easy_setopt(req->curl, CURLOPT_URL, req->url);
curl_easy_setopt(req->curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(req->curl, CURLOPT_WRITEDATA, req);
curl_easy_setopt(req->curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(req->curl, CURLOPT_USERAGENT, "prosperon");
curl_easy_setopt(req->curl, CURLOPT_PRIVATE, req);
// Add to multi-handle
if (!g_curl_multi) {
curl_easy_cleanup(req->curl);
JS_FreeValue(ctx, req->callback);
JS_FreeValue(ctx, req->on_data);
free(req);
return JS_ThrowInternalError(ctx, "CURL multi-handle not initialized");
}
CURLMcode mc = curl_multi_add_handle(g_curl_multi, req->curl);
if (mc != CURLM_OK) {
curl_easy_cleanup(req->curl);
JS_FreeValue(ctx, req->callback);
JS_FreeValue(ctx, req->on_data);
free(req);
return JS_ThrowInternalError(ctx, "curl_multi_add_handle failed: %s", curl_multi_strerror(mc));
}
return JS_UNDEFINED;
}
// -----------------------------------------------------------------------------
// JS function: http.poll()
// - Checks for I/O and completed requests, invoking callbacks as needed
// -----------------------------------------------------------------------------
static JSValue js_http_poll(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
if (!g_curl_multi) {
return JS_UNDEFINED;
}
// Perform pending transfers
int still_running = 0;
CURLMcode mc = curl_multi_perform(g_curl_multi, &still_running);
if (mc != CURLM_OK) {
return JS_ThrowInternalError(ctx, "curl_multi_perform failed: %s", curl_multi_strerror(mc));
}
// Check for completed requests
CURLMsg *msg;
int msgq = 0;
while ((msg = curl_multi_info_read(g_curl_multi, &msgq))) {
if (msg->msg == CURLMSG_DONE) {
CURL *easy = msg->easy_handle;
HttpRequest *req = NULL;
curl_easy_getinfo(easy, CURLINFO_PRIVATE, (char **)&req);
// Remove from multi-handle
curl_multi_remove_handle(g_curl_multi, easy);
// Mark as done
req->curl_result = msg->data.result;
req->done = 1;
// Call completion callback if provided
if (JS_IsFunction(req->ctx, req->callback)) {
JSValue arg = JS_NewObject(req->ctx);
if (req->curl_result == CURLE_OK) {
JSValue data = (req->response && req->size > 0) ?
JS_NewStringLen(req->ctx, req->response, req->size) : JS_NULL;
JS_DefinePropertyValueStr(req->ctx, arg, "data", data, JS_PROP_C_W_E);
JS_DefinePropertyValueStr(req->ctx, arg, "error", JS_NULL, JS_PROP_C_W_E);
} else {
JS_DefinePropertyValueStr(req->ctx, arg, "data", JS_NULL, JS_PROP_C_W_E);
const char *err_str = curl_easy_strerror(req->curl_result);
JS_DefinePropertyValueStr(req->ctx, arg, "error",
JS_NewString(req->ctx, err_str ? err_str : "Unknown error"),
JS_PROP_C_W_E);
}
JSValue ret = JS_Call(req->ctx, req->callback, JS_UNDEFINED, 1, &arg);
JS_FreeValue(req->ctx, arg);
JS_FreeValue(req->ctx, ret);
}
// Cleanup
JS_FreeValue(req->ctx, req->callback);
JS_FreeValue(req->ctx, req->on_data);
curl_easy_cleanup(req->curl);
free(req->response);
free(req);
}
}
return JS_UNDEFINED;
}
// -----------------------------------------------------------------------------
// Module initialization
// -----------------------------------------------------------------------------
static const JSCFunctionListEntry js_http_funcs[] = {
JS_CFUNC_DEF("fetch", 2, js_http_fetch),
JS_CFUNC_DEF("poll", 0, js_http_poll),
};
JSValue js_http_use(JSContext *ctx)
{
// Initialize CURL globally (once per process)
static int s_curl_init_done = 0;
if (!s_curl_init_done) {
s_curl_init_done = 1;
if (curl_global_init(CURL_GLOBAL_ALL) != 0) {
return JS_ThrowInternalError(ctx, "Failed to initialize CURL");
}
}
// Initialize multi-handle (once per module)
if (!g_curl_multi) {
g_curl_multi = curl_multi_init();
if (!g_curl_multi) {
return JS_ThrowInternalError(ctx, "Failed to initialize CURL multi-handle");
}
}
// Export fetch and poll functions
JSValue export_obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, export_obj, js_http_funcs,
sizeof(js_http_funcs) / sizeof(JSCFunctionListEntry));
return export_obj;
}
static int js_http_init(JSContext *ctx, JSModuleDef *m)
{
JS_SetModuleExport(ctx, m, "default", js_http_use(ctx));
return 0;
}
#ifdef JS_SHARED_LIBRARY
#define JS_INIT_MODULE js_init_module
#else
#define JS_INIT_MODULE js_init_module_http
#endif
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name)
{
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_http_init);
if (!m) {
return NULL;
}
JS_AddModuleExport(ctx, m, "default");
return m;
}

8
source/qjs_http.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef QJS_HTTP_H
#define QJS_HTTP_H
#include "quickjs.h"
JSValue js_http_use(JSContext*);
#endif

View File

@@ -11,6 +11,8 @@
#include <assert.h>
#include "quickjs.h"
#include <SDL3/SDL.h>
#if defined(__APPLE__)
#include <malloc/malloc.h>
#elif defined(_WIN32)
@@ -161,8 +163,12 @@ void script_startup(int argc, char **argv) {
on_exception = JS_UNDEFINED;
printf("INTRINSICTS %g\n", (double)SDL_GetTicksNS()/1000000000.0);
ffi_load(js, argc, argv);
printf("FFI LOAD %g\n", (double)SDL_GetTicksNS()/1000000000.0);
PHYSFS_File *eng = PHYSFS_openRead(ENGINE);
if (!eng) {
printf("Could not open file! %s\n", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
@@ -174,6 +180,7 @@ void script_startup(int argc, char **argv) {
void *data = malloc(stat.filesize);
PHYSFS_readBytes(eng,data,stat.filesize);
PHYSFS_close(eng);
printf("LOAD MAIN %g\n", (double)SDL_GetTicksNS()/1000000000.0);
JSValue v = script_eval(js, ENGINE, data);
uncaught_exception(js,v);
free(eng);

13
subprojects/curl.wrap Normal file
View File

@@ -0,0 +1,13 @@
[wrap-file]
directory = curl-8.10.1
source_url = https://github.com/curl/curl/releases/download/curl-8_10_1/curl-8.10.1.tar.xz
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/curl_8.10.1-1/curl-8.10.1.tar.xz
source_filename = curl-8.10.1.tar.xz
source_hash = 73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee
patch_filename = curl_8.10.1-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/curl_8.10.1-1/get_patch
patch_hash = 707c28f35fc9b0e8d68c0c2800712007612f922a31da9637ce706a2159f3ddd8
wrapdb_version = 8.10.1-1
[provide]
dependency_names = libcurl

15
subprojects/openssl.wrap Normal file
View File

@@ -0,0 +1,15 @@
[wrap-file]
directory = openssl-3.0.8
source_url = https://www.openssl.org/source/openssl-3.0.8.tar.gz
source_filename = openssl-3.0.8.tar.gz
source_hash = 6c13d2bf38fdf31eac3ce2a347073673f5d63263398f1f69d0df4a41253e4b3e
patch_filename = openssl_3.0.8-3_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.8-3/get_patch
patch_hash = 300da189e106942347d61a4a4295aa2edbcf06184f8d13b4cee0bed9fb936963
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/openssl_3.0.8-3/openssl-3.0.8.tar.gz
wrapdb_version = 3.0.8-3
[provide]
libcrypto = libcrypto_dep
libssl = libssl_dep
openssl = openssl_dep

13
subprojects/zlib.wrap Normal file
View File

@@ -0,0 +1,13 @@
[wrap-file]
directory = zlib-1.3.1
source_url = http://zlib.net/fossils/zlib-1.3.1.tar.gz
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/zlib_1.3.1-1/zlib-1.3.1.tar.gz
source_filename = zlib-1.3.1.tar.gz
source_hash = 9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23
patch_filename = zlib_1.3.1-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/zlib_1.3.1-1/get_patch
patch_hash = e79b98eb24a75392009cec6f99ca5cdca9881ff20bfa174e8b8926d5c7a47095
wrapdb_version = 1.3.1-1
[provide]
zlib = zlib_dep

246
tests/http.js Normal file
View File

@@ -0,0 +1,246 @@
// http_test.js
var http = use('http');
var os = use('os');
var getter = http.fetch('tortoise.png')
var file = getter.data() // invokes the data stream to wait for it
var got = false
var count = 0
http.fetch("https://dictionary.ink/find?word=theological", {
on_data: e => {
console.log(e.length)
count++
},
callback: e => {
for (var i in e) console.log(i)
console.log(e.data)
got = true
}
})
while (!got) {
http.poll()
}
console.log(`got hit ${count} times`)
os.exit()
// Deep comparison function (unchanged from previous version)
function deepCompare(expected, actual, path = '') {
if (expected === actual) return { passed: true, messages: [] };
if (typeof expected === 'string' && typeof actual === 'string') {
if (expected === actual) {
return { passed: true, messages: [] };
}
return {
passed: false,
messages: [`String mismatch at ${path}: expected "${expected}", got "${actual}"`]
};
}
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)}`]
};
}
// Test cases (slightly modified to include state tracking)
var testCases = [
{
name: "Basic GET request",
url: "https://api.github.com",
expected: { contains: "GitHub" },
validate: function(result) { return result.toLowerCase().includes("github"); },
completed: false,
result: null,
error: null
},
{
name: "JSON response",
url: "https://api.github.com/users/octocat",
expected: { login: "octocat" },
validate: function(result) {
let parsed = JSON.parse(result);
return deepCompare({ login: "octocat" }, { login: parsed.login });
},
completed: false,
result: null,
error: null
},
{
name: "Follow redirect",
url: "http://github.com",
expected: { contains: "gihtub" },
validate: function(result) { return result.toLowerCase().includes("github"); },
completed: false,
result: null,
error: null
},
{
name: "Invalid URL",
url: "http://nonexistent.domain.xyz",
expectError: true,
validate: function(result) { return true; },
completed: false,
result: null,
error: null
},
{
name: "Malformed URL",
url: "not-a-url",
expectError: true,
validate: function(result) { return true; },
completed: false,
result: null,
error: null
},
{
name: "Large response",
url: "https://www.gutenberg.org/files/1342/1342-0.txt",
expected: { contains: "Pride and Prejudice" },
validate: function(result) { return result.includes("Pride and Prejudice"); },
completed: false,
result: null,
error: null
}
];
// Test execution state
var results = [];
var testCount = 0;
var activeRequests = 0;
var timeout = 5000; // 5 seconds timeout per test
// Start tests
function startTests() {
testCount = testCases.length;
activeRequests = testCount;
for (let i = 0; i < testCases.length; i++) {
let test = testCases[i];
let testName = `Test ${i + 1}: ${test.name}`;
http.fetch(test.url, function(result) {
test.completed = true;
activeRequests--;
if (result.error) {
test.error = result.error;
} else {
test.result = result.data;
}
});
}
// Start polling loop
pollTests();
}
// Poll and check test completion
function pollTests() {
let startTime = os.now();
while (true) {
http.poll();
let allCompleted = activeRequests === 0;
let timedOut = (os.now() - startTime) >= timeout;
if (allCompleted || timedOut) {
processResults();
break;
}
// Sleep a bit to avoid pegging the CPU (requires a C function or std.sleep)
os.sleep(0.01);
}
}
// Process and report results
function processResults() {
for (let i = 0; i < testCases.length; i++) {
let test = testCases[i];
let testName = `Test ${i + 1}: ${test.name}`;
let passed = true;
let messages = [];
if (!test.completed) {
passed = false;
messages.push("Test timed out");
} else if (test.error) {
if (test.expectError) {
// Expected error occurred
} else {
passed = false;
messages.push(`Request failed: ${test.error}`);
}
} else if (test.expectError) {
passed = false;
messages.push("Expected request to fail but it succeeded");
} else {
const validation = test.validate(test.result);
if (typeof validation === 'boolean') {
if (!validation) {
passed = false;
messages.push(`Validation failed for ${test.url}`);
messages.push(`Expected to contain: ${JSON.stringify(test.expected)}`);
messages.push(`Got: ${test.result.substring(0, 100)}...`);
}
} else if (!validation.passed) {
passed = false;
messages.push(...validation.messages);
}
}
results.push({ testName, passed, messages });
if (!passed) {
console.log(`\nDetailed Failure Report for ${testName}:`);
console.log(`URL: ${test.url}`);
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.join("\n"));
}
});
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: PASSED");
os.exit(0);
}
}
// Run the tests
startTests();