1 /*
2  * Copyright (C) 2019 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 
17 package com.android.sts.common;
18 
19 import static org.junit.Assert.*;
20 
21 import com.android.ddmlib.Log.LogLevel;
22 import com.android.tradefed.log.LogUtil.CLog;
23 
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26 
27 /** Contains wrappers around JUnit assertions with regex matching in strings */
28 public class RegexUtils {
29     private static final int TIMEOUT_DURATION = 20 * 60_000; // 20 minutes
30     private static final int WARNING_THRESHOLD = 1000; // 1 second
31     private static final int CONTEXT_RANGE = 100; // chars before/after matched input string
32 
assertContains(String pattern, String input)33     public static void assertContains(String pattern, String input) throws Exception {
34         assertFind(pattern, input, false, false);
35     }
36 
assertContainsMultiline(String pattern, String input)37     public static void assertContainsMultiline(String pattern, String input) throws Exception {
38         assertFind(pattern, input, false, true);
39     }
40 
assertNotContains(String pattern, String input)41     public static void assertNotContains(String pattern, String input) throws Exception {
42         assertFind(pattern, input, true, false);
43     }
44 
assertNotContainsMultiline(String pattern, String input)45     public static void assertNotContainsMultiline(String pattern, String input) throws Exception {
46         assertFind(pattern, input, true, true);
47     }
48 
assertFind( String pattern, String input, boolean shouldFind, boolean multiline)49     private static void assertFind(
50             String pattern, String input, boolean shouldFind, boolean multiline) {
51         // The input string throws an error when used after the timeout
52         TimeoutCharSequence timedInput = new TimeoutCharSequence(input, TIMEOUT_DURATION);
53         Matcher matcher = null;
54         if (multiline) {
55             // DOTALL lets .* match line separators
56             // MULTILINE lets ^ and $ match line separators instead of input start and end
57             matcher =
58                     Pattern.compile(pattern, Pattern.DOTALL | Pattern.MULTILINE)
59                             .matcher(timedInput);
60         } else {
61             matcher = Pattern.compile(pattern).matcher(timedInput);
62         }
63 
64         try {
65             long start = System.currentTimeMillis();
66             boolean found = matcher.find();
67             long duration = System.currentTimeMillis() - start;
68 
69             if (duration > WARNING_THRESHOLD) {
70                 // Provide a warning to the test developer that their regex should be optimized.
71                 CLog.logAndDisplay(LogLevel.WARN, "regex match took " + duration + "ms.");
72             }
73 
74             if (found && shouldFind) { // failed notContains
75                 String substring = input.substring(matcher.start(), matcher.end());
76                 String context =
77                         getInputContext(
78                                 input,
79                                 matcher.start(),
80                                 matcher.end(),
81                                 CONTEXT_RANGE,
82                                 CONTEXT_RANGE);
83                 fail(
84                         "Pattern found: '"
85                                 + pattern
86                                 + "' -> '"
87                                 + substring
88                                 + "' for input:\n..."
89                                 + context
90                                 + "...");
91             } else if (!found && !shouldFind) { // failed contains
92                 fail("Pattern not found: '" + pattern + "' for input:\n..." + input + "...");
93             }
94         } catch (TimeoutCharSequence.CharSequenceTimeoutException e) {
95             // regex match has taken longer than the timeout
96             // this usually means the input is extremely long or the regex is catastrophic
97             fail("Regex timeout with pattern: '" + pattern + "' for input:\n..." + input + "...");
98         }
99     }
100 
101     /*
102      * Helper method to grab the nearby chars for a subsequence. Similar to the -A and -B flags for
103      * grep.
104      */
getInputContext(String input, int start, int end, int before, int after)105     private static String getInputContext(String input, int start, int end, int before, int after) {
106         start = Math.max(0, start - before);
107         end = Math.min(input.length(), end + after);
108         return input.substring(start, end);
109     }
110 
111     /*
112      * Wrapper for a given CharSequence. When charAt() is called, the current time is compared
113      * against the timeout. If the current time is greater than the expiration time, an exception is
114      * thrown. The expiration time is (time of object construction) + (timeout in milliseconds).
115      */
116     private static class TimeoutCharSequence implements CharSequence {
117         long expireTime = 0;
118         CharSequence chars = null;
119 
TimeoutCharSequence(CharSequence chars, long timeout)120         TimeoutCharSequence(CharSequence chars, long timeout) {
121             this.chars = chars;
122             expireTime = System.currentTimeMillis() + timeout;
123         }
124 
125         @Override
charAt(int index)126         public char charAt(int index) {
127             if (System.currentTimeMillis() > expireTime) {
128                 throw new CharSequenceTimeoutException(
129                         "TimeoutCharSequence was used after the expiration time.");
130             }
131             return chars.charAt(index);
132         }
133 
134         @Override
length()135         public int length() {
136             return chars.length();
137         }
138 
139         @Override
subSequence(int start, int end)140         public CharSequence subSequence(int start, int end) {
141             return new TimeoutCharSequence(
142                     chars.subSequence(start, end), expireTime - System.currentTimeMillis());
143         }
144 
145         @Override
toString()146         public String toString() {
147             return chars.toString();
148         }
149 
150         private static class CharSequenceTimeoutException extends RuntimeException {
CharSequenceTimeoutException(String message)151             public CharSequenceTimeoutException(String message) {
152                 super(message);
153             }
154         }
155     }
156 }
157