UNPKG

node-os-utils

Version:

Advanced cross-platform operating system monitoring utilities with TypeScript support

280 lines 14.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const chai_1 = require("chai"); const linux_adapter_1 = require("../../../src/adapters/linux-adapter"); const errors_1 = require("../../../src/types/errors"); const os_1 = __importDefault(require("os")); describe('LinuxAdapter 内部解析逻辑', () => { it('应基于差分快照计算 CPU 使用率', () => { const adapter = new linux_adapter_1.LinuxAdapter(); const internal = adapter; const snapshotA = `cpu 100 20 80 1000 10 5 5 0\n` + `cpu0 50 10 40 500 5 2 3 0\n` + `cpu1 50 10 40 500 5 3 2 0\n`; const snapshotB = `cpu 150 30 120 1100 20 10 10 0\n` + `cpu0 80 15 60 550 10 4 5 0\n` + `cpu1 70 15 60 550 10 6 5 0\n`; const first = internal.parseCpuStat(snapshotA); const second = internal.parseCpuStat(snapshotB); const usage = internal.calculateCpuUsage(first, second); (0, chai_1.expect)(usage.overall).to.be.closeTo(54.54, 0.1); (0, chai_1.expect)(usage.user).to.be.closeTo(27.27, 0.1); (0, chai_1.expect)(usage.system).to.be.closeTo(18.18, 0.1); (0, chai_1.expect)(usage.idle).to.be.closeTo(45.45, 0.1); (0, chai_1.expect)(usage.iowait).to.be.closeTo(4.54, 0.1); (0, chai_1.expect)(usage.irq).to.be.closeTo(2.27, 0.1); (0, chai_1.expect)(usage.softirq).to.be.closeTo(2.27, 0.1); (0, chai_1.expect)(usage.cores).to.have.lengthOf(2); (0, chai_1.expect)(usage.cores[0]).to.be.closeTo(56.14, 0.2); (0, chai_1.expect)(usage.cores[1]).to.be.closeTo(52.83, 0.2); }); it('应正确解析 ip addr 输出中的网卡名称与地址', () => { const adapter = new linux_adapter_1.LinuxAdapter(); const internal = adapter; const sampleOutput = `1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000\n` + ` link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00\n` + ` inet 127.0.0.1/8 scope host lo\n` + ` valid_lft forever preferred_lft forever\n` + ` inet6 ::1/128 scope host \n` + ` valid_lft forever preferred_lft forever\n` + `2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000\n` + ` link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff\n` + ` inet 192.168.1.10/24 brd 192.168.1.255 scope global dynamic eth0\n` + ` valid_lft 86325sec preferred_lft 86325sec\n` + ` inet6 fe80::aabb:ccff:fedd:eeff/64 scope link \n` + ` valid_lft forever preferred_lft forever\n`; const interfaces = internal.parseNetworkInterfaces(sampleOutput); (0, chai_1.expect)(interfaces).to.have.lengthOf(2); const loopback = interfaces[0]; const eth0 = interfaces[1]; (0, chai_1.expect)(loopback.name).to.equal('lo'); (0, chai_1.expect)(loopback.state).to.equal('unknown'); (0, chai_1.expect)(loopback.internal).to.be.true; (0, chai_1.expect)(loopback.addresses.map((item) => item.address)).to.include.members(['127.0.0.1', '::1']); (0, chai_1.expect)(eth0.name).to.equal('eth0'); (0, chai_1.expect)(eth0.state).to.equal('up'); (0, chai_1.expect)(eth0.internal).to.be.false; (0, chai_1.expect)(eth0.addresses.map((item) => item.address)).to.include('192.168.1.10'); }); it('应解析进程内存与启动时间', () => { const adapter = new linux_adapter_1.LinuxAdapter(); const internal = adapter; const stat = '123 (bash) R 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 500 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0'; const status = 'Name:\tbash\nThreads:\t2\nVmRSS:\t2048 kB\nVmSize:\t4096 kB\n'; const cmdline = 'bash\0--login\0'; const result = internal.parseProcessInfo(123, stat, status, cmdline); (0, chai_1.expect)(result.memoryUsage).to.equal(2048 * 1024); (0, chai_1.expect)(result.rss).to.equal(2048 * 1024); const now = Date.now(); let systemUptimeMs = 0; try { systemUptimeMs = os_1.default.uptime() * 1000; } catch { systemUptimeMs = 0; } (0, chai_1.expect)(result.startTime).to.be.at.most(now); const lowerBound = systemUptimeMs > 0 ? now - systemUptimeMs - 1000 : now - 1000; (0, chai_1.expect)(result.startTime).to.be.at.least(lowerBound); }); it('应优先使用 uname -m 解析体系结构', () => { const adapter = new linux_adapter_1.LinuxAdapter(); const internal = adapter; const uname = 'Linux test-host 5.15.0-89-generic #99-Ubuntu SMP PREEMPT Fri x86_64 x86_64 x86_64 GNU/Linux'; const version = 'Ubuntu 5.15.0-89-generic'; const uptime = '12345.67 4567.89'; const loadavg = '0.10 0.20 0.30 1/234 567'; const machine = 'x86_64\n'; const info = internal.parseSystemInfo(uname, version, uptime, loadavg, machine); (0, chai_1.expect)(info.arch).to.equal('x86_64'); }); it('在缺少 uname -m 时仍能推断常见架构', () => { const adapter = new linux_adapter_1.LinuxAdapter(); const internal = adapter; const uname = 'Linux test 6.5.0-21 #1 SMP PREEMPT_DYNAMIC Wed aarch64 GNU/Linux'; const version = '#1 SMP PREEMPT'; const uptime = '1000.00 2000.00'; const loadavg = '0.50 0.60 0.70 1/1 2'; const info = internal.parseSystemInfo(uname, version, uptime, loadavg, ''); (0, chai_1.expect)(info.arch.toLowerCase()).to.equal('aarch64'); }); it('应正确解析多种默认网关格式', () => { const adapter = new linux_adapter_1.LinuxAdapter(); const internal = adapter; const viaOutput = 'default via 192.168.1.1 dev eth0 proto dhcp metric 100'; const directOutput = 'default dev ppp0 scope link'; const viaResult = internal.parseDefaultGateway(viaOutput); (0, chai_1.expect)(viaResult).to.deep.equal({ gateway: '192.168.1.1', interface: 'eth0' }); const directResult = internal.parseDefaultGateway(directOutput); (0, chai_1.expect)(directResult).to.deep.equal({ gateway: null, interface: 'ppp0' }); }); it('网络命令回退失败时应保留主次错误', async () => { const adapter = new linux_adapter_1.LinuxAdapter(); let callCount = 0; adapter.executeCommand = async (command) => { callCount += 1; if (command === 'ip addr show') { throw errors_1.MonitorError.createCommandFailed('linux', command, { reason: 'ip missing' }); } throw errors_1.MonitorError.createCommandFailed('linux', command, { reason: 'ifconfig missing' }); }; try { await adapter.getNetworkInterfaces(); chai_1.expect.fail('应该抛出 MonitorError'); } catch (error) { (0, chai_1.expect)(error).to.be.instanceOf(errors_1.MonitorError); (0, chai_1.expect)(error.details.primary.details.reason).to.equal('ip missing'); (0, chai_1.expect)(error.details.fallback.details.reason).to.equal('ifconfig missing'); } (0, chai_1.expect)(callCount).to.equal(2); }); it('容器环境应禁用系统服务能力', async () => { const adapter = new linux_adapter_1.LinuxAdapter(); const internal = adapter; internal.containerMode = true; internal.supportedFeatures.system.services = false; internal.executeCommand = async () => { throw new Error('systemctl should not be called in container mode'); }; (0, chai_1.expect)(adapter.getSupportedFeatures().system.services).to.be.false; try { await adapter.getSystemServices(); chai_1.expect.fail('should throw MonitorError'); } catch (error) { (0, chai_1.expect)(error).to.be.instanceOf(errors_1.MonitorError); (0, chai_1.expect)(error.code).to.equal(errors_1.ErrorCode.PLATFORM_NOT_SUPPORTED); } }); it('T022: getCPUInfo() 在读取 /proc/cpuinfo 失败时应降级到 os.cpus() 数据而非抛出异常', async () => { const adapter = new linux_adapter_1.LinuxAdapter(); const internal = adapter; // stub readFile 使 /proc/cpuinfo 读取失败 internal.readFile = async () => { throw new errors_1.MonitorError('/proc/cpuinfo 不可访问', errors_1.ErrorCode.COMMAND_FAILED, 'linux'); }; const result = await adapter.getCPUInfo(); (0, chai_1.expect)(result).to.be.an('object'); (0, chai_1.expect)(result.cores).to.be.a('number').and.to.be.greaterThan(0); (0, chai_1.expect)(result.threads).to.be.a('number').and.to.be.greaterThan(0); (0, chai_1.expect)(result.model).to.be.a('string').and.to.have.length.greaterThan(0); }); // ——— #37 修复:df 遇到无权限挂载点时不应整体失败 ——— it('getDiskInfo() 在 df 遇到权限错误但 stdout 有效时应正常返回磁盘列表', async () => { const adapter = new linux_adapter_1.LinuxAdapter(); adapter.executeCommand = async () => ({ stdout: [ 'Filesystem Size Used Avail Use% Mounted on', '/dev/sda1 50G 20G 30G 40% /', '/dev/sdb1 100G 60G 40G 60% /data' ].join('\n'), stderr: 'df: /run/user/1000/doc: Operation not permitted', exitCode: 1, platform: 'linux', executionTime: 5, command: 'df -h' }); const result = await adapter.getDiskInfo(); (0, chai_1.expect)(result).to.be.an('array').with.lengthOf(2); (0, chai_1.expect)(result[0].mountpoint).to.equal('/'); (0, chai_1.expect)(result[1].mountpoint).to.equal('/data'); }); it('getDiskInfo() 在 df stdout 为空时应抛出错误', async () => { const adapter = new linux_adapter_1.LinuxAdapter(); adapter.executeCommand = async () => ({ stdout: '', stderr: 'df: command not found', exitCode: 127, platform: 'linux', executionTime: 0, command: 'df -h' }); try { await adapter.getDiskInfo(); chai_1.expect.fail('应该抛出 MonitorError'); } catch (error) { (0, chai_1.expect)(error).to.be.instanceOf(errors_1.MonitorError); } }); it('getDiskUsage() 在 df 遇到权限错误但 stdout 有效时应正常返回磁盘列表', async () => { const adapter = new linux_adapter_1.LinuxAdapter(); adapter.executeCommand = async () => ({ stdout: [ 'Filesystem 1B-blocks Used Available Use% Mounted on', '/dev/sda1 53687091200 21474836480 32212254720 40% /', '/dev/sdb1 107374182400 64424509440 42949672960 60% /data' ].join('\n'), stderr: 'df: /run/user/1000/doc: Operation not permitted', exitCode: 1, platform: 'linux', executionTime: 5, command: 'df -B1' }); const result = await adapter.getDiskUsage(); (0, chai_1.expect)(result).to.be.an('array').with.lengthOf(2); (0, chai_1.expect)(result[0].mountPoint).to.equal('/'); (0, chai_1.expect)(result[1].mountPoint).to.equal('/data'); (0, chai_1.expect)(result[0].usagePercentage).to.equal(40); }); it('getDiskUsage() 在 df stdout 为空时应抛出错误', async () => { const adapter = new linux_adapter_1.LinuxAdapter(); adapter.executeCommand = async () => ({ stdout: '', stderr: 'df: command not found', exitCode: 127, platform: 'linux', executionTime: 0, command: 'df -B1' }); try { await adapter.getDiskUsage(); chai_1.expect.fail('应该抛出 MonitorError'); } catch (error) { (0, chai_1.expect)(error).to.be.instanceOf(errors_1.MonitorError); } }); it('应在 ss 不可用时回退到 netstat 解析连接', async () => { const adapter = new linux_adapter_1.LinuxAdapter(); const internal = adapter; const commands = []; internal.executeCommand = async (command) => { commands.push(command); if (command === 'ss -tuln') { throw errors_1.MonitorError.createCommandFailed('linux', command, { reason: 'missing ss' }); } if (command === 'netstat -tuln') { return { stdout: 'Proto Recv-Q Send-Q Local Address Foreign Address State\n' + 'tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN\n', stderr: '', exitCode: 0, platform: 'linux', executionTime: 1, command }; } return { stdout: '', stderr: 'unsupported', exitCode: 1, platform: 'linux', executionTime: 0, command }; }; const results = await adapter.getNetworkConnections(); (0, chai_1.expect)(results).to.have.lengthOf(1); (0, chai_1.expect)(results[0].protocol).to.equal('tcp'); (0, chai_1.expect)(results[0].state).to.equal('listen'); (0, chai_1.expect)(results[0].localAddress).to.equal('0.0.0.0:22'); (0, chai_1.expect)(results[0].foreignAddress).to.equal('0.0.0.0:*'); (0, chai_1.expect)(commands).to.deep.equal(['ss -tuln', 'netstat -tuln']); }); }); //# sourceMappingURL=linux-adapter.test.js.map