1 /* 2 * Copyright (C) 2013 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 package com.android.tradefed.testtype; 17 18 import com.android.tradefed.config.Option; 19 import com.android.tradefed.config.Option.Importance; 20 import com.android.tradefed.config.OptionClass; 21 import com.android.tradefed.device.DeviceNotAvailableException; 22 import com.android.tradefed.device.ITestDevice; 23 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 24 import com.android.tradefed.result.ITestInvocationListener; 25 import com.android.tradefed.result.TestDescription; 26 27 import java.util.HashMap; 28 import java.util.LinkedHashMap; 29 import java.util.Map; 30 import java.util.regex.Matcher; 31 import java.util.regex.Pattern; 32 33 /** 34 * A fake test whose purpose is to make it easy to generate repeatable test results. 35 */ 36 @OptionClass(alias = "faketest") 37 public class FakeTest implements IDeviceTest, IRemoteTest { 38 39 @Option(name = "run", description = "Specify a new run to include. " + 40 "The key should be the unique name of the TestRun (which may be a Java class name). " + 41 "The value should specify the sequence of test results, using the characters P[ass], " + 42 "or F[ail]. You may use run-length encoding to specify repeats, and you " + 43 "may use parentheses for grouping. So \"(PF)4\" and \"((PF)2)2\" will both expand " + 44 "to \"PFPFPFPF\".", importance = Importance.IF_UNSET) 45 private Map<String, String> mRuns = new LinkedHashMap<String, String>(); 46 47 @Option(name = "fail-invocation-with-cause", description = "If set, the invocation will be " + 48 "reported as a failure, with the specified message as the cause.") 49 private String mFailInvocationWithCause = null; 50 51 /** A pattern to identify an innermost pair of parentheses */ 52 private static final Pattern INNER_PAREN_SEGMENT = Pattern.compile( 53 /* prefix inner parens count suffix */ 54 "(.*?) \\(([^()]*)\\) (\\d+)? (.*?)", Pattern.COMMENTS); 55 56 /** A pattern to identify a run-length-encoded character specification */ 57 private static final Pattern RLE_SEGMENT = Pattern.compile("^(([PFE])(\\d+)?)"); 58 59 static final HashMap<String, Metric> EMPTY_MAP = new HashMap<String, Metric>(); 60 61 private ITestDevice mDevice = null; 62 63 /** 64 * {@inheritDoc} 65 */ 66 @Override getDevice()67 public ITestDevice getDevice() { 68 return mDevice; 69 } 70 71 /** 72 * {@inheritDoc} 73 */ 74 @Override setDevice(ITestDevice device)75 public void setDevice(ITestDevice device) { 76 mDevice = device; 77 } 78 79 /** 80 * A small utility that converts a number encoded in a string to an int. Will convert 81 * {@code null} to {@code defValue}. 82 */ toIntOrDefault(String number, int defValue)83 int toIntOrDefault(String number, int defValue) throws IllegalArgumentException { 84 if (number == null) return defValue; 85 try { 86 return Integer.parseInt(number); 87 } catch (NumberFormatException e) { 88 throw new IllegalArgumentException(e); 89 } 90 } 91 92 /** 93 * Decode a possibly run-length-encoded section of a run specification 94 */ decodeRle(String encoded)95 String decodeRle(String encoded) throws IllegalArgumentException { 96 final StringBuilder out = new StringBuilder(); 97 98 int i = 0; 99 while (i < encoded.length()) { 100 Matcher m = RLE_SEGMENT.matcher(encoded.substring(i)); 101 if (m.find()) { 102 final String c = m.group(2); 103 final int repeat = toIntOrDefault(m.group(3), 1); 104 if (repeat < 1) { 105 throw new IllegalArgumentException(String.format( 106 "Encountered illegal repeat length %d; expecting a length >= 1", 107 repeat)); 108 } 109 110 for (int k = 0; k < repeat; ++k) { 111 out.append(c); 112 } 113 114 // jump forward by the length of the entire match from the encoded string 115 i += m.group(1).length(); 116 } else { 117 throw new IllegalArgumentException(String.format( 118 "Encountered illegal character \"%s\" while parsing segment \"%s\"", 119 encoded.substring(i, i+1), encoded)); 120 } 121 } 122 123 return out.toString(); 124 } 125 126 /** 127 * Decode the run specification 128 */ decode(String encoded)129 String decode(String encoded) throws IllegalArgumentException { 130 String work = encoded.toUpperCase(); 131 132 // The first step is to get expand parenthesized sections so that we have one long RLE 133 // string 134 Matcher m = INNER_PAREN_SEGMENT.matcher(work); 135 for (; m.matches(); m = INNER_PAREN_SEGMENT.matcher(work)) { 136 final String prefix = m.group(1); 137 final String subsection = m.group(2); 138 final int repeat = toIntOrDefault(m.group(3), 1); 139 if (repeat < 1) { 140 throw new IllegalArgumentException(String.format( 141 "Encountered illegal repeat length %d; expecting a length >= 1", 142 repeat)); 143 } 144 final String suffix = m.group(4); 145 146 // At this point, we have a valid next state. Just reassemble everything 147 final StringBuilder nextState = new StringBuilder(prefix); 148 for (int k = 0; k < repeat; ++k) { 149 nextState.append(subsection); 150 } 151 nextState.append(suffix); 152 work = nextState.toString(); 153 } 154 155 // Finally, decode the long RLE string 156 return decodeRle(work); 157 } 158 159 160 /** 161 * Turn a given test specification into a series of test Run, Failure, and Error outputs 162 * 163 * @param listener The test listener to use to report results 164 * @param runName The test run name to use 165 * @param spec A string consisting solely of the characters "P"(ass), "F"(ail), or "E"(rror). 166 * Each character will map to a testcase in the output. Method names will be of the format 167 * "testMethod%d". 168 */ executeTestRun(ITestInvocationListener listener, String runName, String spec)169 void executeTestRun(ITestInvocationListener listener, String runName, String spec) 170 throws IllegalArgumentException { 171 listener.testRunStarted(runName, spec.length()); 172 int i = 0; 173 for (char c : spec.toCharArray()) { 174 if (c != 'P' && c != 'F') { 175 throw new IllegalArgumentException(String.format( 176 "Received unexpected test spec character '%c' in spec \"%s\"", c, spec)); 177 } 178 179 i++; 180 final String testName = String.format("testMethod%d", i); 181 final TestDescription test = new TestDescription(runName, testName); 182 183 listener.testStarted(test); 184 switch (c) { 185 case 'P': 186 // no-op 187 break; 188 case 'F': 189 listener.testFailed(test, 190 String.format("Test %s had a predictable boo-boo.", testName)); 191 break; 192 } 193 listener.testEnded(test, EMPTY_MAP); 194 } 195 listener.testRunEnded(0, EMPTY_MAP); 196 } 197 198 /** 199 * {@inheritDoc} 200 */ 201 @Override run(ITestInvocationListener listener)202 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 203 for (Map.Entry<String, String> run : mRuns.entrySet()) { 204 final String name = run.getKey(); 205 final String testSpec = decode(run.getValue()); 206 executeTestRun(listener, name, testSpec); 207 } 208 209 if (mFailInvocationWithCause != null) { 210 // Goodbye, cruel world 211 throw new RuntimeException(mFailInvocationWithCause); 212 } 213 } 214 } 215 216