mirror of
https://github.com/ruvnet/RuView
synced 2026-08-11 20:41:44 +00:00
a0e72eef50
wifiscan (Tier 2 wlanapi adapter ONLY): - Real native wlanapi.dll BSS-list FFI (new adapter/wlanapi_native.rs): WlanOpenHandle -> WlanEnumInterfaces -> WlanGetNetworkBssList -> WlanFreeMemory/WlanCloseHandle via windows-sys 0.59 (already in lock tree). Per-BSSID RSSI(dBm)/channel/band/radio-type/SSID + CSI-capable filter. #[cfg(windows)] real path; #[cfg(not(windows))] returns typed WifiScanError::Unsupported (honest, never fabricated). - wlanapi_scanner now native-first with documented netsh fallback, native_scans metric, scan_native()/scan_native_csi_capable(), and a benchmark() that MEASURES real Hz (no hardcoded "10x" claim). - MEASURED 9.74 Hz native on ruvzen (30 iters, Native backend) vs netsh ~2 Hz baseline. Live measurement kept as an #[ignore] test. - Cargo.toml: unsafe_code forbid->deny so only the audited wlan_ffi module opts into unsafe; all unsafe confined + null-checked + freed. sensing-server (Matter commissioning): - Replaced the lossy modulo placeholder in matter/commissioning.rs with the real Matter Core Spec 1.3 §5.1.4.1.1 field-packing. Canonical vector (20202021, 3840) now encodes to the published 34970112332. - Added ManualPairingCode::decode + DecodedManualCode proving the code is real/lossless (passcode round-trips bit-for-bit; short discriminator = top 4 bits) with Verhoeff integrity, incl. proptest. Tests: wifi-densepose-wifiscan 145 passed (real FFI exercised on Windows); wifi-densepose-sensing-server 614 passed. 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net>
564 lines
20 KiB
Rust
564 lines
20 KiB
Rust
//! Tier 2: Windows WLAN API adapter with a native `wlanapi.dll` scan path.
|
||
//!
|
||
//! This adapter prefers the **native** [`wlanapi_native::scan_native`] FFI
|
||
//! (`WlanOpenHandle` → `WlanEnumInterfaces` → `WlanGetNetworkBssList`),
|
||
//! which reads the driver's cached BSS list with no `netsh.exe`
|
||
//! subprocess. The native read path is bounded by WLAN-service IPC rather
|
||
//! than a `CreateProcess` per scan (the Tier 1 [`NetshBssidScanner`]'s
|
||
//! ~2 Hz ceiling), so polling it in a loop can observe BSSID updates
|
||
//! faster. The exact achieved rate is **measured** by
|
||
//! [`WlanApiScanner::benchmark`] on the running machine, not assumed —
|
||
//! this module makes no fixed "10×" claim.
|
||
//!
|
||
//! When the native path is unavailable (non-Windows, or the WLAN service
|
||
//! returns an error) the adapter transparently falls back to the
|
||
//! documented `netsh` Tier 1 scanner, so callers always get a result on
|
||
//! Windows and a typed [`WifiScanError::Unsupported`] only where no
|
||
//! backend exists.
|
||
//!
|
||
//! # API
|
||
//!
|
||
//! - **Sync scan** via [`WlanScanPort`] (native-first, netsh fallback).
|
||
//! - **Native-only scan** via [`WlanApiScanner::scan_native`] (no
|
||
//! fallback; surfaces the platform gate honestly).
|
||
//! - **Async scan** (`"wlanapi"` feature) via `tokio::task::spawn_blocking`.
|
||
//! - **Scan metrics** + **measured-rate benchmark**.
|
||
//!
|
||
//! # Platform
|
||
//!
|
||
//! Native FFI is Windows-only and lives in [`wlanapi_native`]; the rest of
|
||
//! this module compiles everywhere.
|
||
|
||
use std::sync::atomic::{AtomicU64, Ordering};
|
||
use std::time::{Duration, Instant};
|
||
|
||
use crate::adapter::netsh_scanner::NetshBssidScanner;
|
||
use crate::adapter::wlanapi_native;
|
||
use crate::domain::bssid::BssidObservation;
|
||
use crate::error::WifiScanError;
|
||
use crate::port::WlanScanPort;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Scan metrics
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Accumulated metrics from scan operations.
|
||
#[derive(Debug, Clone)]
|
||
pub struct ScanMetrics {
|
||
/// Total number of scans performed since creation.
|
||
pub scan_count: u64,
|
||
/// Total number of BSSIDs observed across all scans.
|
||
pub total_bssids_observed: u64,
|
||
/// Duration of the most recent scan.
|
||
pub last_scan_duration: Option<Duration>,
|
||
/// Estimated scan rate in Hz based on the last scan duration.
|
||
/// Returns `None` if no scans have been performed yet.
|
||
pub estimated_rate_hz: Option<f64>,
|
||
/// How many scans so far used the native FFI path (vs the netsh
|
||
/// fallback). Lets callers verify the native path is actually live.
|
||
pub native_scans: u64,
|
||
}
|
||
|
||
/// Outcome of a measured scan-rate benchmark — MEASURED, not claimed.
|
||
#[derive(Debug, Clone)]
|
||
pub struct BenchmarkResult {
|
||
/// Number of scans actually executed.
|
||
pub iterations: u32,
|
||
/// Wall-clock time the benchmark took.
|
||
pub total: Duration,
|
||
/// Measured scans per second over the whole run.
|
||
pub rate_hz: f64,
|
||
/// Mean BSSIDs observed per scan.
|
||
pub mean_bssids: f64,
|
||
/// Which backend produced the samples.
|
||
pub backend: ScanBackend,
|
||
}
|
||
|
||
/// Which backend serviced a scan.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum ScanBackend {
|
||
/// Native `wlanapi.dll` BSS-list FFI.
|
||
Native,
|
||
/// `netsh wlan show networks` subprocess fallback.
|
||
Netsh,
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// WlanApiScanner
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Tier 2 WLAN API scanner: native-first with a netsh fallback, plus scan
|
||
/// metrics and a measured-rate benchmark.
|
||
///
|
||
/// # Example (sync)
|
||
///
|
||
/// ```no_run
|
||
/// use wifi_densepose_wifiscan::adapter::wlanapi_scanner::WlanApiScanner;
|
||
/// use wifi_densepose_wifiscan::port::WlanScanPort;
|
||
///
|
||
/// let scanner = WlanApiScanner::new();
|
||
/// let observations = scanner.scan().unwrap();
|
||
/// for obs in &observations {
|
||
/// println!("{}: {} dBm", obs.bssid, obs.rssi_dbm);
|
||
/// }
|
||
/// // Measure the REAL achieved rate on this machine (no hardcoded claim).
|
||
/// if let Ok(bench) = scanner.benchmark(20) {
|
||
/// println!("measured {:.1} Hz via {:?}", bench.rate_hz, bench.backend);
|
||
/// }
|
||
/// ```
|
||
pub struct WlanApiScanner {
|
||
/// The underlying Tier 1 scanner (fallback path).
|
||
inner: NetshBssidScanner,
|
||
|
||
/// Number of scans performed.
|
||
scan_count: AtomicU64,
|
||
|
||
/// Total BSSIDs observed across all scans.
|
||
total_bssids: AtomicU64,
|
||
|
||
/// Number of scans serviced by the native FFI path.
|
||
native_scans: AtomicU64,
|
||
|
||
/// Timestamp of the most recent scan start (for rate estimation).
|
||
last_scan_start: std::sync::Mutex<Option<Instant>>,
|
||
|
||
/// Duration of the most recent scan.
|
||
last_scan_duration: std::sync::Mutex<Option<Duration>>,
|
||
}
|
||
|
||
impl WlanApiScanner {
|
||
/// Create a new Tier 2 scanner.
|
||
pub fn new() -> Self {
|
||
Self {
|
||
inner: NetshBssidScanner::new(),
|
||
scan_count: AtomicU64::new(0),
|
||
total_bssids: AtomicU64::new(0),
|
||
native_scans: AtomicU64::new(0),
|
||
last_scan_start: std::sync::Mutex::new(None),
|
||
last_scan_duration: std::sync::Mutex::new(None),
|
||
}
|
||
}
|
||
|
||
/// Return accumulated scan metrics.
|
||
pub fn metrics(&self) -> ScanMetrics {
|
||
let scan_count = self.scan_count.load(Ordering::Relaxed);
|
||
let total_bssids_observed = self.total_bssids.load(Ordering::Relaxed);
|
||
let native_scans = self.native_scans.load(Ordering::Relaxed);
|
||
let last_scan_duration = *self
|
||
.last_scan_duration
|
||
.lock()
|
||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||
let estimated_rate_hz = last_scan_duration.map(|d| {
|
||
let secs = d.as_secs_f64();
|
||
if secs > 0.0 {
|
||
1.0 / secs
|
||
} else {
|
||
f64::INFINITY
|
||
}
|
||
});
|
||
|
||
ScanMetrics {
|
||
scan_count,
|
||
total_bssids_observed,
|
||
last_scan_duration,
|
||
estimated_rate_hz,
|
||
native_scans,
|
||
}
|
||
}
|
||
|
||
/// Return the number of scans performed so far.
|
||
pub fn scan_count(&self) -> u64 {
|
||
self.scan_count.load(Ordering::Relaxed)
|
||
}
|
||
|
||
/// Number of scans serviced by the native `wlanapi.dll` FFI path.
|
||
pub fn native_scan_count(&self) -> u64 {
|
||
self.native_scans.load(Ordering::Relaxed)
|
||
}
|
||
|
||
/// Whether the native path is available on this build/platform.
|
||
///
|
||
/// `true` on Windows (FFI compiled), `false` elsewhere. Honest report
|
||
/// of the platform gate without performing a scan.
|
||
pub fn native_available() -> bool {
|
||
cfg!(windows)
|
||
}
|
||
|
||
/// Run one native-only scan with **no** netsh fallback.
|
||
///
|
||
/// Returns [`WifiScanError::Unsupported`] on non-Windows, or a
|
||
/// [`WifiScanError::ScanFailed`] if the WLAN service rejects the call.
|
||
/// Use this when a caller must know whether the native path worked.
|
||
pub fn scan_native(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
|
||
let start = Instant::now();
|
||
let results = wlanapi_native::scan_native()?;
|
||
self.record(start, results.len(), true);
|
||
Ok(results)
|
||
}
|
||
|
||
/// Run one native scan and return only the **CSI-capable** APs.
|
||
///
|
||
/// Filters the native BSS list to access points whose advertised PHY
|
||
/// (HT/VHT/HE/EHT) supports channel sounding — the candidates usable as
|
||
/// a CSI source. Honest about the platform gate: returns
|
||
/// [`WifiScanError::Unsupported`] off-Windows.
|
||
pub fn scan_native_csi_capable(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
|
||
let all = self.scan_native()?;
|
||
Ok(all
|
||
.into_iter()
|
||
.filter(|obs| wlanapi_native::is_csi_capable(obs.radio_type))
|
||
.collect())
|
||
}
|
||
|
||
/// Record metrics for one completed scan.
|
||
fn record(&self, start: Instant, bssid_count: usize, native: bool) {
|
||
if let Ok(mut guard) = self.last_scan_start.lock() {
|
||
*guard = Some(start);
|
||
}
|
||
let elapsed = start.elapsed();
|
||
if let Ok(mut guard) = self.last_scan_duration.lock() {
|
||
*guard = Some(elapsed);
|
||
}
|
||
self.scan_count.fetch_add(1, Ordering::Relaxed);
|
||
self.total_bssids
|
||
.fetch_add(bssid_count as u64, Ordering::Relaxed);
|
||
if native {
|
||
self.native_scans.fetch_add(1, Ordering::Relaxed);
|
||
}
|
||
}
|
||
|
||
/// Perform a synchronous scan: native FFI first, netsh fallback.
|
||
///
|
||
/// On Windows this attempts [`wlanapi_native::scan_native`]; if that
|
||
/// errors (e.g. WLAN service unavailable) it falls back to the Tier 1
|
||
/// netsh scanner. On non-Windows the native path returns `Unsupported`
|
||
/// and the netsh fallback is used directly.
|
||
fn scan_instrumented(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
|
||
let start = Instant::now();
|
||
|
||
match wlanapi_native::scan_native() {
|
||
Ok(results) => {
|
||
self.record(start, results.len(), true);
|
||
tracing::debug!(
|
||
bssid_count = results.len(),
|
||
elapsed_ms = start.elapsed().as_millis(),
|
||
backend = "native",
|
||
"Tier 2 native scan complete"
|
||
);
|
||
Ok(results)
|
||
}
|
||
Err(native_err) => {
|
||
tracing::debug!(%native_err, "native scan unavailable; falling back to netsh");
|
||
let results = self.inner.scan_sync()?;
|
||
self.record(start, results.len(), false);
|
||
tracing::debug!(
|
||
bssid_count = results.len(),
|
||
elapsed_ms = start.elapsed().as_millis(),
|
||
backend = "netsh",
|
||
"Tier 2 netsh fallback scan complete"
|
||
);
|
||
Ok(results)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Measure the **real** achieved scan rate over `iterations` scans.
|
||
///
|
||
/// This is the honest answer to "how fast is the native path on this
|
||
/// box": it runs `iterations` back-to-back scans, times the whole run,
|
||
/// and reports scans/second. No rate is hardcoded or extrapolated. The
|
||
/// reported [`ScanBackend`] tells you whether the samples came from the
|
||
/// native FFI or the netsh fallback.
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// Propagates the first scan error; returns
|
||
/// [`WifiScanError::ScanFailed`] if `iterations` is 0.
|
||
pub fn benchmark(&self, iterations: u32) -> Result<BenchmarkResult, WifiScanError> {
|
||
if iterations == 0 {
|
||
return Err(WifiScanError::ScanFailed {
|
||
reason: "benchmark requires iterations >= 1".to_string(),
|
||
});
|
||
}
|
||
|
||
// Decide the backend once up front so the measurement is single-path.
|
||
let native_first = wlanapi_native::scan_native();
|
||
let (backend, mut total_bssids, mut done) = match &native_first {
|
||
Ok(list) => (ScanBackend::Native, list.len() as u64, 1u32),
|
||
Err(_) => (ScanBackend::Netsh, 0u64, 0u32),
|
||
};
|
||
|
||
let start = Instant::now();
|
||
while done < iterations {
|
||
let list = match backend {
|
||
ScanBackend::Native => wlanapi_native::scan_native()?,
|
||
ScanBackend::Netsh => self.inner.scan_sync()?,
|
||
};
|
||
total_bssids += list.len() as u64;
|
||
done += 1;
|
||
}
|
||
let total = start.elapsed();
|
||
let secs = total.as_secs_f64().max(f64::MIN_POSITIVE);
|
||
|
||
Ok(BenchmarkResult {
|
||
iterations,
|
||
total,
|
||
rate_hz: f64::from(iterations) / secs,
|
||
mean_bssids: total_bssids as f64 / f64::from(iterations),
|
||
backend,
|
||
})
|
||
}
|
||
|
||
/// Perform an async scan by offloading the blocking call to a
|
||
/// background thread (native-first, netsh fallback inside the task).
|
||
///
|
||
/// Gated behind the `"wlanapi"` feature (requires `tokio`).
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// Returns [`WifiScanError::ScanFailed`] if the background task panics
|
||
/// or is cancelled, or propagates any error from the underlying scan.
|
||
#[cfg(feature = "wlanapi")]
|
||
pub async fn scan_async(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
|
||
let inner = NetshBssidScanner::new();
|
||
let start = Instant::now();
|
||
|
||
let (results, native) = tokio::task::spawn_blocking(
|
||
move || -> Result<(Vec<BssidObservation>, bool), WifiScanError> {
|
||
match wlanapi_native::scan_native() {
|
||
Ok(r) => Ok((r, true)),
|
||
Err(_) => Ok((inner.scan_sync()?, false)),
|
||
}
|
||
},
|
||
)
|
||
.await
|
||
.map_err(|e| WifiScanError::ScanFailed {
|
||
reason: format!("async scan task failed: {e}"),
|
||
})??;
|
||
|
||
self.record(start, results.len(), native);
|
||
|
||
tracing::debug!(
|
||
scan_count = self.scan_count.load(Ordering::Relaxed),
|
||
bssid_count = results.len(),
|
||
elapsed_ms = start.elapsed().as_millis(),
|
||
native,
|
||
"Tier 2 async scan complete"
|
||
);
|
||
|
||
Ok(results)
|
||
}
|
||
}
|
||
|
||
impl Default for WlanApiScanner {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// WlanScanPort implementation (sync)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
impl WlanScanPort for WlanApiScanner {
|
||
fn scan(&self) -> Result<Vec<BssidObservation>, WifiScanError> {
|
||
self.scan_instrumented()
|
||
}
|
||
|
||
fn connected(&self) -> Result<Option<BssidObservation>, WifiScanError> {
|
||
// Heuristic: strongest visible BSSID is the likely-connected AP.
|
||
let mut results = self.scan_instrumented()?;
|
||
if results.is_empty() {
|
||
return Ok(None);
|
||
}
|
||
results.sort_by(|a, b| {
|
||
b.rssi_dbm
|
||
.partial_cmp(&a.rssi_dbm)
|
||
.unwrap_or(std::cmp::Ordering::Equal)
|
||
});
|
||
Ok(Some(results.swap_remove(0)))
|
||
}
|
||
}
|
||
|
||
// ===========================================================================
|
||
// Tests
|
||
// ===========================================================================
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
// -- construction ---------------------------------------------------------
|
||
|
||
#[test]
|
||
fn new_creates_scanner_with_zero_metrics() {
|
||
let scanner = WlanApiScanner::new();
|
||
assert_eq!(scanner.scan_count(), 0);
|
||
assert_eq!(scanner.native_scan_count(), 0);
|
||
|
||
let m = scanner.metrics();
|
||
assert_eq!(m.scan_count, 0);
|
||
assert_eq!(m.total_bssids_observed, 0);
|
||
assert_eq!(m.native_scans, 0);
|
||
assert!(m.last_scan_duration.is_none());
|
||
assert!(m.estimated_rate_hz.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn default_creates_scanner() {
|
||
let scanner = WlanApiScanner::default();
|
||
assert_eq!(scanner.scan_count(), 0);
|
||
}
|
||
|
||
// -- native availability is an honest platform gate -----------------------
|
||
|
||
#[test]
|
||
fn native_available_matches_platform() {
|
||
assert_eq!(WlanApiScanner::native_available(), cfg!(windows));
|
||
}
|
||
|
||
/// On non-Windows the native-only path must be a typed `Unsupported`.
|
||
#[cfg(not(windows))]
|
||
#[test]
|
||
fn native_scan_unsupported_off_windows() {
|
||
let scanner = WlanApiScanner::new();
|
||
match scanner.scan_native() {
|
||
Err(WifiScanError::Unsupported(_)) => {}
|
||
other => panic!("expected Unsupported off-Windows, got {other:?}"),
|
||
}
|
||
// A failed native-only scan must not bump counters.
|
||
assert_eq!(scanner.scan_count(), 0);
|
||
assert_eq!(scanner.native_scan_count(), 0);
|
||
}
|
||
|
||
/// On Windows the native-only path runs the real FFI and, on success,
|
||
/// records a native scan in the metrics.
|
||
#[cfg(windows)]
|
||
#[test]
|
||
fn native_scan_records_metrics_on_windows() {
|
||
let scanner = WlanApiScanner::new();
|
||
match scanner.scan_native() {
|
||
Ok(_) => {
|
||
assert_eq!(scanner.native_scan_count(), 1);
|
||
assert_eq!(scanner.scan_count(), 1);
|
||
}
|
||
// WLAN service off in CI is acceptable; just not Unsupported.
|
||
Err(WifiScanError::ScanFailed { .. }) => {}
|
||
Err(e) => panic!("unexpected native scan error on Windows: {e:?}"),
|
||
}
|
||
}
|
||
|
||
// -- benchmark guards -----------------------------------------------------
|
||
|
||
#[test]
|
||
fn benchmark_rejects_zero_iterations() {
|
||
let scanner = WlanApiScanner::new();
|
||
assert!(matches!(
|
||
scanner.benchmark(0),
|
||
Err(WifiScanError::ScanFailed { .. })
|
||
));
|
||
}
|
||
|
||
// -- WlanScanPort trait compliance ----------------------------------------
|
||
|
||
#[test]
|
||
fn implements_wlan_scan_port() {
|
||
fn assert_port<T: WlanScanPort>() {}
|
||
assert_port::<WlanApiScanner>();
|
||
}
|
||
|
||
#[test]
|
||
fn implements_send_and_sync() {
|
||
fn assert_send_sync<T: Send + Sync>() {}
|
||
assert_send_sync::<WlanApiScanner>();
|
||
}
|
||
|
||
// -- metrics structure ----------------------------------------------------
|
||
|
||
#[test]
|
||
fn scan_metrics_debug_display() {
|
||
let m = ScanMetrics {
|
||
scan_count: 42,
|
||
total_bssids_observed: 126,
|
||
last_scan_duration: Some(Duration::from_millis(150)),
|
||
estimated_rate_hz: Some(1.0 / 0.15),
|
||
native_scans: 40,
|
||
};
|
||
let debug = format!("{m:?}");
|
||
assert!(debug.contains("42"));
|
||
assert!(debug.contains("126"));
|
||
}
|
||
|
||
#[test]
|
||
fn scan_metrics_clone() {
|
||
let m = ScanMetrics {
|
||
scan_count: 1,
|
||
total_bssids_observed: 5,
|
||
last_scan_duration: None,
|
||
estimated_rate_hz: None,
|
||
native_scans: 1,
|
||
};
|
||
let m2 = m.clone();
|
||
assert_eq!(m2.scan_count, 1);
|
||
assert_eq!(m2.total_bssids_observed, 5);
|
||
assert_eq!(m2.native_scans, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn benchmark_result_clone_and_fields() {
|
||
let b = BenchmarkResult {
|
||
iterations: 10,
|
||
total: Duration::from_millis(500),
|
||
rate_hz: 20.0,
|
||
mean_bssids: 7.0,
|
||
backend: ScanBackend::Native,
|
||
};
|
||
let b2 = b.clone();
|
||
assert_eq!(b2.iterations, 10);
|
||
assert_eq!(b2.backend, ScanBackend::Native);
|
||
assert!((b2.rate_hz - 20.0).abs() < f64::EPSILON);
|
||
}
|
||
|
||
// -- rate estimation ------------------------------------------------------
|
||
|
||
#[test]
|
||
fn estimated_rate_from_known_duration() {
|
||
let scanner = WlanApiScanner::new();
|
||
{
|
||
let mut guard = scanner.last_scan_duration.lock().unwrap();
|
||
*guard = Some(Duration::from_millis(100));
|
||
}
|
||
let m = scanner.metrics();
|
||
let rate = m.estimated_rate_hz.unwrap();
|
||
assert!((rate - 10.0).abs() < 0.01, "expected ~10 Hz, got {rate}");
|
||
}
|
||
|
||
#[test]
|
||
fn estimated_rate_none_before_first_scan() {
|
||
let scanner = WlanApiScanner::new();
|
||
assert!(scanner.metrics().estimated_rate_hz.is_none());
|
||
}
|
||
|
||
/// MEASURED scan-rate harness. `#[ignore]` so it never runs in CI (it
|
||
/// touches the live WLAN service and takes seconds), but
|
||
/// `cargo test -p wifi-densepose-wifiscan -- --ignored --nocapture
|
||
/// measure_native_scan_rate` prints the *real* Hz on the running box.
|
||
/// This is the honest measurement path: the number it prints is what
|
||
/// the machine actually achieved, not a hardcoded claim.
|
||
#[cfg(windows)]
|
||
#[test]
|
||
#[ignore = "live WLAN measurement; run explicitly with --ignored --nocapture"]
|
||
fn measure_native_scan_rate() {
|
||
let scanner = WlanApiScanner::new();
|
||
let bench = scanner
|
||
.benchmark(30)
|
||
.expect("benchmark should run on a Windows box with a WLAN adapter");
|
||
println!(
|
||
"MEASURED native scan rate: {:.2} Hz over {} iters ({:?} backend), \
|
||
mean {:.1} BSSIDs/scan, total {:?}",
|
||
bench.rate_hz, bench.iterations, bench.backend, bench.mean_bssids, bench.total
|
||
);
|
||
assert!(bench.rate_hz > 0.0);
|
||
}
|
||
}
|