1 // -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
2 // ---------------------------------------------------------------------------
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
.GradleLoggerOutputStream
;
13 import java
.io
.IOException
;
14 import java
.io
.PrintStream
;
15 import java
.nio
.file
.Files
;
16 import java
.nio
.file
.StandardOpenOption
;
17 import java
.time
.Instant
;
18 import java
.time
.LocalDateTime
;
19 import java
.time
.ZoneId
;
20 import java
.time
.format
.DateTimeFormatter
;
22 import java
.util
.concurrent
.ConcurrentHashMap
;
23 import java
.util
.concurrent
.TimeUnit
;
24 import org
.gradle
.api
.logging
.LogLevel
;
25 import org
.gradle
.api
.logging
.Logger
;
26 import org
.gradle
.workers
.WorkAction
;
29 * This performs the actual work of running the VM tests.
33 @SuppressWarnings("UnstableApiUsage")
34 public abstract class VMTestWorkAction
35 implements WorkAction
<VMTestParameters
>
37 /** Logger storage. */
38 static final Map
<String
, __LogHolder__
> _LOGGERS
=
39 new ConcurrentHashMap
<>();
41 /** The timeout for tests, in nanoseconds. */
42 public static final long TEST_TIMEOUT
=
45 /** Skip sequence special. */
46 private static final byte[] _SKIP_SPECIAL
=
47 new byte[]{'%', '!', 'S', 'k', 'O', 'n', 'T', 'i', '!', '%'};
53 @SuppressWarnings("UseOfProcessBuilder")
57 // Determine the name of the test
58 VMTestParameters parameters
= this.getParameters();
59 String testName
= parameters
.getTestName().get();
62 __LogHolder__ logHolder
= VMTestWorkAction
._LOGGERS
.get(
63 parameters
.getUniqueId().get());
64 Logger logger
= logHolder
.logger
;
66 // Threads for processing stream data
67 Thread stdOutThread
= null;
68 Thread stdErrThread
= null;
70 // The current and total test IDs, used to measure progress
71 int count
= parameters
.getCount().get();
72 int total
= parameters
.getTotal().get();
74 // If we are debugging, we do not want to kill the test by a timeout
75 // if it takes forever because we might be very slow at debugging
76 boolean isDebugging
= VMTestTaskAction
.isDebugging();
78 // The process might not be able to execute
79 Process process
= null;
82 // Note this is running
83 logger
.lifecycle(String
.format("???? %s [%d/%d]",
84 testName
, count
, total
));
86 // Clock the starting time
87 long clockStart
= System
.currentTimeMillis();
88 long nsStart
= System
.nanoTime();
90 // Setup output handler
91 ProcessBuilder processBuilder
= new ProcessBuilder(
92 parameters
.getCommandLine().get().toArray(new String
[0]));
93 processBuilder
.redirectOutput(ProcessBuilder
.Redirect
.PIPE
);
94 processBuilder
.redirectError(ProcessBuilder
.Redirect
.PIPE
);
96 // Start the process with the command line that was pre-determined
97 process
= processBuilder
.start();
99 // Setup listening buffer threads
100 VMTestOutputBuffer stdOut
= new VMTestOutputBuffer(
101 process
.getInputStream(), new GradleLoggerOutputStream(logger
102 , LogLevel
.LIFECYCLE
, count
, total
),
104 VMTestOutputBuffer stdErr
= new VMTestOutputBuffer(
105 process
.getErrorStream(), new GradleLoggerOutputStream(logger
106 , LogLevel
.ERROR
, count
, total
),
109 // Setup threads for reading standard output and standard error
110 stdOutThread
= new Thread(stdOut
, "stdOutReader");
111 stdErrThread
= new Thread(stdErr
, "stdErrReader");
113 // Start both threads so console lines can be read as they appear
114 stdOutThread
.start();
115 stdErrThread
.start();
117 // Wait for the process to terminate, the exit code will contain
118 // the result of the test (pass, skip, fail)
120 boolean timeOutHit
= false;
124 // Has the test run expired? Only when not debugging
127 long nsDur
= System
.nanoTime() - nsStart
;
128 if (nsDur
>= VMTestWorkAction
.TEST_TIMEOUT
)
131 logger
.error(String
.format("TIME %s [%d/%d]",
132 testName
, count
, total
));
134 // Set timeout as being hit, used for special
138 // The logic for interrupts is the same
139 throw new InterruptedException("Test Timeout");
143 // Wait for completion
144 if (process
.waitFor(3, TimeUnit
.SECONDS
))
146 exitCode
= process
.waitFor();
150 catch (InterruptedException e
)
152 // Add note that this happened
153 logger
.error(String
.format("INTR %s", testName
));
155 // Stop the processes that are running
157 stdOutThread
.interrupt();
158 stdErrThread
.interrupt();
160 // Stop running the loop
164 // Clock the ending time
165 long nsDur
= System
.nanoTime() - nsStart
;
167 byte[] stdErrBytes
= stdErr
.getBytes(stdErrThread
);
168 if (timeOutHit
&& VMTestWorkAction
.__findTimeoutSkip(stdErrBytes
))
169 exitCode
= VMTestResult
.SKIP
.exitCode
;
171 // Note this has finished
172 VMTestResult testResult
= VMTestResult
.valueOf(exitCode
);
173 logger
.lifecycle(String
.format("%4s %s [%d/%d]",
174 testResult
, testName
, count
, total
));
176 // Write the XML file
177 try (PrintStream out
= new PrintStream(Files
.newOutputStream(
178 parameters
.getResultFile().get().getAsFile().toPath(),
179 StandardOpenOption
.CREATE
,
180 StandardOpenOption
.TRUNCATE_EXISTING
,
181 StandardOpenOption
.WRITE
)))
183 // Write the resultant XML, this will be read later for
184 // detection purposes
185 VMTestWorkAction
.__writeXml(out
, testName
, testResult
,
186 parameters
.getVmName().get(), clockStart
, nsDur
,
187 stdOut
.getBytes(stdOutThread
),
190 // Make sure everything is written
195 // Process failed to execute
196 catch (IOException e
)
198 throw new RuntimeException("I/O Exception in " + testName
, e
);
201 // Interrupt read/write threads
204 // If our test process is still alive, stop it
206 if (process
.isAlive())
207 process
.destroyForcibly();
209 // Stop the standard output thread from running
210 if (stdOutThread
!= null)
211 stdOutThread
.interrupt();
213 // Stop the standard error thread from running
214 if (stdErrThread
!= null)
215 stdErrThread
.interrupt();
220 * Finds the special timeout sequence if this has had a timeout, this is
221 * used to detect if a test should just skip rather than causing a fail.
223 * @param __stdErrBytes The standard error bytes.
224 * @return If the sequence was found.
225 * @throws NullPointerException On null arguments.
228 private static boolean __findTimeoutSkip(byte[] __stdErrBytes
)
229 throws NullPointerException
231 if (__stdErrBytes
== null)
232 throw new NullPointerException("NARG");
234 // Skip special sequence
235 byte[] skipSpecial
= VMTestWorkAction
._SKIP_SPECIAL
;
236 byte firstByte
= skipSpecial
[0];
237 int skipLen
= skipSpecial
.length
;
240 for (int i
= 0, n
= Math
.max(0, __stdErrBytes
.length
- skipLen
);
243 // If the first byte is a match, then
244 byte quick
= __stdErrBytes
[i
];
245 if (quick
!= firstByte
)
248 // Check if the entire sequence matched
249 for (int j
= 0, q
= i
; j
<= skipLen
; j
++, q
++)
252 else if (__stdErrBytes
[q
] != skipSpecial
[j
])
261 * Writes the XML test result to the given output.
263 * @param __out The stream to write the XML to.
264 * @param __testName The name of the test.
265 * @param __result The result of the test.
266 * @param __vmName The virtual machine name.
267 * @param __clockStart The starting time of the test.
268 * @param __nsDur The duration of the test in nanoseconds.
269 * @param __stdOut Standard output.
270 * @param __stdErr Standard error.
271 * @throws NullPointerException On null arguments.
274 @SuppressWarnings("resource")
275 private static void __writeXml(PrintStream __out
, String __testName
,
276 VMTestResult __result
, String __vmName
, long __clockStart
,
277 long __nsDur
, byte[] __stdOut
, byte[] __stdErr
)
278 throws NullPointerException
280 if (__out
== null || __testName
== null || __result
== null ||
281 __stdOut
== null || __stdErr
== null || __vmName
== null)
282 throw new NullPointerException("NARG");
284 // Write the XML header
285 __out
.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
287 // Determine the counts for the test
289 int numSkipped
= (__result
== VMTestResult
.SKIP ?
1 : 0);
290 int numFailed
= (__result
== VMTestResult
.FAIL ?
1 : 0);
292 // The current timestamp
293 String nowTimestamp
= DateTimeFormatter
.ISO_LOCAL_DATE_TIME
.format(
294 LocalDateTime
.ofInstant(Instant
.ofEpochMilli(__clockStart
),
295 ZoneId
.systemDefault()));
297 // Duration in seconds
298 double durationSeconds
= __nsDur
/ 1_000_000_000D
;
301 __out
.printf("<testsuite name=\"%s\" tests=\"%d\" " +
302 "skipped=\"%d\" failures=\"%d\" errors=\"%d\" " +
303 "timestamp=\"%s\" hostname=\"%s\" time=\"%.3f\" " +
305 __testName
, numTests
, numSkipped
, numFailed
, numFailed
,
306 nowTimestamp
, __vmName
, durationSeconds
);
310 __out
.println("<properties>");
312 // A special property is used for a quick search to determine if there
313 // is a pass, skip, or fail as the test result needs to be read to
314 // determine if the task is okay
315 __out
.printf("<property name=\"squirreljme.test.result\" " +
316 "value=\"%s:result:%s:\" />", VMTestTaskAction
._SPECIAL_KEY
,
321 __out
.printf("<property name=\"squirreljme.test.nanoseconds\" " +
322 "value=\"%s:nanoseconds:%s:\" />", VMTestTaskAction
._SPECIAL_KEY
,
327 __out
.println("</properties>");
330 __out
.printf("<testcase name=\"%s\" classname=\"%s\" " +
332 __testName
, __testName
, durationSeconds
);
335 // Failed tests use this tag accordingly, despite there being a
336 // failures indicator
337 if (__result
== VMTestResult
.FAIL
)
339 __out
.printf("<failure type=\"%s\">", __testName
);
340 VMTestWorkAction
.__writeText(__out
, __stdErr
);
341 __out
.println("</failure>");
344 // Write both buffers
345 VMTestWorkAction
.__writeTextTag(__out
, "system-out", __stdOut
);
346 VMTestWorkAction
.__writeTextTag(__out
, "system-err", __stdErr
);
349 __out
.println("</testcase>");
352 __out
.println("</testsuite>");
356 * Writes the given XML Text.
358 * @param __out The target stream.
359 * @param __text The text to write.
360 * @throws NullPointerException On null arguments.
363 private static void __writeText(PrintStream __out
, byte[] __text
)
364 throws NullPointerException
366 if (__out
== null || __text
== null)
367 throw new NullPointerException("NARG");
369 __out
.print("<![CDATA[");
370 __out
.print(new String(__text
));
375 * Writes raw buffer text to the output.
377 * @param __out The stream to write to.
378 * @param __key The tag key.
379 * @param __text The bytes for the key.
380 * @throws NullPointerException On null arguments.
383 @SuppressWarnings("resource")
384 private static void __writeTextTag(PrintStream __out
, String __key
,
386 throws NullPointerException
388 if (__out
== null || __key
== null || __text
== null)
389 throw new NullPointerException("NARG");
391 // Write tag into here
392 __out
.printf("<%s>", __key
);
393 VMTestWorkAction
.__writeText(__out
, __text
);
394 __out
.printf("</%s>", __key
);