diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f18c4d0ae..9e53f8558 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,9 +50,12 @@ jobs: - run: sudo apt-get update && sudo apt-get install socat ${{ matrix.packages }} - run: mvn -P "${{ matrix.profile }}" --batch-mode + # The build rewrites the committed JNI header when SerialNativeInterface.java changed it (ant/build.xml) + - name: Check the JNI header is committed + run: git diff --exit-code -- src/main/cpp/jssc_SerialNativeInterface.h macos: - runs-on: [macos-14] + runs-on: [macos-15] strategy: fail-fast: false matrix: @@ -81,13 +84,14 @@ jobs: - name: Set SDK version run: | export MACOSX_DEPLOYMENT_TARGET=${{ matrix.macos-deployment-target }} - export SDKROOT=$XCODE_16_DEVELOPER_DIR/Platforms/MacOSX.platform/Developer/SDKs/${{ matrix.sdk-version }} - export CMAKE_OSX_SYSROOT=$SDKROOT + export CMAKE_OSX_SYSROOT=$XCODE_16_DEVELOPER_DIR/Platforms/MacOSX.platform/Developer/SDKs/${{ matrix.sdk-version }} echo "MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET" >> $GITHUB_ENV - echo "SDKROOT=$SDKROOT" >> $GITHUB_ENV echo "CMAKE_OSX_SYSROOT=$CMAKE_OSX_SYSROOT" >> $GITHUB_ENV - run: mvn -P "${{ matrix.profile }}" --batch-mode + # The build rewrites the committed JNI header when SerialNativeInterface.java changed it (ant/build.xml) + - name: Check the JNI header is committed + run: git diff --exit-code -- src/main/cpp/jssc_SerialNativeInterface.h windows: runs-on: [windows-latest] @@ -107,3 +111,6 @@ jobs: distribution: temurin - run: mvn -P "${{ matrix.profile }}" --batch-mode + # The build rewrites the committed JNI header when SerialNativeInterface.java changed it (ant/build.xml) + - name: Check the JNI header is committed + run: git diff --exit-code -- src/main/cpp/jssc_SerialNativeInterface.h diff --git a/.github/workflows/cross-compile.yml b/.github/workflows/cross-compile.yml index fc7c7d12d..be2ff6b57 100644 --- a/.github/workflows/cross-compile.yml +++ b/.github/workflows/cross-compile.yml @@ -12,6 +12,8 @@ env: GITHUB_BOT_NAME: github-actions GITHUB_BOT_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com DOCK_CROSS_TAG: 20231208-4e4d3ad + # The targets the matrices below build, checked by PrecompiledNativesTest + NATIVE_TARGETS: linux_32,linux_64,linux_arm,linux_arm64,linux_ppc,linux_riscv32,linux_riscv64,osx_64,osx_arm64,windows_32,windows_64,windows_arm64 jobs: create-branch: @@ -135,7 +137,7 @@ jobs: while git pull --rebase && ! git push; do sleep 5; done macos: - runs-on: macos-14 + runs-on: macos-15 needs: create-branch strategy: matrix: @@ -170,10 +172,8 @@ jobs: - name: Set SDK version run: | export MACOSX_DEPLOYMENT_TARGET=${{ matrix.macos-deployment-target }} - export SDKROOT=$XCODE_16_DEVELOPER_DIR/Platforms/MacOSX.platform/Developer/SDKs/${{ matrix.sdk-version }} - export CMAKE_OSX_SYSROOT=$SDKROOT + export CMAKE_OSX_SYSROOT=$XCODE_16_DEVELOPER_DIR/Platforms/MacOSX.platform/Developer/SDKs/${{ matrix.sdk-version }} echo "MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET" >> $GITHUB_ENV - echo "SDKROOT=$SDKROOT" >> $GITHUB_ENV echo "CMAKE_OSX_SYSROOT=$CMAKE_OSX_SYSROOT" >> $GITHUB_ENV - name: Build with Maven run: | @@ -204,4 +204,16 @@ jobs: git reset --soft ${{ needs.create-branch.outputs.base_rev }} git add src/main/resources-precompiled/** git commit -m "Precompiled natives @${{ needs.create-branch.outputs.base_rev }}" - git push -f + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: 11 + distribution: temurin + # Same check as release:prepare (PrecompiledNativesTest): a missing or stale native fails here, before the squashed branch is pushed + - name: Verify the natives + run: | + mvn -B -P package test -Djssc.native.targets=${NATIVE_TARGETS} \ + -Djssc.build.rev=${{ needs.create-branch.outputs.base_rev }} \ + -Djssc.build.origin=${{ github.repository }} \ + -Djssc.native.release=${{ inputs.release }} + - run: git push -f diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index 641ba991a..ec8de5e05 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -11,12 +11,20 @@ env: JAVA_TOOL_OPTIONS: -Duser.name=io.github.java-native GITHUB_BOT_NAME: github-actions GITHUB_BOT_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + # The targets a release ships, checked by PrecompiledNativesTest; keep in step with cross-compile.yml + NATIVE_TARGETS: linux_32,linux_64,linux_arm,linux_arm64,linux_ppc,linux_riscv32,linux_riscv64,osx_64,osx_arm64,windows_32,windows_64,windows_arm64 jobs: maven-central-release: runs-on: ubuntu-latest + env: + # Passed through the environment, not substituted into the scripts, so the input cannot inject commands + REQUESTED_RELEASE_VERSION: ${{ github.event.inputs.releaseVersion }} steps: - uses: actions/checkout@v3 + with: + # PrecompiledNativesTest, run by release:prepare, diffs the sources against the revision the natives were built from + fetch-depth: 0 - run: git config --global user.email "${GITHUB_BOT_EMAIL}" - run: git config --global user.name "${GITHUB_BOT_NAME}" - name: Set up JDK 11 @@ -30,9 +38,12 @@ jobs: gpg-private-key: ${{ secrets.JAVA_NATIVE_PGP_KEY }} - name: Set release version if provided as input if: github.event.inputs.releaseVersion != '' - run: echo "VERSIONS=-DreleaseVersion=${{ github.event.inputs.releaseVersion }}" >> $GITHUB_ENV + run: echo "VERSIONS=-DreleaseVersion=${REQUESTED_RELEASE_VERSION}" >> $GITHUB_ENV - name: Publish artifacts to Maven Central - run: mvn -B release:prepare release:perform -P package,maven-central-release ${VERSIONS} + # Only natives built by this repository for NATIVE_TARGETS may ship (PrecompiledNativesTest checks them) + run: >- + mvn -B release:prepare release:perform -P package,maven-central-release ${VERSIONS} + "-Djssc.release.arguments=-Djssc.build.origin=${{ github.repository }} -Djssc.native.targets=${NATIVE_TARGETS}" env: MAVEN_CENTRAL_PUBLISHING_TOKEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_PUBLISHING_TOKEN_USERNAME }} MAVEN_CENTRAL_PUBLISHING_TOKEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PUBLISHING_TOKEN_PASSWORD }} diff --git a/CMakeLists.txt b/CMakeLists.txt index b0dde9172..2bba9545f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -205,6 +205,12 @@ endif() if(NOT JSSC_BUILD_ORIGIN) set(JSSC_BUILD_ORIGIN "local") endif() +# The ident is space separated key=value pairs of printable ASCII +foreach(IDENT_FIELD JSSC_VERSION JSSC_BUILD_REV JSSC_BUILD_ORIGIN) + if(NOT "${${IDENT_FIELD}}" MATCHES "^[A-Za-z0-9._+/-]+$") + message(FATAL_ERROR "${IDENT_FIELD} \"${${IDENT_FIELD}}\" must be printable ASCII without spaces or quotes") + endif() +endforeach() set(JSSC_BUILD_INFO "version=${JSSC_VERSION} rev=${JSSC_BUILD_REV} target=${NATIVE_LIB_DIR} origin=${JSSC_BUILD_ORIGIN}") set(JSSC_IDENT "@(#)JSSC-IDENT ${JSSC_BUILD_INFO}") message(STATUS "Native library ident: ${JSSC_IDENT}") diff --git a/ant/build.xml b/ant/build.xml index 9d3c69689..d8a706ca2 100644 --- a/ant/build.xml +++ b/ant/build.xml @@ -1,6 +1,6 @@ - + - + @@ -8,7 +8,11 @@ + + + + @@ -159,8 +163,15 @@ + + + + + + - + @@ -192,17 +203,23 @@ + + + + + + + + + + - - - - - - - - - - + + + + + @@ -211,14 +228,20 @@ - Tests will run only if the TARGET and HOST match:${line.separator}${line.separator} + All tests will run only if the TARGET and HOST match:${line.separator}${line.separator} TARGET: ${os.target.classifier} DETECTED: ${os.detected.classifier} - - + + + + + + + + ===== ${maven.test.message} ===== @@ -258,15 +281,7 @@ - - - - - - - - + @@ -292,6 +307,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 129f68768..5af0e0e0c 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,11 @@ false true true + true + + + + 4.13.2 @@ -327,12 +332,42 @@ ${project.version} + + + ${maven.nativelibdir.path} + + ${jssc.native.version} + + ${jssc.build.rev} + + ${jssc.build.origin} + ${jssc.native.targets} + ${cmake.build.skip} + + + + ${maven.include.tests} + ${maven.exclude.tests} + + **/PrecompiledNativesTest.java + + + + verify-natives + test + + ${jssc.verify.natives.skip} + + PrecompiledNativesTest + + + @@ -500,12 +535,26 @@ + + + verify-natives + + + jssc.native.targets + + + + false + + + package true - true + + true true true @@ -714,6 +763,10 @@ false true deploy + + -Djssc.verify.natives.skip=false ${jssc.release.arguments} + + false diff --git a/src/main/cpp/jssc_SerialNativeInterface.h b/src/main/cpp/jssc_SerialNativeInterface.h index c202f0311..29556a2e4 100644 --- a/src/main/cpp/jssc_SerialNativeInterface.h +++ b/src/main/cpp/jssc_SerialNativeInterface.h @@ -15,6 +15,8 @@ extern "C" { #define jssc_SerialNativeInterface_OS_SOLARIS 2L #undef jssc_SerialNativeInterface_OS_MAC_OS_X #define jssc_SerialNativeInterface_OS_MAC_OS_X 3L +#undef jssc_SerialNativeInterface_OS_UNKNOWN +#define jssc_SerialNativeInterface_OS_UNKNOWN -1L #undef jssc_SerialNativeInterface_ERR_PORT_BUSY #define jssc_SerialNativeInterface_ERR_PORT_BUSY -1LL #undef jssc_SerialNativeInterface_ERR_PORT_NOT_FOUND diff --git a/src/test/java/jssc/NativeIdent.java b/src/test/java/jssc/NativeIdent.java new file mode 100644 index 000000000..ad06c5480 --- /dev/null +++ b/src/test/java/jssc/NativeIdent.java @@ -0,0 +1,69 @@ +package jssc; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Reads the JSSC-IDENT line (see src/main/cpp/ident.cpp) of a native library without + * loading it + */ +final class NativeIdent { + + static final List LIBRARY_NAMES = Arrays.asList("libjssc.so", "libjssc.dylib", "jssc.dll"); + private static final String IDENT_MARKER = "JSSC-IDENT "; + + private NativeIdent() { + } + + /** + * Finds the one printable JSSC-IDENT key=value ... line in the binary + */ + static Map parse(File library) throws IOException { + final InputStream in = new FileInputStream(library); + final String content; + try { + content = new String(readFully(in), "ISO-8859-1"); + } finally { + in.close(); + } + final int start = content.indexOf(IDENT_MARKER); + assertTrue("No " + IDENT_MARKER + "in " + library, start >= 0); + assertEquals("More than one " + IDENT_MARKER + "in " + library, -1, content.indexOf(IDENT_MARKER, start + 1)); + int end = start; + while (end < content.length() && content.charAt(end) >= 0x20 && content.charAt(end) < 0x7f) { + end++; + } + final String line = content.substring(start + IDENT_MARKER.length(), end); + final Map fields = new HashMap(); + for (String field : line.split(" ")) { + final int eq = field.indexOf('='); + assertTrue("Malformed field '" + field + "' in " + line, eq > 0); + fields.put(field.substring(0, eq), field.substring(eq + 1)); + } + return fields; + } + + static byte[] readFully(InputStream in) throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final byte[] buffer = new byte[8192]; + int n; + while ((n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + } + return out.toByteArray(); + } + + static boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } +} diff --git a/src/test/java/jssc/NativeIdentTest.java b/src/test/java/jssc/NativeIdentTest.java new file mode 100644 index 000000000..23f94de56 --- /dev/null +++ b/src/test/java/jssc/NativeIdentTest.java @@ -0,0 +1,76 @@ +package jssc; + +import jssc.junit.rules.DisplayMethodNameRule; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static jssc.NativeIdent.isBlank; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +/** + * Checks the JSSC-IDENT line ({@link NativeIdent}) of the native library built in this run against the + * jssc.expected.* properties. The library is read, not loaded, so any target can be checked. + */ +public class NativeIdentTest extends DisplayMethodNameRule { + + private File nativeDir; + private Map ident; + + @Before + public void readIdent() throws IOException { + final String dir = System.getProperty("jssc.expected.native.dir"); + assumeTrue("jssc.expected.native.dir is not set; skipping", !isBlank(dir)); + assumeFalse("jssc.native.build.skip is set; skipping", Boolean.getBoolean("jssc.native.build.skip")); + nativeDir = new File(dir); + ident = NativeIdent.parse(findLibrary(nativeDir)); + } + + @Test + public void versionIsTheNativeVersion() { + assertEquals("version", System.getProperty("jssc.expected.native.version"), ident.get("version")); + } + + @Test + public void targetIsTheOutputDirectory() { + assertEquals("target", nativeDir.getName(), ident.get("target")); + } + + @Test + public void revIsThePassedRevision() { + final String expected = System.getProperty("jssc.expected.build.rev"); + if (isBlank(expected)) { + // Not given: taken from git + final String rev = ident.get("rev"); + assertTrue("rev is " + rev, rev != null && rev.matches("[0-9a-f]{4,40}(-dirty)?|unknown")); + } else { + assertEquals("rev", expected, ident.get("rev")); + } + } + + @Test + public void originIsThePassedOrigin() { + final String expected = System.getProperty("jssc.expected.build.origin"); + assertEquals("origin", isBlank(expected) ? "local" : expected, ident.get("origin")); + } + + private static File findLibrary(File dir) { + final List found = new ArrayList(); + for (String name : NativeIdent.LIBRARY_NAMES) { + final File file = new File(dir, name); + if (file.isFile()) { + found.add(file); + } + } + assertEquals("Native libraries in " + dir + ": " + found, 1, found.size()); + return found.get(0); + } +} diff --git a/src/test/java/jssc/PrecompiledNativesTest.java b/src/test/java/jssc/PrecompiledNativesTest.java new file mode 100644 index 000000000..6be7906bb --- /dev/null +++ b/src/test/java/jssc/PrecompiledNativesTest.java @@ -0,0 +1,229 @@ +package jssc; + +import jssc.junit.rules.DisplayMethodNameRule; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import static jssc.NativeIdent.isBlank; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +/** + * Checks the JSSC-IDENT ({@link NativeIdent}) of each native library under natives/ on the + * classpath: + *
    + *
  1. Every target in jssc.expected.native.targets (required, comma separated) has exactly one + * library with exactly one ident; there are no other directories.
  2. + *
  3. version is jssc.expected.native.version.
  4. + *
  5. target is the directory the library sits in.
  6. + *
  7. All share one rev (jssc.expected.build.rev, if set) and one origin + * (jssc.expected.build.origin, if set); no "-dirty" or + * "unknown" rev and no "origin=local".
  8. + *
  9. rev is an ancestor of HEAD and the native sources are unchanged since. Needs the git history back to + * rev.
  10. + *
+ * Committed natives are usually stale between releases, so expect failures outside of a release. + */ +public class PrecompiledNativesTest extends DisplayMethodNameRule { + + private static final Logger LOG = LogManager.getLogger(PrecompiledNativesTest.class); + + // src/main/cpp holds the JNI header, so a JNI change counts too + private static final String[] NATIVE_SOURCES = {"src/main/cpp", "toolchain", "CMakeLists.txt"}; + + // The targets the caller expects, each a directory under natives/ + private static final Set targets = new TreeSet(); + private static File nativesDir; + // The idents of the targets whose library could be read; the others are in unreadable + private static final Map> idents = new TreeMap>(); + private static final List unreadable = new ArrayList(); + + @BeforeClass + public static void readIdents() throws Exception { + final String expectedTargets = System.getProperty("jssc.expected.native.targets"); + assertTrue("jssc.expected.native.targets is not set: name the targets to check, e.g." + + " -Djssc.native.targets=linux_64,osx_arm64", !isBlank(expectedTargets)); + targets.addAll(Arrays.asList(expectedTargets.trim().split("\\s*,\\s*"))); + LOG.info("Targets: {}", targets); + final URL natives = PrecompiledNativesTest.class.getClassLoader().getResource("natives"); + assertNotNull("No natives on the classpath", natives); + assertEquals("Natives on the classpath are not a directory: " + natives, "file", natives.getProtocol()); + nativesDir = new File(natives.toURI()); + for (String target : targets) { + final File[] files = new File(nativesDir, target).listFiles(); + if (files == null || files.length == 0) { + unreadable.add(target + ": no native library in " + new File(nativesDir, target)); + } else if (files.length != 1) { + unreadable.add(target + ": expected one library, found " + Arrays.toString(files)); + } else if (!NativeIdent.LIBRARY_NAMES.contains(files[0].getName())) { + unreadable.add(target + ": " + files[0] + " is not one of " + NativeIdent.LIBRARY_NAMES); + } else { + try { + idents.put(target, NativeIdent.parse(files[0])); + } catch (AssertionError e) { + unreadable.add(target + ": " + e.getMessage()); + } + } + } + for (Map.Entry> entry : idents.entrySet()) { + LOG.info("{}: {}", entry.getKey(), entry.getValue()); + } + } + + @Test + public void everyTargetHasOneLibraryWithOneIdent() { + assertNoProblems(unreadable); + } + + @Test + public void onlyTheTargetsAreShipped() { + final List problems = new ArrayList(); + for (File dir : nativesDir.listFiles()) { + if (!targets.contains(dir.getName())) { + problems.add(dir + " is not one of the targets " + targets); + } + } + assertNoProblems(problems); + } + + @Test + public void versionIsTheNativeVersion() { + final String expected = System.getProperty("jssc.expected.native.version"); + assumeTrue("jssc.expected.native.version is not set; skipping", !isBlank(expected)); + final List problems = new ArrayList(); + for (Map.Entry> entry : idents.entrySet()) { + final String version = entry.getValue().get("version"); + if (!expected.equals(version)) { + problems.add(entry.getKey() + ": version is " + version + ", expected " + expected); + } + } + assertNoProblems(problems); + } + + @Test + public void targetIsTheDirectory() { + final List problems = new ArrayList(); + for (Map.Entry> entry : idents.entrySet()) { + final String target = entry.getValue().get("target"); + if (!entry.getKey().equals(target)) { + problems.add(entry.getKey() + ": ident says target=" + target); + } + } + assertNoProblems(problems); + } + + @Test + public void allComeFromOneCiRun() { + final String expectedRev = System.getProperty("jssc.expected.build.rev"); + final String expectedOrigin = System.getProperty("jssc.expected.build.origin"); + final List problems = new ArrayList(); + for (Map.Entry> entry : idents.entrySet()) { + final String target = entry.getKey(); + final String rev = entry.getValue().get("rev"); + final String origin = entry.getValue().get("origin"); + if (!isTraceable(rev)) { + problems.add(target + ": rev=" + rev + " is not a commit"); + } else if (!isBlank(expectedRev) && !expectedRev.equals(rev)) { + problems.add(target + ": rev is " + rev + ", expected " + expectedRev); + } + if (origin == null || origin.equals("local")) { + problems.add(target + ": origin=" + origin); + } else if (!isBlank(expectedOrigin) && !expectedOrigin.equals(origin)) { + problems.add(target + ": origin is " + origin + ", expected " + expectedOrigin); + } + } + final Set revs = distinct("rev"); + if (revs.size() > 1) { + problems.add("natives come from different revisions: " + revs); + } + final Set origins = distinct("origin"); + if (origins.size() > 1) { + problems.add("natives come from different origins: " + origins); + } + assertNoProblems(problems); + } + + @Test + public void nativeSourcesAreUnchangedSinceRev() throws Exception { + assertNotNull("Cannot read the git history: not a git checkout, or git is missing", git("rev-parse", "HEAD")); + final List problems = new ArrayList(); + for (String rev : distinct("rev")) { + if (!isTraceable(rev)) { + continue; // see allComeFromOneCiRun + } + if (git("rev-parse", "-q", "--verify", rev + "^{commit}") == null) { + problems.add("rev " + rev + " is not in the git history (shallow clone?)"); + } else if (git("merge-base", "--is-ancestor", rev, "HEAD") == null) { + problems.add("natives were built from " + rev + ", which is not part of this branch"); + } else { + final List diff = new ArrayList(Arrays.asList("diff", "--name-only", rev, "HEAD", "--")); + diff.addAll(Arrays.asList(NATIVE_SOURCES)); + final String changed = git(diff.toArray(new String[0])); + assertNotNull("git diff failed", changed); + if (!changed.isEmpty()) { + problems.add("native sources changed since " + rev + ": " + + changed.replace('\n', ' ')); + } + } + } + assertNoProblems(problems); + } + + private static boolean isTraceable(String rev) { + return rev != null && rev.matches("[0-9a-f]{4,40}"); + } + + private static Set distinct(String key) { + final Set values = new TreeSet(); + for (Map ident : idents.values()) { + values.add(String.valueOf(ident.get(key))); + } + return values; + } + + /** + * Runs git in the working directory + * + * @return its trimmed output, or null if it failed + */ + private static String git(String... args) throws IOException, InterruptedException { + final List command = new ArrayList(); + command.add("git"); + command.addAll(Arrays.asList(args)); + final Process process = new ProcessBuilder(command).start(); + process.getOutputStream().close(); + final InputStream in = process.getInputStream(); + final String output; + try { + output = new String(NativeIdent.readFully(in), "UTF-8").trim(); + } finally { + in.close(); + } + return process.waitFor() == 0 ? output : null; + } + + private static void assertNoProblems(Collection problems) { + final StringBuilder message = new StringBuilder(problems.size() + " problem(s) with the precompiled natives:"); + for (String problem : problems) { + message.append("\n ").append(problem); + } + assertTrue(message.toString(), problems.isEmpty()); + } +} diff --git a/src/test/java/jssc/SerialNativeInterfaceTest.java b/src/test/java/jssc/SerialNativeInterfaceTest.java index ffd789c99..4e3c5761e 100644 --- a/src/test/java/jssc/SerialNativeInterfaceTest.java +++ b/src/test/java/jssc/SerialNativeInterfaceTest.java @@ -57,6 +57,15 @@ public void libraryVersionIsFilteredFromPom() { assertThat(SerialNativeInterface.getLibraryVersion(), is(expectedVersion)); } + @Test + public void loadsTheNativeLibraryBuiltInThisRun() { + final String expectedVersion = System.getProperty("jssc.expected.native.version"); + Assume.assumeNotNull(expectedVersion); + assumeFalse("jssc.native.build.skip is set; skipping", Boolean.getBoolean("jssc.native.build.skip")); + assertThat("Loaded a native library other than the one built in this run (stale build output?)", + SerialNativeInterface.getNativeLibraryVersion(), is(expectedVersion)); + } + @Test(expected = java.io.IOException.class) public void reportsWriteErrorsAsIOException() throws Exception { Assume.assumeFalse(SerialNativeInterface.getOsType() == SerialNativeInterface.OS_WINDOWS);