use crate::*; const SNAKE: &str = r##" # the game [app] id = "kos.snake" name = "Snake" version = "1.0.0" binary = "snake" author = "K-OS" [capabilities] required = ["display"] optional = ["storage",] # high scores [appearance] accent = "#e91e63" "##; fn with(line: &str, replacing: &str) -> std::string::String { SNAKE.replace(replacing, line) } extern crate std; #[test] fn a_full_manifest_reads_into_checked_values() { let m = Manifest::parse(SNAKE).unwrap(); assert_eq!(m.id.as_str(), "kos.snake"); assert_eq!(m.name.as_str(), "Snake"); assert_eq!( m.version, Version { major: 1, minor: 0, patch: 0 } ); assert_eq!(m.binary.as_str(), "snake"); assert_eq!(m.command.as_str(), "snake", "the binary's name by default"); assert_eq!(m.author.unwrap().as_str(), "K-OS"); assert_eq!(m.required, Capabilities::NONE.with(Capability::Display)); assert_eq!(m.optional, Capabilities::NONE.with(Capability::Storage)); assert_eq!(m.accent, Some(Accent(0xe91e63))); } #[test] fn only_app_identity_is_required() { let m = Manifest::parse( "[app]\nid = \"kos.cat\"\nname = \"cat\"\nversion = \"0.1.0\"\nbinary = \"cat\"\n", ) .unwrap(); assert!(m.requested().is_empty()); for key in ["id", "name", "version", "binary"] { let text = std::format!( "[app]\n{}", [ "id = \"a.b\"", "name = \"n\"", "version = \"1.0.0\"", "binary = \"b\"" ] .iter() .filter(|l| !l.starts_with(key)) .copied() .collect::>() .join("\n") ); assert_eq!(Manifest::parse(&text), Err(Error::Missing(key)), "{key}"); } } #[test] fn anything_unknown_is_refused_not_ignored() { assert_eq!( Manifest::parse(&with( "homepage = \"x\"\nauthor = \"K-OS\"", "author = \"K-OS\"" )), Err(Error::Unknown { line: 8 }) ); assert_eq!( Manifest::parse(&with( "required = [\"teleport\"]", "required = [\"display\"]" )), Err(Error::Unknown { line: 11 }) ); assert!(matches!( Manifest::parse(&with("[extras]\nx = \"y\"\n[appearance]", "[appearance]")), Err(Error::Unknown { .. }) )); } #[test] fn duplicates_are_refused() { assert!(matches!( Manifest::parse(&with( "name = \"Snake\"\nname = \"Other\"", "name = \"Snake\"" )), Err(Error::Duplicate { .. }) )); assert!(matches!( Manifest::parse(&with( "required = [\"display\", \"display\"]", "required = [\"display\"]" )), Err(Error::Duplicate { .. }) )); assert!(matches!( Manifest::parse(&std::format!("{SNAKE}\n[app]\n")), Err(Error::Duplicate { .. }) )); assert_eq!( Manifest::parse(&with( "required = [\"storage\"]", "required = [\"display\"]" )), Err(Error::Conflict) ); } #[test] fn values_are_checked() { for (bad, good) in [ ("id = \"Snake\"", "id = \"kos.snake\""), ("id = \"snake\"", "id = \"kos.snake\""), ("id = \"kos..snake\"", "id = \"kos.snake\""), ("version = \"1.0\"", "version = \"1.0.0\""), ("version = \"01.0.0\"", "version = \"1.0.0\""), ("version = \"1.0.0.0\"", "version = \"1.0.0\""), ("binary = \"../shell\"", "binary = \"snake\""), ("binary = \"bin/snake\"", "binary = \"snake\""), ("binary = \"..\"", "binary = \"snake\""), ("icon = \".\"", "author = \"K-OS\""), ("accent = \"e91e63\"", "accent = \"#e91e63\""), ("name = [\"Snake\"]", "name = \"Snake\""), ] { assert!( matches!( Manifest::parse(&with(bad, good)), Err(Error::Invalid { .. }) ), "{bad}" ); } // A binary that is no valid command needs one spelled out. let odd = with("binary = \"Snake.bin\"", "binary = \"snake\""); assert_eq!(Manifest::parse(&odd), Err(Error::Missing("command"))); let named = with( "binary = \"Snake.bin\"\ncommand = \"snake\"", "binary = \"snake\"", ); assert_eq!( Manifest::parse(&named).unwrap().binary.as_str(), "Snake.bin" ); } #[test] fn only_the_subset_parses() { for bad in [ "id = 'kos.snake'", // single quotes "id = \"kos.\\snake\"", // escapes "id = \"kos.snake", // unclosed "id = kos.snake", // bare value "id \"kos.snake\"", // no = "[app", // unclosed table "id = 12x", // not a whole number "id = 1.5", // no fractions ] { assert!( matches!( Manifest::parse(&with(bad, "id = \"kos.snake\"")), Err(Error::Syntax { .. }) ), "{bad}" ); } assert!( matches!( Manifest::parse("id = \"a.b\"\n"), Err(Error::Syntax { line: 1 }) ), "a key outside any table" ); // A comma inside a list item cannot be split wrongly: refused. assert!(matches!( Manifest::parse(&with( "optional = [\"a, b\"]", "optional = [\"storage\",] # high scores" )), Err(Error::Syntax { .. }) )); // `#` inside a string is not a comment. let hash = with("name = \"Snake #1\"", "name = \"Snake\""); assert_eq!(Manifest::parse(&hash).unwrap().name.as_str(), "Snake #1"); } #[test] fn decisions_round_trip() { let d = Decision { granted: Capabilities::NONE .with(Capability::Display) .with(Capability::Storage), }; let mut buf = [0; MAX_DECISION_TEXT]; let n = d.write(&mut buf); let text = core::str::from_utf8(&buf[..n]).unwrap(); assert_eq!(text, "[decision]\ngranted = [\"display\", \"storage\"]\n"); assert_eq!(Decision::parse(text), Ok(d)); assert_eq!( Decision::parse("[decision]\ngranted = []\n"), Ok(Decision::default()) ); assert_eq!( Decision::parse("[decision]\n"), Err(Error::Missing("granted")) ); assert!(matches!( Decision::parse("[decision]\ngranted = [\"x\"]\n"), Err(Error::Unknown { .. }) )); } #[test] fn every_capability_name_round_trips_and_fits_the_set() { assert!(Capability::ALL.len() <= 32); for c in Capability::ALL { assert_eq!(Capability::from_name(c.name()), Some(*c)); } let all: Capabilities = Capability::ALL.iter().copied().collect(); let mut buf = [0; MAX_DECISION_TEXT]; let n = Decision { granted: all }.write(&mut buf); assert!(n < MAX_DECISION_TEXT, "every name fits a decision file"); assert_eq!( Decision::parse(core::str::from_utf8(&buf[..n]).unwrap()) .unwrap() .granted, all ); } #[test] fn memory_is_in_kib_or_mib_within_bounds() { use crate::Memory; assert_eq!(Memory::parse("3 MiB").map(|m| m.pages()), Some(768)); assert_eq!(Memory::parse("512 KiB").map(|m| m.pages()), Some(128)); for bad in ["3MiB", "3 GiB", "17 MiB", "32 KiB", "6 KiB", "-1 MiB", "x MiB"] { assert_eq!(Memory::parse(bad), None, "{bad}"); } }