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