1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tradefed.command; 18 19 import com.android.tradefed.command.CommandFileParser.CommandLine; 20 import com.android.tradefed.config.ConfigurationException; 21 import com.google.common.collect.ImmutableMap; 22 import com.google.common.collect.ImmutableSet; 23 24 import junit.framework.TestCase; 25 26 import java.io.BufferedReader; 27 import java.io.File; 28 import java.io.IOException; 29 import java.io.StringReader; 30 import java.util.Arrays; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.Set; 34 35 /** 36 * Unit tests for {@link CommandFileParser} 37 */ 38 public class CommandFileParserTest extends TestCase { 39 /** 40 * Uses a <File, String> map to provide {@link CommandFileParser} with mock data 41 * for the mapped Files. 42 */ 43 private static class MockCommandFileParser extends CommandFileParser { 44 private final Map<File, String> mDataMap; 45 MockCommandFileParser(Map<File, String> dataMap)46 MockCommandFileParser(Map<File, String> dataMap) { 47 this.mDataMap = dataMap; 48 } 49 50 @Override createCommandFileReader(File file)51 BufferedReader createCommandFileReader(File file) { 52 return new BufferedReader(new StringReader(mDataMap.get(file))); 53 } 54 } 55 56 /** the {@link CommandFileParser} under test, with all dependencies mocked out */ 57 private CommandFileParser mCommandFile; 58 private String mMockFileData = ""; 59 private static final File MOCK_FILE_PATH = new File("/path/to/"); 60 private File mMockFile = new File(MOCK_FILE_PATH, "original.txt"); 61 62 @Override setUp()63 protected void setUp() throws Exception { 64 super.setUp(); 65 mCommandFile = new CommandFileParser() { 66 @Override 67 BufferedReader createCommandFileReader(File file) { 68 return new BufferedReader(new StringReader(mMockFileData)); 69 } 70 }; 71 } 72 73 /** 74 * Test parsing a command file with a comment and a single config. 75 */ testParse_singleConfig()76 public void testParse_singleConfig() throws Exception { 77 // inject mock file data 78 mMockFileData = " #Comment followed by blank line\n \n--foo config"; 79 List<String> expectedArgs = Arrays.asList("--foo", "config"); 80 81 assertParsedData(expectedArgs); 82 } 83 84 @SafeVarargs assertParsedData(List<String>.... expectedCommands)85 private final void assertParsedData(List<String>... expectedCommands) throws IOException, 86 ConfigurationException { 87 assertParsedData(mCommandFile, mMockFile, expectedCommands); 88 } 89 90 @SafeVarargs assertParsedData(CommandFileParser parser, List<String>... expectedCommands)91 private final void assertParsedData(CommandFileParser parser, List<String>... expectedCommands) 92 throws IOException, ConfigurationException { 93 assertParsedData(parser, mMockFile, expectedCommands); 94 } 95 96 @SafeVarargs assertParsedData(CommandFileParser parser, File file, List<String>... expectedCommands)97 private final void assertParsedData(CommandFileParser parser, File file, 98 List<String>... expectedCommands) throws IOException, ConfigurationException { 99 List<CommandLine> data = parser.parseFile(file); 100 assertEquals(expectedCommands.length, data.size()); 101 102 for (int i = 0; i < expectedCommands.length; i++) { 103 assertEquals(expectedCommands[i].size(), data.get(i).size()); 104 105 for(int j = 0; j < expectedCommands[i].size(); j++) { 106 assertEquals(expectedCommands[i].get(j), data.get(i).get(j)); 107 } 108 } 109 } 110 111 /** 112 * Make sure that a config with a quoted argument is parsed properly. 113 * <p/> 114 * Whitespace inside of the quoted section should be preserved. Also, embedded escaped quotation 115 * marks should be ignored. 116 */ testParseFile_quotedConfig()117 public void testParseFile_quotedConfig() throws IOException, ConfigurationException { 118 // inject mock file data 119 mMockFileData = "--foo \"this is a config\" --bar \"escap\\\\ed \\\" quotation\""; 120 List<String> expectedArgs = Arrays.asList( 121 "--foo", "this is a config", "--bar", "escap\\\\ed \\\" quotation" 122 ); 123 124 assertParsedData(expectedArgs); 125 } 126 127 /** 128 * Test the data where the configuration ends inside a quotation. 129 */ testParseFile_endOnQuote()130 public void testParseFile_endOnQuote() throws IOException { 131 // inject mock file data 132 mMockFileData = "--foo \"this is truncated"; 133 134 try { 135 mCommandFile.parseFile(mMockFile); 136 fail("ConfigurationException not thrown"); 137 } catch (ConfigurationException e) { 138 // expected 139 } 140 } 141 142 /** 143 * Test the scenario where the configuration ends inside a quotation. 144 */ testRun_endWithEscape()145 public void testRun_endWithEscape() throws IOException { 146 // inject mock file data 147 mMockFileData = "--foo escape\\"; 148 try { 149 mCommandFile.parseFile(mMockFile); 150 fail("ConfigurationException not thrown"); 151 } catch (ConfigurationException e) { 152 // expected 153 } 154 } 155 156 // Macro-related tests testSimpleMacro()157 public void testSimpleMacro() throws IOException, ConfigurationException { 158 mMockFileData = "MACRO TeSt = verify\nTeSt()"; 159 List<String> expectedArgs = Arrays.asList("verify"); 160 161 assertParsedData(expectedArgs); 162 } 163 164 /** 165 * Ensure that when a macro is overwritten, the most recent value should be used. 166 * <p /> 167 * Note that a message should also be logged; no good way to verify that currently 168 */ testOverwriteMacro()169 public void testOverwriteMacro() throws IOException, ConfigurationException { 170 mMockFileData = "MACRO test = value 1\nMACRO test = value 2\ntest()"; 171 List<String> expectedArgs = Arrays.asList("value", "2"); 172 173 assertParsedData(expectedArgs); 174 } 175 176 /** 177 * Ensure that parsing of quoted tokens continues to work 178 */ testSimpleMacro_quotedTokens()179 public void testSimpleMacro_quotedTokens() throws IOException, ConfigurationException { 180 mMockFileData = "MACRO test = \"verify varify vorify\"\ntest()"; 181 List<String> expectedArgs = Arrays.asList("verify varify vorify"); 182 183 assertParsedData(expectedArgs); 184 } 185 186 /** 187 * Ensure that parsing of names with embedded underscores works properly. 188 */ testSimpleMacro_underscoreName()189 public void testSimpleMacro_underscoreName() throws IOException, ConfigurationException { 190 mMockFileData = "MACRO under_score = verify\nunder_score()"; 191 List<String> expectedArgs = Arrays.asList("verify"); 192 193 assertParsedData(expectedArgs); 194 } 195 196 /** 197 * Ensure that parsing of names with embedded hyphens works properly. 198 */ testSimpleMacro_hyphenName()199 public void testSimpleMacro_hyphenName() throws IOException, ConfigurationException { 200 mMockFileData = "MACRO hyphen-nated = verify\nhyphen-nated()"; 201 List<String> expectedArgs = Arrays.asList("verify"); 202 203 assertParsedData(expectedArgs); 204 } 205 206 /** 207 * Test the scenario where a macro call doesn't resolve. 208 */ testUndefinedMacro()209 public void testUndefinedMacro() throws IOException { 210 mMockFileData = "test()"; 211 try { 212 mCommandFile.parseFile(mMockFile); 213 fail("ConfigurationException not thrown"); 214 } catch (ConfigurationException e) { 215 // expected 216 } 217 } 218 219 /** 220 * Test the scenario where a syntax problem causes a macro call to not resolve. 221 */ testUndefinedMacro_defSyntaxError()222 public void testUndefinedMacro_defSyntaxError() throws IOException { 223 mMockFileData = "MACRO test = \n" + 224 "test()"; 225 try { 226 mCommandFile.parseFile(mMockFile); 227 fail("ConfigurationException not thrown"); 228 } catch (ConfigurationException e) { 229 // expected 230 } 231 } 232 233 /** 234 * Simple test for LONG MACRO parsing 235 */ testSimpleLongMacro()236 public void testSimpleLongMacro() throws IOException, ConfigurationException { 237 mMockFileData = "LONG MACRO test\n" + 238 "verify\n" + 239 "END MACRO\n" + 240 "test()"; 241 List<String> expectedArgs = Arrays.asList("verify"); 242 243 assertParsedData(expectedArgs); 244 } 245 246 /** 247 * Ensure that when a long macro is overwritten, the most recent value should be used. 248 * <p /> 249 * Note that a message should also be logged; no good way to verify that currently 250 */ 251 252 253 /** 254 * Simple test for LONG MACRO parsing with multi-line expansion 255 */ testSimpleLongMacro_multiline()256 public void testSimpleLongMacro_multiline() throws IOException, ConfigurationException { 257 mMockFileData = "LONG MACRO test\n" + 258 "one two three\n" + 259 "a b c\n" + 260 "do re mi\n" + 261 "END MACRO\n" + 262 "test()"; 263 List<String> expectedArgs1 = Arrays.asList("one", "two", "three"); 264 List<String> expectedArgs2 = Arrays.asList("a", "b", "c"); 265 List<String> expectedArgs3 = Arrays.asList("do", "re", "mi"); 266 assertParsedData(expectedArgs1, expectedArgs2, expectedArgs3); 267 268 } 269 270 /** 271 * Simple test for LONG MACRO parsing with multi-line expansion 272 */ testLongMacro_withComment()273 public void testLongMacro_withComment() throws IOException, ConfigurationException { 274 mMockFileData = "LONG MACRO test\n" + 275 "\n" + // blank line 276 "one two three\n" + 277 "#a b c\n" + 278 "do re mi\n" + 279 "END MACRO\n" + 280 "test()"; 281 List<String> expectedArgs1 = Arrays.asList("one", "two", "three"); 282 List<String> expectedArgs2 = Arrays.asList("do", "re", "mi"); 283 assertParsedData(expectedArgs1, expectedArgs2); 284 } 285 286 /** 287 * Test the scenario where the configuration ends inside of a LONG MACRO definition. 288 */ testLongMacroSyntaxError_eof()289 public void testLongMacroSyntaxError_eof() throws IOException { 290 mMockFileData = "LONG MACRO test\n" + 291 "verify\n" + 292 // "END MACRO\n" (this is the syntax error) 293 "test()"; 294 295 try { 296 mCommandFile.parseFile(mMockFile); 297 fail("ConfigurationException not thrown"); 298 } catch (ConfigurationException e) { 299 // expected 300 } 301 } 302 303 /** 304 * Verifies that the line location data in CommandLine (file, line number) is accurate. 305 */ testCommandLineLocation()306 public void testCommandLineLocation() throws IOException, ConfigurationException { 307 mMockFileData = "# This is a comment\n" + 308 "# This is another comment\n" + 309 "this --is-a-cmd\n" + 310 "one --final-cmd\n" + 311 "# More comments\n" + 312 "two --final-command"; 313 314 Set<CommandLine> expectedSet = ImmutableSet.<CommandLine>builder() 315 .add(new CommandLine(Arrays.asList("this", "--is-a-cmd"), mMockFile, 3)) 316 .add(new CommandLine(Arrays.asList("one", "--final-cmd"), mMockFile, 4)) 317 .add(new CommandLine(Arrays.asList("two", "--final-command"), mMockFile, 6)) 318 .build(); 319 320 321 Set<CommandLine> parsedSet = ImmutableSet.<CommandLine>builder() 322 .addAll(mCommandFile.parseFile(mMockFile)) 323 .build(); 324 325 assertEquals(expectedSet, parsedSet); 326 } 327 328 /** 329 * Verifies that the line location data in CommandLine (file, line number) is accurate for 330 * commands defined in included files. 331 */ testCommandLineLocation_withInclude()332 public void testCommandLineLocation_withInclude() throws IOException, ConfigurationException { 333 final File mockFile = new File(MOCK_FILE_PATH, "file.txt"); 334 final String mockFileData = "# This is a comment\n" + 335 "# This is another comment\n" + 336 "INCLUDE include.txt\n" + 337 "this --is-a-cmd\n" + 338 "one --final-cmd"; 339 340 final File mockIncludedFile = new File(MOCK_FILE_PATH, "include.txt"); 341 final String mockIncludedFileData = "# This is a comment\n" + 342 "# This is another comment\n" + 343 "inc --is-a-cmd\n" + 344 "# More comments\n" + 345 "inc --final-cmd"; 346 347 Set<CommandLine> expectedSet = ImmutableSet.<CommandLine>builder() 348 .add(new CommandLine(Arrays.asList("this", "--is-a-cmd"), mockFile, 4)) 349 .add(new CommandLine(Arrays.asList("one", "--final-cmd"), mockFile, 5)) 350 .add(new CommandLine(Arrays.asList("inc", "--is-a-cmd"), mockIncludedFile, 3)) 351 .add(new CommandLine(Arrays.asList("inc", "--final-cmd"), mockIncludedFile, 5)) 352 .build(); 353 354 355 CommandFileParser commandFileParser = new MockCommandFileParser( 356 ImmutableMap.<File, String>builder() 357 .put(mockFile, mockFileData) 358 .put(mockIncludedFile, mockIncludedFileData) 359 .build()); 360 361 Set<CommandLine> parsedSet = ImmutableSet.<CommandLine>builder() 362 .addAll(commandFileParser.parseFile(mockFile)) 363 .build(); 364 365 assertEquals(expectedSet, parsedSet); 366 } 367 368 /** 369 * Verify that the INCLUDE directive is behaving properly 370 */ testMacroParserInclude()371 public void testMacroParserInclude() throws Exception { 372 final String mockFileData = "INCLUDE somefile.txt\n"; 373 final String mockIncludedFileData = "--foo bar\n"; 374 List<String> expectedArgs = Arrays.asList("--foo", "bar"); 375 376 CommandFileParser commandFile = new CommandFileParser() { 377 private boolean showInclude = true; 378 @Override 379 BufferedReader createCommandFileReader(File file) { 380 if (showInclude) { 381 showInclude = false; 382 return new BufferedReader(new StringReader(mockFileData)); 383 } else { 384 return new BufferedReader(new StringReader(mockIncludedFileData)); 385 } 386 } 387 }; 388 389 assertParsedData(commandFile, expectedArgs); 390 assertEquals(1, commandFile.getIncludedFiles().size()); 391 assertTrue(commandFile.getIncludedFiles().iterator().next().endsWith("somefile.txt")); 392 } 393 394 /** 395 * Verify that the INCLUDE directive works when used for two files 396 */ testMacroParserInclude_twice()397 public void testMacroParserInclude_twice() throws Exception { 398 final String mockFileData = "INCLUDE somefile.txt\n" + 399 "INCLUDE otherfile.txt\n"; 400 final String mockIncludedFileData1 = "--foo bar\n"; 401 final String mockIncludedFileData2 = "--baz quux\n"; 402 List<String> expectedArgs1 = Arrays.asList("--foo", "bar"); 403 List<String> expectedArgs2 = Arrays.asList("--baz", "quux"); 404 405 CommandFileParser commandFile = new CommandFileParser() { 406 private int phase = 0; 407 @Override 408 BufferedReader createCommandFileReader(File file) { 409 if(phase == 0) { 410 phase++; 411 return new BufferedReader(new StringReader(mockFileData)); 412 } else if (phase == 1) { 413 phase++; 414 return new BufferedReader(new StringReader(mockIncludedFileData1)); 415 } else { 416 return new BufferedReader(new StringReader(mockIncludedFileData2)); 417 } 418 } 419 }; 420 assertParsedData(commandFile, expectedArgs1, expectedArgs2); 421 } 422 423 /** 424 * Verify that a file is only ever included once, regardless of how many INCLUDE directives for 425 * that file show up 426 */ testMacroParserInclude_repeat()427 public void testMacroParserInclude_repeat() throws Exception { 428 final String mockFileData = "INCLUDE somefile.txt\n" + 429 "INCLUDE somefile.txt\n"; 430 final String mockIncludedFileData1 = "--foo bar\n"; 431 List<String> expectedArgs1 = Arrays.asList("--foo", "bar"); 432 433 CommandFileParser commandFile = new CommandFileParser() { 434 private int phase = 0; 435 @Override 436 BufferedReader createCommandFileReader(File file) { 437 if(phase == 0) { 438 phase++; 439 return new BufferedReader(new StringReader(mockFileData)); 440 } else { 441 return new BufferedReader(new StringReader(mockIncludedFileData1)); 442 } 443 } 444 }; 445 assertParsedData(commandFile, expectedArgs1); 446 } 447 448 /** 449 * Verify that the path of the file referenced by an INCLUDE directive is considered relative to 450 * the location of the referencing file. 451 */ testMacroParserInclude_parentDir()452 public void testMacroParserInclude_parentDir() throws Exception { 453 // When we pass an unqualified filename, expect it to be taken relative to mMockFile's 454 // parent directory 455 final String includeFileName = "somefile.txt"; 456 final File expectedFile = new File(MOCK_FILE_PATH, includeFileName); 457 458 final String mockFileData = String.format("INCLUDE %s\n", includeFileName); 459 final String mockIncludedFileData = "--foo bar\n"; 460 List<String> expectedArgs = Arrays.asList("--foo", "bar"); 461 462 CommandFileParser commandFile = new CommandFileParser() { 463 @Override 464 BufferedReader createCommandFileReader(File file) { 465 if (mMockFile.equals(file)) { 466 return new BufferedReader(new StringReader(mockFileData)); 467 } else if (expectedFile.equals(file)) { 468 return new BufferedReader(new StringReader(mockIncludedFileData)); 469 } else { 470 fail(String.format("Received unexpected request for contents of file %s", 471 file)); 472 // shouldn't actually reach here 473 throw new RuntimeException(); 474 } 475 } 476 }; 477 assertParsedData(commandFile, expectedArgs); 478 } 479 480 /** 481 * Verify that INCLUDEing an absolute path works, even if a parent directory is specified. 482 * <p /> 483 * This verifies the fix for a bug that existed because {@code File("/tmp", "/usr/bin")} creates 484 * the path {@code /tmp/usr/bin}, rather than {@code /usr/bin} (which might be expected since 485 * the child is actually an absolute path on its own. 486 */ testMacroParserInclude_absoluteInclude()487 public void testMacroParserInclude_absoluteInclude() throws Exception { 488 // When we pass an unqualified filename, expect it to be taken relative to mMockFile's 489 // parent directory 490 final String includeFileName = "/usr/bin/somefile.txt"; 491 final File expectedFile = new File(includeFileName); 492 493 final String mockFileData = String.format("INCLUDE %s\n", includeFileName); 494 final String mockIncludedFileData = "--foo bar\n"; 495 List<String> expectedArgs = Arrays.asList("--foo", "bar"); 496 497 CommandFileParser commandFile = new CommandFileParser() { 498 @Override 499 BufferedReader createCommandFileReader(File file) { 500 if (mMockFile.equals(file)) { 501 return new BufferedReader(new StringReader(mockFileData)); 502 } else if (expectedFile.equals(file)) { 503 return new BufferedReader(new StringReader(mockIncludedFileData)); 504 } else { 505 fail(String.format("Received unexpected request for contents of file %s", 506 file)); 507 // shouldn't actually reach here 508 throw new RuntimeException(); 509 } 510 } 511 }; 512 assertParsedData(commandFile, expectedArgs); 513 } 514 515 /** 516 * Verify that if the original file is relative to no directory (aka ./), that the referenced 517 * file is also relative to no directory. 518 */ testMacroParserInclude_noParentDir()519 public void testMacroParserInclude_noParentDir() throws Exception { 520 final File mockFile = new File("original.txt"); 521 final String includeFileName = "somefile.txt"; 522 final File expectedFile = new File(includeFileName); 523 524 final String mockFileData = String.format("INCLUDE %s\n", includeFileName); 525 final String mockIncludedFileData = "--foo bar\n"; 526 List<String> expectedArgs = Arrays.asList("--foo", "bar"); 527 528 CommandFileParser commandFile = new CommandFileParser() { 529 @Override 530 BufferedReader createCommandFileReader(File file) { 531 if (mockFile.equals(file)) { 532 return new BufferedReader(new StringReader(mockFileData)); 533 } else if (expectedFile.equals(file)) { 534 return new BufferedReader(new StringReader(mockIncludedFileData)); 535 } else { 536 fail(String.format("Received unexpected request for contents of file %s", 537 file)); 538 // shouldn't actually reach here 539 throw new RuntimeException(); 540 } 541 } 542 }; 543 assertParsedData(commandFile, mockFile, expectedArgs); 544 assertEquals(1, commandFile.getIncludedFiles().size()); 545 assertTrue(commandFile.getIncludedFiles().iterator().next().endsWith(includeFileName)); 546 } 547 548 /** 549 * A testcase to make sure that the internal bitmask and mLines stay in sync 550 * <p> 551 * This tickles a bug in the current implementation (before I fix it). The problem is here, 552 * where the inputBitmask is only set to false conditionally, but inputBitmaskCount is 553 * decremented unconditionally: 554 * <code>inputBitmask.set(inputIdx, sawMacro); 555 * --inputBitmaskCount;</code> 556 */ testMacroParserSync_suffix()557 public void testMacroParserSync_suffix() throws IOException, ConfigurationException { 558 mMockFileData = "MACRO alpha = one beta()\n" + 559 "MACRO beta = two\n" + 560 "alpha()\n"; 561 List<String> expectedArgs = Arrays.asList("one", "two"); 562 // When the bug manifests, the result is {"one", "alpha()"} 563 564 assertParsedData(expectedArgs); 565 } 566 567 /** 568 * A testcase to make sure that the internal bitmask and mLines stay in sync 569 * <p> 570 * This tests a case related to the _suffix test above. 571 */ testMacroParserSync_prefix()572 public void testMacroParserSync_prefix() throws IOException, ConfigurationException { 573 mMockFileData = "MACRO alpha = beta() two\n" + 574 "MACRO beta = one\n" + 575 "alpha()\n"; 576 List<String> expectedArgs = Arrays.asList("one", "two"); 577 578 assertParsedData(expectedArgs); 579 } 580 581 /** 582 * Another test to verify a bugfix. Long Macro expansion can cause the inputBitmask and its 583 * cached form, inputBitmaskCount, to get out of sync. 584 * <p /> 585 * The bug is that the cached value isn't incremented when a long macro is expanded (which means 586 * that it may not account for the extra lines that it needs to process). This manifests as a 587 * partially-completed long macro expansion. 588 * <p /> 589 * In this test, when the bug manifests, the first Command to be added will be 590 * {@code ["one", "hbar()", "z", "x"} instead of the correct {@code ["one", "quux", "z", "x"]}. 591 */ testLongMacroSync()592 public void testLongMacroSync() throws IOException, ConfigurationException { 593 mMockFileData = 594 "MACRO hbar = quux\n" + 595 "LONG MACRO bar\n" + 596 "hbar() z\n" + 597 "END MACRO\n" + 598 "LONG MACRO foo\n" + 599 "bar() x\n" + 600 "END MACRO\n" + 601 "LONG MACRO test\n" + 602 "one foo()\n" + 603 "END MACRO\n" + 604 "test()\n" + 605 "hbar()\n"; 606 607 List<String> expectedArgs1 = Arrays.asList("one", "quux", "z", "x"); 608 List<String> expectedArgs2 = Arrays.asList("quux"); 609 610 assertParsedData(expectedArgs1, expectedArgs2); 611 } 612 } 613