diff --git a/.github/workflows/nethermind-tests.yml b/.github/workflows/nethermind-tests.yml index a71c5492c31..6a8d009b0a1 100644 --- a/.github/workflows/nethermind-tests.yml +++ b/.github/workflows/nethermind-tests.yml @@ -23,7 +23,7 @@ env: jobs: tests: - name: Run ${{ matrix.project }} + name: Run ${{ matrix.project }}${{ matrix.chunk && format(' ({0})', matrix.chunk) || '' }} runs-on: ubuntu-latest continue-on-error: true outputs: @@ -40,7 +40,6 @@ jobs: - Ethereum.KeyAddress.Test - Ethereum.KeyStore.Test - Ethereum.Legacy.Blockchain.Block.Test - - Ethereum.Legacy.Blockchain.Test - Ethereum.Legacy.Transition.Test - Ethereum.Legacy.VM.Test - Ethereum.PoW.Test @@ -91,6 +90,16 @@ jobs: - Nethermind.TxPool.Test - Nethermind.Wallet.Test - Nethermind.Xdc.Test + chunk: [''] + include: + - project: Ethereum.Legacy.Blockchain.Test + chunk: 1of4 + - project: Ethereum.Legacy.Blockchain.Test + chunk: 2of4 + - project: Ethereum.Legacy.Blockchain.Test + chunk: 3of4 + - project: Ethereum.Legacy.Blockchain.Test + chunk: 4of4 steps: - name: Check out repository uses: actions/checkout@v6 @@ -103,6 +112,8 @@ jobs: - name: ${{ matrix.project }} id: test working-directory: src/Nethermind/${{ matrix.project }} + env: + TEST_CHUNK: ${{ matrix.chunk }} run: | dotnet test --project ${{ matrix.project }}.csproj -c release \ ${{ env.COLLECT_COVERAGE == 'true' && '--coverage --coverage-output-format cobertura --coverage-settings $GITHUB_WORKSPACE/src/Nethermind/codecoverage.config' || '' }} @@ -115,7 +126,7 @@ jobs: if: success() || failure() uses: actions/upload-artifact@v4 with: - name: ${{ matrix.project }}-outcome + name: ${{ matrix.project }}${{ matrix.chunk && format('-{0}', matrix.chunk) || '' }}-outcome path: test.outcome retention-days: 1 @@ -123,7 +134,7 @@ jobs: if: env.COLLECT_COVERAGE == 'true' uses: actions/upload-artifact@v4 with: - name: ${{ matrix.project }}-coverage + name: ${{ matrix.project }}${{ matrix.chunk && format('-{0}', matrix.chunk) || '' }}-coverage path: src/Nethermind/artifacts/bin/${{ matrix.project }}/release/TestResults/*.cobertura.xml retention-days: 1 diff --git a/src/Nethermind/Ethereum.Test.Base/TestChunkFilter.cs b/src/Nethermind/Ethereum.Test.Base/TestChunkFilter.cs new file mode 100644 index 00000000000..027425574ff --- /dev/null +++ b/src/Nethermind/Ethereum.Test.Base/TestChunkFilter.cs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ethereum.Test.Base; + +/// +/// Filters tests into chunks for parallel CI execution. +/// Set TEST_CHUNK environment variable to "1of3", "2of3", "3of3" etc. +/// Partitions tests by index for consistent and even distribution. +/// +public static class TestChunkFilter +{ + public static IEnumerable FilterByChunk(IEnumerable tests) + { + (int Index, int Total)? chunkConfig = GetChunkConfig(); + + if (chunkConfig is null) + { + return tests; + } + + ICollection testList = tests as ICollection ?? [.. tests]; + (int chunkIndex, int totalChunks) = chunkConfig.Value; + + int count = testList.Count; + int chunkSize = count / totalChunks; + int remainder = count % totalChunks; + + int skip = (chunkIndex - 1) * chunkSize; + int take = chunkSize + (chunkIndex == totalChunks ? remainder : 0); + + return testList.Skip(skip).Take(take); + + static (int Index, int Total)? GetChunkConfig() + { + string? chunkEnv = Environment.GetEnvironmentVariable("TEST_CHUNK"); + + if (string.IsNullOrEmpty(chunkEnv)) + { + return null; + } + + string[] parts = chunkEnv.Split("of", StringSplitOptions.None); + + if (parts.Length != 2 || + !int.TryParse(parts[0], out int index) || + !int.TryParse(parts[1], out int total) || + index < 1 || index > total || total < 1) + { + throw new ArgumentException($"Invalid TEST_CHUNK format: '{chunkEnv}'. Expected format: '1of3', '2of5', etc."); + } + + return (index, total); + } + } +} diff --git a/src/Nethermind/Ethereum.Test.Base/TestsSourceLoader.cs b/src/Nethermind/Ethereum.Test.Base/TestsSourceLoader.cs index 8d8108d6abc..4c20099ce56 100644 --- a/src/Nethermind/Ethereum.Test.Base/TestsSourceLoader.cs +++ b/src/Nethermind/Ethereum.Test.Base/TestsSourceLoader.cs @@ -24,12 +24,15 @@ public TestsSourceLoader(ITestLoadStrategy testLoadStrategy, string path, string public IEnumerable LoadTests() { - return _testLoadStrategy.Load(_path, _wildcard); + IEnumerable tests = _testLoadStrategy.Load(_path, _wildcard); + return TestChunkFilter.FilterByChunk(tests); } + public IEnumerable LoadTests() where TTestType : EthereumTest { - return _testLoadStrategy.Load(_path, _wildcard).Cast(); + IEnumerable tests = _testLoadStrategy.Load(_path, _wildcard).Cast(); + return TestChunkFilter.FilterByChunk(tests); } } }