diff --git a/src/main/java/org/jenkinsci/plugins/parallel_test_executor/Splitter.java b/src/main/java/org/jenkinsci/plugins/parallel_test_executor/Splitter.java index ce20f57..93d8bc7 100644 --- a/src/main/java/org/jenkinsci/plugins/parallel_test_executor/Splitter.java +++ b/src/main/java/org/jenkinsci/plugins/parallel_test_executor/Splitter.java @@ -45,6 +45,7 @@ import java.util.Map; import java.util.PriorityQueue; import java.util.TreeMap; +import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -145,6 +146,18 @@ private static TestResult mayFilterByStageName(@NonNull TestResult tr, @CheckFor tr = ((hudson.tasks.junit.TestResult) tr).getResultForPipelineBlock(stageId.getId()); } else { listener.getLogger().println("No stage \"" + stageName + "\" found in " + run.getFullDisplayName()); + var stages = new TreeSet(); + for (var n : new DepthFirstScanner().allNodes(execution)) { + var a = n.getPersistentAction(LabelAction.class); + if (a != null) { + stages.add(a.getDisplayName()); + } + } + if (stages.isEmpty()) { + listener.getLogger().println("(No possible stages found.)"); + } else { + listener.getLogger().println("(Observed stages: " + stages.stream().collect(Collectors.joining(", ")) + ")"); + } } } else { listener.getLogger().println("No flow execution found in " + run.getFullDisplayName()); diff --git a/src/test/java/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorTest.java b/src/test/java/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorTest.java index 1993163..a49c91f 100644 --- a/src/test/java/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorTest.java +++ b/src/test/java/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorTest.java @@ -7,25 +7,31 @@ import hudson.model.StringParameterDefinition; import hudson.model.StringParameterValue; import hudson.tasks.junit.TestResultAction; +import java.io.File; +import java.nio.charset.StandardCharsets; import org.apache.commons.io.FileUtils; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.cps.SnippetizerTester; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.BuildWatcherExtension; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.jvnet.hudson.test.recipes.LocalData; - -import java.io.File; -import java.nio.charset.StandardCharsets; - +import static jenkins.test.RunMatchers.logContains; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; import static org.junit.jupiter.api.Assertions.assertTrue; @WithJenkins class ParallelTestExecutorTest { + @RegisterExtension + private static final BuildWatcherExtension buildWatcher = new BuildWatcherExtension(); + @Test @LocalData void xmlWithNoAddJUnitPublisherIsLoadedCorrectly(JenkinsRule jenkinsRule) { @@ -43,15 +49,19 @@ void workflowGenerateInclusions(JenkinsRule jenkinsRule) throws Exception { new SnippetizerTester(jenkinsRule).assertRoundTrip(step, "splitTests generateInclusions: true, parallelism: time(3)"); WorkflowJob p = jenkinsRule.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition( - "def splits = splitTests parallelism: count(2), generateInclusions: true\n" + - "echo \"splits.size=${splits.size()}\"; for (int i = 0; i < splits.size(); i++) {\n" + - " def split = splits[i]; echo \"splits[${i}]: includes=${split.includes} list=${split.list}\"\n" + - "}\n" + - "node {\n" + - " writeFile file: 'TEST-1.xml', text: ''\n" + - " writeFile file: 'TEST-2.xml', text: ''\n" + - " junit 'TEST-*.xml'\n" + - "}", true)); + """ + def splits = splitTests parallelism: count(2), generateInclusions: true + echo "splits.size=${splits.size()}" + for (int i = 0; i < splits.size(); i++) { + def split = splits[i] + echo "splits[${i}]: includes=${split.includes} list=${split.list}" + } + node { + writeFile file: 'TEST-1.xml', text: '' + writeFile file: 'TEST-2.xml', text: '' + junit 'TEST-*.xml' + } + """, true)); WorkflowRun b1 = jenkinsRule.assertBuildStatusSuccess(p.scheduleBuild2(0)); jenkinsRule.assertLogContains("splits.size=1", b1); jenkinsRule.assertLogContains("splits[0]: includes=false list=[]", b1); @@ -75,21 +85,25 @@ void workflowDoesNotGenerateInclusionsFromRunningBuild(JenkinsRule jenkinsRule) * b) Once the second build finish the previous one is killed by using milestones */ p.setDefinition(new CpsFlowDefinition( - "lock('test-results') {\n" + - " def splits = splitTests parallelism: count(2), generateInclusions: true\n" + - " echo \"splits.size=${splits.size()}\"; for (int i = 0; i < splits.size(); i++) {\n" + - " def split = splits[i]; echo \"splits[${i}]: includes=${split.includes} list=${split.list}\"\n" + - " }\n" + - " node {\n" + - " writeFile file: 'TEST-1.xml', text: ''\n" + - " writeFile file: 'TEST-2.xml', text: ''\n" + - " junit 'TEST-*.xml'\n" + - " currentBuild.result = 'UNSTABLE'\n" + // Needed due to https://issues.jenkins-ci.org/browse/JENKINS-48178 - " }\n" + - "}\n" + - "milestone 1\n" + - "sleep time: Integer.valueOf(params.SLEEP), unit: 'SECONDS'\n" + - "milestone 2", true)); + """ + lock('test-results') { + def splits = splitTests parallelism: count(2), generateInclusions: true + echo "splits.size=${splits.size()}" + for (int i = 0; i < splits.size(); i++) { + def split = splits[i] + echo "splits[${i}]: includes=${split.includes} list=${split.list}" + } + node { + writeFile file: 'TEST-1.xml', text: '' + writeFile file: 'TEST-2.xml', text: '' + junit 'TEST-*.xml' + currentBuild.result = 'UNSTABLE' // Needed due to https://github.com/jenkinsci/junit-plugin/issues/1094 + } + } + milestone 1 + sleep time: Integer.valueOf(params.SLEEP), unit: 'SECONDS' + milestone 2 + """, true)); WorkflowRun b1 = p.scheduleBuild2(0, new ParametersAction(new StringParameterValue("SLEEP", "100"))).waitForStart(); jenkinsRule.waitForMessage("Lock acquired on", b1); WorkflowRun b2 = p.scheduleBuild2(0, new ParametersAction(new StringParameterValue("SLEEP", "0"))).get(); @@ -109,10 +123,12 @@ void unloadableTestResult(JenkinsRule jenkinsRule) throws Exception { { WorkflowJob p = jenkinsRule.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition( - " node {\n" + - " writeFile file: 'TEST-one.xml', text: $//$\n" + - " junit 'TEST-*.xml'\n" + - " }\n", true)); + """ + node { + writeFile file: 'TEST-one.xml', text: "" + junit 'TEST-*.xml' + } + """, true)); jenkinsRule.buildAndAssertSuccess(p); WorkflowRun b2 = jenkinsRule.buildAndAssertSuccess(p); FileUtils.write(new File(b2.getRootDir(), "junitResult.xml"), "'\n" + - " writeFile file: 'TEST-2.xml', text: ''\n" + - " junit 'TEST-*.xml'\n" + - " }\n" + - "}\n" + - "stage('second') {\n" + - " node {\n" + - " writeFile file: 'TEST-3.xml', text: ''\n" + - " writeFile file: 'TEST-4.xml', text: ''\n" + - " junit 'TEST-*.xml'\n" + - " }\n" + - "}\n", true)); + """ + def splits = splitTests parallelism: count(2), generateInclusions: true, stage: 'first' + echo "splits.size=${splits.size()}" + for (int i = 0; i < splits.size(); i++) { + def split = splits[i]; echo "splits[${i}]: includes=${split.includes} list=${split.list}" + } + def allSplits = splitTests parallelism: count(2), generateInclusions: true + echo "allSplits.size=${allSplits.size()}" + for (int i = 0; i < allSplits.size(); i++) { + def split = allSplits[i]; echo "allSplits[${i}]: includes=${split.includes} list=${split.list}" + } + stage('first') { + node { + writeFile file: 'TEST-1.xml', text: '' + writeFile file: 'TEST-2.xml', text: '' + junit 'TEST-*.xml' + } + } + stage('second') { + node { + writeFile file: 'TEST-3.xml', text: '' + writeFile file: 'TEST-4.xml', text: '' + junit 'TEST-*.xml' + } + } + """, true)); WorkflowRun b1 = jenkinsRule.assertBuildStatusSuccess(p.scheduleBuild2(0)); jenkinsRule.assertLogContains("splits.size=1", b1); jenkinsRule.assertLogContains("splits[0]: includes=false list=[]", b1); @@ -172,31 +192,36 @@ void splitTestsWithinStage(JenkinsRule jenkinsRule) throws Exception { void splitTestsWithinParallelStage(JenkinsRule jenkinsRule) throws Exception { WorkflowJob p = jenkinsRule.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition( - "def splits = splitTests parallelism: count(2), generateInclusions: true, stage: 'Branch: first'\n" + - "echo \"splits.size=${splits.size()}\"; for (int i = 0; i < splits.size(); i++) {\n" + - " def split = splits[i]; echo \"splits[${i}]: includes=${split.includes} list=${split.list}\"\n" + - "}\n" + - "def allSplits = splitTests parallelism: count(2), generateInclusions: true\n" + - "echo \"allSplits.size=${allSplits.size()}\"; for (int i = 0; i < allSplits.size(); i++) {\n" + - " def split = allSplits[i]; echo \"allSplits[${i}]: includes=${split.includes} list=${split.list}\"\n" + - "}\n" + - "def branches = [:]\n" + - "branches['first'] = {\n" + - " node {\n" + - " writeFile file: 'TEST-1.xml', text: ''\n" + - " writeFile file: 'TEST-2.xml', text: ''\n" + - " junit 'TEST-*.xml'\n" + - " }\n" + - "}\n" + - "branches['second'] = {\n" + - " node {\n" + - " writeFile file: 'TEST-3.xml', text: ''\n" + - " writeFile file: 'TEST-4.xml', text: ''\n" + - " junit 'TEST-*.xml'\n" + - " }\n" + - "}\n" + - "parallel branches\n" - , true)); + """ + def splits = splitTests parallelism: count(2), generateInclusions: true, stage: 'Branch: first' + echo "splits.size=${splits.size()}" + for (int i = 0; i < splits.size(); i++) { + def split = splits[i] + echo "splits[${i}]: includes=${split.includes} list=${split.list}" + } + def allSplits = splitTests parallelism: count(2), generateInclusions: true + echo "allSplits.size=${allSplits.size()}" + for (int i = 0; i < allSplits.size(); i++) { + def split = allSplits[i] + echo "allSplits[${i}]: includes=${split.includes} list=${split.list}" + } + def branches = [:] + branches['first'] = { + node { + writeFile file: 'TEST-1.xml', text: '' + writeFile file: 'TEST-2.xml', text: '' + junit 'TEST-*.xml' + } + } + branches['second'] = { + node { + writeFile file: 'TEST-3.xml', text: '' + writeFile file: 'TEST-4.xml', text: '' + junit 'TEST-*.xml' + } + } + parallel branches + """, true)); WorkflowRun b1 = jenkinsRule.assertBuildStatusSuccess(p.scheduleBuild2(0)); jenkinsRule.assertLogContains("splits.size=1", b1); jenkinsRule.assertLogContains("splits[0]: includes=false list=[]", b1); @@ -217,12 +242,40 @@ void splitTestsWithinParallelStage(JenkinsRule jenkinsRule) throws Exception { void estimateTestsFromFiles(JenkinsRule jenkinsRule) throws Exception { jenkinsRule.createSlave("remote", null, null); WorkflowJob p = jenkinsRule.jenkins.createProject(WorkflowJob.class, "p"); - p.setDefinition(new CpsFlowDefinition("" + - "node('remote') {\n" + - " writeFile file: 'src/test/java/TopTest.java', text: ''\n" + - " writeFile file: 'subdir/src/test/java/some/pkg/OtherTest.java', text: ''\n" + - " echo(/splits: ${splitTests(parallelism: count(2), estimateTestsFromFiles: true).flatten().sort()}/)\n" + - "}", true)); + p.setDefinition(new CpsFlowDefinition( + """ + node('remote') { + writeFile file: 'src/test/java/TopTest.java', text: '' + writeFile file: 'subdir/src/test/java/some/pkg/OtherTest.java', text: '' + echo "splits: ${splitTests(parallelism: count(2), estimateTestsFromFiles: true).flatten().sort()}" + } + """, true)); jenkinsRule.assertLogContains("splits: [TopTest.class, TopTest.java, some/pkg/OtherTest.class, some/pkg/OtherTest.java]", jenkinsRule.buildAndAssertSuccess(p)); } + + @Test + void unknownStage(JenkinsRule r) throws Exception { + var p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + """ + stage('first') { + node { + writeFile file: 'TEST-1.xml', text: '' + writeFile file: 'TEST-2.xml', text: '' + junit 'TEST-*.xml' + } + } + stage('second') { + node { + writeFile file: 'TEST-3.xml', text: '' + writeFile file: 'TEST-4.xml', text: '' + junit 'TEST-*.xml' + } + } + splitTests parallelism: count(2), stage: 'nonexistent' + """, true)); + var b1 = r.buildAndAssertSuccess(p); + var b2 = r.buildAndAssertSuccess(p); + assertThat(b2, allOf(logContains("No stage \"nonexistent\" found in p #1"), logContains("(Observed stages: first, second)"))); + } }