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