Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 96 additions & 9 deletions .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
build-and-test:
name: Build and Test (Linux)
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 130

strategy:
fail-fast: false
Expand Down Expand Up @@ -252,7 +252,9 @@ jobs:
hdf.lib.dir=${{ env.HDF4LIB_PATH }}/lib

# Platform Configuration
platform.hdf.lib=${{ env.HDF5LIB_PATH }}/lib
# Both library directories. This value becomes -Djava.library.path for the test
# forks, which replaces the JVM's default.
platform.hdf.lib=${{ env.HDF5LIB_PATH }}/lib:${{ env.HDF4LIB_PATH }}/lib

# CI-specific settings
ci.build=true
Expand Down Expand Up @@ -282,10 +284,14 @@ jobs:

- name: Set up Xvfb for Headless GUI Testing
run: |
echo "Installing Xvfb and GTK dependencies for headless SWT testing..."
echo "Installing Xvfb, GTK and a window manager for headless SWT testing..."
sudo apt-get update
sudo apt-get install -y xvfb libgtk-3-0 libgtk-3-dev x11-xserver-utils
echo "Xvfb and GTK installed successfully"
# metacity: SWTBot resolves widgets against Display.getActiveShell(), and on a bare X
# server with no window manager nothing ever becomes active, so every lookup that waits
# on shell activation would time out. x11-utils supplies xdpyinfo and xprop for the
# readiness checks below.
sudo apt-get install -y xvfb libgtk-3-0 libgtk-3-dev x11-xserver-utils x11-utils metacity
echo "Xvfb, GTK and metacity installed successfully"

- name: Run Tests
run: |
Expand All @@ -300,9 +306,8 @@ jobs:
# Skip repository install (tries to install files that don't exist in CI)
mvn install -B -pl object,hdfview -DskipTests -Ddependency-check.skip=true

# Run object tests only (all 149 tests passing)
# HDFView UI tests disabled in CI - require real display, not Xvfb
# UI tests should be run locally before merging
# Object module tests. These are blocking - they have passed on all three
# OSes for a long time and a regression here is a real failure.
mvn test -B -pl object -Dmaven.test.failure.ignore=false
echo "::endgroup::"

Expand All @@ -323,13 +328,95 @@ jobs:
echo "Target artifacts:"
find target -name "*.jar" -type f 2>/dev/null | head -5 || echo "No target JARs found"

- name: Run UI Tests
# TBD: This step will not be able to fail the job until
# the underlying bugs that CI is detecting are fixed.
# Once that is done, remove continue-on-error.
continue-on-error: true
# Must stay above the surefire fork timeout and below the job timeout.
timeout-minutes: 90
run: |
echo "::group::Run UI Tests (SWTBot under Xvfb)"
# Started by hand rather than through xvfb-run because the window manager has to join
# the same display, which xvfb-run's single-command wrapper does not allow.
Xvfb :99 -screen 0 1280x1024x24 -nolisten tcp &
export DISPLAY=:99

for i in $(seq 1 30); do
xdpyinfo -display :99 >/dev/null 2>&1 && break
sleep 1
done
xdpyinfo -display :99 >/dev/null 2>&1 || { echo "Xvfb never accepted connections"; exit 1; }
echo "Xvfb is up on :99"

# Without a window manager no shell ever becomes active, and the waits on shell
# activation in closeFile() and openDataObject() time out and fail the test.
# Start metacity and wait for it to own the display before running the tests.
metacity --sm-disable --display=:99 &
for i in $(seq 1 20); do
xprop -display :99 -root _NET_SUPPORTING_WM_CHECK 2>/dev/null | grep -q "window id" && break
sleep 1
done
if xprop -display :99 -root _NET_SUPPORTING_WM_CHECK 2>/dev/null | grep -q "window id"; then
echo "metacity is managing :99"
else
echo "WARNING: no window manager on :99 - shell activation waits will time out"
fi

mvn test -B -pl hdfview
echo "::endgroup::"

- name: UI Test Summary
if: always()
continue-on-error: true
run: |
# Surface the counts in the job log even when the step above was cut short, so a
# run is never just a red X with no numbers attached.
python3 - <<'EOF' >> $GITHUB_STEP_SUMMARY
import glob, os, xml.etree.ElementTree as ET
files = glob.glob("hdfview/target/surefire-reports/*.xml")
print("## UI tests\n")
if not files:
print("No surefire XML was written - the suite did not get far enough to report.")
else:
tot = fail = err = skip = 0
rows = []
bad = []
for f in sorted(files):
# A fork killed by a timeout can leave a half-written XML behind. Report that
# file as unreadable rather than letting ParseError abort the whole summary
# halfway through the table, which looks like the suite simply stopped.
try:
r = ET.parse(f).getroot()
except Exception as exc:
bad.append((os.path.basename(f), type(exc).__name__))
continue
t = int(r.get("tests", 0)); fl = int(r.get("failures", 0))
e = int(r.get("errors", 0)); s = int(r.get("skipped", 0))
tot += t; fail += fl; err += e; skip += s
rows.append((r.get("name", f), t, fl, e, s))
print(f"**{tot} tests, {fail} failures, {err} errors, {skip} skipped** "
f"across {len(rows)} classes\n")
print("| Class | Tests | Failures | Errors | Skipped |")
print("| --- | --- | --- | --- | --- |")
for n, t, fl, e, s in rows:
print(f"| {n} | {t} | {fl} | {e} | {s} |")
for n, why in bad:
print(f"| {n} | unreadable ({why}) | | | |")
EOF
cat $GITHUB_STEP_SUMMARY

# Scoped to the object module on purpose. This action publishes a check run, and a check
# is not covered by the step's continue-on-error - so globbing '**' would put the
# UI failures into a failing check on every PR. The UI tier reports through the
# summary step above and the uploaded surefire artifacts below.
- name: Test Report
uses: dorny/test-reporter@v1
if: success() || failure()
continue-on-error: true
with:
name: Maven Test Results (Linux)
path: '**/target/surefire-reports/*.xml'
path: 'object/target/surefire-reports/*.xml'
reporter: java-junit
fail-on-error: false

Expand Down
12 changes: 12 additions & 0 deletions hdfview/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,18 @@
<systemPropertyVariables>
<hdfview.workdir>${project.build.testOutputDirectory}/uitest</hdfview.workdir>
</systemPropertyVariables>
<!--
One JVM per test class. Each UI test class starts its own HDFView window,
so a class that hangs or crashes the VM would otherwise take every remaining
class down with as well and leave no surefire XML behind.
-->
<reuseForks>false</reuseForks>
<!--
Per-class cap for hangs the JUnit timeouts can't interrupt. Must exceed the
sum of a class's JUnit timeouts (worst case TestHDFViewIntConversions, 56m),
or a killed fork loses that class's results.
-->
<forkedProcessTimeoutInSeconds>3600</forkedProcessTimeoutInSeconds>
</configuration>
</plugin>

Expand Down
66 changes: 55 additions & 11 deletions hdfview/src/test/java/uitest/AbstractWindowTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import java.util.Vector;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import hdf.HDFVersions;
import hdf.view.HDFView;
Expand Down Expand Up @@ -89,6 +91,15 @@ public abstract class AbstractWindowTest {

private static final CyclicBarrier swtBarrier = new CyclicBarrier(2);

/*
* How long a test waits for the main window to come up. Shorter than the 2 minute JUnit
* default so a failed launch reports its own message rather than a generic timeout.
*/
private static final int APP_STARTUP_TIMEOUT_SECONDS = 60;

/* Why the UI thread died, if it did, so that waiters can report the real cause. */
private static volatile Throwable appStartupFailure = null;

private static int TEST_DELAY = 10;

private static int open_files = 0;
Expand All @@ -102,11 +113,11 @@ protected static enum FILE_MODE { READ_ONLY, READ_WRITE, MULTI_READ_ONLY }
private static final String objectShellTitleRegex = ".*at.*\\[.*in.*\\]";

@BeforeEach
public final void setupSWTBot(TestInfo testInfo) throws InterruptedException, BrokenBarrierException
public final void setupSWTBot(TestInfo testInfo) throws InterruptedException
{
this.testInfo = testInfo;
// synchronize with the thread opening the shell
swtBarrier.await();
awaitAppWindow();
bot = new SWTBot();

SWTBotPreferences.PLAYBACK_DELAY = TEST_DELAY;
Expand All @@ -119,9 +130,33 @@ public void run()
});
}

/**
* Waits for the UI thread to open this test's HDFView window. Fails the test if the
* window doesn't open within APP_STARTUP_TIMEOUT_SECONDS, or if the UI thread dies first.
*/
private static void awaitAppWindow() throws InterruptedException
{
try {
swtBarrier.await(APP_STARTUP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
catch (TimeoutException e) {
fail("HDFView main window did not open within " + APP_STARTUP_TIMEOUT_SECONDS + " seconds",
appStartupFailure);
}
catch (BrokenBarrierException e) {
fail("HDFView UI thread died before the main window opened", appStartupFailure);
}
}

@AfterEach
public void closeShell() throws InterruptedException
{
// Nothing to tear down if the window never opened. Bail out before touching Display,
// since Display.getDefault() creates a display when none exists, and on a machine with no
// usable X server that call will block forever inside the native gdk_threads_enter().
if (shell == null || shell.isDisposed())
return;

// close the shell
Display.getDefault().syncExec(new Runnable() {
@Override
Expand Down Expand Up @@ -199,17 +234,26 @@ public void run()
window.runMainWindow();
}
}
catch (Exception e) {
e.printStackTrace();
catch (Throwable t) {
// Record the real cause and break the barrier so that any test parked
// in awaitAppWindow() fails immediately with this stack trace, rather
// than waiting out the startup timeout for a window that will never
// arrive. Throwable, not Exception: a linkage or native-library error
// during SWT startup is exactly the case that used to hang CI.
appStartupFailure = t;
t.printStackTrace();
swtBarrier.reset();
}

Display.getDefault().syncExec(new Runnable() {
@Override
public void run()
{
shell.getDisplay().dispose();
}
});
if (shell != null) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run()
{
shell.getDisplay().dispose();
}
});
}
}
});
uiThread.setDaemon(true);
Expand Down
10 changes: 10 additions & 0 deletions hdfview/src/test/java/uitest/TestHDFViewIntConversions.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import static org.junit.jupiter.api.Assertions.fail;

import java.io.File;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

import org.eclipse.swtbot.nebula.nattable.finder.widgets.SWTBotNatTable;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
Expand Down Expand Up @@ -466,7 +468,9 @@ public void checkHDF5GroupDU16()
}
}

// Walks every cell of a large dataset: ~186s locally, slower on CI runners.
@Test
@Timeout(value = 12, unit = TimeUnit.MINUTES)
public void checkHDF5GroupDS32()
{
String[][] expectedData = {
Expand Down Expand Up @@ -772,7 +776,9 @@ public void checkHDF5GroupDS32()
}
}

// Walks every cell of a large dataset: ~186s locally, slower on CI runners.
@Test
@Timeout(value = 12, unit = TimeUnit.MINUTES)
public void checkHDF5GroupDU32()
{
String[][] expectedData = {
Expand Down Expand Up @@ -1075,7 +1081,9 @@ public void checkHDF5GroupDU32()
}
}

// Walks every cell of a large dataset: ~372s locally, slower on CI runners.
@Test
@Timeout(value = 12, unit = TimeUnit.MINUTES)
public void checkHDF5GroupDS64()
{
String[][] expectedData = {{"-1",
Expand Down Expand Up @@ -2386,7 +2394,9 @@ public void checkHDF5GroupDS64()
}
}

// Walks every cell of a large dataset: ~372s locally, slower on CI runners.
@Test
@Timeout(value = 12, unit = TimeUnit.MINUTES)
public void checkHDF5GroupDU64()
{
String[][] expectedData = {
Expand Down
4 changes: 4 additions & 0 deletions hdfview/src/test/java/uitest/TestTreeViewFilters.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import static org.junit.jupiter.api.Assertions.fail;

import java.io.File;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

import org.eclipse.swtbot.nebula.nattable.finder.widgets.SWTBotNatTable;
import org.eclipse.swtbot.swt.finder.waits.Conditions;
Expand Down Expand Up @@ -39,7 +41,9 @@ public class TestTreeViewFilters extends AbstractWindowTest {
{"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"}};
// Walks every cell of a large dataset: ~318s locally, slower on CI runners.
@Test
@Timeout(value = 12, unit = TimeUnit.MINUTES)
public void openHDF5Filters()
{
SWTBotShell tableShell = null;
Expand Down
4 changes: 4 additions & 0 deletions hdfview/src/test/resources/junit-platform.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Default timeout for every UI test and lifecycle method, so that a hung test merely fails
# without blocking. Tests that legitimately run longer can override it with @Timeout. This cannot
# interrupt a blocked native call.
junit.jupiter.execution.timeout.default = 2 m
Loading