panic을 쓸 때 문서의 검증 코드다.
//! panic을 쓸 때의 규칙을 확인한다: catch_unwind는 풀기(unwind) panic만 잡는다,
//! panic = "abort"이면 잡지 못하고 Drop도 돌지 않는다, 스레드 panic은 join()의 Err로 온다,
//! tokio 태스크 panic은 JoinError(is_panic)로 온다, clippy::unwrap_used·expect_used 린트 이름.
//! 의존성: tokio = { version = "1", features = ["full"] }
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/26_panic-policy_verify.rs"
// 정책 예: 제품 코드에서는 unwrap 금지(테스트는 허용). expect는 이유를 적고 쓴다.
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
use std::any::Any;
use std::panic;
use std::process::Command;
fn payload_text(p: &(dyn Any + Send)) -> String {
if let Some(s) = p.downcast_ref::<&str>() {
format!("&str: {s}")
} else if let Some(s) = p.downcast_ref::<String>() {
format!("String: {s}")
} else {
"unknown".to_string()
}
}
fn tmpdir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("vr26_{}_{}", std::process::id(), tag));
std::fs::create_dir_all(&dir).expect("임시 폴더를 만들 수 있어야 한다");
dir
}
/// 같은 프로그램을 panic 전략만 바꿔 컴파일하고 실행한다.
fn run_with_strategy(strategy: &str) -> std::process::Output {
let dir = tmpdir(strategy);
let src = dir.join("main.rs");
let exe = dir.join("prog");
std::fs::write(
&src,
r#"
struct Guard;
impl Drop for Guard {
fn drop(&mut self) { println!("drop guard"); }
}
fn main() {
let r = std::panic::catch_unwind(|| {
let _g = Guard;
panic!("inside");
});
println!("caught = {}", r.is_err());
}
"#,
)
.expect("소스를 쓸 수 있어야 한다");
let build = Command::new("rustc")
.args(["--edition", "2024", "-C"])
.arg(format!("panic={strategy}"))
.arg("-o")
.arg(&exe)
.arg(&src)
.output()
.expect("rustc가 PATH에 있어야 한다");
assert!(
build.status.success(),
"{}",
String::from_utf8_lossy(&build.stderr)
);
Command::new(&exe)
.output()
.expect("만든 프로그램을 실행할 수 있어야 한다")
}
fn clippy_on(code: &str, lints: &[&str]) -> String {
let dir = tmpdir("clippy");
let file = dir.join("snippet.rs");
std::fs::write(&file, code).expect("소스를 쓸 수 있어야 한다");
let mut cmd = Command::new("clippy-driver");
cmd.args([
"--edition",
"2024",
"--crate-type=lib",
"--emit=metadata",
"--out-dir",
])
.arg(&dir);
for l in lints {
cmd.args(["-W", l]);
}
let out = cmd
.arg(&file)
.output()
.expect("clippy-driver가 PATH에 있어야 한다");
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn main() {
// panic 메시지가 stderr를 어지럽히지 않게 훅을 비운다(잡히는 것은 그대로다).
panic::set_hook(Box::new(|_| {}));
// 1. catch_unwind: 풀기 panic을 잡고 payload를 돌려준다
let r = panic::catch_unwind(|| panic!("literal message"));
let p = r.expect_err("panic이 났으니 Err여야 한다");
assert_eq!(payload_text(p.as_ref()), "&str: literal message");
let n = 7;
let r = panic::catch_unwind(|| panic!("formatted {n}"));
let p = r.expect_err("panic이 났으니 Err여야 한다");
assert_eq!(payload_text(p.as_ref()), "String: formatted 7");
println!("1. catch_unwind: &str 또는 String payload");
// 2. 스레드 panic은 join()이 Err로 돌려준다. 다른 스레드는 계속 돈다.
let worker_id = std::hint::black_box(3);
let bad = std::thread::spawn(move || -> u32 { panic!("worker {worker_id} failed") });
let good = std::thread::spawn(|| 40 + 2);
let bad_res = bad.join();
let good_res = good.join();
let p = bad_res.expect_err("panic한 스레드의 join은 Err");
assert_eq!(payload_text(p.as_ref()), "String: worker 3 failed");
assert_eq!(good_res.ok(), Some(42));
println!("2. thread join: Err(payload), 옆 스레드는 Ok(42)");
// 3. tokio 태스크 panic은 JoinError로 온다. 런타임과 다른 태스크는 멀쩡하다.
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.build()
.expect("런타임을 만들 수 있어야 한다");
let (is_panic, other) = rt.block_on(async {
let t = tokio::spawn(async { panic!("task blew up") });
let ok = tokio::spawn(async { 5 });
let e = t.await.expect_err("panic한 태스크는 JoinError");
let msg = payload_text(e.into_panic().as_ref());
assert_eq!(msg, "&str: task blew up");
(true, ok.await.ok())
});
assert!(is_panic);
assert_eq!(other, Some(5));
// 취소한 태스크는 is_cancelled
let cancelled = rt.block_on(async {
let t = tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
});
t.abort();
let e = t.await.expect_err("abort한 태스크는 JoinError");
(e.is_cancelled(), e.is_panic())
});
assert_eq!(cancelled, (true, false));
println!("3. tokio: JoinError::is_panic / is_cancelled");
// 4. panic 전략: unwind면 Drop이 돌고 잡힌다. abort면 그 자리에서 프로세스가 죽는다.
let unwind = run_with_strategy("unwind");
let out = String::from_utf8_lossy(&unwind.stdout);
println!("4. unwind: 종료 {:?}, 출력 {:?}", unwind.status.code(), out);
assert_eq!(unwind.status.code(), Some(0));
assert_eq!(out, "drop guard\ncaught = true\n");
let abort = run_with_strategy("abort");
let out = String::from_utf8_lossy(&abort.stdout);
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
println!(
" abort: 종료 코드 {:?}, 신호 {:?}, 출력 {:?}",
abort.status.code(),
abort.status.signal(),
out
);
assert_eq!(abort.status.signal(), Some(6)); // SIGABRT
}
assert!(!abort.status.success());
assert_eq!(out, ""); // Drop도, "caught" 줄도 없다
// 5. clippy 린트 이름 확인
let msg = clippy_on(
"pub fn a(x: Option<i32>) -> i32 { x.unwrap() }\n\
pub fn b(x: Result<i32, String>) -> i32 { x.expect(\"checked above\") }\n",
&["clippy::unwrap_used", "clippy::expect_used"],
);
assert!(msg.contains("#unwrap_used"), "{msg}");
assert!(msg.contains("#expect_used"), "{msg}");
assert!(
msg.contains("used `unwrap()` on an `Option` value"),
"{msg}"
);
assert!(msg.contains("used `expect()` on a `Result` value"), "{msg}");
println!("5. clippy::unwrap_used, clippy::expect_used 확인");
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
#[test]
#[should_panic(expected = "index out of bounds")]
fn indexing_past_end_panics() {
let v: Vec<i32> = (1..=3).collect();
let i = v.len();
let _ = v[i];
}
#[test]
fn get_returns_none_instead() {
let v = [1, 2, 3];
assert_eq!(v.get(3), None);
// 테스트에서는 unwrap이 허용된다(cfg_attr(not(test), ...)).
assert_eq!(*v.first().unwrap(), 1);
}
}