blob: 5885d8233634873daf687a4140b5388fb3b88165 [file] [log] [blame]
package org.apache.lucene.util;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks;
import com.carrotsearch.randomizedtesting.RandomizedTest;
import com.carrotsearch.randomizedtesting.rules.TestRuleAdapter;
/*
* 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.
*/
/**
* Fails the suite if it prints over the given limit of bytes to either
* {@link System#out} or {@link System#err},
* unless the condition is not enforced (see {@link #isEnforced()}).
*/
public class TestRuleLimitSysouts extends TestRuleAdapter {
/**
* Max limit of bytes printed to either {@link System#out} or {@link System#err}.
* This limit is enforced per-class (suite).
*/
public final static int DEFAULT_SYSOUT_BYTES_THRESHOLD = 8 * 1024;
/**
* An annotation specifying the limit of bytes per class.
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public static @interface Limit {
public int bytes();
}
private final static AtomicInteger bytesWritten = new AtomicInteger();
private final static DelegateStream capturedSystemOut;
private final static DelegateStream capturedSystemErr;
/**
* We capture system output and error streams as early as possible because
* certain components (like the Java logging system) steal these references and
* never refresh them.
*
* Also, for this exact reason, we cannot change delegate streams for every suite.
* This isn't as elegant as it should be, but there's no workaround for this.
*/
static {
System.out.flush();
System.err.flush();
final String csn = Charset.defaultCharset().name();
capturedSystemOut = new DelegateStream(System.out, csn, bytesWritten);
capturedSystemErr = new DelegateStream(System.err, csn, bytesWritten);
System.setOut(capturedSystemOut.printStream);
System.setErr(capturedSystemErr.printStream);
}
/**
* Test failures from any tests or rules before.
*/
private final TestRuleMarkFailure failureMarker;
/**
* Tracks the number of bytes written to an underlying stream by
* incrementing an {@link AtomicInteger}.
*/
static class DelegateStream extends FilterOutputStream {
final PrintStream printStream;
final AtomicInteger bytesCounter;
public DelegateStream(OutputStream delegate, String charset, AtomicInteger bytesCounter) {
super(delegate);
try {
this.printStream = new PrintStream(this, true, charset);
this.bytesCounter = bytesCounter;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
// Do override all three write() methods to make sure nothing slips through.
@Override
public void write(byte[] b) throws IOException {
if (b.length > 0) {
bytesCounter.addAndGet(b.length);
}
super.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (len > 0) {
bytesCounter.addAndGet(len);
}
super.write(b, off, len);
}
@Override
public void write(int b) throws IOException {
bytesCounter.incrementAndGet();
super.write(b);
}
}
public TestRuleLimitSysouts(TestRuleMarkFailure failureMarker) {
this.failureMarker = failureMarker;
}
/** */
@Override
protected void before() throws Throwable {
if (isEnforced()) {
checkCaptureStreams();
}
resetCaptureState();
validateClassAnnotations();
}
private void validateClassAnnotations() {
Class<?> target = RandomizedTest.getContext().getTargetClass();
if (target.isAnnotationPresent(Limit.class)) {
int bytes = target.getAnnotation(Limit.class).bytes();
if (bytes < 0 || bytes > 1 * 1024 * 1024) {
throw new AssertionError("The sysout limit is insane. Did you want to use "
+ "@" + LuceneTestCase.SuppressSysoutChecks.class.getName() + " annotation to "
+ "avoid sysout checks entirely?");
}
}
}
/**
* Ensures {@link System#out} and {@link System#err} point to delegate streams.
*/
public static void checkCaptureStreams() {
// Make sure we still hold the right references to wrapper streams.
if (System.out != capturedSystemOut.printStream) {
throw new AssertionError("Something has changed System.out to: " + System.out.getClass().getName());
}
if (System.err != capturedSystemErr.printStream) {
throw new AssertionError("Something has changed System.err to: " + System.err.getClass().getName());
}
}
protected boolean isEnforced() {
Class<?> target = RandomizedTest.getContext().getTargetClass();
if (LuceneTestCase.VERBOSE ||
LuceneTestCase.INFOSTREAM ||
target.isAnnotationPresent(SuppressSysoutChecks.class)) {
return false;
}
if (!target.isAnnotationPresent(Limit.class)) {
return false;
}
return true;
}
/**
* We're only interested in failing the suite if it was successful (otherwise
* just propagate the original problem and don't bother doing anything else).
*/
@Override
protected void afterIfSuccessful() throws Throwable {
if (isEnforced()) {
checkCaptureStreams();
// Flush any buffers.
capturedSystemOut.printStream.flush();
capturedSystemErr.printStream.flush();
// Check for offenders, but only if everything was successful so far.
int limit = RandomizedTest.getContext().getTargetClass().getAnnotation(Limit.class).bytes();
if (bytesWritten.get() >= limit && failureMarker.wasSuccessful()) {
throw new AssertionError(String.format(Locale.ENGLISH,
"The test or suite printed %d bytes to stdout and stderr," +
" even though the limit was set to %d bytes. Increase the limit with @%s, ignore it completely" +
" with @%s or run with -Dtests.verbose=true",
bytesWritten.get(),
limit,
Limit.class.getSimpleName(),
SuppressSysoutChecks.class.getSimpleName()));
}
}
}
@Override
protected void afterAlways(List<Throwable> errors) throws Throwable {
resetCaptureState();
}
private void resetCaptureState() {
capturedSystemOut.printStream.flush();
capturedSystemErr.printStream.flush();
bytesWritten.set(0);
}
}