Skip to content
Closed
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
14 changes: 14 additions & 0 deletions cr-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ dependencies {
pmd("net.sourceforge.pmd:pmd-java:7.0.0-rc4")

implementation("org:jpastebin:1.0.1")
// jpastebin needs these at runtime (see its own embedded META-INF/maven/org/jpastebin/pom.xml,
// which pins Jackson 2.9.7) but the POM Gradle actually resolves from the JBoss repo is an
// empty Nexus-generated stub with no <dependencies> at all - so without declaring these
// ourselves, PastebinUploadRunnable.call() throws NoClassDefFoundError the first time it
// touches a Jackson class, only when someone actually clicks "Upload". 2.9.7 is a 2018 release
// with known CVEs; Jackson's 2.x line keeps this level of API (ObjectMapper, TypeReference,
// annotations) stable, so a current release is a safe drop-in rather than matching the old pin.
// The BOM (not three separately-pinned versions) because jackson-annotations renumbered its own
// versioning away from core/databind's x.y.z scheme starting at 2.20 - the BOM is what keeps the
// three resolvable together regardless of a given module's own version string.
Comment on lines +65 to +74

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// jpastebin needs these at runtime (see its own embedded META-INF/maven/org/jpastebin/pom.xml,
// which pins Jackson 2.9.7) but the POM Gradle actually resolves from the JBoss repo is an
// empty Nexus-generated stub with no <dependencies> at all - so without declaring these
// ourselves, PastebinUploadRunnable.call() throws NoClassDefFoundError the first time it
// touches a Jackson class, only when someone actually clicks "Upload". 2.9.7 is a 2018 release
// with known CVEs; Jackson's 2.x line keeps this level of API (ObjectMapper, TypeReference,
// annotations) stable, so a current release is a safe drop-in rather than matching the old pin.
// The BOM (not three separately-pinned versions) because jackson-annotations renumbered its own
// versioning away from core/databind's x.y.z scheme starting at 2.20 - the BOM is what keeps the
// three resolvable together regardless of a given module's own version string.

This comment is not particularly helpful. The rationale for the change belongs in the pull request description.

implementation(platform("com.fasterxml.jackson:jackson-bom:2.22.2"))
implementation("com.fasterxml.jackson.core:jackson-databind")
implementation("com.fasterxml.jackson.core:jackson-core")
implementation("com.fasterxml.jackson.core:jackson-annotations")
implementation("org.apache.httpcomponents:httpclient:4.5.13")
implementation("org.apache.httpcomponents:httpmime:4.5.13")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,20 @@ public enum KEY {
public GlobalProperties() {
String propsUrl = "/crashreporter.properties";
String defaultPropsUrl = "/crashreporter_defaults.properties";
try (InputStream stream = CrashReporter.class.getResourceAsStream(defaultPropsUrl)) {
properties.load(stream);
} catch (IOException e) {
// this should never go wrong
System.err.println("Unable to load default properties");
}
try (InputStream stream = CrashReporter.class.getResourceAsStream(propsUrl)) {
properties.load(stream);
loadIfPresent(defaultPropsUrl);
// Only cr-core's downstream consumers (cr-terasology, cr-destsol, ...) ship this file -
// it's absent when cr-core is used standalone, which getResourceAsStream signals with
// null rather than an IOException, so that has to be checked explicitly.
loadIfPresent(propsUrl);
}

private void loadIfPresent(String resourceUrl) {
try (InputStream stream = CrashReporter.class.getResourceAsStream(resourceUrl)) {
if (stream != null) {
properties.load(stream);
}
} catch (IOException e) {
System.err.println("Unable to load " + propsUrl);
System.err.println("Unable to load " + resourceUrl);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ public URL getUploadedFileURL() {
return uploadURL;
}

/** Package-private test hook - equivalent to a real "PasteBin" click, but with a caller-supplied Callable. */
void uploadForTesting(Callable<URL> callable) {
upload(callable);
}
Comment on lines +122 to +125

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test-exclusive methods should not exist within a class. There should be some other means of testing this, perhaps through UI automation?


private void upload(final Callable<URL> callable) {
Runnable runnable = new Runnable() {

Expand Down Expand Up @@ -169,6 +174,14 @@ public void run() {
}

private void uploadFailed(final Exception e) {
// Printed unconditionally, not just shown in the dialog below: a JOptionPane only reaches
// whoever is watching the screen at that exact moment, and leaves no trace at all once
// it's dismissed - nothing else in this codebase logs upload failures anywhere. Whoever
// launched this process (a script, a supervisor, a developer tailing output) needs to be
// able to find out what happened after the fact, not just the person who happened to be
// looking right then.
e.printStackTrace(System.err);

SwingUtilities.invokeLater(new Runnable() {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2026 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

package org.terasology.crashreporter.pages;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.terasology.crashreporter.GlobalProperties;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Callable;

import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Regression test for an upload failure that previously left no trace anywhere once its
* {@code JOptionPane} was dismissed - found while manually testing the reporter dialog: a real
* exception (a missing-Jackson {@code NoClassDefFoundError} - see the jackson-bom fix elsewhere in
* this commit) only ever showed up in a popup, with nothing printed to stderr for whoever launched
* the process to find afterward.
*/
class UploadPanelFailureLoggingTest {

private PrintStream originalErr;
private ByteArrayOutputStream capturedErr;

@BeforeEach
void redirectStderr() {
originalErr = System.err;
capturedErr = new ByteArrayOutputStream();
System.setErr(new PrintStream(capturedErr, true, StandardCharsets.UTF_8));
}

@AfterEach
void restoreStderr() {
System.setErr(originalErr);
}

@Test
void aFailedUploadIsPrintedToStderrNotJustShownInAPopup() throws InterruptedException {
UploadPanel panel = new UploadPanel(new GlobalProperties(), () -> "log text", () -> "log.txt");

final Exception cause = new IllegalStateException("upload failed: missing Jackson class");
panel.uploadForTesting(new Callable<URL>() {
@Override
public URL call() throws Exception {
throw cause;
}
});

long deadline = System.currentTimeMillis() + 2000;
while (capturedErr.size() == 0 && System.currentTimeMillis() < deadline) {
Thread.sleep(20);
}

String stderr = capturedErr.toString(StandardCharsets.UTF_8);
assertTrue(stderr.contains("IllegalStateException"), "Expected the exception type on stderr, got: " + stderr);
assertTrue(stderr.contains("upload failed: missing Jackson class"),
"Expected the exception message on stderr, got: " + stderr);
}
}