mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
749 lines
26 KiB
TypeScript
749 lines
26 KiB
TypeScript
/**
|
|||
|
|
* @fileoverview Tests for src/local-model/hardware.ts: the probe that tells the recommendation
|
||
|
|
* what this Mac is.
|
||
|
|
*
|
||
|
|
* Two halves, mirroring the module:
|
||
|
|
*
|
||
|
|
* 1. `parseSysctl()`, pure. Fed the REAL output of the probe command on the two test Macs (a
|
||
|
|
* MacBook Pro with an M5 Pro and 48 GB, a Mac mini with an M4 and 16 GB), copied here
|
||
|
|
* character for character with the extra lines the capture holds, plus an Intel-shaped
|
||
|
|
* output (three of the seven keys do not exist there) and outputs cut off mid-line.
|
||
|
|
* 2. `readHardware()`, with every piece of IO injected: nothing is spawned and no real disk is
|
||
|
|
* read. What is pinned: a failed spawn is an ANSWER (the `os-fallback` shape), nothing is
|
||
|
|
* spawned off macOS, the result is cached for 60 s and concurrent callers share one probe.
|
||
|
|
*
|
||
|
|
* The IO itself is tested apart, against things this suite controls:
|
||
|
|
*
|
||
|
|
* 3. `spawnSysctl()` runs a small shell script of the test in place of sysctl. That proves what
|
||
|
|
* a fake cannot: the arguments are the seven keys and nothing else, the output of a program
|
||
|
|
* that exits 1 is still read (an Intel Mac lacks three keys, so sysctl exits 1 there), a
|
||
|
|
* program that hangs is given up on, and the server's environment is not handed over.
|
||
|
|
* 4. `dataDirFreeBytes()` with injected probes: a folder that does not exist yet, a mount that
|
||
|
|
* does not answer, and a `statfs` that never comes back, which must not be started twice.
|
||
|
|
*
|
||
|
|
* One test at the end runs the real defaults once (the real spawn and the real `statfs`), to
|
||
|
|
* prove they cannot reject on whatever machine runs the suite. On Linux `sysctl` knows none of
|
||
|
|
* the keys, so that run ends in the fallback; on a Mac it ends in a real answer. Both are fine.
|
||
|
|
*
|
||
|
|
* Port: N/A (no server).
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||
|
|
import { tmpdir } from 'node:os';
|
||
|
|
import { isAbsolute, join } from 'node:path';
|
||
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||
|
|
import {
|
||
|
|
dataDirFreeBytes,
|
||
|
|
FREE_DISK_TIMEOUT_MS,
|
||
|
|
freeDiskBytesOf,
|
||
|
|
HARDWARE_CACHE_TTL_MS,
|
||
|
|
parseSysctl,
|
||
|
|
readHardware,
|
||
|
|
resetHardwareCache,
|
||
|
|
spawnSysctl,
|
||
|
|
SYSCTL_KEYS,
|
||
|
|
SYSCTL_PATH,
|
||
|
|
SYSCTL_TIMEOUT_MS,
|
||
|
|
type HardwareProbeDeps,
|
||
|
|
} from '../src/local-model/hardware.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The capture of the MacBook Pro (Mac17,13), 2026-11-06. The last four lines are not part of
|
||
|
|
* the probe: two extra keys the capture asked for, the shell's exit status and a `df -k` line.
|
||
|
|
*/
|
||
|
|
const MACBOOK_SYSCTL = `hw.memsize: 51539607552
|
||
|
|
machdep.cpu.brand_string: Apple M5 Pro
|
||
|
|
hw.optional.arm64: 1
|
||
|
|
sysctl.proc_translated: 0
|
||
|
|
iogpu.wired_limit_mb: 0
|
||
|
|
kern.osproductversion: 27.2
|
||
|
|
hw.model: Mac17,13
|
||
|
|
hw.perflevel0.physicalcpu: 6
|
||
|
|
hw.perflevel1.physicalcpu: 12
|
||
|
|
exit=0
|
||
|
|
/dev/disk3s5 971094180 54383564 890926084 6% 542146 8909260840 0% /System/Volumes/Data
|
||
|
|
`;
|
||
|
|
|
||
|
|
/** The capture of the Mac mini (Mac16,10), 2026-11-06, with the same four extra lines. */
|
||
|
|
const MACMINI_SYSCTL = `hw.memsize: 17179869184
|
||
|
|
machdep.cpu.brand_string: Apple M4
|
||
|
|
hw.optional.arm64: 1
|
||
|
|
sysctl.proc_translated: 0
|
||
|
|
iogpu.wired_limit_mb: 0
|
||
|
|
kern.osproductversion: 27.2
|
||
|
|
hw.model: Mac16,10
|
||
|
|
hw.perflevel0.physicalcpu: 4
|
||
|
|
hw.perflevel1.physicalcpu: 6
|
||
|
|
exit=0
|
||
|
|
/dev/disk3s5 239362496 172954124 41752848 81% 2353838 417528480 1% /System/Volumes/Data
|
||
|
|
`;
|
||
|
|
|
||
|
|
/** Free disk of the two Macs: the "available" column of their `df -k` lines, in bytes. */
|
||
|
|
const MACBOOK_FREE_DISK = 890_926_084 * 1024;
|
||
|
|
const MACMINI_FREE_DISK = 41_752_848 * 1024;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* What an Intel Mac prints: `hw.optional.arm64`, `sysctl.proc_translated` and
|
||
|
|
* `iogpu.wired_limit_mb` do not exist there, so sysctl leaves their lines out (and exits 1).
|
||
|
|
* NOT a capture: no Intel Mac was available, the shape is recalled.
|
||
|
|
*/
|
||
|
|
const INTEL_SYSCTL = `hw.memsize: 17179869184
|
||
|
|
machdep.cpu.brand_string: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
|
||
|
|
kern.osproductversion: 13.6.7
|
||
|
|
hw.model: MacBookPro15,1
|
||
|
|
`;
|
||
|
|
|
||
|
|
/** Deps that spawn nothing and read no disk. A test overrides what it is about. */
|
||
|
|
function deps(overrides: HardwareProbeDeps = {}): HardwareProbeDeps {
|
||
|
|
return {
|
||
|
|
platform: 'darwin',
|
||
|
|
arch: 'arm64',
|
||
|
|
runSysctl: async () => MACMINI_SYSCTL,
|
||
|
|
freeDiskBytes: async () => MACMINI_FREE_DISK,
|
||
|
|
totalMemBytes: () => 8 * 1024 ** 3,
|
||
|
|
cpuModel: () => 'Fallback CPU',
|
||
|
|
now: () => 1_000_000,
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
resetHardwareCache();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
resetHardwareCache();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('the probe command', () => {
|
||
|
|
it('is an absolute path and asks for exactly the seven keys, never with -n', () => {
|
||
|
|
expect(isAbsolute(SYSCTL_PATH)).toBe(true);
|
||
|
|
expect(SYSCTL_PATH).toBe('/usr/sbin/sysctl');
|
||
|
|
expect([...SYSCTL_KEYS]).toEqual([
|
||
|
|
'hw.memsize',
|
||
|
|
'machdep.cpu.brand_string',
|
||
|
|
'hw.optional.arm64',
|
||
|
|
'sysctl.proc_translated',
|
||
|
|
'iogpu.wired_limit_mb',
|
||
|
|
'kern.osproductversion',
|
||
|
|
'hw.model',
|
||
|
|
]);
|
||
|
|
// Every argument is a key name. `-n` would print bare values, and a key missing on a
|
||
|
|
// machine would then shift every value after it onto the wrong key.
|
||
|
|
for (const key of SYSCTL_KEYS) expect(key).toMatch(/^[a-z][a-z0-9_.]*$/);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is bounded, and its result is reused for 60 seconds', () => {
|
||
|
|
expect(SYSCTL_TIMEOUT_MS).toBe(2000);
|
||
|
|
expect(HARDWARE_CACHE_TTL_MS).toBe(60_000);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('parseSysctl on the two real Macs', () => {
|
||
|
|
it('reads the MacBook Pro: M5 Pro, 48 GB, macOS 27.2, no wired limit set', () => {
|
||
|
|
expect(parseSysctl(MACBOOK_SYSCTL)).toEqual({
|
||
|
|
memBytes: 51_539_607_552,
|
||
|
|
chip: 'Apple M5 Pro',
|
||
|
|
arm64: true,
|
||
|
|
translated: false,
|
||
|
|
wiredLimitMb: 0,
|
||
|
|
macosVersion: '27.2',
|
||
|
|
macosMajor: 27,
|
||
|
|
model: 'Mac17,13',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reads the Mac mini: M4, 16 GB, macOS 27.2, no wired limit set', () => {
|
||
|
|
expect(parseSysctl(MACMINI_SYSCTL)).toEqual({
|
||
|
|
memBytes: 17_179_869_184,
|
||
|
|
chip: 'Apple M4',
|
||
|
|
arm64: true,
|
||
|
|
translated: false,
|
||
|
|
wiredLimitMb: 0,
|
||
|
|
macosVersion: '27.2',
|
||
|
|
macosMajor: 27,
|
||
|
|
model: 'Mac16,10',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('skips the lines it does not know: other keys, the exit status, the df line', () => {
|
||
|
|
const facts = parseSysctl(MACMINI_SYSCTL);
|
||
|
|
expect(Object.keys(facts).sort()).toEqual(
|
||
|
|
['arm64', 'chip', 'macosMajor', 'macosVersion', 'memBytes', 'model', 'translated', 'wiredLimitMb'].sort()
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports a wired limit of 0 as 0 (not set), and a set one as its number', () => {
|
||
|
|
expect(parseSysctl(MACMINI_SYSCTL).wiredLimitMb).toBe(0);
|
||
|
|
expect(parseSysctl('iogpu.wired_limit_mb: 14000\n').wiredLimitMb).toBe(14000);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('parseSysctl on other shapes', () => {
|
||
|
|
it('reads an Intel Mac, whose output lacks three of the keys, without shifting a value', () => {
|
||
|
|
expect(parseSysctl(INTEL_SYSCTL)).toEqual({
|
||
|
|
memBytes: 17_179_869_184,
|
||
|
|
chip: 'Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz',
|
||
|
|
macosVersion: '13.6.7',
|
||
|
|
macosMajor: 13,
|
||
|
|
model: 'MacBookPro15,1',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reads an Intel Mac that does print the arm64 flag, as 0', () => {
|
||
|
|
const facts = parseSysctl(`hw.optional.arm64: 0\n${INTEL_SYSCTL}`);
|
||
|
|
expect(facts.arm64).toBe(false);
|
||
|
|
expect(facts.translated).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reads a process under Rosetta: the machine is arm64 and the process is translated', () => {
|
||
|
|
const facts = parseSysctl(MACMINI_SYSCTL.replace('sysctl.proc_translated: 0', 'sysctl.proc_translated: 1'));
|
||
|
|
expect(facts.arm64).toBe(true);
|
||
|
|
expect(facts.translated).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('drops a line that was cut off: a truncated output never yields half a value', () => {
|
||
|
|
// Cut in the middle of the chip name. The memory line before it is whole.
|
||
|
|
const cutInChip = 'hw.memsize: 17179869184\nmachdep.cpu.brand_string: Apple M';
|
||
|
|
expect(parseSysctl(cutInChip)).toEqual({ memBytes: 17_179_869_184 });
|
||
|
|
|
||
|
|
// Cut in the middle of the memory figure: '1717' would be a valid, wrong number.
|
||
|
|
expect(parseSysctl('hw.memsize: 1717')).toEqual({});
|
||
|
|
|
||
|
|
// Cut right after a whole line: everything up to there counts.
|
||
|
|
const cutAfterLine = MACMINI_SYSCTL.slice(0, MACMINI_SYSCTL.indexOf('hw.optional.arm64'));
|
||
|
|
expect(parseSysctl(cutAfterLine)).toEqual({ memBytes: 17_179_869_184, chip: 'Apple M4' });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reads nothing from an empty output or from error text', () => {
|
||
|
|
expect(parseSysctl('')).toEqual({});
|
||
|
|
expect(parseSysctl('\n\n')).toEqual({});
|
||
|
|
expect(
|
||
|
|
parseSysctl("sysctl: unknown oid 'hw.memsize'\nsysctl: cannot stat /proc/sys/hw/memsize: No such file\n")
|
||
|
|
).toEqual({});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('leaves out a value that does not parse instead of guessing', () => {
|
||
|
|
const facts = parseSysctl(
|
||
|
|
[
|
||
|
|
'hw.memsize: lots',
|
||
|
|
'machdep.cpu.brand_string: ',
|
||
|
|
'hw.optional.arm64: yes',
|
||
|
|
'sysctl.proc_translated: 2',
|
||
|
|
'iogpu.wired_limit_mb: -1',
|
||
|
|
'kern.osproductversion: Sequoia',
|
||
|
|
`hw.model: ${'x'.repeat(200)}`,
|
||
|
|
'',
|
||
|
|
].join('\n')
|
||
|
|
);
|
||
|
|
expect(facts).toEqual({});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('refuses a memory size of 0 or one too large to be a number of bytes', () => {
|
||
|
|
expect(parseSysctl('hw.memsize: 0\n').memBytes).toBeUndefined();
|
||
|
|
expect(parseSysctl('hw.memsize: 99999999999999999999999\n').memBytes).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('accepts Windows line ends and padding, and the first line of a key wins', () => {
|
||
|
|
const facts = parseSysctl('hw.memsize: 17179869184 \r\nhw.model: Mac16,10\r\nhw.model: Other9,9\r\n');
|
||
|
|
expect(facts).toEqual({ memBytes: 17_179_869_184, model: 'Mac16,10' });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reads the macOS major version from one, two or three numbers', () => {
|
||
|
|
expect(parseSysctl('kern.osproductversion: 14\n')).toMatchObject({ macosVersion: '14', macosMajor: 14 });
|
||
|
|
expect(parseSysctl('kern.osproductversion: 15.7\n')).toMatchObject({ macosVersion: '15.7', macosMajor: 15 });
|
||
|
|
expect(parseSysctl('kern.osproductversion: 13.6.7\n')).toMatchObject({ macosVersion: '13.6.7', macosMajor: 13 });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('readHardware on the two real Macs', () => {
|
||
|
|
it('describes the MacBook Pro', async () => {
|
||
|
|
const hw = await readHardware(
|
||
|
|
deps({ runSysctl: async () => MACBOOK_SYSCTL, freeDiskBytes: async () => MACBOOK_FREE_DISK })
|
||
|
|
);
|
||
|
|
expect(hw).toEqual({
|
||
|
|
platform: 'darwin',
|
||
|
|
arch: 'arm64',
|
||
|
|
arm64: true,
|
||
|
|
translated: false,
|
||
|
|
memBytes: 51_539_607_552,
|
||
|
|
memGb: '51.5',
|
||
|
|
memClassGb: 48,
|
||
|
|
chip: 'Apple M5 Pro',
|
||
|
|
model: 'Mac17,13',
|
||
|
|
macosVersion: '27.2',
|
||
|
|
macosMajor: 27,
|
||
|
|
wiredLimitMb: 0,
|
||
|
|
freeDiskBytes: MACBOOK_FREE_DISK,
|
||
|
|
freeDiskGb: '912',
|
||
|
|
source: 'sysctl',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('describes the Mac mini', async () => {
|
||
|
|
const hw = await readHardware(deps());
|
||
|
|
expect(hw).toEqual({
|
||
|
|
platform: 'darwin',
|
||
|
|
arch: 'arm64',
|
||
|
|
arm64: true,
|
||
|
|
translated: false,
|
||
|
|
memBytes: 17_179_869_184,
|
||
|
|
memGb: '17.2',
|
||
|
|
memClassGb: 16,
|
||
|
|
chip: 'Apple M4',
|
||
|
|
model: 'Mac16,10',
|
||
|
|
macosVersion: '27.2',
|
||
|
|
macosMajor: 27,
|
||
|
|
wiredLimitMb: 0,
|
||
|
|
freeDiskBytes: MACMINI_FREE_DISK,
|
||
|
|
freeDiskGb: '42.8',
|
||
|
|
source: 'sysctl',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is plain JSON: it survives a round trip unchanged', async () => {
|
||
|
|
const hw = await readHardware(deps());
|
||
|
|
expect(JSON.parse(JSON.stringify(hw))).toEqual(hw);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('describes an Intel Mac from its shorter output: no arm64 flag, an x64 process', async () => {
|
||
|
|
const hw = await readHardware(deps({ arch: 'x64', runSysctl: async () => INTEL_SYSCTL }));
|
||
|
|
expect(hw.source).toBe('sysctl');
|
||
|
|
expect(hw.arch).toBe('x64');
|
||
|
|
expect(hw.chip).toBe('Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz');
|
||
|
|
expect(hw.macosMajor).toBe(13);
|
||
|
|
expect(hw).not.toHaveProperty('arm64');
|
||
|
|
expect(hw).not.toHaveProperty('translated');
|
||
|
|
expect(hw).not.toHaveProperty('wiredLimitMb');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('takes the chip name from node:os when sysctl printed everything but that', async () => {
|
||
|
|
const hw = await readHardware(
|
||
|
|
deps({ runSysctl: async () => MACMINI_SYSCTL.replace('machdep.cpu.brand_string: Apple M4\n', '') })
|
||
|
|
);
|
||
|
|
expect(hw.source).toBe('sysctl');
|
||
|
|
expect(hw.chip).toBe('Fallback CPU');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('readHardware when the probe fails', () => {
|
||
|
|
it('answers from node:os when the spawn could not run or timed out', async () => {
|
||
|
|
const hw = await readHardware(
|
||
|
|
deps({
|
||
|
|
runSysctl: async () => undefined,
|
||
|
|
totalMemBytes: () => 34_359_738_368,
|
||
|
|
cpuModel: () => 'Apple M2 Max',
|
||
|
|
})
|
||
|
|
);
|
||
|
|
expect(hw).toEqual({
|
||
|
|
platform: 'darwin',
|
||
|
|
arch: 'arm64',
|
||
|
|
memBytes: 34_359_738_368,
|
||
|
|
memGb: '34.4',
|
||
|
|
memClassGb: 32,
|
||
|
|
chip: 'Apple M2 Max',
|
||
|
|
freeDiskBytes: MACMINI_FREE_DISK,
|
||
|
|
freeDiskGb: '42.8',
|
||
|
|
source: 'os-fallback',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('never rejects when a dependency throws or rejects', async () => {
|
||
|
|
const hw = await readHardware(
|
||
|
|
deps({
|
||
|
|
runSysctl: () => Promise.reject(new Error('spawn EACCES')),
|
||
|
|
freeDiskBytes: () => {
|
||
|
|
throw new Error('statfs failed');
|
||
|
|
},
|
||
|
|
cpuModel: () => {
|
||
|
|
throw new Error('no cpus');
|
||
|
|
},
|
||
|
|
})
|
||
|
|
);
|
||
|
|
expect(hw.source).toBe('os-fallback');
|
||
|
|
expect(hw.memBytes).toBe(8 * 1024 ** 3);
|
||
|
|
expect(hw).not.toHaveProperty('chip');
|
||
|
|
expect(hw).not.toHaveProperty('freeDiskBytes');
|
||
|
|
expect(hw).not.toHaveProperty('freeDiskGb');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('still answers when even the memory size cannot be read', async () => {
|
||
|
|
const hw = await readHardware(
|
||
|
|
deps({
|
||
|
|
runSysctl: async () => undefined,
|
||
|
|
totalMemBytes: () => {
|
||
|
|
throw new Error('no memory figure');
|
||
|
|
},
|
||
|
|
})
|
||
|
|
);
|
||
|
|
expect(hw).toMatchObject({ source: 'os-fallback', memBytes: 0, memGb: '0.0', memClassGb: 0 });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('trusts nothing of an output that has no memory size', async () => {
|
||
|
|
// A truncated or foreign output: the chip and version lines are whole, hw.memsize is not there.
|
||
|
|
const withoutMemory = MACMINI_SYSCTL.replace('hw.memsize: 17179869184\n', '');
|
||
|
|
const hw = await readHardware(deps({ runSysctl: async () => withoutMemory }));
|
||
|
|
expect(hw.source).toBe('os-fallback');
|
||
|
|
expect(hw.memBytes).toBe(8 * 1024 ** 3);
|
||
|
|
expect(hw.chip).toBe('Fallback CPU');
|
||
|
|
expect(hw).not.toHaveProperty('macosVersion');
|
||
|
|
expect(hw).not.toHaveProperty('macosMajor');
|
||
|
|
expect(hw).not.toHaveProperty('model');
|
||
|
|
expect(hw).not.toHaveProperty('wiredLimitMb');
|
||
|
|
expect(hw).not.toHaveProperty('arm64');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('falls back when the output was cut off inside the memory figure', async () => {
|
||
|
|
const hw = await readHardware(deps({ runSysctl: async () => 'hw.memsize: 1717' }));
|
||
|
|
expect(hw.source).toBe('os-fallback');
|
||
|
|
expect(hw.memBytes).toBe(8 * 1024 ** 3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('leaves the free disk out when it is unknown, and keeps the rest', async () => {
|
||
|
|
const hw = await readHardware(deps({ freeDiskBytes: async () => undefined }));
|
||
|
|
expect(hw.source).toBe('sysctl');
|
||
|
|
expect(hw).not.toHaveProperty('freeDiskBytes');
|
||
|
|
expect(hw).not.toHaveProperty('freeDiskGb');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports a full disk as 0 bytes free, which is a figure and not "unknown"', async () => {
|
||
|
|
const hw = await readHardware(deps({ freeDiskBytes: async () => 0 }));
|
||
|
|
expect(hw.freeDiskBytes).toBe(0);
|
||
|
|
expect(hw.freeDiskGb).toBe('0.0');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('readHardware off macOS', () => {
|
||
|
|
it('spawns nothing and reads no disk', async () => {
|
||
|
|
let spawned = 0;
|
||
|
|
let diskRead = 0;
|
||
|
|
const hw = await readHardware(
|
||
|
|
deps({
|
||
|
|
platform: 'linux',
|
||
|
|
arch: 'x64',
|
||
|
|
runSysctl: async () => {
|
||
|
|
spawned++;
|
||
|
|
return MACMINI_SYSCTL;
|
||
|
|
},
|
||
|
|
freeDiskBytes: async () => {
|
||
|
|
diskRead++;
|
||
|
|
return 1;
|
||
|
|
},
|
||
|
|
totalMemBytes: () => 67_430_023_168,
|
||
|
|
})
|
||
|
|
);
|
||
|
|
expect(spawned).toBe(0);
|
||
|
|
expect(diskRead).toBe(0);
|
||
|
|
expect(hw).toEqual({
|
||
|
|
platform: 'linux',
|
||
|
|
arch: 'x64',
|
||
|
|
memBytes: 67_430_023_168,
|
||
|
|
memGb: '67.4',
|
||
|
|
memClassGb: 63,
|
||
|
|
source: 'not-probed',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('the cache of readHardware', () => {
|
||
|
|
it('reuses one result for 60 seconds, then probes again', async () => {
|
||
|
|
let clock = 5_000_000;
|
||
|
|
let spawned = 0;
|
||
|
|
const counting = deps({
|
||
|
|
now: () => clock,
|
||
|
|
runSysctl: async () => {
|
||
|
|
spawned++;
|
||
|
|
return spawned === 1 ? MACMINI_SYSCTL : MACBOOK_SYSCTL;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const first = await readHardware(counting);
|
||
|
|
clock += HARDWARE_CACHE_TTL_MS - 1;
|
||
|
|
const second = await readHardware(counting);
|
||
|
|
expect(spawned).toBe(1);
|
||
|
|
expect(second).toBe(first);
|
||
|
|
|
||
|
|
clock += 1;
|
||
|
|
const third = await readHardware(counting);
|
||
|
|
expect(spawned).toBe(2);
|
||
|
|
expect(third.chip).toBe('Apple M5 Pro');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('caches a failed probe too: a broken sysctl is not spawned on every request', async () => {
|
||
|
|
let spawned = 0;
|
||
|
|
const failing = deps({
|
||
|
|
runSysctl: async () => {
|
||
|
|
spawned++;
|
||
|
|
return undefined;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
expect((await readHardware(failing)).source).toBe('os-fallback');
|
||
|
|
expect((await readHardware(failing)).source).toBe('os-fallback');
|
||
|
|
expect(spawned).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('lets concurrent callers share one probe', async () => {
|
||
|
|
let spawned = 0;
|
||
|
|
let release: (text: string) => void = () => {};
|
||
|
|
const slow = deps({
|
||
|
|
runSysctl: () => {
|
||
|
|
spawned++;
|
||
|
|
return new Promise<string>((resolve) => {
|
||
|
|
release = resolve;
|
||
|
|
});
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const callers = [readHardware(slow), readHardware(slow), readHardware(slow)];
|
||
|
|
// Let the three calls reach the spawn before it answers.
|
||
|
|
await new Promise((resolve) => setImmediate(resolve));
|
||
|
|
release(MACMINI_SYSCTL);
|
||
|
|
const results = await Promise.all(callers);
|
||
|
|
expect(spawned).toBe(1);
|
||
|
|
expect(results[1]).toBe(results[0]);
|
||
|
|
expect(results[2]).toBe(results[0]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('probes again after a reset, and does not trust a clock that ran backwards', async () => {
|
||
|
|
let clock = 9_000_000;
|
||
|
|
let spawned = 0;
|
||
|
|
const counting = deps({
|
||
|
|
now: () => clock,
|
||
|
|
runSysctl: async () => {
|
||
|
|
spawned++;
|
||
|
|
return MACMINI_SYSCTL;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
await readHardware(counting);
|
||
|
|
resetHardwareCache();
|
||
|
|
await readHardware(counting);
|
||
|
|
expect(spawned).toBe(2);
|
||
|
|
|
||
|
|
clock -= 10_000;
|
||
|
|
await readHardware(counting);
|
||
|
|
expect(spawned).toBe(3);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// The scripts below are POSIX shell. Codeman's server does not run on Windows.
|
||
|
|
describe.skipIf(process.platform === 'win32')('spawnSysctl, against a script of the test', () => {
|
||
|
|
let folder: string;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
folder = mkdtempSync(join(tmpdir(), 'codeman-local-model-spawn-'));
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
rmSync(folder, { recursive: true, force: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
/** An executable shell script standing in for sysctl. */
|
||
|
|
function script(body: string, mode = 0o755): string {
|
||
|
|
const path = join(folder, 'fake-sysctl');
|
||
|
|
writeFileSync(path, `#!/bin/sh\n${body}\n`, { mode });
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
|
||
|
|
it('passes the seven keys as arguments and nothing else: no -n, no shell', async () => {
|
||
|
|
// printf, not echo: echo would swallow a `-n` argument, the very thing this test looks for.
|
||
|
|
const output = await spawnSysctl(script('printf \'%s\\n\' "$@"'));
|
||
|
|
expect(output?.trimEnd().split('\n')).toEqual([...SYSCTL_KEYS]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reads the output of a program that exits 1, as sysctl does on an Intel Mac', async () => {
|
||
|
|
// Three keys are unknown there: their errors go to stderr, the other four lines to stdout.
|
||
|
|
const output = await spawnSysctl(
|
||
|
|
script(
|
||
|
|
[
|
||
|
|
"echo 'hw.memsize: 17179869184'",
|
||
|
|
"echo 'machdep.cpu.brand_string: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz'",
|
||
|
|
`echo "sysctl: unknown oid 'hw.optional.arm64'" >&2`,
|
||
|
|
`echo "sysctl: unknown oid 'sysctl.proc_translated'" >&2`,
|
||
|
|
`echo "sysctl: unknown oid 'iogpu.wired_limit_mb'" >&2`,
|
||
|
|
"echo 'kern.osproductversion: 13.6.7'",
|
||
|
|
"echo 'hw.model: MacBookPro15,1'",
|
||
|
|
'exit 1',
|
||
|
|
].join('\n')
|
||
|
|
)
|
||
|
|
);
|
||
|
|
expect(output).toBeDefined();
|
||
|
|
expect(output).not.toContain('unknown oid');
|
||
|
|
expect(parseSysctl(output as string)).toEqual({
|
||
|
|
memBytes: 17_179_869_184,
|
||
|
|
chip: 'Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz',
|
||
|
|
macosVersion: '13.6.7',
|
||
|
|
macosMajor: 13,
|
||
|
|
model: 'MacBookPro15,1',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('answers undefined for a program that does not exist or may not be run', async () => {
|
||
|
|
expect(await spawnSysctl(join(folder, 'no-such-program'))).toBeUndefined();
|
||
|
|
expect(await spawnSysctl(script('echo "hw.memsize: 1"', 0o644))).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('gives up on a program that hangs, and does not hand over half an output', async () => {
|
||
|
|
const started = Date.now();
|
||
|
|
// `exec` so the signal reaches the sleeper itself and nothing is left behind.
|
||
|
|
const output = await spawnSysctl(script("echo 'hw.memsize: 17179869184'\nexec sleep 20"), 200);
|
||
|
|
expect(output).toBeUndefined();
|
||
|
|
expect(Date.now() - started).toBeLessThan(5000);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('answers undefined for an output far larger than seven lines', async () => {
|
||
|
|
expect(await spawnSysctl(script("head -c 200000 /dev/zero | tr '\\0' 'x'"))).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not pass the server's environment to the program", async () => {
|
||
|
|
const name = 'CODEMAN_LOCAL_MODEL_TEST_SECRET';
|
||
|
|
process.env[name] = 'hunter2';
|
||
|
|
try {
|
||
|
|
const output = await spawnSysctl(script(`echo "secret=\${${name}:-unset}"`));
|
||
|
|
expect(output?.trim()).toBe('secret=unset');
|
||
|
|
} finally {
|
||
|
|
delete process.env[name];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('dataDirFreeBytes', () => {
|
||
|
|
let folder: string;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
folder = mkdtempSync(join(tmpdir(), 'codeman-local-model-disk-'));
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
rmSync(folder, { recursive: true, force: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reads the free bytes of the data dir with the real probe and the real statfs', async () => {
|
||
|
|
const free = await dataDirFreeBytes({ dataDir: () => folder });
|
||
|
|
expect(typeof free).toBe('number');
|
||
|
|
expect(free).toBeGreaterThanOrEqual(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('uses the nearest existing parent when the data dir does not exist yet', async () => {
|
||
|
|
const asked: string[] = [];
|
||
|
|
const free = await dataDirFreeBytes({
|
||
|
|
dataDir: () => join(folder, 'not', 'yet', 'there'),
|
||
|
|
freeBytesOf: async (path) => {
|
||
|
|
asked.push(path);
|
||
|
|
return 123;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
expect(free).toBe(123);
|
||
|
|
expect(asked).toEqual([folder]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does not touch a folder whose mount does not answer, or a path that is a file', async () => {
|
||
|
|
for (const kind of ['unknown', 'file'] as const) {
|
||
|
|
let touched = 0;
|
||
|
|
const free = await dataDirFreeBytes({
|
||
|
|
dataDir: () => folder,
|
||
|
|
probeKind: async () => kind,
|
||
|
|
freeBytesOf: async () => {
|
||
|
|
touched++;
|
||
|
|
return 1;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
expect(free, kind).toBeUndefined();
|
||
|
|
expect(touched, kind).toBe(0);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('stops walking up after a few parents', async () => {
|
||
|
|
let probes = 0;
|
||
|
|
const free = await dataDirFreeBytes({
|
||
|
|
dataDir: () => '/a/b/c/d/e/f/g/h/i/j/k/l',
|
||
|
|
probeKind: async () => {
|
||
|
|
probes++;
|
||
|
|
return 'absent';
|
||
|
|
},
|
||
|
|
freeBytesOf: async () => 1,
|
||
|
|
});
|
||
|
|
expect(free).toBeUndefined();
|
||
|
|
expect(probes).toBe(7);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('gives up on a statfs that does not come back, and never starts a second one meanwhile', async () => {
|
||
|
|
let started = 0;
|
||
|
|
let finish: (bytes: number) => void = () => {};
|
||
|
|
const hanging = {
|
||
|
|
dataDir: () => folder,
|
||
|
|
freeBytesOf: () => {
|
||
|
|
started++;
|
||
|
|
return new Promise<number>((resolve) => {
|
||
|
|
finish = resolve;
|
||
|
|
});
|
||
|
|
},
|
||
|
|
timeoutMs: 20,
|
||
|
|
};
|
||
|
|
|
||
|
|
// The first read waits its timeout and answers "unknown".
|
||
|
|
expect(await dataDirFreeBytes(hanging)).toBeUndefined();
|
||
|
|
expect(started).toBe(1);
|
||
|
|
|
||
|
|
// While that statfs is still out, later reads answer at once and start nothing.
|
||
|
|
const before = Date.now();
|
||
|
|
expect(await dataDirFreeBytes(hanging)).toBeUndefined();
|
||
|
|
expect(await dataDirFreeBytes({ ...hanging, timeoutMs: 60_000 })).toBeUndefined();
|
||
|
|
expect(Date.now() - before).toBeLessThan(1000);
|
||
|
|
expect(started).toBe(1);
|
||
|
|
|
||
|
|
// Once it finally answers, the next read works again.
|
||
|
|
finish(5);
|
||
|
|
await new Promise((resolve) => setImmediate(resolve));
|
||
|
|
expect(await dataDirFreeBytes({ ...hanging, freeBytesOf: async () => 42 })).toBe(42);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('never rejects when one of its reads does', async () => {
|
||
|
|
const failing = async (): Promise<never> => {
|
||
|
|
throw new Error('EIO');
|
||
|
|
};
|
||
|
|
expect(await dataDirFreeBytes({ dataDir: () => folder, freeBytesOf: failing })).toBeUndefined();
|
||
|
|
expect(await dataDirFreeBytes({ dataDir: () => folder, probeKind: failing })).toBeUndefined();
|
||
|
|
expect(
|
||
|
|
await dataDirFreeBytes({
|
||
|
|
dataDir: () => {
|
||
|
|
throw new Error('no home');
|
||
|
|
},
|
||
|
|
})
|
||
|
|
).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is bounded by default', () => {
|
||
|
|
expect(FREE_DISK_TIMEOUT_MS).toBe(2000);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('the real defaults', () => {
|
||
|
|
let folder: string;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
folder = mkdtempSync(join(tmpdir(), 'codeman-local-model-hw-'));
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
rmSync(folder, { recursive: true, force: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reads the free bytes of a real folder, and undefined for one that does not exist', async () => {
|
||
|
|
const free = await freeDiskBytesOf(folder);
|
||
|
|
expect(typeof free).toBe('number');
|
||
|
|
expect(free).toBeGreaterThanOrEqual(0);
|
||
|
|
expect(await freeDiskBytesOf(join(folder, 'missing', 'deeper'))).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('never rejects with the real spawn and the real disk read, whatever machine this is', async () => {
|
||
|
|
// Pretend to be a Mac so the spawn and the disk read really run. On Linux /usr/sbin/sysctl
|
||
|
|
// either does not exist or knows none of the keys: the answer is then the fallback.
|
||
|
|
const hw = await readHardware({ platform: 'darwin' });
|
||
|
|
expect(['sysctl', 'os-fallback']).toContain(hw.source);
|
||
|
|
expect(hw.memBytes).toBeGreaterThan(0);
|
||
|
|
expect(hw.memGb).toMatch(/^\d+(\.\d)?$/);
|
||
|
|
if (hw.freeDiskBytes !== undefined) {
|
||
|
|
expect(hw.freeDiskBytes).toBeGreaterThanOrEqual(0);
|
||
|
|
expect(hw.freeDiskGb).toMatch(/^\d+(\.\d)?$/);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|