aboutsummaryrefslogtreecommitdiffstats
path: root/tests/surface-probe.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 19:30:25 +0100
committereunhhu <[email protected]>2026-09-27 19:30:25 +0100
commitd72f57af85bb57042332da8bc002a2a0640639a0 (patch)
treeb1a0ad2b798da32532f47ee431ca5ee24ec9f1ca /tests/surface-probe.c
parent6d276be51ba89b04a60bb16b5179c015ad202c7c (diff)
downloadpure-d72f57af85bb57042332da8bc002a2a0640639a0.tar.gz
pure-d72f57af85bb57042332da8bc002a2a0640639a0.zip
Run a surface for people inside the node (design 010)
pure-serve --surface LIBRARY [-- ARGUMENTS] maps a library and runs its pure_surface_run in a thread of its own (serve/surface.h). The surface reads the app as of the last save where it is, a published snapshot in a reader app, as a connection thread reads views; an eventfd tells it of each new snapshot; and its events (a key pressed, text typed) are requests without a host in the owner's queue, applied and saved with the round, their results handed back to the surface's waiter. So one node serves programs over HTTP and people on its display with one owner of its states, and nothing between the states and the screen. pure-serve exports its pure_* functions for the surface to call, one copy of the app model in the process; the table carries sizeof(PureApp) and sizeof(PureModule) so a surface built for another layout refuses. The check builds a probe surface that types, presses and reads: its events show in HTTP views, an HTTP input shows in it, wrong inputs are refused, it stops with the server, and a module without states cannot have one. Without a surface the request path measured the same (small GETs 140.9k vs 139.9k, app totals 126.3k vs 126.3k over eight rounds); the exported symbols cost 11 KB of the file and 24 KiB idle.
Diffstat (limited to 'tests/surface-probe.c')
-rw-r--r--tests/surface-probe.c85
1 files changed, 85 insertions, 0 deletions
diff --git a/tests/surface-probe.c b/tests/surface-probe.c
new file mode 100644
index 0000000..51feb95
--- /dev/null
+++ b/tests/surface-probe.c
@@ -0,0 +1,85 @@
+#define _GNU_SOURCE
+/* A surface for tests/check.py (serve/surface.h): as a person at the node's
+ * display would, it types each line of TEXTS into the text input and then
+ * presses the key input; after each it writes what the views show, and then,
+ * until the server stops, what they show after each save the node made for
+ * others (HTTP requests). One JSON line each into OUTPUT.
+ *
+ * pure-serve ... --surface surface-probe.so -- OUTPUT TEXT_INPUT KEY_INPUT TEXTS VIEW... */
+#include "surface.h"
+#include <poll.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+static int named(const char (*names)[PURE_MODULE_NAME], unsigned count, const char *name) {
+ for (unsigned i = 0; i < count; ++i)
+ if (!strcmp(names[i], name)) return (int)i;
+ return -1;
+}
+
+static bool put_text(void *context, PureSpan span) {
+ fwrite(span.data, 1, span.size, context);
+ return true;
+}
+
+/* {"event":...,"changes":N,"views":{"name":value,...}} as the last save shows them. */
+static void show(const PureSurface *s, PureApp *reader, FILE *out, const char *event, char **views, int count) {
+ PureSnapshot *snapshot = s->read(s->host, reader);
+ fprintf(out, "{\"event\":\"%s\",\"changes\":%llu,\"views\":{", event,
+ snapshot ? (unsigned long long)reader->changes : 0ull);
+ for (int i = 0; i < count; ++i) {
+ int view = named(s->module->view_names, s->module->views, views[i]);
+ fprintf(out, "%s\"%s\":", i ? "," : "", views[i]);
+ double value;
+ if (!snapshot || view < 0) fputs("null", out);
+ else if (s->module->view_type[view] == PURE_VIEW_TEXT) {
+ fputc('"', out);
+ pure_app_view_text(reader, (uint32_t)view, put_text, out);
+ fputc('"', out);
+ } else if (pure_app_view(reader, (uint32_t)view, &value)) fprintf(out, "%g", value);
+ else fputs("null", out);
+ }
+ fputs("}}\n", out);
+ fflush(out);
+ if (snapshot) s->release(s->host, snapshot);
+}
+
+int pure_surface_run(const PureSurface *s, int argc, char **argv) {
+ if (s->version != PURE_SURFACE_VERSION || s->app_size != sizeof(PureApp) || s->module_size != sizeof(PureModule))
+ return 2;
+ if (argc < 5) return 2;
+ FILE *out = fopen(argv[1], "w");
+ int text = named(s->module->input_names, s->module->inputs, argv[2]);
+ int key = named(s->module->input_names, s->module->inputs, argv[3]);
+ if (!out || text < 0 || key < 0) return 2;
+ static PureApp reader; /* zeroed the first time, as read wants */
+ char **views = argv + 5;
+ int view_count = argc - 5;
+ show(s, &reader, out, "start", views, view_count);
+ char texts[256];
+ snprintf(texts, sizeof(texts), "%s", argv[4]);
+ for (char *line = strtok(texts, ","); line; line = strtok(NULL, ",")) {
+ PureResult typed = s->type(s->host, (uint32_t)text, (PureSpan){(const unsigned char *)line, strlen(line)});
+ char event[300];
+ snprintf(event, sizeof(event), "typed %s: %d", line, (int)typed);
+ show(s, &reader, out, event, views, view_count);
+ }
+ char event[64];
+ snprintf(event, sizeof(event), "pressed: %d", (int)s->press(s->host, (uint32_t)key));
+ show(s, &reader, out, event, views, view_count);
+ /* Refusals: an input of another kind, and one past the module's. */
+ snprintf(event, sizeof(event), "wrong kind: %d, no input: %d", (int)s->press(s->host, (uint32_t)text),
+ (int)s->type(s->host, s->module->inputs, (PureSpan){0}));
+ show(s, &reader, out, event, views, view_count);
+ while (!s->stopping(s->host)) {
+ struct pollfd wait = {s->changes, POLLIN, 0};
+ if (poll(&wait, 1, 1000) <= 0) continue;
+ uint64_t times;
+ if (read(s->changes, &times, sizeof(times)) != sizeof(times) || s->stopping(s->host)) continue;
+ show(s, &reader, out, "changed", views, view_count);
+ }
+ pure_app_stop(&reader);
+ fclose(out);
+ return 0;
+}