mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 23:04:22 +01:00
feat: Windows compatibility (#836)
* feat: win compatibility * fix * fix: uptime parse * opt.: linux uptime accuracy * fix: windows temperature fetching * opt. * opt.: powershell exec * refactor: address PR review feedback and improve code quality ### Major Improvements: - **Refactored Windows status parsing**: Broke down large `_getWindowsStatus` method into 13 smaller, focused helper methods for better maintainability and readability - **Extracted system detection logic**: Created dedicated `SystemDetector` helper class to separate OS detection concerns from ServerProvider - **Improved concurrency handling**: Implemented proper synchronization for server updates using Future-based locks to prevent race conditions ### Bug Fixes: - **Fixed CPU percentage parsing**: Removed incorrect '*100' multiplication in BSD CPU parsing (values were already percentages) - **Enhanced memory parsing**: Added validation and error handling to BSD memory fallback parsing with proper logging - **Improved uptime parsing**: Added support for multiple Windows date formats and robust error handling with validation - **Fixed division by zero**: Added safety checks in Swap.usedPercent getter ### Code Quality Enhancements: - **Added comprehensive documentation**: Documented Windows CPU counter limitations and approach - **Strengthened error handling**: Added detailed logging and validation throughout parsing methods - **Improved robustness**: Enhanced BSD CPU parsing with percentage validation and warnings - **Better separation of concerns**: Each parsing method now has single responsibility ### Files Changed: - `lib/data/helper/system_detector.dart` (new): System detection helper - `lib/data/model/server/cpu.dart`: Fixed percentage parsing and added validation - `lib/data/model/server/memory.dart`: Enhanced fallback parsing and division-by-zero protection - `lib/data/model/server/server_status_update_req.dart`: Refactored into 13 focused parsing methods - `lib/data/provider/server.dart`: Improved synchronization and extracted system detection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: parse & shell fn struct --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -313,7 +313,7 @@ void main() {
|
||||
}
|
||||
]
|
||||
''';
|
||||
|
||||
|
||||
final gpu = AmdSmi.fromJson(jsonWithInvalidProcess)[0];
|
||||
expect(gpu.memory.processes.length, 1);
|
||||
expect(gpu.memory.processes[0].pid, 1234);
|
||||
@@ -409,4 +409,4 @@ void main() {
|
||||
expect(gpus[0].clockSpeed, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,25 +9,25 @@ void main() {
|
||||
final disks = Disk.parse(_btrfsRaidJsonOutput);
|
||||
expect(disks, isNotEmpty);
|
||||
expect(disks.length, 4); // Should have 2 parent disks + 2 BTRFS partitions
|
||||
|
||||
|
||||
// We should get two distinct disks with the same UUID but different paths
|
||||
final nvme1Disk = disks.firstWhere((disk) => disk.path == '/dev/nvme1n1p1');
|
||||
final nvme2Disk = disks.firstWhere((disk) => disk.path == '/dev/nvme2n1p1');
|
||||
|
||||
|
||||
// Both should exist
|
||||
expect(nvme1Disk, isNotNull);
|
||||
expect(nvme2Disk, isNotNull);
|
||||
|
||||
|
||||
// They should have the same UUID (since they're part of the same BTRFS volume)
|
||||
expect(nvme1Disk.uuid, nvme2Disk.uuid);
|
||||
|
||||
|
||||
// But they should be treated as distinct disks
|
||||
expect(identical(nvme1Disk, nvme2Disk), isFalse);
|
||||
|
||||
|
||||
// Verify DiskUsage counts physical disks correctly
|
||||
final usage = DiskUsage.parse(disks);
|
||||
// With our unique path+kname identifier, both disks should be counted
|
||||
expect(usage.size, nvme1Disk.size + nvme2Disk.size);
|
||||
expect(usage.size, nvme1Disk.size + nvme2Disk.size);
|
||||
expect(usage.used, nvme1Disk.used + nvme2Disk.used);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ fa1215b4be74 Up 12 hours firefly
|
||||
const images = [
|
||||
'rustdesk/rustdesk-server:latest',
|
||||
'rustdesk/rustdesk-server:latest',
|
||||
'uusec/firefly:latest'
|
||||
'uusec/firefly:latest',
|
||||
];
|
||||
const states = ['Up 2 hours', 'Up 41 minutes', 'Up 12 hours'];
|
||||
for (var idx = 1; idx < lines.length; idx++) {
|
||||
|
||||
@@ -11,12 +11,12 @@ void main() {
|
||||
expect(disks, isNotEmpty);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('parse lsblk JSON output', () {
|
||||
final disks = Disk.parse(_jsonLsblkOutput);
|
||||
expect(disks, isNotEmpty);
|
||||
expect(disks.length, 6); // Should find ext4 root, vfat efi, and ext2 boot
|
||||
|
||||
expect(disks.length, 6); // Should find ext4 root, vfat efi, and ext2 boot
|
||||
|
||||
// Verify root filesystem
|
||||
final rootFs = disks.firstWhere((disk) => disk.mount == '/');
|
||||
expect(rootFs.fsTyp, 'ext4');
|
||||
@@ -24,44 +24,44 @@ void main() {
|
||||
expect(rootFs.used, BigInt.parse('552718364672') ~/ BigInt.from(1024));
|
||||
expect(rootFs.avail, BigInt.parse('379457622016') ~/ BigInt.from(1024));
|
||||
expect(rootFs.usedPercent, 56);
|
||||
|
||||
|
||||
// Verify boot/efi filesystem
|
||||
final efiFs = disks.firstWhere((disk) => disk.mount == '/boot/efi');
|
||||
expect(efiFs.fsTyp, 'vfat');
|
||||
expect(efiFs.size, BigInt.parse('535805952') ~/ BigInt.from(1024));
|
||||
expect(efiFs.usedPercent, 1);
|
||||
|
||||
|
||||
// Verify boot filesystem
|
||||
final bootFs = disks.firstWhere((disk) => disk.mount == '/boot');
|
||||
expect(bootFs.fsTyp, 'ext2');
|
||||
expect(bootFs.usedPercent, 34);
|
||||
});
|
||||
|
||||
|
||||
test('parse nested lsblk JSON output with parent/child relationships', () {
|
||||
final disks = Disk.parse(_nestedJsonLsblkOutput);
|
||||
expect(disks, isNotEmpty);
|
||||
|
||||
|
||||
// Check parent device with children
|
||||
final parentDisk = disks.firstWhere((disk) => disk.path == '/dev/nvme0n1');
|
||||
expect(parentDisk.children, isNotEmpty);
|
||||
expect(parentDisk.children.length, 3);
|
||||
|
||||
|
||||
// Check one of the children
|
||||
final rootPartition = parentDisk.children.firstWhere((disk) => disk.mount == '/');
|
||||
expect(rootPartition.fsTyp, 'ext4');
|
||||
expect(rootPartition.path, '/dev/nvme0n1p2');
|
||||
expect(rootPartition.usedPercent, 45);
|
||||
|
||||
|
||||
// Verify we have a child partition with UUID
|
||||
final bootPartition = parentDisk.children.firstWhere((disk) => disk.mount == '/boot');
|
||||
expect(bootPartition.uuid, '12345678-abcd-1234-abcd-1234567890ab');
|
||||
});
|
||||
|
||||
|
||||
test('DiskUsage handles zero size correctly', () {
|
||||
final usage = DiskUsage(used: BigInt.from(1000), size: BigInt.zero);
|
||||
expect(usage.usedPercent, 0); // Should return 0 instead of throwing
|
||||
});
|
||||
|
||||
|
||||
test('DiskUsage handles null kname', () {
|
||||
final disks = [
|
||||
Disk(
|
||||
@@ -74,7 +74,7 @@ void main() {
|
||||
kname: null, // Explicitly null kname
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
final usage = DiskUsage.parse(disks);
|
||||
expect(usage.used, BigInt.from(5000));
|
||||
expect(usage.size, BigInt.from(10000));
|
||||
@@ -198,16 +198,16 @@ const _nestedJsonLsblkOutput = '''
|
||||
''';
|
||||
|
||||
const _raws = [
|
||||
// '''
|
||||
// Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
// udev 864088 0 864088 0% /dev
|
||||
// tmpfs 176724 688 176036 1% /run
|
||||
// /dev/vda3 40910528 18067948 20951380 47% /
|
||||
// tmpfs 883612 0 883612 0% /dev/shm
|
||||
// tmpfs 5120 0 5120 0% /run/lock
|
||||
// /dev/vda2 192559 11807 180752 7% /boot/efi
|
||||
// tmpfs 176720 104 176616 1% /run/user/1000
|
||||
// ''',
|
||||
// '''
|
||||
// Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
// udev 864088 0 864088 0% /dev
|
||||
// tmpfs 176724 688 176036 1% /run
|
||||
// /dev/vda3 40910528 18067948 20951380 47% /
|
||||
// tmpfs 883612 0 883612 0% /dev/shm
|
||||
// tmpfs 5120 0 5120 0% /run/lock
|
||||
// /dev/vda2 192559 11807 180752 7% /boot/efi
|
||||
// tmpfs 176720 104 176616 1% /run/user/1000
|
||||
// ''',
|
||||
'''
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
udev 16181648 0 16181648 0% /dev
|
||||
|
||||
@@ -113,15 +113,12 @@ void main() {
|
||||
SensorAdaptor.virtual,
|
||||
SensorAdaptor.pci,
|
||||
]);
|
||||
expect(
|
||||
sensors.map((e) => e.summary),
|
||||
[
|
||||
'+56.0°C (high = +105.0°C, crit = +105.0°C)',
|
||||
'+27.8°C (crit = +119.0°C)',
|
||||
'+56.0°C',
|
||||
'+45.9°C (low = -273.1°C, high = +83.8°C)',
|
||||
],
|
||||
);
|
||||
expect(sensors.map((e) => e.summary), [
|
||||
'+56.0°C (high = +105.0°C, crit = +105.0°C)',
|
||||
'+27.8°C (crit = +119.0°C)',
|
||||
'+56.0°C',
|
||||
'+45.9°C (low = -273.1°C, high = +83.8°C)',
|
||||
]);
|
||||
});
|
||||
|
||||
test('parse sensors2', () {
|
||||
@@ -138,14 +135,11 @@ void main() {
|
||||
SensorAdaptor.pci,
|
||||
SensorAdaptor.pci,
|
||||
]);
|
||||
expect(
|
||||
sensors.map((e) => e.summary),
|
||||
[
|
||||
'1.26 V',
|
||||
'1.19 V (min = +0.00 V, max = +1.74 V)',
|
||||
'+45.9°C (low = -273.1°C, high = +69.8°C)',
|
||||
'+44.9°C',
|
||||
],
|
||||
);
|
||||
expect(sensors.map((e) => e.summary), [
|
||||
'1.26 V',
|
||||
'1.19 V (min = +0.00 V, max = +1.74 V)',
|
||||
'+45.9°C (low = -273.1°C, high = +69.8°C)',
|
||||
'+44.9°C',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
87
test/uptime_test.dart
Normal file
87
test/uptime_test.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('Linux uptime parsing tests', () {
|
||||
test('should parse uptime with days and hours:minutes', () {
|
||||
const raw = '19:39:15 up 61 days, 18:16, 1 user, load average: 0.00, 0.00, 0.00';
|
||||
final result = _testParseUpTime(raw);
|
||||
expect(result, '61 days, 18:16');
|
||||
});
|
||||
|
||||
test('should parse uptime with single day and hours:minutes', () {
|
||||
const raw = '19:39:15 up 1 day, 2:34, 1 user, load average: 0.00, 0.00, 0.00';
|
||||
final result = _testParseUpTime(raw);
|
||||
expect(result, '1 day, 2:34');
|
||||
});
|
||||
|
||||
test('should parse uptime with only hours:minutes', () {
|
||||
const raw = '19:39:15 up 2:34, 1 user, load average: 0.00, 0.00, 0.00';
|
||||
final result = _testParseUpTime(raw);
|
||||
expect(result, '2:34');
|
||||
});
|
||||
|
||||
test('should parse uptime with only minutes', () {
|
||||
const raw = '19:39:15 up 34 min, 1 user, load average: 0.00, 0.00, 0.00';
|
||||
final result = _testParseUpTime(raw);
|
||||
expect(result, '34 min');
|
||||
});
|
||||
|
||||
test('should parse uptime with days only (no time part)', () {
|
||||
const raw = '19:39:15 up 5 days, 1 user, load average: 0.00, 0.00, 0.00';
|
||||
final result = _testParseUpTime(raw);
|
||||
expect(result, '5 days');
|
||||
});
|
||||
|
||||
test('should return null for invalid format', () {
|
||||
const raw = 'invalid uptime format';
|
||||
final result = _testParseUpTime(raw);
|
||||
expect(result, null);
|
||||
});
|
||||
|
||||
test('should handle edge case with empty string', () {
|
||||
const raw = '';
|
||||
final result = _testParseUpTime(raw);
|
||||
expect(result, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to test the private _parseUpTime function
|
||||
String? _testParseUpTime(String raw) {
|
||||
final splitedUp = raw.split('up ');
|
||||
if (splitedUp.length == 2) {
|
||||
final uptimePart = splitedUp[1];
|
||||
final splitedComma = uptimePart.split(', ');
|
||||
|
||||
if (splitedComma.isEmpty) return null;
|
||||
|
||||
// Handle different uptime formats
|
||||
final firstPart = splitedComma[0].trim();
|
||||
|
||||
// Case 1: "61 days" or "1 day" - need to get the time part from next segment
|
||||
if (firstPart.contains('day')) {
|
||||
if (splitedComma.length >= 2) {
|
||||
final timePart = splitedComma[1].trim();
|
||||
// Check if it's in HH:MM format
|
||||
if (timePart.contains(':') && !timePart.contains('user') && !timePart.contains('load')) {
|
||||
return '$firstPart, $timePart';
|
||||
}
|
||||
}
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Case 2: "2:34" (hours:minutes) - already in good format
|
||||
if (firstPart.contains(':') && !firstPart.contains('user') && !firstPart.contains('load')) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Case 3: "34 min" - already in good format
|
||||
if (firstPart.contains('min')) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
// Fallback: return first part
|
||||
return firstPart;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
473
test/windows_test.dart
Normal file
473
test/windows_test.dart
Normal file
@@ -0,0 +1,473 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
||||
import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/res/status.dart';
|
||||
|
||||
void main() {
|
||||
group('Windows System Tests', () {
|
||||
test('should verify Windows segments length matches command types', () {
|
||||
final systemType = SystemType.windows;
|
||||
final expectedLength = WindowsStatusCmdType.values.length;
|
||||
expect(systemType.segmentsLen, equals(expectedLength));
|
||||
expect(systemType.isSegmentsLenMatch(expectedLength), isTrue);
|
||||
});
|
||||
|
||||
test('should generate Windows PowerShell script correctly', () {
|
||||
final script = ShellFunc.allScript({'custom_cmd': 'echo "test"'}, systemType: SystemType.windows);
|
||||
|
||||
expect(script, contains('PowerShell script for ServerBox'));
|
||||
expect(script, contains('function SbStatus'));
|
||||
expect(script, contains('function SbProcess'));
|
||||
expect(script, contains('function SbShutdown'));
|
||||
expect(script, contains('function SbReboot'));
|
||||
expect(script, contains('function SbSuspend'));
|
||||
expect(script, contains('switch (\$args[0])'));
|
||||
expect(script, contains('"-s" { SbStatus }'));
|
||||
expect(script, contains('echo "test"'));
|
||||
});
|
||||
|
||||
test('should handle Windows system parsing with real data', () async {
|
||||
final segments = _windowsStatusSegments;
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
final result = await getStatus(req);
|
||||
|
||||
// Verify system information was parsed
|
||||
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations'));
|
||||
expect(result.more[StatusCmdType.host], equals('LKH6'));
|
||||
|
||||
// Verify CPU information
|
||||
expect(result.cpu.now, isNotEmpty);
|
||||
expect(result.cpu.brand.keys.first, contains('12th Gen Intel(R) Core(TM) i5-12490F'));
|
||||
|
||||
// Verify memory information
|
||||
expect(result.mem, isNotNull);
|
||||
expect(result.mem.total, equals(66943944));
|
||||
expect(result.mem.free, equals(58912812));
|
||||
|
||||
// Verify disk information
|
||||
expect(result.disk, isNotEmpty);
|
||||
final cDrive = result.disk.firstWhere((disk) => disk.path == 'C:');
|
||||
expect(cDrive.fsTyp, equals('NTFS'));
|
||||
expect(cDrive.size, equals(BigInt.parse('999271952384') ~/ BigInt.from(1024)));
|
||||
expect(cDrive.avail, equals(BigInt.parse('386084032512') ~/ BigInt.from(1024)));
|
||||
|
||||
// Verify TCP connections
|
||||
expect(result.tcp, isNotNull);
|
||||
expect(result.tcp.active, equals(2));
|
||||
});
|
||||
|
||||
test('should parse Windows CPU data correctly', () async {
|
||||
const cpuJson = '''
|
||||
{
|
||||
"Name": "12th Gen Intel(R) Core(TM) i5-12490F",
|
||||
"LoadPercentage": 42
|
||||
}
|
||||
''';
|
||||
|
||||
final segments = ['__windows', '1754151483', '', '', cpuJson];
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
final result = await getStatus(req);
|
||||
|
||||
expect(result.cpu.now, hasLength(1));
|
||||
expect(result.cpu.now.first.user, equals(42));
|
||||
expect(result.cpu.now.first.idle, equals(58));
|
||||
});
|
||||
|
||||
test('should parse Windows memory data correctly', () async {
|
||||
const memoryJson = '''
|
||||
{
|
||||
"TotalVisibleMemorySize": 66943944,
|
||||
"FreePhysicalMemory": 58912812
|
||||
}
|
||||
''';
|
||||
|
||||
final segments = ['__windows', '1754151483', '', '', '', '', '', '', memoryJson];
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
final result = await getStatus(req);
|
||||
|
||||
expect(result.mem, isNotNull);
|
||||
expect(result.mem.total, equals(66943944));
|
||||
expect(result.mem.free, equals(58912812));
|
||||
expect(result.mem.avail, equals(58912812));
|
||||
});
|
||||
|
||||
test('should parse Windows disk data correctly', () async {
|
||||
const diskJson = '''
|
||||
{
|
||||
"DeviceID": "C:",
|
||||
"Size": 999271952384,
|
||||
"FreeSpace": 386084032512,
|
||||
"FileSystem": "NTFS"
|
||||
}
|
||||
''';
|
||||
|
||||
final segments = ['__windows', '1754151483', '', '', '', '', '', diskJson];
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
final result = await getStatus(req);
|
||||
|
||||
expect(result.disk, hasLength(1));
|
||||
final disk = result.disk.first;
|
||||
expect(disk.path, equals('C:'));
|
||||
expect(disk.mount, equals('C:'));
|
||||
expect(disk.fsTyp, equals('NTFS'));
|
||||
expect(disk.size, equals(BigInt.parse('999271952384') ~/ BigInt.from(1024)));
|
||||
expect(disk.avail, equals(BigInt.parse('386084032512') ~/ BigInt.from(1024)));
|
||||
expect(disk.usedPercent, equals(61));
|
||||
});
|
||||
|
||||
test('should parse Windows battery data correctly', () async {
|
||||
const batteryJson = '''
|
||||
{
|
||||
"EstimatedChargeRemaining": 85,
|
||||
"BatteryStatus": 6
|
||||
}
|
||||
''';
|
||||
|
||||
// Create segments with enough elements to reach battery position
|
||||
final segments = List.filled(WindowsStatusCmdType.values.length, '');
|
||||
segments[0] = '__windows';
|
||||
segments[WindowsStatusCmdType.battery.index] = batteryJson;
|
||||
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
final result = await getStatus(req);
|
||||
|
||||
expect(result.batteries, hasLength(1));
|
||||
final battery = result.batteries.first;
|
||||
expect(battery.name, equals('Battery'));
|
||||
expect(battery.percent, equals(85));
|
||||
expect(battery.status.name, equals('charging'));
|
||||
});
|
||||
|
||||
test('should handle Windows uptime parsing correctly', () async {
|
||||
// Test new format with date line + uptime days
|
||||
const uptimeNewFormat = 'Friday, July 25, 2025 2:26:42 PM\n2';
|
||||
|
||||
final segments = List.filled(WindowsStatusCmdType.values.length, '');
|
||||
segments[0] = '__windows';
|
||||
segments[WindowsStatusCmdType.uptime.index] = uptimeNewFormat;
|
||||
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
final result = await getStatus(req);
|
||||
|
||||
expect(result.more[StatusCmdType.uptime], isNotNull);
|
||||
});
|
||||
|
||||
test('should handle Windows uptime parsing with old format', () async {
|
||||
const uptimeDateTime = 'Friday, July 25, 2025 2:26:42 PM';
|
||||
|
||||
final segments = List.filled(WindowsStatusCmdType.values.length, '');
|
||||
segments[0] = '__windows';
|
||||
segments[WindowsStatusCmdType.uptime.index] = uptimeDateTime;
|
||||
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
final result = await getStatus(req);
|
||||
|
||||
expect(result.more[StatusCmdType.uptime], isNotNull);
|
||||
});
|
||||
|
||||
test('should handle Windows script path generation', () {
|
||||
const serverId = 'test-server';
|
||||
|
||||
final scriptPath = ShellFunc.getScriptPath(serverId, systemType: SystemType.windows);
|
||||
expect(scriptPath, contains('.ps1'));
|
||||
expect(scriptPath, contains('\\'));
|
||||
|
||||
final installCmd = ShellFunc.getInstallShellCmd(serverId, systemType: SystemType.windows);
|
||||
expect(installCmd, contains('New-Item'));
|
||||
expect(installCmd, contains('Set-Content'));
|
||||
// No longer contains 'powershell' prefix as commands now run in PowerShell session
|
||||
});
|
||||
|
||||
test('should execute Windows commands correctly', () {
|
||||
const serverId = 'test-server';
|
||||
|
||||
final statusCmd = ShellFunc.status.exec(serverId, systemType: SystemType.windows);
|
||||
expect(statusCmd, contains('powershell'));
|
||||
expect(statusCmd, contains('-ExecutionPolicy Bypass'));
|
||||
expect(statusCmd, contains('-s'));
|
||||
|
||||
final processCmd = ShellFunc.process.exec(serverId, systemType: SystemType.windows);
|
||||
expect(processCmd, contains('powershell'));
|
||||
expect(processCmd, contains('-p'));
|
||||
});
|
||||
|
||||
test('should handle GPU detection on Windows', () async {
|
||||
const nvidiaNotFound = 'NVIDIA driver not found';
|
||||
const amdNotFound = 'AMD driver not found';
|
||||
|
||||
final segments = List.filled(WindowsStatusCmdType.values.length, '');
|
||||
segments[0] = '__windows';
|
||||
segments[WindowsStatusCmdType.nvidia.index] = nvidiaNotFound;
|
||||
segments[WindowsStatusCmdType.amd.index] = amdNotFound;
|
||||
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
final result = await getStatus(req);
|
||||
|
||||
// Should not throw errors even when GPU drivers are not found
|
||||
expect(result.nvidia, anyOf(isNull, isEmpty));
|
||||
expect(result.amd, anyOf(isNull, isEmpty));
|
||||
});
|
||||
|
||||
test('should handle Windows error conditions gracefully', () async {
|
||||
// Test with malformed JSON and error messages
|
||||
final segments = [
|
||||
'__windows',
|
||||
'1754151483',
|
||||
'Network adapter error',
|
||||
'Microsoft Windows 11 Pro for Workstations',
|
||||
'invalid json {',
|
||||
'uptime error',
|
||||
'connection error',
|
||||
'disk error',
|
||||
'memory error',
|
||||
'temp error',
|
||||
'LKH6',
|
||||
'diskio error',
|
||||
'battery error',
|
||||
'NVIDIA driver not found',
|
||||
'AMD driver not found',
|
||||
'sensor error',
|
||||
'smart error',
|
||||
'12th Gen Intel(R) Core(TM) i5-12490F',
|
||||
];
|
||||
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
// Should not throw exceptions
|
||||
expect(() async => await getStatus(req), returnsNormally);
|
||||
|
||||
final result = await getStatus(req);
|
||||
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations'));
|
||||
expect(result.more[StatusCmdType.host], equals('LKH6'));
|
||||
});
|
||||
|
||||
test('should handle Windows temperature error output gracefully', () async {
|
||||
// Test with actual error output from win_raw.txt
|
||||
final segments = [
|
||||
'__windows',
|
||||
'1754151483',
|
||||
'', // network
|
||||
'Microsoft Windows 11 Pro for Workstations', // system
|
||||
'''
|
||||
{
|
||||
"Name": "12th Gen Intel(R) Core(TM) i5-12490F",
|
||||
"LoadPercentage": 42
|
||||
}
|
||||
''', // cpu
|
||||
'Friday, July 25, 2025 2:26:42 PM', // uptime
|
||||
'2', // connections
|
||||
'''
|
||||
{
|
||||
"DeviceID": "C:",
|
||||
"Size": 999271952384,
|
||||
"FreeSpace": 386084032512,
|
||||
"FileSystem": "NTFS"
|
||||
}
|
||||
''', // disk
|
||||
'''
|
||||
{
|
||||
"TotalVisibleMemorySize": 66943944,
|
||||
"FreePhysicalMemory": 58912812
|
||||
}
|
||||
''', // memory
|
||||
'''
|
||||
The string is missing the terminator: ".
|
||||
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
|
||||
+ FullyQualifiedErrorId : TerminatorExpectedAtEndOfString
|
||||
''', // temp (error output)
|
||||
'LKH6', // host
|
||||
'', // diskio
|
||||
'', // battery
|
||||
'NVIDIA driver not found', // nvidia
|
||||
'AMD driver not found', // amd
|
||||
'', // sensors
|
||||
'''
|
||||
{
|
||||
"DeviceId": "0",
|
||||
"Temperature": 41,
|
||||
"TemperatureMax": 70,
|
||||
"Wear": 0,
|
||||
"PowerOnHours": null
|
||||
}
|
||||
''', // smart
|
||||
'12th Gen Intel(R) Core(TM) i5-12490F', // cpu brand
|
||||
];
|
||||
|
||||
final serverStatus = InitStatus.status;
|
||||
|
||||
final req = ServerStatusUpdateReq(
|
||||
system: SystemType.windows,
|
||||
ss: serverStatus,
|
||||
segments: segments,
|
||||
customCmds: {},
|
||||
);
|
||||
|
||||
// Should not throw exceptions even with error output in temperature values
|
||||
expect(() async => await getStatus(req), returnsNormally);
|
||||
|
||||
final result = await getStatus(req);
|
||||
expect(result.more[StatusCmdType.sys], equals('Microsoft Windows 11 Pro for Workstations'));
|
||||
expect(result.more[StatusCmdType.host], equals('LKH6'));
|
||||
// Temperature should be empty since we got error output
|
||||
expect(result.temps.isEmpty, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Sample Windows status segments based on real PowerShell output
|
||||
final _windowsStatusSegments = [
|
||||
'__windows', // System type marker
|
||||
'1754151483', // Unix timestamp
|
||||
'', // Network data (empty for now)
|
||||
'Microsoft Windows 11 Pro for Workstations', // System name
|
||||
'''
|
||||
{
|
||||
"Name": "12th Gen Intel(R) Core(TM) i5-12490F",
|
||||
"LoadPercentage": 42
|
||||
}
|
||||
''', // CPU data
|
||||
'Friday, July 25, 2025 2:26:42 PM', // Uptime (boot time)
|
||||
'2', // Connection count
|
||||
'''
|
||||
{
|
||||
"DeviceID": "C:",
|
||||
"Size": 999271952384,
|
||||
"FreeSpace": 386084032512,
|
||||
"FileSystem": "NTFS"
|
||||
}
|
||||
''', // Disk data
|
||||
'''
|
||||
{
|
||||
"TotalVisibleMemorySize": 66943944,
|
||||
"FreePhysicalMemory": 58912812
|
||||
}
|
||||
''', // Memory data
|
||||
'', // Temperature (combined command - empty due to OpenHardwareMonitor error)
|
||||
'LKH6', // Hostname
|
||||
'', // Disk I/O (empty for now)
|
||||
'', // Battery data (empty)
|
||||
'NVIDIA driver not found', // NVIDIA GPU
|
||||
'AMD driver not found', // AMD GPU
|
||||
'', // Sensors (empty due to OpenHardwareMonitor error)
|
||||
'''
|
||||
{
|
||||
"CimClass": {
|
||||
"CimSuperClassName": "MSFT_StorageObject",
|
||||
"CimSuperClass": {
|
||||
"CimSuperClassName": null,
|
||||
"CimSuperClass": null,
|
||||
"CimClassProperties": "ObjectId PassThroughClass PassThroughIds PassThroughNamespace PassThroughServer UniqueId",
|
||||
"CimClassQualifiers": "Abstract = True locale = 1033",
|
||||
"CimClassMethods": "",
|
||||
"CimSystemProperties": "Microsoft.Management.Infrastructure.CimSystemProperties"
|
||||
},
|
||||
"CimClassProperties": [
|
||||
"ObjectId",
|
||||
"PassThroughClass",
|
||||
"PassThroughIds",
|
||||
"PassThroughNamespace",
|
||||
"PassThroughServer",
|
||||
"UniqueId",
|
||||
"DeviceId",
|
||||
"FlushLatencyMax",
|
||||
"LoadUnloadCycleCount",
|
||||
"LoadUnloadCycleCountMax",
|
||||
"ManufactureDate",
|
||||
"PowerOnHours",
|
||||
"ReadErrorsCorrected",
|
||||
"ReadErrorsTotal",
|
||||
"ReadErrorsUncorrected",
|
||||
"ReadLatencyMax",
|
||||
"StartStopCycleCount",
|
||||
"StartStopCycleCountMax",
|
||||
"Temperature",
|
||||
"TemperatureMax",
|
||||
"Wear",
|
||||
"WriteErrorsCorrected",
|
||||
"WriteErrorsTotal",
|
||||
"WriteErrorsUncorrected",
|
||||
"WriteLatencyMax"
|
||||
]
|
||||
},
|
||||
"Temperature": 46,
|
||||
"TemperatureMax": 70,
|
||||
"Wear": 0,
|
||||
"ReadLatencyMax": 1930,
|
||||
"WriteLatencyMax": 1903,
|
||||
"FlushLatencyMax": 262
|
||||
}
|
||||
''', // Disk SMART data
|
||||
'12th Gen Intel(R) Core(TM) i5-12490F', // CPU brand
|
||||
];
|
||||
Reference in New Issue
Block a user