diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7abff50 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,256 @@ +name: Build plugin + +on: + push: + branches: [main] + tags: ['v[0-9]+.[0-9]+.[0-9]+'] + pull_request: + workflow_dispatch: + +env: + # Single source of truth: both the clone and the cache key derive from this. + PJ_TAG: '3.9.2' + +jobs: + # Build PlotJuggler once per platform and cache the install tree. Hoisted out of + # the plugin matrix so it is not rebuilt once per build configuration. + plotjuggler: + name: PlotJuggler (${{ matrix.platform.label }}) + runs-on: ${{ matrix.platform.os }} + container: + # Ubuntu is built inside a 22.04 container rather than on a 24.04 runner so + # the artifacts keep the older libstdc++/glibc floor: a binary built against + # GCC 13 needs GLIBCXX_3.4.32 and will not load on Ubuntu 22.04, while a + # 22.04 build loads on both. This also matches the Dockerfile exactly, so a + # CI failure reproduces locally with `docker build`. + image: ${{ matrix.platform.container || '' }} + strategy: + fail-fast: false + matrix: + platform: + - { os: ubuntu-latest, container: 'ubuntu:22.04', label: ubuntu-22.04 } + - { os: windows-2022, label: windows-2022 } + + steps: + # GITHUB_WORKSPACE is backslash-separated on Windows; normalise it so a + # single bash step can hand the path to cmake on every platform. + - name: Resolve install prefix + shell: bash + run: echo "PJ_INSTALL=${GITHUB_WORKSPACE//\\//}/pj_install" >> "$GITHUB_ENV" + + # The label is part of the key because runner.os alone cannot tell a 22.04 + # container build from a build on the runner's own newer userland. + - name: Cache PlotJuggler install + id: cache + uses: actions/cache@v6 + with: + path: pj_install + key: ${{ matrix.platform.label }}-plotjuggler-${{ env.PJ_TAG }} + + - name: Install PlotJuggler dependencies + if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'Linux' + # No sudo: the container runs as root. python3 matters -- CMakeLists falls + # back to an empty LogMessageDescriptions.h without it, silently dropping + # the field tooltips from the built plugin. + run: | + apt-get update + apt-get install -y --no-install-recommends \ + build-essential cmake git ca-certificates python3 zstd \ + qtbase5-dev libqt5svg5-dev libqt5websockets5-dev libqt5opengl5-dev \ + libqt5x11extras5-dev libprotoc-dev libzmq3-dev liblz4-dev libzstd-dev + + - name: Install Qt + if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'Windows' + uses: jurplel/install-qt-action@v4 + with: + version: '5.15.2' + arch: 'win64_msvc2019_64' + + - name: Clone PlotJuggler + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: git clone --depth 1 --branch "$PJ_TAG" https://github.com/facontidavide/PlotJuggler.git plotjuggler + + - name: Configure PlotJuggler + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + # CMAKE_POLICY_VERSION_MINIMUM is insurance: PlotJuggler 3.9.2 ships + # third-party directories declaring cmake_minimum_required(VERSION 3.0), + # which CMake 4 rejects outright. Ignored by CMake older than 3.31. + run: | + cmake -S plotjuggler -B pj_build \ + -DCMAKE_INSTALL_PREFIX="$PJ_INSTALL" \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + + - name: Build & install PlotJuggler + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: cmake --build pj_build --config RelWithDebInfo --target install --parallel "$(nproc || echo 2)" + + plugin: + name: ${{ matrix.config.name }} (${{ matrix.platform.label }}) + needs: plotjuggler + runs-on: ${{ matrix.platform.os }} + container: + image: ${{ matrix.platform.container || '' }} + strategy: + # Never let one configuration mask the others. + fail-fast: false + matrix: + platform: + - { os: ubuntu-latest, container: 'ubuntu:22.04', label: ubuntu-22.04 } + - { os: windows-2022, label: windows-2022 } + config: + - name: units + units: 'ON' + rcou: 'OFF' + - name: units-rcou + units: 'ON' + rcou: 'ON' + - name: plain + units: 'OFF' + rcou: 'OFF' + + steps: + # Before checkout, so that actions/checkout finds git and makes a real clone + # instead of falling back to a source tarball. + - name: Install plugin dependencies + if: runner.os == 'Linux' + run: | + apt-get update + apt-get install -y --no-install-recommends \ + build-essential cmake git ca-certificates python3 zstd \ + qtbase5-dev libqt5svg5-dev libqt5websockets5-dev libqt5opengl5-dev \ + libqt5x11extras5-dev + + - name: Checkout plugin + uses: actions/checkout@v7 + + - name: Resolve install prefix + shell: bash + run: echo "PJ_INSTALL=${GITHUB_WORKSPACE//\\//}/pj_install" >> "$GITHUB_ENV" + + # The plotjuggler job has already populated this key, so a miss means + # something is wrong and should fail loudly rather than rebuild silently. + - name: Restore PlotJuggler install + uses: actions/cache/restore@v6 + with: + path: pj_install + key: ${{ matrix.platform.label }}-plotjuggler-${{ env.PJ_TAG }} + fail-on-cache-miss: true + + - name: Install Qt + if: runner.os == 'Windows' + uses: jurplel/install-qt-action@v4 + with: + version: '5.15.2' + arch: 'win64_msvc2019_64' + + - name: Configure plugin + shell: bash + run: | + cmake -S . -B build \ + -Dplotjuggler_DIR="$PJ_INSTALL/lib/cmake/plotjuggler" \ + -DCMAKE_BUILD_TYPE=Release \ + -DADD_UNITS=${{ matrix.config.units }} \ + -DADD_RCOU_FUNCTION_LABELS=${{ matrix.config.rcou }} + + - name: Build plugin + shell: bash + run: cmake --build build --config Release --parallel "$(nproc || echo 2)" + + - name: Install plugin + shell: bash + run: cmake --install build --prefix plugin_install --config Release + + - name: Build smoke test + shell: bash + run: | + cmake -S tests -B tests/build \ + -DPJ_INCLUDE_DIR="$PJ_INSTALL/include" \ + -DCMAKE_BUILD_TYPE=Release + cmake --build tests/build --config Release --parallel "$(nproc || echo 2)" + + # Loads the plugin exactly as PlotJuggler does and parses a real log, so a + # plugin that builds but cannot be loaded fails here instead of in a user's + # hands. The --expect-* flags additionally assert that this job's CMake + # options actually reached the binary, which a build log cannot tell you. + # offscreen because CI has no display. + - name: Smoke test plugin + shell: bash + env: + QT_QPA_PLATFORM: offscreen + run: | + if [ -x tests/build/pj_plugin_smoketest ]; then + smoketest=tests/build/pj_plugin_smoketest + else + smoketest=tests/build/Release/pj_plugin_smoketest.exe + fi + plugin="$(find plugin_install -type f \( -name '*.so' -o -name '*.dll' \) -print -quit)" + # --verify is unconditional so that the all-OFF 'plain' configuration is + # asserted too, not merely reported. + expect=(--verify) + if [ "${{ matrix.config.units }}" = "ON" ]; then + expect+=(--expect-units) + fi + if [ "${{ matrix.config.rcou }}" = "ON" ]; then + expect+=(--expect-rcou) + fi + echo "Testing $plugin ${expect[*]}" + "$smoketest" "$plugin" tests/logs/00000001.BIN "${expect[@]}" + + - name: Upload plugin artifact + uses: actions/upload-artifact@v7 + with: + name: DataAPBin-${{ matrix.platform.label }}-${{ matrix.config.name }} + path: plugin_install/ + if-no-files-found: error + + release: + name: Draft release + needs: plugin + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout plugin + uses: actions/checkout@v7 + + - name: Download all plugin artifacts + uses: actions/download-artifact@v8 + with: + path: artifacts + + # Each artifact holds a single shared library under bin/. Flatten them + # into uniquely named assets, keeping the real extension so a downloaded + # asset is still recognisable as a plugin. + - name: Collect release assets + run: | + set -euo pipefail + mkdir -p assets + for dir in artifacts/*/; do + variant="$(basename "$dir")" + variant="${variant#DataAPBin-}" + # -print -quit rather than piping to head: under `set -o pipefail` a + # closed pipe would make find fail the whole step. + lib="$(find "$dir" -type f \( -name '*.so' -o -name '*.dll' -o -name '*.dylib' \) -print -quit)" + if [ -z "$lib" ]; then + echo "No plugin library found in $dir" >&2 + exit 1 + fi + case "$lib" in + *.dll) cp "$lib" "assets/DataAPBin-$variant.dll" ;; + *) cp "$lib" "assets/libDataAPBin-$variant.${lib##*.}" ;; + esac + done + ls -l assets + + - name: Create draft release + uses: softprops/action-gh-release@v3 + with: + draft: true + files: assets/* + fail_on_unmatched_files: true diff --git a/.github/workflows/win_build.yml b/.github/workflows/win_build.yml deleted file mode 100644 index 5c9b238..0000000 --- a/.github/workflows/win_build.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Build PlotJuggler & Plugin - -on: [push, pull_request, workflow_dispatch] - -jobs: - build: - runs-on: windows-2022 - - steps: - - name: Checkout plugin - uses: actions/checkout@v4 - - - name: Install Conan - uses: turtlebrowser/get-conan@main - - - name: Install Qt - uses: jurplel/install-qt-action@v4 - with: - version: '5.15.2' - arch: 'win64_msvc2019_64' - - - name: Cache Conan - uses: actions/cache@v4 - with: - path: ~/.conan2/ - key: ${{ runner.os }}-conan-${{ hashFiles('**/conanfile.txt') }} - - - name: Detect Conan profile - shell: pwsh - run: conan profile detect - - - name: Conan install - shell: pwsh - run: | - conan install . -of build --build=missing -pr:b=default ` - -s build_type=Release -s compiler.cppstd=20 ` - -c tools.build:jobs=$([Environment]::ProcessorCount) ` - -c tools.cmake.cmaketoolchain:generator=Ninja - - - name: Cache PlotJuggler install - id: cache-plotjuggler - uses: actions/cache@v4 - with: - path: pj_install/ - key: ${{ runner.os }}-plotjuggler-${{ hashFiles('plotjuggler/**') }} - - - name: Clone PlotJuggler - if: steps.cache-plotjuggler.outputs.cache-hit != 'true' - run: git clone --depth 1 --branch 3.9.2 https://github.com/facontidavide/PlotJuggler.git plotjuggler - - - name: Configure PlotJuggler - if: steps.cache-plotjuggler.outputs.cache-hit != 'true' - shell: pwsh - run: cmake -S plotjuggler -B pj_build -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}\pj_install -DCMAKE_BUILD_TYPE=RelWithDebInfo - - - name: Build & install PlotJuggler - if: steps.cache-plotjuggler.outputs.cache-hit != 'true' - shell: pwsh - run: cmake --build pj_build --config RelWithDebInfo --target install --parallel $([Environment]::ProcessorCount) - - - name: Clean plugin build directory - shell: pwsh - run: Remove-Item -Recurse -Force build -ErrorAction SilentlyContinue - - - name: Configure plugin - shell: pwsh - run: cmake -S . -B build -Dplotjuggler_DIR="${{ github.workspace }}\pj_install\lib\cmake\plotjuggler" -DADD_UNITS=OFF - - - name: Build plugin - shell: pwsh - run: cmake --build build --config Release --parallel $([Environment]::ProcessorCount) - - - name: Install plugin - shell: pwsh - run: cmake --install build --prefix "${{ github.workspace }}\plugin_install" - - - name: Upload plugin artifact - uses: actions/upload-artifact@v4 - with: - name: plugin-build - path: build/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2690231..080b357 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ *.app /CMakeLists.txt.user build/* +tests/build/ diff --git a/DataLoadAPBin/dataload_apbin.cpp b/DataLoadAPBin/dataload_apbin.cpp index 282c07e..a4a6515 100644 --- a/DataLoadAPBin/dataload_apbin.cpp +++ b/DataLoadAPBin/dataload_apbin.cpp @@ -40,8 +40,8 @@ //#define DEBUG_MULTIPLIERS //#define DEBUG_UNITS -// Config -#define LABEL_WITH_UNIT +// LABEL_WITH_UNIT and LABEL_RCOU_FUNCTION are set by the build: see the +// ADD_UNITS and ADD_RCOU_FUNCTION_LABELS options in CMakeLists.txt. bool is_nearly(double val, int val2) { diff --git a/README.md b/README.md index 379ac00..ff75458 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,9 @@ Once compilation is finished, you will find your `.so` plugin in the `artifacts` sudo apt -y install qtbase5-dev libqt5svg5-dev ``` -2. If you want the units to be displayed in PlotJuggler (read at the end), you need to edit `dataload_apbin.cpp` and activate `#define LABEL_WITH_UNIT`. +2. The optional features are selected with cmake options, both `OFF` by default: + - `-DADD_UNITS=ON` displays the units of the logged fields (read at the end). + - `-DADD_RCOU_FUNCTION_LABELS=ON` displays the channel function next to its name. 3. Compile using cmake: @@ -74,6 +76,23 @@ Once compilation is finished, you will find your `.so` plugin in the `artifacts` sudo make install ``` +### Testing a built plugin + +`tests/` holds a headless check that loads a plugin the way PlotJuggler does and +parses the small log in `tests/logs/`, without starting the GUI. +It also verifies that the cmake options above actually reached the binary, and on +Linux reports whether the plugin's libstdc++ requirement is satisfied by your +system — which is what catches a plugin built on a newer distro than the one +running it. + +```bash +cmake -S tests -B tests/build -DPJ_INCLUDE_DIR="$PJ_WS/install/include" +cmake --build tests/build +./tests/test_plugin.sh build/libDataAPBin.so +``` + +Point it at a plugin downloaded from CI or a release to check it before use. + ### Notes for building on MacOS Qt dependencies may not be found automatically. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..f72582a --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.16) +project(pj_plugin_smoketest CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Deliberately no find_package(plotjuggler): the plugin under test is dlopened, so +# it carries its own copy of plotjuggler_base. All this harness needs is the +# headers that declare the DataLoader interface. Avoiding the package config also +# avoids PlotJuggler 3.16's exported target, whose link interface references +# fmt::fmt and lua::lua without ever looking them up. +set(PJ_INCLUDE_DIR "" CACHE PATH "PlotJuggler install include directory") +if(NOT PJ_INCLUDE_DIR) + message(FATAL_ERROR "Set -DPJ_INCLUDE_DIR=/include") +endif() + +find_package(Qt5 REQUIRED COMPONENTS Core Widgets Xml) + +add_executable(pj_plugin_smoketest pj_plugin_smoketest.cpp) +target_include_directories(pj_plugin_smoketest PRIVATE ${PJ_INCLUDE_DIR}) +target_link_libraries(pj_plugin_smoketest PRIVATE Qt5::Core Qt5::Widgets Qt5::Xml) diff --git a/tests/logs/00000001.BIN b/tests/logs/00000001.BIN new file mode 100644 index 0000000..5e13f83 Binary files /dev/null and b/tests/logs/00000001.BIN differ diff --git a/tests/pj_plugin_smoketest.cpp b/tests/pj_plugin_smoketest.cpp new file mode 100644 index 0000000..4940992 --- /dev/null +++ b/tests/pj_plugin_smoketest.cpp @@ -0,0 +1,294 @@ +// Headless smoke test for a PlotJuggler DataLoader plugin. +// +// Usage: pj_plugin_smoketest [logfile.bin] [--verify] [--expect-units] [--expect-rcou] +// +// Exercises exactly what PlotJuggler does when it loads a plugin and opens a +// file, but without a GUI and without ever blocking: QPluginLoader, the +// qobject_cast to PJ::DataLoader, then readDataFromFile() on a real log. +// +// Without --verify the build configuration is only reported. With --verify it is +// asserted: the plugin must have been built with exactly the options named by +// --expect-units / --expect-rcou, and any option not named must be OFF. Both +// options work by changing series names, so the name of one known channel pins +// the build down exactly. Asserting the all-OFF case matters as much as the +// others -- an option wrongly forced on in the source looks fine otherwise. +// +// Exit code 0 means every requested check passed. + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "PlotJuggler/dataloader_base.h" +#include "PlotJuggler/plotdata.h" + +namespace +{ +// tests/logs/00000001.BIN has SERVO1_FUNCTION set to Throttle, so this channel +// carries a function label whenever LABEL_RCOU_FUNCTION is compiled in, and a +// microsecond unit whenever LABEL_WITH_UNIT is. That makes exactly one of the +// four spellings below correct for any given build. +const char* const kChannel = "/RCOU/C1"; +const char* const kFunction = " (Throttle)"; +const char* const kUnit = "\t[us]"; + +std::string expectedName(bool with_units, bool with_rcou) +{ + std::string name = kChannel; + if (with_rcou) + { + name += kFunction; + } + if (with_units) + { + name += kUnit; + } + return name; +} + +std::string describe(bool with_units, bool with_rcou) +{ + return std::string("units=") + (with_units ? "ON" : "OFF") + " rcou=" + (with_rcou ? "ON" : "OFF"); +} + +void printUsage(const char* argv0) +{ + fprintf(stderr, + "usage: %s [logfile.bin] [--verify] [--expect-units] [--expect-rcou]\n", + argv0); +} +} // namespace + +int main(int argc, char** argv) +{ + std::string plugin_path; + std::string log_path; + bool expect_units = false; + bool expect_rcou = false; + bool verify = false; + + for (int i = 1; i < argc; ++i) + { + const std::string arg = argv[i]; + if (arg == "--verify") + { + verify = true; + } + else if (arg == "--expect-units") + { + expect_units = true; + } + else if (arg == "--expect-rcou") + { + expect_rcou = true; + } + else if (arg == "-h" || arg == "--help") + { + printUsage(argv[0]); + return 0; + } + else if (arg.rfind("--", 0) == 0) + { + fprintf(stderr, "unknown option: %s\n", arg.c_str()); + printUsage(argv[0]); + return 2; + } + else if (plugin_path.empty()) + { + plugin_path = arg; + } + else if (log_path.empty()) + { + log_path = arg; + } + else + { + printUsage(argv[0]); + return 2; + } + } + + if (plugin_path.empty()) + { + printUsage(argv[0]); + return 2; + } + if (log_path.empty() && (verify || expect_units || expect_rcou)) + { + fprintf(stderr, "FAIL: --verify needs a log file to check series names against\n"); + return 2; + } + if ((expect_units || expect_rcou) && !verify) + { + fprintf(stderr, "FAIL: --expect-* only means something with --verify\n"); + return 2; + } + + // A QApplication must exist before the plugin is instantiated: the plugin + // links Qt Widgets, and constructing a QWidget without one aborts. + QApplication app(argc, argv); + + const QFileInfo plugin_info(QString::fromStdString(plugin_path)); + if (!plugin_info.isFile()) + { + fprintf(stderr, "FAIL: no such plugin file: %s\n", plugin_path.c_str()); + return 2; + } + + // Must be absolute: QPluginLoader resolves a relative path against + // QCoreApplication::libraryPaths(), never the working directory, so a + // perfectly good relative path is reported as "The shared library was not + // found." -- see locatePlugin() in qpluginloader.cpp. + const QString qplugin = plugin_info.absoluteFilePath(); + + QPluginLoader loader(qplugin); + QObject* instance = loader.instance(); + if (!instance) + { + // This is where an ABI mismatch surfaces, e.g. a missing GLIBCXX version. + fprintf(stderr, "FAIL: could not load %s\n %s\n", qPrintable(qplugin), + qPrintable(loader.errorString())); + return 1; + } + + auto* dataloader = qobject_cast(instance); + if (!dataloader) + { + fprintf(stderr, "FAIL: library loaded but is not a PJ::DataLoader plugin\n"); + return 1; + } + + printf("OK : loaded DataLoader plugin \"%s\"\n", dataloader->name()); + printf(" extensions:"); + for (const char* ext : dataloader->compatibleFileExtensions()) + { + printf(" %s", ext); + } + printf("\n"); + + if (log_path.empty()) + { + printf("OK : load-only check passed (pass a log file to also test parsing)\n"); + return 0; + } + + const QString qlog = QString::fromStdString(log_path); + if (!QFileInfo::exists(qlog)) + { + fprintf(stderr, "FAIL: no such log file: %s\n", log_path.c_str()); + return 2; + } + + PJ::PlotDataMapRef data; + PJ::FileLoadInfo info; + info.filename = qlog; + + bool parsed = false; + try + { + parsed = dataloader->readDataFromFile(&info, data); + } + catch (const std::exception& err) + { + fprintf(stderr, "FAIL: exception while parsing: %s\n", err.what()); + return 1; + } + + if (!parsed) + { + fprintf(stderr, "FAIL: readDataFromFile() returned false\n"); + return 1; + } + + size_t samples = 0; + for (const auto& series : data.numeric) + { + samples += series.second.size(); + } + + printf("OK : parsed \"%s\"\n", qPrintable(QFileInfo(qlog).fileName())); + printf(" numeric series: %zu (%zu samples)\n", data.numeric.size(), samples); + printf(" string series : %zu\n", data.strings.size()); + + if (data.numeric.empty()) + { + fprintf(stderr, "FAIL: parsing reported success but produced no numeric series\n"); + return 1; + } + + // Which of the four spellings of the reference channel is present tells us + // exactly which ifdefs were compiled in. Requiring the other three to be + // absent is what catches a flag stuck on -- or stuck off -- in the source. + const std::string wanted = expectedName(expect_units, expect_rcou); + std::vector found; + for (bool units : { false, true }) + { + for (bool rcou : { false, true }) + { + const std::string candidate = expectedName(units, rcou); + if (data.numeric.count(candidate) != 0) + { + found.push_back(candidate); + printf(" built with : %s\n", describe(units, rcou).c_str()); + } + } + } + + if (found.empty()) + { + fprintf(stderr, "FAIL: reference channel %s not found in this log under any\n", kChannel); + fprintf(stderr, " expected name. Names starting with %s:\n", kChannel); + std::vector nearby; + for (const auto& series : data.numeric) + { + if (series.first.rfind(kChannel, 0) == 0) + { + nearby.push_back(series.first); + } + } + std::sort(nearby.begin(), nearby.end()); + for (const auto& name : nearby) + { + fprintf(stderr, " %s\n", name.c_str()); + } + if (nearby.empty()) + { + fprintf(stderr, " (none)\n"); + } + return 1; + } + + if (found.size() > 1) + { + fprintf(stderr, "FAIL: reference channel matched %zu spellings at once, so the\n", found.size()); + fprintf(stderr, " naming is ambiguous and this check cannot be trusted\n"); + return 1; + } + + if (!verify) + { + // Nothing was asserted, so just report what the build looks like. + printf("OK : parse check passed (pass --verify to assert the build\n"); + printf(" configuration as well)\n"); + return 0; + } + + if (found.front() != wanted) + { + fprintf(stderr, "FAIL: wrong build configuration\n"); + fprintf(stderr, " expected %s -> \"%s\"\n", describe(expect_units, expect_rcou).c_str(), + wanted.c_str()); + fprintf(stderr, " actual -> \"%s\"\n", found.front().c_str()); + fprintf(stderr, " ADD_UNITS/ADD_RCOU_FUNCTION_LABELS are not reaching the binary\n"); + return 1; + } + + printf("OK : build configuration matches %s\n", describe(expect_units, expect_rcou).c_str()); + return 0; +} diff --git a/tests/test_plugin.sh b/tests/test_plugin.sh new file mode 100755 index 0000000..6ff92bc --- /dev/null +++ b/tests/test_plugin.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Test a built PlotJuggler apbin plugin without launching the GUI. +# +# test_plugin.sh [logfile.bin] +# +# Point it at a downloaded CI artifact or a locally built plugin. Runs three checks: +# 1. ABI floor - which libstdc++/glibc the plugin demands, and whether this +# host can satisfy it. This is what catches "built on a newer +# distro than the user runs". Linux only. +# 2. Plugin load - QPluginLoader + cast to PJ::DataLoader, as PlotJuggler does. +# 3. Parse - readDataFromFile() on a real ArduPilot .bin. +# +# Build the helper it drives first: +# cmake -S tests -B tests/build -DPJ_INCLUDE_DIR=/include +# cmake --build tests/build +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOKETEST="${SMOKETEST:-$SCRIPT_DIR/build/pj_plugin_smoketest}" + +if [ $# -lt 1 ]; then + echo "usage: $(basename "$0") [logfile.bin]" >&2 + exit 2 +fi + +PLUGIN="$1" +shift +# Falls back to the small log committed alongside this script. Anything left over +# is forwarded to the helper, so --verify/--expect-* can be used from here too. +LOG="$SCRIPT_DIR/logs/00000001.BIN" +if [ $# -gt 0 ] && [ "${1#--}" = "$1" ]; then + LOG="$1" + shift +fi + +if [ ! -f "$PLUGIN" ]; then + echo "FAIL: no such plugin file: $PLUGIN" >&2 + exit 2 +fi + +echo "=== 1. ABI floor ===" +# highest_version -> the numerically largest entry +highest() { sort -u -V | tail -n 1; } + +need_cxx="$(objdump -T "$PLUGIN" 2>/dev/null | grep -oE 'GLIBCXX_[0-9.]+' | highest)" +need_glibc="$(objdump -T "$PLUGIN" 2>/dev/null | grep -oE 'GLIBC_[0-9.]+' | highest)" +have_cxx="$(strings /lib/*/libstdc++.so.6 2>/dev/null | grep -oE 'GLIBCXX_[0-9.]+' | highest)" + +echo " plugin requires : ${need_cxx:-none} / ${need_glibc:-none}" +echo " this host has : ${have_cxx:-unknown}" + +abi_ok=1 +if [ -n "$need_cxx" ] && [ -n "$have_cxx" ]; then + # If the highest of (needed, have) is not `have`, the host is too old. + if [ "$(printf '%s\n%s\n' "$need_cxx" "$have_cxx" | highest)" != "$have_cxx" ]; then + echo " FAIL: host libstdc++ is older than the plugin requires" + echo " the plugin was built on a newer distro than this one" + abi_ok=0 + else + echo " OK : host satisfies the plugin's libstdc++ requirement" + fi +fi + +echo "=== 2/3. Plugin load and parse ===" +if [ ! -x "$SMOKETEST" ]; then + echo " SKIP: $SMOKETEST not built." >&2 + echo " cmake -S $SCRIPT_DIR -B $SCRIPT_DIR/build -DPJ_INCLUDE_DIR=/include" >&2 + echo " cmake --build $SCRIPT_DIR/build" >&2 + [ "$abi_ok" -eq 1 ] || exit 1 + exit 3 +fi + +# offscreen so this runs over SSH and in CI with no display attached +export QT_QPA_PLATFORM=offscreen +"$SMOKETEST" "$PLUGIN" "$LOG" "$@" +result=$? + +[ "$abi_ok" -eq 1 ] || exit 1 +exit "$result"