Skip to content
Open
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ What the app is and how it is built is in [`README.md`](README.md) and [`docs/ar
|---|---|
| `app/` | `:app`, the UI process: Kotlin and Compose, no Qt |
| `engine-service/` | `:engine-service`: `EngineService` in the `:engine` process, and the Qt libraries `scripts/build-apk.sh` stages into `engine-service/qtdeploy/` |
| `protocol/` | `:protocol`, plain Kotlin/JVM: the socket protocol and `EngineLink`, with the JVM tests |
| `protocol/` | `:protocol`, plain Kotlin/JVM: the socket protocol and `EngineLink`, the script windows' state and layout, what a label draws, with the JVM tests |
| `native/` | C++: the engine host (`enginehost/`), the engine interface (`api/`), the stand-in engine (`standin/`), the host tests (`tests/`); `third_party/` is vendored Lua 5.1.5 |
| `scripts/` | Builds (APK, release, OpenSSL, libmudlet and its dependencies), `check-apk.sh` and `lint.sh` |
| `docs/` | [`architecture.md`](docs/architecture.md), [`protocol.md`](docs/protocol.md), [`ptb.md`](docs/ptb.md), [`decisions/`](docs/decisions), [`foundation-checklist.md`](docs/foundation-checklist.md), [`roadmap.md`](docs/roadmap.md) |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ behind "Debug", the engine's start times, heartbeat, process ids and buttons tha
|---|---|
| [`app/`](app) | `:app`, the UI process: Compose, the app's screens and, in debug builds, the self-test |
| [`engine-service/`](engine-service) | `:engine-service`, an Android library: `EngineService` in the `:engine` process, its notifications, the Qt glue, and the Qt libraries staged for the APK |
| [`protocol/`](protocol) | `:protocol`, plain Kotlin/JVM: the socket protocol's wire format and `EngineLink`, the client that reconnects to the engine, and their tests |
| [`protocol/`](protocol) | `:protocol`, plain Kotlin/JVM: the socket protocol's wire format and `EngineLink`, the client that reconnects to the engine, the script windows' state and layout, what a label draws, and their tests |
| [`native/`](native) | C++: the engine host (`enginehost/`: `main()`, the socket bridge, libmudlet's adapter, the Android glue), the engine interface (`api/`), the stand-in engine (`standin/`), and the host tests (`tests/`) |
| [`scripts/`](scripts) | Building the APK, and libmudlet and its dependencies for Android and the host |
| [`docs/`](docs) | [`architecture.md`](docs/architecture.md), [`protocol.md`](docs/protocol.md), [`ptb.md`](docs/ptb.md), [`decisions/`](docs/decisions), [`roadmap.md`](docs/roadmap.md) |
Expand Down
93 changes: 92 additions & 1 deletion app/src/androidTest/kotlin/org/mudlet/android/ui/FakeEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import java.io.DataInputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.Base64
import java.util.UUID
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -100,7 +101,7 @@ class FakeEngine : AutoCloseable {
val hello: JSONObject

init {
socket.soTimeout = 10_000
socket.soTimeout = READ_TIMEOUT_MS
hello = readFrame(input)
if (hello.optString("t") != "hello") throw IOException("the UI began with $hello")
send(JSONObject().put("t", "hello").put("v", 1).put("pid", FAKE_PID).put("instance", instance))
Expand All @@ -127,6 +128,29 @@ class FakeEngine : AutoCloseable {
throw AssertionError("the UI sent no '$type' within $timeoutMs ms")
}

/**
* The last request of type [type] the UI sent before it went quiet for [quietMs]: what it settled on, where it
* may first have sent what it held from before, as ScriptWindowsClient sends a new engine the last size.
*/
fun receiveLast(type: String, quietMs: Int = 1_000): JSONObject {
var last = receive(type)
socket.soTimeout = quietMs
try {
while (true) {
val frame = try {
readFrame(input)
} catch (_: IOException) {
break
}
received.add(frame)
if (frame.optString("t") == type) last = frame
}
} finally {
socket.soTimeout = READ_TIMEOUT_MS
}
return last
}

fun clear() = send(JSONObject().put("t", "clear"))

/** [runs]: each [start, length, fg, bg, attributes], as docs/protocol.md's "Runs" says. */
Expand All @@ -151,6 +175,46 @@ class FakeEngine : AutoCloseable {

fun synced(seq: Long) = send(JSONObject().put("t", "synced").put("seq", seq))

/** [windows] as [window] makes them; [removed]: each the name and the kind of a window that went. */
fun windows(windows: List<JSONObject>, full: Boolean = false, removed: List<Pair<String, String>> = emptyList()) = send(
JSONObject().put("t", "windows").put("full", full).put("windows", JSONArray(windows))
.put("removed", JSONArray(removed.map { (name, kind) -> JSONObject().put("name", name).put("kind", kind) })),
)

/** The last rows of the console [window], [rows] of them up to its last row, without colours. */
fun windowText(window: String, rows: List<String>, lineCount: Int = rows.size) = send(
JSONObject().put("t", "windowText").put("window", window).put("lineCount", lineCount).put("first", lineCount - rows.size)
.put("lines", JSONArray(rows.map { JSONObject().put("text", it).put("runs", JSONArray()).put("prompt", false).put("wrapped", false) })),
)

fun mainArea(w: Int, h: Int, columns: Int, rows: Int, textX: Int, textY: Int, textW: Int, textH: Int) = send(
JSONObject().put("t", "mainArea").put("w", w).put("h", h).put("columns", columns).put("rows", rows)
.put("textX", textX).put("textY", textY).put("textW", textW).put("textH", textH),
)

fun windowCommandLine(name: String, text: String) = send(JSONObject().put("t", "windowCommandLine").put("name", name).put("text", text))

/** What the label [name] shows (docs/protocol.md, `label`): [text] as rich text, or the file [image]. */
fun label(
name: String,
text: String = "",
image: String = "",
toolTip: String = "",
accessibleName: String = "",
accessibleButton: Boolean = false,
styleSheet: String = "",
) = send(
JSONObject().put("t", "label").put("name", name).put("text", text).put("image", image).put("svg", false)
.put("movie", "").put("moviePaused", false).put("movieFrame", -1).put("movieSpeed", 100).put("toolTip", toolTip)
.put("clickThrough", false).put("accessibleName", accessibleName).put("accessibleButton", accessibleButton)
.put("styleSheet", styleSheet),
)

/** The answer to the `getImage` whose id was [re]: [png], a PNG of [w] by [h] pixels. */
fun image(path: String, png: ByteArray, w: Int, h: Int, re: Any) = send(
JSONObject().put("t", "image").put("path", path).put("png", Base64.getEncoder().encodeToString(png)).put("w", w).put("h", h).put("re", re),
)

/** As the engine process dying: the UI sees the connection end. */
override fun close() {
// shutdownInput wakes the UI's reader at once, where close alone may leave it blocked
Expand All @@ -167,6 +231,33 @@ class FakeEngine : AutoCloseable {
const val FAKE_PID = 4242
const val FIRST_INSTANCE = "fake-1"

// How long a read waits for the UI
const val READ_TIMEOUT_MS = 10_000

/**
* A shown window of a `windows` message (docs/protocol.md): a mini console, unless told, in the main window at
* [x], [y] in Mudlet's pixels. A console has [columns] and [rows]; [dockArea] puts a user window in a dock.
*/
fun window(
name: String,
kind: String = "miniConsole",
x: Int = 0,
y: Int = 0,
w: Int = 100,
h: Int = 100,
columns: Int = 40,
rows: Int = 10,
parent: String = "",
parentKind: String = "",
dockArea: String? = null,
): JSONObject {
val window = JSONObject().put("name", name).put("kind", kind).put("parent", parent).put("parentKind", parentKind)
.put("x", x).put("y", y).put("w", w).put("h", h).put("visible", true).put("shown", true).put("stack", 0)
if (kind == "miniConsole" || kind == "userWindow") window.put("columns", columns).put("rows", rows)
if (dockArea != null) window.put("inDock", true).put("dockArea", dockArea).put("dockFloating", false).put("dockTitle", name)
return window
}

private fun readFrame(input: DataInputStream): JSONObject {
val n = input.readInt()
val payload = ByteArray(n)
Expand Down
Loading
Loading