├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── cd.yml │ ├── dependabot-automerge.yml │ └── jenkins-security-scan.yml ├── .gitignore ├── .mvn ├── extensions.xml └── maven.config ├── Jenkinsfile ├── README.md ├── demo ├── .gitignore ├── Dockerfile ├── JENKINS_HOME │ ├── config.xml │ ├── credentials.xml │ ├── jenkins.model.JenkinsLocationConfiguration.xml │ ├── jobs │ │ ├── demo │ │ │ └── config.xml │ │ └── demo_python │ │ │ └── config.xml │ ├── log │ │ └── Pipeline.xml │ ├── maven-settings-files.xml │ └── org.jenkinsci.plugin.gitea.servers.GiteaServers.xml ├── Makefile ├── README.md ├── create-pr.sh ├── credentials.xml ├── docker-compose.yml ├── gen.sh ├── gen_pytests.sh ├── lib │ └── vars │ │ └── testInParallel.groovy ├── pom.xml ├── repos │ ├── demo │ │ ├── Jenkinsfile │ │ ├── goodbye │ │ │ └── pom.xml │ │ └── hello │ │ │ └── pom.xml │ └── demo_python │ │ └── Jenkinsfile ├── run.sh └── src │ └── main │ └── resources │ └── index.jelly ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── parallel_test_executor │ │ ├── CountDrivenParallelism.java │ │ ├── InclusionExclusionPattern.java │ │ ├── MultipleBinaryFileParameterFactory.java │ │ ├── ParallelTestExecutor.java │ │ ├── Parallelism.java │ │ ├── RunListenerImpl.java │ │ ├── SplitStep.java │ │ ├── Splitter.java │ │ ├── TestCase.java │ │ ├── TestClass.java │ │ ├── TestCollector.java │ │ ├── TestEntity.java │ │ ├── TimeDrivenParallelism.java │ │ └── testmode │ │ ├── JavaClassName.java │ │ ├── JavaParameterizedTestCaseName.java │ │ ├── JavaTestCaseName.java │ │ ├── TestCaseName.java │ │ ├── TestClassAndCaseName.java │ │ └── TestMode.java └── resources │ ├── index.jelly │ └── org │ └── jenkinsci │ └── plugins │ └── parallel_test_executor │ ├── CountDrivenParallelism │ ├── config.groovy │ └── help.html │ ├── ParallelTestExecutor │ ├── config.jelly │ ├── help-archiveTestResults.html │ ├── help-includesPatternFile.html │ ├── help-parallelism.html │ ├── help-parameters.html │ ├── help-patternFile.html │ ├── help-testJob.html │ ├── help-testMode.html │ ├── help-testReportFiles.html │ └── help.html │ ├── SplitStep │ ├── config.jelly │ ├── help-generateInclusions.html │ ├── help-stage.html │ ├── help-testMode.html │ └── help.html │ ├── TimeDrivenParallelism │ ├── config.groovy │ └── help.html │ └── testmode │ ├── JavaClassName │ └── help.html │ ├── JavaParameterizedTestCaseName │ └── help.html │ ├── JavaTestCaseName │ └── help.html │ ├── TestCaseName │ └── help.html │ └── TestClassAndCaseName │ └── help.html └── test ├── java └── org │ └── jenkinsci │ └── plugins │ └── parallel_test_executor │ ├── ParallelTestExecutorTest.java │ └── ParallelTestExecutorUnitTest.java └── resources └── org └── jenkinsci └── plugins └── parallel_test_executor ├── ParallelTestExecutorTest ├── config.xml └── jobs │ └── old │ └── config.xml └── ParallelTestExecutorUnitTest ├── findTestCaseTimeSplitsExclusion ├── report-Test1.xml ├── report-Test2.xml ├── report-Test3.xml ├── report-Test4.xml └── report-Test5.xml ├── findTestCaseTimeSplitsInclusion ├── report-Test1.xml ├── report-Test2.xml ├── report-Test3.xml ├── report-Test4.xml └── report-Test5.xml ├── findTestCasesWithParameters └── report-Test1.xml ├── findTestCasesWithParametersIncluded └── report-Test1.xml ├── findTestDuplicates ├── report-Test1-bis.xml └── report-Test1.xml ├── findTestInJavaProjectDirectory └── src │ └── test │ └── java │ ├── FifthTest.java │ ├── FirstTest.java │ ├── FourthTest.java │ ├── SecondTest.java │ └── ThirdTest.java ├── findTestOfJavaProjectDirectoryInWorkspace ├── file.log └── src │ ├── FakeTest.java │ ├── main │ ├── FakeFifthTest.java │ └── java │ │ ├── FakeFifthTest.java │ │ └── somepackage │ │ ├── FakeFifthTest.java │ │ ├── FakeFirstTest.java │ │ ├── FakeFourthTest.java │ │ └── FakeSecondTest.java │ └── test │ ├── AdditionalFile2.java │ ├── java │ ├── AdditionalFile.java │ ├── FakeTest.txt │ ├── FifthTest.java │ ├── FirstTest.java │ ├── FourthTest.java │ ├── SecondTest.java │ ├── SomeFile3.txt │ ├── ThirdTest.java │ └── somepackage │ │ └── ThirdTest.java │ └── someFile2.txt ├── findTestSplits ├── report-Test1.xml ├── report-Test2.xml ├── report-Test3.xml ├── report-Test4.xml └── report-Test5.xml ├── findTestSplitsInclusions ├── report-Test1.xml ├── report-Test2.xml ├── report-Test3.xml ├── report-Test4.xml └── report-Test5.xml ├── previousBuildIsOngoing └── report-Test1.xml └── testWeDoNotCreateMoreSplitsThanThereAreTests └── report-Test1.xml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/parallel-test-executor-plugin-developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request 2 | 3 | name: Dependabot auto-merge 4 | on: pull_request 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | jobs: 11 | dependabot: 12 | runs-on: ubuntu-latest 13 | if: ${{ github.actor == 'dependabot[bot]' }} 14 | steps: 15 | - name: Enable auto-merge for Dependabot PRs 16 | run: gh pr merge --auto --merge "$PR_URL" 17 | env: 18 | PR_URL: ${{github.event.pull_request.html_url}} 19 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 20 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | target 3 | # Jenkins 4 | work 5 | 6 | # IntelliJ project files 7 | *.iml 8 | *.iws 9 | *.ipr 10 | .idea 11 | out 12 | 13 | # eclipse project file 14 | .settings 15 | .classpath 16 | .project 17 | build 18 | 19 | # vim 20 | *~ 21 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin( 2 | useContainerAgent: true, 3 | configurations: [ 4 | [platform: 'linux', jdk: 21], 5 | [platform: 'windows', jdk: 17], 6 | ]) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenkins Parallel Test Executor Plugin 2 | 3 | This plugin adds a tool that lets you easily execute tests in parallel. 4 | This is achieved by having Jenkins look at the test execution time of the last run, 5 | split tests into multiple units of roughly equal size, then execute them in parallel. 6 | 7 | ## How It Works 8 | 9 | The tool looks at the test execution time from the last time, and divide tests into multiple units of roughly equal size. Each unit is then converted into the exclusion list (by excluding all but the tests that assigned to that unit). 10 | 11 | This tool can be used with any test job that 12 | 13 | 1. produce JUnit-compatible XML files 14 | 2. accept a test-exclusion list in a file. 15 | 16 | You are responsible for configuring the build script to honor the exclusion file. A standard technique is to write the build script to always refer to a fixed exclusion list file, and check in an empty file by that name. You can then specify that file as the "exclusion file name" in the configuration of this builder, and the builder will overwrite the empty file from SCM by the generated one. 17 | 18 | There are two modes: one used with [Jenkins Pipeline](https://jenkins.io/doc/book/pipeline/), the other with freestyle projects. The former is more flexible and straightforward. 19 | 20 | ### Pipeline step 21 | 22 | The `splitTests` step analyzes test results from the last successful build of this job, if any. It returns a set of roughly equal "splits", each representing one chunk of work. Typically you will use the `parallel` step to run each chunk in its own `node`, passing split information to the build tool in various ways. The demo (below) shows this in action. 23 | 24 | ### Freestyle-compatible builder 25 | 26 | For freestyle projects, setup is more complex as you need *two* jobs, an upstream controller and a downstream workhorse. There is a build step which you add to the upstream job and on which you define the downstream job. The builder executes multiple runs of the downstream job concurrently by interleaving tests, saving configuration files to the downstream workspace, achieving the parallel test execution semantics. 27 | 28 | You are responsible for checking **Execute concurrent builds if necessary** on the downstream job to allow the concurrent execution. 29 | 30 | When the downstream builds all finish, the specified report directories are brought back into the upstream job's workspace, where they will be picked up by the standard JUnit test report collector. 31 | 32 | The instructions below for configuring your build tool then apply to the downstream job. 33 | 34 | ## Configuring build tools with exclusions 35 | 36 | ### Maven 37 | 38 | Newer version of Maven Surefire plugin supports `excludesFile` parameter. For example, the following configuration tells Maven to honor `exclusions.txt` at the root of the source tree. 39 | 40 | ```xml 41 | 42 | 43 | 44 | org.apache.maven.plugins 45 | maven-surefire-plugin 46 | 2.14 47 | 48 | ${project.basedir}/exclusions.txt 49 | 50 | 51 | 52 | 53 | ``` 54 | 55 | ### Ant 56 | 57 | Ant JUnit task supports the `excludesfile` attribute in its `` sub-element: 58 | 59 | ```xml 60 | 61 | 62 | 63 | 64 | 65 | ``` 66 | 67 | ## Demo 68 | 69 | The `demo` subdirectory in sources contains a demo of this plugin based on Docker. It shows both Pipeline and freestyle modes. [README](demo/README.md) 70 | 71 | ## Changelog 72 | 73 | See [GitHub releases](https://github.com/jenkinsci/parallel-test-executor-plugin/releases) 74 | or the [old changelog](https://github.com/jenkinsci/parallel-test-executor-plugin/blob/b2f8c46675f19ce0d5041966e611a0f6a5411b79/old-changelog.md). 75 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | plugins 2 | -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.9.6-eclipse-temurin-11 AS maven 2 | FROM jenkins/jenkins:2.387.3 3 | 4 | USER root 5 | 6 | RUN apt-get update && \ 7 | apt-get install -y python3-pytest && \ 8 | apt-get clean 9 | 10 | COPY --from=maven /usr/share/maven /usr/share/maven/ 11 | RUN ln -s /usr/share/maven/bin/mvn /usr/local/bin/mvn && \ 12 | ln -s /usr/share/maven/bin/mvnDebug /usr/local/bin/mvnDebug 13 | 14 | ADD lib /tmp/lib 15 | RUN mkdir -p /m2repo 16 | 17 | RUN chown -R jenkins.jenkins /tmp/lib /m2repo 18 | 19 | USER jenkins 20 | 21 | COPY target/test-classes/test-dependencies/*.hpi /usr/share/jenkins/ref/plugins/ 22 | 23 | RUN cd /tmp/lib && \ 24 | git init && \ 25 | git add . && \ 26 | git -c user.email=demo@jenkins-ci.org -c user.name="Parallel Test Executor Demo" commit -m 'demo' 27 | 28 | ADD JENKINS_HOME /usr/share/jenkins/ref 29 | -------------------------------------------------------------------------------- /demo/JENKINS_HOME/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.375.2 5 | 0 6 | NORMAL 7 | true 8 | 9 | 10 | false 11 | 12 | ${ITEM_ROOTDIR}/workspace 13 | ${ITEM_ROOTDIR}/builds 14 | 15 | 16 | 17 | 18 | 19 | mock 20 | NORMAL 21 | 1 22 | 23 | false 24 | 25 | 26 | 0 27 | 28 | 29 | 30 | All 31 | false 32 | false 33 | 34 | 35 | 36 | all 37 | 0 38 | 39 | 40 | false 41 | 42 | 43 | 44 | false 45 | 46 | -------------------------------------------------------------------------------- /demo/JENKINS_HOME/credentials.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | GLOBAL 11 | gitea 12 | 13 | jenkins 14 | SECRET 15 | false 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/JENKINS_HOME/jenkins.model.JenkinsLocationConfiguration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | address not configured yet <nobody@nowhere> 4 | http://localhost:8080/ 5 | 6 | -------------------------------------------------------------------------------- /demo/JENKINS_HOME/jobs/demo/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 6126b51e-8b62-413c-9e82-333d0cf58bdb 9 | http://gitea:3000 10 | jenkins 11 | demo 12 | gitea 13 | 14 | 15 | 1 16 | 17 | 18 | 1 19 | 20 | 21 | 1 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | true 33 | -1 34 | -1 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | testInParallel 45 | 46 | 47 | whatever 48 | /tmp/lib 49 | * 50 | 51 | false 52 | 53 | 54 | master 55 | false 56 | true 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | All 65 | false 66 | false 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | .* 82 | false 83 | 84 | 85 | 86 | All 87 | 88 | 89 | * * * * * 90 | 60000 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /demo/JENKINS_HOME/jobs/demo_python/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This job uses the pytest framework to execute 100 test cases contained in a single python file. It requires a version of parallel test executor plugin that supports test modes to parallel execution based on test cases and not only test classes. 5 | 6 | 7 | 8 | 6126b51e-8b62-413c-9e82-333d0cf58bdb 9 | http://gitea:3000 10 | jenkins 11 | demo_python 12 | gitea 13 | 14 | 15 | 1 16 | 17 | 18 | 1 19 | 20 | 21 | 1 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | true 33 | -1 34 | -1 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | testInParallel 45 | 46 | 47 | whatever 48 | /tmp/lib 49 | * 50 | 51 | false 52 | 53 | 54 | master 55 | false 56 | true 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | All 65 | false 66 | false 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | .* 82 | false 83 | 84 | 85 | 86 | All 87 | 88 | 89 | * * * * * 90 | 60000 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /demo/JENKINS_HOME/log/Pipeline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pipeline 4 | 5 | 6 | org.jenkinsci.plugins.workflow 7 | 500 8 | 9 | 10 | org.jenkinsci.plugins.durabletask 11 | 500 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/JENKINS_HOME/maven-settings-files.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jenkins-mirror 6 | 7 | jenkins-mirror 8 | Jenkins Mirror 9 | 10 | 11 | 14 | /m2repo 15 | 16 | ]]> 17 | org.jenkinsci.plugins.configfiles.maven.MavenSettingsConfig 18 | 19 | true 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demo/JENKINS_HOME/org.jenkinsci.plugin.gitea.servers.GiteaServers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gitea 6 | http://gitea:3000 7 | false 8 | gitea 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | PHONY: validate 2 | validate: 3 | mvn -Dtest=InjectedTest clean test 4 | 5 | PHONY: copy-plugins 6 | copy-plugins: 7 | if [ \! -f ../target/parallel-test-executor.hpi ]; then mvn -f .. -Pquick-build install; fi 8 | if [ \! -f target/test-classes/test-dependencies/index -o \ 9 | pom.xml -nt target/test-classes/test-dependencies/index -o \ 10 | ../target/parallel-test-executor.hpi -nt target/test-classes/test-dependencies/parallel-test-executor.hpi ]; then \ 11 | mvn clean validate hpi:resolve-test-dependencies; fi 12 | @# TODO would be more efficient to move Dockerfile and all it references into a subdirectory, or use .dockerignore 13 | 14 | PHONY: clean 15 | clean: 16 | rm -rf target 17 | docker compose down -v 18 | docker image rm demo-jenkins || true 19 | 20 | PHONY: run 21 | run: copy-plugins 22 | ./run.sh 23 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | * docker 4 | * docker compose 5 | 6 | # Running 7 | 8 | make run 9 | 10 | and then go to: http://localhost:8080/ 11 | 12 | # Tear down 13 | 14 | make clean 15 | 16 | # Demo contents 17 | 18 | `pipeline` is a self-contained Pipeline project. 19 | 20 | Run one build of `pipeline » main` — Jenkins will attempt to guess how to split 100 tests across agents. 21 | Run a second build and you will see the load split more reliably across five agents running ~20 tests apiece. 22 | 23 | `pipeline-pytest` is a self-contained Pipeline project. It contains a single file containing 100 test cases. 24 | 25 | Run one build of `pytest » master`—all 100 test cases in its single test class/file will be run on one agent. 26 | Run a second build and you will see the load split across five agents running 20 test cases apiece. 27 | -------------------------------------------------------------------------------- /demo/create-pr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | cd $(dirname $0) 4 | export TOKEN=$(cat target/gitea_token.txt) 5 | pushd target/repo > /dev/null 6 | export BRANCH_NAME=experiment-$(openssl rand -hex 6) 7 | export TARGET_BRANCH=main 8 | git checkout -b "$BRANCH_NAME" 9 | git -c commit.gpgsign=false -c user.email=demo@jenkins-ci.org -c user.name="Parallel Test Executor Demo" commit --allow-empty -m "Empty commit" 10 | git push -u origin "$BRANCH_NAME" 11 | curl -X 'POST' \ 12 | 'http://localhost:3000/api/v1/repos/jenkins/demo/pulls' \ 13 | -H "Authorization: token $TOKEN" \ 14 | -H 'accept: application/json' \ 15 | -H 'Content-Type: application/json' \ 16 | -d "{ 17 | \"base\": \"$TARGET_BRANCH\", 18 | \"title\": \"A pull request from $BRANCH_NAME\", 19 | \"body\": \"Some description\", 20 | \"head\": \"$BRANCH_NAME\" 21 | }" 22 | popd > /dev/null 23 | -------------------------------------------------------------------------------- /demo/credentials.xml: -------------------------------------------------------------------------------- 1 | 2 | GLOBAL 3 | gitea 4 | 5 | jenkins 6 | SECRET 7 | false 8 | 9 | -------------------------------------------------------------------------------- /demo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | networks: 3 | demo: 4 | external: false 5 | volumes: 6 | m2repo: 7 | gitea: 8 | services: 9 | gitea: 10 | image: gitea/gitea:1.18.0 11 | container_name: gitea 12 | environment: 13 | - USER_UID=1000 14 | - USER_GID=1000 15 | - GITEA__server__HTTP_PORT=3000 16 | - GITEA__server__ROOT_URL=http://localhost:3000/ 17 | - GITEA__server__DOMAIN=localhost 18 | - GITEA__database__DB_TYPE=sqlite3 19 | - GITEA__security__INSTALL_LOCK=true 20 | restart: always 21 | networks: 22 | - demo 23 | volumes: 24 | - gitea:/data 25 | - /etc/timezone:/etc/timezone:ro 26 | - /etc/localtime:/etc/localtime:ro 27 | ports: 28 | - "3000:3000" 29 | jenkins: 30 | container_name: jenkins 31 | networks: 32 | - demo 33 | build: 34 | context: . 35 | ports: 36 | - "8080:8080" 37 | - "5005:5005" 38 | volumes: 39 | - m2repo:/m2repo:rw 40 | environment: 41 | # TODO without this JENKINS-24752 workaround, it takes too long to provision. 42 | # (Do not add hudson.model.LoadStatistics.decay=0.1; in that case we overprovision slaves which never get used, and OnceRetentionStrategy.check disconnects them after an idle timeout.) 43 | - JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dhudson.model.LoadStatistics.clock=1000 -Dhudson.Main.development=true -Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true 44 | healthcheck: 45 | test: [ "CMD", "curl", "-f", "http://localhost:8080" ] 46 | interval: 5s 47 | timeout: 30s 48 | 49 | -------------------------------------------------------------------------------- /demo/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p hello/src/test/java/foo 3 | mkdir -p goodbye/src/test/java/foo 4 | for i in {00..99}; do 5 | cat > hello/src/test/java/foo/Hello${i}Test.java << EOF 6 | package foo; 7 | import org.junit.Test; 8 | import static org.junit.Assert.*; 9 | 10 | public class Hello${i}Test { 11 | private static final int MULTIPLIER; 12 | static { 13 | String multiplier = System.getenv("MULTIPLIER"); 14 | MULTIPLIER = multiplier != null ? Integer.parseInt(multiplier) : 1; 15 | } 16 | @Test public void one() { 17 | if (Math.random() < 0.015) { 18 | fail("oops"); 19 | } 20 | } 21 | @Test public void two() {} 22 | @Test public void three() throws Exception { 23 | Thread.sleep(${i##0}0 * MULTIPLIER); 24 | } 25 | @Test public void four() throws Exception { 26 | Thread.sleep(1000 * MULTIPLIER); 27 | } 28 | } 29 | EOF 30 | cat > goodbye/src/test/java/foo/Goodbye${i}Test.java << EOF 31 | package foo; 32 | import org.junit.Test; 33 | import static org.junit.Assert.*; 34 | 35 | public class Goodbye${i}Test { 36 | private static final int MULTIPLIER; 37 | static { 38 | String multiplier = System.getenv("MULTIPLIER"); 39 | MULTIPLIER = multiplier != null ? Integer.parseInt(multiplier) : 1; 40 | } 41 | @Test public void one() { 42 | if (Math.random() < 0.015) { 43 | fail("oops"); 44 | } 45 | } 46 | @Test public void two() {} 47 | @Test public void three() throws Exception { 48 | Thread.sleep(${i##0}0 * MULTIPLIER); 49 | } 50 | @Test public void four() throws Exception { 51 | Thread.sleep(1000 * MULTIPLIER); 52 | } 53 | } 54 | EOF 55 | done 56 | -------------------------------------------------------------------------------- /demo/gen_pytests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p src/test/foo 3 | cat > src/test/foo/test_Hello.py << EOF 4 | import pytest 5 | import time 6 | import random 7 | 8 | class TestHello: 9 | EOF 10 | 11 | for i in {00..99}; do 12 | cat >> src/test/foo/test_Hello.py << EOF 13 | 14 | def test_${i}(self): 15 | x = random.random() 16 | time.sleep(x) 17 | if x < 0.015: 18 | pytest.fail("oops") 19 | EOF 20 | done 21 | 22 | -------------------------------------------------------------------------------- /demo/lib/vars/testInParallel.groovy: -------------------------------------------------------------------------------- 1 | def call(parallelism, testMode, inclusionsFile, exclusionsFile, stageName, prepare, run) { 2 | def splits 3 | node { 4 | deleteDir() 5 | prepare() 6 | splits = splitTests parallelism: parallelism, testMode: testMode, generateInclusions: true, stage: stageName 7 | } 8 | def branches = [:] 9 | for (int i = 0; i < splits.size(); i++) { 10 | def num = i 11 | def split = splits[num] 12 | branches["split${num}"] = { 13 | echo "in split$num: $split" 14 | stage("Test Section #${num + 1}") { 15 | node { 16 | stage('Preparation') { 17 | deleteDir() 18 | prepare() 19 | writeFile file: (split.includes ? inclusionsFile : exclusionsFile), text: split.list.join("\n") 20 | writeFile file: (split.includes ? exclusionsFile : inclusionsFile), text: '' 21 | } 22 | stage('Main') { 23 | run() 24 | } 25 | } 26 | } 27 | } 28 | } 29 | parallel branches 30 | } 31 | -------------------------------------------------------------------------------- /demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 4.76 9 | 10 | 11 | parallel-test-executor-demo 12 | 1.0-SNAPSHOT 13 | hpi 14 | 15 | 2.387.3 16 | 17 | Parallel Test Executor Demo 18 | 19 | 20 | repo.jenkins-ci.org 21 | https://repo.jenkins-ci.org/public/ 22 | 23 | 24 | 25 | 26 | repo.jenkins-ci.org 27 | https://repo.jenkins-ci.org/public/ 28 | 29 | 30 | 31 | 32 | org.jenkins-ci.plugins 33 | parallel-test-executor 34 | 999999-SNAPSHOT 35 | test 36 | 37 | 38 | 39 | org.jenkins-ci.plugins.workflow 40 | workflow-job 41 | test 42 | 43 | 44 | org.jenkins-ci.plugins.workflow 45 | workflow-cps 46 | test 47 | 48 | 49 | io.jenkins.plugins 50 | pipeline-groovy-lib 51 | test 52 | 53 | 54 | org.jenkins-ci.plugins.workflow 55 | workflow-multibranch 56 | test 57 | 58 | 59 | org.jenkins-ci.plugins.workflow 60 | workflow-basic-steps 61 | test 62 | 63 | 64 | org.jenkins-ci.plugins.workflow 65 | workflow-durable-task-step 66 | test 67 | 68 | 69 | org.jenkins-ci.plugins 70 | config-file-provider 71 | test 72 | 73 | 74 | org.ow2.asm 75 | asm 76 | 77 | 78 | 79 | 80 | org.jenkins-ci.plugins 81 | git 82 | test 83 | 84 | 85 | org.jenkins-ci.plugins 86 | junit 87 | test 88 | 89 | 90 | org.jenkins-ci.plugins 91 | mock-slave 92 | 125.vcfb_5c627d399 93 | test 94 | 95 | 96 | org.jenkins-ci.plugins 97 | pipeline-stage-step 98 | test 99 | 100 | 101 | org.jenkins-ci.plugins 102 | junit-realtime-test-reporter 103 | test 104 | 105 | 106 | org.jenkins-ci.plugins 107 | gitea 108 | 1.4.5 109 | test 110 | 111 | 112 | 113 | 114 | 115 | io.jenkins.tools.bom 116 | bom-2.387.x 117 | 2543.vfb_1a_5fb_9496d 118 | import 119 | pom 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /demo/repos/demo/Jenkinsfile: -------------------------------------------------------------------------------- 1 | @Library('testInParallel') _ 2 | 3 | properties([ 4 | parameters([ 5 | string(name: 'MULTIPLIER', defaultValue: '1', description: 'Factor by which to artificially slow down tests.'), 6 | string(name: 'SPLIT', defaultValue: '5', description: 'Number of buckets to split tests into.') 7 | ]) 8 | ]) 9 | 10 | stage('Sources') { 11 | node { 12 | checkout scm 13 | dir('hello') { 14 | stash name: 'hello', excludes: 'target/' 15 | } 16 | dir('goodbye') { 17 | stash name: 'goodbye', excludes: 'target/' 18 | } 19 | } 20 | } 21 | 22 | stage('Testing Hello') { 23 | testInParallel(count(Integer.parseInt(params.SPLIT)), javaTestCase(), 'inclusions.txt', 'exclusions.txt', 'Testing Hello', { 24 | unstash 'hello' 25 | }, { 26 | configFileProvider([configFile(fileId: 'jenkins-mirror', variable: 'SETTINGS')]) { 27 | withEnv(["MULTIPLIER=$params.MULTIPLIER"]) { 28 | sh 'mvn -s $SETTINGS -B -ntp -Dmaven.test.failure.ignore clean test' 29 | junit 'target/surefire-reports/*.xml' 30 | } 31 | } 32 | }) 33 | } 34 | 35 | stage('Testing Goodbye') { 36 | testInParallel(count(Integer.parseInt(params.SPLIT)), javaTestCase(), 'inclusions.txt', 'exclusions.txt', 'Testing Goodbye', { 37 | unstash 'goodbye' 38 | }, { 39 | configFileProvider([configFile(fileId: 'jenkins-mirror', variable: 'SETTINGS')]) { 40 | withEnv(["MULTIPLIER=$params.MULTIPLIER"]) { 41 | sh 'mvn -s $SETTINGS -B -ntp -Dmaven.test.failure.ignore clean test' 42 | junit 'target/surefire-reports/*.xml' 43 | } 44 | } 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /demo/repos/demo/goodbye/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | test 6 | goodbye 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | 11 11 | 11 12 | 13 | 14 | 15 | 16 | 17 | org.apache.maven.plugins 18 | maven-surefire-plugin 19 | 3.2.5 20 | 21 | 22 | 23 | 24 | 25 | 26 | junit 27 | junit 28 | 4.13.1 29 | test 30 | 31 | 32 | 33 | 34 | 35 | 36 | exclusions 37 | 38 | 39 | exclusions.txt 40 | 41 | 42 | 43 | 44 | 45 | maven-surefire-plugin 46 | 47 | exclusions.txt 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | inclusions 56 | 57 | 58 | inclusions.txt 59 | 60 | 61 | 62 | 63 | 64 | maven-surefire-plugin 65 | 66 | inclusions.txt 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /demo/repos/demo/hello/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | test 6 | hello 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | 11 11 | 11 12 | 13 | 14 | 15 | 16 | 17 | org.apache.maven.plugins 18 | maven-surefire-plugin 19 | 3.2.5 20 | 21 | 22 | 23 | 24 | 25 | 26 | junit 27 | junit 28 | 4.13.1 29 | test 30 | 31 | 32 | 33 | 34 | 35 | 36 | exclusions 37 | 38 | 39 | exclusions.txt 40 | 41 | 42 | 43 | 44 | 45 | maven-surefire-plugin 46 | 47 | exclusions.txt 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | inclusions 56 | 57 | 58 | inclusions.txt 59 | 60 | 61 | 62 | 63 | 64 | maven-surefire-plugin 65 | 66 | inclusions.txt 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /demo/repos/demo_python/Jenkinsfile: -------------------------------------------------------------------------------- 1 | @Library('testInParallel') _ 2 | 3 | stage('Sources') { 4 | node { 5 | checkout scm 6 | stash name: 'sources', excludes: 'Jenkinsfile' 7 | } 8 | } 9 | 10 | stage('Testing') { 11 | testInParallel(count(5), testCase(), 'inclusions.txt', 'exclusions.txt', 'Testing python', { 12 | unstash 'sources' 13 | }, { 14 | def result = "" 15 | def e = readFile 'exclusions.txt' 16 | def i = readFile 'inclusions.txt' 17 | 18 | if (e) { // use exclusions 19 | e = e.replaceAll("\n", " or ") 20 | result = "not(${e})" 21 | } else if (i) { 22 | i = i.replaceAll("\n", " or ") 23 | result = i 24 | } 25 | 26 | sh "py.test-3 --junit-xml=out.xml -k \'${result}\' || true" 27 | junit 'out.xml' 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /demo/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | function gitea_create_admin_user() { 5 | local username; username="${1:?}" 6 | local email; email="${2:?}" 7 | mkdir -p target 8 | [ -f target/gitea_output.txt ] || docker compose exec -u 1000:1000 gitea gitea admin user create --admin --username "$username" --random-password --email "$email" > target/gitea_output.txt 9 | } 10 | 11 | function gitea_generate_access_token() { 12 | local username; username="${1:?}" 13 | mkdir -p target 14 | [ -f target/gitea_token.txt ] || docker compose exec -u 1000:1000 gitea gitea admin user generate-access-token --username "$username" --raw > target/gitea_token.txt 15 | } 16 | 17 | function gitea_token() { 18 | cat target/gitea_token.txt 19 | } 20 | 21 | function gitea_repository_exists() { 22 | local name; name="${1:?}" 23 | local token; token="$(gitea_token)" 24 | [ "$(curl -s -o /dev/null -w "%{http_code}" -X 'GET' \ 25 | "http://localhost:3000/api/v1/repos/jenkins/$name" \ 26 | -H "Authorization: token $token")" = "200" ] 27 | 28 | } 29 | 30 | function gitea_create_repository() { 31 | local name; name="${1:?}" 32 | local token; token="$(gitea_token)" 33 | curl -X 'POST' \ 34 | 'http://localhost:3000/api/v1/user/repos' \ 35 | -H "Authorization: token $token" \ 36 | -H 'accept: application/json' \ 37 | -H 'Content-Type: application/json' \ 38 | -d "{ 39 | \"auto_init\": true, 40 | \"default_branch\": \"main\", 41 | \"name\": \"$name\" 42 | }" 43 | } 44 | 45 | function gitea_init_repository() { 46 | local name; name="${1:?}" 47 | local script; script="${2:?}" 48 | git clone "http://jenkins:$(gitea_token)@localhost:3000/jenkins/$name.git" "target/$name" 49 | cp -R "repos/$name" target/ 50 | pushd "target/$name" 51 | bash "../../$script" 52 | git add . 53 | git -c commit.gpgsign=false -c user.email=demo@jenkins-ci.org -c user.name="Parallel Test Executor Demo" commit -m "Initial commit" 54 | git push origin -u 55 | popd 56 | } 57 | 58 | function jenkins_download_cli() { 59 | [ -f target/jenkins-cli.jar ] || wget -O target/jenkins-cli.jar -o /dev/null http://localhost:8080/jnlpJars/jenkins-cli.jar 60 | } 61 | 62 | function jenkins_update_credentials() { 63 | local credentialsId; credentialsId="${1:?}" 64 | local token; token="$(gitea_token)" 65 | jenkins_download_cli 66 | sed -e "s/SECRET/$token/" credentials.xml | java -jar target/jenkins-cli.jar -s http://localhost:8080/ update-credentials-by-xml "SystemCredentialsProvider::SystemContextResolver::jenkins" "(global)" "$credentialsId" 67 | } 68 | 69 | function readme() { 70 | local gitea_username; gitea_username=$(grep "New user" < target/gitea_output.txt | cut -f 2 -d "'") 71 | local gitea_password; gitea_password=$(grep password < target/gitea_output.txt | cut -f 2 -d "'") 72 | 73 | echo "Demo initialized" 74 | echo "Gitea is available on http://localhost:3000 using $gitea_username $gitea_password" 75 | echo "Jenkins is available on http://localhost:8080" 76 | echo "The demo git repo is available in target/repo and can be accessed at http://localhost:3000/jenkins/demo" 77 | open http://localhost:8080/ 78 | } 79 | 80 | docker compose up -d --wait 81 | gitea_create_admin_user jenkins demo@jenkins.io 82 | gitea_generate_access_token jenkins 83 | jenkins_update_credentials gitea 84 | if ! gitea_repository_exists demo; then 85 | gitea_create_repository demo 86 | gitea_init_repository demo gen.sh 87 | fi 88 | if ! gitea_repository_exists demo_python; then 89 | gitea_create_repository demo_python 90 | gitea_init_repository demo_python gen_pytests.sh 91 | fi 92 | readme 93 | -------------------------------------------------------------------------------- /demo/src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | (placeholder) 4 |
5 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci.plugins 5 | plugin 6 | 5.17 7 | 8 | 9 | parallel-test-executor 10 | ${changelist} 11 | hpi 12 | Parallel Test Executor Plugin 13 | https://github.com/jenkinsci/${project.artifactId}-plugin 14 | 15 | 16 | MIT License 17 | https://opensource.org/licenses/MIT 18 | 19 | 20 | 21 | 999999-SNAPSHOT 22 | 23 | 2.479 24 | ${jenkins.baseline}.3 25 | jenkinsci/${project.artifactId}-plugin 26 | 27 | 28 | 29 | repo.jenkins-ci.org 30 | https://repo.jenkins-ci.org/public/ 31 | 32 | 33 | 34 | 35 | 36 | repo.jenkins-ci.org 37 | https://repo.jenkins-ci.org/public/ 38 | 39 | 40 | 41 | scm:git:https://github.com/${gitHubRepo} 42 | scm:git:https://github.com/${gitHubRepo} 43 | ${scmTag} 44 | https://github.com/${gitHubRepo} 45 | 46 | 47 | 48 | 49 | io.jenkins.tools.bom 50 | bom-${jenkins.baseline}.x 51 | 4770.v9a_2b_7a_9d8b_7f 52 | import 53 | pom 54 | 55 | 56 | 57 | 58 | 59 | org.jenkins-ci.plugins 60 | parameterized-trigger 61 | true 62 | 63 | 64 | org.jenkins-ci.plugins 65 | variant 66 | 67 | 68 | org.jenkins-ci.plugins 69 | matrix-project 70 | test 71 | 72 | 73 | org.jenkins-ci.plugins 74 | junit 75 | 76 | 77 | org.mockito 78 | mockito-core 79 | test 80 | 81 | 82 | org.jenkins-ci.plugins.workflow 83 | workflow-step-api 84 | 85 | 86 | org.jenkins-ci.plugins 87 | script-security 88 | 89 | 90 | org.jenkins-ci.plugins.workflow 91 | workflow-api 92 | 93 | 94 | org.jenkins-ci.plugins.workflow 95 | workflow-cps 96 | test 97 | 98 | 99 | org.jenkins-ci.plugins.workflow 100 | workflow-job 101 | test 102 | 103 | 104 | org.jenkins-ci.plugins 105 | pipeline-stage-step 106 | test 107 | 108 | 109 | org.jenkins-ci.plugins.workflow 110 | workflow-durable-task-step 111 | test 112 | 113 | 114 | org.jenkins-ci.plugins.workflow 115 | workflow-basic-steps 116 | test 117 | 118 | 119 | org.jenkins-ci.plugins.workflow 120 | workflow-cps 121 | tests 122 | test 123 | 124 | 125 | org.jenkins-ci.plugins 126 | pipeline-milestone-step 127 | test 128 | 129 | 130 | org.6wind.jenkins 131 | lockable-resources 132 | test 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/CountDrivenParallelism.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Descriptor; 5 | import org.kohsuke.stapler.DataBoundConstructor; 6 | 7 | import java.util.List; 8 | import org.jenkinsci.Symbol; 9 | 10 | /** 11 | * @author Kohsuke Kawaguchi 12 | */ 13 | public class CountDrivenParallelism extends Parallelism { 14 | public final int size; 15 | 16 | @DataBoundConstructor 17 | public CountDrivenParallelism(int size) { 18 | this.size = size; 19 | } 20 | 21 | @Override 22 | public int calculate(List tests) { 23 | // Don't split into 5 buckets if we only have 2 tests etc 24 | return tests == null ? 25 | size : 26 | Math.min(size, tests.size()); 27 | } 28 | 29 | @Symbol("count") 30 | @Extension 31 | public static class DescriptorImpl extends Descriptor { 32 | @Override 33 | public String getDisplayName() { 34 | return "Fixed number of batches"; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/InclusionExclusionPattern.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; 4 | 5 | import java.io.Serializable; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | /** 10 | * A list of file name patterns to include or exclude 11 | */ 12 | public class InclusionExclusionPattern implements Serializable { 13 | @Whitelisted 14 | public boolean isIncludes() { 15 | return includes; 16 | } 17 | 18 | @Whitelisted 19 | public List getList() { 20 | return Collections.unmodifiableList(list); 21 | } 22 | 23 | private final boolean includes; 24 | private final List list; 25 | 26 | InclusionExclusionPattern(List list, boolean includes) { 27 | this.list = list; 28 | this.includes = includes; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "InclusionExclusionPattern{" + 34 | "includes=" + includes + 35 | ", list=" + list + 36 | '}'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/MultipleBinaryFileParameterFactory.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import com.google.common.collect.Lists; 4 | import hudson.AbortException; 5 | import hudson.FilePath; 6 | import hudson.model.AbstractBuild; 7 | import hudson.model.Action; 8 | import hudson.model.FileParameterValue; 9 | import hudson.model.ParametersAction; 10 | import hudson.model.TaskListener; 11 | import hudson.plugins.parameterizedtrigger.AbstractBuildParameterFactory; 12 | import hudson.plugins.parameterizedtrigger.AbstractBuildParameterFactoryDescriptor; 13 | import hudson.plugins.parameterizedtrigger.AbstractBuildParameters; 14 | import hudson.plugins.parameterizedtrigger.FileBuildParameterFactory; 15 | import org.kohsuke.stapler.DataBoundConstructor; 16 | 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.util.List; 20 | import java.util.logging.Logger; 21 | import org.jenkinsci.plugins.variant.OptionalExtension; 22 | 23 | /** 24 | * Essentially a copy-paste of {@link hudson.plugins.parameterizedtrigger.BinaryFileParameterFactory} that takes a 25 | * list of mappings {@code name -> filePattern} to generate parameters. 26 | */ 27 | public class MultipleBinaryFileParameterFactory extends AbstractBuildParameterFactory { 28 | public static class ParameterBinding { 29 | public ParameterBinding(String parameterName, String filePattern) { 30 | this.parameterName = parameterName; 31 | this.filePattern = filePattern; 32 | } 33 | public final String parameterName; 34 | public final String filePattern; 35 | } 36 | 37 | private final List parametersList; 38 | private final FileBuildParameterFactory.NoFilesFoundEnum noFilesFoundAction; 39 | 40 | @DataBoundConstructor 41 | public MultipleBinaryFileParameterFactory(List parametersList, FileBuildParameterFactory.NoFilesFoundEnum noFilesFoundAction) { 42 | this.parametersList = parametersList; 43 | this.noFilesFoundAction = noFilesFoundAction; 44 | } 45 | 46 | public MultipleBinaryFileParameterFactory(List parametersList) { 47 | this(parametersList, FileBuildParameterFactory.NoFilesFoundEnum.SKIP); 48 | } 49 | 50 | public FileBuildParameterFactory.NoFilesFoundEnum getNoFilesFoundAction() { 51 | return noFilesFoundAction; 52 | } 53 | 54 | @Override 55 | public List getParameters(AbstractBuild build, TaskListener listener) throws IOException, InterruptedException, AbstractBuildParameters.DontTriggerException { 56 | List result = Lists.newArrayList(); 57 | int totalFiles = 0; 58 | for (final ParameterBinding parameterBinding : parametersList) { 59 | // save them into the master because FileParameterValue might need files after the slave workspace have disappeared/reused 60 | FilePath target = new FilePath(build.getRootDir()).child("parameter-files"); 61 | FilePath workspace = build.getWorkspace(); 62 | if (workspace == null) { 63 | throw new AbortException("no workspace"); 64 | } 65 | int k = workspace.copyRecursiveTo(parameterBinding.filePattern, target); 66 | totalFiles += k; 67 | if (k > 0) { 68 | for (final FilePath f : target.list(parameterBinding.filePattern)) { 69 | LOGGER.fine("Triggering build with " + f.getName()); 70 | 71 | result.add(new AbstractBuildParameters() { 72 | @Override 73 | public Action getAction(AbstractBuild build, TaskListener listener) throws IOException, InterruptedException, DontTriggerException { 74 | assert f.getChannel() == null; // we copied files locally. This file must be local to the master 75 | FileParameterValue fv = new FileParameterValue(parameterBinding.parameterName, new File(f.getRemote()), f.getName()); 76 | return new ParametersAction(fv); 77 | } 78 | }); 79 | } 80 | } 81 | } 82 | if (totalFiles ==0) { 83 | noFilesFoundAction.failCheck(listener); 84 | } 85 | 86 | return result; 87 | } 88 | 89 | 90 | @OptionalExtension(requirePlugins = "parameterized-trigger") 91 | public static class DescriptorImpl extends AbstractBuildParameterFactoryDescriptor { 92 | @Override 93 | public String getDisplayName() { 94 | return "Multiple Binary Files (not meant to be used)"; 95 | } 96 | } 97 | 98 | private static final Logger LOGGER = Logger.getLogger(MultipleBinaryFileParameterFactory.class.getName()); 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import edu.umd.cs.findbugs.annotations.CheckForNull; 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | import hudson.AbortException; 7 | import hudson.FilePath; 8 | import hudson.Launcher; 9 | import hudson.Util; 10 | import hudson.model.AbstractBuild; 11 | import hudson.model.AbstractProject; 12 | import hudson.model.Action; 13 | import hudson.model.AutoCompletionCandidates; 14 | import hudson.model.BuildListener; 15 | import hudson.model.Item; 16 | import hudson.model.ItemGroup; 17 | import hudson.model.Result; 18 | import hudson.model.TaskListener; 19 | import hudson.plugins.parameterizedtrigger.AbstractBuildParameterFactory; 20 | import hudson.plugins.parameterizedtrigger.AbstractBuildParameters; 21 | import hudson.plugins.parameterizedtrigger.BlockableBuildTriggerConfig; 22 | import hudson.plugins.parameterizedtrigger.BlockingBehaviour; 23 | import hudson.plugins.parameterizedtrigger.TriggerBuilder; 24 | import hudson.tasks.BuildStepDescriptor; 25 | import hudson.tasks.Builder; 26 | import hudson.tasks.junit.JUnitResultArchiver; 27 | import java.io.IOException; 28 | import java.io.OutputStream; 29 | import java.io.OutputStreamWriter; 30 | import java.io.PrintWriter; 31 | import java.nio.charset.StandardCharsets; 32 | import java.util.ArrayList; 33 | import java.util.Collections; 34 | import java.util.List; 35 | import java.util.concurrent.atomic.AtomicInteger; 36 | import java.util.logging.Logger; 37 | import org.jenkinsci.plugins.parallel_test_executor.testmode.TestMode; 38 | import org.jenkinsci.plugins.variant.OptionalExtension; 39 | import org.kohsuke.stapler.AncestorInPath; 40 | import org.kohsuke.stapler.DataBoundConstructor; 41 | import org.kohsuke.stapler.DataBoundSetter; 42 | import org.kohsuke.stapler.QueryParameter; 43 | 44 | /** 45 | * @author Kohsuke Kawaguchi 46 | */ 47 | public class ParallelTestExecutor extends Builder { 48 | private static final Logger LOGGER = Logger.getLogger(ParallelTestExecutor.class.getName()); 49 | public static final int NUMBER_OF_BUILDS_TO_SEARCH = 20; 50 | public static final ImmutableSet RESULTS_OF_BUILDS_TO_CONSIDER = ImmutableSet.of(Result.SUCCESS, Result.UNSTABLE); 51 | 52 | private final Parallelism parallelism; 53 | private final String testJob; 54 | private final String patternFile; 55 | private String includesPatternFile; 56 | private final String testReportFiles; 57 | private final boolean doNotArchiveTestResults; 58 | private final List parameters; 59 | private TestMode testMode; 60 | 61 | @DataBoundConstructor 62 | public ParallelTestExecutor(Parallelism parallelism, String testJob, String patternFile, String testReportFiles, boolean archiveTestResults, List parameters) { 63 | this.parallelism = parallelism; 64 | this.testJob = testJob; 65 | this.patternFile = patternFile; 66 | this.testReportFiles = testReportFiles; 67 | this.parameters = parameters; 68 | this.doNotArchiveTestResults = !archiveTestResults; 69 | } 70 | 71 | public Parallelism getParallelism() { 72 | return parallelism; 73 | } 74 | 75 | public String getTestJob() { 76 | return testJob; 77 | } 78 | 79 | public String getPatternFile() { 80 | return patternFile; 81 | } 82 | 83 | @CheckForNull 84 | public String getIncludesPatternFile() { 85 | return includesPatternFile; 86 | } 87 | 88 | @DataBoundSetter 89 | public void setIncludesPatternFile(String includesPatternFile) { 90 | this.includesPatternFile = Util.fixEmpty(includesPatternFile); 91 | } 92 | 93 | public String getTestReportFiles() { 94 | return testReportFiles; 95 | } 96 | 97 | public boolean isArchiveTestResults() { 98 | return !doNotArchiveTestResults; 99 | } 100 | 101 | @SuppressWarnings("unused") // jetty 102 | public TestMode getTestMode() { 103 | return TestMode.fixDefault(testMode); 104 | } 105 | 106 | @DataBoundSetter 107 | public void setTestMode(TestMode testMode) { 108 | this.testMode = testMode; 109 | } 110 | 111 | public List getParameters() { 112 | return parameters; 113 | } 114 | 115 | /** 116 | * {@link TestEntity}es are divided into multiple sets of roughly equal size. 117 | */ 118 | @SuppressFBWarnings(value="EQ_COMPARETO_USE_OBJECT_EQUALS", justification="We wish to consider knapsacks as distinct items, just sort by size.") 119 | static class Knapsack implements Comparable { 120 | /** 121 | * Total duration of all {@link TestEntity}es that are in this knapsack. 122 | */ 123 | long total; 124 | 125 | void add(TestEntity tc) { 126 | assert tc.knapsack == null; 127 | tc.knapsack = this; 128 | total += tc.duration; 129 | } 130 | 131 | public int compareTo(Knapsack that) { 132 | long l = this.total - that.total; 133 | if (l < 0) return -1; 134 | if (l > 0) return 1; 135 | return 0; 136 | } 137 | } 138 | 139 | @Override 140 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { 141 | FilePath workspace = build.getWorkspace(); 142 | if (workspace == null) { 143 | throw new AbortException("no workspace"); 144 | } 145 | FilePath dir = workspace.child("test-splits"); 146 | dir.deleteRecursive(); 147 | List splits = Splitter.findTestSplits(parallelism, testMode, build, listener, includesPatternFile != null, 148 | null, build.getWorkspace()); 149 | for (int i = 0; i < splits.size(); i++) { 150 | InclusionExclusionPattern pattern = splits.get(i); 151 | try (OutputStream os = dir.child("split." + i + "." + (pattern.isIncludes() ? "include" : "exclude") + ".txt").write(); 152 | OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8); 153 | PrintWriter pw = new PrintWriter(osw)) { 154 | for (String filePattern : pattern.getList()) { 155 | pw.println(filePattern); 156 | } 157 | } 158 | } 159 | 160 | createTriggerBuilder().perform(build, launcher, listener); 161 | 162 | if (isArchiveTestResults()) { 163 | tally(build, launcher, listener); 164 | } 165 | 166 | return true; 167 | } 168 | 169 | /** 170 | * Collects all the test reports 171 | */ 172 | private void tally(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { 173 | new JUnitResultArchiver("test-splits/reports/**/*.xml", false, null).perform(build, launcher, listener); 174 | } 175 | 176 | /** 177 | * Create {@link hudson.plugins.parameterizedtrigger.TriggerBuilder} for launching test jobs. 178 | */ 179 | private TriggerBuilder createTriggerBuilder() { 180 | // to let the caller job do a clean up, don't let the failure in the test job early-terminate the build process 181 | // that's why the first argument is ABORTED. 182 | BlockingBehaviour blocking = new BlockingBehaviour(Result.ABORTED, Result.UNSTABLE, Result.FAILURE); 183 | final AtomicInteger iota = new AtomicInteger(0); 184 | 185 | List parameterList = new ArrayList<>(); 186 | parameterList.add( 187 | // put a marker action that we look for to collect test reports 188 | new AbstractBuildParameters() { 189 | @Override 190 | public Action getAction(AbstractBuild build, TaskListener listener) throws IOException, InterruptedException, DontTriggerException { 191 | return new TestCollector(build, ParallelTestExecutor.this, iota.incrementAndGet()); 192 | } 193 | }); 194 | if (parameters != null) { 195 | parameterList.addAll(parameters); 196 | } 197 | 198 | // actual logic of child process triggering is left up to the parameterized build 199 | List parameterBindings = new ArrayList<>(); 200 | parameterBindings.add(new MultipleBinaryFileParameterFactory.ParameterBinding(getPatternFile(), "test-splits/split.*.exclude.txt")); 201 | if (includesPatternFile != null) { 202 | parameterBindings.add(new MultipleBinaryFileParameterFactory.ParameterBinding(getIncludesPatternFile(), "test-splits/split.*.include.txt")); 203 | } 204 | MultipleBinaryFileParameterFactory factory = new MultipleBinaryFileParameterFactory(parameterBindings); 205 | BlockableBuildTriggerConfig config = new BlockableBuildTriggerConfig( 206 | testJob, 207 | blocking, 208 | Collections.singletonList(factory), 209 | parameterList 210 | ); 211 | 212 | return new TriggerBuilder(config); 213 | } 214 | 215 | @OptionalExtension(requirePlugins = "parameterized-trigger") 216 | public static class DescriptorImpl extends BuildStepDescriptor { 217 | @Override 218 | public boolean isApplicable(Class aClass) { 219 | return true; 220 | } 221 | 222 | public AutoCompletionCandidates doAutoCompleteTestJob(@QueryParameter String value, @AncestorInPath Item self, @AncestorInPath ItemGroup container) { 223 | return AutoCompletionCandidates.ofJobNames(AbstractProject.class, value, self, container); 224 | } 225 | 226 | @Override 227 | public String getDisplayName() { 228 | return "Parallel test job execution"; 229 | } 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/Parallelism.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import hudson.model.AbstractDescribableImpl; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Strategy that determines how many knapsacks we'll create. 9 | * 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | public abstract class Parallelism extends AbstractDescribableImpl { 13 | /*package*/ Parallelism() {} 14 | 15 | public abstract int calculate(List tests); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/RunListenerImpl.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import hudson.model.AbstractBuild; 4 | import hudson.model.TaskListener; 5 | import hudson.model.listeners.RunListener; 6 | 7 | import edu.umd.cs.findbugs.annotations.NonNull; 8 | import org.jenkinsci.plugins.variant.OptionalExtension; 9 | 10 | /** 11 | * Looks for {@link TestCollector} in the build and collects the test reports. 12 | * 13 | * @author Kohsuke Kawaguchi 14 | */ 15 | @OptionalExtension(requirePlugins = "parameterized-trigger") 16 | public class RunListenerImpl extends RunListener> { 17 | @Override 18 | public void onCompleted(AbstractBuild build, @NonNull TaskListener listener) { 19 | TestCollector m = build.getAction(TestCollector.class); 20 | if (m!=null) 21 | m.collect(build,listener); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/SplitStep.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import hudson.Extension; 5 | import hudson.FilePath; 6 | import hudson.Util; 7 | import hudson.model.Run; 8 | import hudson.model.TaskListener; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Set; 12 | import org.jenkinsci.plugins.parallel_test_executor.testmode.TestMode; 13 | import org.jenkinsci.plugins.workflow.steps.Step; 14 | import org.jenkinsci.plugins.workflow.steps.StepContext; 15 | import org.jenkinsci.plugins.workflow.steps.StepDescriptor; 16 | import org.jenkinsci.plugins.workflow.steps.StepExecution; 17 | import org.jenkinsci.plugins.workflow.steps.SynchronousStepExecution; 18 | import org.kohsuke.stapler.DataBoundConstructor; 19 | import org.kohsuke.stapler.DataBoundSetter; 20 | 21 | /** 22 | * Allows the splitting logic to be accessed from a workflow. 23 | */ 24 | public final class SplitStep extends Step { 25 | 26 | private final Parallelism parallelism; 27 | 28 | private boolean generateInclusions; 29 | 30 | private String stage; 31 | 32 | private TestMode testMode; 33 | 34 | @DataBoundConstructor 35 | public SplitStep(Parallelism parallelism) { 36 | this.parallelism = parallelism; 37 | } 38 | 39 | public Parallelism getParallelism() { 40 | return parallelism; 41 | } 42 | 43 | public boolean isGenerateInclusions() { 44 | return generateInclusions; 45 | } 46 | 47 | @DataBoundSetter 48 | public void setGenerateInclusions(boolean generateInclusions) { 49 | this.generateInclusions = generateInclusions; 50 | } 51 | 52 | @SuppressWarnings("unused") // jelly 53 | public TestMode getTestMode() { 54 | return TestMode.fixDefault(testMode); 55 | } 56 | 57 | @DataBoundSetter 58 | public void setTestMode(TestMode testMode) { 59 | this.testMode = testMode; 60 | } 61 | 62 | /** 63 | * @param estimateTestsFromFiles true if we should estimate the tests from the files 64 | * @deprecated use {@link #setTestMode(TestMode)} instead. 65 | */ 66 | @Deprecated 67 | @DataBoundSetter 68 | public void setEstimateTestsFromFiles(boolean estimateTestsFromFiles) { 69 | // no-op 70 | } 71 | 72 | /** 73 | * Method kept only to make the snippet generator happy. 74 | * 75 | * @return true if we should estimate the tests from the files 76 | * @deprecated use {@link #getTestMode()} ()} instead 77 | */ 78 | @Deprecated 79 | public boolean isEstimateTestsFromFiles() { 80 | return false; 81 | } 82 | 83 | public String getStage() { 84 | return stage; 85 | } 86 | 87 | @DataBoundSetter 88 | public void setStage(String stage) { 89 | this.stage = Util.fixEmpty(stage); 90 | } 91 | 92 | @Override 93 | public StepExecution start(StepContext context) throws Exception { 94 | return new Execution(context, this); 95 | } 96 | 97 | @Extension 98 | public static final class DescriptorImpl extends StepDescriptor { 99 | 100 | @Override 101 | public Set> getRequiredContext() { 102 | return ImmutableSet.of(TaskListener.class, Run.class); 103 | } 104 | 105 | @Override 106 | public String getFunctionName() { 107 | return "splitTests"; 108 | } 109 | 110 | @Override 111 | public String getDisplayName() { 112 | return "Split Test Runs"; 113 | } 114 | } 115 | 116 | private static final class Execution extends SynchronousStepExecution> { 117 | 118 | private static final long serialVersionUID = 1L; 119 | 120 | private final transient SplitStep step; 121 | 122 | Execution(StepContext context, SplitStep step) { 123 | super(context); 124 | this.step = step; 125 | } 126 | 127 | @Override 128 | protected List run() throws Exception { 129 | StepContext context = getContext(); 130 | Run build = context.get(Run.class); 131 | TaskListener listener = context.get(TaskListener.class); 132 | FilePath path = context.get(FilePath.class); 133 | 134 | if (step.generateInclusions) { 135 | return Splitter.findTestSplits(step.parallelism, step.testMode, build, listener, step.generateInclusions, 136 | step.stage, path); 137 | } else { 138 | List> result = new ArrayList<>(); 139 | for (InclusionExclusionPattern pattern : Splitter.findTestSplits(step.parallelism, step.testMode, build, listener, 140 | step.generateInclusions, step.stage, path)) { 141 | result.add(pattern.getList()); 142 | } 143 | return result; 144 | } 145 | } 146 | 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/Splitter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2024 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.parallel_test_executor; 26 | 27 | import com.google.common.base.Predicate; 28 | import edu.umd.cs.findbugs.annotations.CheckForNull; 29 | import edu.umd.cs.findbugs.annotations.NonNull; 30 | import edu.umd.cs.findbugs.annotations.Nullable; 31 | import hudson.FilePath; 32 | import hudson.console.ModelHyperlinkNote; 33 | import hudson.model.Item; 34 | import hudson.model.Job; 35 | import hudson.model.Run; 36 | import hudson.model.TaskListener; 37 | import hudson.tasks.junit.ClassResult; 38 | import hudson.tasks.test.AbstractTestResultAction; 39 | import hudson.tasks.test.TabulatedResult; 40 | import hudson.tasks.test.TestResult; 41 | import java.util.ArrayDeque; 42 | import java.util.ArrayList; 43 | import java.util.Collections; 44 | import java.util.List; 45 | import java.util.Map; 46 | import java.util.PriorityQueue; 47 | import java.util.TreeMap; 48 | import java.util.logging.Level; 49 | import java.util.logging.Logger; 50 | import java.util.stream.Collectors; 51 | import jenkins.scm.api.SCMHead; 52 | import jenkins.scm.api.mixin.ChangeRequestSCMHead; 53 | import static org.jenkinsci.plugins.parallel_test_executor.ParallelTestExecutor.NUMBER_OF_BUILDS_TO_SEARCH; 54 | import static org.jenkinsci.plugins.parallel_test_executor.ParallelTestExecutor.RESULTS_OF_BUILDS_TO_CONSIDER; 55 | import org.jenkinsci.plugins.parallel_test_executor.testmode.TestMode; 56 | import org.jenkinsci.plugins.workflow.actions.LabelAction; 57 | import org.jenkinsci.plugins.workflow.flow.FlowExecution; 58 | import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; 59 | import org.jenkinsci.plugins.workflow.graph.FlowNode; 60 | import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; 61 | 62 | class Splitter { 63 | 64 | private static final Logger LOGGER = Logger.getLogger(Splitter.class.getName()); 65 | 66 | static List findTestSplits(Parallelism parallelism, @CheckForNull TestMode inputTestMode, Run build, TaskListener listener, 67 | boolean generateInclusions, 68 | @CheckForNull final String stageName, @CheckForNull FilePath workspace) throws InterruptedException { 69 | TestMode testMode = inputTestMode == null ? TestMode.getDefault() : inputTestMode; 70 | TestResult tr = findPreviousTestResult(build, listener); 71 | Map data = new TreeMap<>(); 72 | if (tr != null) { 73 | tr = mayFilterByStageName(tr, stageName, listener); 74 | collect(tr, data, testMode); 75 | } else { 76 | listener.getLogger().println("No record available, try to find test classes"); 77 | data = testMode.estimate(workspace, listener); 78 | if(data.isEmpty()) { 79 | listener.getLogger().println("No test classes was found, so executing everything in one place"); 80 | return List.of(new InclusionExclusionPattern(List.of(), false)); 81 | } 82 | } 83 | 84 | // sort in the descending order of the duration 85 | List sorted = new ArrayList<>(data.values()); 86 | Collections.sort(sorted); 87 | 88 | // degree of the parallelism. we need minimum 1 89 | final int n = Math.max(1, parallelism.calculate(sorted)); 90 | 91 | List knapsacks = new ArrayList<>(n); 92 | for (int i = 0; i < n; i++) 93 | knapsacks.add(new ParallelTestExecutor.Knapsack()); 94 | 95 | /* 96 | This packing problem is a NP-complete problem, so we solve 97 | this simply by a greedy algorithm. We pack heavier items first, 98 | and the result should be of roughly equal size 99 | */ 100 | PriorityQueue q = new PriorityQueue<>(knapsacks); 101 | for (var testEntity : sorted) { 102 | ParallelTestExecutor.Knapsack k = q.poll(); 103 | k.add(testEntity); 104 | q.add(k); 105 | } 106 | 107 | long total = 0, min = Long.MAX_VALUE, max = Long.MIN_VALUE; 108 | for (ParallelTestExecutor.Knapsack k : knapsacks) { 109 | total += k.total; 110 | max = Math.max(max, k.total); 111 | min = Math.min(min, k.total); 112 | } 113 | long average = total / n; 114 | long variance = 0; 115 | for (ParallelTestExecutor.Knapsack k : knapsacks) { 116 | variance += pow(k.total - average); 117 | } 118 | variance /= n; 119 | long stddev = (long) Math.sqrt(variance); 120 | listener.getLogger().printf("%d test %s (%dms) divided into %d sets. Min=%dms, Average=%dms, Max=%dms, stddev=%dms%n", 121 | data.size(), testMode.getWord(), total, n, min, average, max, stddev); 122 | 123 | List r = new ArrayList<>(); 124 | for (int i = 0; i < n; i++) { 125 | ParallelTestExecutor.Knapsack k = knapsacks.get(i); 126 | boolean shouldIncludeElements = generateInclusions && i != 0; 127 | List elements = sorted.stream().filter(testEntity -> shouldIncludeElements == (testEntity.knapsack == k)) 128 | .flatMap(testEntity -> testEntity.getElements().stream()) 129 | .collect(Collectors.toList()); 130 | r.add(new InclusionExclusionPattern(elements, shouldIncludeElements)); 131 | } 132 | return r; 133 | } 134 | 135 | @NonNull 136 | private static TestResult mayFilterByStageName(@NonNull TestResult tr, @CheckForNull String stageName, @NonNull TaskListener listener) { 137 | Run run = tr.getRun(); 138 | if (stageName != null) { 139 | listener.getLogger().println("Looking for stage \"" + stageName + "\" in " + run.getFullDisplayName()); 140 | FlowExecution execution = resolveFlowExecution(run, listener); 141 | if (execution != null) { 142 | FlowNode stageId = new DepthFirstScanner().findFirstMatch(execution, new StageNamePredicate(stageName)); 143 | if (stageId != null) { 144 | listener.getLogger().println("Found stage \"" + stageName + "\" in " + run.getFullDisplayName()); 145 | tr = ((hudson.tasks.junit.TestResult) tr).getResultForPipelineBlock(stageId.getId()); 146 | } else { 147 | listener.getLogger().println("No stage \"" + stageName + "\" found in " + run.getFullDisplayName()); 148 | } 149 | } else { 150 | listener.getLogger().println("No flow execution found in " + run.getFullDisplayName()); 151 | } 152 | } 153 | return tr; 154 | } 155 | 156 | @CheckForNull 157 | private static FlowExecution resolveFlowExecution(Run prevRun, TaskListener listener) { 158 | if (prevRun instanceof FlowExecutionOwner.Executable) { 159 | FlowExecutionOwner owner = ((FlowExecutionOwner.Executable) prevRun).asFlowExecutionOwner(); 160 | if (owner != null) { 161 | return owner.getOrNull(); 162 | } else { 163 | listener.getLogger().println("No flow execution owner found in " + prevRun.getFullDisplayName()); 164 | } 165 | } else { 166 | listener.getLogger().println("Previous run doesn't have the expected type: " + prevRun); 167 | } 168 | return null; 169 | } 170 | 171 | private static long pow(long l) { 172 | return l * l; 173 | } 174 | 175 | /** 176 | * Visits the structure inside {@link hudson.tasks.test.TestResult}. 177 | */ 178 | private static void collect(TestResult r, Map data, TestMode testMode) { 179 | var queue = new ArrayDeque(); 180 | queue.push(r); 181 | while (!queue.isEmpty()) { 182 | var current = queue.pop(); 183 | if (current instanceof ClassResult) { 184 | var classResult = (ClassResult) current; 185 | LOGGER.log(Level.FINE, () -> "Retrieving test entities from " + classResult.getFullName()); 186 | data.putAll(testMode.getTestEntitiesMap(classResult)); 187 | } else if (current instanceof TabulatedResult) { 188 | LOGGER.log(Level.FINE, () -> "Considering children of " + current.getFullName()); 189 | queue.addAll(((TabulatedResult) current).getChildren()); 190 | } else { 191 | LOGGER.log(Level.FINE, () -> "Ignoring " + current.getFullName()); 192 | } 193 | } 194 | } 195 | 196 | private static TestResult findPreviousTestResult(Run b, TaskListener listener) { 197 | Job project = b.getParent(); 198 | // Look for test results starting with the previous build 199 | TestResult result = getTestResult(project, b.getPreviousBuild(), listener); 200 | if (result == null) { 201 | // Look for test results from the target branch builds if this is a change request. 202 | SCMHead head = SCMHead.HeadByItem.findHead(project); 203 | if (head instanceof ChangeRequestSCMHead) { 204 | SCMHead target = ((ChangeRequestSCMHead) head).getTarget(); 205 | Item targetBranch = project.getParent().getItem(target.getName()); 206 | if (targetBranch instanceof Job) { 207 | result = getTestResult(project, ((Job) targetBranch).getLastBuild(), listener); 208 | } 209 | } 210 | } 211 | return result; 212 | } 213 | 214 | 215 | static TestResult getTestResult(Job originProject, Run b, TaskListener listener) { 216 | TestResult result = null; 217 | for (int i = 0; i < NUMBER_OF_BUILDS_TO_SEARCH; i++) {// limit the search to a small number to avoid loading too much 218 | if (b == null) break; 219 | if (RESULTS_OF_BUILDS_TO_CONSIDER.contains(b.getResult()) && !b.isBuilding()) { 220 | String hyperlink = ModelHyperlinkNote.encodeTo('/' + b.getUrl(), originProject != b.getParent() ? b.getFullDisplayName() : b.getDisplayName()); 221 | try { 222 | AbstractTestResultAction tra = b.getAction(AbstractTestResultAction.class); 223 | if (tra != null) { 224 | Object o = tra.getResult(); 225 | if (o instanceof TestResult) { 226 | TestResult tr = (TestResult) o; 227 | if (tr.getTotalCount() == 0) { 228 | listener.getLogger().printf("Build %s has no loadable test results (supposed count %d), skipping%n", hyperlink, tra.getTotalCount()); 229 | } else { 230 | listener.getLogger().printf("Using build %s as reference%n", hyperlink); 231 | result = tr; 232 | break; 233 | } 234 | } 235 | } 236 | } catch (RuntimeException e) { 237 | e.printStackTrace(listener.error("Failed to load (corrupt?) build %s, skipping%n", hyperlink)); 238 | } 239 | } 240 | b = b.getPreviousBuild(); 241 | } 242 | return result; 243 | } 244 | 245 | private static class StageNamePredicate implements Predicate { 246 | private final String stageName; 247 | StageNamePredicate(@NonNull String stageName) { 248 | this.stageName = stageName; 249 | } 250 | @Override 251 | public boolean apply(@Nullable FlowNode input) { 252 | if (input != null) { 253 | LabelAction labelAction = input.getPersistentAction(LabelAction.class); 254 | return labelAction != null && stageName.equals(labelAction.getDisplayName()); 255 | } 256 | return false; 257 | } 258 | } 259 | 260 | private Splitter() {} 261 | 262 | } 263 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/TestCase.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import hudson.tasks.junit.CaseResult; 4 | import java.util.List; 5 | 6 | /** 7 | * Execution time of a specific test case. 8 | */ 9 | public class TestCase extends TestEntity { 10 | String output; 11 | 12 | public TestCase(CaseResult cr) { 13 | this(cr, false); 14 | } 15 | 16 | public TestCase(CaseResult cr, boolean withClassName) { 17 | if (withClassName) { 18 | this.output = cr.getFullName(); 19 | } else { 20 | this.output = cr.getName(); 21 | } 22 | this.duration = (long)(cr.getDuration()*1000); // milliseconds is a good enough precision for us 23 | } 24 | 25 | @Override 26 | public String getKey() { 27 | return output; 28 | } 29 | 30 | @Override 31 | public List getElements() { 32 | return List.of(output); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return output; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/TestClass.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import hudson.tasks.junit.ClassResult; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | /** 8 | * Execution time of a specific test class. 9 | */ 10 | public class TestClass extends TestEntity { 11 | 12 | final String className; 13 | 14 | public TestClass(ClassResult cr) { 15 | String pkgName = cr.getParent().getName(); 16 | if (pkgName.equals("(root)")) // UGH 17 | pkgName = ""; 18 | else 19 | pkgName += '.'; 20 | this.className = pkgName+cr.getName(); 21 | this.duration = (long)(cr.getDuration()*1000); // milliseconds is a good enough precision for us 22 | } 23 | 24 | //for test estimation for first run 25 | public TestClass(String className){ 26 | this.className = className; 27 | this.duration = 10; 28 | } 29 | 30 | @Override 31 | public String getKey() { 32 | return className; 33 | } 34 | 35 | @Override 36 | public List getElements() { 37 | var sanitizedClassName = className.replace('.', '/'); 38 | return List.of(sanitizedClassName +".java", sanitizedClassName +".class"); 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return className +".extension"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/TestCollector.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import hudson.FilePath; 5 | import hudson.Util; 6 | import hudson.console.ModelHyperlinkNote; 7 | import hudson.model.AbstractBuild; 8 | import hudson.model.InvisibleAction; 9 | import hudson.model.TaskListener; 10 | import hudson.remoting.VirtualChannel; 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.io.Serializable; 14 | import jenkins.MasterToSlaveFileCallable; 15 | import org.apache.tools.ant.BuildException; 16 | import org.apache.tools.ant.taskdefs.Copy; 17 | 18 | /** 19 | * Runs at the end of a triggered test sub-task and collects test reports back to the master. 20 | * 21 | * @author Kohsuke Kawaguchi 22 | */ 23 | class TestCollector extends InvisibleAction implements Serializable { 24 | private static final long serialVersionUID = -592264249944063364L; 25 | 26 | // none of this is meant to persist 27 | private final transient AbstractBuild collector; 28 | private final transient ParallelTestExecutor testExecutor; 29 | @SuppressFBWarnings(value="SE_TRANSIENT_FIELD_NOT_RESTORED", justification="not needed after initial use") 30 | private final transient int ordinal; 31 | 32 | public TestCollector(AbstractBuild collector, ParallelTestExecutor testExecutor, int ordinal) { 33 | this.testExecutor = testExecutor; 34 | assert collector!=null; 35 | this.collector = collector; 36 | this.ordinal = ordinal; 37 | } 38 | 39 | /** 40 | * Collects the test reports from the sub build to the master. 41 | */ 42 | public void collect(AbstractBuild build, TaskListener listener) { 43 | if (collector==null) return; // must be deserialized. pretend as if this action doesn't exist. 44 | 45 | try { 46 | listener.getLogger().println("Collecting test reports for the master build: "+ ModelHyperlinkNote.encodeTo(collector)); 47 | 48 | final FilePath src = build.getWorkspace(); 49 | if (src==null) return; // trying to be defensive in case of catastrophic build failure 50 | FilePath workspace = collector.getWorkspace(); 51 | if (workspace == null) { 52 | return; // ditto 53 | } 54 | final FilePath dst = workspace.child("test-splits/reports/"+ordinal); 55 | dst.mkdirs(); 56 | 57 | final String includes = testExecutor.getTestReportFiles(); 58 | 59 | if (src.getChannel()==dst.getChannel()) { 60 | // fast case where a direct copy is possible 61 | // TODO: move this to the core. copyRecursiveTo + 'archive' semantics 62 | src.act(new MasterToSlaveFileCallable() { 63 | private static final long serialVersionUID = 1L; 64 | public Integer invoke(File base, VirtualChannel channel) throws IOException { 65 | if(!base.exists()) return 0; 66 | assert dst.getChannel()==null; 67 | 68 | try { 69 | class CopyImpl extends Copy { 70 | private int copySize; 71 | 72 | public CopyImpl() { 73 | setProject(new org.apache.tools.ant.Project()); 74 | } 75 | 76 | @Override 77 | protected void doFileOperations() { 78 | copySize = super.fileCopyMap.size(); 79 | super.doFileOperations(); 80 | } 81 | 82 | public int getNumCopied() { 83 | return copySize; 84 | } 85 | } 86 | 87 | CopyImpl copyTask = new CopyImpl(); 88 | copyTask.setTodir(new File(dst.getRemote())); 89 | copyTask.addFileset(Util.createFileSet(base,includes)); 90 | copyTask.setOverwrite(true); 91 | copyTask.setIncludeEmptyDirs(false); 92 | copyTask.setPreserveLastModified(true); // this is the only change from stock 'copyRecursiveTo' 93 | 94 | copyTask.execute(); 95 | return copyTask.getNumCopied(); 96 | } catch (BuildException e) { 97 | throw new IOException("Failed to copy " + base + "/" + includes + " to " + dst, e); 98 | } 99 | } 100 | }); 101 | } else 102 | if (src.getChannel()==null || dst.getChannel()==null) { 103 | // this uses tar, so the timestamp gets preserved 104 | src.copyRecursiveTo(includes, dst); 105 | } else { 106 | // copy via master 107 | File t = Util.createTempDir(); 108 | FilePath tmp = new FilePath(t); 109 | try { 110 | src.copyRecursiveTo(testExecutor.getTestReportFiles(), tmp); 111 | tmp.copyRecursiveTo(dst); 112 | } finally { 113 | Util.deleteRecursive(t); 114 | } 115 | } 116 | } catch (IOException | InterruptedException e) { 117 | e.printStackTrace(listener.error("Failed to aggregate test reports for "+collector.getFullDisplayName())); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/TestEntity.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import java.util.List; 5 | import org.jenkinsci.plugins.parallel_test_executor.ParallelTestExecutor.Knapsack; 6 | 7 | /** 8 | * Represents a result of the test parallelization granularity of interest. 9 | */ 10 | @SuppressFBWarnings(value="EQ_COMPARETO_USE_OBJECT_EQUALS", justification="Cf. justification in Knapsack.") 11 | public abstract class TestEntity implements Comparable { 12 | 13 | protected long duration; 14 | /** 15 | * Knapsack that this test class belongs to. 16 | */ 17 | protected Knapsack knapsack; 18 | 19 | protected TestEntity() {} 20 | 21 | public long getDuration() { 22 | return duration; 23 | } 24 | 25 | @Override 26 | public int compareTo(TestEntity that) { 27 | long l = this.duration - that.duration; 28 | // sort them in the descending order 29 | if (l>0) return -1; 30 | if (l<0) return 1; 31 | return 0; 32 | } 33 | 34 | public abstract String getKey(); 35 | 36 | public abstract List getElements(); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/TimeDrivenParallelism.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Descriptor; 5 | import org.kohsuke.stapler.DataBoundConstructor; 6 | 7 | import java.util.List; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import org.jenkinsci.Symbol; 11 | 12 | /** 13 | * @author Kohsuke Kawaguchi 14 | */ 15 | public class TimeDrivenParallelism extends Parallelism { 16 | public final int mins; 17 | 18 | @DataBoundConstructor 19 | public TimeDrivenParallelism(int mins) { 20 | this.mins = mins; 21 | } 22 | 23 | @Override 24 | public int calculate(List tests) { 25 | long total=0; 26 | for (TestEntity test : tests) { 27 | total += test.duration; 28 | } 29 | long chunk = TimeUnit.MINUTES.toMillis(mins); 30 | return (int)((total+chunk-1)/chunk); 31 | } 32 | 33 | @Symbol("time") 34 | @Extension 35 | public static class DescriptorImpl extends Descriptor { 36 | @Override 37 | public String getDisplayName() { 38 | return "Fixed time (minutes) for each batch"; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/testmode/JavaClassName.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor.testmode; 2 | 3 | import static java.util.function.Function.identity; 4 | 5 | import edu.umd.cs.findbugs.annotations.NonNull; 6 | import hudson.Extension; 7 | import hudson.FilePath; 8 | import hudson.model.Descriptor; 9 | import hudson.model.TaskListener; 10 | import hudson.tasks.junit.CaseResult; 11 | import hudson.tasks.junit.ClassResult; 12 | import java.io.IOException; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | import java.util.TreeMap; 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | import java.util.stream.Collectors; 20 | import org.jenkinsci.Symbol; 21 | import org.jenkinsci.plugins.parallel_test_executor.TestClass; 22 | import org.jenkinsci.plugins.parallel_test_executor.TestEntity; 23 | import org.kohsuke.stapler.DataBoundConstructor; 24 | 25 | /** 26 | * This mode works best with java projects. 27 | *

28 | * Each exclusion/inclusion generates two lines by replacing "." with "/" in the fully qualified test 29 | * class name and appending ".java" to one line and ".class" to the second line. 30 | *

31 | *

32 | * It is also able to estimate tests to run from the workspace content if no historical context could be found. 33 | *

34 | */ 35 | public class JavaClassName extends TestMode { 36 | private static final String PATTERNS = String.join(",", List.of( 37 | "**/src/test/java/**/Test*.java", 38 | "**/src/test/java/**/*Test.java", 39 | "**/src/test/java/**/*Tests.java", 40 | "**/src/test/java/**/*TestCase.java" 41 | )); 42 | private static final Pattern TEST = Pattern.compile(".+/src/test/java/(.+)[.]java"); 43 | 44 | @DataBoundConstructor 45 | public JavaClassName() {} 46 | 47 | public boolean isSplitByCase() { 48 | return false; 49 | } 50 | 51 | public boolean useParameters() { 52 | return false; 53 | } 54 | 55 | @Override 56 | @NonNull 57 | public Map getTestEntitiesMap(@NonNull ClassResult classResult) { 58 | if (isSplitByCase()) { 59 | return classResult.getChildren().stream().map(cr -> new JavaTestCase(cr, useParameters())).collect(Collectors.toMap(JavaTestCase::getKey, identity(), JavaTestCase::new)); 60 | } else { 61 | TestClass testClass = new TestClass(classResult); 62 | return Map.of(testClass.getKey(), testClass); 63 | } 64 | } 65 | 66 | @Override 67 | public Map estimate(FilePath workspace, @NonNull TaskListener listener) throws InterruptedException { 68 | // TODO estimate test cases 69 | if (workspace == null) { 70 | return Map.of(); 71 | } 72 | Map data = new TreeMap<>(); 73 | try { 74 | for (FilePath test : workspace.list(PATTERNS)) { 75 | String testPath = test.getRemote().replace('\\', '/'); 76 | Matcher m = TEST.matcher(testPath); 77 | if (!m.matches() || m.groupCount() != 1) { 78 | throw new IllegalStateException(testPath + " didn't match expected format"); 79 | } 80 | String relativePath = m.group(1); // e.g. pkg/subpkg/SomeTest 81 | data.put(relativePath, new TestClass(relativePath)); 82 | } 83 | } catch (IOException e) { 84 | e.printStackTrace(listener.error("Unable to determine tests to run from files")); 85 | } 86 | return data; 87 | } 88 | 89 | @Override 90 | @NonNull 91 | public String getWord() { 92 | return isSplitByCase() ? "cases" : "classes"; 93 | } 94 | 95 | private static class JavaTestCase extends TestEntity { 96 | private final String output; 97 | private JavaTestCase(CaseResult cr, boolean useParams) { 98 | // Parameterized tests use ${fqdnClassName}#${methodName}[{parametersDescription}] format 99 | if (useParams) { 100 | this.output = cr.getClassName() + "#" + cr.getName(); 101 | } else { 102 | // Some surefire versions don't support parameters, so just drop them and will sum durations 103 | this.output = cr.getClassName() + "#" + cr.getName().split("\\[")[0]; 104 | } 105 | this.duration = (long)(cr.getDuration()*1000); // milliseconds is a good enough precision for us 106 | } 107 | 108 | /** 109 | * Merge two java test cases with the same name, summing their durations. 110 | */ 111 | private JavaTestCase(TestEntity te1, TestEntity te2) { 112 | if (!te1.getKey().equals(te2.getKey())) { 113 | throw new IllegalArgumentException("Test cases must have the same key"); 114 | } 115 | this.output = te1.getKey(); 116 | this.duration = te1.getDuration() + te2.getDuration(); 117 | } 118 | 119 | @Override 120 | public String getKey() { 121 | return output; 122 | } 123 | 124 | @Override 125 | public List getElements() { 126 | return List.of(output); 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | return output; 132 | } 133 | 134 | @Override 135 | public boolean equals(Object o) { 136 | if (this == o) return true; 137 | if (o == null || getClass() != o.getClass()) return false; 138 | JavaTestCase that = (JavaTestCase) o; 139 | return Objects.equals(output, that.output); 140 | } 141 | 142 | @Override 143 | public int hashCode() { 144 | return Objects.hash(output); 145 | } 146 | } 147 | 148 | @Extension 149 | @Symbol("javaClass") 150 | public static class DescriptorImpl extends Descriptor { 151 | @Override 152 | @NonNull 153 | public String getDisplayName() { 154 | return "By java class name"; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/testmode/JavaParameterizedTestCaseName.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to you under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package org.jenkinsci.plugins.parallel_test_executor.testmode; 18 | 19 | import edu.umd.cs.findbugs.annotations.NonNull; 20 | import hudson.Extension; 21 | import hudson.model.Descriptor; 22 | import org.jenkinsci.Symbol; 23 | import org.kohsuke.stapler.DataBoundConstructor; 24 | 25 | /** 26 | * This mode works best with java projects. 27 | *

28 | * Parallelize per java test case including parameters if present. 29 | *

30 | *

31 | * It is also able to estimate tests to run from the workspace content if no historical context could be found. 32 | *

33 | */ 34 | public class JavaParameterizedTestCaseName extends JavaClassName { 35 | @DataBoundConstructor 36 | public JavaParameterizedTestCaseName() { 37 | } 38 | 39 | @Override 40 | public boolean isSplitByCase() { 41 | return true; 42 | } 43 | 44 | @Override 45 | public boolean useParameters() { 46 | return true; 47 | } 48 | 49 | @Extension 50 | @Symbol("javaParamTestCase") 51 | public static class DescriptorImpl extends Descriptor { 52 | @Override 53 | @NonNull 54 | public String getDisplayName() { 55 | return "By Java test cases with parameters"; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/testmode/JavaTestCaseName.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor.testmode; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.model.Descriptor; 6 | import org.jenkinsci.Symbol; 7 | import org.kohsuke.stapler.DataBoundConstructor; 8 | 9 | /** 10 | * This mode works best with java projects. 11 | *

12 | * Parallelize per java test case ingoring parameters if present. 13 | *

14 | *

15 | * It is also able to estimate tests to run from the workspace content if no historical context could be found. 16 | *

17 | */ 18 | public class JavaTestCaseName extends JavaClassName { 19 | @DataBoundConstructor 20 | public JavaTestCaseName() {} 21 | 22 | @Override 23 | public boolean isSplitByCase() { 24 | return true; 25 | } 26 | 27 | @Override public boolean useParameters() { 28 | return false; 29 | } 30 | 31 | @Extension 32 | @Symbol("javaTestCase") 33 | public static class DescriptorImpl extends Descriptor { 34 | @Override 35 | @NonNull 36 | public String getDisplayName() { 37 | return "By Java test cases without parameters"; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/testmode/TestCaseName.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor.testmode; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.model.Descriptor; 6 | import hudson.tasks.junit.CaseResult; 7 | import hudson.tasks.junit.ClassResult; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import org.jenkinsci.Symbol; 11 | import org.jenkinsci.plugins.parallel_test_executor.TestCase; 12 | import org.jenkinsci.plugins.parallel_test_executor.TestEntity; 13 | import org.kohsuke.stapler.DataBoundConstructor; 14 | 15 | /** 16 | * Each exclusion/inclusion generates one line consisting of the test case name only. 17 | *

18 | * This is useful where a tool produces JUnit result XML containing unique test case names without any class prefix. 19 | *

20 | */ 21 | public class TestCaseName extends TestMode { 22 | 23 | @DataBoundConstructor 24 | public TestCaseName() { 25 | } 26 | 27 | public boolean isIncludeClassName() { 28 | return false; 29 | } 30 | 31 | @NonNull 32 | @Override 33 | public Map getTestEntitiesMap(@NonNull ClassResult classResult) { 34 | var result = new HashMap(); 35 | for (CaseResult caseResult : classResult.getChildren()) { 36 | var testCase = new TestCase(caseResult, isIncludeClassName()); 37 | result.put(testCase.getKey(), testCase); 38 | } 39 | return result; 40 | } 41 | 42 | @Override 43 | @NonNull 44 | public String getWord() { 45 | return "cases"; 46 | } 47 | 48 | @Extension 49 | @Symbol("testCase") 50 | public static class DescriptorImpl extends Descriptor { 51 | @Override 52 | @NonNull 53 | public String getDisplayName() { 54 | return "By test case name"; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/testmode/TestClassAndCaseName.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor.testmode; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.model.Descriptor; 6 | import org.jenkinsci.Symbol; 7 | import org.kohsuke.stapler.DataBoundConstructor; 8 | 9 | /** 10 | * Each exclusion/inclusion generates one line consisting of the class 11 | * and test case name on a className.testName format. 12 | */ 13 | public class TestClassAndCaseName extends TestCaseName { 14 | 15 | @DataBoundConstructor 16 | public TestClassAndCaseName() {} 17 | 18 | @Override 19 | public boolean isIncludeClassName() { 20 | return true; 21 | } 22 | 23 | @Extension 24 | @Symbol("qualifiedTestCase") 25 | public static class DescriptorImpl extends Descriptor { 26 | @Override 27 | @NonNull 28 | public String getDisplayName() { 29 | return "By test class and case name"; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/parallel_test_executor/testmode/TestMode.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor.testmode; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.ExtensionPoint; 5 | import hudson.FilePath; 6 | import hudson.model.AbstractDescribableImpl; 7 | import hudson.model.TaskListener; 8 | import hudson.tasks.junit.ClassResult; 9 | import java.util.Map; 10 | import org.jenkinsci.plugins.parallel_test_executor.TestEntity; 11 | 12 | /** 13 | * Extension point returning a list of test entities either from previous runs or estimated from the workspace. 14 | */ 15 | public abstract class TestMode extends AbstractDescribableImpl implements ExtensionPoint { 16 | /** 17 | * @param classResult The initial class result 18 | * @return a Map of test entities, keyed by their unique key 19 | */ 20 | @NonNull 21 | public abstract Map getTestEntitiesMap(@NonNull ClassResult classResult); 22 | 23 | /** 24 | * This method will be called if no historical test results can be found. In that case, an estimate can be provided from the workspace content. 25 | * @param workspace The current directory where tests are expected to be found. 26 | * @param listener The build listener if any output needs to be logged. 27 | * @return a Map of test entities, keyed by their unique key 28 | * @throws InterruptedException if the build get interrupted while executing this method. 29 | */ 30 | public Map estimate(FilePath workspace, @NonNull TaskListener listener) throws InterruptedException { 31 | return Map.of(); 32 | } 33 | 34 | /** 35 | * @return a description of the test entity type that is used for splitting, e.g. "cases" 36 | */ 37 | @NonNull 38 | public abstract String getWord(); 39 | 40 | /** 41 | * @return the default implementation for this extension point, if none is defined, 42 | */ 43 | public static TestMode getDefault() { 44 | return new JavaClassName(); 45 | } 46 | 47 | public static TestMode fixDefault(TestMode testMode) { 48 | if (testMode == null) { 49 | return null; 50 | } 51 | return JavaClassName.class.equals(testMode.getClass()) ? null : testMode; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | Adds a system for automatically running tests in multiple parallel branches of approximately equal duration. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/CountDrivenParallelism/config.groovy: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor.CountDrivenParallelism 2 | 3 | def f = namespace(lib.FormTagLib) 4 | 5 | f.entry(title:"# of parallel tests", field:"size") { 6 | f.number() 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/CountDrivenParallelism/help.html: -------------------------------------------------------------------------------- 1 |
2 | Always divide the tests into N parallel sub-tasks of about equal size. 3 | This mode is useful if your build slaves are not elastic to control 4 | the resource consumption by this task. 5 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/help-archiveTestResults.html: -------------------------------------------------------------------------------- 1 |
2 | If you uncheck this you need to configure your own JUnit archiver in the Publisher section. The test reports are copied 3 | to "test-splits/reports/". 4 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/help-includesPatternFile.html: -------------------------------------------------------------------------------- 1 |
2 | A text file that lists one Java source file name per line gets created by this path inside 3 | the workspace of the test job. 4 | The path is relative to the workspace of the test job. 5 | Your test job must honor this inclusion list while executing tests. 6 | See the help of this builder for more details. 7 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/help-parallelism.html: -------------------------------------------------------------------------------- 1 |
2 | Divides the tests into parallel sub-tasks of about equal size. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/help-parameters.html: -------------------------------------------------------------------------------- 1 |
2 | You can specify additional parameters given to the test job. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/help-patternFile.html: -------------------------------------------------------------------------------- 1 |
2 | A text file that lists one Java source file name per line gets created by this path inside 3 | the workspace of the test job. 4 | The path is relative to the workspace of the test job. 5 | Your test job must honor this exclusion list while executing tests. 6 | See the help of this builder for more details. 7 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/help-testJob.html: -------------------------------------------------------------------------------- 1 |
2 | Specify the name of the test job that executes the actual tests. 3 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/help-testMode.html: -------------------------------------------------------------------------------- 1 |
2 | Configure how exclusions and inclusions are formed from the JUnit result XML. The test job 3 | receiving the exclusion/inclusion files must be able to handle the format produced by 4 | the selected mode. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/help-testReportFiles.html: -------------------------------------------------------------------------------- 1 |
2 | The GLOB syntax that specifies test reports and its associated files that need to be copied 3 | back into this build to tally the test reports. This includes the JUnit-compatible XML files, 4 | as well as files that capture stdout/stderr of those tests (if they are placed separately.) 5 | 6 |

7 | The path is relative to the workspace of the test job. For Maven builds, this value is normally 8 | "**/target/surefire-reports/". 9 |

-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutor/help.html: -------------------------------------------------------------------------------- 1 |
2 | Given another job that runs tests, executes multiple runs of it concurrently by interleaving tests, 3 | achieving the parallel test execution semantics. 4 | 5 |

6 | This builder can be used with any test job that (1) produce JUnit-compatible XML files, and 7 | (2) accept a test-exclusion list in a file. This builder looks at the test execution time from 8 | the last time, and divide tests into multiple units of roughly equal size. 9 | Each unit is then converted into the exclusion list (by excluding all but the tests that assigned to that unit), 10 | and the test job is triggered for each unit, with the exclusion file placed inside the workspace at your specified location. 11 | 12 |

13 | Optionally, if your test job supports it, you may provide a test-inclusion file name. If defined, the plugin will 14 | generate inclusion lists for all parallel units but one which will still use exclusion list. This avoids new test 15 | cases from being included in all units on their first run. If you don't use an inclusion file, when a new test is 16 | added, the first build afterward will be executed in all the sub-builds, 17 | because it's not in the exclusion list on any of the units. 18 | 19 |

20 | You are responsible for configuring the build script in the test job to honor the exclusion and inclusion file. 21 | A standard technique is to write the build script to always refer to a fixed exclusion list file, 22 | and check in an empty file by that name to your SCM. You can then specify that file as the "exclusion file name" 23 | in the configuration of this builder, and the builder will overwrite the empty file from SCM 24 | by the generated one. 25 | 26 |

27 | Similarly, you are responsible for checking "Execute concurrent builds if necessary" on the test job 28 | to allow the concurrent execution. 29 | 30 |

31 | At the end of the executions of the test job, the specified report directories are brought back into 32 | this job's workspace, then the standard JUnit test report collector will tally them. 33 |

-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/SplitStep/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/SplitStep/help-generateInclusions.html: -------------------------------------------------------------------------------- 1 |

If disabled, the splitStep call will return a List<List<String>> containing the exclusion patterns for the different buckets.

2 | 3 |

If enabled, the splitStep call won't return a List<List<String>>.
4 | Instead it will return a List of a structure with : 5 |

    6 |
  • boolean includes whether the following list is an inclusion or an exclusion list
  • 7 |
  • List<String> list the list of patterns
  • 8 |
9 |

-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/SplitStep/help-stage.html: -------------------------------------------------------------------------------- 1 |
2 | If defined, only consider tests recorded in the previous build in the named stage. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/SplitStep/help-testMode.html: -------------------------------------------------------------------------------- 1 |

Configure how exclusions and inclusions are formed from the JUnit result XML. The test job 2 | receiving the exclusion/inclusion files must be able to handle the format produced by 3 | the selected mode.

4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/SplitStep/help.html: -------------------------------------------------------------------------------- 1 |
2 | Asks Jenkins to analyze the timing of tests from the last build and divide the tests for this build into roughly equal subsets for parallel execution. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/TimeDrivenParallelism/config.groovy: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor.TimeDrivenParallelism 2 | 3 | def f = namespace(lib.FormTagLib) 4 | 5 | f.entry(title:"Minutes per execution", field:"mins") { 6 | f.number() 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/TimeDrivenParallelism/help.html: -------------------------------------------------------------------------------- 1 |
2 | Divide the tests into parallel sub-tasks each no bigger than N minutes. 3 | Combined with elastic slaves, such as EC2, the turn-around time of the 4 | tests will remain the same no matter how many tests you add. 5 | 6 |

7 | This value counts just the time spent on executing tests, and not including 8 | other time such as checking out the code, building the test, etc. For example, 9 | if your test job spends 50 minutes in tests and 60 minutes total from start to 10 | finish, then you have 10 minutes "fixed" overhead regardless of the number of tests 11 | it executes. If you use this mode and set the value to "10 minutes", then 12 | you'll have 5 parallel sub-tasks that all ends in roughly 20 minutes 13 | (20 mins = 10 mins tests time + 10 mins overhead.) 14 | 15 |

16 | "N minutes per a sub-task" is a goal, not a hard constraint. So a sub-task 17 | can take longer (for example if you have a single test that takes more than 18 | N minutes.) 19 |

-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/testmode/JavaClassName/help.html: -------------------------------------------------------------------------------- 1 |
2 | This mode works best with java projects. 3 | 4 |

5 | Each exclusion/inclusion generates two lines by replacing "." with "/" in the fully qualified test 6 | class name and appending ".java" to one line and ".class" to the second line. 7 |

8 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/testmode/JavaParameterizedTestCaseName/help.html: -------------------------------------------------------------------------------- 1 |
2 | This mode works best with Java projects. 3 |

4 | Parallelize per Java test case including parameters. 5 |

6 | It is also able to estimate tests (per class) to run from the workspace content if no historical context could be found. 7 |

8 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/testmode/JavaTestCaseName/help.html: -------------------------------------------------------------------------------- 1 |
2 | This mode works best with Java projects. 3 |

4 | Parallelize per Java test case ignoring parameters. If parameters are present they are considered the same test. 5 |

6 | It is also able to estimate tests (per class) to run from the workspace content if no historical context could be found. 7 |

8 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/testmode/TestCaseName/help.html: -------------------------------------------------------------------------------- 1 |
2 | Each exclusion/inclusion generates one line consisting of the test case name only. 3 | 4 |

5 | This is useful where a tool produces JUnit result XML containing unique test case names without any class prefix. 6 |

7 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/parallel_test_executor/testmode/TestClassAndCaseName/help.html: -------------------------------------------------------------------------------- 1 |
2 | If checked, each exclusion/inclusion generates one line consisting of the class 3 | and test case name on a className.testName format. 4 |
5 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.parallel_test_executor; 2 | 3 | import hudson.FilePath; 4 | import hudson.model.Job; 5 | import hudson.model.Result; 6 | import hudson.model.Run; 7 | import hudson.model.TaskListener; 8 | import hudson.tasks.junit.TestResult; 9 | import hudson.tasks.test.AbstractTestResultAction; 10 | import java.io.IOException; 11 | import java.util.stream.Collectors; 12 | import org.apache.tools.ant.DirectoryScanner; 13 | import org.hamcrest.Matchers; 14 | import org.jenkinsci.plugins.parallel_test_executor.testmode.JavaParameterizedTestCaseName; 15 | import org.jenkinsci.plugins.parallel_test_executor.testmode.JavaTestCaseName; 16 | import org.jenkinsci.plugins.parallel_test_executor.testmode.JavaClassName; 17 | import org.jenkinsci.plugins.parallel_test_executor.testmode.TestClassAndCaseName; 18 | import org.jenkinsci.plugins.parallel_test_executor.testmode.TestMode; 19 | import org.junit.Before; 20 | import org.junit.Rule; 21 | import org.junit.Test; 22 | import org.junit.rules.TestName; 23 | import org.junit.runner.RunWith; 24 | import org.mockito.Mock; 25 | 26 | import java.io.File; 27 | import java.net.URISyntaxException; 28 | import java.net.URL; 29 | import java.util.ArrayList; 30 | import java.util.Collections; 31 | import java.util.HashSet; 32 | import java.util.List; 33 | import java.util.Map; 34 | import java.util.Set; 35 | 36 | import static org.hamcrest.MatcherAssert.assertThat; 37 | import static org.hamcrest.Matchers.hasItem; 38 | import static org.hamcrest.Matchers.hasSize; 39 | import static org.junit.Assert.assertEquals; 40 | import static org.junit.Assert.assertFalse; 41 | import static org.junit.Assert.assertNotNull; 42 | import static org.junit.Assume.assumeThat; 43 | import org.jvnet.hudson.test.Issue; 44 | 45 | import static org.mockito.ArgumentMatchers.eq; 46 | import static org.mockito.Mockito.mock; 47 | import static org.mockito.Mockito.when; 48 | 49 | import org.mockito.junit.MockitoJUnitRunner; 50 | 51 | @RunWith(MockitoJUnitRunner.class) 52 | public class ParallelTestExecutorUnitTest { 53 | 54 | ParallelTestExecutor instance; 55 | 56 | @Mock Run build; 57 | 58 | @Mock Run previousBuild; 59 | 60 | @Mock TaskListener listener; 61 | 62 | @Mock AbstractTestResultAction action; 63 | 64 | @Rule public TestName name = new TestName(); 65 | 66 | File projectRootDir; 67 | 68 | DirectoryScanner scanner; 69 | 70 | 71 | @Before 72 | public void setUp() throws Exception { 73 | when(build.getPreviousBuild()).thenReturn((Run)previousBuild); 74 | when(previousBuild.getResult()).thenReturn(Result.SUCCESS); 75 | when(previousBuild.getUrl()).thenReturn("job/some-project/1"); 76 | when(previousBuild.getDisplayName()).thenReturn("#1"); 77 | when(listener.getLogger()).thenReturn(System.err); 78 | when(previousBuild.getAction(eq(AbstractTestResultAction.class))).thenReturn(action); 79 | } 80 | 81 | @Before 82 | public void findProjectRoot() throws Exception { 83 | URL url = getClass().getResource(getClass().getSimpleName() + "/" + this.name.getMethodName()); 84 | assumeThat("The test resource for " + this.name.getMethodName() + " exist", url, Matchers.notNullValue()); 85 | try { 86 | projectRootDir = new File(url.toURI()); 87 | } catch (URISyntaxException e) { 88 | projectRootDir = new File(url.getPath()); 89 | } 90 | scanner = new DirectoryScanner(); 91 | scanner.setBasedir(projectRootDir); 92 | scanner.scan(); 93 | } 94 | 95 | @Test 96 | public void findTestSplits() throws Exception { 97 | checkTestSplits(new CountDrivenParallelism(5), 5, null); 98 | checkTestSplits(new CountDrivenParallelism(5), 5, new JavaClassName()); 99 | // Only 5 classes 100 | checkTestSplits(new CountDrivenParallelism(10), 5, new JavaClassName()); 101 | // Splitting by test cases we can parallelize more! 102 | checkTestSplits(new CountDrivenParallelism(10), 10, new JavaTestCaseName()); 103 | } 104 | 105 | @Test 106 | public void findTestDuplicates() throws Exception { 107 | checkTestSplits(new CountDrivenParallelism(10), 10, new JavaTestCaseName()); 108 | } 109 | 110 | @Test 111 | public void findTestCaseTimeSplitsExclusion() throws Exception { 112 | TimeDrivenParallelism parallelism = new TimeDrivenParallelism(2); 113 | checkTestSplits(parallelism, 5, new TestClassAndCaseName()); 114 | } 115 | 116 | public void checkTestSplits(Parallelism parallelism, int expectedSplitSize, TestMode testMode) throws Exception { 117 | TestResult testResult = new TestResult(0L, scanner, false); 118 | testResult.tally(); 119 | when(action.getResult()).thenReturn(testResult); 120 | 121 | List splits = Splitter.findTestSplits(parallelism, testMode, build, listener, false, null, null); 122 | assertEquals(expectedSplitSize, splits.size()); 123 | for (InclusionExclusionPattern split : splits) { 124 | assertFalse(split.isIncludes()); 125 | } 126 | } 127 | 128 | @Test 129 | public void testWeDoNotCreateMoreSplitsThanThereAreTests() throws Exception { 130 | // The test report only has 2 classes, so we should only split into 2 test executors 131 | TestResult testResult = new TestResult(0L, scanner, false); 132 | testResult.tally(); 133 | when(action.getResult()).thenReturn(testResult); 134 | 135 | CountDrivenParallelism parallelism = new CountDrivenParallelism(5); 136 | List splits = Splitter.findTestSplits(parallelism, null, build, listener, false, null, null); 137 | assertEquals(2, splits.size()); 138 | for (InclusionExclusionPattern split : splits) { 139 | assertFalse(split.isIncludes()); 140 | } 141 | } 142 | 143 | @Test 144 | public void findTestCasesWithParameters() throws Exception { 145 | TestResult testResult = new TestResult(0L, scanner, false); 146 | testResult.tally(); 147 | when(action.getResult()).thenReturn(testResult); 148 | CountDrivenParallelism parallelism = new CountDrivenParallelism(3); 149 | List splits = Splitter.findTestSplits(parallelism, new JavaTestCaseName(), build, listener, false, null, null); 150 | assertEquals(3, splits.size()); 151 | var allSplits = splits.stream().flatMap(s -> s.getList().stream()).collect(Collectors.toSet()); 152 | assertThat(allSplits, hasSize(20)); 153 | assertThat(allSplits, hasItem("org.jenkinsci.plugins.parallel_test_executor.Test1#testCase")); 154 | } 155 | 156 | @Test 157 | public void findTestCasesWithParametersIncluded() throws Exception { 158 | TestResult testResult = new TestResult(0L, scanner, false); 159 | testResult.tally(); 160 | when(action.getResult()).thenReturn(testResult); 161 | CountDrivenParallelism parallelism = new CountDrivenParallelism(3); 162 | List splits = Splitter.findTestSplits(parallelism, new JavaParameterizedTestCaseName(), build, listener, false, null, null); 163 | assertEquals(3, splits.size()); 164 | var allSplits = splits.stream().flatMap(s -> s.getList().stream()).collect(Collectors.toSet()); 165 | assertThat(allSplits, hasSize(22)); 166 | assertThat(allSplits, hasItem("org.jenkinsci.plugins.parallel_test_executor.Test1#testCase[param1]")); 167 | } 168 | 169 | @Test 170 | public void findTestSplitsInclusions() throws Exception { 171 | CountDrivenParallelism parallelism = new CountDrivenParallelism(5); 172 | checkTestSplitsInclusions(parallelism, 5, null); 173 | } 174 | 175 | @Test 176 | public void findTestCaseTimeSplitsInclusion() throws Exception { 177 | TimeDrivenParallelism parallelism = new TimeDrivenParallelism(2); 178 | checkTestSplitsInclusions(parallelism, 5, new TestClassAndCaseName()); 179 | } 180 | 181 | private void checkTestSplitsInclusions(Parallelism parallelism, int expectedSplitSize, TestMode testMode) throws Exception { 182 | TestResult testResult = new TestResult(0L, scanner, false); 183 | testResult.tally(); 184 | when(action.getResult()).thenReturn(testResult); 185 | 186 | List splits = Splitter.findTestSplits(parallelism, testMode, build, listener, true, null, null); 187 | assertEquals(expectedSplitSize, splits.size()); 188 | List exclusions = new ArrayList<>(splits.get(0).getList()); 189 | List inclusions = new ArrayList<>(); 190 | for (int i = 0; i < splits.size(); i++) { 191 | InclusionExclusionPattern split = splits.get(i); 192 | assertEquals(i != 0, split.isIncludes()); 193 | if (split.isIncludes()) { 194 | inclusions.addAll(split.getList()); 195 | } 196 | } 197 | Collections.sort(exclusions); 198 | Collections.sort(inclusions); 199 | assertEquals("exclusions set should contain all elements included by inclusions set", inclusions, exclusions); 200 | } 201 | 202 | @Issue("JENKINS-47206") 203 | @Test 204 | public void findTestInJavaProjectDirectory() throws InterruptedException { 205 | CountDrivenParallelism parallelism = new CountDrivenParallelism(5); 206 | List splits = Splitter.findTestSplits(parallelism, null, build, listener, true, null, new FilePath(scanner.getBasedir())); 207 | assertEquals(5, splits.size()); 208 | } 209 | 210 | @Issue("JENKINS-47206") 211 | @Test 212 | public void findTestOfJavaProjectDirectoryInWorkspace() throws InterruptedException { 213 | CountDrivenParallelism parallelism = new CountDrivenParallelism(5); 214 | Map data = TestMode.getDefault().estimate(new FilePath(scanner.getBasedir()), listener); 215 | Set expectedTests = new HashSet<>(); 216 | expectedTests.add("FirstTest"); 217 | expectedTests.add("SecondTest"); 218 | 219 | expectedTests.add("somepackage/ThirdTest"); 220 | expectedTests.add("ThirdTest"); 221 | expectedTests.add("FourthTest"); 222 | expectedTests.add("FifthTest"); 223 | assertEquals("Result does not contains expected tests.", expectedTests, data.keySet()); 224 | List splits = Splitter.findTestSplits(parallelism, null, build, listener, true, null, new FilePath(scanner.getBasedir())); 225 | assertEquals(5, splits.size()); 226 | } 227 | 228 | @Test 229 | public void previousBuildIsOngoing() throws IOException { 230 | Job project = mock(Job.class); 231 | Run previousPreviousBuild = mock(Run.class); 232 | when(previousBuild.getResult()).thenReturn(null); 233 | when(previousBuild.getPreviousBuild()).thenReturn(previousPreviousBuild); 234 | when(previousPreviousBuild.getParent()).thenReturn(project); 235 | when(previousPreviousBuild.getResult()).thenReturn(Result.SUCCESS); 236 | when(previousPreviousBuild.getAction(eq(AbstractTestResultAction.class))).thenReturn(action); 237 | when(previousPreviousBuild.getUrl()).thenReturn("job/some-project/1"); 238 | when(previousPreviousBuild.getDisplayName()).thenReturn("#1"); 239 | TestResult testResult = new TestResult(0L, scanner, false); 240 | testResult.tally(); 241 | when(action.getResult()).thenReturn(testResult); 242 | 243 | assertNotNull(Splitter.getTestResult(project, previousBuild, listener)); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorTest/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.318-SNAPSHOT (private-08/08/2009 09:46-tom) 4 | 2 5 | NORMAL 6 | 7 | 8 | 9 | 5 10 | 0 11 | 0 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorTest/jobs/old/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | false 10 | false 11 | false 12 | 13 | false 14 | 15 | 16 | 17 | 3 18 | 19 | 20 | exclusion.txt 21 | **/target/surefire-reports/ 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsExclusion/report-Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsExclusion/report-Test2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsExclusion/report-Test3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsExclusion/report-Test4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsExclusion/report-Test5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsInclusion/report-Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsInclusion/report-Test2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsInclusion/report-Test3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsInclusion/report-Test4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCaseTimeSplitsInclusion/report-Test5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCasesWithParameters/report-Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestCasesWithParametersIncluded/report-Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestDuplicates/report-Test1-bis.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestDuplicates/report-Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestInJavaProjectDirectory/src/test/java/FifthTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FifthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestInJavaProjectDirectory/src/test/java/FirstTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FirstTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(false); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestInJavaProjectDirectory/src/test/java/FourthTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FourthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(false); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestInJavaProjectDirectory/src/test/java/SecondTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class SecondTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestInJavaProjectDirectory/src/test/java/ThirdTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class ThirdTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/file.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/parallel-test-executor-plugin/acdc4fbda14b014ad1b094f3aa6ae3f5b0c5543b/src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/file.log -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/FakeTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FifthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/main/FakeFifthTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FifthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/main/java/FakeFifthTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FifthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/main/java/somepackage/FakeFifthTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FifthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/main/java/somepackage/FakeFirstTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FirstTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(false); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/main/java/somepackage/FakeFourthTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FourthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(false); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/main/java/somepackage/FakeSecondTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class SecondTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/AdditionalFile2.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FifthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/AdditionalFile.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FifthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/FakeTest.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/parallel-test-executor-plugin/acdc4fbda14b014ad1b094f3aa6ae3f5b0c5543b/src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/FakeTest.txt -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/FifthTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FifthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/FirstTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FirstTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(false); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/FourthTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FourthTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(false); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/SecondTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class SecondTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/SomeFile3.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/parallel-test-executor-plugin/acdc4fbda14b014ad1b094f3aa6ae3f5b0c5543b/src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/SomeFile3.txt -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/ThirdTest.java: -------------------------------------------------------------------------------- 1 | package 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class ThirdTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/java/somepackage/ThirdTest.java: -------------------------------------------------------------------------------- 1 | package somepackage 2 | 3 | 4 | import org.junit.Test; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class ThirdTest { 8 | 9 | 10 | @Test 11 | public void sampleTest() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/someFile2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/parallel-test-executor-plugin/acdc4fbda14b014ad1b094f3aa6ae3f5b0c5543b/src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestOfJavaProjectDirectoryInWorkspace/src/test/someFile2.txt -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplits/report-Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplits/report-Test2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplits/report-Test3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplits/report-Test4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplits/report-Test5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplitsInclusions/report-Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplitsInclusions/report-Test2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplitsInclusions/report-Test3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplitsInclusions/report-Test4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/findTestSplitsInclusions/report-Test5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/previousBuildIsOngoing/report-Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/parallel_test_executor/ParallelTestExecutorUnitTest/testWeDoNotCreateMoreSplitsThanThereAreTests/report-Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | --------------------------------------------------------------------------------