Modernize CTS/STS regex asserts.

New features include:
* regex timeout and warning
* printing what substring matched the pattern
* dump the tested string on test failure
* print the pattern string for the rest of the functions

These features help diagnose test failures and encourage better test
writing.

Test: $ sts-tradefed run sts-engbuild -m CtsSecurityBulletinHostTestCases
Bug: 122900866
Change-Id: I47c3c603e1dda1de4f6198cacad7e0d037ce8a61
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/RegexUtils.java b/hostsidetests/securitybulletin/src/android/security/cts/RegexUtils.java
new file mode 100644
index 0000000..3ab1829
--- /dev/null
+++ b/hostsidetests/securitybulletin/src/android/security/cts/RegexUtils.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import static org.junit.Assert.*;
+
+public class RegexUtils {
+    private static final int CONTEXT_RANGE = 100; // chars before/after matched input string
+
+    public static void assertContains(String pattern, String input) throws Exception {
+        assertFind(pattern, input, false, false);
+    }
+
+    public static void assertContainsMultiline(String pattern, String input) throws Exception {
+        assertFind(pattern, input, false, true);
+    }
+
+    public static void assertNotContains(String pattern, String input) throws Exception {
+        assertFind(pattern, input, true, false);
+    }
+
+    public static void assertNotContainsMultiline(String pattern, String input) throws Exception {
+        assertFind(pattern, input, true, true);
+    }
+
+    private static void assertFind(
+            String pattern, String input, boolean shouldFind, boolean multiline) {
+        // The input string throws an error when used after the timeout
+        TimeoutCharSequence timedInput = new TimeoutCharSequence(input, 60_000); // 1 minute
+        Matcher matcher = null;
+        if (multiline) {
+            // DOTALL lets .* match line separators
+            // MULTILINE lets ^ and $ match line separators instead of input start and end
+            matcher = Pattern.compile(
+                    pattern, Pattern.DOTALL|Pattern.MULTILINE).matcher(timedInput);
+        } else {
+            matcher = Pattern.compile(pattern).matcher(timedInput);
+        }
+
+        try {
+            long start = System.currentTimeMillis();
+            boolean found = matcher.find();
+            long duration = System.currentTimeMillis() - start;
+
+            if (duration > 1000) { // one second
+                // Provide a warning to the test developer that their regex should be optimized.
+                CLog.logAndDisplay(LogLevel.WARN, "regex match took " + duration + "ms.");
+            }
+
+            if (found && shouldFind) { // failed notContains
+                String substring = input.substring(matcher.start(), matcher.end());
+                String context = getInputContext(input, matcher.start(), matcher.end(),
+                        CONTEXT_RANGE, CONTEXT_RANGE);
+                fail("Pattern found: '" + pattern + "' -> '" + substring + "' for input:\n..." +
+                        context + "...");
+            } else if (!found && !shouldFind) { // failed contains
+                fail("Pattern not found: '" + pattern + "' for input:\n..." + input + "...");
+            }
+        } catch (TimeoutCharSequence.CharSequenceTimeoutException e) {
+            // regex match has taken longer than the timeout
+            // this usually means the input is extremely long or the regex is catastrophic
+            fail("Regex timeout with pattern: '" + pattern + "' for input:\n..." + input + "...");
+        }
+    }
+
+    /*
+     * Helper method to grab the nearby chars for a subsequence. Similar to the -A and -B flags for
+     * grep.
+     */
+    private static String getInputContext(String input, int start, int end, int before, int after) {
+        start = Math.max(0, start - before);
+        end = Math.min(input.length(), end + after);
+        return input.substring(start, end);
+    }
+
+    /*
+     * Wrapper for a given CharSequence. When charAt() is called, the current time is compared
+     * against the timeout. If the current time is greater than the expiration time, an exception is
+     * thrown. The expiration time is (time of object construction) + (timeout in milliseconds).
+     */
+    private static class TimeoutCharSequence implements CharSequence {
+        long expireTime = 0;
+        CharSequence chars = null;
+
+        TimeoutCharSequence(CharSequence chars, long timeout) {
+            this.chars = chars;
+            expireTime = System.currentTimeMillis() + timeout;
+        }
+
+        @Override
+        public char charAt(int index) {
+            if (System.currentTimeMillis() > expireTime) {
+                throw new CharSequenceTimeoutException(
+                        "TimeoutCharSequence was used after the expiration time.");
+            }
+            return chars.charAt(index);
+        }
+
+        @Override
+        public int length() {
+            return chars.length();
+        }
+
+        @Override
+        public CharSequence subSequence(int start, int end) {
+            return new TimeoutCharSequence(chars.subSequence(start, end),
+                    expireTime - System.currentTimeMillis());
+        }
+
+        @Override
+        public String toString() {
+            return chars.toString();
+        }
+
+        private static class CharSequenceTimeoutException extends RuntimeException {
+            public CharSequenceTimeoutException(String message) {
+                super(message);
+            }
+        }
+    }
+}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/SecurityTestCase.java b/hostsidetests/securitybulletin/src/android/security/cts/SecurityTestCase.java
index e6b796e..b1bd053 100644
--- a/hostsidetests/securitybulletin/src/android/security/cts/SecurityTestCase.java
+++ b/hostsidetests/securitybulletin/src/android/security/cts/SecurityTestCase.java
@@ -125,22 +125,26 @@
         }
     }
 
+    // TODO convert existing assertMatches*() to RegexUtils.assertMatches*()
+    // b/123237827
+    @Deprecated
     public void assertMatches(String pattern, String input) throws Exception {
-        assertTrue("Pattern not found", Pattern.matches(pattern, input));
+        RegexUtils.assertContains(pattern, input);
     }
 
+    @Deprecated
     public void assertMatchesMultiLine(String pattern, String input) throws Exception {
-        assertTrue("Pattern not found: " + pattern,
-          Pattern.compile(pattern, Pattern.DOTALL|Pattern.MULTILINE).matcher(input).find());
+        RegexUtils.assertContainsMultiline(pattern, input);
     }
 
+    @Deprecated
     public void assertNotMatches(String pattern, String input) throws Exception {
-        assertFalse("Pattern found", Pattern.matches(pattern, input));
+        RegexUtils.assertNotContains(pattern, input);
     }
 
+    @Deprecated
     public void assertNotMatchesMultiLine(String pattern, String input) throws Exception {
-        assertFalse("Pattern found: " + pattern,
-          Pattern.compile(pattern, Pattern.DOTALL|Pattern.MULTILINE).matcher(input).find());
+        RegexUtils.assertNotContainsMultiline(pattern, input);
     }
 
     /**