/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.command;
import com.android.tradefed.config.ConfigurationException;
import junit.framework.TestCase;
import org.easymock.EasyMock;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Unit tests for {@link CommandFileParser}
*/
public class CommandFileParserTest extends TestCase {
/** the {@link CommandFileParser} under test, with all dependencies mocked out */
private CommandFileParser mCommandFile;
private String mMockFileData = "";
private static final File MOCK_FILE_PATH = new File("path/to/");
private File mMockFile = new File(MOCK_FILE_PATH, "original.txt");
private ICommandScheduler mMockScheduler;
@Override
protected void setUp() throws Exception {
super.setUp();
mMockScheduler = EasyMock.createMock(ICommandScheduler.class);
mCommandFile = new CommandFileParser() {
@Override
BufferedReader createCommandFileReader(File file) {
return new BufferedReader(new StringReader(mMockFileData));
}
};
}
/**
* Test parsing a command file with a comment and a single config.
*/
public void testParse_singleConfig() throws Exception {
// inject mock file data
mMockFileData = " #Comment followed by blank line\n \n--foo config";
String[] expectedArgs = new String[] {
"--foo", "config"
};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Make sure that a config with a quoted argument is parsed properly.
* <p/>
* Whitespace inside of the quoted section should be preserved. Also, embedded escaped quotation
* marks should be ignored.
*/
public void testParseFile_quotedConfig() throws IOException, ConfigurationException {
// inject mock file data
mMockFileData = "--foo \"this is a config\" --bar \"escap\\\\ed \\\" quotation\"";
String[] expectedArgs = new String[] {
"--foo", "this is a config", "--bar", "escap\\\\ed \\\" quotation"
};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Test the data where the configuration ends inside a quotation.
*/
public void testParseFile_endOnQuote() throws IOException {
// inject mock file data
mMockFileData = "--foo \"this is truncated";
EasyMock.replay(mMockScheduler);
try {
mCommandFile.parseFile(mMockFile, mMockScheduler);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
}
EasyMock.verify(mMockScheduler);
}
/**
* Test parsing a command file while passing in extra arguments.
*/
public void testParseArgs() throws Exception {
// inject mock file data
mMockFileData = "--foo config\n--foo config2\n";
List<String> args = Arrays.asList("--arg", "cowabunga");
String[] exp1 = new String[] {"--foo", "config", "--arg", "cowabunga"};
String[] exp2 = new String[] {"--foo", "config2", "--arg", "cowabunga"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(exp1))).andReturn(Boolean.TRUE);
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(exp2))).andReturn(Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler, args);
EasyMock.verify(mMockScheduler);
}
/**
* Test the scenario where the configuration ends inside a quotation.
*/
public void testRun_endWithEscape() throws IOException {
// inject mock file data
mMockFileData = "--foo escape\\";
// switch mock objects to verify mode
EasyMock.replay(mMockScheduler);
try {
mCommandFile.parseFile(mMockFile, mMockScheduler);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
}
EasyMock.verify(mMockScheduler);
}
// Macro-related tests
public void testSimpleMacro() throws IOException, ConfigurationException {
mMockFileData = "MACRO TeSt = verify\nTeSt()";
String[] expectedArgs = new String[] {"verify"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Ensure that when a macro is overwritten, the most recent value should be used.
* <p />
* Note that a message should also be logged; no good way to verify that currently
*/
public void testOverwriteMacro() throws IOException, ConfigurationException {
mMockFileData = "MACRO test = value 1\nMACRO test = value 2\ntest()";
String[] expectedArgs = new String[] {"value", "2"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Ensure that parsing of quoted tokens continues to work
*/
public void testSimpleMacro_quotedTokens() throws IOException, ConfigurationException {
mMockFileData = "MACRO test = \"verify varify vorify\"\ntest()";
String[] expectedArgs = new String[] {"verify varify vorify"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Ensure that parsing of names with embedded underscores works properly.
*/
public void testSimpleMacro_underscoreName() throws IOException, ConfigurationException {
mMockFileData = "MACRO under_score = verify\nunder_score()";
String[] expectedArgs = new String[] {"verify"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Ensure that parsing of names with embedded hyphens works properly.
*/
public void testSimpleMacro_hyphenName() throws IOException, ConfigurationException {
mMockFileData = "MACRO hyphen-nated = verify\nhyphen-nated()";
String[] expectedArgs = new String[] {"verify"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Test the scenario where a macro call doesn't resolve.
*/
public void testUndefinedMacro() throws IOException {
mMockFileData = "test()";
EasyMock.replay(mMockScheduler);
try {
mCommandFile.parseFile(mMockFile, mMockScheduler);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
}
EasyMock.verify(mMockScheduler);
}
/**
* Test the scenario where a syntax problem causes a macro call to not resolve.
*/
public void testUndefinedMacro_defSyntaxError() throws IOException {
mMockFileData = "MACRO test = \n" +
"test()";
EasyMock.replay(mMockScheduler);
try {
mCommandFile.parseFile(mMockFile, mMockScheduler);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
}
EasyMock.verify(mMockScheduler);
}
/**
* Simple test for LONG MACRO parsing
*/
public void testSimpleLongMacro() throws IOException, ConfigurationException {
mMockFileData = "LONG MACRO test\n" +
"verify\n" +
"END MACRO\n" +
"test()";
String[] expectedArgs = new String[] {"verify"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Ensure that when a long macro is overwritten, the most recent value should be used.
* <p />
* Note that a message should also be logged; no good way to verify that currently
*/
/**
* Simple test for LONG MACRO parsing with multi-line expansion
*/
public void testSimpleLongMacro_multiline() throws IOException, ConfigurationException {
mMockFileData = "LONG MACRO test\n" +
"one two three\n" +
"a b c\n" +
"do re mi\n" +
"END MACRO\n" +
"test()";
String[] expectedArgs1 = new String[] {"one", "two", "three"};
String[] expectedArgs2 = new String[] {"a", "b", "c"};
String[] expectedArgs3 = new String[] {"do", "re", "mi"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs1))).andReturn(
Boolean.TRUE);
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs2))).andReturn(
Boolean.TRUE);
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs3))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Simple test for LONG MACRO parsing with multi-line expansion
*/
public void testLongMacro_withComment() throws IOException, ConfigurationException {
mMockFileData = "LONG MACRO test\n" +
"\n" + // blank line
"one two three\n" +
"#a b c\n" +
"do re mi\n" +
"END MACRO\n" +
"test()";
String[] expectedArgs1 = new String[] {"one", "two", "three"};
String[] expectedArgs2 = new String[] {"do", "re", "mi"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs1))).andReturn(
Boolean.TRUE);
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs2))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Test the scenario where the configuration ends inside of a LONG MACRO definition.
*/
public void testLongMacroSyntaxError_eof() throws IOException {
mMockFileData = "LONG MACRO test\n" +
"verify\n" +
// "END MACRO\n" (this is the syntax error)
"test()";
EasyMock.replay(mMockScheduler);
try {
mCommandFile.parseFile(mMockFile, mMockScheduler);
fail("ConfigurationException not thrown");
} catch (ConfigurationException e) {
// expected
}
EasyMock.verify(mMockScheduler);
}
/**
* Verify that the INCLUDE directive is behaving properly
*/
public void testMacroParserInclude() throws Exception {
final String mockFileData = "INCLUDE somefile.txt\n";
final String mockIncludedFileData = "--foo bar\n";
String[] expectedArgs = new String[] {"--foo", "bar"};
CommandFileParser commandFile = new CommandFileParser() {
private boolean showInclude = true;
@Override
BufferedReader createCommandFileReader(File file) {
if(showInclude) {
showInclude = false;
return new BufferedReader(new StringReader(mockFileData));
} else {
return new BufferedReader(new StringReader(mockIncludedFileData));
}
}
};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
commandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Verify that the INCLUDE directive works when used for two files
*/
public void testMacroParserInclude_twice() throws Exception {
final String mockFileData = "INCLUDE somefile.txt\n" +
"INCLUDE otherfile.txt\n";
final String mockIncludedFileData1 = "--foo bar\n";
final String mockIncludedFileData2 = "--baz quux\n";
String[] expectedArgs1 = new String[] {"--foo", "bar"};
String[] expectedArgs2 = new String[] {"--baz", "quux"};
CommandFileParser commandFile = new CommandFileParser() {
private int phase = 0;
@Override
BufferedReader createCommandFileReader(File file) {
if(phase == 0) {
phase++;
return new BufferedReader(new StringReader(mockFileData));
} else if (phase == 1) {
phase++;
return new BufferedReader(new StringReader(mockIncludedFileData1));
} else {
return new BufferedReader(new StringReader(mockIncludedFileData2));
}
}
};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs1))).andReturn(
Boolean.TRUE);
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs2))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
commandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Verify that a file is only ever included once, regardless of how many INCLUDE directives for
* that file show up
*/
public void testMacroParserInclude_repeat() throws Exception {
final String mockFileData = "INCLUDE somefile.txt\n" +
"INCLUDE somefile.txt\n";
final String mockIncludedFileData1 = "--foo bar\n";
String[] expectedArgs1 = new String[] {"--foo", "bar"};
CommandFileParser commandFile = new CommandFileParser() {
private int phase = 0;
@Override
BufferedReader createCommandFileReader(File file) {
if(phase == 0) {
phase++;
return new BufferedReader(new StringReader(mockFileData));
} else {
return new BufferedReader(new StringReader(mockIncludedFileData1));
}
}
};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs1))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
commandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Verify that the path of the file referenced by an INCLUDE directive is considered relative to
* the location of the referencing file.
*/
public void testMacroParserInclude_parentDir() throws Exception {
// When we pass an unqualified filename, expect it to be taken relative to mMockFile's
// parent directory
final String includeFileName = "somefile.txt";
final File expectedFile = new File(MOCK_FILE_PATH, includeFileName);
final String mockFileData = String.format("INCLUDE %s\n", includeFileName);
final String mockIncludedFileData = "--foo bar\n";
String[] expectedArgs = new String[] {"--foo", "bar"};
CommandFileParser commandFile = new CommandFileParser() {
@Override
BufferedReader createCommandFileReader(File file) {
if (mMockFile.equals(file)) {
return new BufferedReader(new StringReader(mockFileData));
} else if (expectedFile.equals(file)) {
return new BufferedReader(new StringReader(mockIncludedFileData));
} else {
fail(String.format("Received unexpected request for contents of file %s", file));
// shouldn't actually reach here
throw new RuntimeException();
}
}
};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
commandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Verify that INCLUDEing an absolute path works, even if a parent directory is specified.
* <p />
* This verifies the fix for a bug that existed because {@code File("/tmp", "/usr/bin")} creates
* the path {@code /tmp/usr/bin}, rather than {@code /usr/bin} (which might be expected since
* the child is actually an absolute path on its own.
*/
public void testMacroParserInclude_absoluteInclude() throws Exception {
// When we pass an unqualified filename, expect it to be taken relative to mMockFile's
// parent directory
final String includeFileName = "/usr/bin/somefile.txt";
final File expectedFile = new File(includeFileName);
final String mockFileData = String.format("INCLUDE %s\n", includeFileName);
final String mockIncludedFileData = "--foo bar\n";
String[] expectedArgs = new String[] {"--foo", "bar"};
CommandFileParser commandFile = new CommandFileParser() {
@Override
BufferedReader createCommandFileReader(File file) {
if (mMockFile.equals(file)) {
return new BufferedReader(new StringReader(mockFileData));
} else if (expectedFile.equals(file)) {
return new BufferedReader(new StringReader(mockIncludedFileData));
} else {
fail(String.format("Received unexpected request for contents of file %s", file));
// shouldn't actually reach here
throw new RuntimeException();
}
}
};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
commandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Verify that if the original file is relative to no directory (aka ./), that the referenced
* file is also relative to no directory.
*/
public void testMacroParserInclude_noParentDir() throws Exception {
final File mockFile = new File("original.txt");
final String includeFileName = "somefile.txt";
final File expectedFile = new File(includeFileName);
final String mockFileData = String.format("INCLUDE %s\n", includeFileName);
final String mockIncludedFileData = "--foo bar\n";
String[] expectedArgs = new String[] {"--foo", "bar"};
CommandFileParser commandFile = new CommandFileParser() {
@Override
BufferedReader createCommandFileReader(File file) {
if (mockFile.equals(file)) {
return new BufferedReader(new StringReader(mockFileData));
} else if (expectedFile.equals(file)) {
return new BufferedReader(new StringReader(mockIncludedFileData));
} else {
fail(String.format("Received unexpected request for contents of file %s", file));
// shouldn't actually reach here
throw new RuntimeException();
}
}
};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
commandFile.parseFile(mockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* A testcase to make sure that the internal bitmask and mLines stay in sync
* <p>
* This tickles a bug in the current implementation (before I fix it). The problem is here,
* where the inputBitmask is only set to false conditionally, but inputBitmaskCount is
* decremented unconditionally:
* <code>inputBitmask.set(inputIdx, sawMacro);
* --inputBitmaskCount;</code>
*/
public void testMacroParserSync_suffix() throws IOException, ConfigurationException {
mMockFileData = "MACRO alpha = one beta()\n" +
"MACRO beta = two\n" +
"alpha()\n";
String[] expectedArgs = new String[] {"one", "two"};
// When the bug manifests, the result is {"one", "alpha()"}
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* A testcase to make sure that the internal bitmask and mLines stay in sync
* <p>
* This tests a case related to the _suffix test above.
*/
public void testMacroParserSync_prefix() throws IOException, ConfigurationException {
mMockFileData = "MACRO alpha = beta() two\n" +
"MACRO beta = one\n" +
"alpha()\n";
String[] expectedArgs = new String[] {"one", "two"};
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(expectedArgs))).andReturn(
Boolean.TRUE);
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
/**
* Another test to verify a bugfix. Long Macro expansion can cause the inputBitmask and its
* cached form, inputBitmaskCount, to get out of sync.
* <p />
* The bug is that the cached value isn't incremented when a long macro is expanded (which means
* that it may not account for the extra lines that it needs to process). This manifests as a
* partially-completed long macro expansion.
* <p />
* In this test, when the bug manifests, the first Command to be added will be
* {@code ["one", "hbar()", "z", "x"} instead of the correct {@code ["one", "quux", "z", "x"]}.
*/
public void testLongMacroSync() throws IOException, ConfigurationException {
mMockFileData =
"MACRO hbar = quux\n" +
"LONG MACRO bar\n" +
"hbar() z\n" +
"END MACRO\n" +
"LONG MACRO foo\n" +
"bar() x\n" +
"END MACRO\n" +
"LONG MACRO test\n" +
"one foo()\n" +
"END MACRO\n" +
"test()\n" +
"hbar()\n";
List<String[]> expectedArgs = new ArrayList<String[]>(5);
expectedArgs.add(new String[] {"one", "quux", "z", "x"});
expectedArgs.add(new String[] {"quux"});
for (String[] ary : expectedArgs) {
EasyMock.expect(mMockScheduler.addCommand(EasyMock.aryEq(ary))).andReturn(Boolean.TRUE);
}
EasyMock.replay(mMockScheduler);
mCommandFile.parseFile(mMockFile, mMockScheduler);
EasyMock.verify(mMockScheduler);
}
}