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