서비스 기초 연습 문서의 문제 2 풀이 코드다.
//! 서비스 기초 연습 p2: 몫을 고르게 나누는 함수의 성질을 proptest로 적고, 틀린 구현의 최소 반례를 축소로 찾는다.
//! 의존성: proptest = "1"
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/4.연습문제/45_service-basics-practice_p2.rs"
/// total개를 parts명에게 최대한 고르게 나눈다. 앞사람부터 하나씩 더 받는다.
pub fn split_even(total: u64, parts: usize) -> Vec<u64> {
assert!(parts > 0, "parts must be positive");
let q = total / parts as u64;
let r = (total % parts as u64) as usize;
(0..parts).map(|i| q + u64::from(i < r)).collect()
}
/// 틀린 구현: 나머지를 버린다.
fn split_even_buggy(total: u64, parts: usize) -> Vec<u64> {
vec![total / parts as u64; parts]
}
/// 세 성질을 한 번에 검사한다. 어긋나면 어느 성질인지 돌려준다.
fn properties(f: fn(u64, usize) -> Vec<u64>, total: u64, parts: usize) -> Result<(), String> {
let v = f(total, parts);
if v.len() != parts {
return Err(format!("len {} != {parts}", v.len()));
}
if v.iter().sum::<u64>() != total {
return Err(format!("sum {} != {total}", v.iter().sum::<u64>()));
}
let (mn, mx) = (
v.iter().min().copied().unwrap_or(0),
v.iter().max().copied().unwrap_or(0),
);
if mx - mn > 1 {
return Err(format!("max-min = {}", mx - mn));
}
Ok(())
}
fn find_minimal_counterexample() -> Option<(u64, usize)> {
use proptest::test_runner::{Config, TestError, TestRng, TestRunner};
let config = Config {
failure_persistence: None,
..Config::default()
};
let mut runner = TestRunner::new_with_rng(
config.clone(),
TestRng::deterministic_rng(config.rng_algorithm),
);
let r = runner.run(&(0u64..10_000, 1usize..50), |(t, p)| {
properties(split_even_buggy, t, p).map_err(proptest::test_runner::TestCaseError::fail)
});
match r {
Err(TestError::Fail(_, v)) => Some(v),
_ => None,
}
}
fn main() {
assert_eq!(split_even(10, 3), [4, 3, 3]);
assert_eq!(split_even(2, 4), [1, 1, 0, 0]);
let m = find_minimal_counterexample();
println!("틀린 구현의 최소 반례: {m:?}");
println!(
"그 입력의 결과: {:?}",
m.map(|(t, p)| split_even_buggy(t, p))
);
assert_eq!(m, Some((1, 2)));
assert_eq!(properties(split_even_buggy, 1, 2), Err("sum 0 != 1".into()));
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn split_even_keeps_all_three_properties(total in 0u64..1_000_000, parts in 1usize..100) {
prop_assert_eq!(properties(split_even, total, parts), Ok(()));
}
}
#[test]
#[should_panic(expected = "parts must be positive")]
fn zero_parts_panics() {
split_even(5, 0);
}
}