/* * Copyright (C) 2011 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.sdk.tests; import com.android.ddmlib.testrunner.ITestRunListener.TestFailure; import com.android.ddmlib.testrunner.TestIdentifier; import com.android.tradefed.build.IBuildInfo; import com.android.tradefed.build.ISdkBuildInfo; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.result.ITestInvocationListener; import com.android.tradefed.testtype.IBuildReceiver; import com.android.tradefed.testtype.IRemoteTest; import com.android.tradefed.util.CommandResult; import com.android.tradefed.util.CommandStatus; import com.android.tradefed.util.FileUtil; import com.android.tradefed.util.IRunUtil; import com.android.tradefed.util.RunUtil; import com.android.tradefed.util.xml.AndroidManifestWriter; import junit.framework.Assert; import junit.framework.AssertionFailedError; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.Collections; import java.util.Properties; /** * A class that builds all the test-apps included in an SDK with ant, against all targets included * in SDK, and verifies success. */ public class SdkTestAppTest implements IRemoteTest, IBuildReceiver { private ISdkBuildInfo mSdkBuild; /** * {@inheritDoc} */ @Override public void setBuild(IBuildInfo buildInfo) { mSdkBuild = (ISdkBuildInfo)buildInfo; } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { Assert.assertNotNull("missing sdk build to test", mSdkBuild); Assert.assertNotNull("missing sdk build to test", mSdkBuild.getSdkDir()); CLog.i(String.format("Running test-app tests in sdk %s", mSdkBuild.getSdkDir().getAbsolutePath())); // get the path to the test-apps File sdkTestAppDir = FileUtil.getFileForPath(mSdkBuild.getSdkDir(), "tests", "testapps"); Assert.assertTrue(String.format("could not find tests/testapps folder in sdk %s", mSdkBuild.getSdkDir()), sdkTestAppDir.isDirectory() && sdkTestAppDir.listFiles() != null); Assert.assertTrue(String.format("Could not find targets for sdk %s", mSdkBuild.getSdkDir()), mSdkBuild.getSdkTargets() != null && mSdkBuild.getSdkTargets().length > 0); long startTime = System.currentTimeMillis(); listener.testRunStarted("testapp", 0); for (String target : mSdkBuild.getSdkTargets()) { buildTestAppsForTarget(target, sdkTestAppDir.listFiles(), listener); } listener.testRunEnded(System.currentTimeMillis() - startTime, Collections.EMPTY_MAP); } /** * Builds all test apps found in given SDK, for given target * * @param target the SDK target to build against * @param testAppDirs the list of test-app directories * @param listener the test result listener */ private void buildTestAppsForTarget(String target, File[] testAppDirs, ITestInvocationListener listener) { // first build libraries, since other projects may have dependency on them for (File testAppDir : testAppDirs) { if (isLibrary(testAppDir)) { buildTestApp(target, listener, testAppDir, true); } } // now build the test apps and record the results as dynamically generated test names for (File testAppDir : testAppDirs) { if (isAndroidApp(testAppDir)) { buildTestApp(target, listener, testAppDir, false); } } } /** * Determines if given test app is an Android library project. * <p/> * Looks for the 'android.library' property in this app's default.properties file * * @param testAppDir the app's directory * @return <code>true</code> if app is an Android library */ private boolean isLibrary(File testAppDir) { if (!testAppDir.isDirectory()) { return false; } File propertyFile = new File(testAppDir, "default.properties"); if (propertyFile.isFile()) { Properties defaultProp = new Properties(); InputStream propStream; try { propStream = new BufferedInputStream(new FileInputStream(propertyFile)); defaultProp.load(propStream); String libPropString = defaultProp.getProperty("android.library"); return libPropString != null && Boolean.parseBoolean(libPropString); } catch (IOException e) { CLog.e("Failed to parse %s", propertyFile.getAbsolutePath()); CLog.e(e); } } return false; } /** * Determines if given test app is a Android application project * * @param testAppDir the given test app directory root to evaluate * @return <code>true</code> if <var>testAppDir</var> is a Android application project */ private boolean isAndroidApp(File testAppDir) { if (!testAppDir.isDirectory()) { return false; } if (isLibrary(testAppDir)) { return false; } File manifestFile = new File(testAppDir, "AndroidManifest.xml"); return manifestFile.exists(); } /** * Build test app, and report results to the <var>listener</var> using test name * 'com.android.tradefed.testtype.SdkTestAppTest#(testAppName)(sdkTarget)' * * @param target the sdk target to build against * @param listener the {@link ITestInvocationListener} * @param testAppDir the {@link File} pointing to test app's root directory */ @SuppressWarnings("unchecked") private void buildTestApp(String target, ITestInvocationListener listener, File testAppDir, boolean isLibrary) { CLog.i("Building %s test-app for target %s", testAppDir.getName(), target); // dynamically generate a test name TestIdentifier testId = new TestIdentifier(this.getClass().getName(), String.format( "%s_%s", testAppDir.getName(), target)); listener.testStarted(testId); try { runTestAppTest(target, testAppDir, isLibrary); } catch (AssertionError e) { CLog.w("%s failed. %s", testId, e); listener.testFailed(TestFailure.FAILURE, testId, getThrowableTraceAsString(e)); } catch (Throwable t) { CLog.w("%s failed. %s", testId, t); listener.testFailed(TestFailure.ERROR, testId, getThrowableTraceAsString(t)); } listener.testEnded(testId, Collections.EMPTY_MAP); } private void runTestAppTest(String target, File testAppDir, boolean isLibrary) { updateMinSdkVersion(target, testAppDir); File buildFile = updateProject(target, testAppDir, isLibrary); doAntBuild(testAppDir.getName(), buildFile, isLibrary); // TODO: deploy to emulator and verify success } private void updateMinSdkVersion(String target, File testAppDir) { // if target is a preview, test apps will fail to build unless their manifest declares // minSdkVersion = target // always update minSdkVersion == target for simplicity String api = parseApiFromTarget(target); CLog.i("Updating minSdkVersion to %s for %s", api, testAppDir.getName()); File manifestFile = new File(testAppDir, "AndroidManifest.xml"); Assert.assertTrue(String.format("Could not find AndroidManifest.xml file for %s", testAppDir.getName()), manifestFile.exists()); AndroidManifestWriter manifestWriter = AndroidManifestWriter.parse( manifestFile.getAbsolutePath()); Assert.assertNotNull(String.format("Could not parse AndroidManifest.xml file for %s", testAppDir.getName()), manifestWriter); manifestWriter.setMinSdkVersion(api); } /** * Parse out the API level from the SDK target string. * <p/> * Assumes SDK target is in form: 'android-[API level]' * * @param target the SDK target * @return the API level */ private String parseApiFromTarget(String target) { int sepIndex = target.lastIndexOf('-'); Assert.assertTrue(String.format("Could not determine API level from SDK target %s", target), sepIndex != -1); return target.substring(sepIndex+1, target.length()); } /** * Calls 'android update project' on given test app. * * @param target the sdk target * @param testAppDir the test app directory * @param isLibrary <code>true</code> if app is a library project * @throws AssertionFailedError if update project failed * @return a {@link File} representing the app's build.xml */ private File updateProject(String target, File testAppDir, boolean isLibrary) { CLog.i("Updating project for %s", testAppDir.getName()); String projectCmd = isLibrary ? "lib-project" : "project"; CommandResult result = getRunUtil().runTimedCmd(15 * 1000, mSdkBuild.getAndroidToolPath(), "update", projectCmd, "--path", testAppDir.getAbsolutePath(), "--target", target); Assert.assertEquals(String.format("update project for %s failed. stderr: %s", testAppDir.getName(), result.getStderr()), CommandStatus.SUCCESS, result.getStatus()); File buildFile = new File(testAppDir, "build.xml"); Assert.assertTrue(String.format("%s was not generated", buildFile.getAbsolutePath()), buildFile.exists()); return buildFile; } /** * Build the test app using ant, and verify success * * @param appName the name of the test app. Used for logging * @param buildFile */ private void doAntBuild(String appName, File buildFile, boolean isLibrary) { String antTarget = isLibrary ? "compile" : "debug"; CLog.i("Doing 'ant %s' for %s", antTarget, appName); CommandResult result = getRunUtil().runTimedCmd(30 * 1000, "ant", "-f", buildFile.getAbsolutePath(), antTarget); CLog.d("ant output:\n%s", result.getStdout()); // check that the command returned successful, and that 'BUILD SUCCESSFUL' appeared in // output Assert.assertEquals(String.format("ant %s for %s failed. stderr: %s", antTarget, appName, result.getStderr()), CommandStatus.SUCCESS, result.getStatus()); Assert.assertTrue(String.format("ant %s for %s failed. stderr: %s", antTarget, appName, result.getStderr()), result.getStdout().contains("BUILD SUCCESSFUL")); } /** * Gets the {@link IRunUtil} instance to use. * <p/> * Exposed for mocking */ IRunUtil getRunUtil() { return RunUtil.getDefault(); } /** * Gets a {@link Throwable}'s stack trace as a {@link String} * * @param t the {@link Throwable} * @return the stack trace in {@link String} form */ String getThrowableTraceAsString(Throwable t) { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); PrintWriter printWriter = new PrintWriter(byteStream, true); t.printStackTrace(printWriter); printWriter.close(); return new String(byteStream.toByteArray()); } }