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