Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lampOS/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions lampOS/crates/update/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ rustix = { version = "=1.1.5", features = ["fs"] }

[dev-dependencies]
tempfile = "3"
# The functions a product lamp declares to a session, worked out by the
# voice, the actions part and the skills' own code (tests/functions.rs).
lamp-voice = { path = "../voice" }
lamp-actions = { path = "../actions" }
lamp-skills = { path = "../skills" }
lamp-gemini = { path = "../gemini" }

[lints]
workspace = true
76 changes: 61 additions & 15 deletions lampOS/crates/update/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,52 @@ pub const PRODUCT: &[(&str, &str)] = &[
),
];

/// The skills every lamp runs: each a folder of `lampOS/skills/`, put in
/// `/var/lib/lampOS/skills/` whole but for its `tests/` ([`SETTINGS`]).
/// Every behaviour that has a skill runs as the skill (the owner, 19 Oct):
/// the voice's own hello is off in `lamp-voice.service`, and its reminders
/// and task news wait for the skill's ask (`LAMP_SCHEDULE_SPEAK=skill`,
/// `LAMP_TASK_NEWS=skill`).
pub const PRODUCT_SKILLS: &[&str] = &[
"welcome-back",
"reminders",
"task-news",
"stretch-break",
"stuffy-air",
"dance",
];

/// The skills of `lampOS/skills/` a lamp does not get yet, each with why.
/// A product session's functions are 31 of the 32 it takes
/// (`tests/functions.rs`): the two tools of Desk tutor and its marks would
/// put the last of them past it, left out without a word.
pub const NOT_PRODUCT_SKILLS: &[(&str, &str)] = &[
(
"desk-tutor",
"it reads handwriting from the full look (`LAMP_LOOK_FULL=on` with `lamp-eyes --full-look`), \
which the product doesn't run: its look is the working frame, 320 across; and with its \
marks its two functions would be 33 of a session's 32",
),
(
"desk-tutor-marks",
"it is called only after Desk tutor's look, and Desk tutor isn't in the product",
),
];

/// One file of a product skill, `lampOS/skills/SKILL/FILE`, and where
/// `lamp-skills` reads it on a lamp: `/var/lib/lampOS/skills/SKILL/FILE`.
macro_rules! skill_file {
($skill:literal, $file:literal) => {
(
concat!("/var/lib/lampOS/skills/", $skill, "/", $file),
include_str!(concat!("../../../skills/", $skill, "/", $file)),
)
};
}

/// The product's settings that no build carries, and where each goes:
/// in `/etc/lampOS/<part>/`, readable by the parts' user (docs/install.md).
/// in `/etc/lampOS/<part>/`, readable by the parts' user (docs/install.md),
/// and the product skills' files.
pub const SETTINGS: &[(&str, &str)] = &[
(
"/etc/lampOS/expression/expression.json",
Expand All @@ -177,20 +221,22 @@ pub const SETTINGS: &[(&str, &str)] = &[
"/etc/lampOS/records.json",
include_str!("../../../deploy/product/settings/records.json"),
),
// The first skill (#811): Welcome back, the hello on arrival. Its tests
// stay in the repository; `lamp-skills` reloads the folder by itself.
(
"/var/lib/lampOS/skills/welcome-back/skill.toml",
include_str!("../../../skills/welcome-back/skill.toml"),
),
(
"/var/lib/lampOS/skills/welcome-back/SKILL.md",
include_str!("../../../skills/welcome-back/SKILL.md"),
),
(
"/var/lib/lampOS/skills/welcome-back/lights/warm_glow.toml",
include_str!("../../../skills/welcome-back/lights/warm_glow.toml"),
),
// The product skills ([`PRODUCT_SKILLS`]), every file but their tests,
// which stay in the repository; `lamp-skills` reloads the folder by
// itself. Welcome back, the hello on arrival, came first (#811).
skill_file!("welcome-back", "skill.toml"),
skill_file!("welcome-back", "SKILL.md"),
skill_file!("welcome-back", "lights/warm_glow.toml"),
skill_file!("reminders", "skill.toml"),
skill_file!("reminders", "SKILL.md"),
skill_file!("task-news", "skill.toml"),
skill_file!("task-news", "SKILL.md"),
skill_file!("stretch-break", "skill.toml"),
skill_file!("stretch-break", "SKILL.md"),
skill_file!("stuffy-air", "skill.toml"),
skill_file!("stuffy-air", "SKILL.md"),
skill_file!("dance", "skill.toml"),
skill_file!("dance", "SKILL.md"),
];

/// Setup's two settings (docs/setup.md, "On a lamp"), written only with
Expand Down
166 changes: 166 additions & 0 deletions lampOS/crates/update/tests/functions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! The functions a product lamp's voice tells each session of, with every
//! product skill installed. A session takes at most `lamp_gemini::MAX_TOOLS`
//! (32), and what is past it is left out by order, without a word: the
//! voice's own first, then the actions part's, then the skills' (the
//! coordinator, 19 Oct: lamp-4ace sat at exactly 32, and switching
//! onboarding on there pushed one of the actions part's out). So the
//! product's count is pinned here, worked out by each part's own code: the
//! voice's own from the product voice unit's environment, the actions
//! part's whole catalog (its sound, its light and Schedule's), and the tools
//! of `install::PRODUCT_SKILLS` as `lamp-skills` writes them.
use lamp_update::install;
use std::{
path::{Path, PathBuf},
process::Command,
};

const VOICE: &str = include_str!("../../../deploy/product/lamp-voice.service");
/// Set only for the run in a process of its own: the folder of the
/// catalogs it reads, there as the product's `LAMP_ACTIONS_DIR`.
const CATALOGS: &str = "LAMP_TEST_PRODUCT_CATALOGS";
/// The test run in a process of its own.
const VOICE_SIDE: &str = "the_product_voice_s_functions_in_a_process_of_their_own";
/// How that process says each function given, and each left out: found
/// anywhere in a line, as the test runner may begin the first with the
/// test's name.
const DECLARED: &str = "declared: ";
const LEFT_OUT: &str = "left out: ";

/// What a product lamp declares now: the voice's own 13, the actions part's
/// 17 and the skills' 1 (Dance's), one under the bound. A change to it is a
/// change to what every session is told: say in its pull request what is
/// left out past the bound, if anything.
const PRODUCT_FUNCTIONS: usize = 31;

fn skills_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../skills")
}

/// Every `Environment=` of the product voice unit's `[Service]`.
fn voice_environment() -> Vec<(String, String)> {
let mut inside = false;
let mut set = Vec::new();
for line in VOICE.lines().map(str::trim) {
if line.starts_with('[') {
inside = line == "[Service]";
} else if inside && let Some(pair) = line.strip_prefix("Environment=") {
let (name, value) = pair.split_once('=').unwrap();
set.push((name.to_owned(), value.to_owned()));
}
}
set
}

/// The voice's side, run in a process of its own by
/// [`a_product_lamp_declares_every_function_it_has_within_the_bound`] with
/// the product voice unit's environment and no other `LAMP_` setting, as
/// the voice's provider worker works it out: its flags from that
/// environment, its memory started (the rules' functions are told only
/// then), a session's functions, and what the link would leave out of
/// them. Does nothing in any other run.
#[test]
fn the_product_voice_s_functions_in_a_process_of_their_own() {
if std::env::var_os(CATALOGS).is_none() {
return;
}
let flags = lamp_voice::tools::arguments_from_environment();
let (settings, rest) = lamp_voice::tools::take(&flags).unwrap();
assert!(rest.is_empty(), "{rest:?}");
if let Some(socket) = &settings.memory {
lamp_voice::remember::start(socket.clone(), None);
}
let (kept, left_out) = lamp_voice::tools::admitted(lamp_voice::tools::declared(&settings));
for tool in kept {
println!("{DECLARED}{}", tool.name);
}
for why in left_out {
println!("{LEFT_OUT}{why}");
}
}

/// Every function the actions part and the product skills offer reaches a
/// product lamp's sessions, none left out by order or by the link, and the
/// count is [`PRODUCT_FUNCTIONS`], within `lamp_gemini::MAX_TOOLS`.
#[test]
fn a_product_lamp_declares_every_function_it_has_within_the_bound() {
let folder = tempfile::tempdir().unwrap();
// The actions part's catalog with all it offers: sound and light, both
// there, and Schedule's (the product's LAMP_SCHEDULE_TOOLS=on).
let mut executive = lamp_actions::catalog(true, true);
executive.extend(lamp_actions::schedule::catalog());
std::fs::write(
folder.path().join(lamp_voice::tools::CATALOG_FILE),
lamp_actions::catalog_file(&executive),
)
.unwrap();
// The skills' tools beside it, as `lamp-skills --tools-dir` writes them.
let skills: Vec<lamp_skills::Skill> = install::PRODUCT_SKILLS
.iter()
.map(|name| lamp_skills::load(&skills_root().join(name)).unwrap())
.collect();
let beside = folder.path().join(lamp_voice::tools::SKILLS_DIR);
std::fs::create_dir(&beside).unwrap();
std::fs::write(
beside.join(lamp_voice::tools::CATALOG_FILE),
lamp_skills::tools_file(&skills).to_string(),
)
.unwrap();

let mut command = Command::new(std::env::current_exe().unwrap());
command.args([VOICE_SIDE, "--exact", "--nocapture", "--test-threads=1"]);
for (name, _) in std::env::vars_os() {
if name.to_string_lossy().starts_with("LAMP_") {
command.env_remove(name);
}
}
for (name, value) in voice_environment() {
if name == "LAMP_ACTIONS_DIR" {
command.env(name, folder.path());
} else {
command.env(name, value);
}
}
let output = command.env(CATALOGS, folder.path()).output().unwrap();
let said = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"{said}{}",
String::from_utf8_lossy(&output.stderr)
);
let declared: Vec<&str> = said
.lines()
.filter_map(|line| line.split_once(DECLARED).map(|(_, name)| name))
.collect();
let left_out: Vec<&str> = said
.lines()
.filter_map(|line| line.split_once(LEFT_OUT).map(|(_, why)| why))
.collect();
assert!(left_out.is_empty(), "left out by the link: {left_out:?}");

let skill_tools: Vec<&str> = skills
.iter()
.filter_map(|skill| skill.tool.as_deref())
.collect();
for name in executive
.iter()
.map(|function| function.name)
.chain(skill_tools.iter().copied())
{
assert!(
declared.contains(&name),
"{name} is left out of a product lamp's sessions, past {}: {declared:?}",
lamp_gemini::MAX_TOOLS
);
}
println!(
"A product lamp declares {} functions of at most {}: the voice's own {}, the actions part's {}, the skills' {} ({})",
declared.len(),
lamp_gemini::MAX_TOOLS,
declared.len() - executive.len() - skill_tools.len(),
executive.len(),
skill_tools.len(),
skill_tools.join(", ")
);
assert!(declared.len() <= lamp_gemini::MAX_TOOLS);
assert_eq!(declared.len(), PRODUCT_FUNCTIONS, "{declared:?}");
}
Loading
Loading