From bcedca07964be54de2d4f270581175ab97299266 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:28:12 -0700 Subject: [PATCH 1/4] tools/asahi: avd-av1-tail-tests: extract the reference timestamp helper The suite rebuilds avd_av1_set_prob() and its neighbours from avd-av1.c by name and compiles them with -Werror. A driver whose avd_av1_set_prob() calls a static u64 avd_av1_ref_ts() helper (#260) no longer builds there: the helper is not extracted, the extractor only accepts int and void functions, and u64, s8 and U64_MAX are not defined. The control run then exits 1 before printing anything. Extract avd_av1_ref_ts() ahead of avd_av1_set_prob() when the driver defines it, accept u64 in the extractor and define the three types. A driver without the helper builds exactly as before. The two --mutation suites are registered as expected to fail, and any non-zero exit counted as a caught mutant, including a control program that did not build. In --mutation mode a build failure now reports that no mutant verdict was reached and exits 0, so CI flags the suite instead of passing it. Other harness failures are not changed. Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- tools/asahi/avd-av1-tail-tests.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/asahi/avd-av1-tail-tests.py b/tools/asahi/avd-av1-tail-tests.py index 8a36298013af61..2378bc3ea5c58e 100644 --- a/tools/asahi/avd-av1-tail-tests.py +++ b/tools/asahi/avd-av1-tail-tests.py @@ -15,7 +15,7 @@ def function(source, name): - match = re.search(r'^(?:static )?(?:int|void)\s+' + name + r'\(', source, re.M) + match = re.search(r'^(?:static )?(?:int|void|u64)\s+' + name + r'\(', source, re.M) if not match: raise ValueError(name) start = match.start() @@ -46,6 +46,9 @@ def structure(source, name): typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; +typedef uint64_t u64; +typedef int8_t s8; +#define U64_MAX UINT64_MAX typedef uint64_t dma_addr_t; #define ALIGN(x,a) (((x)+(a)-1)&~((__typeof__(x))(a)-1)) #define ALIGN_DOWN(x,a) ((x)&~((__typeof__(x))(a)-1)) @@ -155,7 +158,8 @@ def main(): text = {name: p.read_text() for name, p in paths.items()} src = text['avd-av1.c'] macros = src[src.index('#define AVD_CDFS_SIZE'):src.index('struct avd_av1_run {')] - functions = {n: function(src, n) for n in ['avd_av1_set_prob', 'avd_color_size', 'avd_av1_run_preamble', 'avd_av1_adjust_decoded_fmt']} + helpers = [n for n in ['avd_av1_ref_ts'] if re.search(r'^static u64 ' + n + r'\(', src, re.M)] + functions = {n: function(src, n) for n in helpers + ['avd_av1_set_prob', 'avd_color_size', 'avd_av1_run_preamble', 'avd_av1_adjust_decoded_fmt']} if args.mutation == 'round-up': if 'ALIGN_DOWN((dst), AVD_ALIGN)' not in macros: parser.error('round-up mutation requires aligned tail macros') @@ -178,6 +182,9 @@ def main(): build = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=environment) (args.out / 'build.log').write_text(build.stdout) if build.returncode: + if args.mutation: + print(f'mutation {args.mutation}: controls did not build (see build.log); no mutant verdict') + raise SystemExit(0) raise SystemExit(build.returncode) run_command = [str(exe)] if args.case != 'all': From a29de9576a90a59f0b3ce07d4a3d6e3834e03bfc Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:34:49 -0700 Subject: [PATCH 2/4] tools/asahi: avd-av1-tail-tests: cover an invalid primary reference index With avd_av1_ref_ts() in the driver, check what avd_av1_set_prob() does when the primary reference slot holds an invalid index: -1, as userspace passes for intra frames, -128, 8 and 127. It must look up no reference slot (timestamp U64_MAX, which matches no capture buffer) and inherit from the destination's own CDF area, the fallback avd_get_ref_buf() takes for an unknown timestamp. avd_av1_set_prob() writes the probabilities back to the destination's CDF area, so the destination after the call cannot show where they were read from. The case fills the destination with a non-periodic pattern, takes the expected CDF bytes from that pattern before the call, compares the probabilities with them, and checks afterwards that every destination byte still matches the pattern. --mutation fallback-offset moves only the fallback read (reference == destination) by one byte; the case catches it. The reference lookup fixture now records the timestamp and falls back to the destination for an unknown one; the existing inheritance cases still require the lookup to ask for timestamp 1000. The new cases and the new mutation apply only when the driver defines the helper; without it the suite runs the same 1792 controls as before, and the new mutation reports that no mutant verdict was reached. Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- tools/asahi/avd-av1-tail-tests.py | 42 ++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/tools/asahi/avd-av1-tail-tests.py b/tools/asahi/avd-av1-tail-tests.py index 2378bc3ea5c58e..a2655560b5ea2c 100644 --- a/tools/asahi/avd-av1-tail-tests.py +++ b/tools/asahi/avd-av1-tail-tests.py @@ -76,9 +76,10 @@ def structure(source, name): static struct v4l2_ctrl_av1_film_grain grain; static struct v4l2_ctrl controls[4]; static unsigned int lookup_calls, preamble_calls; +static uint64_t last_timestamp; static void *vb2_plane_vaddr(struct vb2_buffer *b,unsigned int p){assert(p==0);return b->memory;} static struct avd_decoded_buffer *vb2_to_avd_decoded_buf(struct vb2_buffer *b){return (void *)b;} -static struct avd_decoded_buffer *avd_get_ref_buf(struct avd_ctx *ctx,struct vb2_v4l2_buffer *dst,uint64_t timestamp){(void)ctx;(void)dst;assert(timestamp==1000);lookup_calls++;return reference;} +static struct avd_decoded_buffer *avd_get_ref_buf(struct avd_ctx *ctx,struct vb2_v4l2_buffer *dst,uint64_t timestamp){(void)ctx;last_timestamp=timestamp;lookup_calls++;return timestamp==1000?reference:vb2_to_avd_decoded_buf(&dst->vb2_buf);} static void avd_run_preamble(struct avd_ctx *ctx,struct avd_run *run){(void)ctx;(void)run;preamble_calls++;} static struct v4l2_ctrl *v4l2_ctrl_find(int *h,unsigned int id){ (void)h; @@ -113,7 +114,7 @@ def structure(source, name): assert(avd_av1_run_preamble(&ctx,&run)==0);assert(preamble_calls==1); if(check_addresses){assert(run.addresses.probs_out==run.base.y_out+dst_offset);assert(run.addresses.color==run.base.y_out+dst_offset+9216);} avd_av1_set_prob(&ctx,&run); - assert(lookup_calls==(mode?0:1));assert(memcmp(probabilities,&expected,sizeof(expected))==0); + assert(lookup_calls==(mode?0:1));if(!mode)assert(last_timestamp==1000);assert(memcmp(probabilities,&expected,sizeof(expected))==0); assert(memcmp(dst.base.vb.vb2_buf.memory+dst_offset,&expected,sizeof(expected))==0); for(size_t i=0;i=compressed);assert(cdf+sizeof(struct avd_av1_cdfs)<=co);assert(co+color<=length); assert(cdf==aligned_cdf(length,color));assert(co==cdf+9216); } +#ifdef HAVE_AVD_AV1_REF_TS +/* Not periodic: a read at a wrong offset, even a multiple of 256, sees other bytes. */ +static unsigned char fill(size_t i){uint32_t x=(uint32_t)i*2654435761u;x^=x>>15;x*=2246822519u;x^=x>>13;return (unsigned char)(x>>11);} +static void invalid_primary_index(signed char idx){ + const size_t len=7000064,color=138240,offset=aligned_cdf(len,color); + struct avd_decoded_buffer dst={0};struct avd_av1_ctx av1={0};struct avd_ctx ctx={.priv=&av1};struct avd_av1_run run={0}; + unsigned char probabilities[sizeof(struct avd_av1_cdfs)],expected[sizeof(struct avd_av1_cdfs)]; + dst.base.vb.vb2_buf.planes[0].length=len;dst.base.vb.vb2_buf.memory=malloc(len);assert(dst.base.vb.vb2_buf.memory);dst.av1.color_size=color; + for(size_t i=0;i1&&(strcmp(argv[1],"snapshot")==0||strcmp(argv[1],"snapshot-copy")==0)){assert(argc==3);inheritance(64,0,argv[2],strcmp(argv[1],"snapshot")==0);puts("snapshot inheritance PASS");return 0;} @@ -140,6 +165,9 @@ def structure(source, name): for(unsigned int mode=1;mode<=4;mode++){inheritance(residue,mode,NULL,true);cases++;} reservation(1920,1080,6425727,residue);cases++;reservation(1281,721,3000017,residue);cases++; } +#ifdef HAVE_AVD_AV1_REF_TS + invalid_primary_index(-1);invalid_primary_index(-128);invalid_primary_index(8);invalid_primary_index(127);cases+=4; +#endif printf("%u production controls PASS\n",cases);return 0; } ''' @@ -151,7 +179,7 @@ def main(): parser.add_argument('--out', type=Path, required=True) parser.add_argument('--neighbors', type=Path) parser.add_argument('--case', choices=['all', 'snapshot', 'snapshot-copy', 'copy', 'seed', 'aligned'], default='all') - parser.add_argument('--mutation', choices=['round-up', 'cpu-unaligned']) + parser.add_argument('--mutation', choices=['round-up', 'cpu-unaligned', 'fallback-offset']) args = parser.parse_args() args.out.mkdir(parents=True, exist_ok=True) paths = {name: args.source / DRIVER / name for name in ['avd-av1.c', 'avd-av1-entropymode.h', 'avd-av1-entropymode.c', 'avd.h']} @@ -167,11 +195,19 @@ def main(): elif args.mutation == 'cpu-unaligned': functions['avd_av1_set_prob'] = functions['avd_av1_set_prob'].replace('AVD_AV1_CDFS_OFFSET(', 'UNALIGNED_CDFS_OFFSET(') macros += '\n#define UNALIGNED_CDFS_OFFSET(dst,cl) ((dst)-ALIGN(cl,AVD_ALIGN)-ALIGN(AVD_CDFS_SIZE,AVD_ALIGN))\n' + elif args.mutation == 'fallback-offset': + read = 'vb2_plane_vaddr(&ref->base.vb.vb2_buf, 0) +' + if 'avd_av1_ref_ts' not in helpers or functions['avd_av1_set_prob'].count(read) != 1: + print('mutation fallback-offset: no avd_av1_ref_ts() or not one reference read; no mutant verdict') + raise SystemExit(0) + functions['avd_av1_set_prob'] = functions['avd_av1_set_prob'].replace(read, read + ' (ref == dst ? 1 : 0) +') header = text['avd-av1-entropymode.h'].replace('#include ', '') defaults = re.sub(r'^#include .*$', '', text['avd-av1-entropymode.c'], flags=re.M) alignment = re.search(r'^#define AVD_ALIGN\s+\d+', text['avd.h'], re.M).group() generated = PREFIX.replace('@ALIGNMENT@', alignment).replace('@CDF_HEADER@', header).replace('@CDF_DEFAULTS@', defaults).replace('@RUN_STRUCT@', structure(src, 'avd_av1_run')).replace('@CTX_STRUCT@', structure(src, 'avd_av1_ctx')) generated += macros + '\n' + '\n'.join(functions.values()) + TESTS + if helpers: + generated = '#define HAVE_AVD_AV1_REF_TS 1\n' + generated uapi = args.source / 'include/uapi/linux/v4l2-controls.h' (args.out / 'controls.h').write_bytes(uapi.read_bytes()) cfile = args.out / 'controls.c' From 762142518c411ead6589e46259760633a2222ae1 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:46:52 -0700 Subject: [PATCH 3/4] tools/asahi: g15-display-order-tests: model the system-sleep hold The suite builds admit() from g15/boot.rs against host models. With the T6031 system-sleep hold (#288), admit() reads the ASC control word before it takes the hold, passes whether to hold through system sleep to glue::hold_power(dev, sleep_hold), reads params::sleep_hold() and warns with dev_warn! when g15_sleep_hold=0 on T6031. The model has no dev_warn!, no params module and a one-argument hold_power(), so the admit() control no longer builds. Add dev_warn! (counted), params::sleep_hold() and a failing ASC read to the model, and pick the hold_power() mock from the admit() it is given: with the sleep-hold argument it records the value passed; without it, the model is the old one. Every existing scenario and mutant runs unchanged on both. For an admit() with the sleep-hold argument, also check that the hold is taken through system sleep only on T6031 with g15_sleep_hold on, that g15_sleep_hold=0 on T6031 warns once, that T6030 never holds through sleep, and that a failing ASC read returns before any hold is taken on either SoC. Four mutants (the hold on any platform, ignoring the parameter, no warning, the ASC read after the hold) each fail one of those checks. Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- tools/asahi/g15-display-order-tests.py | 81 ++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 12 deletions(-) diff --git a/tools/asahi/g15-display-order-tests.py b/tools/asahi/g15-display-order-tests.py index 50e0aa48093961..a6666efdf2776b 100644 --- a/tools/asahi/g15-display-order-tests.py +++ b/tools/asahi/g15-display-order-tests.py @@ -227,7 +227,7 @@ def c_source(root): } RS_PREFIX = r''' -#![allow(dead_code, unused_variables, unused_imports, non_snake_case)] +#![allow(dead_code, unused_variables, unused_imports, unused_macros, non_snake_case)] use std::cell::RefCell; use std::marker::PhantomData; #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -236,13 +236,21 @@ def c_source(root): type Result = std::result::Result; const ENODEV: Error = Error(-19); const EINVAL: Error = Error(-22); +const EIO: Error = Error(-5); const EPROBE_DEFER: Error = Error(-517); macro_rules! dev_err { ($($args:tt)*) => {{}}; } macro_rules! dev_info { ($($args:tt)*) => {{}}; } -struct State { board: bool, machine: bool, pmp: i32, display: i32 } +macro_rules! dev_warn { ($($args:tt)*) => {{ warned(); }}; } +struct State { board: bool, machine: bool, pmp: i32, display: i32, sleep_hold: bool, asc_fail: bool } thread_local! { static CALLS: RefCell> = const { RefCell::new(Vec::new()) }; - static STATE: RefCell = const { RefCell::new(State { board: true, machine: true, pmp: 0, display: 0 }) }; + static STATE: RefCell = const { RefCell::new(State { board: true, machine: true, pmp: 0, display: 0, sleep_hold: true, asc_fail: false }) }; + static HOLD: RefCell> = const { RefCell::new(None) }; + static WARNINGS: RefCell = const { RefCell::new(0) }; +} +fn warned() { WARNINGS.with(|w| *w.borrow_mut() += 1); } +mod params { + pub fn sleep_hold() -> bool { super::STATE.with(|s| s.borrow().sleep_hold) } } fn call(name: &'static str) { CALLS.with(|c| c.borrow_mut().push(name)); } mod device { @@ -276,7 +284,9 @@ def c_source(root): pub struct Regs; impl Regs { pub fn new(_: &super::platform::Device) -> super::Result { super::call("regs"); Ok(Self) } - pub fn asc_control(&self) -> super::Result { Ok(0) } + pub fn asc_control(&self) -> super::Result { + if super::STATE.with(|s| s.borrow().asc_fail) { Err(super::EIO) } else { Ok(0) } + } } } mod glue { @@ -286,7 +296,7 @@ def c_source(root): pub unsafe fn asahi_g15_pmp_prepare(_: *mut core::ffi::c_void) -> core::ffi::c_int { call("pmp"); STATE.with(|s| s.borrow().pmp) } pub unsafe fn asahi_g15_display_prepare(_: *mut core::ffi::c_void) -> core::ffi::c_int { call("display"); STATE.with(|s| s.borrow().display) } pub fn dma_coherent(_: &super::device::Device) -> bool { call("coherent"); false } - pub fn hold_power(_: &super::device::Device) { call("hold"); } +@HOLD_POWER@ } struct Admitted { regs: regs::Regs, adt: adt::GpuAdt, board_images: u32 } ''' @@ -297,12 +307,15 @@ def c_source(root): const TAIL: [&str; 6] = ["adt", "identity", "coherent", "board_images", "regs", "hold"]; fn run(cfg: &'static Config, machine: bool, pmp: i32, display: i32) -> (Result<()>, Vec<&'static str>) { CALLS.with(|c| c.borrow_mut().clear()); - STATE.with(|s| *s.borrow_mut() = State { board: true, machine, pmp, display }); + STATE.with(|s| *s.borrow_mut() = State { board: true, machine, pmp, display, sleep_hold: true, asc_fail: false }); + HOLD.with(|h| *h.borrow_mut() = None); + WARNINGS.with(|w| *w.borrow_mut() = 0); let pdev = platform::Device(device::Device, Core(PhantomData)); let ret = admit(&pdev, &AsahiDevice, cfg).map(|_| ()); (ret, CALLS.with(|c| c.borrow().clone())) } fn with_tail(head: &[&'static str]) -> Vec<&'static str> { head.iter().chain(TAIL.iter()).copied().collect() } +@SLEEP_HOLD_RUN@ fn main() { assert_eq!(run(&T6031, true, 0, -517), (Err(EPROBE_DEFER), vec!["machine", "display"]), "T6031 defers before firmware"); assert_eq!(run(&T6031, true, 0, -19), (Err(ENODEV), vec!["machine", "display"]), "T6031 refusal stops admission"); @@ -310,9 +323,42 @@ def c_source(root): assert_eq!(run(&T6031, false, 0, 0), (Err(ENODEV), vec!["machine"]), "the machine check comes first"); assert_eq!(run(&T6030, true, 0, -517), (Ok(()), with_tail(&["board", "pmp"])), "T6030 path unchanged"); assert_eq!(run(&T6030, true, -517, 0), (Err(EPROBE_DEFER), vec!["board", "pmp"]), "T6030 still defers on its PMP link"); +@SLEEP_HOLD_CHECKS@ } ''' +HOLD_PLAIN = ' pub fn hold_power(_: &super::device::Device) { call("hold"); }' +HOLD_SLEEP = (' pub fn hold_power(_: &super::device::Device, sleep_hold: bool) {\n' + ' call("hold"); super::HOLD.with(|h| *h.borrow_mut() = Some(sleep_hold));\n }') +SLEEP_HOLD_RUN = r''' +fn run_hold(cfg: &'static Config, sleep_hold: bool, asc_fail: bool) -> (Result<()>, Vec<&'static str>, Option, u32) { + CALLS.with(|c| c.borrow_mut().clear()); + STATE.with(|s| *s.borrow_mut() = State { board: true, machine: true, pmp: 0, display: 0, sleep_hold, asc_fail }); + HOLD.with(|h| *h.borrow_mut() = None); + WARNINGS.with(|w| *w.borrow_mut() = 0); + let pdev = platform::Device(device::Device, Core(PhantomData)); + let ret = admit(&pdev, &AsahiDevice, cfg).map(|_| ()); + (ret, CALLS.with(|c| c.borrow().clone()), HOLD.with(|h| *h.borrow()), WARNINGS.with(|w| *w.borrow())) +} +fn before_hold(head: &[&'static str]) -> Vec<&'static str> { head.iter().chain(TAIL[..TAIL.len() - 1].iter()).copied().collect() } +''' +SLEEP_HOLD_CHECKS = r''' + // The system-sleep hold: T6031 only, and only while g15_sleep_hold is on; off is warned about. + assert_eq!(run_hold(&T6031, true, false), (Ok(()), with_tail(&["machine", "display"]), Some(true), 0), "T6031 holds gfx through sleep"); + assert_eq!(run_hold(&T6031, false, false), (Ok(()), with_tail(&["machine", "display"]), Some(false), 1), "g15_sleep_hold=0: no sleep hold, one warning"); + assert_eq!(run_hold(&T6030, true, false), (Ok(()), with_tail(&["board", "pmp"]), Some(false), 0), "T6030 never holds through sleep"); + // The ASC read comes before the hold: a failure there leaves no hold behind. + assert_eq!(run_hold(&T6031, true, true), (Err(EIO), before_hold(&["machine", "display"]), None, 0), "T6031: ASC failure takes no hold"); + assert_eq!(run_hold(&T6030, true, true), (Err(EIO), before_hold(&["board", "pmp"]), None, 0), "T6030: ASC failure takes no hold"); +''' +SLEEP_HOLD_MUTANTS = { + "sleep-hold-any-platform": ("let sleep_hold = t6031 && params::sleep_hold();", "let sleep_hold = params::sleep_hold();"), + "sleep-hold-ignores-parameter": ("let sleep_hold = t6031 && params::sleep_hold();", "let sleep_hold = t6031;"), + "no-warning-when-off": ("} else if t6031 {", "} else if false {"), + "asc-read-after-hold": (re.compile(r"(\n\s*)let asc = regs\.asc_control\(\)\?;(.*?glue::hold_power\(dev, sleep_hold\);)", re.S), + r"\2\1let asc = regs.asc_control()?;"), +} + RS_MUTANTS = { "display-refusal-ignored": ("dev_err!(dev, \"native display not admitted: {:?}\\n\", err);\n }\n return Err(err);", "dev_err!(dev, \"native display not admitted: {:?}\\n\", err);\n }"), @@ -346,10 +392,18 @@ def c_failures(exe): return [s for s in C_SCENARIOS if subprocess.run([str(exe), s], capture_output=True).returncode] -def compile_rs(work, rustc, source, name): +def rs_model(admit): + """The prelude and main for this admit(): with the system-sleep hold argument (g15_sleep_hold) or without.""" + sleep = "glue::hold_power(dev, sleep_hold);" in admit + prefix = RS_PREFIX.replace("@HOLD_POWER@", HOLD_SLEEP if sleep else HOLD_PLAIN) + main = RS_MAIN.replace("@SLEEP_HOLD_RUN@", SLEEP_HOLD_RUN if sleep else "").replace("@SLEEP_HOLD_CHECKS@", SLEEP_HOLD_CHECKS if sleep else "") + return sleep, prefix, main + + +def compile_rs(work, rustc, source, name, model): src = work / f"{name}.rs" exe = work / name - src.write_text(RS_PREFIX + source + RS_MAIN) + src.write_text(model[1] + source + model[2]) result = subprocess.run([rustc, "--edition=2021", "-o", str(exe), str(src)], capture_output=True, text=True) if result.returncode: raise SystemExit(f"{name}: does not build with {rustc}:\n{result.stderr}") @@ -387,6 +441,8 @@ def main(): checks = static_checks(args.root) source = c_source(args.root) admit = item((args.root / BOOT).read_text(), "pub(crate) fn admit(") + model = rs_model(admit) + rs_mutants = dict(RS_MUTANTS, **(SLEEP_HOLD_MUTANTS if model[0] else {})) with tempfile.TemporaryDirectory(prefix="g15-display-order-", dir=os.environ.get("TMPDIR")) as tmp: work = Path(tmp) failed = c_failures(compile_c(work, source, "control")) @@ -396,16 +452,17 @@ def main(): assert failed, f"C mutant {name} survived" print(f"mutant {name}: caught by {', '.join(failed)}") for rustc in rustcs: - exe = compile_rs(work, rustc, admit, "admit") + exe = compile_rs(work, rustc, admit, "admit", model) run = subprocess.run([str(exe)], capture_output=True, text=True) assert run.returncode == 0, f"admit() control failed with {rustc}:\n{run.stderr}" - for name, change in RS_MUTANTS.items(): - exe = compile_rs(work, rustc, mutate(admit, change), f"admit-{name}") + for name, change in rs_mutants.items(): + exe = compile_rs(work, rustc, mutate(admit, change), f"admit-{name}", model) run = subprocess.run([str(exe)], capture_output=True, text=True) assert run.returncode, f"admit() mutant {name} survived with {rustc}" print(f"mutant {name} ({rustc}): caught") print(f"PASS: {checks} static checks, {len(C_SCENARIOS)} C scenarios, " - f"{len(C_MUTANTS)} C mutants, admit() on {len(rustcs)} rustc, {len(RS_MUTANTS)} admit() mutants") + f"{len(C_MUTANTS)} C mutants, admit() on {len(rustcs)} rustc, {len(rs_mutants)} admit() mutants" + + (", system-sleep hold checks" if model[0] else "")) if __name__ == "__main__": From dbb6bcce98cb5b0c38428d71933a7f3aea8a2b54 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:00:56 -0700 Subject: [PATCH 4/4] selftests: apple-dcp: follow the one-argument cd321x_display_taken() On a tree where an occupied display pipeline is reported as -ENOSPC on every SoC, cd321x_display_taken() takes only the return code, and -EBUSY means a transient mux or PHY failure. The probe controls still call the two-argument form and no longer build there, and the disconnect scenario injects -EBUSY as an occupied display, which such a tree retries instead. When the tree has the one-argument helper, check its contract in the probe controls (-ENOSPC is taken; -EBUSY, -EAGAIN, -ETIMEDOUT, -EIO and 0 are not). In the disconnect scenario, let the mode stub classify its error through the real helper, inject -ENOSPC as the occupied display, and add a transient -EBUSY case that must retry before the USB role is switched. A tree with the two-argument helper runs the old checks unchanged. The probe control also zero-initializes its cd321x_pm_state: cd321x_pm_init() does not set setup_pending, which cd321x_pm_ready() reads. Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Generated-by: LLM --- .../drivers/gpu/apple-dcp/lifecycle.py | 19 +++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/pm.py | 15 ++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index d8270fb0a89c4a..96fd0b5dcea014 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -459,6 +459,25 @@ def function(file, name): static void __attribute__((unused)) dcp_relink_link_up(int *s, u64 generation, unsigned long now) { (void)s; (void)generation; (void)now; assert(!"legacy relink timestamp in an H17P scenario"); } """ + # Let the real helper classify an occupied display on newer trees, where + # -EBUSY means a transient mux/PHY failure rather than a taken pipeline. + if re.search(r'cd321x_display_taken\(int ret\)', pm): + assert shim.count('mode_error == -EBUSY') == 1 + assert tests.count('mode_error = -EBUSY;') == 2 + shim = shim.replace('mode_error == -EBUSY', 'cd321x_display_taken(mode_error)') + tests = tests.replace('mode_error = -EBUSY;', 'mode_error = -ENOSPC;') + busy = ' CHECK(sink.connected && !notices && !port.display_route_active);\n' + assert tests.count(busy) == 1 + tests = tests.replace(busy, busy + r''' + CHECK(roles == 1 && port.pm.phase == CD321X_PM_RUNNING); + /* A transient busy mux must retry before switching the USB role. */ + init(); port.display_route_active = false; role = USB_ROLE_NONE; + port.update_status.status = TPS_STATUS_PLUG_PRESENT; + port.update_status.data_status = TPS_DATA_STATUS_DP_CONNECTION | CD321X_DATA_STATUS_HPD_LEVEL; + mode_error = -EBUSY; cd321x_update_work(&port.update_work.work); + CHECK(!roles && port.pm.phase == CD321X_PM_REVALIDATE); + CHECK(sink.connected && !notices && !port.display_route_active); +''') # How long a held mux has been retried is measured on the driver's clock. if 'jiffies_to_msecs(jiffies)' in funcs: shim += ('\nstatic unsigned long jiffies;\n' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index 63b1ddb2398cd0..6c30856841943d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -547,7 +547,7 @@ def function(text, name): int main(void) { (void)cd321x_pm_notifications; struct cd321x c; - struct cd321x_pm_state budget; + struct cd321x_pm_state budget={0}; cd321x_pm_init(&budget);assert(cd321x_pm_ready(&budget)); assert(cd321x_display_taken(-ENOSPC,true)&&!cd321x_display_taken(-EBUSY,true)&&!cd321x_display_taken(0,true)&&cd321x_display_taken(-EBUSY,false)&&!cd321x_display_taken(-ENOSPC,false)&&!cd321x_display_taken(0,false)); for(unsigned int i=0;i