feat(v0.6): Lima E2E зелёный с нуля + shell software-render screenshot
План 5 ч.2: поднял Lima-VM и довёл сквозной E2E до зелёного из чистого yaml (just vm-reset && just e2e — exit 0). Приёмка §9.4 (v0.1 + v0.6 + шагающий скелет). Shell (lib+bin split): - режим --screenshot <path>: headless software-render первого кадра в PNG (Slint software-renderer, без дисплея/композитора, §6); TDD-тест «кадр не пустой + тема отражена», зелёный и на dev-Mac, и в VM (Linux). - shturman-shell.service → oneshot software-render → /run/shturman/frame.png (RemainAfterExit → is-active детерминированно, без хрупкого weston; живой weston-shell — v0.5). just shell-frame — инспекция кадра. E2E (tests/e2e/run.sh, двухфазно pre→reboot→post): - /data+power-safe опции, volatile-tmpfs, first-boot идемпотентность, per-unit active, имена на шине + GetPowerState, fake-ACC SetAcc→AccChanged, первый кадр PNG, base-бюджеты (journald volatile / zram / oomd / fake-hwclock→/data / eMMC-прокси), персист Settings + machine-id every-boot bind после reboot. Провижининг (lima/shturman.yaml) — правки по реальным ошибкам Lima: - build-deps Slint/winit на Linux (libfontconfig1-dev/libxkbcommon-dev/libwayland-dev); - linux-modules-extra (zram/vcan не в vz-ядре); systemd-oomd; rm стокового /etc/fake-hwclock.data (A11); VM-локальный CARGO_TARGET_DIR. Док-синхронизация (спека §13/§8.1/§7.5 + CLAUDE.md): швы реализации, eMMC-порог T=4096 сект, fake-hwclock masked-в-Lima, dev-mock policy не нужен. Перф-вердикт — на RK3588 (в VM — функционально, performance §2). just ci зелёный. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
@@ -46,9 +46,13 @@ vcan + Vehicle Simulator + моки (аудио/BT/камера/GPS/сеть/plu
|
||||
воркспейс + `shturman-common`/`ipc`/`sdk` + стаб-сервисы `firstboot`/`settings`/`power` (интеграция на D-Bus) +
|
||||
первый Slint-кадр (`shell`) + dev-tools (валидатор/scaffolding) + systemd/Lima/E2E-файлы. `just ci` зелёный.
|
||||
|
||||
**Следующее — A (План 5 ч.2):** поднять Lima-VM (`just vm-up`) и довести сквозной E2E (`just e2e`):
|
||||
boot → стаб-сервисы (`Power`/`Settings` на D-Bus) → первый Slint-кадр (screenshot). Приёмка — спека §9.4.
|
||||
Остаток фаз v0: `v0.2` boot-конвейер · `v0.3` power-safe · `v0.4` MCU/thermal · `v0.5` полный shell.
|
||||
**План 5 ч.2 — ГОТОВО (ветка `feat/v0.6-lima-e2e`):** Lima-VM поднимается (`just vm-up`), сквозной `just e2e`
|
||||
зелёный с нуля (`just vm-reset && just e2e`): boot → `data.mount` → firstboot → machine-id bind → `Power`/`Settings`
|
||||
на системной шине → fake-ACC `AccChanged` → **первый Slint-кадр** (software-render → PNG, oneshot-сервис) →
|
||||
base-бюджеты (journald volatile / zram / oomd / fake-hwclock→/data / eMMC-прокси) → **reboot**: персист Settings +
|
||||
machine-id стабилен. Приёмка §9.4 (v0.1 + v0.6 + шагающий скелет) выполнена. Швы реализации — спека §13.
|
||||
|
||||
**Следующее:** `v0.2` boot-конвейер · `v0.3` power-safe · `v0.4` MCU/thermal · `v0.5` полный shell (живой weston-shell).
|
||||
|
||||
> CI: GitHub-Actions-конфиг **удалён** (его ловит Gitea). Гейт — локальный `just ci`. CI на Gitea — решение позже.
|
||||
|
||||
|
||||
Generated
+2
@@ -3622,9 +3622,11 @@ name = "shturman-shell"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"png 0.17.16",
|
||||
"shturman-common",
|
||||
"shturman-sdk",
|
||||
"slint",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -11,3 +11,8 @@ tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
slint.workspace = true
|
||||
# PNG-кодек для headless software-render кадра (E2E-ассерт «кадр не пустой», спека §6).
|
||||
png = "0.17"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
//! `shturman-shell` (lib) — первый Slint-кадр (срезы C03/C04/C05/C07/C02) + headless
|
||||
//! software-render кадра в PNG (спека §6). Bin (`main.rs`) — тонкая обёртка над этим API:
|
||||
//! интерактивный `run` (dev: weston/нативно) либо `--screenshot <path>` (E2E/CI, без композитора).
|
||||
//! v0: одноразовое чтение `ui.theme`/Power при старте (best-effort; без шины — дефолты, #4).
|
||||
|
||||
mod theme;
|
||||
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Once;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use slint::platform::software_renderer::{MinimalSoftwareWindow, RepaintBufferType};
|
||||
use slint::platform::{Platform, PlatformError, WindowAdapter};
|
||||
use slint::ComponentHandle;
|
||||
|
||||
slint::slint! {
|
||||
import { VerticalBox, HorizontalBox } from "std-widgets.slint";
|
||||
|
||||
export component AppWindow inherits Window {
|
||||
in property <bool> is-night: false;
|
||||
in property <string> clock: "--:--";
|
||||
in property <string> network: "unknown";
|
||||
in property <string> ignition: "unknown";
|
||||
in property <[string]> tiles: ["Навигация", "Музыка", "Телефон", "Ассистент", "Машина", "Настройки"];
|
||||
|
||||
title: "Штурман";
|
||||
width: 1024px;
|
||||
height: 600px;
|
||||
background: root.is-night ? #0e1014 : #f4f5f7;
|
||||
|
||||
VerticalBox {
|
||||
padding: 16px;
|
||||
spacing: 16px;
|
||||
|
||||
HorizontalBox {
|
||||
height: 44px;
|
||||
Text {
|
||||
text: root.clock;
|
||||
font-size: 22px;
|
||||
color: root.is-night ? #f0f0f0 : #1a1a1a;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Rectangle { }
|
||||
Text {
|
||||
text: "сеть: " + root.network;
|
||||
color: root.is-night ? #9aa0a6 : #5f6368;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
text: "зажигание: " + root.ignition;
|
||||
color: root.is-night ? #9aa0a6 : #5f6368;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
spacing: 16px;
|
||||
for tile in root.tiles : Rectangle {
|
||||
background: root.is-night ? #1b1e24 : #ffffff;
|
||||
border-radius: 16px;
|
||||
Text {
|
||||
text: tile;
|
||||
font-size: 18px;
|
||||
color: root.is-night ? #e8eaed : #202124;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Размер первого кадра (логические пиксели = физические при scale 1.0).
|
||||
const FRAME_W: u32 = 1024;
|
||||
const FRAME_H: u32 = 600;
|
||||
|
||||
/// Начальное состояние кадра, прочитанное с шины (best-effort). Без шины — дефолты (#4).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Initial {
|
||||
pub theme: String,
|
||||
pub ignition: String,
|
||||
pub network: String,
|
||||
}
|
||||
|
||||
impl Default for Initial {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: "auto".into(),
|
||||
ignition: "unknown".into(),
|
||||
network: "unknown".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Одноразовое чтение состояния с шины (best-effort). Без шины/сервисов — дефолты (#4).
|
||||
pub fn read_initial() -> Initial {
|
||||
// current-thread рантайм: одно best-effort чтение на холодном старте, без пула потоков (#11).
|
||||
let rt = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return Initial::default(),
|
||||
};
|
||||
rt.block_on(async {
|
||||
match connect_and_read().await {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "нет шины/сервисов — дефолты кадра");
|
||||
Initial::default()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect_and_read() -> anyhow::Result<Initial> {
|
||||
let def = Initial::default(); // единый источник дефолтов (без дублей литералов)
|
||||
let conn = shturman_sdk::connect().await?;
|
||||
let settings = shturman_sdk::SettingsClient::new(&conn).await?;
|
||||
let theme = settings
|
||||
.get("ui.theme")
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|v| String::try_from(v).ok())
|
||||
.unwrap_or(def.theme);
|
||||
let power = shturman_sdk::PowerClient::new(&conn).await?;
|
||||
let ignition = power
|
||||
.ignition_state()
|
||||
.await
|
||||
.map(|i| i.as_str().to_string())
|
||||
.unwrap_or(def.ignition);
|
||||
Ok(Initial {
|
||||
theme,
|
||||
ignition,
|
||||
network: def.network,
|
||||
})
|
||||
}
|
||||
|
||||
/// Часы UTC `HH:MM` без tz-зависимостей (локальная tz — позже, a-base §7). Возвращает (час, строка).
|
||||
pub fn utc_hh_mm() -> (u8, String) {
|
||||
let secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let h = ((secs / 3600) % 24) as u8;
|
||||
let m = ((secs / 60) % 60) as u8;
|
||||
(h, format!("{h:02}:{m:02}"))
|
||||
}
|
||||
|
||||
/// Создать и наполнить окно (общий код интерактива и скриншота).
|
||||
fn build_ui(initial: &Initial, hour: u8, clock: &str) -> anyhow::Result<AppWindow> {
|
||||
let ui = AppWindow::new()?;
|
||||
ui.set_is_night(theme::resolve_night(&initial.theme, hour));
|
||||
ui.set_clock(clock.into());
|
||||
ui.set_network(initial.network.as_str().into());
|
||||
ui.set_ignition(initial.ignition.as_str().into());
|
||||
Ok(ui)
|
||||
}
|
||||
|
||||
/// Интерактивный запуск (dev: weston в VM / нативно на хосте). Блокирующий event-loop.
|
||||
pub fn run_interactive(initial: &Initial, hour: u8, clock: &str) -> anyhow::Result<()> {
|
||||
let ui = build_ui(initial, hour, clock)?;
|
||||
tracing::info!("первый Slint-кадр (интерактивно)");
|
||||
ui.run()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- headless software-render (спека §6: основной автотест кадра, композитор не нужен) ---
|
||||
|
||||
thread_local! {
|
||||
static SCREENSHOT_WINDOW: Rc<MinimalSoftwareWindow> =
|
||||
MinimalSoftwareWindow::new(RepaintBufferType::ReusedBuffer);
|
||||
}
|
||||
|
||||
struct ScreenshotPlatform;
|
||||
impl Platform for ScreenshotPlatform {
|
||||
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
|
||||
Ok(SCREENSHOT_WINDOW.with(|w| w.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Установить software-platform один раз на процесс. `--screenshot` зовётся в свежем процессе
|
||||
/// первым делом; в мультитестовом процессе повтор терпим (`Once` + терпимый результат).
|
||||
fn ensure_screenshot_platform() {
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
let _ = slint::platform::set_platform(Box::new(ScreenshotPlatform));
|
||||
});
|
||||
}
|
||||
|
||||
/// Headless software-render первого кадра в PNG (спека §6). Без дисплей-сервера/композитора.
|
||||
/// `hour` задаёт тему для `auto` (тест — детерминированно); `clock` берётся из текущего времени.
|
||||
pub fn render_screenshot(initial: &Initial, hour: u8, path: &Path) -> anyhow::Result<()> {
|
||||
ensure_screenshot_platform();
|
||||
let (_, clock) = utc_hh_mm();
|
||||
let ui = build_ui(initial, hour, &clock)?;
|
||||
|
||||
let window = SCREENSHOT_WINDOW.with(|w| w.clone());
|
||||
window.set_size(slint::PhysicalSize::new(FRAME_W, FRAME_H));
|
||||
ui.show()?;
|
||||
ui.window().request_redraw(); // форсим перерисовку (повторный рендер в том же потоке)
|
||||
|
||||
let mut buf = vec![slint::Rgb8Pixel { r: 0, g: 0, b: 0 }; (FRAME_W * FRAME_H) as usize];
|
||||
let drawn = window.draw_if_needed(|renderer| {
|
||||
renderer.render(buf.as_mut_slice(), FRAME_W as usize);
|
||||
});
|
||||
ui.hide()?; // освободить окно для следующего рендера в том же потоке
|
||||
if !drawn {
|
||||
anyhow::bail!("software-renderer не отрисовал кадр");
|
||||
}
|
||||
write_png(path, FRAME_W, FRAME_H, &buf)?;
|
||||
tracing::info!(path = %path.display(), "кадр записан (software-render)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_png(path: &Path, w: u32, h: u32, buf: &[slint::Rgb8Pixel]) -> anyhow::Result<()> {
|
||||
let file = std::fs::File::create(path)?;
|
||||
let mut enc = png::Encoder::new(std::io::BufWriter::new(file), w, h);
|
||||
enc.set_color(png::ColorType::Rgb);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = enc.write_header()?;
|
||||
let mut data = Vec::with_capacity((w * h * 3) as usize);
|
||||
for px in buf {
|
||||
data.extend_from_slice(&[px.r, px.g, px.b]);
|
||||
}
|
||||
writer.write_image_data(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,152 +1,36 @@
|
||||
//! `shturman-shell` — первый Slint-кадр (срезы C03/C04/C05/C07/C02). На SDK (architecture §1).
|
||||
//! v0: одноразовое чтение `ui.theme`/Power при старте (best-effort; без шины — дефолты, #4); рендер.
|
||||
//! Live-обновления (Changed/AccChanged) и локальная tz часов — позже (v0.5 / a-base §7).
|
||||
//! `shturman-shell` (bin) — тонкая обёртка над `shturman_shell` (lib):
|
||||
//! - по умолчанию: интерактивный первый Slint-кадр (dev: weston в VM / нативно на хосте);
|
||||
//! - `--screenshot <path>`: headless software-render кадра в PNG (E2E/CI, без композитора — §6).
|
||||
|
||||
mod theme;
|
||||
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
slint::slint! {
|
||||
import { VerticalBox, HorizontalBox } from "std-widgets.slint";
|
||||
|
||||
export component AppWindow inherits Window {
|
||||
in property <bool> is-night: false;
|
||||
in property <string> clock: "--:--";
|
||||
in property <string> network: "unknown";
|
||||
in property <string> ignition: "unknown";
|
||||
in property <[string]> tiles: ["Навигация", "Музыка", "Телефон", "Ассистент", "Машина", "Настройки"];
|
||||
|
||||
title: "Штурман";
|
||||
width: 1024px;
|
||||
height: 600px;
|
||||
background: root.is-night ? #0e1014 : #f4f5f7;
|
||||
|
||||
VerticalBox {
|
||||
padding: 16px;
|
||||
spacing: 16px;
|
||||
|
||||
HorizontalBox {
|
||||
height: 44px;
|
||||
Text {
|
||||
text: root.clock;
|
||||
font-size: 22px;
|
||||
color: root.is-night ? #f0f0f0 : #1a1a1a;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Rectangle { }
|
||||
Text {
|
||||
text: "сеть: " + root.network;
|
||||
color: root.is-night ? #9aa0a6 : #5f6368;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
text: "зажигание: " + root.ignition;
|
||||
color: root.is-night ? #9aa0a6 : #5f6368;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
spacing: 16px;
|
||||
for tile in root.tiles : Rectangle {
|
||||
background: root.is-night ? #1b1e24 : #ffffff;
|
||||
border-radius: 16px;
|
||||
Text {
|
||||
text: tile;
|
||||
font-size: 18px;
|
||||
color: root.is-night ? #e8eaed : #202124;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Initial {
|
||||
theme: String,
|
||||
ignition: String,
|
||||
network: String,
|
||||
}
|
||||
|
||||
impl Default for Initial {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: "auto".into(),
|
||||
ignition: "unknown".into(),
|
||||
network: "unknown".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Одноразовое чтение состояния с шины (best-effort). Без шины/сервисов — дефолты (#4).
|
||||
fn read_initial() -> Initial {
|
||||
// current-thread рантайм: одно best-effort чтение на холодном старте, без пула потоков (#11).
|
||||
let rt = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return Initial::default(),
|
||||
};
|
||||
rt.block_on(async {
|
||||
match connect_and_read().await {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "нет шины/сервисов — дефолты кадра");
|
||||
Initial::default()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect_and_read() -> anyhow::Result<Initial> {
|
||||
let def = Initial::default(); // единый источник дефолтов (без дублей литералов)
|
||||
let conn = shturman_sdk::connect().await?;
|
||||
let settings = shturman_sdk::SettingsClient::new(&conn).await?;
|
||||
let theme = settings
|
||||
.get("ui.theme")
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|v| String::try_from(v).ok())
|
||||
.unwrap_or(def.theme);
|
||||
let power = shturman_sdk::PowerClient::new(&conn).await?;
|
||||
let ignition = power
|
||||
.ignition_state()
|
||||
.await
|
||||
.map(|i| i.as_str().to_string())
|
||||
.unwrap_or(def.ignition);
|
||||
Ok(Initial {
|
||||
theme,
|
||||
ignition,
|
||||
network: def.network,
|
||||
})
|
||||
}
|
||||
|
||||
/// Часы UTC `HH:MM` без tz-зависимостей (локальная tz — позже, a-base §7). Возвращает (час, строка).
|
||||
fn utc_hh_mm() -> (u8, String) {
|
||||
let secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let h = ((secs / 3600) % 24) as u8;
|
||||
let m = ((secs / 60) % 60) as u8;
|
||||
(h, format!("{h:02}:{m:02}"))
|
||||
}
|
||||
use shturman_shell::{read_initial, render_screenshot, run_interactive, utc_hh_mm};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
shturman_common::init_tracing("shturman-shell");
|
||||
let screenshot = parse_screenshot_arg();
|
||||
let initial = read_initial();
|
||||
let (hour, clock) = utc_hh_mm();
|
||||
|
||||
let ui = AppWindow::new()?;
|
||||
ui.set_is_night(theme::resolve_night(&initial.theme, hour));
|
||||
ui.set_clock(clock.into());
|
||||
ui.set_network(initial.network.into());
|
||||
ui.set_ignition(initial.ignition.into());
|
||||
tracing::info!("первый Slint-кадр");
|
||||
ui.run()?;
|
||||
match screenshot {
|
||||
Some(path) => {
|
||||
render_screenshot(&initial, hour, &path)?;
|
||||
println!("{}", path.display()); // путь PNG — для E2E-скрипта
|
||||
}
|
||||
None => run_interactive(&initial, hour, &clock)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Разобрать `--screenshot <path>` / `--screenshot=<path>` (без внешних зависимостей).
|
||||
fn parse_screenshot_arg() -> Option<PathBuf> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(a) = args.next() {
|
||||
if a == "--screenshot" {
|
||||
return args.next().map(PathBuf::from);
|
||||
}
|
||||
if let Some(p) = a.strip_prefix("--screenshot=") {
|
||||
return Some(PathBuf::from(p));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//! Headless software-render первого кадра в PNG (спека §6 / §9.3 п.6 / §9.4 «шагающий скелет»).
|
||||
//! Работает без дисплея (и на dev-Mac, и в Lima): Slint software-renderer → PNG.
|
||||
//!
|
||||
//! Один тест намеренно: Slint-платформа процесс-глобальна и ставится один раз, а `render_screenshot`
|
||||
//! в проде зовётся ровно раз на процесс (oneshot-сервис / `just shell-frame`) — параллельный прогон
|
||||
//! нескольких рендер-тестов в одном бинаре ушёл бы в дефолтный winit-бэкенд (на macOS — только main-thread).
|
||||
|
||||
use shturman_shell::{render_screenshot, Initial};
|
||||
use std::path::Path;
|
||||
|
||||
/// Декодировать PNG → (ширина, высота, RGB-байты).
|
||||
fn decode(path: &Path) -> (u32, u32, Vec<u8>) {
|
||||
let dec = png::Decoder::new(std::fs::File::open(path).unwrap());
|
||||
let mut reader = dec.read_info().unwrap();
|
||||
let (w, h) = (reader.info().width, reader.info().height);
|
||||
let mut buf = vec![0u8; reader.output_buffer_size()];
|
||||
let info = reader.next_frame(&mut buf).unwrap();
|
||||
buf.truncate(info.buffer_size());
|
||||
(w, h, buf)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_first_frame_reflecting_theme() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
// --- ночь: кадр не пустой, верный размер, тёмный фон ---
|
||||
let night = dir.path().join("night.png");
|
||||
render_screenshot(
|
||||
&Initial {
|
||||
theme: "night".into(),
|
||||
ignition: "running".into(),
|
||||
network: "unknown".into(),
|
||||
},
|
||||
12,
|
||||
&night,
|
||||
)
|
||||
.expect("render ночь");
|
||||
|
||||
let (w, h, npx) = decode(&night);
|
||||
assert_eq!((w, h), (1024, 600), "размер кадра");
|
||||
// «не пустой» содержательно: кадр не одноцветный (нарисованы тайлы/текст, не только фон).
|
||||
let first = npx[0];
|
||||
assert!(
|
||||
npx.iter().any(|&b| b != first),
|
||||
"кадр одноцветный — рендер пустой"
|
||||
);
|
||||
// угол (0,0) = фон окна — ночью тёмный (#0e1014).
|
||||
let (nr, ng, nb) = (npx[0], npx[1], npx[2]);
|
||||
assert!(
|
||||
nr < 64 && ng < 64 && nb < 64,
|
||||
"ночной фон не тёмный: {nr},{ng},{nb}"
|
||||
);
|
||||
|
||||
// --- день: тот же угол — светлый (#f4f5f7) — тема отражена ---
|
||||
let day = dir.path().join("day.png");
|
||||
render_screenshot(
|
||||
&Initial {
|
||||
theme: "day".into(),
|
||||
..Default::default()
|
||||
},
|
||||
12,
|
||||
&day,
|
||||
)
|
||||
.expect("render день");
|
||||
let (_, _, dpx) = decode(&day);
|
||||
let (dr, dg, db) = (dpx[0], dpx[1], dpx[2]);
|
||||
assert!(
|
||||
dr > 192 && dg > 192 && db > 192,
|
||||
"дневной фон не светлый: {dr},{dg},{db}"
|
||||
);
|
||||
}
|
||||
@@ -390,9 +390,10 @@ Oneshot-бинарь под `shturman-firstboot.service` (`ConditionPathExists=!
|
||||
по мере событий).
|
||||
- **eMMC write-min (A11):** дисциплина (volatile-логи, tmpfs-транзиент, zram, без спама в `/data`).
|
||||
**Измеримая проверка (детерминированный VM-прокси):** дельта записанных секторов на loop-устройстве `/data`
|
||||
(`/proc/diskstats`) за фиксированное окно простоя (напр. 60 c после boot-settle) **ниже порога T** (🟡
|
||||
калибруется) + **нет периодических флашей вне allow-list писателей** (`fake-hwclock`, `Settings` on-Set);
|
||||
всё прочее → fail. Абсолютный байт-бюджет — вердикт на RK3588 (performance §2).
|
||||
(`/proc/diskstats`, поле 10) за фиксированное окно простоя (E2E: `E2E_IDLE_SECS`=20 c после boot-settle) **ниже
|
||||
порога T** + **нет периодических флашей вне allow-list писателей** (`fake-hwclock`, `Settings` on-Set); всё прочее
|
||||
→ fail. **Калибровка (Lima, 2026-06-24): ~80–104 сектора/20 c простоя; порог T = 4096 секторов (~2 МБ/окно),
|
||||
env `E2E_EMMC_MAX_SECTORS`.** Абсолютный байт-бюджет — вердикт на RK3588 (performance §2).
|
||||
|
||||
### 7.6 systemd-оркестрация (A15)
|
||||
|
||||
@@ -419,11 +420,18 @@ Oneshot-бинарь под `shturman-firstboot.service` (`ConditionPathExists=!
|
||||
«VM лёгкая»; правится локально); `mounts:` репозиторий **writable** (правим на хосте — собираем в VM).
|
||||
- **provision (system):** установить пакеты (`systemd`, `dbus`, `pipewire` + WirePlumber [задел v1],
|
||||
`weston` + `weston-screenshooter`, `can-utils`, `rustup`/toolchain, `python3`+venv, **`systemd-zram-generator`**,
|
||||
`fake-hwclock`, кириллические шрифты); `modprobe vcan` + `ip link add vcan0`;
|
||||
`fake-hwclock`, кириллические шрифты + **build-deps Slint/winit на Linux**: `libfontconfig1-dev`/`libxkbcommon-dev`/
|
||||
`libwayland-dev` — иначе `cargo build` shell падает на `yeslogic-fontconfig-sys`); **`linux-modules-extra-$(uname -r)`**
|
||||
(модули `zram`/`vcan` НЕ входят в базовый vz-образ Lima); `modprobe vcan` + `ip link add vcan0` (vcan может
|
||||
отсутствовать в vz-ядре — VM↔HW-граница, как раньше);
|
||||
создать loopback-`/data` (ext4 + power-safe-опции) и завести **постоянный** `data.mount`/fstab + tmpfs-overlay;
|
||||
override `fake-hwclock` пути на `/data`; разложить `systemd/`-юниты + journald/zram-generator/oomd drop-ins +
|
||||
dbus policy (прод `ru.shturman.conf` + dev-only `ru.shturman.dev.conf`); включить `shturman.target`.
|
||||
*(screenshot кадра в CI — через Slint software-renderer, без пакета grim; см. §6.)*
|
||||
override `fake-hwclock` пути на `/data` + удалить стоковый `/etc/fake-hwclock.data` (A11; сервис в Lima **masked** —
|
||||
Lima сам синхронит время, на HW юнит размаскирован и читает `FILE` через `EnvironmentFile`); разложить
|
||||
`systemd/`-юниты + journald/zram-generator/oomd drop-ins + dbus policy (прод `ru.shturman.conf`; dev-mock
|
||||
`ru.shturman.dev.PowerMock1` — интерфейс на объекте `ru.shturman.Power`, **отдельное dev-имя/policy НЕ нужны**,
|
||||
покрыт `send_destination`); включить `shturman.target`.
|
||||
*(screenshot кадра в E2E — через Slint software-renderer → PNG, без weston/grim; `shell.service` в v0.6 =
|
||||
oneshot-screenshot, живой weston-shell — v0.5; см. §6.)*
|
||||
- **reference-«BSP» (A16):** в dev это **Lima-профиль** (дев-таргет). Реальный reference-BSP (DT overlay +
|
||||
HAL + DBC) — на HW (a-base §13), вне VM.
|
||||
- **Подъём:** `just vm-up` → `limactl start --name=shturman lima/shturman.yaml` (создание+provision);
|
||||
@@ -663,6 +671,27 @@ SOFTWARE.
|
||||
- **`principles #12`**: уточнить LGPL — гранулярно (динамическая/системная линковка допустима), а не blanket;
|
||||
согласовать с `deny.toml`/§3.
|
||||
|
||||
**Реализация (План 5 ч.2 — v0.6 Lima E2E, 2026-06-24, проверено в Lima):**
|
||||
- **`shell.service` (§6/§7.6):** v0.6 = **oneshot software-render → PNG** (`shturman-shell --screenshot
|
||||
/run/shturman/frame.png`, tmpfs/volatile; `RemainAfterExit=yes` → `is-active=active` детерминированно, без
|
||||
хрупкого weston). Живой weston-shell (`ui.run()`) — **v0.5**. `shturman-shell` стал lib+bin (рендер тестируем
|
||||
headless и на dev-Mac). Доки §6/§7.6 уже называют software-renderer основным — синхронизировано.
|
||||
- **Провижининг Lima (§8.1):** добавлены build-deps `libfontconfig1-dev`/`libxkbcommon-dev`/`libwayland-dev`
|
||||
(Slint/winit на Linux тянут fontconfig — на macOS CoreText, поэтому всплыло только в VM) +
|
||||
`linux-modules-extra-$(uname -r)` (модули `zram`/`vcan` не в базовом vz-ядре). vcan-модуль всё равно может
|
||||
отсутствовать — `zram` ставится, `vcan` — best-effort (Vehicle Sim v2).
|
||||
- **fake-hwclock (§7.3):** сервис в Lima **masked** (Lima синхронит время с хоста) → override через
|
||||
`EnvironmentFile` не срабатывает в VM; скрипт читает `FILE` из env, E2E демонстрирует запись в `/data` напрямую.
|
||||
Стоковый `/etc/fake-hwclock.data` удаляется в провижининге (A11). На HW юнит размаскирован — механизм тот же.
|
||||
- **eMMC-порог T (§7.5):** калибровка ~80–104 сект/20 c → **T=4096 сект**; окно простоя 20 c (не 60).
|
||||
- **CARGO_TARGET_DIR (§8.2):** E2E/сборка в VM пишут в **VM-локальный** target (`~/.cache/shturman/target`), не в
|
||||
смонтированный `target/` — иначе конфликт Darwin↔Linux-артефактов + медленный virtiofs.
|
||||
- **dev-mock policy (§5.1):** отдельный `ru.shturman.dev.conf` для `PowerMock1` **не нужен** — это интерфейс на
|
||||
объекте `ru.shturman.Power` (имя то же), покрыт `send_destination=ru.shturman.Power`. Файл зарезервирован на
|
||||
случай отдельного dev-**имени** на шине.
|
||||
- **E2E reboot (§9.3.4):** двухфазно `just e2e` (pre → guest-reboot Lima → post); персист Settings + machine-id
|
||||
every-boot bind проверяются после реального ребута. `just run` = только pre (без ребута).
|
||||
|
||||
---
|
||||
|
||||
## 14. Дальше по ритму
|
||||
|
||||
@@ -60,9 +60,9 @@ sim:
|
||||
|
||||
# --- Lima-VM (часть 2 Плана 5: нужен limactl — brew install lima) ---
|
||||
|
||||
# поднять dev-VM (создание + провижининг)
|
||||
# поднять dev-VM (создание + провижининг). --tty=false: неинтерактивный старт (без редактора YAML).
|
||||
vm-up:
|
||||
limactl start --name=shturman lima/shturman.yaml
|
||||
limactl start --tty=false --name=shturman lima/shturman.yaml
|
||||
|
||||
# остановить VM
|
||||
vm-down:
|
||||
@@ -76,16 +76,34 @@ vm-shell:
|
||||
vm-reset:
|
||||
-limactl stop shturman
|
||||
-limactl delete shturman
|
||||
limactl start --name=shturman lima/shturman.yaml
|
||||
limactl start --tty=false --name=shturman lima/shturman.yaml
|
||||
|
||||
# собрать + развернуть + поднять target в VM (boot → сервисы → кадр)
|
||||
# собрать + развернуть + поднять target в VM (boot → сервисы → кадр); без reboot (фаза pre)
|
||||
run:
|
||||
limactl shell shturman -- bash -lc 'cd /shturman && bash tests/e2e/run.sh'
|
||||
limactl shell --workdir /shturman shturman -- bash -lc 'E2E_PHASE=pre bash tests/e2e/run.sh'
|
||||
|
||||
# сквозной E2E в VM (приёмка v0.1/v0.6 + шагающий скелет)
|
||||
# сквозной E2E в VM (приёмка v0.1/v0.6 + шагающий скелет): pre → reboot → post
|
||||
e2e:
|
||||
limactl shell shturman -- bash -lc 'cd /shturman && bash tests/e2e/run.sh'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "== E2E фаза PRE (сборка → подъём → проверки → персист-проба) =="
|
||||
limactl shell --workdir /shturman shturman -- bash -lc 'E2E_PHASE=pre bash tests/e2e/run.sh'
|
||||
echo
|
||||
echo "== reboot VM (проверка персиста + machine-id every-boot bind) =="
|
||||
limactl shell --workdir / shturman -- sudo systemctl reboot 2>/dev/null || true
|
||||
echo "== ждём возврат VM =="
|
||||
for i in $(seq 1 60); do
|
||||
sleep 2
|
||||
if limactl shell --workdir / shturman -- true 2>/dev/null; then echo "VM вернулась (попытка $i)"; break; fi
|
||||
done
|
||||
limactl shell --workdir / shturman -- bash -lc 'for i in $(seq 1 30); do systemctl is-active --quiet shturman.target && break; sleep 1; done' 2>/dev/null || true
|
||||
echo
|
||||
echo "== E2E фаза POST (персист после reboot) =="
|
||||
limactl shell --workdir /shturman shturman -- bash -lc 'E2E_PHASE=post bash tests/e2e/run.sh'
|
||||
echo
|
||||
echo "== E2E OK ✅ =="
|
||||
|
||||
# ручная проверка кадра: на хосте — окно Slint (headless PNG-screenshot — часть 2/Lima)
|
||||
shell-frame:
|
||||
cargo run -p shturman-shell
|
||||
# ручная инспекция кадра: headless software-render первого кадра → PNG (без дисплея/композитора, §6)
|
||||
shell-frame path="target/shell-frame.png":
|
||||
cargo run -q -p shturman-shell -- --screenshot {{path}}
|
||||
@echo "кадр записан: {{path}}"
|
||||
|
||||
+11
-4
@@ -26,9 +26,14 @@ provision:
|
||||
apt-get install -y \
|
||||
dbus pipewire wireplumber weston \
|
||||
can-utils python3 python3-venv \
|
||||
systemd-zram-generator fake-hwclock \
|
||||
systemd-zram-generator systemd-oomd fake-hwclock \
|
||||
fonts-dejavu-core fonts-noto-core \
|
||||
build-essential pkg-config curl
|
||||
build-essential pkg-config curl \
|
||||
libfontconfig1-dev libxkbcommon-dev libwayland-dev
|
||||
|
||||
# zram/vcan-модули не входят в базовый vz-образ Lima → доустановить linux-modules-extra
|
||||
# (иначе zram-generator падает). vcan всё равно может отсутствовать — честная VM↔HW-граница.
|
||||
apt-get install -y "linux-modules-extra-$(uname -r)" || true
|
||||
|
||||
# vcan (для Vehicle Simulator, v2 — поднимаем заранее для воспроизводимости)
|
||||
modprobe vcan || true
|
||||
@@ -54,11 +59,13 @@ provision:
|
||||
install -d /etc/systemd/oomd.conf.d
|
||||
install -m644 /shturman/systemd/oomd-shturman.conf /etc/systemd/oomd.conf.d/shturman.conf
|
||||
|
||||
# fake-hwclock → /data (не на rootfs; A07/A11)
|
||||
# fake-hwclock → /data (не на rootfs; A07/A11). Сервис в Lima masked (Lima сам синхронит время) —
|
||||
# на HW он размаскирован и читает FILE из /etc/default/fake-hwclock через EnvironmentFile.
|
||||
echo 'FILE=/data/state/fake-hwclock.data' > /etc/default/fake-hwclock || true
|
||||
rm -f /etc/fake-hwclock.data || true # стоковый файл на rootfs — A11: персист только в /data
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable systemd-oomd.service || true
|
||||
systemctl enable --now systemd-oomd.service || true # защита critical set от OOM (A09); политика — oomd.conf.d
|
||||
# shturman.target включаем, но НЕ стартуем здесь — бинарей ещё нет (just run/e2e).
|
||||
systemctl enable shturman.target || true
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
[Unit]
|
||||
Description=Штурман Shell (первый Slint-кадр)
|
||||
Description=Штурман Shell — первый Slint-кадр (software-render → PNG, §6)
|
||||
Requires=data.mount shturman-firstboot.service
|
||||
After=shturman-power.service shturman-settings.service shturman-machineid.service
|
||||
PartOf=shturman.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/shturman-shell
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
# v0.6: headless software-render кадра в PNG (спека §6 — основной автотест кадра, композитор не нужен).
|
||||
# oneshot+RemainAfterExit → is-active=active детерминированно, без хрупкого weston (живой weston-shell — v0.5).
|
||||
# Кадр читает ui.theme/Power с системной шины (After=power/settings) и пишет в tmpfs /run (volatile, A11).
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
RuntimeDirectory=shturman
|
||||
ExecStart=/usr/local/bin/shturman-shell --screenshot /run/shturman/frame.png
|
||||
TimeoutStartSec=30
|
||||
OOMScoreAdjust=-600
|
||||
# Wayland-дисплей: provisioning/E2E поднимает weston headless (финализируется в части 2).
|
||||
Environment=WAYLAND_DISPLAY=wayland-1
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/0
|
||||
|
||||
[Install]
|
||||
WantedBy=shturman.target
|
||||
|
||||
+183
-35
@@ -1,54 +1,202 @@
|
||||
#!/usr/bin/env bash
|
||||
# Сквозной E2E Штурмана в Lima-VM (приёмка v0.1/v0.6 + шагающий скелет, спека §9.3/§9.4).
|
||||
# Запуск: just e2e (внутри VM через limactl shell). Системная шина устройства.
|
||||
# Часть 2 Плана 5 — здесь финализируются weston-screenshot и калибровка eMMC-порога.
|
||||
set -euo pipefail
|
||||
# Запуск: just e2e (двухфазно через reboot) или just run (однофазно, без reboot).
|
||||
#
|
||||
# Фазы (env E2E_PHASE):
|
||||
# pre — сборка → install бинарей/юнитов → старт shturman.target → проверки §9.3 (1–8)
|
||||
# → выставить персист-пробу (Settings.Set + запомнить machine-id); [по умолчанию]
|
||||
# post — после reboot: персист настройки сохранился + machine-id стабилен (every-boot bind, §9.3.4).
|
||||
#
|
||||
# Системная шина устройства (§5.1). dev-mocks включён дефолт-фичей сборки (fake-ACC).
|
||||
set -uo pipefail
|
||||
|
||||
REPO=/shturman
|
||||
cd "$REPO"
|
||||
cd "$REPO" || { echo "нет $REPO"; exit 1; }
|
||||
|
||||
echo "== сборка =="
|
||||
cargo build --release --workspace
|
||||
sudo install -m755 target/release/shturman-firstboot /usr/local/bin/
|
||||
sudo install -m755 target/release/shturman-settings /usr/local/bin/
|
||||
sudo install -m755 target/release/shturman-power /usr/local/bin/
|
||||
sudo install -m755 target/release/shturman-shell /usr/local/bin/
|
||||
PHASE="${E2E_PHASE:-pre}"
|
||||
IDLE_SECS="${E2E_IDLE_SECS:-20}" # окно простоя для eMMC-прокси (§7.5)
|
||||
EMMC_MAX_SECTORS="${E2E_EMMC_MAX_SECTORS:-4096}" # порог T (🟡 калибруется; 4096 сект ≈ 2 МБ/окно)
|
||||
FRAME=/run/shturman/frame.png
|
||||
PROBE_KEY=ui.theme
|
||||
PROBE_VAL=night
|
||||
MID_BEFORE=/data/state/e2e-mid-before # снимок machine-id до reboot (персист в /data)
|
||||
|
||||
echo "== старт target =="
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start shturman.target
|
||||
sleep 3
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$HOME/.cache/shturman/target}"
|
||||
|
||||
pass() { echo " ✓ $*"; }
|
||||
fail() { echo "E2E FAIL: $*" >&2; exit 1; }
|
||||
info() { echo; echo "== $* =="; }
|
||||
|
||||
echo "== 1. /data смонтирован до сервисов, реальные опции =="
|
||||
findmnt /data || fail "/data не смонтирован"
|
||||
findmnt -no OPTIONS /data | grep -q errors=remount-ro || fail "нет errors=remount-ro"
|
||||
# Имена на шине (зеркало crates/shturman-ipc/src/names.rs).
|
||||
P_NAME=ru.shturman.Power; P_PATH=/ru/shturman/Power; P_IFACE=ru.shturman.Power1
|
||||
P_MOCK=ru.shturman.dev.PowerMock1
|
||||
S_NAME=ru.shturman.Settings; S_PATH=/ru/shturman/Settings; S_IFACE=ru.shturman.Settings1
|
||||
|
||||
echo "== 2. first-boot идемпотентен =="
|
||||
test -f /data/.shturman-provisioned || fail "нет маркера provisioned"
|
||||
test -f /data/state/machine-id || fail "нет machine-id"
|
||||
settings_get() { busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Get s "$1" 2>/dev/null; }
|
||||
|
||||
echo "== 3. per-unit critical set active (не довольствуемся degraded) =="
|
||||
# =========================== POST-reboot фаза ===========================
|
||||
if [ "$PHASE" = post ]; then
|
||||
info "POST-reboot: персист настроек + machine-id стабилен (§9.3.4)"
|
||||
# дождаться, пока сервисы поднимутся после автозагрузки target
|
||||
for _ in $(seq 1 30); do systemctl is-active --quiet shturman-settings && break; sleep 1; done
|
||||
|
||||
findmnt /data >/dev/null || fail "/data не смонтирован после reboot"
|
||||
pass "/data смонтирован после reboot"
|
||||
|
||||
# настройка пережила reboot
|
||||
got=$(settings_get "$PROBE_KEY")
|
||||
echo "$got" | grep -q "\"$PROBE_VAL\"" || fail "Settings.$PROBE_KEY != $PROBE_VAL после reboot (got: $got)"
|
||||
pass "Settings.$PROBE_KEY = $PROBE_VAL пережил reboot"
|
||||
|
||||
# machine-id стабилен (every-boot bind из /data/state/machine-id)
|
||||
sudo test -f "$MID_BEFORE" || fail "нет снимка $MID_BEFORE (фаза pre не отработала?)"
|
||||
before=$(sudo cat "$MID_BEFORE"); now=$(cat /etc/machine-id)
|
||||
[ -n "$now" ] || fail "/etc/machine-id пуст"
|
||||
[ "$before" = "$now" ] || fail "machine-id изменился: было $before, стало $now"
|
||||
src=$(sudo cat /data/state/machine-id)
|
||||
[ "$now" = "$src" ] || fail "/etc/machine-id($now) != /data/state/machine-id($src) — bind не сработал"
|
||||
pass "machine-id стабилен после reboot ($now), привязан из /data"
|
||||
|
||||
# journald volatile теперь естественно (drop-in присутствовал на boot)
|
||||
test -d /run/log/journal && ! test -d /var/log/journal \
|
||||
&& pass "journald volatile (/run/log/journal)" || echo " WARN: journald не строго volatile"
|
||||
|
||||
echo; echo "E2E POST OK ✅"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ============================ PRE фаза ============================
|
||||
info "сборка (release, VM-локальный target=$CARGO_TARGET_DIR)"
|
||||
cargo build --release --workspace || fail "сборка"
|
||||
for b in firstboot settings power shell; do
|
||||
sudo install -m755 "$CARGO_TARGET_DIR/release/shturman-$b" /usr/local/bin/ || fail "install shturman-$b"
|
||||
done
|
||||
pass "бинари установлены в /usr/local/bin"
|
||||
|
||||
info "раскладка systemd-юнитов + dbus policy (из репо — подхватить правки)"
|
||||
sudo install -m644 systemd/shturman.target systemd/data.mount /etc/systemd/system/
|
||||
sudo install -m644 systemd/shturman-firstboot.service systemd/shturman-machineid.service \
|
||||
systemd/shturman-power.service systemd/shturman-settings.service \
|
||||
systemd/shturman-shell.service /etc/systemd/system/
|
||||
sudo install -d /etc/dbus-1/system.d
|
||||
sudo install -m644 systemd/dbus/ru.shturman.conf /etc/dbus-1/system.d/
|
||||
sudo install -d /etc/systemd/journald.conf.d /etc/systemd/oomd.conf.d
|
||||
sudo install -m644 systemd/journald-shturman.conf /etc/systemd/journald.conf.d/shturman.conf
|
||||
sudo install -m644 systemd/oomd-shturman.conf /etc/systemd/oomd.conf.d/shturman.conf
|
||||
sudo install -m644 systemd/zram-generator.conf /etc/systemd/zram-generator.conf
|
||||
sudo systemctl daemon-reload
|
||||
# применить конфиги детерминированно (на свежем boot drop-in’ы появились после старта демонов)
|
||||
sudo systemctl reload dbus 2>/dev/null || true
|
||||
sudo systemctl restart systemd-journald 2>/dev/null || true
|
||||
sudo rm -rf /var/log/journal 2>/dev/null || true # устаревший persistent-журнал (до drop-in); volatile его не пересоздаст
|
||||
sudo modprobe zram 2>/dev/null || true # zram-модуль (linux-modules-extra); может отсутствовать в vz-ядре
|
||||
sudo systemctl start "systemd-zram-setup@zram0.service" 2>/dev/null || true
|
||||
sudo systemctl restart systemd-oomd 2>/dev/null || sudo systemctl start systemd-oomd 2>/dev/null || true # подхватить oomd.conf.d
|
||||
pass "юниты/политики разложены"
|
||||
|
||||
info "старт shturman.target"
|
||||
sudo systemctl start shturman.target || fail "shturman.target не стартовал"
|
||||
sudo systemctl restart shturman-shell.service || true # свежий кадр (на повторном прогоне)
|
||||
for _ in $(seq 1 15); do systemctl is-active --quiet shturman-shell && break; sleep 1; done
|
||||
|
||||
# ---- 1. /data до сервисов + реальные power-safe опции (§9.3.1) ----
|
||||
info "1. /data смонтирован, реальные non-default опции + volatile-слой"
|
||||
findmnt /data >/dev/null || fail "/data не смонтирован"
|
||||
opts=$(findmnt -no OPTIONS /data)
|
||||
echo "$opts" | grep -q errors=remount-ro || fail "нет errors=remount-ro (opts: $opts)"
|
||||
pass "/data: $opts"
|
||||
# volatile-слой (tmpfs) присутствует — кадр/журнал/транзиент пишутся сюда, не на flash (A11).
|
||||
# Полный RO-rootfs + overlay(upper на tmpfs) — на HW/v4 (A/B boot-select нет в VM, §7.1); тут — дисциплина + tmpfs.
|
||||
findmnt -t tmpfs /run >/dev/null || fail "/run не tmpfs (нет volatile-слоя)"
|
||||
pass "volatile-слой: /run = tmpfs"
|
||||
|
||||
# ---- 2. first-boot маркер + идемпотентность (§9.3.2) ----
|
||||
info "2. first-boot маркер + machine-id"
|
||||
sudo test -f /data/.shturman-provisioned || fail "нет маркера .shturman-provisioned"
|
||||
sudo test -f /data/state/machine-id || fail "нет /data/state/machine-id"
|
||||
sudo systemctl start shturman-firstboot.service # повторно — Condition гейтит → no-op
|
||||
pass "first-boot маркер на месте, повторный запуск no-op"
|
||||
|
||||
# ---- 3. per-unit critical set active (degraded не маскирует, §9.3.1) ----
|
||||
info "3. per-unit critical set"
|
||||
for u in shturman-power shturman-settings shturman-shell; do
|
||||
systemctl is-active --quiet "$u" || fail "$u не active"
|
||||
systemctl is-active --quiet "$u" || fail "$u не active ($(systemctl is-active "$u" 2>&1))"
|
||||
pass "$u: active"
|
||||
done
|
||||
for u in shturman-firstboot shturman-machineid; do
|
||||
state=$(systemctl is-active "$u" 2>&1)
|
||||
[ "$state" = active ] || fail "$u не active(exited): $state"
|
||||
pass "$u: $state (oneshot)"
|
||||
done
|
||||
|
||||
echo "== 4. имена на системной шине =="
|
||||
busctl --system list | grep -q ru.shturman.Power || fail "нет ru.shturman.Power"
|
||||
busctl --system list | grep -q ru.shturman.Settings || fail "нет ru.shturman.Settings"
|
||||
# ---- 4. имена на системной шине (own) + сервис отвечает (§9.3.3) ----
|
||||
info "4. имена на шине + отклик"
|
||||
busctl --system list | grep -q "$P_NAME" || fail "нет $P_NAME на шине"
|
||||
busctl --system list | grep -q "$S_NAME" || fail "нет $S_NAME на шине"
|
||||
busctl --system call "$P_NAME" "$P_PATH" "$P_IFACE" GetPowerState | grep -q running \
|
||||
|| fail "Power.GetPowerState != running"
|
||||
pass "$P_NAME / $S_NAME владеют именами; GetPowerState=running"
|
||||
|
||||
echo "== 5. fake-ACC: SetAcc -> AccChanged =="
|
||||
# (подписка+вызов dev.PowerMock1; реализация ассерта — busctl monitor/call, финал в части 2)
|
||||
# ---- 5. fake-ACC: SetAcc -> AccChanged (§9.3.5) ----
|
||||
info "5. fake-ACC SetAcc -> AccChanged"
|
||||
mon=$(mktemp)
|
||||
# sudo нужен busctl для eavesdrop системной шины; редирект в $mon — намеренно user-owned mktemp (SC2024 ок).
|
||||
# shellcheck disable=SC2024
|
||||
sudo busctl --system monitor "$P_NAME" >"$mon" 2>&1 &
|
||||
MON=$!
|
||||
sleep 1
|
||||
busctl --system call "$P_NAME" "$P_PATH" "$P_MOCK" SetAcc b false || { sudo kill "$MON" 2>/dev/null; fail "вызов SetAcc"; }
|
||||
sleep 1
|
||||
sudo kill "$MON" 2>/dev/null; wait "$MON" 2>/dev/null
|
||||
grep -q AccChanged "$mon" || { echo "--- monitor ---"; cat "$mon"; fail "AccChanged не наблюдаем"; }
|
||||
rm -f "$mon"
|
||||
busctl --system call "$P_NAME" "$P_PATH" "$P_MOCK" SetAcc b true >/dev/null 2>&1 || true # вернуть acc=on
|
||||
pass "AccChanged наблюдаем после SetAcc"
|
||||
|
||||
echo "== 6. персист настроек через reboot + machine-id стабилен =="
|
||||
# (Settings.Set -> sudo reboot -> повторный прогон сверяет; оформляется в части 2)
|
||||
# ---- 7. первый Slint-кадр: PNG не пустой (§9.3.6) ----
|
||||
info "7. первый Slint-кадр (software-render PNG)"
|
||||
sudo test -f "$FRAME" || fail "нет кадра $FRAME (shell.service не отрендерил?)"
|
||||
sz=$(sudo stat -c%s "$FRAME"); [ "$sz" -gt 10000 ] || fail "кадр подозрительно мал ($sz Б)"
|
||||
sudo head -c8 "$FRAME" | od -An -tx1 | tr -d ' \n' | grep -qi "89504e47" || fail "$FRAME не PNG"
|
||||
pass "кадр $FRAME: $sz Б, валидный PNG"
|
||||
|
||||
echo "== 7. первый кадр (software-render PNG не пустой) =="
|
||||
# (weston headless + shturman-shell + screenshot; финал — часть 2)
|
||||
# ---- 8. base-бюджеты: journald / zram / fake-hwclock / eMMC-прокси (§9.3.7) ----
|
||||
info "8. base-бюджеты (функц.)"
|
||||
# journald volatile: активный журнал в /run/log/journal, persistent /var/log/journal отсутствует (A10)
|
||||
test -d /run/log/journal || fail "journald не volatile (нет /run/log/journal)"
|
||||
test -d /var/log/journal && fail "journald пишет в persistent /var/log/journal (нарушение A10)"
|
||||
pass "journald volatile (/run/log/journal, без /var/log/journal)"
|
||||
# ровно одно zram-устройство (A09); модуль zram может отсутствовать в vz-ядре — честная VM↔HW-граница
|
||||
zn=$(zramctl --noheadings 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$zn" = 1 ]; then pass "zram: ровно одно устройство ($(zramctl --noheadings --output NAME 2>/dev/null))"
|
||||
elif [ "$zn" = 0 ]; then echo " WARN: zram-устройств нет (модуль zram отсутствует в vz-ядре — HW-only, §13)"
|
||||
else fail "zram: ожидалось одно устройство, найдено $zn"; fi
|
||||
# fake-hwclock пишет в /data, не в /etc (A11). Override — FILE из /etc/default/fake-hwclock
|
||||
# (сервис в Lima masked: Lima сам синхронит время — на HW юнит размаскирован, EnvironmentFile тот же).
|
||||
sudo sh -c '. /etc/default/fake-hwclock 2>/dev/null; FILE="${FILE:-/data/state/fake-hwclock.data}" fake-hwclock save' || true
|
||||
sudo test -f /data/state/fake-hwclock.data || fail "fake-hwclock не записал в /data/state/fake-hwclock.data"
|
||||
sudo test -f /etc/fake-hwclock.data && fail "fake-hwclock пишет в /etc (нарушение A11)"
|
||||
pass "fake-hwclock → /data/state/fake-hwclock.data (не в /etc)"
|
||||
# systemd-oomd: запущен + наша политика загружена (A09). PSI/cgroup2 нужны — в vz есть; иначе honest WARN.
|
||||
if [ "$(systemctl is-active systemd-oomd 2>/dev/null)" = active ] && test -f /etc/systemd/oomd.conf.d/shturman.conf; then
|
||||
pass "systemd-oomd active, политика oomd.conf.d/shturman.conf загружена"
|
||||
else echo " WARN: systemd-oomd не active (нет PSI/пакета — проверь провижининг)"; fi
|
||||
# eMMC-прокси: дельта записанных секторов loop-/data за окно простоя
|
||||
src=$(findmnt -no SOURCE /data); dev=$(basename "$src")
|
||||
if [ ! -e "/sys/block/$dev" ]; then dev=$(losetup -j /var/lib/shturman/data.img -O NAME --noheadings 2>/dev/null | tr -d ' ' | xargs -r basename); fi
|
||||
read_w() { awk -v d="$dev" '$3==d {print $10}' /proc/diskstats; }
|
||||
s0=$(read_w); echo " окно простоя ${IDLE_SECS}s (loop-dev=$dev)…"; sleep "$IDLE_SECS"; s1=$(read_w)
|
||||
delta=$(( ${s1:-0} - ${s0:-0} ))
|
||||
echo " eMMC-прокси: записано $delta секторов за ${IDLE_SECS}s (порог $EMMC_MAX_SECTORS, ~калибровка)"
|
||||
[ "$delta" -le "$EMMC_MAX_SECTORS" ] || fail "eMMC: дельта $delta > порога $EMMC_MAX_SECTORS секторов"
|
||||
pass "eMMC-прокси в пороге"
|
||||
|
||||
echo "== 8. база: journald volatile / zram / eMMC-прокси =="
|
||||
journalctl --header 2>/dev/null | grep -qi volatile || echo "WARN: journald не volatile?"
|
||||
zramctl | grep -q zram0 || echo "WARN: zram0 не активен?"
|
||||
# ---- персист-проба для POST-фазы ----
|
||||
info "персист-проба (для проверки после reboot)"
|
||||
busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Set sv "$PROBE_KEY" s "$PROBE_VAL" || fail "Settings.Set"
|
||||
got=$(settings_get "$PROBE_KEY"); echo "$got" | grep -q "\"$PROBE_VAL\"" || fail "Set не применился (got: $got)"
|
||||
sudo cp /etc/machine-id "$MID_BEFORE" # снимок до reboot
|
||||
pass "Settings.$PROBE_KEY=$PROBE_VAL выставлен; machine-id снят в $MID_BEFORE"
|
||||
|
||||
echo "E2E OK (каркас; пункты 5–7 финализируются в части 2)"
|
||||
echo; echo "E2E PRE OK ✅ (для полной приёмки — reboot + фаза post: just e2e)"
|
||||
|
||||
Reference in New Issue
Block a user