name: CI

on:
  push:
    branches:
      - main
  pull_request:
    paths-ignore:
      - "node-hub/**"
  workflow_dispatch:

env:
  RUST_LOG: INFO

jobs:
  test:
    name: "Test"
    strategy:
      matrix:
        platform: [ubuntu-latest, macos-latest, windows-latest]
      fail-fast: false
    runs-on: ${{ matrix.platform }}
    timeout-minutes: 60
    steps:
      - name: Print available space (Windows only)
        run: Get-PSDrive
        if: runner.os == 'Windows'
      - name: Override cargo target dir (Windows only)
        run: echo "CARGO_TARGET_DIR=C:\cargo-target" >> "$GITHUB_ENV"
        shell: bash
        if: runner.os == 'Windows'

      - uses: actions/checkout@v3
      - uses: r7kamura/rust-problem-matchers@v1.1.0
      - run: cargo --version --verbose
      - name: Free Disk Space (Ubuntu)
        uses: jlumbroso/free-disk-space@main
        if: runner.os == 'Linux'
        with:
          # this might remove tools that are actually needed,
          # if set to "true" but frees about 6 GB
          tool-cache: false

          # all of these default to true, but feel free to set to
          # "false" if necessary for your workflow
          android: true
          dotnet: true
          haskell: true
          large-packages: false
          docker-images: true
          swap-storage: false
      - name: Free disk Space (Windows)
        if: runner.os == 'Windows'
        run: |
          docker system prune --all -f
          Remove-Item "C:\Android" -Force -Recurse
      - uses: Swatinem/rust-cache@v2
        with:
          cache-provider: buildjet
          cache-on-failure: true
          # only save caches for `main` branch
          save-if: ${{ github.ref == 'refs/heads/main' }}
          cache-directories: ${{ env.CARGO_TARGET_DIR }}

      - name: "Check"
        run: cargo check --all --exclude dora-dav1d --exclude dora-rav1e
      - name: "Build  (Without Python dep as it is build with maturin)"
        run: cargo build --all --exclude dora-dav1d --exclude dora-rav1e --exclude dora-node-api-python --exclude dora-operator-api-python --exclude dora-ros2-bridge-python
      - name: "Test"
        run: cargo test --all --exclude dora-dav1d --exclude dora-rav1e --exclude dora-node-api-python --exclude dora-operator-api-python --exclude dora-ros2-bridge-python

  # Run examples as separate job because otherwise we will exhaust the disk
  # space of the GitHub action runners.
  examples:
    name: "Examples"
    strategy:
      matrix:
        platform: [ubuntu-22.04, macos-latest, windows-latest]
      fail-fast: false
    runs-on: ${{ matrix.platform }}
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v3
      - uses: r7kamura/rust-problem-matchers@v1.1.0
      - run: cargo --version --verbose
      - name: Free Disk Space (Ubuntu)
        uses: jlumbroso/free-disk-space@main
        if: runner.os == 'Linux'
        with:
          # this might remove tools that are actually needed,
          # if set to "true" but frees about 6 GB
          tool-cache: true

          # all of these default to true, but feel free to set to
          # "false" if necessary for your workflow
          android: true
          dotnet: true
          haskell: true
          large-packages: false
          docker-images: true
          swap-storage: true
      - name: Free disk Space (Windows)
        if: runner.os == 'Windows'
        run: |
          docker system prune --all -f
          Remove-Item "C:\Android" -Force -Recurse
      - uses: Swatinem/rust-cache@v2
        with:
          cache-provider: buildjet
          cache-on-failure: true
          # only save caches for `main` branch
          save-if: ${{ github.ref == 'refs/heads/main' }}

      # general examples
      - name: "Build examples"
        timeout-minutes: 30
        run: cargo build --examples
      - name: "Rust Dataflow example"
        timeout-minutes: 30
        run: cargo run --example rust-dataflow
      - name: "Multiple Daemons example"
        timeout-minutes: 30
        run: cargo run --example multiple-daemons
      - name: "C Dataflow example"
        timeout-minutes: 15
        run: cargo run --example c-dataflow
      - name: "C++ Dataflow example"
        timeout-minutes: 15
        run: cargo run --example cxx-dataflow
      - name: "Install Arrow C++ Library"
        timeout-minutes: 10
        shell: bash
        run: |
          if [ "$RUNNER_OS" == "Linux" ]; then
            # For Ubuntu
            sudo apt-get update
            sudo apt-get install -y -V ca-certificates lsb-release wget
            wget https://apache.jfrog.io/artifactory/arrow/$(lsb_release --id --short | tr 'A-Z' 'a-z')/apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
            sudo apt-get install -y -V ./apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
            sudo apt-get update
            sudo apt-get install -y -V libarrow-dev libarrow-glib-dev
          elif [ "$RUNNER_OS" == "macOS" ]; then
            # For macOS
            brew update
            brew install apache-arrow
          fi
      - name: "C++ Dataflow2 example"
        timeout-minutes: 15
        run: cargo run --example cxx-arrow-dataflow
      - name: "Cmake example"
        if: runner.os == 'Linux'
        timeout-minutes: 30
        run: cargo run --example cmake-dataflow
      - name: "Unix Domain Socket example"
        if: runner.os == 'Linux'
        run: cargo run --example rust-dataflow -- dataflow_socket.yml

  # ROS2 bridge examples
  ros2-bridge-examples:
    name: "ROS2 Bridge Examples"
    runs-on: ubuntu-22.04
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@v3
      - uses: r7kamura/rust-problem-matchers@v1.1.0

      - name: Free Disk Space (Ubuntu)
        uses: jlumbroso/free-disk-space@main
        if: runner.os == 'Linux'
        with:
          # this might remove tools that are actually needed,
          # if set to "true" but frees about 6 GB
          tool-cache: false

          # all of these default to true, but feel free to set to
          # "false" if necessary for your workflow
          android: true
          dotnet: true
          haskell: true
          large-packages: false
          docker-images: true
          swap-storage: false

      - run: cargo --version --verbose
      - uses: Swatinem/rust-cache@v2
        with:
          cache-provider: buildjet
          cache-on-failure: true
          # only save caches for `main` branch
          save-if: ${{ github.ref == 'refs/heads/main' }}

      - uses: ros-tooling/setup-ros@v0.7
        with:
          required-ros-distributions: humble
      - run: 'source /opt/ros/humble/setup.bash && echo AMENT_PREFIX_PATH=${AMENT_PREFIX_PATH} >> "$GITHUB_ENV"'
      - name: Install the latest version of uv
        uses: astral-sh/setup-uv@v5
        with:
          enable-cache: true
      - name: Install pyarrow
        run: pip install pyarrow
      - name: "Test"
        run: cargo test -p dora-ros2-bridge-python
      - name: "Rust ROS2 Bridge example"
        timeout-minutes: 30
        env:
          QT_QPA_PLATFORM: offscreen
        run: |
          source /opt/ros/humble/setup.bash && ros2 run turtlesim turtlesim_node &
          source /opt/ros/humble/setup.bash && ros2 run examples_rclcpp_minimal_service service_main &
          cargo run --example rust-ros2-dataflow --features="ros2-examples"
      - uses: actions/setup-python@v2
        if: runner.os != 'Windows'
        with:
          python-version: "3.8"
      - uses: actions/setup-python@v2
        if: runner.os == 'Windows'
        with:
          python-version: "3.10"
      - name: "python-ros2-dataflow"
        timeout-minutes: 30
        env:
          QT_QPA_PLATFORM: offscreen
        run: |
          # Reset only the turtlesim instance as it is not destroyed at the end of the previous job
          source /opt/ros/humble/setup.bash && ros2 service call /reset std_srvs/srv/Empty &
          cargo run --example python-ros2-dataflow --features="ros2-examples"
      - name: "c++-ros2-dataflow"
        timeout-minutes: 30
        env:
          QT_QPA_PLATFORM: offscreen
        run: |
          # Reset only the turtlesim instance as it is not destroyed at the end of the previous job
          source /opt/ros/humble/setup.bash && ros2 service call /reset std_srvs/srv/Empty &
          cargo run --example cxx-ros2-dataflow --features="ros2-examples"

  bench:
    name: "Bench"
    strategy:
      matrix:
        platform: [ubuntu-latest, macos-latest, windows-latest]
      fail-fast: false
    runs-on: ${{ matrix.platform }}
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v3
      - uses: r7kamura/rust-problem-matchers@v1.1.0
      - run: cargo --version --verbose
      - uses: Swatinem/rust-cache@v2
        with:
          cache-provider: buildjet
          cache-on-failure: true
          # only save caches for `main` branch
          save-if: ${{ github.ref == 'refs/heads/main' }}

      - name: "Benchmark example"
        timeout-minutes: 30
        run: cargo run --example benchmark --release

  CLI:
    name: "CLI Test"
    strategy:
      matrix:
        platform: [ubuntu-latest, macos-latest, windows-latest]
      fail-fast: false
    runs-on: ${{ matrix.platform }}
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v3
      - uses: r7kamura/rust-problem-matchers@v1.1.0
      - run: cargo --version --verbose
      - name: Free Disk Space (Ubuntu)
        uses: jlumbroso/free-disk-space@main
        if: runner.os == 'Linux'
        with:
          # this might remove tools that are actually needed,
          # if set to "true" but frees about 6 GB
          tool-cache: true

          # all of these default to true, but feel free to set to
          # "false" if necessary for your workflow
          android: true
          dotnet: true
          haskell: true
          large-packages: false
          docker-images: true
          swap-storage: true
      - uses: Swatinem/rust-cache@v2
        with:
          cache-provider: buildjet
          cache-on-failure: true
          # only save caches for `main` branch
          save-if: ${{ github.ref == 'refs/heads/main' }}

      # CLI tests
      - name: "Build cli and binaries"
        timeout-minutes: 45
        # fail-fast by using bash shell explictly
        shell: bash
        run: |
          cargo install --path binaries/cli --locked
      - name: "Test CLI (Rust)"
        timeout-minutes: 45
        # fail-fast by using bash shell explictly
        shell: bash
        run: |
          # Test Rust template Project
          dora new test_rust_project --internal-create-with-path-dependencies
          cd test_rust_project
          cargo build --all --exclude dora-dav1d --exclude dora-rav1e
          dora up
          dora list
          dora start dataflow.yml --name ci-rust-test --detach
          sleep 10
          dora stop --name ci-rust-test  --grace-duration 5s
          cd ..
          dora build examples/rust-dataflow/dataflow_dynamic.yml
          dora start examples/rust-dataflow/dataflow_dynamic.yml --name ci-rust-dynamic --detach
          cargo run -p rust-dataflow-example-sink-dynamic
          sleep 5
          dora stop --name ci-rust-dynamic  --grace-duration 5s
          dora destroy

      - uses: actions/setup-python@v2
        with:
          # TODO: Support Python 3.13 when https://github.com/pytorch/pytorch/issues/130249 is fixed
          python-version: "3.12"

      - name: Install the latest version of uv
        uses: astral-sh/setup-uv@v5
        with:
          enable-cache: true

      - name: "Test CLI (Python)"
        timeout-minutes: 45
        # fail-fast by using bash shell explictly
        shell: bash
        run: |
          # Test Python template Project
          dora new test_python_project --lang python --internal-create-with-path-dependencies
          cd test_python_project
          uv venv --seed -p 3.11
          uv pip install -e ../apis/python/node
          dora build dataflow.yml --uv
          uv pip install ruff pytest

          # Check Compliancy
          uv run ruff check .
          uv run pytest

          export OPERATING_MODE=SAVE
          dora up
          dora list
          dora build dataflow.yml --uv
          echo "Running CI Python Test"
          dora start dataflow.yml --name ci-python-test --detach --uv
          sleep 10
          dora stop --name ci-python-test  --grace-duration 5s
          dora destroy

          cd ..

          # Run Python Node Example
          echo "Running Python Node Example"
          dora up
          uv venv --seed -p 3.11
          uv pip install -e apis/python/node
          dora build examples/python-dataflow/dataflow.yml --uv
          dora start examples/python-dataflow/dataflow.yml --name ci-python --detach --uv
          sleep 10
          dora stop --name ci-python  --grace-duration 30s

          # Run Python Dynamic Node Example
          echo "Running Python Node Dynamic Example"
          dora build examples/python-dataflow/dataflow_dynamic.yml --uv
          dora start examples/python-dataflow/dataflow_dynamic.yml --name ci-python-dynamic --detach --uv
          uv run opencv-plot --name plot
          sleep 10
          dora stop --name ci-python-dynamic  --grace-duration 30s

          # Run Python Operator Example
          echo "Running CI Operator Test"
          dora start examples/python-operator-dataflow/dataflow.yml --name ci-python-operator --detach --uv
          sleep 10
          dora stop --name ci-python-operator  --grace-duration 30s

          dora destroy

          # Run Python queue latency test
          echo "Running CI Queue Latency Test"
          dora run tests/queue_size_latest_data_python/dataflow.yaml --uv

          # Run Python queue latency test + timeout
          echo "Running CI Queue + Timeout Test"
          dora run tests/queue_size_and_timeout_python/dataflow.yaml --uv

          # Run Rust queue latency test
          echo "Running CI Queue Size Latest Data Rust Test"
          dora build tests/queue_size_latest_data_rust/dataflow.yaml --uv
          dora run tests/queue_size_latest_data_rust/dataflow.yaml --uv

      - name: "Test CLI (C)"
        timeout-minutes: 45
        # fail-fast by using bash shell explictly
        shell: bash
        if: runner.os == 'Linux'
        run: |
          # Test C template Project
          dora new test_c_project --lang c --internal-create-with-path-dependencies
          cd test_c_project
          dora up
          dora list
          cmake -B build
          cmake --build build
          cmake --install build
          dora start dataflow.yml --name ci-c-test --detach
          sleep 10
          dora stop --name ci-c-test  --grace-duration 5s
          dora destroy

      - name: "Test CLI (C++)"
        timeout-minutes: 45
        # fail-fast by using bash shell explictly
        shell: bash
        if: runner.os == 'Linux'
        run: |
          # Test C++ template Project
          dora new test_cxx_project --lang cxx --internal-create-with-path-dependencies
          cd test_cxx_project
          dora up
          dora list
          cmake -B build
          cmake --build build
          cmake --install build
          dora start dataflow.yml --name ci-cxx-test --detach
          sleep 10
          dora stop --name ci-cxx-test  --grace-duration 5s
          dora destroy

  clippy:
    name: "Clippy"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: r7kamura/rust-problem-matchers@v1.1.0
      - run: cargo --version --verbose

      - name: "Clippy"
        run: cargo clippy --all --exclude dora-dav1d --exclude dora-rav1e
      - name: "Clippy (tracing feature)"
        run: cargo clippy --all --exclude dora-dav1d --exclude dora-rav1e --features tracing
        if: false # only the dora-runtime has this feature, but it is currently commented out
      - name: "Clippy (metrics feature)"
        run: cargo clippy --all --exclude dora-dav1d --exclude dora-rav1e --features metrics
        if: false # only the dora-runtime has this feature, but it is currently commented out

  rustfmt:
    name: "Formatting"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: r7kamura/rust-problem-matchers@v1.1.0
      - name: "rustfmt"
        run: cargo fmt --all -- --check

  check-license:
    name: "License Checks"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: r7kamura/rust-problem-matchers@v1.1.0
      - run: cargo --version --verbose
      - uses: Swatinem/rust-cache@v2
        with:
          cache-provider: buildjet
          cache-on-failure: true
          # only save caches for `main` branch
          save-if: ${{ github.ref == 'refs/heads/main' }}

      - run: cargo install cargo-lichking
      - name: "Check dependency licenses"
        run: cargo lichking check

  typos:
    name: Typos
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions-rs/toolchain@v1
        with:
          profile: minimal
          toolchain: stable
          override: true
      - name: Typos check with custom config file
        uses: crate-ci/typos@master

  cross-check:
    runs-on: ${{ matrix.platform.runner }}
    strategy:
      matrix:
        platform:
          - runner: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
          - runner: ubuntu-22.04
            target: i686-unknown-linux-gnu
          - runner: ubuntu-22.04
            target: aarch64-unknown-linux-gnu
          - runner: ubuntu-22.04
            target: aarch64-unknown-linux-musl
          - runner: ubuntu-22.04
            target: armv7-unknown-linux-musleabihf
          - runner: ubuntu-22.04
            target: x86_64-pc-windows-gnu
          - runner: macos-13
            target: aarch64-apple-darwin
          - runner: macos-13
            target: x86_64-apple-darwin
      fail-fast: false
    steps:
      - uses: actions/checkout@v3
      - uses: r7kamura/rust-problem-matchers@v1.1.0
      - name: "Add toolchains"
        run: rustup target add ${{ matrix.platform.target }}
      - name: Install system-level dependencies
        if: runner.target == 'x86_64-pc-windows-gnu'
        run: |
          sudo apt install g++-mingw-w64-x86-64 gcc-mingw-w64-x86-64
      - name: "Check"
        uses: actions-rs/cargo@v1
        with:
          use-cross: true
          command: check
          args: --target  ${{ matrix.platform.target }} --all --exclude dora-dav1d --exclude dora-rav1e --exclude dora-node-api-python --exclude dora-operator-api-python --exclude dora-ros2-bridge-python
