Files
k-os/crates/manifest/src/tests.rs

254 lines
7.3 KiB
Rust

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::<std::vec::Vec<_>>()
.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}");
}
}