name: Test

on:
  # Run on pushes to the main branches and on all pull requests.
  # Prevent the build from running when there are only irrelevant changes.
  push:
    branches:
      - 3.x
      - 4.x
    tags:
      - '**'
    paths-ignore:
      - '**.md'
  pull_request:
  # Allow manually triggering the workflow.
  workflow_dispatch:

jobs:
  build:
    # Cancels all previous runs of this particular job for the same branch that have not yet completed.
    concurrency:
      # The concurrency group contains the workflow name, job name and the branch name.
      group: ${{ github.workflow }}-${{ github.job }}-${{ github.ref }}
      cancel-in-progress: true

    name: "Build Phar on PHP: 8.0"

    permissions:
      id-token: write
      contents: read
      attestations: write

    uses: ./.github/workflows/reusable-build-phar.yml
    with:
      uploadArtifacts: true
      retentionDays: 28
      # Only attests the build artifacts which will be used in the published releases as per the guidelines in "what to attest".
      # https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds
      createAttestations: ${{ github.ref_type == 'tag' }}

  test:
    # Cancels all previous runs of this particular job for the same branch that have not yet completed.
    concurrency:
      # The concurrency group contains the workflow name, job name, job index and the branch name.
      group: ${{ github.workflow }}-${{ github.job }}-${{ strategy.job-index }}-${{ github.ref }}
      cancel-in-progress: true

    runs-on: ${{ matrix.os }}
    needs: build

    strategy:
      # Keys:
      # - custom_ini: Whether to run with specific custom ini settings to hit very specific
      #   code conditions.
      matrix:
        os: ['ubuntu-latest', 'windows-latest']
        php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
        custom_ini: [false]

        exclude:
          # Installing on Windows with PHP 5.4 runs into all sorts of problems (which are not ours).
          - php: '5.4'
            os: 'windows-latest'
          # By default, the memory limit should be disabled with setup_php, but we're still running
          # into memory limit issues when the full test suite is run on PHP 5.6 in Windows.
          # As support for PHP < 7.2 will be dropped in PHPCS 4.0 anyhow, let's just skip the build.
          - php: '5.6'
            os: 'windows-latest'

        include:
          # Skip test runs on builds which are also run in the coverage job.
          # Note: the tests on PHP 7.2 will still be run as the coverage build uses custom_ini settings for that version.
          - php: '5.4'
            os: 'ubuntu-latest'
            skip_tests: true
          - php: '5.5'
            os: 'windows-latest'
            skip_tests: true
          - php: '8.4'
            skip_tests: true

          # The default libxml library on Ubuntu images is a little out of date.
          # To safeguard support for the latest libxml we need to update the library on the fly.
          # This only needs to be tested with one PHP version for each libxml minor to verify support.
          # Testing against multiple PHP versions would not yield a difference in results.
          - php: '8.0'
            os: 'ubuntu-latest'
            libxml_minor: '2.11'
          - php: '8.3'
            os: 'ubuntu-latest'
            libxml_minor: '2.13'

          # Extra builds running only the unit tests with different PHP ini settings.
          - php: '5.5'
            os: 'ubuntu-latest'
            custom_ini: true
          - php: '7.0'
            os: 'ubuntu-latest'
            custom_ini: true
          - php: '8.0'
            os: 'ubuntu-latest'
            custom_ini: true
          - php: '8.2'
            os: 'ubuntu-latest'
            custom_ini: true

    # yamllint disable-line rule:line-length
    name: "PHP: ${{ matrix.php }} ${{ matrix.custom_ini && ' with custom ini settings' || '' }}${{ matrix.libxml_minor && format( ' with libxml {0}', matrix.libxml_minor ) || '' }} (${{ matrix.os == 'ubuntu-latest' && 'Linux' || 'Win'  }})"

    continue-on-error: ${{ matrix.php == '8.5' }}

    steps:
      - name: Prepare git to leave line endings alone
        run: git config --global core.autocrlf input

      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

      - name: "libxml2: find the latest relevant tag"
        if: ${{ matrix.libxml_minor }}
        id: libxml_version
        uses: oprypin/find-latest-tag@dd2729fe78b0bb55523ae2b2a310c6773a652bd1 # v1.1.2
        with:
          repository: GNOME/libxml2
          releases-only: false  # The libxml2 repository doesn't use GitHub's "release" feature.
          prefix: 'v${{ matrix.libxml_minor }}.' # Limit the result to the minor we're interested in.
          sort-tags: true # Find the "greatest" version for that minor based on semver.

      # To put it simply: we need to remove the 'v' prefix from the version number.
      - name: "libxml2: parse the version to a patch version"
        if: ${{ matrix.libxml_minor }}
        id: libxml_patch_version
        shell: bash
        env:
          TAG: ${{ steps.libxml_version.outputs.tag }}
        run: echo "PATCH=$( echo "$TAG" | cut -b 2- )" >> "$GITHUB_OUTPUT"

      - name: "libxml2: restore cache"
        if: ${{ matrix.libxml_minor }}
        id: libxml_cache_restore
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: "libxml2-${{ steps.libxml_patch_version.outputs.PATCH }}"
          key: "${{ matrix.os }}-libxml-${{ matrix.libxml_minor }}-${{ steps.libxml_patch_version.outputs.PATCH }}"

      # Updating the lists can fail intermittently, typically after Microsoft has released a new package.
      # This should not be blocking for this job, so ignore any errors from this step.
      # Ref: https://github.com/dotnet/core/issues/4167
      - name: "libxml2: Update the available packages list"
        if: ${{ matrix.libxml_minor && steps.libxml_cache_restore.outputs.cache-hit != 'true' }}
        continue-on-error: true
        run: sudo apt-get update

      - name: "libxml2: Download and build package (linux only)"
        if: ${{ matrix.libxml_minor && steps.libxml_cache_restore.outputs.cache-hit != 'true' }}
        env:
          PATCH: ${{ steps.libxml_patch_version.outputs.PATCH }}
        run: |
          sudo apt-get install -y wget build-essential
          wget "https://download.gnome.org/sources/libxml2/${{ matrix.libxml_minor }}/libxml2-$PATCH.tar.xz"
          tar -xf "libxml2-$PATCH.tar.xz"
          cd "libxml2-$PATCH"
          ./configure --prefix=/usr/local
          make

      - name: "libxml2: save cache"
        if: ${{ matrix.libxml_minor && steps.libxml_cache_restore.outputs.cache-hit != 'true' }}
        id: libxml_cache_save
        uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: "libxml2-${{ steps.libxml_patch_version.outputs.PATCH }}"
          key: ${{ steps.libxml_cache_restore.outputs.cache-primary-key }}

      - name: "libxml2: Install package (linux only)"
        if: ${{ matrix.libxml_minor }}
        env:
          PATCH: ${{ steps.libxml_patch_version.outputs.PATCH }}
        run: |
          cd "libxml2-$PATCH"
          sudo make install
          sudo ldconfig

      - name: Setup ini config
        id: set_ini
        shell: bash
        # yamllint disable rule:line-length
        run: |
          # Set the "short_open_tag" ini to make sure specific conditions are tested.
          # Also turn on error_reporting to ensure all notices are shown.
          if [[ ${{ matrix.custom_ini }} == true && "${{ matrix.php }}" == '5.5' ]]; then
            echo 'PHP_INI=error_reporting=-1, display_errors=On, display_startup_errors=On, date.timezone=Australia/Sydney, short_open_tag=On, asp_tags=On' >> "$GITHUB_OUTPUT"
          elif [[ ${{ matrix.custom_ini }} == true && "${{ matrix.php }}" != '5.5' ]]; then
            echo 'PHP_INI=error_reporting=-1, display_errors=On, display_startup_errors=On, date.timezone=Australia/Sydney, short_open_tag=On' >> "$GITHUB_OUTPUT"
          else
            echo 'PHP_INI=error_reporting=-1, display_errors=On, display_startup_errors=On' >> "$GITHUB_OUTPUT"
          fi
          # yamllint enable rule:line-length

      - name: Install PHP
        uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5
        with:
          php-version: ${{ matrix.php }}
          ini-values: ${{ steps.set_ini.outputs.PHP_INI }}
          coverage: none

      - name: "DEBUG: show libxml loaded version (php)"
        run: php -r 'echo "libxml loaded version = ", LIBXML_LOADED_VERSION, PHP_EOL;'

      # This action also handles the caching of the dependencies.
      - name: Set up node
        if: ${{ matrix.custom_ini == false }}
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
        with:
          node-version: '20'

      - name: Install external tools used in tests
        if: ${{ matrix.custom_ini == false }}
        run: >
          npm install -g --fund false
          csslint
          eslint
          jshint

      # Install dependencies and handle caching in one go.
      # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer
      - name: Install Composer dependencies
        uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1
        with:
          composer-options: ${{ matrix.php == '8.5' && '--ignore-platform-req=php+' || '' }}
          custom-cache-suffix: $(date -u "+%Y-%m")

      - name: 'PHPCS: set the path to PHP'
        run: php "bin/phpcs" --config-set php_path php

      - name: 'PHPUnit: run the full test suite without code coverage'
        if: ${{ matrix.skip_tests != true }}
        run: php "vendor/bin/phpunit" tests/AllTests.php --no-coverage

      # Do one test run against the complete test suite in CBF mode to ensure all tests can run in CBF mode.
      - name: 'PHPUnit: run the full test suite without code coverage in CBF mode (PHP 8.3 only)'
        if: ${{ matrix.php == '8.3' }}
        run: php "vendor/bin/phpunit" tests/AllTests.php --exclude-group nothing --no-coverage
        env:
          PHP_CODESNIFFER_CBF: '1'

      - name: 'PHPUnit: run select tests in CBF mode'
        if: ${{ matrix.skip_tests != true && matrix.php != '8.3' }}
        run: php "vendor/bin/phpunit" tests/AllTests.php --group CBF --exclude-group nothing --no-coverage
        env:
          PHP_CODESNIFFER_CBF: '1'

      # Note: The code style check is run multiple times against every PHP version
      # as it also acts as an integration test.
      - name: 'PHPCS: check code style without cache, no parallel'
        if: ${{ matrix.custom_ini == false }}
        run: php "bin/phpcs" --no-cache --parallel=1

      - name: Download the PHPCS phar
        if: ${{ matrix.custom_ini == false }}
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
        with:
          name: phpcs-phar

      # This test specifically tests that the Phar which will be released works correctly on all PHP versions.
      - name: 'PHPCS: check code style using the Phar file'
        if: ${{ matrix.custom_ini == false }}
        run: php phpcs.phar

  coverage:
    # Explicitly *NOT* setting "concurrency" for this job to allow for monitoring code coverage for all merges.

    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: ['ubuntu-latest', 'windows-latest']
        php: ['8.4']
        custom_ini: [false]

        include:
          - php: '5.4'
            os: 'ubuntu-latest'
            custom_ini: false
          # Installing on Windows with PHP 5.4 runs into all sorts of problems with Composer.
          # Considering PHP 5.4 is ancient, I deem it acceptable to run coverage on Windows on PHP 5.5.
          # See this issue for more context (yes, I've seen this problem before):
          # @link https://github.com/PHPCSStandards/composer-installer/pull/213
          - php: '5.5'
            os: 'windows-latest'
            custom_ini: false

          # Also run one coverage build with custom ini settings.
          - php: '7.2'
            os: 'ubuntu-latest'
            custom_ini: true

    # yamllint disable-line rule:line-length
    name: "Coverage: ${{ matrix.php }} ${{ matrix.custom_ini && ' with custom ini settings' || '' }} (${{ matrix.os == 'ubuntu-latest' && 'Linux' || 'Win'  }})"

    steps:
      - name: Prepare git to leave line endings alone
        run: git config --global core.autocrlf input

      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

      - name: Setup ini config
        if: ${{ matrix.custom_ini == true && matrix.os != 'windows-latest' }}
        id: set_ini
        shell: bash
        run: |
          # Set the "short_open_tag" ini to make sure specific conditions are tested.
          echo 'PHP_INI=, date.timezone=Australia/Sydney, short_open_tag=On' >> "$GITHUB_OUTPUT"

      - name: Install PHP
        uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5
        with:
          php-version: ${{ matrix.php }}
          ini-values: error_reporting=-1, display_errors=On, display_startup_errors=On${{ steps.set_ini.outputs.PHP_INI }}
          coverage: xdebug

      # This action also handles the caching of the dependencies.
      - name: Set up node
        if: ${{ matrix.custom_ini == false }}
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
        with:
          node-version: '20'

      - name: Install external tools used in tests
        if: ${{ matrix.custom_ini == false }}
        run: >
          npm install -g --fund false
          csslint
          eslint
          jshint

      # Install dependencies and handle caching in one go.
      # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer
      - name: Install Composer dependencies
        uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1
        with:
          # Bust the cache at least once a month - output format: YYYY-MM.
          custom-cache-suffix: $(date -u "+%Y-%m")

      - name: Grab PHPUnit version
        id: phpunit_version
        shell: bash
        # yamllint disable-line rule:line-length
        run: echo "VERSION=$(php "vendor/bin/phpunit" --version | grep --only-matching --max-count=1 --extended-regexp '\b[0-9]+\.[0-9]+')" >> "$GITHUB_OUTPUT"

      - name: "DEBUG: Show grabbed version"
        run: echo ${{ steps.phpunit_version.outputs.VERSION }}

      - name: 'PHPCS: set the path to PHP'
        run: php "bin/phpcs" --config-set php_path php

      # PHPUnit 9.3 started using PHP-Parser for code coverage, which can cause issues due to Parser
      # also polyfilling PHP tokens.
      # As of PHPUnit 9.3.4, a cache warming option is available.
      # Using that option prevents issues with PHP-Parser backfilling PHP tokens during our test runs.
      - name: "Warm the PHPUnit cache (PHPUnit 9.3+)"
        if: ${{ steps.phpunit_version.outputs.VERSION >= '9.3' }}
        run: php "vendor/bin/phpunit" --coverage-cache ./build/phpunit-cache --warm-coverage-cache

      - name: "Run the unit tests with code coverage"
        if: ${{ matrix.os != 'windows-latest' }}
        run: >
          php "vendor/bin/phpunit" tests/AllTests.php
          ${{ steps.phpunit_version.outputs.VERSION >= '9.3' && '--coverage-cache ./build/phpunit-cache' || '' }}

      - name: "Run select tests in CBF mode with code coverage"
        if: ${{ matrix.os != 'windows-latest' }}
        run: >
          php "vendor/bin/phpunit" tests/AllTests.php
          ${{ steps.phpunit_version.outputs.VERSION >= '9.3' && '--coverage-cache ./build/phpunit-cache' || '' }}
          --group CBF --exclude-group nothing --coverage-clover build/logs/clover-cbf.xml
        env:
          PHP_CODESNIFFER_CBF: '1'

      - name: "Run the unit tests which may have different outcomes on Windows with code coverage"
        if: ${{ matrix.os == 'windows-latest' }}
        run: >
          php "vendor/bin/phpunit" tests/AllTests.php --group Windows
          ${{ steps.phpunit_version.outputs.VERSION >= '9.3' && '--coverage-cache ./build/phpunit-cache' || '' }}

      - name: "Upload coverage results to Coveralls (normal run)"
        if: ${{ success() }}
        uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
        with:
          format: clover
          file: build/logs/clover.xml
          flag-name: os-${{ matrix.os }}-php-${{ matrix.php }}-custom-ini-${{ matrix.custom_ini }}
          parallel: true

      - name: "Upload coverage results to Coveralls (CBF run)"
        if: ${{ matrix.os != 'windows-latest' && success() }}
        uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
        with:
          format: clover
          file: build/logs/clover-cbf.xml
          flag-name: cbf-os-${{ matrix.os }}-ubuntu-latest-php-${{ matrix.php }}-custom-ini-${{ matrix.custom_ini }}
          parallel: true

  coveralls-finish:
    needs: coverage
    if: always() && needs.coverage.result == 'success'

    runs-on: ubuntu-latest
    name: Coveralls Finish

    steps:
      - name: Coveralls Finished
        uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
        with:
          parallel-finished: true
