From 92529f27adec9ce5ec1cb3dfd79a15df7773e27c Mon Sep 17 00:00:00 2001 From: Raphael Moll Date: Tue, 17 Apr 2012 14:17:40 -0700 Subject: SDK: XML to export platform stats. This would be used later by the NPW. Change-Id: Iec6f24182f234d83217d83f0087aa92eabb7609d --- .../sdklib/internal/repository/SdkStats.java | 589 +++++++++++++++++++++ .../sdklib/repository/SdkStatsConstants.java | 91 ++++ .../com/android/sdklib/repository/sdk-stats-1.xsd | 96 ++++ .../src/com/android/sdklib/util/SparseArray.java | 73 +++ .../sdklib/internal/repository/SdkStatsTest.java | 172 ++++++ .../com/android/sdklib/testdata/stats_sample_1.xml | 48 ++ 6 files changed, 1069 insertions(+) create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkStats.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkStatsConstants.java create mode 100755 sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-stats-1.xsd create mode 100755 sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkStatsTest.java create mode 100755 sdkmanager/libs/sdklib/tests/src/com/android/sdklib/testdata/stats_sample_1.xml (limited to 'sdkmanager') diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkStats.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkStats.java new file mode 100755 index 0000000..4958505 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkStats.java @@ -0,0 +1,589 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.repository; + +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.repository.SdkStatsConstants; +import com.android.sdklib.util.SparseArray; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.UnknownHostException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.SSLKeyException; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + + +/** + * Retrieves stats on platforms. + *

+ * This returns information stored on the repository in a different XML file + * and isn't directly tied to the existence of the listed platforms. + */ +public class SdkStats { + + public static class PlatformStatBase { + private final int mApiLevel; + private final String mVersionName; + private final String mCodeName; + private final float mShare; + + public PlatformStatBase(int apiLevel, + String versionName, + String codeName, + float share) { + mApiLevel = apiLevel; + mVersionName = versionName; + mCodeName = codeName; + mShare = share; + } + + /** The Android API Level for the platform. An int > 0. */ + public int getApiLevel() { + return mApiLevel; + } + + /** The official codename for this platform, for example "Cupcake". */ + public String getCodeName() { + return mCodeName; + } + + /** The official version name of this platform, for example "Android 1.5". */ + public String getVersionName() { + return mVersionName; + } + + /** An approximate share percentage of this platform and all the + * platforms of lower API level. */ + public float getShare() { + return mShare; + } + + /** Returns a string representation of this object, for debugging purposes. */ + @Override + public String toString() { + return String.format("api=%d, code=%s, vers=%s, share=%.1f%%", //$NON-NLS-1$ + mApiLevel, mCodeName, mVersionName, mShare); + } + } + + public static class PlatformStat extends PlatformStatBase { + private final float mAccumShare; + + public PlatformStat(int apiLevel, + String versionName, + String codeName, + float share, + float accumShare) { + super(apiLevel, versionName, codeName, share); + mAccumShare = accumShare; + } + + public PlatformStat(PlatformStatBase base, float accumShare) { + super(base.getApiLevel(), + base.getVersionName(), + base.getCodeName(), + base.getShare()); + mAccumShare = accumShare; + } + + /** The accumulated approximate share percentage of that platform. */ + public float getAccumShare() { + return mAccumShare; + } + + /** Returns a string representation of this object, for debugging purposes. */ + @Override + public String toString() { + return String.format("", super.toString(), mAccumShare); + } + } + + private final SparseArray mStats = new SparseArray(); + + public SdkStats() { + } + + public SparseArray getStats() { + return mStats; + } + + public void load(DownloadCache cache, boolean forceHttp, ITaskMonitor monitor) { + + String url = SdkStatsConstants.URL_STATS; + + if (forceHttp) { + url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + monitor.setProgressMax(5); + monitor.setDescription("Fetching %1$s", url); + monitor.incProgress(1); + + Exception[] exception = new Exception[] { null }; + Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; + String[] validationError = new String[] { null }; + Document validatedDoc = null; + String validatedUri = null; + + InputStream xml = fetchUrl(url, cache, monitor.createSubMonitor(1), exception); + + if (xml != null) { + monitor.setDescription("Validate XML"); + + // Explore the XML to find the potential XML schema version + int version = getXmlSchemaVersion(xml); + + if (version >= 1 && version <= SdkStatsConstants.NS_LATEST_VERSION) { + // This should be a version we can handle. Try to validate it + // and report any error as invalid XML syntax, + + String uri = validateXml(xml, url, version, validationError, validatorFound); + if (uri != null) { + // Validation was successful + validatedDoc = getDocument(xml, monitor); + validatedUri = uri; + + } + } else if (version > SdkStatsConstants.NS_LATEST_VERSION) { + // The schema used is more recent than what is supported by this tool. + // We don't have an upgrade-path support yet, so simply ignore the document. + return; + } + } + + // If any exception was handled during the URL fetch, display it now. + if (exception[0] != null) { + String reason = null; + if (exception[0] instanceof FileNotFoundException) { + // FNF has no useful getMessage, so we need to special handle it. + reason = "File not found"; + } else if (exception[0] instanceof UnknownHostException && + exception[0].getMessage() != null) { + // This has no useful getMessage yet could really use one + reason = String.format("Unknown Host %1$s", exception[0].getMessage()); + } else if (exception[0] instanceof SSLKeyException) { + // That's a common error and we have a pref for it. + reason = "HTTPS SSL error. You might want to force download through HTTP in the settings."; + } else if (exception[0].getMessage() != null) { + reason = exception[0].getMessage(); + } else { + // We don't know what's wrong. Let's give the exception class at least. + reason = String.format("Unknown (%1$s)", exception[0].getClass().getName()); + } + + monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason); + } + + if (validationError[0] != null) { + monitor.logError("%s", validationError[0]); //$NON-NLS-1$ + } + + // Stop here if we failed to validate the XML. We don't want to load it. + if (validatedDoc == null) { + return; + } + + monitor.incProgress(1); + + if (xml != null) { + monitor.setDescription("Parse XML"); + monitor.incProgress(1); + parseStatsDocument(validatedDoc, validatedUri, monitor); + } + + // done + monitor.incProgress(1); + } + + /** + * Fetches the document at the given URL and returns it as a stream. Returns + * null if anything wrong happens. References:
+ * URL Connection: + * + * @param urlString The URL to load, as a string. + * @param monitor {@link ITaskMonitor} related to this URL. + * @param outException If non null, where to store any exception that + * happens during the fetch. + * @see UrlOpener UrlOpener, which handles all URL logic. + */ + private InputStream fetchUrl(String urlString, + DownloadCache cache, + ITaskMonitor monitor, + Exception[] outException) { + try { + return cache.openCachedUrl(urlString, monitor); + } catch (Exception e) { + if (outException != null) { + outException[0] = e; + } + } + + return null; + } + + /** + * Manually parses the root element of the XML to extract the schema version + * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N" + * declaration. + * + * @return 1..{@link SdkStatsConstants#NS_LATEST_VERSION} for a valid schema version + * or 0 if no schema could be found. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected int getXmlSchemaVersion(InputStream xml) { + if (xml == null) { + return 0; + } + + // Get an XML document + Document doc = null; + try { + xml.reset(); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(false); + factory.setValidating(false); + + // Parse the old document using a non namespace aware builder + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + + // We don't want the default handler which prints errors to stderr. + builder.setErrorHandler(new ErrorHandler() { + @Override + public void warning(SAXParseException e) throws SAXException { + // pass + } + @Override + public void fatalError(SAXParseException e) throws SAXException { + throw e; + } + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + }); + + doc = builder.parse(xml); + + // Prepare a new document using a namespace aware builder + factory.setNamespaceAware(true); + builder = factory.newDocumentBuilder(); + + } catch (Exception e) { + // Failed to reset XML stream + // Failed to get builder factor + // Failed to create XML document builder + // Failed to parse XML document + // Failed to read XML document + } + + if (doc == null) { + return 0; + } + + // Check the root element is an XML with at least the following properties: + // + // + // Note that we don't have namespace support enabled, we just do it manually. + + Pattern nsPattern = Pattern.compile(SdkStatsConstants.NS_PATTERN); + + String prefix = null; + for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + prefix = null; + String name = child.getNodeName(); + int pos = name.indexOf(':'); + if (pos > 0 && pos < name.length() - 1) { + prefix = name.substring(0, pos); + name = name.substring(pos + 1); + } + if (SdkStatsConstants.NODE_SDK_STATS.equals(name)) { + NamedNodeMap attrs = child.getAttributes(); + String xmlns = "xmlns"; //$NON-NLS-1$ + if (prefix != null) { + xmlns += ":" + prefix; //$NON-NLS-1$ + } + Node attr = attrs.getNamedItem(xmlns); + if (attr != null) { + String uri = attr.getNodeValue(); + if (uri != null) { + Matcher m = nsPattern.matcher(uri); + if (m.matches()) { + String version = m.group(1); + try { + return Integer.parseInt(version); + } catch (NumberFormatException e) { + return 0; + } + } + } + } + } + } + } + + return 0; + } + + /** + * Validates this XML against one of the requested SDK Repository schemas. + * If the XML was correctly validated, returns the schema that worked. + * If it doesn't validate, returns null and stores the error in outError[0]. + * If we can't find a validator, returns null and set validatorFound[0] to false. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected String validateXml(InputStream xml, String url, int version, + String[] outError, Boolean[] validatorFound) { + + if (xml == null) { + return null; + } + + try { + Validator validator = getValidator(version); + + if (validator == null) { + validatorFound[0] = Boolean.FALSE; + outError[0] = String.format( + "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.", + url); + return null; + } + + validatorFound[0] = Boolean.TRUE; + + // Reset the stream if it supports that operation. + xml.reset(); + + // Validation throws a bunch of possible Exceptions on failure. + validator.validate(new StreamSource(xml)); + return SdkStatsConstants.getSchemaUri(version); + + } catch (SAXParseException e) { + outError[0] = String.format( + "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s", + url, + e.getLineNumber(), + e.getColumnNumber(), + e.toString()); + + } catch (Exception e) { + outError[0] = String.format( + "XML verification failed for %1$s.\nError: %2$s", + url, + e.toString()); + } + return null; + } + + /** + * Helper method that returns a validator for our XSD, or null if the current Java + * implementation can't process XSD schemas. + * + * @param version The version of the XML Schema. + * See {@link SdkStatsConstants#getXsdStream(int)} + */ + private Validator getValidator(int version) throws SAXException { + InputStream xsdStream = SdkStatsConstants.getXsdStream(version); + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + if (factory == null) { + return null; + } + + // This may throw a SAX Exception if the schema itself is not a valid XSD + Schema schema = factory.newSchema(new StreamSource(xsdStream)); + + Validator validator = schema == null ? null : schema.newValidator(); + + return validator; + } + + /** + * Takes an XML document as a string as parameter and returns a DOM for it. + * + * On error, returns null and prints a (hopefully) useful message on the monitor. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected Document getDocument(InputStream xml, ITaskMonitor monitor) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(true); + factory.setNamespaceAware(true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + xml.reset(); + Document doc = builder.parse(new InputSource(xml)); + + return doc; + } catch (ParserConfigurationException e) { + monitor.logError("Failed to create XML document builder"); + + } catch (SAXException e) { + monitor.logError("Failed to parse XML document"); + + } catch (IOException e) { + monitor.logError("Failed to read XML document"); + } + + return null; + } + + /** + * Parses all valid platforms found in the XML. + * Changes the stats array returned by {@link #getStats()} + * (also returns the value directly, useful for unti tests.) + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected SparseArray parseStatsDocument( + Document doc, + String nsUri, + ITaskMonitor monitor) { + + String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ + if (baseUrl != null) { + if (baseUrl.length() <= 0 || !baseUrl.endsWith("/")) { //$NON-NLS-1$ + baseUrl = null; + } + } + + SparseArray platforms = new SparseArray(); + int maxApi = 0; + + Node root = getFirstChild(doc, nsUri, SdkStatsConstants.NODE_SDK_STATS); + if (root != null) { + for (Node child = root.getFirstChild(); + child != null; + child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI()) && + child.getLocalName().equals(SdkStatsConstants.NODE_PLATFORM)) { + + try { + Node node = getFirstChild(child, nsUri, SdkStatsConstants.NODE_API_LEVEL); + int apiLevel = Integer.parseInt(node.getTextContent().trim()); + + if (apiLevel < 1) { + // bad API level, ignore it. + continue; + } + + if (platforms.indexOfKey(apiLevel) >= 0) { + // if we already loaded that API, ignore duplicates + continue; + } + + String codeName = + getFirstChild(child, nsUri, SdkStatsConstants.NODE_CODENAME). + getTextContent().trim(); + String versName = + getFirstChild(child, nsUri, SdkStatsConstants.NODE_VERSION). + getTextContent().trim(); + + if (codeName == null || versName == null || + codeName.length() == 0 || versName.length() == 0) { + // bad names. ignore. + continue; + } + + node = getFirstChild(child, nsUri, SdkStatsConstants.NODE_SHARE); + float percent = Float.parseFloat(node.getTextContent().trim()); + + if (percent < 0 || percent > 100) { + // invalid percentage. ignore. + continue; + } + + PlatformStatBase p = new PlatformStatBase( + apiLevel, versName, codeName, percent); + platforms.put(apiLevel, p); + + maxApi = apiLevel > maxApi ? apiLevel : maxApi; + + } catch (Exception ignore) { + // Error parsing this platform. Ignore it. + continue; + } + } + } + } + + mStats.clear(); + + // Compute cumulative share percents & fill in final map + for (int api = 1; api <= maxApi; api++) { + PlatformStatBase p = platforms.get(api); + if (p == null) { + continue; + } + + float sum = p.getShare(); + for (int j = api + 1; j <= maxApi; j++) { + PlatformStatBase pj = platforms.get(j); + if (pj != null) { + sum += pj.getShare(); + } + } + + mStats.put(api, new PlatformStat(p, sum)); + } + + return mStats; + } + + /** + * Returns the first child element with the given XML local name. + * If xmlLocalName is null, returns the very first child element. + */ + private Node getFirstChild(Node node, String nsUri, String xmlLocalName) { + + for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI())) { + if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) { + return child; + } + } + } + + return null; + } + +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkStatsConstants.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkStatsConstants.java new file mode 100755 index 0000000..06f8d39 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkStatsConstants.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2012 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.sdklib.repository; + + +import java.io.InputStream; + +/** + * Public constants for the sdk-stats XML Schema. + */ +public class SdkStatsConstants { + + /** The canonical URL filename for addons-list XML files. */ + public static final String URL_DEFAULT_FILENAME = "stats-1.xml"; //$NON-NLS-1$ + + /** The URL where to find the official addons list fle. */ + public static final String URL_STATS = + SdkRepoConstants.URL_GOOGLE_SDK_SITE + URL_DEFAULT_FILENAME; + + /** The base of our sdk-addons-list XML namespace. */ + private static final String NS_BASE = + "http://schemas.android.com/sdk/android/stats/"; //$NON-NLS-1$ + + /** + * The pattern of our sdk-stats XML namespace. + * Matcher's group(1) is the schema version (integer). + */ + public static final String NS_PATTERN = NS_BASE + "([1-9][0-9]*)"; //$NON-NLS-1$ + + /** The latest version of the sdk-stats XML Schema. + * Valid version numbers are between 1 and this number, included. */ + public static final int NS_LATEST_VERSION = 1; + + /** The XML namespace of the latest sdk-stats XML. */ + public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); + + /** The root sdk-stats element */ + public static final String NODE_SDK_STATS = "sdk-stats"; //$NON-NLS-1$ + + /** A platform stat. */ + public static final String NODE_PLATFORM = "platform"; //$NON-NLS-1$ + + /** The Android API Level for the platform. An int > 0. */ + public static final String NODE_API_LEVEL = "api-level"; //$NON-NLS-1$ + + /** The official codename for this platform, for example "Cupcake". */ + public static final String NODE_CODENAME = "codename"; //$NON-NLS-1$ + + /** The official version name of this platform, for example "Android 1.5". */ + public static final String NODE_VERSION = "version"; //$NON-NLS-1$ + + /** + * The approximate share percentage of that platform. + * See the caveat in sdk-stats-1.xsd about value freshness and accuracy. + */ + public static final String NODE_SHARE = "share"; //$NON-NLS-1$ + + /** + * Returns a stream to the requested sdk-stats XML Schema. + * + * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. + * @return An {@link InputStream} object for the local XSD file or + * null if there is no schema for the requested version. + */ + public static InputStream getXsdStream(int version) { + String filename = String.format("sdk-stats-%d.xsd", version); //$NON-NLS-1$ + return SdkStatsConstants.class.getResourceAsStream(filename); + } + + /** + * Returns the URI of the sdk-stats schema for the given version number. + * @param version Between 1 and {@link #NS_LATEST_VERSION} included. + */ + public static String getSchemaUri(int version) { + return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ + } +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-stats-1.xsd b/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-stats-1.xsd new file mode 100755 index 0000000..2944b12 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-stats-1.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + A simple list of platform stats. + + + + + + + + + + + + + Stats information for a given Android platform. + The api-level acts as a key, and it is epxected there should only + be one platform listed with the same API-level. + + + + + + + + + + + + + + + + + + + + + + + A decimal percentage, between 0.0 and 100.0%. + + + + + + + + diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/util/SparseArray.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/util/SparseArray.java index b948e8c..7c535a8 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/util/SparseArray.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/util/SparseArray.java @@ -303,6 +303,79 @@ public class SparseArray { mSize = pos + 1; } + public SparseArray getUnmodifiable() { + final SparseArray mStorage = this; + return new SparseArray() { + + @Override + public E get(int key) { + return mStorage.get(key); + } + + @Override + public E get(int key, E valueIfKeyNotFound) { + return mStorage.get(key, valueIfKeyNotFound); + } + + @Override + public void delete(int key) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(int key) { + throw new UnsupportedOperationException(); + } + + @Override + public void put(int key, E value) { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return mStorage.size(); + } + + @Override + public int keyAt(int index) { + return mStorage.keyAt(index); + } + + @Override + @SuppressWarnings("unchecked") + public E valueAt(int index) { + return mStorage.valueAt(index); + } + + @Override + public void setValueAt(int index, E value) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOfKey(int key) { + return mStorage.indexOfKey(key); + } + + @Override + public int indexOfValue(E value) { + return mStorage.indexOfValue(value); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public void append(int key, E value) { + throw new UnsupportedOperationException(); + } + + }; + } + private static int binarySearch(int[] a, int start, int len, int key) { int high = start + len, low = start - 1, guess; diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkStatsTest.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkStatsTest.java new file mode 100755 index 0000000..bc2a181 --- /dev/null +++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkStatsTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.repository; + +import com.android.sdklib.internal.repository.SdkStats.PlatformStat; +import com.android.sdklib.repository.SdkStatsConstants; +import com.android.sdklib.util.SparseArray; + +import org.w3c.dom.Document; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import junit.framework.TestCase; + +/** + * Tests for {@link SdkStats} + */ +public class SdkStatsTest extends TestCase { + + /** + * An internal helper class to give us visibility to the protected members we want + * to test. + */ + private static class MockSdkStats extends SdkStats { + + public SparseArray _parseStatsDocument(Document doc, + String nsUri, + ITaskMonitor monitor) { + return super.parseStatsDocument(doc, nsUri, monitor); + } + + public int _getXmlSchemaVersion(InputStream xml) { + return super.getXmlSchemaVersion(xml); + } + + public String _validateXml(InputStream xml, String url, int version, + String[] outError, Boolean[] validatorFound) { + return super.validateXml(xml, url, version, outError, validatorFound); + } + + public Document _getDocument(InputStream xml, ITaskMonitor monitor) { + return super.getDocument(xml, monitor); + } + + } + + private MockSdkStats mStats; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mStats = new MockSdkStats(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + + mStats = null; + } + + /** + * Validate we can still load a valid addon schema version 1 + */ + public void testLoadSample_1() throws Exception { + InputStream xmlStream = + getTestResource("/com/android/sdklib/testdata/stats_sample_1.xml"); + + // guess the version from the XML document + int version = mStats._getXmlSchemaVersion(xmlStream); + assertEquals(1, version); + + Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; + String[] validationError = new String[] { null }; + String url = "not-a-valid-url://stats.xml"; + + String uri = mStats._validateXml( + xmlStream, url, version, validationError, validatorFound); + assertEquals(Boolean.TRUE, validatorFound[0]); + assertEquals(null, validationError[0]); + assertEquals(SdkStatsConstants.getSchemaUri(1), uri); + + // Validation was successful, load the document + MockMonitor monitor = new MockMonitor(); + Document doc = mStats._getDocument(xmlStream, monitor); + assertNotNull(doc); + + // Get the sites + SparseArray result = mStats._parseStatsDocument(doc, uri, monitor); + + assertEquals("", monitor.getCapturedDescriptions()); + assertEquals("", monitor.getCapturedLog()); + assertEquals("", monitor.getCapturedErrorLog()); + assertEquals("", monitor.getCapturedVerboseLog()); + + // check what we found + assertEquals(3, result.size()); + int len = result.size(); + + int[] keys = new int[len]; + PlatformStat[] stats = new PlatformStat[len]; + for (int i = 0; i < len; i++) { + keys[i] = result.keyAt(i); + stats[i] = result.valueAt(i); + } + + assertEquals( + "[3, 5, 42]", + Arrays.toString(keys)); + + assertEquals( + "[, " + + ", " + + "]", + Arrays.toString(stats)); + } + + /** + * Returns an SdkLib file resource as a {@link ByteArrayInputStream}, + * which has the advantage that we can use {@link InputStream#reset()} on it + * at any time to read it multiple times. + *

+ * The default for getResourceAsStream() is to return a {@link FileInputStream} that + * does not support reset(), yet we need it in the tested code. + * + * @throws IOException if some I/O read fails + */ + private ByteArrayInputStream getTestResource(String filename) throws IOException { + InputStream xmlStream = this.getClass().getResourceAsStream(filename); + + try { + byte[] data = new byte[8192]; + int offset = 0; + int n; + + while ((n = xmlStream.read(data, offset, data.length - offset)) != -1) { + offset += n; + + if (offset == data.length) { + byte[] newData = new byte[offset + 8192]; + System.arraycopy(data, 0, newData, 0, offset); + data = newData; + } + } + + return new ByteArrayInputStream(data, 0, offset); + } finally { + if (xmlStream != null) { + xmlStream.close(); + } + } + } +} diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/testdata/stats_sample_1.xml b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/testdata/stats_sample_1.xml new file mode 100755 index 0000000..344b8a5 --- /dev/null +++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/testdata/stats_sample_1.xml @@ -0,0 +1,48 @@ + + + + + + 3 + Vanilla + Android 0.5 + 0.1 + + + + 5 + Coffee + Android 42.0 + 25.8 + + + + 42 + Chocolate + Android 32.64 + 74.1 + + + + 5 + Ignored + API 5 was already seen above + 99.9 + + + -- cgit v1.1