Skip to content
Merged
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
64 changes: 64 additions & 0 deletions .github/workflows/archetype-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Archetype simulator smoke

# Smoke-tests that an archetype-generated project can run `cn1:css` (the
# CN1CSSCLI subprocess that boots JavaSEPort) with `cn1.autoDefaultResourceBundle`
# forced on. This reproduces the user-side preference state that masked
# issue #4850 in CI for six months: the bug was deterministic on the user
# code path but invisible to CI runners with the default-false preference.
#
# Path-gated to the modules whose changes can break this code path so we
# don't run the heavy archetype-generate-and-build for unrelated edits.
on:
pull_request:
branches: [ master ]
paths:
- 'Ports/JavaSE/**'
- 'CodenameOneDesigner/**'
- 'CodenameOne/src/com/codename1/ui/plaf/UIManager.java'
- 'maven/codenameone-maven-plugin/**'
- 'maven/integration-tests/cn1app-archetype-test.sh'
- 'maven/integration-tests/inc/**'
- '.github/workflows/archetype-smoke.yml'
push:
branches: [ master ]
paths:
- 'Ports/JavaSE/**'
- 'CodenameOneDesigner/**'
- 'CodenameOne/src/com/codename1/ui/plaf/UIManager.java'
- 'maven/codenameone-maven-plugin/**'
- 'maven/integration-tests/cn1app-archetype-test.sh'
- 'maven/integration-tests/inc/**'
- '.github/workflows/archetype-smoke.yml'

jobs:
archetype-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 8
uses: actions/setup-java@v4
with:
java-version: '8'
distribution: 'temurin'
cache: 'maven'
- name: Install xvfb
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb
- name: Build Codename One Maven artifacts
run: |
cd maven
xvfb-run -a mvn install -Plocal-dev-javase -DskipTests -Dmaven.javadoc.skip=true
- name: Fetch and install archetype projects
uses: carlosperate/download-file-action@v1.0.3
with:
file-url: https://github.com/shannah/cn1-maven-archetypes/archive/refs/heads/master.zip
file-name: cn1-maven-archetypes.zip
- name: Install archetype to local repo
run: |
unzip cn1-maven-archetypes.zip
cd cn1-maven-archetypes-master
xvfb-run -a mvn install archetype:update-local-catalog -Plocal-dev-javase
xvfb-run -a mvn archetype:crawl -Plocal-dev-javase
- name: Run archetype simulator smoke (auto-bundle pref forced on)
run: |
cd maven/integration-tests
xvfb-run -a bash cn1app-archetype-test.sh
30 changes: 28 additions & 2 deletions Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -14443,10 +14443,36 @@ private void loadFromFile() {
properties.clear();
properties.load(in);
super.clear();
java.util.List<String> wormholeKeys = null;
for (String name : properties.stringPropertyNames()) {
putInternal(name, properties.getProperty(name));
String value = properties.getProperty(name);
// Legacy self-heal for issue #4850: older Codename One versions
// (<= 7.0.236) whose `get(Object)` echoed missing keys back as
// their own value persisted those echoes to disk. Now that the
// @-key auto-fabrication guard is in place new echoes are
// blocked, but on-disk `@k=@k` entries written by older
// versions still flow through `super.get` as non-null and
// crash `UIManager.setBundle` -> `parseTextFieldInputMode` on
// a token with no `=`. Drop them at load time and rewrite the
// file so legacy projects self-heal on the next simulator
// boot instead of crashing forever.
if (name != null && name.startsWith("@") && name.equals(value)) {
if (wormholeKeys == null) {
wormholeKeys = new java.util.ArrayList<String>();
}
wormholeKeys.add(name);
continue;
}
putInternal(name, value);
}
if (wormholeKeys != null) {
for (String name : wormholeKeys) {
properties.remove(name);
}
persist();
} else {
dirty = false;
}
dirty = false;
} catch (IOException err) {
Log.e(err);
}
Expand Down
7 changes: 6 additions & 1 deletion maven/integration-tests/android-native-interface-test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#!/bin/bash
exit 0 #Failing for some reason... need to investigate, but will do it later
# Disabled pending investigation. Previously this script started with `exit 0`
# directly, which silently masqueraded as a green check in `all.sh` (which uses
# `set -e` and would otherwise have surfaced the disablement). Print an
# explicit SKIP line so the suite log makes the gap visible.
echo "[SKIP] android-native-interface-test.sh - disabled, needs reinvestigation"
exit 0
SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"
set -e
source $SCRIPTPATH/inc/env.sh
Expand Down
9 changes: 9 additions & 0 deletions maven/integration-tests/cn1app-archetype-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"
set -e
source $SCRIPTPATH/inc/env.sh
source $SCRIPTPATH/inc/auto-bundle-pref.sh

# Force `cn1.autoDefaultResourceBundle=true` for the duration of the test so
# the `cn1:css` subprocess takes the JavaSEPort.enableAutoLocalizationBundle
# branch -- the same path that crashed for end users in #4850 but is invisible
# in CI under the default-false preference.
set_auto_bundle_pref true
trap 'set_auto_bundle_pref false' EXIT

cd $SCRIPTPATH/build
if [ -d myapp1 ]; then
rm -rf myapp1
Expand Down
40 changes: 40 additions & 0 deletions maven/integration-tests/inc/auto-bundle-pref.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Helpers to flip the JavaSEPort `cn1.autoDefaultResourceBundle` preference for
# the duration of an integration test. The preference normally defaults to
# false, which masks bugs in JavaSEPort.enableAutoLocalizationBundle from CI -
# end users with the menu toggled on (see issue #4850) experience the gated
# code path instead. Tests that exercise `cn1:css` / `cn1:run` need to flip
# the pref true before the build to reproduce the user-side environment.
#
# Usage:
# source "$SCRIPTPATH/inc/auto-bundle-pref.sh"
# set_auto_bundle_pref true
# trap 'set_auto_bundle_pref false' EXIT
#
# The pref is stored in `Preferences.userRoot().node("/com/codename1/impl/javase")`,
# i.e. `~/.java/.userPrefs/com/codename1/impl/javase/prefs.xml`. Forked
# `cn1:css` subprocesses inherit the same `user.home` from Maven and read
# the same backing store, so a parent-process flush is visible to children.

set_auto_bundle_pref() {
local value="${1:-true}"
local workdir
workdir="$(mktemp -d -t cn1-auto-bundle-pref.XXXXXX)"
cat > "$workdir/SetAutoBundlePref.java" <<'EOF'
import java.util.prefs.Preferences;

public final class SetAutoBundlePref {
public static void main(String[] args) throws Exception {
boolean value = args.length > 0 ? Boolean.parseBoolean(args[0]) : true;
Preferences node = Preferences.userRoot().node("/com/codename1/impl/javase");
node.putBoolean("cn1.autoDefaultResourceBundle", value);
node.flush();
System.out.println("[auto-bundle-pref] cn1.autoDefaultResourceBundle=" + value);
}
}
EOF
( cd "$workdir" && javac SetAutoBundlePref.java && java SetAutoBundlePref "$value" )
local rc=$?
rm -rf "$workdir"
return $rc
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.codename1.impl.javase;

import com.codename1.testing.AbstractTest;
import com.codename1.ui.plaf.UIManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -77,12 +80,112 @@ public boolean runTest() throws Exception {
bundleReloadedMap.put("@rtl", "true");
assertEqual("true", bundleReloadedMap.get("@rtl"), "Existing meta-key values should still be returned");

verifySetBundleSmokeOnFreshProject(ctor, tempDir);
verifySetBundleHealsLegacyWormholeFile(ctor, tempDir);

return true;
} finally {
deleteRecursive(tempDir);
}
}

/// Smoke-tests the full simulator-init handoff: AutoLocalizationBundle wrapped around a
/// fresh project's empty `src/main/l10n` directory, then handed to `UIManager.setBundle`
/// the same way `JavaSEPort.enableAutoLocalizationBundle` does at simulator boot.
///
/// Pre-fix this combination crashed deterministically: `setBundle` queried `@im` on the
/// bundle, the bundle echoed `@im` back, `setBundle` tokenized that to `["@im"]`, queried
/// `@im-@im`, got `@im-@im` back, and `parseTextFieldInputMode` blew up on
/// `substring(0, indexOf('='))` for a token with no `=` (issue #4850).
///
/// This regression was invisible in CI because `enableAutoLocalizationBundle` is gated on
/// `cn1.autoDefaultResourceBundle`, which CI runners default to false but real users have
/// stuck to true via the simulator menu. Exercising the gated code path directly here
/// makes the regression catchable regardless of preference state.
private void verifySetBundleSmokeOnFreshProject(Constructor<?> ctor, File tempDir) throws Exception {
File freshProjectDir = new File(tempDir, "fresh-project");
File freshBundleFile = new File(new File(freshProjectDir, "src" + File.separator + "main" + File.separator + "l10n"), "Bundle.properties");

Object freshBundle = ctor.newInstance(freshBundleFile, null);
@SuppressWarnings("unchecked")
Map<String, String> freshBundleMap = (Map<String, String>) freshBundle;

UIManager manager = UIManager.getInstance();
Map<String, String> savedBundle = manager.getBundle();
try {
// Pre-fix: throws StringIndexOutOfBoundsException out of parseTextFieldInputMode.
manager.setBundle(freshBundleMap);
assertSame(freshBundleMap, manager.getBundle(), "setBundle should install the AutoLocalizationBundle on a fresh project");
} finally {
manager.setBundle(savedBundle);
}
}

/// Reproduces ThomasH99's report on issue #4850: even after the @-key fabrication
/// guard shipped in 7.0.237, existing user projects still crash the same way during
/// `cn1:css` because their on-disk `Bundle.properties` was poisoned by older
/// Codename One versions whose `AutoLocalizationBundle.get` echoed and persisted
/// `@key=@key` self-references.
///
/// Pre-cleanup-fix:
/// - `loadFromFile` reads `@im=@im` and `@im-@im=@im-@im` straight into the Hashtable.
/// - `setBundle` calls `bundle.get("@im")` -> super.get returns the persisted "@im"
/// (not null), so the @-prefix null-guard added in `ba8044ef0` is bypassed.
/// - setBundle tokenizes "@im" to `["@im"]`, looks up "@im-@im" (returns the persisted
/// "@im-@im" string), and `parseTextFieldInputMode("@im-@im")` crashes on
/// `substring(0, indexOf('='))` for a token with no `=`.
///
/// Post-cleanup-fix:
/// - `loadFromFile` drops `@k=@k` self-references at load time and rewrites the file,
/// so the bundle behaves like a fresh one and self-heals legacy projects.
private void verifySetBundleHealsLegacyWormholeFile(Constructor<?> ctor, File tempDir) throws Exception {
File legacyProjectDir = new File(tempDir, "legacy-project");
File legacyL10nDir = new File(legacyProjectDir, "src" + File.separator + "main" + File.separator + "l10n");
if (!legacyL10nDir.mkdirs()) {
throw new RuntimeException("Failed to create legacy l10n dir " + legacyL10nDir);
}
File legacyBundleFile = new File(legacyL10nDir, "Bundle.properties");

Properties poisoned = new Properties();
poisoned.setProperty("@im", "@im");
poisoned.setProperty("@im-@im", "@im-@im");
poisoned.setProperty("@rtl", "@rtl");
poisoned.setProperty("hello", "world");
try (OutputStream out = new FileOutputStream(legacyBundleFile)) {
poisoned.store(out, "legacy wormhole-poisoned bundle");
}

Object legacyBundle = ctor.newInstance(legacyBundleFile, null);
@SuppressWarnings("unchecked")
Map<String, String> legacyBundleMap = (Map<String, String>) legacyBundle;

// The constructor must self-heal the in-memory state: `@k=@k` self-references
// are read from disk but should not flow through to callers.
assertNull(legacyBundleMap.get("@im"), "Legacy `@im=@im` self-reference must be healed at load");
assertNull(legacyBundleMap.get("@im-@im"), "Legacy `@im-@im=@im-@im` self-reference must be healed at load");
assertNull(legacyBundleMap.get("@rtl"), "Legacy `@rtl=@rtl` self-reference must be healed at load");
assertEqual("world", legacyBundleMap.get("hello"), "Non-wormhole entries must survive the load-time clean");

// The on-disk file must also be rewritten so the corruption doesn't keep biting
// on every subsequent simulator boot.
Properties cleaned = load(legacyBundleFile);
assertNull(cleaned.getProperty("@im"), "Legacy `@im=@im` self-reference must be wiped from the file");
assertNull(cleaned.getProperty("@im-@im"), "Legacy `@im-@im=@im-@im` self-reference must be wiped from the file");
assertNull(cleaned.getProperty("@rtl"), "Legacy `@rtl=@rtl` self-reference must be wiped from the file");
assertEqual("world", cleaned.getProperty("hello"), "Non-wormhole entries must survive the file rewrite");

UIManager manager = UIManager.getInstance();
Map<String, String> savedBundle = manager.getBundle();
try {
// Pre-cleanup-fix: throws StringIndexOutOfBoundsException out of
// parseTextFieldInputMode because `bundle.get("@im")` returned "@im".
manager.setBundle(legacyBundleMap);
assertSame(legacyBundleMap, manager.getBundle(), "setBundle should install the bundle on a legacy poisoned project");
} finally {
manager.setBundle(savedBundle);
}
}

private Properties load(File file) throws Exception {
Properties props = new Properties();
if (file.exists()) {
Expand Down
Loading