254 lines
7.3 KiB
Rust
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}");
|
|
}
|
|
}
|