1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//! Long-run reliability smoke suite (#220).
//!
//! Imports a large synthetic graph, runs representative queries, and reopens
//! repeatedly to verify durability and correctness invariants.
//!
//! Skipped in standard CI. Run manually:
//! cargo test --test smoke_test -- --include-ignored --nocapture
//!
//! Nightly CI runs this via .github/workflows/smoke.yml.
#![cfg(not(target_arch = "wasm32"))]
use minigraf::db::Minigraf;
use minigraf::{BindValue, QueryResult, Value};
fn count_results(r: QueryResult) -> usize {
match r {
QueryResult::QueryResults { results, .. } => results.len(),
_ => 0,
}
}
/// The full long-haul smoke test.
///
/// Workload:
/// - 500 entities × 10 attributes = 5,000 facts
/// - 10 reopen + checkpoint cycles
/// - Representative queries after each cycle (basic, temporal, recursive-capable, aggregation)
/// - Invariants checked after each cycle
#[test]
#[ignore]
fn smoke_large_graph_10_cycles() {
eprintln!("smoke: starting long-haul smoke suite");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("smoke.graph");
const NUM_ENTITIES: usize = 500;
const NUM_CYCLES: usize = 10;
// ── Phase 1: Load 5,000 facts ─────────────────────────────────────────────
eprintln!("smoke: loading {} entities × 10 attributes", NUM_ENTITIES);
{
let db = Minigraf::open(&path).unwrap();
// Batch transact in groups of 50 entities to keep individual transactions small.
for batch_start in (0..NUM_ENTITIES).step_by(50) {
let batch_end = (batch_start + 50).min(NUM_ENTITIES);
let facts: String = (batch_start..batch_end)
.flat_map(|i| {
vec![
format!(r#"[:entity{i} :name "entity-{i}"]"#),
format!(r#"[:entity{i} :index {i}]"#),
format!(r#"[:entity{i} :bucket {}]"#, i % 10),
format!(r#"[:entity{i} :active {}]"#, i % 3 != 0),
format!(r#"[:entity{i} :score {}]"#, (i * 7) % 100),
format!(r#"[:entity{i} :group {}]"#, i % 5),
format!(r#"[:entity{i} :tier {}]"#, i % 4),
format!(r#"[:entity{i} :region "region-{}"]"#, i % 8),
format!(r#"[:entity{i} :version 1]"#),
format!(r#"[:entity{i} :label "label-{}"]"#, i % 20),
]
})
.collect::<Vec<_>>()
.join(" ");
db.execute(&format!("(transact [{facts}])")).unwrap();
}
db.checkpoint().unwrap();
}
eprintln!("smoke: initial load complete");
// ── Phase 2: 10 reopen + query + checkpoint cycles ───────────────────────
for cycle in 0..NUM_CYCLES {
eprintln!("smoke: cycle {}/{}", cycle + 1, NUM_CYCLES);
let db = Minigraf::open(&path).unwrap();
// Invariant 1: all 500 entities must be queryable by :name.
let n_names = count_results(
db.execute("(query [:find ?e :where [?e :name ?n]])")
.unwrap(),
);
assert_eq!(
n_names, NUM_ENTITIES,
"cycle {cycle}: expected {NUM_ENTITIES} entities by :name, got {n_names}"
);
// Invariant 2: :index values must be unique per entity.
let n_index = count_results(
db.execute("(query [:find ?e ?i :where [?e :index ?i]])")
.unwrap(),
);
assert_eq!(
n_index, NUM_ENTITIES,
"cycle {cycle}: expected {NUM_ENTITIES} :index facts, got {n_index}"
);
// Invariant 3: bucket distribution — 10 buckets, 50 entities each.
for b in 0..10usize {
let n_bucket = count_results(
db.execute(&format!("(query [:find ?e :where [?e :bucket {b}]])"))
.unwrap(),
);
assert_eq!(
n_bucket, 50,
"cycle {cycle}: bucket {b} should have 50 entities, got {n_bucket}"
);
}
// Invariant 4: active entities (index % 3 != 0) = 333.
let n_active = count_results(
db.execute("(query [:find ?e :where [?e :active true]])")
.unwrap(),
);
// 500 entities, active if i % 3 != 0. Inactive: i=0,3,6,...,498 → 167 entities.
// ceil(NUM_ENTITIES / 3) = number of multiples of 3 in [0, NUM_ENTITIES).
let n_inactive = (NUM_ENTITIES + 2) / 3;
let expected_active = NUM_ENTITIES - n_inactive;
assert_eq!(
n_active, expected_active,
"cycle {cycle}: active entity count mismatch; got {n_active}"
);
// Invariant 5: temporal query (:as-of 1) returns data from tx 1.
let n_temporal = count_results(
db.execute("(query [:find ?e :where [?e :version 1]] :as-of 1)")
.unwrap_or(QueryResult::QueryResults {
vars: vec![],
results: vec![],
}),
);
// Should return some results — exact count depends on tx ordering.
// Just verify it doesn't error and returns > 0 for tx 1.
assert!(
n_temporal > 0,
"cycle {cycle}: temporal query :as-of 1 returned no results"
);
// Invariant 6: prepared query returns consistent results.
let prep = db
.prepare("(query [:find ?e :where [?e :region $region]])")
.unwrap();
let r0 = prep
.execute(&[(
"region",
BindValue::Val(Value::String("region-0".to_string())),
)])
.unwrap();
let n_region0 = count_results(r0);
// region-0: entities where i % 8 == 0 → 0,8,16,...,496 → 63 entities.
assert_eq!(
n_region0, 63,
"cycle {cycle}: prepared query for region-0 returned {n_region0}"
);
// Invariant 7: write a new fact each cycle.
db.execute(&format!(
r#"(transact [[:cycle{cycle} :cycle-fact {cycle}]])"#
))
.unwrap();
// Checkpoint to flush all to disk.
db.checkpoint().unwrap();
eprintln!(
"smoke: cycle {}/{} complete — all invariants passed",
cycle + 1,
NUM_CYCLES
);
}
// ── Phase 3: Final reopen and full invariant check ────────────────────────
eprintln!("smoke: final reopen and invariant verification");
let db_final = Minigraf::open(&path).unwrap();
let n_final = count_results(
db_final
.execute("(query [:find ?e :where [?e :name ?n]])")
.unwrap(),
);
assert_eq!(
n_final, NUM_ENTITIES,
"final reopen: expected {NUM_ENTITIES} entities, got {n_final}"
);
// Cycle facts from all 10 cycles must be present.
let n_cycles = count_results(
db_final
.execute("(query [:find ?e :where [?e :cycle-fact ?c]])")
.unwrap(),
);
assert_eq!(
n_cycles, NUM_CYCLES,
"final reopen: expected {NUM_CYCLES} cycle facts, got {n_cycles}"
);
eprintln!("smoke: all invariants passed across {NUM_CYCLES} cycles");
}