[verified] Publish ReelBox source under GPLv3

This commit is contained in:
Hermes committed 2026-11-12 23:41:13 +00:00
commit db2b505ca3
445 files changed
+105274

No files matched your search

+322
View File
@@ -0,0 +1,322 @@
#!/usr/bin/env python3
"""Lean host Android-view tests, without the Android/Flutter Gradle build.
Run: python3 tool/player_overlay_android_test.py
--setup-only resolves reusable dependencies and exercises Android/Handler smoke tests.
--test METHOD runs one production-overlay method; all methods run by default.
All Gradle/compiler/JVM processes acquire the repository's .tooling.lock.
Outputs, dependency manifests and JUnit XML live in scratch, not the app build.
"""
from __future__ import annotations
import argparse
import fcntl
import hashlib
import json
import os
from pathlib import Path
import shutil
import shlex
import subprocess
import sys
import time
import zipfile
ROOT = Path(__file__).resolve().parents[1]
WORK = Path('/root/.hermes/cache/scratch/reelbox-overlay-android-tests')
CACHE = Path.home() / '.gradle/caches/modules-2/files-2.1'
JDK = Path(os.environ.get('OVERLAY_TEST_JAVA_HOME', '/usr/lib/jvm/java-17-openjdk-amd64'))
ANDROID = Path(os.environ.get('OVERLAY_TEST_ANDROID_JAR', '/root/developer-tools/android/platforms/android-36/android.jar'))
LOCK = ROOT / '.tooling.lock'
LOCK_FD: int | None = None
BUILD = '''
import org.gradle.api.attributes.java.TargetJvmEnvironment
plugins { id 'base' }
repositories {
maven { url = uri('https://repo.maven.apache.org/maven2') }
google { content { includeGroupByRegex 'androidx[.].*' } }
}
configurations {
harness {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
objects.named(TargetJvmEnvironment, TargetJvmEnvironment.STANDARD_JVM))
}
}
}
dependencies {
harness('org.robolectric:robolectric:4.16.1') {
// LEGACY graphics never loads the large native renderer distribution.
exclude group: 'org.robolectric', module: 'nativeruntime-dist-compat'
}
harness 'junit:junit:4.13.2'
}
tasks.register('resolveHarness') {
doLast {
file('classpath.txt').text = configurations.harness.files.collect { it.absolutePath }.sort().join('\\n') + '\\n'
}
}
'''
SMOKE = '''
package org.reelbox.reelbox;
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.Button;
import java.time.Duration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.GraphicsMode;
import org.robolectric.Shadows;
import static org.junit.Assert.*;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 28, manifest = Config.NONE, qualifiers = "w360dp-h780dp-mdpi")
@LooperMode(LooperMode.Mode.PAUSED)
@GraphicsMode(GraphicsMode.Mode.LEGACY)
public class OverlayHarnessSmokeTest {
@Test public void realAndroidLayoutClickAndMainHandler() {
try (var controller = Robolectric.buildActivity(Activity.class).setup().visible()) {
Activity activity = controller.get();
FrameLayout root = new FrameLayout(activity);
Button button = new Button(activity);
root.addView(button, new FrameLayout.LayoutParams(120, 48));
activity.setContentView(root);
root.measure(View.MeasureSpec.makeMeasureSpec(360, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(780, View.MeasureSpec.EXACTLY));
root.layout(0, 0, 360, 780);
assertEquals(120, button.getWidth());
assertEquals(48, button.getHeight());
int[] clicks = {0};
button.setOnClickListener(v -> clicks[0]++);
assertTrue(button.performClick());
assertEquals(1, clicks[0]);
new Handler(Looper.getMainLooper()).postDelayed(() -> button.setVisibility(View.GONE), 3000);
Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(2999));
assertEquals(View.VISIBLE, button.getVisibility());
Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(1));
assertEquals(View.GONE, button.getVisibility());
}
}
}
'''
RUNNER = '''
package org.reelbox.reelbox;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.junit.runner.JUnitCore;
import org.junit.runner.Request;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
public class OverlayHostRunner {
public static void main(String[] args) throws Exception {
JUnitCore core = new JUnitCore();
core.addListener(new RunListener() {
@Override public void testStarted(org.junit.runner.Description d) {
System.out.println("RUN " + d.getMethodName());
}
@Override public void testFailure(Failure f) {
System.err.println("FAIL " + f.getTestHeader() + "\\n" + f.getTrace());
}
});
Class<?> clazz = Class.forName(args[0]);
Result result = core.run(args.length > 2 ? Request.method(clazz, args[2]) : Request.aClass(clazz));
var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
var suite = doc.createElement("testsuite");
suite.setAttribute("name", args[0]);
suite.setAttribute("tests", Integer.toString(result.getRunCount()));
suite.setAttribute("failures", Integer.toString(result.getFailureCount()));
suite.setAttribute("skipped", Integer.toString(result.getIgnoreCount()));
suite.setAttribute("time", Double.toString(result.getRunTime() / 1000.0));
doc.appendChild(suite);
for (Failure failure : result.getFailures()) {
var test = doc.createElement("testcase");
test.setAttribute("name", failure.getTestHeader());
var fail = doc.createElement("failure");
fail.setAttribute("message", String.valueOf(failure.getMessage()));
fail.setTextContent(failure.getTrace());
test.appendChild(fail); suite.appendChild(test);
}
TransformerFactory.newInstance().newTransformer().transform(new DOMSource(doc), new StreamResult(new File(args[1])));
System.out.printf("RESULT tests=%d failures=%d ignored=%d elapsed_ms=%d%n", result.getRunCount(), result.getFailureCount(), result.getIgnoreCount(), result.getRunTime());
System.exit(result.wasSuccessful() && result.getRunCount() > 0 ? 0 : 1);
}
}
'''
def run(command: list[str], *, log: str | None = None) -> None:
env = dict(os.environ, JAVA_HOME=str(JDK), TMPDIR=str(WORK / 'tmp'))
env['PATH'] = str(JDK / 'bin') + os.pathsep + env.get('PATH', '')
display = command.copy()
for flag in ('-cp', '-classpath'):
if flag in display:
index = display.index(flag) + 1
display[index] = f'<{len(display[index].split(os.pathsep))} classpath entries>'
print('+', shlex.join(display), flush=True)
assert LOCK_FD is not None, 'Heavy invocations must hold the tooling flock'
with (WORK / 'commands.log').open('a') as command_log:
command_log.write(shlex.join(command) + '\n')
# Inherit the locked descriptor so a timed-out parent cannot unlock a live JVM.
proc = subprocess.Popen(command, cwd=WORK, env=env, pass_fds=(LOCK_FD,),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
assert proc.stdout is not None
log_path = WORK / (log or 'last-command.log')
if log_path.exists():
log_path.rename(log_path.with_name(log_path.name + '.' + str(time.time_ns())))
with log_path.open('w') as output:
for line in proc.stdout:
print(line, end='', flush=True)
output.write(line)
if proc.wait():
raise subprocess.CalledProcessError(proc.returncode, command)
def cached_jar(artifact: str, version: str = '*') -> Path:
paths = sorted((CACHE / artifact).glob(version + '/*/*.jar'))
if not paths:
raise RuntimeError(f'Missing cached compiler dependency: {artifact}:{version}')
return paths[-1]
def resolve() -> list[Path]:
build = WORK / 'build.gradle'
fingerprint = hashlib.sha256(BUILD.encode()).hexdigest()
stamp = WORK / 'dependency-fingerprint'
manifest = WORK / 'classpath.txt'
fresh = stamp.exists() and stamp.read_text() == fingerprint and manifest.exists()
if fresh:
fresh = all(Path(line).exists() for line in manifest.read_text().splitlines())
if not fresh:
build.write_text(BUILD)
(WORK / 'settings.gradle').write_text("rootProject.name = 'reelbox-overlay-host-tests'\n")
(WORK / 'gradle.properties').write_text('org.gradle.jvmargs=-Xmx256m -XX:MaxMetaspaceSize=192m\norg.gradle.workers.max=1\norg.gradle.daemon=false\norg.gradle.vfs.watch=false\n')
candidates = sorted((Path.home() / '.gradle/wrapper/dists').glob('gradle-9.3.1-bin/*/gradle-9.3.1/bin/gradle'))
gradle = os.environ.get('OVERLAY_TEST_GRADLE') or (str(candidates[0]) if candidates else shutil.which('gradle'))
if not gradle:
raise RuntimeError('No Gradle 9.3.1 distribution found; set OVERLAY_TEST_GRADLE')
run([gradle, '--no-daemon', '--max-workers=1', '--console=plain', 'resolveHarness'], log='dependency-resolution.log')
stamp.write_text(fingerprint)
jars = []
for line in manifest.read_text().splitlines():
path = Path(line)
if path.suffix == '.aar':
destination = WORK / 'aar-jars' / (path.stem + '-classes.jar')
destination.parent.mkdir(exist_ok=True)
if not destination.exists():
with zipfile.ZipFile(path) as aar:
destination.write_bytes(aar.read('classes.jar'))
jars.append(destination)
else:
jars.append(path)
return jars
def main() -> int:
global LOCK_FD
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--setup-only', action='store_true')
parser.add_argument('--test', help='Run one PlayerOverlayAndroidTest method')
args = parser.parse_args()
WORK.mkdir(parents=True, exist_ok=True)
(WORK / 'tmp').mkdir(exist_ok=True)
# Serialize the complete small pipeline, not separate JVMs that let a full
# app verification jump into the middle of compilation and test execution.
print(f'Acquiring flock {LOCK}', flush=True)
lock_file = LOCK.open('a')
fcntl.flock(lock_file, fcntl.LOCK_EX)
LOCK_FD = lock_file.fileno()
if not (JDK / 'bin/java').is_file() or not ANDROID.is_file():
raise RuntimeError('JDK17 or Android compile jar missing; see environment overrides in script')
deps = resolve()
stdlib = cached_jar('org.jetbrains.kotlin/kotlin-stdlib', '2.4.0')
sources = WORK / 'sources'
sources.mkdir(exist_ok=True)
(sources / 'OverlayHarnessSmokeTest.java').write_text(SMOKE)
(sources / 'OverlayHostRunner.java').write_text(RUNNER)
classes = WORK / 'classes'
if classes.exists():
shutil.rmtree(classes)
classes.mkdir()
cp = os.pathsep.join(map(str, [classes, ANDROID, stdlib, *deps]))
javac = [str(JDK / 'bin/javac'), '-J-Xmx192m', '--release', '17', '-cp', cp, '-d', str(classes)]
run([*javac, str(sources / 'OverlayHarnessSmokeTest.java'), str(sources / 'OverlayHostRunner.java')], log='javac-smoke.log')
opens = ['java.base/java.lang', 'java.base/java.util', 'java.base/java.io',
'java.base/java.net', 'java.base/java.security', 'java.base/java.text',
'java.base/jdk.internal.access', 'java.desktop/java.awt.font',
'jdk.compiler/com.sun.tools.javac.api']
java = [str(JDK / 'bin/java'), '-Xmx384m', '-XX:MaxMetaspaceSize=192m',
f'-Djava.io.tmpdir={WORK / "tmp"}',
'-Drobolectric.dependency.repo.url=https://repo.maven.apache.org/maven2',
f'-Dmaven.repo.local={WORK / "m2"}',
'-Drobolectric.graphicsMode=LEGACY',
*['--add-opens=' + item + '=ALL-UNNAMED' for item in opens],
'-cp', cp, 'org.reelbox.reelbox.OverlayHostRunner']
# The SDK resolver downloads only the requested SDK28 instrumented Android jar.
# A separate smoke run establishes that infrastructure errors aren't overlay failures.
run([*java, 'org.reelbox.reelbox.OverlayHarnessSmokeTest', str(WORK / 'smoke-results.xml')], log='smoke.log')
if args.setup_only:
print('SETUP VERIFIED: SDK28 / Java17 / Robolectric4.16.1 / PAUSED / LEGACY. No production-overlay tests requested.')
return 0
source = ROOT / 'android/app/src/main/kotlin/org/reelbox/reelbox/PlayerOverlayView.kt'
if not source.exists():
print(f'BLOCKED: production overlay does not exist yet: {source}', file=sys.stderr)
return 2
compiler = [cached_jar('org.jetbrains.kotlin/kotlin-compiler-embeddable', '2.4.0'), stdlib,
cached_jar('org.jetbrains.kotlin/kotlin-script-runtime', '2.4.0'),
cached_jar('org.jetbrains.kotlin/kotlin-reflect'),
cached_jar('org.jetbrains.kotlinx/kotlinx-coroutines-core-jvm'),
cached_jar('org.jetbrains/annotations')]
trove = sorted((CACHE / 'org.jetbrains.intellij.deps/trove4j').glob('*/*/*.jar'))
compiler += trove[-1:]
# Compile real production source, never a replacement or copied overlay implementation.
overlay_sources = [source, source.with_name('PlayerControlsState.kt')]
state_source = source.with_name('PlayerOverlayState.kt')
if state_source.exists():
overlay_sources.append(state_source)
test = ROOT / 'android/app/src/test/java/org/reelbox/reelbox/PlayerOverlayAndroidTest.java'
input_hashes = {str(path): hashlib.sha256(path.read_bytes()).hexdigest()
for path in [*overlay_sources, test]}
(WORK / 'tested-inputs.json').write_text(json.dumps({
'robolectric': '4.16.1', 'runtime_sdk': 28, 'java': 17,
'looper': 'PAUSED', 'graphics': 'LEGACY', 'sha256': input_hashes,
}, indent=2) + '\n')
run([str(JDK / 'bin/java'), '-Xmx256m', f'-Djava.io.tmpdir={WORK / "tmp"}', '-cp', os.pathsep.join(map(str, compiler)),
'org.jetbrains.kotlin.cli.jvm.K2JVMCompiler', '-jvm-target', '17', '-no-stdlib', '-no-reflect',
'-classpath', cp, '-d', str(classes), *map(str, overlay_sources)], log='kotlin-compile.log')
run([*javac, str(test)], log='javac-overlay.log')
command = [*java, 'org.reelbox.reelbox.PlayerOverlayAndroidTest', str(WORK / 'overlay-results.xml')]
if args.test:
command.append(args.test)
run(command, log='overlay.log')
for path, digest in input_hashes.items():
if hashlib.sha256(Path(path).read_bytes()).hexdigest() != digest:
raise RuntimeError(f'Source changed during test execution; rerun before claiming verification: {path}')
print('Verified real Android view events, Handler timing and layout on host LEGACY graphics; not a device pixel/rendering test.')
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except subprocess.CalledProcessError as error:
print(f'FAILED: command exited {error.returncode}; logs: {WORK}', file=sys.stderr)
sys.exit(error.returncode)
except (OSError, RuntimeError) as error:
print(f'BLOCKED: {error}', file=sys.stderr)
sys.exit(2)