1use std::collections::{HashMap, HashSet};
4use std::path::PathBuf;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU32, Ordering};
7
8use thruput_node_supervisor::{HogdDevConfig, NodeSupervisorConfig, NodeSupervisorHandle};
9use tokio::sync::mpsc;
10use tokio::task::JoinSet;
11
12use crate::account::Account;
13use crate::agent::{Agent, AgentContext, AgentId};
14use crate::artifact::{ArtifactKind, ArtifactSpec, Registry};
15use crate::clock::Clock;
16use crate::error::{AgentFailure, AgentFailures, Result, TestkitError};
17use crate::mailbox::{Envelope, Mailbox};
18use crate::manifest::{AccountSource, load_runtime_manifest};
19use crate::network::{LocalNetworkConfig, Network, NetworkConfig};
20use crate::nonce::NonceNamespace;
21use crate::rng::AgentRng;
22
23#[derive(Debug)]
25pub struct ScenarioBuilder {
26 seed: String,
27 network: Option<NetworkConfig>,
28 artifacts: Vec<ArtifactSpec>,
29 use_bootstrap_accounts: bool,
30 accounts: usize,
31}
32
33impl ScenarioBuilder {
34 #[must_use]
40 pub fn bootstrap_accounts(mut self) -> Self {
41 self.use_bootstrap_accounts = true;
42 self
43 }
44
45 #[must_use]
48 pub fn accounts(mut self, count: usize) -> Self {
49 self.accounts = count;
50 self
51 }
52
53 #[must_use]
55 pub fn network(mut self, network: NetworkConfig) -> Self {
56 self.network = Some(network);
57 self
58 }
59
60 #[must_use]
64 pub fn artifact(mut self, name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
65 self.artifacts.push(ArtifactSpec::Path {
66 name: name.into(),
67 path: path.into(),
68 kind: None,
69 });
70 self
71 }
72
73 #[must_use]
75 pub fn artifact_with_kind(
76 mut self,
77 name: impl Into<String>,
78 path: impl Into<PathBuf>,
79 kind: ArtifactKind,
80 ) -> Self {
81 self.artifacts.push(ArtifactSpec::Path {
82 name: name.into(),
83 path: path.into(),
84 kind: Some(kind),
85 });
86 self
87 }
88
89 #[must_use]
92 pub fn artifact_bytes(
93 mut self,
94 name: impl Into<String>,
95 bytes: impl Into<Vec<u8>>,
96 _kind: ArtifactKind,
97 ) -> Self {
98 self.artifacts.push(ArtifactSpec::Bytes {
99 name: name.into(),
100 bytes: bytes.into(),
101 });
102 self
103 }
104
105 pub async fn build(self) -> Result<Scenario> {
107 let config = self.network.ok_or(TestkitError::NoNetwork)?;
108 let artifacts = Arc::new(Registry::from_specs(self.artifacts)?);
109 let (network, accounts, guard) = acquire_network(
110 config,
111 &self.seed,
112 self.use_bootstrap_accounts,
113 self.accounts,
114 )
115 .await?;
116 Ok(Scenario {
117 seed: self.seed,
118 network,
119 clock: Clock::new(),
120 accounts,
121 artifacts,
122 runs: AtomicU32::new(0),
123 guard,
124 })
125 }
126}
127
128#[derive(Debug)]
134pub struct Scenario {
135 seed: String,
136 network: Network,
137 clock: Clock,
138 accounts: AccountSource,
139 artifacts: Arc<Registry>,
140 runs: AtomicU32,
143 guard: NetworkGuard,
144}
145
146impl Scenario {
147 #[must_use]
149 pub fn builder(seed: impl Into<String>) -> ScenarioBuilder {
150 ScenarioBuilder {
151 seed: seed.into(),
152 network: None,
153 artifacts: Vec::new(),
154 use_bootstrap_accounts: false,
155 accounts: 10,
156 }
157 }
158
159 pub fn network(&self) -> &Network {
161 &self.network
162 }
163
164 pub fn clock(&self) -> &Clock {
166 &self.clock
167 }
168
169 pub async fn run_agents<I>(&self, agents: I) -> Result<ScenarioReport>
181 where
182 I: IntoIterator<Item = Agent>,
183 {
184 let agents: Vec<Agent> = agents.into_iter().collect();
185 let count = agents.len();
186
187 let names: Vec<Arc<str>> = agents.iter().map(Agent::name_arc).collect();
188 validate_unique_agent_names(&names)?;
189
190 let mut test_accounts = self.accounts.accounts()?;
191 if count > test_accounts.len() {
192 return Err(TestkitError::NotEnoughAccounts {
193 needed: count,
194 available: test_accounts.len(),
195 });
196 }
197 test_accounts.truncate(count);
198
199 let namespace =
202 NonceNamespace::for_run(&self.seed, self.runs.fetch_add(1, Ordering::Relaxed));
203
204 let mut senders = Vec::with_capacity(count);
205 let mut receivers = Vec::with_capacity(count);
206 for _ in 0..count {
207 let (tx, rx) = mpsc::unbounded_channel::<Envelope>();
208 senders.push(tx);
209 receivers.push(rx);
210 }
211
212 let mut set: JoinSet<AgentOutcome> = JoinSet::new();
213 for (index, ((agent, inbox), dev)) in agents
214 .into_iter()
215 .zip(receivers)
216 .zip(test_accounts)
217 .enumerate()
218 {
219 let mut peers = HashMap::with_capacity(count.saturating_sub(1));
220 for (peer_index, peer_name) in names.iter().enumerate() {
221 if peer_index != index {
222 peers.insert(peer_name.to_string(), senders[peer_index].clone());
223 }
224 }
225 let name = Arc::clone(&names[index]);
226 let mailbox = Mailbox::new(Arc::clone(&name), inbox, peers);
227 let ctx = AgentContext::new(
228 AgentId::new(index, Arc::clone(&name)),
229 self.network.clone(),
230 self.clock.clone(),
231 AgentRng::derive(&self.seed, index, &name),
232 mailbox,
233 Account::new(dev, namespace),
234 Arc::clone(&self.artifacts),
235 );
236 let body = agent.into_body();
237 set.spawn(async move {
238 let result = body(ctx).await;
239 AgentOutcome {
240 name,
241 index,
242 result,
243 }
244 });
245 }
246 drop(senders);
249
250 let mut outcomes: Vec<Option<AgentOutcome>> = (0..count).map(|_| None).collect();
251 while let Some(joined) = set.join_next().await {
252 match joined {
253 Ok(outcome) => {
254 let index = outcome.index;
255 outcomes[index] = Some(outcome);
256 }
257 Err(join_error) if join_error.is_panic() => {
258 std::panic::resume_unwind(join_error.into_panic());
259 }
260 Err(join_error) => {
261 return Err(TestkitError::AgentJoin {
262 detail: join_error.to_string(),
263 });
264 }
265 }
266 }
267
268 let outcomes = outcomes
269 .into_iter()
270 .map(|outcome| outcome.expect("bug: every agent produced an outcome"))
271 .collect();
272 let report = ScenarioReport { outcomes };
273 if report.all_ok() {
274 return Ok(report);
275 }
276
277 let failures = report
278 .outcomes
279 .into_iter()
280 .filter_map(|outcome| {
281 outcome
282 .result
283 .err()
284 .map(|error| AgentFailure::new(outcome.name.to_string(), outcome.index, error))
285 })
286 .collect();
287 Err(TestkitError::AgentsFailed {
288 failures: AgentFailures::new(failures),
289 })
290 }
291
292 pub async fn stop(self) -> Result<()> {
294 let Scenario { guard, .. } = self;
295 match guard {
296 NetworkGuard::Local(handle) => {
297 handle
298 .stop()
299 .await
300 .map_err(|e| TestkitError::NodeSupervisor {
301 detail: e.to_string(),
302 })?;
303 }
304 NetworkGuard::Existing => {}
305 }
306 Ok(())
307 }
308}
309
310fn validate_unique_agent_names(names: &[Arc<str>]) -> Result<()> {
311 let mut unique_names = HashSet::with_capacity(names.len());
312 for name in names {
313 if !unique_names.insert(name.as_ref()) {
314 return Err(TestkitError::DuplicateAgent {
315 name: name.to_string(),
316 });
317 }
318 }
319 Ok(())
320}
321
322async fn acquire_network(
323 config: NetworkConfig,
324 seed: &str,
325 use_bootstrap_accounts: bool,
326 accounts: usize,
327) -> Result<(Network, AccountSource, NetworkGuard)> {
328 match config {
329 NetworkConfig::Existing {
330 rpc_url,
331 runtime_manifest,
332 } => {
333 let manifest = load_runtime_manifest(&runtime_manifest)?;
334 let network = Network::connect(&rpc_url, &manifest, None).await?;
335 Ok((
336 network,
337 account_source(manifest, seed, use_bootstrap_accounts, accounts)?,
338 NetworkGuard::Existing,
339 ))
340 }
341 NetworkConfig::Local(config) => {
342 if use_bootstrap_accounts {
343 return Err(TestkitError::Manifest {
344 field: "bootstrapAccounts",
345 detail:
346 "Local nodes require scenario-owned accounts; remove bootstrap_accounts()."
347 .into(),
348 });
349 }
350 if accounts > 1000
351 || config.initial_account_tokens == 0
352 || config.initial_account_tokens > i64::MAX as u64
353 {
354 return Err(TestkitError::Manifest { field: "accounts", detail: "local fixture accounts require count <= 1000 and positive initial_account_tokens fitting i64".into() });
355 }
356 let allocations = crate::manifest::derive_native_accounts(accounts, seed, None)
357 .iter()
358 .map(|account| {
359 thruput_json_rpc_types::bootstrap_manifest::BootstrapManifestAllocation::new(
360 thruput_json_rpc_types::base58::Base58(
361 account.signer.verifying_key_bytes().into(),
362 ),
363 config.initial_account_tokens.to_string(),
364 )
365 })
366 .collect();
367 let handle = start_local(config, allocations).await?;
368 let rpc_url = format!("http://{}", handle.rpc_addr());
369 let manifest = handle
370 .hogd_readiness()
371 .runtime_manifest
372 .clone()
373 .ok_or(TestkitError::MissingRuntimeManifest)?;
374 let manual_block_producer = handle.manual_block_producer();
375 let network = Network::connect(&rpc_url, &manifest, manual_block_producer).await?;
376 Ok((
377 network,
378 account_source(manifest, seed, use_bootstrap_accounts, accounts)?,
379 NetworkGuard::Local(Box::new(handle)),
380 ))
381 }
382 NetworkConfig::Offline { accounts } => Ok((
383 Network::offline(),
384 AccountSource::Native {
385 count: accounts,
386 seed: seed.to_owned(),
387 network_hash: None,
388 },
389 NetworkGuard::Existing,
390 )),
391 }
392}
393
394fn account_source(
395 manifest: thruput_json_rpc_types::runtime_manifest::RuntimeManifest,
396 seed: &str,
397 use_bootstrap: bool,
398 count: usize,
399) -> Result<AccountSource> {
400 let source = if use_bootstrap {
401 AccountSource::Manifest(Box::new(manifest))
402 } else {
403 AccountSource::Native {
404 count,
405 seed: seed.to_owned(),
406 network_hash: Some(manifest.network_hash.to_network_hash()),
407 }
408 };
409 source.accounts()?;
410 Ok(source)
411}
412
413async fn start_local(
414 config: LocalNetworkConfig,
415 allocations: Vec<thruput_json_rpc_types::bootstrap_manifest::BootstrapManifestAllocation>,
416) -> Result<NodeSupervisorHandle> {
417 let block_driver = config.block_driver;
418 let mut hogd = HogdDevConfig::default()
419 .with_bootstrap_accounts(allocations)
420 .with_startup_timeout(config.startup_timeout)
421 .with_contract_artifact_directories(config.contract_artifact_directories);
422 if let Some(binary) = config.hog_devnode_binary {
423 hogd = hogd.with_binary_path(binary);
424 }
425 if let Some(log_level) = config.log_level {
426 crate::logging::install_for_level(log_level);
428 hogd = hogd.with_log_level(log_level);
429 }
430 let mut supervisor = NodeSupervisorConfig::default()
431 .with_rpc_listen_address(format!("127.0.0.1:{}", config.rpc_port))
432 .with_hogd_dev(hogd)
433 .with_block_driver(block_driver);
434 if let Some(protocol) = config.hogd_protocol {
435 supervisor = supervisor.with_hogd_protocol_override(protocol);
436 }
437 NodeSupervisorHandle::start(supervisor)
438 .await
439 .map_err(|e| TestkitError::NodeSupervisor {
440 detail: e.to_string(),
441 })
442}
443
444#[derive(Debug)]
451enum NetworkGuard {
452 Existing,
453 Local(Box<NodeSupervisorHandle>),
454}
455
456#[derive(Debug)]
458pub struct AgentOutcome {
459 name: Arc<str>,
460 index: usize,
461 result: Result<()>,
462}
463
464impl AgentOutcome {
465 pub fn name(&self) -> &str {
467 &self.name
468 }
469
470 pub fn index(&self) -> usize {
472 self.index
473 }
474
475 pub fn result(&self) -> &Result<()> {
477 &self.result
478 }
479}
480
481#[derive(Debug)]
483pub struct ScenarioReport {
484 outcomes: Vec<AgentOutcome>,
485}
486
487impl ScenarioReport {
488 pub fn outcomes(&self) -> &[AgentOutcome] {
490 &self.outcomes
491 }
492
493 pub fn all_ok(&self) -> bool {
498 self.outcomes.iter().all(|outcome| outcome.result.is_ok())
499 }
500}