drivers/freshness: hardened + 100% MC/DC (Tier-1, the UI stale-number guard)
freshness.c (ADR-0004 freshness-contract engine) is pure logic via produce/render callbacks — no hardware seam needed, the callbacks are the seam. Host harness reaches 100% MC/DC (66/66 conditions, 100% lines, 27 checks) by driving warden_fresh_decide directly + the bind/tick/invalidate/min-budget state machine through fakes, with -DFRESH_MAX=2 so the table-full and unused/hidden-slot arms are reachable. Directly serves future-features-2's "never a stale number in the UI" requirement. The CI mcdc job now enforces relays + freshness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e07db1d87e
commit
35829eaccb
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* The UI Freshness Contract engine (ADR 0004) — core, LVGL-free.
|
||||
* See freshness.h for the contract. LVGL binding lives in freshness_lv.c.
|
||||
*/
|
||||
#include "freshness.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* The unknown mark the engine renders when a source cannot be evaluated (from
|
||||
* freshness.h so the LVGL layer and tests share the exact literal). */
|
||||
#define FRESH_UNKNOWN_MARK WARDEN_FRESH_UNKNOWN_MARK
|
||||
|
||||
/* Max simultaneous live bindings. Bindings belong to visible pages; the whole
|
||||
* navigable set of a screen is small, so this is generous. A full table drops
|
||||
* the binding (returns NULL) rather than silently overflowing — the LVGL layer
|
||||
* turns that into a visible fault, never a stale value. */
|
||||
#ifndef FRESH_MAX
|
||||
#define FRESH_MAX 96
|
||||
#endif
|
||||
|
||||
#define FRESH_BUFSZ 64
|
||||
|
||||
struct warden_fresh {
|
||||
void * page;
|
||||
warden_fresh_produce_cb produce;
|
||||
void * ud;
|
||||
uint32_t max_stale_ms;
|
||||
const char * source;
|
||||
warden_fresh_render_cb render;
|
||||
void * widget;
|
||||
uint32_t last_ok_ms; /* time of last OK/SAME produce */
|
||||
char last[FRESH_BUFSZ]; /* last good value string */
|
||||
bool ever_ok;
|
||||
bool showing_unknown;
|
||||
bool visible;
|
||||
bool used;
|
||||
};
|
||||
|
||||
/* A fixed table scanned in full: bindings are torn down all at once by
|
||||
* warden_fresh_reset (like the screen timers), never individually, so a running
|
||||
* high-water bound would only hide the free-slot arms from tests without saving
|
||||
* real work — the visible set per screen is a handful. */
|
||||
static struct warden_fresh s_vals[FRESH_MAX];
|
||||
|
||||
warden_fresh_render_t warden_fresh_decide(warden_fresh_result_t produced,
|
||||
bool ever_ok, bool showing_unknown,
|
||||
uint32_t age_ms, uint32_t max_stale_ms)
|
||||
{
|
||||
switch(produced) {
|
||||
case FRESH_OK:
|
||||
return FRESH_RENDER_VALUE;
|
||||
case FRESH_SAME:
|
||||
/* Unchanged and fresh: normally nothing to do. But if the widget is
|
||||
* currently showing UNKNOWN (it went stale), an unchanged value
|
||||
* still has to be repainted to clear the mark. */
|
||||
return showing_unknown ? FRESH_RENDER_VALUE : FRESH_RENDER_NOCHANGE;
|
||||
case FRESH_UNKNOWN:
|
||||
default:
|
||||
/* Never had a value, or the last good value is now older than its
|
||||
* budget: stop asserting a confident number. Otherwise tolerate a
|
||||
* brief blip and hold the last value until the budget expires. */
|
||||
if(!ever_ok) return FRESH_RENDER_UNKNOWN;
|
||||
if(age_ms > max_stale_ms) return FRESH_RENDER_UNKNOWN;
|
||||
return FRESH_RENDER_NOCHANGE;
|
||||
}
|
||||
}
|
||||
|
||||
static void refresh_one(struct warden_fresh *v, uint32_t now)
|
||||
{
|
||||
char buf[FRESH_BUFSZ];
|
||||
buf[0] = '\0';
|
||||
warden_fresh_result_t r = v->produce(buf, sizeof buf, v->ud);
|
||||
|
||||
warden_fresh_render_t what = warden_fresh_decide(
|
||||
r, v->ever_ok, v->showing_unknown, now - v->last_ok_ms, v->max_stale_ms);
|
||||
|
||||
if(r == FRESH_OK) {
|
||||
/* Keep the last good string so a later SAME-recovery can repaint it.
|
||||
* snprintf truncates and null-terminates; buf and last are both
|
||||
* FRESH_BUFSZ, so this cannot overflow. */
|
||||
snprintf(v->last, sizeof v->last, "%s", buf);
|
||||
}
|
||||
if(r == FRESH_OK || r == FRESH_SAME) {
|
||||
v->last_ok_ms = now;
|
||||
v->ever_ok = true;
|
||||
}
|
||||
|
||||
switch(what) {
|
||||
case FRESH_RENDER_VALUE:
|
||||
v->render(v->widget, FRESH_RENDER_VALUE,
|
||||
(r == FRESH_OK) ? buf : v->last, v->ud);
|
||||
v->showing_unknown = false;
|
||||
break;
|
||||
case FRESH_RENDER_UNKNOWN:
|
||||
v->render(v->widget, FRESH_RENDER_UNKNOWN, FRESH_UNKNOWN_MARK, v->ud);
|
||||
v->showing_unknown = true;
|
||||
break;
|
||||
case FRESH_RENDER_NOCHANGE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
warden_fresh_t *warden_fresh_bind(void *page, warden_fresh_produce_cb produce,
|
||||
void *ud, uint32_t max_stale_ms,
|
||||
const char *source,
|
||||
warden_fresh_render_cb render, void *widget)
|
||||
{
|
||||
if(!produce || !render) return NULL;
|
||||
for(uint32_t i = 0; i < FRESH_MAX; i++) {
|
||||
if(s_vals[i].used) continue;
|
||||
struct warden_fresh *v = &s_vals[i];
|
||||
memset(v, 0, sizeof *v);
|
||||
v->page = page;
|
||||
v->produce = produce;
|
||||
v->ud = ud;
|
||||
v->max_stale_ms = max_stale_ms;
|
||||
v->source = source;
|
||||
v->render = render;
|
||||
v->widget = widget;
|
||||
v->visible = true; /* bound while building the page that's about to show */
|
||||
v->used = true;
|
||||
return v;
|
||||
}
|
||||
return NULL; /* table full — caller surfaces a fault, never a stale value */
|
||||
}
|
||||
|
||||
void warden_fresh_set_visible(void *page, bool visible)
|
||||
{
|
||||
for(uint32_t i = 0; i < FRESH_MAX; i++) {
|
||||
if(s_vals[i].used && s_vals[i].page == page) s_vals[i].visible = visible;
|
||||
}
|
||||
}
|
||||
|
||||
void warden_fresh_page_show(void *page, uint32_t now_ms)
|
||||
{
|
||||
for(uint32_t i = 0; i < FRESH_MAX; i++) {
|
||||
struct warden_fresh *v = &s_vals[i];
|
||||
if(v->used && v->page == page) {
|
||||
v->visible = true;
|
||||
refresh_one(v, now_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void warden_fresh_tick(uint32_t now_ms)
|
||||
{
|
||||
for(uint32_t i = 0; i < FRESH_MAX; i++) {
|
||||
if(s_vals[i].used && s_vals[i].visible) refresh_one(&s_vals[i], now_ms);
|
||||
}
|
||||
}
|
||||
|
||||
void warden_fresh_invalidate(const char *source, uint32_t now_ms)
|
||||
{
|
||||
if(!source) return;
|
||||
for(uint32_t i = 0; i < FRESH_MAX; i++) {
|
||||
struct warden_fresh *v = &s_vals[i];
|
||||
if(v->used && v->visible && v->source && strcmp(v->source, source) == 0) {
|
||||
refresh_one(v, now_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void warden_fresh_reset(void)
|
||||
{
|
||||
memset(s_vals, 0, sizeof s_vals);
|
||||
}
|
||||
|
||||
uint32_t warden_fresh_count(void)
|
||||
{
|
||||
uint32_t n = 0;
|
||||
for(uint32_t i = 0; i < FRESH_MAX; i++) if(s_vals[i].used) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
uint32_t warden_fresh_min_budget_ms(void)
|
||||
{
|
||||
uint32_t best = 0;
|
||||
for(uint32_t i = 0; i < FRESH_MAX; i++) {
|
||||
struct warden_fresh *v = &s_vals[i];
|
||||
if(v->used && v->visible && (best == 0 || v->max_stale_ms < best)) {
|
||||
best = v->max_stale_ms;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* The UI Freshness Contract engine (ADR 0004) — core, LVGL-free.
|
||||
*
|
||||
* The panel is read on-site to judge whether hardware is healthy, so a silently
|
||||
* *stale* number is worse than a missing one: a stale IP or hashrate reads as
|
||||
* ground truth and sends a technician the wrong way. This engine is the only
|
||||
* sanctioned way to show a live value. It guarantees a bound value is refreshed
|
||||
* (a) the instant its page becomes visible, (b) periodically while visible
|
||||
* within a declared max-staleness, and (c) promptly when a declared source
|
||||
* changes — and it renders a value whose source cannot be evaluated as an
|
||||
* explicit UNKNOWN, never as its confident last-known number.
|
||||
*
|
||||
* This header is deliberately LVGL-free so the engine and every producer are
|
||||
* unit-testable headlessly. The LVGL label convenience lives in freshness_lv.h.
|
||||
*/
|
||||
#ifndef WARDEN_FRESHNESS_H
|
||||
#define WARDEN_FRESHNESS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* The mark shown when a value's source cannot be evaluated (em dash). The LVGL
|
||||
* layer additionally dims the widget. Public so the label wrapper and tests can
|
||||
* reference the same literal. */
|
||||
#define WARDEN_FRESH_UNKNOWN_MARK "\xE2\x80\x94"
|
||||
|
||||
/* What a producer reports after being asked to produce the current value. */
|
||||
typedef enum {
|
||||
FRESH_OK = 0, /* wrote the current value into buf */
|
||||
FRESH_UNKNOWN, /* source unavailable — no value can be produced now */
|
||||
FRESH_SAME, /* source read fine; value unchanged (cheap re-render) */
|
||||
} warden_fresh_result_t;
|
||||
|
||||
/* Produce the current value of a live datum into buf. Pure w.r.t. LVGL: a plain
|
||||
* function of the model behind `ud`, which is exactly why it is the test seam. */
|
||||
typedef warden_fresh_result_t (*warden_fresh_produce_cb)(char *buf, size_t n,
|
||||
void *ud);
|
||||
|
||||
/* What the engine decided the widget should show this cycle. */
|
||||
typedef enum {
|
||||
FRESH_RENDER_VALUE = 0, /* show the produced/last-good value */
|
||||
FRESH_RENDER_UNKNOWN, /* show the explicit-unknown mark ("—", dimmed) */
|
||||
FRESH_RENDER_NOCHANGE, /* leave the widget exactly as it is */
|
||||
} warden_fresh_render_t;
|
||||
|
||||
/* The one pure decision at the heart of the contract — no state, no I/O, no
|
||||
* LVGL, so every branch is unit-testable. `showing_unknown` is whether the
|
||||
* widget is currently displaying the UNKNOWN mark (so recovery from a stale
|
||||
* blip re-renders the value even when the producer reports it unchanged). */
|
||||
warden_fresh_render_t warden_fresh_decide(warden_fresh_result_t produced,
|
||||
bool ever_ok, bool showing_unknown,
|
||||
uint32_t age_ms, uint32_t max_stale_ms);
|
||||
|
||||
/* The widget-render seam: the engine calls this to actually update a widget.
|
||||
* `buf` is valid only for FRESH_RENDER_VALUE. LVGL lives behind this callback. */
|
||||
typedef void (*warden_fresh_render_cb)(void *widget, warden_fresh_render_t what,
|
||||
const char *buf, void *ud);
|
||||
|
||||
typedef struct warden_fresh warden_fresh_t;
|
||||
|
||||
/* Bind a producer+widget to a page. `source` (may be NULL) is a named change
|
||||
* channel for warden_fresh_invalidate. Returns NULL if the table is full. */
|
||||
warden_fresh_t *warden_fresh_bind(void *page, warden_fresh_produce_cb produce,
|
||||
void *ud, uint32_t max_stale_ms,
|
||||
const char *source,
|
||||
warden_fresh_render_cb render, void *widget);
|
||||
|
||||
/* A page became visible/hidden (tiles via notify_active, subnav leaves via
|
||||
* on_open). Hidden pages are skipped by the periodic tick. */
|
||||
void warden_fresh_set_visible(void *page, bool visible);
|
||||
|
||||
/* Refresh every bound value on `page` right now (the on-show guarantee). Also
|
||||
* marks the page visible. */
|
||||
void warden_fresh_page_show(void *page, uint32_t now_ms);
|
||||
|
||||
/* The shared periodic tick: refresh every currently-visible bound value. */
|
||||
void warden_fresh_tick(uint32_t now_ms);
|
||||
|
||||
/* A producer of change fired: refresh every visible value bound to `source`. */
|
||||
void warden_fresh_invalidate(const char *source, uint32_t now_ms);
|
||||
|
||||
/* Drop all bindings — called on a theme/screen rebuild, like the screen timers. */
|
||||
void warden_fresh_reset(void);
|
||||
|
||||
/* Number of live bindings (introspection / tests). */
|
||||
uint32_t warden_fresh_count(void);
|
||||
|
||||
/* Smallest max_stale_ms among visible bindings, or 0 if none — lets the LVGL
|
||||
* layer size the shared tick to the tightest budget actually on screen. */
|
||||
uint32_t warden_fresh_min_budget_ms(void);
|
||||
|
||||
#endif /* WARDEN_FRESHNESS_H */
|
||||
@@ -0,0 +1,40 @@
|
||||
# MC/DC unit harness for drivers/freshness/freshness.c (the UI stale-number guard).
|
||||
#
|
||||
# make check — build, run, FAIL unless freshness.c hits 100% MC/DC + all checks.
|
||||
# make report — per-condition gcov annotation.
|
||||
# make clean
|
||||
#
|
||||
# FRESH_MAX is forced to 2 so the "binding table full -> NULL" path is reachable
|
||||
# with two binds (the production default is 96). Requires gcc >= 14.
|
||||
CC ?= gcc
|
||||
GCOV ?= gcov
|
||||
CFLAGS := -O0 -g -Wall -Wextra -I.. -DFRESH_MAX=2
|
||||
COVFLAGS := --coverage -fcondition-coverage
|
||||
BUILD := build
|
||||
|
||||
.PHONY: check report clean
|
||||
.DEFAULT_GOAL := check
|
||||
|
||||
$(BUILD):
|
||||
@mkdir -p $(BUILD)
|
||||
|
||||
$(BUILD)/test: test_freshness.c ../freshness.c ../freshness.h | $(BUILD)
|
||||
@$(CC) $(CFLAGS) $(COVFLAGS) -c ../freshness.c -o $(BUILD)/freshness.o
|
||||
@$(CC) $(CFLAGS) -c test_freshness.c -o $(BUILD)/test_freshness.o
|
||||
@$(CC) $(COVFLAGS) $(BUILD)/freshness.o $(BUILD)/test_freshness.o -o $(BUILD)/test
|
||||
|
||||
check: $(BUILD)/test
|
||||
@echo "== running freshness MC/DC harness =="
|
||||
@rm -f $(BUILD)/freshness.gcda
|
||||
@$(BUILD)/test; echo $$? > $(BUILD)/test.rc
|
||||
@echo
|
||||
@echo "== MC/DC (condition) coverage of freshness.c =="
|
||||
@$(GCOV) --conditions --branch-probabilities -o $(BUILD) ../freshness.c >$(BUILD)/gcov.log 2>&1 || true
|
||||
@mv -f *.gcov $(BUILD)/ 2>/dev/null || true
|
||||
@bash enforce-mcdc.sh $(BUILD)/gcov.log $(BUILD)/freshness.c.gcov $(BUILD)/test.rc
|
||||
|
||||
report: check
|
||||
@grep -nE "condition.*not covered|conditions covered" $(BUILD)/freshness.c.gcov || true
|
||||
|
||||
clean:
|
||||
@rm -rf $(BUILD)
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Gate: fail unless every unit check passed AND freshness.c reached 100% MC/DC
|
||||
# (condition) coverage with no uncovered executable lines.
|
||||
# $1 = gcov stdout log $2 = freshness.c.gcov $3 = test exit-code file
|
||||
set -uo pipefail
|
||||
LOG="$1"; GCOV="$2"; RCFILE="$3"
|
||||
rc=0
|
||||
|
||||
testrc="$(cat "$RCFILE" 2>/dev/null || echo 1)"
|
||||
if [ "$testrc" != "0" ]; then
|
||||
echo "RESULT: unit checks FAILED (test exit $testrc)"; rc=1
|
||||
else
|
||||
echo "RESULT: all unit checks passed"
|
||||
fi
|
||||
|
||||
if [ ! -f "$GCOV" ]; then
|
||||
echo "RESULT: no coverage file ($GCOV) produced"; exit 1
|
||||
fi
|
||||
|
||||
notcov="$(grep -nE "condition[s]? .*not covered" "$GCOV" || true)"
|
||||
uncov_lines="$(grep -nE "^ +#####:" "$GCOV" || true)"
|
||||
|
||||
# Summary lines for freshness.c from gcov stdout. Match the exact file so
|
||||
# 'test_freshness.c' (which also contains "freshness.c") is NOT picked up.
|
||||
FMATCH="File '([^']*/)?freshness[.]c'"
|
||||
cond_line="$(awk -v patt="$FMATCH" '$0 ~ patt {f=1} f&&/Condition outcomes covered:/{print; f=0}' "$LOG")"
|
||||
line_line="$(awk -v patt="$FMATCH" '$0 ~ patt {f=1} f&&/Lines executed:/{print; f=0}' "$LOG")"
|
||||
echo " ${line_line:-Lines executed: (n/a)}"
|
||||
echo " ${cond_line:-Condition outcomes covered: (n/a)}"
|
||||
|
||||
if [ -n "$notcov" ]; then
|
||||
echo "RESULT: MC/DC gaps (conditions not covered):"
|
||||
echo "$notcov" | sed 's/^/ /'
|
||||
rc=1
|
||||
fi
|
||||
if [ -n "$uncov_lines" ]; then
|
||||
echo "RESULT: uncovered executable lines in freshness.c:"
|
||||
echo "$uncov_lines" | sed 's/^/ /'
|
||||
rc=1
|
||||
fi
|
||||
|
||||
if ! echo "$cond_line" | grep -q "100.00%"; then
|
||||
echo "RESULT: condition coverage is below 100%"
|
||||
rc=1
|
||||
fi
|
||||
|
||||
[ "$rc" = "0" ] && echo "RESULT: 100% MC/DC + all checks green ✓"
|
||||
exit "$rc"
|
||||
@@ -0,0 +1,173 @@
|
||||
/* MC/DC harness for drivers/freshness/freshness.c (built with -DFRESH_MAX=2).
|
||||
*
|
||||
* freshness.c is pure logic with produce/render callbacks — no hardware seam
|
||||
* needed, the callbacks ARE the seam. We drive the decision function directly and
|
||||
* the bind/tick/invalidate state machine through programmable fakes, covering
|
||||
* every decision (incl. the compound `used && visible`, `!produce || !render`,
|
||||
* `used && visible && source && strcmp==0`, `best==0 || max_stale<best`).
|
||||
*/
|
||||
#include "../freshness.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_fail = 0, g_checks = 0;
|
||||
#define EXPECT(c) do { g_checks++; if(!(c)) { g_fail++; \
|
||||
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #c); } } while(0)
|
||||
|
||||
/* --- programmable produce/render fakes --- */
|
||||
static warden_fresh_result_t g_prod_ret;
|
||||
static const char *g_prod_val;
|
||||
static warden_fresh_result_t fake_produce(char *buf, size_t n, void *ud) {
|
||||
(void)ud;
|
||||
if(g_prod_val) snprintf(buf, n, "%s", g_prod_val);
|
||||
return g_prod_ret;
|
||||
}
|
||||
static int g_render_calls;
|
||||
static warden_fresh_render_t g_render_what;
|
||||
static char g_render_buf[64];
|
||||
static void fake_render(void *widget, warden_fresh_render_t what, const char *buf, void *ud) {
|
||||
(void)widget; (void)ud;
|
||||
g_render_calls++; g_render_what = what;
|
||||
snprintf(g_render_buf, sizeof g_render_buf, "%s", buf ? buf : "");
|
||||
}
|
||||
static void set_produce(warden_fresh_result_t r, const char *v) { g_prod_ret = r; g_prod_val = v; }
|
||||
static void reset_render(void) { g_render_calls = 0; g_render_what = FRESH_RENDER_NOCHANGE; g_render_buf[0] = 0; }
|
||||
|
||||
/* --- 1. the pure decision function: every switch arm + inner condition --- */
|
||||
static void test_decide(void) {
|
||||
EXPECT(warden_fresh_decide(FRESH_OK, false, false, 0, 100) == FRESH_RENDER_VALUE);
|
||||
/* SAME: showing_unknown both ways */
|
||||
EXPECT(warden_fresh_decide(FRESH_SAME, true, true, 0, 100) == FRESH_RENDER_VALUE);
|
||||
EXPECT(warden_fresh_decide(FRESH_SAME, true, false, 0, 100) == FRESH_RENDER_NOCHANGE);
|
||||
/* UNKNOWN: !ever_ok true; then age>max true; then age<=max */
|
||||
EXPECT(warden_fresh_decide(FRESH_UNKNOWN, false, false, 0, 100) == FRESH_RENDER_UNKNOWN);
|
||||
EXPECT(warden_fresh_decide(FRESH_UNKNOWN, true, false, 200, 100) == FRESH_RENDER_UNKNOWN);
|
||||
EXPECT(warden_fresh_decide(FRESH_UNKNOWN, true, false, 50, 100) == FRESH_RENDER_NOCHANGE);
|
||||
}
|
||||
|
||||
/* --- 2. bind: !produce, !render, valid, and table-full (FRESH_MAX=2) --- */
|
||||
static void test_bind(void) {
|
||||
warden_fresh_reset();
|
||||
EXPECT(warden_fresh_bind((void*)1, NULL, NULL, 100, "s", fake_render, (void*)9) == NULL); /* !produce */
|
||||
EXPECT(warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", NULL, (void*)9) == NULL); /* !render */
|
||||
warden_fresh_t *a = warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", fake_render, (void*)9);
|
||||
warden_fresh_t *b = warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", fake_render, (void*)9);
|
||||
EXPECT(a && b); /* both slots taken */
|
||||
EXPECT(warden_fresh_count() == 2);
|
||||
EXPECT(warden_fresh_bind((void*)1, fake_produce, NULL, 100, "t", fake_render, (void*)9) == NULL); /* full */
|
||||
}
|
||||
|
||||
/* --- 3. refresh_one via tick: OK / SAME(recover) / UNKNOWN-hold / UNKNOWN-stale --- */
|
||||
static void test_refresh_paths(void) {
|
||||
warden_fresh_reset();
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", fake_render, (void*)9);
|
||||
|
||||
/* OK at t=0 -> render VALUE(buf), last saved, ever_ok set */
|
||||
set_produce(FRESH_OK, "42"); reset_render();
|
||||
warden_fresh_tick(0);
|
||||
EXPECT(g_render_calls == 1 && g_render_what == FRESH_RENDER_VALUE && strcmp(g_render_buf, "42") == 0);
|
||||
|
||||
/* UNKNOWN, ever_ok, age<=max -> NOCHANGE (r==OK||SAME both false; what NOCHANGE) */
|
||||
set_produce(FRESH_UNKNOWN, NULL); reset_render();
|
||||
warden_fresh_tick(50);
|
||||
EXPECT(g_render_calls == 0);
|
||||
|
||||
/* UNKNOWN, age>max -> UNKNOWN render, showing_unknown=true */
|
||||
reset_render();
|
||||
warden_fresh_tick(500);
|
||||
EXPECT(g_render_calls == 1 && g_render_what == FRESH_RENDER_UNKNOWN);
|
||||
|
||||
/* SAME while showing_unknown -> VALUE render of v->last (r!=OK ternary false-arm) */
|
||||
set_produce(FRESH_SAME, NULL); reset_render();
|
||||
warden_fresh_tick(520);
|
||||
EXPECT(g_render_calls == 1 && g_render_what == FRESH_RENDER_VALUE && strcmp(g_render_buf, "42") == 0);
|
||||
}
|
||||
|
||||
/* --- 4. set_visible / page_show / tick(used&&visible) / count(used) --- */
|
||||
static void test_visibility(void) {
|
||||
warden_fresh_reset();
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", fake_render, (void*)9); /* page 1 */
|
||||
warden_fresh_bind((void*)2, fake_produce, NULL, 100, "s", fake_render, (void*)9); /* page 2 */
|
||||
|
||||
/* set_visible: page match vs no-match (used true both; page==page T/F) */
|
||||
warden_fresh_set_visible((void*)1, false); /* page 1 hidden */
|
||||
set_produce(FRESH_OK, "7"); reset_render();
|
||||
warden_fresh_tick(0); /* only page-2 (visible) refreshes */
|
||||
EXPECT(g_render_calls == 1);
|
||||
|
||||
/* page_show forces visible + refreshes just that page */
|
||||
reset_render();
|
||||
warden_fresh_page_show((void*)1, 1);
|
||||
EXPECT(g_render_calls == 1);
|
||||
|
||||
/* count sees used slots; an unused slot exercises the `used` false arm in the
|
||||
* count/tick loops */
|
||||
EXPECT(warden_fresh_count() == 2);
|
||||
}
|
||||
|
||||
/* --- 5. invalidate: !source; source match/no-match; and a NULL-source binding --- */
|
||||
static void test_invalidate(void) {
|
||||
warden_fresh_reset();
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 100, "alpha", fake_render, (void*)9);
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 100, NULL, fake_render, (void*)9); /* source NULL */
|
||||
|
||||
warden_fresh_invalidate(NULL, 0); /* !source -> early return */
|
||||
set_produce(FRESH_OK, "1"); reset_render();
|
||||
warden_fresh_invalidate("beta", 0); /* no source matches -> no refresh */
|
||||
EXPECT(g_render_calls == 0);
|
||||
reset_render();
|
||||
warden_fresh_invalidate("alpha", 0); /* matches the first binding only */
|
||||
EXPECT(g_render_calls == 1);
|
||||
}
|
||||
|
||||
/* --- 6. min_budget: best==0 first, then max_stale<best true and false --- */
|
||||
static void test_min_budget(void) {
|
||||
warden_fresh_reset();
|
||||
EXPECT(warden_fresh_min_budget_ms() == 0); /* nothing bound */
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 300, "s", fake_render, (void*)9); /* best=0->300 */
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", fake_render, (void*)9); /* 100<300 -> 100 */
|
||||
EXPECT(warden_fresh_min_budget_ms() == 100);
|
||||
/* a third can't bind (full at 2); rebind fresh with the larger-first order so the
|
||||
* `max_stale < best` FALSE arm (200 !< 100) is taken */
|
||||
warden_fresh_reset();
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 100, "s", fake_render, (void*)9);
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 200, "s", fake_render, (void*)9); /* 200<100 false */
|
||||
EXPECT(warden_fresh_min_budget_ms() == 100);
|
||||
/* a hidden binding exercises min_budget's `visible` false arm */
|
||||
warden_fresh_set_visible((void*)1, false);
|
||||
EXPECT(warden_fresh_min_budget_ms() == 0);
|
||||
}
|
||||
|
||||
/* --- 7. the `used`/`visible` FALSE arms of the scan loops: bind ONE (leaving a
|
||||
* slot unused) and hide it, so set_visible/page_show/invalidate/count each
|
||||
* see an unused and an invisible slot. --- */
|
||||
static void test_false_arms(void) {
|
||||
warden_fresh_reset();
|
||||
warden_fresh_bind((void*)1, fake_produce, NULL, 100, "x", fake_render, (void*)9); /* slot0 used; slot1 unused */
|
||||
|
||||
EXPECT(warden_fresh_count() == 1); /* count: slot1 used==false */
|
||||
warden_fresh_set_visible((void*)2, false); /* set_visible: slot0 page-mismatch, slot1 used==false */
|
||||
warden_fresh_page_show((void*)2, 0); /* page_show: slot1 used==false */
|
||||
|
||||
set_produce(FRESH_OK, "1"); reset_render();
|
||||
warden_fresh_invalidate("x", 0); /* invalidate: slot0 matches, slot1 used==false */
|
||||
EXPECT(g_render_calls == 1);
|
||||
|
||||
warden_fresh_set_visible((void*)1, false); /* hide the used binding */
|
||||
reset_render();
|
||||
warden_fresh_invalidate("x", 0); /* invalidate: slot0 used but visible==false */
|
||||
EXPECT(g_render_calls == 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_decide();
|
||||
test_bind();
|
||||
test_refresh_paths();
|
||||
test_visibility();
|
||||
test_invalidate();
|
||||
test_min_budget();
|
||||
test_false_arms();
|
||||
fprintf(stderr, "%d checks, %d failures\n", g_checks, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user