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
.SquirrelJMEPluginConfiguration
;
13 import cc
.squirreljme
.plugin
.multivm
.ident
.SourceTargetClassifier
;
14 import cc
.squirreljme
.plugin
.util
.JavaExecSpecFiller
;
15 import cc
.squirreljme
.plugin
.util
.SerializedPath
;
16 import cc
.squirreljme
.plugin
.util
.SimpleJavaExecSpecFiller
;
17 import java
.io
.BufferedReader
;
18 import java
.io
.IOException
;
19 import java
.io
.InputStreamReader
;
20 import java
.io
.PrintStream
;
21 import java
.nio
.charset
.StandardCharsets
;
22 import java
.nio
.file
.Files
;
23 import java
.nio
.file
.Path
;
24 import java
.nio
.file
.StandardOpenOption
;
25 import java
.util
.ArrayList
;
26 import java
.util
.Arrays
;
27 import java
.util
.LinkedHashMap
;
28 import java
.util
.LinkedHashSet
;
29 import java
.util
.List
;
31 import java
.util
.Objects
;
33 import java
.util
.TreeMap
;
34 import java
.util
.UUID
;
35 import org
.gradle
.api
.Action
;
36 import org
.gradle
.api
.Task
;
37 import org
.gradle
.api
.logging
.Logger
;
38 import org
.gradle
.api
.tasks
.testing
.Test
;
39 import org
.gradle
.workers
.WorkQueue
;
40 import org
.gradle
.workers
.WorkerExecutor
;
43 * Performs the testing of the program.
45 * This is responsible for executing the test and outputting JUnit results as
46 * well which record the test logs accordingly. This also will record snapshots
47 * for profiling and otherwise, if possible.
49 * Entry is via {@code net.multiphasicapps.tac.MainSingleRunner} which is
50 * passed the test to load and execute.
54 public class VMTestTaskAction
55 implements Action
<Task
>
58 /** The special key for quick finding test results. */
59 static final String _SPECIAL_KEY
=
62 /** Print the test manifest? */
63 public static final String PRINT_TEST_MANIFEST
=
64 "net.multiphasicapps.tac.resultManifest";
66 /** The maximum parallel tests that can run at once. */
67 private static final int _MAX_PARALLEL_TESTS
=
70 /** Cached CPU count. */
71 private static volatile int _CACHED_CPU_COUNT
;
73 /** The worker executor. */
74 protected final WorkerExecutor executor
;
76 /** The classifier used. */
77 protected final SourceTargetClassifier classifier
;
80 * Initializes the virtual machine task action.
82 * @param __executor The executor for tasks.
83 * @param __classifier The classifier used.
84 * @throws NullPointerException On null arguments.
87 public VMTestTaskAction(WorkerExecutor __executor
,
88 SourceTargetClassifier __classifier
)
89 throws NullPointerException
91 if (__executor
== null || __classifier
== null)
92 throw new NullPointerException("NARG");
94 this.executor
= __executor
;
95 this.classifier
= __classifier
;
103 public void execute(Task __task
)
105 // The task used for testing
106 Test testTask
= (Test
)__task
;
109 Logger logger
= __task
.getLogger();
110 logger
.debug("Tests: {}", VMHelpers
.runningTests(
111 __task
.getProject(), this.classifier
.getSourceSet()));
113 // We want our tasks to run from within Gradle
114 WorkQueue queue
= this.executor
.noIsolation();
116 // All results will go here
117 String sourceSet
= this.classifier
.getSourceSet();
118 VMSpecifier vmType
= this.classifier
.getVmType();
119 Path resultDir
= VMHelpers
.testResultXmlDir(__task
.getProject(),
120 this.classifier
).get();
122 // All the result files will be read afterwards to determine whether
123 // this task will pass or fail
124 Map
<String
, Path
> xmlResults
= new TreeMap
<>();
126 // How many tests should be run be at once?
127 int maxParallel
= VMTestTaskAction
.maxParallelTests();
129 // Determine the number of tests
130 Map
<String
, CandidateTestFiles
> tests
= VMHelpers
.runningTests(
131 __task
.getProject(), sourceSet
).tests
;
132 Set
<String
> testNames
= tests
.keySet();
133 int numTests
= testNames
.size();
135 // Calculate suite run parameters
136 SuiteRunParameters runSuite
= VMTestTaskAction
.runSuite(
137 (VMBaseTask
)__task
, this.classifier
);
139 // Force non-parallel?
140 if (runSuite
.noParallelTests
)
143 // We only need to set the classpath once
144 Path
[] classPath
= SerializedPath
.unboxPaths(runSuite
.classPath
);
147 logger
.debug("Testing ClassPath: {}",
148 Arrays
.asList(classPath
));
150 // Make unique ID for logger binding for this session
151 String uniqueID
= runSuite
.uniqueId
;
153 // Setup logger for this session
154 __LogHolder__ logHolder
= new __LogHolder__(logger
);
155 VMTestWorkAction
._LOGGERS
.put(uniqueID
, logHolder
);
157 // Execute the tests concurrently but up to the limit, as testing is
158 // very intense on CPU
161 for (String testName
: testNames
)
163 // Calculate test running parameters
164 CandidateTestFiles candidate
= tests
.get(testName
);
165 TestRunParameters runTest
= VMTestTaskAction
.runTest(
166 (VMBaseTask
)__task
, this.classifier
, runSuite
, testName
,
169 // Where will the results be read from?
170 Path xmlResult
= resultDir
.resolve(
171 VMHelpers
.testResultXmlName(testName
));
172 xmlResults
.put(testName
, xmlResult
);
174 // Which test number is this?
175 int submitId
= ++submitCount
;
177 // Submit our work task which should be a simple JVM execute due
178 // to the limitations of Gradle workers
179 queue
.submit(VMTestWorkAction
.class, __params
->
182 __params
.getUniqueId().set(uniqueID
);
184 // The test and where the results will go
185 __params
.getTestName().set(testName
);
186 __params
.getResultFile().set(xmlResult
.toFile());
188 // The command line to execute
189 __params
.getCommandLine().set(runTest
.getCommandLine());
191 // Name of the VM for hostname setting
193 .set(vmType
.vmName(VMNameFormat
.PROPER_NOUN
));
195 // Used for progress tracking
196 __params
.getCount().set(submitId
);
197 __params
.getTotal().set(numTests
);
200 // Already requested the number of tests to run, so let them
202 if (++runCount
>= maxParallel
)
204 // Reset counter so that we can trigger this again
207 // Wait for the current set to finish
212 // Wait for the queue to finish
215 // Get the status of every test
216 Map
<String
, ResultantTestInfo
> testResults
=
217 this.__testResults(xmlResults
);
219 // Determine and print any test failures
220 Set
<String
> failedTests
= new LinkedHashSet
<>();
221 for (ResultantTestInfo test
: testResults
.values())
222 if (test
.result
== VMTestResult
.FAIL
)
224 failedTests
.add(test
.name
);
226 logger
.error("Failed test: {}", test
.name
);
229 // Determine and ensure the directory where CSVs go exist
230 Path csvDir
= VMHelpers
.testResultsCsvDir(__task
.getProject(),
231 this.classifier
).get();
234 Files
.createDirectories(csvDir
);
237 // Ignore failures here
238 catch (IOException e
)
243 // Print a CSV of all the test results
244 try (PrintStream ps
= new PrintStream(Files
.newOutputStream(
245 csvDir
.resolve(VMHelpers
.testResultsCsvName(__task
.getProject())),
246 StandardOpenOption
.WRITE
, StandardOpenOption
.CREATE
,
247 StandardOpenOption
.TRUNCATE_EXISTING
),
251 ps
.println("name,status,duration,simpleDuration");
255 for (ResultantTestInfo e
: testResults
.values())
257 totalTime
+= e
.nanoseconds
;
259 ps
.printf("%s,%s,%d,%s%n",
260 e
.name
, e
.result
.name(), e
.nanoseconds
,
261 VMTestTaskAction
.__simpleDuration(e
.nanoseconds
));
265 ps
.printf("TOTAL,,%d,%s%n",
266 totalTime
, VMTestTaskAction
.__simpleDuration(totalTime
));
268 // Ensure everything is written
272 // Ignore these failures
273 catch (IOException e
)
278 // Wipe logger session
279 VMTestWorkAction
._LOGGERS
.remove(uniqueID
);
281 // If there were failures, then fail this task with an exception
282 if (!failedTests
.isEmpty())
283 throw new RuntimeException(
284 "There were failing tests: " + failedTests
);
288 * Goes through all the XML files obtains the test results.
290 * @param __xmlResults The result paths for tests.
291 * @return The mapping of all tests.
292 * @throws NullPointerException On null arguments.
295 Map
<String
, ResultantTestInfo
> __testResults(
296 Map
<String
, Path
> __xmlResults
)
297 throws NullPointerException
299 if (__xmlResults
== null)
300 throw new NullPointerException("NARG");
303 List
<Integer
> colons
= new ArrayList
<>(4);
306 Map
<String
, ResultantTestInfo
> result
= new TreeMap
<>();
307 for (Map
.Entry
<String
, Path
> test
: __xmlResults
.entrySet())
310 // Resulting status and key
311 VMTestResult testResult
= null;
312 long nanoseconds
= -1;
314 // Check all lines of the file and see if one is found
315 for (String line
: Files
.readAllLines(test
.getValue()))
317 // Locate the special key
318 int keyDx
= line
.indexOf(VMTestTaskAction
._SPECIAL_KEY
);
322 // Find indexes of all the colons after this key
324 for (int at
= keyDx
; at
>= 0;)
326 at
= line
.indexOf(':', at
+ 1);
332 // Need three colons here
333 if (colons
.size() < 3)
336 // Extract the key and value
337 String key
= line
.substring(
338 colons
.get(0) + 1, colons
.get(1));
339 String val
= line
.substring(
340 colons
.get(1) + 1, colons
.get(2));
342 // Depends on the key/value
348 testResult
= VMTestResult
.valueOf(val
);
352 nanoseconds
= Long
.parseLong(val
);
356 catch (IllegalArgumentException e
)
363 result
.put(test
.getKey(), new ResultantTestInfo(test
.getKey(),
364 testResult
, nanoseconds
));
366 catch (IOException e
)
368 throw new RuntimeException("Could not read test XML.", e
);
375 * Is debugging being used?
377 * @return If debugging is being used.
380 public static boolean isDebugging()
382 String jdwpProp
= System
.getProperty("squirreljme.xjdwp",
383 System
.getProperty("squirreljme.jdwp"));
384 return (jdwpProp
!= null && !jdwpProp
.isEmpty());
388 * Sets the maximum number of parallel tests to run.
390 * @return The max parallel tests to run at once.
393 public static int maxParallelTests()
395 // If debugging, do not run in parallel
396 if (null != System
.getProperty("squirreljme.xjdwp",
397 System
.getProperty("squirreljme.jdwp")))
400 int cpuCount
= VMTestTaskAction
.physicalProcessorCount();
401 return (cpuCount
<= 1 ?
1 :
402 Math
.min(Math
.max(2, cpuCount
),
403 VMTestTaskAction
._MAX_PARALLEL_TESTS
));
407 * Returns the physical processor count.
409 * @return The physical processor count.
412 @SuppressWarnings("CallToSystemGetenv")
413 public static int physicalProcessorCount()
415 // Use pre-cached value if it is already known
416 int rv
= VMTestTaskAction
._CACHED_CPU_COUNT
;
420 // If this variable is set, just force all CPUs to be used
421 if (Boolean
.parseBoolean(System
.getenv("USE_ALL_PROCESSORS")))
422 return Math
.max(1, Runtime
.getRuntime().availableProcessors());
424 // We need this so we can make a better guess of our current system
425 String osName
= System
.getProperty("os.name").toLowerCase();
427 // Running on Windows
428 if (rv
<= 0 && osName
.contains("windows"))
429 rv
= VMTestTaskAction
.__cpuCountOnWindows();
431 // Fallback to a generic CPU count
433 rv
= Math
.max(1, Runtime
.getRuntime().availableProcessors() / 2);
435 // Cache it and use it
436 VMTestTaskAction
._CACHED_CPU_COUNT
= rv
;
441 * Initializes the suite parameters.
443 * @param __task The task.
444 * @param __classifier The classifier used.
445 * @return The parameters to run all tests with.
446 * @throws NullPointerException On null arguments.
449 public static SuiteRunParameters
runSuite(VMBaseTask __task
,
450 SourceTargetClassifier __classifier
)
451 throws NullPointerException
453 if (__task
== null || __classifier
== null)
454 throw new NullPointerException("NARG");
457 SuiteRunParameters
.SuiteRunParametersBuilder builder
=
458 SuiteRunParameters
.builder();
460 // Determine system properties to use for testing
461 Map
<String
, String
> sysProps
= new LinkedHashMap
<>();
462 if (Boolean
.getBoolean("java.awt.headless"))
463 sysProps
.put("java.awt.headless", "true");
465 // Any specific changes to how tests run
466 SquirrelJMEPluginConfiguration config
=
467 SquirrelJMEPluginConfiguration
.configurationOrNull(
468 __task
.getProject());
471 // If we define any system properties specifically for tests then
472 // use them here. Could be used for debugging.
473 sysProps
.putAll(config
.testSystemProperties
);
475 // Disable parallelism for these tests?
476 builder
.noParallelTests(config
.noParallelTests
);
479 // Can we directly refer to the emulator library already?
480 Path emuLib
= VMHelpers
.findEmulatorLib(__task
.getProject());
481 if (emuLib
!= null && Files
.exists(emuLib
))
482 sysProps
.put("squirreljme.emulator.libpath", emuLib
.toString());
484 // Setup parameters and build
487 .emuLib(new SerializedPath(emuLib
))
488 .uniqueId(UUID
.randomUUID().toString())
489 .classPath(SerializedPath
.boxPaths(VMHelpers
.runClassPath(
490 __task
, __classifier
, true)))
495 * Determines the command line that is used to run tests.
497 * @param __task The task this is for.
498 * @param __classifier The classifier used.
499 * @param __runSuite The existing run suite.
500 * @param __testName The name of the test.
501 * @param __candidate The test candidate, for test information.
502 * @return The parameters for running this test.
503 * @throws NullPointerException On null arguments.
506 public static TestRunParameters
runTest(VMBaseTask __task
,
507 SourceTargetClassifier __classifier
,
508 SuiteRunParameters __runSuite
, String __testName
,
509 CandidateTestFiles __candidate
)
510 throws NullPointerException
512 if (__task
== null || __classifier
== null ||
513 __runSuite
== null || __testName
== null || __candidate
== null)
514 throw new NullPointerException("NARG");
516 TestRunParameters
.TestRunParametersBuilder builder
=
517 TestRunParameters
.builder();
519 // Default arguments, could be replaced by a proxy main
520 String mainClass
= VMHelpers
.SINGLE_TEST_RUNNER
;
521 String
[] mainArgs
= new String
[]{__testName
};
523 // Are we going to use a different proxy main class for this?
524 String proxyMain
= __candidate
.expectedValues
.get("proxy-main");
525 if (proxyMain
!= null && !proxyMain
.trim().isEmpty())
527 mainArgs
= new String
[]{mainClass
, mainArgs
[0]};
528 mainClass
= proxyMain
.trim();
531 // Deserialize classpath
532 Path
[] classPath
= SerializedPath
.unboxPaths(__runSuite
.classPath
);
534 Map
<String
, String
> sysProps
= new LinkedHashMap
<>(
535 __runSuite
.getSysProps());
536 if (Boolean
.parseBoolean(
537 __candidate
.expectedValues
.get("test-vm-target")))
538 sysProps
.put("cc.squirreljme.test.vm",
539 __classifier
.getBangletVariant().banglet
);
541 // Print test result manifest?
542 if (Boolean
.getBoolean(VMTestTaskAction
.PRINT_TEST_MANIFEST
) ||
543 (__task
.hasProperty(VMTestTaskAction
.PRINT_TEST_MANIFEST
) &&
544 Boolean
.parseBoolean(Objects
.toString(
545 __task
.property(VMTestTaskAction
.PRINT_TEST_MANIFEST
)))))
546 sysProps
.put(VMTestTaskAction
.PRINT_TEST_MANIFEST
, "true");
548 // Determine the arguments that are used to spawn the JVM
549 JavaExecSpecFiller execSpec
= new SimpleJavaExecSpecFiller();
550 __classifier
.getVmType().spawnJvmArguments(__task
.getProject(),
552 execSpec
, mainClass
, __testName
, sysProps
,
553 classPath
, classPath
, mainArgs
);
556 List
<String
> commandLine
= new ArrayList
<>();
557 for (String arg
: execSpec
.getCommandLine())
558 commandLine
.add(arg
);
560 // Build final result
562 .commandLine(commandLine
)
567 * Attempts to get the physical CPU count on Windows.
569 * @return The physical CPU count on Windows, {@code 0} is returned if it
570 * could not be obtained.
573 @SuppressWarnings("UseOfProcessBuilder")
574 private static int __cpuCountOnWindows()
576 // This information could be obtained by running a specific program
579 // Spawn new process that will contain the CPU count
580 Process proc
= new ProcessBuilder()
581 .command("cmd", "/C", "WMIC", "CPU", "Get", "/Format:List")
582 .redirectError(ProcessBuilder
.Redirect
.INHERIT
)
585 // Let the process run accordingly
588 // Command failed to start, so likely unreliable
589 if (proc
.waitFor() != 0)
593 // We probably want the process to go away if this happens
594 catch (InterruptedException ignored
)
596 proc
.destroyForcibly();
598 // The CPU count will be invalid here
602 // Look for the CPU information lines
604 try (BufferedReader br
= new BufferedReader(new InputStreamReader(
605 proc
.getInputStream(), StandardCharsets
.UTF_8
)))
609 String ln
= br
.readLine();
614 // Number of cores in the system
615 if (ln
.startsWith("NumberOfCores="))
616 numCores
= Integer
.parseInt(
617 ln
.substring(ln
.indexOf('=') + 1));
621 // Were we able to glean the CPU count?
622 return Math
.max(numCores
, 0);
626 catch (IOException
|NumberFormatException e
)
629 new RuntimeException("Could not determine physical CPU count.", e
)
637 * Returns the simple duration of the test.
639 * @param __dur The nanoseconds to map.
640 * @return The simple duration string.
643 @SuppressWarnings("MagicNumber")
644 private static String
__simpleDuration(long __dur
)
646 // Instantly finished?
650 StringBuilder sb
= new StringBuilder();
653 long ns
= __dur
% 1_000_000L
;
654 sb
.insert(0, String
.format("%dns", ns
));
658 long ms
= __dur
% 1_000L
;
660 sb
.insert(0, String
.format("%dms ", ms
));
664 long s
= __dur
% 60L;
666 sb
.insert(0, String
.format("%ds ", s
));
670 long m
= __dur
& 60L;
672 sb
.insert(0, String
.format("%dm ", m
));
674 return sb
.toString();