From 8b1acc2bb0922132e1032fd65f044d3ba91785fd Mon Sep 17 00:00:00 2001 From: Eugen Date: Sun, 30 Nov 2025 13:31:21 +0900 Subject: [PATCH] Check predictability of names The README to the exercise states > The names must be random: they should not follow a predictable sequence. Yet there are accepted [solutions](https://exercism.org/tracks/rust/exercises/robot-name/solutions/boso) that violate the requirement. *) The linked solution can unfortunately not be commented on: > Comments have been disabled This new test makes sure that the most obvious kind of predictability - and probably the only one naturally used by anyone - makes tests fail. The test cannot be run concurrently with other tests, for obvious reasons, hence the not very elegant solution using a global RwLock, which every other tests has to `read()`, in order for the new test to run exclusively. --- .../practice/robot-name/tests/robot_name.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/exercises/practice/robot-name/tests/robot_name.rs b/exercises/practice/robot-name/tests/robot_name.rs index abfeadd3f..d69b54353 100644 --- a/exercises/practice/robot-name/tests/robot_name.rs +++ b/exercises/practice/robot-name/tests/robot_name.rs @@ -1,4 +1,22 @@ use robot_name as robot; +use std::sync::{Once, RwLock}; + +static INIT: Once = Once::new(); +static mut PREDICTABLE_SEQ_TEST_LOCK: *const RwLock<()> = std::ptr::null(); + +/// All tests other than names_should_not_follow_predictable_sequence need to take a read lock, +/// so that they will not run concurrently with names_should_not_follow_predictable_sequence, who +/// takes the write lock +fn predictable_seq_test_lock() -> &'static RwLock<()> { + INIT.call_once(|| { + let boxed = Box::new(RwLock::new(())); + unsafe { + PREDICTABLE_SEQ_TEST_LOCK = Box::into_raw(boxed); + } + }); + + unsafe { &*PREDICTABLE_SEQ_TEST_LOCK } +} fn assert_name_matches_pattern(n: &str) { assert!(n.len() == 5, "name is exactly 5 characters long"); @@ -14,6 +32,7 @@ fn assert_name_matches_pattern(n: &str) { #[test] fn name_should_match_expected_pattern() { + let _guard = predictable_seq_test_lock().read(); let r = robot::Robot::new(); assert_name_matches_pattern(r.name()); } @@ -21,6 +40,7 @@ fn name_should_match_expected_pattern() { #[test] #[ignore] fn different_robots_have_different_names() { + let _guard = predictable_seq_test_lock().read(); let r1 = robot::Robot::new(); let r2 = robot::Robot::new(); assert_ne!(r1.name(), r2.name(), "Robot names should be different"); @@ -29,6 +49,7 @@ fn different_robots_have_different_names() { #[test] #[ignore] fn many_different_robots_have_different_names() { + let _guard = predictable_seq_test_lock().read(); use std::collections::HashSet; // In 3,529 random robot names, there is ~99.99% chance of a name collision @@ -42,6 +63,7 @@ fn many_different_robots_have_different_names() { #[test] #[ignore] fn new_name_should_match_expected_pattern() { + let _guard = predictable_seq_test_lock().read(); let mut r = robot::Robot::new(); assert_name_matches_pattern(r.name()); r.reset_name(); @@ -51,9 +73,38 @@ fn new_name_should_match_expected_pattern() { #[test] #[ignore] fn new_name_is_different_from_old_name() { + let _guard = predictable_seq_test_lock().read(); let mut r = robot::Robot::new(); let n1 = r.name().to_string(); r.reset_name(); let n2 = r.name().to_string(); assert_ne!(n1, n2, "Robot name should change when reset"); } + +fn name_to_num(name: &str) -> u32 { + let mut chars = name.chars(); + ((chars.next().unwrap() as u32) - b'A' as u32) * 26 * 10 * 10 * 10 + + ((chars.next().unwrap() as u32) - b'A' as u32) * 10 * 10 * 10 + + ((chars.next().unwrap() as u32) - b'0' as u32) * 10 * 10 + + ((chars.next().unwrap() as u32) - b'0' as u32) * 10 + + ((chars.next().unwrap() as u32) - b'0' as u32) +} + +#[test] +#[ignore] +fn names_should_not_follow_predictable_sequence() { + // needs to run exclusively, otherwise other tests may mess up predictability detection + let _guard = predictable_seq_test_lock().write(); + let nums = (0..10) + .into_iter() + .map(|_| name_to_num(robot::Robot::new().name())) + .collect::>(); + + // just checking the easiest kind of predictable sequences here: the difference between + // numerical representation of consecutive names is always the same + let d = nums[1] - nums[0]; + assert!( + !nums.windows(2).all(|w| w[1] - w[0] == d), + "Name sequence is very predictable: The difference between the numerical representation of names is {d}" + ) +}