- 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>
148 lines
5.6 KiB
Java
148 lines
5.6 KiB
Java
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);
|
|
}
|
|
}
|