Repository navigation
230 lines (218 loc) · 11.6 KB
/
Copy pathjava-bindings.yml
File metadata and controls
230 lines (218 loc) · 11.6 KB
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
name: Java Bindings
# The JNI binding's JUnit suite, on a real document, on every wave of the
# ephemeral lane.
#
# Until 02-09-2026 nothing ran `mvn test` for crates/pdf-java. bindings.yml has
# a Java job, but it fires on release tags only and runs `cargo test`, which
# never loads the library from Java. So the Java classes could call
# System.loadLibrary("pdf_java") while Cargo.toml built libpdfluent_java, and
# every test errored with UnsatisfiedLinkError on every platform for months
# without anybody seeing it (pdfluent/pdfluent-internal#310). The rule this
# repository writes down -- a binding's test calls the method on a real
# document AND runs in CI -- was met on the first half only.
#
# Debug rather than --release: the suite asks whether the binding loads and
# answers, not how fast. The pom defaults to target/release for a local release
# build; the override below points Surefire's forked JVM at target/debug
# instead.
# CALLED, NOT TRIGGERED (#350).
#
# It ran on every push to master, on the desktop. That was the cheap answer
# while the alternative was hosted minutes -- and it stopped being cheap when
# five suites made the same choice: measured 06-09-2026 at 14:00Z, twenty-nine
# runs queued or running on one four-core machine and the queue cancelled by
# hand. A cargo build plus a Maven run is not a thing to do forty to ninety
# times a day next to four other suites doing the same.
#
# So this is a reusable workflow now. `ci-ephemeral.yml` calls it on a
# two-hourly wave, on the throwaway instance that wave buys. `runner_label` has
# no default: the only label to hand would be the desktop's, and that is what
# this change is about.
#
# The concurrency block went with the trigger. A reusable workflow inherits the
# caller's, and ci-ephemeral's is the one that decides whether a wave supersedes
# the one before it.
on:
workflow_call:
inputs:
runner_label:
description: The throwaway instance to run on, from the create-runner job of ci-ephemeral.
required: true
type: string
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
jobs:
jni-tests:
name: JNI tests (mvn test)
runs-on: ${{ inputs.runner_label }}
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
# THE TOOLCHAIN, INSTALLED BY THE JOB THAT NEEDS IT (#495).
#
# This job used to take cargo from the image's PATH, and said so. On
# 09-09-2026 the wave's instance was created from the sealed warm image
# (#482) and three reusable jobs died within minutes of each other on
# `cargo: command not found` -- this one after `Command failed: rustc -vV`
# from the cache action, which needs the compiler before it can key
# anything (wave 34365955374). `workspace`, on the same instance and the
# same image, compiled: it installs a toolchain of its own.
#
# So the image's PATH stops being a contract these jobs rely on. It is
# still repaired on the image side -- the seal writes /etc/profile.d and
# refuses to photograph a machine where `cargo --version` fails
# non-interactively -- but a job that compiles now carries its own
# toolchain, because an image is a thing that changes overnight and a
# step is a thing that is read in review.
#
# BEFORE the cache action, not after: `rustc -vV` is the first thing it
# runs. And the bin directory is put on PATH by hand for the reason
# ci-ephemeral's workspace job does it -- /usr/local/bin/cargo on this
# image is a plain binary that shadows rustup's shims, so `rustup which
# cargo` is the only answer that is about the toolchain just installed.
- uses: dtolnay/rust-toolchain@d1031067263f94b142dd6c0ce24c5eb9d02d52a0 # master 03-09-2026
with:
toolchain: stable
- name: Put the pinned toolchain's bin directory on PATH
run: |
set -euo pipefail
toolchain_bin="$(dirname "$(rustup which cargo)")"
echo "$toolchain_bin" >> "$GITHUB_PATH"
echo "cargo comes from $toolchain_bin"
"$toolchain_bin/cargo" --version
# The desktop had its own JDK and Maven and this workflow deliberately used
# them. A throwaway instance has neither: the snapshot is a Rust CI image.
# So the JDK is installed here, and Maven comes from apt -- both on a
# machine that is deleted afterwards, which is why installing over it
# costs nothing and hides nothing.
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Maven on the instance
run: |
set -euo pipefail
if ! command -v mvn >/dev/null 2>&1; then
# The runner user on this image may already be root, in which case
# there is no sudo to call and asking for one fails the step for the
# wrong reason. Same shape as ci-ephemeral's veraPDF install.
bash scripts/ci/apt_get.sh update -qq
bash scripts/ci/apt_get.sh install -y -qq maven
fi
# Kept from the desktop version, and it is not a formality here either: a
# job that silently does something else after an install failure is worse
# than one that stops, and `mvn` reporting nothing is how an
# UnsatisfiedLinkError three steps later gets blamed on the binding.
- name: The runner has a JDK and Maven
run: |
set -euo pipefail
missing=""
command -v java >/dev/null 2>&1 || missing="java"
command -v mvn >/dev/null 2>&1 || missing="${missing:+$missing and }mvn"
if [ -n "$missing" ]; then
echo "::error::$missing not on PATH for this instance. The JNI suite needs a JDK 17+ and Maven; the steps above are what installs them, so this is an install that failed rather than a machine that lacks them." >&2
exit 1
fi
java -version
mvn --version | head -1
- name: Build the JNI library
run: cargo build -p pdf-java --locked
# Checked by name before Maven starts. A missing or misnamed library
# surfaces inside the JVM as UnsatisfiedLinkError wrapped in
# ExceptionInInitializerError and NoClassDefFoundError, which points at
# the tests rather than at the build. This is the exact fault the
# workflow exists to catch, so it is named here rather than left to be
# read out of a stack trace.
- name: The library the Java side loads is the one cargo built
run: |
set -euo pipefail
test -f target/debug/libpdfluent_java.so || {
echo "::error::target/debug/libpdfluent_java.so was not built; Cargo.toml [lib] name and System.loadLibrary(...) must agree" >&2
ls -la target/debug/*.so 2>/dev/null || true
exit 1
}
# Derived, not listed. Three files stood here by hand while five in this
# crate call System.loadLibrary -- and two of the three uncovered ones
# arrived in the same change that added this check. A hand-kept list stops
# matching the tree the moment somebody adds a file, which is the failure
# this step exists to catch.
#
# Bounded to crates/pdf-java deliberately: bindings/java's NativeLoader
# calls System.loadLibrary(JNI_LIB_NAME) through a constant, so a literal
# grep would call it wrong when it is right.
mapfile -t loaders < <(grep -rl 'System\.loadLibrary' crates/pdf-java --include='*.java' | sort)
# A floor: an empty glob would walk the loop below zero times and pass
# without reading a single file. It is also a ratchet -- a caller that
# disappears takes the count below the floor and is reported.
#
# FOUR, measured 07-09-2026, and it was five. The fifth was
# crates/pdf-java/src/test/java/com/pdfluent/PdfluentLicensing.java,
# deleted on purpose in f9ae3a1d7 with the licence key and every check
# that read one (#226). So the caller was not lost; the number was
# stale, and it had been red on master and in the wave ever since --
# a ratchet reporting a deletion that was the point of the change.
# Lowered with the measurement rather than removed: the empty-glob
# case is still the reason this line exists. (#361)
if [ "${#loaders[@]}" -lt 4 ]; then
echo "::error::found ${#loaders[@]} System.loadLibrary caller(s) under crates/pdf-java, expected at least 4; either the search is broken or a caller was deleted -- say which in the commit that lowers this number" >&2
exit 1
fi
for f in "${loaders[@]}"; do
grep -q 'System.loadLibrary("pdfluent_java")' "$f" || {
echo "::error::$f does not load pdfluent_java" >&2; exit 1; }
done
- name: Rust unit tests of the binding
run: cargo test -p pdf-java --locked
# Not -q: the "Tests run: N, Failures: F, Errors: E" lines are the
# evidence that tests ran, and -q hides them. failIfNoTests is set in the
# pom, so an empty run fails instead of reporting green.
- name: JUnit suite against the built library
run: |
set -euo pipefail
mvn --batch-mode --no-transfer-progress \
-f crates/pdf-java/pom.xml \
-Dnative.lib.dir="$GITHUB_WORKSPACE/target/debug" \
clean test
# Surefire's own XML is the record. Two floors, not one sum: a total of 5
# was satisfied by the licence suite alone (12), so deleting the document
# suite -- the one this workflow exists for -- stayed green. The document
# report must therefore be present with its 6 tests. (review of #1672)
#
# The total was 18, the two suites added up. There is one suite now: the
# licence suite was removed on purpose by f9ae3a1d7 (#199, "remove the
# licence key, and every check that read one"), and the floor kept
# counting it. Wave 34386968239 was the first since #350 to get this job
# past the compiler, and it failed on a suite nobody deleted by accident.
#
# So the total is 6, which is what runs. It carries no independent weight
# while there is one suite -- the per-suite floor above is the real guard
# -- and it is here to be RAISED by whoever adds the second suite, the way
# this one should have been LOWERED by whoever removed it. A floor that
# names a suite that no longer exists reports a deletion that never
# happened, which costs a wave to read and teaches everyone to distrust
# the next one. (#495)
- name: The document suite ran, and so did the rest
run: |
set -euo pipefail
python3 - <<'PY'
import glob, sys, xml.etree.ElementTree as ET
reports = "crates/pdf-java/target/surefire-reports"
doc = f"{reports}/TEST-com.pdfluent.PdfluentDocumentJniTest.xml"
per = {f: int(ET.parse(f).getroot().get("tests", "0"))
for f in glob.glob(f"{reports}/TEST-*.xml")}
total = sum(per.values())
for f, n in sorted(per.items()):
print(f" {n:3d} {f.rsplit('/', 1)[-1]}")
print(f"surefire reports {total} test(s) in {len(per)} report(s)")
bad = []
if doc not in per:
bad.append(f"{doc.rsplit('/', 1)[-1]} is missing: the document suite did not run")
elif per[doc] < 6:
bad.append(f"the document suite ran {per[doc]} test(s), floor is 6")
if total < 6:
bad.append(f"{total} test(s) in total, floor is 6")
for b in bad:
print(f"::error::{b}", file=sys.stderr)
sys.exit(1 if bad else 0)
PY