blob: 11bdcc2313c30c5923dc973635a85fdcdaeee891 [file] [log] [blame]
/*
* Copyright (C) 2013 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.tradefed.testtype;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.TestDescription;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A fake test whose purpose is to make it easy to generate repeatable test results.
*/
@OptionClass(alias = "faketest")
public class FakeTest implements IDeviceTest, IRemoteTest {
@Option(
name = "run",
description =
"Specify a new run to include. "
+ "The key should be the unique name of the TestRun "
+ "(which may be a Java class name). "
+ "The value should specify the sequence of test results, "
+ "using the characters P[ass], F[ail], A[ssumption failure] or I[gnored]. "
+ "You may use run-length encoding to specify repeats, and you "
+ "may use parentheses for grouping. So \"(PF)4\" and \"((PF)2)2\" "
+ "will both expand to \"PFPFPFPF\".",
importance = Importance.IF_UNSET
)
private Map<String, String> mRuns = new LinkedHashMap<String, String>();
@Option(name = "fail-invocation-with-cause", description = "If set, the invocation will be " +
"reported as a failure, with the specified message as the cause.")
private String mFailInvocationWithCause = null;
@Option(
name = "test-log",
description = "Name of the file to report as a log generated by each test"
)
private List<String> mTestLogs = new ArrayList<>();
@Option(
name = "test-run-log",
description = "Name of a file to report as a log for each test run"
)
private List<String> mTestRunLogs = new ArrayList<>();
@Option(
name = "test-invocation-log",
description = "Name of the file to report as log at the end of the invocation"
)
private List<String> mInvocationLogs = new ArrayList<>();
/** A pattern to identify an innermost pair of parentheses */
private static final Pattern INNER_PAREN_SEGMENT = Pattern.compile(
/* prefix inner parens count suffix */
"(.*?) \\(([^()]*)\\) (\\d+)? (.*?)", Pattern.COMMENTS);
/** A pattern to identify a run-length-encoded character specification */
private static final Pattern RLE_SEGMENT = Pattern.compile("^(([PFAI])(\\d+)?)");
static final HashMap<String, Metric> EMPTY_MAP = new HashMap<String, Metric>();
private ITestDevice mDevice = null;
/**
* {@inheritDoc}
*/
@Override
public ITestDevice getDevice() {
return mDevice;
}
/**
* {@inheritDoc}
*/
@Override
public void setDevice(ITestDevice device) {
mDevice = device;
}
/**
* A small utility that converts a number encoded in a string to an int. Will convert
* {@code null} to {@code defValue}.
*/
int toIntOrDefault(String number, int defValue) throws IllegalArgumentException {
if (number == null) return defValue;
try {
return Integer.parseInt(number);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Decode a possibly run-length-encoded section of a run specification
*/
String decodeRle(String encoded) throws IllegalArgumentException {
final StringBuilder out = new StringBuilder();
int i = 0;
while (i < encoded.length()) {
Matcher m = RLE_SEGMENT.matcher(encoded.substring(i));
if (m.find()) {
final String c = m.group(2);
final int repeat = toIntOrDefault(m.group(3), 1);
if (repeat < 1) {
throw new IllegalArgumentException(String.format(
"Encountered illegal repeat length %d; expecting a length >= 1",
repeat));
}
for (int k = 0; k < repeat; ++k) {
out.append(c);
}
// jump forward by the length of the entire match from the encoded string
i += m.group(1).length();
} else {
throw new IllegalArgumentException(String.format(
"Encountered illegal character \"%s\" while parsing segment \"%s\"",
encoded.substring(i, i+1), encoded));
}
}
return out.toString();
}
/**
* Decode the run specification
*/
String decode(String encoded) throws IllegalArgumentException {
String work = encoded.toUpperCase();
// The first step is to get expand parenthesized sections so that we have one long RLE
// string
Matcher m = INNER_PAREN_SEGMENT.matcher(work);
for (; m.matches(); m = INNER_PAREN_SEGMENT.matcher(work)) {
final String prefix = m.group(1);
final String subsection = m.group(2);
final int repeat = toIntOrDefault(m.group(3), 1);
if (repeat < 1) {
throw new IllegalArgumentException(String.format(
"Encountered illegal repeat length %d; expecting a length >= 1",
repeat));
}
final String suffix = m.group(4);
// At this point, we have a valid next state. Just reassemble everything
final StringBuilder nextState = new StringBuilder(prefix);
for (int k = 0; k < repeat; ++k) {
nextState.append(subsection);
}
nextState.append(suffix);
work = nextState.toString();
}
// Finally, decode the long RLE string
return decodeRle(work);
}
/**
* Turn a given test specification into a series of test Run, Failure, and Error outputs
*
* @param listener The test listener to use to report results
* @param runName The test run name to use
* @param spec A string consisting solely of the characters "P"(ass), "F"(ail), A(ssumption
* failure) or I(gnored). Each character will map to a testcase in the output. Method names
* will be of the format "testMethod%d".
*/
void executeTestRun(ITestInvocationListener listener, String runName, String spec)
throws IllegalArgumentException {
listener.testRunStarted(runName, spec.length());
int i = 0;
for (char c : spec.toCharArray()) {
if (c != 'P' && c != 'F' && c != 'A' && c != 'I') {
throw new IllegalArgumentException(String.format(
"Received unexpected test spec character '%c' in spec \"%s\"", c, spec));
}
i++;
final String testName = String.format("testMethod%d", i);
final TestDescription test = new TestDescription(runName, testName);
listener.testStarted(test);
switch (c) {
case 'P':
// no-op
break;
case 'F':
listener.testFailed(test,
String.format("Test %s had a predictable boo-boo.", testName));
break;
case 'A':
listener.testAssumptionFailure(
test, String.format("Test %s had an assumption failure", testName));
break;
case 'I':
listener.testIgnored(test);
break;
}
saveLogs(listener, mTestLogs);
listener.testEnded(test, EMPTY_MAP);
}
saveLogs(listener, mTestRunLogs);
listener.testRunEnded(0, EMPTY_MAP);
}
/** {@inheritDoc} */
@Override
public void run(TestInformation testInfo, ITestInvocationListener listener)
throws DeviceNotAvailableException {
for (Map.Entry<String, String> run : mRuns.entrySet()) {
final String name = run.getKey();
final String testSpec = decode(run.getValue());
executeTestRun(listener, name, testSpec);
}
saveLogs(listener, mInvocationLogs);
if (mFailInvocationWithCause != null) {
// Goodbye, cruel world
throw new RuntimeException(mFailInvocationWithCause);
}
}
private void saveLogs(ITestInvocationListener listener, List<String> logs) {
for (String filename : logs) {
File log = new File(filename);
if (!log.isFile()) {
continue;
}
try (InputStreamSource in = new FileInputStreamSource(log)) {
listener.testLog(log.getName(), LogDataType.UNKNOWN, in);
}
}
}
}