Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ public void setAlertSound(String alertSound) {
///
/// The ID can also be used to cancel the notification later using `com.codename1.ui.Display#cancelLocalNotification(java.lang.String)`
///
/// **Platform note (iOS):** Notification IDs map to `UNNotificationRequest` identifiers.
/// Scheduling another local notification with the same ID replaces the existing pending notification
/// rather than adding a second one.
///
/// #### Returns
///
/// the id
Expand All @@ -216,6 +220,10 @@ public String getId() {
///
/// The ID can also be used to cancel the notification later using `com.codename1.ui.Display#cancelLocalNotification(java.lang.String)`
///
/// **Platform note (iOS):** Notification IDs map to `UNNotificationRequest` identifiers.
/// Scheduling another local notification with the same ID replaces the existing pending notification
/// rather than adding a second one.
///
/// #### Parameters
///
/// - `id`: the id to set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9204,9 +9204,13 @@ public void splitString(String source, char separator, ArrayList<String> out) {
}

public void scheduleLocalNotification(LocalNotification n, long firstTime, int repeat) {

String id = n.getId();
if (id != null) {
nativeInstance.cancelLocalNotification(id);
}

nativeInstance.sendLocalNotification(
n.getId(),
id,
n.getAlertTitle(),
n.getAlertBody(),
n.getAlertSound(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.codenameone.examples.hellocodenameone;

public class LocalNotificationNativeImpl {
public int countPendingNotificationsWithId(String notificationId) {
return -1;
}

public boolean isSupported() {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.codenameone.examples.hellocodenameone;

public class TestDiagnosticsNativeImpl {
public void enableNativeCrashSignalLogging(String reason) {
}

public void dumpNativeThreads(String reason) {
}

public void failFastWithNativeThreadDump(String reason) {
}

public boolean isSupported() {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.codenameone.examples.hellocodenameone;

import com.codename1.system.NativeInterface;

public interface LocalNotificationNative extends NativeInterface {
int countPendingNotificationsWithId(String notificationId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.codenameone.examples.hellocodenameone;

import com.codename1.system.NativeInterface;

public interface TestDiagnosticsNative extends NativeInterface {
void enableNativeCrashSignalLogging(String reason);
void dumpNativeThreads(String reason);
void failFastWithNativeThreadDump(String reason);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ protected Form createForm(String title, Layout layout, final String imageName) {
@Override
protected void onShowCompleted() {
registerReadyCallback(this, () -> {
Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot(imageName);
done();
if (Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot(imageName)) {
done();
} else {
fail("Screenshot capture failed for " + imageName);
}
});
}
};
Expand All @@ -53,4 +56,4 @@ protected synchronized void done() {
public synchronized boolean isDone() {
return done;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import com.codename1.io.Util;
import com.codename1.testing.DeviceRunner;
import com.codename1.testing.TestReporting;
import com.codename1.system.NativeLookup;
import com.codename1.ui.CN;
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.util.StringUtil;
import com.codenameone.examples.hellocodenameone.TestDiagnosticsNative;
import com.codenameone.examples.hellocodenameone.tests.graphics.AffineScale;
import com.codenameone.examples.hellocodenameone.tests.graphics.Clip;
import com.codenameone.examples.hellocodenameone.tests.graphics.DrawArc;
Expand Down Expand Up @@ -71,6 +73,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner {
new OrientationLockScreenshotTest(),
new SheetScreenshotTest(),
new InPlaceEditViewTest(),
new LocalNotificationIdOverrideTest(),
new BytecodeTranslatorRegressionTest(),
new BackgroundThreadUiAccessTest(),
new VPNDetectionAPITest(),
Expand All @@ -82,6 +85,8 @@ public static void addTest(BaseTest test) {
}

public void runSuite() {
boolean suiteFailed = false;
enableNativeCrashSignalLogging();
CN.callSerially(() -> {
Display.getInstance().addEdtErrorHandler(e -> {
log("CN1SS:ERR:exception caught in EDT " + e.getSource());
Expand All @@ -104,6 +109,7 @@ public void runSuite() {
} catch (Throwable t) {
log("CN1SS:ERR:suite test=" + testClass + " failed=" + t);
t.printStackTrace();
testClass.fail("Unhandled exception: " + t.getMessage());
}
});
int timeout = 30000;
Expand All @@ -114,8 +120,12 @@ public void runSuite() {
testClass.cleanup();
if(timeout == 0) {
log("CN1SS:ERR:suite test=" + testClass + " failed due to timeout waiting for DONE");
dumpDiagnostics("timeout test=" + testClass);
suiteFailed = true;
} else if (testClass.isFailed()) {
log("CN1SS:ERR:suite test=" + testClass + " failed: " + testClass.getFailMessage());
dumpDiagnostics("failure test=" + testClass + " message=" + testClass.getFailMessage());
suiteFailed = true;
} else {
if (!testClass.shouldTakeScreenshot()) {
log("CN1SS:INFO:test=" + testClass + " screenshot=none");
Expand All @@ -124,7 +134,14 @@ public void runSuite() {
log("CN1SS:INFO:suite finished test=" + testClass);
}
log("CN1SS:SUITE:FINISHED");
if (suiteFailed) {
log("CN1SS:ERR:suite failed due to one or more test errors");
}
TestReporting.getInstance().testExecutionFinished(getClass().getName());
if (suiteFailed) {
failFastWithNativeCrash("CN1SS suite failed");
throw new RuntimeException("CN1SS suite failed");
}
if (CN.isSimulator()) {
Display.getInstance().exitApplication();
}
Expand All @@ -134,6 +151,55 @@ private static void log(String msg) {
System.out.println(msg);
}

private static void dumpDiagnostics(String reason) {
try {
Thread current = Thread.currentThread();
log("CN1SS:ERR:capturing java stack reason=" + reason + " thread=" + current.getName());
String stack = Display.getInstance().getStackTrace(current, new RuntimeException("CN1SS diagnostics: " + reason));
for (String s : StringUtil.tokenize(stack, '\n')) {
if (s.length() > 400) {
s = s.substring(0, 400);
}
log("CN1SS:STACK:" + s);
}
} catch (Throwable t) {
log("CN1SS:ERR:failed to capture java stack=" + t);
}

try {
TestDiagnosticsNative nativeDiagnostics = NativeLookup.create(TestDiagnosticsNative.class);
if (nativeDiagnostics != null && nativeDiagnostics.isSupported()) {
nativeDiagnostics.dumpNativeThreads(reason);
} else {
log("CN1SS:INFO:native thread dump unsupported");
}
} catch (Throwable t) {
log("CN1SS:ERR:failed to capture native thread dump=" + t);
}
}

private static void failFastWithNativeCrash(String reason) {
try {
TestDiagnosticsNative nativeDiagnostics = NativeLookup.create(TestDiagnosticsNative.class);
if (nativeDiagnostics != null && nativeDiagnostics.isSupported()) {
nativeDiagnostics.failFastWithNativeThreadDump(reason);
}
} catch (Throwable t) {
log("CN1SS:ERR:failed to invoke native fail-fast dump=" + t);
}
}

private static void enableNativeCrashSignalLogging() {
try {
TestDiagnosticsNative nativeDiagnostics = NativeLookup.create(TestDiagnosticsNative.class);
if (nativeDiagnostics != null && nativeDiagnostics.isSupported()) {
nativeDiagnostics.enableNativeCrashSignalLogging("CN1SS runSuite start");
}
} catch (Throwable t) {
log("CN1SS:ERR:failed to install native crash signal handlers=" + t);
}
}

@Override
protected void startApplicationInstance() {
Cn1ssDeviceRunnerHelper.runOnEdtSync(() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,37 +29,28 @@ static void runOnEdtSync(Runnable runnable) {
}
}

static void emitCurrentFormScreenshot(String testName) {
static boolean emitCurrentFormScreenshot(String testName) {
String safeName = sanitizeTestName(testName);
Form current = Display.getInstance().getCurrent();
if (current == null) {
println("CN1SS:ERR:test=" + safeName + " message=Current form is null");
println("CN1SS:END:" + safeName);
return false;
}
int width = Math.max(1, current.getWidth());
int height = Math.max(1, current.getHeight());
Image[] img = new Image[1];
Display.getInstance().screenshot(screen -> img[0] = screen);
long time = System.currentTimeMillis();
Display.getInstance().invokeAndBlock(() -> {
while(img[0] == null) {
Util.sleep(50);
// timeout
if (System.currentTimeMillis() - time > 2000) {
return;
}
}
});
if (img[0] == null) {
println("CN1SS:ERR:test=" + safeName + " message=Screenshot process timed out");
Image screenshot = captureScreenshotWithRetries(safeName, current, width, height);
if (screenshot == null) {
println("CN1SS:ERR:test=" + safeName + " message=Unable to capture screenshot");
println("CN1SS:END:" + safeName);
return false;
}
Image screenshot = img[0];
try {
ImageIO io = ImageIO.getImageIO();
if (io == null || !io.isFormatSupported(ImageIO.FORMAT_PNG)) {
println("CN1SS:ERR:test=" + safeName + " message=PNG encoding unavailable");
println("CN1SS:END:" + safeName);
return false;
}
if(Display.getInstance().isSimulator()) {
io.save(screenshot, Storage.getInstance().createOutputStream(safeName + ".png"), ImageIO.FORMAT_PNG, 1);
Expand All @@ -76,15 +67,72 @@ static void emitCurrentFormScreenshot(String testName) {
} else {
println("CN1SS:INFO:test=" + safeName + " preview_jpeg_bytes=0 preview_quality=0");
}
return true;
} catch (IOException ex) {
println("CN1SS:ERR:test=" + safeName + " message=" + ex);
Log.e(ex);
println("CN1SS:END:" + safeName);
return false;
} finally {
screenshot.dispose();
}
}

static Image captureScreenshotWithRetries(String safeName, Form current, int width, int height) {
final int maxAttempts = 3;
Image screenshot = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
current.revalidate();
Image[] img = new Image[1];
Display.getInstance().screenshot(screen -> img[0] = screen);
long time = System.currentTimeMillis();
Display.getInstance().invokeAndBlock(() -> {
while(img[0] == null) {
Util.sleep(50);
if (System.currentTimeMillis() - time > 2000) {
return;
}
}
});
screenshot = img[0];
if (screenshot == null) {
println("CN1SS:WARN:test=" + safeName + " message=Screenshot process timed out attempt=" + attempt);
continue;
}

int[] imageData = screenshot.getRGBCached();
if (!isLikelyBlankImage(imageData)) {
return screenshot;
}

int sample = imageData.length > 0 ? imageData[0] : 0;
println("CN1SS:WARN:test=" + safeName + " message=Blank screenshot detected width=" + width + " height=" + height + " rgb0=" + sample + " attempt=" + attempt + " form=" + current.getClass().getName());
if (attempt < maxAttempts) {
screenshot.dispose();
screenshot = null;
Util.sleep(250);
} else {
screenshot.dispose();
screenshot = null;
}
}
return screenshot;
}

static boolean isLikelyBlankImage(int[] imageData) {
if (imageData == null || imageData.length == 0) {
return true;
}
int first = imageData[0];
int maxSamples = Math.min(imageData.length, 4096);
for (int i = 1; i < maxSamples; i++) {
if (imageData[i] != first) {
return false;
}
}
return true;
}

static byte[] encodePreview(ImageIO io, Image screenshot, String safeName) throws IOException {
byte[] chosenPreview = null;
int chosenQuality = 0;
Expand Down
Loading
Loading