• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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) {
117             return defValue;
118         }
119         try {
120             return Integer.parseInt(number);
121         } catch (NumberFormatException e) {
122             throw new IllegalArgumentException(e);
123         }
124     }
125 
126     /**
127      * Decode a possibly run-length-encoded section of a run specification
128      */
decodeRle(String encoded)129     String decodeRle(String encoded) throws IllegalArgumentException {
130         final StringBuilder out = new StringBuilder();
131 
132         int i = 0;
133         while (i < encoded.length()) {
134             Matcher m = RLE_SEGMENT.matcher(encoded.substring(i));
135             if (m.find()) {
136                 final String c = m.group(2);
137                 final int repeat = toIntOrDefault(m.group(3), 1);
138                 if (repeat < 1) {
139                     throw new IllegalArgumentException(String.format(
140                             "Encountered illegal repeat length %d; expecting a length >= 1",
141                             repeat));
142                 }
143 
144                 for (int k = 0; k < repeat; ++k) {
145                     out.append(c);
146                 }
147 
148                 // jump forward by the length of the entire match from the encoded string
149                 i += m.group(1).length();
150             } else {
151                 throw new IllegalArgumentException(String.format(
152                         "Encountered illegal character \"%s\" while parsing segment \"%s\"",
153                         encoded.substring(i, i+1), encoded));
154             }
155         }
156 
157         return out.toString();
158     }
159 
160     /**
161      * Decode the run specification
162      */
decode(String encoded)163     String decode(String encoded) throws IllegalArgumentException {
164         String work = encoded.toUpperCase();
165 
166         // The first step is to get expand parenthesized sections so that we have one long RLE
167         // string
168         Matcher m = INNER_PAREN_SEGMENT.matcher(work);
169         for (; m.matches(); m = INNER_PAREN_SEGMENT.matcher(work)) {
170             final String prefix = m.group(1);
171             final String subsection = m.group(2);
172             final int repeat = toIntOrDefault(m.group(3), 1);
173             if (repeat < 1) {
174                 throw new IllegalArgumentException(String.format(
175                         "Encountered illegal repeat length %d; expecting a length >= 1",
176                         repeat));
177             }
178             final String suffix = m.group(4);
179 
180             // At this point, we have a valid next state.  Just reassemble everything
181             final StringBuilder nextState = new StringBuilder(prefix);
182             for (int k = 0; k < repeat; ++k) {
183                 nextState.append(subsection);
184             }
185             nextState.append(suffix);
186             work = nextState.toString();
187         }
188 
189         // Finally, decode the long RLE string
190         return decodeRle(work);
191     }
192 
193     /**
194      * Turn a given test specification into a series of test Run, Failure, and Error outputs
195      *
196      * @param listener The test listener to use to report results
197      * @param runName The test run name to use
198      * @param spec A string consisting solely of the characters "P"(ass), "F"(ail), A(ssumption
199      *     failure) or I(gnored). Each character will map to a testcase in the output. Method names
200      *     will be of the format "testMethod%d".
201      */
executeTestRun(ITestInvocationListener listener, String runName, String spec)202     void executeTestRun(ITestInvocationListener listener, String runName, String spec)
203             throws IllegalArgumentException {
204         listener.testRunStarted(runName, spec.length());
205         int i = 0;
206         for (char c : spec.toCharArray()) {
207             if (c != 'P' && c != 'F' && c != 'A' && c != 'I') {
208                 throw new IllegalArgumentException(String.format(
209                         "Received unexpected test spec character '%c' in spec \"%s\"", c, spec));
210             }
211 
212             i++;
213             final String testName = String.format("testMethod%d", i);
214             final TestDescription test = new TestDescription(runName, testName);
215 
216             listener.testStarted(test);
217             switch (c) {
218                 case 'P':
219                     // no-op
220                     break;
221                 case 'F':
222                     listener.testFailed(test,
223                             String.format("Test %s had a predictable boo-boo.", testName));
224                     break;
225                 case 'A':
226                     listener.testAssumptionFailure(
227                             test, String.format("Test %s had an assumption failure", testName));
228                     break;
229                 case 'I':
230                     listener.testIgnored(test);
231                     break;
232             }
233             saveLogs(listener, mTestLogs);
234             listener.testEnded(test, EMPTY_MAP);
235         }
236         saveLogs(listener, mTestRunLogs);
237         listener.testRunEnded(0, EMPTY_MAP);
238     }
239 
240     /** {@inheritDoc} */
241     @Override
run(TestInformation testInfo, ITestInvocationListener listener)242     public void run(TestInformation testInfo, ITestInvocationListener listener)
243             throws DeviceNotAvailableException {
244         for (Map.Entry<String, String> run : mRuns.entrySet()) {
245             final String name = run.getKey();
246             final String testSpec = decode(run.getValue());
247             executeTestRun(listener, name, testSpec);
248         }
249 
250         saveLogs(listener, mInvocationLogs);
251 
252         if (mFailInvocationWithCause != null) {
253             // Goodbye, cruel world
254             throw new RuntimeException(mFailInvocationWithCause);
255         }
256     }
257 
saveLogs(ITestInvocationListener listener, List<String> logs)258     private void saveLogs(ITestInvocationListener listener, List<String> logs) {
259         for (String filename : logs) {
260             File log = new File(filename);
261             if (!log.isFile()) {
262                 continue;
263             }
264             try (InputStreamSource in = new FileInputStreamSource(log)) {
265                 listener.testLog(log.getName(), LogDataType.UNKNOWN, in);
266             }
267         }
268     }
269 }
270