feat(v0.3): Power-сервис на FSM — dev-mock кормит события, grace+durable-barrier
P7.2: service.rs оборачивает PowerFsm — D-Bus state/signals из FSM; apply_event исполняет действия (эмит сигналов, фоновый grace-таймер, durable-barrier sync). dev-mock SetAcc/SetIgnition/TriggerShutdown/AbortShutdown кормят входы FSM. FSM: AccOff → AccChanged(false)+ShutdownImminent (сохранён walking-skeleton-регресс). Integration: ShutdownImminent + abort. zbus → tokio-executor (default-features=false, features=["tokio"]) — иначе tokio::spawn в хендлере паникует (async-io). test-integration --test-threads=1 (тесты владеют одним именем на шине). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
Generated
+14
-17
@@ -165,17 +165,6 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-fs"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
|
|
||||||
dependencies = [
|
|
||||||
"async-lock",
|
|
||||||
"blocking",
|
|
||||||
"futures-lite",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-io"
|
name = "async-io"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
@@ -3885,6 +3874,16 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "softbuffer"
|
name = "softbuffer"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -4209,11 +4208,14 @@ version = "1.52.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
|
"tracing",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5441,15 +5443,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
|
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
"async-executor",
|
|
||||||
"async-fs",
|
|
||||||
"async-io",
|
|
||||||
"async-lock",
|
|
||||||
"async-process",
|
"async-process",
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-task",
|
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"blocking",
|
|
||||||
"enumflags2",
|
"enumflags2",
|
||||||
"event-listener",
|
"event-listener",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -5463,6 +5459,7 @@ dependencies = [
|
|||||||
"serde_repr",
|
"serde_repr",
|
||||||
"sha1",
|
"sha1",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uds_windows",
|
"uds_windows",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
|
|||||||
+2
-1
@@ -22,7 +22,8 @@ rust-version = "1.96"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "time"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "time"] }
|
||||||
zbus = "4"
|
# tokio-executor у zbus (а не async-io) — сервисы на #[tokio::main]; нужно для tokio::spawn в хендлерах (grace-таймер).
|
||||||
|
zbus = { version = "4", default-features = false, features = ["tokio"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
|||||||
@@ -102,7 +102,16 @@ impl PowerFsm {
|
|||||||
self.state = Accessory;
|
self.state = Accessory;
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
(Accessory | Running, E::AccOff) => self.begin_shutdown(ShutdownReason::AccOff),
|
// ACC-off: линия ACC сменилась (AccChanged) + старт shutdown.
|
||||||
|
(Accessory | Running, E::AccOff) => {
|
||||||
|
self.state = ShuttingDown { phase: Abortable, reason: ShutdownReason::AccOff };
|
||||||
|
vec![
|
||||||
|
Action::AccChanged(false),
|
||||||
|
Action::ShutdownImminent(ShutdownReason::AccOff),
|
||||||
|
Action::StartGrace,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// under-voltage/thermal: ACC не менялся → без AccChanged.
|
||||||
(Accessory | Running, E::UnderVoltage) => self.begin_shutdown(ShutdownReason::UnderVoltage),
|
(Accessory | Running, E::UnderVoltage) => self.begin_shutdown(ShutdownReason::UnderVoltage),
|
||||||
(Accessory | Running, E::ThermalTrip) => self.begin_shutdown(ShutdownReason::Thermal),
|
(Accessory | Running, E::ThermalTrip) => self.begin_shutdown(ShutdownReason::Thermal),
|
||||||
(ShuttingDown { phase: Abortable, .. }, E::AccOn) => {
|
(ShuttingDown { phase: Abortable, .. }, E::AccOn) => {
|
||||||
@@ -149,7 +158,11 @@ mod tests {
|
|||||||
let mut f = PowerFsm::new(); // Running
|
let mut f = PowerFsm::new(); // Running
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
f.step(Event::AccOff),
|
f.step(Event::AccOff),
|
||||||
vec![Action::ShutdownImminent(ShutdownReason::AccOff), Action::StartGrace]
|
vec![
|
||||||
|
Action::AccChanged(false),
|
||||||
|
Action::ShutdownImminent(ShutdownReason::AccOff),
|
||||||
|
Action::StartGrace
|
||||||
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(f.power_state(), PowerState::ShuttingDown);
|
assert_eq!(f.power_state(), PowerState::ShuttingDown);
|
||||||
assert_eq!(f.source(), PowerSource::HoldupCap);
|
assert_eq!(f.source(), PowerSource::HoldupCap);
|
||||||
|
|||||||
@@ -1,39 +1,24 @@
|
|||||||
//! Server-стаб `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC).
|
//! Server `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC).
|
||||||
//! zbus 4: несколько интерфейсов на одном объекте — это РАЗНЫЕ типы на одном пути, разделяющие
|
//! v0.3: оборачивает чистый `PowerFsm` (спека §5–§7). dev-mock кормит входы FSM (не флипает состояние).
|
||||||
//! состояние через `Arc<Mutex<State>>` (а не два `#[interface]` на одном типе).
|
|
||||||
|
|
||||||
|
use crate::fsm::{Action, Event, PowerFsm};
|
||||||
use shturman_common::monotonic_secs;
|
use shturman_common::monotonic_secs;
|
||||||
use shturman_ipc::types::{IgnitionState, PowerSource, PowerState};
|
use shturman_ipc::types::{IgnitionState, PowerSource, PowerState};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
use zbus::interface;
|
use zbus::interface;
|
||||||
use zbus::object_server::SignalContext;
|
use zbus::object_server::SignalContext;
|
||||||
|
|
||||||
struct State {
|
/// Grace-окно (сек): и поле сигнала `ShutdownImminent`, и длительность таймера. v0.3 — фикс. (конфиг — позже).
|
||||||
power: PowerState,
|
const GRACE_SECS: u32 = 2;
|
||||||
ignition: IgnitionState,
|
|
||||||
source: PowerSource,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for State {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
power: PowerState::Running,
|
|
||||||
ignition: IgnitionState::Running,
|
|
||||||
source: PowerSource::Vehicle12v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Стаб питания (`Power1`). В v0 стартует в `running`; запись/actuator отсутствуют (#2).
|
|
||||||
pub struct PowerService {
|
pub struct PowerService {
|
||||||
state: Arc<Mutex<State>>,
|
fsm: Arc<Mutex<PowerFsm>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PowerService {
|
impl Default for PowerService {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self { fsm: Arc::new(Mutex::new(PowerFsm::new())) }
|
||||||
state: Arc::new(Mutex::new(State::default())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,25 +26,60 @@ impl PowerService {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inherent-аксессоры (тесты + источник для interface-методов).
|
|
||||||
pub fn power_state(&self) -> PowerState {
|
pub fn power_state(&self) -> PowerState {
|
||||||
self.state.lock().unwrap().power
|
self.fsm.lock().unwrap().power_state()
|
||||||
}
|
}
|
||||||
pub fn ignition(&self) -> IgnitionState {
|
pub fn ignition(&self) -> IgnitionState {
|
||||||
self.state.lock().unwrap().ignition
|
self.fsm.lock().unwrap().ignition()
|
||||||
}
|
}
|
||||||
pub fn source(&self) -> PowerSource {
|
pub fn source(&self) -> PowerSource {
|
||||||
self.state.lock().unwrap().source
|
self.fsm.lock().unwrap().source()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// dev-mock «fake-ACC», разделяющий состояние (только в dev-сборке).
|
|
||||||
#[cfg(feature = "dev-mocks")]
|
#[cfg(feature = "dev-mocks")]
|
||||||
pub fn mock(&self) -> PowerMock {
|
pub fn mock(&self) -> PowerMock {
|
||||||
PowerMock {
|
PowerMock { fsm: Arc::clone(&self.fsm) }
|
||||||
state: Arc::clone(&self.state),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Durable-write barrier (#5): сбросить грязные страницы `/data` ДО PONR (Settings уже синхронен).
|
||||||
|
fn durable_barrier() {
|
||||||
|
let _ = std::process::Command::new("sync").status();
|
||||||
|
tracing::info!(
|
||||||
|
"power: commit (PONR) — durable barrier sync; load-shed: amp/backlight/modem (нет реальных нагрузок в v0)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Шагнуть FSM и исполнить действия (эмит сигналов, grace-таймер, durable-barrier).
|
||||||
|
async fn apply_event(
|
||||||
|
fsm: &Arc<Mutex<PowerFsm>>,
|
||||||
|
ev: Event,
|
||||||
|
ctx: &SignalContext<'_>,
|
||||||
|
) -> zbus::Result<()> {
|
||||||
|
let actions = fsm.lock().unwrap().step(ev);
|
||||||
|
for a in actions {
|
||||||
|
match a {
|
||||||
|
Action::ShutdownImminent(r) => {
|
||||||
|
PowerService::shutdown_imminent(ctx, GRACE_SECS, r.as_str()).await?
|
||||||
|
}
|
||||||
|
Action::ShutdownAborted => PowerService::shutdown_aborted(ctx).await?,
|
||||||
|
Action::AccChanged(on) => PowerService::acc_changed(ctx, on).await?,
|
||||||
|
Action::StartGrace => {
|
||||||
|
// Фоновый grace-таймер (монотоника tokio). По истечении — GraceExpired:
|
||||||
|
// commit (durable-barrier), если FSM ещё в abortable; если был re-power (abort) — no-op.
|
||||||
|
let fsm = Arc::clone(fsm);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_secs(GRACE_SECS as u64)).await;
|
||||||
|
let acts = fsm.lock().unwrap().step(Event::GraceExpired);
|
||||||
|
if acts.contains(&Action::Commit) {
|
||||||
|
durable_barrier();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Action::Commit => durable_barrier(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[interface(name = "ru.shturman.Power1")]
|
#[interface(name = "ru.shturman.Power1")]
|
||||||
@@ -68,19 +88,17 @@ impl PowerService {
|
|||||||
self.power_state().as_str().to_string()
|
self.power_state().as_str().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Внутренний; в v0-стабе — no-op (полная sleep/wake — v1/v2, B §7).
|
/// Внутренний; sleep/wake — v1/v2 (B §7). В v0.3 — no-op.
|
||||||
async fn request_sleep(&self) {}
|
async fn request_sleep(&self) {}
|
||||||
|
|
||||||
#[zbus(property)]
|
#[zbus(property)]
|
||||||
async fn ignition_state(&self) -> String {
|
async fn ignition_state(&self) -> String {
|
||||||
self.ignition().as_str().to_string()
|
self.ignition().as_str().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[zbus(property)]
|
#[zbus(property)]
|
||||||
async fn uptime(&self) -> u64 {
|
async fn uptime(&self) -> u64 {
|
||||||
monotonic_secs()
|
monotonic_secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[zbus(property)]
|
#[zbus(property)]
|
||||||
async fn power_source(&self) -> String {
|
async fn power_source(&self) -> String {
|
||||||
self.source().as_str().to_string()
|
self.source().as_str().to_string()
|
||||||
@@ -102,51 +120,46 @@ impl PowerService {
|
|||||||
async fn wake(ctx: &SignalContext<'_>) -> zbus::Result<()>;
|
async fn wake(ctx: &SignalContext<'_>) -> zbus::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// dev-mock «fake-ACC» — отдельный тип на том же пути. Прод (`--no-default-features`) его НЕ регистрирует.
|
/// dev-mock «fake-ACC/voltage/thermal» — кормит входы FSM. Прод (`--no-default-features`) не регистрирует.
|
||||||
/// Методы возвращают `()` (ошибку эмита сигнала игнорируем — мок не отвечает D-Bus-ошибкой).
|
|
||||||
#[cfg(feature = "dev-mocks")]
|
#[cfg(feature = "dev-mocks")]
|
||||||
pub struct PowerMock {
|
pub struct PowerMock {
|
||||||
state: Arc<Mutex<State>>,
|
fsm: Arc<Mutex<PowerFsm>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "dev-mocks")]
|
#[cfg(feature = "dev-mocks")]
|
||||||
#[interface(name = "ru.shturman.dev.PowerMock1")]
|
#[interface(name = "ru.shturman.dev.PowerMock1")]
|
||||||
impl PowerMock {
|
impl PowerMock {
|
||||||
async fn set_acc(&self, on: bool, #[zbus(signal_context)] ctx: SignalContext<'_>) {
|
async fn set_acc(&self, on: bool, #[zbus(signal_context)] ctx: SignalContext<'_>) {
|
||||||
{
|
let ev = if on { Event::AccOn } else { Event::AccOff };
|
||||||
let mut st = self.state.lock().unwrap();
|
let _ = apply_event(&self.fsm, ev, &ctx).await;
|
||||||
st.ignition = if on {
|
|
||||||
IgnitionState::Running
|
|
||||||
} else {
|
|
||||||
IgnitionState::Off
|
|
||||||
};
|
|
||||||
st.power = if on {
|
|
||||||
PowerState::Running
|
|
||||||
} else {
|
|
||||||
PowerState::Off
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Эмитим Power1-сигнал (тот же путь; имя интерфейса добавляет сама acc_changed).
|
|
||||||
let _ = PowerService::acc_changed(&ctx, on).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_ignition(&self, state: String) {
|
async fn set_ignition(&self, state: String, #[zbus(signal_context)] ctx: SignalContext<'_>) {
|
||||||
if let Ok(ig) = state.parse::<IgnitionState>() {
|
// accessory↔running — через EngineOn/Off; off — AccOff.
|
||||||
self.state.lock().unwrap().ignition = ig;
|
let ev = match state.as_str() {
|
||||||
}
|
"running" => Event::EngineOn,
|
||||||
|
"accessory" => Event::EngineOff,
|
||||||
|
_ => Event::AccOff,
|
||||||
|
};
|
||||||
|
let _ = apply_event(&self.fsm, ev, &ctx).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn trigger_shutdown(
|
async fn trigger_shutdown(
|
||||||
&self,
|
&self,
|
||||||
seconds: u32,
|
_seconds: u32,
|
||||||
reason: String,
|
reason: String,
|
||||||
#[zbus(signal_context)] ctx: SignalContext<'_>,
|
#[zbus(signal_context)] ctx: SignalContext<'_>,
|
||||||
) {
|
) {
|
||||||
let _ = PowerService::shutdown_imminent(&ctx, seconds, &reason).await;
|
let ev = match reason.as_str() {
|
||||||
|
"thermal" => Event::ThermalTrip,
|
||||||
|
"under_voltage" => Event::UnderVoltage,
|
||||||
|
_ => Event::AccOff,
|
||||||
|
};
|
||||||
|
let _ = apply_event(&self.fsm, ev, &ctx).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) {
|
async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) {
|
||||||
let _ = PowerService::shutdown_aborted(&ctx).await;
|
let _ = apply_event(&self.fsm, Event::AccOn, &ctx).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,3 +48,48 @@ async fn power_state_and_fake_acc() {
|
|||||||
let sig = acc.next().await.unwrap();
|
let sig = acc.next().await.unwrap();
|
||||||
assert!(!sig.args().unwrap().on());
|
assert!(!sig.args().unwrap().on());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "нужна session-шина: just test-integration"]
|
||||||
|
async fn shutdown_imminent_then_abort() {
|
||||||
|
let svc = PowerService::new();
|
||||||
|
let mock = svc.mock();
|
||||||
|
let server = zbus::Connection::session().await.unwrap();
|
||||||
|
server.object_server().at(names::power::PATH, svc).await.unwrap();
|
||||||
|
server.object_server().at(names::power::PATH, mock).await.unwrap();
|
||||||
|
server.request_name(names::power::NAME).await.unwrap();
|
||||||
|
|
||||||
|
let client = zbus::Connection::session().await.unwrap();
|
||||||
|
let power = PowerClient::new(&client).await.unwrap();
|
||||||
|
let mut imminent = power.proxy().receive_shutdown_imminent().await.unwrap();
|
||||||
|
let mut aborted = power.proxy().receive_shutdown_aborted().await.unwrap();
|
||||||
|
|
||||||
|
// ACC-off → ShutdownImminent(acc_off), состояние shutting_down
|
||||||
|
client
|
||||||
|
.call_method(
|
||||||
|
Some(names::power::NAME),
|
||||||
|
names::power::PATH,
|
||||||
|
Some(names::power::MOCK_IFACE),
|
||||||
|
"SetAcc",
|
||||||
|
&(false,),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let sig = imminent.next().await.unwrap();
|
||||||
|
assert_eq!(sig.args().unwrap().reason(), "acc_off");
|
||||||
|
assert_eq!(power.power_state().await.unwrap(), PowerState::ShuttingDown);
|
||||||
|
|
||||||
|
// re-power до grace → ShutdownAborted + running
|
||||||
|
client
|
||||||
|
.call_method(
|
||||||
|
Some(names::power::NAME),
|
||||||
|
names::power::PATH,
|
||||||
|
Some(names::power::MOCK_IFACE),
|
||||||
|
"SetAcc",
|
||||||
|
&(true,),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
aborted.next().await.unwrap();
|
||||||
|
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ lint:
|
|||||||
deny:
|
deny:
|
||||||
cargo deny check
|
cargo deny check
|
||||||
|
|
||||||
# интеграционные тесты на session-шине (нужен dbus: brew install dbus / в Lima)
|
# интеграционные тесты на session-шине (нужен dbus: brew install dbus / в Lima).
|
||||||
|
# --test-threads=1: тесты владеют одними well-known именами на общей шине → серийно (иначе кросс-talk/вис).
|
||||||
test-integration:
|
test-integration:
|
||||||
dbus-run-session -- cargo test --workspace -- --ignored
|
dbus-run-session -- cargo test --workspace -- --ignored --test-threads=1
|
||||||
|
|
||||||
# полный локальный гейт
|
# полный локальный гейт
|
||||||
ci: lint test deny
|
ci: lint test deny
|
||||||
|
|||||||
Reference in New Issue
Block a user