blob: def79fd25b1b3b504df3266398609bd3c4263d24 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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.manifmerger;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.manifmerger.IMergerLog.FileAndLine;
import com.android.sdklib.mock.MockLog;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.w3c.dom.Document;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Some utilities to reduce repetitions in the {@link ManifestMergerTest}s.
* <p/>
* See {@link #loadTestData(String)} for an explanation of the data file format.
*/
public class ManifestMergerTest extends TestCase {
/**
* Delimiter that indicates the test must fail.
* An XML output and errors are still generated and checked.
*/
private static final String DELIM_FAILS = "fails";
/**
* Delimiter that starts a library XML content.
* The delimiter name must be in the form {@code @libSomeName} and it will be
* used as the base for the test file name. Using separate lib names is encouraged
* since it makes the error output easier to read.
*/
private static final String DELIM_LIB = "lib";
/**
* Delimiter that starts the main manifest XML content.
*/
private static final String DELIM_MAIN = "main";
/**
* Delimiter that starts an overlay XML content.
* The delimiter must follow the same rules as {@link #DELIM_LIB}
*/
private static final String DELIM_OVERLAY = "overlay";
/**
* Delimiter that starts the resulting XML content, whatever is generated by the merge.
*/
private static final String DELIM_RESULT = "result";
/**
* Delimiter that starts the SdkLog output.
* The logger prints each entry on its lines, prefixed with E for errors,
* W for warnings and P for regular printfs.
*/
private static final String DELIM_ERRORS = "errors";
/**
* Delimiter for starts a section that declares how to inject an attribute.
* The section is composed of one or more lines with the
* syntax: "/node/node|attr-URI attrName=attrValue".
* This is essentially a pseudo XPath-like expression that is described in
* {@link ManifestMerger#process(Document, File[], Map, String)}.
*/
private static final String DELIM_INJECT_ATTR = "inject";
/**
* Delimiter for a section that declares how to toggle a ManifMerger option.
* The section is composed of one or more lines with the
* syntax: "functionName=false|true".
*/
private static final String DELIM_FEATURES = "features";
/**
* Delimiter for a section that declares how to override the package.
* The section is composed of one line containing the new package name.
*/
private static final String DELIM_PACKAGE = "package";
/*
* Wait, I hear you, where are the tests?
*
* processTestFiles() uses loadTestData(), which uses one of the data filename
* indicated below.
*
* We could simplify this even further by dynamically finding the data
* files to use; however there's some value in having tests break when out
* of sync with the known data file set.
*/
private static String[] sDataFiles = new String[] {
"00_noop",
"01_ignore_app_attr",
"02_ignore_instrumentation",
"03_inject_attributes",
"04_inject_attributes",
"05_inject_package",
"10_activity_merge",
"11_activity_dup",
"12_alias_dup",
"13_service_dup",
"14_receiver_dup",
"15_provider_dup",
"16_fqcn_merge",
"17_fqcn_conflict",
"20_uses_lib_merge",
"21_uses_lib_errors",
"25_permission_merge",
"26_permission_dup",
"28_uses_perm_merge",
"30_uses_sdk_ok",
"32_uses_sdk_minsdk_ok",
"33_uses_sdk_minsdk_conflict",
"36_uses_sdk_targetsdk_warning",
"40_uses_feat_merge",
"41_uses_feat_errors",
"45_uses_feat_gles_once",
"47_uses_feat_gles_conflict",
"50_uses_conf_warning",
"52_support_screens_warning",
"54_compat_screens_warning",
"56_support_gltext_warning",
"60_merge_order",
"65_override_app",
"66_remove_app",
"67_override_activities",
"68_override_uses",
"69_remove_uses",
"70_expand_fqcns",
"71_extract_package_prefix",
"75_app_metadata_merge",
"76_app_metadata_ignore",
"77_app_metadata_conflict",
};
/**
* This overrides the default test suite created by junit.
* The test suite is a bland TestSuite with a dedicated name.
* We inject as many instances of {@link ManifestMergerTest} in the suite
* as we have declared data files above.
*
* @return A new {@link TestSuite}.
*/
public static Test suite() {
TestSuite suite = new TestSuite();
// Give a non-generic name to our test suite, for better unit reports.
suite.setName("ManifestMergerTestSuite");
for (String fileName : sDataFiles) {
suite.addTest(TestSuite.createTest(ManifestMergerTest.class, fileName));
}
return suite;
}
/**
* Default constructor invoked by {@link TestSuite#createTest(Class, String)}.
*
* @param testName The test name provided to {@code TestSuite.createTest()}.
* This is later accessible via {@link #getName()}.
*/
public ManifestMergerTest(String testName) {
super(testName);
}
/**
* Invoked by the test framework to run the specific test which name
* has been passed to the constructor.
* Note that we create one instance of this class per test to run in
* the associated {@link TestSuite}.
*/
@Override
protected void runTest() throws Throwable {
String testName = getName();
assertNotNull(testName);
processTestFiles(loadTestData(testName));
}
static class TestFiles {
private final File[] mOverlayFiles;
private final File mMain;
private final File[] mLibs;
private final Map<String, String> mInjectAttributes;
private final String mPackageOverride;
private final File mActualResult;
private final String mExpectedResult;
private final String mExpectedErrors;
private final boolean mShouldFail;
private final Map<String, Boolean> mFeatures;
/** Files used by a given test case. */
public TestFiles(
boolean shouldFail,
@NonNull File[] overlayFiles,
@NonNull File main,
@NonNull File[] libs,
@NonNull Map<String, Boolean> features,
@NonNull Map<String, String> injectAttributes,
@Nullable String packageOverride,
@Nullable File actualResult,
@NonNull String expectedResult,
@NonNull String expectedErrors) {
mShouldFail = shouldFail;
mMain = main;
mLibs = libs;
mFeatures = features;
mPackageOverride = packageOverride;
mInjectAttributes = injectAttributes;
mActualResult = actualResult;
mExpectedResult = expectedResult;
mExpectedErrors = expectedErrors;
mOverlayFiles = overlayFiles;
}
public boolean getShouldFail() {
return mShouldFail;
}
@NonNull
public File[] getOverlayFiles() {
return mOverlayFiles;
}
@NonNull
public File getMain() {
return mMain;
}
@NonNull
public File[] getLibs() {
return mLibs;
}
@NonNull
public Map<String, Boolean> getFeatures() {
return mFeatures;
}
@NonNull
public Map<String, String> getInjectAttributes() {
return mInjectAttributes;
}
@Nullable
public String getPackageOverride() {
return mPackageOverride;
}
@Nullable
public File getActualResult() {
return mActualResult;
}
@NonNull
public String getExpectedResult() {
return mExpectedResult;
}
public String getExpectedErrors() {
return mExpectedErrors;
}
// Try to delete any temp file potentially created.
public void cleanup() {
if (mMain != null && mMain.isFile()) {
mMain.delete();
}
if (mActualResult != null && mActualResult.isFile()) {
mActualResult.delete();
}
for (File f : mLibs) {
if (f != null && f.isFile()) {
f.delete();
}
}
}
}
/**
* Calls {@link #loadTestData(String)} by
* inferring the data filename from the caller's method name.
* <p/>
* The caller method name must be composed of "test" + the leaf filename.
* Extensions ".xml" or ".txt" are implied.
* <p/>
* E.g. to use the data file "12_foo.xml", simply call this from a method
* named "test12_foo".
*
* @return A new {@link TestFiles} instance. Never null.
* @throws Exception when things go wrong.
* @see #loadTestData(String)
*/
@NonNull
TestFiles loadTestData() throws Exception {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
for (int i = 0, n = stack.length; i < n; i++) {
StackTraceElement caller = stack[i];
String name = caller.getMethodName();
if (name.startsWith("test")) {
return loadTestData(name.substring(4));
}
}
throw new IllegalArgumentException("No caller method found which name started with 'test'");
}
/**
* Returns the relative path the test data directory
*/
protected String getTestDataDirectory() {
return "data";
}
/**
* Loads test data for a given test case.
* The input (main + libs) are stored in temp files.
* A new destination temp file is created to store the actual result output.
* The expected result is actually kept in a string.
* <p/>
* Data File Syntax:
* <ul>
* <li> Lines starting with # are ignored (anywhere, as long as # is the first char).
* <li> Lines before the first {@code @delimiter} are ignored.
* <li> Empty lines just after the {@code @delimiter}
* and before the first &lt; XML line are ignored.
* <li> Valid delimiters are {@code @main} for the XML of the main app manifest.
* <li> Following delimiters are {@code @libXYZ}, read in the order of definition.
* The name can be anything as long as it starts with "{@code @lib}".
* </ul>
*
* @param filename The test data filename. If no extension is provided, this will
* try with .xml or .txt. Must not be null.
* @return A new {@link TestFiles} instance. Must not be null.
* @throws Exception when things fail to load properly.
*/
@NonNull
TestFiles loadTestData(@NonNull String filename) throws Exception {
String resName = getTestDataDirectory() + File.separator + filename;
InputStream is = null;
BufferedReader reader = null;
BufferedWriter writer = null;
try {
is = this.getClass().getResourceAsStream(resName);
if (is == null && !filename.endsWith(".xml")) {
String resName2 = resName + ".xml";
is = this.getClass().getResourceAsStream(resName2);
if (is != null) {
filename = resName2;
}
}
if (is == null && !filename.endsWith(".txt")) {
String resName3 = resName + ".txt";
is = this.getClass().getResourceAsStream(resName3);
if (is != null) {
filename = resName3;
}
}
assertNotNull("Test data file not found for " + filename, is);
reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
// Get the temporary directory to use. Just create a temp file, extracts its
// directory and remove the file.
File tempFile = File.createTempFile(this.getClass().getSimpleName(), ".tmp");
File tempDir = tempFile.getParentFile();
if (!tempFile.delete()) {
tempFile.deleteOnExit();
}
String line = null;
String delimiter = null;
boolean skipEmpty = true;
boolean shouldFail = false;
Map<String, Boolean> features = new HashMap<String, Boolean>();
String packageOverride = null;
Map<String, String> injectAttributes = new HashMap<String, String>();
StringBuilder expectedResult = new StringBuilder();
StringBuilder expectedErrors = new StringBuilder();
File mainFile = null;
File actualResultFile = null;
List<File> libFiles = new ArrayList<File>();
List<File> overlayFiles = new ArrayList<File>();
int tempIndex = 0;
while ((line = reader.readLine()) != null) {
if (skipEmpty && line.trim().isEmpty()) {
continue;
}
if (!line.isEmpty() && line.charAt(0) == '#') {
continue;
}
if (!line.isEmpty() && line.charAt(0) == '@') {
delimiter = line.substring(1);
assertTrue(
"Unknown delimiter @" + delimiter + " in " + filename,
delimiter.startsWith(DELIM_OVERLAY) ||
delimiter.startsWith(DELIM_LIB) ||
delimiter.equals(DELIM_MAIN) ||
delimiter.equals(DELIM_RESULT) ||
delimiter.equals(DELIM_ERRORS) ||
delimiter.equals(DELIM_FAILS) ||
delimiter.equals(DELIM_FEATURES) ||
delimiter.equals(DELIM_INJECT_ATTR) ||
delimiter.equals(DELIM_PACKAGE));
skipEmpty = true;
if (writer != null) {
try {
writer.close();
} catch (IOException ignore) {}
writer = null;
}
if (delimiter.equals(DELIM_FAILS)) {
shouldFail = true;
} else if (!delimiter.equals(DELIM_ERRORS) &&
!delimiter.equals(DELIM_FEATURES) &&
!delimiter.equals(DELIM_INJECT_ATTR) &&
!delimiter.equals(DELIM_PACKAGE)) {
tempFile = new File(tempDir, String.format("%1$s%2$d_%3$s.xml",
this.getClass().getSimpleName(),
tempIndex++,
delimiter.replaceAll("[^a-zA-Z0-9_-]", "")
));
tempFile.deleteOnExit();
if (delimiter.startsWith(DELIM_OVERLAY)) {
overlayFiles.add(tempFile);
} else if (delimiter.startsWith(DELIM_LIB)) {
libFiles.add(tempFile);
} else if (delimiter.equals(DELIM_MAIN)) {
mainFile = tempFile;
} else if (delimiter.equals(DELIM_RESULT)) {
actualResultFile = tempFile;
} else {
fail("Unexpected data file delimiter @" + delimiter +
" in " + filename);
}
if (!delimiter.equals(DELIM_RESULT)) {
writer = new BufferedWriter(new FileWriter(tempFile));
}
}
continue;
}
if (delimiter != null &&
skipEmpty &&
!line.isEmpty() &&
line.charAt(0) != '#' &&
line.charAt(0) != '@') {
skipEmpty = false;
}
if (writer != null) {
writer.write(line);
writer.write('\n');
} else if (DELIM_RESULT.equals(delimiter)) {
expectedResult.append(line).append('\n');
} else if (DELIM_ERRORS.equals(delimiter)) {
expectedErrors.append(line).append('\n');
} else if (DELIM_INJECT_ATTR.equals(delimiter)) {
String[] in = line.split("=");
if (in != null && in.length == 2) {
injectAttributes.put(in[0], "null".equals(in[1]) ? null : in[1]);
}
} else if (DELIM_FEATURES.equals(delimiter)) {
String[] in = line.split("=");
if (in != null && in.length == 2) {
features.put(in[0], Boolean.parseBoolean(in[1]));
}
} else if (DELIM_PACKAGE.equals(delimiter)) {
if (packageOverride == null) {
packageOverride = line;
}
}
}
assertNotNull("Missing @" + DELIM_MAIN + " in " + filename, mainFile);
assert mainFile != null;
Collections.sort(libFiles);
return new TestFiles(
shouldFail,
overlayFiles.toArray(new File[overlayFiles.size()]),
mainFile,
libFiles.toArray(new File[libFiles.size()]),
features,
injectAttributes,
packageOverride,
actualResultFile,
expectedResult.toString(),
expectedErrors.toString());
} catch (UnsupportedEncodingException e) {
// BufferedReader failed to decode UTF-8, O'RLY?
throw e;
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException ignore) {}
}
if (reader != null) {
try {
reader.close();
} catch (IOException ignore) {}
}
if (is != null) {
try {
is.close();
} catch (IOException ignore) {}
}
}
}
// /**
// * Loads the data test files using {@link #loadTestData()} and then
// * invokes {@link #processTestFiles(TestFiles)} to test them.
// *
// * @see #loadTestData()
// * @see #processTestFiles(TestFiles)
// */
// void processTestFiles() throws Exception {
// processTestFiles(loadTestData());
// }
/**
* Processes the data from the given {@link TestFiles} by
* invoking {@link ManifestMerger#process(File, File, File[], Map, String)}:
* the given library files are applied consecutively to the main XML
* document and the output is generated.
* <p/>
* Then the expected and actual outputs are loaded into a DOM,
* dumped again to a String using an XML transform and compared.
* This makes sure only the structure is checked and that any
* formatting is ignored in the comparison.
*
* @param testFiles The test files to process. Must not be null.
* @throws Exception when this go wrong.
*/
void processTestFiles(TestFiles testFiles) throws Exception {
MockLog log = new MockLog();
IMergerLog mergerLog = MergerLog.wrapSdkLog(log);
ManifestMerger merger = new ManifestMerger(mergerLog, new ICallback() {
@Override
public int queryCodenameApiLevel(@NonNull String codename) {
if ("ApiCodename1".equals(codename)) {
return 1;
} else if ("ApiCodename10".equals(codename)) {
return 10;
}
return ICallback.UNKNOWN_CODENAME;
}
});
for (Entry<String, Boolean> feature : testFiles.getFeatures().entrySet()) {
Method m = merger.getClass().getMethod(
feature.getKey(),
new Class<?>[] { boolean.class } );
m.invoke(merger, new Object[] { feature.getValue() } );
}
boolean processOK = merger.process(testFiles.getActualResult(),
testFiles.getMain(),
testFiles.getLibs(),
testFiles.getInjectAttributes(),
testFiles.getPackageOverride());
// Convert relative pathnames to absolute.
String expectedErrors = testFiles.getExpectedErrors().trim();
expectedErrors = expectedErrors.replaceAll(
Pattern.quote(testFiles.getMain().getName()),
Matcher.quoteReplacement(testFiles.getMain().getAbsolutePath()));
for (File file : testFiles.getLibs()) {
expectedErrors = expectedErrors.replaceAll(
Pattern.quote(file.getName()),
Matcher.quoteReplacement(file.getAbsolutePath()));
}
StringBuilder actualErrors = new StringBuilder();
for (String s : log.getMessages()) {
actualErrors.append(s);
if (!s.endsWith("\n")) {
actualErrors.append('\n');
}
}
assertEquals("Error generated during merging",
expectedErrors, actualErrors.toString().trim());
if (testFiles.getShouldFail()) {
assertFalse("Merge process() returned true, expected false", processOK);
} else {
assertTrue("Merge process() returned false, expected true", processOK);
}
// Test result XML. There should always be one created
// since the process action does not stop on errors.
log.clear();
Document document = MergerXmlUtils.parseDocument(testFiles.getActualResult(), mergerLog,
merger);
assertNotNull(document);
assert document != null; // for Eclipse null analysis
String actual = MergerXmlUtils.printXmlString(document, mergerLog);
assertEquals("Error parsing actual result XML", "", log.toString());
log.clear();
document = MergerXmlUtils.parseDocument(
testFiles.getExpectedResult(),
mergerLog,
new FileAndLine("<expected-result>", 0));
assertNotNull("Failed to parse result document: " + testFiles.getExpectedResult(),document);
assert document != null;
String expected = MergerXmlUtils.printXmlString(document, mergerLog);
assertEquals("Error parsing expected result XML", "", log.toString());
assertEquals("Error comparing expected to actual result", expected, actual);
testFiles.cleanup();
}
}