aboutsummaryrefslogtreecommitdiffstats
path: root/tests
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
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')
-rw-r--r--tests/check.py87
-rw-r--r--tests/surface-probe.c85
2 files changed, 172 insertions, 0 deletions
diff --git a/tests/check.py b/tests/check.py
index 76a23e3..9ff771a 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -937,6 +937,92 @@ def server_sealed(aup, pure, work):
relay.shutdown()
+SURFACE_SOURCE = """record Task {
+ title: text
+ done = 0
+}
+state tasks: list of Task
+state at: index of tasks
+input typed = text
+input toggle = key Space
+on typed {
+ tasks.append(Task { title: typed })
+ at = tasks.last
+}
+on toggle.down { at.done = !at.done }
+view count = tasks.count
+view current = if tasks.count > 0 { at.title } else { "(empty)" }
+view finished = at.done
+"""
+
+
+def server_surface(aup, pure, work):
+ """A surface in the node (--surface, serve/surface.h): a library that
+ pure-serve maps and runs beside its connection threads types text and
+ presses a key as a person at the node's display would. The owner applies
+ its events with the requests, one save a round, and it reads each save's
+ views where they are: its events show in HTTP views, and an HTTP input
+ shows in it through the changes descriptor. An event for an input of
+ another kind, or for none, is refused; it stops with the server; and a
+ module without states cannot have one."""
+ import select
+ import signal
+ import time
+ probe = work / 'surface-probe.so'
+ subprocess.run(['cc', '-O2', '-std=c17', '-Wall', '-Wextra', '-Werror', '-shared', '-fPIC', f'-I{pure / "core"}',
+ f'-I{pure / "serve"}', str(pure / 'tests' / 'surface-probe.c'), '-o', str(probe)], check=True)
+ source, module = work / 'surface.aup', work / 'surface.pmod'
+ source.write_text(SURFACE_SOURCE)
+ compile_module(aup, source, module)
+ out = work / 'surface.jsonl'
+ process = subprocess.Popen([str(a) for a in (pure / 'build/pure-serve', '--module', module, '--store',
+ work / 'surface-store', '--port', '0', '--surface', probe, '--', out,
+ 'typed', 'toggle', 'one,two,three', 'count', 'current', 'finished')],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ def lines():
+ return [json.loads(line) for line in out.read_text().splitlines()] if out.exists() else []
+
+ def wait_for(condition):
+ deadline = time.time() + 20
+ while not condition() and time.time() < deadline:
+ time.sleep(0.05)
+ return condition()
+
+ try:
+ ready, _, _ = select.select([process.stdout], [], [], 10)
+ assert ready, 'host did not start'
+ port = json.loads(process.stdout.readline())['port']
+ assert wait_for(lambda: any(line['event'].startswith('wrong kind') for line in lines())), lines()
+ assert http(port, 'GET', '/views/count')[3] == b'3' and http(port, 'GET', '/views/current')[3] == b'three'
+ assert http(port, 'POST', '/inputs/typed', b'four')[0] == 200
+ assert wait_for(lambda: any(line['event'] == 'changed' and line['views']['count'] == 4 for line in lines()))
+ finally:
+ process.send_signal(signal.SIGTERM)
+ _, error = process.communicate(timeout=30)
+ assert process.returncode == 0 and b'"returned":0' in error, error[-500:]
+ got = lines()
+ views = [line['views'] for line in got]
+ assert [line['event'] for line in got[:6]] == ['start', 'typed one: 0', 'typed two: 0', 'typed three: 0', 'pressed: 0',
+ 'wrong kind: 8, no input: 8'], got[:6]
+ assert views[:6] == [{'count': 0, 'current': '(empty)', 'finished': None},
+ {'count': 1, 'current': 'one', 'finished': 0}, {'count': 2, 'current': 'two', 'finished': 0},
+ {'count': 3, 'current': 'three', 'finished': 0}, {'count': 3, 'current': 'three', 'finished': 1},
+ {'count': 3, 'current': 'three', 'finished': 1}], views[:6]
+ assert views[-1] == {'count': 4, 'current': 'four', 'finished': 0}, views[-1]
+ changes = [line['changes'] for line in got]
+ assert changes[1:5] == sorted(set(changes[1:5])) and changes[-1] > changes[4], changes
+ # A module without states has no owner to apply a surface's events.
+ stateless = work / 'surface-echo.aup'
+ stateless.write_text('input shout = text\non shout { }\n')
+ compile_module(aup, stateless, stateless.with_suffix('.pmod'))
+ refused = subprocess.run([str(a) for a in (pure / 'build/pure-serve', '--module', stateless.with_suffix('.pmod'),
+ '--store', work / 'surface-echo', '--port', '0', '--surface', probe, '--',
+ out, 'shout', 'shout', 'x')], capture_output=True, timeout=30)
+ assert refused.returncode == 2 and b'needs an app with states' in refused.stderr, refused.stderr
+ return {'events': 5, 'lines': len(got)}
+
+
def server_webhook(aup, pure, work):
"""An output wired to a third-party HTTPS endpoint (a Python server with
a certificate of its own): five orders arrive in order as JSON with the
@@ -1396,6 +1482,7 @@ def main():
report['server_links'] = server_links(aup, pure, work)
report['server_webhook'] = server_webhook(aup, pure, work)
report['server_sealed'] = server_sealed(aup, pure, work)
+ report['server_surface'] = server_surface(aup, pure, work)
report['server_answers'] = server_answers(aup, pure, work)
print(json.dumps(report))
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;
+}