diff --git a/site/en/docs/user-manual.md b/site/en/docs/user-manual.md
index ec2edc6b7d7a74..fd40216441e193 100644
--- a/site/en/docs/user-manual.md
+++ b/site/en/docs/user-manual.md
@@ -1521,10 +1521,15 @@ or total number of passed tests. Tests that fail all allowed attempts are
considered to be failed.
By default (when this option is not specified, or when it is set to
-default), only a single attempt is allowed for regular tests, and
-3 for test rules with the `flaky` attribute set. You can specify
-an integer value to override the maximum limit of test attempts. Bazel allows
-a maximum of 10 test attempts in order to prevent abuse of the system.
+default), non-flaky targets (`flaky = 0`) run once. Flaky targets run
+`1 + flaky` times. For example, `flaky = 1` runs twice and `flaky = 2`
+runs three times.
+
+When set to an integer, that value is the total number of attempts for
+all matching targets, including non-flaky ones, and overrides the flaky
+attribute. You can specify an integer value globally or per target regex.
+Bazel allows a maximum of 10 test attempts in order to prevent abuse of
+the system.
#### `--runs_per_test={{ "" }}[regex@]number{{ "" }}` {:#runs-per-test}
diff --git a/src/main/java/com/google/devtools/build/docgen/templates/attributes/test/flaky.html b/src/main/java/com/google/devtools/build/docgen/templates/attributes/test/flaky.html
index 0bd8fe4304405d..e3e494878dd09c 100644
--- a/src/main/java/com/google/devtools/build/docgen/templates/attributes/test/flaky.html
+++ b/src/main/java/com/google/devtools/build/docgen/templates/attributes/test/flaky.html
@@ -1,13 +1,17 @@
-
-Marks test as flaky.
+Declares how many additional retry attempts this flaky test should receive
+when --flaky_test_attempts is default. A value of
+0 means the test is not flaky and runs once unless a numeric
+--flaky_test_attempts value overrides all targets. For example,
+with the default flag, flaky = 1 runs the test twice and
+flaky = 2 runs it three times.
-If set, executes the test up to three times, marking it as failed only if it
-fails each time. By default, this attribute is set to False and the test is
-executed only once. Note, that use of this attribute is generally discouraged -
-tests should pass reliably when their assertions are upheld.
+Note that use of this attribute is generally discouraged - tests should pass
+reliably when their assertions are upheld.
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java
index 7069c4b54b1da7..32ffec4701ac1c 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java
@@ -230,8 +230,8 @@ public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env)
.nonconfigurable("policy decision: should be consistent across configurations")
.value(TIMEOUT_DEFAULT))
.add(
- attr("flaky", BOOLEAN)
- .value(false)
+ attr("flaky", Type.FLAKY_TEST_RETRIES)
+ .value(StarlarkInt.of(0))
.taggable()
.nonconfigurable("policy decision: should be consistent across configurations"))
.add(attr("shard_count", INTEGER).value(StarlarkInt.of(-1)))
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java
index 8758db0cb8351f..8014646dde301f 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java
@@ -238,8 +238,8 @@ public static RuleClass getTestBaseRule(RuleDefinitionEnvironment env) {
.nonconfigurable("policy decision: should be consistent across configurations")
.value(TIMEOUT_DEFAULT))
.add(
- attr("flaky", BOOLEAN)
- .value(false)
+ attr("flaky", Type.FLAKY_TEST_RETRIES)
+ .value(StarlarkInt.of(0))
.taggable()
.nonconfigurable("taggable - called in Rule.getRuleTags"))
.add(attr("shard_count", INTEGER).value(StarlarkInt.of(-1)))
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestStrategy.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestStrategy.java
index 63c27226b1c0d8..49c182f4363f0c 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestStrategy.java
@@ -276,40 +276,44 @@ private static void addRunUnderArgs(TestRunnerAction testAction, List ar
}
/**
- * Returns the number of attempts specific test action can be retried.
+ * Returns the number of attempts for the given test action.
*
- * For rules with "flaky = 1" attribute, this method will return 3 unless --flaky_test_attempts
- * option is given and specifies another value.
+ *
When {@code --flaky_test_attempts} resolves to a numeric value for this target, that value
+ * is the total number of attempts for all tests (including {@code flaky = 0}). When the flag is
+ * {@code default}, non-flaky targets run once and flaky targets run {@code 1 + flaky} times.
*/
@VisibleForTesting /* protected */
public int getTestAttempts(TestRunnerAction action) {
- return action.getTestProperties().isFlaky()
- ? getTestAttemptsForFlakyTest(action)
- : getTestAttempts(action, /* defaultTestAttempts= */ 1);
+ return computeTestAttempts(
+ action.getTestProperties().getFlakyRetries(),
+ executionOptions,
+ action.getOwner().getLabel());
}
- private int getTestAttempts(TestRunnerAction action, int defaultTestAttempts) {
- Label testLabel = action.getOwner().getLabel();
- return getTestAttemptsPerLabel(executionOptions, testLabel, defaultTestAttempts);
+ @VisibleForTesting
+ static int computeTestAttempts(int flakyRetries, ExecutionOptions options, Label label) {
+ String flagValue = resolveFlakyTestAttemptsFlagValue(options, label);
+ if (!"default".equals(flagValue)) {
+ return Math.min(MAX_TEST_ATTEMPTS, Integer.parseInt(flagValue));
+ }
+ if (flakyRetries == 0) {
+ return 1;
+ }
+ return Math.min(MAX_TEST_ATTEMPTS, 1 + flakyRetries);
}
- public int getTestAttemptsForFlakyTest(TestRunnerAction action) {
- return getTestAttempts(action, /* defaultTestAttempts= */ 3);
- }
+ private static final int MAX_TEST_ATTEMPTS = 10;
- private static int getTestAttemptsPerLabel(
- ExecutionOptions options, Label label, int defaultTestAttempts) {
- // Check from the last provided, so that the last option provided takes precedence.
+ private static String resolveFlakyTestAttemptsFlagValue(ExecutionOptions options, Label label) {
+ if (options.testAttempts == null || options.testAttempts.isEmpty()) {
+ return "default";
+ }
for (PerLabelOptions perLabelAttempts : Lists.reverse(options.testAttempts)) {
if (perLabelAttempts.isIncluded(label)) {
- String attempts = Iterables.getOnlyElement(perLabelAttempts.getOptions());
- if ("default".equals(attempts)) {
- return defaultTestAttempts;
- }
- return Integer.parseInt(attempts);
+ return Iterables.getOnlyElement(perLabelAttempts.getOptions());
}
}
- return defaultTestAttempts;
+ return "default";
}
/**
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java
index 68ab87a4c440db..28b47b4f433120 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java
@@ -68,7 +68,7 @@ private static ResourceSet getResourceSetFromSize(TestSize size) {
private final TestTimeout timeout;
private final List tags;
private final boolean isRemotable;
- private final boolean isFlaky;
+ private final int flakyRetries;
private final boolean isExternal;
private final String language;
private final ImmutableMap executionInfo;
@@ -87,7 +87,8 @@ private static ResourceSet getResourceSetFromSize(TestSize size) {
tags = ruleContext.attributes().get("tags", Types.STRING_LIST);
// We need to use method on ruleConfiguredTarget to perform validation.
- isFlaky = ruleContext.attributes().get("flaky", Type.BOOLEAN);
+ flakyRetries =
+ ruleContext.attributes().get("flaky", Type.FLAKY_TEST_RETRIES).toIntUnchecked();
isExternal = TargetUtils.isExternalTestRule(rule);
Map executionInfo = Maps.newLinkedHashMap();
@@ -148,8 +149,12 @@ public boolean isRemotable() {
return isRemotable;
}
+ public int getFlakyRetries() {
+ return flakyRetries;
+ }
+
public boolean isFlaky() {
- return isFlaky;
+ return flakyRetries > 0;
}
public boolean isExternal() {
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
index f32d1c1504bacb..61aad64dbfad42 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
@@ -222,16 +222,16 @@ public boolean shouldMaterializeParamFiles() {
"Each test will be retried up to the specified number of times in case of any test"
+ " failure. Tests that required more than one attempt to pass are marked as 'FLAKY'"
+ " in the test summary. Normally the value specified is just an integer or the"
- + " string 'default'. If an integer, then all tests will be run up to N times. If"
- + " 'default', then only a single test attempt will be made for regular tests and"
- + " three for tests marked explicitly as flaky by their rule (flaky=1 attribute)."
- + " Alternate syntax: regex_filter@flaky_test_attempts. Where flaky_test_attempts is"
- + " as above and regex_filter stands for a list of include and exclude regular"
- + " expression patterns (Also see --runs_per_test). Example:"
- + " --flaky_test_attempts=//foo/.*,-//foo/bar/.*@3 deflakes all tests in //foo/"
- + " except those under foo/bar three times. This option can be passed multiple"
- + " times. The most recently passed argument that matches takes precedence. If"
- + " nothing matches, behavior is as if 'default' above.")
+ + " string 'default'. If an integer, then that value is the total number of attempts"
+ + " for all matching targets, overriding the flaky attribute. If 'default', non-flaky"
+ + " targets (flaky = 0) run once and flaky targets run 1 + flaky times. Alternate"
+ + " syntax: regex_filter@flaky_test_attempts. Where flaky_test_attempts is as above"
+ + " and regex_filter stands for a list of include and exclude regular expression"
+ + " patterns (Also see --runs_per_test). Example:"
+ + " --flaky_test_attempts=//foo/.*,-//foo/bar/.*@3 runs all tests in //foo/ except"
+ + " foo/bar up to three times. This option can be passed multiple times. The most"
+ + " recently passed argument that matches takes precedence. If nothing matches,"
+ + " behavior is as if 'default' above.")
public List testAttempts;
@Option(
diff --git a/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java b/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java
index e8d1786c418fc9..b2bf475f4b60c9 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/ProtoUtils.java
@@ -30,6 +30,7 @@
import static com.google.devtools.build.lib.packages.BuildType.OUTPUT_LIST;
import static com.google.devtools.build.lib.packages.BuildType.TRISTATE;
import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
+import static com.google.devtools.build.lib.packages.Type.FLAKY_TEST_RETRIES;
import static com.google.devtools.build.lib.packages.Type.INTEGER;
import static com.google.devtools.build.lib.packages.Type.STRING;
import static com.google.devtools.build.lib.packages.Type.STRING_NO_INTERN;
@@ -48,6 +49,7 @@ public class ProtoUtils {
private static final ImmutableMap, Discriminator> TYPE_MAP =
new ImmutableMap.Builder, Discriminator>()
.put(INTEGER, Discriminator.INTEGER)
+ .put(FLAKY_TEST_RETRIES, Discriminator.INTEGER)
.put(DISTRIBUTIONS, Discriminator.DISTRIBUTION_SET)
.put(LABEL, Discriminator.LABEL)
// NODEP_LABEL attributes are not really strings. This is implemented
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java b/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
index abb5ccf0bac3f2..e7ad45c21f23a5 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
@@ -684,7 +684,7 @@ public String toString() {
attr("tags", Types.STRING_LIST).build(),
attr("size", Type.STRING).build(),
attr("timeout", Type.STRING).build(),
- attr("flaky", Type.BOOLEAN).build(),
+ attr("flaky", Type.FLAKY_TEST_RETRIES).build(),
attr("shard_count", Type.INTEGER).build(),
attr("local", Type.BOOLEAN).build());
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Type.java b/src/main/java/com/google/devtools/build/lib/packages/Type.java
index 36e496f2490689..c1e408b2983da5 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Type.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Type.java
@@ -260,6 +260,13 @@ public Set toTagSet(Object value, String name) {
/** The type of a Starlark integer in the signed 32-bit range. */
@SerializationConstant public static final Type INTEGER = new IntegerType();
+ /**
+ * The type of the {@code flaky} test attribute: a non-negative int counting additional retry
+ * attempts, composed with {@code --flaky_test_attempts}. Also accepts legacy boolean values.
+ */
+ @SerializationConstant
+ public static final Type FLAKY_TEST_RETRIES = new FlakyTestRetriesType();
+
/** The type of a string which interns the instance with String#intern. */
@SerializationConstant
public static final Type STRING = new StringType(/* internString= */ true);
@@ -386,6 +393,60 @@ public StarlarkInt concat(Iterable elements) {
}
}
+ private static final class FlakyTestRetriesType extends Type {
+ @Override
+ public StarlarkInt cast(Object value) {
+ return (StarlarkInt) value;
+ }
+
+ @Override
+ public StarlarkInt getDefaultValue() {
+ return StarlarkInt.of(0);
+ }
+
+ @Override
+ public void visitLabels(LabelVisitor visitor, StarlarkInt value, @Nullable Attribute context) {}
+
+ @Override
+ public String toString() {
+ return "int";
+ }
+
+ @Override
+ public StarlarkInt convert(Object x, Object what, LabelConverter labelConverter)
+ throws ConversionException {
+ if (x instanceof Boolean) {
+ return StarlarkInt.of((Boolean) x ? 1 : 0);
+ }
+ if (x instanceof StarlarkInt) {
+ StarlarkInt i = (StarlarkInt) x;
+ try {
+ if (i.toIntUnchecked() < 0) {
+ throw new ConversionException("non-negative int", x, what);
+ }
+ } catch (
+ @SuppressWarnings("UnusedException")
+ IllegalArgumentException ex) {
+ String prefix = what != null ? ("for " + what + ", ") : "";
+ throw new ConversionException(
+ String.format("%sgot %s, want value in signed 32-bit range", prefix, i));
+ }
+ return i;
+ }
+ throw new ConversionException(this, x, what);
+ }
+
+ @Override
+ public Set toTagSet(Object value, String name) {
+ if (value == null) {
+ String msg = "Illegal tag conversion from null on Attribute " + name + ".";
+ throw new IllegalStateException(msg);
+ }
+ int retries = ((StarlarkInt) value).toIntUnchecked();
+ return retries == 0 ? ImmutableSet.of("noflaky") : ImmutableSet.of("flaky");
+ }
+ }
+
private static final class BooleanType extends Type {
@Override
public Boolean cast(Object value) {
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/BUILD b/src/test/java/com/google/devtools/build/lib/analysis/BUILD
index 40d31a6c4eeea7..cd852b8ebb8c24 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/analysis/BUILD
@@ -79,6 +79,7 @@ java_library(
"//src/main/java/com/google/devtools/build/lib/analysis:build_view",
"//src/main/java/com/google/devtools/build/lib/analysis:config/build_configuration",
"//src/main/java/com/google/devtools/build/lib/analysis:config/build_options",
+ "//src/main/java/com/google/devtools/build/lib/analysis:config/per_label_options",
"//src/main/java/com/google/devtools/build/lib/analysis:config/core_options",
"//src/main/java/com/google/devtools/build/lib/analysis:config/execution_transition_factory",
"//src/main/java/com/google/devtools/build/lib/analysis:config/fragment",
@@ -126,6 +127,7 @@ java_library(
"//src/main/java/com/google/devtools/build/lib/collect/nestedset",
"//src/main/java/com/google/devtools/build/lib/events",
"//src/main/java/com/google/devtools/build/lib/exec:bin_tools",
+ "//src/main/java/com/google/devtools/build/lib/exec:execution_options",
"//src/main/java/com/google/devtools/build/lib/packages",
"//src/main/java/com/google/devtools/build/lib/packages:configured_attribute_mapper",
"//src/main/java/com/google/devtools/build/lib/packages:exec_group",
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/test/TestActionBuilderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/test/TestActionBuilderTest.java
index f93c994943e26a..29c30b31366070 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/test/TestActionBuilderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/test/TestActionBuilderTest.java
@@ -267,28 +267,52 @@ public void testFlakyAttributeValidation() throws Exception {
srcs = ["a.sh"],
flaky = 1,
)
+
+ foo_test(
+ name = "flaky_true_test",
+ srcs = ["a.sh"],
+ flaky = True,
+ )
+
+ foo_test(
+ name = "very_flaky_test",
+ srcs = ["a.sh"],
+ flaky = 2,
+ )
""");
Artifact testStatus = Iterables.getOnlyElement(getTestStatusArtifacts("//flaky:good_test"));
assertThat(testStatus).isNotNull();
TestRunnerAction action = (TestRunnerAction) getGeneratingAction(testStatus);
+ assertThat(action.getTestProperties().getFlakyRetries()).isEqualTo(0);
assertThat(action.getTestProperties().isFlaky()).isFalse();
testStatus = Iterables.getOnlyElement(getTestStatusArtifacts("//flaky:flaky_test"));
assertThat(testStatus).isNotNull();
action = (TestRunnerAction) getGeneratingAction(testStatus);
+ assertThat(action.getTestProperties().getFlakyRetries()).isEqualTo(1);
assertThat(action.getTestProperties().isFlaky()).isTrue();
+
+ testStatus = Iterables.getOnlyElement(getTestStatusArtifacts("//flaky:flaky_true_test"));
+ assertThat(testStatus).isNotNull();
+ action = (TestRunnerAction) getGeneratingAction(testStatus);
+ assertThat(action.getTestProperties().getFlakyRetries()).isEqualTo(1);
+
+ testStatus = Iterables.getOnlyElement(getTestStatusArtifacts("//flaky:very_flaky_test"));
+ assertThat(testStatus).isNotNull();
+ action = (TestRunnerAction) getGeneratingAction(testStatus);
+ assertThat(action.getTestProperties().getFlakyRetries()).isEqualTo(2);
}
@Test
- public void testIllegalBooleanFlakySetting() throws Exception {
+ public void testIllegalNegativeFlakySetting() throws Exception {
checkError(
"flaky",
"bad_test",
- "expected one of [False, True, 0, 1]",
+ "expected non-negative int",
"load('//test_defs:foo_test.bzl', 'foo_test')",
"foo_test(name = 'bad_test',",
" srcs = ['a.sh'],",
- " flaky = 2)");
+ " flaky = -1)");
}
@Test
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/test/TestStrategyTest.java b/src/test/java/com/google/devtools/build/lib/analysis/test/TestStrategyTest.java
new file mode 100644
index 00000000000000..df93c0b31d8fca
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/analysis/test/TestStrategyTest.java
@@ -0,0 +1,75 @@
+// Copyright 2026 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.analysis.test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.config.PerLabelOptions;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.exec.ExecutionOptions;
+import com.google.devtools.build.lib.util.RegexFilter;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class TestStrategyTest {
+
+ private static final Label LABEL = Label.parseCanonicalUnchecked("//pkg:test");
+
+ @Test
+ public void numericOverrideAppliesToDeterministicTargets() {
+ ExecutionOptions options = optionsWithCatchAllAttempts("3");
+ assertThat(TestStrategy.computeTestAttempts(0, options, LABEL)).isEqualTo(3);
+ }
+
+ @Test
+ public void numericOverrideIgnoresFlakyAttribute() {
+ ExecutionOptions options = optionsWithCatchAllAttempts("3");
+ assertThat(TestStrategy.computeTestAttempts(1, options, LABEL)).isEqualTo(3);
+ assertThat(TestStrategy.computeTestAttempts(2, options, LABEL)).isEqualTo(3);
+ }
+
+ @Test
+ public void defaultFlagUsesBaselinePlusFlakyAttribute() {
+ ExecutionOptions options = optionsWithCatchAllAttempts("default");
+ assertThat(TestStrategy.computeTestAttempts(0, options, LABEL)).isEqualTo(1);
+ assertThat(TestStrategy.computeTestAttempts(1, options, LABEL)).isEqualTo(2);
+ assertThat(TestStrategy.computeTestAttempts(2, options, LABEL)).isEqualTo(3);
+ }
+
+ @Test
+ public void unsetFlagBehavesLikeDefault() {
+ ExecutionOptions options = new ExecutionOptions();
+ assertThat(TestStrategy.computeTestAttempts(0, options, LABEL)).isEqualTo(1);
+ assertThat(TestStrategy.computeTestAttempts(1, options, LABEL)).isEqualTo(2);
+ }
+
+ @Test
+ public void attemptsAreCappedAtTen() {
+ ExecutionOptions options = optionsWithCatchAllAttempts("12");
+ assertThat(TestStrategy.computeTestAttempts(0, options, LABEL)).isEqualTo(10);
+ }
+
+ private static ExecutionOptions optionsWithCatchAllAttempts(String attempts) {
+ ExecutionOptions options = new ExecutionOptions();
+ options.testAttempts =
+ ImmutableList.of(
+ new PerLabelOptions(
+ new RegexFilter(ImmutableList.of(".*"), ImmutableList.of()),
+ ImmutableList.of(attempts)));
+ return options;
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/PackageTest.java b/src/test/java/com/google/devtools/build/lib/packages/PackageTest.java
index 8341256a52e9fc..16fa85ca78460e 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/PackageTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/PackageTest.java
@@ -50,7 +50,7 @@ public class PackageTest {
Attribute.attr("tags", Types.STRING_LIST).nonconfigurable("tags aren't").build())
.addAttribute(Attribute.attr("size", Type.STRING).nonconfigurable("size isn't").build())
.addAttribute(Attribute.attr("timeout", Type.STRING).build())
- .addAttribute(Attribute.attr("flaky", Type.BOOLEAN).build())
+ .addAttribute(Attribute.attr("flaky", Type.FLAKY_TEST_RETRIES).build())
.addAttribute(Attribute.attr("shard_count", Type.INTEGER).build())
.addAttribute(Attribute.attr("local", Type.BOOLEAN).build())
.setConfiguredTargetFunction(mock(StarlarkCallable.class))
diff --git a/src/test/java/com/google/devtools/build/lib/packages/RuleClassBuilderTest.java b/src/test/java/com/google/devtools/build/lib/packages/RuleClassBuilderTest.java
index e973a974dc6f5f..8eed1e19fd7b69 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/RuleClassBuilderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/RuleClassBuilderTest.java
@@ -85,7 +85,7 @@ public void testRuleClassBuilderTestIsBinary() throws Exception {
.add(attr("tags", STRING_LIST))
.add(attr("size", STRING).value("medium"))
.add(attr("timeout", STRING))
- .add(attr("flaky", BOOLEAN).value(false))
+ .add(attr("flaky", Type.FLAKY_TEST_RETRIES).value(StarlarkInt.of(0)))
.add(attr("shard_count", INTEGER).value(StarlarkInt.of(-1)))
.add(attr("local", BOOLEAN))
.build();
diff --git a/src/test/java/com/google/devtools/build/lib/packages/RuleFactoryTest.java b/src/test/java/com/google/devtools/build/lib/packages/RuleFactoryTest.java
index 8bbc73d6828036..6664509d29f7e7 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/RuleFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/RuleFactoryTest.java
@@ -262,7 +262,7 @@ public void testTestRules() throws Exception {
if (TargetUtils.isTestRule(rule)) {
assertAttr(ruleClass, "tags", Types.STRING_LIST);
assertAttr(ruleClass, "size", Type.STRING);
- assertAttr(ruleClass, "flaky", Type.BOOLEAN);
+ assertAttr(ruleClass, "flaky", Type.FLAKY_TEST_RETRIES);
assertAttr(ruleClass, "shard_count", Type.INTEGER);
assertAttr(ruleClass, "local", Type.BOOLEAN);
}
diff --git a/src/test/java/com/google/devtools/build/lib/packages/TypeTest.java b/src/test/java/com/google/devtools/build/lib/packages/TypeTest.java
index d52cbd480279bf..55bf4f908cfb4a 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/TypeTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/TypeTest.java
@@ -122,6 +122,27 @@ public void testBoolean() throws Exception {
assertThat(collectLabels(Type.BOOLEAN, myTrue)).isEmpty();
}
+ @Test
+ public void testFlakyTestRetries() throws Exception {
+ assertThat(Type.FLAKY_TEST_RETRIES.convert(StarlarkInt.of(0), null)).isEqualTo(StarlarkInt.of(0));
+ assertThat(Type.FLAKY_TEST_RETRIES.convert(StarlarkInt.of(2), null)).isEqualTo(StarlarkInt.of(2));
+ assertThat(Type.FLAKY_TEST_RETRIES.convert(true, null)).isEqualTo(StarlarkInt.of(1));
+ assertThat(Type.FLAKY_TEST_RETRIES.convert(false, null)).isEqualTo(StarlarkInt.of(0));
+ assertThat(Type.FLAKY_TEST_RETRIES.toTagSet(StarlarkInt.of(0), "flaky"))
+ .containsExactly("noflaky");
+ assertThat(Type.FLAKY_TEST_RETRIES.toTagSet(StarlarkInt.of(2), "flaky"))
+ .containsExactly("flaky");
+ }
+
+ @Test
+ public void testFlakyTestRetriesRejectsNegativeValues() throws Exception {
+ Type.ConversionException e =
+ assertThrows(
+ Type.ConversionException.class,
+ () -> Type.FLAKY_TEST_RETRIES.convert(StarlarkInt.of(-1), null));
+ assertThat(e).hasMessageThat().isEqualTo("expected non-negative int, but got -1 (int)");
+ }
+
@Test
public void testNonBoolean() throws Exception {
Type.ConversionException e =