| 1207 |
1207 |
|
}
|
| 1208 |
1208 |
|
}
|
| 1209 |
1209 |
|
|
|
1210 |
+ |
/// Where the image names its own components, one per line.
|
|
1211 |
+ |
///
|
|
1212 |
+ |
/// Written at build time rather than hardcoded here, because the profile
|
|
1213 |
+ |
/// decides the list: `server` has no compositor and never lays shop down
|
|
1214 |
+ |
/// (Containerfile, the components block). Reading it means a profile that ships
|
|
1215 |
+ |
/// a different set does not make this screen lie about whose package is whose.
|
|
1216 |
+ |
const COMPONENTS_PATH: &str = "/usr/share/alloy/components";
|
|
1217 |
+ |
|
|
1218 |
+ |
/// Alloy's own components, as this image names them.
|
|
1219 |
+ |
///
|
|
1220 |
+ |
/// `None` when the file is absent, which is not a failure and is not rare: a
|
|
1221 |
+ |
/// development box, a Fedora ostree machine that is not Alloy, or an image
|
|
1222 |
+ |
/// built before the component flip (GO alloy `d866e125`). The caller says less
|
|
1223 |
+ |
/// in that case rather than falling back to a guess, because the guess here is
|
|
1224 |
+ |
/// labelling somebody else's package as ours.
|
|
1225 |
+ |
fn components() -> Option<Vec<String>> {
|
|
1226 |
+ |
let raw = std::fs::read_to_string(COMPONENTS_PATH).ok()?;
|
|
1227 |
+ |
let names: Vec<String> = raw.split_whitespace().map(str::to_string).collect();
|
|
1228 |
+ |
(!names.is_empty()).then_some(names)
|
|
1229 |
+ |
}
|
|
1230 |
+ |
|
|
1231 |
+ |
/// Whether a layered entry is the component `name`.
|
|
1232 |
+ |
///
|
|
1233 |
+ |
/// Two forms, because a request is recorded under the string that installed it.
|
|
1234 |
+ |
/// `alloy-layer-components.service` installs by bare name, so that is what an
|
|
1235 |
+ |
/// ordinary Alloy machine reports, and NEVRA is what a machine somebody layered
|
|
1236 |
+ |
/// by hand reports. The version has to start with a digit for the second form
|
|
1237 |
+ |
/// to match, so `alloy-utils` is not read as `alloy` wearing a version.
|
|
1238 |
+ |
fn is_component(entry: &str, name: &str) -> bool {
|
|
1239 |
+ |
entry == name
|
|
1240 |
+ |
|| entry
|
|
1241 |
+ |
.strip_prefix(name)
|
|
1242 |
+ |
.and_then(|rest| rest.strip_prefix('-'))
|
|
1243 |
+ |
.is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit()))
|
|
1244 |
+ |
}
|
|
1245 |
+ |
|
|
1246 |
+ |
/// What the booted deployment's layering means, as words.
|
|
1247 |
+ |
///
|
|
1248 |
+ |
/// Empty when nothing is layered, which is the answer on every non-Alloy ostree
|
|
1249 |
+ |
/// machine and was the answer here too until the component flip. Since
|
|
1250 |
+ |
/// `cad26d5` every Alloy machine carries `alloy` and `shop` as layers for as
|
|
1251 |
+ |
/// long as it exists, and that has a consequence the user meets at the worst
|
|
1252 |
+ |
/// possible moment otherwise: `bootc upgrade`, the verb bootc's own
|
|
1253 |
+ |
/// documentation names, refuses outright on a machine with local modifications
|
|
1254 |
+ |
/// and does nothing. Saying so on the screen that owns updates is cheaper than
|
|
1255 |
+ |
/// letting them read the error.
|
|
1256 |
+ |
///
|
|
1257 |
+ |
/// `ours` is `None` when the machine does not name its components; then the
|
|
1258 |
+ |
/// list is printed unattributed rather than sorted into ours and theirs.
|
|
1259 |
+ |
///
|
|
1260 |
+ |
/// Words here and paint in the caller, the same split
|
|
1261 |
+ |
/// [`Staleness::report`](crate::stale::Staleness::report) makes, and for the
|
|
1262 |
+ |
/// same reason: this copy makes claims about what two commands do, and a test
|
|
1263 |
+ |
/// can read a `String`.
|
|
1264 |
+ |
fn layer_note(layered: &[String], ours: Option<&[String]>) -> Vec<String> {
|
|
1265 |
+ |
if layered.is_empty() {
|
|
1266 |
+ |
return Vec::new();
|
|
1267 |
+ |
}
|
|
1268 |
+ |
|
|
1269 |
+ |
let mut lines = Vec::new();
|
|
1270 |
+ |
|
|
1271 |
+ |
match ours {
|
|
1272 |
+ |
Some(components) => {
|
|
1273 |
+ |
let (mine, theirs): (Vec<&String>, Vec<&String>) = layered
|
|
1274 |
+ |
.iter()
|
|
1275 |
+ |
.partition(|entry| components.iter().any(|name| is_component(entry, name)));
|
|
1276 |
+ |
let named = |set: &[&String]| {
|
|
1277 |
+ |
set.iter()
|
|
1278 |
+ |
.map(|entry| entry.as_str())
|
|
1279 |
+ |
.collect::<Vec<_>>()
|
|
1280 |
+ |
.join(", ")
|
|
1281 |
+ |
};
|
|
1282 |
+ |
lines.push(match (mine.is_empty(), theirs.is_empty()) {
|
|
1283 |
+ |
(false, true) => format!("layered: {} (Alloy's own)", named(&mine)),
|
|
1284 |
+ |
(false, false) => format!(
|
|
1285 |
+ |
"layered: {} (Alloy's own), {} (yours)",
|
|
1286 |
+ |
named(&mine),
|
|
1287 |
+ |
named(&theirs)
|
|
1288 |
+ |
),
|
|
1289 |
+ |
_ => format!("layered: {}", named(&theirs)),
|
|
1290 |
+ |
});
|
|
1291 |
+ |
}
|
|
1292 |
+ |
None => {
|
|
1293 |
+ |
lines.push(format!("layered: {}", layered.join(", ")));
|
|
1294 |
+ |
}
|
|
1295 |
+ |
}
|
|
1296 |
+ |
|
|
1297 |
+ |
// The caution goes with any layer, not only ours: bootc reads local
|
|
1298 |
+ |
// modifications and does not care who asked for them.
|
|
1299 |
+ |
lines.push(
|
|
1300 |
+ |
"bootc upgrade refuses while anything is layered. bootc switch does not, and is the path \
|
|
1301 |
+ |
Alloy takes."
|
|
1302 |
+ |
.to_string(),
|
|
1303 |
+ |
);
|
|
1304 |
+ |
|
|
1305 |
+ |
// Only when some of it is ours, because the self-heal is ours. A switch
|
|
1306 |
+ |
// discards the layer outright, `alloy-layer-components.service` sees no
|
|
1307 |
+ |
// /usr/bin/alloy and lays the new image's copies down, and that second
|
|
1308 |
+ |
// reboot is the mechanism by which a rebuilt console reaches anyone.
|
|
1309 |
+ |
// Measured 2026-08-15, build/layertest/README.md.
|
|
1310 |
+ |
if ours.is_some_and(|components| {
|
|
1311 |
+ |
layered
|
|
1312 |
+ |
.iter()
|
|
1313 |
+ |
.any(|entry| components.iter().any(|name| is_component(entry, name)))
|
|
1314 |
+ |
}) {
|
|
1315 |
+ |
lines.push(
|
|
1316 |
+ |
"a switch discards these and the next boot lays the new image's copies down, so the \
|
|
1317 |
+ |
machine boots twice."
|
|
1318 |
+ |
.to_string(),
|
|
1319 |
+ |
);
|
|
1320 |
+ |
}
|
|
1321 |
+ |
|
|
1322 |
+ |
lines
|
|
1323 |
+ |
}
|
|
1324 |
+ |
|
| 1210 |
1325 |
|
/// Reads `rpm-ostree status --json`.
|
| 1211 |
1326 |
|
///
|
| 1212 |
1327 |
|
/// Deliberately not a [`Backend`]: those answer for containers and yield [`Box`]
|
| 1327 |
1442 |
|
/// Whether dnf is on this machine, probed once. Guards the staleness check
|
| 1328 |
1443 |
|
/// rather than the tab: a machine without dnf can still show deployments.
|
| 1329 |
1444 |
|
has_dnf: bool,
|
|
1445 |
+ |
/// Alloy's own components, read once at construction. `None` off an Alloy
|
|
1446 |
+ |
/// image. A file read, and the file is part of `/usr` on a read-only ostree
|
|
1447 |
+ |
/// tree, so it cannot change while this view is up.
|
|
1448 |
+ |
components: Option<Vec<String>>,
|
| 1330 |
1449 |
|
/// The last staleness check, or the error from trying. `None` until the user
|
| 1331 |
1450 |
|
/// presses `u`, and it stays `None` otherwise — that is the pull-not-push
|
| 1332 |
1451 |
|
/// constraint, and [`refresh`] deliberately does not touch it.
|
| 1363 |
1482 |
|
has_rpm: Rpm::present(),
|
| 1364 |
1483 |
|
rpm: None,
|
| 1365 |
1484 |
|
has_dnf: stale::available(),
|
|
1485 |
+ |
components: components(),
|
| 1366 |
1486 |
|
stale: None,
|
| 1367 |
1487 |
|
last_checked: stale::last_checked(),
|
| 1368 |
1488 |
|
};
|
| 1765 |
1885 |
|
// The deployments take exactly the rows they need and the staleness
|
| 1766 |
1886 |
|
// block takes the rest. The other way round — deployments on `Min` —
|
| 1767 |
1887 |
|
// would grow a two-row list into half the screen and push the thing the
|
| 1768 |
|
- |
// user pressed a key for off the bottom.
|
|
1888 |
+ |
// user pressed a key for off the bottom. The layer note sizes to itself
|
|
1889 |
+ |
// for the same reason, and is zero rows on a machine with no layer.
|
| 1769 |
1890 |
|
let deployment_rows = u16::try_from(status.deployments.len().max(1)).unwrap_or(u16::MAX);
|
| 1770 |
|
- |
let [list_area, summary_area, _gap, stale_area] = Layout::vertical([
|
|
1891 |
+ |
let note = status
|
|
1892 |
+ |
.booted()
|
|
1893 |
+ |
.map(|dep| layer_note(dep.layered(), self.components.as_deref()))
|
|
1894 |
+ |
.unwrap_or_default();
|
|
1895 |
+ |
let note_rows = u16::try_from(note.len()).unwrap_or(u16::MAX);
|
|
1896 |
+ |
// The note sits with the summary rather than with the staleness block:
|
|
1897 |
+ |
// both answer for the machine as it is now, and staleness answers for
|
|
1898 |
+ |
// what a rebuild would change.
|
|
1899 |
+ |
let [list_area, summary_area, layer_area, _gap, stale_area] = Layout::vertical([
|
| 1771 |
1900 |
|
Constraint::Length(deployment_rows),
|
| 1772 |
1901 |
|
Constraint::Length(1),
|
|
1902 |
+ |
Constraint::Length(note_rows),
|
| 1773 |
1903 |
|
Constraint::Length(1),
|
| 1774 |
1904 |
|
Constraint::Min(0),
|
| 1775 |
1905 |
|
])
|
| 1801 |
1931 |
|
frame.render_widget(Line::from(text::muted(theme, summary)), summary_area);
|
| 1802 |
1932 |
|
}
|
| 1803 |
1933 |
|
|
|
1934 |
+ |
if !note.is_empty() {
|
|
1935 |
+ |
let lines: Vec<Line> = note
|
|
1936 |
+ |
.iter()
|
|
1937 |
+ |
.map(|line| Line::from(text::muted(theme, line.clone())))
|
|
1938 |
+ |
.collect();
|
|
1939 |
+ |
frame.render_widget(AlloyList::new(theme, lines), layer_area);
|
|
1940 |
+ |
}
|
|
1941 |
+ |
|
| 1804 |
1942 |
|
self.render_staleness(frame, stale_area, theme);
|
| 1805 |
1943 |
|
}
|
| 1806 |
1944 |
|
|
| 2210 |
2348 |
|
// `rpm-ostree status --json` on the installed system. The `base-commit-meta`
|
| 2211 |
2349 |
|
// manifest blob (a 38 KB embedded OCI manifest) and the null `transaction` /
|
| 2212 |
2350 |
|
// `cached-update` / `update-driver` keys are elided; the parser reads none of
|
| 2213 |
|
- |
// them. This is the whole of a stock Alloy deployment: one booted
|
| 2214 |
|
- |
// container-image deployment with nothing layered, which is the state the
|
| 2215 |
|
- |
// installed tab exists to show.
|
|
2351 |
+ |
// them. One booted container-image deployment with nothing layered.
|
|
2352 |
+ |
//
|
|
2353 |
+ |
// It predates the component flip (`cad26d5`), and is kept rather than
|
|
2354 |
+ |
// recaptured: an Alloy machine now carries `alloy` and `shop` as layers for
|
|
2355 |
+ |
// as long as it exists, so this is no longer what one looks like. What it is
|
|
2356 |
+ |
// is the unlayered case, which the parser still has to read and which every
|
|
2357 |
+ |
// non-Alloy ostree machine is in. `RPM_OSTREE_ALLOY` below is the current
|
|
2358 |
+ |
// shape.
|
| 2216 |
2359 |
|
const RPM_OSTREE: &str = r#"{
|
| 2217 |
2360 |
|
"deployments": [
|
| 2218 |
2361 |
|
{
|
| 2230 |
2373 |
|
]
|
| 2231 |
2374 |
|
}"#;
|
| 2232 |
2375 |
|
|
| 2233 |
|
- |
// Not a real Alloy capture: a stock image layers nothing, so this state does
|
| 2234 |
|
- |
// not occur on Alloy. Hand-authored to rpm-ostree's documented schema (the
|
| 2235 |
|
- |
// field shapes match the real capture above) to exercise the cases the
|
|
2376 |
+ |
// Not a real Alloy capture. Hand-authored to rpm-ostree's documented schema
|
|
2377 |
+ |
// (the field shapes match the real capture above) to exercise the cases the
|
| 2236 |
2378 |
|
// pristine one cannot — a booted deployment with layered packages, a staged
|
| 2237 |
2379 |
|
// change pending underneath it, and a classic ostree-origin rollback target
|
| 2238 |
|
- |
// with no container reference. It is what a machine someone had run
|
| 2239 |
|
- |
// `rpm-ostree install` on would report, which is exactly the exception the
|
| 2240 |
|
- |
// installed tab is for.
|
|
2380 |
+ |
// with no container reference. Everything layered here is a user's own, and
|
|
2381 |
+ |
// none of it is a component, which is the half of the layer note that says
|
|
2382 |
+ |
// "yours".
|
| 2241 |
2383 |
|
const RPM_OSTREE_LAYERED: &str = r#"{
|
| 2242 |
2384 |
|
"deployments": [
|
| 2243 |
2385 |
|
{
|
| 2268 |
2410 |
|
]
|
| 2269 |
2411 |
|
}"#;
|
| 2270 |
2412 |
|
|
|
2413 |
+ |
// What an Alloy machine has reported since the component flip: the two
|
|
2414 |
+ |
// components layered by bare name, which is the string
|
|
2415 |
+ |
// `alloy-layer-components.service` installs them under, over a base image
|
|
2416 |
+ |
// that carries neither binary. Hand-authored on the shape of the capture
|
|
2417 |
+ |
// above rather than captured, because the machine it would be captured from
|
|
2418 |
+ |
// has no console until it has already layered.
|
|
2419 |
+ |
const RPM_OSTREE_ALLOY: &str = r#"{
|
|
2420 |
+ |
"deployments": [
|
|
2421 |
+ |
{
|
|
2422 |
+ |
"booted": true,
|
|
2423 |
+ |
"staged": false,
|
|
2424 |
+ |
"pinned": false,
|
|
2425 |
+ |
"checksum": "4444444444444444444444444444444444444444444444444444444444444444",
|
|
2426 |
+ |
"container-image-reference": "ostree-unverified-registry:localhost/alloy:local",
|
|
2427 |
+ |
"version": "43.20260815.0",
|
|
2428 |
+ |
"requested-packages": ["alloy", "shop"],
|
|
2429 |
+ |
"packages": ["alloy", "shop"]
|
|
2430 |
+ |
}
|
|
2431 |
+ |
]
|
|
2432 |
+ |
}"#;
|
|
2433 |
+ |
|
| 2271 |
2434 |
|
/// A spec declaring each name at a level, with no source fields.
|
| 2272 |
2435 |
|
///
|
| 2273 |
2436 |
|
/// Enough for the parsers, which only ask the spec for a level and a
|
| 3084 |
3247 |
|
// `false` also pins the unchecked-state copy, since a fixture that
|
| 3085 |
3248 |
|
// claimed dnf would offer a key these tests cannot press.
|
| 3086 |
3249 |
|
has_dnf: false,
|
|
3250 |
+ |
// Not read from the host either. The layer note is a pure function
|
|
3251 |
+ |
// over this and the booted deployment, and is tested as one.
|
|
3252 |
+ |
components: None,
|
| 3087 |
3253 |
|
stale: None,
|
| 3088 |
3254 |
|
// Not read from disk: a fixture that picked up this machine's real
|
| 3089 |
3255 |
|
// state file would render a different line depending on when the
|
| 3771 |
3937 |
|
);
|
| 3772 |
3938 |
|
}
|
| 3773 |
3939 |
|
|
| 3774 |
|
- |
// The whole point of the installed tab: a stock image layers nothing, and
|
| 3775 |
|
- |
// that empty answer is correct rather than a failure to read.
|
|
3940 |
+ |
// An unlayered machine reads as unlayered rather than as a failed read. Not
|
|
3941 |
+ |
// an Alloy machine any more (see the fixture), and still the state every
|
|
3942 |
+ |
// other ostree machine is in.
|
| 3776 |
3943 |
|
#[test]
|
| 3777 |
3944 |
|
fn a_stock_image_has_no_layered_packages() {
|
| 3778 |
3945 |
|
let status = Rpm::parse(RPM_OSTREE).unwrap();
|
| 3837 |
4004 |
|
assert!(status.booted().is_none());
|
| 3838 |
4005 |
|
}
|
| 3839 |
4006 |
|
|
|
4007 |
+ |
// ---- the layer note ----
|
|
4008 |
+ |
|
|
4009 |
+ |
// The components file is what the image says, and this is the ordinary
|
|
4010 |
+ |
// Alloy machine: both of ours layered and nothing else.
|
|
4011 |
+ |
#[test]
|
|
4012 |
+ |
fn an_alloy_machine_is_told_which_layers_are_alloys() {
|
|
4013 |
+ |
let ours = ["alloy".to_string(), "shop".to_string()];
|
|
4014 |
+ |
let note = layer_note(&ours, Some(&ours));
|
|
4015 |
+ |
assert_eq!(note[0], "layered: alloy, shop (Alloy's own)");
|
|
4016 |
+ |
assert_eq!(note.len(), 3, "composition, the caution, the double boot");
|
|
4017 |
+ |
}
|
|
4018 |
+ |
|
|
4019 |
+ |
// A user's own layers are named as theirs. Sorting them into the wrong half
|
|
4020 |
+ |
// would tell somebody a package they installed came with the machine.
|
|
4021 |
+ |
#[test]
|
|
4022 |
+ |
fn a_users_own_layers_are_kept_apart_from_ours() {
|
|
4023 |
+ |
let layered = [
|
|
4024 |
+ |
"alloy".to_string(),
|
|
4025 |
+ |
"vim-enhanced".to_string(),
|
|
4026 |
+ |
"shop".to_string(),
|
|
4027 |
+ |
];
|
|
4028 |
+ |
let ours = ["alloy".to_string(), "shop".to_string()];
|
|
4029 |
+ |
let note = layer_note(&layered, Some(&ours));
|
|
4030 |
+ |
assert_eq!(
|
|
4031 |
+ |
note[0],
|
|
4032 |
+ |
"layered: alloy, shop (Alloy's own), vim-enhanced (yours)"
|
|
4033 |
+ |
);
|
|
4034 |
+ |
}
|
|
4035 |
+ |
|
|
4036 |
+ |
// Nothing of ours layered: the bootc caution still applies, because bootc
|
|
4037 |
+ |
// reads local modifications and does not care who asked for them. The
|
|
4038 |
+ |
// double-boot line does not, because the self-heal is ours.
|
|
4039 |
+ |
#[test]
|
|
4040 |
+ |
fn layers_that_are_none_of_ours_get_the_caution_and_not_the_self_heal() {
|
|
4041 |
+ |
let ours = ["alloy".to_string(), "shop".to_string()];
|
|
4042 |
+ |
let note = layer_note(&["vim-enhanced".to_string()], Some(&ours));
|
|
4043 |
+ |
assert_eq!(note.len(), 2);
|
|
4044 |
+ |
assert_eq!(note[0], "layered: vim-enhanced");
|
|
4045 |
+ |
assert!(note[1].contains("bootc upgrade refuses"));
|
|
4046 |
+ |
}
|
|
4047 |
+ |
|
|
4048 |
+ |
// Off an Alloy image there is no components file, and the note attributes
|
|
4049 |
+ |
// nothing rather than guessing. A guess here labels somebody else's package
|
|
4050 |
+ |
// as ours on a machine we did not build.
|
|
4051 |
+ |
#[test]
|
|
4052 |
+ |
fn a_machine_that_names_no_components_attributes_nothing() {
|
|
4053 |
+ |
let note = layer_note(&["vim-enhanced".to_string(), "tmux".to_string()], None);
|
|
4054 |
+ |
assert_eq!(note.len(), 2);
|
|
4055 |
+ |
assert_eq!(note[0], "layered: vim-enhanced, tmux");
|
|
4056 |
+ |
assert!(
|
|
4057 |
+ |
!note.iter().any(|line| line.contains("Alloy's own")),
|
|
4058 |
+ |
"nothing may be claimed as ours without the file that says so: {note:?}"
|
|
4059 |
+ |
);
|
|
4060 |
+ |
}
|
|
4061 |
+ |
|
|
4062 |
+ |
// End to end over the parse, which is how the view reaches this: the booted
|
|
4063 |
+ |
// deployment of a real Alloy machine, against the components its image
|
|
4064 |
+ |
// names.
|
|
4065 |
+ |
#[test]
|
|
4066 |
+ |
fn an_alloy_deployment_reads_through_to_the_note() {
|
|
4067 |
+ |
let status = Rpm::parse(RPM_OSTREE_ALLOY).unwrap();
|
|
4068 |
+ |
let ours = ["alloy".to_string(), "shop".to_string()];
|
|
4069 |
+ |
let note = layer_note(status.booted().unwrap().layered(), Some(&ours));
|
|
4070 |
+ |
assert_eq!(note[0], "layered: alloy, shop (Alloy's own)");
|
|
4071 |
+ |
assert!(note[2].contains("boots twice"));
|
|
4072 |
+ |
}
|
|
4073 |
+ |
|
|
4074 |
+ |
// And the machine the note is not for: an ostree machine that is not Alloy,
|
|
4075 |
+ |
// layering only its own packages, gets the caution and no claim about a
|
|
4076 |
+ |
// console it does not have.
|
|
4077 |
+ |
#[test]
|
|
4078 |
+ |
fn a_machine_layering_only_its_own_packages_hears_nothing_about_components() {
|
|
4079 |
+ |
let status = Rpm::parse(RPM_OSTREE_LAYERED).unwrap();
|
|
4080 |
+ |
let ours = ["alloy".to_string(), "shop".to_string()];
|
|
4081 |
+ |
let note = layer_note(status.booted().unwrap().layered(), Some(&ours));
|
|
4082 |
+ |
assert_eq!(note[0], "layered: vim-enhanced, rsync");
|
|
4083 |
+ |
assert_eq!(note.len(), 2);
|
|
4084 |
+ |
}
|
|
4085 |
+ |
|
|
4086 |
+ |
// The state this note does not exist for. A machine with no layer hears
|
|
4087 |
+ |
// nothing about bootc's two verbs, because for it they behave the same.
|
|
4088 |
+ |
#[test]
|
|
4089 |
+ |
fn an_unlayered_machine_gets_no_note() {
|
|
4090 |
+ |
assert!(layer_note(&[], Some(&["alloy".to_string()])).is_empty());
|
|
4091 |
+ |
}
|
|
4092 |
+ |
|
|
4093 |
+ |
// The whole reason the note exists: `bootc upgrade` is the verb bootc's own
|
|
4094 |
+ |
// documentation names and the one that no longer works here, and `bootc
|
|
4095 |
+ |
// switch` is the one that does. Both measured, 2026-08-15,
|
|
4096 |
+ |
// build/layertest/README.md. If this assertion is ever "fixed" onto
|
|
4097 |
+ |
// `rpm-ostree upgrade`, read that file first.
|
|
4098 |
+ |
#[test]
|
|
4099 |
+ |
fn the_caution_names_both_verbs_and_which_one_refuses() {
|
|
4100 |
+ |
let ours = ["alloy".to_string()];
|
|
4101 |
+ |
let note = layer_note(&ours, Some(&ours));
|
|
4102 |
+ |
let caution = note.iter().find(|line| line.contains("bootc")).unwrap();
|
|
4103 |
+ |
assert!(caution.contains("bootc upgrade refuses"));
|
|
4104 |
+ |
assert!(caution.contains("bootc switch does not"));
|
|
4105 |
+ |
}
|
|
4106 |
+ |
|
|
4107 |
+ |
// A hand-layered component is recorded under the NEVRA that installed it,
|
|
4108 |
+ |
// and is still ours. `alloy-layer-components.service` installs by bare name
|
|
4109 |
+ |
// so this is not the ordinary shape, but a machine somebody fixed by hand
|
|
4110 |
+ |
// reports it and should not be told its console is somebody else's package.
|
|
4111 |
+ |
#[test]
|
|
4112 |
+ |
fn a_component_layered_by_nevra_is_still_ours() {
|
|
4113 |
+ |
let note = layer_note(
|
|
4114 |
+ |
&["alloy-0.1.0-1.fc43.x86_64".to_string()],
|
|
4115 |
+ |
Some(&["alloy".to_string()]),
|
|
4116 |
+ |
);
|
|
4117 |
+ |
assert_eq!(note[0], "layered: alloy-0.1.0-1.fc43.x86_64 (Alloy's own)");
|
|
4118 |
+ |
}
|
|
4119 |
+ |
|
|
4120 |
+ |
// The near-miss the NEVRA match has to refuse: a package whose name merely
|
|
4121 |
+ |
// starts with ours is not ours.
|
|
4122 |
+ |
#[test]
|
|
4123 |
+ |
fn a_package_named_after_ours_is_not_ours() {
|
|
4124 |
+ |
assert!(is_component("alloy-0.1.0-1.fc43.x86_64", "alloy"));
|
|
4125 |
+ |
assert!(is_component("alloy", "alloy"));
|
|
4126 |
+ |
assert!(!is_component("alloy-utils", "alloy"));
|
|
4127 |
+ |
assert!(!is_component("alloys", "alloy"));
|
|
4128 |
+ |
assert!(!is_component("shop", "alloy"));
|
|
4129 |
+ |
}
|
|
4130 |
+ |
|
| 3840 |
4131 |
|
/// Parse this machine's real `rpm-ostree status --json`.
|
| 3841 |
4132 |
|
///
|
| 3842 |
4133 |
|
/// Ignored by default: needs an ostree system, which the dev box is not. Run
|