fix(node): clarify wireless enrollment and restore active K1 after upgrade
This commit is contained in:
@@ -130,3 +130,29 @@ test('a confirmed scan with zero candidates remains a valid empty result',async(
|
||||
})};
|
||||
assert.deepEqual((await api.enroll(transport,initial,'scan')).candidates,[]);
|
||||
});
|
||||
|
||||
test('finishing enrollment requires a verified current device session and survives no stale fence',()=>{
|
||||
const connected={...initial,connected:true,discovery_generation:3,mode_revision:2,
|
||||
device_session:{device_session_id:'session-one'},command_result:{status:'succeeded'}};
|
||||
assert.equal(api.enrollmentProof(initial,'synthetic-ble'),null);
|
||||
const proof=api.enrollmentProof(connected,'synthetic-ble');
|
||||
assert.ok(proof);
|
||||
assert.equal(api.enrollmentProofCurrent(proof,connected,'synthetic-ble'),true);
|
||||
for(const patch of [{fresh:false},{available:false},{connected:false},{runtime_id:'other-runtime'},
|
||||
{discovery_generation:4},{mode_revision:3},{selected_device_id:'other-device'},
|
||||
{device_session:{device_session_id:'other-session'}},{command_result:{status:'rejected'}}]){
|
||||
assert.equal(api.enrollmentProofCurrent(proof,{...connected,...patch},'synthetic-ble'),false);
|
||||
}
|
||||
assert.equal(api.enrollmentProofCurrent(null,connected,'synthetic-ble'),false);
|
||||
assert.equal(api.enrollmentProofCurrent(proof,connected,'other-device'),false);
|
||||
});
|
||||
|
||||
test('lost BLE candidate tells the operator to rescan without blaming Ethernet or Wi-Fi credentials',()=>{
|
||||
const notice=api.enrollmentNotice({...initial,connection_attempt:{
|
||||
schema_version:'missioncore.xgrids-k1-connection-attempt/v1',attempt_id:'test',status:'failed',
|
||||
public_error_code:'network-provision-candidate-not-fresh',
|
||||
}});
|
||||
assert.match(notice,/Найдите K1 ещё раз/);
|
||||
assert.match(notice,/Настройки Wi-Fi не были отправлены/);
|
||||
assert.doesNotMatch(notice,/пароль|Ethernet/);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
VERSION = "0.8.2"
|
||||
VERSION = "0.8.3"
|
||||
sys.path.insert(0, str(ROOT.parents[1] / "scripts/packaging"))
|
||||
from debian import package
|
||||
|
||||
|
||||
@@ -15,6 +15,14 @@ case "$1" in
|
||||
systemctl enable mission-core-node.service
|
||||
systemctl restart mission-core-node.service
|
||||
systemctl try-restart mission-core-realsense.service
|
||||
if [ -f /run/mission-core-node-k1-upgrade-active ]; then
|
||||
# A jointly upgraded plugin starts itself after its own configuration.
|
||||
# Restore it here only when that package is already configured.
|
||||
if [ "$(dpkg-query -W -f='${Status}' mission-core-xgrids-k1 2>/dev/null || true)" = "install ok installed" ]; then
|
||||
systemctl start mission-core-k1.service
|
||||
fi
|
||||
rm -f /run/mission-core-node-k1-upgrade-active
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -28,7 +28,10 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then
|
||||
esac
|
||||
# End the admitted idle worker before dpkg replaces its Python modules.
|
||||
# New UI commands now fail unavailable instead of racing the package copy.
|
||||
if [ -f /usr/lib/systemd/system/mission-core-k1.service ]; then
|
||||
if systemctl is-active --quiet mission-core-k1.service; then
|
||||
# Preserve an optional worker's prior running state across a Node-only
|
||||
# update. /run is root-owned; the plugin still owns initial activation.
|
||||
(umask 077; : > /run/mission-core-node-k1-upgrade-active)
|
||||
systemctl stop mission-core-k1.service
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Wireless enrollment sequence R5
|
||||
|
||||
The owner confirmed onboard Bluetooth discovery and requested visible device
|
||||
rows, no redundant bottom Close button, and no Connect footer before a
|
||||
successful connection check. The board is connected to its router by Ethernet;
|
||||
only K1 needs that router's Wi-Fi. LinuxWifiAssociationProbe already represents
|
||||
Ethernet as a valid not-wifi path with continuity evidence. No host Wi-Fi
|
||||
association or network switching is added.
|
||||
|
||||
## Observed R4 connection result
|
||||
|
||||
The operation completed at 09:44:26 UTC with action=connect, status=failed,
|
||||
error_code=network-provision-candidate-not-fresh, phase=network_not_applied and
|
||||
side_effect_status=none. The selected native BLE handle was no longer usable;
|
||||
this attempt did not transmit the network settings. The exact preceding event
|
||||
that invalidated that handle is not established by the available projection.
|
||||
Do not describe this as a wrong Wi-Fi password, an Ethernet failure or a K1
|
||||
station refusal. Private journal inspection without interactive sudo was
|
||||
unavailable; no authentication bypass was used.
|
||||
|
||||
## Interface sequence
|
||||
|
||||
WirelessEnrollmentWindow keeps the top close action and omits the footer when
|
||||
there are no contributed actions. The supported-model selector remains first.
|
||||
The K1 contribution renders discovered devices as canonical ResourceList /
|
||||
ResourceRow entries with explicit Select actions. Wi-Fi appears after selection.
|
||||
|
||||
Check connection explicitly says it sends the entered Wi-Fi settings to K1 and
|
||||
checks communication with the board. It uses the existing single provision
|
||||
intent and observes its owned bootstrap; it does not issue a second command.
|
||||
The separate read-only state check is reserved for an unconfirmed network
|
||||
outcome, preserving recovery without another provisioning write.
|
||||
|
||||
Connect appears only after the dialog obtains a successful current connection
|
||||
proof, including the exact runtime, discovery generation, mode revision,
|
||||
selected device and device session. Closing with this button finishes the
|
||||
dialog and refreshes inventory; it does not provision again or create another
|
||||
backend enrollment phase. A restart, outage, new scan, changed mode/device or
|
||||
changed session invalidates the displayed completion action. The ordinary
|
||||
backend session is established by the verified connection itself.
|
||||
|
||||
A candidate-not-fresh result explicitly asks for a new Bluetooth scan and says
|
||||
that Wi-Fi settings were not sent. The failed selection is cleared and cannot
|
||||
be reselected from that same invalid generation in the open dialog. No runtime
|
||||
fence or BLE safety rule was relaxed. The device protocol, optional K1 backend,
|
||||
Rerun profiles and recovery supervisors are unchanged.
|
||||
|
||||
## Standalone Node upgrade
|
||||
|
||||
Inspection found that Node preinst stopped K1, while Node postinst left it
|
||||
stopped during a Node-only update. Preinst now records a previously active K1
|
||||
worker in a root-owned /run marker; postinst restores it if its optional package
|
||||
is already configured. A jointly upgraded plugin starts itself after its own
|
||||
configuration. An inactive plugin is not started by this path.
|
||||
|
||||
The real shell scripts were executed against isolated OS paths and fake OS
|
||||
commands for active/configured, inactive/configured and active/unpacked cases.
|
||||
The focused Python suite has 21 passing tests, including Ethernet without a
|
||||
host Wi-Fi request and retention of a usable native BLE capture after scanning.
|
||||
The Core frontend suite has 791 passing tests; Core TypeScript and production
|
||||
build passed. Ruff, shell syntax and diff checks passed.
|
||||
|
||||
Node 0.8.3 is the replacement UI/host package. Installed optional K1
|
||||
0.1.2+private.1 is retained. Exact artifact and installation acceptance follows;
|
||||
a new fresh-cache UI connection, live streams and recovery remain pending.
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useState} from 'react';
|
||||
import {Button,Select,Window,WindowFooterActions} from '@nodedc/ui-react';
|
||||
import {Select,Window,WindowFooterActions} from '@nodedc/ui-react';
|
||||
import {wirelessContributions,type SensorEnrollmentProps,type SensorUiContribution} from './extensions';
|
||||
|
||||
export function WirelessEnrollmentWindow({contributions,transport,onClose,onChange}:{
|
||||
@@ -11,7 +11,7 @@ export function WirelessEnrollmentWindow({contributions,transport,onClose,onChan
|
||||
const Enrollment=supported.find(value=>value.kind===selected)?.wirelessEnrollment?.View;
|
||||
const renderWindow:SensorEnrollmentProps['renderWindow']=({content,actions,busy=false})=>(
|
||||
<Window open title="Подключение беспроводных устройств к БК" onClose={onClose}
|
||||
footer={<WindowFooterActions><Button onClick={onClose}>Закрыть</Button>{actions}</WindowFooterActions>}>
|
||||
footer={actions?<WindowFooterActions>{actions}</WindowFooterActions>:undefined}>
|
||||
<div className="sensor-content">
|
||||
<Select label="Выбор поддерживаемого устройства" value={selected} disabled={busy}
|
||||
options={[{value:'',label:'Выберите поддерживаемое устройство',disabled:true},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {useEffect,useRef,useState} from 'react';
|
||||
import {ActivityIndicator,Button,ResourceRow,Select,SettingsCard,StatusBadge,TextField,ToastStack} from '@nodedc/ui-react';
|
||||
import {ActivityIndicator,Button,ResourceList,ResourceRow,Select,SettingsCard,StatusBadge,TextField,ToastStack} from '@nodedc/ui-react';
|
||||
import type {SensorEnrollmentProps} from '@mission-core/sensor-sdk';
|
||||
import {bridgeFormValid,enroll,enrollmentAllowed,connectionAttempt,enrollmentNotice,mergeEnrollmentState,type EnrollmentState} from './enrollment';
|
||||
import {bridgeFormValid,enroll,enrollmentAllowed,connectionAttempt,enrollmentNotice,mergeEnrollmentState,enrollmentProof,enrollmentProofCurrent,type EnrollmentProof,type EnrollmentState} from './enrollment';
|
||||
|
||||
export function DeviceEnrollmentWindow({transport,onChange,renderWindow}:SensorEnrollmentProps){
|
||||
export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow}:SensorEnrollmentProps){
|
||||
const [state,setState]=useState<EnrollmentState|null>(null);
|
||||
const [device,setDevice]=useState('');
|
||||
const [ssid,setSSID]=useState('');
|
||||
@@ -13,6 +13,8 @@ export function DeviceEnrollmentWindow({transport,onChange,renderWindow}:SensorE
|
||||
const [busy,setBusy]=useState('loading');
|
||||
const [error,setError]=useState('');
|
||||
const [scanned,setScanned]=useState(false);
|
||||
const [verified,setVerified]=useState<EnrollmentProof|null>(null);
|
||||
const [needsScan,setNeedsScan]=useState(false);
|
||||
const lifetime=useRef<AbortController|null>(null);
|
||||
const running=useRef(false);
|
||||
|
||||
@@ -42,17 +44,21 @@ export function DeviceEnrollmentWindow({transport,onChange,renderWindow}:SensorE
|
||||
const notice=state?enrollmentNotice(state):'';
|
||||
useEffect(()=>{
|
||||
setDevice('');setPassword('');setSSID('');setNetwork('manual');setNetworks([]);
|
||||
setVerified(null);
|
||||
setNeedsScan(false);
|
||||
},[state?.runtime_id,state?.discovery_generation,state?.mode_revision]);
|
||||
useEffect(()=>{setScanned(false);},[state?.runtime_id,state?.mode_revision]);
|
||||
const ready=state?.available&&state.fresh!==false&&!!state.runtime_id;
|
||||
const selected=state?.candidates?.some(value=>value.id===device)??false;
|
||||
const pending=!!busy||waiting;
|
||||
const confirmed=enrollmentProofCurrent(verified,state,device);
|
||||
|
||||
async function run(action:'scan'|'networks'|'connect'|'verify'){
|
||||
if(!state||busy||running.current)return;
|
||||
running.current=true;
|
||||
const signal=lifetime.current?.signal;
|
||||
setBusy(action);setError('');
|
||||
if(action!=='networks')setVerified(null);
|
||||
if(action==='scan')setScanned(false);
|
||||
const parameters=action==='connect'||action==='verify'?{
|
||||
device_id:device,discovery_generation:state.discovery_generation,mode_revision:state.mode_revision,
|
||||
@@ -68,6 +74,10 @@ export function DeviceEnrollmentWindow({transport,onChange,renderWindow}:SensorE
|
||||
setState(current=>mergeEnrollmentState(current,{...result,node_id:state.node_id,name:state.name}));
|
||||
if(action==='scan'){setDevice('');setScanned(true);}
|
||||
if(action==='networks')setNetworks(result.networks??[]);
|
||||
if(action==='connect'||action==='verify')setVerified(enrollmentProof(result,device));
|
||||
if(action==='connect'&&connectionAttempt(result)?.public_error_code==='network-provision-candidate-not-fresh'){
|
||||
setNeedsScan(true);setDevice('');
|
||||
}
|
||||
if(result.command_result?.status==='rejected')setError(enrollmentNotice(result));
|
||||
}catch(cause){
|
||||
if(!signal?.aborted)setError(cause instanceof Error?cause.message:'Не удалось выполнить действие K1.');
|
||||
@@ -78,12 +88,11 @@ export function DeviceEnrollmentWindow({transport,onChange,renderWindow}:SensorE
|
||||
}
|
||||
|
||||
return renderWindow({busy:pending,
|
||||
actions:selected?<Button variant="primary" disabled={!ready||pending||!enrollmentAllowed(state,'connect')||!bridgeFormValid(ssid,password)}
|
||||
aria-busy={busy==='connect'} icon={busy==='connect'?<ActivityIndicator size="compact"/>:undefined}
|
||||
onClick={()=>void run('connect')}>{busy==='connect'?'Подключаем K1':'Подключить'}</Button>:undefined,
|
||||
actions:confirmed?<Button variant="primary" disabled={pending}
|
||||
onClick={()=>{if(enrollmentProofCurrent(verified,state,device)){onChange();onClose();}}}>Подключить</Button>:undefined,
|
||||
content:<>
|
||||
<SettingsCard title={state?.name||'Бортовой компьютер'}
|
||||
description="K1 подключится к выбранной сети Wi-Fi рядом с этим БК. Сеть компьютера оператора может отличаться.">
|
||||
description="K1 подключится к выбранной сети Wi-Fi рядом с БК. БК может работать через Ethernet или Wi-Fi; сеть компьютера оператора может отличаться.">
|
||||
<ResourceRow title="Общая сеть · Bridge" status={busy==='loading'?<ActivityIndicator label="Получаем состояние БК"/>:
|
||||
<StatusBadge tone={ready?'success':'neutral'}>{ready?'БК доступен':'Подключение недоступно'}</StatusBadge>}/>
|
||||
</SettingsCard>
|
||||
@@ -95,10 +104,12 @@ export function DeviceEnrollmentWindow({transport,onChange,renderWindow}:SensorE
|
||||
onClick={()=>void run('scan')}>{busy==='scan'?'Ищем K1':'Найти K1'}</Button>}/>
|
||||
{scanned&&!state?.candidates?.length&&<SettingsCard title="K1 не найден"
|
||||
description="Проверьте питание K1 и Bluetooth на бортовом компьютере, затем повторите поиск."/>}
|
||||
{!!state?.candidates?.length&&<Select label="Устройство K1" value={device}
|
||||
options={[{value:'',label:'Выберите K1',disabled:true},...state.candidates.map(value=>({value:value.id,label:value.name}))]}
|
||||
onChange={value=>{setDevice(value);setPassword('');}} disabled={pending}/>}
|
||||
{selected&&<>
|
||||
{!!state?.candidates?.length&&<ResourceList aria-label="Найденные устройства K1">
|
||||
{state.candidates.map(value=><li key={value.id}><ResourceRow title={value.name}
|
||||
actions={<Button disabled={pending||needsScan||device===value.id}
|
||||
onClick={()=>{setDevice(value.id);setPassword('');setVerified(null);}}>{device===value.id?'Выбрано':'Выбрать'}</Button>}/></li>)}
|
||||
</ResourceList>}
|
||||
{selected&&!confirmed&&<>
|
||||
<ResourceRow title="Wi-Fi рядом с БК" description="Выберите сеть, доступную бортовому компьютеру." actions={
|
||||
<Button disabled={pending} aria-busy={busy==='networks'} icon={busy==='networks'?<ActivityIndicator size="compact"/>:undefined}
|
||||
onClick={()=>void run('networks')}>{busy==='networks'?'Ищем сети':'Найти сети'}</Button>}/>
|
||||
@@ -107,9 +118,14 @@ export function DeviceEnrollmentWindow({transport,onChange,renderWindow}:SensorE
|
||||
onChange={value=>{setNetwork(value);if(value!=='manual')setSSID(networks[Number(value)].ssid);setPassword('');}} disabled={pending}/>}
|
||||
<TextField label="Название сети Wi-Fi" value={ssid} onChange={event=>{setSSID(event.target.value);setNetwork('manual');}} disabled={pending} autoComplete="off"/>
|
||||
<TextField label="Пароль Wi-Fi" type="password" value={password} onChange={event=>setPassword(event.target.value)} disabled={pending} autoComplete="new-password"/>
|
||||
<Button disabled={pending||!enrollmentAllowed(state,'verify')} aria-busy={busy==='verify'}
|
||||
icon={busy==='verify'?<ActivityIndicator size="compact"/>:undefined}
|
||||
onClick={()=>void run('verify')}>{busy==='verify'?'Проверяем подключение':'Проверить текущее подключение'}</Button>
|
||||
<ResourceRow title="Проверка подключения" description="Передадим настройки Wi-Fi сканеру K1 и проверим связь с БК." actions={
|
||||
<Button disabled={pending||!enrollmentAllowed(state,'connect')||!bridgeFormValid(ssid,password)} aria-busy={busy==='connect'}
|
||||
icon={busy==='connect'?<ActivityIndicator size="compact"/>:undefined}
|
||||
onClick={()=>void run('connect')}>{busy==='connect'?'Проверяем подключение':'Проверить подключение'}</Button>}/>
|
||||
{attempt&&attempt.phase==='network_outcome_unknown'&&enrollmentAllowed(state,'verify')&&
|
||||
<Button disabled={pending} aria-busy={busy==='verify'}
|
||||
icon={busy==='verify'?<ActivityIndicator size="compact"/>:undefined}
|
||||
onClick={()=>void run('verify')}>{busy==='verify'?'Проверяем состояние':'Проверить состояние K1'}</Button>}
|
||||
</>}
|
||||
</>}
|
||||
{notice&&<SettingsCard title={notice}/>}
|
||||
|
||||
@@ -115,6 +115,7 @@ export function enrollmentNotice(state:EnrollmentState):string {
|
||||
return attempt.phase==='network_applied'?'K1 подключился к Wi-Fi. Проверяем канал управления.':'Подключаем K1 к выбранной сети Wi-Fi.';
|
||||
}
|
||||
const code=attempt?.public_error_code??state.command_result?.error_code;
|
||||
if(code==='network-provision-candidate-not-fresh')return 'Результат Bluetooth-поиска больше недоступен. Найдите K1 ещё раз и выберите его из списка. Настройки Wi-Fi не были отправлены.';
|
||||
if(state.command_result?.status==='rejected')return 'Выбранное устройство или сеанс изменились. Обновите сведения и выберите K1 заново.';
|
||||
if(code&&STATION_WIFI_FAILURE_MESSAGES[code])return STATION_WIFI_FAILURE_MESSAGES[code];
|
||||
if(state.connected)return 'K1 подключён к БК.';
|
||||
@@ -124,6 +125,25 @@ export function enrollmentNotice(state:EnrollmentState):string {
|
||||
return '';
|
||||
}
|
||||
|
||||
export interface EnrollmentProof {
|
||||
runtime:string; generation:number|undefined; modeRevision:number|undefined;
|
||||
device:string; session:string;
|
||||
}
|
||||
|
||||
export function enrollmentProof(state:EnrollmentState,device:string):EnrollmentProof|null {
|
||||
if(!state.available||state.fresh===false||!state.connected||!state.runtime_id||
|
||||
state.selected_device_id!==device||!state.device_session?.device_session_id||
|
||||
['failed','rejected'].includes(state.command_result?.status??''))return null;
|
||||
return {runtime:state.runtime_id,generation:state.discovery_generation,modeRevision:state.mode_revision,
|
||||
device,session:state.device_session.device_session_id};
|
||||
}
|
||||
|
||||
export function enrollmentProofCurrent(proof:EnrollmentProof|null,state:EnrollmentState|null,device:string):boolean {
|
||||
if(!proof||!state)return false;
|
||||
const current=enrollmentProof(state,device);
|
||||
return !!current&&Object.entries(proof).every(([key,value])=>current[key as keyof EnrollmentProof]===value);
|
||||
}
|
||||
|
||||
export function bridgeFormValid(ssid:string,password:string):boolean{
|
||||
const bytes=new TextEncoder();return bytes.encode(ssid).length>0&&bytes.encode(ssid).length<=32&&bytes.encode(password).length>0&&bytes.encode(password).length<=64;
|
||||
}
|
||||
|
||||
@@ -165,6 +165,8 @@ def test_node_scan_real_service_preserves_operation_identity_and_replay(
|
||||
"action": "scan", "parameters": {},
|
||||
}
|
||||
result = await device.deliver(command)
|
||||
if not radio_failure:
|
||||
assert scanner.capture_discovered_device("AA:BB:CC:DD:EE:FF") is not None
|
||||
repeated = await device.deliver(command)
|
||||
assert len(calls) == 1
|
||||
assert result["discovery_generation"] == 1
|
||||
@@ -307,6 +309,21 @@ def test_networkmanager_ssids_are_not_split_at_escaped_colons():
|
||||
assert nm_fields(r"field\:network\\name:88:WPA2") == ["field:network\\name", "88", "WPA2"]
|
||||
|
||||
|
||||
def test_wired_board_bridge_does_not_require_host_wifi_association(tmp_path, monkeypatch):
|
||||
from k1link.device_plugins.xgrids_k1 import linux_host
|
||||
|
||||
(tmp_path / "enp1s0").mkdir()
|
||||
|
||||
def forbidden(*_args, **_kwargs):
|
||||
raise AssertionError("Ethernet must not request Wi-Fi association")
|
||||
|
||||
monkeypatch.setattr(linux_host, "_run", forbidden)
|
||||
value = linux_host.LinuxWifiAssociationProbe(tmp_path).observe(interface_name="enp1s0")
|
||||
assert value["association_state"] == "not-wifi"
|
||||
assert value["continuity_proven"] is True
|
||||
assert value["reason_code"] is None
|
||||
|
||||
|
||||
def test_linux_kernel_route_is_matched_route_not_resolved_host(monkeypatch):
|
||||
from k1link.device_plugins.xgrids_k1 import linux_host
|
||||
from k1link.device_plugins.xgrids_k1.facade import _classify_host_route
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Execute the real maintainer scripts with isolated OS paths and commands."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PACKAGING = Path(__file__).resolve().parents[1] / "apps/node-agent/packaging"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("active,configured", [(True, True), (False, True), (True, False)])
|
||||
def test_node_update_restores_only_previously_running_configured_plugin(
|
||||
tmp_path, active, configured,
|
||||
):
|
||||
binary = tmp_path / "bin"
|
||||
binary.mkdir()
|
||||
run = tmp_path / "run"
|
||||
(run / "systemd/system").mkdir(parents=True)
|
||||
release = tmp_path / "os-release"
|
||||
release.write_text("ID=ubuntu\nVERSION_ID=24.04\n")
|
||||
state = tmp_path / "active"
|
||||
if active:
|
||||
state.touch()
|
||||
events = tmp_path / "events"
|
||||
systemctl = binary / "systemctl"
|
||||
systemctl.write_text('''#!/bin/sh
|
||||
printf '%s\\n' "$*" >> "$TEST_EVENTS"
|
||||
case "$1" in
|
||||
is-active) [ -f "$TEST_ACTIVE" ]; exit $? ;;
|
||||
show) echo inactive ;;
|
||||
stop) rm -f "$TEST_ACTIVE" ;;
|
||||
start) touch "$TEST_ACTIVE" ;;
|
||||
esac
|
||||
''')
|
||||
(binary / "getent").write_text("#!/bin/sh\nexit 0\n")
|
||||
(binary / "dpkg-query").write_text(
|
||||
"#!/bin/sh\necho 'install ok " + ("installed" if configured else "unpacked") + "'\n"
|
||||
)
|
||||
for file in binary.iterdir():
|
||||
file.chmod(0o700)
|
||||
env = dict(os.environ, PATH=str(binary) + os.pathsep + os.environ["PATH"],
|
||||
TEST_EVENTS=str(events), TEST_ACTIVE=str(state))
|
||||
for name, args in [("preinst", ["upgrade", "0.8.2"]), ("postinst", ["configure", "0.8.2"])]:
|
||||
script = tmp_path / name
|
||||
script.write_text((PACKAGING / name).read_text()
|
||||
.replace("/run/", str(run) + "/")
|
||||
.replace("/etc/os-release", str(release)))
|
||||
subprocess.run(["/bin/sh", str(script), *args], env=env, check=True,
|
||||
capture_output=True, text=True)
|
||||
if name == "preinst":
|
||||
assert not state.exists()
|
||||
calls = events.read_text().splitlines()
|
||||
assert ("start mission-core-k1.service" in calls) is (active and configured)
|
||||
assert state.exists() is (active and configured)
|
||||
assert not (run / "mission-core-node-k1-upgrade-active").exists()
|
||||
Reference in New Issue
Block a user