255 lines
8.6 KiB
TypeScript
255 lines
8.6 KiB
TypeScript
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||
|
|
import {
|
||
|
|
VMConfig,
|
||
|
|
SocketClient,
|
||
|
|
NetworkManager,
|
||
|
|
SmartVM,
|
||
|
|
} from '../ts/index.js';
|
||
|
|
import type { IMicroVMConfig } from '../ts/index.js';
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// VMConfig Tests
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
const sampleConfig: IMicroVMConfig = {
|
||
|
|
id: 'test-vm-1',
|
||
|
|
bootSource: {
|
||
|
|
kernelImagePath: '/path/to/vmlinux',
|
||
|
|
bootArgs: 'console=ttyS0 reboot=k panic=1',
|
||
|
|
},
|
||
|
|
machineConfig: {
|
||
|
|
vcpuCount: 2,
|
||
|
|
memSizeMib: 256,
|
||
|
|
smt: false,
|
||
|
|
},
|
||
|
|
drives: [
|
||
|
|
{
|
||
|
|
driveId: 'rootfs',
|
||
|
|
pathOnHost: '/path/to/rootfs.ext4',
|
||
|
|
isRootDevice: true,
|
||
|
|
isReadOnly: false,
|
||
|
|
cacheType: 'Unsafe',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
networkInterfaces: [
|
||
|
|
{
|
||
|
|
ifaceId: 'eth0',
|
||
|
|
hostDevName: 'tap0',
|
||
|
|
guestMac: '02:00:00:00:00:01',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
};
|
||
|
|
|
||
|
|
tap.test('VMConfig - validate() should pass for valid config', async () => {
|
||
|
|
const vmConfig = new VMConfig(sampleConfig);
|
||
|
|
const result = vmConfig.validate();
|
||
|
|
expect(result.valid).toBeTrue();
|
||
|
|
expect(result.errors).toHaveLength(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - validate() should fail for missing bootSource', async () => {
|
||
|
|
const vmConfig = new VMConfig({
|
||
|
|
machineConfig: { vcpuCount: 1, memSizeMib: 128 },
|
||
|
|
} as any);
|
||
|
|
const result = vmConfig.validate();
|
||
|
|
expect(result.valid).toBeFalse();
|
||
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - validate() should fail for invalid vcpuCount', async () => {
|
||
|
|
const vmConfig = new VMConfig({
|
||
|
|
bootSource: { kernelImagePath: '/vmlinux' },
|
||
|
|
machineConfig: { vcpuCount: 0, memSizeMib: 128 },
|
||
|
|
});
|
||
|
|
const result = vmConfig.validate();
|
||
|
|
expect(result.valid).toBeFalse();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - validate() should fail for vcpuCount > 32', async () => {
|
||
|
|
const vmConfig = new VMConfig({
|
||
|
|
bootSource: { kernelImagePath: '/vmlinux' },
|
||
|
|
machineConfig: { vcpuCount: 64, memSizeMib: 128 },
|
||
|
|
});
|
||
|
|
const result = vmConfig.validate();
|
||
|
|
expect(result.valid).toBeFalse();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - validate() should fail for multiple root drives', async () => {
|
||
|
|
const vmConfig = new VMConfig({
|
||
|
|
bootSource: { kernelImagePath: '/vmlinux' },
|
||
|
|
machineConfig: { vcpuCount: 1, memSizeMib: 128 },
|
||
|
|
drives: [
|
||
|
|
{ driveId: 'rootfs1', pathOnHost: '/rootfs1', isRootDevice: true },
|
||
|
|
{ driveId: 'rootfs2', pathOnHost: '/rootfs2', isRootDevice: true },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
const result = vmConfig.validate();
|
||
|
|
expect(result.valid).toBeFalse();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - toBootSourcePayload() should generate correct snake_case', async () => {
|
||
|
|
const vmConfig = new VMConfig(sampleConfig);
|
||
|
|
const payload = vmConfig.toBootSourcePayload();
|
||
|
|
expect(payload.kernel_image_path).toEqual('/path/to/vmlinux');
|
||
|
|
expect(payload.boot_args).toEqual('console=ttyS0 reboot=k panic=1');
|
||
|
|
expect(payload.initrd_path).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - toMachineConfigPayload() should generate correct payload', async () => {
|
||
|
|
const vmConfig = new VMConfig(sampleConfig);
|
||
|
|
const payload = vmConfig.toMachineConfigPayload();
|
||
|
|
expect(payload.vcpu_count).toEqual(2);
|
||
|
|
expect(payload.mem_size_mib).toEqual(256);
|
||
|
|
expect(payload.smt).toEqual(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - toDrivePayload() should generate correct payload', async () => {
|
||
|
|
const vmConfig = new VMConfig(sampleConfig);
|
||
|
|
const payload = vmConfig.toDrivePayload(sampleConfig.drives![0]);
|
||
|
|
expect(payload.drive_id).toEqual('rootfs');
|
||
|
|
expect(payload.path_on_host).toEqual('/path/to/rootfs.ext4');
|
||
|
|
expect(payload.is_root_device).toEqual(true);
|
||
|
|
expect(payload.is_read_only).toEqual(false);
|
||
|
|
expect(payload.cache_type).toEqual('Unsafe');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - toNetworkInterfacePayload() should generate correct payload', async () => {
|
||
|
|
const vmConfig = new VMConfig(sampleConfig);
|
||
|
|
const payload = vmConfig.toNetworkInterfacePayload(sampleConfig.networkInterfaces![0]);
|
||
|
|
expect(payload.iface_id).toEqual('eth0');
|
||
|
|
expect(payload.host_dev_name).toEqual('tap0');
|
||
|
|
expect(payload.guest_mac).toEqual('02:00:00:00:00:01');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - toVsockPayload() should return null when no vsock configured', async () => {
|
||
|
|
const vmConfig = new VMConfig(sampleConfig);
|
||
|
|
expect(vmConfig.toVsockPayload()).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - toBalloonPayload() should return null when no balloon configured', async () => {
|
||
|
|
const vmConfig = new VMConfig(sampleConfig);
|
||
|
|
expect(vmConfig.toBalloonPayload()).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - toBalloonPayload() should generate correct payload', async () => {
|
||
|
|
const config: IMicroVMConfig = {
|
||
|
|
...sampleConfig,
|
||
|
|
balloon: { amountMib: 64, deflateOnOom: true, statsPollingIntervalS: 5 },
|
||
|
|
};
|
||
|
|
const vmConfig = new VMConfig(config);
|
||
|
|
const payload = vmConfig.toBalloonPayload();
|
||
|
|
expect(payload).not.toBeNull();
|
||
|
|
expect(payload!.amount_mib).toEqual(64);
|
||
|
|
expect(payload!.deflate_on_oom).toEqual(true);
|
||
|
|
expect(payload!.stats_polling_interval_s).toEqual(5);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('VMConfig - toLoggerPayload() should generate correct payload', async () => {
|
||
|
|
const config: IMicroVMConfig = {
|
||
|
|
...sampleConfig,
|
||
|
|
logger: { logPath: '/tmp/fc.log', level: 'Debug', showLogOrigin: true },
|
||
|
|
};
|
||
|
|
const vmConfig = new VMConfig(config);
|
||
|
|
const payload = vmConfig.toLoggerPayload();
|
||
|
|
expect(payload).not.toBeNull();
|
||
|
|
expect(payload!.log_path).toEqual('/tmp/fc.log');
|
||
|
|
expect(payload!.level).toEqual('Debug');
|
||
|
|
expect(payload!.show_log_origin).toEqual(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// SocketClient Tests
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
tap.test('SocketClient - URL construction', async () => {
|
||
|
|
const client = new SocketClient({ socketPath: '/tmp/test.sock' });
|
||
|
|
// We can't directly access buildUrl, but we can verify the client instantiates
|
||
|
|
expect(client).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// NetworkManager Tests
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
tap.test('NetworkManager - allocateIp() should allocate sequential IPs', async () => {
|
||
|
|
const nm = new NetworkManager({ subnet: '172.30.0.0/24' });
|
||
|
|
const ip1 = nm.allocateIp();
|
||
|
|
const ip2 = nm.allocateIp();
|
||
|
|
const ip3 = nm.allocateIp();
|
||
|
|
expect(ip1).toEqual('172.30.0.2');
|
||
|
|
expect(ip2).toEqual('172.30.0.3');
|
||
|
|
expect(ip3).toEqual('172.30.0.4');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('NetworkManager - generateMac() should generate locally-administered MACs', async () => {
|
||
|
|
const nm = new NetworkManager();
|
||
|
|
const mac1 = nm.generateMac('vm1', 'eth0');
|
||
|
|
const mac2 = nm.generateMac('vm2', 'eth0');
|
||
|
|
const mac3 = nm.generateMac('vm1', 'eth0');
|
||
|
|
|
||
|
|
// Should start with 02: (locally administered)
|
||
|
|
expect(mac1.startsWith('02:')).toBeTrue();
|
||
|
|
expect(mac2.startsWith('02:')).toBeTrue();
|
||
|
|
|
||
|
|
// Same inputs should produce same MAC (deterministic)
|
||
|
|
expect(mac1).toEqual(mac3);
|
||
|
|
|
||
|
|
// Different inputs should produce different MACs
|
||
|
|
expect(mac1).not.toEqual(mac2);
|
||
|
|
|
||
|
|
// Should be valid MAC format (6 hex pairs separated by colons)
|
||
|
|
expect(mac1).toMatch(/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('NetworkManager - generateTapName() should respect IFNAMSIZ limit', async () => {
|
||
|
|
const nm = new NetworkManager();
|
||
|
|
const name1 = nm.generateTapName('abcdefgh-1234-5678', 'eth0');
|
||
|
|
const name2 = nm.generateTapName('short', 'eth0');
|
||
|
|
|
||
|
|
// Should not exceed 15 characters
|
||
|
|
expect(name1.length).toBeLessThanOrEqual(15);
|
||
|
|
expect(name2.length).toBeLessThanOrEqual(15);
|
||
|
|
|
||
|
|
// Should start with 'sv' prefix
|
||
|
|
expect(name1.startsWith('sv')).toBeTrue();
|
||
|
|
expect(name2.startsWith('sv')).toBeTrue();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('NetworkManager - getGuestNetworkBootArgs() should format correctly', async () => {
|
||
|
|
const nm = new NetworkManager();
|
||
|
|
const tap = {
|
||
|
|
tapName: 'svtest0eth0',
|
||
|
|
guestIp: '172.30.0.2',
|
||
|
|
gatewayIp: '172.30.0.1',
|
||
|
|
subnetMask: '255.255.255.0',
|
||
|
|
mac: '02:00:00:00:00:01',
|
||
|
|
};
|
||
|
|
const bootArgs = nm.getGuestNetworkBootArgs(tap);
|
||
|
|
expect(bootArgs).toEqual('ip=172.30.0.2::172.30.0.1:255.255.255.0::eth0:off');
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// SmartVM Tests
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
tap.test('SmartVM - instantiation with defaults', async () => {
|
||
|
|
const smartvm = new SmartVM();
|
||
|
|
expect(smartvm).toBeTruthy();
|
||
|
|
expect(smartvm.imageManager).toBeTruthy();
|
||
|
|
expect(smartvm.networkManager).toBeTruthy();
|
||
|
|
expect(smartvm.vmCount).toEqual(0);
|
||
|
|
expect(smartvm.listVMs()).toHaveLength(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('SmartVM - instantiation with custom options', async () => {
|
||
|
|
const smartvm = new SmartVM({
|
||
|
|
dataDir: '/tmp/smartvm-test',
|
||
|
|
arch: 'aarch64',
|
||
|
|
bridgeName: 'testbr0',
|
||
|
|
subnet: '10.0.0.0/24',
|
||
|
|
});
|
||
|
|
expect(smartvm).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
export default tap.start();
|