[wpinet] Add mDNS discovery tests and fix mDNS JNI bugs (#8682)

In https://github.com/wpilibsuite/allwpilib/issues/8681 we discovered
that multicast service types need to be valid (end with _tcp or _udp),
or else errors are silently swallowed. Let's make our C++ unit test use
a valid name and also check that it works. I think if we
should/shouldn't do this is up for debate still.

I also discovered two bugs in the JNI code that lead to incorrect
results being returned
- Return array index was always 0
- Use of JLocal for the return value seems to mean that the array will
always be NULL in java
This commit is contained in:
Matt Morley
2026-03-29 20:41:32 -07:00
committed by GitHub
parent ceb712b089
commit db42c6cbba
8 changed files with 92 additions and 11 deletions

View File

@@ -73,7 +73,7 @@ jobs:
- name: Install apt dependencies - name: Install apt dependencies
if: matrix.os == 'ubuntu-24.04' if: matrix.os == 'ubuntu-24.04'
run: sudo apt-get update && sudo apt-get install -y libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev run: sudo apt-get update && sudo apt-get install -y libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev avahi-daemon
- if: matrix.os == 'ubuntu-24.04' - if: matrix.os == 'ubuntu-24.04'
uses: bazel-contrib/setup-bazel@0.15.0 uses: bazel-contrib/setup-bazel@0.15.0

View File

@@ -37,6 +37,12 @@ jobs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv-java ninja-build run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv-java ninja-build
- name: Setup avahi-daemon
if: runner.os == 'Linux'
run: |
sudo service dbus start
sudo avahi-daemon -D
- name: Install dependencies (macOS) - name: Install dependencies (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: brew install opencv ninja run: brew install opencv ninja

View File

@@ -60,7 +60,11 @@ jobs:
with: with:
image: ${{ matrix.container }} image: ${{ matrix.container }}
options: -v ${{ github.workspace }}:/work -w /work -e ARTIFACTORY_PUBLISH_USERNAME -e ARTIFACTORY_PUBLISH_PASSWORD -e GITHUB_REF -e CI options: -v ${{ github.workspace }}:/work -w /work -e ARTIFACTORY_PUBLISH_USERNAME -e ARTIFACTORY_PUBLISH_PASSWORD -e GITHUB_REF -e CI
run: ./gradlew build --build-cache -PbuildServer -PskipJavaFormat ${{ matrix.build-options }} ${{ env.EXTRA_GRADLE_ARGS }} # Start avahi-daemon and build
run: |
service dbus start
avahi-daemon -D
./gradlew build --build-cache -PbuildServer -PskipJavaFormat ${{ matrix.build-options }} ${{ env.EXTRA_GRADLE_ARGS }}
env: env:
ARTIFACTORY_PUBLISH_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PUBLISH_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PUBLISH_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} ARTIFACTORY_PUBLISH_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}

View File

@@ -22,7 +22,7 @@ jobs:
ctest-flags: "-E 'wpilibc'" ctest-flags: "-E 'wpilibc'"
- name: tsan - name: tsan
cmake-flags: "-DCMAKE_BUILD_TYPE=Tsan" cmake-flags: "-DCMAKE_BUILD_TYPE=Tsan"
ctest-env: "TSAN_OPTIONS=second_deadlock_stack=1" ctest-env: "TSAN_OPTIONS=second_deadlock_stack=1:suppressions=$GITHUB_WORKSPACE/tsan_suppressions.txt"
ctest-flags: "-E 'cscore|cameraserver'" ctest-flags: "-E 'cscore|cameraserver'"
- name: ubsan - name: ubsan
cmake-flags: "-DCMAKE_BUILD_TYPE=Ubsan" cmake-flags: "-DCMAKE_BUILD_TYPE=Ubsan"
@@ -33,7 +33,7 @@ jobs:
container: wpilib/roborio-cross-ubuntu:2025-24.04 container: wpilib/roborio-cross-ubuntu:2025-24.04
steps: steps:
- name: Install Dependencies - name: Install Dependencies
run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv-java clang-18 ninja-build run: sudo apt-get update && sudo apt-get install -y libopencv-dev libopencv-java clang-18 ninja-build avahi-daemon
- name: Install sccache - name: Install sccache
uses: mozilla-actions/sccache-action@v0.0.9 uses: mozilla-actions/sccache-action@v0.0.9
@@ -46,6 +46,11 @@ jobs:
SCCACHE_WEBDAV_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} SCCACHE_WEBDAV_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
SCCACHE_WEBDAV_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} SCCACHE_WEBDAV_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
- name: Setup avahi-daemon
run: |
sudo service dbus start
sudo avahi-daemon -D
- name: build - name: build
working-directory: build working-directory: build
run: cmake --build . --parallel $(nproc) run: cmake --build . --parallel $(nproc)

1
tsan_suppressions.txt Normal file
View File

@@ -0,0 +1 @@
deadlock:libdbus-1.so.3

View File

@@ -330,9 +330,10 @@ Java_org_wpilib_net_WPINetJNI_getMulticastServiceResolverData
return serviceDataEmptyArray; return serviceDataEmptyArray;
} }
JLocal<jobjectArray> returnData{ jobjectArray returnData =
env, env->NewObjectArray(allData.size(), serviceDataCls, nullptr)}; env->NewObjectArray(allData.size(), serviceDataCls, nullptr);
size_t index = 0;
for (auto&& data : allData) { for (auto&& data : allData) {
JLocal<jstring> serviceName{env, MakeJString(env, data.serviceName)}; JLocal<jstring> serviceName{env, MakeJString(env, data.serviceName)};
JLocal<jstring> hostName{env, MakeJString(env, data.hostName)}; JLocal<jstring> hostName{env, MakeJString(env, data.hostName)};
@@ -340,7 +341,6 @@ Java_org_wpilib_net_WPINetJNI_getMulticastServiceResolverData
wpi::util::SmallVector<std::string_view, 8> keysRef; wpi::util::SmallVector<std::string_view, 8> keysRef;
wpi::util::SmallVector<std::string_view, 8> valuesRef; wpi::util::SmallVector<std::string_view, 8> valuesRef;
size_t index = 0;
for (auto&& txt : data.txt) { for (auto&& txt : data.txt) {
keysRef.emplace_back(txt.first); keysRef.emplace_back(txt.first);
valuesRef.emplace_back(txt.second); valuesRef.emplace_back(txt.second);

View File

@@ -0,0 +1,53 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package org.wpilib.net;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
class MulticastServiceAnnouncerTest {
@Test
void emptyText() throws InterruptedException, UnknownHostException {
final String serviceName = "Foaksdfjasklfj";
final String serviceType = "_wpinotxt._tcp";
final int port = 12345;
try (MulticastServiceAnnouncer announcer =
new MulticastServiceAnnouncer(serviceName, serviceType, port);
MulticastServiceResolver resolver = new MulticastServiceResolver(serviceType)) {
assertTrue(announcer.hasImplementation() && resolver.hasImplementation());
announcer.start();
resolver.start();
List<ServiceData> allData = new ArrayList<>();
for (int i = 0; i < 10; i++) {
ServiceData[] data = resolver.getData();
if (data == null) {
continue;
}
allData.addAll(List.of(data));
if (!allData.isEmpty()) {
break;
}
Thread.sleep(1000);
}
assertFalse(allData.isEmpty(), "Expected at least one service entry, but got none");
resolver.stop();
announcer.stop();
}
}
}

View File

@@ -3,11 +3,10 @@
// the WPILib BSD license file in the root directory of this project. // the WPILib BSD license file in the root directory of this project.
#include <array> #include <array>
#include <chrono>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <thread> #include <thread>
#include <utility> #include <vector>
#include <gtest/gtest.h> #include <gtest/gtest.h>
@@ -17,7 +16,7 @@
TEST(MulticastServiceAnnouncerTest, EmptyText) { TEST(MulticastServiceAnnouncerTest, EmptyText) {
const std::string_view serviceName = "TestServiceNoText"; const std::string_view serviceName = "TestServiceNoText";
const std::string_view serviceType = "_wpinotxt"; const std::string_view serviceType = "_wpinotxt._tcp";
const int port = std::rand(); const int port = std::rand();
wpi::net::MulticastServiceAnnouncer announcer(serviceName, serviceType, port); wpi::net::MulticastServiceAnnouncer announcer(serviceName, serviceType, port);
wpi::net::MulticastServiceResolver resolver(serviceType); wpi::net::MulticastServiceResolver resolver(serviceType);
@@ -26,7 +25,20 @@ TEST(MulticastServiceAnnouncerTest, EmptyText) {
announcer.Start(); announcer.Start();
resolver.Start(); resolver.Start();
std::this_thread::sleep_for(std::chrono::seconds(1)); std::vector<wpi::net::MulticastServiceResolver::ServiceData> allData;
for (int i = 0; i < 15; i++) {
// GetData gives me new data since last time. This smoketest is just
// looking for -any- response
allData = resolver.GetData();
if (!allData.empty()) {
break;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
ASSERT_GT(allData.size(), 0ul);
resolver.Stop(); resolver.Stop();
announcer.Stop(); announcer.Stop();