edgefirst-schemas 3.3.0

Message schemas for EdgeFirst Perception - ROS2 Common Interfaces, Foxglove, and custom types
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
name: Test

on:
  push:
    branches:
      - '**'
  pull_request:
    types: [opened, synchronize, reopened]

env:
  CARGO_TERM_COLOR: always

jobs:
  # ============================================================================
  # Version Sync Check (fast, runs first)
  # ============================================================================
  version-check:
    name: Version Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

      - name: Check version synchronization
        run: bash .github/scripts/check_version_sync.sh

  # ============================================================================
  # Rust Format Check (fast, runs in parallel)
  # ============================================================================
  format:
    name: Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

      - name: Install Rust
        run: |
          rustup toolchain install stable --profile minimal
          rustup component add rustfmt

      - name: Check formatting
        run: cargo fmt -- --check

  # ============================================================================
  # Rust and C Tests with Combined Coverage
  # ============================================================================
  # Both Rust unit tests and C API tests run in the same job to properly merge
  # coverage data. When tests run in separate jobs, profraw files are generated
  # separately and cannot be merged by cargo-llvm-cov.
  rust-and-c-test:
    name: Rust & C Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0  # Full history for SonarCloud blame
          lfs: true

      - name: Install Rust toolchain
        run: |
          rustup toolchain install stable --profile minimal
          rustup component add llvm-tools-preview clippy

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@0bc4cd8a3e21db4cb9519857377b0c8c5e150de5 # v2.67.2
        with:
          tool: cargo-llvm-cov

      - name: Install cargo-nextest
        uses: taiki-e/install-action@0bc4cd8a3e21db4cb9519857377b0c8c5e150de5 # v2.67.2
        with:
          tool: nextest

      - name: Install Criterion test framework
        run: |
          sudo apt-get update
          sudo apt-get install -y libcriterion-dev

      - name: Run Clippy
        run: cargo clippy --all-targets --all-features -- -D warnings

      - name: Generate Clippy report for SonarCloud
        run: |
          # Generate JSON report for SonarCloud Rust analysis
          cargo clippy --all-targets --all-features --message-format=json > clippy-report.json 2>&1 || true
          echo "Clippy report generated: $(wc -l < clippy-report.json) lines"

      - name: Run Rust unit tests with coverage
        run: |
          # Run Rust tests with coverage instrumentation using cargo-llvm-cov
          # The --no-report flag delays report generation so we can add C test coverage
          cargo llvm-cov nextest --all-features --workspace --profile ci --no-report
          echo "=== Rust unit tests completed ==="

      - name: Run Rust doc tests
        run: |
          # Run doc tests separately (nextest doesn't support doc tests)
          # Note: doc tests don't contribute to llvm-cov coverage
          cargo test --doc

      - name: Set up C test coverage environment
        run: |
          # Get the coverage environment from llvm-cov to run C tests with same profraw location
          # This ensures C test coverage is accumulated with Rust test coverage
          source <(cargo llvm-cov show-env --export-prefix)
          # Update profile path to write to llvm-cov-target directory
          export LLVM_PROFILE_FILE="${PWD}/target/llvm-cov-target/schemas-c-%p-%m.profraw"
          echo "LLVM_PROFILE_FILE=$LLVM_PROFILE_FILE" >> $GITHUB_ENV
          echo "C tests will write profraw to: $LLVM_PROFILE_FILE"

      - name: Run C API tests
        run: |
          # Copy instrumented library to where Makefile/gcc expect it
          # This overwrites any non-instrumented library that might exist
          mkdir -p target/debug/deps
          cp -f target/llvm-cov-target/debug/deps/libedgefirst_schemas.* target/debug/deps/ 2>/dev/null || true
          cp -f target/llvm-cov-target/debug/deps/libedgefirst_schemas.* target/debug/ 2>/dev/null || true

          # Create SONAME symlink so the runtime linker can find the library.
          # build.rs sets SONAME to libedgefirst_schemas.so.<major>, but cargo
          # produces the file as libedgefirst_schemas.so — the symlink bridges this.
          MAJOR=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\([0-9]*\).*/\1/')
          ln -sf libedgefirst_schemas.so target/debug/deps/libedgefirst_schemas.so.${MAJOR}

          # Verify we have the instrumented library (check for profile symbols)
          echo "Verifying instrumented library:"
          nm target/debug/deps/libedgefirst_schemas.so 2>/dev/null | grep -c "__llvm_profile" || \
          nm target/debug/deps/libedgefirst_schemas.dylib 2>/dev/null | grep -c "__llvm_profile" || \
          echo "Warning: Library may not be instrumented"
          
          # Build C tests without rebuilding the library (lib target would overwrite instrumented library)
          # Build test binaries directly
          mkdir -p build build/test-results
          for src in tests/c/test_*.c; do
            name=$(basename $src .c)
            echo "Compiling $name..."
            gcc -Wall -Wextra -Werror -std=c11 -I./include -I/usr/include/criterion \
                -o build/$name $src \
                -L./target/debug/deps -ledgefirst_schemas -lcriterion -lm \
                -Wl,-rpath,./target/debug/deps
          done
          
          # Run C tests
          echo "Running C test suite..."
          for test in build/test_*; do
            name=$(basename $test)
            echo "Running $name..."
            ./$test --output=xml:build/test-results/$name.xml || exit 1
          done
          
          echo "=== C API tests completed ==="
          echo "Profraw files in coverage directory:"
          find target/llvm-cov-target -name "*.profraw" | wc -l

      - name: Generate combined coverage report
        run: |
          # Generate single LCOV report from all accumulated profraw files
          # This includes coverage from both Rust unit tests and C API tests
          cargo llvm-cov report --lcov --output-path lcov.info
          
          # Convert absolute paths to relative for SonarCloud compatibility
          sed -i "s|SF:$PWD/|SF:|g" lcov.info
          
          echo "=== Combined Coverage Summary ==="
          if [ -f lcov.info ]; then
            RUST_FILES=$(grep -c "^SF:" lcov.info || echo "0")
            echo "Source files with coverage data: $RUST_FILES"
          fi

      - name: Generate coverage summary
        id: coverage
        run: |
          TOTAL_LINES=$(grep -E "^LF:" lcov.info 2>/dev/null | cut -d: -f2 | paste -sd+ | bc 2>/dev/null || echo "0")
          COVERED_LINES=$(grep -E "^LH:" lcov.info 2>/dev/null | cut -d: -f2 | paste -sd+ | bc 2>/dev/null || echo "0")
          if [ -z "$TOTAL_LINES" ]; then TOTAL_LINES=0; fi
          if [ -z "$COVERED_LINES" ]; then COVERED_LINES=0; fi
          if [ "$TOTAL_LINES" -gt 0 ]; then
            COVERAGE=$(echo "scale=2; $COVERED_LINES * 100 / $TOTAL_LINES" | bc)
          else
            COVERAGE="0"
          fi
          echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
          echo "Combined coverage: $COVERAGE% ($COVERED_LINES/$TOTAL_LINES lines)"

      - name: Upload combined coverage artifact
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: rust-coverage
          path: lcov.info
          retention-days: 7

      - name: Upload Clippy report artifact
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: clippy-report
          path: clippy-report.json
          retention-days: 7

      - name: Test Summary
        uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2.22.0
        with:
          files: |
            target/nextest/ci/junit.xml
            build/test-results/*.xml
          check_name: "Test Results (Rust + C API)"
          comment_mode: "off"
        if: always()

  # ============================================================================
  # Python Tests with Coverage
  # ============================================================================
  python-test:
    name: Python Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          lfs: true

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: '3.11'

      # The pyo3 wheel needs an instrumented Rust build for cargo-llvm-cov
      # to attribute Python-driven coverage to the Rust source files. Once
      # the env from `cargo llvm-cov show-env` is exported, `maturin
      # develop` produces a .so with profraw emission wired in; pytest
      # invocations then accumulate Python-driven coverage into the same
      # llvm-cov target dir as the Rust unit tests.
      - name: Install Rust toolchain (with llvm-tools)
        uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master
        with:
          toolchain: stable
          components: llvm-tools-preview

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@0bc4cd8a3e21db4cb9519857377b0c8c5e150de5 # v2.67.2
        with:
          tool: cargo-llvm-cov

      - name: Install Python build/test dependencies
        run: |
          python -m venv .venv
          source .venv/bin/activate
          python -m pip install --upgrade pip
          pip install maturin pytest junitparser numpy

      - name: Run pytest under cargo-llvm-cov
        # `cargo llvm-cov` exports RUSTFLAGS / LLVM_PROFILE_FILE so any
        # subsequent maturin build picks up the instrumentation. The
        # Python module is built via `maturin develop` directly, not
        # `pip install`, because maturin is what links the cdylib with
        # cargo's instrumentation flags. After the suite runs, the .lcov
        # report attributes Python-driven coverage to Rust source files.
        run: |
          set -euo pipefail
          source .venv/bin/activate
          source <(cargo llvm-cov show-env --export-prefix)
          maturin develop --manifest-path crates/python/Cargo.toml
          pytest tests/python/ --junitxml=pytest-results.xml -v
          cargo llvm-cov report --lcov --output-path coverage-python.lcov
          echo "=== Python-driven Rust source coverage written to coverage-python.lcov ==="

      - name: Verify package import
        run: |
          source .venv/bin/activate
          python -c "import edgefirst.schemas; print('Package import OK')"

      - name: Upload Python coverage artifact
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: python-coverage
          path: coverage-python.lcov
          retention-days: 7

      - name: Upload test results
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: python-test-results
          path: pytest-results.xml
          retention-days: 7
        if: always()

      - name: Test Summary
        uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2.22.0
        with:
          files: pytest-results.xml
          check_name: "Test Results (Python)"
          comment_mode: "off"
        if: always()

  # ============================================================================
  # C++ Tests (GCC + Clang matrix)
  # ============================================================================
  cpp-test:
    name: C++ Tests (${{ matrix.compiler }}, ${{ matrix.cxxstd }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        compiler: [gcc, clang]
        cxxstd: [c++17, c++20]
        include:
          - compiler: gcc
            cc: gcc
            cxx: g++
            packages: ""
          - compiler: clang
            cc: clang
            cxx: clang++
            packages: "clang"
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          lfs: true

      - name: Install Rust toolchain
        run: rustup toolchain install stable --profile minimal

      - name: Install compiler
        if: matrix.packages != ''
        run: |
          sudo apt-get update
          sudo apt-get install -y ${{ matrix.packages }}

      - name: Build Rust library
        run: cargo build --release

      - name: Run C++ tests with XML output
        env:
          CXX: ${{ matrix.cxx }}
        run: make test-cpp-xml CXXSTD=${{ matrix.cxxstd }}

      - name: Upload C++ test results
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: cpp-test-results-${{ matrix.compiler }}-${{ matrix.cxxstd }}
          path: build/test-results/*.xml
          retention-days: 7
        if: always()

      - name: Test Summary
        uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2.22.0
        with:
          files: build/test-results/*.xml
          check_name: "Test Results (C++ ${{ matrix.compiler }}, ${{ matrix.cxxstd }})"
          comment_mode: "off"
        if: always()

  # ============================================================================
  # C++ Tests with AddressSanitizer + UBSan
  # ============================================================================
  cpp-test-asan:
    name: C++ Tests (ASan/UBSan)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          lfs: true

      - name: Install Rust toolchain
        run: rustup toolchain install stable --profile minimal

      - name: Build Rust library
        run: cargo build --release

      - name: Run C++ tests under ASan/UBSan
        run: make test-cpp-asan

  # ============================================================================
  # Documentation build smoke test
  # ============================================================================
  docs-build:
    name: Docs Build (Doxygen)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

      - name: Install Rust toolchain
        run: rustup toolchain install stable --profile minimal

      - name: Install Doxygen
        run: |
          sudo apt-get update
          sudo apt-get install -y doxygen graphviz

      - name: Build Rust library
        run: make lib

      - name: Generate documentation
        run: make docs

      - name: Upload documentation artifact
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: doxygen-html
          path: build/docs/html
          retention-days: 7

  # ============================================================================
  # C++23 smoke build (best-effort, non-blocking)
  # ============================================================================
  # Exercises the `std::expected` and `std::span` alias paths in
  # include/edgefirst/stdlib/*.hpp that only compile on C++23+. Since not all
  # compilers / stdlib versions ship std::expected yet, this job is
  # continue-on-error: true and informational-only.
  cpp-test-cxx23:
    name: C++ Tests (C++23 smoke)
    runs-on: ubuntu-latest
    continue-on-error: true
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          lfs: true  # Golden CDR fixtures under testdata/ are LFS-tracked

      - name: Install Rust toolchain
        run: rustup toolchain install stable --profile minimal

      - name: Install GCC 13 (for C++23 std::expected support)
        run: |
          sudo apt-get update
          sudo apt-get install -y g++-13
          echo "CXX=g++-13" >> $GITHUB_ENV

      - name: Build Rust library
        run: make lib

      - name: Build and run C++ tests on C++23
        run: make test-cpp CXX=$CXX CXXSTD=c++23

  # ============================================================================
  # SonarCloud Analysis (runs after all tests complete)
  # ============================================================================
  sonarcloud:
    name: SonarCloud
    runs-on: ubuntu-latest
    needs: [rust-and-c-test, cpp-test, cpp-test-asan, python-test]
    # Only run on main repo (not forks) due to secrets
    if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push'
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0  # Full history for accurate blame

      - name: Download combined Rust/C coverage
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
        with:
          name: rust-coverage
          path: .

      - name: Download Python coverage
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
        with:
          name: python-coverage
          path: .

      - name: Download Clippy report
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
        with:
          name: clippy-report
          path: .

      - name: Verify coverage files
        run: |
          echo "=== Coverage Files ==="
          ls -la lcov.info coverage-python.lcov clippy-report.json 2>/dev/null || echo "Some files missing"
          echo ""
          if [ -f lcov.info ]; then
            echo "=== Combined LCOV Summary (Rust unit + C API) ==="
            RUST_FILES=$(grep -c "^SF:" lcov.info || echo "0")
            echo "Source files with coverage: $RUST_FILES"
          fi
          if [ -f coverage-python.lcov ]; then
            echo "=== Python-driven Rust LCOV Summary ==="
            PY_FILES=$(grep -c "^SF:" coverage-python.lcov || echo "0")
            echo "Source files with Python-driven coverage: $PY_FILES"
          fi

      - name: SonarCloud Scan
        uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6.0.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        with:
          # Both LCOV files describe the same Rust source files; Sonar
          # accepts a comma-separated list and merges them, so coverage
          # exercised by *either* the Rust unit tests or the Python tests
          # counts toward the line/branch totals. The Python module is now
          # written in Rust (pyo3); there's no separate Python-language
          # source to track via the old `sonar.python.coverage` setting.
          args: >
            -Dsonar.rust.lcov.reportPaths=lcov.info,coverage-python.lcov
            -Dsonar.rust.clippy.reportPaths=clippy-report.json