Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all ci fmt fmt-check lint pre-pr pre-pr-snapshots pre-pr-js pre-pr-selftest perf-gate perf-gate-check perf-gate-remote bench-engine-remote bench-sessions-remote soak soak-remote secret-check install-gitleaks install-hooks uninstall-hooks hooks-test bench-genbi bench-sessions test test-fast test-release unit-test integration-test oracle-test e2e-test e2e-reactive e2e-update test-affected doc doc-check check build build-release clean fix release snapshot-test test-all ci-test-all flush-dev docker docker-run docker-deploy docker-deploy-no-tls docker-logs docker-stop k8s-check deny python-test python-test-live python-test-examples vc-gate js-test js-test-live python-sdk-live front-build front-deploy gui-build run run-server demo coverage view-coverage static-analysis
.PHONY: all ci fmt fmt-check lint pre-pr pre-pr-snapshots pre-pr-js pre-pr-selftest perf-gate perf-gate-check perf-gate-remote bench-engine-remote bench-sessions-remote soak soak-remote secret-check install-gitleaks install-hooks uninstall-hooks hooks-test bench-genbi bench-sessions test test-fast test-release unit-test xfail-list integration-test oracle-test e2e-test e2e-reactive e2e-update test-affected doc doc-check check build build-release clean fix release snapshot-test test-all ci-test-all flush-dev docker docker-run docker-deploy docker-deploy-no-tls docker-logs docker-stop k8s-check deny python-test python-test-live python-test-examples vc-gate js-test js-test-live python-sdk-live front-build front-deploy gui-build run run-server demo coverage view-coverage static-analysis

SHELL := /bin/bash

Expand Down Expand Up @@ -295,6 +295,8 @@ ci-test-all:
printf " | %-14s | %-48s |\n" "Python Tests" "$${PY_PASSED:-0} passed, $${PY_FAILED:-0} failed"; \
printf " | %-14s | %-48s |\n" "JS/TS Tests" "$${JS_PASSED:-0} passed, $${JS_FAILED:-0} failed"; \
echo ""; \
$(MAKE) --no-print-directory xfail-list; \
echo ""; \
echo "==========================================="; \
if [ $$FAILURES -ne 0 ]; then \
echo "SOME CHECKS FAILED ($$FAILURES)"; \
Expand All @@ -308,9 +310,27 @@ ci: check ci-test-all

# Individual Test Tiers

# Expected failures (inputlayer_testkit::KnownDefect) append their XFAIL line
# to this file; the test targets print the list when the run ends, pass or fail.
XFAIL_LOG ?= target/xfail.log
define print_xfails
@if [ -s $(XFAIL_LOG) ]; then \
echo "Expected failures still open ($$(sort -u $(XFAIL_LOG) | wc -l)):"; \
sort -u $(XFAIL_LOG); \
else \
echo "Expected failures still open: none"; \
fi
endef

# Tier 1: Unit tests (cargo test - includes all #[test] functions)
unit-test:
cargo test --workspace --all-features
@mkdir -p $(dir $(XFAIL_LOG)) && : > $(XFAIL_LOG)
INPUTLAYER_XFAIL_LOG=$(abspath $(XFAIL_LOG)) cargo test --workspace --all-features; \
status=$$?; $(MAKE) --no-print-directory xfail-list; exit $$status

# The expected-failure list of the last test run.
xfail-list:
$(print_xfails)

# Tier 2: Integration tests only
integration-test:
Expand Down Expand Up @@ -342,8 +362,11 @@ E2E_REACTIVE_SAMPLES ?= target/e2e-reactive
e2e-reactive:
rm -rf $(E2E_REACTIVE_SAMPLES)
cargo test --release -p inputlayer-testkit
@mkdir -p $(dir $(XFAIL_LOG)) && : > $(XFAIL_LOG)
INPUTLAYER_REACTIVE_SAMPLES_DIR=$(abspath $(E2E_REACTIVE_SAMPLES)) \
cargo test --release --test scenarios -- --nocapture --include-ignored
INPUTLAYER_XFAIL_LOG=$(abspath $(XFAIL_LOG)) \
cargo test --release --test scenarios -- --nocapture --include-ignored; \
status=$$?; $(MAKE) --no-print-directory xfail-list; exit $$status
@ls $(E2E_REACTIVE_SAMPLES)/*.jsonl >/dev/null || { echo "ERROR: no latency samples written"; exit 1; }

# Regenerate snapshot .iql.out files (sequential mode)
Expand Down
45 changes: 40 additions & 5 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,11 @@ INPUTLAYER_SCENARIO_VIEWS=maintained cargo test --test scenarios # refused unti

Modules: `reactive`, `stream`, `delivery`, `saturation` and `wire` (the agent
path below), `harness` (the testkit pieces scenarios build on, checked against
a real engine), and the catalogue scenarios `lifecycle`, `claims`,
`retraction`, `restart` and `tenancy` (below). The suite runs in about 25 s in
debug on 4 cores. `make test-all` and `make ci-test-all` run it once, in
a real engine), the catalogue scenarios `lifecycle`, `claims`, `retraction`,
`restart` and `tenancy` (below), and the scenarios that hold the milestone 9
contract as expected failures, `reads`, `consistency`, `fanout`,
`subscribe_rules`, `generations` and `burst` (below). The suite runs in about
25 s in debug on 4 cores. `make test-all` and `make ci-test-all` run it once, in
release through `make e2e-reactive`: their debug unit stage runs every other
workspace test without the scenarios binary.

Expand Down Expand Up @@ -178,6 +180,8 @@ workspace test without the scenarios binary.
`Expect` that can also pin `expect_epoch`) return the engine's `Refusal`
with its structured `code` instead of a violation, for scenarios that assert
how a request is refused.
`try_execute_at` does the same for a read `at` a revision, and
`QueryResult::views_at` is a write acknowledgement's `views_at` (V14 #320).
- `Fixture::shop_pack(Size)` installs one knowledge graph whose rules cover
join, comparison, negation, recursion, negation over recursion and an
aggregate (`Size::Vector` adds embeddings, the `emb_idx` HNSW index and the
Expand Down Expand Up @@ -231,6 +235,33 @@ permission refusal carries no structured `code` today; S15 asserts
`access_denied` as an expected failure (`KnownDefect`, #364), so it fails
loudly once the code is sent.

### Expected-failure scenarios

The scenarios whose contract is milestone 9's: each runs the part that holds
today as a required pass and asserts the rest as an expected failure
(`Reproduction::Deterministic`) naming the issue that makes it hold. When that
issue lands the XFAIL turns into an XPASS, which fails the run until the
marker is removed and the part becomes required.

| Scenario | Test | Required today | Expected failure (issue) |
|----------|------|----------------|--------------------------|
| S2 one-row view read is a lookup | `reads::s2_one_row_view_read_is_a_lookup` | rows of 50 `eligible` reads and the recursive `related` | no rule evaluated, every read served from a view (V1 #308 counters, then V9 #315) |
| S3 ad-hoc query joins facts and views | `reads::s3_ad_hoc_query_joins_facts_and_views` | rows equal the question asked of base relations; a session rule joins the `offer` view | only the query's own work (V1 #308, then V9 #315) |
| S4 subscription and query agree at a revision | `consistency::s4_subscription_and_query_agree_at_a_revision` | exact deltas at each write's revision | query replies carry `revision` (V9 #315); `at: r` reads answer at r, `at: s` equals the snapshot, a compacted revision is refused `revision_compacted` (V13 #316) |
| S5 many agents, each hears its key | `fanout::s5_many_agents_each_hear_only_their_key` | 200 keyed and 10 unkeyed agents: exactly the touched keys and the unkeyed agents hear a write, nobody hears a write that changes no row | one maintenance pass per commit, no rule evaluated by refreshes (V1 #308, then V10 #317) |
| S6 subscribe only to a deployed rule | `subscribe_rules::s6_subscribe_to_a_query_is_refused` | the same bodies answer as queries; a rule subscription still works | `.subscribe` of a join or a filtered view refused `invalid_request`, `deploy a rule first` (V11 #318) |
| S8 rule replaced while subscribed | `generations::s8_rule_replaced_while_subscribed` | one exact old-vs-new delta at the replacement's revision, concurrent reads see one generation, `.rule remove` is one exact delta | dropping `link` under `related` refused with `conflict` naming the dependents (V7 #314) |
| S17 write burst, read-your-writes | `burst::s17_write_burst_reads_its_writes` | 4 writers x 100 writes, each read back on two connections; the agent hears every row once, through a rule rebuild | the rebuild's acknowledgement carries `views_at` (V14 #320) |

A check that runs into two issues one after the other (the counters do not
exist before V1, and once they do they show the work V9 or V10 removes) is
judged with `KnownDefect::judge_first`, so each stage is an XFAIL of its own
issue. The counter checks and S4's `revision` are asserted for the target
contract, which the default `recompute` mode never reaches; until V20 removes
it, a `recompute` run of the matrix keeps those markers when the `maintained`
run flips. Latency halves (S2's p50 at lab size, S17's 1,000-write shape) are
the perf tier's B1/B2 and B15 on the benchmark host.

### Reactive agent path

The `reactive` module drives the supported agent path end to end: each test
Expand Down Expand Up @@ -290,13 +321,17 @@ nightly workflow exists, `make e2e-reactive` stands in for it and passes
quarantined for a rare engine hang in `.index create` (#377).

Tracked defects run as **expected failures** through
`inputlayer_testkit::KnownDefect`, naming the issue that fixes them (today:
`inputlayer_testkit::KnownDefect`, naming the issue that fixes them (the
milestone 9 ones are listed under Expected-failure scenarios;
`harness::counters_scrape_the_running_engine`, #308;
`tenancy::s15_tenants_and_scoped_keys_are_isolated`, #364;
`restart::s12_restart_mid_scenario_preserves_revisions`, #380). Each asserts the
correct contract; its own violation passes as `XFAIL`, any other violation
fails, and a holding contract fails as `XPASS` so the marker is removed and
the scenario becomes required when the issue lands.
the scenario becomes required when the issue lands. `make unit-test`,
`make ci-test-all` and `make e2e-reactive` print the open `XFAIL` list when
the run ends (`make xfail-list` prints the last run's): `KnownDefect` appends
each line to the file named by `INPUTLAYER_XFAIL_LOG`.

Not yet covered by this pipeline (each is added when the work that enables it
lands):
Expand Down
18 changes: 18 additions & 0 deletions testkit/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ pub struct QueryResult {
/// The reply's `revision`: for a write, the revision its changes are
/// visible at. Queries carry none until V9 (#315).
pub revision: Option<u64>,
/// The reply's `views_at`: for a write acknowledged before the deployed
/// views include it (a rule rebuild in progress), the revision they have
/// reached (V14 #320). `None` when the views include the write.
pub views_at: Option<u64>,
/// Effective counts of the fact statements a write committed
/// (`statements`), in program order.
pub statements: Vec<Value>,
Expand Down Expand Up @@ -109,6 +113,7 @@ impl QueryResult {
at,
subscribed_revision: header["subscribed"]["revision"].as_u64(),
revision: header["revision"].as_u64(),
views_at: header["views_at"].as_u64(),
statements: header["statements"].as_array().cloned().unwrap_or_default(),
}
}
Expand Down Expand Up @@ -479,6 +484,19 @@ impl WsClient {
self.result().await
}

/// [`Self::execute_at`] returning the engine's refusal with its code
/// (`revision_compacted` once V13 #316 retains a window) instead of a
/// violation.
pub async fn try_execute_at(
&mut self,
program: &str,
revision: u64,
) -> Checked<Result<QueryResult, Refusal>> {
self.send(json!({"type": "execute", "program": program, "at": revision}))
.await?;
self.try_result().await
}

/// Send `program` without waiting; returns its request id (for
/// [`Self::send_cancel`]). Read its reply with [`Self::result`].
pub async fn send_execute(&mut self, program: &str) -> Checked<String> {
Expand Down
4 changes: 4 additions & 0 deletions testkit/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ pub enum Violation {
Transport(String),
/// The engine does not export a counter the assertion needs.
NotMeasurable(String),
/// The engine's counters show work the step must not cause, such as a
/// read of a deployed rule evaluating the rule instead of reading its view.
UnexpectedWork(String),
}

impl fmt::Display for Violation {
Expand Down Expand Up @@ -143,6 +146,7 @@ impl fmt::Display for Violation {
}
Self::Transport(detail) => write!(f, "transport: {detail}"),
Self::NotMeasurable(counter) => write!(f, "counter {counter} is not exported"),
Self::UnexpectedWork(detail) => write!(f, "unexpected work: {detail}"),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion testkit/src/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl Fixture {
/// Create the knowledge graph on `engine` and run every statement.
///
/// Consecutive statements commit together as one program of up to
/// [`PROGRAM_BYTES`]; a meta command (`.index create ...`) runs alone.
/// `PROGRAM_BYTES` (256 KiB); a meta command (`.index create ...`) runs alone.
pub async fn install(&self, engine: &Engine) -> Checked<()> {
let mut admin = WsClient::connect(engine, "default").await?;
admin
Expand Down
86 changes: 83 additions & 3 deletions testkit/src/known_defect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@
//! A [`Reproduction::Racy`] defect depends on thread timing: its probe retries
//! within a bounded budget and a run that does not reproduce it passes with a
//! `NOT REPRODUCED` note instead of XPASS, so the suite never flakes.
//!
//! When [`XFAIL_LOG_ENV`] names a file, every XFAIL and NOT REPRODUCED line
//! is also appended to it, so a run can print the list of open expected
//! failures at its end (`make unit-test`, `make e2e-reactive`).

use std::io::Write;

use crate::contract::{Checked, Violation};

/// Environment variable naming the file expected-failure lines append to.
pub const XFAIL_LOG_ENV: &str = "INPUTLAYER_XFAIL_LOG";

/// Whether a defect shows on every run.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reproduction {
Expand Down Expand Up @@ -44,20 +53,62 @@ impl KnownDefect {
} = self;
match outcome {
Err(violation) if (self.signature)(&violation) => {
println!("XFAIL [{plan_item}] {summary}: {violation}");
record(&format!("XFAIL [{plan_item}] {summary}: {violation}"));
}
Err(violation) => panic!(
"[{plan_item}] expected failure '{summary}' hit an unrelated violation: {violation}"
),
Ok(()) if self.reproduction == Reproduction::Racy => println!(
Ok(()) if self.reproduction == Reproduction::Racy => record(&format!(
"NOT REPRODUCED [{plan_item}] {summary}: racy defect not observed this run"
),
)),
Ok(()) => panic!(
"XPASS [{plan_item}] {summary}: the contract now holds. Remove this \
known-defect marker so the scenario becomes a required pass."
),
}
}

/// Judge `outcome` against defects that stand in front of one another:
/// `defects[0]` is the first one the check runs into, and once it is
/// fixed the next one's violation shows. The first defect whose signature
/// matches is the expected failure; when the contract holds, the last
/// defect judges it (XPASS unless it is racy).
///
/// # Panics
/// When `defects` is empty, on XPASS, or on a violation none of them
/// produces.
pub fn judge_first(defects: &[Self], outcome: Checked<()>) {
let last = defects.last().expect("at least one known defect");
let defect = match &outcome {
Err(violation) => defects
.iter()
.find(|defect| (defect.signature)(violation))
.unwrap_or(last),
Ok(()) => last,
};
defect.judge(outcome);
}
}

/// Print an expected-failure line and append it to the [`XFAIL_LOG_ENV`] file.
/// This crate's own unit tests only print, so the list holds real XFAILs.
fn record(line: &str) {
println!("{line}");
if cfg!(test) {
return;
}
let Some(path) = std::env::var_os(XFAIL_LOG_ENV) else {
return;
};
let appended = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.and_then(|mut file| file.write_all(format!("{line}\n").as_bytes()));
if let Err(e) = appended {
let path = std::path::Path::new(&path).display();
eprintln!("cannot append to {XFAIL_LOG_ENV} file {path}: {e}");
}
}

#[cfg(test)]
Expand Down Expand Up @@ -96,4 +147,33 @@ mod tests {
fn test_other_violation_is_real_failure() {
DEFECT.judge(Err(Violation::Transport("closed".into())));
}

const LATER: KnownDefect = KnownDefect {
plan_item: "X01",
summary: "the defect behind the first one",
signature: |v| matches!(v, Violation::UnexpectedWork(_)),
reproduction: Reproduction::Deterministic,
};

#[test]
fn test_first_defect_in_front_is_expected_failure() {
KnownDefect::judge_first(&[DEFECT, LATER], Err(Violation::Timeout("delta".into())));
}

#[test]
fn test_defect_behind_shows_once_the_first_is_fixed() {
KnownDefect::judge_first(&[DEFECT, LATER], Err(Violation::UnexpectedWork("x".into())));
}

#[test]
#[should_panic(expected = "XPASS [X01]")]
fn test_holding_contract_is_unexpected_pass_of_the_last_defect() {
KnownDefect::judge_first(&[DEFECT, LATER], Ok(()));
}

#[test]
#[should_panic(expected = "[X01] expected failure")]
fn test_violation_of_no_defect_is_real_failure() {
KnownDefect::judge_first(&[DEFECT, LATER], Err(Violation::Transport("closed".into())));
}
}
2 changes: 1 addition & 1 deletion testkit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ pub use contract::{Checked, Violation};
pub use counters::Counters;
pub use engine::{Engine, EngineBuilder, Mode, Replication};
pub use fixture::{Fixture, Size};
pub use known_defect::{KnownDefect, Reproduction};
pub use known_defect::{KnownDefect, Reproduction, XFAIL_LOG_ENV};
pub use metrics::SampleLog;
Loading
Loading