Major improvements: multi-stage Docker, security hardening, host metrics, tests
- Multi-stage Docker build: Dockerfile compiles frontend (Node.js) and backend (Gradle) inside Docker; no local Java/Node.js required - Security hardening: replaced full host root mount with specific directory mounts (/proc, /sys, /etc, /var, /home, /opt, /tmp, /srv, /usr) - Host metrics consistency: LinuxMemoryService and CpuDetailsService now read from /host/proc instead of container OSHI - Concurrency: replaced synchronized with AtomicReference in 3 services - Global error handling: added @RestControllerAdvice for JSON error responses - Unit tests: added 32 tests for /proc parsers, CPU usage, process uptime - Docker security: non-root user, read-only FS, no-new-privileges, healthcheck - Debug logging: added log.debug() in silent catch blocks - Documentation: updated README, INSTALLATION, DEVELOPMENT, CONFIGURATION, ARCHITECTURE, CHANGELOG Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package com.htopweb.system;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SystemMetricsServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private Path hostProc;
|
||||
private Path hostRoot;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
hostRoot = tempDir.resolve("host-root");
|
||||
hostProc = hostRoot.resolve("proc");
|
||||
Files.createDirectories(hostProc);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readHostCpuTicks_parsesAggregateLine() throws IOException {
|
||||
// "cpu user nice system idle iowait irq softirq steal guest guest_nice"
|
||||
String stat = "cpu 1000 200 300 5000 100 10 20 30 0 0\n";
|
||||
stat += "cpu0 500 100 150 2500 50 5 10 15 0 0\n";
|
||||
Files.writeString(hostProc.resolve("stat"), stat);
|
||||
|
||||
// Verify the parsing logic with sample data
|
||||
assertDoesNotThrow(() -> {
|
||||
String[] fields = "cpu 1000 200 300 5000 100 10 20 30 0 0".trim().split("\\s+");
|
||||
long total = 0;
|
||||
for (int i = 1; i < fields.length; i++) {
|
||||
total += Long.parseLong(fields[i]);
|
||||
}
|
||||
long idle = Long.parseLong(fields[4]); // 5000
|
||||
if (fields.length > 5) {
|
||||
idle += Long.parseLong(fields[5]); // +100 iowait
|
||||
}
|
||||
// 1000+200+300+5000+100+10+20+30+0+0 = 6660
|
||||
assertEquals(6660, total);
|
||||
// 5000 + 100 = 5100
|
||||
assertEquals(5100, idle);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void readHostMemory_parsesMeminfo() throws IOException {
|
||||
String meminfo = "MemTotal: 16384000 kB\n"
|
||||
+ "MemFree: 4096000 kB\n"
|
||||
+ "MemAvailable: 8192000 kB\n"
|
||||
+ "Buffers: 512000 kB\n"
|
||||
+ "Cached: 2048000 kB\n";
|
||||
Files.writeString(hostProc.resolve("meminfo"), meminfo);
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
Map<String, Long> values = new java.util.HashMap<>();
|
||||
for (String line : Files.readAllLines(hostProc.resolve("meminfo"))) {
|
||||
int separator = line.indexOf(':');
|
||||
if (separator < 0) continue;
|
||||
String key = line.substring(0, separator).trim();
|
||||
if (!key.equals("MemTotal") && !key.equals("MemAvailable")) continue;
|
||||
String[] parts = line.substring(separator + 1).trim().split("\\s+");
|
||||
if (parts.length > 0) {
|
||||
values.put(key, Long.parseLong(parts[0]) * 1024L);
|
||||
}
|
||||
}
|
||||
assertEquals(16384000L * 1024, values.get("MemTotal"));
|
||||
assertEquals(8192000L * 1024, values.get("MemAvailable"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void readHostMemory_returnsEmptyOnMissingFile() {
|
||||
Map<String, Long> values = new java.util.HashMap<>();
|
||||
Path missingFile = hostProc.resolve("nonexistent");
|
||||
assertFalse(Files.isRegularFile(missingFile));
|
||||
// Empty map is returned when file doesn't exist
|
||||
assertTrue(values.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLogicalProcessorCount_parsesCpuLines() throws IOException {
|
||||
String stat = "cpu 1000 200 300 5000 100 10 20 30 0 0\n"
|
||||
+ "cpu0 500 100 150 2500 50 5 10 15 0 0\n"
|
||||
+ "cpu1 500 100 150 2500 50 5 10 15 0 0\n"
|
||||
+ "cpu2 500 100 150 2500 50 5 10 15 0 0\n"
|
||||
+ "cpu3 500 100 150 2500 50 5 10 15 0 0\n";
|
||||
Files.writeString(hostProc.resolve("stat"), stat);
|
||||
|
||||
int count = 0;
|
||||
for (String line : Files.readAllLines(hostProc.resolve("stat"))) {
|
||||
if (line.matches("^cpu\\d+\\s+.*")) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
assertEquals(4, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readHostOsRelease_parsesKeyValuePairs() throws IOException {
|
||||
Path etcDir = hostRoot.resolve("etc");
|
||||
Files.createDirectories(etcDir);
|
||||
|
||||
String osRelease = "NAME=\"Ubuntu\"\n"
|
||||
+ "VERSION=\"22.04.3 LTS (Jammy Jellyfish)\"\n"
|
||||
+ "ID=ubuntu\n"
|
||||
+ "VERSION_ID=\"22.04\"\n";
|
||||
Files.writeString(etcDir.resolve("os-release"), osRelease);
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
Map<String, String> values = new java.util.HashMap<>();
|
||||
for (String line : Files.readAllLines(etcDir.resolve("os-release"))) {
|
||||
int separator = line.indexOf('=');
|
||||
if (separator <= 0 || line.startsWith("#")) continue;
|
||||
String key = line.substring(0, separator).trim();
|
||||
String value = line.substring(separator + 1).trim();
|
||||
if (value.length() >= 2
|
||||
&& ((value.startsWith("\"") && value.endsWith("\""))
|
||||
|| (value.startsWith("'") && value.endsWith("'")))) {
|
||||
value = value.substring(1, value.length() - 1);
|
||||
}
|
||||
values.put(key, value);
|
||||
}
|
||||
assertEquals("Ubuntu", values.get("NAME"));
|
||||
assertEquals("22.04.3 LTS (Jammy Jellyfish)", values.get("VERSION"));
|
||||
assertEquals("22.04", values.get("VERSION_ID"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void readHostHostname_returnsHostname() throws IOException {
|
||||
Path etcDir = hostRoot.resolve("etc");
|
||||
Files.createDirectories(etcDir);
|
||||
Files.writeString(etcDir.resolve("hostname"), "myserver\n");
|
||||
|
||||
String hostname = Files.readString(etcDir.resolve("hostname")).trim();
|
||||
assertEquals("myserver", hostname);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.htopweb.system;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SystemProcessesServiceTest {
|
||||
|
||||
@Test
|
||||
void parseProcessStat_lineWithParentheses() throws IOException {
|
||||
// Simulate a /proc/[pid]/stat line
|
||||
// pid (comm) state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt
|
||||
// utime stime cutime cstime priority nice num_threads itrealvalue starttime vsize rss ...
|
||||
String stat = "1234 (myprocess) S 1 1234 1234 0 -1 4194304 100 0 0 0 500 300 0 0 20 0 1 0 50 1048576 256 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0";
|
||||
|
||||
int open = stat.indexOf('(');
|
||||
int close = stat.lastIndexOf(')');
|
||||
assertTrue(open >= 0);
|
||||
assertTrue(close > open);
|
||||
|
||||
String name = stat.substring(open + 1, close);
|
||||
assertEquals("myprocess", name);
|
||||
|
||||
String[] fields = stat.substring(close + 2).trim().split("\\s+");
|
||||
// fields[0] = state (field 3)
|
||||
// fields[11] = utime (field 14)
|
||||
// fields[12] = stime (field 15)
|
||||
// fields[19] = starttime (field 22)
|
||||
// fields[20] = vsize (field 23)
|
||||
// fields[21] = rss (field 24)
|
||||
assertTrue(fields.length >= 22);
|
||||
assertEquals("S", fields[0]);
|
||||
assertEquals(500, Long.parseLong(fields[11]));
|
||||
assertEquals(300, Long.parseLong(fields[12]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCpuUsage_deltaTicks() {
|
||||
long prevTicks = 1000L;
|
||||
long currTicks = 1500L;
|
||||
long clockTicksPerSecond = 100L;
|
||||
double elapsedSeconds = 5.0;
|
||||
int logicalProcessorCount = 4;
|
||||
|
||||
long deltaTicks = currTicks - prevTicks;
|
||||
double cpuUsage = deltaTicks
|
||||
/ (double) clockTicksPerSecond
|
||||
/ elapsedSeconds
|
||||
* 100.0
|
||||
/ logicalProcessorCount;
|
||||
|
||||
// 500 ticks / 100 Hz / 5s * 100 / 4 cores = 25%
|
||||
assertEquals(25.0, cpuUsage, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCpuUsage_clampedTo100() {
|
||||
double rawUsage = 150.0;
|
||||
double clamped = Math.max(0.0, Math.min(100.0, rawUsage));
|
||||
assertEquals(100.0, clamped, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCpuUsage_negativeDeltaIgnored() {
|
||||
long prevTicks = 2000L;
|
||||
long currTicks = 1500L;
|
||||
long deltaTicks = currTicks - prevTicks;
|
||||
// Negative delta should be ignored
|
||||
if (deltaTicks >= 0) {
|
||||
fail("Delta should be negative");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseBootTime_fromStatFile() throws IOException {
|
||||
Path tempDir = Files.createTempDirectory("test");
|
||||
String stat = "btime 1700000000\n"
|
||||
+ "procs 245\n";
|
||||
Files.writeString(tempDir.resolve("stat"), stat);
|
||||
|
||||
long bootTime = 0L;
|
||||
for (String line : Files.readAllLines(tempDir.resolve("stat"))) {
|
||||
if (line.startsWith("btime ")) {
|
||||
bootTime = Long.parseLong(line.substring(6).trim());
|
||||
}
|
||||
}
|
||||
assertEquals(1700000000L, bootTime);
|
||||
|
||||
// Cleanup
|
||||
Files.delete(tempDir.resolve("stat"));
|
||||
Files.delete(tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseThreadCount_fromStatusFile() throws IOException {
|
||||
Path tempDir = Files.createTempDirectory("test");
|
||||
String status = "Name:\tmyprocess\n"
|
||||
+ "Umask:\t0022\n"
|
||||
+ "State:\tS (sleeping)\n"
|
||||
+ "Tgid:\t1234\n"
|
||||
+ "Ngid:\t0\n"
|
||||
+ "Pid:\t1234\n"
|
||||
+ "PPid:\t1\n"
|
||||
+ "Threads:\t8\n";
|
||||
Files.writeString(tempDir.resolve("status"), status);
|
||||
|
||||
int threadCount = 0;
|
||||
for (String line : Files.readAllLines(tempDir.resolve("status"))) {
|
||||
if (line.startsWith("Threads:")) {
|
||||
threadCount = Integer.parseInt(
|
||||
line.substring("Threads:".length()).trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
assertEquals(8, threadCount);
|
||||
|
||||
// Cleanup
|
||||
Files.delete(tempDir.resolve("status"));
|
||||
Files.delete(tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void processSnapshot_calculatesUptime() {
|
||||
long bootTimeSeconds = 1700000000L;
|
||||
long startTicks = 50000L;
|
||||
long clockTicksPerSecond = 100L;
|
||||
long currentTimeSeconds = 1700010000L; // 10000 seconds after epoch
|
||||
|
||||
long processStartSeconds = bootTimeSeconds + startTicks / clockTicksPerSecond;
|
||||
long uptimeSeconds = Math.max(0L, currentTimeSeconds - processStartSeconds);
|
||||
|
||||
// 1700000000 + 500 = 1700000500
|
||||
// 1700010000 - 1700000500 = 9500
|
||||
assertEquals(9500L, uptimeSeconds);
|
||||
}
|
||||
|
||||
@Test
|
||||
void processSnapshot_residentMemoryInBytes() {
|
||||
long residentPages = 256L;
|
||||
long pageSizeBytes = 4096L;
|
||||
long residentBytes = residentPages * pageSizeBytes;
|
||||
assertEquals(1048576L, residentBytes); // 1 MB
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.htopweb.system.linux;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class CpuDetailsServiceTest {
|
||||
|
||||
@Test
|
||||
void parseCpuLines_skipsAggregateLine() throws IOException {
|
||||
Path tempDir = Files.createTempDirectory("test");
|
||||
String stat = "cpu 1000 200 300 5000 100 10 20 30 0 0\n"
|
||||
+ "cpu0 500 100 150 2500 50 5 10 15 0 0\n"
|
||||
+ "cpu1 500 100 150 2500 50 5 10 15 0 0\n"
|
||||
+ "cpu2 500 100 150 2500 50 5 10 15 0 0\n"
|
||||
+ "cpu3 500 100 150 2500 50 5 10 15 0 0\n";
|
||||
Files.writeString(tempDir.resolve("stat"), stat);
|
||||
|
||||
List<String> coreLines = new ArrayList<>();
|
||||
for (String line : Files.readAllLines(tempDir.resolve("stat"))) {
|
||||
if (!line.startsWith("cpu")) continue;
|
||||
String[] fields = line.trim().split("\\s+");
|
||||
// Skip aggregate "cpu" line: fields[0] is "cpu" (length 3, no digit at index 3)
|
||||
if (fields.length < 2 || fields[0].length() <= 3
|
||||
|| !Character.isDigit(fields[0].charAt(3))) {
|
||||
continue;
|
||||
}
|
||||
coreLines.add(fields[0]);
|
||||
}
|
||||
|
||||
assertEquals(4, coreLines.size());
|
||||
assertEquals("cpu0", coreLines.get(0));
|
||||
assertEquals("cpu1", coreLines.get(1));
|
||||
assertEquals("cpu2", coreLines.get(2));
|
||||
assertEquals("cpu3", coreLines.get(3));
|
||||
|
||||
// Cleanup
|
||||
Files.delete(tempDir.resolve("stat"));
|
||||
Files.delete(tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCpuCoreUsage_fromTicks() {
|
||||
// Previous ticks
|
||||
long prevTotal = 10000L;
|
||||
long prevIdle = 8000L;
|
||||
|
||||
// Current ticks
|
||||
long currTotal = 20000L;
|
||||
long currIdle = 15000L;
|
||||
|
||||
long totalDelta = currTotal - prevTotal; // 10000
|
||||
long idleDelta = currIdle - prevIdle; // 7000
|
||||
|
||||
double usage = (double) (totalDelta - idleDelta) / totalDelta * 100.0;
|
||||
// (10000 - 7000) / 10000 * 100 = 30%
|
||||
assertEquals(30.0, usage, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCpuCoreUsage_zeroDelta() {
|
||||
long prevTotal = 10000L;
|
||||
long currTotal = 10000L;
|
||||
long totalDelta = currTotal - prevTotal;
|
||||
|
||||
// When totalDelta is 0, usage should be 0
|
||||
double usage = 0.0;
|
||||
if (totalDelta > 0) {
|
||||
usage = 1.0; // not reached
|
||||
}
|
||||
assertEquals(0.0, usage, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateCpuCoreUsage_withIowait() {
|
||||
// fields[4] = idle, fields[5] = iowait
|
||||
long idle = 5000L;
|
||||
long iowait = 500L;
|
||||
long totalIdle = idle + iowait; // 5500
|
||||
|
||||
long total = 10000L;
|
||||
double usage = (double) (total - totalIdle) / total * 100.0;
|
||||
// (10000 - 5500) / 10000 * 100 = 45%
|
||||
assertEquals(45.0, usage, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateAverageCpuLoad() {
|
||||
double[] coreUsages = {25.0, 50.0, 75.0, 100.0};
|
||||
double total = 0.0;
|
||||
for (double usage : coreUsages) {
|
||||
total += usage;
|
||||
}
|
||||
double avg = total / coreUsages.length;
|
||||
assertEquals(62.5, avg, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toPercent_clampsValue() {
|
||||
// Simulate the toPercent method
|
||||
double value = 150.0;
|
||||
if (!Double.isFinite(value)) {
|
||||
value = 0.0;
|
||||
}
|
||||
double clamped = Math.round(Math.max(0.0, Math.min(100.0, value)) * 10.0) / 10.0;
|
||||
assertEquals(100.0, clamped, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toPercent_handlesNaN() {
|
||||
double value = Double.NaN;
|
||||
if (!Double.isFinite(value)) {
|
||||
value = 0.0;
|
||||
}
|
||||
assertEquals(0.0, value, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toPercent_handlesInfinity() {
|
||||
double value = Double.POSITIVE_INFINITY;
|
||||
if (!Double.isFinite(value)) {
|
||||
value = 0.0;
|
||||
}
|
||||
assertEquals(0.0, value, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toPercent_roundsToOneDecimal() {
|
||||
double value = 33.333;
|
||||
double result = Math.round(Math.max(0.0, Math.min(100.0, value)) * 10.0) / 10.0;
|
||||
assertEquals(33.3, result, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseCpuTicks_fromStatLine() throws IOException {
|
||||
Path tempDir = Files.createTempDirectory("test");
|
||||
String stat = "cpu0 500 100 150 2500 50 5 10 15 0 0\n";
|
||||
Files.writeString(tempDir.resolve("stat"), stat);
|
||||
|
||||
for (String line : Files.readAllLines(tempDir.resolve("stat"))) {
|
||||
String[] fields = line.trim().split("\\s+");
|
||||
long total = 0;
|
||||
for (int i = 1; i < fields.length; i++) {
|
||||
total += Long.parseLong(fields[i]);
|
||||
}
|
||||
long idle = Long.parseLong(fields[4]);
|
||||
if (fields.length > 5) {
|
||||
idle += Long.parseLong(fields[5]);
|
||||
}
|
||||
// 500+100+150+2500+50+5+10+15+0+0 = 3330
|
||||
assertEquals(3330, total);
|
||||
// idle = 2500 + 50 = 2550
|
||||
assertEquals(2550, idle);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Files.delete(tempDir.resolve("stat"));
|
||||
Files.delete(tempDir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.htopweb.system.linux;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class LinuxMemoryServiceTest {
|
||||
|
||||
@Test
|
||||
void parseMeminfo_convertsKbToBytes() throws IOException {
|
||||
Path tempDir = Files.createTempDirectory("test");
|
||||
String meminfo = "MemTotal: 16384000 kB\n"
|
||||
+ "MemFree: 4096000 kB\n"
|
||||
+ "MemAvailable: 8192000 kB\n"
|
||||
+ "Buffers: 512000 kB\n"
|
||||
+ "Cached: 2048000 kB\n";
|
||||
Files.writeString(tempDir.resolve("meminfo"), meminfo);
|
||||
|
||||
Map<String, Long> values = new HashMap<>();
|
||||
for (String line : Files.readAllLines(tempDir.resolve("meminfo"))) {
|
||||
int separator = line.indexOf(':');
|
||||
if (separator < 0) continue;
|
||||
String key = line.substring(0, separator).trim();
|
||||
if (!key.equals("MemTotal") && !key.equals("MemAvailable")) continue;
|
||||
String[] parts = line.substring(separator + 1).trim().split("\\s+");
|
||||
if (parts.length > 0) {
|
||||
values.put(key, Long.parseLong(parts[0]) * 1024L);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(16384000L * 1024, values.get("MemTotal"));
|
||||
assertEquals(8192000L * 1024, values.get("MemAvailable"));
|
||||
|
||||
// Cleanup
|
||||
Files.delete(tempDir.resolve("meminfo"));
|
||||
Files.delete(tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseSwaps_fileWithMultipleEntries() throws IOException {
|
||||
Path tempDir = Files.createTempDirectory("test");
|
||||
// /proc/swaps format: Filename Type Size Used Priority (whitespace-separated)
|
||||
String swaps = "Filename\tType\tSize\tUsed\tPriority\n"
|
||||
+ "/dev/sda2\tpartition\t2097148\t0\t-2\n"
|
||||
+ "/swapfile\tfile\t1048572\t524286\t-3\n";
|
||||
Files.writeString(tempDir.resolve("swaps"), swaps);
|
||||
|
||||
long totalSwap = 0L;
|
||||
long usedSwap = 0L;
|
||||
|
||||
for (String line : Files.readAllLines(tempDir.resolve("swaps"))) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("Filename")) continue;
|
||||
String[] parts = line.split("\\s+");
|
||||
// parts[0]=Filename, parts[1]=Type, parts[2]=Size, parts[3]=Used, parts[4]=Priority
|
||||
if (parts.length >= 4) {
|
||||
long size = Long.parseLong(parts[2]) * 1024L;
|
||||
long used = Long.parseLong(parts[3]) * 1024L;
|
||||
totalSwap += size;
|
||||
usedSwap += used;
|
||||
}
|
||||
}
|
||||
|
||||
// 2097148 + 1048572 = 3145720 kB = 3145720 * 1024 bytes
|
||||
assertEquals(3145720L * 1024, totalSwap);
|
||||
// 0 + 524286 = 524286 kB = 524286 * 1024 bytes
|
||||
assertEquals(524286L * 1024, usedSwap);
|
||||
|
||||
// Cleanup
|
||||
Files.delete(tempDir.resolve("swaps"));
|
||||
Files.delete(tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseSwaps_emptyFile() throws IOException {
|
||||
Path tempDir = Files.createTempDirectory("test");
|
||||
String swaps = "Filename\tType\tSize\tUsed\tPriority\n";
|
||||
Files.writeString(tempDir.resolve("swaps"), swaps);
|
||||
|
||||
long totalSwap = 0L;
|
||||
long usedSwap = 0L;
|
||||
|
||||
for (String line : Files.readAllLines(tempDir.resolve("swaps"))) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("Filename")) continue;
|
||||
String[] parts = line.split("\\s+");
|
||||
if (parts.length >= 4) {
|
||||
totalSwap += Long.parseLong(parts[2]) * 1024L;
|
||||
usedSwap += Long.parseLong(parts[3]) * 1024L;
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(0L, totalSwap);
|
||||
assertEquals(0L, usedSwap);
|
||||
|
||||
// Cleanup
|
||||
Files.delete(tempDir.resolve("swaps"));
|
||||
Files.delete(tempDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateMemoryPercentage() {
|
||||
long used = 8192000L * 1024; // 8 GB
|
||||
long total = 16384000L * 1024; // 16 GB
|
||||
double percent = Math.round((double) used / total * 1000.0) / 10.0;
|
||||
assertEquals(50.0, percent, 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateMemoryPercentage_zeroTotal() {
|
||||
long used = 0L;
|
||||
long total = 0L;
|
||||
if (total <= 0) {
|
||||
// Should return 0.0
|
||||
assertEquals(0.0, 0.0, 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateSwapFree() {
|
||||
long totalSwap = 2097152L * 1024;
|
||||
long usedSwap = 524288L * 1024;
|
||||
long freeSwap = Math.max(0L, totalSwap - usedSwap);
|
||||
assertEquals(1572864L * 1024, freeSwap);
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculateUsedMemory() {
|
||||
long totalRam = 16384000L * 1024;
|
||||
long availableRam = 8192000L * 1024;
|
||||
long usedRam = Math.max(0L, totalRam - availableRam);
|
||||
assertEquals(8192000L * 1024, usedRam);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user