use super::*;
#[tokio::test]
async fn wait_resolves_again_after_it_fired() {
let (fire, mut interrupt) = Interrupt::manual();
fire.send(()).unwrap();
interrupt.wait().await;
interrupt.wait().await;
}
#[tokio::test]
async fn wait_pends_while_nothing_fired() {
let (_fire, mut interrupt) = Interrupt::manual();
let waited = tokio::time::timeout(std::time::Duration::from_millis(20), interrupt.wait()).await;
assert!(waited.is_err());
}
fn open_tx(byte: u8, nonce: u64) -> OpenUnconfirmed {
OpenUnconfirmed {
tx: Some(alloy::primitives::B256::repeat_byte(byte)),
nonce,
}
}
fn open(byte: u8, nonce: u64) -> EscrowUnconfirmed {
open_tx(byte, nonce).into()
}
fn top_up_tx(byte: u8, nonce: u64) -> TopUpUnconfirmed {
TopUpUnconfirmed {
tx: Some(alloy::primitives::B256::repeat_byte(byte)),
nonce,
}
}
fn top_up(byte: u8, nonce: u64) -> EscrowUnconfirmed {
top_up_tx(byte, nonce).into()
}
fn gave_up() -> anyhow::Error {
GaveUp {
idle: std::time::Duration::from_secs(600),
}
.into()
}
fn name<T>(result: anyhow::Result<T>, escrows: &[EscrowUnconfirmed]) -> anyhow::Result<T> {
name_unconfirmed_escrows(result, escrows, &[])
}
fn pending(pending: Vec<EscrowUnconfirmed>) -> UnreconciledEscrows {
UnreconciledEscrows {
pending,
untracked: Vec::new(),
}
}
fn holding(bytes: &[u8]) -> (UnconfirmedEscrows, Vec<UntrackedEscrow>) {
let escrows = UnconfirmedEscrows::default();
for byte in bytes {
escrows.hold_untracked_for_test(&decdn_client::buyer_pool::escrowed_but_untracked(
"pool 0x01 topped up by 5 µUSDC",
alloy::primitives::B256::repeat_byte(*byte),
"deposit credit lost",
));
}
let held = escrows.untracked();
(escrows, held)
}
#[test]
fn a_command_with_no_dropped_open_ends_as_it_did() {
assert_eq!(name(Ok(7), &[]).unwrap(), 7);
let err = name::<()>(Err(Interrupted.into()), &[]).unwrap_err();
assert_eq!(
stop_message(&err).unwrap(),
"interrupted; run the same command again to resume"
);
assert_eq!(stop_exit(&err), Some(INTERRUPTED_EXIT));
let err = name::<()>(Err(gave_up()), &[]).unwrap_err();
assert_eq!(
stop_message(&err).unwrap(),
"no progress for 10m; rerun to resume"
);
assert_eq!(stop_exit(&err), Some(GAVE_UP_EXIT));
}
#[test]
fn an_interrupt_names_the_resume_directory() {
let dir = Path::new("/abs/dir");
let err = name_resume_dir::<()>(Err(Interrupted.into()), dir).unwrap_err();
assert_eq!(
err.downcast_ref::<ResumeFrom>(),
Some(&ResumeFrom(dir.to_path_buf()))
);
assert_eq!(
stop_message(&err).unwrap(),
"interrupted; run the same command again from /abs/dir to resume"
);
assert_eq!(stop_exit(&err), Some(INTERRUPTED_EXIT));
}
#[test]
fn a_give_up_names_the_resume_directory() {
let err = name_resume_dir::<()>(Err(gave_up()), Path::new("/abs/dir")).unwrap_err();
assert_eq!(
stop_message(&err).unwrap(),
"no progress for 10m; rerun from /abs/dir to resume"
);
assert_eq!(stop_exit(&err), Some(GAVE_UP_EXIT));
}
#[test]
fn name_resume_dir_leaves_anything_but_a_stop_alone() {
let dir = Path::new("/abs/dir");
assert_eq!(name_resume_dir(Ok(7), dir).unwrap(), 7);
let err = name_resume_dir::<()>(Err(anyhow::anyhow!("pool empty")), dir).unwrap_err();
assert!(err.downcast_ref::<ResumeFrom>().is_none());
assert_eq!(stop_message(&err), None);
}
#[test]
fn an_escrow_in_flight_wins_over_the_resume_directory_on_a_give_up() {
let err = name::<()>(Err(gave_up()), &[open(0xef, 5)]);
let err = name_resume_dir(err, Path::new("/abs/dir")).unwrap_err();
let line = stop_message(&err).unwrap();
assert!(line.starts_with("no progress for 600s: "), "{line}");
assert!(line.contains("nonce 5"), "{line}");
assert!(!line.contains("/abs/dir"), "{line}");
assert_eq!(stop_exit(&err), None);
}
#[test]
fn an_escrow_in_flight_wins_over_the_resume_directory() {
let err = name::<()>(Err(Interrupted.into()), &[open(0xef, 5)]);
let err = name_resume_dir(err, Path::new("/abs/dir")).unwrap_err();
let line = stop_message(&err).unwrap();
assert!(line.starts_with("interrupted: "), "{line}");
assert!(line.contains("nonce 5"), "{line}");
assert!(!line.contains("/abs/dir"), "{line}");
assert_eq!(stop_exit(&err), Some(INTERRUPTED_EXIT));
}
#[test]
fn a_dropped_open_turns_success_into_an_error_naming_the_tx() {
let err = name(Ok(()), &[open(0xef, 5)]).unwrap_err();
assert_eq!(
err.downcast_ref::<UnreconciledEscrows>(),
Some(&pending(vec![open(0xef, 5)]))
);
assert!(format!("{err:#}").contains("nonce 5"), "{err:#}");
assert_eq!(stop_message(&err), None);
assert_eq!(stop_exit(&err), None);
}
#[test]
fn an_interrupted_open_keeps_the_interrupt_and_names_the_tx() {
let err = name::<()>(Err(Interrupted.into()), &[open(0xef, 5)]).unwrap_err();
assert!(err.downcast_ref::<Interrupted>().is_some());
assert_eq!(stop_exit(&err), Some(INTERRUPTED_EXIT));
let message = stop_message(&err).unwrap();
assert!(
message.starts_with("interrupted: openPool tx 0xefef"),
"{message}"
);
assert!(!message.contains("run the same command again"), "{message}");
}
#[test]
fn a_give_up_during_an_open_names_the_tx_and_is_not_a_tempfail() {
let err = name::<()>(Err(gave_up()), &[open(0xef, 5)]).unwrap_err();
let message = stop_message(&err).unwrap();
assert!(
message.starts_with("no progress for 600s: openPool tx 0xefef"),
"{message}"
);
assert!(!message.contains("rerun"), "{message}");
assert_eq!(stop_exit(&err), None);
}
#[test]
fn every_pending_open_is_named() {
let err = name::<()>(Err(Interrupted.into()), &[open(0xef, 5), open(0xab, 6)]).unwrap_err();
let message = stop_message(&err).unwrap();
assert!(
message.contains("0xefef") && message.contains("0xabab"),
"{message}"
);
assert_eq!(message.lines().count(), 2, "{message}");
}
#[test]
fn an_open_the_error_carries_is_not_named_twice() {
let carried = anyhow::anyhow!("receipt read failed").context(open_tx(0xef, 5));
let err = name::<()>(Err(carried), &[open(0xef, 5)]).unwrap_err();
assert!(
err.downcast_ref::<UnreconciledEscrows>().is_none(),
"{err:#}"
);
assert_eq!(format!("{err:#}").matches("0xefef").count(), 1, "{err:#}");
let carried = anyhow::anyhow!("receipt read failed").context(open_tx(0xef, 5));
let err = name::<()>(Err(carried), &[open(0xef, 5), open(0xab, 6)]).unwrap_err();
assert_eq!(
err.downcast_ref::<UnreconciledEscrows>(),
Some(&pending(vec![open(0xab, 6)]))
);
}
#[test]
fn a_give_up_during_a_top_up_names_the_tx_and_is_not_a_tempfail() {
let err = name::<()>(Err(gave_up()), &[top_up(0xcd, 7)]).unwrap_err();
let message = stop_message(&err).unwrap();
assert!(
message.starts_with("no progress for 600s: topUp tx 0xcdcd"),
"{message}"
);
assert!(message.contains("nonce 7"), "{message}");
assert!(!message.contains("rerun"), "{message}");
assert_eq!(stop_exit(&err), None);
}
#[test]
fn a_dropped_top_up_turns_success_into_an_error_naming_the_tx() {
let err = name(Ok(()), &[top_up(0xcd, 7)]).unwrap_err();
assert_eq!(
err.downcast_ref::<UnreconciledEscrows>(),
Some(&pending(vec![top_up(0xcd, 7)]))
);
assert!(format!("{err:#}").contains("topUp tx 0xcdcd"), "{err:#}");
assert_eq!(stop_message(&err), None);
assert_eq!(stop_exit(&err), None);
}
#[test]
fn an_interrupt_names_pending_opens_and_top_ups() {
let err = name::<()>(Err(Interrupted.into()), &[open(0xef, 5), top_up(0xcd, 7)]).unwrap_err();
assert_eq!(stop_exit(&err), Some(INTERRUPTED_EXIT));
let message = stop_message(&err).unwrap();
assert!(
message.starts_with("interrupted: openPool tx 0xefef"),
"{message}"
);
assert!(message.contains("topUp tx 0xcdcd"), "{message}");
assert_eq!(message.lines().count(), 2, "{message}");
}
#[test]
fn a_top_up_the_error_carries_is_not_named_twice() {
let carried = anyhow::anyhow!("receipt wait lapsed").context(top_up_tx(0xcd, 7));
let err = name::<()>(Err(carried), &[top_up(0xcd, 7)]).unwrap_err();
assert!(
err.downcast_ref::<UnreconciledEscrows>().is_none(),
"{err:#}"
);
assert_eq!(format!("{err:#}").matches("0xcdcd").count(), 1, "{err:#}");
let carried = gave_up().context(top_up_tx(0xcd, 7));
assert_eq!(stop_exit(&carried), None);
let message = stop_message(&carried).unwrap();
assert!(message.contains("topUp tx 0xcdcd"), "{message}");
}
#[test]
fn pending_escrows_the_error_names_as_text_are_not_named_twice() {
let (first, second) = (top_up(0xab, 7), top_up(0xcd, 8));
let folded = anyhow::Error::new(top_up_tx(0xab, 7)).context(format!(
"another openPool or topUp of this command may still escrow USDC: {second}"
));
let err = name::<()>(Err(folded), &[first, second]).unwrap_err();
assert!(
err.downcast_ref::<UnreconciledEscrows>().is_none(),
"{err:#}"
);
let text = format!("{err:#}");
assert_eq!(text.matches("0xabab").count(), 1, "{text}");
assert_eq!(text.matches("0xcdcd").count(), 1, "{text}");
}
#[test]
fn a_top_up_dropped_mid_submit_names_its_nonce() {
let submitted = TopUpUnconfirmed { tx: None, nonce: 7 };
let err = name::<()>(Err(Interrupted.into()), &[submitted.into()]).unwrap_err();
let message = stop_message(&err).unwrap();
assert!(
message.starts_with("interrupted: topUp submit (nonce 7) returned no tx hash"),
"{message}"
);
assert!(!message.contains("transport"), "{message}");
}
#[test]
fn an_interrupt_names_the_escrow_the_handle_holds() {
let (escrows, held) = holding(&[0x9a]);
let err = name_held_escrows::<()>(Err(Interrupted.into()), &escrows).unwrap_err();
assert_eq!(stop_exit(&err), Some(INTERRUPTED_EXIT));
let message = stop_message(&err).unwrap();
assert!(
message.starts_with("interrupted: pool 0x01 topped up"),
"{message}"
);
assert!(
message.contains(&held.first().unwrap().tx.to_string()),
"{message}"
);
assert!(!message.contains("run the same command again"), "{message}");
}
#[test]
fn a_give_up_with_an_untracked_escrow_is_not_a_tempfail() {
let (escrows, held) = holding(&[0x9a]);
let err = name_held_escrows::<()>(Err(gave_up()), &escrows).unwrap_err();
assert_eq!(stop_exit(&err), None);
let message = stop_message(&err).unwrap();
assert!(
message.contains(&held.first().unwrap().tx.to_string()),
"{message}"
);
assert!(!message.contains("rerun"), "{message}");
}
#[test]
fn a_stop_whose_text_names_the_escrow_still_names_it() {
let (escrows, held) = holding(&[0x9a]);
let tx = held.first().unwrap().tx.to_string();
let gave_up = gave_up().context(format!("a late lane build: {}", held.first().unwrap()));
let err = name_held_escrows::<()>(Err(gave_up), &escrows).unwrap_err();
assert_eq!(stop_exit(&err), None);
assert!(stop_message(&err).unwrap().contains(&tx), "{err:#}");
let interrupted = anyhow::Error::new(Interrupted)
.context(format!("a late lane build: {}", held.first().unwrap()));
let err = name_held_escrows::<()>(Err(interrupted), &escrows).unwrap_err();
let message = stop_message(&err).unwrap();
assert!(message.contains(&tx), "{message}");
assert!(!message.contains("run the same command again"), "{message}");
}
#[test]
fn an_untracked_escrow_turns_success_into_an_error() {
let (escrows, held) = holding(&[0x9a]);
let err = name_held_escrows(Ok(()), &escrows).unwrap_err();
assert_eq!(
err.downcast_ref::<UnreconciledEscrows>(),
Some(&UnreconciledEscrows {
pending: Vec::new(),
untracked: held,
})
);
assert_eq!(stop_exit(&err), None);
}
#[test]
fn an_untracked_escrow_the_error_names_is_not_named_twice() {
let (_, held) = holding(&[0x9a]);
let tx = held.first().unwrap().tx.to_string();
let reported = || {
anyhow::anyhow!(
"building the payment lane failed: {}",
held.first().unwrap()
)
};
let err = name_unconfirmed_escrows::<()>(Err(reported()), &[], &held).unwrap_err();
assert!(
err.downcast_ref::<UnreconciledEscrows>().is_none(),
"{err:#}"
);
assert_eq!(format!("{err:#}").matches(&tx).count(), 1, "{err:#}");
let err =
name_unconfirmed_escrows::<()>(Err(reported()), &[top_up(0xcd, 7)], &held).unwrap_err();
assert_eq!(
err.downcast_ref::<UnreconciledEscrows>(),
Some(&pending(vec![top_up(0xcd, 7)]))
);
assert_eq!(format!("{err:#}").matches(&tx).count(), 1, "{err:#}");
}
#[test]
fn pending_and_untracked_escrows_are_all_named() {
let (_, held) = holding(&[0x9a, 0x8b]);
let err = name_unconfirmed_escrows::<()>(Err(Interrupted.into()), &[top_up(0xcd, 7)], &held)
.unwrap_err();
let message = stop_message(&err).unwrap();
assert!(
message.starts_with("interrupted: topUp tx 0xcdcd"),
"{message}"
);
for escrow in &held {
assert!(message.contains(&escrow.tx.to_string()), "{message}");
}
assert_eq!(message.lines().count(), 3, "{message}");
}
#[test]
fn a_carried_top_up_and_an_untracked_escrow_are_both_named() {
let (escrows, held) = holding(&[0x9a]);
let carried = gave_up().context(top_up_tx(0xcd, 7));
let err = name_held_escrows::<()>(Err(carried), &escrows).unwrap_err();
let message = stop_message(&err).unwrap();
assert!(message.contains("topUp tx 0xcdcd"), "{message}");
assert!(
message.contains(&held.first().unwrap().tx.to_string()),
"{message}"
);
assert_eq!(stop_exit(&err), None);
}
#[test]
fn the_immediate_exit_names_untracked_escrows() {
let (escrows, held) = holding(&[0x9a]);
let lines = exit_lines(&escrows);
assert_eq!(lines.len(), 1, "{lines:?}");
assert!(
lines
.first()
.unwrap()
.starts_with("interrupted: pool 0x01 topped up"),
"{lines:?}"
);
assert!(
lines
.first()
.unwrap()
.contains(&held.first().unwrap().tx.to_string()),
"{lines:?}"
);
assert!(exit_lines(&UnconfirmedEscrows::default()).is_empty());
}