1/*
2 [The "BSD licence"]
3 Copyright (c) 2007-2008 Leon Jen-Yuan Su
4 All rights reserved.
5
6 Redistribution and use in source and binary forms, with or without
7 modification, are permitted provided that the following conditions
8 are met:
9 1. Redistributions of source code must retain the above copyright
10    notice, this list of conditions and the following disclaimer.
11 2. Redistributions in binary form must reproduce the above copyright
12    notice, this list of conditions and the following disclaimer in the
13    documentation and/or other materials provided with the distribution.
14 3. The name of the author may not be used to endorse or promote products
15    derived from this software without specific prior written permission.
16
17 THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27*/
28package org.antlr.gunit;
29
30import org.antlr.stringtemplate.StringTemplate;
31import org.antlr.stringtemplate.StringTemplateGroup;
32import org.antlr.stringtemplate.StringTemplateGroupLoader;
33import org.antlr.stringtemplate.CommonGroupLoader;
34import org.antlr.stringtemplate.language.AngleBracketTemplateLexer;
35
36import java.io.*;
37import java.lang.reflect.Method;
38import java.util.ArrayList;
39import java.util.HashMap;
40import java.util.List;
41import java.util.Map;
42import java.util.logging.ConsoleHandler;
43import java.util.logging.Handler;
44import java.util.logging.Level;
45import java.util.logging.Logger;
46
47public class JUnitCodeGen {
48    public GrammarInfo grammarInfo;
49    public Map<String, String> ruleWithReturn;
50    private final String testsuiteDir;
51    private String outputDirectoryPath = ".";
52
53    private final static Handler console = new ConsoleHandler();
54    private static final Logger logger = Logger.getLogger(JUnitCodeGen.class.getName());
55    static {
56        logger.addHandler(console);
57    }
58
59    public JUnitCodeGen(GrammarInfo grammarInfo, String testsuiteDir) throws ClassNotFoundException {
60        this( grammarInfo, determineClassLoader(), testsuiteDir);
61    }
62
63    private static ClassLoader determineClassLoader() {
64        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
65        if ( classLoader == null ) {
66            classLoader = JUnitCodeGen.class.getClassLoader();
67        }
68        return classLoader;
69    }
70
71    public JUnitCodeGen(GrammarInfo grammarInfo, ClassLoader classLoader, String testsuiteDir) throws ClassNotFoundException {
72        this.grammarInfo = grammarInfo;
73        this.testsuiteDir = testsuiteDir;
74        /** Map the name of rules having return value to its return type */
75        ruleWithReturn = new HashMap<String, String>();
76        Class parserClass = locateParserClass( grammarInfo, classLoader );
77        Method[] methods = parserClass.getDeclaredMethods();
78        for(Method method : methods) {
79            if ( !method.getReturnType().getName().equals("void") ) {
80                ruleWithReturn.put(method.getName(), method.getReturnType().getName().replace('$', '.'));
81            }
82        }
83    }
84
85    private Class locateParserClass(GrammarInfo grammarInfo, ClassLoader classLoader) throws ClassNotFoundException {
86        String parserClassName = grammarInfo.getGrammarName() + "Parser";
87        if ( grammarInfo.getGrammarPackage() != null ) {
88            parserClassName = grammarInfo.getGrammarPackage()+ "." + parserClassName;
89        }
90        return classLoader.loadClass( parserClassName );
91    }
92
93    public String getOutputDirectoryPath() {
94        return outputDirectoryPath;
95    }
96
97    public void setOutputDirectoryPath(String outputDirectoryPath) {
98        this.outputDirectoryPath = outputDirectoryPath;
99    }
100
101    public void compile() throws IOException{
102        String junitFileName;
103        if ( grammarInfo.getTreeGrammarName()!=null ) {
104            junitFileName = "Test"+grammarInfo.getTreeGrammarName();
105        }
106        else {
107            junitFileName = "Test"+grammarInfo.getGrammarName();
108        }
109        String lexerName = grammarInfo.getGrammarName()+"Lexer";
110        String parserName = grammarInfo.getGrammarName()+"Parser";
111
112        StringTemplateGroupLoader loader = new CommonGroupLoader("org/antlr/gunit", null);
113        StringTemplateGroup.registerGroupLoader(loader);
114        StringTemplateGroup.registerDefaultLexer(AngleBracketTemplateLexer.class);
115        StringBuffer buf = compileToBuffer(junitFileName, lexerName, parserName);
116        writeTestFile(".", junitFileName+".java", buf.toString());
117    }
118
119    public StringBuffer compileToBuffer(String className, String lexerName, String parserName) {
120        StringTemplateGroup group = StringTemplateGroup.loadGroup("junit");
121        StringBuffer buf = new StringBuffer();
122        buf.append(genClassHeader(group, className, lexerName, parserName));
123        buf.append(genTestRuleMethods(group));
124        buf.append("\n\n}");
125        return buf;
126    }
127
128    protected String genClassHeader(StringTemplateGroup group, String junitFileName, String lexerName, String parserName) {
129        StringTemplate classHeaderST = group.getInstanceOf("classHeader");
130        if ( grammarInfo.getTestPackage()!=null ) {	// Set up class package if there is
131            classHeaderST.setAttribute("header", "package "+grammarInfo.getTestPackage()+";");
132        }
133        classHeaderST.setAttribute("junitFileName", junitFileName);
134
135        String lexerPath = null;
136        String parserPath = null;
137        String treeParserPath = null;
138        String packagePath = null;
139        boolean isTreeGrammar = false;
140        boolean hasPackage = false;
141        /** Set up appropriate class path for parser/tree parser if using package */
142        if ( grammarInfo.getGrammarPackage()!=null ) {
143            hasPackage = true;
144            packagePath = "./"+grammarInfo.getGrammarPackage().replace('.', '/');
145            lexerPath = grammarInfo.getGrammarPackage()+"."+lexerName;
146            parserPath = grammarInfo.getGrammarPackage()+"."+parserName;
147            if ( grammarInfo.getTreeGrammarName()!=null ) {
148                treeParserPath = grammarInfo.getGrammarPackage()+"."+grammarInfo.getTreeGrammarName();
149                isTreeGrammar = true;
150            }
151        }
152        else {
153            lexerPath = lexerName;
154            parserPath = parserName;
155            if ( grammarInfo.getTreeGrammarName()!=null ) {
156                treeParserPath = grammarInfo.getTreeGrammarName();
157                isTreeGrammar = true;
158            }
159        }
160        // also set up custom tree adaptor if necessary
161        String treeAdaptorPath = null;
162        boolean hasTreeAdaptor = false;
163        if ( grammarInfo.getAdaptor()!=null ) {
164            hasTreeAdaptor = true;
165            treeAdaptorPath = grammarInfo.getAdaptor();
166        }
167        classHeaderST.setAttribute("hasTreeAdaptor", hasTreeAdaptor);
168        classHeaderST.setAttribute("treeAdaptorPath", treeAdaptorPath);
169        classHeaderST.setAttribute("hasPackage", hasPackage);
170        classHeaderST.setAttribute("packagePath", packagePath);
171        classHeaderST.setAttribute("lexerPath", lexerPath);
172        classHeaderST.setAttribute("parserPath", parserPath);
173        classHeaderST.setAttribute("treeParserPath", treeParserPath);
174        classHeaderST.setAttribute("isTreeGrammar", isTreeGrammar);
175        return classHeaderST.toString();
176    }
177
178    protected String genTestRuleMethods(StringTemplateGroup group) {
179        StringBuffer buf = new StringBuffer();
180        if ( grammarInfo.getTreeGrammarName()!=null ) {	// Generate junit codes of for tree grammar rule
181            genTreeMethods(group, buf);
182        }
183        else {	// Generate junit codes of for grammar rule
184            genParserMethods(group, buf);
185        }
186        return buf.toString();
187    }
188
189    private void genParserMethods(StringTemplateGroup group, StringBuffer buf) {
190        for ( gUnitTestSuite ts: grammarInfo.getRuleTestSuites() ) {
191            int i = 0;
192            for ( gUnitTestInput input: ts.testSuites.keySet() ) {	// each rule may contain multiple tests
193                i++;
194                StringTemplate testRuleMethodST;
195                /** If rule has multiple return values or ast*/
196                if ( ts.testSuites.get(input).getType()== gUnitParser.ACTION && ruleWithReturn.containsKey(ts.getRuleName()) ) {
197                    testRuleMethodST = group.getInstanceOf("testRuleMethod2");
198                    String outputString = ts.testSuites.get(input).getText();
199                    testRuleMethodST.setAttribute("methodName", "test"+changeFirstCapital(ts.getRuleName())+i);
200                    testRuleMethodST.setAttribute("testRuleName", '"'+ts.getRuleName()+'"');
201                    testRuleMethodST.setAttribute("test", input);
202                    testRuleMethodST.setAttribute("returnType", ruleWithReturn.get(ts.getRuleName()));
203                    testRuleMethodST.setAttribute("expecting", outputString);
204                }
205                else {
206                    String testRuleName;
207                    // need to determine whether it's a test for parser rule or lexer rule
208                    if ( ts.isLexicalRule() ) testRuleName = ts.getLexicalRuleName();
209                    else testRuleName = ts.getRuleName();
210                    testRuleMethodST = group.getInstanceOf("testRuleMethod");
211                    String outputString = ts.testSuites.get(input).getText();
212                    testRuleMethodST.setAttribute("isLexicalRule", ts.isLexicalRule());
213                    testRuleMethodST.setAttribute("methodName", "test"+changeFirstCapital(testRuleName)+i);
214                    testRuleMethodST.setAttribute("testRuleName", '"'+testRuleName+'"');
215                    testRuleMethodST.setAttribute("test", input);
216                    testRuleMethodST.setAttribute("tokenType", getTypeString(ts.testSuites.get(input).getType()));
217
218                    // normalize whitespace
219                    outputString = normalizeTreeSpec(outputString);
220
221                    if ( ts.testSuites.get(input).getType()==gUnitParser.ACTION ) {	// trim ';' at the end of ACTION if there is...
222                        //testRuleMethodST.setAttribute("expecting", outputString.substring(0, outputString.length()-1));
223                        testRuleMethodST.setAttribute("expecting", outputString);
224                    }
225                    else if ( ts.testSuites.get(input).getType()==gUnitParser.RETVAL ) {	// Expected: RETVAL
226                        testRuleMethodST.setAttribute("expecting", outputString);
227                    }
228                    else {	// Attach "" to expected STRING or AST
229                        // strip newlines for (...) tree stuff
230                        outputString = outputString.replaceAll("\n", "");
231                        testRuleMethodST.setAttribute("expecting", '"'+escapeForJava(outputString)+'"');
232                    }
233                }
234                buf.append(testRuleMethodST.toString());
235            }
236        }
237    }
238
239    private void genTreeMethods(StringTemplateGroup group, StringBuffer buf) {
240        for ( gUnitTestSuite ts: grammarInfo.getRuleTestSuites() ) {
241            int i = 0;
242            for ( gUnitTestInput input: ts.testSuites.keySet() ) {	// each rule may contain multiple tests
243                i++;
244                StringTemplate testRuleMethodST;
245                /** If rule has multiple return values or ast*/
246                if ( ts.testSuites.get(input).getType()== gUnitParser.ACTION && ruleWithReturn.containsKey(ts.getTreeRuleName()) ) {
247                    testRuleMethodST = group.getInstanceOf("testTreeRuleMethod2");
248                    String outputString = ts.testSuites.get(input).getText();
249                    testRuleMethodST.setAttribute("methodName", "test"+changeFirstCapital(ts.getTreeRuleName())+"_walks_"+
250                                                                changeFirstCapital(ts.getRuleName())+i);
251                    testRuleMethodST.setAttribute("testTreeRuleName", '"'+ts.getTreeRuleName()+'"');
252                    testRuleMethodST.setAttribute("testRuleName", '"'+ts.getRuleName()+'"');
253                    testRuleMethodST.setAttribute("test", input);
254                    testRuleMethodST.setAttribute("returnType", ruleWithReturn.get(ts.getTreeRuleName()));
255                    testRuleMethodST.setAttribute("expecting", outputString);
256                }
257                else {
258                    testRuleMethodST = group.getInstanceOf("testTreeRuleMethod");
259                    String outputString = ts.testSuites.get(input).getText();
260                    testRuleMethodST.setAttribute("methodName", "test"+changeFirstCapital(ts.getTreeRuleName())+"_walks_"+
261                                                                changeFirstCapital(ts.getRuleName())+i);
262                    testRuleMethodST.setAttribute("testTreeRuleName", '"'+ts.getTreeRuleName()+'"');
263                    testRuleMethodST.setAttribute("testRuleName", '"'+ts.getRuleName()+'"');
264                    testRuleMethodST.setAttribute("test", input);
265                    testRuleMethodST.setAttribute("tokenType", getTypeString(ts.testSuites.get(input).getType()));
266
267                    if ( ts.testSuites.get(input).getType()==gUnitParser.ACTION ) {	// trim ';' at the end of ACTION if there is...
268                        //testRuleMethodST.setAttribute("expecting", outputString.substring(0, outputString.length()-1));
269                        testRuleMethodST.setAttribute("expecting", outputString);
270                    }
271                    else if ( ts.testSuites.get(input).getType()==gUnitParser.RETVAL ) {	// Expected: RETVAL
272                        testRuleMethodST.setAttribute("expecting", outputString);
273                    }
274                    else {	// Attach "" to expected STRING or AST
275                        testRuleMethodST.setAttribute("expecting", '"'+escapeForJava(outputString)+'"');
276                    }
277                }
278                buf.append(testRuleMethodST.toString());
279            }
280        }
281    }
282
283    // return a meaningful gUnit token type name instead of using the magic number
284    public String getTypeString(int type) {
285        String typeText;
286        switch (type) {
287            case gUnitParser.OK :
288                typeText = "org.antlr.gunit.gUnitParser.OK";
289                break;
290            case gUnitParser.FAIL :
291                typeText = "org.antlr.gunit.gUnitParser.FAIL";
292                break;
293            case gUnitParser.STRING :
294                typeText = "org.antlr.gunit.gUnitParser.STRING";
295                break;
296            case gUnitParser.ML_STRING :
297                typeText = "org.antlr.gunit.gUnitParser.ML_STRING";
298                break;
299            case gUnitParser.RETVAL :
300                typeText = "org.antlr.gunit.gUnitParser.RETVAL";
301                break;
302            case gUnitParser.AST :
303                typeText = "org.antlr.gunit.gUnitParser.AST";
304                break;
305            default :
306                typeText = "org.antlr.gunit.gUnitParser.EOF";
307                break;
308        }
309        return typeText;
310    }
311
312    protected void writeTestFile(String dir, String fileName, String content) {
313        try {
314            File f = new File(dir, fileName);
315            FileWriter w = new FileWriter(f);
316            BufferedWriter bw = new BufferedWriter(w);
317            bw.write(content);
318            bw.close();
319            w.close();
320        }
321        catch (IOException ioe) {
322            logger.log(Level.SEVERE, "can't write file", ioe);
323        }
324    }
325
326    public static String escapeForJava(String inputString) {
327        // Gotta escape literal backslash before putting in specials that use escape.
328        inputString = inputString.replace("\\", "\\\\");
329        // Then double quotes need escaping (singles are OK of course).
330        inputString = inputString.replace("\"", "\\\"");
331        // note: replace newline to String ".\n", replace tab to String ".\t"
332        inputString = inputString.replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r").replace("\b", "\\b").replace("\f", "\\f");
333
334        return inputString;
335    }
336
337    protected String changeFirstCapital(String ruleName) {
338        String firstChar = String.valueOf(ruleName.charAt(0));
339        return firstChar.toUpperCase()+ruleName.substring(1);
340    }
341
342    public static String normalizeTreeSpec(String t) {
343        List<String> words = new ArrayList<String>();
344        int i = 0;
345        StringBuilder word = new StringBuilder();
346        while ( i<t.length() ) {
347            if ( t.charAt(i)=='(' || t.charAt(i)==')' ) {
348                if ( word.length()>0 ) {
349                    words.add(word.toString());
350                    word.setLength(0);
351                }
352                words.add(String.valueOf(t.charAt(i)));
353                i++;
354                continue;
355            }
356            if ( Character.isWhitespace(t.charAt(i)) ) {
357                // upon WS, save word
358                if ( word.length()>0 ) {
359                    words.add(word.toString());
360                    word.setLength(0);
361                }
362                i++;
363                continue;
364            }
365
366            // ... "x" or ...("x"
367            if ( t.charAt(i)=='"' && (i-1)>=0 &&
368                 (t.charAt(i-1)=='(' || Character.isWhitespace(t.charAt(i-1))) )
369            {
370                i++;
371                while ( i<t.length() && t.charAt(i)!='"' ) {
372                    if ( t.charAt(i)=='\\' &&
373                         (i+1)<t.length() && t.charAt(i+1)=='"' ) // handle \"
374                    {
375                        word.append('"');
376                        i+=2;
377                        continue;
378                    }
379                    word.append(t.charAt(i));
380                    i++;
381                }
382                i++; // skip final "
383                words.add(word.toString());
384                word.setLength(0);
385                continue;
386            }
387            word.append(t.charAt(i));
388            i++;
389        }
390        if ( word.length()>0 ) {
391            words.add(word.toString());
392        }
393        //System.out.println("words="+words);
394        StringBuilder buf = new StringBuilder();
395        for (int j=0; j<words.size(); j++) {
396            if ( j>0 && !words.get(j).equals(")") &&
397                 !words.get(j-1).equals("(") ) {
398                buf.append(' ');
399            }
400            buf.append(words.get(j));
401        }
402        return buf.toString();
403    }
404
405}
406