33
44//! The [`Recording`] and other types used in recorded tests.
55
6- // cspell:ignore csprng seedable
6+ // cspell:ignore csprng seedable tpbwhbkhckmk
77use crate :: {
88 proxy:: {
99 client:: {
@@ -26,7 +26,7 @@ use azure_core::{
2626} ;
2727use azure_identity:: DefaultAzureCredential ;
2828use rand:: {
29- distributions:: { Distribution , Standard } ,
29+ distributions:: { Alphanumeric , DistString , Distribution , Standard } ,
3030 Rng , SeedableRng ,
3131} ;
3232use rand_chacha:: ChaCha20Rng ;
@@ -143,10 +143,29 @@ impl Recording {
143143 ///
144144 /// # Examples
145145 ///
146+ /// Generate a random integer.
147+ ///
148+ /// ```
149+ /// # let recording = azure_core_test::Recording::with_seed();
150+ /// let i: i32 = recording.random();
151+ /// # assert_eq!(i, 1054672670);
152+ /// ```
153+ ///
146154 /// Generate a symmetric data encryption key (DEK).
147155 ///
148- /// ```no_compile
156+ /// ```
157+ /// # let recording = azure_core_test::Recording::with_seed();
149158 /// let dek: [u8; 32] = recording.random();
159+ /// # assert_eq!(typespec_client_core::base64::encode(dek), "HumPRAN6RqKWf0YhFV2CAFWu/8L/pwh0LRzeam5VlGo=");
160+ /// ```
161+ ///
162+ /// Generate a UUID.
163+ ///
164+ /// ```
165+ /// use azure_core::Uuid;
166+ /// # let recording = azure_core_test::Recording::with_seed();
167+ /// let uuid: Uuid = Uuid::from_u128(recording.random());
168+ /// # assert_eq!(uuid.to_string(), "fe906b44-5838-cc8f-05e3-c7e93edd071e");
150169 /// ```
151170 ///
152171 /// # Panics
@@ -158,52 +177,68 @@ impl Recording {
158177 where
159178 Standard : Distribution < T > ,
160179 {
161- const NAME : & str = "RandomSeed" ;
162-
163- // Use ChaCha20 for a deterministic, portable CSPRNG.
164- let rng = self . rand . get_or_init ( || match self . test_mode {
165- TestMode :: Live => ChaCha20Rng :: from_entropy ( ) . into ( ) ,
166- TestMode :: Playback => {
167- let variables = self
168- . variables
169- . read ( )
170- . map_err ( read_lock_error)
171- . unwrap_or_else ( |err| panic ! ( "{err}" ) ) ;
172- let seed: String = variables
173- . get ( NAME )
174- . map ( Into :: into)
175- . unwrap_or_else ( || panic ! ( "random seed variable not set" ) ) ;
176- let seed = base64:: decode ( seed)
177- . unwrap_or_else ( |err| panic ! ( "failed to decode random seed: {err}" ) ) ;
178- let seed = seed
179- . first_chunk :: < 32 > ( )
180- . unwrap_or_else ( || panic ! ( "insufficient random seed variable" ) ) ;
181-
182- ChaCha20Rng :: from_seed ( * seed) . into ( )
183- }
184- TestMode :: Record => {
185- let rng = ChaCha20Rng :: from_entropy ( ) ;
186- let seed = rng. get_seed ( ) ;
187- let seed = base64:: encode ( seed) ;
180+ let rng = self . rng ( ) ;
181+ let Ok ( mut rng) = rng. lock ( ) else {
182+ panic ! ( "failed to lock RNG" ) ;
183+ } ;
188184
189- let mut variables = self
190- . variables
191- . write ( )
192- . map_err ( write_lock_error)
193- . unwrap_or_else ( |err| panic ! ( "{err}" ) ) ;
194- variables. insert ( NAME . to_string ( ) , Value :: from ( Some ( seed) , None ) ) ;
185+ rng. gen ( )
186+ }
195187
196- rng. into ( )
188+ /// Generate a random string with optional prefix.
189+ ///
190+ /// This will always be the OS cryptographically secure pseudo-random number generator (CSPRNG) when running live.
191+ /// When recording, it will initialize from the OS CSPRNG but save the seed value to the recording file.
192+ /// When playing back, the saved seed value is read from the recording to reproduce the same sequence of random data.
193+ ///
194+ /// # Examples
195+ ///
196+ /// Generate a random string.
197+ ///
198+ /// ```
199+ /// # let recording = azure_core_test::Recording::with_seed();
200+ /// let id = recording.random_string::<12>(Some("t")).to_ascii_lowercase();
201+ /// # assert_eq!(id, "tpbwhbkhckmk");
202+ /// ```
203+ ///
204+ /// # Panics
205+ ///
206+ /// Panics if the recording variables cannot be locked for reading or writing,
207+ /// if the random seed cannot be encoded or decoded properly,
208+ /// if `LEN` is 0,
209+ /// or if the length of `prefix` is greater than or equal to `LEN`.
210+ ///
211+ /// ```should_panic
212+ /// # let recording = azure_core_test::Recording::with_seed();
213+ /// let vault_name = recording.random_string::<8>(Some("keyvault"));
214+ /// ```
215+ ///
216+ pub fn random_string < const LEN : usize > ( & self , prefix : Option < & str > ) -> String {
217+ struct NonZero < const N : usize > ;
218+ impl < const N : usize > NonZero < N > {
219+ const ASSERT : ( ) = assert ! ( N > 0 , "LEN must be greater than 0" ) ;
220+ }
221+ #[ allow( clippy:: let_unit_value) ]
222+ let _ = NonZero :: < LEN > :: ASSERT ;
223+ let len = match prefix {
224+ Some ( p) => {
225+ assert ! ( p. len( ) < LEN , "prefix length must be less than LEN" ) ;
226+ LEN - p. len ( )
197227 }
198- } ) ;
228+ None => LEN ,
229+ } ;
199230
231+ let rng = self . rng ( ) ;
200232 let Ok ( mut rng) = rng. lock ( ) else {
201233 panic ! ( "failed to lock RNG" ) ;
202234 } ;
203235
204- rng. gen ( )
236+ let value = Alphanumeric . sample_string ( & mut * rng, len) ;
237+ match prefix {
238+ Some ( prefix) => prefix. to_string ( ) + & value,
239+ None => value,
240+ }
205241 }
206-
207242 /// Removes the list of sanitizers from the recording.
208243 ///
209244 /// You can find a list of default sanitizers in [source code](https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/SanitizerDictionary.cs).
@@ -285,6 +320,8 @@ impl Recording {
285320 }
286321}
287322
323+ const RANDOM_SEED_NAME : & str = "RandomSeed" ;
324+
288325impl Recording {
289326 pub ( crate ) fn new (
290327 test_mode : TestMode ,
@@ -310,6 +347,28 @@ impl Recording {
310347 }
311348 }
312349
350+ // #[cfg(any(test, doctest))] // BUGBUG: https://github.com/rust-lang/rust/issues/67295
351+ #[ doc( hidden) ]
352+ pub fn with_seed ( ) -> Self {
353+ let span = tracing:: trace_span!( "Recording::seeded" ) ;
354+ Self {
355+ test_mode : TestMode :: Playback ,
356+ span : span. entered ( ) ,
357+ _proxy : None ,
358+ client : None ,
359+ policy : OnceCell :: new ( ) ,
360+ service_directory : String :: from ( "sdk/core" ) ,
361+ recording_file : String :: from ( "none" ) ,
362+ recording_assets_file : None ,
363+ id : None ,
364+ variables : RwLock :: new ( HashMap :: from ( [ (
365+ RANDOM_SEED_NAME . into ( ) ,
366+ "8S9UCR2yV8LU01tq+VNEwGssAXVUbL0Hd488GAYVosM=" . into ( ) ,
367+ ) ] ) ) ,
368+ rand : OnceLock :: new ( ) ,
369+ }
370+ }
371+
313372 fn env < K > ( & self , key : K ) -> Option < String >
314373 where
315374 K : AsRef < str > ,
@@ -322,6 +381,45 @@ impl Recording {
322381 . and_then ( |v| v. into_string ( ) . ok ( ) )
323382 }
324383
384+ fn rng ( & self ) -> & Mutex < ChaCha20Rng > {
385+ // Use ChaCha20 for a deterministic, portable CSPRNG.
386+ self . rand . get_or_init ( || match self . test_mode {
387+ TestMode :: Live => ChaCha20Rng :: from_entropy ( ) . into ( ) ,
388+ TestMode :: Playback => {
389+ let variables = self
390+ . variables
391+ . read ( )
392+ . map_err ( read_lock_error)
393+ . unwrap_or_else ( |err| panic ! ( "{err}" ) ) ;
394+ let seed: String = variables
395+ . get ( RANDOM_SEED_NAME )
396+ . map ( Into :: into)
397+ . unwrap_or_else ( || panic ! ( "random seed variable not set" ) ) ;
398+ let seed = base64:: decode ( seed)
399+ . unwrap_or_else ( |err| panic ! ( "failed to decode random seed: {err}" ) ) ;
400+ let seed = seed
401+ . first_chunk :: < 32 > ( )
402+ . unwrap_or_else ( || panic ! ( "insufficient random seed variable" ) ) ;
403+
404+ ChaCha20Rng :: from_seed ( * seed) . into ( )
405+ }
406+ TestMode :: Record => {
407+ let rng = ChaCha20Rng :: from_entropy ( ) ;
408+ let seed = rng. get_seed ( ) ;
409+ let seed = base64:: encode ( seed) ;
410+
411+ let mut variables = self
412+ . variables
413+ . write ( )
414+ . map_err ( write_lock_error)
415+ . unwrap_or_else ( |err| panic ! ( "{err}" ) ) ;
416+ variables. insert ( RANDOM_SEED_NAME . to_string ( ) , Value :: from ( Some ( seed) , None ) ) ;
417+
418+ rng. into ( )
419+ }
420+ } )
421+ }
422+
325423 fn set_skip ( & self , skip : Option < Skip > ) -> azure_core:: Result < ( ) > {
326424 let Some ( policy) = self . policy . get ( ) else {
327425 return Ok ( ( ) ) ;
@@ -502,6 +600,15 @@ impl Value {
502600 }
503601}
504602
603+ impl From < & str > for Value {
604+ fn from ( value : & str ) -> Self {
605+ Self {
606+ value : value. into ( ) ,
607+ sanitized : None ,
608+ }
609+ }
610+ }
611+
505612impl From < String > for Value {
506613 fn from ( value : String ) -> Self {
507614 Self {
0 commit comments