name: Run tests with twister

on:
  push:
    branches:
      - main
      - v*-branch
      - collab-*
  pull_request_target:
    branches:
      - main
      - v*-branch
      - collab-*
  schedule:
    # Run at 03:00 UTC on every Sunday
    - cron: '0 3 * * 0'

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref }}
  cancel-in-progress: true

jobs:
  twister-build-prep:
    if: github.repository_owner == 'zephyrproject-rtos'
    runs-on:
      group: zephyr-runner-v2-linux-x64-4xlarge
    container:
      image: ghcr.io/zephyrproject-rtos/ci-repo-cache:v0.26.11.20240324
      options: '--entrypoint /bin/bash'
    outputs:
      subset: ${{ steps.output-services.outputs.subset }}
      size: ${{ steps.output-services.outputs.size }}
      fullrun: ${{ steps.output-services.outputs.fullrun }}
    env:
      MATRIX_SIZE: 10
      PUSH_MATRIX_SIZE: 20
      DAILY_MATRIX_SIZE: 80
      BSIM_OUT_PATH: /opt/bsim/
      BSIM_COMPONENTS_PATH: /opt/bsim/components
      TESTS_PER_BUILDER: 700
      COMMIT_RANGE: ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}
      BASE_REF: ${{ github.base_ref }}
    steps:
      - name: Apply container owner mismatch workaround
        run: |
          # FIXME: The owner UID of the GITHUB_WORKSPACE directory may not
          #        match the container user UID because of the way GitHub
          #        Actions runner is implemented. Remove this workaround when
          #        GitHub comes up with a fundamental fix for this problem.
          git config --global --add safe.directory ${GITHUB_WORKSPACE}

      - name: Print cloud service information
        run: |
          echo "ZEPHYR_RUNNER_CLOUD_PROVIDER = ${ZEPHYR_RUNNER_CLOUD_PROVIDER}"
          echo "ZEPHYR_RUNNER_CLOUD_NODE = ${ZEPHYR_RUNNER_CLOUD_NODE}"
          echo "ZEPHYR_RUNNER_CLOUD_POD = ${ZEPHYR_RUNNER_CLOUD_POD}"

      - name: Clone cached Zephyr repository
        if: github.event_name == 'pull_request_target'
        continue-on-error: true
        run: |
          git clone --shared /repo-cache/zephyrproject/zephyr .
          git remote set-url origin ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}

      - name: Checkout
        if: github.event_name == 'pull_request_target'
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0
          persist-credentials: false

      - name: Environment Setup
        if: github.event_name == 'pull_request_target'
        run: |
          git config --global user.email "bot@zephyrproject.org"
          git config --global user.name "Zephyr Bot"
          rm -fr ".git/rebase-apply"
          git rebase origin/${BASE_REF}
          git log  --pretty=oneline | head -n 10
          west init -l . || true
          west config manifest.group-filter -- +ci,+optional
          west config --global update.narrow true
          west update --path-cache /repo-cache/zephyrproject 2>&1 1> west.update.log || west update --path-cache /repo-cache/zephyrproject 2>&1 1> west.update.log || ( rm -rf ../modules ../bootloader ../tools && west update --path-cache /repo-cache/zephyrproject)
          west forall -c 'git reset --hard HEAD'

          echo "ZEPHYR_SDK_INSTALL_DIR=/opt/toolchains/zephyr-sdk-$( cat SDK_VERSION )" >> $GITHUB_ENV

      - name: Generate Test Plan with Twister
        if: github.event_name == 'pull_request_target'
        id: test-plan
        run: |
          export ZEPHYR_BASE=${PWD}
          export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
          python3 ./scripts/ci/test_plan.py -c origin/${BASE_REF}.. --pull-request -t $TESTS_PER_BUILDER
          if [ -s .testplan ]; then
            cat .testplan >> $GITHUB_ENV
          else
            echo "TWISTER_NODES=${MATRIX_SIZE}" >> $GITHUB_ENV
          fi
          rm -f testplan.json .testplan

      - name: Determine matrix size
        id: output-services
        run: |
          if [ "${{github.event_name}}" = "pull_request_target" ]; then
            if [ -n "${TWISTER_NODES}" ]; then
              subset="[$(seq -s',' 1 ${TWISTER_NODES})]"
            else
              subset="[$(seq -s',' 1 ${MATRIX_SIZE})]"
            fi
            size=${TWISTER_NODES}
          elif [ "${{github.event_name}}" = "push" ]; then
            subset="[$(seq -s',' 1 ${PUSH_MATRIX_SIZE})]"
            size=${MATRIX_SIZE}
          elif [ "${{github.event_name}}" = "schedule" -a "${{github.repository}}" = "zephyrproject-rtos/zephyr" ]; then
            subset="[$(seq -s',' 1 ${DAILY_MATRIX_SIZE})]"
            size=${DAILY_MATRIX_SIZE}
          else
            size=0
          fi
          echo "subset=${subset}" >> $GITHUB_OUTPUT
          echo "size=${size}" >> $GITHUB_OUTPUT
          echo "fullrun=${TWISTER_FULL}" >> $GITHUB_OUTPUT

  twister-build:
    runs-on:
      group: zephyr-runner-v2-linux-x64-4xlarge
    needs: twister-build-prep
    if: needs.twister-build-prep.outputs.size != 0
    container:
      image: ghcr.io/zephyrproject-rtos/ci-repo-cache:v0.26.11.20240324
      options: '--entrypoint /bin/bash'
    strategy:
      fail-fast: false
      matrix:
        subset: ${{fromJSON(needs.twister-build-prep.outputs.subset)}}
    timeout-minutes: 1440
    env:
      CCACHE_DIR: /node-cache/ccache-zephyr
      CCACHE_REMOTE_STORAGE: "redis://cache-*.keydb-cache.svc.cluster.local|shards=1,2,3"
      CCACHE_REMOTE_ONLY: "true"
      # `--specs` is ignored because ccache is unable to resolve the toolchain specs file path.
      CCACHE_IGNOREOPTIONS: '--specs=*'
      BSIM_OUT_PATH: /opt/bsim/
      BSIM_COMPONENTS_PATH: /opt/bsim/components
      TWISTER_COMMON: ' --force-color --inline-logs -v -N -M --retry-failed 3 --timeout-multiplier 2 '
      DAILY_OPTIONS: ' -M --build-only --all --show-footprint'
      PR_OPTIONS: ' --clobber-output --integration'
      PUSH_OPTIONS: ' --clobber-output -M --show-footprint'
      COMMIT_RANGE: ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}
      BASE_REF: ${{ github.base_ref }}
    steps:
      - name: Print cloud service information
        run: |
          echo "ZEPHYR_RUNNER_CLOUD_PROVIDER = ${ZEPHYR_RUNNER_CLOUD_PROVIDER}"
          echo "ZEPHYR_RUNNER_CLOUD_NODE = ${ZEPHYR_RUNNER_CLOUD_NODE}"
          echo "ZEPHYR_RUNNER_CLOUD_POD = ${ZEPHYR_RUNNER_CLOUD_POD}"

      - name: Apply container owner mismatch workaround
        run: |
          # FIXME: The owner UID of the GITHUB_WORKSPACE directory may not
          #        match the container user UID because of the way GitHub
          #        Actions runner is implemented. Remove this workaround when
          #        GitHub comes up with a fundamental fix for this problem.
          git config --global --add safe.directory ${GITHUB_WORKSPACE}

      - name: Clone cached Zephyr repository
        continue-on-error: true
        run: |
          git clone --shared /repo-cache/zephyrproject/zephyr .
          git remote set-url origin ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}

      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0
          persist-credentials: false

      - name: Environment Setup
        run: |
          if [ "${{github.event_name}}" = "pull_request_target" ]; then
            git config --global user.email "bot@zephyrproject.org"
            git config --global user.name "Zephyr Builder"
            rm -fr ".git/rebase-apply"
            git rebase origin/${BASE_REF}
            git log  --pretty=oneline | head -n 10
          fi
          echo "$HOME/.local/bin" >> $GITHUB_PATH

          west init -l . || true
          west config manifest.group-filter -- +ci,+optional
          west config --global update.narrow true
          west update --path-cache /repo-cache/zephyrproject 2>&1 1> west.update.log || west update --path-cache /repo-cache/zephyrproject 2>&1 1> west.update.log || ( rm -rf ../modules ../bootloader ../tools && west update --path-cache /repo-cache/zephyrproject)
          west forall -c 'git reset --hard HEAD'

          echo "ZEPHYR_SDK_INSTALL_DIR=/opt/toolchains/zephyr-sdk-$( cat SDK_VERSION )" >> $GITHUB_ENV

      - name: Check Environment
        run: |
          cmake --version
          gcc --version
          ls -la
          echo "github.ref: ${{ github.ref }}"
          echo "github.base_ref: ${{ github.base_ref }}"
          echo "github.ref_name: ${{ github.ref_name }}"

      - name: Set up ccache
        run: |
          mkdir -p ${CCACHE_DIR}
          ccache -M 10G
          ccache -p
          ccache -z -s -vv

      - name: Update BabbleSim to manifest revision
        run: |
          export BSIM_VERSION=$( west list bsim -f {revision} )
          echo "Manifest points to bsim sha $BSIM_VERSION"
          cd /opt/bsim_west/bsim
          git fetch -n origin ${BSIM_VERSION}
          git -c advice.detachedHead=false checkout ${BSIM_VERSION}
          west update
          make everything -s -j 8

      - if: github.event_name == 'push'
        name: Run Tests with Twister (Push)
        run: |
          export ZEPHYR_BASE=${PWD}
          export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
          ./scripts/twister --subset ${{matrix.subset}}/${{ strategy.job-total }} ${TWISTER_COMMON} ${PUSH_OPTIONS}
          if [ "${{matrix.subset}}" = "1" ]; then
            ./scripts/zephyr_module.py --twister-out module_tests.args
            if [ -s module_tests.args ]; then
              ./scripts/twister +module_tests.args --outdir module_tests ${TWISTER_COMMON} ${PUSH_OPTIONS}
            fi
          fi

      - if: github.event_name == 'pull_request_target'
        name: Run Tests with Twister (Pull Request)
        run: |
          rm -f testplan.json
          export ZEPHYR_BASE=${PWD}
          export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
          python3 ./scripts/ci/test_plan.py -c origin/${BASE_REF}.. --pull-request
          ./scripts/twister --subset ${{matrix.subset}}/${{ strategy.job-total }} --load-tests testplan.json ${TWISTER_COMMON} ${PR_OPTIONS}
          if [ "${{matrix.subset}}" = "1" -a ${{needs.twister-build-prep.outputs.fullrun}} = 'True' ]; then
            ./scripts/zephyr_module.py --twister-out module_tests.args
            if [ -s module_tests.args ]; then
              ./scripts/twister +module_tests.args --outdir module_tests ${TWISTER_COMMON} ${PR_OPTIONS}
            fi
          fi

      - if: github.event_name == 'schedule'
        name: Run Tests with Twister (Daily)
        run: |
          export ZEPHYR_BASE=${PWD}
          export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
          ./scripts/twister --subset ${{matrix.subset}}/${{ strategy.job-total }} ${TWISTER_COMMON} ${DAILY_OPTIONS}
          if [ "${{matrix.subset}}" = "1" ]; then
            ./scripts/zephyr_module.py --twister-out module_tests.args
            if [ -s module_tests.args ]; then
              ./scripts/twister +module_tests.args --outdir module_tests ${TWISTER_COMMON} ${DAILY_OPTIONS}
            fi
          fi

      - name: Print ccache stats
        if: always()
        run: |
          ccache -s -vv

      - name: Upload Unit Test Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: Unit Test Results (Subset ${{ matrix.subset }})
          if-no-files-found: ignore
          path: |
            twister-out/twister.xml
            twister-out/twister.json
            module_tests/twister.xml
            testplan.json

      - if: matrix.subset == 1 && github.event_name == 'push'
        name: Save the list of Python packages
        shell: bash
        run: |
          FREEZE_FILE="frozen-requirements.txt"
          timestamp="$(date)"
          version="$(git describe --abbrev=12 --always)"
          echo -e "# Generated at $timestamp ($version)\n" > $FREEZE_FILE
          pip3 freeze | tee -a $FREEZE_FILE

      - if: matrix.subset == 1 && github.event_name == 'push'
        name: Upload the list of Python packages
        uses: actions/upload-artifact@v4
        with:
          name: Frozen PIP package set
          path: |
            frozen-requirements.txt

  twister-test-results:
    name: "Publish Unit Tests Results"
    env:
      ELASTICSEARCH_KEY: ${{ secrets.ELASTICSEARCH_KEY }}
      ELASTICSEARCH_SERVER: "https://elasticsearch.zephyrproject.io:443"
    needs: twister-build
    runs-on: ubuntu-22.04
    # the build-and-test job might be skipped, we don't need to run this job then
    if: success() || failure()

    steps:
      # Needed for opensearch and upload script
      - if: github.event_name == 'push' || github.event_name == 'schedule'
        name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Download Artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - if: github.event_name == 'push' || github.event_name == 'schedule'
        name: Upload to opensearch
        run: |
          pip3 install elasticsearch
          # set run date on upload to get consistent and unified data across the matrix.
          run_date=`date --iso-8601=minutes`
          if [ "${{github.event_name}}" = "push" ]; then
            python3 ./scripts/ci/upload_test_results_es.py -r ${run_date} \
              --index zephyr-main-ci-push-1 artifacts/*/*/twister.json
          elif [ "${{github.event_name}}" = "schedule" ]; then
            python3 ./scripts/ci/upload_test_results_es.py -r ${run_date} \
              --index zephyr-main-ci-weekly-1 artifacts/*/*/twister.json
          fi

      - name: Merge Test Results
        run: |
          pip3 install junitparser junit2html
          junitparser merge artifacts/*/*/twister.xml junit.xml
          junit2html junit.xml junit.html

      - name: Upload Unit Test Results in HTML
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: HTML Unit Test Results
          if-no-files-found: ignore
          path: |
            junit.html

      - name: Publish Unit Test Results
        uses: EnricoMi/publish-unit-test-result-action@v2
        with:
          check_name: Unit Test Results
          files: "**/twister.xml"
          comment_mode: off
  twister-status-check:
    if: always()
    name: "Check Twister Status"
    needs:
      - twister-build-prep
      - twister-build
    uses: ./.github/workflows/ready-to-merge.yml
    with:
      needs_context: ${{ toJson(needs) }}
