name: tests

on:
  push:
    branches:
      - master
  pull_request:
  workflow_dispatch:
  workflow_call:
    inputs:
      run_number:
        default: '1'
        required: true
        type: string

concurrency:
  group: tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
  cancel-in-progress: true

env:
  CI: 1
  PYTHONPATH: ${{ github.workspace }}
  PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical

jobs:
  build_release:
    name: build release
    runs-on: ${{
      (github.repository == 'commaai/openpilot') &&
      ((github.event_name != 'pull_request') ||
      (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
      && fromJSON('["namespace-profile-amd64-8x16"]')
      || fromJSON('["ubuntu-24.04"]') }}
    env:
      STRIPPED_DIR: /tmp/releasepilot
      PYTHONPATH: /tmp/releasepilot
    steps:
    - uses: actions/checkout@v6
      with:
        submodules: true
    - name: Getting LFS files
      uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e
      with:
        timeout_minutes: 2
        max_attempts: 3
        command: git lfs pull
    - name: Build devel
      timeout-minutes: 1
      run: TARGET_DIR=$STRIPPED_DIR release/build_stripped.sh
    - run: ./tools/op.sh setup
    - name: Build openpilot and run checks
      timeout-minutes: 30
      working-directory: ${{ env.STRIPPED_DIR }}
      run: python3 system/manager/build.py
    - name: Run tests
      timeout-minutes: 1
      working-directory: ${{ env.STRIPPED_DIR }}
      run: release/check-dirty.sh
    - name: Check submodules
      if: github.repository == 'sunnypilot/sunnypilot'
      timeout-minutes: 3
      run: |
        if [ "${{ github.ref }}" != "refs/heads/master" ]; then
          git fetch  origin master:refs/remotes/origin/master

          SUBMODULE_PATHS=$(git diff origin/master HEAD --name-only | grep -E '^[^/]+$' | while read path; do
            if git ls-files --stage "$path" | grep -q "^160000"; then
              echo "$path"
            fi
          done | tr '\n' ' ')

          if [ -n "$SUBMODULE_PATHS" ]; then
            echo "Changed submodule paths: $SUBMODULE_PATHS"
            export SUBMODULE_PATHS="$SUBMODULE_PATHS"
            export CHECK_PR_REFS=true
          fi
        fi
        release/check-submodules.sh

  build_mac:
    name: build macOS
    runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }}
    steps:
    - uses: actions/checkout@v6
      with:
        submodules: true
    - name: Remove Homebrew from environment
      run: |
        FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':')
        echo "PATH=${FILTERED}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_ENV
    - run: ./tools/op.sh setup
    - name: Building openpilot
      run: scons

  static_analysis:
    name: static analysis
    runs-on: ${{
      (github.repository == 'commaai/openpilot') &&
      ((github.event_name != 'pull_request') ||
      (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
      && fromJSON('["namespace-profile-amd64-8x16"]')
      || fromJSON('["ubuntu-24.04"]') }}
    steps:
    - uses: actions/checkout@v6
      with:
        submodules: true
    - run: ./tools/op.sh setup
    - name: Static analysis
      timeout-minutes: 1
      run: scripts/lint/lint.sh

  unit_tests:
    name: unit tests
    runs-on: ${{
      (github.repository == 'commaai/openpilot') &&
      ((github.event_name != 'pull_request') ||
      (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
      && fromJSON('["namespace-profile-amd64-8x16"]')
      || fromJSON('["ubuntu-24.04"]') }}
    steps:
    - uses: actions/checkout@v6
      with:
        submodules: true
    - run: ./tools/op.sh setup
    - name: Build openpilot
      run: scons -j$(nproc)
    - name: Run unit tests
      timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 999 }}
      run: |
        source selfdrive/test/setup_xvfb.sh
        # Pre-compile Python bytecode so each pytest worker doesn't need to
        $PYTEST --collect-only -m 'not slow' -qq
        MAX_EXAMPLES=1 $PYTEST -m 'not slow'

  process_replay:
    name: process replay
    if: false  # disable process_replay for forks
    runs-on: ${{
      (github.repository == 'commaai/openpilot') &&
      ((github.event_name != 'pull_request') ||
      (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
      && fromJSON('["namespace-profile-amd64-8x16"]')
      || fromJSON('["ubuntu-24.04"]') }}
    steps:
    - uses: actions/checkout@v6
      with:
        submodules: true
    - run: ./tools/op.sh setup
    - name: Build openpilot
      run: scons -j$(nproc)
    - name: Run replay
      timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 20 }}
      continue-on-error: ${{ github.ref == 'refs/heads/master' }}
      run: selfdrive/test/process_replay/test_processes.py -j$(nproc)
    - name: Print diff
      id: print-diff
      if: always()
      run: cat selfdrive/test/process_replay/diff.txt
    - uses: actions/upload-artifact@v6
      if: always()
      continue-on-error: true
      with:
        name: process_replay_diff.txt
        path: selfdrive/test/process_replay/diff.txt
    - name: Checkout ci-artifacts
      if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
      uses: actions/checkout@v4
      with:
        repository: commaai/ci-artifacts
        ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
        path: ${{ github.workspace }}/ci-artifacts
    - name: Push refs
      if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
      working-directory: ${{ github.workspace }}/ci-artifacts
      run: |
        git config user.name "GitHub Actions Bot"
        git config user.email "<>"
        git fetch origin process-replay || true
        git checkout process-replay 2>/dev/null || git checkout --orphan process-replay
        cp ${{ github.workspace }}/selfdrive/test/process_replay/fakedata/*.zst .
        echo "${{ github.sha }}" > ref_commit
        git add .
        git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
        git push origin process-replay
    - name: Run regen
      if: false
      timeout-minutes: 4
      env:
        ONNXCPU: 1
      run: $PYTEST selfdrive/test/process_replay/test_regen.py

  simulator_driving:
    name: simulator driving
    runs-on: ${{
      (github.repository == 'commaai/openpilot') &&
      ((github.event_name != 'pull_request') ||
      (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
      && fromJSON('["namespace-profile-amd64-8x16"]')
      || fromJSON('["ubuntu-24.04"]') }}
    if: false  # FIXME: Started to timeout recently
    steps:
    - uses: actions/checkout@v6
      with:
        submodules: true
    - run: ./tools/op.sh setup
    - name: Build openpilot
      run: scons -j$(nproc)
    - name: Driving test
      timeout-minutes: 2
      run: |
        source selfdrive/test/setup_xvfb.sh
        pytest -s tools/sim/tests/test_metadrive_bridge.py

  create_ui_report:
    name: Create UI Report
    runs-on: ${{
      (github.repository == 'commaai/openpilot') &&
      ((github.event_name != 'pull_request') ||
      (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
      && fromJSON('["namespace-profile-amd64-8x16"]')
      || fromJSON('["ubuntu-24.04"]') }}
    steps:
      - uses: actions/checkout@v6
        with:
          submodules: true
      - run: ./tools/op.sh setup
      - name: Build openpilot
        run: scons -j$(nproc)
      - name: Create UI Report
        run: |
          source selfdrive/test/setup_xvfb.sh
          python3 selfdrive/ui/tests/diff/replay.py
          python3 selfdrive/ui/tests/diff/replay.py --big
      - name: Upload UI Report
        uses: actions/upload-artifact@v6
        with:
          name: ui-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
          path: selfdrive/ui/tests/diff/report
