From d08aa7c5d8f2e5c3cb6978a9c30d27b96898e3db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20S=C3=A9guin?= <162352622+francois-pass-culture@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:50:22 +0100 Subject: [PATCH] (PC-34429)[API] ci: use cached pytest duration file --- .../workflows/dev_on_workflow_tests_api.yml | 125 +++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev_on_workflow_tests_api.yml b/.github/workflows/dev_on_workflow_tests_api.yml index 169faeb91b4..e8047658ac7 100644 --- a/.github/workflows/dev_on_workflow_tests_api.yml +++ b/.github/workflows/dev_on_workflow_tests_api.yml @@ -393,6 +393,53 @@ jobs: env: SLACK_BOT_TOKEN: ${{ steps.secrets.outputs.SLACK_BOT_TOKEN }} + restore-test-durations: + name: Restore pytest durations + runs-on: ubuntu-22.04 + steps: +# # http://man7.org/linux/man-pages/man1/date.1.html +# - name: Get Date +# id: get-date +# run: | +# echo "date=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_OUTPUT +# shell: bash +# Then we'll use tests-durations-${{ steps.get-date.outputs.date }} + + # It's mandatory to use the exact same path when saving/restoring cache, otherwise it won't work + # (the same key is not enough - see documentation: + # https://github.com/actions/cache/blob/main/README.md#cache-version). + # I went with `/tests/.test_durations`. + - name: Restore test durations + id: restore-test-durations + uses: actions/cache/restore@v4 + with: + path: /tests/.test_durations + key: tests-durations-${{ github.sha }} + restore-keys: | + tests-durations-${{ github.sha }}- + tests-durations- + fail-on-cache-miss: false + + # Then we upload the restored test durations as an artifact. This way, each matrix job will download + # it when it starts. When a matrix job will be manually retried, it will also reuse the artifact (to + # retry the exact same tests, even if the cache has been updated in the meantime). + - name: Upload test durations + if: steps.restore-test-durations.outputs.cache-hit != '' + uses: actions/upload-artifact@v4 + with: + name: test-durations-before + path: /tests/.test_durations + + outputs: + # This output will be used to know if we had a cache hit (exact match or not), or no cache hit at all. + # See documentation about the `cache-hit` output: + # https://github.com/actions/cache/blob/main/README.md#outputs + # > cache-hit - A string value to indicate an exact match was found for the key. + # > If there's a cache hit, this will be 'true' or 'false' to indicate if there's an exact match + # > for key. + # > If there's a cache miss, this will be an empty string. + restored: ${{ steps.restore-test-durations.outputs.cache-hit == '' && 'false' || 'true' }} + pytest: name: "Pytest ${{ matrix.pytest_args.description }} : ${{ matrix.group }}/4" env: @@ -490,6 +537,7 @@ jobs: alembic upgrade pre@head alembic upgrade post@head flask install_data + - name: "Mount a Volume with pcapi rights" uses: addnab/docker-run-action@v3 with: @@ -499,6 +547,25 @@ jobs: run: | echo "Changing owner and group fort directory test" chown -R pcapi:pcapi /tests + + # These two steps will be executed only when there IS a cache hit (exact match or not). When a matrix + # job is retried, it will reuse the same artifact, to execute the exact same split. + - name: Download test durations + if: needs.restore-test-durations.outputs.restored == 'true' + uses: actions/download-artifact@v4 + with: + name: test-durations-before + + - name: Use cached test durations + if: needs.restore-test-durations.outputs.restored == 'true' + run: mv .test_durations ${{ runner.workspace }}/pass-culture-main/api/tests/.test_durations + + # When running pytest, we write the new test durations using options + # `--store-durations --clean-durations`. + # Option `--clean-durations` is undocumented but you can check its implementation here: + # https://github.com/jerry-git/pytest-split/blob/fb9af7e0122c18a96a7c01ca734c4ab01027f8d9/src/pytest_split/plugin.py#L68-L76 + # > Removes the test duration info for tests which are not present while running the suite with + # > '--store-durations'. - name: "Run pytest" uses: addnab/docker-run-action@v3 with: @@ -506,7 +573,22 @@ jobs: shell: bash options: -e RUN_ENV -e DATABASE_URL_TEST -e REDIS_URL -e SQLALCHEMY_WARN_20 -v ${{ runner.workspace }}/pass-culture-main/api/tests/:/tests run: | - pytest ${{ matrix.pytest_args.collection }} --splits 4 --group ${{ matrix.group }} --durations=10 --junitxml='/tests/junit.xml' -q --color=yes + pytest ${{ matrix.pytest_args.collection }} \ + --splits 4 --group ${{ matrix.group }} \ + --store-durations --durations-path /tests/.test_durations --clean-durations \ + --durations=10 --junitxml='/tests/junit.xml' \ + -q --color=yes + + - name: Upload test durations +# if: github.run_attempt == 1 + if: always() + uses: actions/upload-artifact@v4 + with: + name: "test-durations-after-partial-${{ matrix.pytest_args.description }}-${{ matrix.group }}" + path: "${{ runner.workspace }}/pass-culture-main/api/tests/.test_durations" + if-no-files-found: warn + include-hidden-files: true + - name: "Publish Test Report" uses: mikepenz/action-junit-report@v5 if: always() # always run even if the previous step fails @@ -540,3 +622,44 @@ jobs: } env: SLACK_BOT_TOKEN: ${{ steps.secrets.outputs.SLACK_BOT_TOKEN }} + + cache-test-durations: + name: Cache test durations + needs: pytest + if: github.run_attempt == 1 && (success() || failure()) + runs-on: ubuntu-22.04 + steps: + - name: Download all partial test durations + uses: actions/download-artifact@v4 + with: + pattern: test-durations-after-partial-* + path: ${{ runner.temp }} + + # This step regroups the 8 partial files and sorts keys alphabetically: + - name: Merge all partial test durations + # overrides workflow default + working-directory: / + run: | + jq -s 'add' ${{ runner.temp }}/test-durations-after-partial-*/.test_durations \ + | jq 'to_entries | sort_by(.key) | from_entries' \ + > ${{ runner.temp }}/.test_durations + + # This step uploads the final file as an artifact. You can then download it from the Github GUI, + # and use it to manually commit file `.test_durations_fallback` from time to time, + # to keep an up-to-date fallback: + - name: Upload final test durations + uses: actions/upload-artifact@v4 + with: + name: test-durations-after + path: ${{ runner.temp }}/.test_durations + if-no-files-found: warn + include-hidden-files: true + + # Finally, we cache the new test durations. This file will be restored in next CI execution + # (see step "Restore test durations" above). + - name: Cache final test durations + uses: actions/cache/save@v4 + with: + path: ${{ runner.temp }}/.test_durations + key: tests-durations-${{ github.sha }} +# key: tests-durations-${{ steps.get-date.outputs.date }}}-