buninu-linux 0.13.56 → 0.14.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +46 -46
- package/README.md +44 -14
- package/fetch-alpine.sh +1 -1
- package/initramfs/bin/dhclient +8 -0
- package/initramfs/bin/ping +8 -0
- package/initramfs/init.js +8 -7
- package/initramfs/lib/bunterm/renderer.js +1 -1
- package/initramfs/lib/bunterm/terminal.js +1 -1
- package/initramfs/lib/dhclient.js +135 -0
- package/initramfs/lib/dhcp-start.js +85 -0
- package/initramfs/lib/dhcp-transport.js +44 -0
- package/initramfs/lib/dhcp.js +124 -0
- package/initramfs/lib/dlopen.js +58 -11
- package/initramfs/lib/ping.js +110 -0
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +1 -1
- package/initramfs/usr/share/doc/buninu-linux/dhclient.md +82 -0
- package/initramfs/usr/share/doc/buninu-linux/ping.md +91 -0
- package/package.json +2 -2
- package/scripts/arch.js +5 -5
- package/scripts/arch.sh +5 -5
- package/scripts/modules.js +1 -0
- package/scripts/npm-packages.md +8 -8
- package/test/dhcp-start.test.js +67 -0
- package/test/dhcp.test.js +99 -0
- package/test/fetch-npm.test.js +1 -1
- package/test/libc.test.js +41 -0
- package/test/ping.test.js +80 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {createDhcpLauncher} from '../initramfs/lib/dhcp-start.js';
|
|
3
|
+
const pause=ms=>Bun.sleep(ms);
|
|
4
|
+
function fakeChild(){
|
|
5
|
+
let resolve,reject,stdout;
|
|
6
|
+
const exited=new Promise((yes,no)=>{resolve=yes;reject=no;});
|
|
7
|
+
return {exited,resolve,reject,kills:[],kill(signal){this.kills.push(signal);},
|
|
8
|
+
stdout:new ReadableStream({start(c){stdout=c;}}),stderr:null,
|
|
9
|
+
output(line){stdout.enqueue(Buffer.from(line));},end(){stdout.close();}};
|
|
10
|
+
}
|
|
11
|
+
test('launch returns immediately, isolates spawn failures and reuses running children',async()=>{
|
|
12
|
+
const calls=[],logs=[],children=[];
|
|
13
|
+
const launch=createDhcpLauncher({spawn(args){calls.push(args);if(args.at(-1)==='bad')throw new Error('spawn denied');const child=fakeChild();children.push(child);return child;},log:s=>logs.push(s),watchdogMs:500});
|
|
14
|
+
expect(launch(['bad','eth0','eth1'])).toEqual(['eth0','eth1']);
|
|
15
|
+
expect(launch(['eth0','eth1'])).toEqual([]);expect(calls.length).toBe(3);
|
|
16
|
+
expect(calls[2]).toContain('--no-dns');expect(calls[2]).toContain('--metric');
|
|
17
|
+
expect(logs.some(s=>s.includes('spawn denied'))).toBe(true);
|
|
18
|
+
for(const child of children){child.end();child.resolve(1);}await pause(5);
|
|
19
|
+
expect(logs.some(s=>s.includes('(exit 1); other interfaces are unaffected'))).toBe(true);
|
|
20
|
+
expect(launch(['eth0'])).toEqual(['eth0']);children.at(-1).end();children.at(-1).resolve(0);
|
|
21
|
+
});
|
|
22
|
+
test('watchdog terminates hung acquisition and escalates if SIGTERM does not exit',async()=>{
|
|
23
|
+
const child=fakeChild(),logs=[];
|
|
24
|
+
const launch=createDhcpLauncher({spawn:()=>child,log:s=>logs.push(s),watchdogMs:10,killGraceMs:10});
|
|
25
|
+
launch(['eth0']);await pause(60);
|
|
26
|
+
expect(child.kills).toEqual(['SIGTERM','SIGKILL']);child.end();child.resolve(137);await pause(5);
|
|
27
|
+
expect(logs.some(s=>s.includes('timed out; other interfaces are unaffected'))).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
test('successful one-shot exits and clears the watchdog',async()=>{
|
|
30
|
+
const child=fakeChild();const launch=createDhcpLauncher({spawn:()=>child,log:()=>{},watchdogMs:20,killGraceMs:5});
|
|
31
|
+
launch(['eth0']);child.output('dhclient: bound to 10.0.2.15/24, lease 86400s\n');child.end();child.resolve(0);await pause(50);
|
|
32
|
+
expect(child.kills).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
test('stream rejection, exit rejection, kill failure and throwing logger cannot escape',async()=>{
|
|
35
|
+
const child=fakeChild(),logs=[];
|
|
36
|
+
child.stderr=new ReadableStream({start(c){c.error(null);}});
|
|
37
|
+
child.kill=()=>{throw null;};
|
|
38
|
+
const launch=createDhcpLauncher({spawn:()=>child,log:s=>logs.push(s),watchdogMs:5,killGraceMs:5});
|
|
39
|
+
expect(()=>launch(['eth0'])).not.toThrow();await pause(30);child.end();child.reject(null);await pause(5);
|
|
40
|
+
expect(logs.some(s=>s.startsWith('[dhcp eth0] failed: timed out'))).toBe(true);
|
|
41
|
+
const bad=createDhcpLauncher({spawn(){throw null;},log(){throw new Error('console failed');}});
|
|
42
|
+
expect(()=>bad(['eth0'])).not.toThrow();expect(()=>bad(null)).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('usb0 success and late eth0 failure each produce one correctly attributed result',async()=>{
|
|
46
|
+
const children={eth0:fakeChild(),usb0:fakeChild()},logs=[];let stderr;
|
|
47
|
+
children.eth0.stderr=new ReadableStream({start(c){stderr=c;}});
|
|
48
|
+
const launch=createDhcpLauncher({spawn:args=>children[args.at(-1)],log:s=>logs.push(s),watchdogMs:500});
|
|
49
|
+
launch(['eth0','usb0']);
|
|
50
|
+
children.usb0.output('dhclient: bound to 192.168.42.2/24, lease 3600s\n');children.usb0.end();children.usb0.resolve(0);
|
|
51
|
+
await pause(5);
|
|
52
|
+
children.eth0.resolve(1);children.eth0.end();
|
|
53
|
+
// Exit notification precedes delivery of the error; the final result must wait.
|
|
54
|
+
await pause(5);stderr.enqueue(Buffer.from('dhclient: no DHCP lease received before timeout\n'));stderr.close();await pause(10);
|
|
55
|
+
expect(logs.filter(s=>s.includes('configured '))).toEqual(['[dhcp usb0] configured 192.168.42.2/24; command finished']);
|
|
56
|
+
expect(logs.filter(s=>s.includes('failed:'))).toEqual(['[dhcp eth0] failed: no DHCP lease received before timeout (exit 1); other interfaces are unaffected']);
|
|
57
|
+
expect(logs.some(s=>s.includes('usb0')&&s.includes('failed'))).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('unterminated output cannot hold the result open or prevent later invocations',async()=>{
|
|
61
|
+
const children=[],logs=[];
|
|
62
|
+
const launch=createDhcpLauncher({spawn(){const c=fakeChild();children.push(c);return c;},log:s=>logs.push(s),watchdogMs:500,outputGraceMs:10});
|
|
63
|
+
launch(['usb0']);children[0].output('dhclient: bound to 192.168.42.2/24, lease 3600s\n');children[0].resolve(0);
|
|
64
|
+
await pause(30);
|
|
65
|
+
expect(logs.some(s=>s.includes('configured 192.168.42.2/24'))).toBe(true);
|
|
66
|
+
expect(launch(['usb0'])).toEqual(['usb0']);children[1].end();children[1].resolve(0);
|
|
67
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,readFileSync,writeFileSync,rmSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {TYPE,encode,decode,leaseFrom,wrapIPv4,unwrapIPv4,checksum,DhcpClient,NakError} from '../initramfs/lib/dhcp.js';
|
|
5
|
+
import {parseDhclient,Configuration,manageLease} from '../initramfs/lib/dhclient.js';
|
|
6
|
+
const mac=Buffer.from('020000000001','hex');
|
|
7
|
+
function reply(request,type,{ip='10.20.0.10',server=[10,20,0,1],duration=120}={}){
|
|
8
|
+
const b=Buffer.from(request.subarray(0,240));b[0]=2;Buffer.from(ip.split('.').map(Number)).copy(b,16);
|
|
9
|
+
const lease=Buffer.alloc(4);lease.writeUInt32BE(duration);
|
|
10
|
+
return Buffer.concat([b,Buffer.from([53,1,type,54,4,...server,1,4,255,255,255,0,51,4]),lease,Buffer.from([3,4,...server,6,4,1,1,1,1,255])]);
|
|
11
|
+
}
|
|
12
|
+
test('DHCP CLI validates options before opening sockets',()=>{
|
|
13
|
+
expect(parseDhclient(['-4','-d','-1','-v','-timeout','8','--no-dns','-H','buninu','eth0'])).toMatchObject({interface:'eth0',once:true,timeout:8,dns:false});
|
|
14
|
+
expect(parseDhclient(['usb0']).interface).toBe('usb0');
|
|
15
|
+
expect(parseDhclient(['--metric','120','usb0']).metric).toBe(120);
|
|
16
|
+
for(const args of [['--metric','32767','eth0'],[],['-6','eth0'],['-timeout','NaN','eth0'],['-r','-1','eth0'],['eth0','eth1'],['../bad']])expect(()=>parseDhclient(args)).toThrow();
|
|
17
|
+
});
|
|
18
|
+
test('codec validates xid, MAC, truncation, options, mask and lease timers',()=>{
|
|
19
|
+
const req=encode({type:TYPE.DISCOVER,xid:123,mac}),ack=reply(req,TYPE.ACK);
|
|
20
|
+
expect(req.length).toBeGreaterThanOrEqual(300);expect(req.readUInt16BE(10)).toBe(0x8000);
|
|
21
|
+
const parsed=decode(ack,{xid:123,mac});expect(parsed.type).toBe(TYPE.ACK);
|
|
22
|
+
expect(decode(ack,{xid:456,mac})).toBeNull();expect(decode(ack,{mac:Buffer.alloc(6)})).toBeNull();
|
|
23
|
+
expect(decode(ack.subarray(0,ack.length-3))).toBeNull();
|
|
24
|
+
expect(leaseFrom(parsed,null,1000)).toMatchObject({prefix:24,renewAt:61000,rebindAt:106000,expiresAt:121000});
|
|
25
|
+
expect(()=>leaseFrom({...parsed,mask:'255.0.255.0'},null,0)).toThrow();
|
|
26
|
+
expect(leaseFrom({...parsed,t1:200,t2:100},null,0).renewAt).toBe(60000);
|
|
27
|
+
});
|
|
28
|
+
test('packet transport checks header checksum, fragmentation, ports and UDP checksum',()=>{
|
|
29
|
+
const payload=reply(encode({type:1,xid:1,mac}),5);
|
|
30
|
+
const packet=wrapIPv4(payload,'10.20.0.1');packet.writeUInt16BE(67,20);packet.writeUInt16BE(68,22);
|
|
31
|
+
expect(unwrapIPv4(packet)).toEqual(payload);
|
|
32
|
+
const bad=Buffer.from(packet);bad[12]^=1;expect(unwrapIPv4(bad)).toBeNull();
|
|
33
|
+
const fragment=Buffer.from(packet);fragment.writeUInt16BE(0x2000,6);fragment.writeUInt16BE(0,10);fragment.writeUInt16BE(checksum(fragment.subarray(0,20)),10);expect(unwrapIPv4(fragment)).toBeNull();
|
|
34
|
+
packet.writeUInt16BE(123,26);expect(unwrapIPv4(packet)).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
test('DORA retries, ignores unrelated replies, then renews and releases via unicast',async()=>{
|
|
37
|
+
let time=0,discover=0;const queue=[],sent=[];
|
|
38
|
+
const transport={send(b,server){sent.push({b,server});const t=b[242];
|
|
39
|
+
if(t===1&&++discover===1)return;
|
|
40
|
+
if(t===1){const wrong=reply(b,2);wrong.writeUInt32BE(999,4);queue.push(wrong,reply(b,2));}
|
|
41
|
+
if(t===3)queue.push(reply(b,5));
|
|
42
|
+
},receive(){return queue.shift();}};
|
|
43
|
+
const client=new DhcpClient({transport,mac,timeout:10,now:()=>time,sleep:async ms=>{time+=ms;},random:()=>.5});
|
|
44
|
+
const lease=await client.acquire();expect(lease.address).toBe('10.20.0.10');expect(discover).toBe(2);
|
|
45
|
+
time=lease.renewAt;const renewed=await client.renew(lease);expect(renewed.address).toBe(lease.address);
|
|
46
|
+
const renew=sent.at(-1);expect(renew.server).toBe('10.20.0.1');expect(renew.b.subarray(12,16)).toEqual(Buffer.from([10,20,0,10]));
|
|
47
|
+
expect(renew.b.readUInt16BE(10)).toBe(0);
|
|
48
|
+
client.release(renewed);expect(sent.at(-1).b[242]).toBe(7);expect(sent.at(-1).server).toBe(lease.server);
|
|
49
|
+
});
|
|
50
|
+
test('acquisition times out and NAK is surfaced during renewal',async()=>{
|
|
51
|
+
let time=0;const queue=[];
|
|
52
|
+
const client=new DhcpClient({transport:{send(){},receive:()=>queue.shift()},mac,timeout:.05,now:()=>time,sleep:async ms=>{time+=ms;}});
|
|
53
|
+
expect(await client.acquire()).toBeNull();
|
|
54
|
+
client.transport.send=b=>queue.push(reply(b,6));
|
|
55
|
+
await expect(client.renew({address:'10.20.0.10',server:'10.20.0.1',rebindAt:1000})).rejects.toBeInstanceOf(NakError);
|
|
56
|
+
});
|
|
57
|
+
test('configuration uses existing ip commands, preserves DNS across renewal and restores it',async()=>{
|
|
58
|
+
const dir=mkdtempSync(`${tmpdir()}/dhcp-test-`),path=`${dir}/resolv.conf`,calls=[];
|
|
59
|
+
try {
|
|
60
|
+
writeFileSync(path,'nameserver 9.9.9.9\n');
|
|
61
|
+
const config=new Configuration({interface:'eth0',dns:true,resolv:path,metric:100},async args=>calls.push(args));
|
|
62
|
+
const lease={address:'10.20.0.10',prefix:24,routers:['10.20.0.1'],dns:['1.1.1.1']};
|
|
63
|
+
await config.apply(lease);const renewed={...lease,dns:['8.8.8.8']};await config.apply(renewed,lease);
|
|
64
|
+
expect(calls[0]).toEqual(['addr','replace','10.20.0.10/24','brd','+','dev','eth0']);
|
|
65
|
+
expect(readFileSync(path,'utf8')).toContain('8.8.8.8');await config.clear(renewed);
|
|
66
|
+
expect(calls).toContainEqual(['route','replace','default','via','10.20.0.1','dev','eth0','metric','100']);
|
|
67
|
+
expect(calls).toContainEqual(['route','del','default','via','10.20.0.1','dev','eth0','metric','100']);
|
|
68
|
+
expect(readFileSync(path,'utf8')).toBe('nameserver 9.9.9.9\n');
|
|
69
|
+
await config.apply(lease);writeFileSync(path,'external edit\n');await config.clear(lease);expect(readFileSync(path,'utf8')).toBe('external edit\n');
|
|
70
|
+
}finally{rmSync(dir,{recursive:true,force:true});}
|
|
71
|
+
});
|
|
72
|
+
test('default invocation configures once and returns without starting renewal',async()=>{
|
|
73
|
+
const events=[],lease={address:'10.20.0.10',prefix:24,seconds:60};
|
|
74
|
+
const client={async acquire(){events.push('acquire');return lease;},renew(){throw new Error('must not renew');},release(){events.push('release');}};
|
|
75
|
+
await manageLease(client,{async apply(){events.push('apply');},async clear(){events.push('clear');}},{log:()=>{}});
|
|
76
|
+
expect(events).toEqual(['acquire','apply']);
|
|
77
|
+
expect(parseDhclient(['usb0']).once).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
test('one-shot keeps a successful lease but cleans up failed configuration',async()=>{
|
|
80
|
+
const events=[],client={async acquire(){return {address:'10.0.0.2',prefix:24,seconds:60};},release(){events.push('release');}};
|
|
81
|
+
const config={async apply(){},async clear(){events.push('clear');}};
|
|
82
|
+
await manageLease(client,config,{once:true,log:()=>{}});expect(events).toEqual([]);
|
|
83
|
+
config.apply=async()=>{throw new Error('route failed');};
|
|
84
|
+
await expect(manageLease(client,config,{once:true,log:()=>{}})).rejects.toThrow('route failed');expect(events).toEqual(['release','clear']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('runtime libc sendto delivers a UDP datagram on loopback',async()=>{
|
|
88
|
+
const {libc,ptr,check}=await import('../initramfs/lib/dlopen.js');
|
|
89
|
+
let received;
|
|
90
|
+
const server=await Bun.udpSocket({hostname:'127.0.0.1',port:0,socket:{data(_socket,data){received=Buffer.from(data);}}});
|
|
91
|
+
const fd=check('socket',libc.symbols.socket(2,2,17));
|
|
92
|
+
try{
|
|
93
|
+
const target=Buffer.alloc(16);target.writeUInt16LE(2);target.writeUInt16BE(server.port,2);target.set([127,0,0,1],4);
|
|
94
|
+
const data=Buffer.from('dhcp transport libc probe');
|
|
95
|
+
expect(check('sendto',Number(libc.symbols.sendto(fd,ptr(data),data.length,0,ptr(target),16)))).toBe(data.length);
|
|
96
|
+
for(let i=0;i<100&&!received;i++)await Bun.sleep(5);
|
|
97
|
+
expect(received).toEqual(data);
|
|
98
|
+
}finally{libc.symbols.close(fd);server.close();}
|
|
99
|
+
});
|
package/test/fetch-npm.test.js
CHANGED
|
@@ -23,7 +23,7 @@ const url = `http://127.0.0.1:${server.port}/archive`;
|
|
|
23
23
|
|
|
24
24
|
test("five exact registry pins share an architecture-independent cache", () => {
|
|
25
25
|
expect(npmPackages.map(({ name, version }) => `${name}@${version}`)).toEqual([
|
|
26
|
-
"buninu-browser@0.2.
|
|
26
|
+
"buninu-browser@0.2.4", "buninu-land@0.1.4", "@drxiaozhi/casty@1.3.7",
|
|
27
27
|
"@drxiaozhi/jspulse@0.2.1", "@drxiaozhi/jspinyin@0.1.1",
|
|
28
28
|
]);
|
|
29
29
|
expect(basename(npmDownloadsDir)).toBe("npm");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {libc,libcPath,errno,strerror,SysError,withInetSocket} from '../initramfs/lib/dlopen.js';
|
|
3
|
+
import {spawnSync} from 'node:child_process';
|
|
4
|
+
test('host libc loads automatically and exposes the platform errno',()=>{
|
|
5
|
+
expect(libc.symbols.close(-1)).toBe(-1);expect(libcPath.length).toBeGreaterThan(0);expect(errno()).toBe(9);expect(strerror(9)).toContain('descriptor');expect(new SysError('close').errno).toBe(9);
|
|
6
|
+
if(process.platform==='android')expect(libcPath).toBe('libc.so');
|
|
7
|
+
});
|
|
8
|
+
test('an explicit invalid libc path fails instead of silently falling back',()=>{
|
|
9
|
+
const r=spawnSync(process.execPath,['-e',`const {libc}=await import(${JSON.stringify(new URL('../initramfs/lib/dlopen.js',import.meta.url).href)});libc.symbols.close(-1)`],{env:{...process.env,BUNINU_LIBC:'/nonexistent/buninu-libc.so'},encoding:'utf8'});
|
|
10
|
+
expect(r.status).not.toBe(0);expect(r.stderr).toContain('/nonexistent/buninu-libc.so');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('existing libc socket/ioctl helper still queries loopback without sysfs',()=>{
|
|
14
|
+
const request=Buffer.alloc(40);request.write('lo');
|
|
15
|
+
withInetSocket(ioctl=>ioctl(0x8933,request,'SIOCGIFINDEX'));
|
|
16
|
+
expect(request.readInt32LE(16)).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('manual library resolves only requested symbols and keeps working after a missing symbol',()=>{
|
|
20
|
+
const moduleURL=new URL('../initramfs/lib/dlopen.js',import.meta.url).href;
|
|
21
|
+
const script=`
|
|
22
|
+
import {mock} from 'bun:test';
|
|
23
|
+
const ffi=await import('bun:ffi');
|
|
24
|
+
const calls=[];
|
|
25
|
+
mock.module('bun:ffi',()=>({...ffi,dlopen(path,symbols){
|
|
26
|
+
if(path!=='/manual/partial.so')throw new Error('unexpected fallback');
|
|
27
|
+
const name=Object.keys(symbols)[0];calls.push(name);
|
|
28
|
+
if(name!=='close')throw new Error('missing '+name);
|
|
29
|
+
return {symbols:{close:()=>7},close(){}};
|
|
30
|
+
}}));
|
|
31
|
+
const {libc}=await import(${JSON.stringify(moduleURL)});
|
|
32
|
+
if(calls.length)throw new Error('eager symbol resolution');
|
|
33
|
+
if(libc.symbols.close(0)!==7)throw new Error('supported symbol failed');
|
|
34
|
+
try {libc.symbols.mount;throw new Error('missing symbol accepted');}
|
|
35
|
+
catch(e){if(!e.message.includes('missing mount'))throw e;}
|
|
36
|
+
if(libc.symbols.close(0)!==7)throw new Error('missing symbol poisoned library');
|
|
37
|
+
if(calls.filter(x=>x==='close').length!==1)throw new Error('symbol not cached');
|
|
38
|
+
`;
|
|
39
|
+
const r=spawnSync(process.execPath,['-e',script],{env:{...process.env,BUNINU_LIBC:'/manual/partial.so'},encoding:'utf8'});
|
|
40
|
+
expect(r.stderr).toBe('');expect(r.status).toBe(0);
|
|
41
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {spawnSync} from 'node:child_process';
|
|
3
|
+
import {resolve} from 'node:path';
|
|
4
|
+
import {parsePing,checksum,echoPacket,isEchoReply} from '../initramfs/lib/ping.js';
|
|
5
|
+
const command=resolve(import.meta.dir,'../initramfs/bin/ping');
|
|
6
|
+
const system=Bun.which('ping');
|
|
7
|
+
const separatePing6=Bun.which('ping6');
|
|
8
|
+
const supports6=system&&spawnSync(system,['-6','-c','1','-W','1','::1'],{encoding:'utf8',timeout:2000}).status===0;
|
|
9
|
+
const run=(args,reference=false)=>{
|
|
10
|
+
const usePing6=reference&&args.includes('-6')&&!supports6&&separatePing6;
|
|
11
|
+
const executable=reference?(usePing6?separatePing6:system):process.execPath;
|
|
12
|
+
const options=reference?args.filter(a=>a!=='-4'&&(!usePing6||a!=='-6')):[command,...args];
|
|
13
|
+
return spawnSync(executable,options,{encoding:'utf8',timeout:6000,env:{...process.env,LC_ALL:'C'}});
|
|
14
|
+
};
|
|
15
|
+
const summary=s=>{const m=/(\d+) packets transmitted, (\d+) (?:packets )?received,.*?(\d+)% packet loss/.exec(s);return m?m.slice(1).map(Number):null};
|
|
16
|
+
const probe=system?run(['-4','-c','1','-W','1','127.0.0.1'],true):null;
|
|
17
|
+
const available=probe?.status===0;
|
|
18
|
+
|
|
19
|
+
test('ping parses common flags and rejects malformed arguments',()=>{
|
|
20
|
+
expect(parsePing(['-4nqD','-c3','-i','.2','-W','1','-w','5','-s','17','-t64','-I','lo','localhost'])).toMatchObject({family:4,count:3,interval:.2,timeout:1,deadline:5,size:17,ttl:64,source:'lo',host:'localhost',quiet:true,timestamp:true});
|
|
21
|
+
expect(parsePing(['localhost']).count).toBe(4);
|
|
22
|
+
expect(parsePing(['-6','--','::1']).family).toBe(6);
|
|
23
|
+
for(const args of [[],['-c'],['-c','0','localhost'],['-c','Infinity','localhost'],['-W','NaN','localhost'],['-s','65508','localhost'],['-s','-1','localhost'],['-t','256','localhost'],['-i','0','localhost'],['-w','0','localhost'],['-Z','localhost'],['a','b']])expect(()=>parsePing(args)).toThrow();
|
|
24
|
+
expect(parsePing(['--help']).help).toBe(true);
|
|
25
|
+
expect(run(['-c','0','127.0.0.1']).status).toBe(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('ICMP checksum, reply identity, data integrity and raw IPv4 header validation',()=>{
|
|
29
|
+
for(const size of [0,1,17,56]){
|
|
30
|
+
const request=echoPacket(4,123,7,size);expect(checksum(request)).toBe(0);
|
|
31
|
+
const reply=Buffer.from(request);reply[0]=0;reply.writeUInt16BE(0,2);reply.writeUInt16BE(checksum(reply),2);
|
|
32
|
+
const info={family:4,raw:false,id:123,seq:7,packet:request};
|
|
33
|
+
expect(isEchoReply(reply,info)).toBe(true);expect(isEchoReply(request,info)).toBe(false);
|
|
34
|
+
expect(isEchoReply(reply,{...info,id:124})).toBe(false);expect(isEchoReply(reply,{...info,seq:8})).toBe(false);
|
|
35
|
+
const ip=Buffer.alloc(24);ip[0]=0x46;ip[9]=1;
|
|
36
|
+
expect(isEchoReply(Buffer.concat([ip,reply]),{...info,raw:true})).toBe(true);
|
|
37
|
+
expect(isEchoReply(reply.subarray(0,7),info)).toBe(false);
|
|
38
|
+
if(size){reply[8]^=1;expect(isEchoReply(reply,info)).toBe(false);}
|
|
39
|
+
}
|
|
40
|
+
const packet=echoPacket(6,3,4,8),reply=Buffer.from(packet);reply[0]=129;
|
|
41
|
+
expect(isEchoReply(reply,{family:6,raw:false,id:3,seq:4,packet})).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
for(const size of [0,17,56,1400])test.skipIf(!available)(`system ping comparison: IPv4 count, payload ${size}, statistics and exit status`,()=>{
|
|
45
|
+
const args=['-4','-n','-c','3','-i','0.2','-W','1','-s',String(size),'127.0.0.1'];
|
|
46
|
+
const reference=run(args,true),actual=run(args);
|
|
47
|
+
expect(actual.status).toBe(reference.status);expect(actual.status).toBe(0);
|
|
48
|
+
expect(summary(actual.stdout)).toEqual(summary(reference.stdout));expect(summary(actual.stdout)).toEqual([3,3,0]);
|
|
49
|
+
expect(actual.stdout).toContain(`${size+8} bytes from 127.0.0.1`);expect(actual.stdout).toContain('icmp_seq=3');
|
|
50
|
+
expect(actual.stdout).toMatch(/rtt min\/avg\/max\/mdev = [\d.]+\/[\d.]+\/[\d.]+\/[\d.]+ ms/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test.skipIf(!available)('system ping comparison: hostname, source address, quiet mode and TTL',()=>{
|
|
54
|
+
const args=['-4','-n','-q','-c','2','-i','.2','-W','1','-t','32','-I','127.0.0.1','localhost'];
|
|
55
|
+
const reference=run(args,true),actual=run(args);
|
|
56
|
+
expect(actual.status).toBe(reference.status);expect(actual.status).toBe(0);expect(summary(actual.stdout)).toEqual(summary(reference.stdout));expect(actual.stdout).not.toContain('icmp_seq=');
|
|
57
|
+
});
|
|
58
|
+
const ipv6=available&&run(['-6','-c','1','-W','1','::1'],true).status===0;
|
|
59
|
+
test.skipIf(!ipv6)('system ping comparison: IPv6 loopback',()=>{
|
|
60
|
+
const args=['-6','-n','-c','3','-i','.2','-W','1','::1'],reference=run(args,true),actual=run(args);
|
|
61
|
+
expect(actual.status).toBe(reference.status);expect(actual.status).toBe(0);expect(summary(actual.stdout)).toEqual([3,3,0]);expect(summary(actual.stdout)).toEqual(summary(reference.stdout));
|
|
62
|
+
});
|
|
63
|
+
test.skipIf(!available)('system ping comparison: deadline before requested count returns failure',()=>{
|
|
64
|
+
const args=['-4','-q','-c','100','-i','.2','-w','1','127.0.0.1'],reference=run(args,true),begin=performance.now(),actual=run(args);
|
|
65
|
+
expect(actual.status).toBe(reference.status);expect(actual.status).toBe(1);expect(performance.now()-begin).toBeLessThan(2500);expect(summary(actual.stdout)[0]).toBeLessThan(100);
|
|
66
|
+
});
|
|
67
|
+
test.skipIf(!available)('Ctrl-C prints statistics and releases the socket',async()=>{
|
|
68
|
+
const child=Bun.spawn([process.execPath,command,'-i','.2','127.0.0.1'],{stdout:'pipe',stderr:'pipe'});
|
|
69
|
+
const reader=child.stdout.getReader();let output='';
|
|
70
|
+
try{
|
|
71
|
+
for(;;){const {value,done}=await reader.read();if(done)break;output+=new TextDecoder().decode(value);if(output.includes('icmp_seq=')){child.kill('SIGINT');break;}}
|
|
72
|
+
for(;;){const {value,done}=await reader.read();if(done)break;output+=new TextDecoder().decode(value);}
|
|
73
|
+
expect(await child.exited).toBe(0);expect(output).toContain('ping statistics');expect(summary(output)[1]).toBeGreaterThan(0);
|
|
74
|
+
}finally{child.kill();reader.releaseLock();}
|
|
75
|
+
},6000);
|
|
76
|
+
|
|
77
|
+
test.skipIf(!available)('sub-200ms interval and fractional timeout are supported independently of old Android ping',()=>{
|
|
78
|
+
const r=run(['-c','3','-i','.01','-W','.1','-D','127.0.0.1']);
|
|
79
|
+
expect(r.status).toBe(0);expect(summary(r.stdout)).toEqual([3,3,0]);expect(r.stdout).toMatch(/\[\d+\.\d+\] 64 bytes/);
|
|
80
|
+
});
|