View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2026 Christian Kohlschütter
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.newsclub.net.unix.selftest;
19  
20  import java.io.File;
21  import java.io.FileInputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.InputStreamReader;
25  import java.io.OutputStreamWriter;
26  import java.io.PipedInputStream;
27  import java.io.PipedOutputStream;
28  import java.io.PrintStream;
29  import java.io.PrintWriter;
30  import java.io.StringWriter;
31  import java.io.Writer;
32  import java.nio.charset.Charset;
33  import java.nio.charset.StandardCharsets;
34  import java.nio.file.Files;
35  import java.nio.file.Path;
36  import java.time.Duration;
37  import java.util.ArrayList;
38  import java.util.Arrays;
39  import java.util.Collections;
40  import java.util.HashSet;
41  import java.util.LinkedHashMap;
42  import java.util.LinkedHashSet;
43  import java.util.List;
44  import java.util.Locale;
45  import java.util.Map;
46  import java.util.Map.Entry;
47  import java.util.Objects;
48  import java.util.Optional;
49  import java.util.Properties;
50  import java.util.Set;
51  import java.util.TreeMap;
52  import java.util.TreeSet;
53  import java.util.function.Supplier;
54  
55  import org.junit.jupiter.engine.JupiterTestEngine;
56  import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver;
57  import org.junit.platform.engine.TestExecutionResult;
58  import org.junit.platform.engine.support.descriptor.EngineDescriptor;
59  import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine;
60  import org.junit.platform.launcher.TestIdentifier;
61  import org.junit.platform.launcher.listeners.TestExecutionSummary;
62  import org.newsclub.net.unix.AFSocket;
63  import org.newsclub.net.unix.AFSocketCapability;
64  import org.newsclub.net.unix.AFUNIXSocket;
65  
66  import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
67  import com.kohlschutter.testutil.TestAbortedNotAnIssueException;
68  import com.kohlschutter.testutil.TestAbortedWithImportantMessageException;
69  import com.kohlschutter.testutil.TestAbortedWithImportantMessageException.MessageType;
70  import com.kohlschutter.util.ConsolePrintStream;
71  import com.kohlschutter.util.ProcessUtil;
72  import com.kohlschutter.util.SystemPropertyUtil;
73  
74  /**
75   * Performs a series of self-tests.
76   *
77   * Specifically, we run all unit tests of junixsocket-core and junixsocket-rmi.
78   *
79   * NOTE: The Selftest will fail when run from within Eclipse due to test classes not being present.
80   * Invoke via <code>java -jar junixsocket-selftest-...-jar-with-dependencies.jar</code>.
81   *
82   * @author Christian Kohlschütter
83   */
84  @SuppressWarnings({
85      "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", "PMD.CouplingBetweenObjects",
86      "PMD.ExcessiveImports"})
87  @SuppressFBWarnings({"PATH_TRAVERSAL_IN", "INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE"})
88  public class Selftest {
89    private final Class<?> diagnosticsHelperClass = resolveOptionalClass(
90        "org.newsclub.net.unix.SelftestDiagnosticsHelper");
91    private final ConsolePrintStream out;
92    private final Map<String, ModuleResult> results = new LinkedHashMap<>();
93    private final List<AFSocketCapability> supportedCapabilites = new ArrayList<>();
94    private final List<AFSocketCapability> unsupportedCapabilites = new ArrayList<>();
95    private boolean withIssues = false;
96    private boolean fail = false;
97    private boolean modified = false;
98    private boolean isSupportedAFUNIX = false;
99    private final Set<String> important = new LinkedHashSet<>();
100   private boolean inconclusive = false;
101   private final SelftestProvider sp;
102 
103   private enum Result {
104     AUTOSKIP, SKIP, PASS, DONE, NONE, FAIL
105   }
106 
107   private enum SkipMode {
108     UNDECLARED(false), KEEP(false), SKIP(true), SKIP_FORCE(true), SKIP_AUTO(true);
109 
110     final boolean skip;
111 
112     SkipMode(boolean skip) {
113       this.skip = skip;
114     }
115 
116     boolean isSkip() {
117       return skip;
118     }
119 
120     boolean isDeclared() {
121       return this != UNDECLARED;
122     }
123 
124     boolean isForce() {
125       return this == SKIP_FORCE || this == SKIP_AUTO;
126     }
127 
128     public static SkipMode parse(String skipMode) {
129       if (skipMode == null || skipMode.isEmpty()) {
130         return SkipMode.UNDECLARED;
131       } else if ("force".equalsIgnoreCase(skipMode)) {
132         return SkipMode.SKIP_FORCE;
133       } else if ("force_auto".equalsIgnoreCase(skipMode)) {
134         return SkipMode.SKIP_AUTO;
135       } else {
136         return Boolean.parseBoolean(skipMode) ? SkipMode.SKIP : SkipMode.KEEP;
137       }
138     }
139 
140   }
141 
142   /**
143    * maven-shade-plugin's minimizeJar isn't perfect, so we give it a little hint by adding static
144    * references to classes that are otherwise only found via reflection.
145    *
146    * @author Christian Kohlschütter
147    */
148   @SuppressFBWarnings("UUF_UNUSED_FIELD")
149   static final class MinimizeJarDependencies {
150     JupiterTestEngine jte;
151     HierarchicalTestEngine<?> hte;
152     EngineDescriptor ed;
153     DiscoverySelectorResolver dsr;
154     org.newsclub.lib.junixsocket.common.NarMetadata nmCommon;
155     org.newsclub.lib.junixsocket.custom.NarMetadata nmCustom;
156   }
157 
158   public Selftest(PrintStream out, SelftestProvider sp) {
159     System.setProperty("com.kohlschutter.selftest", getClass().getName());
160 
161     this.out = ConsolePrintStream.wrapPrintStream(out);
162     this.sp = sp;
163 
164     checkSystemProperties();
165   }
166 
167   private void checkSystemProperties() {
168     String tmpDir = System.getProperty("java.io.tmpdir", "");
169     if (System.getProperty("java.home", "").isEmpty()) {
170       System.setProperty("java.home", tmpDir);
171       out.println("Setting java.home to temporary directory: " + tmpDir);
172     }
173   }
174 
175   public void checkVM() {
176     boolean isSubstrateVM = "Substrate VM".equals(System.getProperty("java.vm.name"));
177 
178     if (isSubstrateVM) {
179       important.add("Substrate VM detected: Support for native-images is work in progress");
180 
181       String vendorVersion = System.getProperty("java.vendor.version", "");
182       if (vendorVersion.contains("GraalVM 20") || vendorVersion.contains("GraalVM 19")) {
183         if (!getSkipModeForModule("junixsocket-rmi").isDeclared()) {
184           important.add("Auto-skipping junixsocket-rmi tests due to old Substrate VM");
185           System.setProperty("selftest.skip.junixsocket-rmi", "force_auto");
186           withIssues = true;
187         }
188 
189         if (!getSkipModeForClass("org.newsclub.net.unix.FileDescriptorCastTest").isDeclared()) {
190           important.add("Auto-skipping FileDescriptorCastTest tests due to Substrate VM");
191           System.setProperty("selftest.skip.FileDescriptorCastTest", "force_auto");
192           withIssues = true;
193         }
194       }
195     } else {
196       if (!getSkipModeForModule("junixsocket-rmi").isDeclared()) {
197         try {
198           Class.forName("java.rmi.Remote");
199         } catch (ClassNotFoundException e) {
200           important.add("Auto-skipping junixsocket-rmi tests due to java.rmi.Remote class missing");
201           System.setProperty("selftest.skip.junixsocket-rmi", "force_auto");
202           withIssues = true;
203         }
204 
205         if (!AFSocket.supports(AFSocketCapability.CAPABILITY_LARGE_PORTS)) {
206           important.add(
207               "Auto-skipping junixsocket-rmi tests due to missing CAPABILITY_LARGE_PORTS");
208           System.setProperty("selftest.skip.junixsocket-rmi", "force_auto");
209           withIssues = true;
210         }
211       }
212     }
213   }
214 
215   /**
216    * Run this from the command line to ensure junixsocket works correctly on the target system.
217    *
218    * A zero error code indicates success.
219    *
220    * @param args Ignored.
221    * @throws Exception on error.
222    */
223   public static void main(String[] args) throws Exception {
224     int delay = SystemPropertyUtil.getIntSystemProperty("selftest.delay.at-start", 0);
225     if (delay > 0) {
226       System.out.println("Delaying execution of selftest by " + delay + " seconds");
227       Thread.sleep(Duration.ofSeconds(delay).toMillis());
228     }
229 
230     int rc = runSelftest();
231 
232     if (SystemPropertyUtil.getBooleanSystemProperty("selftest.wait.at-end", false)) {
233       System.gc(); // NOPMD
234       System.out.print("Press any key to end test. ");
235       System.out.flush();
236       System.in.read();
237       System.out.println("RC=" + rc);
238     }
239     System.out.flush();
240 
241     System.exit(rc); // NOPMD
242   }
243 
244   /**
245    * Run this from some other Java code to ensure junixsocket works correctly on the target system.
246    *
247    * A zero return value indicates success.
248    *
249    * @throws Exception on error.
250    */
251   public static int runSelftest() throws Exception {
252     return runSelftest(System.out);
253   }
254 
255   private static void printStackTrace(Throwable t) {
256     t.printStackTrace();
257   }
258 
259   public static int runSelftest(Writer out) throws Exception {
260     PipedInputStream pis = new PipedInputStream();
261     @SuppressWarnings("all")
262     PipedOutputStream pos = new PipedOutputStream(pis);
263     @SuppressWarnings("all")
264     PrintStream ps = new PrintStream(pos, false, Charset.defaultCharset().name());
265 
266     InputStreamReader isr = new InputStreamReader(pis, Charset.defaultCharset());
267 
268     Thread t = new Thread(new Runnable() {
269       @Override
270       public void run() {
271         char[] buf = new char[4096];
272         int read;
273         try {
274           while ((read = isr.read(buf)) >= 0) {
275             out.write(buf, 0, read);
276             out.flush();
277           }
278         } catch (IOException e) {
279           printStackTrace(e);
280         }
281       }
282     });
283     t.start();
284 
285     return runSelftest0(ps, () -> {
286       ps.close();
287       try {
288         t.join();
289       } catch (InterruptedException e) {
290         printStackTrace(e);
291       }
292     });
293   }
294 
295   public static int runSelftest(PrintStream out) throws Exception {
296     return runSelftest0(out, null);
297   }
298 
299   private static int runSelftest0(PrintStream out, Runnable whenDone) throws Exception {
300     int rc;
301     PrintStream origSystemOut = System.out;
302     System.setOut(out);
303     try {
304       rc = runSelftest0(out);
305     } finally {
306       out.flush();
307       System.setOut(origSystemOut);
308       if (whenDone != null) {
309         whenDone.run();
310       }
311     }
312     return rc;
313   }
314 
315   private static int runSelftest0(PrintStream out) throws Exception {
316     SelftestProvider sp = new SelftestProvider();
317     Selftest st = new Selftest(out, sp);
318 
319     st.checkVM();
320     st.printExplanation();
321     st.dumpAdditionalProperties();
322     st.dumpSystemProperties();
323     st.dumpOSReleaseFiles();
324     st.dumpPid();
325     st.checkSupported();
326     st.checkCapabilities();
327 
328     Set<String> disabledModules = sp.modulesDisabledByDefault();
329     List<String> messagesAtEnd = new ArrayList<>();
330 
331     boolean skipModules = false;
332     String only = System.getProperty("selftest.only", "");
333     if (!only.isEmpty()) {
334       if ("mini".equals(only)) {
335         skipModules = true;
336         st.important.add("Selftest was modified, only a mini-selftest was run");
337         st.inconclusive = true;
338         AFUNIXSocket.main(new String[0]);
339       }
340     }
341 
342     if (!skipModules) {
343       for (Entry<String, Class<?>[]> en : sp.tests().entrySet()) {
344         String module = en.getKey();
345         if (disabledModules.contains(module)) {
346           if (SystemPropertyUtil.getBooleanSystemProperty("selftest.enable-module." + module,
347               false)) {
348             out.println("Enabling optional module: " + module
349                 + " (consult documentation for errors)");
350             st.modified = true;
351           } else {
352             messagesAtEnd.add("Skipping optional module: " + module
353                 + "; enable by launching with -Dselftest.enable-module." + module
354                 + "=true (consult documentation first)");
355             continue;
356           }
357         } else if (SystemPropertyUtil.getBooleanSystemProperty("selftest.disable-module." + module,
358             false)) {
359           messagesAtEnd.add("Skipping required module: " + module + "; this taints the test");
360           st.withIssues = true;
361           st.results.put(module, new ModuleResult(Result.SKIP, null, null));
362           continue;
363         }
364         try {
365           st.runTests(module, en.getValue());
366         } catch (Error | RuntimeException t) { // NOPMD
367           messagesAtEnd.add("INTERNAL INCONSISTENCY: Unexpected error while running tests for  "
368               + module + ": " + t);
369           t.printStackTrace();
370           st.fail = true;
371         }
372       }
373     }
374 
375     if (!messagesAtEnd.isEmpty()) {
376       for (String m : messagesAtEnd) {
377         out.println(m);
378       }
379     }
380 
381     st.checkInitError();
382     st.dumpResults();
383 
384     int rc = st.isFail() ? 1 : 0;
385 
386     out.flush();
387     return rc;
388   }
389 
390   private void dumpPid() {
391     String pid;
392     try {
393       pid = Long.toString(ProcessUtil.getPid());
394     } catch (Exception e) {
395       pid = "(unknown)";
396     }
397     out.println("Selftest process PID: " + pid);
398     String javaCmd = ProcessUtil.getJavaCommand();
399     String[] javaArgs = ProcessUtil.getJavaCommandArguments();
400     if (javaCmd != null && javaArgs != null) {
401       out.println("Selftest process command: " + javaCmd + " " + Arrays.toString(javaArgs));
402     }
403     out.println();
404   }
405 
406   private void dumpAdditionalProperties() {
407     PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, Charset.defaultCharset()));
408     sp.printAdditionalProperties(pw);
409     pw.flush();
410     out.println();
411   }
412 
413   public void printExplanation() throws IOException {
414     out.println(
415         "This program determines whether junixsocket is supported on the current platform.");
416     out.println("The final line should say whether the selftest passed or failed.");
417     out.println();
418     out.println(
419         "If the selftest failed, please visit https://github.com/kohlschutter/junixsocket/issues");
420     out.println("and file a new bug report with the output below.");
421     out.println();
422     out.println("junixsocket selftest version " + AFUNIXSocket.getVersion());
423 
424     Map<String, String> buildProperties = new LinkedHashMap<>(retrieveBuildProperties());
425     try (InputStream in = getClass().getResourceAsStream(
426         "/META-INF/maven/com.kohlschutter.junixsocket/junixsocket-selftest/git.properties")) {
427       if (in != null) {
428         Properties props = new Properties();
429         props.load(in);
430         for (String key : new TreeSet<>(props.stringPropertyNames())) {
431           buildProperties.put(key, props.getProperty(key));
432         }
433       }
434     }
435     out.println();
436     out.println("Build properties:");
437     for (Map.Entry<String, String> en : buildProperties.entrySet()) {
438       out.println(en.getKey() + ": " + en.getValue());
439     }
440     out.println();
441   }
442 
443   public void dumpSystemProperties() {
444     Map<Object, Object> map = new TreeMap<>(System.getProperties());
445     // NOTE: Some environments, such as Android, do not enumerate all available properties upon
446     // calling System.getProperties(). Let's make sure we catch the most important properties
447     // by looking them up manually, which seems to work.
448 
449     // https://github.com/AndroidSDKSources/android-sdk-sources-for-api-level-33/blob/master/
450     // java/lang/System.java
451     // java/lang/AndroidHardcodedSystemProperties.java
452     for (String expectedKey : new String[] {
453         "android.icu.library.version", //
454         "android.icu.unicode.version", //
455         "android.icu.cldr.version", //
456         "ICUDebug", //
457         "android.icu.text.DecimalFormat.SkipExtendedSeparatorParsing", //
458         "android.icu.text.MessagePattern.ApostropheMode", //
459         "sun.io.useCanonCaches", //
460         "sun.io.useCanonPrefixCache", //
461         "sun.stdout.encoding", //
462         "sun.stderr.encoding", //
463         "http.keepAlive", //
464         "http.keepAliveDuration", //
465         "http.maxConnections", //
466         "javax.net.debug", //
467         "com.sun.security.preserveOldDCEncoding", //
468         "java.util.logging.manager", //
469         //
470         "file.encoding", //
471         "file.separator", //
472         "line.separator", //
473         "path.separator", //
474         "java.boot.class.path", //
475         "java.class.path", //
476         "java.class.version", //
477         "java.compiler", //
478         "java.ext.dirs", //
479         "java.home", //
480         "java.io.tmpdir", //
481         "java.library.path", //
482         "java.vendor", //
483         "java.vendor.url", //
484         "java.version", //
485         "java.net.preferIPv6Addresses", //
486         "java.specification.version", //
487         "java.specification.vendor", //
488         "java.specification.name", //
489         "java.vm.version", //
490         "java.vm.vendor", //
491         "java.vm.vendor.url", //
492         "java.vm.name", //
493         "java.vm.specification.version", //
494         "java.vm.specification.vendor", //
495         "java.vm.specification.name", //
496         "os.arch", //
497         "os.name", //
498         "os.version", //
499         "user.dir", //
500         "user.home", //
501         "user.language", //
502         "user.region", //
503         "user.variant", //
504         "user.name" //
505     }) {
506       if (!map.containsKey(expectedKey)) {
507         String value = System.getProperty(expectedKey);
508         if (value != null) {
509           map.put(expectedKey, value);
510         }
511       }
512     }
513 
514     out.println("System properties:");
515     out.println();
516     for (Map.Entry<Object, Object> en : map.entrySet()) {
517       String key = String.valueOf(en.getKey());
518       String value = String.valueOf(en.getValue());
519       StringBuilder sb = new StringBuilder();
520       for (int i = 0; i < value.length(); i++) {
521         char c = value.charAt(i);
522         switch (c) {
523           case '\n':
524             sb.append("\\n");
525             break;
526           case '\r':
527             sb.append("\\r");
528             break;
529           case '\t':
530             sb.append("\\r");
531             break;
532           default:
533             if (c < 32 || c >= 127) {
534               sb.append(String.format(Locale.ENGLISH, "\\u%04x", (int) c));
535             }
536             sb.append(c);
537             break;
538         }
539       }
540       out.println(key + ": " + sb.toString());
541     }
542     out.println();
543   }
544 
545   public void checkSupported() {
546     out.print("AFSocket.isSupported: ");
547     out.flush();
548 
549     boolean isSupported = AFSocket.isSupported();
550     out.println(isSupported);
551     out.println();
552     out.flush();
553 
554     if (!isSupported) {
555       out.println("FAIL: junixsocket is not supported on this platform");
556       out.println();
557       fail = true;
558     }
559 
560     out.print("AFUNIXSocket.isSupported: ");
561     out.flush();
562 
563     isSupportedAFUNIX = AFUNIXSocket.isSupported();
564     out.println(isSupportedAFUNIX);
565     out.println();
566     out.flush();
567 
568     if (!isSupportedAFUNIX) {
569       out.println("WARNING: AF_UNIX sockets are not supported on this platform");
570       out.println();
571       withIssues = true;
572     }
573   }
574 
575   public void checkCapabilities() {
576     for (AFSocketCapability cap : AFSocketCapability.values()) {
577       boolean supported = AFSocket.supports(cap);
578       (supported ? supportedCapabilites : unsupportedCapabilites).add(cap);
579     }
580   }
581 
582   /**
583    * Checks if any test has failed so far.
584    *
585    * @return {@code true} if failed.
586    */
587   public boolean isFail() {
588     return fail;
589   }
590 
591   private void checkInitError() {
592     Throwable t = retrieveInitError();
593     if (t == null) {
594       return;
595     }
596 
597     important.add("The native library failed to load.");
598 
599     StringWriter sw = new StringWriter();
600     PrintWriter pw = new PrintWriter(sw);
601     t.printStackTrace(pw);
602     pw.flush();
603     String ts = sw.toString();
604     String tsLower = ts.toLowerCase(Locale.ENGLISH);
605 
606     if (tsLower.contains("not permitted") || ts.contains("permission")) {
607       important.add("It looks like there were some permission errors.");
608     }
609 
610     if (tsLower.contains("failed to map segment")) {
611       important.add("Your temporary directory is probably mounted with \"noexec\", "
612           + "which prevents the native library from loading.");
613       important.add("see: https://github.com/kohlschutter/junixsocket/issues/99");
614       Object tmpDir = retrieveTempDir();
615       if (tmpDir == null) {
616         tmpDir = System.getProperty("java.io.tmpdir");
617       }
618       if (tmpDir != null) {
619         important.add("Temp dir: " + tmpDir);
620       }
621       important.add(
622           "You can specify a different directory using -Dorg.newsclub.net.unix.library.tmpdir=");
623     }
624   }
625 
626   /**
627    * Dumps the results of the selftest.
628    *
629    */
630   public void dumpResults() { // NOPMD
631     if (modified) {
632       important.add("Selftest was modified, for example to exclude/include certain tests.");
633       inconclusive = true;
634     }
635     if (!isSupportedAFUNIX) {
636       important.add(
637           "Environment does not support UNIX sockets, which is an important part of junixsocket.");
638       // inconclusive = true;
639     }
640     if (inconclusive) {
641       important.add("Selftest results may be inconclusive.");
642     }
643 
644     if (withIssues) {
645       important.add("\"With issues\": "
646           + "Please carefully check the output above; the software may not be able to do what you want.");
647     }
648 
649     out.println();
650     out.println("Selftest results:");
651 
652     for (Map.Entry<String, ModuleResult> en : results.entrySet()) {
653       ModuleResult res = en.getValue();
654 
655       String result = res == null ? null : res.result.name();
656       String extra;
657       if (res == null || ((res.result == Result.SKIP || res.result == Result.AUTOSKIP)
658           && res.throwable == null)) {
659         result = "SKIP";
660         if (res != null && res.result == Result.AUTOSKIP) {
661           extra = "(skipped automatically)";
662         } else {
663           extra = "(skipped by user request)";
664         }
665       } else if (res.summary == null) {
666         extra = res.throwable == null ? "(unknown error)" : res.throwable.toString();
667         fail = true;
668       } else {
669         TestExecutionSummary summary = res.summary;
670 
671         long nSucceeded = (summary.getTestsSucceededCount() + res.getNumAbortedNonIssues());
672         extra = nSucceeded + "/" + summary.getTestsFoundCount();
673         long nSkipped = summary.getTestsSkippedCount();
674         if (nSkipped > 0) {
675           extra += " (" + nSkipped + " skipped)";
676         }
677         long failures = summary.getTestsFailedCount();
678         if (failures > 0) {
679           extra += " failures: " + failures + "/" + summary.getTestsStartedCount();
680         }
681       }
682 
683       out.println(result + "\t" + en.getKey() + "\t" + extra);
684     }
685     out.println();
686 
687     if (!important.isEmpty()) {
688       for (String l : important) {
689         out.println("IMPORTANT: " + l);
690       }
691       out.println();
692     }
693 
694     out.println("Supported capabilities:   " + supportedCapabilites);
695     out.println("Unsupported capabilities: " + unsupportedCapabilites);
696     out.println();
697 
698     if (fail) {
699       out.println("Selftest FAILED");
700     } else if (inconclusive || modified) {
701       out.println("Selftest INCONCLUSIVE");
702     } else if (withIssues) {
703       out.println("Selftest PASSED WITH ISSUES");
704     } else {
705       out.println("Selftest PASSED");
706     }
707   }
708 
709   private SkipMode getSkipModeForModule(String moduleName) {
710     return SkipMode.parse(System.getProperty("selftest.skip." + moduleName));
711   }
712 
713   private SkipMode getSkipModeForClass(String className) {
714     SkipMode skipMode = SkipMode.parse(System.getProperty("selftest.skip." + className));
715     if (skipMode.isDeclared()) {
716       return skipMode;
717     }
718     int i = className.lastIndexOf('.');
719     if (i < 0) {
720       return SkipMode.UNDECLARED;
721     }
722 
723     className = className.substring(i + 1);
724     return SkipMode.parse(System.getProperty("selftest.skip." + className));
725   }
726 
727   /**
728    * Runs the given test classes for the specified module.
729    *
730    * @param module The module name.
731    * @param testClasses The test classes.
732    */
733   @SuppressWarnings({"PMD.ExcessiveMethodLength", "PMD.NcssCount", "PMD.NPathComplexity"})
734   public void runTests(String module, Class<?>[] testClasses) {
735     String prefix = "Testing \"" + module + "\"... ";
736     out.markPosition();
737     out.update(prefix);
738     out.flush();
739 
740     String only = System.getProperty("selftest.only", "");
741     if (!only.isEmpty()) {
742       modified = true;
743     }
744 
745     final ModuleResult moduleResult;
746 
747     SkipMode skipMode;
748 
749     if ((skipMode = getSkipModeForModule(module)).isSkip()) {
750       boolean autoSkip = skipMode == SkipMode.SKIP_AUTO;
751 
752       out.println("Skipping module " + module + "; skipped " + (autoSkip ? "automatically"
753           : "by user request" + (skipMode.isForce() ? " (force)" : "")));
754       if (!skipMode.isForce()) {
755         withIssues = true;
756         modified = true;
757       }
758       moduleResult = new ModuleResult(autoSkip ? Result.AUTOSKIP : Result.SKIP, null, null);
759     } else {
760       List<Class<?>> list = new ArrayList<>(testClasses.length);
761       for (Class<?> testClass : testClasses) {
762         if (testClass == null) {
763           // ignore
764           continue;
765         }
766         String className = testClass.getName();
767         String simpleName = testClass.getSimpleName();
768 
769         if (!only.isEmpty() && !only.equals(className) && !only.equals(simpleName)) {
770           continue;
771         }
772 
773         if ((skipMode = getSkipModeForClass(className)).isSkip()) {
774           out.println("Skipping test class " + className + "; skipped by request" + (skipMode
775               .isForce() ? " (force)" : ""));
776           if (!skipMode.isForce()) {
777             modified = true;
778             withIssues = true;
779           }
780         } else {
781           list.add(testClass);
782         }
783       }
784 
785       TestExecutionSummary summary = null;
786       Exception exception = null;
787       long numAbortedNonIssues = 0;
788       try {
789         SelftestExecutor ex = new SelftestExecutor(list, prefix);
790         summary = ex.execute(out);
791 
792         for (Map.Entry<TestIdentifier, TestExecutionResult> en : ex.getTestsWithWarnings()
793             .entrySet()) {
794           TestIdentifier tid = en.getKey();
795           TestExecutionResult res = en.getValue();
796           Optional<Throwable> t = res.getThrowable();
797           if (!t.isPresent()) {
798             continue;
799           }
800           Throwable throwable = t.get();
801           if (throwable instanceof TestAbortedWithImportantMessageException) {
802             String key = module + ": " + ex.getTestIdentifier(tid.getParentId().get())
803                 .getDisplayName() + "." + tid.getDisplayName();
804             TestAbortedWithImportantMessageException ime =
805                 (TestAbortedWithImportantMessageException) t.get();
806 
807             MessageType messageType = ime.messageType();
808             if (messageType.isIncludeTestInfo()) {
809               important.add(ime.getSummaryMessage() + "; " + key);
810             } else {
811               String msg = ime.getSummaryMessage();
812               if (!msg.isEmpty()) {
813                 important.add(msg);
814               }
815             }
816             if (!messageType.isWithIssues()) {
817               numAbortedNonIssues++;
818             }
819           } else if (throwable instanceof TestAbortedNotAnIssueException) {
820             numAbortedNonIssues++;
821           }
822         }
823       } catch (Exception e) {
824         e.printStackTrace(out);
825         exception = e;
826       }
827 
828       if (exception != null || summary == null) {
829         moduleResult = new ModuleResult(Result.FAIL, null, exception);
830         fail = true;
831       } else {
832         final Result result;
833         if (summary.getTestsFailedCount() > 0) {
834           result = Result.FAIL;
835           fail = true;
836         } else if (summary.getTestsFoundCount() == 0) {
837           result = Result.NONE;
838         } else if ((summary.getTestsSucceededCount() + summary.getTestsSkippedCount()
839             + numAbortedNonIssues) == summary.getTestsFoundCount()) {
840           result = Result.PASS;
841         } else if (summary.getTestsAbortedCount() > 0) {
842           result = Result.DONE;
843           withIssues = true;
844         } else {
845           result = Result.DONE;
846         }
847 
848         moduleResult = new ModuleResult(result, summary, null);
849         moduleResult.numAbortedNonIssues = numAbortedNonIssues;
850       }
851     }
852     results.put(module, moduleResult);
853   }
854 
855   private void dumpContentsOfSystemConfigFile(File file) {
856     if (!file.exists()) {
857       return;
858     }
859     String p = file.getAbsolutePath();
860     out.println("BEGIN contents of file: " + p);
861 
862     final int maxToRead = 4096;
863     char[] buf = new char[4096];
864     int numRead = 0;
865     try (InputStreamReader isr = new InputStreamReader(new FileInputStream(file),
866         StandardCharsets.UTF_8);) {
867 
868       OutputStreamWriter outWriter = new OutputStreamWriter(out, Charset.defaultCharset());
869       int read = -1;
870       boolean lastWasNewline = false;
871       while (numRead < maxToRead && (read = isr.read(buf)) != -1) {
872         numRead += read;
873         outWriter.write(buf, 0, read);
874         outWriter.flush();
875         lastWasNewline = (read > 0 && buf[read - 1] == '\n');
876       }
877       if (!lastWasNewline) {
878         out.println();
879       }
880       if (read != -1) {
881         out.println("[...]");
882       }
883     } catch (Exception e) {
884       out.println("ERROR while reading contents of file: " + p + ": " + e);
885     }
886     out.println("=END= contents of file: " + p);
887     out.println();
888   }
889 
890   public void dumpOSReleaseFiles() throws IOException {
891     Set<Path> canonicalPaths = new HashSet<>();
892     for (String f : new String[] {
893         "/etc/os-release", "/etc/lsb-release", "/etc/lsb_release", "/etc/system-release",
894         "/etc/system-release-cpe",
895         //
896         "/etc/debian_version", "/etc/fedora-release", "/etc/redhat-release", "/etc/centos-release",
897         "/etc/centos-release-upstream", "/etc/SuSE-release", "/etc/arch-release",
898         "/etc/gentoo-release", "/etc/ubuntu-release",}) {
899 
900       File file = new File(f);
901       if (!file.exists() || file.isDirectory()) {
902         continue;
903       }
904       Path p = file.toPath().toAbsolutePath();
905       for (int i = 0; i < 2; i++) {
906         if (Files.isSymbolicLink(p)) {
907           Path p2 = Files.readSymbolicLink(p);
908           if (!p2.isAbsolute()) {
909             p = new File(p.toFile().getParentFile(), p2.toString()).toPath().toAbsolutePath();
910           }
911         }
912       }
913 
914       if (!canonicalPaths.add(p)) {
915         continue;
916       }
917 
918       dumpContentsOfSystemConfigFile(file);
919     }
920   }
921 
922   private Throwable retrieveInitError() {
923     return callStaticMethod(diagnosticsHelperClass, "initError", null);
924   }
925 
926   private File retrieveTempDir() {
927     return callStaticMethod(diagnosticsHelperClass, "tempDir", null);
928   }
929 
930   private Map<String, String> retrieveBuildProperties() {
931     return callStaticMethod(diagnosticsHelperClass, "buildProperties", Collections::emptyMap);
932   }
933 
934   private static Class<?> resolveOptionalClass(String name) {
935     try {
936       return Class.forName(name);
937     } catch (Exception e) {
938       return null;
939     }
940   }
941 
942   @SuppressWarnings({"unchecked", "null"})
943   private static <T> T callStaticMethod(Class<?> clazz, String methodName,
944       Supplier<T> defaultSupplier) {
945     try {
946       return (T) clazz.getMethod(methodName).invoke(null);
947     } catch (Exception e) {
948       return defaultSupplier == null ? (T) null : defaultSupplier.get();
949     }
950   }
951 
952   private static final class ModuleResult {
953     private final Result result;
954     private final TestExecutionSummary summary;
955     private final Throwable throwable;
956     private long numAbortedNonIssues = 0;
957 
958     ModuleResult(Result result, TestExecutionSummary summary, Throwable t) {
959       Objects.requireNonNull(result);
960       this.result = result;
961       this.summary = summary;
962       this.throwable = t;
963     }
964 
965     long getNumAbortedNonIssues() {
966       return numAbortedNonIssues;
967     }
968   }
969 }