1use std::collections::HashMap;
18
19pub(crate) const SCHED_OUTPUT_START: &str = "===SCHED_OUTPUT_START===";
23pub(crate) const SCHED_OUTPUT_END: &str = "===SCHED_OUTPUT_END===";
27
28pub(crate) fn parse_sched_output(output: &str) -> Option<&str> {
39 let start = output.find(SCHED_OUTPUT_START)?;
40 let end = output.rfind(SCHED_OUTPUT_END)?;
41 let after_marker = start + SCHED_OUTPUT_START.len();
42 if after_marker >= end {
43 return None;
44 }
45 let content = output[after_marker..end].trim();
46 if content.is_empty() {
47 return None;
48 }
49 Some(content)
50}
51
52pub(crate) fn concat_sched_log_chunks(
65 drain: Option<&crate::vmm::host_comms::BulkDrainResult>,
66) -> String {
67 let Some(drain) = drain else {
68 return String::new();
69 };
70 let mut acc = String::new();
71 for e in &drain.entries {
72 if e.msg_type != crate::vmm::wire::MSG_TYPE_SCHED_LOG || !e.crc_ok {
73 continue;
74 }
75 acc.push_str(&String::from_utf8_lossy(&e.payload));
76 }
77 acc
78}
79
80pub(crate) fn parse_sched_output_partial(output: &str) -> Option<&str> {
93 if let Some(content) = parse_sched_output(output) {
94 return Some(content);
95 }
96 let start = output.find(SCHED_OUTPUT_START)?;
97 let after_marker = start + SCHED_OUTPUT_START.len();
98 let content = output[after_marker..].trim();
99 if content.is_empty() {
100 return None;
101 }
102 Some(content)
103}
104
105pub struct VerifierStats {
108 pub processed_insns: u64,
110 pub total_states: u64,
112 pub peak_states: u64,
114 pub time_usec: Option<u64>,
117 pub stack_depth: Option<String>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
124pub struct ProgStats {
125 pub name: String,
127 pub verified_insns: u32,
130}
131
132pub struct DiffRow {
134 pub name: String,
136 pub a: u64,
138 pub b: u64,
140 pub delta: i64,
143}
144
145fn parse_or_warn(raw: &str, field: &str) -> u64 {
151 match raw.parse() {
152 Ok(n) => n,
153 Err(e) => {
154 tracing::warn!(
155 field,
156 word = raw,
157 err = %e,
158 "malformed BPF verifier count; leaving 0",
159 );
160 0
161 }
162 }
163}
164
165pub fn parse_verifier_stats(log: &str) -> VerifierStats {
171 let mut stats = VerifierStats {
172 processed_insns: 0,
173 total_states: 0,
174 peak_states: 0,
175 time_usec: None,
176 stack_depth: None,
177 };
178
179 let mut found_insns = false;
180 let mut found_time = false;
181 let mut found_stack = false;
182
183 for line in log.lines().rev() {
184 if !found_insns && line.starts_with("processed ") {
185 found_insns = true;
186 let words: Vec<&str> = line.split_whitespace().collect();
187 if words.len() >= 2 {
188 stats.processed_insns = parse_or_warn(words[1], "processed_insns");
189 }
190 for (i, &w) in words.iter().enumerate() {
191 if w == "total_states"
192 && let Some(v) = words.get(i + 1)
193 {
194 stats.total_states = parse_or_warn(v, "total_states");
195 }
196 if w == "peak_states"
197 && let Some(v) = words.get(i + 1)
198 {
199 stats.peak_states = parse_or_warn(v, "peak_states");
200 }
201 }
202 }
203 if !found_time && line.contains("verification time") {
204 found_time = true;
205 for word in line.split_whitespace() {
206 if let Ok(n) = word.parse::<u64>() {
207 stats.time_usec = Some(n);
208 break;
209 }
210 }
211 }
212 if !found_stack && line.contains("stack depth") {
213 found_stack = true;
214 if let Some(pos) = line.find("stack depth") {
215 let after = &line[pos + "stack depth".len()..];
216 let depth_str = after.trim();
217 if !depth_str.is_empty() {
218 stats.stack_depth = Some(depth_str.to_string());
219 }
220 }
221 }
222 if found_insns && found_time && found_stack {
223 break;
224 }
225 }
226
227 stats
228}
229
230pub fn normalize_verifier_line(line: &str) -> &str {
243 let trimmed = line.trim();
244 if trimmed.is_empty() || !trimmed.as_bytes()[0].is_ascii_digit() {
245 return trimmed;
246 }
247 if let Some(colon) = trimmed.find(": ") {
250 let after = &trimmed[colon + 2..];
251 if after.starts_with("frame")
252 || (after.starts_with('R')
253 && after.as_bytes().get(1).is_some_and(|b| b.is_ascii_digit()))
254 {
255 return &trimmed[..colon + 1];
256 }
257 }
258 if let Some(pos) = trimmed.find("; frame") {
260 return trimmed[..pos].trim_end();
261 }
262 if let Some(pos) = trimmed.find("; R")
264 && trimmed
265 .as_bytes()
266 .get(pos + 3)
267 .is_some_and(|b| b.is_ascii_digit())
268 {
269 return trimmed[..pos].trim_end();
270 }
271 if let Some(goto_pos) = trimmed.find("goto pc") {
273 let after_goto = &trimmed[goto_pos + 7..];
274 let end = after_goto
275 .find(|c: char| c != '+' && c != '-' && !c.is_ascii_digit())
276 .unwrap_or(after_goto.len());
277 let insn_end = goto_pos + 7 + end;
278 if insn_end < trimmed.len() {
279 return trimmed[..insn_end].trim_end();
280 }
281 }
282 trimmed
283}
284
285fn normalize_for_cycle_detection(line: &str) -> &str {
290 let n = normalize_verifier_line(line);
291 if let Some(colon) = n.find(": ") {
293 let before = &n[..colon];
294 if !before.is_empty() && before.bytes().all(|b| b.is_ascii_digit()) {
295 return &n[colon + 2..];
296 }
297 }
298 n
299}
300
301pub fn detect_cycle(lines: &[&str]) -> Option<(usize, usize, usize)> {
306 const MIN_PERIOD: usize = 5;
307 const MIN_REPS: usize = 3;
308
309 if lines.len() < MIN_PERIOD * MIN_REPS {
310 return None;
311 }
312
313 let anchor_norms: Vec<&str> = lines.iter().map(|l| normalize_verifier_line(l)).collect();
320 let block_norms: Vec<&str> = lines
321 .iter()
322 .map(|l| normalize_for_cycle_detection(l))
323 .collect();
324
325 let mut sorted_norms: Vec<&str> = anchor_norms
327 .iter()
328 .filter(|l| l.len() >= 10)
329 .copied()
330 .collect();
331 sorted_norms.sort_unstable();
332
333 let mut best_anchor: Option<(&str, usize)> = None;
334 let mut i = 0;
335 while i < sorted_norms.len() {
336 let mut j = i + 1;
337 while j < sorted_norms.len() && sorted_norms[j] == sorted_norms[i] {
338 j += 1;
339 }
340 let count = j - i;
341 if count >= MIN_REPS && best_anchor.is_none_or(|(_, best)| count > best) {
342 best_anchor = Some((sorted_norms[i], count));
343 }
344 i = j;
345 }
346
347 let (anchor, use_block_norms_for_positions) = match best_anchor {
350 Some((a, _)) => (a, false),
351 None => {
352 let mut sorted_block: Vec<&str> = block_norms
353 .iter()
354 .filter(|l| l.len() >= 10)
355 .copied()
356 .collect();
357 sorted_block.sort_unstable();
358 let mut ba: Option<(&str, usize)> = None;
359 let mut bi = 0;
360 while bi < sorted_block.len() {
361 let mut bj = bi + 1;
362 while bj < sorted_block.len() && sorted_block[bj] == sorted_block[bi] {
363 bj += 1;
364 }
365 let c = bj - bi;
366 if c >= MIN_REPS && ba.is_none_or(|(_, best)| c > best) {
367 ba = Some((sorted_block[bi], c));
368 }
369 bi = bj;
370 }
371 match ba {
372 Some((a, _)) => (a, true),
373 None => return None,
374 }
375 }
376 };
377
378 let norms_for_pos = if use_block_norms_for_positions {
379 &block_norms
380 } else {
381 &anchor_norms
382 };
383 let positions: Vec<usize> = norms_for_pos
384 .iter()
385 .enumerate()
386 .filter(|(_, l)| **l == anchor)
387 .map(|(i, _)| i)
388 .collect();
389
390 for stride in 1..=3usize {
392 if positions.len() <= stride {
393 continue;
394 }
395
396 let mut gaps: Vec<usize> = positions
397 .windows(stride + 1)
398 .map(|w| w[stride] - w[0])
399 .filter(|g| *g >= MIN_PERIOD)
400 .collect();
401 gaps.sort_unstable();
402
403 let mut best_period = 0;
404 let mut best_gap_count = 0;
405 let mut gi = 0;
406 while gi < gaps.len() {
407 let mut gj = gi + 1;
408 while gj < gaps.len() && gaps[gj] == gaps[gi] {
409 gj += 1;
410 }
411 let count = gj - gi;
412 if count > best_gap_count {
413 best_gap_count = count;
414 best_period = gaps[gi];
415 }
416 gi = gj;
417 }
418 if best_period == 0 || best_gap_count < MIN_REPS - 1 {
419 continue;
420 }
421 let period = best_period;
422
423 for &pos in &positions {
424 if pos + 2 * period > lines.len() {
425 break;
426 }
427 if block_norms[pos..pos + period] == block_norms[pos + period..pos + 2 * period] {
428 let first_block = &block_norms[pos..pos + period];
429 let mut count = 1;
430 while pos + (count + 1) * period <= lines.len() {
431 if block_norms[pos + count * period..pos + (count + 1) * period] != *first_block
432 {
433 break;
434 }
435 count += 1;
436 }
437 let mut best_start = pos;
439 let mut best_count = count;
440 for offset in 1..period {
441 let Some(cand) = pos.checked_sub(offset) else {
442 break;
443 };
444 if cand + 2 * period > lines.len() {
445 continue;
446 }
447 if block_norms[cand..cand + period]
448 != block_norms[cand + period..cand + 2 * period]
449 {
450 continue;
451 }
452 let mut c = 2;
453 while cand + (c + 1) * period <= lines.len()
454 && block_norms[cand + c * period..cand + (c + 1) * period]
455 == block_norms[cand..cand + period]
456 {
457 c += 1;
458 }
459 if c > best_count {
460 best_start = cand;
461 best_count = c;
462 }
463 }
464 if best_count >= MIN_REPS {
465 return Some((best_start, period, best_count));
466 }
467 }
468 }
469 }
470
471 None
472}
473
474pub fn collapse_cycles(log: &str) -> String {
484 const MAX_PASSES: usize = 5;
485 let mut text = log.to_string();
486
487 for _ in 0..MAX_PASSES {
488 let lines: Vec<&str> = text.lines().collect();
489 let (start, period, count) = match detect_cycle(&lines) {
490 Some(c) => c,
491 None => break,
492 };
493
494 let mut out = String::new();
495 for line in &lines[..start] {
496 out.push_str(line);
497 out.push('\n');
498 }
499 out.push_str(&format!(
500 "--- {}x of the following {} lines ---\n",
501 count, period
502 ));
503 for line in &lines[start..start + period] {
504 out.push_str(line);
505 out.push('\n');
506 }
507 out.push_str(&format!(
508 "--- {} identical iterations omitted ---\n",
509 count - 2
510 ));
511 let last_start = start + (count - 1) * period;
512 for line in &lines[last_start..last_start + period] {
513 out.push_str(line);
514 out.push('\n');
515 }
516 out.push_str("--- end repeat ---\n");
517 let suffix_start = start + count * period;
518 for line in &lines[suffix_start..] {
519 out.push_str(line);
520 out.push('\n');
521 }
522 text = out;
523 }
524
525 text
526}
527
528pub fn build_diff_rows(stats_a: &[ProgStats], b_map: &HashMap<String, u64>) -> Vec<DiffRow> {
530 let mut rows = Vec::new();
531 for ps in stats_a {
532 let a = ps.verified_insns as u64;
533 let b = b_map.get(&ps.name).copied().unwrap_or(0);
534 rows.push(DiffRow {
535 name: ps.name.clone(),
536 a,
537 b,
538 delta: a as i64 - b as i64,
539 });
540 }
541 rows
542}
543
544pub fn build_b_map(stats_b: &[ProgStats]) -> HashMap<String, u64> {
546 stats_b
547 .iter()
548 .map(|ps| (ps.name.clone(), ps.verified_insns as u64))
549 .collect()
550}
551
552#[derive(Debug, Clone, PartialEq, Eq)]
583pub enum AttachOutcome {
584 Attached,
588 Died,
591 NotAttached(String),
595 Unconfirmed,
601}
602
603impl AttachOutcome {
604 pub fn failure_reason(&self) -> Option<String> {
606 match self {
607 AttachOutcome::Attached => None,
608 AttachOutcome::Died => {
609 Some("scheduler process exited during BPF load/startup".to_string())
610 }
611 AttachOutcome::NotAttached(reason) if reason.is_empty() => {
612 Some("scheduler never reached sched_ext 'enabled'".to_string())
613 }
614 AttachOutcome::NotAttached(reason) => Some(format!(
615 "scheduler never reached sched_ext 'enabled': {reason}"
616 )),
617 AttachOutcome::Unconfirmed => Some(
618 "scheduler attach unconfirmed — guest never reached the dispatch phase \
619 (no PayloadStarting frame; possible early guest kernel panic)"
620 .to_string(),
621 ),
622 }
623 }
624}
625
626pub(crate) fn attach_outcome_from_messages(
643 guest_messages: Option<&crate::vmm::host_comms::BulkDrainResult>,
644) -> AttachOutcome {
645 let Some(drain) = guest_messages else {
646 return AttachOutcome::Unconfirmed;
647 };
648 let mut not_attached: Option<String> = None;
649 let mut payload_starting = false;
650 for e in &drain.entries {
651 if e.msg_type != crate::vmm::wire::MSG_TYPE_LIFECYCLE || !e.crc_ok || e.payload.is_empty() {
652 continue;
653 }
654 match crate::vmm::wire::LifecyclePhase::from_wire(e.payload[0]) {
655 Some(crate::vmm::wire::LifecyclePhase::SchedulerDied) => return AttachOutcome::Died,
656 Some(crate::vmm::wire::LifecyclePhase::SchedulerNotAttached) => {
657 not_attached = Some(String::from_utf8_lossy(&e.payload[1..]).into_owned());
658 }
659 Some(crate::vmm::wire::LifecyclePhase::PayloadStarting) => {
660 payload_starting = true;
661 }
662 _ => {}
663 }
664 }
665 if let Some(reason) = not_attached {
666 AttachOutcome::NotAttached(reason)
667 } else if payload_starting {
668 AttachOutcome::Attached
669 } else {
670 AttachOutcome::Unconfirmed
671 }
672}
673
674fn dispatch_confirmed_from_messages(
685 guest_messages: Option<&crate::vmm::host_comms::BulkDrainResult>,
686) -> bool {
687 let Some(drain) = guest_messages else {
688 return false;
689 };
690 drain.entries.iter().any(|e| {
691 e.msg_type == crate::vmm::wire::MSG_TYPE_LIFECYCLE
692 && e.crc_ok
693 && !e.payload.is_empty()
694 && crate::vmm::wire::LifecyclePhase::from_wire(e.payload[0])
695 == Some(crate::vmm::wire::LifecyclePhase::WorkloadDispatched)
696 })
697}
698
699pub struct VerifierVmResult {
701 pub stats: Vec<ProgStats>,
704 pub scheduler_log: String,
707 pub attach: AttachOutcome,
716 pub dispatched: bool,
732 pub timed_out: bool,
743}
744
745impl VerifierVmResult {
746 pub fn cell_verdict(&self) -> Result<(), String> {
756 if self.timed_out {
762 return Err("VM timed out (hung after attach, before exit)".to_string());
763 }
764 if let Some(reason) = self.attach.failure_reason() {
767 return Err(format!("scheduler did not turn on — {reason}"));
768 }
769 if !self.dispatched {
775 return Err(
776 "scheduler attached but did not dispatch the injected workload (0 iterations)"
777 .to_string(),
778 );
779 }
780 Ok(())
781 }
782}
783
784pub fn collect_verifier_output(
801 sched_bin: &std::path::Path,
802 ktstr_bin: &std::path::Path,
803 kernel: &std::path::Path,
804 extra_sched_args: &[String],
805 topology: crate::test_support::TopologyJson,
806) -> anyhow::Result<VerifierVmResult> {
807 use anyhow::Context;
808
809 let validated: crate::vmm::topology::Topology = topology
813 .try_into()
814 .map_err(|e: String| anyhow::anyhow!("invalid topology {topology:?}: {e}"))?;
815
816 let sched_args: Vec<String> = extra_sched_args.to_vec();
817
818 let vm = crate::vmm::KtstrVm::builder()
841 .kernel(kernel)
842 .init_binary(ktstr_bin)
843 .scheduler_binary(sched_bin)
844 .sched_args(&sched_args)
845 .run_args(&[crate::test_support::VERIFIER_WORKLOAD_FLAG.to_string()])
850 .topology(validated)
851 .memory_mib(2048)
852 .timeout(std::time::Duration::from_secs(120))
853 .no_perf_mode(true)
854 .build()
855 .context("build verifier VM")?;
856
857 let result = vm.run().context("run verifier VM")?;
858
859 let merged = concat_sched_log_chunks(result.guest_messages.as_ref());
867 let scheduler_log = if !merged.is_empty() {
868 parse_sched_output(&merged).unwrap_or("").to_string()
869 } else {
870 parse_sched_output(&result.output).unwrap_or("").to_string()
871 };
872
873 let stats: Vec<ProgStats> = result
877 .verifier_stats
878 .iter()
879 .map(|pvs| ProgStats {
880 name: pvs.name.clone(),
881 verified_insns: pvs.verified_insns,
882 })
883 .collect();
884
885 let attach = attach_outcome_from_messages(result.guest_messages.as_ref());
886 let dispatched = dispatch_confirmed_from_messages(result.guest_messages.as_ref());
887
888 Ok(VerifierVmResult {
889 stats,
890 scheduler_log,
891 attach,
892 dispatched,
893 timed_out: result.timed_out,
894 })
895}
896
897pub fn extract_verifier_log(scheduler_log: &str) -> Option<&str> {
907 const BEGIN: &str = "-- BEGIN PROG LOAD LOG --";
908 const END: &str = "-- END PROG LOAD LOG --";
909
910 let begin_pos = scheduler_log.find(BEGIN)?;
911 let content_start = begin_pos + BEGIN.len();
912 let content_start = if scheduler_log.as_bytes().get(content_start) == Some(&b'\n') {
914 content_start + 1
915 } else {
916 content_start
917 };
918 let end_pos = scheduler_log[content_start..].find(END)?;
919 let content = &scheduler_log[content_start..content_start + end_pos];
920 let content = content
923 .rfind('\n')
924 .map(|p| &content[..p])
925 .unwrap_or(content);
926 Some(content.trim_end_matches('\n'))
927}
928
929pub fn format_verifier_output(label: &str, result: &VerifierVmResult, raw: bool) -> String {
932 let mut out = String::new();
933 out.push_str(&format!("\n{label}\n"));
934 if result.timed_out {
935 out.push_str(" scheduler: UNKNOWN — VM timed out before exit\n");
936 } else {
937 match result.attach.failure_reason() {
938 None => {
939 out.push_str(" scheduler: attached (sched_ext enabled)\n");
940 if result.dispatched {
941 out.push_str(" dispatch: confirmed (injected workload ran)\n");
942 } else {
943 out.push_str(
944 " dispatch: NOT CONFIRMED — attached but injected workload made no progress\n",
945 );
946 }
947 }
948 Some(reason) => out.push_str(&format!(" scheduler: NOT ATTACHED — {reason}\n")),
949 }
950 }
951 for ps in &result.stats {
952 out.push_str(&format!(
953 " {:<40} verified_insns={}\n",
954 ps.name, ps.verified_insns
955 ));
956 }
957
958 if !result.scheduler_log.is_empty() {
959 let verifier_log =
962 extract_verifier_log(&result.scheduler_log).unwrap_or(&result.scheduler_log);
963
964 let vs = parse_verifier_stats(verifier_log);
965 if vs.processed_insns > 0 {
966 out.push_str(&format!("\n{label} --- verifier stats ---\n"));
967 out.push_str(&format!(
968 " processed={} states={}/{}",
969 vs.processed_insns, vs.peak_states, vs.total_states
970 ));
971 if let Some(t) = vs.time_usec {
972 out.push_str(&format!(" time={t}us"));
973 }
974 if let Some(ref s) = vs.stack_depth {
975 out.push_str(&format!(" stack={s}"));
976 }
977 out.push('\n');
978 }
979
980 out.push_str(&format!("\n{label} --- scheduler log ---\n"));
981 if raw {
982 out.push_str(&result.scheduler_log);
983 } else {
984 out.push_str(&collapse_cycles(verifier_log));
985 }
986 }
987
988 out
989}
990
991pub fn format_verifier_diff(
993 label_a: &str,
994 stats_a: &[ProgStats],
995 label_b: &str,
996 stats_b: &[ProgStats],
997) -> String {
998 let b_map = build_b_map(stats_b);
999 let diff_rows = build_diff_rows(stats_a, &b_map);
1000
1001 let mut out = String::new();
1002 out.push_str(&format!("\ndelta A/B diff: {label_a} vs {label_b}\n"));
1003 let mut table = crate::cli::new_table();
1004 table.set_header(vec!["program", "A", "B", "delta"]);
1005 for row in &diff_rows {
1006 table.add_row(vec![
1007 row.name.clone(),
1008 row.a.to_string(),
1009 row.b.to_string(),
1010 format!("{:+}", row.delta),
1011 ]);
1012 }
1013 out.push_str(&table.to_string());
1014 out.push('\n');
1015 out
1016}
1017
1018#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1031pub struct VerifierCellRecord {
1032 pub scheduler: String,
1034 pub kernel: String,
1036 pub topology: String,
1038 pub passed: bool,
1040 pub stats: Vec<ProgStats>,
1048}
1049
1050fn cell_record_filename(full_name: &str) -> String {
1057 let mut s: String = full_name
1058 .chars()
1059 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
1060 .collect();
1061 s.push_str(".json");
1062 s
1063}
1064
1065pub(crate) fn write_cell_record(
1077 dir: &std::path::Path,
1078 full_name: &str,
1079 passed: bool,
1080 stats: &[ProgStats],
1081) {
1082 let Some(rest) = full_name.strip_prefix("verifier/") else {
1083 return;
1084 };
1085 let parts: Vec<&str> = rest.splitn(3, '/').collect();
1086 if parts.len() != 3 {
1087 return;
1088 }
1089 let record = VerifierCellRecord {
1090 scheduler: parts[0].to_string(),
1091 kernel: parts[1].to_string(),
1092 topology: parts[2].to_string(),
1093 passed,
1094 stats: stats.to_vec(),
1095 };
1096 let path = dir.join(cell_record_filename(full_name));
1097 match serde_json::to_vec(&record) {
1098 Ok(bytes) => {
1099 if let Err(e) = std::fs::write(&path, bytes) {
1100 eprintln!(
1101 "ktstr verifier: warning: could not write result record {}: {e}",
1102 path.display(),
1103 );
1104 }
1105 }
1106 Err(e) => eprintln!("ktstr verifier: warning: serialize result record: {e}"),
1107 }
1108}
1109
1110pub fn read_cell_records(dir: &std::path::Path) -> Vec<VerifierCellRecord> {
1115 let Ok(entries) = std::fs::read_dir(dir) else {
1116 return Vec::new();
1117 };
1118 entries
1119 .flatten()
1120 .filter(|e| {
1121 e.path()
1122 .extension()
1123 .is_some_and(|x| x.eq_ignore_ascii_case("json"))
1124 })
1125 .filter_map(|e| std::fs::read(e.path()).ok())
1126 .filter_map(|bytes| serde_json::from_slice::<VerifierCellRecord>(&bytes).ok())
1127 .collect()
1128}
1129
1130pub fn classify_run_outcome(
1148 success: bool,
1149 records_empty: bool,
1150 scheduler: Option<&str>,
1151 exit_code: Option<i32>,
1152) -> Result<(), String> {
1153 if !success {
1154 let code = exit_code.map_or_else(|| "signal".to_string(), |c| c.to_string());
1155 return Err(format!("cargo nextest run exited with {code}"));
1156 }
1157 if records_empty {
1158 return Err(match scheduler {
1159 Some(name) => format!(
1160 "--scheduler {name:?}: matched no verifier cell — no declared BPF \
1161 scheduler by that name, or no topology preset fits this host for \
1162 it. Run `cargo ktstr verifier` with no --scheduler to see the \
1163 swept set."
1164 ),
1165 None => "no verifier cells ran — no scheduler is declared via \
1166 declare_scheduler! in a linked test binary, or every declared \
1167 scheduler's constraints rejected all topology presets on this \
1168 host."
1169 .to_string(),
1170 });
1171 }
1172 Ok(())
1173}
1174
1175pub fn build_nextest_args(nextest_profile: Option<&str>, forward: &[String]) -> Vec<String> {
1190 let mut args = vec![
1191 "nextest".to_string(),
1192 "run".to_string(),
1193 "--run-ignored".to_string(),
1194 "all".to_string(),
1195 "--no-tests".to_string(),
1196 "pass".to_string(),
1197 "-E".to_string(),
1198 "test(/^verifier/) & !test(/^verifier::/)".to_string(),
1199 ];
1200 if let Some(np) = nextest_profile {
1201 args.push("--profile".to_string());
1202 args.push(np.to_string());
1203 }
1204 args.extend(forward.iter().cloned());
1205 args
1206}
1207
1208pub fn render_result_table(records: &[VerifierCellRecord]) -> Option<String> {
1226 if records.is_empty() {
1227 return None;
1228 }
1229 use std::collections::{BTreeMap, BTreeSet};
1230 let mut schedulers: BTreeSet<String> = BTreeSet::new(); let mut rows: BTreeSet<String> = BTreeSet::new(); let mut agg: BTreeMap<(String, String), (u32, u32)> = BTreeMap::new();
1234 let mut failing: BTreeSet<(String, String, String)> = BTreeSet::new();
1236 for r in records {
1237 schedulers.insert(r.scheduler.clone());
1238 rows.insert(r.topology.clone());
1239 let counts = agg
1240 .entry((r.topology.clone(), r.scheduler.clone()))
1241 .or_insert((0, 0));
1242 if r.passed {
1243 counts.0 += 1;
1244 } else {
1245 counts.1 += 1;
1246 failing.insert((r.scheduler.clone(), r.kernel.clone(), r.topology.clone()));
1247 }
1248 }
1249
1250 let (mut n_pass, mut n_fail, mut n_mixed) = (0usize, 0usize, 0usize);
1251 let mut table = crate::cli::new_table();
1252 let mut header: Vec<String> = vec!["topology".to_string()];
1253 for sched in &schedulers {
1254 header.push(sched.clone());
1255 }
1256 table.set_header(header);
1257 for topo in &rows {
1258 let mut line: Vec<String> = vec![topo.clone()];
1259 for sched in &schedulers {
1260 let text = match agg.get(&(topo.clone(), sched.clone())) {
1263 None => "-",
1264 Some((_, 0)) => {
1265 n_pass += 1;
1266 "✅"
1267 }
1268 Some((0, _)) => {
1269 n_fail += 1;
1270 "❌"
1271 }
1272 Some(_) => {
1273 n_mixed += 1;
1274 "🇽"
1275 }
1276 };
1277 line.push(text.to_string());
1278 }
1279 table.add_row(line);
1280 }
1281
1282 let mut out = format!("\nverifier summary: {n_pass} ✅ {n_fail} ❌ {n_mixed} 🇽\n{table}\n");
1283 if !failing.is_empty() {
1284 out.push_str("\nfailing combinations (scheduler / kernel / topology):\n");
1285 for (sched, kernel, topo) in &failing {
1286 out.push_str(&format!(" {sched} / {kernel} / {topo}\n"));
1287 }
1288 }
1289 Some(out)
1290}
1291
1292pub fn render_instruction_count_tables(records: &[VerifierCellRecord]) -> Option<String> {
1315 use std::collections::{BTreeMap, BTreeSet};
1316 type VerifiedInsnSpans = BTreeMap<String, BTreeMap<String, BTreeMap<String, (u32, u32)>>>;
1321 let mut by_sched: VerifiedInsnSpans = BTreeMap::new();
1322 let mut sched_progs: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1324 for r in records {
1325 for s in &r.stats {
1326 let span = by_sched
1327 .entry(r.scheduler.clone())
1328 .or_default()
1329 .entry(r.kernel.clone())
1330 .or_default()
1331 .entry(s.name.clone())
1332 .or_insert((s.verified_insns, s.verified_insns));
1333 span.0 = span.0.min(s.verified_insns);
1334 span.1 = span.1.max(s.verified_insns);
1335 sched_progs
1336 .entry(r.scheduler.clone())
1337 .or_default()
1338 .insert(s.name.clone());
1339 }
1340 }
1341 if by_sched.is_empty() {
1342 return None;
1343 }
1344
1345 let mut out = String::from(
1346 "\nverifier verified_insns (per scheduler; rows: kernel, cols: BPF program, \
1347 cell: range across topologies):\n",
1348 );
1349 for (sched, kernels) in &by_sched {
1350 let progs = &sched_progs[sched];
1351 let mut table = crate::cli::new_table();
1352 let mut header: Vec<String> = vec!["kernel".to_string()];
1353 for p in progs {
1354 header.push(p.clone());
1355 }
1356 table.set_header(header);
1357 for (kernel, prog_map) in kernels {
1358 let mut line: Vec<String> = vec![kernel.clone()];
1359 for p in progs {
1360 let text = match prog_map.get(p) {
1361 Some((lo, hi)) if lo == hi => lo.to_string(),
1362 Some((lo, hi)) => format!("{lo}..{hi}"),
1363 None => "-".to_string(),
1364 };
1365 line.push(text);
1366 }
1367 table.add_row(line);
1368 }
1369 out.push_str(&format!("\n{sched}:\n{table}\n"));
1370 }
1371 Some(out)
1372}
1373
1374#[cfg(test)]
1375mod tests {
1376 use super::*;
1377
1378 #[test]
1387 fn cell_record_write_read_roundtrip_and_retry_overwrites() {
1388 let dir = std::env::temp_dir().join(format!("ktstr-verif-rec-{}", std::process::id()));
1389 std::fs::create_dir_all(&dir).expect("mk temp dir");
1390 write_cell_record(&dir, "not_a_cell", true, &[]);
1393 write_cell_record(&dir, "verifier/only/two", true, &[]);
1394 let name = "verifier/scx_a/kernel_6_14/tiny-1llc";
1396 write_cell_record(&dir, name, false, &[]);
1397 let stats = [
1400 ProgStats {
1401 name: "ktstr_dispatch".into(),
1402 verified_insns: 321,
1403 },
1404 ProgStats {
1405 name: "ktstr_enqueue".into(),
1406 verified_insns: 123,
1407 },
1408 ];
1409 write_cell_record(&dir, name, true, &stats);
1410 let recs = read_cell_records(&dir);
1411 assert_eq!(
1412 recs.len(),
1413 1,
1414 "malformed names skipped; the retry overwrote its own record (one file): {recs:?}",
1415 );
1416 assert_eq!(recs[0].scheduler, "scx_a");
1417 assert_eq!(recs[0].kernel, "kernel_6_14");
1418 assert_eq!(recs[0].topology, "tiny-1llc");
1419 assert!(
1420 recs[0].passed,
1421 "final retry outcome (PASS) wins over the earlier FAIL"
1422 );
1423 assert_eq!(recs[0].stats, stats, "stats roundtrip via serde");
1426 std::fs::remove_dir_all(&dir).ok();
1427 }
1428
1429 #[test]
1434 fn render_result_table_matrix_tally_and_empty() {
1435 let recs = vec![
1436 VerifierCellRecord {
1437 scheduler: "scx_a".into(),
1438 kernel: "kernel_6_14".into(),
1439 topology: "tiny-1llc".into(),
1440 passed: true,
1441 stats: vec![],
1442 },
1443 VerifierCellRecord {
1444 scheduler: "scx_a".into(),
1445 kernel: "kernel_6_14".into(),
1446 topology: "large-4llc".into(),
1447 passed: false,
1448 stats: vec![],
1449 },
1450 ];
1451 let out = render_result_table(&recs).expect("non-empty records -> Some");
1452 assert!(
1453 out.contains("verifier summary: 1 ✅ 1 ❌ 0 🇽"),
1454 "tally: {out}"
1455 );
1456 assert!(
1459 out.contains("scx_a") && !out.contains(" @ "),
1460 "columns: {out}"
1461 );
1462 let pass_row = out
1466 .lines()
1467 .find(|l| l.contains("tiny-1llc"))
1468 .expect("tiny-1llc row present");
1469 assert!(
1470 pass_row.contains('✅'),
1471 "all-pass cell renders ✅ in the grid row: {pass_row}"
1472 );
1473 let fail_row = out
1474 .lines()
1475 .find(|l| l.contains("large-4llc"))
1476 .expect("large-4llc row present");
1477 assert!(
1478 fail_row.contains('❌'),
1479 "all-fail cell renders ❌ in the grid row: {fail_row}"
1480 );
1481 assert!(
1483 out.contains("failing combinations (scheduler / kernel / topology):")
1484 && out.contains("scx_a / kernel_6_14 / large-4llc"),
1485 "failing combinations listed: {out}"
1486 );
1487 assert!(render_result_table(&[]).is_none(), "empty -> None");
1488 }
1489
1490 #[test]
1494 fn render_result_table_mixed_kernels_blue_x() {
1495 let recs = vec![
1496 VerifierCellRecord {
1498 scheduler: "scx_a".into(),
1499 kernel: "kernel_6_14".into(),
1500 topology: "tiny-1llc".into(),
1501 passed: true,
1502 stats: vec![],
1503 },
1504 VerifierCellRecord {
1505 scheduler: "scx_a".into(),
1506 kernel: "kernel_6_15".into(),
1507 topology: "tiny-1llc".into(),
1508 passed: false,
1509 stats: vec![],
1510 },
1511 VerifierCellRecord {
1513 scheduler: "scx_a".into(),
1514 kernel: "kernel_6_14".into(),
1515 topology: "smt-2llc".into(),
1516 passed: true,
1517 stats: vec![],
1518 },
1519 VerifierCellRecord {
1520 scheduler: "scx_a".into(),
1521 kernel: "kernel_6_15".into(),
1522 topology: "smt-2llc".into(),
1523 passed: true,
1524 stats: vec![],
1525 },
1526 ];
1527 let out = render_result_table(&recs).expect("Some");
1528 assert!(
1529 out.contains("verifier summary: 1 ✅ 0 ❌ 1 🇽"),
1530 "tally counts one all-pass + one mixed cell: {out}"
1531 );
1532 let mixed_row = out
1533 .lines()
1534 .find(|l| l.contains("tiny-1llc"))
1535 .expect("tiny-1llc row present");
1536 assert!(
1537 mixed_row.contains('🇽'),
1538 "mixed (some pass, some fail) cell renders 🇽: {mixed_row}"
1539 );
1540 let pass_row = out
1541 .lines()
1542 .find(|l| l.contains("smt-2llc"))
1543 .expect("smt-2llc row present");
1544 assert!(
1545 pass_row.contains('✅'),
1546 "all-kernels-pass cell renders ✅: {pass_row}"
1547 );
1548 assert!(
1550 out.contains("scx_a / kernel_6_15 / tiny-1llc"),
1551 "the failing kernel is listed: {out}"
1552 );
1553 assert!(
1554 !out.contains("kernel_6_14 / tiny-1llc"),
1555 "the passing kernel on the mixed topology is not listed: {out}"
1556 );
1557 assert!(
1558 !out.contains("/ smt-2llc"),
1559 "the all-pass topology contributes no failing combination: {out}"
1560 );
1561 }
1562
1563 #[test]
1570 fn instruction_count_tables_per_scheduler_kernel_program_range() {
1571 let recs = vec![
1572 VerifierCellRecord {
1575 scheduler: "scx_a".into(),
1576 kernel: "kernel_6_14".into(),
1577 topology: "tiny".into(),
1578 passed: true,
1579 stats: vec![
1580 ProgStats {
1581 name: "ktstr_dispatch".into(),
1582 verified_insns: 128,
1583 },
1584 ProgStats {
1585 name: "ktstr_enqueue".into(),
1586 verified_insns: 64,
1587 },
1588 ],
1589 },
1590 VerifierCellRecord {
1591 scheduler: "scx_a".into(),
1592 kernel: "kernel_6_14".into(),
1593 topology: "large".into(),
1594 passed: true,
1595 stats: vec![
1596 ProgStats {
1597 name: "ktstr_dispatch".into(),
1598 verified_insns: 128,
1599 },
1600 ProgStats {
1601 name: "ktstr_enqueue".into(),
1602 verified_insns: 64,
1603 },
1604 ],
1605 },
1606 VerifierCellRecord {
1610 scheduler: "scx_a".into(),
1611 kernel: "kernel_6_15".into(),
1612 topology: "tiny".into(),
1613 passed: true,
1614 stats: vec![ProgStats {
1615 name: "ktstr_dispatch".into(),
1616 verified_insns: 130,
1617 }],
1618 },
1619 VerifierCellRecord {
1620 scheduler: "scx_a".into(),
1621 kernel: "kernel_6_15".into(),
1622 topology: "large".into(),
1623 passed: true,
1624 stats: vec![ProgStats {
1625 name: "ktstr_dispatch".into(),
1626 verified_insns: 150,
1627 }],
1628 },
1629 VerifierCellRecord {
1631 scheduler: "scx_b".into(),
1632 kernel: "kernel_6_14".into(),
1633 topology: "tiny".into(),
1634 passed: true,
1635 stats: vec![ProgStats {
1636 name: "ktstr_dispatch".into(),
1637 verified_insns: 200,
1638 }],
1639 },
1640 ];
1641 let out = render_instruction_count_tables(&recs).expect("stats present -> Some");
1642 assert!(
1644 out.contains("scx_a:") && out.contains("scx_b:"),
1645 "one section per declared scheduler: {out}"
1646 );
1647 assert!(
1649 out.contains("ktstr_dispatch") && out.contains("ktstr_enqueue"),
1650 "BPF-program columns: {out}"
1651 );
1652 assert!(
1653 out.contains("kernel_6_14") && out.contains("kernel_6_15"),
1654 "kernel-version rows: {out}"
1655 );
1656 assert!(
1659 out.contains("128"),
1660 "topology-flat cell is a single number: {out}"
1661 );
1662 assert!(
1663 out.contains("130..150"),
1664 "topology-varying cell is a lo..hi range: {out}"
1665 );
1666 assert!(
1667 out.contains("64") && out.contains("200"),
1668 "other counts render: {out}"
1669 );
1670 assert!(
1672 out.contains('-'),
1673 "a (kernel, program) with no stats renders '-': {out}"
1674 );
1675 assert!(
1678 !out.contains("tiny") && !out.contains("large"),
1679 "topology is not a table axis: {out}"
1680 );
1681
1682 let bare = vec![VerifierCellRecord {
1684 scheduler: "scx_a".into(),
1685 kernel: "kernel_6_14".into(),
1686 topology: "tiny".into(),
1687 passed: false,
1688 stats: vec![],
1689 }];
1690 assert!(
1691 render_instruction_count_tables(&bare).is_none(),
1692 "no stats -> None"
1693 );
1694 }
1695
1696 #[test]
1701 fn classify_run_outcome_cases() {
1702 assert!(classify_run_outcome(true, false, None, Some(0)).is_ok());
1704 assert!(classify_run_outcome(true, false, Some("ktstr_sched"), Some(0)).is_ok());
1705
1706 let e = classify_run_outcome(true, true, Some("nope"), Some(0)).unwrap_err();
1711 assert!(
1712 e.contains("--scheduler \"nope\"") && e.contains("matched no verifier cell"),
1713 "scheduler-empty diagnostic: {e}"
1714 );
1715
1716 let e = classify_run_outcome(true, true, None, Some(0)).unwrap_err();
1719 assert!(
1720 e.contains("no verifier cells ran") && e.contains("declare_scheduler!"),
1721 "no-cells diagnostic: {e}"
1722 );
1723
1724 assert_eq!(
1727 classify_run_outcome(false, true, None, Some(4)).unwrap_err(),
1728 "cargo nextest run exited with 4"
1729 );
1730 assert_eq!(
1731 classify_run_outcome(false, false, Some("x"), None).unwrap_err(),
1732 "cargo nextest run exited with signal"
1733 );
1734 }
1735
1736 #[test]
1743 fn build_nextest_args_carries_load_bearing_flags() {
1744 let args = build_nextest_args(None, &[]);
1745 let ri = args
1746 .iter()
1747 .position(|a| a == "--run-ignored")
1748 .expect("--run-ignored present");
1749 assert_eq!(args[ri + 1], "all", "--run-ignored all");
1750 let nt = args
1751 .iter()
1752 .position(|a| a == "--no-tests")
1753 .expect("--no-tests present");
1754 assert_eq!(args[nt + 1], "pass", "--no-tests pass");
1755 assert!(
1756 args.iter()
1757 .any(|a| a == "test(/^verifier/) & !test(/^verifier::/)"),
1758 "verifier-cell filter present: {args:?}"
1759 );
1760
1761 let args = build_nextest_args(Some("ci"), &["--features".to_string(), "wprof".to_string()]);
1764 let p = args
1765 .iter()
1766 .position(|a| a == "--profile")
1767 .expect("--profile present");
1768 assert_eq!(args[p + 1], "ci");
1769 let f = args
1770 .iter()
1771 .position(|a| a == "--features")
1772 .expect("forwarded --features present");
1773 assert!(p < f, "profile emitted before forwarded args: {args:?}");
1774 }
1775
1776 #[test]
1787 fn attach_outcome_from_lifecycle_frames() {
1788 use crate::vmm::host_comms::BulkDrainResult;
1789 use crate::vmm::wire::{LifecyclePhase, MSG_TYPE_LIFECYCLE, ShmEntry};
1790
1791 let frame = |phase: LifecyclePhase, reason: &str| -> ShmEntry {
1792 let mut payload = vec![phase.wire_value()];
1793 payload.extend_from_slice(reason.as_bytes());
1794 ShmEntry {
1795 msg_type: MSG_TYPE_LIFECYCLE,
1796 payload,
1797 crc_ok: true,
1798 }
1799 };
1800 let drain = |entries: Vec<ShmEntry>| BulkDrainResult { entries };
1801
1802 assert_eq!(
1805 attach_outcome_from_messages(None),
1806 AttachOutcome::Unconfirmed,
1807 );
1808
1809 let init_only = drain(vec![frame(LifecyclePhase::InitStarted, "")]);
1811 assert_eq!(
1812 attach_outcome_from_messages(Some(&init_only)),
1813 AttachOutcome::Unconfirmed,
1814 );
1815
1816 let progress = drain(vec![
1818 frame(LifecyclePhase::InitStarted, ""),
1819 frame(LifecyclePhase::PayloadStarting, ""),
1820 ]);
1821 assert_eq!(
1822 attach_outcome_from_messages(Some(&progress)),
1823 AttachOutcome::Attached,
1824 );
1825
1826 let not_attached = drain(vec![frame(LifecyclePhase::SchedulerNotAttached, "timeout")]);
1828 assert_eq!(
1829 attach_outcome_from_messages(Some(¬_attached)),
1830 AttachOutcome::NotAttached("timeout".to_string()),
1831 );
1832
1833 let fail_beats_positive = drain(vec![
1836 frame(LifecyclePhase::PayloadStarting, ""),
1837 frame(LifecyclePhase::SchedulerNotAttached, "sysfs absent"),
1838 ]);
1839 assert_eq!(
1840 attach_outcome_from_messages(Some(&fail_beats_positive)),
1841 AttachOutcome::NotAttached("sysfs absent".to_string()),
1842 );
1843
1844 for entries in [
1846 vec![
1847 frame(LifecyclePhase::SchedulerNotAttached, "timeout"),
1848 frame(LifecyclePhase::SchedulerDied, ""),
1849 ],
1850 vec![
1851 frame(LifecyclePhase::SchedulerDied, ""),
1852 frame(LifecyclePhase::SchedulerNotAttached, "timeout"),
1853 ],
1854 ] {
1855 let d = drain(entries);
1856 assert_eq!(attach_outcome_from_messages(Some(&d)), AttachOutcome::Died);
1857 }
1858
1859 let died_beats_positive = drain(vec![
1861 frame(LifecyclePhase::PayloadStarting, ""),
1862 frame(LifecyclePhase::SchedulerDied, ""),
1863 ]);
1864 assert_eq!(
1865 attach_outcome_from_messages(Some(&died_beats_positive)),
1866 AttachOutcome::Died,
1867 );
1868
1869 let corrupt_died = ShmEntry {
1875 msg_type: MSG_TYPE_LIFECYCLE,
1876 payload: vec![LifecyclePhase::SchedulerDied.wire_value()],
1877 crc_ok: false,
1878 };
1879 let empty = ShmEntry {
1880 msg_type: MSG_TYPE_LIFECYCLE,
1881 payload: Vec::new(),
1882 crc_ok: true,
1883 };
1884 let non_lifecycle_died = ShmEntry {
1885 msg_type: MSG_TYPE_LIFECYCLE + 1,
1886 payload: vec![LifecyclePhase::SchedulerDied.wire_value()],
1887 crc_ok: true,
1888 };
1889 let unknown_phase = ShmEntry {
1890 msg_type: MSG_TYPE_LIFECYCLE,
1891 payload: vec![250],
1892 crc_ok: true,
1893 };
1894 for skipped in [corrupt_died, empty, non_lifecycle_died, unknown_phase] {
1895 let d = drain(vec![skipped, frame(LifecyclePhase::PayloadStarting, "")]);
1896 assert_eq!(
1897 attach_outcome_from_messages(Some(&d)),
1898 AttachOutcome::Attached,
1899 "a skipped frame must not suppress a valid PayloadStarting",
1900 );
1901 }
1902 }
1903
1904 #[test]
1908 fn dispatch_confirmed_from_lifecycle_frames() {
1909 use crate::vmm::host_comms::BulkDrainResult;
1910 use crate::vmm::wire::{LifecyclePhase, MSG_TYPE_LIFECYCLE, ShmEntry};
1911
1912 let frame = |phase: LifecyclePhase| -> ShmEntry {
1913 ShmEntry {
1914 msg_type: MSG_TYPE_LIFECYCLE,
1915 payload: vec![phase.wire_value()],
1916 crc_ok: true,
1917 }
1918 };
1919 let drain = |entries: Vec<ShmEntry>| BulkDrainResult { entries };
1920
1921 assert!(!dispatch_confirmed_from_messages(None));
1923
1924 let attached_only = drain(vec![frame(LifecyclePhase::PayloadStarting)]);
1927 assert!(!dispatch_confirmed_from_messages(Some(&attached_only)));
1928
1929 let dispatched = drain(vec![
1931 frame(LifecyclePhase::PayloadStarting),
1932 frame(LifecyclePhase::WorkloadDispatched),
1933 ]);
1934 assert!(dispatch_confirmed_from_messages(Some(&dispatched)));
1935
1936 let corrupt = ShmEntry {
1939 msg_type: MSG_TYPE_LIFECYCLE,
1940 payload: vec![LifecyclePhase::WorkloadDispatched.wire_value()],
1941 crc_ok: false,
1942 };
1943 let empty = ShmEntry {
1944 msg_type: MSG_TYPE_LIFECYCLE,
1945 payload: Vec::new(),
1946 crc_ok: true,
1947 };
1948 let non_lifecycle = ShmEntry {
1949 msg_type: MSG_TYPE_LIFECYCLE + 1,
1950 payload: vec![LifecyclePhase::WorkloadDispatched.wire_value()],
1951 crc_ok: true,
1952 };
1953 for skipped in [corrupt, empty, non_lifecycle] {
1954 let d = drain(vec![skipped]);
1955 assert!(
1956 !dispatch_confirmed_from_messages(Some(&d)),
1957 "a corrupt/empty/non-LIFECYCLE frame must not confirm dispatch",
1958 );
1959 }
1960 }
1961
1962 #[test]
1965 fn cell_verdict_gate_order_and_messages() {
1966 let base = |attach: AttachOutcome, dispatched: bool, timed_out: bool| VerifierVmResult {
1967 stats: Vec::new(),
1968 scheduler_log: String::new(),
1969 attach,
1970 dispatched,
1971 timed_out,
1972 };
1973
1974 assert_eq!(
1976 base(AttachOutcome::Attached, true, false).cell_verdict(),
1977 Ok(()),
1978 );
1979
1980 let no_dispatch = base(AttachOutcome::Attached, false, false).cell_verdict();
1982 assert!(
1983 no_dispatch
1984 .as_ref()
1985 .unwrap_err()
1986 .contains("did not dispatch"),
1987 "dispatch gate must name the failure: {no_dispatch:?}",
1988 );
1989
1990 let attach_fail = base(AttachOutcome::Died, true, false).cell_verdict();
1993 assert!(
1994 attach_fail
1995 .as_ref()
1996 .unwrap_err()
1997 .contains("did not turn on"),
1998 "attach gate must win over dispatch: {attach_fail:?}",
1999 );
2000
2001 let hung = base(AttachOutcome::Attached, true, true).cell_verdict();
2003 assert!(
2004 hung.as_ref().unwrap_err().contains("timed out"),
2005 "timed_out must win: {hung:?}",
2006 );
2007
2008 let both = base(AttachOutcome::Died, false, false).cell_verdict();
2011 assert!(
2012 both.as_ref().unwrap_err().contains("did not turn on"),
2013 "attach failure reported before dispatch failure: {both:?}",
2014 );
2015 }
2016
2017 #[test]
2020 fn attach_outcome_failure_reason() {
2021 assert_eq!(AttachOutcome::Attached.failure_reason(), None);
2022 assert!(
2023 AttachOutcome::Died
2024 .failure_reason()
2025 .unwrap()
2026 .contains("exited during BPF load"),
2027 );
2028 assert!(
2029 AttachOutcome::NotAttached(String::new())
2030 .failure_reason()
2031 .unwrap()
2032 .contains("never reached sched_ext 'enabled'"),
2033 );
2034 assert_eq!(
2035 AttachOutcome::NotAttached("sysfs absent".to_string()).failure_reason(),
2036 Some("scheduler never reached sched_ext 'enabled': sysfs absent".to_string()),
2037 );
2038 assert!(
2039 AttachOutcome::Unconfirmed
2040 .failure_reason()
2041 .unwrap()
2042 .contains("attach unconfirmed"),
2043 );
2044 }
2045
2046 #[test]
2050 fn format_verifier_output_timed_out_shows_unknown() {
2051 let result = VerifierVmResult {
2052 stats: Vec::new(),
2053 scheduler_log: String::new(),
2054 attach: AttachOutcome::Attached,
2055 dispatched: false,
2056 timed_out: true,
2057 };
2058 let out = format_verifier_output("verifier", &result, false);
2059 assert!(
2060 out.contains("scheduler: UNKNOWN — VM timed out"),
2061 "timed-out run must show UNKNOWN: {out}",
2062 );
2063 assert!(
2064 !out.contains("scheduler: attached"),
2065 "timed-out run must not claim attached: {out}",
2066 );
2067 }
2068
2069 #[test]
2075 fn format_verifier_output_attached_not_dispatched_shows_not_confirmed() {
2076 let result = VerifierVmResult {
2077 stats: Vec::new(),
2078 scheduler_log: String::new(),
2079 attach: AttachOutcome::Attached,
2080 dispatched: false,
2081 timed_out: false,
2082 };
2083 let out = format_verifier_output("verifier", &result, false);
2084 assert!(
2085 out.contains("scheduler: attached"),
2086 "attached run must show the attach line: {out}",
2087 );
2088 assert!(
2089 out.contains("dispatch: NOT CONFIRMED"),
2090 "attached-but-not-dispatched must render the NOT CONFIRMED signal: {out}",
2091 );
2092 }
2093
2094 #[test]
2099 fn parse_verifier_stats_full_line() {
2100 let log = "processed 1234 insns (limit 1000000) max_states_per_insn 5 total_states 200 peak_states 50 mark_read 10\nverification time 42 usec\nstack depth 32+0\n";
2101 let vs = parse_verifier_stats(log);
2102 assert_eq!(vs.processed_insns, 1234);
2103 assert_eq!(vs.total_states, 200);
2104 assert_eq!(vs.peak_states, 50);
2105 assert_eq!(vs.time_usec, Some(42));
2106 assert_eq!(vs.stack_depth.as_deref(), Some("32+0"));
2107 }
2108
2109 #[test]
2110 fn parse_verifier_stats_insns_only() {
2111 let log = "processed 500 insns (limit 1000000) max_states_per_insn 1 total_states 10 peak_states 3 mark_read 0\n";
2112 let vs = parse_verifier_stats(log);
2113 assert_eq!(vs.processed_insns, 500);
2114 assert_eq!(vs.total_states, 10);
2115 assert_eq!(vs.peak_states, 3);
2116 assert!(vs.time_usec.is_none());
2117 assert!(vs.stack_depth.is_none());
2118 }
2119
2120 #[test]
2121 fn parse_verifier_stats_empty() {
2122 let vs = parse_verifier_stats("");
2123 assert_eq!(vs.processed_insns, 0);
2124 assert_eq!(vs.total_states, 0);
2125 assert_eq!(vs.peak_states, 0);
2126 assert!(vs.time_usec.is_none());
2127 assert!(vs.stack_depth.is_none());
2128 }
2129
2130 #[test]
2131 fn parse_verifier_stats_garbage_lines() {
2132 let log = "some random output\nnot a stats line\n";
2133 let vs = parse_verifier_stats(log);
2134 assert_eq!(vs.processed_insns, 0);
2135 assert_eq!(vs.total_states, 0);
2136 assert!(vs.time_usec.is_none());
2137 }
2138
2139 #[test]
2140 fn parse_verifier_stats_time_without_insns() {
2141 let log = "verification time 100 usec\nstack depth 64\n";
2142 let vs = parse_verifier_stats(log);
2143 assert_eq!(vs.processed_insns, 0);
2144 assert_eq!(vs.time_usec, Some(100));
2145 assert_eq!(vs.stack_depth.as_deref(), Some("64"));
2146 }
2147
2148 #[test]
2149 fn parse_verifier_stats_multi_subprogram_stack() {
2150 let log = "processed 42 insns (limit 1000000) max_states_per_insn 1 total_states 5 peak_states 2 mark_read 0\nstack depth 32+16+8\n";
2151 let vs = parse_verifier_stats(log);
2152 assert_eq!(vs.processed_insns, 42);
2153 assert_eq!(vs.stack_depth.as_deref(), Some("32+16+8"));
2154 }
2155
2156 #[test]
2157 fn parse_verifier_stats_noise_between_lines() {
2158 let log = "\
2159libbpf: loading something
2160processed 999 insns (limit 1000000) max_states_per_insn 3 total_states 77 peak_states 20 mark_read 5
2161libbpf: prog 'dispatch': attached
2162verification time 7 usec
2163stack depth 48+0
2164";
2165 let vs = parse_verifier_stats(log);
2166 assert_eq!(vs.processed_insns, 999);
2167 assert_eq!(vs.total_states, 77);
2168 assert_eq!(vs.peak_states, 20);
2169 assert_eq!(vs.time_usec, Some(7));
2170 assert_eq!(vs.stack_depth.as_deref(), Some("48+0"));
2171 }
2172
2173 #[test]
2174 fn parse_verifier_stats_partial_insns_line() {
2175 let log = "processed 123\n";
2176 let vs = parse_verifier_stats(log);
2177 assert_eq!(vs.processed_insns, 123);
2178 assert_eq!(vs.total_states, 0);
2179 assert_eq!(vs.peak_states, 0);
2180 }
2181
2182 #[test]
2183 fn parse_verifier_stats_only_stack_depth() {
2184 let log = "stack depth 128\n";
2185 let vs = parse_verifier_stats(log);
2186 assert_eq!(vs.stack_depth.as_deref(), Some("128"));
2187 assert_eq!(vs.processed_insns, 0);
2188 }
2189
2190 #[test]
2191 fn parse_verifier_stats_zero_insns() {
2192 let log = "processed 0 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0\n";
2193 let vs = parse_verifier_stats(log);
2194 assert_eq!(vs.processed_insns, 0);
2195 assert_eq!(vs.total_states, 0);
2196 assert_eq!(vs.peak_states, 0);
2197 }
2198
2199 #[test]
2200 fn parse_verifier_stats_large_values() {
2201 let log = "processed 999999 insns (limit 1000000) max_states_per_insn 100 total_states 50000 peak_states 12345 mark_read 9999\nverification time 123456 usec\n";
2202 let vs = parse_verifier_stats(log);
2203 assert_eq!(vs.processed_insns, 999999);
2204 assert_eq!(vs.total_states, 50000);
2205 assert_eq!(vs.peak_states, 12345);
2206 assert_eq!(vs.time_usec, Some(123456));
2207 }
2208
2209 #[test]
2210 fn parse_verifier_stats_stack_depth_single() {
2211 let log = "stack depth 64\n";
2212 let vs = parse_verifier_stats(log);
2213 assert_eq!(vs.stack_depth.as_deref(), Some("64"));
2214 }
2215
2216 #[test]
2217 fn parse_verifier_stats_stack_depth_many_subprograms() {
2218 let log = "stack depth 32+16+8+0+0\n";
2219 let vs = parse_verifier_stats(log);
2220 assert_eq!(vs.stack_depth.as_deref(), Some("32+16+8+0+0"));
2221 }
2222
2223 #[test]
2224 fn parse_verifier_stats_multiple_processed_lines_takes_last() {
2225 let log = "processed 100 insns (limit 1000000) max_states_per_insn 1 total_states 5 peak_states 2 mark_read 0\nprocessed 200 insns (limit 1000000) max_states_per_insn 2 total_states 10 peak_states 4 mark_read 0\n";
2226 let vs = parse_verifier_stats(log);
2227 assert_eq!(vs.processed_insns, 200);
2228 assert_eq!(vs.total_states, 10);
2229 }
2230
2231 #[test]
2232 fn parse_verifier_stats_complexity_error_with_stats() {
2233 let log = "\
2234func#0 @0
22350: R1=ctx() R10=fp0
22361: (bf) r6 = r1 ; R1=ctx() R6_w=ctx()
2237back-edge from insn 42 to 10
2238BPF program is too complex
2239processed 131071 insns (limit 131072) max_states_per_insn 12 total_states 9999 peak_states 5000 mark_read 800
2240verification time 250000 usec
2241stack depth 96+32
2242";
2243 let vs = parse_verifier_stats(log);
2244 assert_eq!(vs.processed_insns, 131071);
2245 assert_eq!(vs.total_states, 9999);
2246 assert_eq!(vs.peak_states, 5000);
2247 assert_eq!(vs.time_usec, Some(250000));
2248 assert_eq!(vs.stack_depth.as_deref(), Some("96+32"));
2249 }
2250
2251 #[test]
2252 fn parse_verifier_stats_complexity_error_no_stats() {
2253 let log = "\
2254func#0 @0
22550: R1=ctx() R10=fp0
2256R1 type=ctx expected=fp
2257";
2258 let vs = parse_verifier_stats(log);
2259 assert_eq!(vs.processed_insns, 0);
2260 assert_eq!(vs.total_states, 0);
2261 assert!(vs.time_usec.is_none());
2262 assert!(vs.stack_depth.is_none());
2263 }
2264
2265 #[test]
2266 fn parse_verifier_stats_loop_warning_with_stats() {
2267 let log = "\
2268infinite loop detected at insn 15
2269back-edge from insn 30 to 15
2270processed 500 insns (limit 1000000) max_states_per_insn 3 total_states 40 peak_states 15 mark_read 5
2271verification time 100 usec
2272";
2273 let vs = parse_verifier_stats(log);
2274 assert_eq!(vs.processed_insns, 500);
2275 assert_eq!(vs.total_states, 40);
2276 assert_eq!(vs.peak_states, 15);
2277 assert_eq!(vs.time_usec, Some(100));
2278 }
2279
2280 #[test]
2281 fn parse_verifier_stats_processed_no_number() {
2282 let log = "processed\n";
2283 let vs = parse_verifier_stats(log);
2284 assert_eq!(vs.processed_insns, 0);
2285 }
2286
2287 #[test]
2288 fn parse_verifier_stats_keyword_at_end_no_value() {
2289 let log = "processed 100 insns (limit 1000000) max_states_per_insn 1 total_states\n";
2290 let vs = parse_verifier_stats(log);
2291 assert_eq!(vs.processed_insns, 100);
2292 assert_eq!(vs.total_states, 0);
2293 }
2294
2295 #[test]
2296 fn parse_verifier_stats_non_numeric_values() {
2297 let log = "processed 100 insns (limit 1000000) max_states_per_insn 1 total_states abc peak_states xyz mark_read 0\n";
2298 let vs = parse_verifier_stats(log);
2299 assert_eq!(vs.processed_insns, 100);
2300 assert_eq!(vs.total_states, 0);
2301 assert_eq!(vs.peak_states, 0);
2302 }
2303
2304 #[test]
2305 fn parse_verifier_stats_verification_time_no_number() {
2306 let log = "verification time unknown usec\n";
2307 let vs = parse_verifier_stats(log);
2308 assert!(vs.time_usec.is_none());
2309 }
2310
2311 #[test]
2312 fn parse_verifier_stats_stack_depth_empty() {
2313 let log = "stack depth \n";
2314 let vs = parse_verifier_stats(log);
2315 assert!(vs.stack_depth.is_none());
2316 }
2317
2318 #[test]
2319 fn parse_verifier_stats_peak_states_at_end() {
2320 let log = "processed 50 insns (limit 1000000) max_states_per_insn 1 total_states 10 peak_states\n";
2321 let vs = parse_verifier_stats(log);
2322 assert_eq!(vs.processed_insns, 50);
2323 assert_eq!(vs.total_states, 10);
2324 assert_eq!(vs.peak_states, 0);
2325 }
2326
2327 #[test]
2328 fn parse_verifier_stats_windows_line_endings() {
2329 let log = "processed 42 insns (limit 1000000) max_states_per_insn 1 total_states 5 peak_states 2 mark_read 0\r\nverification time 10 usec\r\nstack depth 16\r\n";
2330 let vs = parse_verifier_stats(log);
2331 assert_eq!(vs.processed_insns, 42);
2332 assert_eq!(vs.time_usec, Some(10));
2333 assert!(vs.stack_depth.is_some());
2334 }
2335
2336 #[test]
2341 fn normalize_plain_instruction() {
2342 assert_eq!(
2343 normalize_verifier_line("100: (07) r1 += 8"),
2344 "100: (07) r1 += 8"
2345 );
2346 }
2347
2348 #[test]
2349 fn normalize_strips_frame_annotation() {
2350 assert_eq!(
2351 normalize_verifier_line("3006: (07) r9 += 1 ; frame1: R9_w=2"),
2352 "3006: (07) r9 += 1"
2353 );
2354 }
2355
2356 #[test]
2357 fn normalize_strips_register_annotation() {
2358 assert_eq!(
2359 normalize_verifier_line("42: (bf) r6 = r1 ; R1=ctx() R6_w=ctx()"),
2360 "42: (bf) r6 = r1"
2361 );
2362 }
2363
2364 #[test]
2365 fn normalize_standalone_register_dump() {
2366 assert_eq!(
2367 normalize_verifier_line("3041: frame1: R0_w=scalar()"),
2368 "3041:"
2369 );
2370 }
2371
2372 #[test]
2373 fn normalize_goto_inline_state() {
2374 assert_eq!(
2375 normalize_verifier_line(
2376 "3026: (b5) if r6 <= 0x11dc0 goto pc+2 3029: frame1: R0=1 R6=scalar()"
2377 ),
2378 "3026: (b5) if r6 <= 0x11dc0 goto pc+2"
2379 );
2380 }
2381
2382 #[test]
2383 fn normalize_goto_no_inline_state() {
2384 assert_eq!(
2385 normalize_verifier_line("50: (05) goto pc+10"),
2386 "50: (05) goto pc+10"
2387 );
2388 }
2389
2390 #[test]
2391 fn normalize_non_instruction_line() {
2392 assert_eq!(normalize_verifier_line("func#0 @0"), "func#0 @0");
2393 }
2394
2395 #[test]
2396 fn normalize_empty() {
2397 assert_eq!(normalize_verifier_line(""), "");
2398 }
2399
2400 #[test]
2401 fn normalize_goto_negative_offset() {
2402 assert_eq!(
2403 normalize_verifier_line("50: (05) goto pc-10 60: frame1: R0=1"),
2404 "50: (05) goto pc-10"
2405 );
2406 }
2407
2408 #[test]
2409 fn normalize_semicolon_source_comment() {
2410 let line = "100: (07) r1 += 8 ; for (int j = 0; j < n; j++)";
2411 assert_eq!(normalize_verifier_line(line), line);
2412 }
2413
2414 #[test]
2415 fn normalize_semicolon_return_value_comment() {
2416 let line = "200: (b7) r0 = 0 ; Return value";
2417 assert_eq!(normalize_verifier_line(line), line);
2418 }
2419
2420 #[test]
2421 fn normalize_standalone_bare_register_dump() {
2422 assert_eq!(
2423 normalize_verifier_line("3029: R0=1 R6=scalar(id=1)"),
2424 "3029:"
2425 );
2426 }
2427
2428 #[test]
2429 fn normalize_standalone_r10_dump() {
2430 assert_eq!(normalize_verifier_line("42: R10=fp0"), "42:");
2431 }
2432
2433 fn repeating_log(prefix: usize, period: usize, reps: usize, suffix: usize) -> String {
2438 let mut lines = Vec::new();
2439 for i in 0..prefix {
2440 lines.push(format!("{}: (07) r1 += {i}", 1000 + i));
2441 }
2442 for rep in 0..reps {
2443 for j in 0..period {
2444 let insn = 100 + j;
2445 lines.push(format!(
2446 "{insn}: (bf) r{} = r{} ; frame1: R{}_w={}",
2447 j % 10,
2448 (j + 1) % 10,
2449 j % 10,
2450 rep * 100 + j
2451 ));
2452 }
2453 }
2454 for i in 0..suffix {
2455 lines.push(format!("{}: (95) exit_{i}", 2000 + i));
2456 }
2457 lines.join("\n")
2458 }
2459
2460 #[test]
2461 fn detect_cycle_basic() {
2462 let log = repeating_log(0, 10, 8, 0);
2463 let lines: Vec<&str> = log.lines().collect();
2464 let result = detect_cycle(&lines);
2465 assert!(result.is_some(), "should detect cycle");
2466 let (start, period, count) = result.unwrap();
2467 assert_eq!(period, 10);
2468 assert!(count >= 6, "count={count}");
2469 assert_eq!(start, 0);
2470 }
2471
2472 #[test]
2473 fn detect_cycle_with_prefix_suffix() {
2474 let log = repeating_log(5, 10, 8, 5);
2475 let lines: Vec<&str> = log.lines().collect();
2476 let result = detect_cycle(&lines);
2477 assert!(result.is_some(), "should detect cycle with prefix/suffix");
2478 let (_start, period, count) = result.unwrap();
2479 assert_eq!(period, 10);
2480 assert!(count >= 6);
2481 }
2482
2483 #[test]
2484 fn detect_cycle_too_few_reps() {
2485 let log = repeating_log(0, 10, 2, 0);
2486 let lines: Vec<&str> = log.lines().collect();
2487 assert!(detect_cycle(&lines).is_none());
2488 }
2489
2490 #[test]
2491 fn detect_cycle_too_few_lines() {
2492 let lines: Vec<String> = (0..20)
2493 .map(|i| format!("{}: (07) r1 += {i}", 100 + i % 3))
2494 .collect();
2495 let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
2496 assert!(detect_cycle(&refs).is_none());
2497 }
2498
2499 #[test]
2500 fn detect_cycle_no_cycle() {
2501 let lines: Vec<String> = (0..100).map(|i| format!("{i}: unique_insn_{i}")).collect();
2502 let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
2503 assert!(detect_cycle(&refs).is_none());
2504 }
2505
2506 #[test]
2507 fn detect_cycle_empty() {
2508 let empty: Vec<&str> = vec![];
2509 assert!(detect_cycle(&empty).is_none());
2510 }
2511
2512 #[test]
2513 fn detect_cycle_exact_boundary() {
2514 let log = repeating_log(0, 5, 6, 0);
2515 let lines: Vec<&str> = log.lines().collect();
2516 assert_eq!(lines.len(), 30);
2517 let result = detect_cycle(&lines);
2518 assert!(result.is_some(), "boundary case should detect cycle");
2519 let (_start, period, count) = result.unwrap();
2520 assert_eq!(period, 5);
2521 assert_eq!(count, 6);
2522 }
2523
2524 #[test]
2525 fn collapse_cycles_empty_string() {
2526 assert_eq!(collapse_cycles(""), "");
2527 }
2528
2529 #[test]
2530 fn collapse_cycles_basic() {
2531 let log = repeating_log(2, 10, 8, 2);
2532 let collapsed = collapse_cycles(&log);
2533 assert!(collapsed.contains("identical iterations omitted"));
2534 assert!(collapsed.contains("8x of the following 10 lines"));
2535 assert!(collapsed.contains("end repeat"));
2536 assert!(collapsed.lines().count() < log.lines().count());
2537 }
2538
2539 #[test]
2540 fn collapse_cycles_no_cycle() {
2541 let log = "line 1\nline 2\nline 3\n";
2542 let collapsed = collapse_cycles(log);
2543 assert_eq!(collapsed, log);
2544 }
2545
2546 #[test]
2547 fn collapse_cycles_preserves_stats() {
2548 let mut log = repeating_log(0, 10, 8, 0);
2549 log.push_str("\nprocessed 1000 insns (limit 1000000) max_states_per_insn 5 total_states 100 peak_states 30 mark_read 10\n");
2550 let collapsed = collapse_cycles(&log);
2551 assert!(collapsed.contains("processed 1000 insns"));
2552 }
2553
2554 #[test]
2555 fn collapse_cycles_with_register_annotations() {
2556 let mut lines = Vec::new();
2557 lines.push("0: (07) r1 += 1".to_string());
2558 for rep in 0..8 {
2559 for j in 0..6 {
2560 let insn = 100 + j;
2561 lines.push(format!(
2562 "{insn}: (bf) r{} = r{} ; frame1: R{}_w={}",
2563 j % 10,
2564 (j + 1) % 10,
2565 j % 10,
2566 rep * 100 + j
2567 ));
2568 }
2569 }
2570 lines.push("200: (95) exit".to_string());
2571 let log = lines.join("\n");
2572 let collapsed = collapse_cycles(&log);
2573 assert!(collapsed.contains("identical iterations omitted"));
2574 }
2575
2576 fn prog(name: &str, verified_insns: u32) -> ProgStats {
2581 ProgStats {
2582 name: name.to_string(),
2583 verified_insns,
2584 }
2585 }
2586
2587 #[test]
2588 fn build_b_map_basic() {
2589 let stats_b = vec![prog("dispatch", 500)];
2590 let map = build_b_map(&stats_b);
2591 assert_eq!(map.get("dispatch"), Some(&500));
2592 }
2593
2594 #[test]
2595 fn build_b_map_empty() {
2596 let map = build_b_map(&[]);
2597 assert!(map.is_empty());
2598 }
2599
2600 #[test]
2601 fn build_diff_rows_matching_programs() {
2602 let stats_a = vec![prog("dispatch", 500)];
2603 let mut b_map = HashMap::new();
2604 b_map.insert("dispatch".to_string(), 300u64);
2605 let rows = build_diff_rows(&stats_a, &b_map);
2606 assert_eq!(rows.len(), 1);
2607 assert_eq!(rows[0].name, "dispatch");
2608 assert_eq!(rows[0].a, 500);
2609 assert_eq!(rows[0].b, 300);
2610 assert_eq!(rows[0].delta, 200);
2611 }
2612
2613 #[test]
2614 fn build_diff_rows_program_missing_from_b() {
2615 let stats_a = vec![prog("new_prog", 100)];
2616 let b_map = HashMap::new();
2617 let rows = build_diff_rows(&stats_a, &b_map);
2618 assert_eq!(rows.len(), 1);
2619 assert_eq!(rows[0].a, 100);
2620 assert_eq!(rows[0].b, 0);
2621 assert_eq!(rows[0].delta, 100);
2622 }
2623
2624 #[test]
2625 fn build_diff_rows_negative_delta() {
2626 let stats_a = vec![prog("dispatch", 200)];
2627 let mut b_map = HashMap::new();
2628 b_map.insert("dispatch".to_string(), 500u64);
2629 let rows = build_diff_rows(&stats_a, &b_map);
2630 assert_eq!(rows[0].delta, -300);
2631 }
2632
2633 #[test]
2634 fn build_diff_rows_empty_a() {
2635 let b_map = HashMap::new();
2636 let rows = build_diff_rows(&[], &b_map);
2637 assert!(rows.is_empty());
2638 }
2639
2640 fn unrolled_verifier_log(copies: usize, body_len: usize) -> String {
2645 let ops = [
2646 "(85) call bpf_ktime_get_ns#5",
2647 "(bf) r2 = r0",
2648 "(77) r0 >>= 16",
2649 "(af) r1 ^= r0",
2650 "(77) r2 >>= 32",
2651 "(0f) r1 += r2",
2652 "(24) w1 *= 7",
2653 "(04) w1 += 1",
2654 ];
2655 let mut lines = Vec::new();
2656 lines.push("func#0 @0".to_string());
2657 lines.push("0: R1=ctx() R10=fp0".to_string());
2658 let mut addr = 10;
2659 for copy in 0..copies {
2660 for (j, op) in ops.iter().enumerate().take(body_len) {
2661 lines.push(format!(
2662 "{}: {op} ; R0_w=scalar(id={})",
2663 addr,
2664 copy * 100 + j
2665 ));
2666 addr += 1;
2667 }
2668 }
2669 lines.push(format!("{addr}: (05) goto pc-1"));
2670 lines.push(
2671 "processed 1000 insns (limit 1000000) max_states_per_insn 3 \
2672 total_states 50 peak_states 20 mark_read 5"
2673 .to_string(),
2674 );
2675 lines.join("\n")
2676 }
2677
2678 #[test]
2679 fn detect_cycle_unrolled_loop() {
2680 let log = unrolled_verifier_log(8, 6);
2681 let lines: Vec<&str> = log.lines().collect();
2682 let result = detect_cycle(&lines);
2683 assert!(result.is_some(), "should detect cycle in unrolled loop");
2684 let (_start, period, count) = result.unwrap();
2685 assert_eq!(period, 6);
2686 assert!(count >= 6, "count={count}");
2687 }
2688
2689 #[test]
2690 fn collapse_cycles_unrolled_loop() {
2691 let log = unrolled_verifier_log(8, 6);
2692 let collapsed = collapse_cycles(&log);
2693 assert!(
2694 collapsed.contains("identical iterations omitted"),
2695 "should collapse unrolled loop"
2696 );
2697 assert!(collapsed.lines().count() < log.lines().count());
2698 }
2699
2700 #[test]
2705 fn extract_verifier_log_basic() {
2706 let log = "\
2707libbpf: prog 'dispatch': BPF program load failed: -22
2708-- BEGIN PROG LOAD LOG --
2709func#0 @0
27100: R1=ctx() R10=fp0
2711processed 100 insns (limit 1000000) max_states_per_insn 1 total_states 5 peak_states 2 mark_read 0
2712-- END PROG LOAD LOG --
2713libbpf: failed to load object 'ktstr_ops'
2714";
2715 let extracted = extract_verifier_log(log);
2716 assert!(extracted.is_some());
2717 let v = extracted.unwrap();
2718 assert!(v.starts_with("func#0 @0"));
2719 assert!(v.contains("processed 100 insns"));
2720 assert!(!v.contains("BEGIN PROG LOAD LOG"));
2721 assert!(!v.contains("END PROG LOAD LOG"));
2722 assert!(!v.contains("libbpf:"));
2723 }
2724
2725 #[test]
2726 fn extract_verifier_log_none_without_markers() {
2727 let log = "func#0 @0\n0: R1=ctx()\nprocessed 50 insns\n";
2728 assert!(extract_verifier_log(log).is_none());
2729 }
2730
2731 #[test]
2732 fn extract_verifier_log_empty() {
2733 assert!(extract_verifier_log("").is_none());
2734 }
2735
2736 #[test]
2742 fn extract_verifier_log_attack1_stats_parse() {
2743 let blob = "\
2744libbpf: prog 'ktstr_ops_dispatch': BPF program load failed: -22
2745libbpf: -- BEGIN PROG LOAD LOG --
2746func#0 @0
27470: R1=ctx() R10=fp0
27481: (bf) r6 = r1 ; R1=ctx() R6_w=ctx()
2749back-edge from insn 42 to 10
2750BPF program is too complex
2751processed 131071 insns (limit 131072) max_states_per_insn 12 total_states 9999 peak_states 5000 mark_read 800
2752verification time 250000 usec
2753stack depth 96+32
2754libbpf: -- END PROG LOAD LOG --
2755libbpf: failed to load BPF skeleton 'ktstr_ops': -22
2756";
2757 let extracted = extract_verifier_log(blob);
2758 assert!(extracted.is_some(), "should find markers");
2759 let v = extracted.unwrap();
2760 let vs = parse_verifier_stats(v);
2761 assert_eq!(vs.processed_insns, 131071);
2762 assert_eq!(vs.total_states, 9999);
2763 assert_eq!(vs.peak_states, 5000);
2764 assert_eq!(vs.time_usec, Some(250000));
2765 assert_eq!(vs.stack_depth.as_deref(), Some("96+32"));
2766
2767 let vs_raw = parse_verifier_stats(blob);
2771 assert_eq!(vs_raw.processed_insns, 131071);
2772 }
2773
2774 #[test]
2778 fn extract_verifier_log_attack3_no_false_collapse() {
2779 let blob = "\
2780libbpf: prog 'init': BPF program load failed: -22
2781libbpf: -- BEGIN PROG LOAD LOG --
2782func#0 @0
27830: R1=ctx() R10=fp0
27841: (bf) r6 = r1
27852: (07) r6 += 8
27863: (61) r0 = *(u32 *)(r6 + 0)
27874: (95) exit
2788processed 5 insns (limit 1000000) max_states_per_insn 1 total_states 3 peak_states 1 mark_read 0
2789libbpf: -- END PROG LOAD LOG --
2790libbpf: prog 'dispatch': BPF program load failed: -22
2791libbpf: -- BEGIN PROG LOAD LOG --
2792func#1 @10
279310: R1=ctx() R10=fp0
279411: (bf) r7 = r1
279512: (85) call bpf_ktime_get_ns#5
279613: (77) r0 >>= 32
279714: (95) exit
2798processed 5 insns (limit 1000000) max_states_per_insn 1 total_states 3 peak_states 1 mark_read 0
2799libbpf: -- END PROG LOAD LOG --
2800libbpf: prog 'enqueue': BPF program load failed: -22
2801libbpf: -- BEGIN PROG LOAD LOG --
2802func#2 @20
280320: R1=ctx() R10=fp0
280421: (b7) r0 = 0
280522: (63) *(u32 *)(r10 - 4) = r0
280623: (61) r1 = *(u32 *)(r10 - 4)
280724: (95) exit
2808processed 5 insns (limit 1000000) max_states_per_insn 1 total_states 3 peak_states 1 mark_read 0
2809libbpf: -- END PROG LOAD LOG --
2810libbpf: failed to load BPF skeleton 'ktstr_ops': -22
2811";
2812 let extracted = extract_verifier_log(blob);
2814 assert!(extracted.is_some());
2815 let v = extracted.unwrap();
2816 assert!(v.contains("func#0 @0"), "should get first program's log");
2817 assert!(!v.contains("func#1"), "should not include second program");
2818
2819 let collapsed = collapse_cycles(v);
2822 assert!(
2823 !collapsed.contains("identical iterations omitted"),
2824 "must not false-collapse distinct program logs"
2825 );
2826 }
2827
2828 #[test]
2831 fn snapshot_format_verifier_output_no_log() {
2832 let result = VerifierVmResult {
2833 stats: vec![
2834 ProgStats {
2835 name: "enqueue".into(),
2836 verified_insns: 500,
2837 },
2838 ProgStats {
2839 name: "dispatch".into(),
2840 verified_insns: 1200,
2841 },
2842 ProgStats {
2843 name: "init".into(),
2844 verified_insns: 300,
2845 },
2846 ],
2847 scheduler_log: String::new(),
2848 attach: AttachOutcome::Attached,
2849 dispatched: true,
2850 timed_out: false,
2851 };
2852 insta::assert_snapshot!(format_verifier_output("default", &result, false));
2853 }
2854
2855 #[test]
2856 fn snapshot_format_verifier_output_with_log() {
2857 let log = "\
2858-- BEGIN PROG LOAD LOG --\n\
2859func#0 @0\n\
28600: R1=ctx() R10=fp0\n\
2861processed 42 insns (limit 1000000) max_states_per_insn 1 total_states 10 peak_states 8 mark_read 5\n\
2862-- END PROG LOAD LOG --";
2863 let result = VerifierVmResult {
2864 stats: vec![ProgStats {
2865 name: "enqueue".into(),
2866 verified_insns: 42,
2867 }],
2868 scheduler_log: log.into(),
2869 attach: AttachOutcome::Died,
2872 dispatched: false,
2873 timed_out: false,
2874 };
2875 insta::assert_snapshot!(format_verifier_output("llc+steal", &result, false));
2876 }
2877
2878 #[test]
2879 fn snapshot_format_verifier_diff() {
2880 let stats_a = vec![
2881 ProgStats {
2882 name: "enqueue".into(),
2883 verified_insns: 500,
2884 },
2885 ProgStats {
2886 name: "dispatch".into(),
2887 verified_insns: 1200,
2888 },
2889 ProgStats {
2890 name: "init".into(),
2891 verified_insns: 300,
2892 },
2893 ];
2894 let stats_b = vec![
2895 ProgStats {
2896 name: "enqueue".into(),
2897 verified_insns: 480,
2898 },
2899 ProgStats {
2900 name: "dispatch".into(),
2901 verified_insns: 1350,
2902 },
2903 ProgStats {
2904 name: "init".into(),
2905 verified_insns: 300,
2906 },
2907 ];
2908 insta::assert_snapshot!(format_verifier_diff("default", &stats_a, "llc", &stats_b));
2909 }
2910
2911 #[test]
2912 fn snapshot_format_verifier_diff_missing_program() {
2913 let stats_a = vec![
2914 ProgStats {
2915 name: "enqueue".into(),
2916 verified_insns: 500,
2917 },
2918 ProgStats {
2919 name: "new_prog".into(),
2920 verified_insns: 100,
2921 },
2922 ];
2923 let stats_b = vec![ProgStats {
2924 name: "enqueue".into(),
2925 verified_insns: 500,
2926 }];
2927 insta::assert_snapshot!(format_verifier_diff("A", &stats_a, "B", &stats_b));
2928 }
2929
2930 #[test]
2936 fn extract_verifier_log_between_begin_end_markers() {
2937 let blob = "\
2941 unrelated preamble\n\
2942 libbpf: -- BEGIN PROG LOAD LOG --\n\
2943 processed 1234 insns (limit 1000000) max_states_per_insn 5 total_states 200 peak_states 50 mark_read 10\n\
2944 libbpf: -- END PROG LOAD LOG --\n\
2945 trailing diagnostics\n";
2946 let log = extract_verifier_log(blob).expect("markers present");
2947 assert!(log.contains("processed 1234 insns"));
2948 assert!(!log.contains("BEGIN PROG LOAD LOG"));
2949 assert!(!log.contains("END PROG LOAD LOG"));
2950 }
2951
2952 #[test]
2953 fn extract_verifier_log_returns_none_when_markers_absent() {
2954 assert!(extract_verifier_log("no markers in here").is_none());
2957 assert!(extract_verifier_log("only BEGIN marker -- BEGIN PROG LOAD LOG --").is_none());
2958 }
2959
2960 #[test]
2961 fn extract_verifier_log_consistent_with_parse_sched_output() {
2962 let sched_inner = "\
2969 libbpf: -- BEGIN PROG LOAD LOG --\n\
2970 processed 7 insns (limit 1000000) max_states_per_insn 1 total_states 1 peak_states 1 mark_read 0\n\
2971 libbpf: -- END PROG LOAD LOG --\n";
2972 let vm_output = format!(
2973 "kernel boot junk\n{SCHED_OUTPUT_START}\n{sched_inner}{SCHED_OUTPUT_END}\nafterward\n",
2974 );
2975 let sched = parse_sched_output(&vm_output).expect("SCHED_OUTPUT block");
2976 let verifier_log = extract_verifier_log(sched).expect("verifier markers");
2977 assert!(verifier_log.contains("processed 7 insns"));
2978 assert!(!verifier_log.contains("SCHED_OUTPUT"));
2979 assert!(!verifier_log.contains("BEGIN PROG LOAD LOG"));
2980 }
2981
2982 #[test]
2983 fn parse_sched_output_valid() {
2984 let output = format!(
2985 "noise\n{SCHED_OUTPUT_START}\nscheduler log line 1\nline 2\n{SCHED_OUTPUT_END}\nmore"
2986 );
2987 let parsed = parse_sched_output(&output);
2988 assert!(parsed.is_some());
2989 let content = parsed.unwrap();
2990 assert!(content.contains("scheduler log line 1"));
2991 assert!(content.contains("line 2"));
2992 }
2993
2994 #[test]
2995 fn parse_sched_output_missing_start() {
2996 let output = format!("no start\n{SCHED_OUTPUT_END}\n");
2997 assert!(parse_sched_output(&output).is_none());
2998 }
2999
3000 #[test]
3001 fn parse_sched_output_missing_end() {
3002 let output = format!("{SCHED_OUTPUT_START}\nsome content");
3003 assert!(parse_sched_output(&output).is_none());
3004 }
3005
3006 #[test]
3007 fn parse_sched_output_empty_content() {
3008 let output = format!("{SCHED_OUTPUT_START}\n\n{SCHED_OUTPUT_END}");
3009 assert!(parse_sched_output(&output).is_none());
3010 }
3011
3012 #[test]
3013 fn parse_sched_output_with_stack_traces() {
3014 let stack = "do_enqueue_task+0x1a0/0x380\nbalance_one+0x50/0x100\n";
3015 let output = format!("{SCHED_OUTPUT_START}\n{stack}\n{SCHED_OUTPUT_END}");
3016 let parsed = parse_sched_output(&output).unwrap();
3017 assert!(parsed.contains("do_enqueue_task"));
3018 assert!(parsed.contains("balance_one"));
3019 }
3020
3021 #[test]
3022 fn parse_sched_output_rfind_survives_end_marker_in_content() {
3023 let content = format!("line1\nfake {SCHED_OUTPUT_END} inside\nline3");
3030 let output = format!("{SCHED_OUTPUT_START}\n{content}\n{SCHED_OUTPUT_END}\n");
3031 let parsed = parse_sched_output(&output).unwrap();
3032 assert!(
3033 parsed.contains("line3"),
3034 "rfind must keep content after an embedded END marker: {parsed:?}"
3035 );
3036 assert!(
3037 parsed.contains("fake"),
3038 "content before the embedded marker must also survive: {parsed:?}"
3039 );
3040 }
3041
3042 #[test]
3045 fn parse_sched_output_partial_well_formed_matches_strict() {
3046 let output = format!(
3049 "noise\n{SCHED_OUTPUT_START}\nscheduler log line 1\nline 2\n{SCHED_OUTPUT_END}\nmore"
3050 );
3051 assert_eq!(
3052 parse_sched_output_partial(&output),
3053 parse_sched_output(&output),
3054 );
3055 }
3056
3057 #[test]
3058 fn parse_sched_output_partial_missing_end_returns_partial() {
3059 let output = format!("{SCHED_OUTPUT_START}\nstack frame 1\nstack frame 2");
3064 assert!(parse_sched_output(&output).is_none());
3065 let partial = parse_sched_output_partial(&output).unwrap();
3066 assert!(partial.contains("stack frame 1"));
3067 assert!(partial.contains("stack frame 2"));
3068 }
3069
3070 #[test]
3071 fn parse_sched_output_partial_missing_start_returns_none() {
3072 let output = format!("garbage\n{SCHED_OUTPUT_END}\n");
3076 assert!(parse_sched_output_partial(&output).is_none());
3077 }
3078
3079 #[test]
3080 fn parse_sched_output_partial_empty_content_returns_none() {
3081 let output = format!("{SCHED_OUTPUT_START}\n");
3083 assert!(parse_sched_output_partial(&output).is_none());
3084 }
3085}