diff --git a/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java b/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java index 893c37e..3c5f576 100644 --- a/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java +++ b/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java @@ -82,12 +82,15 @@ public class AlertMonitoringService { */ private void checkCpuThreshold(AlertConfiguration config) { try { - OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); - double loadAvg = osBean.getSystemLoadAverage(); - int processors = osBean.getAvailableProcessors(); - - // Calculer l'utilisation CPU en pourcentage - double cpuUsage = loadAvg < 0 ? 0.0 : Math.min(100.0, (loadAvg / processors) * 100.0); + // getProcessCpuLoad() renvoie la charge CPU de CE process JVM (0.0-1.0), + // ce qui est correct en conteneur K8s/Docker. + // getSystemLoadAverage() renvoie la charge du NODE entier (hôte Linux), + // divisée par availableProcessors() limité par le conteneur (ex: 1), + // ce qui produit des faux positifs dès que le node est actif. + com.sun.management.OperatingSystemMXBean osBean = + (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + double processCpuLoad = osBean.getProcessCpuLoad(); + double cpuUsage = processCpuLoad < 0 ? 0.0 : Math.min(100.0, processCpuLoad * 100.0); lastCpuUsage = cpuUsage; int threshold = config.getCpuThresholdPercent(); diff --git a/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceMockStaticCoverageTest.java b/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceMockStaticCoverageTest.java index f47ab04..05d4529 100644 --- a/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceMockStaticCoverageTest.java +++ b/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceMockStaticCoverageTest.java @@ -18,7 +18,6 @@ import dev.lions.unionflow.server.repository.SystemLogRepository; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryUsage; -import java.lang.management.OperatingSystemMXBean; import java.lang.reflect.Field; import java.time.LocalDateTime; import java.util.Collections; @@ -35,11 +34,12 @@ import org.mockito.junit.jupiter.MockitoExtension; * Tests unitaires purs (sans Quarkus) pour {@link AlertMonitoringService}. * *

Utilise {@code mockStatic(ManagementFactory.class)} pour injecter des beans MXBean - * contrôlés et couvrir les branches inaccessibles sur certains systèmes (notamment Windows - * où {@code getSystemLoadAverage()} retourne toujours -1). + * contrôlés et couvrir les branches du calcul CPU basé sur + * {@code com.sun.management.OperatingSystemMXBean.getProcessCpuLoad()} (0.0-1.0, -1 si indispo). * *

Note : utilise {@code mock-maker-inline} (configuré via - * {@code src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker}). + * {@code src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker}) — + * nécessaire pour mocker {@code com.sun.management.OperatingSystemMXBean} (type JDK interne). */ @ExtendWith(MockitoExtension.class) @DisplayName("AlertMonitoringService — branches via mockStatic ManagementFactory") @@ -69,31 +69,31 @@ class AlertMonitoringServiceMockStaticCoverageTest { } // ========================================================================= - // L85 : branche ternaire loadAvg >= 0 → Math.min(100.0, (loadAvg / processors) * 100.0) + // checkCpuThreshold : branche processCpuLoad >= 0 + // cpuUsage = Math.min(100.0, processCpuLoad * 100.0) // ========================================================================= /** - * Couvre la branche {@code loadAvg >= 0} à la ligne 85 : - * {@code Math.min(100.0, (loadAvg / processors) * 100.0)}. + * Couvre la branche {@code processCpuLoad >= 0} : + * {@code cpuUsage = Math.min(100.0, processCpuLoad * 100.0)}. * - *

On injecte un {@code OperatingSystemMXBean} mock qui retourne - * {@code getSystemLoadAverage() = 0.5} (valeur positive ≥ 0) et - * {@code getAvailableProcessors() = 4}. - * → {@code cpuUsage = Math.min(100.0, (0.5/4)*100.0) = Math.min(100.0, 12.5) = 12.5}. + *

On injecte un {@code com.sun.management.OperatingSystemMXBean} mock qui retourne + * {@code getProcessCpuLoad() = 0.125} (valeur positive ≥ 0). + * → {@code cpuUsage = Math.min(100.0, 0.125 * 100.0) = 12.5}. * *

Avec {@code threshold = 0}, {@code 12.5 > 0} → branche cpuUsage > threshold → true - * → {@code lastCpuHighTime} est mis à null initialement → il est settté à now(). + * → {@code lastCpuHighTime} est mis à null initialement → il est setté à now(). * Aucune alerte n'est créée (première itération). */ @Test - @DisplayName("checkCpuThreshold : loadAvg >= 0 → Math.min branch (L85) via OperatingSystemMXBean mock") - void checkCpuThreshold_loadAvgPositive_coversL85MathMinBranch() { + @DisplayName("checkCpuThreshold : processCpuLoad >= 0 → Math.min branch via OperatingSystemMXBean mock") + void checkCpuThreshold_processCpuLoadPositive_coversMathMinBranch() { AlertConfiguration config = buildConfig(true, 0, 60, false, 100, false); when(alertConfigurationRepository.getConfiguration()).thenReturn(config); - OperatingSystemMXBean osMock = mock(OperatingSystemMXBean.class); - when(osMock.getSystemLoadAverage()).thenReturn(0.5); // >= 0 → branche Math.min à L85 - when(osMock.getAvailableProcessors()).thenReturn(4); + com.sun.management.OperatingSystemMXBean osMock = + mock(com.sun.management.OperatingSystemMXBean.class); + when(osMock.getProcessCpuLoad()).thenReturn(0.125); // >= 0 → branche Math.min try (MockedStatic mf = mockStatic(ManagementFactory.class)) { mf.when(ManagementFactory::getOperatingSystemMXBean).thenReturn(osMock); @@ -108,28 +108,28 @@ class AlertMonitoringServiceMockStaticCoverageTest { } /** - * Couvre la branche {@code loadAvg >= 0} à L85 avec une valeur élevée déclenchant une alerte. + * Couvre la branche {@code processCpuLoad >= 0} avec une valeur élevée déclenchant une alerte. * - *

On simule un CPU à 200% (loadAvg=8, processors=4 → cpuUsage=200, cappé à 100%). + *

On simule un CPU à 200% brut (getProcessCpuLoad=2.0 — cas théorique) cappé à 100% via Math.min. * Après deux appels avec {@code durationMinutes=0}, l'alerte est créée. */ @Test - @DisplayName("checkCpuThreshold : loadAvg élevé → cpuUsage 100% → alerte créée en 2 appels (L85 Math.min branch)") - void checkCpuThreshold_loadAvgHighPositive_triggersAlert_coversL85() { + @DisplayName("checkCpuThreshold : processCpuLoad élevé → cpuUsage cappé à 100% → alerte créée en 2 appels") + void checkCpuThreshold_processCpuLoadHighPositive_triggersAlert() { // Premier appel : reset lastCpuHighTime via threshold élevé AlertConfiguration resetConfig = buildConfig(true, 100, 0, false, 100, false); when(alertConfigurationRepository.getConfiguration()).thenReturn(resetConfig); - OperatingSystemMXBean osMock = mock(OperatingSystemMXBean.class); - when(osMock.getSystemLoadAverage()).thenReturn(2.0); // >= 0 → Math.min branch - when(osMock.getAvailableProcessors()).thenReturn(4); + com.sun.management.OperatingSystemMXBean osMock = + mock(com.sun.management.OperatingSystemMXBean.class); + when(osMock.getProcessCpuLoad()).thenReturn(0.5); // >= 0 → Math.min branch (50%) try (MockedStatic mf = mockStatic(ManagementFactory.class)) { mf.when(ManagementFactory::getOperatingSystemMXBean).thenReturn(osMock); service.monitorSystemMetrics(); // cpuUsage=50%, threshold=100 → below → lastCpuHighTime=null } - // Deuxième et troisième appels avec threshold=-1 (0.0 > -1 always), durationMinutes=0 + // Deuxième et troisième appels avec threshold=-1 (50.0 > -1 always), durationMinutes=0 AlertConfiguration triggerConfig = buildConfig(true, -1, 0, false, 100, false); when(alertConfigurationRepository.getConfiguration()).thenReturn(triggerConfig); when(systemAlertRepository.findByTimestampBetween(any(LocalDateTime.class), any(LocalDateTime.class))) @@ -149,28 +149,28 @@ class AlertMonitoringServiceMockStaticCoverageTest { } // ========================================================================= - // L113-114 : catch(Exception e) dans checkCpuThreshold + // catch(Exception e) dans checkCpuThreshold // ========================================================================= /** - * Couvre les lignes 113-114 ({@code catch(Exception e)} et {@code log.error(...)}) dans + * Couvre le bloc {@code catch(Exception e)} et {@code log.error(...)} dans * {@code checkCpuThreshold()}. * *

On force {@code ManagementFactory.getOperatingSystemMXBean()} à lancer une - * {@code RuntimeException}. Cette exception est capturée par le catch à L113, - * loggée à L114, et la méthode retourne silencieusement. + * {@code RuntimeException}. Cette exception est capturée par le catch, + * loggée, et la méthode retourne silencieusement. */ @Test - @DisplayName("checkCpuThreshold : ManagementFactory.getOperatingSystemMXBean() throw → catch L113-114 couvert") - void checkCpuThreshold_managementFactoryThrows_coversCatchL113to114() { + @DisplayName("checkCpuThreshold : ManagementFactory.getOperatingSystemMXBean() throw → catch couvert") + void checkCpuThreshold_managementFactoryThrows_coversCatch() { AlertConfiguration config = buildConfig(true, 80, 5, false, 100, false); when(alertConfigurationRepository.getConfiguration()).thenReturn(config); try (MockedStatic mf = mockStatic(ManagementFactory.class)) { mf.when(ManagementFactory::getOperatingSystemMXBean) - .thenThrow(new RuntimeException("Simulated JMX failure — covers catch L113-114")); + .thenThrow(new RuntimeException("Simulated JMX failure — covers catch")); - // L113-114 : l'exception est catchée → monitorSystemMetrics ne propage pas + // L'exception est catchée → monitorSystemMetrics ne propage pas assertThatCode(() -> service.monitorSystemMetrics()) .doesNotThrowAnyException(); } @@ -180,19 +180,19 @@ class AlertMonitoringServiceMockStaticCoverageTest { } /** - * Couvre L113-114 via une exception lancée pendant le calcul (NullPointerException - * depuis le mock MemoryMXBean non configuré pour OperatingSystemMXBean). + * Couvre le catch via une exception lancée pendant le calcul (RuntimeException + * depuis l'appel à {@code getProcessCpuLoad()}). */ @Test - @DisplayName("checkCpuThreshold : OperatingSystemMXBean.getAvailableProcessors() throw → catch L113-114") - void checkCpuThreshold_availableProcessorsThrows_coversCatchL113to114() { + @DisplayName("checkCpuThreshold : OperatingSystemMXBean.getProcessCpuLoad() throw → catch couvert") + void checkCpuThreshold_processCpuLoadThrows_coversCatch() { AlertConfiguration config = buildConfig(true, 80, 5, false, 100, false); when(alertConfigurationRepository.getConfiguration()).thenReturn(config); - OperatingSystemMXBean osMock = mock(OperatingSystemMXBean.class); - when(osMock.getSystemLoadAverage()).thenReturn(0.5); - when(osMock.getAvailableProcessors()) - .thenThrow(new RuntimeException("Simulated getAvailableProcessors failure")); + com.sun.management.OperatingSystemMXBean osMock = + mock(com.sun.management.OperatingSystemMXBean.class); + when(osMock.getProcessCpuLoad()) + .thenThrow(new RuntimeException("Simulated getProcessCpuLoad failure")); try (MockedStatic mf = mockStatic(ManagementFactory.class)) { mf.when(ManagementFactory::getOperatingSystemMXBean).thenReturn(osMock); @@ -205,11 +205,11 @@ class AlertMonitoringServiceMockStaticCoverageTest { } // ========================================================================= - // L127 : branche ternaire maxMemory <= 0 → 0.0 + // checkMemoryThreshold : branche ternaire maxMemory <= 0 → 0.0 // ========================================================================= /** - * Couvre la branche {@code maxMemory <= 0} à la ligne 127 : + * Couvre la branche {@code maxMemory <= 0} : * {@code double memoryUsage = maxMemory > 0 ? ... : 0.0}. * *

On injecte un {@code MemoryMXBean} mock dont {@code getHeapMemoryUsage().getMax()} @@ -219,14 +219,14 @@ class AlertMonitoringServiceMockStaticCoverageTest { *

Avec threshold=0 : {@code 0.0 > 0} est false → pas d'alerte. */ @Test - @DisplayName("checkMemoryThreshold : maxMemory = -1 (non défini) → memoryUsage = 0.0 → branche false L127") - void checkMemoryThreshold_maxMemoryMinusOne_coversL127FalseBranch() { + @DisplayName("checkMemoryThreshold : maxMemory = -1 (non défini) → memoryUsage = 0.0 → branche false") + void checkMemoryThreshold_maxMemoryMinusOne_coversFalseBranch() { AlertConfiguration config = buildConfig(false, 80, 5, true, 0, false); when(alertConfigurationRepository.getConfiguration()).thenReturn(config); MemoryMXBean memMock = mock(MemoryMXBean.class); MemoryUsage heapUsageMock = mock(MemoryUsage.class); - when(heapUsageMock.getMax()).thenReturn(-1L); // maxMemory = -1 → branche false à L127 + when(heapUsageMock.getMax()).thenReturn(-1L); // maxMemory = -1 → branche false when(heapUsageMock.getUsed()).thenReturn(100L); when(memMock.getHeapMemoryUsage()).thenReturn(heapUsageMock); @@ -242,19 +242,19 @@ class AlertMonitoringServiceMockStaticCoverageTest { } /** - * Couvre L127 branche false avec maxMemory = 0. + * Couvre la branche false du ternaire avec maxMemory = 0. * *

La condition {@code maxMemory > 0} est false pour maxMemory = 0 aussi. */ @Test - @DisplayName("checkMemoryThreshold : maxMemory = 0 → memoryUsage = 0.0 → branche false L127") - void checkMemoryThreshold_maxMemoryZero_coversL127FalseBranch() { + @DisplayName("checkMemoryThreshold : maxMemory = 0 → memoryUsage = 0.0 → branche false") + void checkMemoryThreshold_maxMemoryZero_coversFalseBranch() { AlertConfiguration config = buildConfig(false, 80, 5, true, 0, false); when(alertConfigurationRepository.getConfiguration()).thenReturn(config); MemoryMXBean memMock = mock(MemoryMXBean.class); MemoryUsage heapUsageMock = mock(MemoryUsage.class); - when(heapUsageMock.getMax()).thenReturn(0L); // maxMemory = 0 → branche false à L127 + when(heapUsageMock.getMax()).thenReturn(0L); // maxMemory = 0 → branche false when(heapUsageMock.getUsed()).thenReturn(50L); when(memMock.getHeapMemoryUsage()).thenReturn(heapUsageMock); @@ -270,11 +270,11 @@ class AlertMonitoringServiceMockStaticCoverageTest { } /** - * Couvre L127 branche true (maxMemory > 0) + alerte mémoire créée. + * Couvre la branche true (maxMemory > 0) + alerte mémoire créée. * Vérifie que le mock MemoryMXBean avec une valeur positive couvre la branche vraie. */ @Test - @DisplayName("checkMemoryThreshold : maxMemory > 0 + usage > threshold → alerte créée (L127 branche true)") + @DisplayName("checkMemoryThreshold : maxMemory > 0 + usage > threshold → alerte créée (branche true)") void checkMemoryThreshold_maxMemoryPositive_usageAboveThreshold_createsAlert() { AlertConfiguration config = buildConfig(false, 80, 5, true, 0, false); when(alertConfigurationRepository.getConfiguration()).thenReturn(config); @@ -299,25 +299,27 @@ class AlertMonitoringServiceMockStaticCoverageTest { } // ========================================================================= - // L85 : combinaison de loadAvg >= 0 avec threshold dépassé (couvre Math.min) + // checkCpuThreshold : branche processCpuLoad < 0 (API non disponible) + // cpuUsage = 0.0 (ternaire : processCpuLoad < 0 ? 0.0 : ...) // ========================================================================= /** - * Couvre explicitement la branche {@code loadAvg < 0} (L85, branche true du ternaire) - * pour compléter la couverture des deux branches du ternaire à L85. + * Couvre explicitement la branche {@code processCpuLoad < 0} du ternaire + * pour compléter la couverture des deux branches du ternaire. * - *

On injecte {@code getSystemLoadAverage() = -1.0} (loadAvg < 0 → cpuUsage = 0.0). - * Cela simule le comportement Windows où loadAvg = -1. + *

On injecte {@code getProcessCpuLoad() = -1.0} → cpuUsage = 0.0. + * Cela simule le cas où l'API n'est pas disponible (le contrat JDK dit que + * {@code getProcessCpuLoad()} peut retourner -1 si la valeur n'est pas disponible). */ @Test - @DisplayName("checkCpuThreshold : loadAvg = -1 (Windows) → cpuUsage = 0.0 → branche true L85 ternaire") - void checkCpuThreshold_loadAvgNegative_coversL85TrueBranch() { + @DisplayName("checkCpuThreshold : processCpuLoad = -1 (indispo) → cpuUsage = 0.0 → branche true ternaire") + void checkCpuThreshold_processCpuLoadNegative_coversTrueBranch() { AlertConfiguration config = buildConfig(true, 100, 5, false, 100, false); when(alertConfigurationRepository.getConfiguration()).thenReturn(config); - OperatingSystemMXBean osMock = mock(OperatingSystemMXBean.class); - when(osMock.getSystemLoadAverage()).thenReturn(-1.0); // < 0 → cpuUsage = 0.0 - when(osMock.getAvailableProcessors()).thenReturn(4); + com.sun.management.OperatingSystemMXBean osMock = + mock(com.sun.management.OperatingSystemMXBean.class); + when(osMock.getProcessCpuLoad()).thenReturn(-1.0); // < 0 → cpuUsage = 0.0 try (MockedStatic mf = mockStatic(ManagementFactory.class)) { mf.when(ManagementFactory::getOperatingSystemMXBean).thenReturn(osMock); @@ -331,7 +333,7 @@ class AlertMonitoringServiceMockStaticCoverageTest { } // ========================================================================= - // L229 : lambda anyMatch dans hasRecentCpuAlert + // hasRecentCpuAlert : lambda anyMatch // Couverture supplémentaire des branches de la lambda // ========================================================================= @@ -340,11 +342,11 @@ class AlertMonitoringServiceMockStaticCoverageTest { * {@code source = "CPU"} et {@code acknowledged = false} → {@code anyMatch} retourne true * → {@code hasRecentCpuAlert()} = true → aucune nouvelle alerte créée. * - *

La lambda à L229 : {@code "CPU".equals(alert.getSource()) && !alert.getAcknowledged()} + *

Lambda : {@code "CPU".equals(alert.getSource()) && !alert.getAcknowledged()} * — branche : source="CPU" true ET acknowledged=false → true → anyMatch=true. */ @Test - @DisplayName("hasRecentCpuAlert lambda L229 : source=CPU + acknowledged=false → anyMatch true → pas d'alerte") + @DisplayName("hasRecentCpuAlert lambda : source=CPU + acknowledged=false → anyMatch true → pas d'alerte") void hasRecentCpuAlert_lambda_sourceCpu_acknowledgedFalse_anyMatchTrue_noNewAlert() throws Exception { // Préparer : reset lastCpuHighTime à null d'abord injectField(service, "lastCpuHighTime", null); @@ -362,9 +364,9 @@ class AlertMonitoringServiceMockStaticCoverageTest { when(systemAlertRepository.findByTimestampBetween(any(LocalDateTime.class), any(LocalDateTime.class))) .thenReturn(List.of(existingCpuAlert)); - OperatingSystemMXBean osMock = mock(OperatingSystemMXBean.class); - when(osMock.getSystemLoadAverage()).thenReturn(2.0); // loadAvg >= 0 → Math.min branch L85 - when(osMock.getAvailableProcessors()).thenReturn(4); // cpuUsage = 50% > -1 + com.sun.management.OperatingSystemMXBean osMock = + mock(com.sun.management.OperatingSystemMXBean.class); + when(osMock.getProcessCpuLoad()).thenReturn(0.5); // >= 0 → Math.min branch → cpuUsage=50% try (MockedStatic mf = mockStatic(ManagementFactory.class)) { mf.when(ManagementFactory::getOperatingSystemMXBean).thenReturn(osMock); diff --git a/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceTest.java b/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceTest.java index 90e66ae..c6ca885 100644 --- a/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceTest.java +++ b/src/test/java/dev/lions/unionflow/server/service/AlertMonitoringServiceTest.java @@ -215,14 +215,14 @@ class AlertMonitoringServiceTest { @Test @DisplayName("monitorSystemMetrics - CPU enabled, duration elapsed, no recent CPU alert => alert is created") void monitorSystemMetrics_cpuEnabled_durationElapsed_noRecentAlert_createsCpuAlert() { - // Reset state: threshold=100 forces CPU (0.0 on Windows) to be below threshold → lastCpuHighTime = null + // Reset state: threshold=100 forces CPU (always <= 100% via Math.min cap) to be below threshold → lastCpuHighTime = null AlertConfiguration resetConfig = buildDisabledConfig(); resetConfig.setCpuHighAlertEnabled(true); resetConfig.setCpuThresholdPercent(100); when(alertConfigurationRepository.getConfiguration()).thenReturn(resetConfig); alertMonitoringService.monitorSystemMetrics(); - // threshold=-1: any CPU (even 0.0 on Windows where loadAvg=-1) satisfies 0.0 > -1 + // threshold=-1: any CPU (even 0.0 when getProcessCpuLoad() returns -1 or 0 on idle JVM) satisfies 0.0 > -1 AlertConfiguration config = buildDisabledConfig(); config.setCpuHighAlertEnabled(true); config.setCpuThresholdPercent(-1); // guaranteed: 0.0 > -1 is always true @@ -488,7 +488,7 @@ class AlertMonitoringServiceTest { when(alertConfigurationRepository.getConfiguration()).thenReturn(resetConfig); alertMonitoringService.monitorSystemMetrics(); - // threshold=-1: 0.0 > -1 is always true (even on Windows with cpuUsage=0.0) + // threshold=-1: 0.0 > -1 is always true (getProcessCpuLoad() returns 0.0..1.0 or -1; cpuUsage clamped to [0, 100]) AlertConfiguration config = buildDisabledConfig(); config.setCpuHighAlertEnabled(true); config.setCpuThresholdPercent(-1); @@ -556,7 +556,7 @@ class AlertMonitoringServiceTest { when(alertConfigurationRepository.getConfiguration()).thenReturn(resetConfig); alertMonitoringService.monitorSystemMetrics(); - // threshold=-1: 0.0 > -1 is always true (even on Windows with cpuUsage=0.0) + // threshold=-1: 0.0 > -1 is always true (getProcessCpuLoad() returns 0.0..1.0 or -1; cpuUsage clamped to [0, 100]) AlertConfiguration config = buildDisabledConfig(); config.setCpuHighAlertEnabled(true); config.setCpuThresholdPercent(-1);