name: CI

on: [push, pull_request]

jobs:
  ci:
    name: ${{ matrix.name }}
    runs-on: ${{ matrix.os }}

    env:
      CMAKE_GENERATOR: Ninja
      ASAN_OPTIONS: strict_string_checks=true:detect_odr_violation=2:detect_stack_use_after_return=true:check_initialization_order=true:strict_init_order=true
      TSAN_OPTIONS: force_seq_cst_atomics=1
      CTEST_OUTPUT_ON_FAILURE: ON
      CTEST_PARALLEL_LEVEL: 2
      ACTIONS_ALLOW_UNSECURE_COMMANDS: true # because of the set-env calls

    strategy:
      fail-fast: false
      matrix:
        # Github Actions requires a single row to be added to the build matrix.
        # See https://help.github.com/en/articles/workflow-syntax-for-github-actions.
        name: [
          ubuntu-18.04-gcc-4.8,
          ubuntu-18.04-gcc-4.9,
          ubuntu-18.04-gcc-5,
          ubuntu-18.04-gcc-6,
          ubuntu-18.04-gcc-7,
          ubuntu-18.04-gcc-8,
          ubuntu-18.04-gcc-9,
          ubuntu-latest-gcc-10,
          ubuntu-18.04-clang-3.5,
          ubuntu-18.04-clang-3.6,
          ubuntu-18.04-clang-3.7,
          ubuntu-18.04-clang-3.8,
          ubuntu-18.04-clang-3.9,
          ubuntu-18.04-clang-4.0,
          ubuntu-18.04-clang-5.0,
          ubuntu-18.04-clang-6.0,
          ubuntu-18.04-clang-7,
          ubuntu-18.04-clang-8,
          # ubuntu-latest-clang-9,
          ubuntu-latest-clang-10,
          # ubuntu-latest-clang-11,
          windows-2016-cl,
          windows-2016-clang-cl,
          windows-2016-clang,
          # windows-2016-gcc,
          windows-2019-cl,
          windows-2019-clang-cl,
          windows-2019-clang,
          # windows-2019-gcc,
          macOS-latest-xcode-11.3,
        ]

        include:
          - name: ubuntu-18.04-gcc-4.8
            os: ubuntu-18.04
            compiler: gcc
            version: "4.8"

          - name: ubuntu-18.04-gcc-4.9
            os: ubuntu-18.04
            compiler: gcc
            version: "4.9"

          - name: ubuntu-18.04-gcc-5
            os: ubuntu-18.04
            compiler: gcc
            version: "5"

          - name: ubuntu-18.04-gcc-6
            os: ubuntu-18.04
            compiler: gcc
            version: "6"

          - name: ubuntu-18.04-gcc-7
            os: ubuntu-18.04
            compiler: gcc
            version: "7"

          - name: ubuntu-18.04-gcc-8
            os: ubuntu-18.04
            compiler: gcc
            version: "8"

          - name: ubuntu-18.04-gcc-9
            os: ubuntu-18.04
            compiler: gcc
            version: "9"

          - name: ubuntu-latest-gcc-10
            os: ubuntu-latest
            compiler: gcc
            version: "10"

          - name: ubuntu-18.04-clang-3.5
            os: ubuntu-18.04
            compiler: clang
            version: "3.5"

          - name: ubuntu-18.04-clang-3.6
            os: ubuntu-18.04
            compiler: clang
            version: "3.6"

          - name: ubuntu-18.04-clang-3.7
            os: ubuntu-18.04
            compiler: clang
            version: "3.7"

          - name: ubuntu-18.04-clang-3.8
            os: ubuntu-18.04
            compiler: clang
            version: "3.8"

          - name: ubuntu-18.04-clang-3.9
            os: ubuntu-18.04
            compiler: clang
            version: "3.9"

          - name: ubuntu-18.04-clang-4.0
            os: ubuntu-18.04
            compiler: clang
            version: "4.0"

          - name: ubuntu-18.04-clang-5.0
            os: ubuntu-18.04
            compiler: clang
            version: "5.0"

          - name: ubuntu-18.04-clang-6.0
            os: ubuntu-18.04
            compiler: clang
            version: "6.0"

          - name: ubuntu-18.04-clang-7
            os: ubuntu-18.04
            compiler: clang
            version: "7"

          - name: ubuntu-18.04-clang-8
            os: ubuntu-18.04
            compiler: clang
            version: "8"

          # fails like this: https://github.com/onqtam/doctest/runs/1562896476?check_suite_focus=true
          # - name: ubuntu-latest-clang-9
          #   os: ubuntu-latest
          #   compiler: clang
          #   version: "9"

          - name: ubuntu-latest-clang-10
            os: ubuntu-latest
            compiler: clang
            version: "10"

          # fails like this: https://github.com/onqtam/doctest/runs/1562896512?check_suite_focus=true
          # - name: ubuntu-latest-clang-11
          #   os: ubuntu-latest
          #   compiler: clang
          #   version: "11"

          - name: windows-2016-cl
            os: windows-2016
            compiler: cl

          - name: windows-2016-clang-cl
            os: windows-2016
            compiler: clang-cl

          - name: windows-2016-clang
            os: windows-2016
            compiler: clang

          - name: windows-2019-cl
            os: windows-2019
            compiler: cl

          # fails to install it...
          # - name: windows-2016-gcc
          #   os: windows-2016
          #   compiler: gcc

          - name: windows-2019-clang-cl
            os: windows-2019
            compiler: clang-cl

          - name: windows-2019-clang
            os: windows-2019
            compiler: clang

          # fails to install it...
          # - name: windows-2019-gcc
          #   os: windows-2019
          #   compiler: gcc

          - name: macOS-latest-xcode-11.3
            os: macOS-10.15
            compiler: xcode
            version: "11.3"

    steps:
      - uses: actions/checkout@v1

      - name: Install (Linux)
        if: runner.os == 'Linux'
        run: |
          # CMake 3.15 allows specifying the generator using the CMAKE_GENERATOR
          # environment variable.
          curl -sSL https://github.com/Kitware/CMake/releases/download/v3.15.4/cmake-3.15.4-Linux-x86_64.tar.gz -o cmake.tar.gz
          sudo tar xf cmake.tar.gz --strip 1 -C /usr/local

          # Required for libc6-dbg:i386 and g++-multilib packages which are
          # needed for x86 builds.
          sudo dpkg --add-architecture i386

          # clang-3.7 and earlier are not available in Bionic anymore so we get
          # them from the Xenial repositories instead.
          sudo add-apt-repository "deb http://dk.archive.ubuntu.com/ubuntu/ xenial main"
          sudo add-apt-repository "deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe"

          # LLVM 9 is not in Bionic's repositories so we add the official LLVM repository.
          if [ "${{ matrix.compiler }}" = "clang" ] && [ "${{ matrix.version }}" = "9" ]; then
            sudo add-apt-repository "deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-9 main"
          fi

          sudo apt-get update
          # sudo apt-get install libopenmpi-dev
          # sudo apt-get install openmpi-bin

          # libc6-dbg:i386 is required for running valgrind on x86.
          sudo apt-get install -y ninja-build valgrind libc6-dbg:i386

          if [ "${{ matrix.compiler }}" = "gcc" ]; then
            sudo apt-get install -y g++-${{ matrix.version }} g++-${{ matrix.version }}-multilib
            echo "::set-env name=CC::gcc-${{ matrix.version }}"
            echo "::set-env name=CXX::g++-${{ matrix.version }}"
          else
            sudo apt-get install -y clang-${{ matrix.version }} g++-multilib
            echo "::set-env name=CC::clang-${{ matrix.version }}"
            echo "::set-env name=CXX::clang++-${{ matrix.version }}"
          fi

      - name: Install (macOS)
        if: runner.os == 'macOS'
        run: |
          brew install cmake ninja

          if [ "${{ matrix.compiler }}" = "gcc" ]; then
            brew install gcc@${{ matrix.version }}
            echo "::set-env name=CC::gcc-${{ matrix.version }}"
            echo "::set-env name=CXX::g++-${{ matrix.version }}"
          else
            ls -ls /Applications/
            sudo xcode-select -switch /Applications/Xcode_${{ matrix.version }}.app
            echo "::set-env name=CC::clang"
            echo "::set-env name=CXX::clang++"
          fi

      - name: Install (Windows)
        if: runner.os == 'Windows'
        shell: powershell
        run: |
          Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
          scoop install ninja --global

          if ("${{ matrix.compiler }}".StartsWith("clang")) {
            scoop install llvm --global
          }

          if ("${{ matrix.compiler }}" -eq "gcc") {
            # Chocolatey GCC is broken on the windows-2019 image.
            # See: https://github.com/DaanDeMeyer/doctest/runs/231595515
            # See: https://github.community/t5/GitHub-Actions/Something-is-wrong-with-the-chocolatey-installed-version-of-gcc/td-p/32413
            scoop install gcc --global
            echo "::set-env name=CC::gcc"
            echo "::set-env name=CXX::g++"
          } elseif ("${{ matrix.compiler }}" -eq "clang") {
            echo "::set-env name=CC::clang"
            echo "::set-env name=CXX::clang++"
          } else {
            echo "::set-env name=CC::${{ matrix.compiler }}"
            echo "::set-env name=CXX::${{ matrix.compiler }}"
          }

          # Scoop modifies the PATH so we make the modified PATH global.
          echo "::set-env name=PATH::$env:PATH"

      - name: Configure ASAN/UBSAN
        if: runner.os == 'Linux' || runner.os == 'macOS'
        run: |
          # https://stackoverflow.com/a/37939589/11900641
          function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }

          # Disable sanitizers in configurations where we know they are buggy.

          # TODO: Move these conditions to the if clause if Github Actions ever
          # adds support for comparing versions.
          # See: https://github.community/t5/GitHub-Actions/Allow-comparing-versions-in-if-conditions/m-p/33912#M1710

          if [ "${{ runner.os }}" = "Linux" ] && \
             [ "${{ matrix.compiler }}" = "gcc" ] && \
             [ $(version ${{ matrix.version }}) -le $(version "5.0") ]; then
            exit 0
          fi

          if [ "${{ runner.os }}" = "Linux" ] && \
             [ "${{ matrix.compiler }}" = "clang" ] && \
             [ $(version ${{ matrix.version }}) -le $(version "6.0") ]; then
            exit 0
          fi

          if [ "${{ runner.os }}" = "macOS" ] && \
             [ "${{ matrix.compiler }}" = "xcode" ] && \
             [ $(version ${{ matrix.version }}) -le $(version "9.4.1") ]; then
            exit 0
          fi

          if [ "${{ runner.os }}" = "macOS" ] && \
             [ "${{ matrix.compiler }}" = "gcc" ]; then
            exit 0
          fi

          ASAN_UBSAN_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"

          # Link statically against ASAN libraries because dynamically linking
          # against ASAN libraries causes problems when using dlopen on Ubuntu.
          # See: https://github.com/DaanDeMeyer/doctest/runs/249002713
          if [ "${{ runner.os }}" = "Linux" ] && [ "${{ matrix.compiler }}" = "gcc" ]; then
            ASAN_UBSAN_FLAGS="$ASAN_UBSAN_FLAGS -static-libasan"
          fi

          # Compiling in bash on Windows doesn't work and powershell doesn't
          # exit on non-zero exit codes so we're forced to use cmd which means
          # we don't have a cross platform way to access environment variables.
          # To circumvent this, we put the sanitizer flags in an environment
          # variable that is automatically picked up by CMake.
          echo "::set-env name=CXXFLAGS::$ASAN_UBSAN_FLAGS"

      - name: Configure TSAN
        if: runner.os == 'Linux' || runner.os == 'macOS'
        run: |
          # https://stackoverflow.com/a/37939589/11900641
          function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }

          if [ "${{ runner.os }}" = "Linux" ] && \
             [ "${{ matrix.compiler }}" = "gcc" ] && \
             [ $(version ${{ matrix.version }}) -le $(version "6.0") ]; then
            exit 0
          fi

          if [ "${{ runner.os }}" = "Linux" ] && \
             [ "${{ matrix.compiler }}" = "clang" ] && \
             [ $(version ${{ matrix.version }}) -le $(version "3.9") ]; then
            exit 0
          fi

          if [ "${{ runner.os }}" = "macOS" ] && \
             [ "${{ matrix.compiler }}" = "gcc" ]; then
            exit 0
          fi

          TSAN_FLAGS="-fsanitize=thread -pie -fPIE"

          if [ "${{ runner.os }}" = "Linux" ] && [ "${{ matrix.compiler }}" = "gcc" ]; then
            TSAN_FLAGS="$TSAN_FLAGS -static-libtsan"
          fi

          # The thread sanitizers build does not run on Windows so we can just
          # use bash syntax to access the TSAN flags in the thread sanitizers
          # build step.
          echo "::set-env name=TSAN_FLAGS::$TSAN_FLAGS"

      - name: Configure x64
        if: runner.os == 'Windows'
        run: .github\workflows\vsenv.bat -arch=x64 -host_arch=x64

      - name: Build & Test Debug x64
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DDOCTEST_TEST_MODE=COMPARE
          cmake --build build
          cd build
          ctest

      - name: Build & Test Release x64
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DDOCTEST_TEST_MODE=COMPARE
          cmake --build build
          cd build
          ctest

      # Valgrind doesn't support the latest macOS versions.
      # `-DCMAKE_CXX_FLAGS=""` overrides CXXFLAGS (disables sanitizers).

      - name: Build & Test Debug x64 Valgrind
        if: runner.os == 'Linux'
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="" -DDOCTEST_TEST_MODE=VALGRIND
          cmake --build build
          cd build
          ctest

      - name: Build & Test Release x64 Valgrind
        if: runner.os == 'Linux'
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="" -DDOCTEST_TEST_MODE=VALGRIND
          cmake --build build
          cd build
          ctest

      - name: Build & Test Debug x64 Thread Sanitizers
        if: runner.os == 'Linux' || runner.os == 'macOS'
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="$TSAN_FLAGS" -DDOCTEST_TEST_MODE=COMPARE
          cmake --build build
          cd build
          ctest

      - name: Build & Test Debug x64 without RTTI
        if: runner.os == 'Linux' || runner.os == 'macOS'
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fno-rtti" -DDOCTEST_TEST_MODE=COMPARE
          cmake --build build
          cd build
          ctest

      - name: Build x64 Debug without exceptions
        if: runner.os == 'Linux' || runner.os == 'macOS'
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fno-exceptions -DDOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS"
          cmake --build build

      # MinGW x86 tests fail on Windows: https://github.com/DaanDeMeyer/doctest/runs/240600881.
      # MacOS doesn't support x86 from Xcode 10 onwards.

      - name: Configure x86
        shell: pwsh
        if: (runner.os == 'Windows' && matrix.compiler != 'gcc') || runner.os == 'Linux'
        run: |
          if ("${{ runner.os }}" -eq "Windows") {
            & .github\workflows\vsenv.bat -arch=x86 -host_arch=x64
          }

          if ("${{ matrix.compiler }}" -notcontains "cl") {
            echo "::set-env name=CXXFLAGS::$env:CXXFLAGS -m32"
          }

      - name: Build & Test Debug x86
        if: (runner.os == 'Windows' && matrix.compiler != 'gcc') || runner.os == 'Linux'
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DDOCTEST_TEST_MODE=COMPARE
          cmake --build build
          cd build
          ctest

      - name: Build & Test Release x86
        if: (runner.os == 'Windows' && matrix.compiler != 'gcc') || runner.os == 'Linux'
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DDOCTEST_TEST_MODE=COMPARE
          cmake --build build
          cd build
          ctest

      - name: Build & Test Debug x86 Valgrind
        if: runner.os == 'Linux'
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-m32" -DDOCTEST_TEST_MODE=VALGRIND
          cmake --build build
          cd build
          ctest

      - name: Build & Test Release x86 Valgrind
        if: runner.os == 'Linux'
        run: |
          cmake -E remove_directory build
          cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-m32" -DDOCTEST_TEST_MODE=VALGRIND
          cmake --build build
          cd build
          ctest
