name: GnuTests

# spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem
# spell-checker:ignore (jargon) submodules devel
# spell-checker:ignore (libs/utils) autopoint chksum getenforce gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e
# spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic
# spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay
# spell-checker:ignore (vars) FILESET SUBDIRS XPASS

# * note: to run a single test => `REPO/util/run-gnu-test.sh PATH/TO/TEST/SCRIPT`

on:
  pull_request:
  push:
    branches:
      - '*'

permissions:
  contents: read

# End the current execution if there is a new changeset in the PR.
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

env:
  DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
  TEST_FULL_SUMMARY_FILE: 'gnu-full-result.json'
  TEST_ROOT_FULL_SUMMARY_FILE: 'gnu-root-full-result.json'
  TEST_SELINUX_FULL_SUMMARY_FILE: 'selinux-gnu-full-result.json'
  TEST_SELINUX_ROOT_FULL_SUMMARY_FILE: 'selinux-root-gnu-full-result.json'
  REPO_GNU_REF: "v9.7"

jobs:
  native:
    name: Run GNU tests (native)
    runs-on: ubuntu-24.04
    steps:
    #### Get the code, setup cache
    - name: Checkout code (uutils)
      uses: actions/checkout@v5
      with:
        path: 'uutils'
        persist-credentials: false
    - uses: dtolnay/rust-toolchain@master
      with:
        toolchain: stable
        components: rustfmt
    - uses: Swatinem/rust-cache@v2
      with:
        workspaces: "./uutils -> target"
    - name: Checkout code (GNU coreutils)
      uses: actions/checkout@v5
      with:
        repository: 'coreutils/coreutils'
        path: 'gnu'
        ref: ${{ env.REPO_GNU_REF }}
        submodules: false
        persist-credentials: false
    - name: Override submodule URL and initialize submodules
      # Use github instead of upstream git server
      run: |
        git submodule sync --recursive
        git config submodule.gnulib.url https://github.com/coreutils/gnulib.git
        git submodule update --init --recursive --depth 1
      working-directory: gnu

    #### Build environment setup
    - name: Install dependencies
      shell: bash
      run: |
        ## Install dependencies
        sudo apt-get update
        sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr quilt
    - name: Add various locales
      shell: bash
      run: |
        ## Add various locales
        echo "Before:"
        locale -a
        ## Some tests fail with 'cannot change locale (en_US.ISO-8859-1): No such file or directory'
        ## Some others need a French locale
        sudo locale-gen
        sudo locale-gen --keep-existing fr_FR
        sudo locale-gen --keep-existing fr_FR.UTF-8
        sudo locale-gen --keep-existing es_ES.UTF-8
        sudo locale-gen --keep-existing sv_SE
        sudo locale-gen --keep-existing sv_SE.UTF-8
        sudo locale-gen --keep-existing en_US
        sudo locale-gen --keep-existing en_US.UTF-8
        sudo locale-gen --keep-existing ru_RU.KOI8-R

        sudo update-locale
        echo "After:"
        locale -a

    ### Build
    - name: Build binaries
      shell: bash
      run: |
        ## Build binaries
        cd 'uutils'
        bash util/build-gnu.sh --release-build

    ### Run tests as user
    - name: Run GNU tests
      shell: bash
      run: |
        ## Run GNU tests
        path_GNU='gnu'
        path_UUTILS='uutils'
        bash "uutils/util/run-gnu-test.sh"
    - name: Extract testing info from individual logs into JSON
      shell: bash
      run : |
        path_UUTILS='uutils'
        python uutils/util/gnu-json-result.py gnu/tests > ${{ env.TEST_FULL_SUMMARY_FILE }}

    ### Run tests as root
    - name: Run GNU root tests
      shell: bash
      run: |
        ## Run GNU root tests
        path_GNU='gnu'
        path_UUTILS='uutils'
        bash "uutils/util/run-gnu-test.sh" run-root
    - name: Extract testing info from individual logs (run as root) into JSON
      shell: bash
      run : |
        path_UUTILS='uutils'
        python uutils/util/gnu-json-result.py gnu/tests > ${{ env.TEST_ROOT_FULL_SUMMARY_FILE }}

    ### Upload artifacts
    - name: Upload full json results
      uses: actions/upload-artifact@v4
      with:
        name: gnu-full-result
        path: ${{ env.TEST_FULL_SUMMARY_FILE }}
    - name: Upload root json results
      uses: actions/upload-artifact@v4
      with:
        name: gnu-root-full-result
        path: ${{ env.TEST_ROOT_FULL_SUMMARY_FILE }}
    - name: Compress test logs
      shell: bash
      run : |
        # Compress logs before upload (fails otherwise)
        gzip gnu/tests/*/*.log
    - name: Upload test logs
      uses: actions/upload-artifact@v4
      with:
        name: test-logs
        path: |
          gnu/tests/*.log
          gnu/tests/*/*.log.gz

  selinux:
    name: Run GNU tests (SELinux)
    runs-on: ubuntu-24.04
    steps:
    #### Get the code, setup cache
    - name: Checkout code (uutils)
      uses: actions/checkout@v5
      with:
        path: 'uutils'
        persist-credentials: false
    - uses: dtolnay/rust-toolchain@master
      with:
        toolchain: stable
        components: rustfmt
    - uses: Swatinem/rust-cache@v2
      with:
        workspaces: "./uutils -> target"
    - name: Checkout code (GNU coreutils)
      uses: actions/checkout@v5
      with:
        repository: 'coreutils/coreutils'
        path: 'gnu'
        ref: ${{ env.REPO_GNU_REF }}
        submodules: false
        persist-credentials: false
    - name: Override submodule URL and initialize submodules
      # Use github instead of upstream git server
      run: |
        git submodule sync --recursive
        git config submodule.gnulib.url https://github.com/coreutils/gnulib.git
        git submodule update --init --recursive --depth 1
      working-directory: gnu

    #### Lima build environment setup
    - name: Setup Lima
      uses: lima-vm/lima-actions/setup@v1
      id: lima-actions-setup
    - name: Cache ~/.cache/lima
      uses: actions/cache@v4
      with:
        path: ~/.cache/lima
        key: lima-${{ steps.lima-actions-setup.outputs.version }}
    - name: Start Fedora VM with SELinux
      run: limactl start --plain --name=default --cpus=4 --disk=40 --memory=8 --network=lima:user-v2 template://fedora
    - name: Setup SSH
      uses: lima-vm/lima-actions/ssh@v1
    - name: Verify SELinux Status and Configuration
      run: |
        lima getenforce
        lima ls -laZ /etc/selinux
        lima sudo sestatus

        # Ensure we're running in enforcing mode
        lima sudo setenforce 1
        lima getenforce

        # Create test files with SELinux contexts for testing
        lima sudo mkdir -p /var/test_selinux
        lima sudo touch /var/test_selinux/test_file
        lima sudo chcon -t etc_t /var/test_selinux/test_file
        lima ls -Z /var/test_selinux/test_file  # Verify context
    - name: Install dependencies in VM
      run: |
        lima sudo dnf -y update
        lima sudo dnf -y install git autoconf autopoint bison texinfo gperf gcc g++ gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel texinfo-tex wget automake patch quilt
        lima rustup-init -y --default-toolchain stable
    - name: Copy the sources to VM
      run: |
        rsync -a -e ssh . lima-default:~/work/

    ### Build
    - name: Build binaries
      run: |
        lima bash -c "cd ~/work/uutils/ && bash util/build-gnu.sh --release-build"

    ### Run tests as user
    - name: Generate SELinux tests list
      run: |
        # Find and list all tests that require SELinux
        lima bash -c "cd ~/work/gnu/ && grep -l 'require_selinux_' -r tests/ > ~/work/uutils/selinux-tests.txt"
        lima bash -c "cd ~/work/uutils/ && cat selinux-tests.txt"

        # Count the tests
        lima bash -c "cd ~/work/uutils/ && echo 'Found SELinux tests:'; wc -l selinux-tests.txt"
    - name: Run GNU SELinux tests
      run: |
        lima sudo setenforce 1
        lima getenforce
        lima cat /proc/filesystems
        lima bash -c "cd ~/work/uutils/ && bash util/run-gnu-test.sh \$(cat selinux-tests.txt)"
    - name: Extract testing info from individual logs into JSON
      shell: bash
      run : |
        lima bash -c "cd ~/work/gnu/ && python3 ../uutils/util/gnu-json-result.py tests > ~/work/${{ env.TEST_SELINUX_FULL_SUMMARY_FILE }}"

    ### Run tests as root
    - name: Run GNU SELinux root tests
      run: |
        lima bash -c "cd ~/work/uutils/ && CI=1 bash util/run-gnu-test.sh run-root \$(cat selinux-tests.txt)"
    - name: Extract testing info from individual logs (run as root) into JSON
      shell: bash
      run : |
        lima bash -c "cd ~/work/gnu/ && python3 ../uutils/util/gnu-json-result.py tests > ~/work/${{ env.TEST_SELINUX_ROOT_FULL_SUMMARY_FILE }}"

    ### Upload artifacts
    - name: Collect test logs and test results from VM
      run: |
        mkdir -p gnu/tests-selinux

        # Copy the json output back from the Lima VM to the host
        rsync -v -a -e ssh lima-default:~/work/*.json ./
        # Copy the test directory now
        rsync -v -a -e ssh lima-default:~/work/gnu/tests/ ./gnu/tests-selinux/
    - name: Upload SELinux json results
      uses: actions/upload-artifact@v4
      with:
        name: selinux-gnu-full-result
        path: ${{ env.TEST_SELINUX_FULL_SUMMARY_FILE }}
    - name: Upload SELinux root json results
      uses: actions/upload-artifact@v4
      with:
        name: selinux-root-gnu-full-result
        path: ${{ env.TEST_SELINUX_ROOT_FULL_SUMMARY_FILE }}
    - name: Compress SELinux test logs
      shell: bash
      run : |
        # Compress logs before upload (fails otherwise)
        gzip gnu/tests-selinux/*/*.log
    - name: Upload SELinux test logs
      uses: actions/upload-artifact@v4
      with:
        name: selinux-test-logs
        path: |
          gnu/tests-selinux/*.log
          gnu/tests-selinux/*/*.log.gz

  aggregate:
    needs: [native, selinux]
    permissions:
      actions: read  # for dawidd6/action-download-artifact to query and download artifacts
      contents: read  # for actions/checkout to fetch code
      pull-requests: read  # for dawidd6/action-download-artifact to query commit hash
    name: Aggregate GNU test results
    runs-on: ubuntu-24.04
    steps:
    - name: Initialize workflow variables
      id: vars
      shell: bash
      run: |
        ## VARs setup
        outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
        #
        TEST_SUMMARY_FILE='gnu-result.json'
        AGGREGATED_SUMMARY_FILE='aggregated-result.json'

        outputs TEST_SUMMARY_FILE AGGREGATED_SUMMARY_FILE
    - name: Checkout code (uutils)
      uses: actions/checkout@v5
      with:
        path: 'uutils'
        persist-credentials: false
    - name: Retrieve reference artifacts
      uses: dawidd6/action-download-artifact@v11
      # ref: <https://github.com/dawidd6/action-download-artifact>
      continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet)
      with:
        workflow: GnuTests.yml
        branch: "${{ env.DEFAULT_BRANCH }}"
        # workflow_conclusion: success ## (default); * but, if commit with failed GnuTests is merged into the default branch, future commits will all show regression errors in GnuTests CI until o/w fixed
        workflow_conclusion: completed ## continually recalibrates to last commit of default branch with a successful GnuTests (ie, "self-heals" from GnuTest regressions, but needs more supervision for/of regressions)
        path: "reference"
    - name: Download full json results
      uses: actions/download-artifact@v5
      with:
        name: gnu-full-result
        path: results
        merge-multiple: true
    - name: Download root json results
      uses: actions/download-artifact@v5
      with:
        name: gnu-root-full-result
        path: results
        merge-multiple: true
    - name: Download selinux json results
      uses: actions/download-artifact@v5
      with:
        name: selinux-gnu-full-result
        path: results
        merge-multiple: true
    - name: Download selinux root json results
      uses: actions/download-artifact@v5
      with:
        name: selinux-root-gnu-full-result
        path: results
        merge-multiple: true
    - name: Extract/summarize testing info
      id: summary
      shell: bash
      run: |
        ## Extract/summarize testing info
        outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }

        path_UUTILS='uutils'

        json_count=$(ls -l results/*.json | wc -l)
        if [[ "$json_count" -ne 4 ]]; then
          echo "::error ::Failed to download all results json files (expected 4 files, found $json_count); failing early"
          ls -lR results || true
          exit 1
        fi

        # Look at all individual results and summarize
        eval $(python3 uutils/util/analyze-gnu-results.py -o=${{ steps.vars.outputs.AGGREGATED_SUMMARY_FILE }} results/*.json)

        if [[ "$TOTAL" -eq 0 || "$TOTAL" -eq 1 ]]; then
          echo "::error ::Failed to parse test results from '${{ env.TEST_FULL_SUMMARY_FILE }}'; failing early"
          exit 1
        fi

        output="GNU tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR / SKIP: $SKIP"
        echo "${output}"

        if [[ "$FAIL" -gt 0 || "$ERROR" -gt 0 ]]; then
          echo "::warning ::${output}"
        fi

        jq -n \
              --arg date "$(date --rfc-email)" \
              --arg sha "$GITHUB_SHA" \
              --arg total "$TOTAL" \
              --arg pass "$PASS" \
              --arg skip "$SKIP" \
              --arg fail "$FAIL" \
              --arg xpass "$XPASS" \
              --arg error "$ERROR" \
              '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}'
        HASH=$(sha1sum '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}' | cut --delim=" " -f 1)
        outputs HASH
    - name: Upload SHA1/ID of 'test-summary'
      uses: actions/upload-artifact@v4
      with:
        name: "${{ steps.summary.outputs.HASH }}"
        path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}"
    - name: Upload test results summary
      uses: actions/upload-artifact@v4
      with:
        name: test-summary
        path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}"
    - name: Upload aggregated json results
      uses: actions/upload-artifact@v4
      with:
        name: aggregated-result
        path: ${{ steps.vars.outputs.AGGREGATED_SUMMARY_FILE }}
    - name: Compare test failures VS reference
      shell: bash
      run: |
        ## Compare test failures VS reference using JSON files
        REF_SUMMARY_FILE='reference/aggregated-result/aggregated-result.json'
        CURRENT_SUMMARY_FILE='${{ steps.vars.outputs.AGGREGATED_SUMMARY_FILE }}'
        REPO_DEFAULT_BRANCH='${{ env.DEFAULT_BRANCH }}'
        path_UUTILS='uutils'

        # Path to ignore file for intermittent issues
        IGNORE_INTERMITTENT="uutils/.github/workflows/ignore-intermittent.txt"

        # Set up comment directory
        COMMENT_DIR="reference/comment"
        mkdir -p ${COMMENT_DIR}
        echo ${{ github.event.number }} > ${COMMENT_DIR}/NR
        COMMENT_LOG="${COMMENT_DIR}/result.txt"

        COMPARISON_RESULT=0
        if test -f "${CURRENT_SUMMARY_FILE}"; then
          if test -f "${REF_SUMMARY_FILE}"; then
            echo "Reference summary SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")"
            echo "Current summary SHA1/ID: $(sha1sum -- "${CURRENT_SUMMARY_FILE}")"

            python3 uutils/util/compare_test_results.py \
              --ignore-file "${IGNORE_INTERMITTENT}" \
              --output "${COMMENT_LOG}" \
              "${CURRENT_SUMMARY_FILE}" "${REF_SUMMARY_FILE}"

            COMPARISON_RESULT=$?
          else
            echo "::warning ::Skipping test comparison; no prior reference summary is available at '${REF_SUMMARY_FILE}'."
          fi
        else
          echo "::error ::Failed to find summary of test results (missing '${CURRENT_SUMMARY_FILE}'); failing early"
          exit 1
        fi

        if [ ${COMPARISON_RESULT} -eq 1 ]; then
          echo "ONLY_INTERMITTENT=false" >> $GITHUB_ENV
          echo "::error ::Found new non-intermittent test failures"
          exit 1
        else
          echo "ONLY_INTERMITTENT=true" >> $GITHUB_ENV
          echo "::notice ::No new test failures detected"
        fi
    - name: Upload comparison log (for GnuComment workflow)
      if: success() || failure() # run regardless of prior step success/failure
      uses: actions/upload-artifact@v4
      with:
        name: comment
        path: reference/comment/
    - name: Compare test summary VS reference
      if: success() || failure() # run regardless of prior step success/failure
      shell: bash
      run: |
        ## Compare test summary VS reference
        REF_SUMMARY_FILE='reference/test-summary/gnu-result.json'
        if test -f "${REF_SUMMARY_FILE}"; then
          echo "Reference SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")"
          mv "${REF_SUMMARY_FILE}" main-gnu-result.json
          python uutils/util/compare_gnu_result.py
        else
          echo "::warning ::Skipping test summary comparison; no prior reference summary is available."
        fi
