Line | Count | Source |
1 | | // SPDX-License-Identifier: Apache-2.0 |
2 | | //! Apple/macOS detection via `system_profiler SPDisplaysDataType`, `sysctl |
3 | | //! hw.memsize` and `vm_stat`. No Metal framework linkage required. Apple |
4 | | //! Silicon uses a unified memory architecture (no dedicated VRAM), so |
5 | | //! `total_bytes` falls back to total physical memory and the used/free split |
6 | | //! comes from system-wide paging statistics. |
7 | | |
8 | | /// Map a vendor id as printed by `system_profiler` (e.g. `0x106b`) to a |
9 | | /// [`Vendor`](crate::Vendor). |
10 | | #[allow(dead_code)] // used on macOS + in tests; unused on other targets |
11 | 10 | fn vendor_from_id(id: &str) -> crate::Vendor { |
12 | | use crate::Vendor; |
13 | 10 | match id.trim().to_ascii_lowercase().as_str() { |
14 | 10 | "0x106b" => Vendor::Apple3 , |
15 | 7 | "0x1002" => Vendor::Amd3 , |
16 | 4 | "0x10de" => Vendor::Nvidia1 , |
17 | 3 | "0x8086" => Vendor::Intel2 , |
18 | 1 | _ => Vendor::Unknown, |
19 | | } |
20 | 10 | } |
21 | | |
22 | | /// Parse a `system_profiler` VRAM value like `"8 GB"` or `"1536 MB"` into bytes |
23 | | /// (binary units). |
24 | | #[allow(dead_code)] // used on macOS + in tests; unused on other targets |
25 | 12 | fn parse_vram(value: &str) -> Option<u64> { |
26 | 12 | let (num9 , unit9 ) = value.trim().split_once(' ')?3 ; |
27 | 9 | let amount8 : u648 = num.trim().parse().ok()?1 ; |
28 | 8 | let mult7 : u647 = match unit.trim().to_ascii_uppercase().as_str() { |
29 | 8 | "GB" => 1024 * 1024 * 10243 , |
30 | 5 | "MB" => 1024 * 10243 , |
31 | 2 | "KB" => 10241 , |
32 | 1 | _ => return None, |
33 | | }; |
34 | 7 | Some(amount * mult) |
35 | 12 | } |
36 | | |
37 | | /// Parse `sysctl -n hw.memsize` output (a decimal byte count). |
38 | | #[allow(dead_code)] // used on macOS + in tests; unused on other targets |
39 | 4 | fn parse_memsize(content: &str) -> Option<u64> { |
40 | 4 | content.trim().parse().ok() |
41 | 4 | } |
42 | | |
43 | | /// Pull the page size out of the `vm_stat` header, which reads |
44 | | /// `Mach Virtual Memory Statistics: (page size of 16384 bytes)`. |
45 | | /// |
46 | | /// Taken from the report itself rather than `hw.pagesize` so the counts and |
47 | | /// their multiplier always come from the same source: Apple Silicon pages are |
48 | | /// 16 KiB where Intel Macs use 4 KiB, and mixing the two would be off by 4x. |
49 | | #[allow(dead_code)] // used on macOS + in tests; unused on other targets |
50 | 9 | fn parse_page_size(text: &str) -> Option<u64> { |
51 | 9 | let rest7 = text.split_once("page size of ")?2 .1; |
52 | 7 | rest.split_once(" bytes")?1 .0.trim6 ().parse6 ().ok6 () |
53 | 9 | } |
54 | | |
55 | | /// Look up one `Pages ...: <count>.` row in `vm_stat` output. The trailing |
56 | | /// period is part of the format, not the number. |
57 | | #[allow(dead_code)] // used on macOS + in tests; unused on other targets |
58 | 12 | fn page_count(text: &str, key: &str) -> Option<u64> { |
59 | 61 | for line in text12 .lines12 () { |
60 | 61 | if let Some(value10 ) = line.trim().strip_prefix(key) { |
61 | 10 | return value.trim().trim_end_matches('.').trim().parse().ok(); |
62 | 51 | } |
63 | | } |
64 | 2 | None |
65 | 12 | } |
66 | | |
67 | | /// Bytes in use, as macOS itself accounts for them: resident anonymous and |
68 | | /// kernel pages (`active` + `wired down`) plus the compressor's footprint. |
69 | | /// This is the figure Activity Monitor labels "Memory Used". |
70 | | /// |
71 | | /// Apple Silicon shares one pool between CPU and GPU, so system-wide usage *is* |
72 | | /// the GPU's usage; there is no separate VRAM to account for. Callers apply |
73 | | /// this only to unified-memory GPUs — a discrete card on an Intel Mac reports |
74 | | /// its own VRAM and must not be described by these numbers. |
75 | | /// |
76 | | /// Reads *occupied by* rather than *stored in* the compressor: the former is |
77 | | /// the compressor's real physical footprint, while the latter counts pages as |
78 | | /// they were before compression and routinely exceeds installed memory. |
79 | | #[allow(dead_code)] // used on macOS + in tests; unused on other targets |
80 | 4 | fn parse_vm_stat_used(text: &str) -> Option<u64> { |
81 | 4 | let page_size3 = parse_page_size(text)?1 ; |
82 | 3 | let active = page_count(text, "Pages active:")?0 ; |
83 | 3 | let wired2 = page_count(text, "Pages wired down:")?1 ; |
84 | 2 | let compressor = page_count(text, "Pages occupied by compressor:")?0 ; |
85 | 2 | active |
86 | 2 | .checked_add(wired)?0 |
87 | 2 | .checked_add(compressor)?0 |
88 | 2 | .checked_mul(page_size) |
89 | 4 | } |
90 | | |
91 | | /// Parse plain-text `system_profiler SPDisplaysDataType` output into one |
92 | | /// Metal GPU family for an Apple Silicon chip name — `Apple M2 Pro` is |
93 | | /// `apple8`. |
94 | | /// |
95 | | /// Derived from the name rather than queried, because the real source is |
96 | | /// `MTLDevice.supportsFamily()`, which means linking Metal. Chips this table |
97 | | /// does not know report `None`: a wrong family would be acted on, an absent one |
98 | | /// would not. Non-Apple chipsets (`AMD Radeon Pro 5500M` on an Intel Mac) fall |
99 | | /// out naturally, since the prefix will not match. |
100 | | /// |
101 | | /// The rows come from Apple's published Metal Feature Set Tables (May 21, |
102 | | /// 2026), which list M1 as `Apple7`, M2 as `Apple8`, M3 and M4 as `Apple9`, and |
103 | | /// M5 as `Apple10`. Apple documents these per *series*, so one row covers a |
104 | | /// generation's Pro, Max and Ultra variants — which is what falls out of |
105 | | /// reading only the leading digits. |
106 | | #[allow(dead_code)] // used on macOS + in tests; unused on other targets |
107 | 22 | fn apple_family(name: &str) -> Option<crate::AppleFamily> { |
108 | 22 | let rest12 = name.strip_prefix("Apple M")?10 ; |
109 | 12 | let end = rest |
110 | 19 | .find12 (|c: char| !c.is_ascii_digit()) |
111 | 12 | .unwrap_or(rest.len()); |
112 | 12 | let chip: u32 = rest[..end].parse().ok()?0 ; |
113 | 12 | let generation10 = match chip { |
114 | 2 | 1 => 7, |
115 | 2 | 2 => 8, |
116 | | // M3 introduced apple9 and M4 stayed on it; M5 moved to apple10. |
117 | 2 | 3 | 4 => 9, |
118 | 4 | 5 => 10, |
119 | 2 | _ => return None, |
120 | | }; |
121 | 10 | Some(crate::AppleFamily::new(generation)) |
122 | 22 | } |
123 | | |
124 | | /// [`GpuInfo`](crate::GpuInfo) per "Chipset Model:" block. `total_bytes` is `0` |
125 | | /// when no VRAM line is present (Apple Silicon unified memory); callers |
126 | | /// backfill it from physical memory. |
127 | | #[allow(dead_code)] // used on macOS + in tests; unused on other targets |
128 | 8 | fn parse_system_profiler(text: &str) -> Vec<crate::GpuInfo> { |
129 | | use crate::{GpuInfo, Vendor}; |
130 | | |
131 | 8 | let mut gpus: Vec<GpuInfo> = Vec::new(); |
132 | 29 | for raw in text8 .lines8 () { |
133 | 29 | let line = raw.trim(); |
134 | 29 | if let Some(name7 ) = line.strip_prefix("Chipset Model:") { |
135 | 7 | let name = name.trim(); |
136 | 7 | gpus.push(GpuInfo { |
137 | 7 | name: name.to_string(), |
138 | 7 | vendor: Vendor::Apple, |
139 | 7 | total_bytes: 0, |
140 | 7 | free_bytes: None, |
141 | 7 | used_bytes: None, |
142 | 7 | arch_target: apple_family(name).map(crate::ArchTarget::Apple), |
143 | 7 | }); |
144 | 22 | } else if let Some(gpu16 ) = gpus.last_mut() { |
145 | 16 | if let Some(v7 ) = line.strip_prefix("Vendor:") { |
146 | | // e.g. "Apple (0x106b)" — pull out the parenthesized id. |
147 | 7 | if let Some(id6 ) = v.split('(').nth(1).and_then(|s| s.split(')')6 .next6 ()) { |
148 | 6 | gpu.vendor = vendor_from_id(id); |
149 | 6 | }1 |
150 | 9 | } else if let Some(v5 ) = line |
151 | 9 | .strip_prefix("VRAM (Total):") |
152 | 9 | .or_else(|| line6 .strip_prefix6 ("VRAM (Dynamic, Max):")) |
153 | 5 | && let Some(bytes4 ) = parse_vram(v) |
154 | 4 | { |
155 | 4 | gpu.total_bytes = bytes; |
156 | 5 | } |
157 | 6 | } |
158 | | } |
159 | 8 | gpus |
160 | 8 | } |
161 | | |
162 | | #[cfg(target_os = "macos")] |
163 | | pub(crate) fn detect() -> Vec<crate::GpuInfo> { |
164 | | use crate::{GpuInfo, Vendor}; |
165 | | |
166 | | let mut gpus = std::process::Command::new("system_profiler") |
167 | | .arg("SPDisplaysDataType") |
168 | | .output() |
169 | | .ok() |
170 | | .filter(|o| o.status.success()) |
171 | | .map(|o| parse_system_profiler(&String::from_utf8_lossy(&o.stdout))) |
172 | | .unwrap_or_default(); |
173 | | |
174 | | // Apple Silicon reports no VRAM line — backfill from physical memory, and |
175 | | // with it the unified pool's usage. A zero total is what marks a GPU as |
176 | | // unified here, so a discrete card that already reported its own VRAM keeps |
177 | | // the `None` split rather than being handed system-wide figures. |
178 | | let memsize = sysctl_memsize(); |
179 | | // Read lazily, the way the DRM probe defers its system-memory lookup: the |
180 | | // split costs a subprocess, and an Intel Mac with only a discrete card |
181 | | // never needs one. |
182 | | let mut split: Option<(Option<u64>, Option<u64>)> = None; |
183 | | for gpu in &mut gpus { |
184 | | if gpu.total_bytes == 0 |
185 | | && let Some(mem) = memsize |
186 | | { |
187 | | gpu.total_bytes = mem; |
188 | | let (used, free) = *split.get_or_insert_with(|| unified_split(mem)); |
189 | | gpu.used_bytes = used; |
190 | | gpu.free_bytes = free; |
191 | | } |
192 | | } |
193 | | // Nothing was parsed, so the loop above never ran and never read the split. |
194 | | if gpus.is_empty() |
195 | | && let Some(mem) = memsize |
196 | | { |
197 | | let (used, free) = unified_split(mem); |
198 | | gpus.push(GpuInfo { |
199 | | name: "Apple GPU".to_string(), |
200 | | vendor: Vendor::Apple, |
201 | | total_bytes: mem, |
202 | | free_bytes: free, |
203 | | used_bytes: used, |
204 | | arch_target: None, |
205 | | }); |
206 | | } |
207 | | gpus |
208 | | } |
209 | | |
210 | | /// The `(used, free)` split of a unified pool of `total` bytes, or `(None, |
211 | | /// None)` when `vm_stat` is unavailable or reports more than is installed — |
212 | | /// a total that small would make `free` underflow, and a partial answer is |
213 | | /// worse than admitting the split is unknown. |
214 | | #[cfg(target_os = "macos")] |
215 | | fn unified_split(total: u64) -> (Option<u64>, Option<u64>) { |
216 | | let Some(used) = vm_stat_used().filter(|&used| used <= total) else { |
217 | | return (None, None); |
218 | | }; |
219 | | (Some(used), Some(total - used)) |
220 | | } |
221 | | |
222 | | #[cfg(target_os = "macos")] |
223 | | fn vm_stat_used() -> Option<u64> { |
224 | | let output = std::process::Command::new("vm_stat").output().ok()?; |
225 | | output |
226 | | .status |
227 | | .success() |
228 | | .then(|| parse_vm_stat_used(&String::from_utf8_lossy(&output.stdout))) |
229 | | .flatten() |
230 | | } |
231 | | |
232 | | #[cfg(target_os = "macos")] |
233 | | fn sysctl_memsize() -> Option<u64> { |
234 | | let output = std::process::Command::new("sysctl") |
235 | | .args(["-n", "hw.memsize"]) |
236 | | .output() |
237 | | .ok()?; |
238 | | output |
239 | | .status |
240 | | .success() |
241 | | .then(|| parse_memsize(&String::from_utf8_lossy(&output.stdout))) |
242 | | .flatten() |
243 | | } |
244 | | |
245 | | #[cfg(not(target_os = "macos"))] |
246 | 219 | pub(crate) fn detect() -> Vec<crate::GpuInfo> { |
247 | 219 | Vec::new() |
248 | 219 | } |
249 | | |
250 | | #[cfg(test)] |
251 | | mod tests { |
252 | | #[test] |
253 | 1 | fn maps_apple_silicon_chips_to_metal_families() { |
254 | | use crate::AppleFamily; |
255 | 1 | assert_eq!(apple_family("Apple M1"), Some(AppleFamily::new(7))); |
256 | 1 | assert_eq!(apple_family("Apple M1 Max"), Some(AppleFamily::new(7))); |
257 | 1 | assert_eq!(apple_family("Apple M2 Pro"), Some(AppleFamily::new(8))); |
258 | 1 | assert_eq!(apple_family("Apple M3 Ultra"), Some(AppleFamily::new(9))); |
259 | 1 | assert_eq!(apple_family("Apple M4"), Some(AppleFamily::new(9))); |
260 | | // M5 is the first generation on apple10. |
261 | 1 | assert_eq!(apple_family("Apple M5"), Some(AppleFamily::new(10))); |
262 | 1 | assert_eq!(apple_family("Apple M5 Pro"), Some(AppleFamily::new(10))); |
263 | 1 | assert_eq!(apple_family("Apple M5 Max"), Some(AppleFamily::new(10))); |
264 | 1 | } |
265 | | |
266 | | #[test] |
267 | 1 | fn two_digit_families_render_unpacked() { |
268 | | // `apple10` must not collapse to `apple1`. |
269 | 1 | assert_eq!( |
270 | 1 | apple_family("Apple M5").map(|family| family.to_string()), |
271 | 1 | Some("apple10".to_string()) |
272 | | ); |
273 | 1 | } |
274 | | |
275 | | #[test] |
276 | 1 | fn unknown_chips_report_no_family() { |
277 | | // A generation this table predates must not be guessed at. |
278 | 1 | assert_eq!(apple_family("Apple M9"), None); |
279 | | // Multi-digit chips must not be read as their first digit. |
280 | 1 | assert_eq!(apple_family("Apple M10"), None, "not M1"); |
281 | | // Intel Macs and the sysctl fallback name. |
282 | 1 | assert_eq!(apple_family("AMD Radeon Pro 5500M"), None); |
283 | 1 | assert_eq!(apple_family("Intel Iris Plus Graphics"), None); |
284 | 1 | assert_eq!(apple_family("Apple GPU"), None); |
285 | 1 | assert_eq!(apple_family(""), None); |
286 | 1 | } |
287 | | |
288 | | use super::*; |
289 | | use crate::Vendor; |
290 | | |
291 | | #[test] |
292 | 1 | fn apple_silicon_block_has_no_vram() { |
293 | 1 | let text = "Graphics/Displays:\n\n Apple M2 Pro:\n\n Chipset Model: Apple M2 Pro\n Type: GPU\n Vendor: Apple (0x106b)\n Metal Support: Metal 3\n"; |
294 | 1 | let gpus = parse_system_profiler(text); |
295 | 1 | assert_eq!(gpus.len(), 1); |
296 | 1 | assert_eq!(gpus[0].name, "Apple M2 Pro"); |
297 | 1 | assert_eq!(gpus[0].vendor, Vendor::Apple); |
298 | 1 | assert_eq!( |
299 | 1 | gpus[0].total_bytes, 0, |
300 | | "unified memory; backfilled in detect()" |
301 | | ); |
302 | 1 | } |
303 | | |
304 | | #[test] |
305 | 1 | fn intel_mac_discrete_gpu_reports_vram() { |
306 | 1 | let text = " Chipset Model: AMD Radeon Pro 5500M\n Type: GPU\n Bus: PCIe\n VRAM (Total): 8 GB\n Vendor: AMD (0x1002)\n"; |
307 | 1 | let gpus = parse_system_profiler(text); |
308 | 1 | assert_eq!(gpus.len(), 1); |
309 | 1 | assert_eq!(gpus[0].vendor, Vendor::Amd); |
310 | 1 | assert_eq!(gpus[0].total_bytes, 8 * 1024 * 1024 * 1024); |
311 | 1 | } |
312 | | |
313 | | #[test] |
314 | 1 | fn dynamic_vram_line_is_parsed() { |
315 | 1 | let text = " Chipset Model: Intel Iris Pro\n VRAM (Dynamic, Max): 1536 MB\n Vendor: Intel (0x8086)\n"; |
316 | 1 | let gpus = parse_system_profiler(text); |
317 | 1 | assert_eq!(gpus[0].vendor, Vendor::Intel); |
318 | 1 | assert_eq!(gpus[0].total_bytes, 1536 * 1024 * 1024); |
319 | 1 | } |
320 | | |
321 | | #[test] |
322 | 1 | fn vram_and_memsize_parsers() { |
323 | 1 | assert_eq!(parse_vram("8 GB"), Some(8 * 1024 * 1024 * 1024)); |
324 | 1 | assert_eq!(parse_vram("1536 MB"), Some(1536 * 1024 * 1024)); |
325 | 1 | assert_eq!(parse_vram("weird"), None); |
326 | 1 | assert_eq!(parse_memsize("17179869184\n"), Some(17_179_869_184)); |
327 | 1 | } |
328 | | |
329 | | #[test] |
330 | 1 | fn vendor_id_mapping() { |
331 | 1 | assert_eq!(vendor_from_id("0x106b"), Vendor::Apple); |
332 | 1 | assert_eq!(vendor_from_id("0x10DE"), Vendor::Nvidia); |
333 | 1 | assert_eq!(vendor_from_id("0x1002"), Vendor::Amd); |
334 | 1 | assert_eq!(vendor_from_id("0xbeef"), Vendor::Unknown); |
335 | 1 | } |
336 | | |
337 | | #[test] |
338 | 1 | fn parses_multiple_gpu_blocks() { |
339 | 1 | let text = " Chipset Model: AMD Radeon Pro 5500M\n VRAM (Total): 8 GB\n Vendor: AMD (0x1002)\n Chipset Model: Intel UHD Graphics 630\n VRAM (Dynamic, Max): 1536 MB\n Vendor: Intel (0x8086)\n"; |
340 | 1 | let gpus = parse_system_profiler(text); |
341 | 1 | assert_eq!(gpus.len(), 2); |
342 | 1 | assert_eq!(gpus[0].vendor, Vendor::Amd); |
343 | 1 | assert_eq!(gpus[0].total_bytes, 8 * 1024 * 1024 * 1024); |
344 | 1 | assert_eq!(gpus[1].vendor, Vendor::Intel); |
345 | 1 | assert_eq!(gpus[1].total_bytes, 1536 * 1024 * 1024); |
346 | 1 | } |
347 | | |
348 | | #[test] |
349 | 1 | fn empty_output_yields_no_gpus() { |
350 | 1 | assert!(parse_system_profiler("").is_empty()); |
351 | | // Lines before any "Chipset Model:" have no GPU to attach to. |
352 | 1 | assert!(parse_system_profiler("Graphics/Displays:\n VRAM (Total): 8 GB\n").is_empty()); |
353 | 1 | } |
354 | | |
355 | | #[test] |
356 | 1 | fn vendor_line_without_parens_keeps_default() { |
357 | 1 | let text = " Chipset Model: Mystery GPU\n Vendor: sieve\n"; |
358 | 1 | let gpus = parse_system_profiler(text); |
359 | 1 | assert_eq!(gpus.len(), 1); |
360 | | // No "(id)" to parse, so the Apple default placed at block start stands. |
361 | 1 | assert_eq!(gpus[0].vendor, Vendor::Apple); |
362 | 1 | } |
363 | | |
364 | | #[test] |
365 | 1 | fn malformed_vram_leaves_total_at_zero() { |
366 | 1 | let text = |
367 | 1 | " Chipset Model: Broken\n VRAM (Total): lots\n Vendor: Apple (0x106b)\n"; |
368 | 1 | let gpus = parse_system_profiler(text); |
369 | 1 | assert_eq!(gpus[0].total_bytes, 0); |
370 | 1 | } |
371 | | |
372 | | #[test] |
373 | 1 | fn parse_vram_rejects_unknown_units_and_bad_numbers() { |
374 | 1 | assert_eq!(parse_vram("8 TB"), None); |
375 | 1 | assert_eq!(parse_vram("8"), None); |
376 | 1 | assert_eq!(parse_vram("eight GB"), None); |
377 | 1 | assert_eq!( |
378 | 1 | parse_vram("512 kb"), |
379 | 1 | Some(512 * 1024), |
380 | | "unit is case-insensitive" |
381 | | ); |
382 | 1 | } |
383 | | |
384 | | #[test] |
385 | 1 | fn parse_memsize_rejects_non_numeric() { |
386 | 1 | assert_eq!(parse_memsize("nope"), None); |
387 | 1 | assert_eq!(parse_memsize(""), None); |
388 | 1 | assert_eq!(parse_memsize("0"), Some(0)); |
389 | 1 | } |
390 | | |
391 | | /// A representative `vm_stat` report on a 16 KiB-page host, trimmed to the |
392 | | /// rows the parser reads plus the compressor pair it must tell apart. |
393 | | const VM_STAT: &str = "Mach Virtual Memory Statistics: (page size of 16384 bytes)\n\ |
394 | | Pages free: 3480.\n\ |
395 | | Pages active: 77803.\n\ |
396 | | Pages inactive: 74322.\n\ |
397 | | Pages speculative: 2300.\n\ |
398 | | Pages wired down: 137287.\n\ |
399 | | Pages purgeable: 0.\n\ |
400 | | Pages stored in compressor: 1263882.\n\ |
401 | | Pages occupied by compressor: 192142.\n"; |
402 | | |
403 | | #[test] |
404 | 1 | fn reads_vm_stat_page_size_and_counts() { |
405 | 1 | assert_eq!(parse_page_size(VM_STAT), Some(16384)); |
406 | 1 | assert_eq!(page_count(VM_STAT, "Pages active:"), Some(77803)); |
407 | 1 | assert_eq!(page_count(VM_STAT, "Pages wired down:"), Some(137_287)); |
408 | 1 | assert_eq!(page_count(VM_STAT, "Pages free:"), Some(3480)); |
409 | 1 | assert_eq!(page_count(VM_STAT, "Pages absent:"), None); |
410 | 1 | } |
411 | | |
412 | | #[test] |
413 | 1 | fn sums_used_pages_the_way_activity_monitor_does() { |
414 | | // active + wired + compressor, at 16 KiB per page. |
415 | 1 | let expected = (77803 + 137_287 + 192_142) * 16384; |
416 | 1 | assert_eq!(parse_vm_stat_used(VM_STAT), Some(expected)); |
417 | | // Sums to a plausible fraction of installed memory, not a multiple. |
418 | 1 | assert!(expected < 8 * 1024 * 1024 * 1024); |
419 | 1 | } |
420 | | |
421 | | #[test] |
422 | 1 | fn used_counts_the_compressor_footprint_not_its_contents() { |
423 | | // "stored in" is the pre-compression count and dwarfs installed memory; |
424 | | // reading it instead of "occupied by" overshoots by several times. |
425 | 1 | let stored = 1_263_882u64 * 16384; |
426 | 1 | assert!( |
427 | 1 | stored > 8 * 1024 * 1024 * 1024, |
428 | | "fixture must expose the trap" |
429 | | ); |
430 | 1 | assert!(parse_vm_stat_used(VM_STAT).is_some_and(|used| used < stored)); |
431 | 1 | } |
432 | | |
433 | | #[test] |
434 | 1 | fn vm_stat_parsing_needs_every_field() { |
435 | | // A report missing any row the sum needs yields nothing, rather than |
436 | | // silently undercounting. |
437 | 1 | assert_eq!(parse_vm_stat_used(""), None); |
438 | 1 | assert_eq!(parse_page_size("Pages free: 1.\n"), None); |
439 | 1 | let no_wired = "Mach Virtual Memory Statistics: (page size of 16384 bytes)\n\ |
440 | 1 | Pages active: 77803.\n\ |
441 | 1 | Pages occupied by compressor: 192142.\n"; |
442 | 1 | assert_eq!(parse_vm_stat_used(no_wired), None); |
443 | 1 | } |
444 | | |
445 | | #[test] |
446 | 1 | fn page_size_survives_a_four_kib_intel_header() { |
447 | 1 | let intel = "Mach Virtual Memory Statistics: (page size of 4096 bytes)\n"; |
448 | 1 | assert_eq!(parse_page_size(intel), Some(4096)); |
449 | 1 | assert_eq!(parse_page_size("(page size of many bytes)"), None); |
450 | 1 | assert_eq!(parse_page_size("(page size of 4096)"), None); |
451 | 1 | } |
452 | | } |