name: Continuous Integration

on: [push, pull_request]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-22.04
    steps:
      - name: Install
        run: sudo apt-get install -y clang-format
      - name: Checkout
        uses: actions/checkout@v4
      - name: Symlinks
        run: find * -type l -printf "::error::%p is a symlink. This is forbidden by the Arduino Library Specification." -exec false {} +
      - name: Clang-format
        run: |
          find src/ extras/ -name '*.[ch]pp' | xargs clang-format -i --verbose --style=file
          git diff --exit-code
      - name: Check URLs
        run: |
          grep -hREo "(http|https)://[a-zA-Z0-9./?=_%:-]*" src/ | sort -u | while read -r URL
          do
            STATUS=$(curl -s -o /dev/null -I -w "%{http_code}" "$URL")
            [ "$STATUS" -ge 400 ] && echo "::warning title=HTTP $STATUS::$URL returned $STATUS"
          done || true

  gcc:
    name: GCC
    needs: lint
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false
      matrix:
        include:
          - gcc: "4.8"
          - gcc: "5"
          - gcc: "6"
          - gcc: "7"
            cxxflags: -fsanitize=leak -fno-sanitize-recover=all
          - gcc: "8"
            cxxflags: -fsanitize=undefined -fno-sanitize-recover=all
          - gcc: "9"
            cxxflags: -fsanitize=address -fno-sanitize-recover=all
          - gcc: "10"
            cxxflags: -funsigned-char # Issue #1715
          - gcc: "11"
          - gcc: "12"
    steps:
      - name: Workaround for actions/runner-images#9491
        run: sudo sysctl vm.mmap_rnd_bits=28

      - name: Install
        run: |
          sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5 3B4FE6ACC0B21F32
          sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ xenial main universe'
          sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ bionic main universe'
          sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ focal main universe'
          sudo apt-get update
          sudo apt-get install -y gcc-${{ matrix.gcc }} g++-${{ matrix.gcc }}
        timeout-minutes: 5

      - name: Checkout
        uses: actions/checkout@v4
        timeout-minutes: 1

      - name: Configure
        run: cmake -DCMAKE_BUILD_TYPE=Debug .
        env:
          CC: gcc-${{ matrix.gcc }}
          CXX: g++-${{ matrix.gcc }}
          CXXFLAGS: ${{ matrix.cxxflags }}
        timeout-minutes: 1

      - name: Build
        run: cmake --build .
        timeout-minutes: 10

      - name: Test
        run: ctest --output-on-failure -C Debug .
        env:
          UBSAN_OPTIONS: print_stacktrace=1
        timeout-minutes: 2

  clang:
    name: Clang
    needs: lint
    strategy:
      fail-fast: false
      matrix:
        include:
          - clang: "3.9"
            runner: ubuntu-20.04
            archive: bionic
          - clang: "4.0"
            runner: ubuntu-20.04
            archive: bionic
          - clang: "5.0"
            runner: ubuntu-20.04
            archive: bionic
          - clang: "6.0"
            runner: ubuntu-20.04
            archive: bionic
          - clang: "7"
            runner: ubuntu-20.04
          - clang: "8"
            cxxflags: -fsanitize=leak -fno-sanitize-recover=all
            runner: ubuntu-20.04
          - clang: "9"
            cxxflags: -fsanitize=undefined -fno-sanitize-recover=all
            runner: ubuntu-20.04
          - clang: "10"
            cxxflags: -fsanitize=address -fno-sanitize-recover=all
            runner: ubuntu-20.04
          - clang: "11"
            runner: ubuntu-22.04
          - clang: "12"
            runner: ubuntu-22.04
          - clang: "13"
            runner: ubuntu-22.04
          - clang: "14"
            runner: ubuntu-22.04
          - clang: "15"
            runner: ubuntu-22.04
    runs-on: ${{ matrix.runner }}
    steps:
      - name: Add archive repositories
        if: matrix.archive
        run: |
          sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ ${{ matrix.archive }} main'
          sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ ${{ matrix.archive }} universe'
      - name: Install Clang ${{ matrix.clang }}
        run: |
          sudo apt-get update
          sudo apt-get install -y clang-${{ matrix.clang }}
      - name: Install libc++ ${{ matrix.clang }}
        if: matrix.clang >= 11
        run: sudo apt-get install -y libc++-${{ matrix.clang }}-dev libc++abi-${{ matrix.clang }}-dev
      - name: Install libunwind ${{ matrix.clang }}
        if: matrix.clang == 12  # dependency is missing in Ubuntu 22.04
        run: sudo apt-get install -y libunwind-${{ matrix.clang }}-dev
      - name: Checkout
        uses: actions/checkout@v4
      - name: Configure
        run: cmake -DCMAKE_BUILD_TYPE=Debug .
        env:
          CC: clang-${{ matrix.clang }}
          CXX: clang++-${{ matrix.clang }}
          CXXFLAGS: >-
            ${{ matrix.cxxflags }}
            ${{ matrix.clang < 11 && '-I/usr/lib/llvm-10/include/c++/v1/' || '' }}
      - name: Build
        run: cmake --build .
      - name: Test
        run: ctest --output-on-failure -C Debug .
        env:
          UBSAN_OPTIONS: print_stacktrace=1

  conf_test:
    name: Test configuration on Linux
    needs: [gcc, clang]
    runs-on: ubuntu-20.04
    steps:
      - name: Install
        run: |
          sudo apt-get update
          sudo apt-get install -y g++-multilib gcc-avr avr-libc
      - name: Checkout
        uses: actions/checkout@v4
      - name: AVR
        run: avr-g++ -std=c++11 -Isrc extras/conf_test/avr.cpp
      - name: GCC 32-bit
        run: g++ -std=c++11 -m32 -Isrc extras/conf_test/x86.cpp
      - name: GCC 64-bit
        run: g++ -std=c++11 -m64 -Isrc extras/conf_test/x64.cpp
      - name: Clang 32-bit
        run: clang++ -std=c++11 -m32 -Isrc extras/conf_test/x86.cpp
      - name: Clang 64-bit
        run: clang++ -std=c++11 -m64 -Isrc extras/conf_test/x64.cpp

  conf_test_windows:
    name: Test configuration on Windows
    runs-on: windows-2019
    needs: [gcc, clang]
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: 32-bit
        run: |
          call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars32.bat"
          cl /Isrc extras/conf_test/x86.cpp
        shell: cmd
      - name: 64-bit
        run: |
          call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
          cl /Isrc extras/conf_test/x64.cpp
        shell: cmd

  xcode:
    name: XCode
    needs: clang
    runs-on: macos-13
    strategy:
      fail-fast: false
      matrix:
        include:
          - xcode: "14.1"
          - xcode: "14.2"
          - xcode: "14.3.1"
          - xcode: "15.0.1"
          - xcode: "15.1"
          - xcode: "15.2"
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Select XCode version
        run: sudo xcode-select --switch /Applications/Xcode_${{ matrix.xcode }}.app
      - name: Configure
        run: cmake -DCMAKE_BUILD_TYPE=Debug .
      - name: Build
        run: cmake --build .
      - name: Test
        run: ctest --output-on-failure -C Debug .

  # DISABLED: Running on AppVeyor instead because it supports older versions of the compiler
  # msvc:
  #   name: Visual Studio
  #   strategy:
  #     fail-fast: false
  #     matrix:
  #       include:
  #         - os: windows-2016
  #         - os: windows-2019
  #   runs-on: ${{ matrix.os }}
  #   steps:
  #     - name: Checkout
  #       uses: actions/checkout@v4
  #     - name: Configure
  #       run: cmake -DCMAKE_BUILD_TYPE=Debug .
  #     - name: Build
  #       run: cmake --build .
  #     - name: Test
  #       run: ctest --output-on-failure -C Debug .

  arduino:
    name: Arduino
    needs: gcc
    strategy:
      fail-fast: false
      matrix:
        include:
          - core: arduino:avr
            board: arduino:avr:uno
          - core: arduino:samd
            board: arduino:samd:mkr1000
    runs-on: ubuntu-20.04
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Install arduino-cli
        run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
      - name: Install core
        run: arduino-cli core install ${{ matrix.core }}
      - name: Install libraries
        run: arduino-cli lib install SD Ethernet
      - name: Build JsonConfigFile
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/JsonConfigFile/JsonConfigFile.ino"
      - name: Build JsonFilterExample
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/JsonFilterExample/JsonFilterExample.ino"
      - name: Build JsonGeneratorExample
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/JsonGeneratorExample/JsonGeneratorExample.ino"
      - name: Build JsonHttpClient
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/JsonHttpClient/JsonHttpClient.ino"
      - name: Build JsonParserExample
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/JsonParserExample/JsonParserExample.ino"
      - name: Build JsonServer
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/JsonServer/JsonServer.ino"
      - name: Build JsonUdpBeacon
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/JsonUdpBeacon/JsonUdpBeacon.ino"
      - name: Build MsgPackParser
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/MsgPackParser/MsgPackParser.ino"
      - name: Build ProgmemExample
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/ProgmemExample/ProgmemExample.ino"
      - name: Build StringExample
        run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/StringExample/StringExample.ino"

  platformio:
    name: PlatformIO
    needs: gcc
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: atmelavr
            board: leonardo
            libraries:
              - SD
              - Ethernet
            conf_test: avr
          - platform: espressif8266
            board: huzzah
            conf_test: esp8266
          - platform: espressif32
            board: esp32dev
            libraries:
              - Ethernet
            conf_test: esp8266
          - platform: atmelsam
            board: mkr1000USB
            libraries:
              - SD
              - Ethernet
            conf_test: esp8266
          - platform: teensy
            board: teensy31
            conf_test: esp8266
          - platform: ststm32
            board: adafruit_feather_f405
            libraries:
              - SD
              - Ethernet
            conf_test: esp8266
          - platform: nordicnrf52
            board: adafruit_feather_nrf52840
            libraries:
              - SD
              - Ethernet
            conf_test: esp8266
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set up cache for pip
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip
      - name: Set up Python 3.x
        uses: actions/setup-python@v5
        with:
          python-version: "3.x"
      - name: Install PlatformIO
        run: pip install platformio
      - name: Install adafruit-nrfutil
        if: ${{ matrix.platform == 'nordicnrf52' }}
        run: pip install adafruit-nrfutil
      - name: Include Adafruit_TinyUSB.h # https://github.com/adafruit/Adafruit_nRF52_Arduino/issues/653
        if: ${{ matrix.platform == 'nordicnrf52' }}
        run: find examples/ -name '*.ino' -exec sed -i 's/\(#include <ArduinoJson.h>\)/\1\n#include <Adafruit_TinyUSB.h>/' {} +
      - name: Set up cache for platformio
        uses: actions/cache@v4
        with:
          path: ~/.platformio
          key: ${{ runner.os }}-platformio-${{ matrix.platform }}
      - name: Install platform "${{ matrix.platform }}"
        run: platformio platform install ${{ matrix.platform }}
      - name: Install libraries
        if: ${{ matrix.libraries }}
        run: platformio lib install arduino-libraries/${{ join(matrix.libraries, ' arduino-libraries/') }}
      - name: Test configuration
        run: platformio ci "extras/conf_test/${{ matrix.conf_test }}.cpp" -l '.' -b ${{ matrix.board }}
        if: ${{ matrix.conf_test }}
      - name: Build JsonConfigFile
        run: platformio ci "examples/JsonConfigFile/JsonConfigFile.ino" -l '.' -b ${{ matrix.board }}
      - name: Build JsonFilterExample
        run: platformio ci "examples/JsonFilterExample/JsonFilterExample.ino" -l '.' -b ${{ matrix.board }}
      - name: Build JsonGeneratorExample
        run: platformio ci "examples/JsonGeneratorExample/JsonGeneratorExample.ino" -l '.' -b ${{ matrix.board }}
      - name: Build JsonHttpClient
        run: platformio ci "examples/JsonHttpClient/JsonHttpClient.ino" -l '.' -b ${{ matrix.board }}
      - name: Build JsonParserExample
        run: platformio ci "examples/JsonParserExample/JsonParserExample.ino" -l '.' -b ${{ matrix.board }}
      - name: Build JsonServer
        if: ${{ matrix.platform != 'espressif32' }}
        run: platformio ci "examples/JsonServer/JsonServer.ino" -l '.' -b ${{ matrix.board }}
      - name: Build JsonUdpBeacon
        run: platformio ci "examples/JsonUdpBeacon/JsonUdpBeacon.ino" -l '.' -b ${{ matrix.board }}
      - name: Build MsgPackParser
        run: platformio ci "examples/MsgPackParser/MsgPackParser.ino" -l '.' -b ${{ matrix.board }}
      - name: Build ProgmemExample
        run: platformio ci "examples/ProgmemExample/ProgmemExample.ino" -l '.' -b ${{ matrix.board }}
      - name: Build StringExample
        run: platformio ci "examples/StringExample/StringExample.ino" -l '.' -b ${{ matrix.board }}
      - name: PlatformIO prune
        if: ${{ always() }}
        run: platformio system prune -f

  particle:
    name: Particle
    needs: gcc
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    strategy:
      fail-fast: false
      matrix:
        include:
          - board: argon
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Install Particle CLI
        run: sudo npm install -g particle-cli
      - name: Login to Particle
        run: particle login -t "${{ secrets.PARTICLE_TOKEN }}"
      - name: Compile
        run: extras/ci/particle.sh ${{ matrix.board }}

  arm:
    name: GCC for ARM processor
    needs: gcc
    runs-on: ubuntu-20.04
    steps:
      - name: Install
        run: |
          sudo apt-get update
          sudo apt-get install -y g++-arm-linux-gnueabihf
      - name: Checkout
        uses: actions/checkout@v4
      - name: Configure
        run: cmake .
        env:
          CC: arm-linux-gnueabihf-gcc
          CXX: arm-linux-gnueabihf-g++
      - name: Build
        run: cmake --build .

  coverage:
    needs: gcc
    name: Coverage
    runs-on: ubuntu-20.04
    steps:
      - name: Install
        run: sudo apt-get install -y lcov ninja-build
      - name: Checkout
        uses: actions/checkout@v4
      - name: Configure
        run: cmake -G Ninja -DCOVERAGE=true .
      - name: Build
        run: ninja
      - name: Test
        run: ctest --output-on-failure -LE 'WillFail|Fuzzing' -T test
      - name: lcov --capture
        run: lcov --capture --no-external --directory . --output-file coverage.info
      - name: lcov --remove
        run: lcov --remove coverage.info "$(pwd)/extras/*" --output-file coverage_filtered.info
      - name: genhtml
        run: mkdir coverage && genhtml coverage_filtered.info -o coverage -t ArduinoJson
      - name: Upload HTML report
        uses: actions/upload-artifact@v4
        with:
          name: Coverage report
          path: coverage
      - name: Upload to Coveralls
        uses: coverallsapp/github-action@v2
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          path-to-lcov: coverage_filtered.info

  valgrind:
    needs: gcc
    name: Valgrind
    runs-on: ubuntu-20.04
    steps:
      - name: Install
        run: |
          sudo apt-get update
          sudo apt-get install -y valgrind ninja-build
      - name: Checkout
        uses: actions/checkout@v4
      - name: Configure
        run: cmake -G Ninja -D MEMORYCHECK_COMMAND_OPTIONS="--error-exitcode=1 --leak-check=full"  .
      - name: Build
        run: ninja
      - name: Memcheck
        run: ctest --output-on-failure -LE WillFail -T memcheck
        id: memcheck
      - name: MemoryChecker.*.log
        run: cat Testing/Temporary/MemoryChecker.*.log > $GITHUB_STEP_SUMMARY
        if: failure()

  clang-tidy:
    needs: clang
    name: Clang-Tidy
    runs-on: ubuntu-20.04
    steps:
      - name: Install
        run: sudo apt-get install -y clang-tidy cmake ninja-build
      - name: Checkout
        uses: actions/checkout@v4
      - name: Configure
        run: cmake -G Ninja -DCMAKE_CXX_CLANG_TIDY="clang-tidy-10;--warnings-as-errors=*" -DCMAKE_BUILD_TYPE=Debug .
        env:
          CC: clang-10
          CXX: clang++-10
      - name: Check
        run: cmake --build . -- -k 0

  amalgamate:
    needs: gcc
    name: Amalgamate ArduinoJson.h
    runs-on: ubuntu-20.04
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup
        run: |
          if [[ $GITHUB_REF == refs/tags/* ]]; then
            VERSION=${GITHUB_REF#refs/tags/}
          else
            VERSION=${GITHUB_SHA::7}
          fi
          echo "ARDUINOJSON_H=ArduinoJson-$VERSION.h" >> $GITHUB_ENV
          echo "ARDUINOJSON_HPP=ArduinoJson-$VERSION.hpp" >> $GITHUB_ENV
      - name: Amalgamate ArduinoJson.h
        run: extras/scripts/build-single-header.sh "src/ArduinoJson.h" "$ARDUINOJSON_H"
      - name: Amalgamate ArduinoJson.hpp
        run: extras/scripts/build-single-header.sh "src/ArduinoJson.hpp" "$ARDUINOJSON_HPP"
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: Single headers
          path: |
            ${{ env.ARDUINOJSON_H }}
            ${{ env.ARDUINOJSON_HPP }}
      - name: Smoke test ArduinoJson.h
        run: |
          g++ -x c++ - <<END
          #include "$ARDUINOJSON_H"
          int main() {
            JsonDocument doc;
            deserializeJson(doc, "{}");
          }
          END
      - name: Smoke test ArduinoJson.hpp
        run: |
          g++ -x c++ - <<END
          #include "$ARDUINOJSON_HPP"
          int main() {
            ArduinoJson::JsonDocument doc;
            deserializeJson(doc, "{}");
          }
          END

  esp-idf:
    needs: gcc
    name: ESP-IDF
    runs-on: ubuntu-latest
    steps:
      - name: Setup cache
        uses: actions/cache@v4
        with:
          path: ~/.espressif
          key: ${{ runner.os }}-esp-idf
      - name: Checkout ArduinoJson
        uses: actions/checkout@v4
      - name: Checkout ESP-IDF
        uses: actions/checkout@v4
        with:
          repository: espressif/esp-idf
          path: esp-idf
          submodules: true
      - name: Install ESP-IDF
        run: ./esp-idf/install.sh
      - name: Add component
        # NOTE: we cannot commit the symlink because the Arduino Library Specification forbids it.
        run: |
          mkdir -p extras/ci/espidf/components
          ln -s $PWD extras/ci/espidf/components/ArduinoJson
      - name: Build example
        run: |
          source esp-idf/export.sh
          cd extras/ci/espidf
          idf.py build

  codeql:
    name: CodeQL
    runs-on: ubuntu-20.04
    needs: gcc

    permissions:
      actions: read
      contents: read
      security-events: write

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: cpp

    - name: Build
      run: |
        cmake -DCMAKE_BUILD_TYPE=Debug .
        cmake --build .

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3
      with:
        category: "/language:cpp"
