| // Copyright 2020 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.buildtool; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import static org.junit.Assert.fail; | |
| import com.google.devtools.build.lib.actions.BuildFailedException; | |
| import com.google.devtools.build.lib.analysis.ViewCreationFailedException; | |
| import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; | |
| import com.google.devtools.build.lib.skyframe.BuildResultListener; | |
| import com.google.devtools.build.lib.util.io.OutErr; | |
| import com.google.devtools.build.lib.util.io.RecordingOutErr; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests correctness of the build summary output produced by BuildTool.showBuildResult() method. */ | |
| public abstract class BuildResultTestCase extends BuildIntegrationTestCase { | |
| private RecordingOutErr recOutErr = new RecordingOutErr(); | |
| private static final String GENRULE_ERROR = "Exit 42"; | |
| /** | |
| * Hook for subclasses to define which executor we use. (The two concrete | |
| * subclasses, {Sequential,Parallel}BuildResultTest, are at the bottom of this | |
| * source file.) | |
| */ | |
| protected abstract int numJobs(); | |
| @Before | |
| public final void addOptions() throws Exception { | |
| this.outErr = recOutErr; | |
| addOptions("--jobs", "" + numJobs(), "--show_result", "1000"); | |
| } | |
| private void build(boolean expectFailure, String expectedError, String... targets) | |
| throws Exception { | |
| try { | |
| buildTarget(targets); | |
| if (expectFailure) { | |
| fail(); | |
| } | |
| } catch (BuildFailedException e) { | |
| if (e.getMessage() != null) { | |
| assertThat(e).hasMessageThat().containsMatch(expectedError); | |
| } | |
| } finally { | |
| OutErr.SYSTEM_OUT_ERR.printErr(recOutErr.errAsLatin1()); | |
| OutErr.SYSTEM_OUT_ERR.printOut(recOutErr.outAsLatin1()); | |
| } | |
| } | |
| @Test | |
| public void testKeepGoingResult() throws Exception { | |
| write("test/BUILD", | |
| "genrule(name='A', srcs=['A1','A2'], outs=['A.out']," + | |
| " cmd='/bin/cp test/in $(location A.out)')\n" + | |
| "genrule(name='A1', srcs=[], outs=['A1.out']," + | |
| " cmd='/bin/cp test/in $(location A1.out)')\n" + | |
| "genrule(name='A2', srcs=[], outs=['A2.out']," + | |
| " cmd='exit 42')\n" + | |
| "genrule(name='B', srcs=[], outs=['B.out']," + | |
| " cmd='/bin/cp test/in $(location B.out)')\n"); | |
| write("test/in", "(input)"); | |
| addOptions("--keep_going"); | |
| build(true, GENRULE_ERROR, "//test:B", "//test:A"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //test:A failed to build\n"); | |
| assertThat(stderr).containsMatch("Target //test:B up-to-date:\n" + " .*/test/B\\.out\n"); | |
| } | |
| @Test | |
| public void testWithOnlyInternalOutputGroups() throws Exception { | |
| write( | |
| "foo/BUILD", | |
| """ | |
| load("//test_defs:foo_binary.bzl", "foo_binary") | |
| genrule( | |
| name = "gen", | |
| outs = ["gen.out"], | |
| cmd = "touch $@", | |
| ) | |
| foo_binary( | |
| name = "foo", | |
| srcs = ["foo.sh"], | |
| data = [":gen"], | |
| ) | |
| """); | |
| write("foo/foo.sh", "echo foo").setExecutable(true); | |
| // Subtracting the default output group leaves the internal output group carrying the runfiles, | |
| // which are built but never shown. | |
| addOptions("--output_groups=-default"); | |
| build(false, "no-error", "//foo"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr) | |
| .contains("Target //foo:foo up-to-date (nothing to build except internal output groups)\n"); | |
| } | |
| /** | |
| * Writes {@code //groups:t}, whose default output is {@code t_default.txt} and whose {@code | |
| * custom} output group contains {@code t_custom.txt} and {@code t_shared.txt}. {@code | |
| * t_shared.txt} is also in the {@code shared} output group. | |
| */ | |
| private void writeOutputGroupsTarget() throws Exception { | |
| write( | |
| "groups/defs.bzl", | |
| """ | |
| def _impl(ctx): | |
| default = ctx.actions.declare_file(ctx.label.name + "_default.txt") | |
| custom = ctx.actions.declare_file(ctx.label.name + "_custom.txt") | |
| shared = ctx.actions.declare_file(ctx.label.name + "_shared.txt") | |
| for f in [default, custom, shared]: | |
| ctx.actions.write(f, f.basename) | |
| return [ | |
| DefaultInfo(files = depset([default])), | |
| OutputGroupInfo( | |
| custom = depset([custom, shared]), | |
| shared = depset([shared]), | |
| ), | |
| ] | |
| groups_rule = rule(implementation = _impl) | |
| """); | |
| write( | |
| "groups/BUILD", | |
| """ | |
| load(":defs.bzl", "groups_rule") | |
| groups_rule(name = "t") | |
| """); | |
| } | |
| @Test | |
| public void testHideOutputGroupResults() throws Exception { | |
| writeOutputGroupsTarget(); | |
| addOptions("--output_groups=+custom", "--hide_output_group_results=custom"); | |
| build(false, "no-error", "//groups:t"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr) | |
| .containsMatch("Target //groups:t up-to-date:\n .*/groups/t_default\\.txt\n"); | |
| assertThat(stderr).doesNotContain("t_custom.txt"); | |
| assertThat(stderr).doesNotContain("t_shared.txt"); | |
| } | |
| @Test | |
| public void testHideOutputGroupResults_defaultOutputGroup() throws Exception { | |
| writeOutputGroupsTarget(); | |
| addOptions("--output_groups=+custom", "--hide_output_group_results=default"); | |
| build(false, "no-error", "//groups:t"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).doesNotContain("t_default.txt"); | |
| assertThat(stderr).contains("/groups/t_custom.txt\n"); | |
| assertThat(stderr).contains("/groups/t_shared.txt\n"); | |
| } | |
| @Test | |
| public void testHideOutputGroupResults_artifactInVisibleGroupStillShown() throws Exception { | |
| writeOutputGroupsTarget(); | |
| addOptions("--output_groups=+custom,+shared", "--hide_output_group_results=custom"); | |
| build(false, "no-error", "//groups:t"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("/groups/t_default.txt\n"); | |
| assertThat(stderr).contains("/groups/t_shared.txt\n"); | |
| assertThat(stderr).doesNotContain("t_custom.txt"); | |
| } | |
| @Test | |
| public void testHideOutputGroupResults_suppressesUnknownOutputGroupWarning() throws Exception { | |
| writeOutputGroupsTarget(); | |
| addOptions( | |
| "--output_groups=+hidden_missing,+shown_missing", | |
| "--hide_output_group_results=hidden_missing"); | |
| build(false, "no-error", "//groups:t"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).doesNotContain("Output group 'hidden_missing' was requested"); | |
| assertThat(stderr).contains("Output group 'shown_missing' was requested"); | |
| } | |
| @Test | |
| public void testHideOutputGroupResults_doesNotSuppressUnknownOutputGroupError() throws Exception { | |
| writeOutputGroupsTarget(); | |
| addOptions( | |
| "--output_groups=+hidden_missing", | |
| "--hide_output_group_results=hidden_missing", | |
| "--incompatible_fail_on_unknown_output_groups"); | |
| ViewCreationFailedException e = | |
| assertThrows(ViewCreationFailedException.class, () -> buildTarget("//groups:t")); | |
| assertThat(e).hasMessageThat().contains("Output group 'hidden_missing' was requested"); | |
| } | |
| @Test | |
| public void testNoKeepGoingResult() throws Exception { | |
| write("test/BUILD", | |
| "genrule(name='A', srcs=['A2'], outs=['A.out']," + | |
| " cmd='/bin/cp test/in $(location A.out)')\n" + | |
| "genrule(name='A2', srcs=[], outs=['A2.out']," + | |
| " cmd='exit 42')\n" + | |
| "genrule(name='B', srcs=[], outs=['B.out']," + | |
| " cmd='/bin/cp test/in $(location B.out)')\n"); | |
| write("test/in", "(input)"); | |
| build(true, GENRULE_ERROR, "//test:A", "//test:B"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).containsMatch("Target //test:A failed to build\n"); | |
| } | |
| /** | |
| * This is a regression test for bug #823077, in which presence of artifacts | |
| * with null generating action caused targets always to be reported as failed. | |
| */ | |
| @Test | |
| public void testKeepGoingResultWithNullActions() throws Exception { | |
| write("foo/BUILD", | |
| "sh_test(name='test_bar', srcs=['test_bar.sh'])\n" + | |
| "sh_test(name='test_foo', srcs=['test_foo.sh'])\n"); | |
| write("foo/test_bar.sh", "echo bar is fine").setExecutable(true); | |
| addOptions("--keep_going"); | |
| build(true, "no-error", "//foo:test_foo", "//foo:test_bar"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //foo:test_bar up-to-date:\n"); | |
| assertThat(stderr).containsMatch("\n .*/foo/test_bar\n"); | |
| assertThat(stderr).doesNotContainMatch("\n .*/foo/test_bar\\.sh\n"); | |
| assertThat(stderr).contains("Target //foo:test_foo failed to build\n"); | |
| } | |
| /** | |
| * This is a regression test for bug #1044470, in which targets with failed data dependencies | |
| * were still being reported as "up-to-date". | |
| */ | |
| @Test | |
| public void testWithMissingData() throws Exception { | |
| write( | |
| "needsdata/BUILD", | |
| """ | |
| load("@rules_cc//cc:cc_library.bzl", "cc_library") | |
| cc_library( | |
| name = "needsdata", | |
| data = [":data_lib"], | |
| ) | |
| cc_library( | |
| name = "data_lib", | |
| data = [":does_not_exist"], | |
| ) | |
| """); | |
| // TODO(bazel-team): figure out why error message is non-deterministic with Skyframe full. | |
| // LOADING_AND_ANALYSIS loading_and_analysis cleanup. | |
| build(true, "input", "//needsdata"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).doesNotContain("Target //needsdata:needsdata up-to-date:\n"); | |
| assertThat(stderr).contains("Target //needsdata:needsdata failed to build\n"); | |
| assertThat(stderr) | |
| .doesNotContain("Use --verbose_failures to see the command lines of failed build steps.\n"); | |
| } | |
| /** | |
| * This is a regression test for bug #987608, in which the temp artifacts (.ii and .s) | |
| * were not being reported as having been created. | |
| */ | |
| @Test | |
| public void testWithSaveTemps() throws Exception { | |
| write( | |
| "my_clib/BUILD", | |
| "load('@rules_cc//cc:cc_library.bzl', 'cc_library')", | |
| "cc_library(name='my_clib', srcs=['myclib.cc'])\n"); | |
| write("my_clib/myclib.cc", | |
| "void f() {}"); | |
| addOptions("--save_temps"); | |
| build(false, "no-error", "//my_clib"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //my_clib:my_clib up-to-date:\n"); | |
| assertThat(stderr).contains("blaze-bin/my_clib/_objs/my_clib/myclib.pic.s\n"); | |
| assertThat(stderr).contains("blaze-bin/my_clib/_objs/my_clib/myclib.pic.ii\n"); | |
| } | |
| /** | |
| * Test that changing the symlink prefix builds into another directory. | |
| */ | |
| @Test | |
| public void testSymlinkPrefix() throws Exception { | |
| write( | |
| "my_clib/BUILD", | |
| "load('@rules_cc//cc:cc_library.bzl', 'cc_library')", | |
| "cc_library(name='my_clib', srcs=['myclib.cc'])\n"); | |
| write("my_clib/myclib.cc", | |
| "void f() {}"); | |
| addOptions("--symlink_prefix=myblaze-"); | |
| build(false, "no-error", "//my_clib"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //my_clib:my_clib up-to-date:\n"); | |
| assertThat(stderr).contains("myblaze-bin/my_clib/libmy_clib.so\n"); | |
| assertThat(stderr).contains("myblaze-bin/my_clib/libmy_clib.a\n"); | |
| } | |
| /** | |
| * This is a regression test for bug #1013874, in which the temp artifacts (.ii and .s) | |
| * were not being reported as having been created within a failing target. | |
| * It's possible that one or more temps were successfully created, even if the | |
| * compilation failed. | |
| */ | |
| @Test | |
| public void testFailedTargetWithSaveTemps() throws Exception { | |
| write( | |
| "bad_clib/BUILD", | |
| "load('@rules_cc//cc:cc_library.bzl', 'cc_library')", | |
| "cc_library(name='bad_clib', srcs=['badlib.cc'])\n"); | |
| // trigger a warning to make the build fail: | |
| // "control reaches end of non-void function [-Werror,-Wreturn-type]" | |
| write("bad_clib/badlib.cc", | |
| "int f() { }"); | |
| // We need to set --keep_going so that the temps get built even though the compilation fails. | |
| addOptions("--save_temps", "--keep_going"); | |
| build(true, "compilation of rule '//bad_clib:bad_clib' failed", "//bad_clib"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //bad_clib:bad_clib failed to build"); | |
| assertThat(stderr).contains("See temp at blaze-bin/bad_clib/_objs/bad_clib/badlib.pic.ii\n"); | |
| } | |
| @Test | |
| public void testFailedDependencyActionAttributionAndShowResult() throws Exception { | |
| write( | |
| "test/BUILD", | |
| "genrule(name='top1', srcs=['dep'], outs=['top1.out']," | |
| + " cmd='/bin/cp test/in $(location top1.out)')\n" | |
| + "genrule(name='top2', srcs=['dep'], outs=['top2.out']," | |
| + " cmd='/bin/cp test/in $(location top2.out)')\n" | |
| + "genrule(name='dep', srcs=[], outs=['dep.out']," | |
| + " cmd='exit 42')\n"); | |
| write("test/in", "(input)"); | |
| addOptions("--keep_going", "--show_result=2"); | |
| build(true, GENRULE_ERROR, "//test:top1", "//test:top2"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //test:top1 failed to build\n"); | |
| assertThat(stderr).contains("Target //test:top2 failed to build\n"); | |
| assertThat(stderr).contains(" due to action in //test:dep\n"); | |
| } | |
| @Test | |
| public void testFailedTargetsSuppressedWhenExceedingShowResult() throws Exception { | |
| write( | |
| "test/BUILD", | |
| "genrule(name='top1', srcs=['dep'], outs=['top1.out']," | |
| + " cmd='/bin/cp test/in $(location top1.out)')\n" | |
| + "genrule(name='top2', srcs=['dep'], outs=['top2.out']," | |
| + " cmd='/bin/cp test/in $(location top2.out)')\n" | |
| + "genrule(name='dep', srcs=[], outs=['dep.out']," | |
| + " cmd='exit 42')\n"); | |
| write("test/in", "(input)"); | |
| addOptions("--keep_going", "--show_result=1"); | |
| build(true, GENRULE_ERROR, "//test:top1", "//test:top2"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).doesNotContain("Target //test:top1 failed to build\n"); | |
| assertThat(stderr).doesNotContain("Target //test:top2 failed to build\n"); | |
| assertThat(stderr) | |
| .contains("Use --verbose_failures to see the command lines of failed build steps.\n"); | |
| } | |
| @Test | |
| public void testNoKeepGoingUnbuiltTargetsExcludedFromFailedBudget() throws Exception { | |
| write( | |
| "test/defs.bzl", | |
| "def _fail_rule_impl(ctx):\n" | |
| + " out = ctx.actions.declare_file(ctx.label.name + '.out')\n" | |
| + " ctx.actions.run_shell(outputs = [out], inputs = [], tools = [], command = 'exit" | |
| + " 42')\n" | |
| + " return [DefaultInfo(files = depset([out]))]\n" | |
| + "fail_rule = rule(implementation = _fail_rule_impl)\n" | |
| + "def _pass_rule_impl(ctx):\n" | |
| + " out = ctx.actions.declare_file(ctx.label.name + '.out')\n" | |
| + " ctx.actions.run_shell(outputs = [out], inputs = ctx.files.srcs, tools = []," | |
| + " command = 'touch ' + out.path)\n" | |
| + " return [DefaultInfo(files = depset([out]))]\n" | |
| + "pass_rule = rule(implementation = _pass_rule_impl, attrs = {'srcs':" | |
| + " attr.label_list(allow_files = True)})\n"); | |
| write( | |
| "test/BUILD", | |
| "load(':defs.bzl', 'fail_rule', 'pass_rule')\n" | |
| + "pass_rule(name='top1', srcs=[':dep'])\n" | |
| + "fail_rule(name='dep')\n" | |
| + "genrule(name='unbuilt', srcs=['u1'], outs=['unbuilt.out']," | |
| + " cmd='/bin/cp $(location u1) $(location unbuilt.out)')\n" | |
| + "genrule(name='u1', srcs=['u2'], outs=['u1.out']," | |
| + " cmd='/bin/cp $(location u2) $(location u1.out)')\n" | |
| + "genrule(name='u2', srcs=['u3'], outs=['u2.out']," | |
| + " cmd='/bin/cp $(location u3) $(location u2.out)')\n" | |
| + "genrule(name='u3', srcs=['in'], outs=['u3.out']," | |
| + " cmd='/bin/cp $(location in) $(location u3.out)')\n"); | |
| write("test/in", "(input)"); | |
| addOptions("--nokeep_going", "--show_result=1"); | |
| build(true, GENRULE_ERROR, "//test:top1", "//test:unbuilt"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //test:top1 failed to build\n"); | |
| assertThat(stderr).contains(" due to action in //test:dep\n"); | |
| assertThat(stderr).doesNotContain("Target //test:unbuilt failed to build\n"); | |
| assertThat(stderr).doesNotContain("Target //test:unbuilt up-to-date"); | |
| } | |
| @Test | |
| public void testShowResultZeroSuppressesFailedTargets() throws Exception { | |
| write("test/BUILD", "genrule(name='fail', srcs=[], outs=['fail.out'], cmd='exit 42')\n"); | |
| addOptions("--show_result=0"); | |
| build(true, GENRULE_ERROR, "//test:fail"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).doesNotContain("Target //test:fail failed to build\n"); | |
| assertThat(stderr) | |
| .contains("Use --verbose_failures to see the command lines of failed build steps.\n"); | |
| } | |
| @Test | |
| public void testFailedTargetNotSuppressedBySuccessfulTargetBudget() throws Exception { | |
| write( | |
| "mix/BUILD", | |
| "genrule(name='succ1', srcs=['in'], outs=['succ1.out'], cmd='/bin/cp $(location in)" | |
| + " $(location succ1.out)')\n" | |
| + "genrule(name='succ2', srcs=['in'], outs=['succ2.out'], cmd='/bin/cp $(location in)" | |
| + " $(location succ2.out)')\n" | |
| + "genrule(name='fail', srcs=[], outs=['fail.out'], cmd='exit 42')\n"); | |
| write("mix/in", "(input)"); | |
| addOptions("--keep_going", "--show_result=1"); | |
| build(true, GENRULE_ERROR, "//mix:succ1", "//mix:succ2", "//mix:fail"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //mix:fail failed to build\n"); | |
| assertThat(stderr).doesNotContain("Target //mix:succ1 up-to-date:\n"); | |
| assertThat(stderr).doesNotContain("Target //mix:succ2 up-to-date:\n"); | |
| } | |
| @Test | |
| public void testActionFailureShowsVerboseFailuresSuggestion() throws Exception { | |
| write("test/BUILD", "genrule(name='fail', srcs=[], outs=['fail.out'], cmd='exit 42')\n"); | |
| build(true, GENRULE_ERROR, "//test:fail"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //test:fail failed to build\n"); | |
| assertThat(stderr) | |
| .contains("Use --verbose_failures to see the command lines of failed build steps.\n"); | |
| assertThat(stderr).doesNotContain("Use --sandbox_debug"); | |
| } | |
| @Test | |
| public void testSandboxDebugSuppressesVerboseFailuresSuggestion() throws Exception { | |
| write("test/BUILD", "genrule(name='fail', srcs=[], outs=['fail.out'], cmd='exit 42')\n"); | |
| addOptions("--sandbox_debug"); | |
| build(true, GENRULE_ERROR, "//test:fail"); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr).contains("Target //test:fail failed to build\n"); | |
| assertThat(stderr) | |
| .doesNotContain("Use --verbose_failures to see the command lines of failed build steps.\n"); | |
| } | |
| @Test | |
| public void testSandboxedActionFailureShowsSandboxDebugSuggestion() throws Exception { | |
| write("test/BUILD", "genrule(name='fail', srcs=[], outs=['fail.out'], cmd='exit 42')\n"); | |
| build(true, GENRULE_ERROR, "//test:fail"); | |
| BuildResultListener listener = getCommandEnvironment().getBuildResultListener(); | |
| BuildResultPrinter printer = new BuildResultPrinter(getCommandEnvironment()); | |
| printer.showBuildResult( | |
| getRequest(), | |
| getResult(), | |
| listener.getAnalyzedTargets(), | |
| listener.getSkippedTargets(), | |
| listener.getAnalyzedAspects(), | |
| listener.getTargetRootCauses(), | |
| listener.getAspectRootCauses(), | |
| /* hasSandboxedActionFailures= */ true); | |
| String stderr = recOutErr.errAsLatin1(); | |
| assertThat(stderr) | |
| .contains( | |
| "Use --sandbox_debug to see verbose messages from the sandbox and retain the" | |
| + " sandbox build root for debugging\n"); | |
| } | |
| @Test | |
| public void testIsSandboxedRunner() { | |
| assertThat(BuildResultListener.isSandboxedRunner("linux-sandbox")).isTrue(); | |
| assertThat(BuildResultListener.isSandboxedRunner("docker")).isTrue(); | |
| assertThat(BuildResultListener.isSandboxedRunner("standalone")).isFalse(); | |
| assertThat(BuildResultListener.isSandboxedRunner(null)).isFalse(); | |
| } | |
| // Concrete implementations of this abstract test: | |
| /** Tests with 1 job. */ | |
| @RunWith(JUnit4.class) | |
| public static class SequentialBuildResultTest extends BuildResultTestCase { | |
| @Override | |
| protected int numJobs() { | |
| return 1; | |
| } | |
| } | |
| /** Tests with 100 jobs. */ | |
| @RunWith(JUnit4.class) | |
| public static class ParallelBuildResultTest extends BuildResultTestCase { | |
| @Override | |
| protected int numJobs() { return 100; } | |
| } | |
| } |