diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..b998ee120 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @doodspav diff --git a/.github/workflows/_reusable-test-native.yml b/.github/workflows/_reusable-test-native.yml new file mode 100644 index 000000000..1bd95a31d --- /dev/null +++ b/.github/workflows/_reusable-test-native.yml @@ -0,0 +1,251 @@ +name: Run Tests on Native Platforms + +on: + workflow_call: + inputs: + os: + description: 'Used to set the runs-on attribute, must be one of macos, ubuntu, windows' + required: true + type: string + default: '' + preset: + description: 'CMake preset used to build patomic and googletest, will be conditionally suffixed with one of ansi, coverage, sanitize, warning' + required: true + type: string + default: '' + architecture: + description: 'CPU architecture tests are run on, used for naming (does not affect how tests are run)' + required: true + type: string + default: '' + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Check No Empty Inputs + shell: bash + run: | + # checks input is not empty + check_input() { + input_name="${1}" + input_value="${2}" + if [[ -z "${input_value}" ]]; then + echo "Error: required input '${input_name}' is empty or was not specified." + exit 1 + else + echo "${input_name}: ${input_value}" + fi + } + + # apply function to all inputs + check_input "os" "${{ inputs.os }}" + check_input "preset" "${{ inputs.preset }}" + check_input "architecture" "${{ inputs.architecture }}" + + run: + needs: check + runs-on: ${{ inputs.os }}-latest + strategy: + fail-fast: false + matrix: + kind: + - ansi + - coverage + - sanitize + - warning + build_shared: + - static + - shared + include: + # hidden single variable + - compiler: ${{ contains(inputs.preset, 'clang') && 'clang' || ( contains(inputs.preset, 'gcc') && 'gcc' || 'msvc' ) }} + # alias variables + - build_shared: static + cmake_build_shared: OFF + - build_shared: shared + cmake_build_shared: ON + env: + # these have to be in env context because they depend on matrix context + UNIQUE_ID: ${{ matrix.kind }}-${{ inputs.architecture }}-${{ inputs.os }}-${{ matrix.compiler }}-${{ matrix.build_shared }} + UNIQUE_NAME: ${{ inputs.architecture }}-${{ inputs.os }}-${{ matrix.compiler }}-${{ matrix.build_shared }} + CMAKE_PRESET: ${{ inputs.preset }}-${{ matrix.kind }} + CMAKE_BUILD_TYPE: ${{ ( matrix.kind == 'ansi' || matrix.kind == 'warning' ) && 'Release' || 'Debug' }} + # currently not possible to skip job using matrix context, so this is the next best thing + SKIP_JOB: >- + ${{ ( + ( matrix.kind == 'ansi' && matrix.compiler == 'msvc' ) || + ( matrix.kind == 'coverage' && inputs.os == 'windows' ) || + ( matrix.kind == 'sanitize' && inputs.os == 'windows' ) + ) }} + # this job supports multiple os, each with their own root path + # this screws up lcov when it tries to merge the coverage path mappings + # we can't just modify mappings because: + # - GitHub issue #65006 on llvm/llvm-project suggests to NOT use -fcoverage-prefix-map + # - macos runner has AppleClang 14 which doesn't support -fcoverage-compilation-dir + # workaround is to have a fixed root path that everyone works from + # use /Users as root dir because /home is not supported on macos + ROOT_PATH: /Users/${{ matrix.kind }} + + steps: + - name: Set Up Root Path + if: env.SKIP_JOB != 'true' + shell: bash + run: | + # macos and ubuntu need sudo, windows does not + prefix="" + if [[ "${{ inputs.os }}" != "windows" ]]; then + prefix="sudo " + fi + + ${prefix}mkdir -p "${{ env.ROOT_PATH }}" + ${prefix}chmod -R a+rwx "${{ env.ROOT_PATH }}" + + - name: Checkout patomic + if: env.SKIP_JOB != 'true' + uses: actions/checkout@v4 + with: + # cannot prefix with ROOT_PATH here, so copy over in next step + path: patomic + + - name: Move patomic to Root Path Location + if: env.SKIP_JOB != 'true' + run: | + cp -R ./patomic/ "${{ env.ROOT_PATH }}/patomic" + + - name: Restore Cached GoogleTest + if: env.SKIP_JOB != 'true' + id: cache-googletest + uses: actions/cache@v3 + with: + path: ${{ env.ROOT_PATH }}/googletest/build/install + key: googletest-${{ env.UNIQUE_ID }} + + - name: Checkout GoogleTest + if: env.SKIP_JOB != 'true' && steps.cache-googletest.outputs.cache-hit != 'true' + uses: actions/checkout@v4 + with: + repository: google/googletest + # cannot prefix with ROOT_PATH here, so copy over in next step + path: googletest + + - name: Move GoogleTest to Root Path Location + if: env.SKIP_JOB != 'true' && steps.cache-googletest.outputs.cache-hit != 'true' + run: | + cp -R ./googletest/ "${{ env.ROOT_PATH }}/googletest" + + - name: Build and Install GoogleTest + if: env.SKIP_JOB != 'true' && steps.cache-googletest.outputs.cache-hit != 'true' + run: | + cd ${{ env.ROOT_PATH }} + + cd googletest + cp ../patomic/CMakePresets.json . + mkdir build + cd build + cmake --preset ${{ env.CMAKE_PRESET }} -DBUILD_TESTING=OFF -DCMAKE_CXX_FLAGS="" -DBUILD_SHARED_LIBS=${{ matrix.cmake_build_shared }} -DCMAKE_BUILD_TYPE=${{ env.CMAKE_BUILD_TYPE }} -Dgtest_force_shared_crt=ON -Dgtest_hide_internal_symbols=ON .. + cmake --build . --verbose --config ${{ env.CMAKE_BUILD_TYPE }} + cmake --install . --config ${{ env.CMAKE_BUILD_TYPE }} --prefix install + + # TODO: figure out how to cache this for builds where we don't change any source code (GHI #31) + - name: Build patomic + if: env.SKIP_JOB != 'true' + run: | + cd ${{ env.ROOT_PATH }} + + cd patomic + mkdir build + cd build + cmake --preset ${{ env.CMAKE_PRESET }} -DBUILD_SHARED_LIBS=${{ matrix.cmake_build_shared }} -DCMAKE_BUILD_TYPE=${{ env.CMAKE_BUILD_TYPE }} -DGTest_ROOT:PATH="../../googletest/build/install" .. + cmake --build . --verbose --config ${{ env.CMAKE_BUILD_TYPE }} + + - name: Test patomic + if: env.SKIP_JOB != 'true' + continue-on-error: true + env: + LABEL_REGEX: ${{ matrix.kind == 'coverage' && '^(ut)$' || '^(.*)$' }} + run: | + cd ${{ env.ROOT_PATH }} + + cd patomic/build + ctest --label-regex "${{ env.LABEL_REGEX }}" --verbose --output-junit Testing/Temporary/results.xml --build-config ${{ env.CMAKE_BUILD_TYPE }} . + + - name: Prepare Test Results + if: env.SKIP_JOB != 'true' + run: | + cd ${{ env.ROOT_PATH }} + + mkdir -p upload/test/${{ matrix.kind }} + python3 patomic/test/improve_ctest_xml.py --input patomic/build/Testing/Temporary/results.xml --triple ${{ matrix.kind }}-${{ env.UNIQUE_NAME }} --output upload/test/${{ matrix.kind }}/${{ env.UNIQUE_NAME }}.xml + + - name: Upload Test Results + if: env.SKIP_JOB != 'true' + uses: actions/upload-artifact@v3 + with: + name: test-results + path: ${{ env.ROOT_PATH }}/upload/test/ + + - name: Generate Lcov Tracefile and Root Path File (clang) + if: env.SKIP_JOB != 'true' && matrix.kind == 'coverage' && matrix.compiler == 'clang' + shell: bash + run: | + cd ${{ env.ROOT_PATH }} + + # set up directory + mkdir -p upload/cov/${{ env.UNIQUE_NAME }} + cd patomic/build + + # macos needs xcrun to help use xcode to run llvm tools + # ubuntu needs llvm tools to be installed + prefix="" + if [[ "${{ inputs.os }}" == "macos" ]]; then + prefix="xcrun " + else # [[ "${{ inputs.os }}" == "ubuntu" ]]; then + sudo apt install llvm + fi + + # merge coverage output from all tests + # use find because bash on GitHub Actions currently does not support '**' + find test/working -type f -name "*.profraw" -print0 | xargs -0 ${prefix}llvm-profdata merge -output=patomic.profdata + + # convert profdata to lcov tracefile, and copy to upload + lib_files=(libpatomic.*) # matches shared/static lib files and symlinks + ${prefix}llvm-cov export -format=lcov -instr-profile=patomic.profdata -object="${lib_files[0]}" >> patomic.lcov + cp patomic.lcov ../../upload/cov/${{ env.UNIQUE_NAME }}/patomic.lcov + + # we need original source mapping to make use of lcov tracefile + # we can't just modify mapping because: + # - GitHub issue #65006 on llvm/llvm-project suggests to NOT use -fcoverage-prefix-map + # - macos runner has AppleClang 14 which doesn't support -fcoverage-compilation-dir + # workaround is to store root path to help use coverage directory mappings later + cd ../.. + echo "$PWD" >> upload/cov/${{ env.UNIQUE_NAME }}/patomic.rootpath + + - name: Generate Lcov Tracefile and Root Path File (gcc) + if: env.SKIP_JOB != 'true' && matrix.kind == 'coverage' && matrix.compiler == 'gcc' + shell: bash + run: | + cd ${{ env.ROOT_PATH }} + + # install lcov + sudo apt install lcov + + # set up directory + mkdir -p upload/cov/${{ env.UNIQUE_NAME }} + + # merge all .gcda files into an lcov tracefile, and copy to upload + # all tests have .gcno, but only tests that were executed have .gcda + lcov --directory --rc lcov_branch_coverage=1 patomic/build --capture --output-file patomic.lcov + cp patomic.lcov upload/cov/${{ env.UNIQUE_NAME }}/patomic.lcov + + # we need root path file because next job needs it because clang (above) needs it + # we might also need it for gcc, but at the moment i have no idea + echo "$PWD" >> upload/cov/${{ env.UNIQUE_NAME }}/patomic.rootpath + + - name: Upload Internal Coverage Results + if: env.SKIP_JOB != 'true' && matrix.kind == 'coverage' + uses: actions/upload-artifact@v3 + with: + name: test-coverage-internal + path: ${{ env.ROOT_PATH }}/upload/cov/ diff --git a/.github/workflows/_reusable-test-qemu.yml b/.github/workflows/_reusable-test-qemu.yml new file mode 100644 index 000000000..0b48097fa --- /dev/null +++ b/.github/workflows/_reusable-test-qemu.yml @@ -0,0 +1,276 @@ +name: Run Tests on Emulated Platforms + +on: + workflow_call: + inputs: + triple: + description: 'Platform triple to compile for, used to install correct compiler and sysroot' + required: true + type: string + default: '' + architecture: + description: 'CPU architecture tests are run on, passed to QEMU' + required: true + type: string + default: '' + gcc_version: + description: 'Major version of GCC toolchain to use' + required: false + type: number + default: 10 + llvm_version: + description: 'Major version of LLVM toolchain to use' + required: false + type: number + default: 15 + skip_gcc: + description: "Conditionally skip 'run' job if gcc does not support the triple" + required: false + type: boolean + default: false + skip_llvm: + description: "Conditionally skip 'run' job if llvm does not support the triple" + required: false + type: boolean + default: false + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Check No Empty Inputs + shell: bash + run: | + # checks input is not empty + check_input() { + input_name="${1}" + input_value="${2}" + if [[ -z "${input_value}" ]]; then + echo "Error: required input '${input_name}' is empty or was not specified." + exit 1 + else + echo "${input_name}: ${input_value}" + fi + } + + # apply function to all inputs + check_input "triple" "${{ inputs.triple }}" + check_input "architecture" "${{ inputs.architecture }}" + + run: + needs: check + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + compiler: + - clang + - gcc + kind: + - ansi + - coverage + - sanitize + - warning + build_shared: + - static + - shared + exclude: + # fixme: clang can't find runtime coverage libraries (GHI #28) + - kind: coverage + compiler: clang + # fixme: not sure which archs asan/ubsan support (GHI #29) + - kind: sanitize + include: + # alias variables + - compiler: clang + compiler_version: ${{ inputs.llvm_version }} + preset: patomic-ci-qemu-ubuntu-clang + - compiler: gcc + compiler_version: ${{ inputs.gcc_version }} + preset: patomic-ci-qemu-ubuntu-gcc + - build_shared: static + cmake_build_shared: OFF + - build_shared: shared + cmake_build_shared: ON + env: + # required by toolchain file + PATOMIC_CI_XARCH: ${{ inputs.architecture }} + PATOMIC_CI_XTRIPLE: ${{ inputs.triple }} + PATOMIC_CI_XCOMPILER: ${{ matrix.compiler }} + PATOMIC_CI_XCOMPILER_VERSION: ${{ matrix.compiler_version }} + PATOMIC_CI_SYSROOT: /Users/${{ matrix.kind }}/sysroot + # these have to be in env context because they depend on matrix context + UNIQUE_ID: ${{ matrix.kind }}-${{ inputs.triple }}-ubuntu-${{ matrix.compiler }}-${{ matrix.build_shared }}-${{ inputs.gcc_version }}-${{ inputs.llvm_version }} + UNIQUE_NAME: ${{ inputs.architecture }}-ubuntu-${{ matrix.compiler }}-${{ matrix.build_shared }} + CMAKE_PRESET: ${{ matrix.preset }}-${{ matrix.kind }} + CMAKE_BUILD_TYPE: ${{ ( matrix.kind == 'ansi' || matrix.kind == 'warning' ) && 'Release' || 'Debug' }} + # apparently not possible to skip job using matrix context, so this is the next best thing + SKIP_JOB: >- + ${{ ( + ( matrix.compiler == 'clang' && inputs.skip_llvm ) || + ( matrix.compiler == 'gcc' && inputs.skip_gcc ) + ) }} + # necessary because publish-coverage job expects everything to have the same root path + ROOT_PATH: /Users/${{ matrix.kind }} + + steps: + - name: Install Toolchains and Qemu + if: env.SKIP_JOB != 'true' + run: | + sudo apt update + sudo apt install g++-${{ inputs.gcc_version }}-multilib + sudo apt install g++-${{ inputs.gcc_version }}-${{ inputs.triple }} + if [[ '${{ matrix.compiler }}' == 'clang' ]]; then + sudo apt install llvm-${{ inputs.llvm_version }} + sudo apt install clang-${{ inputs.llvm_version }} + fi + sudo apt install qemu-user + + - name: Set Up Root Path + if: env.SKIP_JOB != 'true' + run: | + sudo mkdir -p "${{ env.ROOT_PATH }}" + sudo chmod -R a+rwx "${{ env.ROOT_PATH }}" + + - name: Checkout patomic + if: env.SKIP_JOB != 'true' + uses: actions/checkout@v4 + with: + # cannot prefix with ROOT_PATH here, so copy over in next step + path: patomic + + - name: Move patomic to Root Path Location + if: env.SKIP_JOB != 'true' + run: | + cp -R ./patomic/ "${{ env.ROOT_PATH }}/patomic" + + - name: Restore Cached Sysroot (with GoogleTest) + if: env.SKIP_JOB != 'true' + id: cache-sysroot + uses: actions/cache@v3 + with: + path: ${{ env.ROOT_PATH }}/sysroot + key: sysroot-${{ env.UNIQUE_ID }} + + - name: Set Up Sysroot + if: env.SKIP_JOB != 'true' && steps.cache-sysroot.outputs.cache-hit != 'true' + run: | + cp -R /usr/${{ inputs.triple }}/ ${{ env.ROOT_PATH }}/sysroot + + - name: Checkout GoogleTest + if: env.SKIP_JOB != 'true' && steps.cache-sysroot.outputs.cache-hit != 'true' + uses: actions/checkout@v4 + with: + repository: google/googletest + # cannot prefix with ROOT_PATH here, so copy over in next step + path: googletest + + - name: Move GoogleTest to Root Path Location + if: env.SKIP_JOB != 'true' && steps.cache-sysroot.outputs.cache-hit != 'true' + run: | + cp -R ./googletest/ "${{ env.ROOT_PATH }}/googletest" + + - name: Build and Install GoogleTest + if: env.SKIP_JOB != 'true' && steps.cache-sysroot.outputs.cache-hit != 'true' + run: | + cd ${{ env.ROOT_PATH }} + + cd googletest + cp -R ../patomic/ci/ ./ + cp ../patomic/CMakePresets.json . + mkdir build + cd build + cmake --preset ${{ env.CMAKE_PRESET }} -DBUILD_TESTING=OFF -DCMAKE_CXX_FLAGS="" -DBUILD_SHARED_LIBS=${{ matrix.cmake_build_shared }} -DCMAKE_BUILD_TYPE=${{ env.CMAKE_BUILD_TYPE }} -Dgtest_force_shared_crt=ON -Dgtest_hide_internal_symbols=ON .. + cmake --build . --verbose + cmake --install . --prefix "${{ env.ROOT_PATH }}/sysroot" + + # TODO: figure out how to cache this for builds where we don't change any source code (GHI #31) + - name: Build patomic + if: env.SKIP_JOB != 'true' + run: | + cd ${{ env.ROOT_PATH }} + + cd patomic + mkdir build + cd build + cmake --preset ${{ env.CMAKE_PRESET }} -DBUILD_SHARED_LIBS=${{ matrix.cmake_build_shared }} -DCMAKE_BUILD_TYPE=${{ env.CMAKE_BUILD_TYPE }} -DGTest_ROOT:PATH="${{ env.ROOT_PATH }}/sysroot" .. + cmake --build . --verbose + + - name: Test patomic + if: env.SKIP_JOB != 'true' + continue-on-error: true + env: + LABEL_REGEX: ${{ matrix.kind == 'coverage' && '^(ut)$' || '^(.*)$' }} + run: | + cd ${{ env.ROOT_PATH }} + + cd patomic/build + ctest --label-regex "${{ env.LABEL_REGEX }}" --verbose --output-junit Testing/Temporary/results.xml . + + - name: Prepare Test Results + if: env.SKIP_JOB != 'true' + run: | + cd ${{ env.ROOT_PATH }} + + mkdir -p upload/test/${{ matrix.kind }} + python3 patomic/test/improve_ctest_xml.py --input patomic/build/Testing/Temporary/results.xml --triple ${{ matrix.kind }}-${{ env.UNIQUE_NAME }} --output upload/test/${{ matrix.kind }}/${{ env.UNIQUE_NAME }}.xml + + - name: Upload Test Results + if: env.SKIP_JOB != 'true' + uses: actions/upload-artifact@v3 + with: + name: test-results + path: ${{ env.ROOT_PATH }}/upload/test/ + + - name: Generate Lcov Tracefile and Root Path File (clang) + if: env.SKIP_JOB != 'true' && matrix.kind == 'coverage' && matrix.compiler == 'clang' + run: | + cd ${{ env.ROOT_PATH }} + + # set up directory + mkdir -p upload/cov/${{ env.UNIQUE_NAME }} + cd patomic/build + + # need llvm tools to be installed + sudo apt install llvm + + # merge coverage output from all tests + # use find because bash on GitHub Actions currently does not support '**' + find test/working -type f -name "*.profraw" -print0 | xargs -0 llvm-profdata merge -output=patomic.profdata + + # convert profdata to lcov tracefile, and copy to upload + lib_files=(libpatomic.*) # matches shared/static lib files and symlinks + llvm-cov export -format=lcov -instr-profile=patomic.profdata -object="${lib_files[0]}" >> patomic.lcov + cp patomic.lcov ../../upload/cov/${{ env.UNIQUE_NAME }}/patomic.lcov + + # necessary because publish-coverage expects this file due to workaround for native test coverage + cd ../.. + echo "$PWD" >> upload/cov/${{ env.UNIQUE_NAME }}/patomic.rootpath + + - name: Generate Lcov Tracefile and Root Path File (gcc) + if: env.SKIP_JOB != 'true' && matrix.kind == 'coverage' && matrix.compiler == 'gcc' + run: | + cd ${{ env.ROOT_PATH }} + + # install lcov + sudo apt install lcov + + # set up directory + mkdir -p upload/cov/${{ env.UNIQUE_NAME }} + + # merge all .gcda files into an lcov tracefile, and copy to upload + # all tests have .gcno, but only tests that were executed have .gcda + lcov --directory --rc lcov_branch_coverage=1 patomic/build --capture --output-file patomic.lcov + cp patomic.lcov upload/cov/${{ env.UNIQUE_NAME }}/patomic.lcov + + # we need root path file because next job needs it because clang (above) needs it + # we might also need it for gcc, but at the moment i have no idea + echo "$PWD" >> upload/cov/${{ env.UNIQUE_NAME }}/patomic.rootpath + + - name: Upload Internal Coverage Results + if: env.SKIP_JOB != 'true' && matrix.kind == 'coverage' + uses: actions/upload-artifact@v3 + with: + name: test-coverage-internal + path: ${{ env.ROOT_PATH }}/upload/cov/ \ No newline at end of file diff --git a/.github/workflows/check-todo-fixme.yml b/.github/workflows/check-todo-fixme.yml new file mode 100644 index 000000000..efa004361 --- /dev/null +++ b/.github/workflows/check-todo-fixme.yml @@ -0,0 +1,153 @@ +name: Check todo and fixme + +on: + pull_request: + branches: + - '**' + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout patomic + uses: actions/checkout@v4 + with: + path: patomic + + - name: Get Open Issues + id: get-open-issues + uses: actions/github-script@v7 + with: + script: | + // get repository details + const [owner, repo] = "${{ github.repository }}".split("/"); + + // make graphql query + const graphql = `query listOpenIssues($repository: String!, $owner: String!, $cursor: String) { + repository(name: $repository, owner: $owner) { + issues(first: 100, states: OPEN, after: $cursor) { + nodes { + number + } + pageInfo { + endCursor + hasNextPage + } + } + } + }`; + + // get information from graphql api + let cursor; + let openIssueNumbers = []; + do { + const response = await github.graphql(graphql, { repository: repo, owner: owner, cursor }); + response.repository.issues.nodes.forEach(({ number }) => openIssueNumbers.push(number)); + cursor = response.repository.issues.pageInfo.endCursor; + } while (cursor); + + // save data as json by returning + console.log(openIssueNumbers); + return openIssueNumbers; + + - name: Scan Files for Fixme and Todo + id: scan-files + env: + BASE_PATH: './patomic' + uses: actions/github-script@v7 + with: + script: | + // dependencies + const fs = require('fs'); + const path = require('path'); + + // get issue id if line contains todo or fixme + // return: null | { issueId: null } | { issueId: 'number' } + function parseLine(line) { + const regex = /(fixme|todo)/i; + const issueIdRegex = /ghi\s#(\d+)/i; + if (regex.test(line)) { + const match = line.match(issueIdRegex); + const issueId = match ? match[1] : null; + return { issueId }; + } else { + return null; + } + } + + // list all files in directory (except excluded ones) + // return: [ path1, path2, ... ] + const excludedDirs = ['.git']; + const excludedFiles = ['check-todo-fixme.yml']; + function listFiles(dir) { + let result = []; + fs.readdirSync(dir).forEach(file => { + // base set up + let fullPath = path.join(dir, file); + let stat = fs.statSync(fullPath); + // path is a directory + if (stat && stat.isDirectory()) { + if (!excludedDirs.includes(file)) { + result = result.concat(listFiles(fullPath)); + } + // path is a file + } else if (!excludedFiles.includes(file)) { + result.push(fullPath) + } + }); + return result; + } + + // parse all lines in all files + // return: [ { filePath: , lineNum: , issueId: }, ... ] + function parseAll(dir) { + const files = listFiles(dir); + const items = [] + files.forEach(file => { + const content = fs.readFileSync(file, 'utf-8'); + const lines = content.split('\n'); + lines.forEach((line, index) => { + const item = parseLine(line); + if (item) { + item.filePath = file; + item.lineNum = index + 1; + items.push(item); + } + }); + }); + return items; + } + + // save data as json by returning + const ret = parseAll('${{ env.BASE_PATH }}'); + console.log(ret); + return ret; + + - name: Check Issues + uses: actions/github-script@v7 + with: + script: | + // get outputs + const openIssueIds = ${{ steps.get-open-issues.outputs.result }}; + const parsedItems = ${{ steps.scan-files.outputs.result }}; + + // go through all parsed items + let errorCount = 0; + parsedItems.forEach(item => { + const filePath = item.filePath.replace('patomic/', ''); + if (item.issueId === null) { + ++errorCount; + const msg = "No GitHub issue mentioned in todo/fixme comment (must match pattern '/ghi\\s#(\\d+)/i')."; + console.error(`::error file=${filePath},line=${item.lineNum}::${msg}`); + } else if (!openIssueIds.includes(parseInt(item.issueId))) { + ++errorCount; + const msg = `GitHub issue #${item.issueId} does not correspond to an open issue number for GitHub repository ${{ github.repository }} (maybe it was resolved and closed).` + console.error(`::error file=${filePath},line=${item.lineNum}::${msg}`); + } + }); + + // fail if error was printed + if (errorCount > 0) { + process.exit(1); + } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..104b86925 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,371 @@ +name: Run All Tests + +on: + pull_request: + branches: + - '**' + schedule: + - cron: "0 0 * * *" + +jobs: + test-native: + strategy: + matrix: + # verbose labels make things easier to read in GitHub Actions + # platform gets converted to os, preset, compiler, architecture + platform: + - windows-gcc-x86_64 + - windows-msvc-x86_32 + - windows-msvc-x86_64 + - macos-clang-x86_64 + - linux-clang-x86_64 + - linux-gcc-x86_64 + # convert verbose labels to usable values (which don't appear in GitHub Actions GUI) + include: + # platform -> os, preset, compiler, architecture + - platform: windows-gcc-x86_64 + os: windows + preset: patomic-ci-native-unix-gcc + architecture: x86_64 + - platform: windows-msvc-x86_32 + os: windows + preset: patomic-ci-native-win32-msvc + architecture: x86_32 + - platform: windows-msvc-x86_64 + os: windows + preset: patomic-ci-native-win64-msvc + architecture: x86_64 + - platform: macos-clang-x86_64 + os: macos + preset: patomic-ci-native-unix-clang + architecture: x86_64 + - platform: linux-clang-x86_64 + os: ubuntu + preset: patomic-ci-native-unix-clang + architecture: x86_64 + - platform: linux-gcc-x86_64 + os: ubuntu + preset: patomic-ci-native-unix-gcc + architecture: x86_64 + + uses: ./.github/workflows/_reusable-test-native.yml + with: + os: ${{ matrix.os }} + preset: ${{ matrix.preset }} + architecture: ${{ matrix.architecture }} + + test-qemu: + strategy: + matrix: + # architecture gets converted to triple + # short form here so that it doesn't take up the whole GitHub Action name + architecture: + - aarch64 + - alpha + - arm + - armhf + - hppa + - m68k + - mips + - mips64 + - mips64el + - mipsel + - ppc + - ppc64 + - ppc64le + - riscv64 + - s390x + - sh4 + - sparc64 + - x86_32 + # convert architectures to triples + include: + - architecture: aarch64 + triple: aarch64-linux-gnu + - architecture: alpha + triple: alpha-linux-gnu + skip_llvm: true # unsupported + - architecture: arm + triple: arm-linux-gnueabi + - architecture: armhf + triple: arm-linux-gnueabihf + - architecture: hppa + triple: hppa-linux-gnu + skip_llvm: true # unsupported + - architecture: m68k + triple: m68k-linux-gnu + skip_gcc: true # fixme: segfaults qemu (GHI #25) + skip_llvm: true # fixme: ICEs clang (GHI #25) + - architecture: mips + triple: mips-linux-gnu + - architecture: mips64 + triple: mips64-linux-gnuabi64 + - architecture: mips64el + triple: mips64el-linux-gnuabi64 + - architecture: mipsel + triple: mipsel-linux-gnu + - architecture: ppc + triple: powerpc-linux-gnu + - architecture: ppc64 + triple: powerpc64-linux-gnu + - architecture: ppc64le + triple: powerpc64le-linux-gnu + - architecture: riscv64 + triple: riscv64-linux-gnu + skip_gcc: true # fixme: errors on system header (GHI #25) + - architecture: s390x + triple: s390x-linux-gnu + - architecture: sh4 + triple: sh4-linux-gnu + skip_gcc: true # fixme: segfaults qemu (GHI #25) + skip_llvm: true # unsupported + - architecture: sparc64 + triple: sparc64-linux-gnu + skip_gcc: true # fixme: segfaults qemu (GHI #25) + - architecture: x86_32 + triple: i686-linux-gnu + skip_llvm: true # fixme: supported but not sure how (GHI #25) + uses: ./.github/workflows/_reusable-test-qemu.yml + with: + triple: ${{ matrix.triple }} + architecture: ${{ matrix.architecture }} + skip_gcc: ${{ matrix.skip_gcc == true }} + skip_llvm: ${{ matrix.skip_llvm == true }} + + publish-results: + runs-on: ubuntu-latest + needs: + - test-native + - test-qemu + + steps: + - name: Download Test Result Artifacts + uses: actions/download-artifact@v3 + with: + name: test-results + path: test-results/ + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + action_fail: true + action_fail_on_inconclusive: true + check_name: "Test Results" + files: test-results/**/*.xml + + publish-coverage: + runs-on: ubuntu-latest + needs: + - test-native + - test-qemu + env: + HI_LIMIT: 100 + MED_LIMIT: 90 + + steps: + - name: Install Dependencies + run: | + sudo apt update + sudo apt install binutils # for c++filt + sudo apt install lcov + + - name: Download Test Coverage Artifacts + uses: actions/download-artifact@v3 + with: + name: test-coverage-internal + path: test-coverage-internal/ + + - name: Checkout patomic + uses: actions/checkout@v4 + with: + path: patomic + + - name: Copy patomic To All Original Mapping Paths + run: | + # go through all patomic.rootpath files + find ./test-coverage-internal -type f -name '*patomic.rootpath' -print0 | while IFS= read -r -d '' rp_file; do + root_path=$(cat "${rp_file}") + + # create root_path if it doesn't exist + if [ ! -d "${root_path}" ]; then + sudo mkdir -p "${root_path}" + fi + + # ensure full permissions even if it exists + sudo chmod -R a+rwx "${root_path}" + + # copy patomic to root_path if not already there + if [ ! -d "${root_path}/patomic" ]; then + cp -R ./patomic/ "${root_path}/patomic" + fi + done + + - name: Create Lcov Config File + run: | + touch ./lcovrc + echo "genhtml_hi_limit = ${{ env.HI_LIMIT }}" >> ./lcovrc + echo "genhtml_med_limit = ${{ env.MED_LIMIT }}" >> ./lcovrc + echo "genhtml_function_hi_limit = ${{ env.HI_LIMIT }}" >> ./lcovrc + echo "genhtml_function_med_limit = ${{ env.MED_LIMIT }}" >> ./lcovrc + echo "lcov_branch_coverage = 1" >> ./lcovrc + + echo "Contents of lcovrc:" + cat ./lcovrc + sudo cp ./lcovrc /etc/lcovrc + + - name: Generate HTML Files + run: | + # directory into which to put user artifacts + mkdir test-coverage + + # keep only coverage data relating to patomic source files + find ./test-coverage-internal -mindepth 1 -maxdepth 1 -type d -not -path '*/.*' -print0 | while IFS= read -r -d '' dir; do + mv "${dir}/patomic.lcov" "${dir}/patomic.lcov.old" + root_path=$(cat "${dir}/patomic.rootpath") + lcov --output-file "${dir}/patomic.lcov" --extract "${dir}/patomic.lcov.old" "${root_path}/patomic/src/*" + done + + # generate html files for each separate compilation + find ./test-coverage-internal -mindepth 1 -maxdepth 1 -type d -not -path '*/.*' -print0 | while IFS= read -r -d '' dir; do + arch=$(basename "${dir}") + mkdir "./test-coverage/${arch}" + genhtml --output-directory "./test-coverage/${arch}" --title "patomic-${arch}" --show-details --num-spaces 4 --legend --demangle-cpp --precision 2 "./test-coverage-internal/${arch}/patomic.lcov" + done + + # generate html files for combined coverage + mkdir ./test-coverage-internal/universal + mkdir ./test-coverage/universal + lcov_combine_command=("lcov") + while IFS= read -r -d '' lcov_file; do + lcov_combine_command+=("-a" "${lcov_file}") + done < <(find ./test-coverage-internal -type f -name '*patomic.lcov' -print0) + lcov_combine_command+=("-o" "./test-coverage-internal/universal/patomic.lcov") + "${lcov_combine_command[@]}" + genhtml --output-directory "./test-coverage/universal" --title "patomic-universal" --show-details --num-spaces 4 --legend --demangle-cpp --precision 2 ./test-coverage-internal/universal/patomic.lcov + + - name: Upload Coverage HTML Results + uses: actions/upload-artifact@v3 + with: + name: test-coverage + path: test-coverage/ + + - name: Parse Lcov Files Into Json + shell: python + run: | + import json, os, pathlib, re, subprocess + + # constants + BASE_DIR = pathlib.Path("./test-coverage-internal") + OUT_FILE = pathlib.Path("./patomic.json") + + # iterate through all non-hidden directories + archs = [d for d in next(os.walk(BASE_DIR))[1] if d[0] != '.'] + json_out = {} + for arch in archs: + json_out[arch] = {} + + # get lcov summary + cmd = f"lcov --summary {BASE_DIR / arch / 'patomic.lcov'}" + lcov_summary = subprocess.run(cmd, capture_output=True, shell=True, check=True).stdout.decode() + + # go through lcov summary + for line in lcov_summary.split('\n'): + + # skip non-info lines + if not line.lstrip().startswith(("lines", "functions", "branches")): + continue + attr_name = line.lstrip().split('.', maxsplit=1)[0] + + # parse coverage stats + if "no data found" in line: + json_out[arch][attr_name] = { "percentage": 100.0, "count": 0, "total": 0 } + else: + pattern = r"(\d+\.\d+)% \((\d+) of (\d+)" + matches = re.findall(pattern, line)[0] + json_out[arch][attr_name] = { "percentage": float(matches[0]), "count": int(matches[1]), "total": int(matches[2]) } + + # write results to json file + with open(OUT_FILE, 'w') as f: + json.dump(json_out, f, indent=4) + + - name: Create Coverage Summary File + id: coverage-summary + shell: python + run: | + import json, os, pathlib + + # constants + HI_LIMIT, MED_LIMIT = ${{ env.HI_LIMIT }}, ${{ env.MED_LIMIT }} + IN_FILE = pathlib.Path("./patomic.json") + OUT_FILE = pathlib.Path("./patomic.md") + + # markdown prologue + md_string = "## Test Coverage\n" + md_string += "| Target | Lines | Functions | Branches |\n" + md_string += "| :----- | ----: | --------: | -------: |\n" + + # keep track of issues + any_fail = False + universal_warn = False + + # go through json file + with open(IN_FILE, 'r') as fp: + j = json.load(fp) + + # sort archs except for 'universal' first + archs = sorted(list(j.keys()), key=lambda a: a if a != 'universal' else '\0') + + # helper lambdas + make_emoji = lambda p: "✅" if p >= HI_LIMIT else ("⚠️" if p >= MED_LIMIT else "🛑") + field_2str = lambda f: f"{float(f['percentage']):.2f}% ({f['count']}/{f['total']}) {make_emoji(float(f['percentage']))}" + + # write summary of each arch to markdown file + for arch in archs: + line = f"| `{arch}` | `{field_2str(j[arch]['lines'])}` | `{field_2str(j[arch]['functions'])}` | `{field_2str(j[arch]['branches'])}` |\n" + md_string += line + + # check for warnings + any_fail = any_fail or "🛑" in line + universal_warn = universal_warn or (arch == 'universal' and "⚠️" in line) + + # markdown epilogue + sha = "${{ github.event.pull_request.head.sha }}" + md_string += f"\n`✅ >= {HI_LIMIT}`, `⚠️ >= {MED_LIMIT}`, `🛑 < {MED_LIMIT}`\n" + md_string += f"\nResults for commit [`{sha[:8]}`](${{ github.server_url }}/${{ github.repository }}/commit/{sha}).\n" + + # write to file + with open(OUT_FILE, 'wb') as fp: + fp.write(md_string.encode()) + + # write checks to output + with open(os.environ["GITHUB_OUTPUT"], 'a') as fp: + print(f"any_fail={int(any_fail)}", file=fp) + print(f"universal_warn={int(universal_warn)}", file=fp) + + - name: Publish Coverage in Job Summary + run: | + echo "$(cat ./patomic.md)" >> $GITHUB_STEP_SUMMARY + + - name: Publish Coverage in Pull Request Comment + uses: thollander/actions-comment-pull-request@v2 + with: + comment_tag: coverage-summary + filePath: patomic.md + + - name: Fail if Any Coverage Fails or Universal Coverage Warns + run: | + if [[ '${{ steps.coverage-summary.outputs.any_fail }}' != '0' ]]; then + msg="At least one coverage result was below the threshold of ${{ env.MED_LIMIT }}% (a.k.a. 🛑)" + echo "::error title=Any Coverage Failure::${msg}" + exit 1 + elif [[ '${{ steps.coverage-summary.outputs.universal_warn }}' != '0' ]]; then + msg="Universal coverage result was below the threshold of ${{ env.HI_LIMIT }}% (a.k.a. ⚠️)" + echo "::error title=Universal Coverage Warning::${msg}" + exit 1 + fi + + # keep this last so that we have it for debugging if something fails + - name: Delete Test Coverage Artifacts + uses: geekyeggo/delete-artifact@v2 + with: + name: test-coverage-internal \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6127b38c..413d6cf4f 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ modules.order Module.symvers Mkfile.old dkms.conf + +# CMake +CMakeUserPresets.json diff --git a/CMakeLists.txt b/CMakeLists.txt index f284e6903..84ac005cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,7 @@ target_sources( add_subdirectory(src) -# ---- Generate Build Info Headers ---- +# ---- Generate Export Headers ---- # used in export header generated below if(NOT PATOMIC_BUILD_SHARED) @@ -102,6 +102,7 @@ endif() if(PATOMIC_BUILD_TESTING) # need to enable testing in case BUILD_TESTING is disabled + # CTest expects that the top level project enables testing if(PROJECT_IS_TOP_LEVEL) enable_testing() endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..bdea28a21 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,261 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "_patomic-ci-base", + "hidden": true, + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "BUILD_TESTING": true, + "CMAKE_C_STANDARD": "11", + "CMAKE_C_STANDARD_REQUIRED": true, + "CMAKE_C_EXTENSIONS": false, + "CMAKE_CXX_STANDARD": "14", + "CMAKE_CXX_STANDARD_REQUIRED": true, + "CMAKE_CXX_EXTENSIONS": false + } + }, + { + "name": "_patomic-ci-qemu-base", + "hidden": true, + "toolchainFile": "ci/toolchain/qemu-linux-gnu.cmake", + "inherits": [ + "_patomic-ci-base" + ] + }, + + { + "name": "_patomic-ci-native-compiler-clang", + "hidden": true, + "generator": "Unix Makefiles", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "_patomic-ci-native-compiler-gcc", + "hidden": true, + "generator": "Unix Makefiles", + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++" + } + }, + { + "name": "_patomic-ci-native-compiler-msvc", + "hidden": true, + "generator": "Visual Studio 17 2022", + "cacheVariables": { + "CMAKE_C_COMPILER": "cl", + "CMAKE_CXX_COMPILER": "cl" + } + }, + + { + "name": "_patomic-ci-flags-ansi-gnu", + "hidden": true, + "cacheVariables": { + "CMAKE_C_FLAGS": "-Wall -Wextra -Werror -Wpedantic -Wno-unused-function -Wno-atomic-alignment", + "CMAKE_C_STANDARD": "90" + } + }, + { + "name": "_patomic-ci-flags-coverage-clang", + "hidden": true, + "cacheVariables": { + "CMAKE_C_FLAGS_INIT": "-fprofile-instr-generate -fcoverage-mapping", + "CMAKE_CXX_FLAGS_INIT": "-fprofile-instr-generate -fcoverage-mapping", + "CMAKE_EXE_LINKER_FLAGS_INIT": "-fprofile-instr-generate -fcoverage-mapping", + "CMAKE_SHARED_LINKER_FLAGS_INIT": "-fprofile-instr-generate -fcoverage-mapping" + } + }, + { + "name": "_patomic-ci-flags-coverage-gcc", + "hidden": true, + "cacheVariables": { + "CMAKE_C_FLAGS_INIT": "--coverage", + "CMAKE_CXX_FLAGS_INIT": "--coverage", + "CMAKE_EXE_LINKER_FLAGS_INIT": "--coverage", + "CMAKE_SHARED_LINKER_FLAGS_INIT": "--coverage" + } + }, + { + "name": "_patomic-ci-flags-sanitize-gnu", + "hidden": true, + "cacheVariables": { + "CMAKE_C_FLAGS_INIT": "-fsanitize=address,undefined -fsanitize-recover=all -fno-omit-frame-pointer", + "CMAKE_CXX_FLAGS_INIT": "-fsanitize=address,undefined -fsanitize-recover=all -fno-omit-frame-pointer '-DPATOMIC_HAS_ASAN=1' '-DPATOMIC_HAS_UBSAN=1'", + "CMAKE_EXE_LINKER_FLAGS_INIT": "-fsanitize=address,undefined -fsanitize-recover=all", + "CMAKE_SHARED_LINKER_FLAGS_INIT": "-fsanitize=address,undefined -fsanitize-recover=all" + } + }, + { + "name": "_patomic-ci-flags-warning-clang", + "hidden": true, + "cacheVariables": { + "CMAKE_C_FLAGS_INIT": "-Weverything -Werror -Wpedantic -Wno-c++98-compat -Wno-covered-switch-default -Wno-padded -Wno-unused-function -Wno-atomic-alignment -Wno-poison-system-directories", + "CMAKE_CXX_FLAGS_INIT": "-Wall -Wextra -Werror -Wpedantic" + } + }, + { + "name": "_patomic-ci-flags-warning-gcc", + "hidden": true, + "cacheVariables": { + "CMAKE_C_FLAGS_INIT": "-Wall -Wextra -Werror -Wpedantic -Wshadow -Wcast-align -Wconversion -Wsign-conversion -Wnull-dereference -Wdouble-promotion -Wstrict-prototypes -Wmisleading-indentation -Wduplicated-branches -Wlogical-op -Wdeclaration-after-statement -Wno-unused-function", + "CMAKE_CXX_FLAGS_INIT": "-Wall -Wextra -Werror -Wpedantic" + } + }, + { + "name": "_patomic-ci-flags-warning-msvc", + "hidden": true, + "cacheVariables": { + "CMAKE_C_FLAGS_INIT": "/permissive- /volatile:iso /Wall /WX /wd4464 /wd4132 /wd4820 /wd4127 /wd5045 /wd4710 /wd4711 /wd4668", + "CMAKE_CXX_FLAGS_INIT": "/permissive- /volatile:iso /W4 /WX" + } + }, + + { + "name": "patomic-ci-native-win32-msvc-warning", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-msvc", + "_patomic-ci-flags-warning-msvc" + ], + "architecture": "Win32" + }, + { + "name": "patomic-ci-native-win64-msvc-warning", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-msvc", + "_patomic-ci-flags-warning-msvc" + ], + "architecture": "x64" + }, + + { + "name": "patomic-ci-native-unix-clang-ansi", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-clang", + "_patomic-ci-flags-ansi-gnu" + ] + }, + { + "name": "patomic-ci-native-unix-clang-coverage", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-clang", + "_patomic-ci-flags-coverage-clang" + ] + }, + { + "name": "patomic-ci-native-unix-clang-sanitize", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-clang", + "_patomic-ci-flags-sanitize-gnu" + ] + }, + { + "name": "patomic-ci-native-unix-clang-warning", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-clang", + "_patomic-ci-flags-warning-clang" + ] + }, + + { + "name": "patomic-ci-native-unix-gcc-ansi", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-gcc", + "_patomic-ci-flags-ansi-gnu" + ] + }, + { + "name": "patomic-ci-native-unix-gcc-coverage", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-gcc", + "_patomic-ci-flags-coverage-gcc" + ] + }, + { + "name": "patomic-ci-native-unix-gcc-sanitize", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-gcc", + "_patomic-ci-flags-sanitize-gnu" + ] + }, + { + "name": "patomic-ci-native-unix-gcc-warning", + "inherits": [ + "_patomic-ci-base", + "_patomic-ci-native-compiler-gcc", + "_patomic-ci-flags-warning-gcc" + ] + }, + + { + "name": "patomic-ci-qemu-ubuntu-clang-ansi", + "inherits": [ + "_patomic-ci-qemu-base", + "_patomic-ci-flags-ansi-gnu" + ] + }, + { + "name": "patomic-ci-qemu-ubuntu-clang-coverage", + "inherits": [ + "_patomic-ci-qemu-base", + "_patomic-ci-flags-coverage-clang" + ] + }, + { + "name": "patomic-ci-qemu-ubuntu-clang-sanitize", + "inherits": [ + "_patomic-ci-qemu-base", + "_patomic-ci-flags-sanitize-gnu" + ] + }, + { + "name": "patomic-ci-qemu-ubuntu-clang-warning", + "inherits": [ + "_patomic-ci-qemu-base", + "_patomic-ci-flags-warning-clang" + ] + }, + + { + "name": "patomic-ci-qemu-ubuntu-gcc-ansi", + "inherits": [ + "_patomic-ci-qemu-base", + "_patomic-ci-flags-ansi-gnu" + ] + }, + { + "name": "patomic-ci-qemu-ubuntu-gcc-coverage", + "inherits": [ + "_patomic-ci-qemu-base", + "_patomic-ci-flags-coverage-gcc" + ] + }, + { + "name": "patomic-ci-qemu-ubuntu-gcc-sanitize", + "inherits": [ + "_patomic-ci-qemu-base", + "_patomic-ci-flags-sanitize-gnu" + ] + }, + { + "name": "patomic-ci-qemu-ubuntu-gcc-warning", + "inherits": [ + "_patomic-ci-qemu-base", + "_patomic-ci-flags-warning-gcc" + ] + } + ] +} diff --git a/ci/toolchain/qemu-linux-gnu.cmake b/ci/toolchain/qemu-linux-gnu.cmake new file mode 100644 index 000000000..9c09f9134 --- /dev/null +++ b/ci/toolchain/qemu-linux-gnu.cmake @@ -0,0 +1,123 @@ +# ---- Options Summary ---- + +# The follow options MUST ALL be set, either as cache or environment variables. + +# ------------------------------------------------------------------------------------------------------------- +# | Option | Docs | +# |==============================|============================================================================| +# | PATOMIC_CI_SYSROOT | sysroot directory path, passed on to CMake | +# | PATOMIC_CI_XARCH | target architecture, used to select qemu architecture, e.g. aarch64 | +# | PATOMIC_CI_XCOMPILER | host cross-compiler, supported values are 'gcc' and 'clang' | +# | PATOMIC_CI_XCOMPILER_VERSION | host cross-compiler version, major version of the compiler being used | +# | PATOMIC_CI_XTRIPLE | target triple, used to set compiler and its target, e.g. aarch64-linux-gnu | +# ------------------------------------------------------------------------------------------------------------- + + +# ---- Get Cache/Environment Variables ---- + +# target triple, used to set compiler and its target, e.g. aarch64-linux-gnu +set( + PATOMIC_CI_XTRIPLE "$ENV{PATOMIC_CI_XTRIPLE}" CACHE STRING + "target triple, used to set compiler and its target, e.g. aarch64-linux-gnu" +) +if(NOT PATOMIC_CI_XTRIPLE) + message(FATAL_ERROR "PATOMIC_CI_XTRIPLE cache/environment variable is not set") +elseif(NOT PATOMIC_CI_XTRIPLE MATCHES "^([a-zA-Z0-9]+-[a-zA-Z0-9]+-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?)$") + message(FATAL_ERROR "PATOMIC_CI_XTRIPLE value '${PATOMIC_CI_XTRIPLE}' does not match expected pattern '---'") +endif() + +# target architecture, used to select qemu architecture, e.g. aarch64 +set( + PATOMIC_CI_XARCH "$ENV{PATOMIC_CI_XARCH}" CACHE STRING + "target architecture, used to select qemu architecture, e.g. aarch64" +) +if(NOT PATOMIC_CI_XARCH) + message(FATAL_ERROR "PATOMIC_CI_XARCH cache/environment variable is not set") +endif() + +# host cross-compiler, supported values are 'gcc' and 'clang' +set( + PATOMIC_CI_XCOMPILER "$ENV{PATOMIC_CI_XCOMPILER}" CACHE STRING + "host cross-compiler, supported values are 'gcc' and 'clang'" +) +if(NOT PATOMIC_CI_XCOMPILER) + message(FATAL_ERROR "PATOMIC_CI_XCOMPILER cache/environment variable is not set") +elseif(NOT PATOMIC_CI_XCOMPILER MATCHES "^(clang|gcc)$") + message(FATAL_ERROR "PATOMIC_CI_XCOMPILER value '${PATOMIC_CI_XCOMPILER}' is not 'clang' or 'gcc'") +endif() + +# host cross-compiler version, major version of the compiler being used +set( + PATOMIC_CI_XCOMPILER_VERSION "$ENV{PATOMIC_CI_XCOMPILER_VERSION}" CACHE STRING + "host cross-compiler version, major version of the compiler being used" +) +if(NOT PATOMIC_CI_XCOMPILER_VERSION) + message(FATAL_ERROR "PATOMIC_CI_XCOMPILER_VERSION cache/environment variable is not set") +elseif(NOT PATOMIC_CI_XCOMPILER_VERSION MATCHES "^([0-9]+)$") + message(FATAL_ERROR "PATOMIC_CI_XCOMPILER_VERSION '${PATOMIC_CI_XCOMPILER_VERSION}' does not match regex '[0-9]+'") +endif() + +# sysroot directory path, passed on to CMake +set( + PATOMIC_CI_SYSROOT "$ENV{PATOMIC_CI_SYSROOT}" CACHE FILEPATH + "sysroot directory path, passed on to CMake" +) +if(NOT PATOMIC_CI_SYSROOT) + message(FATAL_ERROR "PATOMIC_CI_SYSROOT cache/environment variable is not set") +endif() + + +# ---- Force Use of Cache Variables ---- + +set(cache_variables ) +list(APPEND cache_variables + "PATOMIC_CI_SYSROOT" + "PATOMIC_CI_XARCH" + "PATOMIC_CI_XCOMPILER" + "PATOMIC_CI_XCOMPILER_VERSION" + "PATOMIC_CI_XTRIPLE" +) + +# tells CMake to pass these variables when try_compile is invoked (runs toolchain file) +list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES "${cache_variables}") +list(REMOVE_DUPLICATES CMAKE_TRY_COMPILE_PLATFORM_VARIABLES) + + +# ---- Set CMake Variables ---- + +# set basic target information +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR "${PATOMIC_CI_XARCH}") + +# use sysroot at compile time and pass to QEMU when executing target binaries +set(qemu_command "qemu-${PATOMIC_CI_XARCH}") +if(qemu_command STREQUAL "qemu-armhf") + set(qemu_command "qemu-arm") +elseif(qemu_command STREQUAL "qemu-x86_32") + set(qemu_command "qemu-i386") +endif() +set(CMAKE_SYSROOT "${PATOMIC_CI_SYSROOT}") +set(CMAKE_CROSSCOMPILING_EMULATOR "${qemu_command};-L;${PATOMIC_CI_SYSROOT}") + +# set the appropriate cross compilers and archiver +if(PATOMIC_CI_XCOMPILER STREQUAL "clang") + set(CMAKE_C_COMPILER "clang-${PATOMIC_CI_XCOMPILER_VERSION}") + set(CMAKE_CXX_COMPILER "clang++-${PATOMIC_CI_XCOMPILER_VERSION}") + set(CMAKE_AR "llvm-ar-${PATOMIC_CI_XCOMPILER_VERSION}") +elseif(PATOMIC_CI_XCOMPILER STREQUAL "gcc") + set(CMAKE_C_COMPILER "${PATOMIC_CI_XTRIPLE}-gcc-${PATOMIC_CI_XCOMPILER_VERSION}") + set(CMAKE_CXX_COMPILER "${PATOMIC_CI_XTRIPLE}-g++-${PATOMIC_CI_XCOMPILER_VERSION}") + set(CMAKE_AR "${PATOMIC_CI_XTRIPLE}-ar") +endif() + +# set the target triple matching the cross compilers above +set(CMAKE_C_COMPILER_TARGET "${PATOMIC_CI_XTRIPLE}") +set(CMAKE_CXX_COMPILER_TARGET "${PATOMIC_CI_XTRIPLE}") + +# search for programs in the host environment +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + +# search for libraries, headers, and packages in the target environment +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/cmake/OptionVariables.cmake b/cmake/OptionVariables.cmake index a7480567a..3f5c61e14 100644 --- a/cmake/OptionVariables.cmake +++ b/cmake/OptionVariables.cmake @@ -4,18 +4,18 @@ # ---- Options Summary ---- -# ------------------------------------------------------------------------------------------------ -# | Option | Availability | Default | -# |==============================|===============|===============================================| -# | BUILD_SHARED_LIBS | Top-Level | OFF | -# | BUILD_TESTING | Top-Level | OFF | -# | CMAKE_INSTALL_INCLUDEDIR | Top-Level | include/${package_name}-${PROJECT_VERSION} | -# |------------------------------|---------------|-----------------------------------------------| -# | PATOMIC_BUILD_SHARED | Always | ${BUILD_SHARED_LIBS} | -# | PATOMIC_BUILD_TESTING | Always | ${BUILD_TESTING} | -# | PATOMIC_INCLUDES_WITH_SYSTEM | Not Top-Level | ON | -# | PATOMIC_INSTALL_CMAKEDIR | Always | ${CMAKE_INSTALL_LIBDIR}/cmake/${package_name} | -# ------------------------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------------------------------------------- +# | Option | Availability | Default | +# |==============================|===============|==================================================================| +# | BUILD_SHARED_LIBS | Top-Level | OFF | +# | BUILD_TESTING | Top-Level | OFF | +# | CMAKE_INSTALL_INCLUDEDIR | Top-Level | include/${package_name}-${PROJECT_VERSION} | +# |------------------------------|---------------|------------------------------------------------------------------| +# | PATOMIC_BUILD_SHARED | Always | ${BUILD_SHARED_LIBS} | +# | PATOMIC_BUILD_TESTING | Always | ${BUILD_TESTING} AND ${PROJECT_IS_TOP_LEVEL} | +# | PATOMIC_INCLUDES_WITH_SYSTEM | Not Top-Level | ON | +# | PATOMIC_INSTALL_CMAKEDIR | Always | ${CMAKE_INSTALL_LIBDIR}/cmake/${package_name}-${PROJECT_VERSION} | +# ------------------------------------------------------------------------------------------------------------------- # ---- Build Shared ---- @@ -66,20 +66,26 @@ endif() if(PROJECT_IS_TOP_LEVEL) option(BUILD_TESTING "Build tests" OFF) endif() +if(PROJECT_IS_TOP_LEVEL AND BUILD_TESTING) + set(build_testing ON) +endif() option( PATOMIC_BUILD_TESTING "Override BUILD_TESTING for ${package_name} library" - ${BUILD_TESTING} + ${build_testing} ) +set(build_testing ) mark_as_advanced(PATOMIC_BUILD_TESTING) # ---- Install Include Directory ---- # Adds an extra directory to the include path by default, so that when you link -# against the target, you get `/include/-X.Y.Z` added to your +# against the target, you get `/include/-X` added to your # include paths rather than ` patomic-test-${kind}-${name} (e.g. patomic-test-bt-SomeExample) # - executable name -> ${name} (e.g. SomeExample on Unix or SomeExample.exe on Windows) # - install directory -> ${CMAKE_INSTALL_TESTDIR}/${kind} (e.g. share/test/bt) # +# Labels: +# - each test (not target) will have ${kind} appended to its LABELS property +# - main use case is for code coverage to be able to only run unit tests +# +# Working Directory (CTest): +# - each test target has its working directory set to ${PROJECT_BINARY_DIR}/working/${kind}/${name} +# - if a multi-config generator is used, 'working' is replaced with 'working/$' +# - reasoning: +# - coverage files generated by executables may overwrite existing coverage files (e.g. with clang) +# - this is an issue if multiple test executables exist in the same directory +# - this solution solves that by running each executable in its own directory +# # Hierarchy: # - patomic-test -> base custom target & component (build/install) for all tests # - patomic-test-${kind} -> custom target & component (build install) for all tests of a specific kind @@ -24,8 +41,8 @@ function(_create_test) # setup what arguments we expect - set(all_kinds "BT;UT") # list of all kinds we can iterate over - set(all_kinds_opt_msg "BT|UT") # string to use in debug message + set(all_kinds "BT;ST;UT") # list of all kinds we can iterate over + set(all_kinds_opt_msg "BT|ST|UT") # string to use in debug message cmake_parse_arguments( "ARG" @@ -101,22 +118,33 @@ function(_create_test) endif() # create target with sources - add_executable( - ${target} + add_executable(${target}) + + # add sources to target + target_sources( + ${target} PRIVATE ${ARG_SOURCE} ) # add include directories target_include_directories( ${target} PRIVATE + "$" ${ARG_INCLUDE} ) + # add /src and /include sources that apply to all test targets + target_link_libraries( + ${target} PRIVATE + patomic-test-include + patomic-test-src + ) + # update dependencies list directly because we use it in Windows PATH stuff later if("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.20.0") list(APPEND ARG_LINK GTest::gtest_main) - # TODO: this prevents test lookup on Windows; fix once pipeline exists - # TODO: see https://github.com/google/googletest/issues/2157 + # TODO: this prevents test lookup on Windows; fix once pipeline exists (GHI #30) + # TODO: see https://github.com/google/googletest/issues/2157 (GHI #30) # if ("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.23.0") # list(APPEND ARG_LINK GTest::gmock) # endif() @@ -140,7 +168,7 @@ function(_create_test) string(TOUPPER "${kind}" kind_upper) target_compile_definitions( ${target} PRIVATE - "PATOMIC_${kind_upper}" + "PATOMIC_TEST_KIND_${kind_upper}=1" ) # set binary name instead of using default @@ -152,49 +180,76 @@ function(_create_test) # register test with GTest/CTest and parent target + # setup path differently depending on generator + get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + set(test_working_dir "${PROJECT_BINARY_DIR}/working") + if(is_multi_config) + set(test_working_dir "${test_working_dir}/$/${kind}/${name}") + else() + set(test_working_dir "${test_working_dir}/${kind}/${name}") + endif() + # must be run in same directory scope as target gtest_add_tests( TARGET ${target} TEST_LIST added_tests + WORKING_DIRECTORY "${test_working_dir}" ) - add_dependencies(${parent_target} ${target}) - + # add label to tests so ctest can run them by kind + foreach(test IN LISTS added_tests) + set_property( + TEST "${test}" + APPEND PROPERTY LABELS "${kind}" + ) + endforeach() - # deal with Windows runtime linker issues + # custom target to make sure the working directory exists for the test + add_custom_target( + ${target}-create-working-dir + COMMAND "${CMAKE_COMMAND}" -E make_directory "${test_working_dir}" + ) - if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + # set build dependencies + add_dependencies(${target} ${target}-create-working-dir) + add_dependencies(${parent_target} ${target}) - # check we actually care about Windows PATH stuff - if(NOT PATOMIC_WINDOWS_SET_CTEST_PATH_ENV AND - NOT PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE) - return() - endif() - # get paths to all shared library dependencies (DLLs) - windows_deps_paths( - deps_paths - ${ARG_LINK} - ) + # deal with Windows runtime linker lookup issues - # set environment variable for each test so that CTest works - if(deps_paths AND PATOMIC_WINDOWS_SET_CTEST_PATH_ENV) - foreach(test IN LISTS added_tests) - set_property( - TEST "${test}" - PROPERTY ENVIRONMENT "PATH=${deps_paths}" - ) - endforeach() - endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Windows" AND + PATOMIC_WINDOWS_MODIFY_CTEST_PATH_ENV) - # make dependencies accessible from parent target - # so that we can create single file for all tests in kind - if(PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE) - set_property( - TARGET ${parent_target} - APPEND PROPERTY WIN_DEPS_TARGETS "${ARG_LINK}" + # get paths to all shared library dependencies (DLLs) + # this should just be patomic and gtest + set(deps_paths ) + foreach(dep_target IN LISTS ARG_LINK) + # This will fail if passed a link option that isn't a target + # This is intentional; don't do that. + # Instead, create an IMPORTED library, and set its target properties + # such as IMPORTED_LOCATION for the library path and set + # INTERFACE_INCLUDE_DIRECTORIES to the directories containing any + # necessary header files. + if(NOT TARGET "${dep_target}") + message(FATAL_ERROR "Encountered non-target dependency '${dep_target}' for target ${target} (check source comment)") + endif() + get_target_property(type "${dep_target}" TYPE) + if(type STREQUAL "SHARED_LIBRARY") + list(APPEND deps_paths "$") + endif() + endforeach() + + # tidy up the paths + list(REMOVE_DUPLICATES deps_paths) + string(REPLACE ";" "\;" deps_paths "${deps_paths}") + + # modify environment variable for each test so that CTest can find DLLs + foreach(test IN LISTS added_tests) + set_tests_properties( + "${test}" PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${deps_paths}" ) - endif() + endforeach() endif() @@ -257,6 +312,30 @@ function(create_bt) endfunction() +# Creates target patomic-test-st-${name} corresponding to ST test executable. +# +# create_st( +# NAME +# ) +function(create_st) + + cmake_parse_arguments( + "ARG" + "" + "NAME" + "SOURCE" + ${ARGN} + ) + + _create_test( + ${ARG_UNPARSED_ARGUMENTS} + ST ${ARG_NAME} + SOURCE ${ARG_SOURCE} + ) + +endfunction() + + # Creates target patomic-test-ut-${name} corresponding to UT test executable. # # create_ut( @@ -290,89 +369,3 @@ function(create_ut) target_compile_definitions(${target_name} PRIVATE PATOMIC_STATIC_DEFINE) endfunction() - - -# ---- Create Test Dependency File ---- - -# Creates a file containing the output of windows_deps_paths for all tests of -# the given kind registered so far. -# The file path will be "${CMAKE_CURRENT_BINARY_DIR}/windows_dependencies_path.txt". -# Expects a target named patomic-test-${kind} to exist -# E.g. if you call it as create_test_win_deps_paths_file(BT) then patomic-bt must -# exist. -# This function has no effect when not running on Windows. -# -# create_test_win_deps_paths_file( -# BT|UT -# ) -function(create_test_win_deps_paths_file ARG_KIND) - - # check we actually want to generate file - - if(NOT PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE) - return() - endif() - - - # check KIND is valid - - set(all_kinds_opt_msg "BT|UT") - set(func_name "create_test_win_deps_paths_file") - - if(NOT ARG_KIND MATCHES "^(${all_kinds_option})$") - message(WARNING "${all_kinds_option} option needs to be specified when invoking '${func_name}'") - message(FATAL_ERROR "Aborting '${func_name}' due to invalid arguments") - endif() - - string(TOLOWER ${ARG_KIND} kind) - - - # create and install file with dependencies path - - if(CMAKE_SYSTEM_NAME STREQUAL "Windows") - - # get dependencies set by create_test from target - get_target_property(dep_targets patomic-test-${kind} WIN_DEP_TARGETS) - if("${dep_targets}" STREQUAL "dep_targets-NOTFOUND") - message(VERBOSE "Skipping creation of Windows dependencies PATH file for ${ARG_KIND}; no relevant test targets created") - return() - endif() - list(REMOVE_DUPLICATES dep_targets) - - # get paths to all shared library dependencies (DLLs) - windows_deps_paths( - deps_paths - ${dep_targets} - ) - - # create file - set(file_path "${CMAKE_CURRENT_BINARY_DIR}/windows_dependencies_path.txt") - file(GENERATE - OUTPUT ${file_path} - CONTENT "${deps_paths}" - ) - - # copy file to install location - if(NOT CMAKE_SKIP_INSTALL_RULES) - - # install as part of patomic-test-${kind} component - install( - FILES ${file_path} - COMPONENT patomic-test-${kind} - DESTINATION "${CMAKE_INSTALL_TESTDIR}/patomic/${kind}" - EXCLUDE_FROM_ALL - ) - - # install as part of patomic-test component - install( - FILES ${file_path} - COMPONENT patomic-test - DESTINATION "${CMAKE_INSTALL_TESTDIR}/patomic/${kind}" - EXCLUDE_FROM_ALL - ) - - endif() - - endif() - -endfunction() diff --git a/test/cmake/OptionVariables.cmake b/test/cmake/OptionVariables.cmake index 81a860ad1..e05f963a2 100644 --- a/test/cmake/OptionVariables.cmake +++ b/test/cmake/OptionVariables.cmake @@ -1,14 +1,13 @@ # ---- Options Summary ---- -# -------------------------------------------------------------------- -# | Option | Availability | Default | -# |======================================|==============|============| -# | CMAKE_INSTALL_TESTDIR (unofficial) | Always | share/test | -# |--------------------------------------|--------------|------------| -# | PATOMIC_CREATE_TEST_TARGETS_MATCHING | Always | ^(.*)$ | -# | PATOMIC_WINDOWS_SET_CTEST_PATH_ENV | Always | ON | -# | PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE | Always | OFF | -# -------------------------------------------------------------------- +# ----------------------------------------------------------------------- +# | Option | Availability | Default | +# |=======================================|================|============| +# | CMAKE_INSTALL_TESTDIR (unofficial) | Always | share/test | +# |---------------------------------------|----------------|------------| +# | PATOMIC_CREATE_TEST_TARGETS_MATCHING | Always | ^(.*)$ | +# | PATOMIC_WINDOWS_MODIFY_CTEST_PATH_ENV | Always (3.22+) | ON | +# ----------------------------------------------------------------------- # ---- Test Install Directory ---- @@ -43,37 +42,24 @@ set( mark_as_advanced(PATOMIC_CREATE_TEST_TARGETS_MATCHING) -# ---- Windows Tests Path ---- +# ---- Windows Tests Paths ---- -# By default we set PATH for tests run with CTest on Windows in order to prevent -# linker errors. -# Due to limitations in CMake, we can only completely override the PATH, rather -# than prepend or append to it. -# This gives users the option to disable this behaviour. +# By default we prepend the PATH environment variable for tests run with CTest +# on Windows with the directory paths of all its shared library dependencies. +# This is done to prevent linker errors (because Windows doesn't support rpath). # This option has no effect when not running on Windows. -option( - PATOMIC_WINDOWS_SET_CTEST_PATH_ENV - "Set PATH environment variable for tests when run CTest on Windows" - ON -) -mark_as_advanced(PATOMIC_WINDOWS_SET_CTEST_PATH_ENV) - - -# ---- Windows Path File ---- - -# On Windows we need to set PATH for tests but may not want to have the PATH be -# completely overridden, like with PATOMIC_WINDOWS_SET_CTEST_PATH_ENV. -# Instead we can generated a file per test kind that contains a string that can -# be manually prepended to PATH before running tests, in order to ensure that -# runtime dependencies can be found. -# Most of the time we don't need this file (since CTest will take care of that -# for us), so we don't generate it by default. -# Additionally disabled by default because it contains potentially private -# information about the target platform. -# This option has no effect when not running on Windows. -option( - PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE - "Create file with PATH environment variables for tests on Windows" - OFF -) -mark_as_advanced(PATOMIC_WINDOWS_CREATE_PATH_ENV_FILE) +# This option requires CMake 3.22 or later. +if("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.22.0") + option( + PATOMIC_WINDOWS_MODIFY_CTEST_PATH_ENV + "Modify PATH environment variable for tests when run with CTest on Windows" + ON + ) + mark_as_advanced(PATOMIC_WINDOWS_MODIFY_CTEST_PATH_ENV) +elseif(PATOMIC_WINDOWS_MODIFY_CTEST_PATH_ENV) + message( + WARNING + "Option 'PATOMIC_WINDOWS_MODIFY_CTEST_PATH_ENV' for 'patomic_test' requires CMake 3.22+, currently running ${CMAKE_VERSION}, option is disabled" + ) + set(PATOMIC_WINDOWS_MODIFY_CTEST_PATH_ENV ) +endif() \ No newline at end of file diff --git a/test/cmake/WindowsDependenciesPath.cmake b/test/cmake/WindowsDependenciesPath.cmake deleted file mode 100644 index c3cedb418..000000000 --- a/test/cmake/WindowsDependenciesPath.cmake +++ /dev/null @@ -1,46 +0,0 @@ -# ---- Windows Path Prefix ---- - -# Windows doesn't support rpath, so when linking dynamically the libraries need -# to either be in the same directory or in PATH. -# This function sets a variable to a list of GENERATOR strings which resolve to -# the paths of the linked dependencies. -# See: https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order -# Usage: windows_deps_paths( ...) -function(windows_deps_paths ARG_VAR) - - # check that we're on Windows - if(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows") - set(${ARG_VAR} "" PARENT_SCOPE) - return() - endif() - - # create a list of paths to dependency shared library locations - set(paths "") - foreach(target IN LISTS ARGN) - # This will fail if passed a link option that isn't a target. - # This is intentional; don't do that. - # Instead, create an IMPORTED library, and set its target properties - # such as IMPORTED_LOCATION for the library (.a .so etc.) path and - # set INTERFACE_INCLUDE_DIRECTORIES to the directory containing any - # necessary header files. - get_target_property(type "${target}" TYPE) - if(type STREQUAL "SHARED_LIBRARY") - list(APPEND paths "$") - endif() - endforeach() - - # remove duplicates - # makes generated file more human readable (if generated) - list(REMOVE_DUPLICATES paths) - - # concatenate list into string with same format as PATH on Windows - set(path "") - set(glue "") - foreach(p IN LISTS paths) - set(path "${path}${glue}${p}") - set(glue "\;") # backslash is important - endforeach() - - # "return" string with PATH data - set(${ARG_VAR} "${path}" PARENT_SCOPE) -endfunction() diff --git a/test/improve_ctest_xml.py b/test/improve_ctest_xml.py new file mode 100644 index 000000000..dd112670a --- /dev/null +++ b/test/improve_ctest_xml.py @@ -0,0 +1,199 @@ +import argparse +import xml.etree.ElementTree as ETree + +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional + + +@dataclass +class ProgArgs: + input_path: Path + output_path: Path + triple: str + + +def get_program_arguments() -> ProgArgs: + # define program arguments + parser = argparse.ArgumentParser(description="Tidy up CTest's JUnit XML to more closely match GTest output.") + parser.add_argument("-i", "--input", type=str, required=True, help="JUnit XML file generated by CTest") + parser.add_argument("-o", "--output", type=str, required=True, help="File path to write output into") + parser.add_argument("-t", "--triple", type=str, required=True, help="Platform identifier on which input file was generated from") + + # obtain program arguments + args = parser.parse_args() + return ProgArgs(input_path=Path(args.input), output_path=Path(args.output), triple=args.triple) + + +def _make_gtest_default_testsuite(suite_name: str) -> ETree.Element: + elem = ETree.Element("testsuite") + elem.attrib.update({ + "name": suite_name, + "tests": "0", + "failures": "0", + "disabled": "0", + "skipped": "0", + "errors": "0", + "time": "0." + }) + return elem + + +def _make_gtest_testcase_result(suite_name: str, case_name: str, status: str, sysout: str) -> Optional[str]: + # test failed, possibly crashed + if status == "fail": + if f"[ FAILED ] {suite_name}.{case_name}" in sysout: + return "failed" + else: + return "crashed" + + # test was either skipped while running, or not run at all + elif status == "notrun": + if f"[ SKIPPED ] {suite_name}.{case_name}" in sysout: + return "skipped" + else: + return None + + # test was run to completion + elif status == "run": + return "completed" + + # should be unreachable + else: + raise ValueError(f"status attribute of {suite_name}.{case_name} has unknown value: {status}") + + +def _add_gtest_testcase_to_testsuite(suite: ETree.Element, case: ETree.Element) -> ETree.Element: + # helper lambda + inc_attr = lambda elem, key: elem.set(key, str(int(elem.get(key)) + 1)) + + # increment time and tests count + inc_attr(suite, "tests") + new_time = str(float(suite.get("time")) + float(case.get("time"))) + if '.' not in new_time: + new_time += '.' + suite.set("time", new_time) + + # increment diagnostic counters + status: str = case.get("status") + result: Optional[str] = case.get("result") + if status == "run": + if result == "completed": + pass + elif result == "skipped": + inc_attr(suite, "skipped") + elif result == "failed": + inc_attr(suite, "failures") + elif result == "crashed": + inc_attr(suite, "errors") + else: + raise ValueError(f"result attribute of {suite.get('name')}.{case.get('name')} has unknown value: {result}") + elif status == "notrun": + inc_attr(suite, "disabled") + else: + raise ValueError(f"status attribute of {suite.get('name')}.{case.get('name')} has unknown value: {status}") + + # attach case to suite + suite.append(case) + return suite + + +def _parse_gtest_testsuites_from_ctest_testsuite(ctest_testsuite: ETree.Element) -> List[ETree.Element]: + # get all gtest testsuite names, mapped to their elements + gtest_suites: Dict[str, ETree.Element] = {} + for case in ctest_testsuite: + + # all testcase element names are expected to be "Suite.Case" format + suite_name, case_name = case.get("name").split('.', maxsplit=1) + case.set("name", case_name) + case.set("classname", suite_name) + + # make sure we recognise status + status: str = case.get("status") + if status not in ["run", "notrun", "fail"]: + raise ValueError(f"status attribute of {suite_name}.{case_name} has unknown value: {status}") + + # get sub-element for checking later + sysout: str = "" + all_elem_sysout = case.findall("system-out") + if len(all_elem_sysout) > 1: + raise ValueError(f"more than one sub-element found in {suite_name}.{case_name}") + if len(all_elem_sysout) > 0: + sysout = "" if all_elem_sysout[0].text is None else str(all_elem_sysout[0].text) + + # remove sub-element if test runs with no issues + if status in "run" and sysout and not any(s in sysout.lower() for s in ["warn", "error", "fail"]): + case.remove(all_elem_sysout[0]) + + # set result to gtest values + result = _make_gtest_testcase_result(suite_name, case_name, status, sysout) + if result is not None: + case.set("result", result) + if status == "fail" or result == "skipped": + case.set("status", "run") + + # populate dictionary + suite = gtest_suites.get(suite_name) + if suite is None: + suite = _make_gtest_default_testsuite(suite_name) + gtest_suites[suite_name] = _add_gtest_testcase_to_testsuite(suite, case) + + # return all suites + return list(gtest_suites.values()) + + +def convert_ctest_root_to_gtest_root(triple: str, ctest_root: ETree.Element) -> ETree.Element: + # helper lambda + add_attr = lambda elem, from_, key: elem.set(key, str(int(elem.get(key)) + int(from_.get(key)))) + + # make gtest root element + gtest_root = ETree.Element("testsuites") + gtest_root.attrib.update({ + "name": triple, + "tests": ctest_root.get("tests"), + "failures": "0", + "disabled": "0", + "skipped": "0", + "errors": "0", + "time": "0.", + "timestamp": ctest_root.get("timestamp") + }) + + # get attributes from all suites + gtest_suites = _parse_gtest_testsuites_from_ctest_testsuite(ctest_root) + for suite in gtest_suites: + + # increment time + new_time = str(float(gtest_root.get("time")) + float(suite.get("time"))) + if '.' not in new_time: + new_time += '.' + gtest_root.set("time", new_time) + + # increment diagnostic counters + add_attr(gtest_root, suite, "failures") + add_attr(gtest_root, suite, "disabled") + add_attr(gtest_root, suite, "skipped") + add_attr(gtest_root, suite, "errors") + + # add as sub-element + gtest_root.append(suite) + + # return complete root + return gtest_root + + +if __name__ == "__main__": + + args = get_program_arguments() + + # parse input file as xml + ctest_tree = ETree.parse(args.input_path) + ctest_root = ctest_tree.getroot() + + # convert to gtest + gtest_root = convert_ctest_root_to_gtest_root(args.triple, ctest_root) + gtest_tree = ETree.ElementTree(gtest_root) + + # write gtest xml + ETree.indent(gtest_tree) + gtest_tree.write(args.output_path, encoding="utf-8", xml_declaration=True) diff --git a/test/include/CMakeLists.txt b/test/include/CMakeLists.txt new file mode 100644 index 000000000..6205c9ebb --- /dev/null +++ b/test/include/CMakeLists.txt @@ -0,0 +1,15 @@ +# fine with CML file here because include isn't installed for tests + +# create interface target that is automatically linked to all tests +# this is solely so that IDEs understand that these are source files +# include directories are set in test creation +add_library( + patomic-test-include INTERFACE + # . +) + +# require C++14 as minimum +target_compile_features( + patomic-test-include INTERFACE + cxx_std_14 +) diff --git a/test/kind/CMakeLists.txt b/test/kind/CMakeLists.txt new file mode 100644 index 000000000..6a2917e2c --- /dev/null +++ b/test/kind/CMakeLists.txt @@ -0,0 +1,20 @@ +# only add BTs if patomic target is available +if(TARGET patomic::patomic) + add_subdirectory(bt) + message(STATUS "Enabled binary tests") +else() + message(STATUS "Skipping binary tests; patomic target not available") +endif() + +# always STs since they don't require patomic in any way +add_subdirectory(st) +message(STATUS "Enabled system tests") + +# only add UTs if patomic files are available +# these are currently set by patomic before including this project +if(PATOMIC_BINARY_DIR AND PATOMIC_SOURCE_DIR) + add_subdirectory(ut) + message(STATUS "Enabled unit tests") +else() + message(STATUS "Skipping unit tests; not building as sub-project of patomic") +endif() diff --git a/test/bt/CMakeLists.txt b/test/kind/bt/CMakeLists.txt similarity index 57% rename from test/bt/CMakeLists.txt rename to test/kind/bt/CMakeLists.txt index 9cccd846e..cd4a859cb 100644 --- a/test/bt/CMakeLists.txt +++ b/test/kind/bt/CMakeLists.txt @@ -1,5 +1,4 @@ -include(../cmake/CreateTest.cmake) -include(../cmake/WindowsDependenciesPath.cmake) +include(../../cmake/CreateTest.cmake) # ---- Setup Tests ---- @@ -9,8 +8,3 @@ add_dependencies(patomic-test patomic-test-bt) create_bt(NAME example_add SOURCE example_add.cpp) create_bt(NAME example_sub SOURCE example_sub.cpp) - - -# ---- Windows Path Issues ---- - -create_test_win_deps_paths_file(BT) diff --git a/test/bt/example_add.cpp b/test/kind/bt/example_add.cpp similarity index 100% rename from test/bt/example_add.cpp rename to test/kind/bt/example_add.cpp diff --git a/test/bt/example_sub.cpp b/test/kind/bt/example_sub.cpp similarity index 100% rename from test/bt/example_sub.cpp rename to test/kind/bt/example_sub.cpp diff --git a/test/kind/st/CMakeLists.txt b/test/kind/st/CMakeLists.txt new file mode 100644 index 000000000..5312dc3a9 --- /dev/null +++ b/test/kind/st/CMakeLists.txt @@ -0,0 +1,10 @@ +include(../../cmake/CreateTest.cmake) + + +# ---- Setup Tests ---- + +add_custom_target(patomic-test-st) +add_dependencies(patomic-test patomic-test-st) + +create_st(NAME check_asan SOURCE check_asan.cpp) +create_st(NAME check_ubsan SOURCE check_ubsan.cpp) diff --git a/test/kind/st/check_asan.cpp b/test/kind/st/check_asan.cpp new file mode 100644 index 000000000..4ddf1c882 --- /dev/null +++ b/test/kind/st/check_asan.cpp @@ -0,0 +1,70 @@ +#include +#include + + +class StAsan : public testing::Test +{}; + + +TEST_F(StAsan, UseAfterFree) +{ +#if PATOMIC_HAS_ASAN + EXPECT_FATAL_FAILURE({ + int *p = new int(5); + volatile int val = *p; + delete p; + + volatile int _ = *p; // <-- use after free (intentional) + }, "asan"); +#endif +} + +TEST_F(StAsan, HeapBufferOverflow) +{ +#if PATOMIC_HAS_ASAN + EXPECT_FATAL_FAILURE({ + int *arr = new int[100]{}; + int i = 1; + + volatile int _ = arr[100 + i]; // <-- buffer overflow (intentional) + + delete[] arr; + }, "asan"); +#endif +} + +TEST_F(StAsan, StackBufferOverflow) +{ +#if PATOMIC_HAS_ASAN + EXPECT_NONFATAL_FAILURE({ + EXPECT_FATAL_FAILURE({ + int arr[100]{}; + int i = 1; + + volatile int _ = arr[100 + i]; // <-- buffer overflow (intentional) + }, "asan"); +#if defined(__clang__) + }, "Actual: 2"); // this is also caught by ubsan on clang once more +#elif defined(__GNUG__) + }, "Actual: 3"); // this is also caught by ubsan on gcc twice more +#endif +#endif +} + +TEST_F(StAsan, GlobalBufferOverflow) +{ +#if PATOMIC_HAS_ASAN + EXPECT_NONFATAL_FAILURE({ + EXPECT_FATAL_FAILURE({ + static int arr[100]{}; + int i = 1; + + volatile int _ = arr[100 + i]; // <-- buffer overflow (intentional) + }, "asan"); +#if defined(__clang__) + }, "Actual: 2"); // this is also caught by ubsan on clang once more +#elif defined(__GNUG__) + }, "Actual: 3"); // this is also caught by ubsan on gcc twice more +#endif +#endif +} diff --git a/test/kind/st/check_ubsan.cpp b/test/kind/st/check_ubsan.cpp new file mode 100644 index 000000000..b97f683da --- /dev/null +++ b/test/kind/st/check_ubsan.cpp @@ -0,0 +1,51 @@ +#include +#include + +#include +#include + + +class StUbsan : public testing::Test +{}; + + +TEST_F(StUbsan, ShiftExponentTooLarge) +{ +#if PATOMIC_HAS_UBSAN + EXPECT_FATAL_FAILURE({ + volatile int _ = sizeof(int) * CHAR_BIT; + int x = 10; + int e = _; + + _ = x << e; // <-- shift exponent too large (intentional) + }, "ubsan"); +#endif +} + +TEST_F(StUbsan, SignedIntegerOverflow) +{ +#if PATOMIC_HAS_UBSAN + EXPECT_FATAL_FAILURE({ + volatile int _ = 5; + int x = _; + + x += INT_MAX; // <-- signed integer overflow (intentional) + + _ = x; + }, "ubsan"); +#endif +} + +TEST_F(StUbsan, FloatCastOverflow) +{ +#if PATOMIC_HAS_UBSAN && defined(__clang__) + EXPECT_FATAL_FAILURE({ + volatile auto _ = std::numeric_limits::max(); + int x; + + x = static_cast(_); // <-- float cast overflow (intentional) + + _ = x; + }, "ubsan"); +#endif +} diff --git a/test/ut/CMakeLists.txt b/test/kind/ut/CMakeLists.txt similarity index 65% rename from test/ut/CMakeLists.txt rename to test/kind/ut/CMakeLists.txt index efdd8230e..a87c6a9ef 100644 --- a/test/ut/CMakeLists.txt +++ b/test/kind/ut/CMakeLists.txt @@ -1,5 +1,4 @@ -include(../cmake/CreateTest.cmake) -include(../cmake/WindowsDependenciesPath.cmake) +include(../../cmake/CreateTest.cmake) # ---- Setup Tests ---- @@ -9,8 +8,3 @@ add_dependencies(patomic-test patomic-test-ut) create_ut(NAME example_add SOURCE example_add.cpp "${PATOMIC_SOURCE_DIR}/src/patomic.c") create_ut(NAME example_sub SOURCE example_sub.cpp "${PATOMIC_SOURCE_DIR}/src/patomic.c") - - -# ---- Windows Path Issues ---- - -create_test_win_deps_paths_file(UT) diff --git a/test/ut/example_add.cpp b/test/kind/ut/example_add.cpp similarity index 100% rename from test/ut/example_add.cpp rename to test/kind/ut/example_add.cpp diff --git a/test/ut/example_sub.cpp b/test/kind/ut/example_sub.cpp similarity index 100% rename from test/ut/example_sub.cpp rename to test/kind/ut/example_sub.cpp diff --git a/test/src/CMakeLists.txt b/test/src/CMakeLists.txt new file mode 100644 index 000000000..2c59f583b --- /dev/null +++ b/test/src/CMakeLists.txt @@ -0,0 +1,24 @@ +# create object target that is automatically linked to all tests +add_library( + patomic-test-src OBJECT + # . + "sanitizer_error.cpp" + "sanitizer_options.cpp" +) + +# add include directories +target_include_directories( + patomic-test-src PRIVATE + "$" + "$" +) + +# don't need to link against GTest or patomic: +# - the test libraries already link against GTest which should be enough +# - we shouldn't need anything from patomic here (or UTs would break) + +# require C++14 as minimum +target_compile_features( + patomic-test-src PRIVATE + cxx_std_14 +) diff --git a/test/src/sanitizer_error.cpp b/test/src/sanitizer_error.cpp new file mode 100644 index 000000000..589aba567 --- /dev/null +++ b/test/src/sanitizer_error.cpp @@ -0,0 +1,30 @@ +#include + + +extern "C" { + + /// @brief Called by asan when an error is detected. + /// @note Asan may still abort after calling this function. + void + __asan_on_error() + { + GTEST_FAIL() << "Encountered an address sanitizer (asan) error"; + } + + /// @brief Called by tsan when an error is detected. + /// @note Tsan may still abort after calling this function. + void + __tsan_on_report() + { + GTEST_FAIL() << "Encountered a thread sanitizer (tsan) error"; + } + + /// @brief Called by ubsan when an error is detected . + /// @note Ubsan may still abort after calling this function. + void + __ubsan_on_report() + { + GTEST_FAIL() << "Encountered an undefined behaviour sanitizer (ubsan) error"; + } + +} // extern "C" diff --git a/test/src/sanitizer_options.cpp b/test/src/sanitizer_options.cpp new file mode 100644 index 000000000..a5af4d834 --- /dev/null +++ b/test/src/sanitizer_options.cpp @@ -0,0 +1,24 @@ +extern "C" { + + /// @brief Called by asan to set default options. + const char * + __asan_default_options() + { + return "halt_on_error=0"; + } + + /// @brief Called by tsan to set default options. + const char * + __tsan_default_options() + { + return ""; + } + + /// @brief Called by ubsan to set default options. + const char * + __ubsan_default_options() + { + return ""; + } + +} // extern "C"