/* * Copyright (C) 2018 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.regression.tests; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.result.TestDescription; import com.android.tradefed.util.MultiMap; import com.android.tradefed.util.Pair; import com.google.common.annotations.VisibleForTesting; /** A metrics object to hold run metrics and test metrics parsed by {@link MetricsXmlParser} */ public class Metrics { private int mNumRuns; private int mNumTests = -1; private final boolean mStrictMode; private final MultiMap mRunMetrics = new MultiMap<>(); private final MultiMap, Double> mTestMetrics = new MultiMap<>(); /** Throw when metrics validation fails in strict mode. */ public static class MetricsException extends RuntimeException { MetricsException(String cause) { super(cause); } } /** * Constructs an empty Metrics object. * * @param strictMode whether exception should be thrown when validation fails */ public Metrics(boolean strictMode) { mStrictMode = strictMode; } /** * Sets the number of tests. This method also checks if each call sets the same number of test, * since this number should be consistent across multiple runs. * * @param numTests the number of tests * @throws MetricsException if subsequent calls set a different number. */ public void setNumTests(int numTests) { if (mNumTests == -1) { mNumTests = numTests; } else { if (mNumTests != numTests) { String msg = String.format( "Number of test entries differ: expect #%d actual #%d", mNumTests, numTests); throw new MetricsException(msg); } } } /** * Adds a run metric. * * @param name metric name * @param value metric value */ public void addRunMetric(String name, String value) { try { mRunMetrics.put(name, Double.parseDouble(value)); } catch (NumberFormatException e) { // This is normal. We often get some string metrics like device name. Just log it. CLog.w(String.format("Run metric \"%s\" is not a number: \"%s\"", name, value)); } } /** * Adds a test metric. * * @param testId TestDescription of the metric * @param name metric name * @param value metric value */ public void addTestMetric(TestDescription testId, String name, String value) { Pair metricId = new Pair<>(testId, name); try { mTestMetrics.put(metricId, Double.parseDouble(value)); } catch (NumberFormatException e) { // This is normal. We often get some string metrics like device name. Just log it. CLog.w( String.format( "Test %s metric \"%s\" is not a number: \"%s\"", testId, name, value)); } } /** * Validates that the number of entries of each metric equals to the number of runs. * * @param numRuns number of runs * @throws MetricsException when validation fails in strict mode */ public void validate(int numRuns) { mNumRuns = numRuns; for (String name : mRunMetrics.keySet()) { if (mRunMetrics.get(name).size() < mNumRuns) { error( String.format( "Run metric \"%s\" too few entries: expected #%d actual #%d", name, mNumRuns, mRunMetrics.get(name).size())); } } for (Pair id : mTestMetrics.keySet()) { if (mTestMetrics.get(id).size() < mNumRuns) { error( String.format( "Test %s metric \"%s\" too few entries: expected #%d actual #%d", id.first, id.second, mNumRuns, mTestMetrics.get(id).size())); } } } /** * Validates with after-patch Metrics object. Make sure two metrics object contain same run * metric entries and test metric entries. Assume this object contains before-patch metrics. * * @param after a Metrics object containing after-patch metrics * @throws MetricsException when cross validation fails in strict mode */ public void crossValidate(Metrics after) { if (mNumTests != after.mNumTests) { error( String.format( "Number of test entries differ: before #%d after #%d", mNumTests, after.mNumTests)); } for (String name : mRunMetrics.keySet()) { if (!after.mRunMetrics.containsKey(name)) { warn(String.format("Run metric \"%s\" only in before-patch run.", name)); } } for (String name : after.mRunMetrics.keySet()) { if (!mRunMetrics.containsKey(name)) { warn(String.format("Run metric \"%s\" only in after-patch run.", name)); } } for (Pair id : mTestMetrics.keySet()) { if (!after.mTestMetrics.containsKey(id)) { warn( String.format( "Test %s metric \"%s\" only in before-patch run.", id.first, id.second)); } } for (Pair id : after.mTestMetrics.keySet()) { if (!mTestMetrics.containsKey(id)) { warn( String.format( "Test %s metric \"%s\" only in after-patch run.", id.first, id.second)); } } } @VisibleForTesting void error(String msg) { if (mStrictMode) { throw new MetricsException(msg); } else { CLog.e(msg); } } @VisibleForTesting void warn(String msg) { if (mStrictMode) { throw new MetricsException(msg); } else { CLog.w(msg); } } /** * Gets the number of test runs stored in this object. * * @return number of test runs */ public int getNumRuns() { return mNumRuns; } /** * Gets the number of tests stored in this object. * * @return number of tests */ public int getNumTests() { return mNumTests; } /** * Gets all run metrics stored in this object. * * @return a {@link MultiMap} from test name String to Double */ public MultiMap getRunMetrics() { return mRunMetrics; } /** * Gets all test metrics stored in this object. * * @return a {@link MultiMap} from (TestDescription, test name) pair to Double */ public MultiMap, Double> getTestMetrics() { return mTestMetrics; } }