diff --git a/engine/src/main/java/com/ibm/engine/detection/DetectionStore.java b/engine/src/main/java/com/ibm/engine/detection/DetectionStore.java index 7cac4e199..432fa6b9d 100644 --- a/engine/src/main/java/com/ibm/engine/detection/DetectionStore.java +++ b/engine/src/main/java/com/ibm/engine/detection/DetectionStore.java @@ -259,6 +259,15 @@ public void analyse(@Nonnull final T tree) { this.statusReporting.emitFinding(); } + public void release() { + for (DetectionStore child : getChildren()) { + child.release(); + } + detectionValues.clear(); + children.clear(); + actionValue = null; + } + @SuppressWarnings("java:S3776") public void onReceivingNewDetection(@Nonnull IDetection detection) { if (detection instanceof MethodDetection methodDetection) { @@ -293,34 +302,32 @@ public void onReceivingNewDetection(@Nonnull IDetection detection) { } else if (detection instanceof ValueDetection valueDetection) { final DetectableParameter detectableParameter = valueDetection.detectableParameter(); + Optional> emittedValue = + valueDetection.toValue(valueDetection.detectableParameter().getiValueFactory()); final Optional positionMove = detectableParameter.getShouldBeMovedUnder(); // Check if the parameter should be moved under if (positionMove.isPresent()) { final int id = positionMove.get(); // Get the iValue to be detected and store it in a variable - valueDetection - .toValue(valueDetection.detectableParameter().getiValueFactory()) - .ifPresent( - iValue -> { - // Create a detection store with the given parameters - DetectionStore detectionStore = - new DetectionStore<>( - level + 1, - detectionRule, - scanContext, - handler, - statusReporting); - // Compute the detection values for the given id - addValue(detectionStore, id, iValue); - // Attach the detection store to the given id - this.attach(id, detectionStore); - }); + emittedValue.ifPresent( + iValue -> { + // Create a detection store with the given parameters + DetectionStore detectionStore = + new DetectionStore<>( + level + 1, + detectionRule, + scanContext, + handler, + statusReporting); + // Compute the detection values for the given id + addValue(detectionStore, id, iValue); + // Attach the detection store to the given id + this.attach(id, detectionStore); + }); } else { - valueDetection - .toValue(valueDetection.detectableParameter().getiValueFactory()) - .ifPresent( - iValue -> addValue(this, detectableParameter.getIndex(), iValue)); + emittedValue.ifPresent( + iValue -> addValue(this, detectableParameter.getIndex(), iValue)); } // follow method parameter related detection rules @@ -392,6 +399,7 @@ public void onDetectedDependingParameter( } public void onNewHookRegistration(@Nonnull IHook hook) { + statusReporting.onDeferredHookRegistration(); handler.subscribeToHookDetectionObservable(hook, this); } diff --git a/engine/src/main/java/com/ibm/engine/executive/DetectionExecutive.java b/engine/src/main/java/com/ibm/engine/executive/DetectionExecutive.java index b7cf5f760..e1e2ff2ea 100644 --- a/engine/src/main/java/com/ibm/engine/executive/DetectionExecutive.java +++ b/engine/src/main/java/com/ibm/engine/executive/DetectionExecutive.java @@ -30,15 +30,18 @@ import java.util.Collection; import java.util.List; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public class DetectionExecutive implements IStatusReporting, IDomainEvent> { @Nonnull private final List>> listeners = new ArrayList<>(); @Nonnull private final DetectionStore rootDetectionStore; - @Nonnull private final T tree; + @Nullable private T tree; private int expectedRuleVisits; private int visitedRules = 0; + private boolean deferredHookRegistered = false; + private boolean released = false; public DetectionExecutive( @Nonnull final T tree, @@ -52,7 +55,11 @@ public DetectionExecutive( } public void start() { - this.rootDetectionStore.analyse(tree); + try { + this.rootDetectionStore.analyse(tree); + } finally { + this.tree = null; + } } @Override @@ -65,12 +72,18 @@ public void emitFinding(@Nonnull final DetectionStore rootDetectionS if (this.expectedRuleVisits != this.visitedRules) { return; } - getRootStoresWithValue(rootDetectionStore) - .forEach( - store -> { - final Finding finding = new Finding<>(store); - this.notify(finding); - }); + try { + getRootStoresWithValue(rootDetectionStore) + .forEach( + store -> { + final Finding finding = new Finding<>(store); + this.notify(finding); + }); + } finally { + if (!deferredHookRegistered) { + releaseResources(); + } + } } @Override @@ -83,6 +96,23 @@ public void addAdditionalExpectedRuleVisits(int number) { this.expectedRuleVisits += number; } + @Override + public void onDeferredHookRegistration() { + this.deferredHookRegistered = true; + } + + public boolean hasDeferredHooks() { + return deferredHookRegistered; + } + + public boolean isReleased() { + return released; + } + + public void releaseDeferredResources() { + releaseResources(); + } + @Nonnull private List> getRootStoresWithValue( @Nonnull DetectionStore detectionStore) { @@ -110,4 +140,14 @@ public void unsubscribe(@Nonnull IObserver> listener) { public void notify(@Nonnull Finding finding) { this.listeners.forEach(listener -> listener.update(finding)); } + + private void releaseResources() { + if (released) { + return; + } + released = true; + rootDetectionStore.release(); + listeners.clear(); + tree = null; + } } diff --git a/engine/src/main/java/com/ibm/engine/executive/IStatusReporting.java b/engine/src/main/java/com/ibm/engine/executive/IStatusReporting.java index d699e2786..b61e4a041 100644 --- a/engine/src/main/java/com/ibm/engine/executive/IStatusReporting.java +++ b/engine/src/main/java/com/ibm/engine/executive/IStatusReporting.java @@ -30,4 +30,8 @@ public interface IStatusReporting { void incrementVisitedRules(); void addAdditionalExpectedRuleVisits(int number); + + default void onDeferredHookRegistration() { + // Optional lifecycle callback for deferred hook-based findings. + } } diff --git a/engine/src/test/java/com/ibm/engine/detection/DetectionStoreReleaseTest.java b/engine/src/test/java/com/ibm/engine/detection/DetectionStoreReleaseTest.java new file mode 100644 index 000000000..56128f26f --- /dev/null +++ b/engine/src/test/java/com/ibm/engine/detection/DetectionStoreReleaseTest.java @@ -0,0 +1,148 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2026 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.engine.detection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; + +import com.ibm.engine.executive.IStatusReporting; +import com.ibm.engine.language.IScanContext; +import com.ibm.engine.model.IAction; +import com.ibm.engine.model.IValue; +import com.ibm.engine.rule.IDetectionRule; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DetectionStoreReleaseTest { + + private IDetectionRule detectionRule; + private IScanContext scanContext; + private Handler handler; + private IStatusReporting statusReporting; + private IValue mockValue; + private IAction mockAction; + + private DetectionStore store; + + @SuppressWarnings("unchecked") + @BeforeEach + void setUp() { + detectionRule = mock(IDetectionRule.class); + scanContext = mock(IScanContext.class); + handler = mock(Handler.class); + statusReporting = mock(IStatusReporting.class); + mockValue = mock(IValue.class); + mockAction = mock(IAction.class); + store = new DetectionStore<>(0, detectionRule, scanContext, handler, statusReporting); + } + + @Test + void release_clearsDetectionValues() { + store.detectionValues.put(0, new ArrayList<>(List.of(mockValue))); + assertThat(store.getDetectionValues()).isNotEmpty(); + + store.release(); + + assertThat(store.getDetectionValues()).isEmpty(); + } + + @Test + void release_clearsChildren() { + DetectionStore child = + new DetectionStore<>(1, detectionRule, scanContext, handler, statusReporting); + store.children.put(0, new ArrayList<>(List.of(child))); + assertThat(store.getChildren()).isNotEmpty(); + + store.release(); + + assertThat(store.getChildren()).isEmpty(); + } + + @Test + void release_clearsActionValue() { + store.actionValue = mockAction; + assertThat(store.getActionValue()).isPresent(); + + store.release(); + + assertThat(store.getActionValue()).isEmpty(); + } + + @Test + void release_onEmptyStore_doesNotThrow() { + assertThatCode(() -> store.release()).doesNotThrowAnyException(); + assertThat(store.getDetectionValues()).isEmpty(); + assertThat(store.getChildren()).isEmpty(); + } + + @Test + void release_isRecursive_clearsChildDetectionValues() { + DetectionStore child = + new DetectionStore<>(1, detectionRule, scanContext, handler, statusReporting); + child.detectionValues.put(0, new ArrayList<>(List.of(mockValue))); + store.children.put(0, new ArrayList<>(List.of(child))); + + store.release(); + + assertThat(store.getChildren()).isEmpty(); + assertThat(child.getDetectionValues()).isEmpty(); + } + + @Test + void release_isRecursive_clearsDeepNestedChildren() { + DetectionStore child = + new DetectionStore<>(1, detectionRule, scanContext, handler, statusReporting); + DetectionStore grandchild = + new DetectionStore<>(2, detectionRule, scanContext, handler, statusReporting); + + grandchild.detectionValues.put(0, new ArrayList<>(List.of(mockValue))); + child.children.put(0, new ArrayList<>(List.of(grandchild))); + store.children.put(0, new ArrayList<>(List.of(child))); + + store.release(); + + assertThat(store.getChildren()).isEmpty(); + assertThat(child.getChildren()).isEmpty(); + assertThat(grandchild.getDetectionValues()).isEmpty(); + } + + @Test + void release_multipleValuesAndChildren_allCleared() { + store.detectionValues.put(0, new ArrayList<>(List.of(mockValue))); + store.detectionValues.put(1, new ArrayList<>(List.of(mockValue))); + store.actionValue = mockAction; + + DetectionStore child1 = + new DetectionStore<>(1, detectionRule, scanContext, handler, statusReporting); + DetectionStore child2 = + new DetectionStore<>(1, detectionRule, scanContext, handler, statusReporting); + store.children.put(0, new ArrayList<>(List.of(child1))); + store.children.put(1, new ArrayList<>(List.of(child2))); + + store.release(); + + assertThat(store.getDetectionValues()).isEmpty(); + assertThat(store.getChildren()).isEmpty(); + assertThat(store.getActionValue()).isEmpty(); + } +} diff --git a/engine/src/test/java/com/ibm/engine/executive/DetectionExecutiveLifecycleTest.java b/engine/src/test/java/com/ibm/engine/executive/DetectionExecutiveLifecycleTest.java new file mode 100644 index 000000000..148a10080 --- /dev/null +++ b/engine/src/test/java/com/ibm/engine/executive/DetectionExecutiveLifecycleTest.java @@ -0,0 +1,106 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2026 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.engine.executive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; + +import com.ibm.engine.detection.Handler; +import com.ibm.engine.language.IScanContext; +import com.ibm.engine.rule.IDetectionRule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DetectionExecutiveLifecycleTest { + + private DetectionExecutive executive; + + @SuppressWarnings("unchecked") + @BeforeEach + void setUp() { + IDetectionRule detectionRule = mock(IDetectionRule.class); + IScanContext scanContext = mock(IScanContext.class); + Handler handler = mock(Handler.class); + executive = new DetectionExecutive<>(new Object(), detectionRule, scanContext, handler); + } + + @Test + void hasDeferredHooks_falseByDefault() { + assertThat(executive.hasDeferredHooks()).isFalse(); + } + + @Test + void onDeferredHookRegistration_setsDeferredFlagToTrue() { + executive.onDeferredHookRegistration(); + + assertThat(executive.hasDeferredHooks()).isTrue(); + } + + @Test + void onDeferredHookRegistration_calledMultipleTimes_remainsTrue() { + executive.onDeferredHookRegistration(); + executive.onDeferredHookRegistration(); + + assertThat(executive.hasDeferredHooks()).isTrue(); + } + + @Test + void isReleased_falseInitially() { + assertThat(executive.isReleased()).isFalse(); + } + + @Test + void releaseDeferredResources_setsIsReleasedTrue() { + executive.releaseDeferredResources(); + + assertThat(executive.isReleased()).isTrue(); + } + + @Test + void releaseDeferredResources_isIdempotent() { + assertThatCode( + () -> { + executive.releaseDeferredResources(); + executive.releaseDeferredResources(); + }) + .doesNotThrowAnyException(); + + assertThat(executive.isReleased()).isTrue(); + } + + @Test + void hasDeferredHooks_doesNotImplyReleased() { + executive.onDeferredHookRegistration(); + + assertThat(executive.hasDeferredHooks()).isTrue(); + assertThat(executive.isReleased()).isFalse(); + } + + @Test + void releaseDeferredResources_doesNotClearDeferredFlag() { + executive.onDeferredHookRegistration(); + executive.releaseDeferredResources(); + + // released, but the deferred flag records what happened during the executive's lifetime + assertThat(executive.hasDeferredHooks()).isTrue(); + assertThat(executive.isReleased()).isTrue(); + } +} diff --git a/java/src/main/java/com/ibm/plugin/JavaAggregator.java b/java/src/main/java/com/ibm/plugin/JavaAggregator.java index 0cfbdd973..962750d62 100644 --- a/java/src/main/java/com/ibm/plugin/JavaAggregator.java +++ b/java/src/main/java/com/ibm/plugin/JavaAggregator.java @@ -61,5 +61,10 @@ public static List getDetectedNodes() { public static void reset() { javaLanguageSupport = LanguageSupporter.javaLanguageSupporter(); detectedNodes = new ArrayList<>(); + JavaScanMemoryLogger.reset(); + } + + public static void resetLanguageSupport() { + javaLanguageSupport = LanguageSupporter.javaLanguageSupporter(); } } diff --git a/java/src/main/java/com/ibm/plugin/JavaScanMemoryLogger.java b/java/src/main/java/com/ibm/plugin/JavaScanMemoryLogger.java new file mode 100644 index 000000000..e3088ddcd --- /dev/null +++ b/java/src/main/java/com/ibm/plugin/JavaScanMemoryLogger.java @@ -0,0 +1,90 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2026 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin; + +import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; + +public final class JavaScanMemoryLogger { + public record Snapshot( + long javaFilesProcessed, + long successfulFileStateResets, + long usedMb, + long totalMb, + long maxMb, + long peakUsedMb) {} + + private static final AtomicLong JAVA_FILES_PROCESSED = new AtomicLong(0); + private static final AtomicLong SUCCESSFUL_FILE_STATE_RESETS = new AtomicLong(0); + private static final AtomicLong PEAK_USED_MB = new AtomicLong(0); + + private JavaScanMemoryLogger() { + // utility class + } + + public static void logFileProgress(Logger logger, long every) { + Runtime runtime = Runtime.getRuntime(); + long usedMb = usedMb(runtime); + long totalMb = runtime.totalMemory() / (1024 * 1024); + long maxMb = runtime.maxMemory() / (1024 * 1024); + long javaFilesProcessed = JAVA_FILES_PROCESSED.incrementAndGet(); + SUCCESSFUL_FILE_STATE_RESETS.incrementAndGet(); + long peakUsedMb = updatePeak(usedMb); + + if (every <= 1 || javaFilesProcessed % every == 0) { + logger.info( + "CBOM memory progress: javaFiles={}, used={} MB, total={} MB, max={} MB, peak={} MB", + javaFilesProcessed, + usedMb, + totalMb, + maxMb, + peakUsedMb); + } + } + + public static Snapshot snapshot() { + Runtime runtime = Runtime.getRuntime(); + long usedMb = usedMb(runtime); + long javaFilesProcessed = JAVA_FILES_PROCESSED.get(); + long successfulFileStateResets = SUCCESSFUL_FILE_STATE_RESETS.get(); + long peakUsedMb = updatePeak(usedMb); + return new Snapshot( + javaFilesProcessed, + successfulFileStateResets, + usedMb, + runtime.totalMemory() / (1024 * 1024), + runtime.maxMemory() / (1024 * 1024), + peakUsedMb); + } + + public static void reset() { + JAVA_FILES_PROCESSED.set(0); + SUCCESSFUL_FILE_STATE_RESETS.set(0); + PEAK_USED_MB.set(0); + } + + private static long usedMb(Runtime runtime) { + return (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024); + } + + private static long updatePeak(long usedMb) { + return PEAK_USED_MB.updateAndGet(previousPeak -> Math.max(previousPeak, usedMb)); + } +} diff --git a/java/src/main/java/com/ibm/plugin/rules/detection/JavaBaseDetectionRule.java b/java/src/main/java/com/ibm/plugin/rules/detection/JavaBaseDetectionRule.java index 9389c3f22..4a8302c43 100644 --- a/java/src/main/java/com/ibm/plugin/rules/detection/JavaBaseDetectionRule.java +++ b/java/src/main/java/com/ibm/plugin/rules/detection/JavaBaseDetectionRule.java @@ -27,13 +27,17 @@ import com.ibm.mapper.model.INode; import com.ibm.mapper.reorganizer.IReorganizerRule; import com.ibm.plugin.JavaAggregator; +import com.ibm.plugin.JavaScanMemoryLogger; import com.ibm.plugin.translation.JavaTranslationProcess; import com.ibm.plugin.translation.reorganizer.JavaReorganizerRules; import com.ibm.rules.IReportableDetectionRule; import com.ibm.rules.issue.Issue; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.plugins.java.api.JavaFileScannerContext; @@ -43,8 +47,15 @@ public abstract class JavaBaseDetectionRule extends IssuableSubscriptionVisitor implements IObserver>, IReportableDetectionRule { + private static final Logger LOGGER = LoggerFactory.getLogger(JavaBaseDetectionRule.class); + private static final long FILE_PROGRESS_LOG_EVERY = 2500L; private final boolean isInventory; + + @Nonnull + private final List> + deferredDetectionExecutives = new ArrayList<>(); + @Nonnull protected final JavaTranslationProcess javaTranslationProcess; @Nonnull protected final List> detectionRules; @@ -89,6 +100,9 @@ public void visitNode(@Nonnull Tree tree) { tree, rule, new JavaScanContext(this.context)); detectionExecutive.subscribe(this); detectionExecutive.start(); + if (detectionExecutive.hasDeferredHooks() && !detectionExecutive.isReleased()) { + deferredDetectionExecutives.add(detectionExecutive); + } }); } @@ -112,6 +126,19 @@ public void update(@Nonnull Finding> report( @@ -119,4 +146,9 @@ public List> report( // override by higher level rule, to report an issue return Collections.emptyList(); } + + private void releaseDeferredExecutives() { + deferredDetectionExecutives.forEach(DetectionExecutive::releaseDeferredResources); + deferredDetectionExecutives.clear(); + } } diff --git a/java/src/test/java/com/ibm/plugin/JavaScanMemoryLoggerTest.java b/java/src/test/java/com/ibm/plugin/JavaScanMemoryLoggerTest.java new file mode 100644 index 000000000..0e830fa0e --- /dev/null +++ b/java/src/test/java/com/ibm/plugin/JavaScanMemoryLoggerTest.java @@ -0,0 +1,131 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2026 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; + +class JavaScanMemoryLoggerTest { + + private Logger logger; + + @BeforeEach + void setUp() { + logger = mock(Logger.class); + JavaScanMemoryLogger.reset(); + } + + @Test + void logFileProgress_incrementsJavaFilesProcessed() { + JavaScanMemoryLogger.logFileProgress(logger, 1); + JavaScanMemoryLogger.logFileProgress(logger, 1); + + assertThat(JavaScanMemoryLogger.snapshot().javaFilesProcessed()).isEqualTo(2); + } + + @Test + void logFileProgress_logsEveryFile_whenEveryIsOne() { + JavaScanMemoryLogger.logFileProgress(logger, 1); + JavaScanMemoryLogger.logFileProgress(logger, 1); + JavaScanMemoryLogger.logFileProgress(logger, 1); + + verify(logger, times(3)).info(anyString(), any(), any(), any(), any(), any()); + } + + @Test + void logFileProgress_throttlesLogging_whenEveryIsGreaterThanOne() { + // With every=3, only the 3rd call should log + JavaScanMemoryLogger.logFileProgress(logger, 3); + JavaScanMemoryLogger.logFileProgress(logger, 3); + JavaScanMemoryLogger.logFileProgress(logger, 3); + + verify(logger, times(1)).info(anyString(), any(), any(), any(), any(), any()); + } + + @Test + void logFileProgress_doesNotLog_whenCountDoesNotMatchInterval() { + // With every=5, calls 1 and 2 should not log + JavaScanMemoryLogger.logFileProgress(logger, 5); + JavaScanMemoryLogger.logFileProgress(logger, 5); + + verify(logger, never()).info(anyString(), any(), any(), any(), any(), any()); + } + + @Test + void snapshot_returnsNonNegativeMemoryValues() { + JavaScanMemoryLogger.Snapshot snapshot = JavaScanMemoryLogger.snapshot(); + + assertThat(snapshot.usedMb()).isGreaterThanOrEqualTo(0); + assertThat(snapshot.totalMb()).isGreaterThan(0); + assertThat(snapshot.maxMb()).isGreaterThan(0); + assertThat(snapshot.peakUsedMb()).isGreaterThanOrEqualTo(0); + } + + @Test + void snapshot_javaFilesProcessed_reflectsLogProgressCalls() { + JavaScanMemoryLogger.logFileProgress(logger, 1); + JavaScanMemoryLogger.logFileProgress(logger, 1); + JavaScanMemoryLogger.logFileProgress(logger, 1); + + assertThat(JavaScanMemoryLogger.snapshot().javaFilesProcessed()).isEqualTo(3); + } + + @Test + void snapshot_peakUsedMb_isAtLeastCurrentUsedMb() { + JavaScanMemoryLogger.logFileProgress(logger, 1); + JavaScanMemoryLogger.Snapshot snapshot = JavaScanMemoryLogger.snapshot(); + + assertThat(snapshot.peakUsedMb()).isGreaterThanOrEqualTo(snapshot.usedMb()); + } + + @Test + void reset_clearsAllCounters() { + JavaScanMemoryLogger.logFileProgress(logger, 1); + JavaScanMemoryLogger.logFileProgress(logger, 1); + + JavaScanMemoryLogger.reset(); + JavaScanMemoryLogger.Snapshot snapshot = JavaScanMemoryLogger.snapshot(); + + // File counters are cleared to zero by reset() + assertThat(snapshot.javaFilesProcessed()).isZero(); + assertThat(snapshot.successfulFileStateResets()).isZero(); + // peakUsedMb is not checked here: snapshot() re-samples live JVM memory and + // immediately updates the peak, so it cannot be zero after a reset() + snapshot() call. + } + + @Test + void reset_isIdempotent() { + JavaScanMemoryLogger.logFileProgress(logger, 1); + + JavaScanMemoryLogger.reset(); + JavaScanMemoryLogger.reset(); + + assertThat(JavaScanMemoryLogger.snapshot().javaFilesProcessed()).isZero(); + } +} diff --git a/sonar-cryptography-plugin/src/main/java/com/ibm/plugin/OutputFileJob.java b/sonar-cryptography-plugin/src/main/java/com/ibm/plugin/OutputFileJob.java index ba8eaa1de..640ea5f8b 100644 --- a/sonar-cryptography-plugin/src/main/java/com/ibm/plugin/OutputFileJob.java +++ b/sonar-cryptography-plugin/src/main/java/com/ibm/plugin/OutputFileJob.java @@ -46,6 +46,16 @@ public void execute(PostJobContext postJobContext) { final File cbom = new File(cbomFilename + ".json"); scannerManager.getOutputFile().saveTo(cbom); LOGGER.info("CBOM was successfully generated '{}'.", cbom.getAbsolutePath()); + + JavaScanMemoryLogger.Snapshot javaScan = JavaScanMemoryLogger.snapshot(); + LOGGER.info( + "CBOM summary: javaFiles={}, used={} MB, total={} MB, max={} MB, peak={} MB", + javaScan.javaFilesProcessed(), + javaScan.usedMb(), + javaScan.totalMb(), + javaScan.maxMb(), + javaScan.peakUsedMb()); + scannerManager.getStatistics().print(LOGGER::info); scannerManager.reset(); }