1 /*
2  * Copyright (C) 2018 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.regression.tests;
18 
19 import com.android.tradefed.result.MetricsXMLResultReporter;
20 import com.android.tradefed.result.TestDescription;
21 
22 import com.google.common.annotations.VisibleForTesting;
23 
24 import org.xml.sax.Attributes;
25 import org.xml.sax.SAXException;
26 import org.xml.sax.helpers.DefaultHandler;
27 
28 import java.io.BufferedInputStream;
29 import java.io.File;
30 import java.io.FileInputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.util.List;
34 import java.util.Set;
35 
36 import javax.xml.parsers.ParserConfigurationException;
37 import javax.xml.parsers.SAXParser;
38 import javax.xml.parsers.SAXParserFactory;
39 
40 /** Parser that extracts test metrics result data generated by {@link MetricsXMLResultReporter}. */
41 public class MetricsXmlParser {
42 
43     /** Thrown when MetricsXmlParser fails to parse a metrics xml file. */
44     public static class ParseException extends Exception {
ParseException(Throwable cause)45         public ParseException(Throwable cause) {
46             super(cause);
47         }
48 
ParseException(String msg, Throwable cause)49         public ParseException(String msg, Throwable cause) {
50             super(msg, cause);
51         }
52     }
53 
54     /*
55      * Parses the xml format. Expected tags/attributes are:
56      * testsuite name="runname" tests="X"
57      *   runmetric name="metric1" value="1.0"
58      *   testcase classname="FooTest" testname="testMethodName"
59      *     testmetric name="metric2" value="1.0"
60      */
61     private static class MetricsXmlHandler extends DefaultHandler {
62 
63         private static final String TESTSUITE_TAG = "testsuite";
64         private static final String TESTCASE_TAG = "testcase";
65         private static final String TIME_TAG = "time";
66         private static final String RUNMETRIC_TAG = "runmetric";
67         private static final String TESTMETRIC_TAG = "testmetric";
68 
69         private TestDescription mCurrentTest = null;
70 
71         private Metrics mMetrics;
72         private Set<String> mBlocklistMetrics;
73 
MetricsXmlHandler(Metrics metrics, Set<String> blocklistMetrics)74         public MetricsXmlHandler(Metrics metrics, Set<String> blocklistMetrics) {
75             mMetrics = metrics;
76             mBlocklistMetrics = blocklistMetrics;
77         }
78 
79         @Override
startElement(String uri, String localName, String name, Attributes attributes)80         public void startElement(String uri, String localName, String name, Attributes attributes)
81                 throws SAXException {
82             if (TESTSUITE_TAG.equalsIgnoreCase(name)) {
83                 // top level tag - maps to a test run in TF terminology
84                 String testCount = getMandatoryAttribute(name, "tests", attributes);
85                 mMetrics.setNumTests(Integer.parseInt(testCount));
86                 mMetrics.addRunMetric(TIME_TAG, getMandatoryAttribute(name, TIME_TAG, attributes));
87             }
88             if (TESTCASE_TAG.equalsIgnoreCase(name)) {
89                 // start of description of an individual test method
90                 String testClassName = getMandatoryAttribute(name, "classname", attributes);
91                 String methodName = getMandatoryAttribute(name, "testname", attributes);
92                 mCurrentTest = new TestDescription(testClassName, methodName);
93             }
94             if (RUNMETRIC_TAG.equalsIgnoreCase(name)) {
95                 String metricName = getMandatoryAttribute(name, "name", attributes);
96                 String metricValue = getMandatoryAttribute(name, "value", attributes);
97                 if (!mBlocklistMetrics.contains(metricName)) {
98                     mMetrics.addRunMetric(metricName, metricValue);
99                 }
100             }
101             if (TESTMETRIC_TAG.equalsIgnoreCase(name)) {
102                 String metricName = getMandatoryAttribute(name, "name", attributes);
103                 String metricValue = getMandatoryAttribute(name, "value", attributes);
104                 if (!mBlocklistMetrics.contains(metricName)) {
105                     mMetrics.addTestMetric(mCurrentTest, metricName, metricValue);
106                 }
107             }
108         }
109 
getMandatoryAttribute(String tagName, String attrName, Attributes attributes)110         private String getMandatoryAttribute(String tagName, String attrName, Attributes attributes)
111                 throws SAXException {
112             String value = attributes.getValue(attrName);
113             if (value == null) {
114                 throw new SAXException(
115                         String.format(
116                                 "Malformed XML, could not find '%s' attribute in '%s'",
117                                 attrName, tagName));
118             }
119             return value;
120         }
121     }
122 
123     /**
124      * Parses xml data contained in given input files.
125      *
126      * @param blocklistMetrics ignore the metrics with these names
127      * @param strictMode whether to throw an exception when metric validation fails
128      * @param metricXmlFiles a list of metric xml files
129      * @return a Metric object containing metrics from all metric files
130      * @throws ParseException if input could not be parsed
131      */
parse( Set<String> blocklistMetrics, boolean strictMode, List<File> metricXmlFiles)132     public static Metrics parse(
133             Set<String> blocklistMetrics, boolean strictMode, List<File> metricXmlFiles)
134             throws ParseException {
135         Metrics metrics = new Metrics(strictMode);
136         for (File xml : metricXmlFiles) {
137             try (InputStream is = new BufferedInputStream(new FileInputStream(xml))) {
138                 parse(metrics, blocklistMetrics, is);
139             } catch (Exception e) {
140                 throw new ParseException("Unable to parse " + xml.getPath(), e);
141             }
142         }
143         metrics.validate(metricXmlFiles.size());
144         return metrics;
145     }
146 
147     @VisibleForTesting
parse(Metrics metrics, Set<String> blocklistMetrics, InputStream is)148     public static Metrics parse(Metrics metrics, Set<String> blocklistMetrics, InputStream is)
149             throws ParseException {
150         try {
151             SAXParserFactory parserFactory = SAXParserFactory.newInstance();
152             parserFactory.setNamespaceAware(true);
153             SAXParser parser = parserFactory.newSAXParser();
154             parser.parse(is, new MetricsXmlHandler(metrics, blocklistMetrics));
155             return metrics;
156         } catch (ParserConfigurationException | SAXException | IOException e) {
157             throw new ParseException(e);
158         }
159     }
160 }
161