Limit how often GC can be run.
[SquirrelJME.git] / buildSrc / src / main / java / cc / squirreljme / plugin / multivm / VMTestFrameworkTestClassProcessor.java
blob7d9d1182d1228d95ecbf5b510f0de6903fc49d74
1 // -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
2 // ---------------------------------------------------------------------------
3 // Multi-Phasic Applications: SquirrelJME
4 // Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
5 // ---------------------------------------------------------------------------
6 // SquirrelJME is under the Mozilla Public License Version 2.0.
7 // See license.mkd for licensing and copyright information.
8 // ---------------------------------------------------------------------------
10 package cc.squirreljme.plugin.multivm;
12 import cc.squirreljme.plugin.util.TestResultOutputStream;
13 import java.io.IOException;
14 import java.nio.charset.StandardCharsets;
15 import java.util.ArrayList;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.Set;
19 import java.util.TreeMap;
20 import java.util.TreeSet;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.atomic.AtomicReference;
23 import java.util.function.Consumer;
24 import org.gradle.api.internal.tasks.testing.DefaultTestClassDescriptor;
25 import org.gradle.api.internal.tasks.testing.DefaultTestDescriptor;
26 import org.gradle.api.internal.tasks.testing.DefaultTestMethodDescriptor;
27 import org.gradle.api.internal.tasks.testing.DefaultTestSuiteDescriptor;
28 import org.gradle.api.internal.tasks.testing.TestClassProcessor;
29 import org.gradle.api.internal.tasks.testing.TestClassRunInfo;
30 import org.gradle.api.internal.tasks.testing.TestCompleteEvent;
31 import org.gradle.api.internal.tasks.testing.TestResultProcessor;
32 import org.gradle.api.internal.tasks.testing.TestStartEvent;
33 import org.gradle.api.tasks.testing.TestFailure;
34 import org.gradle.api.tasks.testing.TestOutputEvent;
35 import org.gradle.api.tasks.testing.TestResult;
36 import org.gradle.internal.id.IdGenerator;
38 /**
39 * Processor for test classes.
41 * @since 2022/09/11
43 public class VMTestFrameworkTestClassProcessor
44 implements TestClassProcessor
46 /** Tests to run. */
47 protected final Map<String, Set<VMTestFrameworkTestClass>> runTests =
48 new TreeMap<>();
50 /** Test result output. */
51 protected final AtomicReference<TestResultProcessor> resultProcessor =
52 new AtomicReference<>();
54 /** The tests that are available. */
55 protected final Map<String, CandidateTestFiles> availableTests;
57 /** The ID generator to use. */
58 protected final IdGenerator<?> idGenerator;
60 /** The name of this project. */
61 protected final String projectName;
63 /** The run parameters. */
64 protected final Map<String, TestRunParameters> runParameters;
66 /** The thread that is running tests. */
67 private volatile Thread _runningThread;
69 /** Stop running tests? */
70 private volatile boolean _stopNow;
72 /** The final test result. */
73 private volatile TestResult.ResultType _finalResult =
74 TestResult.ResultType.SKIPPED;
76 /**
77 * Initializes the processor.
79 * @param __availableTests The tests that are available.
80 * @param __projectName The name of the project.
81 * @param __runParameters The run parameters.
82 * @throws NullPointerException On null arguments.
83 * @since 2022/09/11
85 public VMTestFrameworkTestClassProcessor(
86 Map<String, CandidateTestFiles> __availableTests,
87 IdGenerator<?> __idGenerator, String __projectName,
88 Map<String, TestRunParameters> __runParameters)
89 throws NullPointerException
91 if (__availableTests == null || __idGenerator == null ||
92 __projectName == null || __runParameters == null)
93 throw new NullPointerException("NARG");
95 this.availableTests = __availableTests;
96 this.idGenerator = __idGenerator;
97 this.projectName = __projectName;
98 this.runParameters = __runParameters;
102 * {@inheritDoc}
103 * @since 2022/09/11
105 @Override
106 public void startProcessing(TestResultProcessor __resultProcessor)
108 // Store this for late
109 synchronized (this.resultProcessor)
111 this.resultProcessor.set(__resultProcessor);
116 * {@inheritDoc}
117 * @since 2022/09/11
119 @Override
120 public void processTestClass(TestClassRunInfo __testClass)
122 // Build test name for later usage
123 String test = __testClass.getTestClassName();
124 VMTestFrameworkTestClass testClass =
125 new VMTestFrameworkTestClass(test);
127 // Was there a match at all?
128 boolean match = false;
130 Map<String, Set<VMTestFrameworkTestClass>> runTests = this.runTests;
131 for (String availableTest : this.availableTests.keySet())
133 VMTestFrameworkTestClass available =
134 new VMTestFrameworkTestClass(availableTest);
136 // Belong to a different class, ignore completely
137 if (!testClass.className.equals(available.className))
138 continue;
140 // Is this pure match?
141 boolean currentMatch = false;
142 currentMatch = testClass.normal.equals(available.normal);
144 // Is there a variant match?
145 if (!currentMatch && testClass.variant != null)
146 currentMatch = testClass.variant.equals(available.variant);
148 // We have a primary sub-variant which matches, but we asked for no
149 // secondary sub-variant and there is one... we want to grab it
150 if (!currentMatch && testClass.primarySubVariant != null)
151 currentMatch = testClass.primarySubVariant
152 .equals(available.primarySubVariant) &&
153 testClass.secondarySubVariant == null &&
154 available.secondarySubVariant != null;
156 // We did not ask for a variant but there is one, so include the
157 // test
158 if (!currentMatch && testClass.variant == null &&
159 available.variant != null)
160 currentMatch = true;
162 // Matched, so add the test
163 if (currentMatch)
164 runTests.computeIfAbsent(testClass.className,
165 (__k) -> new TreeSet<>())
166 .add(new VMTestFrameworkTestClass(availableTest));
168 // Matching emits a match
169 match |= currentMatch;
172 // If there is no match and a variant was specified, we likely want
173 // a slightly different parameter
174 if (!match && testClass.variant != null)
176 // Remember class for later, sort by classes all together
177 runTests.computeIfAbsent(testClass.className,
178 (__k) -> new TreeSet<>()).add(testClass);
183 * {@inheritDoc}
184 * @since 2022/09/11
186 @Override
187 public void stop()
189 // Stop is a bit of a misnomer, it means stop processing and then run
190 // all the tests...
192 // Remember thread for later stop
193 synchronized (this)
195 this._runningThread = Thread.currentThread();
198 AtomicReference<TestResultProcessor> resultProcessor =
199 this.resultProcessor;
201 // Suite for the entire project group
202 IdGenerator<?> idGenerator = this.idGenerator;
203 DefaultTestSuiteDescriptor suiteDesc =
204 new DefaultTestSuiteDescriptor(
205 idGenerator.generateId(),
206 this.projectName);
208 // Suite has started
209 VMTestFrameworkTestClassProcessor.resultAction(resultProcessor,
210 (__rp) -> __rp.started(suiteDesc,
211 new TestStartEvent(System.currentTimeMillis())));
213 // Go through and actually run all the tests
214 for (Map.Entry<String, Set<VMTestFrameworkTestClass>> byClass :
215 this.runTests.entrySet())
217 // Do not run any more classes?
218 synchronized (this)
220 if (this._stopNow)
221 break;
224 // Start class test
225 DefaultTestClassDescriptor classDesc =
226 new DefaultTestClassDescriptor(idGenerator.generateId(),
227 byClass.getKey(), byClass.getKey());
228 VMTestFrameworkTestClassProcessor.resultAction(resultProcessor,
229 (__rp) -> __rp.started(classDesc,
230 new TestStartEvent(System.currentTimeMillis(),
231 suiteDesc.getId())));
233 // Go through all tests in this class
234 TestResult.ResultType classResult = TestResult.ResultType.SKIPPED;
235 for (VMTestFrameworkTestClass testName : byClass.getValue())
237 // Do not run any more tests?
238 synchronized (this)
240 if (this._stopNow)
241 break;
244 // We do not know how to run the test, so do nothing
245 if (this.runParameters.get(testName.normal) == null)
246 continue;
248 // Run test and get its result
249 TestResult.ResultType result = this.__runSingleTest(suiteDesc,
250 testName, classDesc);
252 // Update class based result
253 classResult = VMTestFrameworkTestClassProcessor
254 .calculateResult(classResult, result);
256 // Change the global suite test result here
257 this._finalResult = VMTestFrameworkTestClassProcessor
258 .calculateResult(this._finalResult, result);
261 // Mark class as completed
262 TestResult.ResultType finalClassResult = classResult;
263 VMTestFrameworkTestClassProcessor.resultAction(resultProcessor,
264 (__rp) -> __rp.completed(classDesc.getId(),
265 new TestCompleteEvent(System.currentTimeMillis(),
266 finalClassResult)));
269 // If failed, emit a throwable... if we do not do this then Gradle does
270 // not care if a test failed and will just continue on happily like
271 // nothing ever happened
272 TestResult.ResultType finalResult = this._finalResult;
273 if (finalResult == TestResult.ResultType.FAILURE)
274 VMTestFrameworkTestClassProcessor.resultAction(resultProcessor,
275 (__rp) -> __rp.failure(suiteDesc.getId(),
276 TestFailure.fromTestFrameworkFailure(VMTestFrameworkTestClassProcessor
277 .messageThrow("Tests have failed."))));
279 // Use the final result from all the test runs
280 VMTestFrameworkTestClassProcessor.resultAction(resultProcessor,
281 (__rp) -> __rp.completed(suiteDesc.getId(),
282 new TestCompleteEvent(System.currentTimeMillis(),
283 finalResult)));
287 * {@inheritDoc}
288 * @since 2022/09/11
290 @Override
291 public void stopNow()
293 // Signal that tests should stop
294 synchronized (this)
296 // Do stop now
297 this._stopNow = true;
299 // Make this invalid
300 synchronized (this.resultProcessor)
302 this.resultProcessor.set(null);
305 // Interrupt thread quickly
306 Thread runningThread = this._runningThread;
307 if (runningThread != null)
308 runningThread.interrupt();
310 // Make sure to notify on all monitors
311 this.notifyAll();
316 * Runs the test and gives the result of it.
318 * @param __suiteDesc The suite descriptor.
319 * @param __testName The name of the test.
320 * @param __classDesc The owning class descriptor, since this is a group.
321 * @return The result of the test.
322 * @since 2022/09/11
324 @SuppressWarnings("UseOfProcessBuilder")
325 private TestResult.ResultType __runSingleTest(
326 DefaultTestSuiteDescriptor __suiteDesc,
327 VMTestFrameworkTestClass __testName,
328 DefaultTestClassDescriptor __classDesc)
330 Map<String, TestRunParameters> runParameters = this.runParameters;
331 AtomicReference<TestResultProcessor> resultProcessor =
332 this.resultProcessor;
333 IdGenerator<?> idGenerator = this.idGenerator;
335 // Default to failure
336 TestResult.ResultType testResult = TestResult.ResultType.FAILURE;
338 // Threads for processing stream data
339 Process process = null;
340 Thread stdOutThread = null;
341 Thread stdErrThread = null;
343 // Make sure process and thread are killed at the end
346 // Check to see if we are stopping testing
347 synchronized (this)
349 // If we are forcing a stop, mark as failure
350 if (this._stopNow)
351 return TestResult.ResultType.FAILURE;
354 // Get parameters for this test run
355 TestRunParameters runTest = runParameters.get(__testName.normal);
357 // Setup process to run
358 ProcessBuilder builder = new ProcessBuilder();
360 // The command we are executing
361 builder.command(runTest.getCommandLine());
363 // Redirect all the outputs we have
364 builder.redirectOutput(ProcessBuilder.Redirect.PIPE);
365 builder.redirectError(ProcessBuilder.Redirect.PIPE);
367 // Start method test
368 DefaultTestDescriptor methodDesc = new DefaultTestMethodDescriptor(
369 idGenerator.generateId(), __testName.className,
370 (__testName.variant == null ? "test" :
371 String.format("test[%s]", __testName.variant)));
372 VMTestFrameworkTestClassProcessor.resultAction(resultProcessor,
373 (__rp) -> __rp.started(methodDesc,
374 new TestStartEvent(System.currentTimeMillis(),
375 __classDesc.getId())));
377 // Start the process
380 process = builder.start();
383 // Failed to start?
384 catch (IOException __e)
386 // Failed to start test
387 VMTestFrameworkTestClassProcessor.resultAction(resultProcessor,
388 (__rp) -> __rp.failure(__suiteDesc.getId(),
389 TestFailure.fromTestFrameworkFailure(new Throwable("Failed to start test."))));
391 throw new RuntimeException(__e);
394 // Setup listening buffer threads
395 VMTestOutputBuffer stdOut = new VMTestOutputBuffer(
396 process.getInputStream(),
397 new TestResultOutputStream(resultProcessor, methodDesc.getId(),
398 TestOutputEvent.Destination.StdOut),
399 false);
400 VMTestOutputBuffer stdErr = new VMTestOutputBuffer(
401 process.getErrorStream(),
402 new TestResultOutputStream(resultProcessor, methodDesc.getId(),
403 TestOutputEvent.Destination.StdErr),
404 true);
406 // Setup threads for reading standard output and error
407 stdOutThread = new Thread(stdOut, "stdOutReader");
408 stdErrThread = new Thread(stdErr, "stdErrReader");
410 // Start threads so console lines can be read as they appear
411 stdOutThread.start();
412 stdErrThread.start();
414 // Run the test, default to failed exit code
415 int exitCode = -1;
416 boolean isDebugging = VMTestTaskAction.isDebugging();
417 for (long startTime = System.nanoTime();;)
420 // How much time is left? If debugging time never runs
421 // out
422 long timeLeft = Math.max(VMTestWorkAction.TEST_TIMEOUT -
423 (System.nanoTime() - startTime),
424 (isDebugging ? 1000L : 0L));
426 // Did we run out of time?
427 // Wait for however long this takes to complete
428 if (timeLeft <= 0 || this._stopNow ||
429 process.waitFor(timeLeft, TimeUnit.NANOSECONDS))
431 exitCode = process.exitValue();
432 break;
436 // We got interrupted or ran out of time, make sure the process
437 // stops, and we continue with the result
438 catch (IllegalThreadStateException|InterruptedException
439 ignored)
441 // Forcibly destroy the process if it is alive
442 if (process.isAlive())
443 process.destroyForcibly();
445 // Make sure the threads process their output
446 stdOutThread.interrupt();
447 stdErrThread.interrupt();
450 // Force completion of the read thread, we cannot continue if
451 // the other thread is currently working...
452 stdOut.getBytes(stdOutThread);
453 byte[] stdErrBytes = stdErr.getBytes(stdErrThread);
455 // The result determines whether this succeeded, skipped, or
456 // failed
457 switch (exitCode)
459 // Success for zero
460 case 0:
461 testResult = TestResult.ResultType.SUCCESS;
462 break;
464 // Skipped is just two
465 case 2:
466 testResult = TestResult.ResultType.SKIPPED;
467 break;
469 // Treat anything else as failure
470 default:
471 testResult = TestResult.ResultType.FAILURE;
472 break;
475 // If failed, emit a throwable...
476 TestResult.ResultType finalResult = testResult;
477 if (finalResult == TestResult.ResultType.FAILURE)
478 VMTestFrameworkTestClassProcessor.resultAction(resultProcessor,
479 (__rp) -> __rp.failure(methodDesc.getId(),
480 TestFailure.fromTestFrameworkFailure(VMTestFrameworkTestClassProcessor
481 .messageThrow("Test failed: " +
482 __testName.normal))));
484 // Mark method as completed
485 VMTestFrameworkTestClassProcessor.resultAction(resultProcessor,
486 (__rp) -> __rp.completed(methodDesc.getId(),
487 new TestCompleteEvent(System.currentTimeMillis(),
488 finalResult)));
491 // Interrupt read/write threads and kill the process if it is alive
492 finally
494 // If our test process is still alive, stop it
495 if (process != null)
496 if (process.isAlive())
497 process.destroyForcibly();
499 // Stop the standard output thread from running
500 if (stdOutThread != null)
501 stdOutThread.interrupt();
503 // Stop the standard error thread from running
504 if (stdErrThread != null)
505 stdErrThread.interrupt();
508 return testResult;
512 * Calculates the test result.
514 * @param __input The input result.
515 * @param __modifier The modifier to the result.
516 * @return The new result that should be used.
517 * @since 2022/09/14
519 public static TestResult.ResultType calculateResult(
520 TestResult.ResultType __input, TestResult.ResultType __modifier)
522 if (__input == null || __modifier == null)
523 throw new NullPointerException("NARG");
525 // Depends on our target result
526 switch (__modifier)
528 // Keep the old state, this does not cause a change
529 case SKIPPED:
530 return __input;
532 // Change skipped to success, but nothing else
533 case SUCCESS:
534 if (__input == TestResult.ResultType.SKIPPED)
535 return __modifier;
536 return __input;
538 // Otherwise, always mark failure
539 case FAILURE:
540 return __modifier;
543 throw new Error("OOPS");
547 * Generates a throwable useful for printing the output.
549 * @param __output The output.
550 * @return The throwable to use for the message.
551 * @since 2022/09/14
553 public static Throwable messageThrow(byte[] __output)
555 return VMTestFrameworkTestClassProcessor.messageThrow(
556 (__output == null || __output.length <= 0 ? "" :
557 new String(__output, StandardCharsets.UTF_8)));
561 * Generates a throwable useful for printing the output.
563 * @param __output The output.
564 * @return The throwable to use for the message.
565 * @since 2022/09/14
567 private static Throwable messageThrow(String __output)
569 return new VMTestFrameworkThrowableOutput(__output);
573 * Runs the given test result processor action, assuming that it is still
574 * a valid one. When {@link #stopNow()} then any processor that did exist
575 * must not be used ever again.
577 * @param __atom The atomic to check on.
578 * @param __doThis Do this action.
579 * @throws NullPointerException On null arguments.
580 * @since 2022/09/11
582 @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
583 public static void resultAction(
584 AtomicReference<TestResultProcessor> __atom,
585 Consumer<TestResultProcessor> __doThis)
586 throws NullPointerException
588 if (__atom == null || __doThis == null)
589 throw new NullPointerException("NARG");
591 synchronized (__atom)
593 TestResultProcessor processor = __atom.get();
594 if (processor != null)
595 __doThis.accept(processor);