1/*
2 * Copyright (c) 2013, 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.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24import java.io.*;
25import java.util.*;
26import java.lang.annotation.*;
27import java.lang.reflect.InvocationTargetException;
28
29/**
30 * {@code JavapTester} is an abstract test-driver that provides the logic
31 * to execute test-cases, grouped by test classes.
32 * A test class is a main class extending this class, that instantiate
33 * itself, and calls the {@link run} method, passing any command line
34 * arguments.
35 * <p>
36 * The {@code run} method, expects arguments to identify test-case classes.
37 * A test-case class is a class extending the test class, and annotated
38 * with {@code TestCase}.
39 * <p>
40 * If no test-cases are specified, the test class directory is searched for
41 * co-located test-case classes (i.e. any class extending the test class,
42 * annotated with  {@code TestCase}).
43 * <p>
44 * Besides serving to group test-cases, extending the driver allow
45 * setting up a test-case template, and possibly overwrite default
46 * test-driver behaviour.
47 */
48public abstract class JavapTester {
49
50    private static boolean debug = false;
51    private static final PrintStream out = System.err;
52    private static final PrintStream err = System.err;
53
54
55    protected void run(String... args) throws Exception {
56
57        final File classesdir = new File(System.getProperty("test.classes", "."));
58
59        String[] classNames = args;
60
61        // If no test-cases are specified, we regard all co-located classes
62        // as potential test-cases.
63        if (args.length == 0) {
64            final String pattern =  ".*\\.class";
65            final File classFiles[] = classesdir.listFiles(new FileFilter() {
66                    public boolean accept(File f) {
67                        return f.getName().matches(pattern);
68                    }
69                });
70            ArrayList<String> names = new ArrayList<String>(classFiles.length);
71            for (File f : classFiles) {
72                String fname = f.getName();
73                names.add(fname.substring(0, fname.length() -6));
74            }
75            classNames = names.toArray(new String[names.size()]);
76        } else {
77            debug = true;
78        }
79        // Test-cases must extend the driver type, and be marked
80        // @TestCase. Other arguments (classes) are ignored.
81        // Test-cases are instantiated, and thereby executed.
82        for (String clname : classNames) {
83            try {
84                final Class tclass = Class.forName(clname);
85                if  (!getClass().isAssignableFrom(tclass)) continue;
86                TestCase anno = (TestCase) tclass.getAnnotation(TestCase.class);
87                if (anno == null) continue;
88                if (!debug) {
89                    ignore i = (ignore) tclass.getAnnotation(ignore.class);
90                    if (i != null) {
91                        out.println("Ignore: " + clname);
92                        ignored++;
93                        continue;
94                    }
95                }
96                out.println("TestCase: " + clname);
97                cases++;
98                JavapTester tc = (JavapTester) tclass.getConstructor().newInstance();
99                if (tc.errors > 0) {
100                    error("" + tc.errors + " test points failed in " + clname);
101                    errors += tc.errors - 1;
102                    fcases++;
103                }
104            } catch(ReflectiveOperationException roe) {
105                error("Warning: " + clname + " - ReflectiveOperationException");
106                roe.printStackTrace(err);
107            } catch(Exception unknown) {
108                error("Warning: " + clname + " - uncaught exception");
109                unknown.printStackTrace(err);
110            }
111        }
112
113        String imsg = ignored > 0 ? " (" +  ignored + " ignored)" : "";
114        if (errors > 0)
115            throw new Error(errors + " error, in " + fcases + " of " + cases + " test-cases" + imsg);
116        else
117            err.println("" + cases + " test-cases executed" + imsg + ", no errors");
118    }
119
120
121    /**
122     * Test-cases must be marked with the {@code TestCase} annotation,
123     * as well as extend {@code JavapTester} (or an driver extension
124     * specified as the first argument to the {@code main()} method.
125     */
126    @Retention(RetentionPolicy.RUNTIME)
127    @interface TestCase { }
128
129    /**
130     * Individual test-cases failing due to product bugs, may temporarily
131     * be excluded by marking them like this, (where "at-" is replaced by "@")
132     * at-ignore // 1234567: bug synopsis
133     */
134    @Retention(RetentionPolicy.RUNTIME)
135    @interface ignore { }
136
137    /**
138     * Test-cases are classes extending {@code JavapTester}, and
139     * calling {@link setSrc}, followed by one or more invocations
140     * of {@link verify} in the body of the constructor.
141     * <p>
142     * Sets a default test-case template, which is empty except
143     * for a key of {@code "TESTCASE"}.
144     * Subclasses will typically call {@code setSrc(TestSource)}
145     * to setup a useful test-case template.
146     */
147    public JavapTester() {
148        this.testCase = this.getClass().getName();
149        src = new TestSource("TESTCASE");
150    }
151
152    /**
153     * Set the top-level source template.
154     */
155    protected JavapTester setSrc(TestSource src) {
156        this.src = src;
157        return this;
158    }
159
160    /**
161     * Convenience method for calling {@code innerSrc("TESTCASE", ...)}.
162     */
163    protected JavapTester setSrc(String... lines) {
164        return innerSrc("TESTCASE", lines);
165    }
166
167    /**
168     * Convenience method for calling {@code innerSrc(key, new TestSource(...))}.
169     */
170    protected JavapTester innerSrc(String key, String... lines) {
171        return innerSrc(key, new TestSource(lines));
172    }
173
174    /**
175     * Specialize the testcase template, setting replacement content
176     * for the specified key.
177     */
178    protected JavapTester innerSrc(String key, TestSource content) {
179        if (src == null) {
180            src = new TestSource(key);
181        }
182        src.setInner(key, content);
183        return this;
184    }
185
186    /**
187     * On the first invocation, call {@code execute()} to compile
188     * the test-case source and process the resulting class(se)
189     * into verifiable output.
190     * <p>
191     * Verify that the output matches each of the regular expressions
192     * given as argument.
193     * <p>
194     * Any failure to match constitutes a test failure, but doesn't
195     * abort the test-case.
196     * <p>
197     * Any exception (e.g. bad regular expression syntax) results in
198     * a test failure, and aborts the test-case.
199     */
200    protected void verify(String... expect) {
201        if (!didExecute) {
202            try {
203                execute();
204            } catch(Exception ue) {
205                throw new Error(ue);
206            } finally {
207                didExecute = true;
208            }
209        }
210        if (output == null) {
211            error("output is null");
212            return;
213        }
214        for (String e: expect) {
215            // Escape regular expressions (to allow input to be literals).
216            // Notice, characters to be escaped are themselves identified
217            // using regular expressions
218            String rc[] = { "(", ")", "[", "]", "{", "}", "$" };
219            for (String c : rc) {
220                e = e.replace(c, "\\" + c);
221            }
222            // DEBUG: Uncomment this to test modulo constant pool index.
223            // e = e.replaceAll("#[0-9]{2}", "#[0-9]{2}");
224            if (!output.matches("(?s).*" + e + ".*")) {
225                if (!didPrint) {
226                    out.println(output);
227                    didPrint = true;
228                }
229                error("not matched: '" + e + "'");
230            } else if(debug) {
231                out.println("matched: '" + e + "'");
232            }
233        }
234    }
235
236    /**
237     * Calls {@code writeTestFile()} to write out the test-case source
238     * content to a file, then call {@code compileTestFile()} to
239     * compile it, and finally run the {@link process} method to produce
240     * verifiable output. The default {@code process} method runs javap.
241     * <p>
242     * If an exception occurs, it results in a test failure, and
243     * aborts the test-case.
244     */
245    protected void execute() throws IOException {
246        err.println("TestCase: " + testCase);
247        writeTestFile();
248        compileTestFile();
249        process();
250    }
251
252    /**
253     * Generate java source from test-case.
254     * TBD: change to use javaFileObject, possibly make
255     * this class extend JavaFileObject.
256     */
257    protected void writeTestFile() throws IOException {
258        javaFile = new File("Test.java");
259        FileWriter fw = new FileWriter(javaFile);
260        BufferedWriter bw = new BufferedWriter(fw);
261        PrintWriter pw = new PrintWriter(bw);
262        for (String line : src) {
263            pw.println(line);
264            if (debug) out.println(line);
265        }
266        pw.close();
267    }
268
269    /**
270     * Compile the Java source code.
271     */
272    protected void compileTestFile() {
273        String path = javaFile.getPath();
274        String params[] =  {"-g", path };
275        int rc = com.sun.tools.javac.Main.compile(params);
276        if (rc != 0)
277            throw new Error("compilation failed. rc=" + rc);
278        classFile = new File(path.substring(0, path.length() - 5) + ".class");
279    }
280
281
282    /**
283     * Process class file to generate output for verification.
284     * The default implementation simply runs javap. This might be
285     * overwritten to generate output in a different manner.
286     */
287    protected void process() {
288        String testClasses = "."; //System.getProperty("test.classes", ".");
289        StringWriter sw = new StringWriter();
290        PrintWriter pw = new PrintWriter(sw);
291        String[] args = { "-v", "-classpath", testClasses, "Test" };
292        int rc = com.sun.tools.javap.Main.run(args, pw);
293        if (rc != 0)
294            throw new Error("javap failed. rc=" + rc);
295        pw.close();
296        output = sw.toString();
297        if (debug) {
298            out.println(output);
299            didPrint = true;
300        }
301
302    }
303
304
305    private String testCase;
306    private TestSource src;
307    private File javaFile = null;
308    private File classFile = null;
309    private String output = null;
310    private boolean didExecute = false;
311    private boolean didPrint = false;
312
313
314    protected void error(String msg) {
315        err.println("Error: " + msg);
316        errors++;
317    }
318
319    private int cases;
320    private int fcases;
321    private int errors;
322    private int ignored;
323
324    /**
325     * The TestSource class provides a simple container for
326     * test cases. It contains an array of source code lines,
327     * where zero or more lines may be markers for nested lines.
328     * This allows representing templates, with specialization.
329     * <P>
330     * This may be generalized to support more advance combo
331     * tests, but presently it's only used with a static template,
332     * and one level of specialization.
333     */
334    public class TestSource implements Iterable<String> {
335
336        private String[] lines;
337        private Hashtable<String, TestSource> innerSrc;
338
339        public TestSource(String... lines) {
340            this.lines = lines;
341            innerSrc = new Hashtable<String, TestSource>();
342        }
343
344        public void setInner(String key, TestSource inner) {
345            innerSrc.put(key, inner);
346        }
347
348        public void setInner(String key, String... lines) {
349            innerSrc.put(key, new TestSource(lines));
350        }
351
352        public Iterator<String> iterator() {
353            return new LineIterator();
354        }
355
356        private class LineIterator implements Iterator<String> {
357
358            int nextLine = 0;
359            Iterator<String> innerIt = null;
360
361            public  boolean hasNext() {
362                return nextLine < lines.length;
363            }
364
365            public String next() {
366                if (!hasNext()) throw new NoSuchElementException();
367                String str = lines[nextLine];
368                TestSource inner = innerSrc.get(str);
369                if (inner == null) {
370                    nextLine++;
371                    return str;
372                }
373                if (innerIt == null) {
374                    innerIt = inner.iterator();
375                }
376                if (innerIt.hasNext()) {
377                    return innerIt.next();
378                }
379                innerIt = null;
380                nextLine++;
381                return next();
382            }
383
384            public void remove() {
385                throw new UnsupportedOperationException();
386            }
387        }
388    }
389}
390