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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
//! Provides TestType enum with shared code for acceptance tests
use std::{
env,
path::{Path, PathBuf},
time::Duration,
};
use indexmap::IndexSet;
use zebra_chain::parameters::Network;
use zebra_network::CacheDir;
use zebra_test::{command::NO_MATCHES_REGEX_ITER, prelude::*};
use zebrad::config::ZebradConfig;
use super::{
config::{default_test_config, random_known_rpc_port_config},
failure_messages::{
LIGHTWALLETD_EMPTY_ZEBRA_STATE_IGNORE_MESSAGES, LIGHTWALLETD_FAILURE_MESSAGES,
PROCESS_FAILURE_MESSAGES, ZEBRA_FAILURE_MESSAGES,
},
launch::{LIGHTWALLETD_DELAY, LIGHTWALLETD_FULL_SYNC_TIP_DELAY, LIGHTWALLETD_UPDATE_TIP_DELAY},
lightwalletd::LWD_CACHE_DIR,
sync::FINISH_PARTIAL_SYNC_TIMEOUT,
};
use TestType::*;
/// Returns the default platform-specific lightwalletd cache directory.
fn default_lwd_cache_dir() -> PathBuf {
// Mirror zebra_state::default_cache_dir() layout but for lwd
#[cfg(target_os = "macos")]
{
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home)
.join("Library")
.join("Caches")
.join("lwd");
}
}
#[cfg(target_os = "windows")]
{
if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") {
return PathBuf::from(local_app_data).join("lwd");
}
}
// Linux / Other: $XDG_CACHE_HOME/lwd or $HOME/.cache/lwd
if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
return PathBuf::from(xdg).join("lwd");
}
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home).join(".cache").join("lwd");
}
// Fallback: temp dir
std::env::temp_dir().join("lwd")
}
/// The type of integration test that we're running.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TestType {
/// Launch with an empty Zebra and lightwalletd state.
LaunchWithEmptyState {
/// Configures whether the test uses lightwalletd.
launches_lightwalletd: bool,
},
/// Do a full sync from an empty lightwalletd state.
///
/// This test requires a cached Zebra state.
//
// Only used with `--features=lightwalletd-grpc-tests`.
#[allow(dead_code)]
FullSyncFromGenesis {
/// Should the test allow a cached lightwalletd state?
///
/// If `false`, the test fails if the lightwalletd state is populated.
allow_lightwalletd_cached_state: bool,
},
/// Launch with a Zebra and lightwalletd state that might or might not be empty.
UseAnyState,
/// Sync to tip from a lightwalletd cached state.
///
/// This test requires a cached Zebra and lightwalletd state.
UpdateCachedState,
/// Launch `zebrad` and sync it to the tip, but don't launch `lightwalletd`.
///
/// If this test fails, the failure is in `zebrad` without RPCs or `lightwalletd`.
/// If it succeeds, but the RPC tests fail, the problem is caused by RPCs or `lightwalletd`.
///
/// This test requires a cached Zebra state.
UpdateZebraCachedStateNoRpc,
/// Launch `zebrad` and sync it to the tip, but don't launch `lightwalletd`.
///
/// This test requires a cached Zebra state.
#[allow(dead_code)]
UpdateZebraCachedStateWithRpc,
}
impl TestType {
/// Does this test need a Zebra cached state?
pub fn needs_zebra_cached_state(&self) -> bool {
// Handle the Zebra state directory based on the test type:
// - LaunchWithEmptyState: ignore the state directory
// - FullSyncFromGenesis, UpdateCachedState, UpdateZebraCachedStateNoRpc:
// skip the test if it is not available
match self {
LaunchWithEmptyState { .. } | UseAnyState => false,
FullSyncFromGenesis { .. }
| UpdateCachedState
| UpdateZebraCachedStateNoRpc
| UpdateZebraCachedStateWithRpc => true,
}
}
/// Does this test need a Zebra rpc server?
pub fn needs_zebra_rpc_server(&self) -> bool {
match self {
UpdateZebraCachedStateWithRpc | LaunchWithEmptyState { .. } => true,
UseAnyState
| UpdateZebraCachedStateNoRpc
| FullSyncFromGenesis { .. }
| UpdateCachedState => self.launches_lightwalletd(),
}
}
/// Does this test launch `lightwalletd`?
pub fn launches_lightwalletd(&self) -> bool {
match self {
UseAnyState | UpdateZebraCachedStateNoRpc | UpdateZebraCachedStateWithRpc => false,
FullSyncFromGenesis { .. } | UpdateCachedState => true,
LaunchWithEmptyState {
launches_lightwalletd,
} => *launches_lightwalletd,
}
}
/// Does this test need a `lightwalletd` cached state?
pub fn needs_lightwalletd_cached_state(&self) -> bool {
// Handle the lightwalletd state directory based on the test type:
// - LaunchWithEmptyState, UpdateZebraCachedStateNoRpc: ignore the state directory
// - FullSyncFromGenesis: use it if available, timeout if it is already populated
// - UpdateCachedState: skip the test if it is not available
match self {
LaunchWithEmptyState { .. }
| UseAnyState
| FullSyncFromGenesis { .. }
| UpdateZebraCachedStateNoRpc
| UpdateZebraCachedStateWithRpc => false,
UpdateCachedState => true,
}
}
/// Does this test allow a `lightwalletd` cached state, even if it is not required?
pub fn allow_lightwalletd_cached_state(&self) -> bool {
match self {
LaunchWithEmptyState { .. } => false,
FullSyncFromGenesis {
allow_lightwalletd_cached_state,
} => *allow_lightwalletd_cached_state,
UseAnyState
| UpdateCachedState
| UpdateZebraCachedStateNoRpc
| UpdateZebraCachedStateWithRpc => true,
}
}
/// Can this test create a new `LWD_CACHE_DIR` cached state?
pub fn can_create_lightwalletd_cached_state(&self) -> bool {
match self {
LaunchWithEmptyState { .. } | UseAnyState => false,
FullSyncFromGenesis { .. } | UpdateCachedState => true,
UpdateZebraCachedStateNoRpc | UpdateZebraCachedStateWithRpc => false,
}
}
/// Returns the Zebra state path for this test, if set.
#[allow(clippy::print_stderr)]
pub fn zebrad_state_path<S: AsRef<str>>(&self, test_name: S) -> Option<PathBuf> {
// Load the effective config (defaults + optional TOML + env overrides)
let test_name = test_name.as_ref();
let cfg = match ZebradConfig::load(None) {
Ok(c) => c,
Err(_) => {
eprintln!(
"skipped {test_name:?} {self:?} lightwalletd test, \
could not load Zebra configuration",
);
return None;
}
};
// Skip if the configured state is ephemeral; otherwise use the configured cache_dir
if cfg.state.ephemeral {
eprintln!(
"skipped {test_name:?} {self:?} lightwalletd test, \
configure a persistent state cache (e.g., set [state].ephemeral=false and [state].cache_dir)",
);
return None;
}
Some(cfg.state.cache_dir)
}
/// Returns a Zebra config for this test.
///
/// `replace_cache_dir` replaces any cached or ephemeral state.
///
/// Returns `None` if the test should be skipped,
/// and `Some(Err(_))` if the config could not be created.
pub fn zebrad_config<Str: AsRef<str>>(
&self,
test_name: Str,
use_internet_connection: bool,
replace_cache_dir: Option<&Path>,
network: &Network,
) -> Option<Result<ZebradConfig>> {
let config = if self.needs_zebra_rpc_server() {
// This is what we recommend our users configure.
random_known_rpc_port_config(true, network)
} else {
default_test_config(network)
};
let mut config = match config {
Ok(config) => config,
Err(error) => return Some(Err(error)),
};
// We want to run multi-threaded RPCs, if we're using them
if self.launches_lightwalletd() {
// Automatically runs one thread per available CPU core
config.rpc.parallel_cpu_threads = 0;
}
if !use_internet_connection {
config.network.initial_mainnet_peers = IndexSet::new();
config.network.initial_testnet_peers = IndexSet::new();
// Avoid reusing cached peers from disk when we're supposed to be a disconnected instance
config.network.cache_dir = CacheDir::disabled();
// Activate the mempool immediately by default
config.mempool.debug_enable_at_height = Some(0);
}
// If we have a cached state, or we don't want to be ephemeral, update the config to use it
if replace_cache_dir.is_some() || self.needs_zebra_cached_state() {
let zebra_state_path = replace_cache_dir
.map(|path| path.to_owned())
.or_else(|| self.zebrad_state_path(test_name))?;
config.state.ephemeral = false;
config.state.cache_dir = zebra_state_path;
// And reset the concurrency to the default value
config.sync.checkpoint_verify_concurrency_limit =
zebrad::components::sync::DEFAULT_CHECKPOINT_CONCURRENCY_LIMIT;
}
Some(Ok(config))
}
/// Returns the `lightwalletd` state path for this test, if set, and if allowed for this test.
pub fn lightwalletd_state_path<S: AsRef<str>>(&self, test_name: S) -> Option<PathBuf> {
let test_name = test_name.as_ref();
// Can this test type use a lwd cached state, or create/update one?
let use_or_create_lwd_cache =
self.allow_lightwalletd_cached_state() || self.can_create_lightwalletd_cached_state();
if !self.launches_lightwalletd() || !use_or_create_lwd_cache {
tracing::info!(
"running {test_name:?} {self:?} lightwalletd test, \
ignoring any cached state in the {LWD_CACHE_DIR:?} environment variable",
);
return None;
}
// Explicit override via env var takes precedence
if let Some(path) = env::var_os(LWD_CACHE_DIR) {
return Some(path.into());
}
// Otherwise, try a deterministic platform default
let default_path = default_lwd_cache_dir();
if self.needs_lightwalletd_cached_state() {
// Only run if a cached state actually exists at the default path
if default_path.exists() {
Some(default_path)
} else {
tracing::warn!(
?default_path,
"skipped {test_name:?} {self:?} lightwalletd test: no cached state found at default path",
);
None
}
} else if self.allow_lightwalletd_cached_state() {
// Use default if present; otherwise run without cached state
if default_path.exists() {
Some(default_path)
} else {
tracing::warn!(
?default_path,
"running {test_name:?} {self:?} lightwalletd test without cached state (no default found)",
);
None
}
} else if self.can_create_lightwalletd_cached_state() {
// Ensure the directory exists so FullSyncFromGenesis can populate it.
if let Err(error) = std::fs::create_dir_all(&default_path) {
tracing::warn!(
?default_path,
?error,
"failed to create default lightwalletd cache directory; using an ephemeral temp dir instead",
);
None
} else {
Some(default_path)
}
} else {
None
}
}
/// Returns the `zebrad` timeout for this test type.
pub fn zebrad_timeout(&self) -> Duration {
match self {
LaunchWithEmptyState { .. } | UseAnyState => LIGHTWALLETD_DELAY,
FullSyncFromGenesis { .. } => LIGHTWALLETD_FULL_SYNC_TIP_DELAY,
UpdateCachedState | UpdateZebraCachedStateNoRpc => LIGHTWALLETD_UPDATE_TIP_DELAY,
UpdateZebraCachedStateWithRpc => FINISH_PARTIAL_SYNC_TIMEOUT,
}
}
/// Returns the `lightwalletd` timeout for this test type.
#[track_caller]
pub fn lightwalletd_timeout(&self) -> Duration {
if !self.launches_lightwalletd() {
panic!("lightwalletd must not be launched in the {self:?} test");
}
// We use the same timeouts for zebrad and lightwalletd,
// because the tests check zebrad and lightwalletd concurrently.
match self {
LaunchWithEmptyState { .. } | UseAnyState => LIGHTWALLETD_DELAY,
FullSyncFromGenesis { .. } => LIGHTWALLETD_FULL_SYNC_TIP_DELAY,
UpdateCachedState | UpdateZebraCachedStateNoRpc | UpdateZebraCachedStateWithRpc => {
LIGHTWALLETD_UPDATE_TIP_DELAY
}
}
}
/// Returns Zebra log regexes that indicate the tests have failed,
/// and regexes of any failures that should be ignored.
pub fn zebrad_failure_messages(&self) -> (Vec<String>, Vec<String>) {
let mut zebrad_failure_messages: Vec<String> = ZEBRA_FAILURE_MESSAGES
.iter()
.chain(PROCESS_FAILURE_MESSAGES)
.map(ToString::to_string)
.collect();
if self.needs_zebra_cached_state() {
// Fail if we need a cached Zebra state, but it's empty
zebrad_failure_messages.push("loaded Zebra state cache .*tip.*=.*None".to_string());
}
if matches!(*self, LaunchWithEmptyState { .. }) {
// Fail if we need an empty Zebra state, but it has blocks
zebrad_failure_messages
.push(r"loaded Zebra state cache .*tip.*=.*Height\([1-9][0-9]*\)".to_string());
}
let zebrad_ignore_messages = Vec::new();
(zebrad_failure_messages, zebrad_ignore_messages)
}
/// Returns `lightwalletd` log regexes that indicate the tests have failed,
/// and regexes of any failures that should be ignored.
#[track_caller]
pub fn lightwalletd_failure_messages(&self) -> (Vec<String>, Vec<String>) {
if !self.launches_lightwalletd() {
panic!("lightwalletd must not be launched in the {self:?} test");
}
let mut lightwalletd_failure_messages: Vec<String> = LIGHTWALLETD_FAILURE_MESSAGES
.iter()
.chain(PROCESS_FAILURE_MESSAGES)
.map(ToString::to_string)
.collect();
// Zebra state failures
if self.needs_zebra_cached_state() {
// Fail if we need a cached Zebra state, but it's empty
lightwalletd_failure_messages.push("no chain tip available yet".to_string());
}
// lightwalletd state failures
if self.needs_lightwalletd_cached_state() {
// Fail if we need a cached lightwalletd state, but it isn't near the tip
lightwalletd_failure_messages.push("Found [0-9]{1,6} blocks in cache".to_string());
}
if !self.allow_lightwalletd_cached_state() {
// Fail if we need an empty lightwalletd state, but it has blocks
lightwalletd_failure_messages.push("Found [1-9][0-9]* blocks in cache".to_string());
}
let lightwalletd_ignore_messages = if matches!(*self, LaunchWithEmptyState { .. }) {
LIGHTWALLETD_EMPTY_ZEBRA_STATE_IGNORE_MESSAGES.iter()
} else {
NO_MATCHES_REGEX_ITER.iter()
}
.map(ToString::to_string)
.collect();
(lightwalletd_failure_messages, lightwalletd_ignore_messages)
}
}