//! The published interop vectors, run against every identifier parser. //! //! A vector file is one case per line; the reference harness treats a leading //! `#` as a comment, so this does too even though that drops one case. use std::{fs, path::Path, str::FromStr}; use manapds::syntax::{AtIdentifier, AtUri, Did, Handle, Nsid, RecordKey, Tid, TidClock}; #[test] fn at_identifier() { accepts::("atidentifier_syntax_valid.txt"); rejects::("atidentifier_syntax_invalid.txt"); } #[test] fn at_uri() { accepts::("aturi_syntax_valid.txt"); rejects::("aturi_syntax_invalid.txt"); } #[test] fn did() { accepts::("did_syntax_valid.txt"); rejects::("did_syntax_invalid.txt"); } #[test] fn handle() { accepts::("handle_syntax_valid.txt"); rejects::("handle_syntax_invalid.txt"); } #[test] fn nsid() { accepts::("nsid_syntax_valid.txt"); rejects::("nsid_syntax_invalid.txt"); } #[test] fn record_key() { accepts::("recordkey_syntax_valid.txt"); rejects::("recordkey_syntax_invalid.txt"); } #[test] fn tid() { accepts::("tid_syntax_valid.txt"); rejects::("tid_syntax_invalid.txt"); } #[test] fn at_uri_round_trips() { for vector in vectors("aturi_syntax_valid.txt") { let parsed = AtUri::from_str(&vector).expect("valid"); assert_eq!(parsed.to_string(), vector); } } #[test] fn an_at_uri_comes_apart_into_the_pieces_it_names() { let uri = AtUri::from_str("at://alice.test/app.bsky.feed.post/3jui7kd54zh2y").expect("an AT-URI"); assert_eq!(uri.authority().to_string(), "alice.test"); assert_eq!( uri.collection().map(ToString::to_string).as_deref(), Some("app.bsky.feed.post") ); assert_eq!( uri.rkey().map(ToString::to_string).as_deref(), Some("3jui7kd54zh2y") ); assert_eq!(uri.fragment(), None); // A bare authority has neither, and a fragment is kept without its hash so // that it reads as the JSON pointer it is. let repo = AtUri::from_str("at://did:plc:4cjoyc3cgpal7gnrpzyjhnv3").expect("an AT-URI"); assert_eq!(repo.collection(), None); assert_eq!(repo.rkey(), None); let field = AtUri::from_str("at://alice.test/app.bsky.feed.post/3jui7kd54zh2y#/text") .expect("an AT-URI"); assert_eq!(field.fragment(), Some("/text")); } #[test] fn tid_from_parts() { // The reference pads the clock id to two digits and the timestamp not at // all, so it agrees only above a timestamp of 32^10. assert_eq!( Tid::from_parts(1_700_000_000_000_000, 17).as_str(), "3ke6kg3wk222l" ); assert_eq!( Tid::from_parts(1_758_000_000_000_000, 1023).as_str(), "3lywl4sbs22zz" ); assert_eq!(Tid::from_parts(0, 0).as_str(), "2222222222222"); } #[test] fn minted_tids_parse_and_climb() { let mut clock = TidClock::new(); let mut previous = clock.mint(); for _ in 0..10_000 { let tid = clock.mint(); assert!(tid.as_str().parse::().is_ok(), "minted {tid}"); assert!(tid > previous, "{tid} follows {previous}"); previous = tid; } } #[test] fn a_tid_decodes_to_the_parts_it_was_made_from() { for (micros, clock_id) in [(1_700_000_000_000_000, 17), (0, 0), (1 << 52, 1023)] { assert_eq!( Tid::from_parts(micros, clock_id).to_parts(), (micros, clock_id) ); } } #[test] fn minting_clears_a_tid_already_written() { // A clock this far ahead of the wall clock only moves on by being pushed. let ahead = Tid::from_parts(1 << 52, 1023); let mut clock = TidClock::new(); let next = clock.mint_after(&ahead).expect("there is room above it"); assert!(next > ahead, "{next} follows {ahead}"); assert!(clock.mint() > next, "the floor sticks for the next one too"); } #[test] fn a_clock_at_the_top_of_its_range_still_only_goes_forward() { // The field is 53 bits of microseconds, and a rev read from a stored // commit is whatever that commit says. One at the ceiling must not wrap a // minted TID back under it. let ceiling = Tid::from_parts((1 << 53) - 1, 1023); let mut clock = TidClock::new(); assert_eq!( clock.mint_after(&ceiling), None, "nothing follows {ceiling}" ); // One step under it still has room, and what comes back is above it. let near = Tid::from_parts((1 << 53) - 2, 0); let mut clock = TidClock::new(); let next = clock.mint_after(&near).expect("one left"); assert!(next > near, "{next} follows {near}"); } fn accepts(file: &str) { for vector in vectors(file) { assert!(T::from_str(&vector).is_ok(), "rejected {vector:?}"); } } fn rejects(file: &str) { for vector in vectors(file) { assert!(T::from_str(&vector).is_err(), "accepted {vector:?}"); } } fn vectors(file: &str) -> Vec { let path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/syntax") .join(file); let text = fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); text.lines() .filter(|line| !line.is_empty() && !line.starts_with('#')) .map(str::to_owned) .collect() }