ParallelTestRunner.java revision 1612:0da44ab8c417
1/*
2 * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.internal.test.framework;
27
28import static jdk.nashorn.internal.test.framework.TestConfig.TEST_FAILED_LIST_FILE;
29import static jdk.nashorn.internal.test.framework.TestConfig.TEST_JS_ENABLE_STRICT_MODE;
30import static jdk.nashorn.internal.test.framework.TestConfig.TEST_JS_EXCLUDES_FILE;
31import static jdk.nashorn.internal.test.framework.TestConfig.TEST_JS_EXCLUDE_LIST;
32import static jdk.nashorn.internal.test.framework.TestConfig.TEST_JS_FRAMEWORK;
33import static jdk.nashorn.internal.test.framework.TestConfig.TEST_JS_ROOTS;
34import java.io.BufferedReader;
35import java.io.ByteArrayOutputStream;
36import java.io.File;
37import java.io.FileInputStream;
38import java.io.FileOutputStream;
39import java.io.FileReader;
40import java.io.FileWriter;
41import java.io.IOException;
42import java.io.InputStreamReader;
43import java.io.OutputStream;
44import java.io.PrintStream;
45import java.io.PrintWriter;
46import java.io.StringReader;
47import java.nio.file.FileSystems;
48import java.nio.file.Files;
49import java.nio.file.StandardCopyOption;
50import java.util.ArrayList;
51import java.util.Collections;
52import java.util.Comparator;
53import java.util.List;
54import java.util.Locale;
55import java.util.Map;
56import java.util.Properties;
57import java.util.Set;
58import java.util.TreeSet;
59import java.util.concurrent.Callable;
60import java.util.concurrent.CancellationException;
61import java.util.concurrent.CountDownLatch;
62import java.util.concurrent.ExecutionException;
63import java.util.concurrent.ExecutorService;
64import java.util.concurrent.Executors;
65import java.util.concurrent.Future;
66import java.util.concurrent.TimeUnit;
67import java.util.regex.Matcher;
68import java.util.regex.Pattern;
69import jdk.nashorn.internal.test.framework.TestFinder.TestFactory;
70
71/**
72 * Parallel test runner runs tests in multiple threads - but avoids any dependency
73 * on third-party test framework library such as TestNG.
74 */
75@SuppressWarnings("javadoc")
76public class ParallelTestRunner {
77
78    // ParallelTestRunner-specific
79    private static final String    TEST_JS_THREADS     = "test.js.threads";
80    private static final String    TEST_JS_REPORT_FILE = "test.js.report.file";
81    // test262 does a lot of eval's and the JVM hates multithreaded class definition, so lower thread count is usually faster.
82    private static final int       THREADS = Integer.getInteger(TEST_JS_THREADS, Runtime.getRuntime().availableProcessors() > 4 ? 4 : 2);
83
84    private final List<ScriptRunnable> tests    = new ArrayList<>();
85    private final Set<String>      orphans  = new TreeSet<>();
86    private final ExecutorService  executor = Executors.newFixedThreadPool(THREADS);
87
88    // Ctrl-C handling
89    private final CountDownLatch   finishedLatch = new CountDownLatch(1);
90    private final Thread           shutdownHook  = new Thread() {
91                                                       @Override
92                                                       public void run() {
93                                                           if (!executor.isTerminated()) {
94                                                               executor.shutdownNow();
95                                                               try {
96                                                                   executor.awaitTermination(25, TimeUnit.SECONDS);
97                                                                   finishedLatch.await(5, TimeUnit.SECONDS);
98                                                               } catch (final InterruptedException e) {
99                                                                   // empty
100                                                               }
101                                                           }
102                                                       }
103                                                   };
104
105    public ParallelTestRunner() throws Exception {
106        suite();
107    }
108
109    private static PrintStream outputStream() {
110        final String reportFile = System.getProperty(TEST_JS_REPORT_FILE, "");
111        PrintStream output = System.out;
112
113        if (!reportFile.isEmpty()) {
114            try {
115                output = new PrintStream(new OutputStreamDelegator(System.out, new FileOutputStream(reportFile)));
116            } catch (final IOException e) {
117                System.err.println(e);
118            }
119        }
120
121        return output;
122    }
123
124    public static final class ScriptRunnable extends AbstractScriptRunnable implements Callable<ScriptRunnable.Result> {
125        private final Result                result   = new Result();
126
127        public class Result {
128            private boolean  passed = true;
129            public String    expected;
130            public String    out;
131            public String    err;
132            public Throwable exception;
133
134            public ScriptRunnable getTest() {
135                return ScriptRunnable.this;
136            }
137
138            public boolean passed() {
139                return passed;
140            }
141
142            @Override
143            public String toString() {
144                return getTest().toString();
145            }
146        }
147
148        public ScriptRunnable(final String framework, final File testFile, final List<String> engineOptions, final Map<String, String> testOptions, final List<String> scriptArguments) {
149            super(framework, testFile, engineOptions, testOptions, scriptArguments);
150        }
151
152        @Override
153        protected void log(final String msg) {
154            System.err.println(msg);
155        }
156
157        @Override
158        protected void fail(final String message) {
159            throw new TestFailedError(message);
160        }
161
162        @Override
163        protected void compile() throws IOException {
164            final ByteArrayOutputStream out = new ByteArrayOutputStream();
165            final ByteArrayOutputStream err = new ByteArrayOutputStream();
166            final List<String> args = getCompilerArgs();
167            int errors;
168            try {
169                errors = evaluateScript(out, err, args.toArray(new String[0]));
170            } catch (final AssertionError e) {
171                final PrintWriter writer = new PrintWriter(err);
172                e.printStackTrace(writer);
173                writer.flush();
174                errors = 1;
175            }
176            if (errors != 0 || checkCompilerMsg) {
177                result.err = err.toString();
178                if (expectCompileFailure || checkCompilerMsg) {
179                    final PrintStream outputDest = new PrintStream(new FileOutputStream(getErrorFileName()));
180                    TestHelper.dumpFile(outputDest, new StringReader(new String(err.toByteArray())));
181                    outputDest.println("--");
182                }
183                if (errors != 0 && !expectCompileFailure) {
184                    fail(String.format("%d errors compiling %s", errors, testFile));
185                }
186                if (checkCompilerMsg) {
187                    compare(getErrorFileName(), expectedFileName, true);
188                }
189            }
190            if (expectCompileFailure && errors == 0) {
191                fail(String.format("No errors encountered compiling negative test %s", testFile));
192            }
193        }
194
195        @Override
196        protected void execute() {
197            final List<String> args = getRuntimeArgs();
198            final ByteArrayOutputStream out = new ByteArrayOutputStream();
199            final ByteArrayOutputStream err = new ByteArrayOutputStream();
200
201            try {
202                final int errors = evaluateScript(out, err, args.toArray(new String[0]));
203
204                if (errors != 0 || err.size() > 0) {
205                    if (expectRunFailure) {
206                        return;
207                    }
208                    if (!ignoreStdError) {
209
210                        try (OutputStream outputFile = new FileOutputStream(getOutputFileName()); OutputStream errorFile = new FileOutputStream(getErrorFileName())) {
211                            outputFile.write(out.toByteArray());
212                            errorFile.write(err.toByteArray());
213                        }
214
215                        result.out = out.toString();
216                        result.err = err.toString();
217                        fail(err.toString());
218                    }
219                }
220
221                if (compare) {
222                    final File expectedFile = new File(expectedFileName);
223                    try {
224                        BufferedReader expected;
225                        if (expectedFile.exists()) {
226                            expected = new BufferedReader(new FileReader(expectedFile));
227                        } else {
228                            expected = new BufferedReader(new StringReader(""));
229                        }
230                        compare(new BufferedReader(new StringReader(out.toString())), expected, false);
231                    } catch (final Throwable ex) {
232                        if (expectedFile.exists()) {
233                            copyExpectedFile();
234                        }
235                        try (OutputStream outputFile = new FileOutputStream(getOutputFileName()); OutputStream errorFile = new FileOutputStream(getErrorFileName())) {
236                            outputFile.write(out.toByteArray());
237                            errorFile.write(err.toByteArray());
238                        }
239                        ex.printStackTrace();
240                        throw ex;
241                    }
242                }
243            } catch (final IOException e) {
244                if (!expectRunFailure) {
245                    fail("Failure running test " + testFile + ": " + e.getMessage());
246                } // else success
247            }
248        }
249
250        private void compare(final String fileName, final String expected, final boolean compareCompilerMsg) throws IOException {
251            final File expectedFile = new File(expected);
252
253            BufferedReader expectedReader;
254            if (expectedFile.exists()) {
255                expectedReader = new BufferedReader(new InputStreamReader(new FileInputStream(expectedFileName)));
256            } else {
257                expectedReader = new BufferedReader(new StringReader(""));
258            }
259
260            final BufferedReader actual = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)));
261
262            compare(actual, expectedReader, compareCompilerMsg);
263        }
264
265        private void copyExpectedFile() {
266            if (!new File(expectedFileName).exists()) {
267                return;
268            }
269            // copy expected file overwriting existing file and preserving last
270            // modified time of source
271            try {
272                Files.copy(FileSystems.getDefault().getPath(expectedFileName), FileSystems.getDefault().getPath(getCopyExpectedFileName()), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
273            } catch (final IOException ex) {
274                fail("failed to copy expected " + expectedFileName + " to " + getCopyExpectedFileName() + ": " + ex.getMessage());
275            }
276        }
277
278        @Override
279        public Result call() {
280            try {
281                runTest();
282            } catch (final Throwable ex) {
283                result.exception = ex;
284                result.passed = false;
285                ex.printStackTrace();
286            }
287            return result;
288        }
289
290        private String getOutputFileName() {
291            buildDir.mkdirs();
292            return outputFileName;
293        }
294
295        private String getErrorFileName() {
296            buildDir.mkdirs();
297            return errorFileName;
298        }
299
300        private String getCopyExpectedFileName() {
301            buildDir.mkdirs();
302            return copyExpectedFileName;
303        }
304    }
305
306    private void suite() throws Exception {
307        Locale.setDefault(new Locale(""));
308        System.setOut(outputStream());
309
310        final TestFactory<ScriptRunnable> testFactory = new TestFactory<ScriptRunnable>() {
311            @Override
312            public ScriptRunnable createTest(final String framework, final File testFile, final List<String> engineOptions, final Map<String, String> testOptions, final List<String> arguments) {
313                return new ScriptRunnable(framework, testFile, engineOptions, testOptions, arguments);
314            }
315
316            @Override
317            public void log(final String msg) {
318                System.err.println(msg);
319            }
320        };
321
322        TestFinder.findAllTests(tests, orphans, testFactory);
323
324        Collections.sort(tests, new Comparator<ScriptRunnable>() {
325            @Override
326            public int compare(final ScriptRunnable o1, final ScriptRunnable o2) {
327                return o1.testFile.compareTo(o2.testFile);
328            }
329        });
330    }
331
332    @SuppressWarnings("resource")
333    public boolean run() throws IOException {
334        final int testCount = tests.size();
335        int passCount = 0;
336        int doneCount = 0;
337        System.out.printf("Found %d tests.\n", testCount);
338        final long startTime = System.nanoTime();
339
340        Runtime.getRuntime().addShutdownHook(shutdownHook);
341
342        final List<Future<ScriptRunnable.Result>> futures = new ArrayList<>();
343        for (final ScriptRunnable test : tests) {
344            futures.add(executor.submit(test));
345        }
346
347        executor.shutdown();
348        try {
349            executor.awaitTermination(60, TimeUnit.MINUTES);
350        } catch (final InterruptedException ex) {
351            // empty
352        }
353
354        final List<ScriptRunnable.Result> results = new ArrayList<>();
355        for (final Future<ScriptRunnable.Result> future : futures) {
356            if (future.isDone()) {
357                try {
358                    final ScriptRunnable.Result result = future.get();
359                    results.add(result);
360                    doneCount++;
361                    if (result.passed()) {
362                        passCount++;
363                    }
364                } catch (CancellationException | ExecutionException ex) {
365                    ex.printStackTrace();
366                } catch (final InterruptedException ex) {
367                    assert false : "should not reach here";
368                }
369            }
370        }
371
372        Collections.sort(results, new Comparator<ScriptRunnable.Result>() {
373            @Override
374            public int compare(final ScriptRunnable.Result o1, final ScriptRunnable.Result o2) {
375                return o1.getTest().testFile.compareTo(o2.getTest().testFile);
376            }
377        });
378
379        boolean hasFailed = false;
380        final String failedList = System.getProperty(TEST_FAILED_LIST_FILE);
381        final boolean hasFailedList = failedList != null;
382        final boolean hadPreviouslyFailingTests = hasFailedList && new File(failedList).length() > 0;
383        final FileWriter failedFileWriter = hasFailedList ? new FileWriter(failedList) : null;
384        try {
385            final PrintWriter failedListWriter = failedFileWriter == null ? null : new PrintWriter(failedFileWriter);
386            for (final ScriptRunnable.Result result : results) {
387                if (!result.passed()) {
388                    if (hasFailed == false) {
389                        hasFailed = true;
390                        System.out.println();
391                        System.out.println("FAILED TESTS");
392                    }
393
394                    System.out.println(result.getTest());
395                    if(failedFileWriter != null) {
396                        failedListWriter.println(result.getTest().testFile.getPath());
397                    }
398                    if (result.exception != null) {
399                        final String exceptionString = result.exception instanceof TestFailedError ? result.exception.getMessage() : result.exception.toString();
400                        System.out.print(exceptionString.endsWith("\n") ? exceptionString : exceptionString + "\n");
401                        System.out.print(result.out != null ? result.out : "");
402                    }
403                }
404            }
405        } finally {
406            if(failedFileWriter != null) {
407                failedFileWriter.close();
408            }
409        }
410        final double timeElapsed = (System.nanoTime() - startTime) / 1e9; // [s]
411        System.out.printf("Tests run: %d/%d tests, passed: %d (%.2f%%), failed: %d. Time elapsed: %.0fmin %.0fs.\n", doneCount, testCount, passCount, 100d * passCount / doneCount, doneCount - passCount, timeElapsed / 60, timeElapsed % 60);
412        System.out.flush();
413
414        finishedLatch.countDown();
415
416        if (hasFailed) {
417            throw new AssertionError("TEST FAILED");
418        }
419
420        if(hasFailedList) {
421            new File(failedList).delete();
422        }
423
424        if(hadPreviouslyFailingTests) {
425            System.out.println();
426            System.out.println("Good job on getting all your previously failing tests pass!");
427            System.out.println("NOW re-running all tests to make sure you haven't caused any NEW test failures.");
428            System.out.println();
429        }
430
431        return hadPreviouslyFailingTests;
432    }
433
434    public static void main(final String[] args) throws Exception {
435        parseArgs(args);
436
437        while (new ParallelTestRunner().run()) {
438            //empty
439        }
440    }
441
442    private static void parseArgs(final String[] args) {
443        if (args.length > 0) {
444            String roots = "";
445            String reportFile = "";
446            for (int i = 0; i < args.length; i++) {
447                if (args[i].equals("--roots") && i != args.length - 1) {
448                    roots += args[++i] + " ";
449                } else if (args[i].equals("--report-file") && i != args.length - 1) {
450                    reportFile = args[++i];
451                } else if (args[i].equals("--test262")) {
452                    try {
453                        setTest262Properties();
454                    } catch (final IOException ex) {
455                        System.err.println(ex);
456                    }
457                }
458            }
459            if (!roots.isEmpty()) {
460                System.setProperty(TEST_JS_ROOTS, roots.trim());
461            }
462            if (!reportFile.isEmpty()) {
463                System.setProperty(TEST_JS_REPORT_FILE, reportFile);
464            }
465        }
466    }
467
468    private static void setTest262Properties() throws IOException {
469        System.setProperty(TEST_JS_ROOTS, "test/test262/test/suite/");
470        System.setProperty(TEST_JS_FRAMEWORK, "test/script/test262.js test/test262/test/harness/framework.js test/test262/test/harness/sta.js");
471        System.setProperty(TEST_JS_EXCLUDES_FILE, "test/test262/test/config/excludelist.xml");
472        System.setProperty(TEST_JS_ENABLE_STRICT_MODE, "true");
473
474        final Properties projectProperties = new Properties();
475        projectProperties.load(new FileInputStream("project.properties"));
476        String excludeList = projectProperties.getProperty("test262-test-sys-prop.test.js.exclude.list", "");
477        final Pattern pattern = Pattern.compile("\\$\\{([^}]+)}");
478        for (;;) {
479            final Matcher matcher = pattern.matcher(excludeList);
480            if (!matcher.find()) {
481                break;
482            }
483            final String propertyValue = projectProperties.getProperty(matcher.group(1), "");
484            excludeList = excludeList.substring(0, matcher.start()) + propertyValue + excludeList.substring(matcher.end());
485        }
486        System.setProperty(TEST_JS_EXCLUDE_LIST, excludeList);
487    }
488
489    public static final class OutputStreamDelegator extends OutputStream {
490        private final OutputStream[] streams;
491
492        public OutputStreamDelegator(final OutputStream... streams) {
493            this.streams = streams;
494        }
495
496        @Override
497        public void write(final int b) throws IOException {
498            for (final OutputStream stream : streams) {
499                stream.write(b);
500            }
501        }
502
503        @Override
504        public void flush() throws IOException {
505            for (final OutputStream stream : streams) {
506                stream.flush();
507            }
508        }
509    }
510}
511
512final class TestFailedError extends Error {
513    private static final long serialVersionUID = 1L;
514
515    public TestFailedError(final String message) {
516        super(message);
517    }
518
519    public TestFailedError(final String message, final Throwable cause) {
520        super(message, cause);
521    }
522
523    public TestFailedError(final Throwable cause) {
524        super(cause);
525    }
526}
527