blob: 6e834a70d7853d99184b4bf10c05bcf1874a5d52 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.packages;
16
17import com.google.common.base.Splitter;
Googler7c177212017-06-16 15:46:55 +020018import com.google.common.collect.ImmutableMap;
19import com.google.common.collect.ImmutableRangeMap;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010020import com.google.common.collect.Maps;
Googler7c177212017-06-16 15:46:55 +020021import com.google.common.collect.Range;
22import com.google.common.collect.RangeMap;
Lukacs Berkiffa73ad2015-09-18 11:40:12 +000023import com.google.devtools.build.lib.syntax.Type;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010024import com.google.devtools.common.options.Converter;
25import com.google.devtools.common.options.OptionsParsingException;
ulfjackdeab0cf2017-08-08 13:08:24 +020026import java.time.Duration;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010027import java.util.ArrayList;
Googler7c177212017-06-16 15:46:55 +020028import java.util.Arrays;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010029import java.util.EnumMap;
Googler7c177212017-06-16 15:46:55 +020030import java.util.Iterator;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010031import java.util.List;
32import java.util.Map;
33import java.util.Set;
34
35/**
36 * Symbolic labels of test timeout. Borrows heavily from {@link TestSize}.
37 */
38public enum TestTimeout {
39
40 // These symbolic labels are used in the build files.
Googler7c177212017-06-16 15:46:55 +020041 SHORT(60),
42 MODERATE(300),
43 LONG(900),
44 ETERNAL(3600);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010045
46 /**
47 * Default --test_timeout flag, used when collecting code coverage.
48 */
Lukacs Berkide2bba72015-04-16 11:16:38 +000049 public static final String COVERAGE_CMD_TIMEOUT = "--test_timeout=300,600,1200,3600";
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010050
Googler7c177212017-06-16 15:46:55 +020051 /** Map from test time to suggested TestTimeout. */
52 private static final RangeMap<Integer, TestTimeout> SUGGESTED_TIMEOUT;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010053
Googler7c177212017-06-16 15:46:55 +020054 /**
55 * Map from TestTimeout to fuzzy range.
56 *
57 * <p>The fuzzy range is used to check whether the actual timeout is close to the upper bound of
58 * the current timeout or much smaller than the next shorter timeout. This is used to give
59 * suggestions to developers to update their timeouts.
60 */
61 private static final Map<TestTimeout, Range<Integer>> TIMEOUT_FUZZY_RANGE;
62
63 static {
64 // For the largest timeout, cap suggested and fuzzy ranges at one year.
65 final int maxTimeout = 365 * 24 * 60 * 60 /* One year */;
66
67 ImmutableRangeMap.Builder<Integer, TestTimeout> suggestedTimeoutBuilder =
68 ImmutableRangeMap.builder();
69 ImmutableMap.Builder<TestTimeout, Range<Integer>> timeoutFuzzyRangeBuilder =
70 ImmutableMap.builder();
71
72 int previousMaxSuggested = 0;
73 int previousTimeout = 0;
74
75 Iterator<TestTimeout> timeoutIterator = Arrays.asList(values()).iterator();
76 while (timeoutIterator.hasNext()) {
77 TestTimeout timeout = timeoutIterator.next();
78
79 // Set up time ranges for suggested timeouts and fuzzy timeouts. Fuzzy timeout ranges should
80 // be looser than suggested timeout ranges in order to make sure that after a test size is
81 // adjusted, it's difficult for normal time variance to push it outside the fuzzy timeout
82 // range.
83
84 // This should be exactly the previous max because there should be exactly one suggested
85 // timeout for any given time.
86 final int minSuggested = previousMaxSuggested;
87 // Only suggest timeouts that are less than 75% of the actual timeout (unless there are no
88 // higher timeouts). This should be low enough to prevent suggested times from causing test
89 // timeout flakiness.
90 final int maxSuggested =
91 timeoutIterator.hasNext() ? (int) (timeout.timeout * 0.75) : maxTimeout;
92
93 // Set fuzzy minimum timeout to half the previous timeout. If the test is that fast, it should
94 // be safe to use the shorter timeout.
95 final int minFuzzy = previousTimeout / 2;
96 // Set fuzzy maximum timeout to 90% of the timeout. A test this close to the limit can easily
97 // become timeout flaky.
98 final int maxFuzzy = timeoutIterator.hasNext() ? (int) (timeout.timeout * 0.9) : maxTimeout;
99
100 timeoutFuzzyRangeBuilder.put(timeout, Range.closedOpen(minFuzzy, maxFuzzy));
101
102 suggestedTimeoutBuilder.put(Range.closedOpen(minSuggested, maxSuggested), timeout);
103
104 previousMaxSuggested = maxSuggested;
105 previousTimeout = timeout.timeout;
106 }
107 SUGGESTED_TIMEOUT = suggestedTimeoutBuilder.build();
108 TIMEOUT_FUZZY_RANGE = timeoutFuzzyRangeBuilder.build();
109 }
110
111 private final int timeout;
112
113 private TestTimeout(int timeout) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100114 this.timeout = timeout;
115 }
116
117 /**
118 * Returns the enum associated with a test's timeout or null if the tag is
119 * not lower case or an unknown size.
120 */
121 public static TestTimeout getTestTimeout(String attr) {
122 if (!attr.equals(attr.toLowerCase())) {
123 return null;
124 }
125 try {
126 return TestTimeout.valueOf(attr.toUpperCase());
127 } catch (IllegalArgumentException e) {
128 return null;
129 }
130 }
131
132 @Override
133 public String toString() {
134 return super.toString().toLowerCase();
135 }
136
137 /**
138 * We print to upper case to make the test timeout warnings more readable.
139 */
140 public String prettyPrint() {
141 return super.toString().toUpperCase();
142 }
143
ulfjackdeab0cf2017-08-08 13:08:24 +0200144 @Deprecated // use getTimeout instead
145 public int getTimeoutSeconds() {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100146 return timeout;
147 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100148
ulfjackdeab0cf2017-08-08 13:08:24 +0200149 public Duration getTimeout() {
150 return Duration.ofSeconds(timeout);
151 }
152
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100153 /**
Googler7c177212017-06-16 15:46:55 +0200154 * Returns true iff the given time is not close to the upper bound timeout and is so short that it
155 * should be assigned a different timeout.
156 *
157 * <p>This is used to give suggestions to developers to update their timeouts. If this returns
158 * true, a more reasonable timeout can be selected with {@link #getSuggestedTestTimeout(int)}
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100159 */
Googler7c177212017-06-16 15:46:55 +0200160 public boolean isInRangeFuzzy(int timeInSeconds) {
161 return TIMEOUT_FUZZY_RANGE.get(this).contains(timeInSeconds);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100162 }
163
164 /**
165 * Returns suggested test size for the given time in seconds.
Googler7c177212017-06-16 15:46:55 +0200166 *
167 * <p>Will suggest times that are unlikely to result in timeout flakiness even if the test has a
168 * significant amount of time variance.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100169 */
Googler7c177212017-06-16 15:46:55 +0200170 public static TestTimeout getSuggestedTestTimeout(int timeInSeconds) {
171 return SUGGESTED_TIMEOUT.get(timeInSeconds);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100172 }
173
174 /**
175 * Returns test timeout of the given test target using explicitly specified timeout
176 * or default through to the size label's associated default.
177 */
178 public static TestTimeout getTestTimeout(Rule testTarget) {
179 String attr = NonconfigurableAttributeMapper.of(testTarget).get("timeout", Type.STRING);
180 if (!attr.equals(attr.toLowerCase())) {
181 return null; // attribute values must be lowercase
182 }
183 try {
184 return TestTimeout.valueOf(attr.toUpperCase());
185 } catch (IllegalArgumentException e) {
186 return null;
187 }
188 }
189
190 /**
191 * Converter for the --test_timeout option.
192 */
ulfjackdeab0cf2017-08-08 13:08:24 +0200193 public static class TestTimeoutConverter implements Converter<Map<TestTimeout, Duration>> {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100194 public TestTimeoutConverter() {}
195
196 @Override
ulfjackdeab0cf2017-08-08 13:08:24 +0200197 public Map<TestTimeout, Duration> convert(String input) throws OptionsParsingException {
198 List<Duration> values = new ArrayList<>();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100199 for (String token : Splitter.on(',').limit(6).split(input)) {
200 // Handle the case of "2," which is accepted as legal... Because Splitter.split is lazy,
201 // there's no way of knowing if an empty string is a trailing or an intermediate one,
202 // so we can't fully emulate String.split(String, 0).
203 if (!token.isEmpty() || values.size() > 1) {
204 try {
ulfjackdeab0cf2017-08-08 13:08:24 +0200205 values.add(Duration.ofSeconds(Integer.valueOf(token)));
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100206 } catch (NumberFormatException e) {
207 throw new OptionsParsingException("'" + input + "' is not an int");
208 }
209 }
210 }
ulfjackdeab0cf2017-08-08 13:08:24 +0200211 EnumMap<TestTimeout, Duration> timeouts = Maps.newEnumMap(TestTimeout.class);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100212 if (values.size() == 1) {
213 timeouts.put(SHORT, values.get(0));
214 timeouts.put(MODERATE, values.get(0));
215 timeouts.put(LONG, values.get(0));
216 timeouts.put(ETERNAL, values.get(0));
217 } else if (values.size() == 4) {
218 timeouts.put(SHORT, values.get(0));
219 timeouts.put(MODERATE, values.get(1));
220 timeouts.put(LONG, values.get(2));
221 timeouts.put(ETERNAL, values.get(3));
222 } else {
223 throw new OptionsParsingException("Invalid number of comma-separated entries");
224 }
225 for (TestTimeout label : values()) {
ulfjackdeab0cf2017-08-08 13:08:24 +0200226 if (!timeouts.containsKey(label) || timeouts.get(label).compareTo(Duration.ZERO) <= 0) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100227 timeouts.put(label, label.getTimeout());
228 }
229 }
230 return timeouts;
231 }
232
233 @Override
234 public String getTypeDescription() {
235 return "a single integer or comma-separated list of 4 integers";
236 }
237 }
238
239 /**
240 * Converter for the --test_timeout_filters option.
241 */
242 public static class TestTimeoutFilterConverter extends EnumFilterConverter<TestTimeout> {
243 public TestTimeoutFilterConverter() {
244 super(TestTimeout.class, "test timeout");
245 }
246
247 /**
248 * {@inheritDoc}
249 *
250 * <p>This override is necessary to prevent OptionsData
251 * from throwing a "must be assignable from the converter return type" exception.
252 * OptionsData doesn't recognize the generic type and actual type are the same.
253 */
254 @Override
255 public final Set<TestTimeout> convert(String input) throws OptionsParsingException {
256 return super.convert(input);
257 }
258 }
259}