eggress-testkit 1.0.4

Test utilities for eggress proxy
Documentation
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
//! Reusable differential test harness for comparing eggress with Python pproxy.
//!
//! All tests using this harness are gated on `EGGRESS_RUN_PPROXY_DIFFERENTIAL=1`
//! and require Python 3 with pproxy installed (`pip install pproxy==2.7.9`).
//!
//! # Usage
//!
//! ```rust,no_run
//! use eggress_testkit::differential::*;
//!
//! # async fn example() {
//! require_differential_gate();
//!
//! let (echo_addr, echo_jh) = eggress_testkit::start_echo_server().await;
//! let mut pproxy = start_pproxy_server("socks5", 1080).await;
//! // ... run tests ...
//! pproxy.kill();
//! echo_jh.abort();
//! # }
//! ```

use std::net::SocketAddr;
use std::time::Duration;
use tokio::io::AsyncReadExt;

/// Environment variable that gates differential tests.
pub const GATE_VAR: &str = "EGRESS_RUN_PPROXY_DIFFERENTIAL";

/// Pinned pproxy version for reproducible test results.
pub const PINNED_PPROXY_VERSION: &str = "2.7.9";

/// Canonical environment variable for the oracle Python interpreter.
///
/// Set by the top-level certification runner. Resolution order:
/// 1. `EGRESS_ORACLE_PYTHON`
/// 2. `EGRESS_PYTHON_BIN` (legacy fallback for standalone use)
/// 3. discovery of system python with pproxy
pub const ORACLE_PYTHON_VAR: &str = "EGRESS_ORACLE_PYTHON";

/// Legacy environment variable for the Python binary path.
///
/// Retained for standalone developer use. During certification,
/// `EGRESS_ORACLE_PYTHON` takes precedence.
pub const LEGACY_PYTHON_VAR: &str = "EGGRESS_PYTHON_BIN";

/// Deprecated alias — prefer [`ORACLE_PYTHON_VAR`] or [`LEGACY_PYTHON_VAR`].
pub const PYTHON_BIN_VAR: &str = LEGACY_PYTHON_VAR;

/// Check if the differential test gate is enabled.
pub fn differential_gate_enabled() -> bool {
    std::env::var(GATE_VAR).map(|v| v == "1").unwrap_or(false)
}

/// Require the differential gate to be enabled.
///
/// Panics with a clear message if `EGRESS_RUN_PPROXY_DIFFERENTIAL` is not set
/// or if the pproxy package is not installed.
pub fn require_differential_gate() {
    if !differential_gate_enabled() {
        panic!(
            "differential tests require {}=1 and pproxy=={}",
            GATE_VAR, PINNED_PPROXY_VERSION
        );
    }
    if !pproxy_available() {
        panic!(
            "pproxy not available; install with: pip install pproxy=={}",
            PINNED_PPROXY_VERSION
        );
    }
}

/// Validate that a Python interpreter has pproxy==PINNED_PPROXY_VERSION.
///
/// Uses `importlib.metadata.version("pproxy")` for reliable distribution
/// metadata checks rather than module `__version__` attributes.
fn validate_oracle_python(path: &str) -> Result<String, String> {
    let output = std::process::Command::new(path)
        .args([
            "-c",
            "from importlib.metadata import version; print(version('pproxy'))",
        ])
        .output()
        .map_err(|e| format!("failed to execute {}: {}", path, e))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!("{} cannot import pproxy: {}", path, stderr.trim()));
    }

    let actual = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if actual != PINNED_PPROXY_VERSION {
        return Err(format!(
            "expected pproxy=={}, got {} at {}",
            PINNED_PPROXY_VERSION, actual, path
        ));
    }

    Ok(path.to_string())
}

/// Find the oracle Python interpreter with version validation.
///
/// Resolution order:
/// 1. `EGRESS_ORACLE_PYTHON` — must have pproxy==PINNED_PPROXY_VERSION
/// 2. `EGRESS_PYTHON_BIN` — must have pproxy==PINNED_PPROXY_VERSION
///
/// When `require_explicit` is true (certification mode), missing or invalid
/// interpreters cause a panic. When false, falls back to discovery.
pub fn find_oracle_python(require_explicit: bool) -> String {
    if let Ok(path) = std::env::var(ORACLE_PYTHON_VAR) {
        return validate_oracle_python(&path).unwrap_or_else(|e| {
            if require_explicit {
                panic!("certification oracle interpreter: {}", e);
            }
            eprintln!("WARNING: {}", e);
            find_python_binary()
        });
    }

    if let Ok(path) = std::env::var(LEGACY_PYTHON_VAR) {
        return validate_oracle_python(&path).unwrap_or_else(|e| {
            if require_explicit {
                panic!("certification oracle interpreter: {}", e);
            }
            eprintln!("WARNING: {}", e);
            find_python_binary()
        });
    }

    if require_explicit {
        panic!(
            "certification requires {} to point to pproxy=={}",
            ORACLE_PYTHON_VAR, PINNED_PPROXY_VERSION
        );
    }

    find_python_binary()
}

/// Find a working Python binary that has pproxy installed.
///
/// Checks `EGRESS_ORACLE_PYTHON` first, then `EGRESS_PYTHON_BIN`,
/// then tries `python3.11`, `python3.12`, `python3.13`, and finally `python3`.
pub fn find_python_binary() -> String {
    if let Ok(path) = std::env::var(ORACLE_PYTHON_VAR) {
        if std::process::Command::new(&path)
            .args(["-c", "import pproxy"])
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .map(|s| s.success())
            .unwrap_or(false)
        {
            return path;
        }
    }
    if let Ok(path) = std::env::var(LEGACY_PYTHON_VAR) {
        if std::process::Command::new(&path)
            .args(["-c", "import pproxy"])
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .map(|s| s.success())
            .unwrap_or(false)
        {
            return path;
        }
    }
    for candidate in &["python3.11", "python3.12", "python3.13", "python3"] {
        if std::process::Command::new(candidate)
            .args(["-c", "import pproxy"])
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .map(|s| s.success())
            .unwrap_or(false)
        {
            return candidate.to_string();
        }
    }
    panic!(
        "no Python binary with pproxy found; install pproxy: pip install pproxy=={}",
        PINNED_PPROXY_VERSION
    );
}

fn pproxy_available() -> bool {
    let python = find_python_binary();
    std::process::Command::new(&python)
        .args(["-c", "import pproxy"])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

// ===== Process Management =====

/// RAII guard that kills a child process on drop.
///
/// Wraps `std::process::Child` and ensures the process is killed and waited
/// on when the guard is dropped. Call [`kill`](ProcessGuard::kill) explicitly
/// to terminate early.
pub struct ProcessGuard {
    child: Option<std::process::Child>,
}

impl ProcessGuard {
    /// Create a new guard wrapping the given child process.
    pub fn new(child: std::process::Child) -> Self {
        Self { child: Some(child) }
    }

    /// Kill the process early (before drop).
    pub fn kill(&mut self) {
        if let Some(ref mut child) = self.child {
            let _ = child.kill();
            let _ = child.wait();
        }
    }

    /// Drain and return all available stderr output from the process.
    pub fn drain_stderr(&mut self) -> String {
        use std::io::Read;
        if let Some(ref mut child) = self.child {
            if let Some(ref mut stderr) = child.stderr {
                let mut output = String::new();
                let _ = stderr.read_to_string(&mut output);
                return output;
            }
        }
        String::new()
    }
}

impl Drop for ProcessGuard {
    fn drop(&mut self) {
        if let Some(ref mut child) = self.child {
            let _ = child.kill();
            let _ = child.wait();
        }
    }
}

// ===== Pproxy Process Management =====

/// Start a pproxy server with the given protocol and port.
///
/// Uses the resolved oracle interpreter (checks `EGRESS_ORACLE_PYTHON` first)
/// to spawn `python -m pproxy -l {proto}://127.0.0.1:{port} -r direct`.
/// Returns a [`ProcessGuard`] that kills the process on drop.
pub async fn start_pproxy_server(protocol: &str, port: u16) -> ProcessGuard {
    let python = find_oracle_python(false);
    let listen = format!("{}://127.0.0.1:{}", protocol, port);
    let child = std::process::Command::new(&python)
        .args(["-m", "pproxy", "-l", &listen, "-r", "direct"])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .expect("failed to start pproxy");
    ProcessGuard::new(child)
}

/// Start a pproxy server with username/password authentication.
pub async fn start_pproxy_server_with_auth(
    protocol: &str,
    port: u16,
    username: &str,
    password: &str,
) -> ProcessGuard {
    let python = find_oracle_python(false);
    let listen = format!(
        "{}://127.0.0.1:{}#{}:{}",
        protocol, port, username, password
    );
    let child = std::process::Command::new(&python)
        .args(["-m", "pproxy", "-l", &listen, "-r", "direct"])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .expect("failed to start pproxy");
    ProcessGuard::new(child)
}

/// Start a pproxy server with arbitrary CLI arguments.
pub async fn start_pproxy_with_args(args: &[&str]) -> ProcessGuard {
    let python = find_oracle_python(false);
    let child = std::process::Command::new(&python)
        .args(["-m", "pproxy"])
        .args(args)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .expect("failed to start pproxy");
    ProcessGuard::new(child)
}

/// Wait for a TCP port to become reachable.
///
/// Returns `true` if the port is reachable within the timeout, `false` otherwise.
pub async fn wait_for_port(port: u16, timeout: Duration) -> bool {
    let start = std::time::Instant::now();
    while start.elapsed() < timeout {
        if tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port))
            .await
            .is_ok()
        {
            return true;
        }
        tokio::time::sleep(Duration::from_millis(50)).await;
    }
    false
}

/// Assert that a port becomes reachable within the timeout.
///
/// Panics if the port does not become ready.
pub async fn assert_port_ready(port: u16, timeout: Duration) {
    assert!(
        wait_for_port(port, timeout).await,
        "port {port} not ready within {}ms",
        timeout.as_millis()
    );
}

// ===== Echo Servers =====

/// Start a UDP echo server that echoes received packets back to the sender.
///
/// Returns the listening address and a join handle.
pub async fn start_udp_echo() -> (SocketAddr, tokio::task::JoinHandle<()>) {
    let socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
    let addr = socket.local_addr().unwrap();
    let jh = tokio::spawn(async move {
        let mut buf = [0u8; 65535];
        while let Ok((n, peer)) = socket.recv_from(&mut buf).await {
            let _ = socket.send_to(&buf[..n], peer).await;
        }
    });
    (addr, jh)
}

// ===== SOCKS5 UDP Utilities =====

/// Build a SOCKS5 UDP datagram with an IPv4 or IPv6 target.
pub fn build_socks5_udp_packet(target: SocketAddr, payload: &[u8]) -> Vec<u8> {
    let mut pkt = vec![0x00, 0x00, 0x00]; // RSV + FRAG
    match target.ip() {
        std::net::IpAddr::V4(ip) => {
            pkt.push(0x01); // ATYP IPv4
            pkt.extend_from_slice(&ip.octets());
        }
        std::net::IpAddr::V6(ip) => {
            pkt.push(0x04); // ATYP IPv6
            pkt.extend_from_slice(&ip.octets());
        }
    }
    pkt.extend_from_slice(&target.port().to_be_bytes());
    pkt.extend_from_slice(payload);
    pkt
}

/// Build a SOCKS5 UDP datagram with a domain target.
pub fn build_socks5_udp_packet_domain(host: &str, port: u16, payload: &[u8]) -> Vec<u8> {
    let mut pkt = vec![0x00, 0x00, 0x00]; // RSV + FRAG
    pkt.push(0x03); // ATYP Domain
    pkt.push(host.len() as u8);
    pkt.extend_from_slice(host.as_bytes());
    pkt.extend_from_slice(&port.to_be_bytes());
    pkt.extend_from_slice(payload);
    pkt
}

/// Build a SOCKS5 UDP datagram with a custom FRAG field.
pub fn build_socks5_udp_packet_frag(target: SocketAddr, frag: u8, payload: &[u8]) -> Vec<u8> {
    let mut pkt = vec![0x00, 0x00, frag]; // RSV + FRAG
    match target.ip() {
        std::net::IpAddr::V4(ip) => {
            pkt.push(0x01);
            pkt.extend_from_slice(&ip.octets());
        }
        std::net::IpAddr::V6(ip) => {
            pkt.push(0x04);
            pkt.extend_from_slice(&ip.octets());
        }
    }
    pkt.extend_from_slice(&target.port().to_be_bytes());
    pkt.extend_from_slice(payload);
    pkt
}

/// Extract the payload from a SOCKS5 UDP datagram.
///
/// Parses the SOCKS5 UDP header (RSV + FRAG + ATYP + address) and returns
/// the payload bytes.
pub fn extract_udp_payload(datagram: &[u8]) -> Vec<u8> {
    if datagram.len() < 4 {
        return vec![];
    }
    let atyp = datagram[3];
    let header_len = match atyp {
        0x01 => 4 + 4 + 2,  // RSV(2) + FRAG(1) + ATYP(1) + IPv4(4) + PORT(2)
        0x04 => 4 + 16 + 2, // RSV(2) + FRAG(1) + ATYP(1) + IPv6(16) + PORT(2)
        0x03 => {
            if datagram.len() < 5 {
                return vec![];
            }
            let domain_len = datagram[4] as usize;
            4 + 1 + domain_len + 2 // RSV(2) + FRAG(1) + ATYP(1) + LEN(1) + DOMAIN + PORT(2)
        }
        _ => return vec![],
    };
    if datagram.len() <= header_len {
        return vec![];
    }
    datagram[header_len..].to_vec()
}

/// Receive a UDP response with a timeout.
///
/// Returns the raw datagram bytes, or `None` if no response is received
/// within the timeout.
pub async fn recv_udp_response(sock: &tokio::net::UdpSocket, timeout: Duration) -> Option<Vec<u8>> {
    let mut buf = [0u8; 65535];
    let deadline = std::time::Instant::now() + timeout;
    while std::time::Instant::now() < deadline {
        match tokio::time::timeout(Duration::from_millis(200), sock.recv_from(&mut buf)).await {
            Ok(Ok((n, _))) => return Some(buf[..n].to_vec()),
            _ => continue,
        }
    }
    None
}

// ===== Comparison Utilities =====

/// Read all available data from an `AsyncRead` within a timeout.
///
/// Reads chunks until the timeout expires or the remote closes. Returns the
/// accumulated bytes.
pub async fn read_with_timeout(
    reader: &mut (impl tokio::io::AsyncRead + Unpin),
    timeout: Duration,
) -> Vec<u8> {
    let mut buf = Vec::new();
    let mut tmp = [0u8; 4096];
    let deadline = std::time::Instant::now() + timeout;
    loop {
        let remaining = deadline.saturating_duration_since(std::time::Instant::now());
        if remaining.is_zero() {
            break;
        }
        match tokio::time::timeout(remaining, reader.read(&mut tmp)).await {
            Ok(Ok(0)) => break, // EOF
            Ok(Ok(n)) => buf.extend_from_slice(&tmp[..n]),
            Ok(Err(_)) => break,
            Err(_) => break, // timeout
        }
    }
    buf
}

/// Compare two TCP echo results.
///
/// Both results should be `Ok(Vec<u8>)` with identical payloads.
/// Panics with a descriptive message on mismatch.
pub fn compare_tcp_echo(
    label_a: &str,
    result_a: &Result<Vec<u8>, String>,
    label_b: &str,
    result_b: &Result<Vec<u8>, String>,
) {
    match (result_a, result_b) {
        (Ok(payload_a), Ok(payload_b)) => {
            assert_eq!(
                payload_a, payload_b,
                "TCP echo payload mismatch: {label_a} returned {} bytes, {label_b} returned {} bytes",
                payload_a.len(),
                payload_b.len()
            );
        }
        (Err(e), _) => panic!("{label_a} failed: {e}"),
        (_, Err(e)) => panic!("{label_b} failed: {e}"),
    }
}

/// Compare two UDP echo results.
///
/// Both should succeed with matching payloads.
pub fn compare_udp_echo(
    label_a: &str,
    result_a: &Option<Vec<u8>>,
    label_b: &str,
    result_b: &Option<Vec<u8>>,
) {
    match (result_a, result_b) {
        (Some(payload_a), Some(payload_b)) => {
            assert_eq!(
                payload_a, payload_b,
                "UDP echo payload mismatch: {label_a} returned {} bytes, {label_b} returned {} bytes",
                payload_a.len(),
                payload_b.len()
            );
        }
        (None, _) => panic!("{label_a} did not receive UDP response"),
        (_, None) => panic!("{label_b} did not receive UDP response"),
    }
}

/// Assert coarse failure equivalence: both succeeded or both failed.
///
/// This is useful when the exact payload may differ (e.g., different error
/// messages) but the success/failure class should match.
pub fn assert_coarse_failure_equivalence<T>(
    label_a: &str,
    result_a: &Result<T, String>,
    label_b: &str,
    result_b: &Result<T, String>,
) {
    match (result_a, result_b) {
        (Ok(_), Ok(_)) => {
            // Both succeeded — acceptable
        }
        (Err(e), Ok(_)) => {
            panic!("{label_a} failed but {label_b} succeeded: {label_a} error: {e}");
        }
        (Ok(_), Err(e)) => {
            panic!("{label_a} succeeded but {label_b} failed: {label_b} error: {e}");
        }
        (Err(e_a), Err(e_b)) => {
            // Both failed — acceptable
            eprintln!("both failed (expected): {label_a}: {e_a}, {label_b}: {e_b}");
        }
    }
}

// ===== HTTP Utilities =====

/// Extract the body from an HTTP response (after the first `\r\n\r\n`).
pub fn extract_http_body(response: &[u8]) -> String {
    let text = String::from_utf8_lossy(response);
    if let Some(pos) = text.find("\r\n\r\n") {
        text[pos + 4..].to_string()
    } else {
        text.to_string()
    }
}

/// Extract the HTTP status code from a response (e.g., "200" from "HTTP/1.1 200 OK").
pub fn extract_http_status(response: &[u8]) -> String {
    let text = String::from_utf8_lossy(response);
    text.lines()
        .next()
        .and_then(|line| line.split_whitespace().nth(1))
        .unwrap_or("unknown")
        .to_string()
}

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

    static ENV_LOCK: Mutex<()> = Mutex::new(());

    fn lock_env() -> std::sync::MutexGuard<'static, ()> {
        ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
    }

    fn with_env_reset<F: FnOnce()>(f: F) {
        let _lock = lock_env();
        let saved_oracle = std::env::var(ORACLE_PYTHON_VAR).ok();
        let saved_legacy = std::env::var(LEGACY_PYTHON_VAR).ok();

        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));

        match saved_oracle {
            Some(v) => std::env::set_var(ORACLE_PYTHON_VAR, v),
            None => std::env::remove_var(ORACLE_PYTHON_VAR),
        }
        match saved_legacy {
            Some(v) => std::env::set_var(LEGACY_PYTHON_VAR, v),
            None => std::env::remove_var(LEGACY_PYTHON_VAR),
        }

        if let Err(e) = result {
            std::panic::resume_unwind(e);
        }
    }

    #[test]
    fn nonexistent_interpreter_path_fails_clearly() {
        let result = validate_oracle_python("/nonexistent/python3.99");
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.contains("failed to execute"), "error: {}", err);
    }

    #[test]
    fn strict_certification_rejects_missing_explicit() {
        with_env_reset(|| {
            std::env::remove_var(ORACLE_PYTHON_VAR);
            std::env::remove_var(LEGACY_PYTHON_VAR);

            let result = std::panic::catch_unwind(|| {
                find_oracle_python(true);
            });

            assert!(result.is_err(), "should panic when require_explicit=true");
        });
    }

    #[test]
    fn pinned_version_matches() {
        assert_eq!(PINNED_PPROXY_VERSION, "2.7.9");
    }

    #[test]
    fn constant_names_are_correct() {
        assert_eq!(ORACLE_PYTHON_VAR, "EGRESS_ORACLE_PYTHON");
        assert_eq!(LEGACY_PYTHON_VAR, "EGGRESS_PYTHON_BIN");
        assert_eq!(PYTHON_BIN_VAR, LEGACY_PYTHON_VAR);
    }

    #[test]
    fn oracle_python_checked_before_legacy_in_find_binary() {
        with_env_reset(|| {
            // Set oracle to a nonexistent path, legacy to nonexistent
            std::env::set_var(ORACLE_PYTHON_VAR, "/nonexistent/oracle_py");
            std::env::set_var(LEGACY_PYTHON_VAR, "/nonexistent/legacy_py");

            // find_python_binary should check oracle first, then legacy, then system.
            // Since all are invalid, it should panic (system python without pproxy
            // may or may not be found — the key test is that oracle is checked first).
            let _result = std::panic::catch_unwind(|| {
                find_python_binary();
            });

            // Should panic because no valid python with pproxy found
            // (the nonexistent paths won't work, and system python may not have pproxy)
            // The important thing is no crash in the function itself.
            // If system python has pproxy, this won't panic — that's fine.
        });
    }
}