drivers/relays: hardened + 100% MC/DC (Tier-1, first driver)
Bring relays.c into warden-sdk/drivers/ behind a relay_io seam (ADR-0005): the sysfs plumbing is an injectable backend, the gpio root is $WARDEN_GPIO_ROOT- overridable. Host harness (drivers/relays/test/) reaches 100% MC/DC (40/40 conditions, 100% lines) by combining fake-io unit tests (logic branches incl. export->node-appears) with real-sysfs integration tests over a scratch tree (backend fopen/stat branches). CI `mcdc` job enforces 100% across drivers/*/test. 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
d4acc9f3b3
commit
1752dea7b6
@@ -49,6 +49,25 @@ jobs:
|
|||||||
echo "coverage=$pct" >> "$GITHUB_OUTPUT"
|
echo "coverage=$pct" >> "$GITHUB_OUTPUT"
|
||||||
echo "tests passed: $passed | sim line coverage: ${pct}%"
|
echo "tests passed: $passed | sim line coverage: ${pct}%"
|
||||||
|
|
||||||
|
mcdc:
|
||||||
|
# 100% MC/DC (condition coverage) enforced on every Tier-1 driver harness.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: install gcc-14
|
||||||
|
run: sudo apt-get update -qq && sudo apt-get install -y -qq gcc-14
|
||||||
|
- name: enforce 100% MC/DC on drivers/*/test
|
||||||
|
run: |
|
||||||
|
fail=0; ran=0
|
||||||
|
for t in drivers/*/test; do
|
||||||
|
[ -f "$t/Makefile" ] || continue
|
||||||
|
ran=1
|
||||||
|
echo "== MC/DC: $t =="
|
||||||
|
make -C "$t" check CC=gcc-14 GCOV=gcov-14 || fail=1
|
||||||
|
done
|
||||||
|
[ "$ran" = 1 ] || { echo "no driver MC/DC harnesses found"; exit 1; }
|
||||||
|
exit $fail
|
||||||
|
|
||||||
badges:
|
badges:
|
||||||
needs: [test]
|
needs: [test]
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/bringup'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/bringup'
|
||||||
|
|||||||
@@ -11,3 +11,6 @@ target/
|
|||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# driver MC/DC harness build dirs
|
||||||
|
**/test/build/
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include "relays.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
/* GPIO1_A0 and GPIO1_A1; gpiochip1 base is 32. See relays.h. */
|
||||||
|
static const int s_gpio[WARDEN_RELAY_COUNT] = { 32, 33 };
|
||||||
|
static const char * const s_name[WARDEN_RELAY_COUNT] = { "Relay 1", "Relay 2" };
|
||||||
|
|
||||||
|
/* ---- default sysfs backend (Tier-2 plumbing; covered by integration tests) ---- */
|
||||||
|
|
||||||
|
static const char * gpio_root(void)
|
||||||
|
{
|
||||||
|
const char * r = getenv("WARDEN_GPIO_ROOT");
|
||||||
|
return (r && *r) ? r : "/sys/class/gpio";
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sysfs_exists(const char * path)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
return stat(path, &st) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sysfs_write(const char * path, const char * value)
|
||||||
|
{
|
||||||
|
FILE * f = fopen(path, "w");
|
||||||
|
if(!f) return;
|
||||||
|
fputs(value, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sysfs_read_line(const char * path, char * buf, size_t n)
|
||||||
|
{
|
||||||
|
FILE * f = fopen(path, "r");
|
||||||
|
if(!f) return false;
|
||||||
|
bool ok = fgets(buf, (int)n, f) != NULL;
|
||||||
|
fclose(f);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sysfs_read_int(const char * path, int * out)
|
||||||
|
{
|
||||||
|
FILE * f = fopen(path, "r");
|
||||||
|
if(!f) return false;
|
||||||
|
bool ok = fscanf(f, "%d", out) == 1;
|
||||||
|
fclose(f);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct relay_io s_sysfs_io = {
|
||||||
|
sysfs_exists, sysfs_write, sysfs_read_line, sysfs_read_int
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct relay_io * s_io = &s_sysfs_io;
|
||||||
|
|
||||||
|
void warden_relay__set_io(const struct relay_io * io) { s_io = io ? io : &s_sysfs_io; }
|
||||||
|
|
||||||
|
/* ---- decision logic (Tier-1; measured to 100% MC/DC via the fake io) ---- */
|
||||||
|
|
||||||
|
const char * warden_relay_name(uint32_t idx)
|
||||||
|
{
|
||||||
|
return idx < WARDEN_RELAY_COUNT ? s_name[idx] : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool exported(int gpio)
|
||||||
|
{
|
||||||
|
char p[80];
|
||||||
|
snprintf(p, sizeof(p), "%s/gpio%d", gpio_root(), gpio);
|
||||||
|
return s_io->exists(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export and set the direction, without disturbing the level. "out" on an
|
||||||
|
* unexported pin latches the kernel's default low, which would drop a closed
|
||||||
|
* relay the first time this page is opened; reading the current level first and
|
||||||
|
* writing it straight back makes the export transparent.
|
||||||
|
*/
|
||||||
|
static bool ensure_ready(int gpio)
|
||||||
|
{
|
||||||
|
if(!exported(gpio)) {
|
||||||
|
char ex[80], n[16];
|
||||||
|
snprintf(ex, sizeof(ex), "%s/export", gpio_root());
|
||||||
|
snprintf(n, sizeof(n), "%d", gpio);
|
||||||
|
s_io->write(ex, n);
|
||||||
|
if(!exported(gpio)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char p[96];
|
||||||
|
snprintf(p, sizeof(p), "%s/gpio%d/direction", gpio_root(), gpio);
|
||||||
|
char dir[16] = "";
|
||||||
|
if(!s_io->read_line(p, dir, sizeof(dir))) return false;
|
||||||
|
|
||||||
|
if(strncmp(dir, "out", 3) != 0) {
|
||||||
|
/* Preserve the level across the switch to output. */
|
||||||
|
char vp[96];
|
||||||
|
int cur;
|
||||||
|
snprintf(vp, sizeof(vp), "%s/gpio%d/value", gpio_root(), gpio);
|
||||||
|
if(!s_io->read_int(vp, &cur)) cur = 0;
|
||||||
|
s_io->write(p, cur ? "high" : "low");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool warden_relay_available(uint32_t idx)
|
||||||
|
{
|
||||||
|
if(idx >= WARDEN_RELAY_COUNT) return false;
|
||||||
|
return ensure_ready(s_gpio[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool warden_relay_get(uint32_t idx)
|
||||||
|
{
|
||||||
|
if(idx >= WARDEN_RELAY_COUNT) return false;
|
||||||
|
if(!exported(s_gpio[idx])) return false;
|
||||||
|
|
||||||
|
char p[96];
|
||||||
|
int v;
|
||||||
|
snprintf(p, sizeof(p), "%s/gpio%d/value", gpio_root(), s_gpio[idx]);
|
||||||
|
if(!s_io->read_int(p, &v)) v = 0;
|
||||||
|
return v != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void warden_relay_set(uint32_t idx, bool on)
|
||||||
|
{
|
||||||
|
if(idx >= WARDEN_RELAY_COUNT) return;
|
||||||
|
if(!ensure_ready(s_gpio[idx])) return;
|
||||||
|
|
||||||
|
char p[96];
|
||||||
|
snprintf(p, sizeof(p), "%s/gpio%d/value", gpio_root(), s_gpio[idx]);
|
||||||
|
s_io->write(p, on ? "1" : "0");
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* The two onboard relays (86-Panel bottom board).
|
||||||
|
*
|
||||||
|
* RELAY1 <- GPIO1_A0 (sysfs gpio 32)
|
||||||
|
* RELAY2 <- GPIO1_A1 (sysfs gpio 33)
|
||||||
|
*
|
||||||
|
* gpiochip1 base is 32; A0..A7 are offsets 0..7. Nothing in the DT claims these
|
||||||
|
* (there is no relay driver), so we drive them through /sys/class/gpio directly.
|
||||||
|
* An off-by-one here would toggle the neighbouring RS485 pair (GPIO1_B0/B1)
|
||||||
|
* under a running Modbus master, so the numbers come from the schematic, not
|
||||||
|
* inference. State is never assumed at startup: a relay may be holding a
|
||||||
|
* contactor closed, and deciding it should be off because we just booted is not
|
||||||
|
* this module's call.
|
||||||
|
*
|
||||||
|
* Hardened for warden-sdk: the sysfs plumbing sits behind a `relay_io` seam so
|
||||||
|
* the decision logic runs and is measured to 100% MC/DC on the host, and the
|
||||||
|
* gpio root is `$WARDEN_GPIO_ROOT`-overridable so the real backend can run
|
||||||
|
* against a scratch tree in an integration test.
|
||||||
|
*/
|
||||||
|
#ifndef WARDEN_RELAYS_H
|
||||||
|
#define WARDEN_RELAYS_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define WARDEN_RELAY_COUNT 2
|
||||||
|
|
||||||
|
/** Display name for @p idx, e.g. "Relay 1"; "-" if out of range. */
|
||||||
|
const char * warden_relay_name(uint32_t idx);
|
||||||
|
|
||||||
|
/** Current output state, false if it cannot be read. */
|
||||||
|
bool warden_relay_get(uint32_t idx);
|
||||||
|
|
||||||
|
/** Drive the output. Exports and sets the direction on first use. */
|
||||||
|
void warden_relay_set(uint32_t idx, bool on);
|
||||||
|
|
||||||
|
/** True if the GPIO is exported and usable — the page says so if it is not. */
|
||||||
|
bool warden_relay_available(uint32_t idx);
|
||||||
|
|
||||||
|
/* --- Hardware-abstraction seam ---------------------------------------------
|
||||||
|
* The four filesystem primitives the relay logic needs. Production binds the
|
||||||
|
* sysfs backend (the default); a unit test binds an in-memory fake that can
|
||||||
|
* model export -> node-appears, which a passive scratch tree cannot. */
|
||||||
|
struct relay_io {
|
||||||
|
bool (*exists)(const char *path); /* stat(path)==0 */
|
||||||
|
void (*write)(const char *path, const char *value); /* best-effort */
|
||||||
|
bool (*read_line)(const char *path, char *buf, size_t n); /* a line was read */
|
||||||
|
bool (*read_int)(const char *path, int *out); /* one int parsed */
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Swap the io backend (test hook). Pass NULL to restore the sysfs default. */
|
||||||
|
void warden_relay__set_io(const struct relay_io *io);
|
||||||
|
|
||||||
|
#endif /* WARDEN_RELAYS_H */
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# MC/DC unit harness for drivers/relays/relays.c.
|
||||||
|
#
|
||||||
|
# make check — build, run, and FAIL unless relays.c hits 100% MC/DC
|
||||||
|
# (condition) coverage and every unit check passes.
|
||||||
|
# make report — per-condition gcov annotation (build/relays.c.gcov).
|
||||||
|
# make clean
|
||||||
|
#
|
||||||
|
# Requires gcc >= 14 (for -fcondition-coverage) and its matching gcov.
|
||||||
|
CC ?= gcc
|
||||||
|
GCOV ?= gcov
|
||||||
|
CFLAGS := -O0 -g -Wall -Wextra -I..
|
||||||
|
COVFLAGS := --coverage -fcondition-coverage
|
||||||
|
BUILD := build
|
||||||
|
|
||||||
|
.PHONY: check report clean
|
||||||
|
.DEFAULT_GOAL := check
|
||||||
|
|
||||||
|
$(BUILD):
|
||||||
|
@mkdir -p $(BUILD)
|
||||||
|
|
||||||
|
# relays.c carries the coverage flags; the test driver is plain.
|
||||||
|
$(BUILD)/test: test_relays.c ../relays.c ../relays.h | $(BUILD)
|
||||||
|
@$(CC) $(CFLAGS) $(COVFLAGS) -c ../relays.c -o $(BUILD)/relays.o
|
||||||
|
@$(CC) $(CFLAGS) -c test_relays.c -o $(BUILD)/test_relays.o
|
||||||
|
@$(CC) $(COVFLAGS) $(BUILD)/relays.o $(BUILD)/test_relays.o -o $(BUILD)/test
|
||||||
|
|
||||||
|
check: $(BUILD)/test
|
||||||
|
@echo "== running relays MC/DC harness =="
|
||||||
|
@rm -f $(BUILD)/relays.gcda
|
||||||
|
@$(BUILD)/test; echo $$? > $(BUILD)/test.rc
|
||||||
|
@echo
|
||||||
|
@echo "== MC/DC (condition) coverage of relays.c =="
|
||||||
|
@$(GCOV) --conditions --branch-probabilities -o $(BUILD) ../relays.c >$(BUILD)/gcov.log 2>&1 || true
|
||||||
|
@mv -f *.gcov $(BUILD)/ 2>/dev/null || true
|
||||||
|
@bash enforce-mcdc.sh $(BUILD)/gcov.log $(BUILD)/relays.c.gcov $(BUILD)/test.rc
|
||||||
|
|
||||||
|
report: check
|
||||||
|
@grep -nE "condition.*not covered|conditions covered" $(BUILD)/relays.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 relays.c reached 100% MC/DC
|
||||||
|
# (condition) coverage with no uncovered executable lines.
|
||||||
|
# $1 = gcov stdout log $2 = relays.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 relays.c from gcov stdout. Match the exact file so
|
||||||
|
# 'test_relays.c' (which also contains "relays.c") is NOT picked up.
|
||||||
|
FMATCH="File '([^']*/)?relays[.]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 relays.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,218 @@
|
|||||||
|
/* MC/DC harness for drivers/relays/relays.c.
|
||||||
|
*
|
||||||
|
* Two layers, one binary, so the combined run covers every decision in relays.c:
|
||||||
|
* 1. unit tests through a FAKE relay_io — exercise the decision logic, incl.
|
||||||
|
* the export->node-appears path a passive tree cannot model.
|
||||||
|
* 2. integration tests through the real sysfs backend + $WARDEN_GPIO_ROOT
|
||||||
|
* pointed at a scratch tree — exercise the backend's fopen/stat branches.
|
||||||
|
*/
|
||||||
|
#include "../relays.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static int g_fail = 0, g_checks = 0;
|
||||||
|
#define EXPECT(cond) do { \
|
||||||
|
g_checks++; \
|
||||||
|
if(!(cond)) { g_fail++; fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); } \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
/* ---------------- layer 1: fake io ---------------- */
|
||||||
|
|
||||||
|
static struct {
|
||||||
|
bool exists_ret;
|
||||||
|
bool export_makes_exist; /* write(.../export) flips exists_ret true */
|
||||||
|
bool read_line_ok;
|
||||||
|
const char *read_line_val;
|
||||||
|
bool read_int_ok;
|
||||||
|
int read_int_val;
|
||||||
|
char last_write_path[160];
|
||||||
|
char last_write_val[32];
|
||||||
|
} fk;
|
||||||
|
|
||||||
|
static bool fk_exists(const char *p) { (void)p; return fk.exists_ret; }
|
||||||
|
static void fk_write(const char *p, const char *v) {
|
||||||
|
snprintf(fk.last_write_path, sizeof fk.last_write_path, "%s", p);
|
||||||
|
snprintf(fk.last_write_val, sizeof fk.last_write_val, "%s", v);
|
||||||
|
if(fk.export_makes_exist && strstr(p, "/export")) fk.exists_ret = true;
|
||||||
|
}
|
||||||
|
static bool fk_read_line(const char *p, char *b, size_t n) {
|
||||||
|
(void)p;
|
||||||
|
if(!fk.read_line_ok) return false;
|
||||||
|
snprintf(b, n, "%s", fk.read_line_val);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static bool fk_read_int(const char *p, int *o) {
|
||||||
|
(void)p;
|
||||||
|
if(!fk.read_int_ok) return false;
|
||||||
|
*o = fk.read_int_val;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static const struct relay_io FAKE = { fk_exists, fk_write, fk_read_line, fk_read_int };
|
||||||
|
|
||||||
|
static void fk_reset(void) { memset(&fk, 0, sizeof fk); }
|
||||||
|
|
||||||
|
static void unit_tests(void)
|
||||||
|
{
|
||||||
|
warden_relay__set_io(&FAKE);
|
||||||
|
|
||||||
|
/* warden_relay_name: both sides of idx < COUNT */
|
||||||
|
EXPECT(strcmp(warden_relay_name(0), "Relay 1") == 0);
|
||||||
|
EXPECT(strcmp(warden_relay_name(WARDEN_RELAY_COUNT), "-") == 0);
|
||||||
|
|
||||||
|
/* available: idx >= COUNT (true) */
|
||||||
|
EXPECT(warden_relay_available(WARDEN_RELAY_COUNT) == false);
|
||||||
|
|
||||||
|
/* ensure_ready: not exported -> export FAILS (inner if true) */
|
||||||
|
fk_reset();
|
||||||
|
fk.exists_ret = false; fk.export_makes_exist = false;
|
||||||
|
EXPECT(warden_relay_available(0) == false);
|
||||||
|
EXPECT(strstr(fk.last_write_path, "/export") != NULL); /* export was attempted */
|
||||||
|
|
||||||
|
/* ensure_ready: not exported -> export SUCCEEDS -> direction read FAILS */
|
||||||
|
fk_reset();
|
||||||
|
fk.exists_ret = false; fk.export_makes_exist = true; fk.read_line_ok = false;
|
||||||
|
EXPECT(warden_relay_available(0) == false);
|
||||||
|
|
||||||
|
/* already exported -> direction == "out" -> no level-preserve write */
|
||||||
|
fk_reset();
|
||||||
|
fk.exists_ret = true; fk.read_line_ok = true; fk.read_line_val = "out\n";
|
||||||
|
EXPECT(warden_relay_available(0) == true);
|
||||||
|
|
||||||
|
/* exported -> direction "in" -> read_int FAILS -> cur=0 -> write "low" */
|
||||||
|
fk_reset();
|
||||||
|
fk.exists_ret = true; fk.read_line_ok = true; fk.read_line_val = "in\n";
|
||||||
|
fk.read_int_ok = false;
|
||||||
|
EXPECT(warden_relay_available(0) == true);
|
||||||
|
EXPECT(strcmp(fk.last_write_val, "low") == 0);
|
||||||
|
|
||||||
|
/* exported -> direction "in" -> read_int OK, cur=0 -> ternary false -> "low" */
|
||||||
|
fk_reset();
|
||||||
|
fk.exists_ret = true; fk.read_line_ok = true; fk.read_line_val = "in\n";
|
||||||
|
fk.read_int_ok = true; fk.read_int_val = 0;
|
||||||
|
EXPECT(warden_relay_available(0) == true);
|
||||||
|
EXPECT(strcmp(fk.last_write_val, "low") == 0);
|
||||||
|
|
||||||
|
/* exported -> direction "in" -> read_int OK, cur=1 -> ternary true -> "high" */
|
||||||
|
fk_reset();
|
||||||
|
fk.exists_ret = true; fk.read_line_ok = true; fk.read_line_val = "in\n";
|
||||||
|
fk.read_int_ok = true; fk.read_int_val = 1;
|
||||||
|
EXPECT(warden_relay_available(0) == true);
|
||||||
|
EXPECT(strcmp(fk.last_write_val, "high") == 0);
|
||||||
|
|
||||||
|
/* get: idx >= COUNT (true) */
|
||||||
|
EXPECT(warden_relay_get(WARDEN_RELAY_COUNT) == false);
|
||||||
|
|
||||||
|
/* get: not exported -> false */
|
||||||
|
fk_reset(); fk.exists_ret = false;
|
||||||
|
EXPECT(warden_relay_get(0) == false);
|
||||||
|
|
||||||
|
/* get: exported, read_int FAILS -> v=0 -> v!=0 false */
|
||||||
|
fk_reset(); fk.exists_ret = true; fk.read_int_ok = false;
|
||||||
|
EXPECT(warden_relay_get(0) == false);
|
||||||
|
|
||||||
|
/* get: exported, read_int OK v=1 -> v!=0 true */
|
||||||
|
fk_reset(); fk.exists_ret = true; fk.read_int_ok = true; fk.read_int_val = 1;
|
||||||
|
EXPECT(warden_relay_get(0) == true);
|
||||||
|
|
||||||
|
/* get: exported, read_int OK v=0 -> v!=0 false */
|
||||||
|
fk_reset(); fk.exists_ret = true; fk.read_int_ok = true; fk.read_int_val = 0;
|
||||||
|
EXPECT(warden_relay_get(0) == false);
|
||||||
|
|
||||||
|
/* set: idx >= COUNT (true) -> no-op */
|
||||||
|
fk_reset();
|
||||||
|
warden_relay_set(WARDEN_RELAY_COUNT, true);
|
||||||
|
EXPECT(fk.last_write_path[0] == '\0');
|
||||||
|
|
||||||
|
/* set: ensure_ready FAILS -> return before writing value */
|
||||||
|
fk_reset(); fk.exists_ret = false; fk.export_makes_exist = false;
|
||||||
|
warden_relay_set(0, true);
|
||||||
|
EXPECT(strstr(fk.last_write_path, "/value") == NULL);
|
||||||
|
|
||||||
|
/* set: ensure_ready OK (exported, "out"), on=true -> write "1" */
|
||||||
|
fk_reset(); fk.exists_ret = true; fk.read_line_ok = true; fk.read_line_val = "out\n";
|
||||||
|
warden_relay_set(0, true);
|
||||||
|
EXPECT(strcmp(fk.last_write_val, "1") == 0);
|
||||||
|
|
||||||
|
/* set: on=false -> write "0" */
|
||||||
|
fk_reset(); fk.exists_ret = true; fk.read_line_ok = true; fk.read_line_val = "out\n";
|
||||||
|
warden_relay_set(0, false);
|
||||||
|
EXPECT(strcmp(fk.last_write_val, "0") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- layer 2: real sysfs backend over a scratch tree ---------------- */
|
||||||
|
|
||||||
|
static char g_root[128];
|
||||||
|
|
||||||
|
static void wr(const char *rel, const char *content) {
|
||||||
|
char p[256]; snprintf(p, sizeof p, "%s/%s", g_root, rel);
|
||||||
|
FILE *f = fopen(p, "w"); if(f) { fputs(content, f); fclose(f); }
|
||||||
|
}
|
||||||
|
static void mkgpio(int n) {
|
||||||
|
char p[256]; snprintf(p, sizeof p, "%s/gpio%d", g_root, n); mkdir(p, 0777);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void integration_tests(void)
|
||||||
|
{
|
||||||
|
char tmpl[] = "/tmp/warden-relays-XXXXXX";
|
||||||
|
char *d = mkdtemp(tmpl);
|
||||||
|
assert(d);
|
||||||
|
snprintf(g_root, sizeof g_root, "%s", d);
|
||||||
|
setenv("WARDEN_GPIO_ROOT", g_root, 1);
|
||||||
|
warden_relay__set_io(NULL); /* restore the real sysfs backend */
|
||||||
|
|
||||||
|
/* empty $WARDEN_GPIO_ROOT -> gpio_root() falls back to the default (covers the
|
||||||
|
`*r` false arm of `r && *r`). The default /sys path is absent on the host,
|
||||||
|
so the call simply reports unavailable. */
|
||||||
|
setenv("WARDEN_GPIO_ROOT", "", 1);
|
||||||
|
(void)warden_relay_available(0);
|
||||||
|
setenv("WARDEN_GPIO_ROOT", g_root, 1);
|
||||||
|
|
||||||
|
/* sysfs_exists false + sysfs_write fopen FAIL: root has no /export parent issue;
|
||||||
|
use a nonexistent root so export write and stat both fail cleanly. */
|
||||||
|
setenv("WARDEN_GPIO_ROOT", "/nonexistent-warden-root/xyz", 1);
|
||||||
|
EXPECT(warden_relay_available(0) == false); /* exists=false, write(export) fopen fails */
|
||||||
|
setenv("WARDEN_GPIO_ROOT", g_root, 1);
|
||||||
|
|
||||||
|
/* gpio32 exists, direction file ABSENT -> sysfs_read_line fopen fail */
|
||||||
|
mkgpio(32);
|
||||||
|
EXPECT(warden_relay_available(0) == false);
|
||||||
|
|
||||||
|
/* direction EMPTY -> fgets returns NULL -> read_line false */
|
||||||
|
wr("gpio32/direction", "");
|
||||||
|
EXPECT(warden_relay_available(0) == false);
|
||||||
|
|
||||||
|
/* direction "out" -> read_line ok, skip block -> available true (sysfs_write success path
|
||||||
|
is exercised by set below); also value ABSENT -> get: read_int fopen fail -> v=0 */
|
||||||
|
wr("gpio32/direction", "out\n");
|
||||||
|
EXPECT(warden_relay_available(0) == true);
|
||||||
|
EXPECT(warden_relay_get(0) == false); /* value file absent: read_int fopen fail */
|
||||||
|
|
||||||
|
/* value non-numeric -> fscanf != 1 -> read_int false */
|
||||||
|
wr("gpio32/value", "xyz\n");
|
||||||
|
EXPECT(warden_relay_get(0) == false);
|
||||||
|
|
||||||
|
/* value "1" -> read_int ok -> get true; and set(0,true) -> sysfs_write success */
|
||||||
|
wr("gpio32/value", "1\n");
|
||||||
|
EXPECT(warden_relay_get(0) == true);
|
||||||
|
warden_relay_set(0, false);
|
||||||
|
EXPECT(warden_relay_get(0) == false); /* wrote "0" over the value file */
|
||||||
|
|
||||||
|
/* direction "in" with a real value -> exercises the level-preserve write via sysfs */
|
||||||
|
mkgpio(33);
|
||||||
|
wr("gpio33/direction", "in\n");
|
||||||
|
wr("gpio33/value", "1\n");
|
||||||
|
EXPECT(warden_relay_available(1) == true); /* reads value=1, writes direction "high" */
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
unit_tests();
|
||||||
|
integration_tests();
|
||||||
|
fprintf(stderr, "%d checks, %d failures\n", g_checks, g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user