blob: 64616db15259c6cf0f4c4f21397bd70206f873e7 [file] [log] [blame]
/*
* 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.ignite.testframework;
import java.time.temporal.ValueRange;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.NotNull;
/**
* The basic listener for custom log contents checking in {@link ListeningTestLogger}.<br><br>
*
* Supports {@link #matches(String) substring}, {@link #matches(Pattern) regular expression} or
* {@link #matches(Predicate) predicate} listeners and the following optional modifiers:
* <ul>
* <li>{@link Builder#times times()} sets the exact number of occurrences</li>
* <li>{@link Builder#atLeast atLeast()} sets the minimum number of occurrences</li>
* <li>{@link Builder#atMost atMost()} sets the maximum number of occurrences</li>
* </ul>
* {@link Builder#atLeast atLeast()} and {@link Builder#atMost atMost()} can be used together.<br><br>
*
* If the expected number of occurrences is not specified for the listener,
* then at least one occurence is expected by default. In other words:<pre>
*
* {@code LogListener.matches(msg).build();}
*
* is equivalent to
*
* {@code LogListener.matches(msg).atLeast(1).build();}
* </pre>
*
* If only the expected maximum number of occurrences is specified, then
* the minimum number of entries for successful validation is zero. In other words:<pre>
*
* {@code LogListener.matches(msg).atMost(10).build();}
*
* is equivalent to
*
* {@code LogListener.matches(msg).atLeast(0).atMost(10).build();}
* </pre>
*/
public abstract class LogListener implements Consumer<String> {
/**
* Checks that all conditions are met.
*
* @return {@code True} if all conditions are met.
*/
public abstract boolean check();
/**
* Checks that all conditions are met with timeout.
*
* @return {@code True} if all conditions are met.
*/
public boolean check(long millis) throws InterruptedException {
long startTime = System.currentTimeMillis();
while (startTime + millis >= System.currentTimeMillis()) {
if (check())
return true;
Thread.sleep(1000);
}
return check();
}
/**
* Reset listener state.
*/
public abstract void reset();
/**
* Creates new builder.
*
* @return new builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* Creates new listener builder.
*
* @param substr Substring to search for in a log message.
* @return Log message listener builder.
*/
public static Builder matches(String substr) {
return new Builder().andMatches(substr);
}
/**
* Creates new listener builder.
*
* @param regexp Regular expression to search for in a log message.
* @return Log message listener builder.
*/
public static Builder matches(Pattern regexp) {
return new Builder().andMatches(regexp);
}
/**
* Creates new listener builder.
*
* @param pred Log message predicate.
* @return Log message listener builder.
*/
public static Builder matches(Predicate<String> pred) {
return new Builder().andMatches(pred);
}
/**
* Log listener builder.
*/
public static class Builder {
/** */
private final CompositeMessageListener lsnr = new CompositeMessageListener();
/** */
private Node prev;
/**
* Add new substring predicate.
*
* @param substr Substring.
* @return current builder instance.
*/
public Builder andMatches(String substr) {
addLast(new Node(msg -> {
if (substr.isEmpty())
return msg.isEmpty() ? 1 : 0;
int cnt = 0;
for (int idx = 0; (idx = msg.indexOf(substr, idx)) != -1; idx++)
++cnt;
return cnt;
}));
return this;
}
/**
* Add new regular expression predicate.
*
* @param regexp Regular expression.
* @return current builder instance.
*/
public Builder andMatches(Pattern regexp) {
addLast(new Node(msg -> {
int cnt = 0;
Matcher matcher = regexp.matcher(msg);
while (matcher.find())
++cnt;
return cnt;
}));
return this;
}
/**
* Add new log message predicate.
*
* @param pred Log message predicate.
* @return current builder instance.
*/
public Builder andMatches(Predicate<String> pred) {
addLast(new Node(msg -> pred.test(msg) ? 1 : 0));
return this;
}
/**
* Set expected number of matches.<br>
* Each log message may contain several matches that will be counted,
* except {@code Predicate} which can have only one match for message.
*
* @param n Expected number of matches.
* @return current builder instance.
*/
public Builder times(int n) {
if (prev != null)
prev.cnt = n;
return this;
}
/**
* Set expected minimum number of matches.<br>
* Each log message may contain several matches that will be counted,
* except {@code Predicate} which can have only one match for message.
*
* @param n Expected number of matches.
* @return current builder instance.
*/
public Builder atLeast(int n) {
if (prev != null) {
prev.min = n;
prev.cnt = null;
}
return this;
}
/**
* Set expected maximum number of matches.<br>
* Each log message may contain several matches that will be counted,
* except {@code Predicate} which can have only one match for message.
*
* @param n Expected number of matches.
* @return current builder instance.
*/
public Builder atMost(int n) {
if (prev != null) {
prev.max = n;
prev.cnt = null;
}
return this;
}
/**
* Constructs message listener.
*
* @return Log message listener.
*/
public LogListener build() {
addLast(null);
return lsnr.lsnrs.size() == 1 ? lsnr.lsnrs.get(0) : lsnr;
}
/**
* @param node Log listener attributes.
*/
private void addLast(Node node) {
if (prev != null)
lsnr.add(prev.listener());
prev = node;
}
/** */
private Builder() {}
/**
* Mutable attributes for log listener.
*/
static final class Node {
/** */
final Function<String, Integer> func;
/** */
Integer min;
/** */
Integer max;
/** */
Integer cnt;
/** */
Node(Function<String, Integer> func) {
this.func = func;
}
/** */
LogMessageListener listener() {
ValueRange range;
if (cnt != null)
range = ValueRange.of(cnt, cnt);
else if (min == null && max == null)
range = ValueRange.of(1, Integer.MAX_VALUE);
else
range = ValueRange.of(min == null ? 0 : min, max == null ? Integer.MAX_VALUE : max);
return new LogMessageListener(func, range);
}
}
}
/** */
private static class LogMessageListener extends LogListener {
/** */
private final Function<String, Integer> func;
/** */
private final AtomicReference<Throwable> err = new AtomicReference<>();
/** */
private final AtomicInteger matches = new AtomicInteger();
/** */
private final ValueRange exp;
/**
* @param exp Expected occurrences.
* @param func Function of counting matches in the message.
*/
private LogMessageListener(@NotNull Function<String, Integer> func, @NotNull ValueRange exp) {
this.func = func;
this.exp = exp;
}
/** {@inheritDoc} */
@Override public void accept(String msg) {
if (err.get() != null)
return;
try {
int cnt = func.apply(msg);
if (cnt > 0)
matches.addAndGet(cnt);
}
catch (Throwable t) {
err.compareAndSet(null, t);
if (t instanceof VirtualMachineError)
throw t;
}
}
/** {@inheritDoc} */
@Override public boolean check() {
errCheck();
int matchesCnt = matches.get();
return exp.isValidIntValue(matchesCnt);
}
/** {@inheritDoc} */
@Override public void reset() {
matches.set(0);
}
/**
* Check that there were no runtime errors.
*/
private void errCheck() {
Throwable t = err.get();
if (t instanceof Error)
throw (Error)t;
if (t instanceof RuntimeException)
throw (RuntimeException)t;
assert t == null : t;
}
}
/** */
private static class CompositeMessageListener extends LogListener {
/** */
private final List<LogMessageListener> lsnrs = new ArrayList<>();
/** {@inheritDoc} */
@Override public boolean check() {
for (LogMessageListener lsnr : lsnrs)
if (!lsnr.check())
return false;
return true;
}
/** {@inheritDoc} */
@Override public void reset() {
for (LogMessageListener lsnr : lsnrs)
lsnr.reset();
}
/** {@inheritDoc} */
@Override public void accept(String msg) {
for (LogMessageListener lsnr : lsnrs)
lsnr.accept(msg);
}
/**
* @param lsnr Listener.
*/
private void add(LogMessageListener lsnr) {
lsnrs.add(lsnr);
}
}
}