From d58a5e4080162f9572d076ad5a14bb54d6ea764f Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 16 Jul 2026 10:46:22 -0400 Subject: [PATCH 1/4] Add Lean Python unit tests CI workflow Runs Lean's Python/Pandas unit test suites plus every other Lean unit test class that exercises Python code against the freshly built Python.Runtime.dll. Test classes are selected by scanning Lean's test sources for Python interop usage and passing the resulting FullyQualifiedName filter through a runsettings file. Regression suites are excluded since Python regression algorithms are already covered by the Lean Python Regression Tests workflow. --- .github/workflows/lean-python-unit-tests.yml | 110 +++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/workflows/lean-python-unit-tests.yml diff --git a/.github/workflows/lean-python-unit-tests.yml b/.github/workflows/lean-python-unit-tests.yml new file mode 100644 index 000000000..538637f3a --- /dev/null +++ b/.github/workflows/lean-python-unit-tests.yml @@ -0,0 +1,110 @@ +name: Lean Python Unit Tests + +# Validates a Python.Runtime.dll change against Lean's Python unit tests: +# the Python/Pandas test suites (QuantConnect.Tests.Python) plus every other +# Lean unit test class that exercises Python code. We build this repo's +# Python.Runtime.dll, build Lean against its NuGet QuantConnect.pythonnet +# reference, drop the freshly built DLL over Lean's test output, and run only +# the test classes whose sources reference the Python interop layer. + +on: + push: + branches: + - master + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lean-python-unit-tests: + runs-on: ubuntu-24.04 + timeout-minutes: 360 + steps: + - name: Checkout pythonnet + uses: actions/checkout@v4 + with: + path: pythonnet + + - name: Checkout Lean + uses: actions/checkout@v4 + with: + repository: QuantConnect/Lean + path: Lean + + - name: Liberate disk space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: true + large-packages: false + docker-images: false + swap-storage: false + + - name: Define docker helper + run: | + echo 'runInContainer() { docker exec test-container "$@"; }' > $HOME/ci_functions.sh + echo "BASH_ENV=$HOME/ci_functions.sh" >> $GITHUB_ENV + + - name: Start container + run: | + docker run -d \ + --workdir /__w/pythonnet/pythonnet \ + -v /home/runner/work:/__w \ + --name test-container \ + quantconnect/lean:foundation \ + tail -f /dev/null + + - name: Build Python.Runtime.dll + run: | + runInContainer dotnet build pythonnet/src/runtime/Python.Runtime.csproj \ + -c Release /v:quiet /p:WarningLevel=1 + + - name: Build Lean + run: | + runInContainer dotnet build /p:Configuration=Release /v:quiet /p:WarningLevel=1 \ + Lean/QuantConnect.Lean.sln + + - name: Inject freshly built Python.Runtime.dll into Lean test output + run: | + runInContainer cp pythonnet/pythonnet/runtime/Python.Runtime.dll \ + Lean/Tests/bin/Release/Python.Runtime.dll + + - name: Generate Python unit test filter + run: | + # Select every test class whose source references Lean's Python + # interop layer (Py.GIL, PythonEngine, PyObject, Language.Python + # test cases, ...). Class names are extracted per matching file and + # turned into a FullyQualifiedName filter, so the selection tracks + # Lean automatically. Non-test helper classes that slip in simply + # match nothing. Regression suites are excluded: Python regression + # algorithms already run in the Lean Python Regression Tests + # workflow, and TravisExclude/ResearchRegressionTests mirror Lean's + # own unit test CI exclusions. The filter is passed through a + # runsettings file because it is far too long for a command line. + filter=$(grep -rlE 'Py\.GIL|PythonEngine|PyModule|Language\.Python|\bPyObject\b' \ + Lean/Tests --include='*.cs' --exclude-dir=bin --exclude-dir=obj \ + | while read -r file; do + ns=$(sed -n 's/^namespace \([A-Za-z0-9_.]*\).*/\1/p' "$file" | head -1) + [ -z "$ns" ] && continue + sed -n 's/.*\bclass \([A-Za-z0-9_]*\).*/\1/p' "$file" | sort -u \ + | while read -r cls; do echo "FullyQualifiedName~$ns.$cls"; done + done | sort -u | paste -sd'|') + echo "Selected $(tr '|' '\n' <<< "$filter" | wc -l) test class name filters" + cat > lean-python-unit-tests.runsettings < + + + ($filter)&TestCategory!=TravisExclude&TestCategory!=ResearchRegressionTests&TestCategory!=RegressionTests + + + + + + EOF + + - name: Run Lean Python unit tests + run: | + runInContainer dotnet test Lean/Tests/bin/Release/QuantConnect.Tests.dll \ + --blame-hang-timeout 300seconds --blame-crash \ + --settings lean-python-unit-tests.runsettings From af7b73f8f9332b578bea05edfcefff337cb890e4 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 16 Jul 2026 11:15:50 -0400 Subject: [PATCH 2/4] Run Lean Python unit tests job on self-hosted runner Follow the pattern of Lean's own unit test CI job and this repo's main workflow: run directly in a quantconnect/lean:foundation container on a self-hosted runner instead of manually managing a docker container on a GitHub-hosted runner. --- .github/workflows/lean-python-unit-tests.yml | 39 +++++--------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/.github/workflows/lean-python-unit-tests.yml b/.github/workflows/lean-python-unit-tests.yml index 538637f3a..81a779d52 100644 --- a/.github/workflows/lean-python-unit-tests.yml +++ b/.github/workflows/lean-python-unit-tests.yml @@ -5,7 +5,8 @@ name: Lean Python Unit Tests # Lean unit test class that exercises Python code. We build this repo's # Python.Runtime.dll, build Lean against its NuGet QuantConnect.pythonnet # reference, drop the freshly built DLL over Lean's test output, and run only -# the test classes whose sources reference the Python interop layer. +# the test classes whose sources reference the Python interop layer, +# mirroring Lean's own .github/workflows/gh-actions.yml unit test job. on: push: @@ -19,8 +20,10 @@ concurrency: jobs: lean-python-unit-tests: - runs-on: ubuntu-24.04 - timeout-minutes: 360 + runs-on: self-hosted + container: + image: quantconnect/lean:foundation + options: --cpus 12 --memory 12g steps: - name: Checkout pythonnet uses: actions/checkout@v4 @@ -33,41 +36,19 @@ jobs: repository: QuantConnect/Lean path: Lean - - name: Liberate disk space - uses: jlumbroso/free-disk-space@main - with: - tool-cache: true - large-packages: false - docker-images: false - swap-storage: false - - - name: Define docker helper - run: | - echo 'runInContainer() { docker exec test-container "$@"; }' > $HOME/ci_functions.sh - echo "BASH_ENV=$HOME/ci_functions.sh" >> $GITHUB_ENV - - - name: Start container - run: | - docker run -d \ - --workdir /__w/pythonnet/pythonnet \ - -v /home/runner/work:/__w \ - --name test-container \ - quantconnect/lean:foundation \ - tail -f /dev/null - - name: Build Python.Runtime.dll run: | - runInContainer dotnet build pythonnet/src/runtime/Python.Runtime.csproj \ + dotnet build pythonnet/src/runtime/Python.Runtime.csproj \ -c Release /v:quiet /p:WarningLevel=1 - name: Build Lean run: | - runInContainer dotnet build /p:Configuration=Release /v:quiet /p:WarningLevel=1 \ + dotnet build /p:Configuration=Release /v:quiet /p:WarningLevel=1 \ Lean/QuantConnect.Lean.sln - name: Inject freshly built Python.Runtime.dll into Lean test output run: | - runInContainer cp pythonnet/pythonnet/runtime/Python.Runtime.dll \ + cp pythonnet/pythonnet/runtime/Python.Runtime.dll \ Lean/Tests/bin/Release/Python.Runtime.dll - name: Generate Python unit test filter @@ -105,6 +86,6 @@ jobs: - name: Run Lean Python unit tests run: | - runInContainer dotnet test Lean/Tests/bin/Release/QuantConnect.Tests.dll \ + dotnet test Lean/Tests/bin/Release/QuantConnect.Tests.dll \ --blame-hang-timeout 300seconds --blame-crash \ --settings lean-python-unit-tests.runsettings From 8e384a300f2c5e9ccc400908e1e6b726ec0816f9 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 16 Jul 2026 11:21:18 -0400 Subject: [PATCH 3/4] Use bash for the test filter generation step Container jobs default to sh, which does not support the here-string used when writing the runsettings file. --- .github/workflows/lean-python-unit-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lean-python-unit-tests.yml b/.github/workflows/lean-python-unit-tests.yml index 81a779d52..87c2d4db1 100644 --- a/.github/workflows/lean-python-unit-tests.yml +++ b/.github/workflows/lean-python-unit-tests.yml @@ -52,6 +52,7 @@ jobs: Lean/Tests/bin/Release/Python.Runtime.dll - name: Generate Python unit test filter + shell: bash run: | # Select every test class whose source references Lean's Python # interop layer (Py.GIL, PythonEngine, PyObject, Language.Python From e2dd855a1bc5285a1e9c1c8e676229d45c588261 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 16 Jul 2026 11:26:18 -0400 Subject: [PATCH 4/4] Run Lean Python regression tests job on self-hosted runner Follow the pattern of Lean's own regression tests CI job: run directly in a quantconnect/lean:foundation container on a self-hosted runner instead of manually managing a docker container on a GitHub-hosted runner. --- .../lean-python-regression-tests.yml | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/.github/workflows/lean-python-regression-tests.yml b/.github/workflows/lean-python-regression-tests.yml index d88946c7a..5f8aaf5a2 100644 --- a/.github/workflows/lean-python-regression-tests.yml +++ b/.github/workflows/lean-python-regression-tests.yml @@ -18,8 +18,10 @@ concurrency: jobs: lean-python-regression: - runs-on: ubuntu-24.04 - timeout-minutes: 360 + runs-on: self-hosted + container: + image: quantconnect/lean:foundation + options: --cpus 12 --memory 12g steps: - name: Checkout pythonnet uses: actions/checkout@v4 @@ -32,41 +34,19 @@ jobs: repository: QuantConnect/Lean path: Lean - - name: Liberate disk space - uses: jlumbroso/free-disk-space@main - with: - tool-cache: true - large-packages: false - docker-images: false - swap-storage: false - - - name: Define docker helper - run: | - echo 'runInContainer() { docker exec test-container "$@"; }' > $HOME/ci_functions.sh - echo "BASH_ENV=$HOME/ci_functions.sh" >> $GITHUB_ENV - - - name: Start container - run: | - docker run -d \ - --workdir /__w/pythonnet/pythonnet \ - -v /home/runner/work:/__w \ - --name test-container \ - quantconnect/lean:foundation \ - tail -f /dev/null - - name: Build Python.Runtime.dll run: | - runInContainer dotnet build pythonnet/src/runtime/Python.Runtime.csproj \ + dotnet build pythonnet/src/runtime/Python.Runtime.csproj \ -c Release /v:quiet /p:WarningLevel=1 - name: Build Lean run: | - runInContainer dotnet build /p:Configuration=Release /v:quiet /p:WarningLevel=1 \ + dotnet build /p:Configuration=Release /v:quiet /p:WarningLevel=1 \ Lean/QuantConnect.Lean.sln - name: Inject freshly built Python.Runtime.dll into Lean test output run: | - runInContainer cp pythonnet/pythonnet/runtime/Python.Runtime.dll \ + cp pythonnet/pythonnet/runtime/Python.Runtime.dll \ Lean/Tests/bin/Release/Python.Runtime.dll - name: Restrict regression tests to Python only @@ -75,12 +55,12 @@ jobs: # key (defaults to CSharp + Python). Setting it to Python only makes the # test factory emit Python test cases exclusively. The config file is # JSONC; Newtonsoft tolerates the inserted line. - runInContainer sed -i '1a\ "regression-test-languages": ["Python"],' \ + sed -i '1a\ "regression-test-languages": ["Python"],' \ Lean/Tests/bin/Release/config.json - name: Run Lean Python regression tests run: | - runInContainer dotnet test Lean/Tests/bin/Release/QuantConnect.Tests.dll \ + dotnet test Lean/Tests/bin/Release/QuantConnect.Tests.dll \ --blame-hang-timeout 300seconds --blame-crash \ --filter "TestCategory=RegressionTests & Name~Python/" \ -- TestRunParameters.Parameter\(name=\"log-handler\", value=\"ConsoleErrorLogHandler\"\) \