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