htoprs 0.2.0

A faithful Rust port of htop — the interactive process viewer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
//! Partial port of `Scheduling.c` — htop's CPU-scheduling policy/priority
//! selection UI plus the per-process scheduling-policy read/set helpers.
//!
//! The whole file sits under the C `#ifdef SCHEDULER_SUPPORT`
//! (`HAVE_SCHED_SETSCHEDULER && HAVE_SCHED_GETSCHEDULER`, i.e. the Linux
//! build); this module ports that variant. C names are preserved verbatim
//! (htop uses `CamelCase_snake`), so `non_snake_case` is allowed for the
//! whole module — matching the spec name-for-name is the point of the
//! port. C `Foo(Panel* this, …)` maps to a free fn `Foo(this: &mut Panel,
//! …)` (the same shape the `Panel.c` / `History.c` ports use). The C
//! `Panel*` return becomes an owned `Panel` (as `Panel_new` itself
//! returns), and a NULL return becomes `Option<Panel>::None`.
//!
//! The four `sched_*`-syscall helpers below (`Scheduling_setPolicy`,
//! `Scheduling_readProcessPolicy`, `Scheduling_newPriorityPanel`, and the
//! `Scheduling_rowSetPolicy` that delegates to the first) are the Linux
//! kernel scheduler syscalls `sched_setscheduler` / `sched_getscheduler` /
//! `sched_get_priority_min` / `sched_get_priority_max`. Because
//! `SCHEDULER_SUPPORT` is itself Linux-gated in htop, their bodies are
//! `#[cfg(target_os = "linux")]` over
//! `libc::{sched_setscheduler, sched_getscheduler, sched_get_priority_min,
//! sched_get_priority_max, sched_param}`. On a non-Linux target — the
//! darwin dev host — `SCHEDULER_SUPPORT` is undefined and the C functions
//! are compiled out entirely; the `#[cfg(not(target_os = "linux"))]` arms
//! reproduce that absence as a no-op (the panel/set helpers yield "no
//! panel"/"not set", the reader leaves `scheduling_policy` untouched).
//! This is the faithful non-`SCHEDULER_SUPPORT` behavior, not a fake
//! syscall, and keeps `cargo build` clean on darwin (which lacks the
//! Linux-only `sched_setscheduler`/`sched_getscheduler` symbols).
//!
//! Ported:
//! - `Scheduling_newPolicyPanel` (`Scheduling.c:42`) — builds the
//!   "New policy" `Panel` of `ListItem`s (the reset-on-fork toggle row,
//!   then one row per named policy) on the ported `Panel` / `ListItem` /
//!   `FunctionBar` substrate. Each row is built with a `ListItem { value,
//!   key, moving: false }` struct literal (the same value `ListItem_new` +
//!   `ListItem_init` produce), matching the `panel.rs` tests.
//! - `Scheduling_togglePolicyPanelResetOnFork` (`Scheduling.c:62`) — flips
//!   the file-static `reset_on_fork` flag and rewrites the panel's row 0.
//! - `Scheduling_newPriorityPanel` (`Scheduling.c:74`) — validates the
//!   policy (platform-independent early returns), then on Linux ranges the
//!   rows over `sched_get_priority_min(policy)..=sched_get_priority_max`.
//! - `Scheduling_setPolicy` (`Scheduling.c:102`) — builds a `sched_param`
//!   and calls `sched_setscheduler` on Linux.
//! - `Scheduling_rowSetPolicy` (`Scheduling.c:124`) — the `Row*`→`Process*`
//!   cast is modeled as `Object_isA(&Process_class)` + an `Any` downcast to
//!   `&Process`, then delegates to `Scheduling_setPolicy`.
//! - `Scheduling_formatPolicy` (`Scheduling.c:130`) — the pure policy-id →
//!   short display-string map.
//! - `Scheduling_readProcessPolicy` (`Scheduling.c:162`) — sets
//!   `proc->scheduling_policy = sched_getscheduler(pid)` on Linux.
//!
//! Stubbed: none.
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(dead_code)]

use core::any::Any;
use std::sync::atomic::{AtomicBool, Ordering};

use crate::ported::functionbar::FunctionBar_newEnterEsc;
use crate::ported::listitem::ListItem;
use crate::ported::object::{Object, Object_isA};
use crate::ported::panel::{Panel, Panel_add, Panel_new, Panel_setHeader, Panel_setSelected};
use crate::ported::process::{Process, Process_class};

// Linux-only substrate: the `ListItem_new` constructor is used solely by
// the Linux priority-panel body, and `Process_getPid` solely by the two
// syscall bodies — importing them unconditionally would be an unused
// import on the darwin build, so they are `cfg`-gated to match.
#[cfg(target_os = "linux")]
use crate::ported::listitem::ListItem_new;
#[cfg(target_os = "linux")]
use crate::ported::process::Process_getPid;

// Linux `<sched.h>` scheduling-policy constants, used by
// `Scheduling_formatPolicy` and by the `policies` table below. Values
// match `libc` 0.2.186 (`unix/linux_like`): SCHED_OTHER=0, SCHED_FIFO=1,
// SCHED_RR=2, SCHED_BATCH=3, SCHED_IDLE=5, SCHED_DEADLINE=6,
// SCHED_RESET_ON_FORK=0x40000000. On the platforms where htop defines
// `SCHEDULER_SUPPORT` these are all available, so every `#ifdef` arm of
// the C switch is included.
const SCHED_OTHER: i32 = 0;
const SCHED_FIFO: i32 = 1;
const SCHED_RR: i32 = 2;
const SCHED_BATCH: i32 = 3;
const SCHED_IDLE: i32 = 5;
const SCHED_DEADLINE: i32 = 6;
const SCHED_RESET_ON_FORK: i32 = 0x4000_0000;

/// Port of `typedef struct { const char* name; int id; bool
/// prioritySupport; } SchedulingPolicy` from `Scheduling.h:22`. `name` is
/// `Option<&'static str>` so the designated-initializer hole in the
/// `policies` table (see below) is a faithful `None` (C `NULL`).
struct SchedulingPolicy {
    name: Option<&'static str>,
    id: i32,
    prioritySupport: bool,
}

/// Port of `static const SchedulingPolicy policies[]` from
/// `Scheduling.c:25`. The C array uses C99 designated initializers keyed
/// by the `SCHED_*` id (`[SCHED_OTHER] = …`), so the entries land at
/// their id's index and the largest id present (`SCHED_IDLE == 5`) fixes
/// `ARRAYSIZE(policies) == 6`. Index `4` is never assigned, so it is the
/// zero-initialized hole (`{ NULL, 0, false }`) the `!policies[i].name`
/// guard in `Scheduling_newPolicyPanel` skips. Reproduced index-for-index
/// (note `SCHED_DEADLINE` is deliberately absent — it appears only in
/// `Scheduling_formatPolicy`).
static policies: [SchedulingPolicy; 6] = [
    // [SCHED_OTHER == 0]
    SchedulingPolicy {
        name: Some("Other"),
        id: SCHED_OTHER,
        prioritySupport: false,
    },
    // [SCHED_FIFO == 1]
    SchedulingPolicy {
        name: Some("FiFo"),
        id: SCHED_FIFO,
        prioritySupport: true,
    },
    // [SCHED_RR == 2]
    SchedulingPolicy {
        name: Some("RoundRobin"),
        id: SCHED_RR,
        prioritySupport: true,
    },
    // [SCHED_BATCH == 3]
    SchedulingPolicy {
        name: Some("Batch"),
        id: SCHED_BATCH,
        prioritySupport: false,
    },
    // [4] — the zero-initialized designated-initializer hole (C NULL name)
    SchedulingPolicy {
        name: None,
        id: 0,
        prioritySupport: false,
    },
    // [SCHED_IDLE == 5]
    SchedulingPolicy {
        name: Some("Idle"),
        id: SCHED_IDLE,
        prioritySupport: false,
    },
];

/// Port of `static bool reset_on_fork` from `Scheduling.c:38` (guarded by
/// `#ifdef SCHED_RESET_ON_FORK`). The file-static toggle shared between
/// `Scheduling_newPolicyPanel` (which seeds row 0's label from it) and
/// `Scheduling_togglePolicyPanelResetOnFork` (which flips it), modeled as
/// an atomic file-static exactly like `FunctionBar.c`'s `currentLen`.
static reset_on_fork: AtomicBool = AtomicBool::new(false);

/// Port of `typedef struct { int policy; int priority; } SchedulingArg`
/// from `Scheduling.h:37`. The callback payload `Scheduling_setPolicy`
/// reads out of the `Arg` union's `v` pointer; the ported
/// `Scheduling_setPolicy` / `Scheduling_rowSetPolicy` take a
/// `&SchedulingArg` in place of that `void*` (the faithful typed borrow).
/// `pub` because the C type lives in the public `Scheduling.h` header (and
/// so appears in the `pub fn` `Scheduling_setPolicy`/`_rowSetPolicy`
/// signatures).
pub struct SchedulingArg {
    pub policy: i32,
    pub priority: i32,
}

/// Port of `Panel* Scheduling_newPolicyPanel(int preSelectedPolicy)` from
/// `Scheduling.c:42`. Builds a `ListItem` panel with an Enter/Esc
/// function bar and the header "New policy:": row 0 is the reset-on-fork
/// toggle (label chosen from the `reset_on_fork` static), then one row per
/// `policies` entry with a non-NULL name (the hole at index 4 is skipped).
///
/// Faithful quirk: the C `Panel_setSelected(this, (int) i)` selects by the
/// `policies` *array* index `i`, not by the panel row position — with the
/// reset-on-fork row prepended these differ by one, so a preselected
/// policy highlights the row above its own. Ported verbatim (no fix).
pub fn Scheduling_newPolicyPanel(preSelectedPolicy: i32) -> Panel {
    let mut this = Panel_new(
        0,
        0,
        0,
        0,
        Some(FunctionBar_newEnterEsc("Select ", "Cancel ")),
    );
    Panel_setHeader(&mut this, "New policy:");

    // #ifdef SCHED_RESET_ON_FORK
    let rof = reset_on_fork.load(Ordering::Relaxed);
    Panel_add(
        &mut this,
        Box::new(ListItem {
            value: if rof {
                "Reset on fork: on".to_string()
            } else {
                "Reset on fork: off".to_string()
            },
            key: -1,
            moving: false,
        }),
    );

    for i in 0..policies.len() {
        let name = match policies[i].name {
            Some(n) => n,
            None => continue,
        };

        Panel_add(
            &mut this,
            Box::new(ListItem {
                value: name.to_string(),
                key: policies[i].id,
                moving: false,
            }),
        );
        if policies[i].id == preSelectedPolicy {
            Panel_setSelected(&mut this, i as i32);
        }
    }

    this
}

/// Port of `void Scheduling_togglePolicyPanelResetOnFork(Panel* schedPanel)`
/// from `Scheduling.c:62` (`#ifdef SCHED_RESET_ON_FORK`). Flips the
/// `reset_on_fork` static, then rewrites row 0's label to match.
///
/// The C reads the row via `Panel_get(schedPanel, 0)` and mutates
/// `item->value` through the returned pointer. The ported `Panel_get`
/// returns a shared `&dyn Object`, so the mutation instead goes through
/// the panel's `items` vector with a mutable `Any` downcast to `ListItem`
/// (the safe-Rust analog of the C `(ListItem*) Panel_get(...)` cast).
/// `free_and_xStrdup(&item->value, s)` (free old, dup new) is a plain
/// string assignment.
pub fn Scheduling_togglePolicyPanelResetOnFork(schedPanel: &mut Panel) {
    let rof = !reset_on_fork.load(Ordering::Relaxed);
    reset_on_fork.store(rof, Ordering::Relaxed);

    let item = (schedPanel.items[0].object_mut() as &mut dyn Any)
        .downcast_mut::<ListItem>()
        .expect("Scheduling_togglePolicyPanelResetOnFork: panel row 0 is not a ListItem");
    item.value = if rof {
        "Reset on fork: on".to_string()
    } else {
        "Reset on fork: off".to_string()
    };
}

/// Port of `Panel* Scheduling_newPriorityPanel(int policy, int
/// preSelectedPriority)` from `Scheduling.c:74`. Returns `None` (C `NULL`)
/// when `policy` is out of range, is the unnamed hole, or does not support
/// priorities — these guards are platform-independent so they run before
/// the `cfg` split. On Linux the row range is
/// `sched_get_priority_min(policy)..=sched_get_priority_max(policy)`
/// (each negative result short-circuits to `None`, as the C `return NULL`
/// does), with one `ListItem` per priority and the `preSelectedPriority`
/// row highlighted. Off Linux the syscall bounds do not exist
/// (`SCHEDULER_SUPPORT` undefined), so — after the shared guards — the
/// function yields `None`, matching the compiled-out C.
pub fn Scheduling_newPriorityPanel(policy: i32, preSelectedPriority: i32) -> Option<Panel> {
    if policy < 0 || policy as usize >= policies.len() || policies[policy as usize].name.is_none() {
        return None;
    }

    if !policies[policy as usize].prioritySupport {
        return None;
    }

    #[cfg(target_os = "linux")]
    {
        let min = unsafe { libc::sched_get_priority_min(policy) };
        if min < 0 {
            return None;
        }
        let max = unsafe { libc::sched_get_priority_max(policy) };
        if max < 0 {
            return None;
        }

        let mut this = Panel_new(
            0,
            0,
            0,
            0,
            Some(FunctionBar_newEnterEsc("Select ", "Cancel ")),
        );
        Panel_setHeader(&mut this, "Priority:");

        for i in min..=max {
            // xSnprintf(buf, sizeof(buf), "%d", i)
            let buf = format!("{}", i);
            Panel_add(&mut this, Box::new(ListItem_new(&buf, i)));
            if i == preSelectedPriority {
                Panel_setSelected(&mut this, i);
            }
        }

        Some(this)
    }

    #[cfg(not(target_os = "linux"))]
    {
        // SCHEDULER_SUPPORT is undefined off Linux: no priority-min/max
        // syscalls exist, so there is no panel to build.
        let _ = preSelectedPriority;
        None
    }
}

/// Port of `static bool Scheduling_setPolicy(Process* p, Arg arg)` from
/// `Scheduling.c:102`. The C reads its payload out of the `Arg` union
/// (`const SchedulingArg* sarg = arg.v`); the faithful safe-Rust analog
/// takes the typed payload `&SchedulingArg` directly (the pointer is only
/// ever dereferenced, so — unlike the `Affinity` `Arg` precedent that
/// keeps a never-dereferenced raw pointer — passing the concrete borrow is
/// the honest model). The three `assert()`s become `debug_assert!` (the C
/// `assert()` is `NDEBUG`-gated), matching the `vector.rs` precedent.
///
/// On Linux the priority comes from `sarg->priority` only when the policy
/// supports it (else `0`), the `reset_on_fork` static folds into `policy`
/// (the C `policy &= SCHED_RESET_ON_FORK` is ported verbatim, `&=` and
/// all), and `sched_setscheduler` is invoked; `r != -1` is the result
/// (POSIX returns the previous policy, Linux always `0`). Off Linux the
/// syscall does not exist (`SCHEDULER_SUPPORT` undefined), so the no-op
/// arm reports "not set" (`false`).
pub fn Scheduling_setPolicy(p: &Process, arg: &SchedulingArg) -> bool {
    let sarg = arg;
    let policy = sarg.policy;

    debug_assert!(policy >= 0);
    debug_assert!((policy as usize) < policies.len());
    debug_assert!(policies[policy as usize].name.is_some());

    #[cfg(target_os = "linux")]
    {
        let param = libc::sched_param {
            sched_priority: if policies[policy as usize].prioritySupport {
                sarg.priority
            } else {
                0
            },
        };

        // #ifdef SCHED_RESET_ON_FORK
        let mut policy = policy;
        if reset_on_fork.load(Ordering::Relaxed) {
            policy &= SCHED_RESET_ON_FORK;
        }

        let r = unsafe { libc::sched_setscheduler(Process_getPid(p), policy, &param) };

        // POSIX says on success the previous scheduling policy should be
        // returned, but Linux always returns 0.
        r != -1
    }

    #[cfg(not(target_os = "linux"))]
    {
        // SCHEDULER_SUPPORT is undefined off Linux: sched_setscheduler
        // does not exist, so no policy can be set.
        let _ = p;
        false
    }
}

/// Port of `bool Scheduling_rowSetPolicy(Row* row, Arg arg)` from
/// `Scheduling.c:124`. The C `Process* p = (Process*) row` cast (safe
/// because `Row` is the first member of `Process`) is modeled as an
/// `Object_isA(&Process_class)` guard followed by an `Any` downcast to
/// `&Process` — the same `(&dyn Object as &dyn Any).downcast_ref` idiom
/// `Process_compare` uses for its `const void*` cast. The C `assert(...)`
/// becomes `debug_assert!` (matching `vector.rs`). Delegates to
/// [`Scheduling_setPolicy`].
pub fn Scheduling_rowSetPolicy(row: &dyn Object, arg: &SchedulingArg) -> bool {
    // Process* p = (Process*) row;
    // assert(Object_isA((const Object*) p, (const ObjectClass*) &Process_class));
    debug_assert!(Object_isA(Some(row), &Process_class));
    let any: &dyn Any = row;
    let p = any
        .downcast_ref::<Process>()
        .expect("Scheduling_rowSetPolicy: row is not a Process");
    Scheduling_setPolicy(p, arg)
}

/// Port of `Scheduling.c:130`.
///
/// `const char* Scheduling_formatPolicy(int policy)`. Strips the
/// `SCHED_RESET_ON_FORK` bit, then maps the base policy id to its short
/// display string, returning `"???"` for anything unrecognized. The C
/// returns a `const char*` into static storage; Rust returns the
/// equivalent `&'static str`.
pub fn Scheduling_formatPolicy(policy: i32) -> &'static str {
    let policy = policy & !SCHED_RESET_ON_FORK;

    match policy {
        SCHED_OTHER => "OTHER",
        SCHED_FIFO => "FIFO",
        SCHED_RR => "RR",
        SCHED_BATCH => "BATCH",
        SCHED_IDLE => "IDLE",
        SCHED_DEADLINE => "EDF",
        _ => "???",
    }
}

/// Port of `void Scheduling_readProcessPolicy(Process* proc)` from
/// `Scheduling.c:162`: `proc->scheduling_policy =
/// sched_getscheduler(Process_getPid(proc))`. On Linux the syscall fills
/// the field; off Linux (`SCHEDULER_SUPPORT` undefined) the C function is
/// compiled out, so the no-op arm leaves `scheduling_policy` untouched.
/// (`proc` is a Rust-reserved word, so the parameter is spelled `proc_`.)
pub fn Scheduling_readProcessPolicy(proc_: &mut Process) {
    #[cfg(target_os = "linux")]
    {
        proc_.scheduling_policy = unsafe { libc::sched_getscheduler(Process_getPid(proc_)) };
    }

    #[cfg(not(target_os = "linux"))]
    {
        let _ = proc_;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Mutex;

    // `reset_on_fork` is a process-wide file-static; the two tests that
    // read/flip it must not run concurrently. Serialize them and reset the
    // flag to a known state at the start of each.
    static GLOBAL_LOCK: Mutex<()> = Mutex::new(());

    /// Downcast a panel row to its `ListItem` value for assertions.
    fn row_value(p: &Panel, i: usize) -> &str {
        let any: &dyn Any = p.items[i].object();
        &any.downcast_ref::<ListItem>().unwrap().value
    }

    #[test]
    fn maps_each_known_policy() {
        assert_eq!(Scheduling_formatPolicy(SCHED_OTHER), "OTHER");
        assert_eq!(Scheduling_formatPolicy(SCHED_FIFO), "FIFO");
        assert_eq!(Scheduling_formatPolicy(SCHED_RR), "RR");
        assert_eq!(Scheduling_formatPolicy(SCHED_BATCH), "BATCH");
        assert_eq!(Scheduling_formatPolicy(SCHED_IDLE), "IDLE");
        // SCHED_DEADLINE renders as "EDF", not "DEADLINE".
        assert_eq!(Scheduling_formatPolicy(SCHED_DEADLINE), "EDF");
    }

    #[test]
    fn unknown_policy_is_question_marks() {
        assert_eq!(Scheduling_formatPolicy(4), "???");
        assert_eq!(Scheduling_formatPolicy(7), "???");
        assert_eq!(Scheduling_formatPolicy(-1), "???");
    }

    #[test]
    fn reset_on_fork_bit_is_stripped_before_lookup() {
        // The RESET_ON_FORK flag must not change the rendered policy name.
        assert_eq!(
            Scheduling_formatPolicy(SCHED_FIFO | SCHED_RESET_ON_FORK),
            "FIFO"
        );
        assert_eq!(
            Scheduling_formatPolicy(SCHED_OTHER | SCHED_RESET_ON_FORK),
            "OTHER"
        );
        // Bare flag with no base policy masks down to SCHED_OTHER (0).
        assert_eq!(Scheduling_formatPolicy(SCHED_RESET_ON_FORK), "OTHER");
    }

    #[test]
    fn new_policy_panel_builds_reset_row_then_named_policies() {
        let _guard = GLOBAL_LOCK.lock().unwrap();
        reset_on_fork.store(false, Ordering::Relaxed);

        let p = Scheduling_newPolicyPanel(SCHED_RR);

        // Row 0 is the reset-on-fork toggle (off by default), then the five
        // named policies in `policies` array order (the index-4 hole is
        // skipped): Other, FiFo, RoundRobin, Batch, Idle.
        assert_eq!(p.items.len(), 6);
        assert_eq!(row_value(&p, 0), "Reset on fork: off");
        assert_eq!(row_value(&p, 1), "Other");
        assert_eq!(row_value(&p, 2), "FiFo");
        assert_eq!(row_value(&p, 3), "RoundRobin");
        assert_eq!(row_value(&p, 4), "Batch");
        assert_eq!(row_value(&p, 5), "Idle");

        // preSelectedPolicy == SCHED_RR (id 2) selects by the policies array
        // index i == 2 (the verbatim C quirk — that row is "FiFo", not
        // "RoundRobin", because row 0 is the reset toggle).
        assert_eq!(p.selected, 2);
    }

    #[test]
    fn new_policy_panel_reflects_reset_on_fork_on() {
        let _guard = GLOBAL_LOCK.lock().unwrap();
        reset_on_fork.store(true, Ordering::Relaxed);

        let p = Scheduling_newPolicyPanel(SCHED_OTHER);
        assert_eq!(row_value(&p, 0), "Reset on fork: on");
        // preSelectedPolicy SCHED_OTHER (id 0) -> array index 0.
        assert_eq!(p.selected, 0);

        reset_on_fork.store(false, Ordering::Relaxed);
    }

    #[test]
    fn toggle_flips_global_and_row_zero() {
        let _guard = GLOBAL_LOCK.lock().unwrap();
        reset_on_fork.store(false, Ordering::Relaxed);

        let mut p = Scheduling_newPolicyPanel(SCHED_OTHER);
        assert_eq!(row_value(&p, 0), "Reset on fork: off");

        Scheduling_togglePolicyPanelResetOnFork(&mut p);
        assert!(reset_on_fork.load(Ordering::Relaxed));
        assert_eq!(row_value(&p, 0), "Reset on fork: on");

        Scheduling_togglePolicyPanelResetOnFork(&mut p);
        assert!(!reset_on_fork.load(Ordering::Relaxed));
        assert_eq!(row_value(&p, 0), "Reset on fork: off");
    }

    // --- Scheduling_newPriorityPanel: platform-independent guards -----------
    // The `policy` validity / prioritySupport early returns (Scheduling.c:75,
    // 78) run before the `#[cfg(target_os = "linux")]` split, so they yield
    // `None` on every target and are testable on the darwin dev host.

    #[test]
    fn priority_panel_rejects_out_of_range_policy() {
        // policy < 0  and  policy >= ARRAYSIZE(policies) (== 6).
        assert!(Scheduling_newPriorityPanel(-1, 50).is_none());
        assert!(Scheduling_newPriorityPanel(6, 50).is_none());
        assert!(Scheduling_newPriorityPanel(100, 50).is_none());
    }

    #[test]
    fn priority_panel_rejects_unnamed_hole() {
        // Index 4 is the designated-initializer hole (name == None): the
        // `policies[policy].name == NULL` guard rejects it.
        assert!(policies[4].name.is_none());
        assert!(Scheduling_newPriorityPanel(4, 50).is_none());
    }

    #[test]
    fn priority_panel_rejects_policies_without_priority_support() {
        // OTHER (0), BATCH (3), IDLE (5) have prioritySupport == false.
        assert!(Scheduling_newPriorityPanel(SCHED_OTHER, 50).is_none());
        assert!(Scheduling_newPriorityPanel(SCHED_BATCH, 50).is_none());
        assert!(Scheduling_newPriorityPanel(SCHED_IDLE, 50).is_none());
    }

    // --- Scheduling_rowSetPolicy: the Row*->Process* cast ------------------
    // The `Object_isA` guard + `Any` downcast run before the syscall `cfg`
    // split, so the type-check behavior is testable on any target.

    #[test]
    fn row_set_policy_accepts_a_process() {
        // A Process IS-A Row/Process; the debug_assert + downcast succeed
        // and control reaches the (cfg'd) syscall body. On non-Linux the
        // no-op body returns false; on Linux setting the current process to
        // its existing policy is exercised by the linux-only test below.
        let p = Process::default();
        let arg = SchedulingArg {
            policy: SCHED_OTHER,
            priority: 0,
        };
        let obj: &dyn Object = &p;
        let r = Scheduling_rowSetPolicy(obj, &arg);
        #[cfg(not(target_os = "linux"))]
        assert!(!r);
        // On Linux the result depends on privileges; just require it ran.
        #[cfg(target_os = "linux")]
        let _ = r;
    }

    #[test]
    #[should_panic]
    fn row_set_policy_rejects_non_process() {
        // A ListItem is not a Process; Object_isA(&Process_class) is false,
        // so the debug_assert fires (the C assert()).
        let item = ListItem {
            value: String::new(),
            key: 0,
            moving: false,
        };
        let arg = SchedulingArg {
            policy: SCHED_OTHER,
            priority: 0,
        };
        let obj: &dyn Object = &item;
        let _ = Scheduling_rowSetPolicy(obj, &arg);
    }

    // --- Linux-only: the real syscalls ------------------------------------

    #[cfg(target_os = "linux")]
    #[test]
    fn priority_panel_builds_for_fifo_on_linux() {
        // SCHED_FIFO supports priorities; the panel spans
        // sched_get_priority_min..=max, one row each, header "Priority:".
        let min = unsafe { libc::sched_get_priority_min(SCHED_FIFO) };
        let max = unsafe { libc::sched_get_priority_max(SCHED_FIFO) };
        assert!(min >= 0 && max >= min);

        let pre = min; // preselect the first priority
        let panel = Scheduling_newPriorityPanel(SCHED_FIFO, pre)
            .expect("FIFO priority panel must be Some on Linux");

        let expected_rows = (max - min + 1) as usize;
        assert_eq!(panel.items.len(), expected_rows);
        // First row's value is the numeric min; selection landed on it.
        assert_eq!(row_value(&panel, 0), format!("{}", min));
        assert_eq!(panel.selected, min);
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn read_process_policy_reads_own_scheduler_on_linux() {
        // sched_getscheduler(getpid()) on an ordinary process is a valid,
        // non-negative policy (SCHED_OTHER == 0 for a normal process).
        let pid = unsafe { libc::getpid() };
        let mut proc_ = Process::default();
        proc_.super_.id = pid;

        Scheduling_readProcessPolicy(&mut proc_);

        let direct = unsafe { libc::sched_getscheduler(pid) };
        assert!(direct >= 0);
        assert_eq!(proc_.scheduling_policy, direct);
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn set_policy_to_other_succeeds_on_linux() {
        // Setting the current process to SCHED_OTHER with priority 0 is
        // permitted without privileges (it is a no-op if already OTHER).
        let pid = unsafe { libc::getpid() };
        let mut proc_ = Process::default();
        proc_.super_.id = pid;

        let arg = SchedulingArg {
            policy: SCHED_OTHER,
            priority: 0,
        };
        assert!(Scheduling_setPolicy(&proc_, &arg));
    }
}