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
;
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
;
39 * Processor for test classes.
43 public class VMTestFrameworkTestClassProcessor
44 implements TestClassProcessor
47 protected final Map
<String
, Set
<VMTestFrameworkTestClass
>> runTests
=
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
;
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.
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
;
106 public void startProcessing(TestResultProcessor __resultProcessor
)
108 // Store this for late
109 synchronized (this.resultProcessor
)
111 this.resultProcessor
.set(__resultProcessor
);
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
))
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
158 if (!currentMatch
&& testClass
.variant
== null &&
159 available
.variant
!= null)
162 // Matched, so add the test
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
);
189 // Stop is a bit of a misnomer, it means stop processing and then run
192 // Remember thread for later stop
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(),
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?
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?
244 // We do not know how to run the test, so do nothing
245 if (this.runParameters
.get(testName
.normal
) == null)
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(),
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(),
291 public void stopNow()
293 // Signal that tests should stop
297 this._stopNow
= true;
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
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.
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
349 // If we are forcing a stop, mark as failure
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
);
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())));
380 process
= builder
.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
),
400 VMTestOutputBuffer stdErr
= new VMTestOutputBuffer(
401 process
.getErrorStream(),
402 new TestResultOutputStream(resultProcessor
, methodDesc
.getId(),
403 TestOutputEvent
.Destination
.StdErr
),
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
416 boolean isDebugging
= VMTestTaskAction
.isDebugging();
417 for (long startTime
= System
.nanoTime();;)
420 // How much time is left? If debugging time never runs
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();
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
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
461 testResult
= TestResult
.ResultType
.SUCCESS
;
464 // Skipped is just two
466 testResult
= TestResult
.ResultType
.SKIPPED
;
469 // Treat anything else as failure
471 testResult
= TestResult
.ResultType
.FAILURE
;
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(),
491 // Interrupt read/write threads and kill the process if it is alive
494 // If our test process is still alive, stop it
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();
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.
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
528 // Keep the old state, this does not cause a change
532 // Change skipped to success, but nothing else
534 if (__input
== TestResult
.ResultType
.SKIPPED
)
538 // Otherwise, always mark failure
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.
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.
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.
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
);