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,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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user