diff --git a/src/main/java/de/rub/nds/scanner/core/execution/ScannerThreadPoolExecutor.java b/src/main/java/de/rub/nds/scanner/core/execution/ScannerThreadPoolExecutor.java index 4e7c408..f662ccf 100644 --- a/src/main/java/de/rub/nds/scanner/core/execution/ScannerThreadPoolExecutor.java +++ b/src/main/java/de/rub/nds/scanner/core/execution/ScannerThreadPoolExecutor.java @@ -8,6 +8,9 @@ */ package de.rub.nds.scanner.core.execution; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionHandler; @@ -15,14 +18,13 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * Extends {@link ThreadPoolExecutor} with its own afterExecute function. A * ScannerThreadPoolExecutor hold a semaphore which is released each time a Thread finished - * executing. + * executing or is aborted on timeout. */ public class ScannerThreadPoolExecutor extends ScheduledThreadPoolExecutor { @@ -35,6 +37,8 @@ public class ScannerThreadPoolExecutor extends ScheduledThreadPoolExecutor { /** The time after which tasks are automatically cancelled */ private final long timeout; + private final Timer timer; + /** * Call super and assign the semaphore * @@ -50,6 +54,7 @@ public ScannerThreadPoolExecutor( this.timeout = timeout; this.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); this.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + this.timer = new Timer(); } /** @@ -84,6 +89,7 @@ public Future submit(Runnable task) { * @param result the result to return when the task completes * @return a Future representing pending completion of the task */ + @Override public Future submit(Runnable task, T result) { Future future = super.submit(task, result); cancelFuture(future); @@ -98,28 +104,48 @@ public Future submit(Runnable task, T result) { * @param task the task to submit * @return a Future representing pending completion of the task */ + @Override public Future submit(Callable task) { Future future = super.submit(task); cancelFuture(future); return future; } + @Override + public void shutdown() { + super.shutdown(); + timer.cancel(); + } + + @Override + public List shutdownNow() { + timer.cancel(); + return super.shutdownNow(); + } + + @Override + public void close() { + super.close(); + timer.cancel(); + } + private void cancelFuture(Future future) { - this.schedule( - () -> { - if (!future.isDone()) { - future.cancel(true); - if (future.isCancelled()) { - LOGGER.error("Killed task {}", future); + timer.schedule( + new TimerTask() { + @Override + public void run() { + if (!future.isDone()) { + future.cancel(true); + if (future.isCancelled()) { + LOGGER.error("Killed task {}", future); + } else { + LOGGER.error("Could not kill task {}", future); + } } else { - LOGGER.error("Could not kill task {}", future); + LOGGER.debug("Future already done! {}", future); } - } else { - LOGGER.debug("Future already done! {}", future); } - semaphore.release(); }, - timeout, - TimeUnit.MILLISECONDS); + timeout); } } diff --git a/src/main/java/de/rub/nds/scanner/core/execution/ThreadedScanJobExecutor.java b/src/main/java/de/rub/nds/scanner/core/execution/ThreadedScanJobExecutor.java index 0941027..c254a1d 100644 --- a/src/main/java/de/rub/nds/scanner/core/execution/ThreadedScanJobExecutor.java +++ b/src/main/java/de/rub/nds/scanner/core/execution/ThreadedScanJobExecutor.java @@ -20,10 +20,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.Semaphore; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -57,7 +54,7 @@ public class ThreadedScanJobExecutor< private final ThreadPoolExecutor executor; // Used for waiting for Threads in the ThreadPoolExecutor - private final Semaphore semaphore = new Semaphore(0); + private final Semaphore semaphore; private int probeCount; private int finishedProbes = 0; @@ -76,7 +73,8 @@ public ThreadedScanJobExecutor( int threadCount, String prefix) { long probeTimeout = config.getProbeTimeout(); - executor = + this.semaphore = new Semaphore(0); + this.executor = new ScannerThreadPoolExecutor( threadCount, new NamedThreadFactory(prefix), semaphore, probeTimeout); this.config = config; @@ -90,11 +88,14 @@ public ThreadedScanJobExecutor( * @param config the executor configuration * @param scanJob the scan job containing probes to execute * @param executor the thread pool executor to use + * @param semaphore the semaphore released by the executor when a probe finishes */ public ThreadedScanJobExecutor( ExecutorConfig config, ScanJob scanJob, - ThreadPoolExecutor executor) { + ThreadPoolExecutor executor, + Semaphore semaphore) { + this.semaphore = semaphore; this.executor = executor; this.config = config; this.scanJob = scanJob; diff --git a/src/test/java/de/rub/nds/scanner/core/execution/NamedThreadFactoryTest.java b/src/test/java/de/rub/nds/scanner/core/execution/NamedThreadFactoryTest.java new file mode 100644 index 0000000..bd2b5a9 --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/execution/NamedThreadFactoryTest.java @@ -0,0 +1,95 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.execution; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +public class NamedThreadFactoryTest { + + @Test + public void testNewThreadWithPrefix() { + String prefix = "TestThread"; + NamedThreadFactory factory = new NamedThreadFactory(prefix); + + Runnable runnable = () -> {}; + Thread thread = factory.newThread(runnable); + + assertNotNull(thread); + assertEquals(prefix + "-1", thread.getName()); + } + + @Test + public void testMultipleThreadsWithIncrementingNumbers() { + String prefix = "Worker"; + NamedThreadFactory factory = new NamedThreadFactory(prefix); + + Thread thread1 = factory.newThread(() -> {}); + Thread thread2 = factory.newThread(() -> {}); + Thread thread3 = factory.newThread(() -> {}); + + assertEquals(prefix + "-1", thread1.getName()); + assertEquals(prefix + "-2", thread2.getName()); + assertEquals(prefix + "-3", thread3.getName()); + } + + @Test + public void testThreadsAreNotDaemon() { + NamedThreadFactory factory = new NamedThreadFactory("Test"); + Thread thread = factory.newThread(() -> {}); + + // Default thread factory creates non-daemon threads + assertFalse(thread.isDaemon()); + } + + @Test + public void testThreadsWithDifferentPrefixes() { + NamedThreadFactory factory1 = new NamedThreadFactory("Factory1"); + NamedThreadFactory factory2 = new NamedThreadFactory("Factory2"); + + Thread thread1 = factory1.newThread(() -> {}); + Thread thread2 = factory2.newThread(() -> {}); + + assertEquals("Factory1-1", thread1.getName()); + assertEquals("Factory2-1", thread2.getName()); + } + + @Test + public void testThreadExecutesRunnable() throws InterruptedException { + NamedThreadFactory factory = new NamedThreadFactory("Executor"); + AtomicInteger counter = new AtomicInteger(0); + + Runnable runnable = counter::incrementAndGet; + + Thread thread = factory.newThread(runnable); + thread.start(); + thread.join(); + + assertEquals(1, counter.get()); + } + + @Test + public void testEmptyPrefix() { + NamedThreadFactory factory = new NamedThreadFactory(""); + Thread thread = factory.newThread(() -> {}); + + assertEquals("-1", thread.getName()); + } + + @Test + public void testSpecialCharactersInPrefix() { + String prefix = "Test-Thread_123@#$"; + NamedThreadFactory factory = new NamedThreadFactory(prefix); + Thread thread = factory.newThread(() -> {}); + + assertEquals(prefix + "-1", thread.getName()); + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/execution/ScanJobExecutorTest.java b/src/test/java/de/rub/nds/scanner/core/execution/ScanJobExecutorTest.java new file mode 100644 index 0000000..04c57e5 --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/execution/ScanJobExecutorTest.java @@ -0,0 +1,151 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.execution; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.scanner.core.report.ScanReport; +import java.lang.reflect.Modifier; +import org.junit.jupiter.api.Test; + +public class ScanJobExecutorTest { + + // Mock ScanReport for testing + static class TestReport extends ScanReport { + private boolean executed = false; + + @Override + public String getRemoteName() { + return "TestHost"; + } + + @Override + public void serializeToJson(java.io.OutputStream outputStream) { + // Simple implementation for testing + } + + public boolean isExecuted() { + return executed; + } + + public void setExecuted(boolean executed) { + this.executed = executed; + } + } + + // Test implementation of ScanJobExecutor + static class TestScanJobExecutor extends ScanJobExecutor { + private boolean shutdownCalled = false; + private int executeCallCount = 0; + + @Override + public void execute(TestReport report) { + executeCallCount++; + report.setExecuted(true); + } + + @Override + public void shutdown() { + shutdownCalled = true; + } + + public boolean isShutdownCalled() { + return shutdownCalled; + } + + public int getExecuteCallCount() { + return executeCallCount; + } + } + + // Test implementation that throws InterruptedException + static class InterruptingExecutor extends ScanJobExecutor { + @Override + public void execute(TestReport report) throws InterruptedException { + throw new InterruptedException("Test interruption"); + } + + @Override + public void shutdown() {} + } + + @Test + public void testExecuteMethod() { + TestScanJobExecutor executor = new TestScanJobExecutor(); + TestReport report = new TestReport(); + + assertFalse(report.isExecuted()); + executor.execute(report); + + assertTrue(report.isExecuted()); + assertEquals(1, executor.getExecuteCallCount()); + } + + @Test + public void testShutdownMethod() { + TestScanJobExecutor executor = new TestScanJobExecutor(); + + assertFalse(executor.isShutdownCalled()); + executor.shutdown(); + assertTrue(executor.isShutdownCalled()); + } + + @Test + public void testMultipleExecuteCalls() { + TestScanJobExecutor executor = new TestScanJobExecutor(); + TestReport report1 = new TestReport(); + TestReport report2 = new TestReport(); + TestReport report3 = new TestReport(); + + executor.execute(report1); + executor.execute(report2); + executor.execute(report3); + + assertEquals(3, executor.getExecuteCallCount()); + assertTrue(report1.isExecuted()); + assertTrue(report2.isExecuted()); + assertTrue(report3.isExecuted()); + } + + @Test + public void testExecuteThrowsInterruptedException() { + InterruptingExecutor executor = new InterruptingExecutor(); + TestReport report = new TestReport(); + + assertThrows(InterruptedException.class, () -> executor.execute(report)); + } + + @Test + public void testAbstractClass() { + // Verify that ScanJobExecutor is abstract and cannot be instantiated directly + assertTrue(Modifier.isAbstract(ScanJobExecutor.class.getModifiers())); + } + + @Test + public void testExecuteMethodSignature() throws NoSuchMethodException { + // Verify the execute method signature + var method = ScanJobExecutor.class.getDeclaredMethod("execute", ScanReport.class); + assertTrue(Modifier.isAbstract(method.getModifiers())); + assertEquals(void.class, method.getReturnType()); + + // Check that it declares InterruptedException + Class[] exceptionTypes = method.getExceptionTypes(); + assertEquals(1, exceptionTypes.length); + assertEquals(InterruptedException.class, exceptionTypes[0]); + } + + @Test + public void testShutdownMethodSignature() throws NoSuchMethodException { + // Verify the shutdown method signature + var method = ScanJobExecutor.class.getDeclaredMethod("shutdown"); + assertTrue(Modifier.isAbstract(method.getModifiers())); + assertEquals(void.class, method.getReturnType()); + assertEquals(0, method.getExceptionTypes().length); + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/execution/ScanJobTest.java b/src/test/java/de/rub/nds/scanner/core/execution/ScanJobTest.java new file mode 100644 index 0000000..821e962 --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/execution/ScanJobTest.java @@ -0,0 +1,192 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.execution; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.scanner.core.afterprobe.AfterProbe; +import de.rub.nds.scanner.core.probe.ProbeType; +import de.rub.nds.scanner.core.probe.ScannerProbe; +import de.rub.nds.scanner.core.probe.requirements.FulfilledRequirement; +import de.rub.nds.scanner.core.probe.requirements.Requirement; +import de.rub.nds.scanner.core.report.ScanReport; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class ScanJobTest { + + // Mock classes for testing + static class TestReport extends ScanReport { + @Override + public String getRemoteName() { + return "TestHost"; + } + + @Override + public void serializeToJson(OutputStream outputStream) { + // Simple implementation for testing + } + } + + static class TestState {} + + static class TestProbe extends ScannerProbe { + + TestProbe(String name) { + super(new TestProbeType(name)); + } + + @Override + public void executeTest() {} + + @Override + public Requirement getRequirements() { + return new FulfilledRequirement<>(); + } + + @Override + public void adjustConfig(TestReport report) {} + + @Override + protected void mergeData(TestReport report) {} + } + + static class TestAfterProbe extends AfterProbe { + private final String name; + + TestAfterProbe(String name) { + this.name = name; + } + + @Override + public void analyze(TestReport report) {} + + public String getName() { + return name; + } + } + + static class TestProbeType implements ProbeType { + private final String name; + + TestProbeType(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + } + + @Test + public void testConstructorWithEmptyLists() { + List probeList = new ArrayList<>(); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + assertNotNull(scanJob.getProbeList()); + assertNotNull(scanJob.getAfterList()); + assertTrue(scanJob.getProbeList().isEmpty()); + assertTrue(scanJob.getAfterList().isEmpty()); + } + + @Test + public void testConstructorWithNonEmptyLists() { + List probeList = new ArrayList<>(); + probeList.add(new TestProbe("probe1")); + probeList.add(new TestProbe("probe2")); + + List afterList = new ArrayList<>(); + afterList.add(new TestAfterProbe("after1")); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + assertEquals(2, scanJob.getProbeList().size()); + assertEquals(1, scanJob.getAfterList().size()); + } + + @Test + public void testGetProbeListReturnsCopy() { + List probeList = new ArrayList<>(); + TestProbe probe = new TestProbe("probe1"); + probeList.add(probe); + + ScanJob scanJob = + new ScanJob<>(probeList, new ArrayList<>()); + + List returnedList = scanJob.getProbeList(); + returnedList.add(new TestProbe("probe2")); + + // Original list should not be modified + assertEquals(1, scanJob.getProbeList().size()); + assertEquals(2, returnedList.size()); + } + + @Test + public void testGetAfterListReturnsCopy() { + List afterList = new ArrayList<>(); + TestAfterProbe afterProbe = new TestAfterProbe("after1"); + afterList.add(afterProbe); + + ScanJob scanJob = + new ScanJob<>(new ArrayList<>(), afterList); + + List returnedList = scanJob.getAfterList(); + returnedList.add(new TestAfterProbe("after2")); + + // Original list should not be modified + assertEquals(1, scanJob.getAfterList().size()); + assertEquals(2, returnedList.size()); + } + + @Test + public void testImmutabilityOfInternalLists() { + List probeList = new ArrayList<>(); + probeList.add(new TestProbe("probe1")); + + List afterList = new ArrayList<>(); + afterList.add(new TestAfterProbe("after1")); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + // Modify original lists + probeList.add(new TestProbe("probe2")); + afterList.add(new TestAfterProbe("after2")); + + // ScanJob should not be affected + assertEquals(1, scanJob.getProbeList().size()); + assertEquals(1, scanJob.getAfterList().size()); + } + + @Test + public void testPreservesListOrder() { + List probeList = new ArrayList<>(); + TestProbe probe1 = new TestProbe("probe1"); + TestProbe probe2 = new TestProbe("probe2"); + TestProbe probe3 = new TestProbe("probe3"); + probeList.add(probe1); + probeList.add(probe2); + probeList.add(probe3); + + ScanJob scanJob = + new ScanJob<>(probeList, new ArrayList<>()); + + List returnedList = scanJob.getProbeList(); + assertEquals("probe1", returnedList.get(0).getProbeName()); + assertEquals("probe2", returnedList.get(1).getProbeName()); + assertEquals("probe3", returnedList.get(2).getProbeName()); + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java b/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java new file mode 100644 index 0000000..aa6983d --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java @@ -0,0 +1,434 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.execution; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.scanner.core.afterprobe.AfterProbe; +import de.rub.nds.scanner.core.config.ExecutorConfig; +import de.rub.nds.scanner.core.guideline.Guideline; +import de.rub.nds.scanner.core.passive.StatsWriter; +import de.rub.nds.scanner.core.probe.ProbeType; +import de.rub.nds.scanner.core.probe.ScannerProbe; +import de.rub.nds.scanner.core.probe.requirements.FulfilledRequirement; +import de.rub.nds.scanner.core.probe.requirements.Requirement; +import de.rub.nds.scanner.core.probe.requirements.UnfulfillableRequirement; +import de.rub.nds.scanner.core.report.ScanReport; +import de.rub.nds.scanner.core.report.rating.RatingInfluencers; +import de.rub.nds.scanner.core.report.rating.Recommendations; +import de.rub.nds.scanner.core.report.rating.SiteReportRater; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class ScannerTest { + + @TempDir private File tempDir; + + private ExecutorConfig executorConfig; + + // Test implementations + static class TestProbeType implements ProbeType { + private final String name; + + TestProbeType(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + } + + static class TestState {} + + static class TestReport extends ScanReport { + private boolean prerequisitesFulfilled = true; + + @Override + public String getRemoteName() { + return "TestHost"; + } + + @Override + public void serializeToJson(OutputStream outputStream) { + // Simple implementation for testing + try { + outputStream.write("{\"remoteName\":\"TestHost\"}".getBytes()); + } catch (IOException e) { + // Ignore for testing + } + } + + public void setPrerequisitesFulfilled(boolean fulfilled) { + this.prerequisitesFulfilled = fulfilled; + } + + public boolean arePrerequisitesFulfilled() { + return prerequisitesFulfilled; + } + } + + static class TestProbe extends ScannerProbe { + private boolean executed = false; + private boolean canExecute = true; + + TestProbe(ProbeType type) { + super(type); + } + + @Override + public void executeTest() { + executed = true; + } + + @Override + public Requirement getRequirements() { + if (canExecute) { + return new FulfilledRequirement<>(); + } else { + return new UnfulfillableRequirement<>(); + } + } + + @Override + public void adjustConfig(TestReport report) {} + + @Override + protected void mergeData(TestReport report) {} + + public boolean isExecuted() { + return executed; + } + + public void setCanExecute(boolean canExecute) { + this.canExecute = canExecute; + } + } + + static class TestAfterProbe extends AfterProbe { + private boolean analyzed = false; + + @Override + public void analyze(TestReport report) { + analyzed = true; + } + + public boolean isAnalyzed() { + return analyzed; + } + } + + static class TestStatsWriter extends StatsWriter { + @Override + public void extract(TestState state) {} + } + + static class TestScanner extends Scanner { + private boolean fillProbesCalled = false; + private boolean onScanStartCalled = false; + private boolean onScanEndCalled = false; + private TestReport reportToReturn; + private boolean checkPrerequisites = true; + private SiteReportRater rater; + private List> guidelines = new ArrayList<>(); + + TestScanner(ExecutorConfig config) { + super(config); + } + + TestScanner( + ExecutorConfig config, List probeList, List afterList) { + super(config, probeList, afterList); + } + + @Override + public void close() { + // Implementation for AutoCloseable + } + + @Override + protected void fillProbeLists() { + fillProbesCalled = true; + } + + @Override + protected StatsWriter getDefaultProbeWriter() { + return new TestStatsWriter(); + } + + @Override + protected TestReport getEmptyReport() { + if (reportToReturn != null) { + return reportToReturn; + } + return new TestReport(); + } + + @Override + protected boolean checkScanPrerequisites(TestReport report) { + return checkPrerequisites && report.arePrerequisitesFulfilled(); + } + + @Override + protected void onScanStart() { + onScanStartCalled = true; + } + + @Override + protected void onScanEnd() { + onScanEndCalled = true; + } + + @Override + protected SiteReportRater getSiteReportRater() { + return rater; + } + + @Override + protected List> getGuidelines() { + return guidelines; + } + + public void setReportToReturn(TestReport report) { + this.reportToReturn = report; + } + + public void setCheckPrerequisites(boolean check) { + this.checkPrerequisites = check; + } + + public void setSiteReportRater(SiteReportRater rater) { + this.rater = rater; + } + + public void setGuidelines(List> guidelines) { + this.guidelines = guidelines; + } + + public boolean isFillProbesCalled() { + return fillProbesCalled; + } + + public boolean isOnScanStartCalled() { + return onScanStartCalled; + } + + public boolean isOnScanEndCalled() { + return onScanEndCalled; + } + } + + @BeforeEach + public void setUp() { + executorConfig = new ExecutorConfig(); + executorConfig.setParallelProbes(1); + executorConfig.setProbeTimeout(1000); + } + + @Test + public void testScanWithDefaultConstructor() { + try (TestScanner scanner = new TestScanner(executorConfig)) { + TestReport report = scanner.scan(); + + assertNotNull(report); + assertTrue(scanner.isFillProbesCalled()); + assertTrue(scanner.isOnScanStartCalled()); + assertTrue(scanner.isOnScanEndCalled()); + } + } + + @Test + public void testScanWithProbesConstructor() { + List probeList = new ArrayList<>(); + probeList.add(new TestProbe(new TestProbeType("probe1"))); + + List afterList = new ArrayList<>(); + afterList.add(new TestAfterProbe()); + + try (TestScanner scanner = new TestScanner(executorConfig, probeList, afterList)) { + TestReport report = scanner.scan(); + + assertNotNull(report); + assertFalse( + scanner.isFillProbesCalled()); // Should not be called when probes are provided + } + } + + @Test + public void testScanPrerequisitesNotFulfilled() { + try (TestScanner scanner = new TestScanner(executorConfig)) { + TestReport report = new TestReport(); + report.setPrerequisitesFulfilled(false); + scanner.setReportToReturn(report); + + TestReport result = scanner.scan(); + + assertSame(report, result); + assertTrue(scanner.isOnScanStartCalled()); + assertFalse(scanner.isOnScanEndCalled()); // Should not reach end if prerequisites fail + } + } + + @Test + public void testRegisterProbeForExecution() { + try (TestScanner scanner = new TestScanner(executorConfig)) { + TestProbe probe = new TestProbe(new TestProbeType("test")); + + scanner.registerProbeForExecution(probe); + } + // Since this adds to internal probe list, we can't directly verify + // But the test ensures no exceptions are thrown + } + + @Test + public void testRegisterProbeWithExecuteByDefault() { + try (TestScanner scanner = new TestScanner(executorConfig)) { + TestProbe probe = new TestProbe(new TestProbeType("test")); + + scanner.registerProbeForExecution(probe, false); + } + // Test with executeByDefault = false + } + + @Test + public void testRegisterProbeWithSpecificProbesConfig() { + executorConfig.setProbes(List.of(new TestProbeType("allowed"))); + try (TestScanner scanner = new TestScanner(executorConfig)) { + + TestProbe allowedProbe = new TestProbe(new TestProbeType("allowed")); + TestProbe notAllowedProbe = new TestProbe(new TestProbeType("notallowed")); + + scanner.registerProbeForExecution(allowedProbe); + scanner.registerProbeForExecution(notAllowedProbe); + } + } + + @Test + public void testRegisterProbeWithExcludedProbes() { + executorConfig.setExcludedProbes(List.of(new TestProbeType("excluded"))); + try (TestScanner scanner = new TestScanner(executorConfig)) { + + TestProbe excludedProbe = new TestProbe(new TestProbeType("excluded")); + scanner.registerProbeForExecution(excludedProbe); + } + } + + @Test + public void testRegisterAfterProbe() { + try (TestScanner scanner = new TestScanner(executorConfig)) { + TestAfterProbe afterProbe = new TestAfterProbe(); + + scanner.registerProbeForExecution(afterProbe); + } + } + + @Test + public void testScanWithSiteReportRater() { + TestReport report; + try (TestScanner scanner = new TestScanner(executorConfig)) { + RatingInfluencers influencers = new RatingInfluencers(new LinkedList<>()); + Recommendations recommendations = new Recommendations(new LinkedList<>()); + SiteReportRater rater = new SiteReportRater(influencers, recommendations); + scanner.setSiteReportRater(rater); + + report = scanner.scan(); + } + + assertNotNull(report.getScoreReport()); + } + + @Test + public void testScanWithGuidelines() { + TestReport report; + try (TestScanner scanner = new TestScanner(executorConfig)) { + + Guideline guideline = + new Guideline<>("TestGuideline", "http://example.com", new ArrayList<>()); + + scanner.setGuidelines(List.of(guideline)); + report = scanner.scan(); + } + + assertNotNull(report); + } + + @Test + public void testScanWithFileOutput() throws IOException { + File outputFile = new File(tempDir, "test-report.json"); + executorConfig.setOutputFile(outputFile.getAbsolutePath()); + + try (TestScanner scanner = new TestScanner(executorConfig)) { + TestReport report = scanner.scan(); + } + + assertTrue(outputFile.exists()); + String content = Files.readString(outputFile.toPath()); + assertFalse(content.isEmpty()); + } + + @Test + public void testScanWithInvalidOutputFile() { + File invalidFile = new File("/invalid/path/that/does/not/exist/report.json"); + executorConfig.setOutputFile(invalidFile.getAbsolutePath()); + + try (TestScanner scanner = new TestScanner(executorConfig)) { + assertThrows(RuntimeException.class, scanner::scan); + } + } + + @Test + public void testAutoCloseable() throws Exception { + TestScanner scanner = new TestScanner(executorConfig); + + // Test that Scanner implements AutoCloseable + assertInstanceOf(AutoCloseable.class, scanner); + + // Test close method + scanner.close(); + } + + @Test + public void testScanTimingMeasurement() { + TestReport report; + try (TestScanner scanner = new TestScanner(executorConfig)) { + report = scanner.scan(); + } + + assertTrue(report.getScanStartTime() > 0); + assertTrue(report.getScanEndTime() > 0); + assertTrue(report.getScanEndTime() >= report.getScanStartTime()); + } + + @Test + public void testInterruptedScan() throws InterruptedException { + Thread testThread; + try (TestScanner scanner = new TestScanner(executorConfig)) { + testThread = + new Thread( + () -> { + Thread.currentThread().interrupt(); + scanner.scan(); + }); + } + + testThread.start(); + testThread.join(); + + assertTrue(testThread.isInterrupted() || !testThread.isAlive()); + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/execution/ScannerThreadPoolExecutorTest.java b/src/test/java/de/rub/nds/scanner/core/execution/ScannerThreadPoolExecutorTest.java new file mode 100644 index 0000000..95050b0 --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/execution/ScannerThreadPoolExecutorTest.java @@ -0,0 +1,248 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.execution; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +public class ScannerThreadPoolExecutorTest { + + @Test + public void testBasicConstruction() { + Semaphore semaphore = new Semaphore(0); + ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(2, new NamedThreadFactory("Test"), semaphore, 5000); + + assertNotNull(executor); + assertEquals(2, executor.getCorePoolSize()); + assertFalse(executor.isShutdown()); + + executor.shutdown(); + } + + @Test + public void testSubmitRunnable() throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Test"), semaphore, 5000)) { + AtomicBoolean taskExecuted = new AtomicBoolean(false); + Future future = executor.submit(() -> taskExecuted.set(true)); + + // Wait for task completion + semaphore.acquire(); + + assertTrue(taskExecuted.get()); + assertTrue(future.isDone()); + + executor.shutdown(); + } + } + + @Test + public void testSubmitRunnableWithResult() throws InterruptedException, ExecutionException { + Semaphore semaphore = new Semaphore(0); + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Test"), semaphore, 5000)) { + String result = "TestResult"; + AtomicBoolean taskExecuted = new AtomicBoolean(false); + Future future = executor.submit(() -> taskExecuted.set(true), result); + + // Wait for task completion + semaphore.acquire(); + + assertTrue(taskExecuted.get()); + assertEquals(result, future.get()); + + executor.shutdown(); + } + } + + @Test + public void testSubmitCallable() throws InterruptedException, ExecutionException { + Semaphore semaphore = new Semaphore(0); + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Test"), semaphore, 5000)) { + Callable callable = () -> 42; + Future future = executor.submit(callable); + + // Wait for task completion + semaphore.acquire(); + + assertEquals(42, future.get()); + + executor.shutdown(); + } + } + + @Test + public void testSemaphoreReleasedAfterTaskCompletion() throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Test"), semaphore, 5000)) { + executor.submit( + () -> { + // Simple task + }); + + // Should be able to acquire, proving semaphore was released + assertTrue(semaphore.tryAcquire(1, TimeUnit.SECONDS)); + + executor.shutdown(); + } + } + + @Test + public void testSemaphoreReleasedOnException() throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Test"), semaphore, 5000)) { + executor.submit( + () -> { + throw new RuntimeException("Test exception"); + }); + + // Semaphore should still be released even when task throws exception + assertTrue(semaphore.tryAcquire(1, TimeUnit.SECONDS)); + + executor.shutdown(); + } + } + + @Test + public void testTaskTimeout() throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + // Set very short timeout + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Test"), semaphore, 100)) { + CountDownLatch taskStarted = new CountDownLatch(1); + AtomicBoolean taskCompleted = new AtomicBoolean(false); + + Future future = + executor.submit( + () -> { + taskStarted.countDown(); + try { + Thread.sleep(500); // Sleep longer than timeout + taskCompleted.set(true); + } catch (InterruptedException e) { + // Expected when task is cancelled + } + }); + + // Wait for task to start + taskStarted.await(); + + // Wait for timeout to trigger + Thread.sleep(200); + + assertTrue(future.isCancelled()); + assertFalse(taskCompleted.get()); + + // Semaphore should be released after timeout + assertTrue(semaphore.tryAcquire(1, TimeUnit.SECONDS)); + + executor.shutdown(); + } + } + + @Test + public void testMultipleTasks() throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(3, new NamedThreadFactory("Test"), semaphore, 5000)) { + AtomicInteger counter = new AtomicInteger(0); + int taskCount = 10; + + for (int i = 0; i < taskCount; i++) { + executor.submit(counter::incrementAndGet); + } + + // Wait for all tasks + for (int i = 0; i < taskCount; i++) { + semaphore.acquire(); + } + + assertEquals(taskCount, counter.get()); + + executor.shutdown(); + } + } + + @Test + public void testShutdownBehavior() throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Test"), semaphore, 5000)) { + AtomicBoolean taskExecuted = new AtomicBoolean(false); + executor.submit(() -> taskExecuted.set(true)); + + semaphore.acquire(); + assertTrue(taskExecuted.get()); + + executor.shutdown(); + assertTrue(executor.isShutdown()); + + // Should not accept new tasks after shutdown + assertThrows( + RejectedExecutionException.class, + () -> { + executor.submit(() -> {}); + }); + } + } + + @Test + public void testContinueExistingPeriodicTasksPolicy() { + Semaphore semaphore = new Semaphore(0); + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Test"), semaphore, 5000)) { + // These are set in constructor + assertFalse(executor.getContinueExistingPeriodicTasksAfterShutdownPolicy()); + assertFalse(executor.getExecuteExistingDelayedTasksAfterShutdownPolicy()); + + executor.shutdown(); + } + } + + @Test + public void testTaskAlreadyDoneBeforeTimeout() throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + try (ScannerThreadPoolExecutor executor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Test"), semaphore, 1000)) { + // Submit a quick task + Future future = + executor.submit( + () -> { + // Quick task that completes before timeout + }); + + // Wait for task completion + semaphore.acquire(); + assertTrue(future.isDone()); + + // Wait a bit to ensure timeout task runs + Thread.sleep(1100); + + // The timeout task should have run but found the future already done + // No exception should be thrown + + executor.shutdown(); + } + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/execution/ThreadedScanJobExecutorTest.java b/src/test/java/de/rub/nds/scanner/core/execution/ThreadedScanJobExecutorTest.java new file mode 100644 index 0000000..59d5a05 --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/execution/ThreadedScanJobExecutorTest.java @@ -0,0 +1,494 @@ +/* + * Scanner Core - A Modular Framework for Probe Definition, Execution, and Result Analysis. + * + * Copyright 2017-2023 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.scanner.core.execution; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.scanner.core.afterprobe.AfterProbe; +import de.rub.nds.scanner.core.config.ExecutorConfig; +import de.rub.nds.scanner.core.passive.ExtractedValueContainer; +import de.rub.nds.scanner.core.passive.StatsWriter; +import de.rub.nds.scanner.core.passive.TrackableValue; +import de.rub.nds.scanner.core.probe.ProbeType; +import de.rub.nds.scanner.core.probe.ScannerProbe; +import de.rub.nds.scanner.core.probe.requirements.FulfilledRequirement; +import de.rub.nds.scanner.core.probe.requirements.Requirement; +import de.rub.nds.scanner.core.probe.requirements.UnfulfillableRequirement; +import de.rub.nds.scanner.core.report.ScanReport; +import java.beans.PropertyChangeEvent; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadPoolExecutor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ThreadedScanJobExecutorTest { + + private ExecutorConfig executorConfig; + + // Test implementations + static class TestProbeType implements ProbeType { + private final String name; + + TestProbeType(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TestProbeType) { + return name.equals(((TestProbeType) obj).name); + } + return false; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + } + + static class TestState {} + + static class TestReport extends ScanReport { + private final Set executedProbeTypes = new HashSet<>(); + private final Set unexecutedProbeTypes = new HashSet<>(); + private final Map> extractedContainers = + new HashMap<>(); + + @Override + public String getRemoteName() { + return "TestHost"; + } + + @Override + public void serializeToJson(OutputStream outputStream) { + // Simple implementation for testing + } + + @Override + public void markProbeAsExecuted(ScannerProbe probe) { + executedProbeTypes.add(probe.getType()); + super.markProbeAsExecuted(probe); + } + + @Override + public void markProbeAsUnexecuted(ScannerProbe probe) { + unexecutedProbeTypes.add(probe.getType()); + super.markProbeAsUnexecuted(probe); + } + + public Set getExecutedProbeTypes() { + return executedProbeTypes; + } + + public Set getUnexecutedProbeTypes() { + return unexecutedProbeTypes; + } + + public Map> getExtractedValueContainers() { + return extractedContainers; + } + + @Override + public void putAllExtractedValueContainers( + Map> containers) { + extractedContainers.putAll(containers); + } + } + + static class TestProbe extends ScannerProbe { + private boolean wasExecuted = false; + private boolean shouldThrowException = false; + private Requirement requirement = new FulfilledRequirement<>(); + + TestProbe(ProbeType type) { + super(type); + setWriter(new TestStatsWriter()); + } + + @Override + public TestProbe call() { + if (shouldThrowException) { + throw new RuntimeException("Test exception"); + } + super.call(); + wasExecuted = true; + return this; + } + + @Override + public void executeTest() { + wasExecuted = true; + } + + @Override + public Requirement getRequirements() { + return requirement; + } + + @Override + public void adjustConfig(TestReport report) {} + + @Override + protected void mergeData(TestReport report) {} + + public void setCanExecute(boolean canExecute) { + if (canExecute) { + this.requirement = new FulfilledRequirement<>(); + } else { + this.requirement = new UnfulfillableRequirement<>(); + } + } + + public boolean wasExecuted() { + return wasExecuted; + } + + public void setShouldThrowException(boolean shouldThrow) { + this.shouldThrowException = shouldThrow; + } + + public void addRequirement(Requirement requirement) { + this.requirement = requirement; + } + } + + static class TestAfterProbe extends AfterProbe { + private boolean analyzed = false; + + @Override + public void analyze(TestReport report) { + analyzed = true; + } + + public boolean isAnalyzed() { + return analyzed; + } + } + + static class TestStatsWriter extends StatsWriter { + private int stateCount = 0; + + @Override + public void extract(TestState state) {} + + @Override + public int getStateCounter() { + return stateCount; + } + + public void setStateCount(int count) { + this.stateCount = count; + } + + @Override + public List> getCumulatedExtractedValues() { + List> containers = new ArrayList<>(); + containers.add(new TestExtractedValueContainer()); + return containers; + } + } + + static class TestExtractedValueContainer extends ExtractedValueContainer { + TestExtractedValueContainer() { + super(TestTrackableValue.TEST_VALUE); + put("test1"); + put("test2"); + } + } + + enum TestTrackableValue implements TrackableValue { + TEST_VALUE + } + + @BeforeEach + public void setUp() { + executorConfig = new ExecutorConfig(); + executorConfig.setParallelProbes(2); + executorConfig.setProbeTimeout(5000); + } + + @Test + public void testBasicExecution() throws InterruptedException { + List probeList = + Arrays.asList( + new TestProbe(new TestProbeType("probe1")), + new TestProbe(new TestProbeType("probe2"))); + List afterList = List.of(new TestAfterProbe()); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + try (ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 2, "Test")) { + + TestReport report = new TestReport(); + executor.execute(report); + + assertEquals(2, report.getExecutedProbeTypes().size()); + assertTrue(probeList.getFirst().wasExecuted()); + assertTrue(probeList.get(1).wasExecuted()); + assertTrue(afterList.getFirst().isAnalyzed()); + } + } + + @Test + public void testProbesThatCannotBeExecuted() throws InterruptedException { + TestProbe executableProbe = new TestProbe(new TestProbeType("executable")); + TestProbe nonExecutableProbe = new TestProbe(new TestProbeType("nonExecutable")); + nonExecutableProbe.setCanExecute(false); + + List probeList = Arrays.asList(executableProbe, nonExecutableProbe); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + try (ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 1, "Test")) { + + TestReport report = new TestReport(); + executor.execute(report); + + assertEquals(1, report.getExecutedProbeTypes().size()); + assertEquals(1, report.getUnexecutedProbeTypes().size()); + assertTrue(executableProbe.wasExecuted()); + assertFalse(nonExecutableProbe.wasExecuted()); + } + } + + @Test + public void testProbeExecutionException() { + TestProbe normalProbe = new TestProbe(new TestProbeType("normal")); + TestProbe failingProbe = new TestProbe(new TestProbeType("failing")); + failingProbe.setShouldThrowException(true); + + List probeList = Arrays.asList(normalProbe, failingProbe); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + try (ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 2, "Test")) { + + TestReport report = new TestReport(); + + assertThrows(RuntimeException.class, () -> executor.execute(report)); + } + } + + @Test + public void testPropertyChangeListener() throws InterruptedException { + TestProbe probe1 = new TestProbe(new TestProbeType("probe1")); + TestProbe probe2 = new TestProbe(new TestProbeType("probe2")); + probe2.setCanExecute(false); // Initially cannot execute + + // Add requirement that probe1 must be executed first + probe2.addRequirement( + new Requirement<>() { + @Override + public boolean evaluate(TestReport report) { + return report.getExecutedProbeTypes().contains(new TestProbeType("probe1")); + } + }); + + List probeList = Arrays.asList(probe1, probe2); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + try (ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 1, "Test")) { + + TestReport report = new TestReport(); + + // Simulate property change after probe1 executes + report.addPropertyChangeListener( + evt -> { + if ("supportedProbe".equals(evt.getPropertyName()) + && evt.getNewValue().equals(new TestProbeType("probe1"))) { + probe2.setCanExecute(true); + } + }); + + executor.execute(report); + + assertEquals(2, report.getExecutedProbeTypes().size()); + assertTrue(probe1.wasExecuted()); + assertTrue(probe2.wasExecuted()); + } + } + + @Test + public void testStatisticsCollection() throws InterruptedException { + TestProbe probe1 = new TestProbe(new TestProbeType("probe1")); + TestProbe probe2 = new TestProbe(new TestProbeType("probe2")); + + TestStatsWriter writer1 = (TestStatsWriter) probe1.getWriter(); + TestStatsWriter writer2 = (TestStatsWriter) probe2.getWriter(); + writer1.setStateCount(5); + writer2.setStateCount(3); + + List probeList = Arrays.asList(probe1, probe2); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + try (ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 2, "Test")) { + + TestReport report = new TestReport(); + executor.execute(report); + + assertEquals(8, report.getPerformedConnections()); // 5 + 3 + assertFalse(report.getExtractedValueContainers().isEmpty()); + } + } + + @Test + public void testShutdown() throws InterruptedException { + List probeList = List.of(new TestProbe(new TestProbeType("probe1"))); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + try (ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 1, "Test")) { + TestReport report = new TestReport(); + executor.execute(report); + + executor.shutdown(); + } + // Should not throw exception + } + + @Test + public void testAutoCloseable() throws Exception { + List probeList = List.of(new TestProbe(new TestProbeType("probe1"))); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 1, "Test"); + + TestReport report = new TestReport(); + executor.execute(report); + + executor.close(); // Test AutoCloseable + } + + @Test + public void testConstructorWithCustomExecutor() throws InterruptedException { + List probeList = List.of(new TestProbe(new TestProbeType("probe1"))); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + Semaphore semaphore = new Semaphore(0); + ThreadPoolExecutor customExecutor = + new ScannerThreadPoolExecutor(1, new NamedThreadFactory("Custom"), semaphore, 5000); + + ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, customExecutor, semaphore); + + TestReport report = new TestReport(); + executor.execute(report); + + assertEquals(1, report.getExecutedProbeTypes().size()); + + executor.shutdown(); + customExecutor.shutdown(); + } + + @Test + public void testPropertyChangeWithInvalidSource() { + List probeList = new ArrayList<>(); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + try (ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 1, "Test")) { + + // Test with non-ScanReport source + PropertyChangeEvent event = + new PropertyChangeEvent( + new Object(), "supportedProbe", null, new TestProbeType("test")); + + executor.propertyChange(event); // Should log error but not throw + } + } + + @Test + public void testEmptyProbeList() throws InterruptedException { + List probeList = new ArrayList<>(); + List afterList = List.of(new TestAfterProbe()); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + try (ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 1, "Test")) { + + TestReport report = new TestReport(); + executor.execute(report); + + assertTrue(report.getExecutedProbeTypes().isEmpty()); + assertTrue(afterList.get(0).isAnalyzed()); // After probes should still run + } + } + + @Test + public void testMultipleExtractedValueContainersOfSameType() throws InterruptedException { + TestProbe probe1 = new TestProbe(new TestProbeType("probe1")); + TestProbe probe2 = new TestProbe(new TestProbeType("probe2")); + + List probeList = Arrays.asList(probe1, probe2); + List afterList = new ArrayList<>(); + + ScanJob scanJob = + new ScanJob<>(probeList, afterList); + + try (ThreadedScanJobExecutor executor = + new ThreadedScanJobExecutor<>(executorConfig, scanJob, 2, "Test")) { + + TestReport report = new TestReport(); + executor.execute(report); + + // Should merge containers of same type + assertEquals(1, report.getExtractedValueContainers().size()); + ExtractedValueContainer container = + report.getExtractedValueContainers().get(TestTrackableValue.TEST_VALUE); + assertNotNull(container); + // Each probe contributes 2 values, so we should have 4 total + assertEquals(4, container.getExtractedValueList().size()); + } + } +}