diff --git a/Build/bamboo/src/main/java/core/NightlySpec.java b/Build/bamboo/src/main/java/core/NightlySpec.java index a4e0c8f19358db24bcd72de1efda8fcc757398ce..5170b18dca17cb37b92e956105c7921f31c52ba2 100644 --- a/Build/bamboo/src/main/java/core/NightlySpec.java +++ b/Build/bamboo/src/main/java/core/NightlySpec.java @@ -28,6 +28,7 @@ import com.atlassian.bamboo.specs.api.builders.task.Task; import com.atlassian.bamboo.specs.builders.notification.PlanCompletedNotification; import com.atlassian.bamboo.specs.builders.trigger.ScheduledTrigger; import com.atlassian.bamboo.specs.util.BambooServer; +import core.utilities.LimitedChunker; import java.util.ArrayList; import java.util.Collections; @@ -59,7 +60,7 @@ public class NightlySpec extends AbstractCoreSpec { private String[] sqLiteVersions = {"3.15", "3.20", "3.25", "3.30"}; private String[] postGreSqlVersions = {"9.3", "9.4", "9.5", "9.6", "10.11", "11.6", "12.1"}; - private int jobListSize = 50; + private int totalJobsPerStage = 50; private int mssqlJobsPerStage = 25; /** @@ -107,50 +108,23 @@ public class NightlySpec extends AbstractCoreSpec { stages.add(stagePreparation); stages.add(stageIntegrity); - int otherJobsStages = jobs.size() / (jobListSize - mssqlJobsPerStage); - int mssqlJobsStages = mssqlJobs.size() / mssqlJobsPerStage; - int handledJobs = 0; - int jobCount = 0; - int mssqlCount = 0; - int otherCount = 0; - for (int i = 0; i < Math.max(otherJobsStages, mssqlJobsStages); i++) { - List<Job> mssqlJobsChunk = new ArrayList<Job>(); - int chunkMinIndex = i * mssqlJobsPerStage; - int chunkMaxIndex = (i + 1) * mssqlJobsPerStage; - - if (mssqlJobs.size() >= chunkMaxIndex) { - mssqlJobsChunk = mssqlJobs.subList(chunkMinIndex, chunkMaxIndex); - } else { - if (mssqlJobs.size() >= chunkMinIndex) { - mssqlJobsChunk = mssqlJobs.subList(chunkMinIndex, mssqlJobs.size()); - } - } - - List<Job> otherJobsChunk; - chunkMinIndex = handledJobs; - chunkMaxIndex = (jobListSize - mssqlJobsChunk.size() + handledJobs); - if (jobs.size() >= chunkMaxIndex) { - otherJobsChunk = jobs.subList(chunkMinIndex, chunkMaxIndex); - } else { - otherJobsChunk = jobs.subList(chunkMinIndex, jobs.size()); - } - handledJobs = handledJobs + otherJobsChunk.size(); - - ArrayList<Job> stagingJobs = new ArrayList<Job>(); - stagingJobs.addAll(mssqlJobsChunk); - stagingJobs.addAll(otherJobsChunk); - otherCount = otherCount + otherJobsChunk.size(); - mssqlCount = mssqlCount + mssqlJobsChunk.size(); - jobCount = jobCount + otherJobsChunk.size() + mssqlJobsChunk.size(); - if (stagingJobs.size() > 0) { - Collections.shuffle(stagingJobs); - Stage stage = new Stage("Stage " + (i + 1) + ", Jobs " + (i * jobListSize) + " - " + (((i + 1) * jobListSize) - 1)); - System.out.println("Stage " + (i + 1) + " got " + stagingJobs.size() + " Jobs, " + otherJobsChunk.size() + " jobs and " + mssqlJobsChunk.size() + " mssql jobs"); - stage.jobs(stagingJobs.toArray(new Job[stagingJobs.size()])); - stages.add(stage); - } + LimitedChunker<Job> chunker = new LimitedChunker<>(mssqlJobsPerStage, totalJobsPerStage); + List<List<Job>> chunks = chunker.chunk(mssqlJobs, jobs); + + int totalNumberOfJobs = jobs.size() + mssqlJobs.size(); + int stageIndex = 0; + for (List<Job> chunk : chunks) { + int firstJobIndex = totalJobsPerStage * stageIndex + 1; + int lastJobIndex = totalJobsPerStage * (stageIndex + 1); + lastJobIndex = Math.min(lastJobIndex, totalNumberOfJobs); + + Collections.shuffle(chunk); + Stage stage = new Stage("Stage " + (stageIndex + 1) + ", Jobs " + firstJobIndex + " - " + lastJobIndex); + stage.jobs(chunk.toArray(new Job[chunk.size()])); + stages.add(stage); + System.out.println("Stage " + (stageIndex + 1) + " got " + chunk.size() + " Jobs"); + stageIndex++; } - System.out.println("deployed " + jobCount + " (" + mssqlCount + " mssql and " + otherCount + " other) " + " out of " + (jobs.size() + mssqlJobs.size()) + " Jobs (" + mssqlJobs.size() + " mssql and " + jobs.size() + " other) in " + (stages.size() - 2) + " Stages"); // Compile plan return new Plan(project(), planName, planKey).description("Execute TYPO3 core master nightly tests. Auto generated! See Build/bamboo of core git repository.") diff --git a/Build/bamboo/src/main/java/core/utilities/LimitedChunker.java b/Build/bamboo/src/main/java/core/utilities/LimitedChunker.java new file mode 100644 index 0000000000000000000000000000000000000000..6ba171dc100099ebd1266a5d9c18f2668e71a8a4 --- /dev/null +++ b/Build/bamboo/src/main/java/core/utilities/LimitedChunker.java @@ -0,0 +1,63 @@ +package core.utilities; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * Chunk two lists into chunks of a given size + * where only `numLimitedJobsPerChunk` elements from one list are in each chunk + * + * @param <T> + */ +public class LimitedChunker<T> { + + private final int numLimitedElementsPerChunk; + private final int totalChunkSize; + + public LimitedChunker(int numLimitedElementsPerChunk, int totalChunkSize) { + this.numLimitedElementsPerChunk = numLimitedElementsPerChunk; + this.totalChunkSize = totalChunkSize; + + if (numLimitedElementsPerChunk < 1) { + throw new IllegalArgumentException("Number of limited Elements per Chunk must be greater than one"); + } + if (totalChunkSize < 1) { + throw new IllegalArgumentException("Total chunk size must be greater than one"); + } + if (totalChunkSize < numLimitedElementsPerChunk) { + throw new IllegalArgumentException("Number of limited elements must not be greater than total chunk size"); + } + } + + public List<List<T>> chunk(List<T> limitedElements, List<T> normalElements) { + // chunk the limited elements in sets of limitedChunkSize + LinkedList<List<T>> chunks = new LinkedList<>(prepareChunks(limitedElements, numLimitedElementsPerChunk)); + + // fill up these chunks with normal elements + Iterator<T> normalJobIterator = normalElements.iterator(); + for (List<T> chunk : chunks) { + fillChunkToSize(chunk, totalChunkSize, normalJobIterator); + } + // add chunks for all remaining normal elements, if any + while (normalJobIterator.hasNext()) { + List<T> chunk = new ArrayList<>(totalChunkSize); + fillChunkToSize(chunk, totalChunkSize, normalJobIterator); + chunks.add(chunk); + } + + return chunks; + } + + private void fillChunkToSize(List<T> chunk, int totalChunkSize, Iterator<T> remainingElementsIterator) { + while (remainingElementsIterator.hasNext() && chunk.size() < totalChunkSize) { + chunk.add(remainingElementsIterator.next()); + } + } + + private Collection<List<T>> prepareChunks(List<T> inputList, int chunkSize) { + AtomicInteger counter = new AtomicInteger(); + return inputList.stream().collect(Collectors.groupingBy(it -> counter.getAndIncrement() / chunkSize)).values(); + } + +} diff --git a/Build/bamboo/src/test/java/utilities/JobFixture.java b/Build/bamboo/src/test/java/utilities/JobFixture.java new file mode 100644 index 0000000000000000000000000000000000000000..8f03fffd88a68514b51d9ce73b53e6b9cd8ddd7a --- /dev/null +++ b/Build/bamboo/src/test/java/utilities/JobFixture.java @@ -0,0 +1,13 @@ +package utilities; + +public class JobFixture { + private final boolean isLimited; + + public JobFixture(boolean isLimited) { + this.isLimited = isLimited; + } + + public boolean isLimited(){ + return isLimited; + } +} diff --git a/Build/bamboo/src/test/java/utilities/LimitedChunkerTest.java b/Build/bamboo/src/test/java/utilities/LimitedChunkerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b3af82f6464c8f3b53e4800e4bd42fc51202cdd0 --- /dev/null +++ b/Build/bamboo/src/test/java/utilities/LimitedChunkerTest.java @@ -0,0 +1,115 @@ +package utilities; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +import core.utilities.LimitedChunker; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@RunWith(Parameterized.class) +public class LimitedChunkerTest { + private final int numLimitedJobs; + private final int numNormalJobs; + private final int limitedChunkSize; + private final int totalChunkSize; + private int expectedNumberOfChunks; + + @Test(expected = IllegalArgumentException.class) + public void throwsOnZeroLimitedElementsPerChunk() { + LimitedChunker<JobFixture> chunker = new LimitedChunker<>(0, 25); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsOnZeroTotalChunkSize() { + LimitedChunker<JobFixture> chunker = new LimitedChunker<>(25, 0); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsOnMoreLimitedThanChunkSize() { + LimitedChunker<JobFixture> chunker = new LimitedChunker<>(25, 12); + } + + @Test + public void returnsEmptyChunkListWithEmptyInput() { + LimitedChunker<JobFixture> chunker = new LimitedChunker<>(12, 25); + + ArrayList<JobFixture> jobs = new ArrayList<>(0); + ArrayList<JobFixture> limitedJobs = new ArrayList<>(0); + + List<List<JobFixture>> chunks = chunker.chunk(limitedJobs, jobs); + + assert chunks.size() == 0; + } + + @Parameterized.Parameters + public static Collection<Object[]> data() { + return Arrays.asList( + new Integer[]{25, 25, 25, 50, 1}, + new Integer[]{25, 0, 25, 25, 1}, + new Integer[]{100, 100, 25, 50, 4}, + new Integer[]{110, 100, 25, 50, 5}, + new Integer[]{100, 110, 25, 50, 5} + ); + } + + public LimitedChunkerTest(int numLimitedJobs, int numNormalJobs, int limitedChunkSize, int totalChunkSize, int expectedNumberOfChunks) { + this.numLimitedJobs = numLimitedJobs; + this.numNormalJobs = numNormalJobs; + this.limitedChunkSize = limitedChunkSize; + this.totalChunkSize = totalChunkSize; + this.expectedNumberOfChunks = expectedNumberOfChunks; + } + + @Test + public void chunksCorrectly() { + ArrayList<JobFixture> limitedJobs = getJobs(numLimitedJobs, true); + ArrayList<JobFixture> normalJobs = getJobs(numNormalJobs, false); + + List<List<JobFixture>> chunks = new LimitedChunker<JobFixture>(limitedChunkSize, totalChunkSize).chunk(limitedJobs, normalJobs); + + assert chunks.size() == expectedNumberOfChunks; + + int actualNumberOfJobs = chunks.stream().map(List::size).reduce(0, Integer::sum); + assert actualNumberOfJobs == numLimitedJobs + numNormalJobs; + + + int chunkIndex = 0; + for (List<JobFixture> chunk : chunks) { + long numLimited = chunk.stream().filter(JobFixture::isLimited).count(); + assert(numLimited <= limitedChunkSize); + if (chunkIndex < chunks.size() - 1) { + // all chunks must be full + assert(chunk.size() == totalChunkSize); + } else { + //except the last one + assert(chunk.size() <= totalChunkSize); + } + chunkIndex++; + } + } + + private ArrayList<JobFixture> getJobs(int numLimitedJobs, boolean isLimited) { + ArrayList<JobFixture> jobs = new ArrayList<>(numLimitedJobs); + for (int i = 0; i < numLimitedJobs; i++) { + jobs.add(new JobFixture(isLimited)); + } + return jobs; + } +}