blob: 79cd1a6f028c580041b887fb64ceca73573e5d38 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.lib.packages;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableRangeMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.OptionsParsingException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Symbolic labels of test timeout. Borrows heavily from {@link TestSize}.
*/
public enum TestTimeout {
// These symbolic labels are used in the build files.
SHORT(60),
MODERATE(300),
LONG(900),
ETERNAL(3600);
/**
* Default --test_timeout flag, used when collecting code coverage.
*/
public static final String COVERAGE_CMD_TIMEOUT = "--test_timeout=300,600,1200,3600";
/** Map from test time to suggested TestTimeout. */
private static final RangeMap<Integer, TestTimeout> SUGGESTED_TIMEOUT;
/**
* Map from TestTimeout to fuzzy range.
*
* <p>The fuzzy range is used to check whether the actual timeout is close to the upper bound of
* the current timeout or much smaller than the next shorter timeout. This is used to give
* suggestions to developers to update their timeouts.
*/
private static final Map<TestTimeout, Range<Integer>> TIMEOUT_FUZZY_RANGE;
static {
// For the largest timeout, cap suggested and fuzzy ranges at one year.
final int maxTimeout = 365 * 24 * 60 * 60 /* One year */;
ImmutableRangeMap.Builder<Integer, TestTimeout> suggestedTimeoutBuilder =
ImmutableRangeMap.builder();
ImmutableMap.Builder<TestTimeout, Range<Integer>> timeoutFuzzyRangeBuilder =
ImmutableMap.builder();
int previousMaxSuggested = 0;
int previousTimeout = 0;
Iterator<TestTimeout> timeoutIterator = Arrays.asList(values()).iterator();
while (timeoutIterator.hasNext()) {
TestTimeout timeout = timeoutIterator.next();
// Set up time ranges for suggested timeouts and fuzzy timeouts. Fuzzy timeout ranges should
// be looser than suggested timeout ranges in order to make sure that after a test size is
// adjusted, it's difficult for normal time variance to push it outside the fuzzy timeout
// range.
// This should be exactly the previous max because there should be exactly one suggested
// timeout for any given time.
final int minSuggested = previousMaxSuggested;
// Only suggest timeouts that are less than 75% of the actual timeout (unless there are no
// higher timeouts). This should be low enough to prevent suggested times from causing test
// timeout flakiness.
final int maxSuggested =
timeoutIterator.hasNext() ? (int) (timeout.timeout * 0.75) : maxTimeout;
// Set fuzzy minimum timeout to half the previous timeout. If the test is that fast, it should
// be safe to use the shorter timeout.
final int minFuzzy = previousTimeout / 2;
// Set fuzzy maximum timeout to 90% of the timeout. A test this close to the limit can easily
// become timeout flaky.
final int maxFuzzy = timeoutIterator.hasNext() ? (int) (timeout.timeout * 0.9) : maxTimeout;
timeoutFuzzyRangeBuilder.put(timeout, Range.closedOpen(minFuzzy, maxFuzzy));
suggestedTimeoutBuilder.put(Range.closedOpen(minSuggested, maxSuggested), timeout);
previousMaxSuggested = maxSuggested;
previousTimeout = timeout.timeout;
}
SUGGESTED_TIMEOUT = suggestedTimeoutBuilder.build();
TIMEOUT_FUZZY_RANGE = timeoutFuzzyRangeBuilder.buildOrThrow();
}
private final int timeout;
TestTimeout(int timeout) {
this.timeout = timeout;
}
/**
* Returns the enum associated with a test's timeout or null if the tag is not lower case or an
* unknown size.
*/
@Nullable
public static TestTimeout getTestTimeout(String attr) {
if (!attr.equals(attr.toLowerCase())) {
return null;
}
try {
return TestTimeout.valueOf(attr.toUpperCase(Locale.ENGLISH));
} catch (IllegalArgumentException e) {
return null;
}
}
/**
* Returns test timeout of the given target using explicitly specified timeout or default through
* the size label's associated default or null if the target is not a test.
*/
@Nullable
public static TestTimeout getTestTimeout(Rule target) {
String attr = NonconfigurableAttributeMapper.attributeOrNull(target, "timeout", Type.STRING);
if (attr == null) {
// The target is not a test. This is reached by serialization code as it tries to serialize
// essential target fields. There's not enough context there to pre-determine whether a
// target is a test or not, so it simply serializes any String timeout field.
//
// TODO(b/297857068): refactor ConfiguredTargetAndData and remove this branch.
return null;
}
if (!attr.equals(attr.toLowerCase())) {
return null; // attribute values must be lowercase
}
try {
return TestTimeout.valueOf(attr.toUpperCase(Locale.ENGLISH));
} catch (IllegalArgumentException e) {
return null;
}
}
@Override
public String toString() {
return super.toString().toLowerCase();
}
/**
* We print to upper case to make the test timeout warnings more readable.
*/
public String prettyPrint() {
return super.toString().toUpperCase();
}
@Deprecated // use getTimeout instead
public int getTimeoutSeconds() {
return timeout;
}
public Duration getTimeout() {
return Duration.ofSeconds(timeout);
}
/**
* Returns true iff the given time is not close to the upper bound timeout and is so short that it
* should be assigned a different timeout.
*
* <p>This is used to give suggestions to developers to update their timeouts. If this returns
* true, a more reasonable timeout can be selected with {@link #getSuggestedTestTimeout(int)}
*/
public boolean isInRangeFuzzy(int timeInSeconds) {
return TIMEOUT_FUZZY_RANGE.get(this).contains(timeInSeconds);
}
/**
* Returns suggested test size for the given time in seconds.
*
* <p>Will suggest times that are unlikely to result in timeout flakiness even if the test has a
* significant amount of time variance.
*/
public static TestTimeout getSuggestedTestTimeout(int timeInSeconds) {
return SUGGESTED_TIMEOUT.get(timeInSeconds);
}
/** Converter for the --test_timeout option. */
public static class TestTimeoutConverter
extends Converter.Contextless<Map<TestTimeout, Duration>> {
public TestTimeoutConverter() {}
@Override
public Map<TestTimeout, Duration> convert(String input) throws OptionsParsingException {
List<Duration> values = new ArrayList<>();
for (String token : Splitter.on(',').limit(6).split(input)) {
// Handle the case of "2," which is accepted as legal... Because Splitter.split is lazy,
// there's no way of knowing if an empty string is a trailing or an intermediate one,
// so we can't fully emulate String.split(String, 0).
if (!token.isEmpty() || values.size() > 1) {
try {
values.add(Duration.ofSeconds(Integer.parseInt(token)));
} catch (NumberFormatException e) {
throw new OptionsParsingException("'" + input + "' is not an int", e);
}
}
}
EnumMap<TestTimeout, Duration> timeouts = Maps.newEnumMap(TestTimeout.class);
if (values.size() == 1) {
timeouts.put(SHORT, values.get(0));
timeouts.put(MODERATE, values.get(0));
timeouts.put(LONG, values.get(0));
timeouts.put(ETERNAL, values.get(0));
} else if (values.size() == 4) {
timeouts.put(SHORT, values.get(0));
timeouts.put(MODERATE, values.get(1));
timeouts.put(LONG, values.get(2));
timeouts.put(ETERNAL, values.get(3));
} else {
throw new OptionsParsingException("Invalid number of comma-separated entries");
}
for (TestTimeout label : values()) {
if (!timeouts.containsKey(label) || timeouts.get(label).compareTo(Duration.ZERO) <= 0) {
timeouts.put(label, label.getTimeout());
}
}
return timeouts;
}
@Override
public String getTypeDescription() {
return "a single integer or comma-separated list of 4 integers";
}
}
/**
* Converter for the --test_timeout_filters option.
*/
public static class TestTimeoutFilterConverter extends EnumFilterConverter<TestTimeout> {
public TestTimeoutFilterConverter() {
super(TestTimeout.class, "test timeout");
}
/**
* {@inheritDoc}
*
* <p>This override is necessary to prevent OptionsData from throwing a "must be assignable from
* the converter return type" exception. OptionsData doesn't recognize the generic type and
* actual type are the same.
*/
@Override
public final Set<TestTimeout> convert(String input) throws OptionsParsingException {
return super.convert(input);
}
}
}