Skip to content

Commit 13afe7a

Browse files
committed
Switched to androidx testing
1 parent 4151c2a commit 13afe7a

File tree

2 files changed

+429
-215
lines changed

2 files changed

+429
-215
lines changed

scripts/build-android-app.sh

Lines changed: 210 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ xmlstarlet sel -N "$NS" -t -c "/mvn:project/mvn:build/mvn:plugins" -n "$ROOT_POM
201201
[ -f "$APP_DIR/build.sh" ] && chmod +x "$APP_DIR/build.sh"
202202

203203
SETTINGS_FILE="$APP_DIR/common/codenameone_settings.properties"
204+
echo "codename1.arg.android.useAndroidX=true" >> "$SETTINGS_FILE"
204205
[ -f "$SETTINGS_FILE" ] || { ba_log "codenameone_settings.properties not found at $SETTINGS_FILE" >&2; exit 1; }
205206

206207
# --- Read settings ---
@@ -266,112 +267,232 @@ if [ -z "$GRADLE_PROJECT_DIR" ]; then
266267
fi
267268

268269
ba_log "Configuring instrumentation test sources in $GRADLE_PROJECT_DIR"
270+
271+
# Ensure AndroidX flags in gradle.properties
272+
# --- BEGIN: robust Gradle patch for AndroidX tests ---
273+
GRADLE_PROPS="$GRADLE_PROJECT_DIR/gradle.properties"
274+
grep -q '^android.useAndroidX=' "$GRADLE_PROPS" 2>/dev/null || echo 'android.useAndroidX=true' >> "$GRADLE_PROPS"
275+
grep -q '^android.enableJetifier=' "$GRADLE_PROPS" 2>/dev/null || echo 'android.enableJetifier=true' >> "$GRADLE_PROPS"
276+
269277
APP_BUILD_GRADLE="$GRADLE_PROJECT_DIR/app/build.gradle"
270-
if [ -f "$APP_BUILD_GRADLE" ]; then
271-
python3 - "$APP_BUILD_GRADLE" <<'PYTHON'
272-
import pathlib
273-
import re
274-
import sys
275-
276-
path = pathlib.Path(sys.argv[1])
277-
text = path.read_text()
278-
modified = False
279-
280-
if "android.test.InstrumentationTestRunner" not in text:
281-
def add_runner(match):
282-
prefix = match.group(0)
283-
return prefix + "\n testInstrumentationRunner \"android.test.InstrumentationTestRunner\""
284-
285-
new_text, count = re.subn(r"(defaultConfig\s*\{)", add_runner, text, count=1, flags=re.MULTILINE)
286-
if count:
287-
text = new_text
288-
modified = True
289-
else:
290-
raise SystemExit("defaultConfig block not found while adding instrumentation runner")
278+
ROOT_BUILD_GRADLE="$GRADLE_PROJECT_DIR/build.gradle"
291279

292-
libraries = [
293-
"useLibrary 'android.test.base'",
294-
"useLibrary 'android.test.mock'",
295-
"useLibrary 'android.test.runner'",
296-
]
280+
# Ensure repos in both root and app
281+
for F in "$ROOT_BUILD_GRADLE" "$APP_BUILD_GRADLE"; do
282+
if [ -f "$F" ]; then
283+
if ! grep -qE '^\s*repositories\s*{' "$F"; then
284+
cat >> "$F" <<'EOS'
297285
298-
missing_libraries = [lib for lib in libraries if lib not in text]
299-
if missing_libraries:
300-
match = re.search(r"^(\s*android\s*\{)", text, flags=re.MULTILINE)
301-
if not match:
302-
raise SystemExit("android block not found while adding instrumentation libraries")
303-
line = match.group(1)
304-
indent = re.match(r"^(\s*)", line).group(1)
305-
insertion = "".join(f"\n{indent} {lib}" for lib in missing_libraries)
306-
text = text[: match.end()] + insertion + text[match.end():]
307-
modified = True
308-
309-
if modified:
310-
if not text.endswith("\n"):
311-
text += "\n"
312-
path.write_text(text)
313-
PYTHON
314-
ba_log "Ensured instrumentation runner and libraries are declared"
315-
else
316-
ba_log "Warning: Gradle build file not found at $APP_BUILD_GRADLE; skipping instrumentation dependency configuration" >&2
317-
fi
286+
repositories {
287+
google()
288+
mavenCentral()
289+
}
290+
EOS
291+
else
292+
grep -q 'google()' "$F" || sed -E -i '0,/repositories[[:space:]]*\{/s//repositories {\n google()\n mavenCentral()/' "$F"
293+
grep -q 'mavenCentral()' "$F" || sed -E -i '0,/repositories[[:space:]]*\{/s//repositories {\n google()\n mavenCentral()/' "$F"
294+
fi
295+
fi
296+
done
297+
298+
# Edit app/build.gradle
299+
python3 - "$APP_BUILD_GRADLE" <<'PY'
300+
import sys, re, pathlib
301+
p = pathlib.Path(sys.argv[1]); txt = p.read_text(); orig = txt; changed = False
302+
303+
def strip_block(name, s):
304+
return re.sub(rf'(?ms)^\s*{name}\s*\{{.*?\}}\s*', '', s)
305+
306+
module_view = strip_block('buildscript', strip_block('pluginManagement', txt))
307+
308+
# 1) android { compileSdkVersion/targetSdkVersion }
309+
def ensure_sdk(body):
310+
# If android { ... } exists, update/insert inside defaultConfig and the android block
311+
if re.search(r'(?m)^\s*android\s*\{', body):
312+
# compileSdkVersion
313+
if re.search(r'(?m)^\s*compileSdkVersion\s+\d+', body) is None:
314+
body = re.sub(r'(?m)(^\s*android\s*\{)', r'\1\n compileSdkVersion 33', body, count=1)
315+
else:
316+
body = re.sub(r'(?m)^\s*compileSdkVersion\s+\d+', ' compileSdkVersion 33', body)
317+
# targetSdkVersion
318+
if re.search(r'(?ms)^\s*defaultConfig\s*\{.*?^\s*\}', body):
319+
dc = re.search(r'(?ms)^\s*defaultConfig\s*\{.*?^\s*\}', body)
320+
block = dc.group(0)
321+
if re.search(r'(?m)^\s*targetSdkVersion\s+\d+', block):
322+
block2 = re.sub(r'(?m)^\s*targetSdkVersion\s+\d+', ' targetSdkVersion 33', block)
323+
else:
324+
block2 = re.sub(r'(\{\s*)', r'\1\n targetSdkVersion 33', block, count=1)
325+
body = body[:dc.start()] + block2 + body[dc.end():]
326+
else:
327+
body = re.sub(r'(?m)(^\s*android\s*\{)', r'\1\n defaultConfig {\n targetSdkVersion 33\n }', body, count=1)
328+
else:
329+
# No android block at all: add minimal
330+
body += '\n\nandroid {\n compileSdkVersion 33\n defaultConfig { targetSdkVersion 33 }\n}\n'
331+
return body
332+
333+
txt2 = ensure_sdk(txt)
334+
if txt2 != txt: txt = txt2; module_view = strip_block('buildscript', strip_block('pluginManagement', txt)); changed = True
335+
336+
# 2) testInstrumentationRunner -> AndroidX
337+
if "androidx.test.runner.AndroidJUnitRunner" not in module_view:
338+
t2, n = re.subn(r'(?m)^\s*testInstrumentationRunner\s*".*?"\s*$', ' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"', txt)
339+
if n == 0:
340+
t2, n = re.subn(r'(?m)(^\s*defaultConfig\s*\{)', r'\1\n testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"', txt, count=1)
341+
if n == 0:
342+
t2, n = re.subn(r'(?ms)(^\s*android\s*\{)', r'\1\n defaultConfig {\n testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"\n }', txt, count=1)
343+
if n: txt = t2; module_view = strip_block('buildscript', strip_block('pluginManagement', txt)); changed = True
344+
345+
# 3) remove legacy useLibrary lines
346+
t2, n = re.subn(r'(?m)^\s*useLibrary\s+\'android\.test\.(base|mock|runner)\'\s*$', '', txt)
347+
if n: txt = t2; module_view = strip_block('buildscript', strip_block('pluginManagement', txt)); changed = True
348+
349+
# 4) deps: choose androidTestImplementation vs androidTestCompile
350+
uses_modern = re.search(r'(?m)^\s*(implementation|api|testImplementation|androidTestImplementation)\b', module_view) is not None
351+
conf = "androidTestImplementation" if uses_modern else "androidTestCompile"
352+
need = [
353+
("androidx.test.ext:junit:1.1.5", conf), # AndroidJUnit4
354+
("androidx.test:runner:1.5.2", conf),
355+
("androidx.test:core:1.5.0", conf),
356+
("androidx.test.services:storage:1.4.2", conf),
357+
]
358+
to_add = [(c, k) for (c, k) in need if c not in module_view]
359+
360+
if to_add:
361+
block = "\n\ndependencies {\n" + "".join([f" {k} \"{c}\"\n" for c, k in to_add]) + "}\n"
362+
txt = txt.rstrip() + block
363+
changed = True
364+
365+
if changed and txt != orig:
366+
if not txt.endswith("\n"): txt += "\n"
367+
p.write_text(txt)
368+
print(f"Patched app/build.gradle (SDK=33; deps via {conf})")
369+
else:
370+
print("No changes needed in app/build.gradle")
371+
PY
372+
# --- END: robust Gradle patch ---
373+
374+
echo "----- app/build.gradle tail -----"
375+
tail -n 80 "$APP_BUILD_GRADLE" | sed 's/^/| /'
376+
echo "---------------------------------"
318377

319378
TEST_SRC_DIR="$GRADLE_PROJECT_DIR/app/src/androidTest/java/${PACKAGE_PATH}"
320379
mkdir -p "$TEST_SRC_DIR"
321380
TEST_CLASS="$TEST_SRC_DIR/HelloCodenameOneInstrumentedTest.java"
322-
cat >"$TEST_CLASS" <<EOF
323-
package $PACKAGE_NAME;
381+
cat >"$TEST_CLASS" <<'EOF'
382+
package @PACKAGE@;
324383
384+
import android.app.Activity;
325385
import android.content.Context;
326-
import android.test.InstrumentationTestCase;
327-
328-
import android.app.Instrumentation;
329-
import android.os.ParcelFileDescriptor;
386+
import android.content.Intent;
387+
import android.graphics.Bitmap;
388+
import android.graphics.Canvas;
330389
import android.util.Base64;
390+
import android.util.DisplayMetrics;
391+
import android.view.View;
392+
393+
import androidx.test.core.app.ActivityScenario;
394+
import androidx.test.core.app.ApplicationProvider;
395+
import androidx.test.ext.junit.runners.AndroidJUnit4;
396+
397+
import org.junit.Assert;
398+
import org.junit.Test;
399+
import org.junit.runner.RunWith;
400+
401+
import java.io.ByteArrayOutputStream;
402+
403+
@RunWith(AndroidJUnit4.class)
404+
public class HelloCodenameOneInstrumentedTest {
405+
406+
private static void println(String s) { System.out.println(s); }
407+
408+
@Test
409+
public void testUseAppContext_andEmitScreenshot() throws Exception {
410+
Context ctx = ApplicationProvider.getApplicationContext();
411+
String pkg = "@PACKAGE@";
412+
Assert.assertEquals("Package mismatch", pkg, ctx.getPackageName());
413+
414+
// Resolve real launcher intent (don’t hard-code activity)
415+
Intent launch = ctx.getPackageManager().getLaunchIntentForPackage(pkg);
416+
if (launch == null) {
417+
// Fallback MAIN/LAUNCHER inside this package
418+
Intent q = new Intent(Intent.ACTION_MAIN);
419+
q.addCategory(Intent.CATEGORY_LAUNCHER);
420+
q.setPackage(pkg);
421+
launch = q;
422+
}
423+
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
424+
425+
println("CN1SS:INFO: about to launch Activity");
426+
byte[] pngBytes = null;
427+
428+
try (ActivityScenario<Activity> scenario = ActivityScenario.launch(launch)) {
429+
// give the activity a tiny moment to layout
430+
Thread.sleep(750);
431+
432+
println("CN1SS:INFO: activity launched");
433+
434+
final byte[][] holder = new byte[1][];
435+
scenario.onActivity(activity -> {
436+
try {
437+
View root = activity.getWindow().getDecorView().getRootView();
438+
int w = root.getWidth();
439+
int h = root.getHeight();
440+
if (w <= 0 || h <= 0) {
441+
DisplayMetrics dm = activity.getResources().getDisplayMetrics();
442+
w = Math.max(1, dm.widthPixels);
443+
h = Math.max(1, dm.heightPixels);
444+
int sw = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY);
445+
int sh = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY);
446+
root.measure(sw, sh);
447+
root.layout(0, 0, w, h);
448+
println("CN1SS:INFO: forced layout to " + w + "x" + h);
449+
} else {
450+
println("CN1SS:INFO: natural layout " + w + "x" + h);
451+
}
452+
453+
Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
454+
Canvas c = new Canvas(bmp);
455+
root.draw(c);
456+
457+
ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.max(1024, w * h / 2));
458+
boolean ok = bmp.compress(Bitmap.CompressFormat.PNG, 100, baos);
459+
if (!ok) throw new RuntimeException("Bitmap.compress returned false");
460+
holder[0] = baos.toByteArray();
461+
println("CN1SS:INFO: png_bytes=" + holder[0].length);
462+
} catch (Throwable t) {
463+
println("CN1SS:ERR: onActivity " + t);
464+
t.printStackTrace(System.out);
465+
}
466+
});
467+
468+
pngBytes = holder[0];
469+
} catch (Throwable t) {
470+
println("CN1SS:ERR: launch " + t);
471+
t.printStackTrace(System.out);
472+
}
331473
332-
import java.io.FileInputStream;
333-
import java.io.IOException;
334-
import java.io.InputStream;
335-
336-
public class HelloCodenameOneInstrumentedTest extends InstrumentationTestCase {
337-
338-
public void testUseAppContext() {
339-
Context appContext = getInstrumentation().getTargetContext();
340-
assertEquals("$PACKAGE_NAME", appContext.getPackageName());
341-
}
342-
343-
private static final int CHUNK = 200_000;
344-
345-
public static void printPngToStdout(Instrumentation inst) {
346-
try {
347-
ParcelFileDescriptor pfd = inst.getUiAutomation().executeShellCommand("screencap -p");
348-
byte[] png;
349-
try (InputStream in = new FileInputStream(pfd.getFileDescriptor())) {
350-
png = readAll(in);
474+
if (pngBytes == null || pngBytes.length == 0) {
475+
println("CN1SS:END"); // terminator for the runner parser
476+
Assert.fail("Screenshot capture produced 0 bytes");
477+
return;
351478
}
352-
String b64 = Base64.encodeToString(png, Base64.NO_WRAP);
353-
System.out.println("<<CN1_SCREENSHOT_BEGIN>>");
354-
for (int i = 0; i < b64.length(); i += CHUNK) {
355-
int end = Math.min(i + CHUNK, b64.length());
356-
System.out.println(b64.substring(i, end));
479+
480+
// Chunk & emit (safe for Gradle/logcat capture)
481+
String b64 = Base64.encodeToString(pngBytes, Base64.NO_WRAP);
482+
final int CHUNK = 2000;
483+
int count = 0;
484+
for (int pos = 0; pos < b64.length(); pos += CHUNK) {
485+
int end = Math.min(pos + CHUNK, b64.length());
486+
System.out.println("CN1SS:" + String.format("%06d", pos) + ":" + b64.substring(pos, end));
487+
count++;
357488
}
358-
System.out.println("<<CN1_SCREENSHOT_END>>");
489+
println("CN1SS:INFO: chunks=" + count + " total_b64_len=" + b64.length());
490+
System.out.println("CN1SS:END");
359491
System.out.flush();
360-
} catch (IOException err) {
361-
err.printStackTrace();
362-
throw new RuntimeException(err);
363-
}
364-
}
365-
366-
private static byte[] readAll(InputStream in) throws IOException {
367-
byte[] buf = new byte[64 * 1024];
368-
int n;
369-
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
370-
while ((n = in.read(buf)) != -1) out.write(buf, 0, n);
371-
return out.toByteArray();
372492
}
373493
}
374494
EOF
495+
sed -i "s|@PACKAGE@|$PACKAGE_NAME|g" "$TEST_CLASS"
375496
ba_log "Created instrumentation test at $TEST_CLASS"
376497

377498
DEFAULT_ANDROID_TEST="$GRADLE_PROJECT_DIR/app/src/androidTest/java/com/example/myapplication2/ExampleInstrumentedTest.java"

0 commit comments

Comments
 (0)