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