diff --git a/.github/workflows/provable.yml b/.github/workflows/provable.yml new file mode 100644 index 0000000..9fc1702 --- /dev/null +++ b/.github/workflows/provable.yml @@ -0,0 +1,112 @@ +# SPDX-License-Identifier: MPL-2.0 +# Provable — machine-checks the claims VeriSimiser makes outside the Rust layer. +# +# The Rust CLI/codegen is covered by rust-ci.yml (fmt + clippy + test). This +# workflow verifies the four things that are otherwise only asserted in prose: +# 1. idris2-proofs — the Idris2 ABI proofs actually type-check (`idris2 --check`). +# 2. zig-ffi — the Zig FFI reference impl builds and its tests pass. +# 3. codegen-drift — the committed golden sample matches fresh codegen output. +# 4. sql-golden — the generated overlay SQL actually applies to a real +# SQLite database and every table/view builds and queries. +# +# Until every job here is green, the proofs / FFI / generated SQL are "written" +# but NOT "verified" — ROADMAP.adoc and README.adoc are worded accordingly. +name: Provable + +on: + push: + branches: [main, master, "claude/**"] + pull_request: + +permissions: + contents: read + +jobs: + idris2-proofs: + name: Idris2 — machine-check ABI proofs + runs-on: ubuntu-latest + timeout-minutes: 20 + container: ghcr.io/stefan-hoeck/idris2-pack:latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - name: idris2 build the ABI package (Types, Layout, Proofs, Foreign) + working-directory: src/interface/abi + # Modules are namespaced Verisimiser.ABI.* under Verisimiser/ABI/. + # Building the .ipkg type-checks every module in the package — including + # Proofs.idr, which carries the machine-checked C-ABI / layout theorems — + # in one invocation, which is also the canonical command in the .ipkg. + run: | + idris2 --version + idris2 --build verisimiser-abi.ipkg + + zig-ffi: + name: Zig — build + test FFI reference impl + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: mlugg/setup-zig@53fc45b17fe98b52f92ee5ea08ff48a85a3e7eb7 # v1.2.2 + with: + version: 0.14.0 + - name: zig build + test + working-directory: src/interface/ffi + # `zig build test` runs the 8 in-module unit tests AND the 35 C-ABI + # integration tests (which link the compiled libverisimiser.so). + run: | + zig version + zig build test + zig build + + codegen-drift: + name: Codegen — golden sample is up to date + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - name: Regenerate golden artifacts and diff against committed tree + run: | + # retry: fresh runners occasionally flake fetching crates from crates.io + built=0 + for i in 1 2 3; do + cargo build --quiet && { built=1; break; } + echo "build attempt $i failed; retrying after backoff…"; sleep $((i * 5)) + done + [ "$built" = 1 ] || { echo "cargo build failed after retries"; exit 1; } + ( cd examples/golden && ../../target/debug/verisimiser generate -m verisimiser.toml -o /tmp/golden ) + diff -ru examples/golden/generated /tmp/golden + + sql-golden: + name: SQLite — generated overlay applies and views build + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - name: Ensure sqlite3 is available + run: sudo apt-get update && sudo apt-get install -y sqlite3 + - name: Apply schema + generated overlay + interceptors into a fresh SQLite DB + run: | + sqlite3 --version + DB="$(mktemp -d)/golden.db" + sqlite3 "$DB" < examples/golden/schema.sql + sqlite3 "$DB" < examples/golden/generated/sidecar_schema.sql + sqlite3 "$DB" < examples/golden/generated/interceptors.sql + + # Every enabled octad dimension must have produced its sidecar table. + for t in verisimdb_metadata verisimdb_provenance_log \ + verisimdb_lineage_graph verisimdb_temporal_versions \ + verisimdb_access_policies; do + sqlite3 "$DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$t';" \ + | grep -qx "$t" || { echo "missing sidecar table: $t"; exit 1; } + done + + # Every target table must have provenance + temporal enrichment views, + # and each view must be queryable (catches invalid SQL in view bodies). + for v in verisimdb_users_with_provenance verisimdb_users_with_temporal \ + verisimdb_posts_with_provenance verisimdb_posts_with_temporal; do + sqlite3 "$DB" "SELECT name FROM sqlite_master WHERE type='view' AND name='$v';" \ + | grep -qx "$v" || { echo "missing enrichment view: $v"; exit 1; } + sqlite3 "$DB" "SELECT count(*) FROM $v;" >/dev/null \ + || { echo "view not queryable: $v"; exit 1; } + done + + echo "Generated overlay applied cleanly; all sidecar tables + views present and queryable." diff --git a/examples/golden/README.adoc b/examples/golden/README.adoc new file mode 100644 index 0000000..e147541 --- /dev/null +++ b/examples/golden/README.adoc @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MPL-2.0 += VeriSimiser Codegen Golden Sample +:toc: macro +:source-highlighter: rouge + +This directory is the *frozen golden sample* that the `codegen-drift` and +`sql-golden` jobs of `.github/workflows/provable.yml` check on every push and +pull request. It exists to turn "the generator produces correct SQL" from a +prose claim into a machine-checked fact. + +toc::[] + +== What's here + +[cols="1,3"] +|=== +| File | Role + +| `verisimiser.toml` +| Frozen input manifest (SQLite backend, 7 of 8 octad dimensions enabled). + +| `schema.sql` +| Frozen input schema — two tables (`users`, `posts`) with a typical author + relationship and a spread of column types. + +| `generated/sidecar_schema.sql` +| Committed expected output: the octad sidecar overlay DDL. + +| `generated/interceptors.sql` +| Committed expected output: the per-table provenance/temporal enrichment + views and lineage/access-control query templates. +|=== + +== What the CI checks + +`codegen-drift`:: regenerates `generated/` from `verisimiser.toml` + `schema.sql` +into a temp directory and `diff`s it against the committed tree. The generator +is deterministic, so any difference fails the job — this catches *unintended* +changes to codegen output. + +`sql-golden`:: applies `schema.sql`, then the two generated files, into a fresh +SQLite database and asserts every sidecar table and enrichment view is created +and queryable. This is the SQL analogue of "the generated code compiles and +runs": it proves the emitted DDL is valid and applies cleanly against the +target schema (the views use `ROW_NUMBER() OVER (…)`, so SQLite >= 3.25). + +== Regenerating after an intentional codegen change + +If you change the generator and the new output is correct, refresh the golden +in the *same* PR as the code change: + +[source,sh] +---- +cargo build +( cd examples/golden && ../../target/debug/verisimiser generate -m verisimiser.toml -o generated ) +git add examples/golden/generated +---- + +Then review the diff carefully — it is the human-readable record of how your +change affected every consumer's generated overlay. diff --git a/examples/golden/generated/interceptors.sql b/examples/golden/generated/interceptors.sql new file mode 100644 index 0000000..5173eee --- /dev/null +++ b/examples/golden/generated/interceptors.sql @@ -0,0 +1,145 @@ +-- SPDX-License-Identifier: MPL-2.0 +-- VeriSimiser query interceptors (auto-generated) + +-- ========================================================== +-- Table: users +-- ========================================================== + +-- Provenance-enriched view for 'users' +-- Joins each row with its latest provenance entry from the sidecar. +CREATE VIEW IF NOT EXISTS verisimdb_users_with_provenance AS +SELECT + users.id, + users.username, + users.email, + users.created_at, + prov.operation AS _verisimdb_last_operation, + prov.actor AS _verisimdb_last_actor, + prov.timestamp AS _verisimdb_last_modified, + prov.hash AS _verisimdb_provenance_hash +FROM users +LEFT JOIN ( + SELECT entity_id, operation, actor, timestamp, hash + FROM ( + SELECT entity_id, operation, actor, timestamp, hash, + ROW_NUMBER() OVER (PARTITION BY entity_id ORDER BY timestamp DESC) AS _rn + FROM verisimdb_provenance_log + WHERE table_name = 'users' + ) ranked + WHERE _rn = 1 +) prov ON prov.entity_id = (CAST(users.id AS TEXT)); + +-- Temporal-enriched view for 'users' +-- Joins each row with its current version metadata. +CREATE VIEW IF NOT EXISTS verisimdb_users_with_temporal AS +SELECT + users.id, + users.username, + users.email, + users.created_at, + tv.version AS _verisimdb_version, + tv.valid_from AS _verisimdb_valid_from, + tv.operation AS _verisimdb_version_operation +FROM users +LEFT JOIN verisimdb_temporal_versions tv + ON tv.entity_id = (CAST(users.id AS TEXT)) + AND tv.table_name = 'users' + AND tv.valid_to IS NULL; + +-- Lineage queries for 'users' + +-- Upstream: what data was this entity derived from? +-- SELECT * FROM verisimdb_lineage_graph +-- WHERE target_entity = :entity_id AND target_table = 'users'; + +-- Downstream: what entities depend on this entity? +-- SELECT * FROM verisimdb_lineage_graph +-- WHERE source_entity = :entity_id AND source_table = 'users'; + +-- Access control filter for 'users' +-- Apply this as a WHERE clause addition to enforce row-level security. +-- +-- Example usage (parameterised): +-- SELECT * FROM users +-- WHERE ... AND EXISTS ( +-- SELECT 1 FROM verisimdb_access_policies +-- WHERE target_table = 'users' +-- AND principal = :current_principal +-- AND access_level IN ('read', 'admin') +-- AND active = 1 +-- AND (condition IS NULL OR :row_matches_condition) +-- ); + +-- ========================================================== +-- Table: posts +-- ========================================================== + +-- Provenance-enriched view for 'posts' +-- Joins each row with its latest provenance entry from the sidecar. +CREATE VIEW IF NOT EXISTS verisimdb_posts_with_provenance AS +SELECT + posts.id, + posts.author_id, + posts.title, + posts.body, + posts.published, + posts.created_at, + prov.operation AS _verisimdb_last_operation, + prov.actor AS _verisimdb_last_actor, + prov.timestamp AS _verisimdb_last_modified, + prov.hash AS _verisimdb_provenance_hash +FROM posts +LEFT JOIN ( + SELECT entity_id, operation, actor, timestamp, hash + FROM ( + SELECT entity_id, operation, actor, timestamp, hash, + ROW_NUMBER() OVER (PARTITION BY entity_id ORDER BY timestamp DESC) AS _rn + FROM verisimdb_provenance_log + WHERE table_name = 'posts' + ) ranked + WHERE _rn = 1 +) prov ON prov.entity_id = (CAST(posts.id AS TEXT)); + +-- Temporal-enriched view for 'posts' +-- Joins each row with its current version metadata. +CREATE VIEW IF NOT EXISTS verisimdb_posts_with_temporal AS +SELECT + posts.id, + posts.author_id, + posts.title, + posts.body, + posts.published, + posts.created_at, + tv.version AS _verisimdb_version, + tv.valid_from AS _verisimdb_valid_from, + tv.operation AS _verisimdb_version_operation +FROM posts +LEFT JOIN verisimdb_temporal_versions tv + ON tv.entity_id = (CAST(posts.id AS TEXT)) + AND tv.table_name = 'posts' + AND tv.valid_to IS NULL; + +-- Lineage queries for 'posts' + +-- Upstream: what data was this entity derived from? +-- SELECT * FROM verisimdb_lineage_graph +-- WHERE target_entity = :entity_id AND target_table = 'posts'; + +-- Downstream: what entities depend on this entity? +-- SELECT * FROM verisimdb_lineage_graph +-- WHERE source_entity = :entity_id AND source_table = 'posts'; + +-- Access control filter for 'posts' +-- Apply this as a WHERE clause addition to enforce row-level security. +-- +-- Example usage (parameterised): +-- SELECT * FROM posts +-- WHERE ... AND EXISTS ( +-- SELECT 1 FROM verisimdb_access_policies +-- WHERE target_table = 'posts' +-- AND principal = :current_principal +-- AND access_level IN ('read', 'admin') +-- AND active = 1 +-- AND (condition IS NULL OR :row_matches_condition) +-- ); + diff --git a/examples/golden/generated/sidecar_schema.sql b/examples/golden/generated/sidecar_schema.sql new file mode 100644 index 0000000..8bd56d7 --- /dev/null +++ b/examples/golden/generated/sidecar_schema.sql @@ -0,0 +1,112 @@ +-- SPDX-License-Identifier: MPL-2.0 +-- VeriSimiser sidecar schema (auto-generated) +-- Do not edit manually; regenerate with `verisimiser init`. + +-- Metadata: tracks augmented target tables +CREATE TABLE IF NOT EXISTS verisimdb_metadata ( + table_name TEXT PRIMARY KEY, + column_count INTEGER NOT NULL, + pk_columns TEXT NOT NULL, -- comma-separated list of PK column names + discovered_at TEXT NOT NULL -- ISO 8601 timestamp +); + +-- Seed metadata from parsed schema (SQLite) +INSERT OR IGNORE INTO verisimdb_metadata (table_name, column_count, pk_columns, discovered_at) + VALUES ('users', 4, 'id', CURRENT_TIMESTAMP); +INSERT OR IGNORE INTO verisimdb_metadata (table_name, column_count, pk_columns, discovered_at) + VALUES ('posts', 6, 'id', CURRENT_TIMESTAMP); + +-- Provenance: SHA-256 hash-chained audit trail (ADR-0010) +CREATE TABLE IF NOT EXISTS verisimdb_provenance_log ( + hash TEXT PRIMARY KEY, + previous_hash TEXT NOT NULL, + entity_id TEXT NOT NULL, + table_name TEXT NOT NULL, + operation TEXT NOT NULL CHECK (operation IN ('insert','update','delete','transform')), -- V-L2-J1 + actor TEXT NOT NULL, + timestamp TEXT NOT NULL, -- ISO 8601 + before_snapshot TEXT, -- JSON of entity state before operation + transformation TEXT, -- description of transformation applied + CHECK (operation IN ('insert','update','delete','transform')) +); +-- ADR-0010 #32 (superseded): NO UNIQUE(entity_id, previous_hash) — +-- a fork that cannot be written cannot be detected or audited. The +-- non-unique index below makes fork detection O(log n) instead. +CREATE INDEX IF NOT EXISTS idx_provenance_predecessor + ON verisimdb_provenance_log(entity_id, previous_hash); +CREATE INDEX IF NOT EXISTS idx_provenance_entity ON verisimdb_provenance_log(entity_id); +CREATE INDEX IF NOT EXISTS idx_provenance_table ON verisimdb_provenance_log(table_name); + +-- ADR-0010 #31: chain-tip *set*. `append_provenance` keeps a +-- BEGIN IMMEDIATE write so racing duplicate appends on one node +-- still serialise; a linear append swaps its single tip, a +-- deliberate fork adds a tip without removing one. +CREATE TABLE IF NOT EXISTS verisimdb_provenance_chain_heads ( + entity_id TEXT NOT NULL, + head_hash TEXT NOT NULL, + PRIMARY KEY (entity_id, head_hash) +); +-- Legacy single-head table: kept one release for non-destructive +-- migration (see tier1::provenance::SIDECAR_DDL). No DROP ships here. +CREATE TABLE IF NOT EXISTS verisimdb_provenance_chain_head ( + entity_id TEXT PRIMARY KEY, + head_hash TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- Lineage: data derivation graph (DAG by intent; cycle prevention is +-- a runtime concern — see V-L1-G1 / V-L2-I2). +CREATE TABLE IF NOT EXISTS verisimdb_lineage_graph ( + edge_id TEXT PRIMARY KEY, + source_entity TEXT NOT NULL, + source_table TEXT NOT NULL, + target_entity TEXT NOT NULL, + target_table TEXT NOT NULL, + derivation_type TEXT NOT NULL + CHECK (derivation_type IN ('copy','transform','aggregate','join','filter')), -- V-L2-J1 + description TEXT, + created_at TEXT NOT NULL, -- ISO 8601 + -- V-L2-I1: self-edges are not derivations; rejected at DB level. + CHECK (NOT (source_entity = target_entity AND source_table = target_table)) +); +CREATE INDEX IF NOT EXISTS idx_lineage_source ON verisimdb_lineage_graph(source_entity); +CREATE INDEX IF NOT EXISTS idx_lineage_target ON verisimdb_lineage_graph(target_entity); + +-- Temporal: version history with point-in-time support. +-- V-L2-H1: the partial UNIQUE INDEX enforces exactly one +-- current row per (entity, table) — "only one version is +-- valid right now" was an application-layer invariant before; +-- now it's structural. +-- V-L2-J1: operation is a closed set. +-- V-L2-H2: valid_to (if set) must not predate valid_from. +CREATE TABLE IF NOT EXISTS verisimdb_temporal_versions ( + entity_id TEXT NOT NULL, + table_name TEXT NOT NULL, + version INTEGER NOT NULL CHECK (version >= 1), + valid_from TEXT NOT NULL, -- ISO 8601 + valid_to TEXT, -- ISO 8601, NULL if current + snapshot TEXT NOT NULL, -- JSON serialisation of entity state + operation TEXT NOT NULL CHECK (operation IN ('insert','update','rollback')), + PRIMARY KEY (entity_id, table_name, version), + CHECK (valid_to IS NULL OR valid_to >= valid_from) +); +CREATE UNIQUE INDEX IF NOT EXISTS ux_temporal_current + ON verisimdb_temporal_versions(entity_id, table_name) + WHERE valid_to IS NULL; + +-- Access Control: row/column-level access policies. +-- V-L2-J1: access_level is a closed set. +CREATE TABLE IF NOT EXISTS verisimdb_access_policies ( + policy_id TEXT PRIMARY KEY, + target_table TEXT NOT NULL, + target_column TEXT, -- NULL means whole-row policy + principal TEXT NOT NULL, -- user, role, or group identifier + access_level TEXT NOT NULL + CHECK (access_level IN ('read','write','admin','deny')), + condition TEXT, -- SQL-like filter condition (V-L1-H1) + created_at TEXT NOT NULL, -- ISO 8601 + active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0,1)) +); +CREATE INDEX IF NOT EXISTS idx_access_table ON verisimdb_access_policies(target_table); +CREATE INDEX IF NOT EXISTS idx_access_principal ON verisimdb_access_policies(principal); + diff --git a/examples/golden/schema.sql b/examples/golden/schema.sql new file mode 100644 index 0000000..aece3c7 --- /dev/null +++ b/examples/golden/schema.sql @@ -0,0 +1,22 @@ +-- SPDX-License-Identifier: MPL-2.0 +-- Frozen golden schema for the codegen-drift check in provable.yml. +-- +-- Two tables with a typical author relationship — small enough that the +-- generated sidecar overlay + interceptors stay reviewable in a diff, but +-- enough to exercise multiple column types and more than one table. + +CREATE TABLE users ( + id INTEGER PRIMARY KEY, + username VARCHAR(64) NOT NULL, + email VARCHAR(320) NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE TABLE posts ( + id INTEGER PRIMARY KEY, + author_id INTEGER NOT NULL, + title TEXT NOT NULL, + body TEXT, + published BOOLEAN NOT NULL, + created_at TIMESTAMP NOT NULL +); diff --git a/examples/golden/verisimiser.toml b/examples/golden/verisimiser.toml new file mode 100644 index 0000000..79f6b57 --- /dev/null +++ b/examples/golden/verisimiser.toml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MPL-2.0 +# Frozen golden manifest for the codegen-drift check in provable.yml. +# +# examples/golden/generated/ is regenerated from this manifest + schema.sql in +# CI and diffed against the committed tree. Do not edit casually: if you intend +# to change the generator's output, regenerate the golden (see +# examples/golden/README.adoc) and commit the code change and the refreshed +# golden together in the same PR. + +[project] +name = "golden" +version = "0.1.0" +description = "Frozen codegen golden sample (provable.yml codegen-drift)" + +[database] +backend = "sqlite" +connection-string-env = "GOLDEN_DATABASE_URL" +schema-source = "schema.sql" + +[octad] +enable-provenance = true +enable-lineage = true +enable-temporal = true +enable-access-control = true +enable-simulation = false + +[sidecar] +storage = "sqlite" +path = ".verisim/sidecar.db" diff --git a/src/interface/ffi/build.zig b/src/interface/ffi/build.zig index f750f12..a939917 100644 --- a/src/interface/ffi/build.zig +++ b/src/interface/ffi/build.zig @@ -8,88 +8,72 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - // Shared library (.so, .dylib, .dll) + // Shared library (.so, .dylib, .dll). + // linkLibC: the implementation uses std.heap.c_allocator. const lib = b.addSharedLibrary(.{ .name = "verisimiser", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); + lib.linkLibC(); - // Set version - lib.version = .{ .major = 0, .minor = 1, .patch = 0 }; - - // Static library (.a) + // Static library (.a). const lib_static = b.addStaticLibrary(.{ .name = "verisimiser", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); + lib_static.linkLibC(); - // Install artifacts + // Install artifacts. b.installArtifact(lib); b.installArtifact(lib_static); - // Generate header file for C compatibility - const header = b.addInstallHeader( - b.path("include/verisimiser.h"), - "verisimiser.h", - ); - b.getInstallStep().dependOn(&header.step); - - // Unit tests + // Unit tests (in-module tests in src/main.zig). const lib_tests = b.addTest(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); - + lib_tests.linkLibC(); const run_lib_tests = b.addRunArtifact(lib_tests); - const test_step = b.step("test", "Run VeriSimiser FFI unit tests"); - test_step.dependOn(&run_lib_tests.step); - - // Integration tests + // Integration tests (test/integration_test.zig) exercise the exported + // C-ABI symbols through the actual compiled shared library. const integration_tests = b.addTest(.{ .root_source_file = b.path("test/integration_test.zig"), .target = target, .optimize = optimize, }); - integration_tests.linkLibrary(lib); - + integration_tests.linkLibC(); const run_integration_tests = b.addRunArtifact(integration_tests); - const integration_test_step = b.step("test-integration", "Run VeriSimiser integration tests"); + // `zig build test` runs BOTH the unit tests and the C-ABI integration tests. + const test_step = b.step("test", "Run VeriSimiser FFI unit + integration tests"); + test_step.dependOn(&run_lib_tests.step); + test_step.dependOn(&run_integration_tests.step); + + // `zig build test-unit` runs only the in-module unit tests. + const unit_test_step = b.step("test-unit", "Run VeriSimiser FFI unit tests only"); + unit_test_step.dependOn(&run_lib_tests.step); + + // `zig build test-integration` runs only the C-ABI integration tests. + const integration_test_step = b.step("test-integration", "Run VeriSimiser integration tests only"); integration_test_step.dependOn(&run_integration_tests.step); - // Documentation + // Documentation. const docs = b.addTest(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = .Debug, }); - const docs_step = b.step("docs", "Generate VeriSimiser FFI documentation"); docs_step.dependOn(&b.addInstallDirectory(.{ .source_dir = docs.getEmittedDocs(), .install_dir = .prefix, .install_subdir = "docs", }).step); - - // Benchmark - const bench = b.addExecutable(.{ - .name = "verisimiser-bench", - .root_source_file = b.path("bench/bench.zig"), - .target = target, - .optimize = .ReleaseFast, - }); - - bench.linkLibrary(lib); - - const run_bench = b.addRunArtifact(bench); - - const bench_step = b.step("bench", "Run VeriSimiser FFI benchmarks"); - bench_step.dependOn(&run_bench.step); } diff --git a/src/interface/ffi/test/integration_test.zig b/src/interface/ffi/test/integration_test.zig index 2b1782e..b0debf2 100644 --- a/src/interface/ffi/test/integration_test.zig +++ b/src/interface/ffi/test/integration_test.zig @@ -8,29 +8,35 @@ const std = @import("std"); const testing = std.testing; +// Opaque handle type: C callers (and these tests) only ever hold a pointer to +// it. Declaring it once — rather than inline `?*Handle` per declaration — +// means every signature shares the *same* opaque type, so the handle returned +// by `verisimiser_init` is accepted by every other function. +const Handle = opaque {}; + // Import VeriSimiser FFI functions -extern fn verisimiser_init() ?*opaque {}; -extern fn verisimiser_free(?*opaque {}) void; -extern fn verisimiser_connect(?*opaque {}, u32, u64) u64; -extern fn verisimiser_disconnect(?*opaque {}, u64) void; -extern fn verisimiser_enable_dimension(?*opaque {}, u64, u32) c_int; -extern fn verisimiser_get_active_dimensions(?*opaque {}, u64) u32; -extern fn verisimiser_record_provenance(?*opaque {}, u64, u32, u64) c_int; -extern fn verisimiser_verify_provenance(?*opaque {}, u64) c_int; -extern fn verisimiser_provenance_length(?*opaque {}, u64) u64; -extern fn verisimiser_record_version(?*opaque {}, u64, u64, u32) c_int; -extern fn verisimiser_query_at_time(?*opaque {}, u64, u64) u64; -extern fn verisimiser_current_version(?*opaque {}, u64) u64; -extern fn verisimiser_measure_drift(?*opaque {}, u64) u64; -extern fn verisimiser_drift_score(?*opaque {}, u64) f64; -extern fn verisimiser_drift_category_score(?*opaque {}, u64, u32) f64; -extern fn verisimiser_vql_query(?*opaque {}, u64) u64; +extern fn verisimiser_init() ?*Handle; +extern fn verisimiser_free(?*Handle) void; +extern fn verisimiser_connect(?*Handle, u32, u64) u64; +extern fn verisimiser_disconnect(?*Handle, u64) void; +extern fn verisimiser_enable_dimension(?*Handle, u64, u32) c_int; +extern fn verisimiser_get_active_dimensions(?*Handle, u64) u32; +extern fn verisimiser_record_provenance(?*Handle, u64, u32, u64) c_int; +extern fn verisimiser_verify_provenance(?*Handle, u64) c_int; +extern fn verisimiser_provenance_length(?*Handle, u64) u64; +extern fn verisimiser_record_version(?*Handle, u64, u64, u32) c_int; +extern fn verisimiser_query_at_time(?*Handle, u64, u64) u64; +extern fn verisimiser_current_version(?*Handle, u64) u64; +extern fn verisimiser_measure_drift(?*Handle, u64) u64; +extern fn verisimiser_drift_score(?*Handle, u64) f64; +extern fn verisimiser_drift_category_score(?*Handle, u64, u32) f64; +extern fn verisimiser_vql_query(?*Handle, u64) u64; extern fn verisimiser_vql_free_result(u64) void; -extern fn verisimiser_get_string(?*opaque {}) ?[*:0]const u8; +extern fn verisimiser_get_string(?*Handle) ?[*:0]const u8; extern fn verisimiser_free_string(?[*:0]const u8) void; extern fn verisimiser_last_error() ?[*:0]const u8; extern fn verisimiser_version() [*:0]const u8; -extern fn verisimiser_is_initialized(?*opaque {}) u32; +extern fn verisimiser_is_initialized(?*Handle) u32; extern fn verisimiser_backend_supported(u32) u32; //============================================================================== @@ -38,10 +44,12 @@ extern fn verisimiser_backend_supported(u32) u32; //============================================================================== test "create and destroy VeriSimiser handle" { + // `orelse return error.InitFailed` already proves the handle is non-null; + // the body just confirms init succeeds and free (via defer) does not crash. const handle = verisimiser_init() orelse return error.InitFailed; defer verisimiser_free(handle); - try testing.expect(handle != null); + try testing.expectEqual(@as(u32, 1), verisimiser_is_initialized(handle)); } test "handle is initialized" { @@ -301,7 +309,7 @@ test "concurrent provenance operations" { defer verisimiser_free(handle); const ThreadContext = struct { - h: *opaque {}, + h: *Handle, entity_id: u64, };