Skip to main content

gromnie_runner/
client_naming.rs

1//! Client naming utilities for load testing
2//!
3//! Converts client IDs to unique account and character names using base-26 encoding.
4//! Supports up to 456,976 clients (26^4).
5
6/// Convert a client ID to a 4-character base-26 code (AAAA to ZZZZ)
7pub fn encode_client_id(id: u32) -> String {
8    // Clamp to max valid ID (26^4 - 1 = 456975)
9    let id = id.min(456975);
10
11    let mut id = id;
12    let mut code = String::with_capacity(4);
13
14    for _ in 0..4 {
15        let char_idx = (id % 26) as u8;
16        code.insert(0, (b'A' + char_idx) as char);
17        id /= 26;
18    }
19
20    code
21}
22
23/// Decode a 4-character base-26 code back to a client ID
24///
25/// Returns None if the string is not exactly 4 characters or contains non-A-Z characters.
26#[allow(dead_code)]
27pub fn decode_client_id(code: &str) -> Option<u32> {
28    if code.len() != 4 {
29        return None;
30    }
31
32    let mut id: u32 = 0;
33
34    for (pos, ch) in code.chars().enumerate() {
35        if !ch.is_ascii_uppercase() {
36            return None;
37        }
38
39        let digit = ch as u32 - b'A' as u32;
40        let power = 4 - 1 - pos; // positions from left: 3, 2, 1, 0
41        id += digit * 26_u32.pow(power as u32);
42    }
43
44    Some(id)
45}
46
47/// Encapsulates client naming for load testing
48pub struct ClientNaming {
49    #[allow(dead_code)]
50    client_id: u32,
51    code: String,
52}
53
54impl ClientNaming {
55    /// Create naming for a client with the given ID
56    pub fn new(client_id: u32) -> Self {
57        let code = encode_client_id(client_id);
58        Self { client_id, code }
59    }
60
61    /// Get the account name for this client
62    ///
63    /// Format: Load-XXXX (e.g., Load-AAAA)
64    pub fn account_name(&self) -> String {
65        format!("Load-{}", self.code)
66    }
67
68    /// Get the password for this client (same as account name)
69    pub fn password(&self) -> String {
70        self.account_name()
71    }
72
73    /// Get the character name for this client
74    ///
75    /// Format: Load-XXXX-A (e.g., Load-AAAA-A)
76    pub fn character_name(&self) -> String {
77        format!("{}-A", self.account_name())
78    }
79
80    /// Get the 4-character code for this client
81    #[allow(dead_code)]
82    pub fn code(&self) -> &str {
83        &self.code
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_encode_decode_roundtrip() {
93        for id in [0, 1, 25, 26, 27, 100, 675, 456975] {
94            let encoded = encode_client_id(id);
95            let decoded = decode_client_id(&encoded).expect("decode should succeed");
96            assert_eq!(
97                id, decoded,
98                "Roundtrip failed for id {}: {} -> {} -> {}",
99                id, encoded, encoded, decoded
100            );
101        }
102    }
103
104    #[test]
105    fn test_encode_known_values() {
106        assert_eq!(encode_client_id(0), "AAAA");
107        assert_eq!(encode_client_id(1), "AAAB");
108        assert_eq!(encode_client_id(25), "AAAZ");
109        assert_eq!(encode_client_id(26), "AABA");
110        assert_eq!(encode_client_id(27), "AABB");
111    }
112
113    #[test]
114    fn test_client_naming() {
115        let naming = ClientNaming::new(0);
116        assert_eq!(naming.code(), "AAAA");
117        assert_eq!(naming.account_name(), "Load-AAAA");
118        assert_eq!(naming.password(), "Load-AAAA");
119        assert_eq!(naming.character_name(), "Load-AAAA-A");
120    }
121
122    #[test]
123    fn test_encode_higher_values() {
124        assert_eq!(encode_client_id(675), "AAZZ");
125        assert_eq!(encode_client_id(456975), "ZZZZ");
126        assert_eq!(encode_client_id(456976), "ZZZZ"); // Clamped
127    }
128
129    #[test]
130    fn test_decode_known_values() {
131        assert_eq!(decode_client_id("AAAA"), Some(0));
132        assert_eq!(decode_client_id("AAAB"), Some(1));
133        assert_eq!(decode_client_id("AAAZ"), Some(25));
134        assert_eq!(decode_client_id("AABA"), Some(26));
135        assert_eq!(decode_client_id("AABB"), Some(27));
136        assert_eq!(decode_client_id("ZZZZ"), Some(456975));
137    }
138
139    #[test]
140    fn test_decode_invalid_input() {
141        assert_eq!(decode_client_id("AAA"), None);
142        assert_eq!(decode_client_id("AAAAA"), None);
143        assert_eq!(decode_client_id("AAAa"), None);
144        assert_eq!(decode_client_id("AA!A"), None);
145        assert_eq!(decode_client_id(""), None);
146    }
147
148    #[test]
149    fn test_client_naming_higher_id() {
150        let naming = ClientNaming::new(123);
151        let encoded = encode_client_id(123);
152        assert_eq!(naming.code(), encoded.as_str());
153        assert_eq!(naming.account_name(), format!("Load-{}", encoded));
154        assert_eq!(naming.character_name(), format!("Load-{}-A", encoded));
155    }
156
157    #[test]
158    fn test_max_clients() {
159        let max_id = 456975; // 26^4 - 1
160        let naming = ClientNaming::new(max_id);
161        assert_eq!(naming.code(), "ZZZZ");
162        assert_eq!(naming.account_name(), "Load-ZZZZ");
163
164        let over_max = ClientNaming::new(456976);
165        assert_eq!(over_max.code(), "ZZZZ");
166    }
167
168    #[test]
169    fn test_encode_sequential() {
170        let expected = vec![
171            "AAAA", "AAAB", "AAAC", "AAAD", "AAAE", "AAAF", "AAAG", "AAAH", "AAAI", "AAAJ",
172        ];
173
174        for (i, &exp) in expected.iter().enumerate() {
175            assert_eq!(encode_client_id(i as u32), exp, "Failed at index {}", i);
176        }
177    }
178}