diff --git a/issues/a-shared-members-allowance-sets-the-kernels-deadline-at-several-times-the-work.md b/issues/a-shared-members-allowance-sets-the-kernels-deadline-at-several-times-the-work.md index 81e83d2a4cb..3ae9e28fd58 100644 --- a/issues/a-shared-members-allowance-sets-the-kernels-deadline-at-several-times-the-work.md +++ b/issues/a-shared-members-allowance-sets-the-kernels-deadline-at-several-times-the-work.md @@ -63,20 +63,45 @@ the 60 000 ms list bound and 120 000 ms deadline of a boot no member rides: |---|---|---|---| | no member | 60.0 s | 120.0 s | 420 s | | `shared-debug` | 62.1 s | 124.2 s | 425 s | -| `ccorpus` | 73.7 s | 147.4 s | 448 s | -| `shared` | 86.4 s | 172.8 s | 473 s | +| `testcases` | 100.1 s | 200.2 s | 501 s | **It grows with every member and has no ceiling**: 0.6 s of deadline a Rust -member and 0.2 s a C case. The 88 Rust members and 137 C cases on one boot, -which is what joining `shared` and `ccorpus` to `testcases` makes, arm a list -bound of 100 100 ms and a deadline of 200 200 ms, waited 501 s. A clamp is no -answer: a ceiling under the derived bound ends a healthy list. +member and 0.2 s a C case. The 88 Rust members and 137 C cases the readings +above took on `shared` and `ccorpus` ride `testcases` behind its rows' jobs, +and those two boots are gone. A clamp is no answer: a ceiling under the +derived bound ends a healthy list. + +## The merged list + +The T14 ran the merged `testcases` once, at `2c7e1be1a`, armed 200 200 and +100 100 ms by the kernel's own lines; each part between its own markers, the +kernel's clock counted from its `Boot: complete (1135ms)` line. Beside it, the +sum of the three boots it was, three readings each: `testcases` at +`9e70cd2e3`, `473efea22` and `accbd79dd`, `shared` and `ccorpus` at +`eff8b20ee`, `d6d008e88` and `49e12f23b`. + +| part | merged, `2c7e1be1a` | its parts apart | its share of the bound | +|---|---|---|---| +| the boot, to its first job | 1 191 ms | 1 190 to 1 196 ms | | +| the rows' ten jobs | 41 924 ms | 41 913 to 41 983 ms | 60 000 ms | +| the 225 members | 9 253 ms | 8 735 to 9 298 ms | 40 100 ms | +| the list's last record | 52 352 ms | 51 838 to 52 477 ms | 100 100 ms | + +**The two shares are wide by different factors, and the rows' is the +tighter.** The members took 9 253 ms of the 40 100 ms they add: the +allowances stand at about 4.3 times their work. The rows' jobs ended +43 115 ms into the kernel's clock, of the 60 000 ms `toyos_tco::JOB_BOUND_MS` +gives them: about 1.4 times theirs, `counters_metal` alone 32.7 s of it. That +constant is the same for a boot of no job and for this boot of ten, and no +allowance widens it. The list's last record left 47.7 s of its bound, and +nothing reads that margin. **A late expiry adds to the longer bound.** `issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md` is open: -that deadline was reached at 252 859 ms, and the same lateness on `shared` is -305 659 ms. The wait moves with the deadline, so what is left of it after such -an expiry is the same 167 s; the machine is held 52.8 s longer. +that deadline was reached at 252 859 ms, and the same lateness on `testcases` +is 333 059 ms. The wait moves with the deadline, so what is left of it after +such an expiry is 167 s there and 168 s here; the machine is held 80.2 s +longer. ## Owner @@ -85,13 +110,18 @@ The metal suite: `tests/common/metal.rs`, which derives the bounds, and ## Exit -Both, on the T14: - -- Each allowance is derived at its declaration from what the T14 measures of a - list's total over its members, by one stated factor: a factor over a - per-member mean says nothing of a list of slow members. And the judge reds a - shared boot whose list's last record came past half its bound, which today - is a line a reader reads. -- The boot that carries `shared`, `ccorpus` and `testcases` together reads its - list's last record within half its list bound, with the pair read from the - kernel's own `boot deadline:` and `hard lockup:` lines. +Three parts, each on the T14. None is built. + +1. **The members.** Each allowance is derived at its declaration from the + members' sum between their own markers, by one stated factor: a factor over + a per-member mean says nothing of a list of slow members. And the judge + reds a boot whose members' sum is past the share they add, which today is a + line a reader reads. +2. **The rows' jobs.** What a boot's rows' jobs are given is derived the same + way from what they take, where today it is one constant, + `toyos_tco::JOB_BOUND_MS`, for a boot of no job and a boot of ten, one of + them 32.7 s. And the judge reds a boot whose rows' jobs end past their + share, which today nothing reads. +3. **`testcases` reads both inside their shares**, with the armed pair read + from the kernel's own `boot deadline:` and `hard lockup:` lines and the + margin from the list's last record to its list bound stated beside them. diff --git a/issues/a-t14-boot-that-outlogs-its-retention-loses-its-middle-and-the-rows-whose-lines-sat-there.md b/issues/a-t14-boot-that-outlogs-its-retention-loses-its-middle-and-the-rows-whose-lines-sat-there.md index ccfe2c6fdbf..1f69982c6dc 100644 --- a/issues/a-t14-boot-that-outlogs-its-retention-loses-its-middle-and-the-rows-whose-lines-sat-there.md +++ b/issues/a-t14-boot-that-outlogs-its-retention-loses-its-middle-and-the-rows-whose-lines-sat-there.md @@ -80,9 +80,11 @@ count line is there at 42.579 s (`28 SCIs; embedded controller queries taken: in seconds 36 to 47. `acpi_server_events` and `acpi_tables_loaded` ride `testcases` again, with -`test_rs_acpi_hold` its last job, and `testcases-hold` is deleted. The job -waits on the `log` port for the server's count line, bounded by the runner's -bound less a tenth, and exits non-zero without it. That readback judged +`test_rs_acpi_hold` the last of its rows' jobs and the shared members behind +it, and `testcases-hold` is deleted. The job waits on the `log` port for the +server's count line until 54 000 ms after boot +(`issues/acpi-hold-gives-up-at-a-time-counted-from-boot-whatever-its-runner-was-given.md`), +and exits non-zero without it. That readback judged offline by this harness reds by name: `testcases's own log is missing its parts 2 to 26; no row is judged on it`. diff --git a/issues/acpi-hold-gives-up-at-a-time-counted-from-boot-whatever-its-runner-was-given.md b/issues/acpi-hold-gives-up-at-a-time-counted-from-boot-whatever-its-runner-was-given.md new file mode 100644 index 00000000000..c1283ae44fe --- /dev/null +++ b/issues/acpi-hold-gives-up-at-a-time-counted-from-boot-whatever-its-runner-was-given.md @@ -0,0 +1,40 @@ +--- +status: open +kind: tooling +opened: 2026-10-09 +--- + +# `acpi_hold` gives up at a time counted from boot, whatever its runner was given + +`tests/toyos-rust-tests/src/bin/acpi_hold.rs` holds `testcases` open until +`/system/bin/acpiserver` has logged its count, which `acpi_server_events` +judges. Its wait ends at `UNTIL_MS`, `toyos_tco::JOB_BOUND_MS` less a tenth: +54 000 ms, counted from the kernel's zero and not from the hold's own start. +Two things follow. + +**What the hold is given shrinks with every job before it.** It is the last of +its boot's rows' jobs. On the T14 at `accbd79dd` it started 43 161 ms in, with +10.8 s left; on the merged `testcases` at `2c7e1be1a`, 43 095 ms in, with +10.9 s left. A rows' job that adds 10.8 s to that list leaves it nothing: +`served_log::Log::until` then panics before it reads a line, with `the log did +not show … within 0ns`, and `acpi_server_events` reds over a count line that +was written some 20 s earlier and is in the log. The red names the server's +count, not the list that grew. + +**The constant is not the bound its runner was given.** `UNTIL_MS` is written +as "the runner's bound less a tenth", and `testcases`' runner is given +`--bound-ms=100100`: the 60 000 ms and what its 225 members add. The members +were ordered behind the hold for this reason and no other. Behind them it +would start 51.8 to 52.5 s in by the readings on record, with 1.5 to 2.2 s +left. + +## Owner + +The harness: `tests/toyos-rust-tests/src/bin/acpi_hold.rs`, and +`TESTCASES_HELD` in `tests/toyos.rs`, which places it. + +## Exit + +The hold is bounded from its own start, or by the bound its runner was given, +and a job added before it cannot red `acpi_server_events` over a line the log +holds. diff --git a/issues/the-t14-reboots-through-ubuntu-for-every-test.md b/issues/the-t14-reboots-through-ubuntu-for-every-test.md index 2278d88133e..b80d322d7d0 100644 --- a/issues/the-t14-reboots-through-ubuntu-for-every-test.md +++ b/issues/the-t14-reboots-through-ubuntu-for-every-test.md @@ -44,9 +44,9 @@ metal rows. Every stage below has the host drive a T14 that runs ToyOS, so the track is blocked on `issues/the-host-cannot-reach-the-t14-while-it-runs-toyos.md`. -**Next: two sessions in place of five boots.** `shared`, `ccorpus`, -`testcases`, `testcases-mkdir` and `testcases-readdir` share a config, a -parameter line and the shipping kernel. The session image is `tests/testcases` +**Next: two sessions in place of three boots.** `testcases`, which every +shipping-kernel member rides, `testcases-mkdir` and `testcases-readdir` share +a config, a parameter line and the shipping kernel. The session image is `tests/testcases` with the netd, sshd and streaming `logd` of `tests/lantalkcase`, which is deleted (`issues/the-host-cannot-reach-the-t14-while-it-runs-toyos.md`), under a `boot-deadline=` derived as a metal boot's is: twice its list's bound diff --git a/tests/checks/metal.rs b/tests/checks/metal.rs index a3fa5d3b35f..9b20fc8c72d 100644 --- a/tests/checks/metal.rs +++ b/tests/checks/metal.rs @@ -370,6 +370,10 @@ pub fn a_run_under_another_bios_fails_and_records_nothing() { assert_eq!(judged_against(&lacking(&bios)), (false, committed(&bios, 1165))); } +fn member(job: &str, adds_ms: u64) -> metal::Member { + metal::Member { job: job.to_string(), adds_ms } +} + /// **A failing shared member fails itself alone**: the run is red, and the /// boot it rode records its own numbers as the boot whose members all passed /// does. @@ -391,8 +395,7 @@ pub fn a_failing_shared_member_fails_itself_alone() { config: "tests/testcases", params: &[], features: &[], - member_ms: toyos_tco::RUST_MEMBER_MS, - jobs: Vec::from(jobs.map(String::from)), + members: jobs.iter().map(|job| member(job, toyos_tco::RUST_MEMBER_MS)).collect(), files: Vec::new(), links: Vec::new(), }; @@ -436,12 +439,13 @@ pub fn a_boots_last_job_is_behind_every_other() { assert!(refused.contains("other ends the boot \"own\" on later and another row ends it on hold"), "{refused}"); } -/// **A boot's rows' jobs run before its members, its last job behind both, -/// and its bounds follow from who rides it**: the runner's bound over the list -/// is the rows' and what each member adds, the kernel's over the boot twice -/// that, and a boot no member rides keeps the bounds a row's boot had. A +/// **A boot's rows' jobs run before its members, the rows' last job among +/// them, and its bounds follow from who rides it**: the runner's bound over +/// the list is the rows' and what each member adds, the kernel's over the boot +/// twice that, and a boot no member rides keeps the bounds a row's boot had. A /// shared boot that disagrees with a row about the image is refused, and so -/// are two under one name. +/// are two under one name, and two jobs of one boot the kernel records under +/// one name. pub fn rows_run_before_members_under_a_bound_the_members_widen() { static EARLY: Metal = Metal { arms: &[metal::once("shared", "tests/testcases", &[], &["tone", "cost"])], judge: |_| Ok(()) }; @@ -456,23 +460,25 @@ pub fn rows_run_before_members_under_a_bound_the_members_widen() { arms: &[metal::once("wedge", "tests/jobcase", &[toyos_build::metal::WEDGE_ARM], &[])], judge: |_| Ok(()), }; - let members = |boot: &str, member_ms: u64, jobs: &[&str]| metal::SharedBoot { + let members = |boot: &str, members: &[(&str, u64)]| metal::SharedBoot { boot: boot.to_string(), config: "tests/testcases", params: &[], features: &[], - member_ms, - jobs: jobs.iter().map(ToString::to_string).collect(), - files: vec![(format!("expect/{}", jobs[0]), Vec::new())], + members: members.iter().map(|(job, adds_ms)| member(job, *adds_ms)).collect(), + files: vec![(format!("expect/{}", members[0].0), Vec::new())], links: Vec::new(), }; let rows = [("early", &EARLY), ("held", &HELD), ("wedges", &WEDGES)]; - let shared = [members("shared", 860, &["m1", "m2", "m3"]), members("corpus", 260, &["c1", "c2"])]; + let shared = [ + members("shared", &[("m1", 860), ("c1", 260), ("m2", 860)]), + members("corpus", &[("c2", 260), ("c3", 260)]), + ]; let boots = metal::batches(&rows, &shared).expect("one image a boot"); let bounds = |boot: &str| (boots[boot].bound_ms(), boots[boot].deadline_ms()); - assert_eq!(boots["shared"].jobs, ["tone", "cost", "late", "m1", "m2", "m3", "hold"]); - assert_eq!(bounds("shared"), (62_580, 125_160)); - assert_eq!(boots["corpus"].jobs, ["c1", "c2"]); + assert_eq!(boots["shared"].jobs, ["tone", "cost", "late", "hold", "m1", "c1", "m2"]); + assert_eq!(bounds("shared"), (61_980, 123_960)); + assert_eq!(boots["corpus"].jobs, ["c2", "c3"]); assert_eq!(bounds("corpus"), (60_520, 121_040)); // No member: the bounds every row's boot had before a list could widen. assert_eq!(bounds("own"), (60_000, 120_000)); @@ -488,6 +494,11 @@ pub fn rows_run_before_members_under_a_bound_the_members_widen() { panic!("two shared boots under one name were batched"); }; assert!(refused.contains("two shared boots are both named \"shared\""), "{refused}"); + let long = members("shared", &[("test_rs_a_name_past_what_is_kept", 860), ("test_rs_a_name_past_what_is_lost", 860)]); + let Err(refused) = metal::batches(&rows, &[long]) else { + panic!("two jobs the kernel records under one name were batched"); + }; + assert!(refused.contains("the kernel records both as \"test_rs_a_name_past_what_is\""), "{refused}"); } /// **What a run's words take**: no word the whole profile; a name word every @@ -505,8 +516,7 @@ pub fn words_take_rows_members_and_whole_boots() { config: "tests/testcases", params: &[], features: &[], - member_ms: toyos_tco::RUST_MEMBER_MS, - jobs: jobs.iter().map(ToString::to_string).collect(), + members: jobs.iter().map(|job| member(job, toyos_tco::RUST_MEMBER_MS)).collect(), files: jobs .iter() .flat_map(|job| [(format!("expect/{job}"), Vec::new()), (format!("bin/test_c_{job}"), Vec::new())]) @@ -514,11 +524,12 @@ pub fn words_take_rows_members_and_whole_boots() { links: jobs.iter().map(|job| (format!("bin/{job}"), "/system/bin/test_rs_ccheck".to_string())).collect(), }; let profile = [boot("shared", &["test_rs_a1", "test_rs_a2", "test_rs_b1"]), boot("ccorpus", &["c1", "c2"])]; + let jobs_of = |boot: &metal::SharedBoot| boot.members.iter().map(|m| m.job.clone()).collect::>(); let taken = |names: &[&str], boots: &[&str]| { metal::select(names, boots, &ROWS, &profile).map(|(rows, shared)| { let rows: Vec<&str> = rows.iter().map(|(name, _)| *name).collect(); let shared: Vec = - shared.iter().map(|boot| format!("{}={}", boot.boot, boot.jobs.join("+"))).collect(); + shared.iter().map(|boot| format!("{}={}", boot.boot, jobs_of(boot).join("+"))).collect(); (rows.join(","), shared.join(",")) }) }; diff --git a/tests/common/metal.rs b/tests/common/metal.rs index 4fab02f6f74..7810ecabe11 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -56,9 +56,10 @@ pub struct Arm { /// kernel, and that is what most of the suite wants: it is the artifact the /// owner flashes. pub features: &'static [&'static str], - /// The job this boot ends on, before `reboot`: [`batches`] puts it after - /// every job any arm on the boot names and every member riding it, so none - /// can land behind it, and refuses two arms that name different ones. + /// The job this boot's rows end on: [`batches`] puts it after every job any + /// arm on the boot names, so no row's can land behind it, and refuses two + /// arms that name different ones. The members riding the boot run behind + /// it: it is a row's job, and a row's jobs have the machine first. pub last: Option<&'static str>, } @@ -97,12 +98,8 @@ pub struct SharedBoot { pub params: &'static [&'static str], /// The kernel build, empty for the one an image ships. pub features: &'static [&'static str], - /// What each member adds to the bound the runner gives the boot's whole - /// list (`toyos_tco::list_bound_ms`), in milliseconds. - pub member_ms: u64, - /// What the runner spawns, in order — the whole binary name, `test_rs_` - /// prefix and all, because that is what the kernel records it under. - pub jobs: Vec, + /// What the runner spawns behind the rows' jobs, in order. + pub members: Vec, /// Files this boot needs on ROOT beside the binaries: a corpus's committed /// expectations, which the guest compares against because on this machine /// no host can read what a case printed. @@ -112,10 +109,21 @@ pub struct SharedBoot { pub links: Vec<(String, String)>, } +/// One discovered member of a [`SharedBoot`]. +#[derive(Clone)] +pub struct Member { + /// The whole binary name, `test_rs_` prefix and all, because that is what + /// the kernel records it under. + pub job: String, + /// What it adds to the bound the runner gives its boot's whole list + /// (`toyos_tco::list_bound_ms`), in milliseconds. + pub adds_ms: u64, +} + impl SharedBoot { /// What the members add to their boot's list bound. fn members_ms(&self) -> u64 { - self.jobs.len() as u64 * self.member_ms + self.members.iter().map(|member| member.adds_ms).sum() } /// This boot with the members `named` and none else, or `None` where that @@ -123,15 +131,15 @@ impl SharedBoot { /// name**: the C corpus stages a binary and an expectation per case, and a /// stick is written over `ssh`. fn keeping(&self, named: impl Fn(&str) -> bool) -> Option { - let jobs: Vec = self.jobs.iter().filter(|job| named(job)).cloned().collect(); + let members: Vec = self.members.iter().filter(|member| named(&member.job)).cloned().collect(); let mine = |path: &str| { let last = path.rsplit('/').next().unwrap_or(path); let case = last.strip_prefix("test_c_").unwrap_or(last); - jobs.iter().any(|job| job == case) + members.iter().any(|member| member.job == case) }; let files = self.files.iter().filter(|(path, _)| mine(path)).cloned().collect(); let links = self.links.iter().filter(|(from, _)| mine(from)).cloned().collect(); - (!jobs.is_empty()).then(|| SharedBoot { jobs, files, links, ..self.clone() }) + (!members.is_empty()).then(|| SharedBoot { members, files, links, ..self.clone() }) } } @@ -155,7 +163,7 @@ pub fn select( job.strip_prefix("test_rs_").unwrap_or(job) } let all = names.is_empty() && boots.is_empty(); - let jobs = || shared.iter().flat_map(|boot| &boot.jobs).map(|job| bare(job)); + let jobs = || shared.iter().flat_map(|boot| &boot.members).map(|member| bare(&member.job)); if let Some(dead) = names.iter().find(|word| { !rows.iter().any(|(name, _)| name.contains(**word)) && !jobs().any(|job| job.contains(**word)) }) { @@ -431,6 +439,24 @@ impl Readback { }) } + /// How long the runner had each job running, between the job's own two + /// markers on the log's clock; a job with no such pair has no entry. + fn jobs_ms(&self) -> BTreeMap<&str, u64> { + let mut started: BTreeMap<&str, u64> = BTreeMap::new(); + let mut took = BTreeMap::new(); + for line in self.log.lines() { + let Some(at) = toyos_logstream::parse(line).and_then(|parsed| parsed.ms) else { continue }; + if let Some(job) = line.split("===TEST_START ").nth(1).and_then(|rest| rest.strip_suffix("===")) { + started.insert(job, at); + } else if let Some(job) = line.split("===TEST_END ").nth(1).and_then(|rest| rest.split(' ').next()) { + if let Some(from) = started.get(job) { + took.insert(job, at.saturating_sub(*from)); + } + } + } + took + } + /// One number measured on this boot. It is its measurer's, the row whose /// judge called this or the boot itself for what [`judge_readbacks`] reads /// off every boot, and is judged against this machine's record once every @@ -581,6 +607,10 @@ pub fn at(dir: &Path, label: &str) -> PathBuf { /// **A boot's rows' jobs run before its members**, so a row that measures has /// the machine its jobs alone would give it, and what the members write to the /// log lands behind every row's. +/// +/// **Two jobs of one boot the kernel records under one name are refused** +/// ([`bootlog::recorded_name`]): the second one's verdict would be read off +/// the first one's exit. pub fn batches( tests: &[(&str, &'static Metal)], shared: &[SharedBoot], @@ -604,6 +634,12 @@ pub fn batches( } } } + for batch in out.values_mut() { + if let Some(last) = batch.last { + batch.jobs.retain(|job| job != last); + batch.jobs.push(last.to_string()); + } + } let mut ridden: BTreeSet<&str> = BTreeSet::new(); for boot in shared { if !ridden.insert(&boot.boot) { @@ -611,15 +647,21 @@ pub fn batches( } let who = format!("the shared boot {:?}", boot.boot); let batch = ride(&mut out, &who, &boot.boot, boot.config, boot.params, boot.features)?; - batch.add(boot.jobs.iter().cloned()); + batch.add(boot.members.iter().map(|member| member.job.clone())); batch.members_ms += boot.members_ms(); batch.files.extend(boot.files.iter().cloned()); batch.links.extend(boot.links.iter().cloned()); } - for batch in out.values_mut() { - if let Some(last) = batch.last { - batch.jobs.retain(|job| job != last); - batch.jobs.push(last.to_string()); + for (label, batch) in &out { + let mut recorded: BTreeMap = BTreeMap::new(); + for job in &batch.jobs { + if let Some(other) = recorded.insert(bootlog::recorded_name(job), job) { + return Err(format!( + "the boot {label:?} runs {other} and {job}, and the kernel records both as {:?}: one \ + boot's log cannot tell their verdicts apart", + bootlog::recorded_name(job) + )); + } } } Ok(out) @@ -893,7 +935,7 @@ pub fn run( eprintln!( "[metal] {} registration(s) and {} shared member(s) over {} boot(s)", runs.len(), - shared.iter().map(|b| b.jobs.len()).sum::(), + shared.iter().map(|b| b.members.len()).sum::(), batches.len(), ); @@ -1140,10 +1182,10 @@ pub fn judge_readbacks( } let mut members = 0usize; for boot in shared { - eprintln!("\n[metal] {}: {} member(s)", boot.boot, boot.jobs.len()); + eprintln!("\n[metal] {}: {} member(s)", boot.boot, boot.members.len()); let back = readbacks.get(&boot.boot).expect("every shared boot was batched"); let mut ran = 0usize; - for job in &boot.jobs { + for Member { job, .. } in &boot.members { members += 1; let verdict = match back { Err(why) => Err(why.clone()), @@ -1163,18 +1205,33 @@ pub fn judge_readbacks( } } if let (Ok(back), true) = (back, ran > 0) { - if let (Some(complete), Some(last)) = (back.complete_record_ms(), back.last_record_ms()) { - let each = last.saturating_sub(complete) / ran as u64; - eprintln!(" {} ms per member over the {ran} that ran", each); - // On the clock the runner counts its bound on, the kernel's - // own, whose reading `Boot: complete` states beside its stamp. - if let Some(boot_ms) = back.boot_ms { - eprintln!( - " its last record came {} ms into a list bound of {} ms", - (last + boot_ms).saturating_sub(complete), - toyos_tco::list_bound_ms(boot.members_ms()) - ); - } + // Each member between its own markers: the rows' jobs share the + // list. One cut inside its run has no pair and is named, not + // summed as nothing. + let jobs = back.jobs_ms(); + let (timed, unpaired): (Vec<&Member>, Vec<&Member>) = + boot.members.iter().partition(|member| jobs.contains_key(member.job.as_str())); + let took: u64 = timed.iter().map(|member| jobs[member.job.as_str()]).sum(); + eprintln!( + " its members took {took} ms of the {} ms they add to the list's bound, summed over the {} of \ + {} with both markers", + boot.members_ms(), + timed.len(), + boot.members.len() + ); + if let Some(first) = unpaired.first() { + eprintln!(" {} without a start and an end marker, the first {}", unpaired.len(), first.job); + } + // On the clock the runner counts its bound on, the kernel's own, + // whose reading `Boot: complete` states beside its stamp. + if let (Some(complete), Some(last), Some(boot_ms)) = + (back.complete_record_ms(), back.last_record_ms(), back.boot_ms) + { + eprintln!( + " its last record came {} ms into a list bound of {} ms", + (last + boot_ms).saturating_sub(complete), + toyos_tco::list_bound_ms(boot.members_ms()) + ); } } } diff --git a/tests/metal/lenovo-20w0003amz.toml b/tests/metal/lenovo-20w0003amz.toml index fc3981df3d9..b846dd8239a 100644 --- a/tests/metal/lenovo-20w0003amz.toml +++ b/tests/metal/lenovo-20w0003amz.toml @@ -6,9 +6,6 @@ bios = "N34ET71W (1.71 )" "boot.acpicase.complete_ms" = 1207 "boot.acpicase.panel_max_us" = 2495 "boot.acpicase.panel_us" = 12966 -"boot.ccorpus.complete_ms" = 1165 -"boot.ccorpus.panel_max_us" = 3759 -"boot.ccorpus.panel_us" = 22178 "boot.deadlinewedge.complete_ms" = 1165 "boot.deadlinewedge.panel_max_us" = 3850 "boot.deadlinewedge.panel_us" = 21662 @@ -40,15 +37,9 @@ bios = "N34ET71W (1.71 )" "boot.shared-debug.complete_ms" = 1165 "boot.shared-debug.panel_max_us" = 3791 "boot.shared-debug.panel_us" = 21483 -"boot.shared.complete_ms" = 1154 -"boot.shared.panel_max_us" = 3608 -"boot.shared.panel_us" = 21429 "boot.testcases-deaf.complete_ms" = 1165 "boot.testcases-deaf.panel_max_us" = 5247 "boot.testcases-deaf.panel_us" = 26659 -"boot.testcases-debug.complete_ms" = 1165 -"boot.testcases-debug.panel_max_us" = 3824 -"boot.testcases-debug.panel_us" = 22032 "boot.testcases-mkdir.complete_ms" = 1166 "boot.testcases-mkdir.panel_max_us" = 3839 "boot.testcases-mkdir.panel_us" = 22100 diff --git a/tests/toyos-rust-tests/src/bin/endowment_denied.rs b/tests/toyos-rust-tests/src/bin/endowment_denied.rs index 5971baff5e5..717c6f03835 100644 --- a/tests/toyos-rust-tests/src/bin/endowment_denied.rs +++ b/tests/toyos-rust-tests/src/bin/endowment_denied.rs @@ -50,7 +50,10 @@ //! //! **The applets nobody compared.** A row's granularity is the binary and //! `/system/bin/toybox` is many programs behind many links, so every applet is endowed -//! the union; the last half holds each against a policy table. +//! the union; the last half holds each against a policy table. A link is an +//! applet only where its target has a row: a link to a binary no row names +//! buys no row's authority, since the supervisor answers it undeclared and its +//! spawner endows it, and the C corpus's cases are such links. //! //! **A wrong-typed handle is refused with a word here, and that is a property of //! the check rather than an exception to the policy.** The table resolves rights @@ -533,6 +536,7 @@ fn every_applet_holds_only_what_its_policy_names() { let rows = manifest_rows(&manifest); let mut applets: Vec = Vec::new(); + let mut unrowed = 0; for entry in std::fs::read_dir(BIN).expect("read /system/bin") { let path = entry.expect("a /system/bin entry").path(); let meta = std::fs::symlink_metadata(&path).expect("lstat a /system/bin entry"); @@ -540,6 +544,11 @@ fn every_applet_holds_only_what_its_policy_names() { continue; } let target = std::fs::read_link(&path).expect("read a /system/bin link"); + // A row is matched by its whole path, as the supervisor matches one. + if !target.to_str().is_some_and(|to| rows.contains_key(to)) { + unrowed += 1; + continue; + } assert_eq!( target.to_str(), Some(MULTICALL), @@ -574,7 +583,8 @@ fn every_applet_holds_only_what_its_policy_names() { list this test declares — a row was split, or one grew", ); println!( - " applets: {} links behind {MULTICALL}, {} declared over-grants and no undeclared one", + " applets: {} links behind {MULTICALL}, {} declared over-grants and no undeclared one; \ + {unrowed} links to a binary no row names", applets.len(), over.len(), ); diff --git a/tests/toyos.rs b/tests/toyos.rs index b9e778df55c..fee49f300f0 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -177,9 +177,10 @@ const RUST_SKIP: &[&str] = &[ /// The shared boot's last members, in this order: each fills a bound of its /// own process — tens of thousands of mappings, thousands of threads, a /// thousand 2 MiB images — and its exit gives all of it back. Behind every -/// discovered member, so none of those runs after one of these. **This is -/// their one declaration**: [`discover_rust_tests`] leaves them to it, so a -/// name taken off this list is a discovered member again. +/// discovered member and every corpus case, so nothing but the reboot runs +/// after one of these. **This is their one declaration**: +/// [`discover_rust_tests`] leaves them to it, so a name taken off this list is +/// a discovered member again. const LAST_MEMBERS: &[&str] = &["abuse_mmap_regions", "abuse_thread_table", "abuse_dlopen_ledger"]; /// Binaries a metal row or a guest test drives that the shared boot also runs @@ -515,12 +516,13 @@ const METAL: &[(&str, metal::Metal)] = &[ ), ( // `SYS_DEBUG` holds each other CPU's shootdown acknowledgement back, - // so the kernel that carries it; the verdict is the guest's own exit. + // so the kernel that carries it; the verdict is the guest's own exit, + // and every wait it asserts is a floor, which no load shortens. "tlb_shootdown_waits", metal::Metal { arms: &[metal::Arm { - features: toyos_build::build::TEST_KERNEL, - ..metal::once("testcases-debug", "tests/testcases", &[], &["test_rs_tlb_shootdown_waits"]) + features: ACTUATOR_KERNEL, + ..metal::once("shared-debug", "tests/testcases", SELFTEST_PARAMS, &["test_rs_tlb_shootdown_waits"]) }], judge: |b| b[0].job_passed("test_rs_tlb_shootdown_waits"), }, @@ -532,8 +534,8 @@ const METAL: &[(&str, metal::Metal)] = &[ "trace_record_cost", metal::Metal { arms: &[metal::Arm { - features: toyos_build::build::TEST_KERNEL, - ..metal::once("testcases-debug", "tests/testcases", &[], &["test_rs_trace_flood"]) + features: ACTUATOR_KERNEL, + ..metal::once("shared-debug", "tests/testcases", SELFTEST_PARAMS, &["test_rs_trace_flood"]) }], judge: |b| { b[0].job_passed("test_rs_trace_flood")?; @@ -906,8 +908,8 @@ const ISA_WITHHELD: &[metal::Arm] = &[metal::once( &["test_rs_isa_grant", "test_rs_isa_lines"], )]; -/// The boot most of the first tranche rides: the plain `tests/testcases` shape -/// with a job list that ends it. +/// The boot most rows ride, and every shipping-kernel member behind them +/// ([`shared_metal`]): the plain `tests/testcases` shape. const TESTCASES: &[metal::Arm] = &[metal::once( "testcases", "tests/testcases", @@ -924,8 +926,10 @@ const TESTCASES: &[metal::Arm] = &[metal::once( ], )]; -/// The same boot, ended on the hold: the count it waits for comes thirty -/// seconds after the server arms, and a job behind it would wait that out too. +/// The same boot, its rows ended on the hold: the count it waits for comes +/// thirty seconds after the server arms, and a row's job behind it would wait +/// that out too. The members run behind it: the hold gives up at a time since +/// boot, which a list of members before it would spend. const TESTCASES_HELD: &[metal::Arm] = &[metal::Arm { last: Some("test_rs_acpi_hold"), ..metal::once("testcases", "tests/testcases", &[], &[]) @@ -1025,37 +1029,29 @@ const SELFTESTS: &[metal::Arm] = &[metal::Arm { ..metal::once("shared-debug", "tests/testcases", SELFTEST_PARAMS, &[]) }]; -/// The boots every discovered Rust binary rides on the T14: the shipping -/// kernel's, with [`LAST_MEMBERS`] behind them, and [`ACTUATOR_TESTS`] on the -/// kernel that carries `SYS_DEBUG`, armed with [`SELFTEST_PARAMS`]. -fn shared_metal() -> Vec { - let (debug, mut shipping): (Vec, Vec) = discover_rust_tests() +/// The two boots every discovered member rides on the T14, each behind its +/// rows' jobs: `testcases`, the shipping kernel's, carries every discovered +/// Rust binary, then the C corpus, then [`LAST_MEMBERS`]; `shared-debug` +/// carries [`ACTUATOR_TESTS`] on the kernel that has `SYS_DEBUG`, armed with +/// [`SELFTEST_PARAMS`]. A second boot rather than one image for the whole +/// set: what those need is a syscall number the rest must not have. +fn shared_metal(c_bins: &[(String, Vec)]) -> Vec { + let rust = |names: Vec| { + names.into_iter().map(|n| metal::Member { job: format!("test_rs_{n}"), adds_ms: toyos_tco::RUST_MEMBER_MS }) + }; + let (debug, shipping): (Vec, Vec) = discover_rust_tests() .into_iter() .partition(|name| ACTUATOR_TESTS.contains(&name.as_str())); - shipping.extend(LAST_MEMBERS.iter().map(ToString::to_string)); + let corpus = c_corpus_metal(c_bins); + let last = rust(LAST_MEMBERS.iter().map(ToString::to_string).collect()); vec![ - metal::SharedBoot { - boot: "shared".to_string(), - config: "tests/testcases", - params: &[], - features: &[], - member_ms: toyos_tco::RUST_MEMBER_MS, - jobs: shipping.iter().map(|n| format!("test_rs_{n}")).collect(), - files: Vec::new(), - links: Vec::new(), - }, - // The same list's other half, on the kernel that carries `SYS_DEBUG`. - // A second boot rather than a second image for the whole set: what - // these need is a syscall number the rest must not have, and a boot - // where every binary could call it would stop being the shipping - // machine for the other seventy. + metal::SharedBoot { members: rust(shipping).chain(corpus.members.clone()).chain(last).collect(), ..corpus }, metal::SharedBoot { boot: "shared-debug".to_string(), config: "tests/testcases", params: SELFTEST_PARAMS, features: ACTUATOR_KERNEL, - member_ms: toyos_tco::RUST_MEMBER_MS, - jobs: debug.iter().map(|n| format!("test_rs_{n}")).collect(), + members: rust(debug).collect(), files: Vec::new(), links: Vec::new(), }, @@ -1075,8 +1071,8 @@ const LATENCYCASE: &[metal::Arm] = &[metal::once( &["test_rs_cyclictest", "test_rs_sched_stress"], )]; -/// The C corpus on the T14: one boot, one job per case, each judged in the -/// guest. +/// The C corpus's part of the `testcases` boot, which [`shared_metal`] puts +/// the Rust members around: one job per case, each judged in the guest. /// /// **Every case in the corpus `return 0`s unconditionally**, so a bare /// exit-code verdict would be vacuous — the comparison is the whole point. It @@ -1090,40 +1086,19 @@ const LATENCYCASE: &[metal::Arm] = &[metal::once( /// job list of a hundred and nineteen `ccheck`s would leave one name and a /// hundred and nineteen records told apart only by position. fn c_corpus_metal(c_bins: &[(String, Vec)]) -> metal::SharedBoot { - let mut jobs = Vec::new(); + let mut members = Vec::new(); let mut files = Vec::new(); let mut links = Vec::new(); - let mut seen: BTreeMap = BTreeMap::new(); for (case, data) in c_bins { // A case with no committed expectation is one nothing could judge, and // shipping it would be a job that passes by comparing nothing. let Some(expected) = c_expectation(case) else { continue }; - // **The kernel truncates a process name**, so two cases whose names - // agree that far would land under one record. Refused rather than - // reported, because the second one's verdict would be read as the - // first's. - let recorded = bootlog::recorded_name(case); - if let Some(other) = seen.insert(recorded.clone(), case.clone()) { - panic!( - "the C cases {other:?} and {case:?} are both recorded as {recorded:?}, so one \ - boot's log cannot tell their verdicts apart" - ); - } files.push((format!("expect/{case}"), expected.into_bytes())); files.push((format!("bin/test_c_{case}"), data.clone())); links.push((format!("bin/{case}"), format!("/system/bin/{CCHECK}"))); - jobs.push(case.clone()); - } - metal::SharedBoot { - boot: "ccorpus".to_string(), - config: "tests/testcases", - params: &[], - features: &[], - member_ms: toyos_tco::C_MEMBER_MS, - jobs, - files, - links, + members.push(metal::Member { job: case.clone(), adds_ms: toyos_tco::C_MEMBER_MS }); } + metal::SharedBoot { boot: "testcases".to_string(), config: "tests/testcases", params: &[], features: &[], members, files, links } } /// The comparator's own staged name. It is a `RUST_SKIP` helper, so discovery @@ -5352,8 +5327,7 @@ fn main() { if let Some(mode) = parsed.metal { let (c_bins, rust_bins) = build_shared_bins(); - let mut boots = shared_metal(); - boots.push(c_corpus_metal(&c_bins)); + let boots = shared_metal(&c_bins); let (selected, boots) = match metal::select(filters, &parsed.boots, METAL, &boots) { Ok(selection) => selection, Err(refusal) => {