You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by dc...@apache.org on 2020/09/18 21:54:11 UTC

[cassandra-in-jvm-dtest-api] branch master updated: Add ability for jvm-dtest to grep instance logs

This is an automated email from the ASF dual-hosted git repository.

dcapwell pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cassandra-in-jvm-dtest-api.git


The following commit(s) were added to refs/heads/master by this push:
     new 672af9b  Add ability for jvm-dtest to grep instance logs
672af9b is described below

commit 672af9b56f1729c6511a2279923eb435df4b7b9b
Author: David Capwell <dc...@apache.org>
AuthorDate: Fri Sep 18 14:53:07 2020 -0700

    Add ability for jvm-dtest to grep instance logs
    
    Patch by David Capwell; reviewed by Alex Petrov, Yifan Cai for CASSANDRA-16120
---
 pom.xml                                            |   2 +-
 .../cassandra/distributed/api/IInstance.java       |  19 +
 .../cassandra/distributed/api/LineIterator.java    |  37 ++
 .../cassandra/distributed/api/LogAction.java       | 416 +++++++++++++++++++++
 .../cassandra/distributed/api/LogResult.java       |  25 ++
 .../cassandra/distributed/api/LogActionTest.java   | 242 ++++++++++++
 6 files changed, 740 insertions(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 49113d7..020b404 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,7 +70,7 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-           <groupId>org.mockito</groupId>
+            <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
             <version>3.5.10</version>
             <scope>test</scope>
diff --git a/src/main/java/org/apache/cassandra/distributed/api/IInstance.java b/src/main/java/org/apache/cassandra/distributed/api/IInstance.java
index 496d33d..4ffc36a 100644
--- a/src/main/java/org/apache/cassandra/distributed/api/IInstance.java
+++ b/src/main/java/org/apache/cassandra/distributed/api/IInstance.java
@@ -90,4 +90,23 @@ public interface IInstance extends IIsolatedExecutor
 
     void forceCompact(String keyspace, String table);
 
+    List<Throwable> getUncaughtExceptions();
+
+    default boolean getLogsEnabled()
+    {
+        try
+        {
+            logs();
+            return true;
+        }
+        catch (UnsupportedOperationException e)
+        {
+            return false;
+        }
+    }
+
+    default LogAction logs()
+    {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/src/main/java/org/apache/cassandra/distributed/api/LineIterator.java b/src/main/java/org/apache/cassandra/distributed/api/LineIterator.java
new file mode 100644
index 0000000..8971232
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/distributed/api/LineIterator.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.distributed.api;
+
+import java.util.Iterator;
+
+public interface LineIterator extends Iterator<String>, AutoCloseable
+{
+    /**
+     * @return current position of the iterator
+     * @see LogAction#grep(long, String)
+     * @see LogAction#watchFor(long, String)
+     */
+    long mark();
+
+    @Override
+    default void close()
+    {
+
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/distributed/api/LogAction.java b/src/main/java/org/apache/cassandra/distributed/api/LogAction.java
new file mode 100644
index 0000000..8f9e693
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/distributed/api/LogAction.java
@@ -0,0 +1,416 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.distributed.api;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public interface LogAction
+{
+    /**
+     * @return current position of the iterator
+     * @see LogAction#grep(long, String)
+     * @see LogAction#watchFor(long, String)
+     */
+    long mark();
+
+    LineIterator match(long startPosition, Predicate<String> fn);
+
+    default LineIterator match(Predicate<String> fn){
+        return match(Internal.DEFAULT_START_POSITION, fn);
+    }
+
+    default LogResult<List<String>> watchFor(long startPosition, Duration timeout, Predicate<String> fn) throws TimeoutException
+    {
+        long nowNanos = System.nanoTime();
+        long deadlineNanos = nowNanos + timeout.toNanos();
+        long previousPosition = startPosition;
+        List<String> matches = new ArrayList<>();
+        while (System.nanoTime() <= deadlineNanos)
+        {
+            if (previousPosition == mark())
+            {
+                // still matching... wait a bit
+                Internal.sleepUninterruptibly(1, TimeUnit.SECONDS);
+                continue;
+            }
+            // position not matching, try to read
+            try (LineIterator it = match(previousPosition, fn))
+            {
+                while (it.hasNext())
+                    matches.add(it.next());
+                if (!matches.isEmpty())
+                    return new BasicLogResult<>(it.mark(), matches);
+                previousPosition = it.mark();
+            }
+        }
+        throw new TimeoutException();
+    }
+
+    default LogResult<List<String>> watchFor(Duration timeout, Predicate<String> fn) throws TimeoutException {
+        return watchFor(Internal.DEFAULT_START_POSITION, timeout, fn);
+    }
+
+    default LogResult<List<String>> watchFor(Predicate<String> fn) throws TimeoutException {
+        return watchFor(Internal.DEFAULT_START_POSITION, Internal.DEFAULT_TIMEOUT, fn);
+    }
+
+    default LogResult<List<String>> watchFor(long startPosition, Duration timeout, List<Pattern> patterns) throws TimeoutException
+    {
+        return watchFor(startPosition, timeout, Internal.regexPredicate(patterns));
+    }
+
+    default LogResult<List<String>> watchFor(Duration timeout, List<Pattern> patterns) throws TimeoutException
+    {
+        return watchFor(Internal.DEFAULT_START_POSITION, timeout, Internal.regexPredicate(patterns));
+    }
+
+    default LogResult<List<String>> watchFor(long startPosition, List<Pattern> patterns) throws TimeoutException
+    {
+        return watchFor(startPosition, Internal.DEFAULT_TIMEOUT, Internal.regexPredicate(patterns));
+    }
+
+    default LogResult<List<String>> watchFor(List<Pattern> patterns) throws TimeoutException
+    {
+        return watchFor(Internal.DEFAULT_START_POSITION, Internal.DEFAULT_TIMEOUT, Internal.regexPredicate(patterns));
+    }
+
+    default LogResult<List<String>> watchFor(long startPosition, Duration timeout, Pattern pattern) throws TimeoutException
+    {
+        return watchFor(startPosition, timeout, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> watchFor(Duration timeout, Pattern pattern) throws TimeoutException
+    {
+        return watchFor(Internal.DEFAULT_START_POSITION, timeout, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> watchFor(long startPosition, Pattern pattern) throws TimeoutException
+    {
+        return watchFor(startPosition, Internal.DEFAULT_TIMEOUT, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> watchFor(Pattern pattern) throws TimeoutException
+    {
+        return watchFor(Internal.DEFAULT_START_POSITION, Internal.DEFAULT_TIMEOUT, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> watchFor(long startPosition, Duration timeout, String pattern) throws TimeoutException
+    {
+        return watchFor(startPosition, timeout, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> watchFor(Duration timeout, String pattern) throws TimeoutException
+    {
+        return watchFor(Internal.DEFAULT_START_POSITION, timeout, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> watchFor(long startPosition, String pattern) throws TimeoutException
+    {
+        return watchFor(startPosition, Internal.DEFAULT_TIMEOUT, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> watchFor(String pattern) throws TimeoutException
+    {
+        return watchFor(Internal.DEFAULT_START_POSITION, Internal.DEFAULT_TIMEOUT, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> watchFor(long startPosition, Duration timeout, String pattern, String... others) throws TimeoutException
+    {
+
+        return watchFor(startPosition, timeout, Internal.regexPredicate(pattern, others));
+    }
+
+    default LogResult<List<String>> watchFor(Duration timeout, String pattern, String... others) throws TimeoutException
+    {
+
+        return watchFor(Internal.DEFAULT_START_POSITION, timeout, Internal.regexPredicate(pattern, others));
+    }
+
+    default LogResult<List<String>> watchFor(long startPosition, String pattern, String... others) throws TimeoutException
+    {
+
+        return watchFor(startPosition, Internal.DEFAULT_TIMEOUT, Internal.regexPredicate(pattern, others));
+    }
+
+    default LogResult<List<String>> watchFor(String pattern, String... others) throws TimeoutException
+    {
+        return watchFor(Internal.DEFAULT_START_POSITION, Internal.DEFAULT_TIMEOUT, Internal.regexPredicate(pattern, others));
+    }
+
+    default LogResult<List<String>> grep(long startPosition, Predicate<String> fn)
+    {
+        try (LineIterator it = match(startPosition, fn))
+        {
+            return new BasicLogResult<>(it.mark(), Internal.collect(it));
+        }
+    }
+
+    default LogResult<List<String>> grep(Predicate<String> fn)
+    {
+        return grep(Internal.DEFAULT_START_POSITION, fn);
+    }
+
+    default LogResult<List<String>> grep(long startPosition, List<Pattern> patterns)
+    {
+        return grep(startPosition, Internal.regexPredicate(patterns));
+    }
+
+    default LogResult<List<String>> grep(List<Pattern> patterns)
+    {
+        return grep(Internal.DEFAULT_START_POSITION, Internal.regexPredicate(patterns));
+    }
+
+    default LogResult<List<String>> grep(long startPosition, Pattern pattern)
+    {
+        return grep(startPosition, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> grep(Pattern pattern)
+    {
+        return grep(Internal.DEFAULT_START_POSITION, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> grep(long startPosition, String pattern)
+    {
+        return grep(startPosition, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> grep(String pattern)
+    {
+        return grep(Internal.DEFAULT_START_POSITION, Internal.regexPredicate(pattern));
+    }
+
+    default LogResult<List<String>> grep(long startPosition, String pattern, String... others)
+    {
+
+        return grep(startPosition, Internal.regexPredicate(pattern, others));
+    }
+
+    default LogResult<List<String>> grep(String pattern, String... others)
+    {
+        return grep(Internal.DEFAULT_START_POSITION, Internal.regexPredicate(pattern, others));
+    }
+
+    /**
+     * Attempt to find all errors in the log.  This method is different from {@code grep("^ERROR")} as it will also
+     * attempt to stitch the exception stack trace into a single line.
+     *
+     * This method is modeled after python dtests's grep_for_errors and matches the semantics there.
+     *
+     * @param startPosition to grep from
+     * @param exceptionPattern for WARN logs to check if they might have an exception
+     * @return result of all found exceptions, with stitched errors
+     */
+    default LogResult<List<String>> grepForErrors(long startPosition, Pattern exceptionPattern)
+    {
+        Objects.requireNonNull(exceptionPattern, "exceptionPattern");
+
+        Pattern logLevelPattern = Internal.LOG_LEVEL_PATTERN;
+        Function<String, String> extractLogLevel = line -> {
+            Matcher matcher = logLevelPattern.matcher(line);
+            if (!matcher.find())
+                return null;
+            return matcher.group(1);
+        };
+        List<String> matches = new ArrayList<>();
+        try (LineIterator it = match(startPosition, ignore -> true))
+        {
+            StringBuilder lineBuffer = new StringBuilder();
+            while (it.hasNext())
+            {
+                String line = it.next();
+                String logLevelOrNull = extractLogLevel.apply(line);
+                if (logLevelOrNull == null)
+                {
+                    // found a log which isn't the start of a logger line; assume its an exception
+                    if (lineBuffer.length() == 0)
+                    {
+                        // no previous start of line found, so skip this
+                        continue;
+                    }
+                    lineBuffer.append('\n').append(line);
+                    continue;
+                }
+                // line is a start of a log, reset state
+                if (lineBuffer.length() != 0)
+                {
+                    // buffer has content, add and move on
+                    matches.add(lineBuffer.toString());
+                    lineBuffer.setLength(0);
+                }
+                switch (logLevelOrNull)
+                {
+                    case "ERROR":
+                        lineBuffer.append(line);
+                        break;
+                    case "WARN":
+                        if (exceptionPattern.matcher(line).find())
+                            lineBuffer.append(line);
+                        break;
+                    default:
+                        // ignore
+                }
+            }
+            if (lineBuffer.length() != 0)
+            {
+                matches.add(lineBuffer.toString());
+            }
+            return new BasicLogResult<>(it.mark(), matches);
+        }
+    }
+
+    /**
+     * Attempt to find all errors in the log.  This method is different from {@code grep("^ERROR")} as it will also
+     * attempt to stitch the exception stack trace into a single line.
+     *
+     * This method is modeled after python dtests's grep_for_errors and matches the semantics there.
+     *
+     * @param startPosition to grep from
+     * @return result of all found exceptions, with stitched errors
+     */
+    default LogResult<List<String>> grepForErrors(long startPosition)
+    {
+        return grepForErrors(startPosition, Internal.LOG_EXCEPTION_PATTERN);
+    }
+
+    /**
+     * Attempt to find all errors in the log.  This method is different from {@code grep("^ERROR")} as it will also
+     * attempt to stitch the exception stack trace into a single line.
+     *
+     * This method is modeled after python dtests's grep_for_errors and matches the semantics there.
+     *
+     * @return result of all found exceptions, with stitched errors
+     */
+    default LogResult<List<String>> grepForErrors()
+    {
+        return grepForErrors(Internal.DEFAULT_START_POSITION, Internal.LOG_EXCEPTION_PATTERN);
+    }
+
+    class BasicLogResult<T> implements LogResult<T>
+    {
+        private final long mark;
+        private final T result;
+
+        public BasicLogResult(long mark, T result) {
+            this.mark = mark;
+            this.result = Objects.requireNonNull(result);
+        }
+
+        @Override
+        public long getMark() {
+            return mark;
+        }
+
+        @Override
+        public T getResult() {
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return "LogResult{" +
+                    "mark=" + mark +
+                    ", result=" + result +
+                    '}';
+        }
+    }
+    
+    class Internal
+    {
+        private static final int DEFAULT_START_POSITION = -1;
+        // why 10m?  This is the default for python dtest...
+        private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(10);
+        private static final Pattern LOG_LEVEL_PATTERN = Pattern.compile("^(INFO|DEBUG|WARN|ERROR)");
+        private static final Pattern LOG_EXCEPTION_PATTERN = Pattern.compile("[Ee]xception|AssertionError");
+
+        private static List<String> collect(LineIterator it)
+        {
+            List<String> matches = new ArrayList<>();
+            while (it.hasNext())
+                matches.add(it.next());
+            return matches.isEmpty() ? Collections.emptyList() : matches;
+        }
+
+        private static Predicate<String> regexPredicate(List<Pattern> patterns)
+        {
+            return line -> {
+                for (Pattern regex : patterns)
+                {
+                    Matcher m = regex.matcher(line);
+                    if (m.find())
+                        return true;
+                }
+                return false;
+            };
+        }
+
+        private static Predicate<String> regexPredicate(String pattern, String... others)
+        {
+            List<Pattern> patterns = new ArrayList<>(others.length + 1);
+            patterns.add(Pattern.compile(pattern));
+            for (String s : others)
+                patterns.add(Pattern.compile(s));
+            return regexPredicate(patterns);
+        }
+
+        private static Predicate<String> regexPredicate(Pattern pattern)
+        {
+            return line -> pattern.matcher(line).find();
+        }
+
+        private static Predicate<String> regexPredicate(String pattern)
+        {
+            return regexPredicate(Pattern.compile(pattern));
+        }
+
+        private static void sleepUninterruptibly(long sleepFor, TimeUnit unit) {
+            // copied from guava since dtest can't depend on guava
+            boolean interrupted = false;
+
+            try {
+                long remainingNanos = unit.toNanos(sleepFor);
+                long end = System.nanoTime() + remainingNanos;
+
+                while(true) {
+                    try {
+                        TimeUnit.NANOSECONDS.sleep(remainingNanos);
+                        return;
+                    } catch (InterruptedException var12) {
+                        interrupted = true;
+                        remainingNanos = end - System.nanoTime();
+                    }
+                }
+            } finally {
+                if (interrupted) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/distributed/api/LogResult.java b/src/main/java/org/apache/cassandra/distributed/api/LogResult.java
new file mode 100644
index 0000000..a8cfcc4
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/distributed/api/LogResult.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.distributed.api;
+
+public interface LogResult<T> {
+    long getMark();
+
+    T getResult();
+}
diff --git a/src/test/java/org/apache/cassandra/distributed/api/LogActionTest.java b/src/test/java/org/apache/cassandra/distributed/api/LogActionTest.java
new file mode 100644
index 0000000..bc2100e
--- /dev/null
+++ b/src/test/java/org/apache/cassandra/distributed/api/LogActionTest.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.distributed.api;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+
+class LogActionTest
+{
+    @Test
+    public void watchForTimeout() {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn, "a", "b", "c", "d"));
+
+        Duration duration = Duration.ofSeconds(1);
+        long startNanos = System.nanoTime();
+        Assertions.assertThatThrownBy(() -> logs.watchFor(duration, "^ERROR"))
+                .isInstanceOf(TimeoutException.class);
+        Assertions.assertThat(System.nanoTime())
+                .as("duration was smaller than expected timeout")
+                .isGreaterThanOrEqualTo(startNanos + duration.toNanos());
+    }
+
+    @Test
+    public void watchForAndFindFirstAttempt() throws TimeoutException {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn, "a", "b", "ERROR match", "d"));
+
+        List<String> matches = logs.watchFor("^ERROR").getResult();
+        Assertions.assertThat(matches).isEqualTo(Arrays.asList("ERROR match"));
+    }
+
+    @Test
+    public void watchForAndFindThirdAttempt() throws TimeoutException {
+        class Counter
+        {
+            int count;
+        }
+        Counter counter = new Counter();
+        LogAction logs = mockLogAction(fn -> {
+            if (++counter.count == 3) {
+                return lineIterator(fn, "a", "b", "ERROR match", "d");
+            } else {
+                return lineIterator(fn, "a", "b", "c", "d");
+            }
+        });
+
+        List<String> matches = logs.watchFor("^ERROR").getResult();
+        Assertions.assertThat(matches).isEqualTo(Arrays.asList("ERROR match"));
+        Assertions.assertThat(counter.count).isEqualTo(3);
+    }
+
+    @Test
+    public void grepNoMatch() {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn, "a", "b", "c", "d"));
+
+        List<String> matches = logs.grep("^ERROR").getResult();
+        Assertions.assertThat(matches).isEmpty();
+    }
+
+    @Test
+    public void grepMatch() {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn, "a", "b", "ERROR match", "d"));
+
+        List<String> matches = logs.grep("^ERROR").getResult();
+        Assertions.assertThat(matches).isEqualTo(Arrays.asList("ERROR match"));
+    }
+
+    @Test
+    public void grepForErrorsNoMatch() {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn, "a", "b", "c", "d"));
+
+        List<String> matches = logs.grepForErrors().getResult();
+        Assertions.assertThat(matches).isEmpty();
+    }
+
+    @Test
+    public void grepForErrorsNoStacktrace() {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn, "INFO a", "INFO b", "ERROR match", "INFO d"));
+
+        List<String> matches = logs.grepForErrors().getResult();
+        Assertions.assertThat(matches).isEqualTo(Arrays.asList("ERROR match"));
+    }
+
+    @Test
+    public void grepForErrorsWithStacktrace() {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn,
+                "INFO a", "INFO b",
+                "ERROR match",
+                "\t\tat class.method(42)",
+                "\t\tat class.method(42)"));
+
+        List<String> matches = logs.grepForErrors().getResult();
+        Assertions.assertThat(matches).isEqualTo(Arrays.asList("ERROR match\n" +
+                "\t\tat class.method(42)\n" +
+                "\t\tat class.method(42)"));
+    }
+
+    @Test
+    public void grepForErrorsMultilineWarnNotException() {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn,
+                "INFO a", "INFO b",
+                "WARN match",
+                "\t\tat class.method(42)",
+                "\t\tat class.method(42)"));
+
+        List<String> matches = logs.grepForErrors().getResult();
+        Assertions.assertThat(matches).isEmpty();
+    }
+
+    @Test
+    public void grepForErrorsWARNWithStacktrace() {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn,
+                "INFO a", "INFO b",
+                "WARN match but exception in test",
+                "\t\tat class.method(42)",
+                "\t\tat class.method(42)"));
+
+        List<String> matches = logs.grepForErrors().getResult();
+        Assertions.assertThat(matches).isEqualTo(Arrays.asList("WARN match but exception in test\n" +
+                "\t\tat class.method(42)\n" +
+                "\t\tat class.method(42)"));
+    }
+
+    @Test
+    public void grepForErrorsWARNWithStacktraceFromAssert() {
+        LogAction logs = mockLogAction(fn -> lineIterator(fn,
+                "INFO a", "INFO b",
+                "WARN match but AssertionError in test",
+                "\t\tat class.method(42)",
+                "\t\tat class.method(42)"));
+
+        List<String> matches = logs.grepForErrors().getResult();
+        Assertions.assertThat(matches).isEqualTo(Arrays.asList("WARN match but AssertionError in test\n" +
+                "\t\tat class.method(42)\n" +
+                "\t\tat class.method(42)"));
+    }
+
+    private static LogAction mockLogActionAnswer(Answer<?> matchAnswer) {
+        LogAction logs = Mockito.mock(LogAction.class, Mockito.CALLS_REAL_METHODS);
+        // mark is only used by matching, which we also mock out, so its ok to be a constant
+        Mockito.when(logs.mark()).thenReturn(0L);
+        Mockito.when(logs.match(Mockito.anyLong(), Mockito.any())).thenAnswer(matchAnswer);
+        return logs;
+    }
+
+    private static LogAction mockLogAction(MockLogMatch match) {
+        return mockLogActionAnswer(invoke -> match.answer(invoke.getArgument(0), invoke.getArgument(1)));
+    }
+
+    private static LogAction mockLogAction(MockLogMatchPredicate match) {
+        return mockLogAction((MockLogMatch) match);
+    }
+
+    @FunctionalInterface
+    private interface MockLogMatch
+    {
+        LineIterator answer(long startPosition, Predicate<String> fn) throws Throwable;
+    }
+
+    @FunctionalInterface
+    private interface MockLogMatchPredicate extends MockLogMatch
+    {
+        LineIterator answer(Predicate<String> fn) throws Throwable;
+
+        @Override
+        default LineIterator answer(long startPosition, Predicate<String> fn) throws Throwable
+        {
+            return answer(fn);
+        }
+    }
+
+    private static LineIterator lineIterator(Predicate<String> fn, String... values)
+    {
+        return new LineIteratorImpl(Arrays.asList(values).iterator(), fn);
+    }
+
+    private static final class LineIteratorImpl implements LineIterator
+    {
+        private final Iterator<String> it;
+        private final Predicate<String> fn;
+        private String next = null;
+        private long count = 0;
+
+        private LineIteratorImpl(Iterator<String> it, Predicate<String> fn) {
+            this.it = it;
+            this.fn = fn;
+        }
+
+        @Override
+        public long mark() {
+            return count;
+        }
+
+        @Override
+        public boolean hasNext() {
+            if (next != null) // only move forward if consumed
+                return true;
+            while (it.hasNext())
+            {
+                count++;
+                String next = it.next();
+                if (fn.test(next))
+                {
+                    this.next = next;
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public String next() {
+            String ret = next;
+            next = null;
+            return ret;
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org