Port Android server runtime to GaCy Go

This commit is contained in:
CAtlas0784 committed 2026-11-02 02:39:37 +07:00
1 parent 92a4b4f545
commit 3c1186c781
20 files changed
+830 -798

No files matched your search

@@ -0,0 +1,119 @@
package com.gacy.android
import com.gacy.android.data.GoRuntimeInstaller
import org.json.JSONObject
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class GoRuntimeInstallerTest {
@get:Rule val tmp = TemporaryFolder()
private fun archive(resource: String, hotfix: String, extra: Pair<String, String>? = null, versions: String = "{}"): ByteArray {
val entries = linkedMapOf(
"persistent" to "original-save", "freesr-data.json" to "original-roster",
"server.json" to "server-config", "hotfix.json" to hotfix, "versions.json" to versions,
"res.json" to resource, "teleports.json" to "{}", "resources/MapEntrance.json" to "{}"
)
if (extra != null) entries[extra.first] = extra.second
val output = ByteArrayOutputStream()
ZipOutputStream(output).use { zip ->
entries.forEach { (name, content) ->
zip.putNextEntry(ZipEntry(name)); zip.write(content.toByteArray()); zip.closeEntry()
}
}
return output.toByteArray()
}
@Test fun updatePreservesUserDataAndRepairsMissingResources() {
val directory = tmp.newFolder("runtime")
val old = archive("old", """{"existing":{"asset":"bundled"}}""")
GoRuntimeInstaller(directory) { ByteArrayInputStream(old) }.prepare()
File(directory, "persistent").writeText("user-save")
File(directory, "freesr-data.json").writeText("user-roster")
File(directory, "hotfix.json").writeText("""{"existing":{"asset":"user"}}""")
File(directory, "versions.json").writeText("""{"existing":{"asset":"user"}}""")
val updated = archive("new", """{"existing":{"asset":"new"},"added":{"asset":"new"}}""", versions = """{"existing":{"asset":"new"},"added":{"asset":"new"}}""")
val installer = GoRuntimeInstaller(directory) { ByteArrayInputStream(updated) }
installer.prepare()
assertEquals("user-save", File(directory, "persistent").readText())
assertEquals("user-roster", File(directory, "freesr-data.json").readText())
assertEquals("new", File(directory, "res.json").readText())
val hotfix = JSONObject(File(directory, "hotfix.json").readText())
assertEquals("user", hotfix.getJSONObject("existing").getString("asset"))
assertEquals("new", hotfix.getJSONObject("added").getString("asset"))
val versions = JSONObject(File(directory, "versions.json").readText())
assertEquals("user", versions.getJSONObject("existing").getString("asset"))
assertEquals("new", versions.getJSONObject("added").getString("asset"))
File(directory, "resources/MapEntrance.json").delete()
installer.prepare()
assertTrue(File(directory, "resources/MapEntrance.json").isFile)
assertEquals("user-save", File(directory, "persistent").readText())
}
@Test fun rejectsPathsOutsideRuntimeDirectory() {
val directory = tmp.newFolder("runtime")
val zip = archive("data", "{}", "../outside" to "bad")
try {
GoRuntimeInstaller(directory) { ByteArrayInputStream(zip) }.prepare()
fail("Path traversal should be rejected")
} catch (_: IllegalArgumentException) { }
assertFalse(File(directory.parentFile, "outside").exists())
assertFalse(File(directory, ".asset-version").exists())
}
@Test fun installerUpgradeMergesVersionsEvenWhenArchiveIsUnchanged() {
val directory = tmp.newFolder("migration")
val zip = archive("data", "{}", versions = """{"new":{"asset":"bundle"}}""")
val installer = GoRuntimeInstaller(directory) { ByteArrayInputStream(zip) }
installer.prepare()
File(directory, "versions.json").writeText("""{"custom":{"asset":"user"}}""")
val marker = File(directory, ".asset-version")
marker.writeText(marker.readText().removePrefix("2:"))
installer.prepare()
val versions = JSONObject(File(directory, "versions.json").readText())
assertEquals("user", versions.getJSONObject("custom").getString("asset"))
assertEquals("bundle", versions.getJSONObject("new").getString("asset"))
}
@Test fun simultaneousStartsSerializeExtraction() {
val directory = tmp.newFolder("runtime")
val zip = archive("data", "{}")
val firstOpened = CountDownLatch(1)
val release = CountDownLatch(1)
val secondOpened = CountDownLatch(1)
val opens = AtomicInteger()
val installer = GoRuntimeInstaller(directory) {
if (opens.incrementAndGet() == 1) {
firstOpened.countDown()
check(release.await(5, TimeUnit.SECONDS))
} else {
secondOpened.countDown()
}
ByteArrayInputStream(zip)
}
val workers = Executors.newFixedThreadPool(2)
try {
val first = workers.submit { installer.prepare() }
assertTrue(firstOpened.await(5, TimeUnit.SECONDS))
val second = workers.submit { installer.prepare() }
assertFalse("a second install entered before the first was released", secondOpened.await(200, TimeUnit.MILLISECONDS))
release.countDown()
first.get(10, TimeUnit.SECONDS); second.get(10, TimeUnit.SECONDS)
assertEquals("data", File(directory, "res.json").readText())
} finally {
release.countDown()
workers.shutdownNow()
}
}
}
@@ -0,0 +1,23 @@
package com.gacy.android
import com.gacy.android.service.LogBuffer
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class LogBufferTest {
@After fun cleanup() = LogBuffer.clear()
@Test fun preservesBoundedDiagnosticsWhenRecentLogRollsOver() {
LogBuffer.clear()
repeat(101) { LogBuffer.append("[gateway] diagnostic-$it") }
repeat(501) { LogBuffer.append("ordinary-$it") }
val copied = LogBuffer.getAllText()
assertFalse(copied.contains("diagnostic-0\n"))
assertTrue(copied.contains("diagnostic-1\n"))
assertTrue(copied.contains("ordinary-500"))
LogBuffer.clear()
assertTrue(LogBuffer.getAllText().isEmpty())
}
}
@@ -1,14 +1,37 @@
package com.gacy.android
import com.gacy.android.model.SceneConfig
import com.gacy.android.model.ServerConfig
import com.gacy.android.model.ServerExecutionMode
import com.gacy.android.model.TrailblazerConfig
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.assertNull
import org.junit.Test
class ModelAndConfigTest {
@Test
fun networkSettingsRejectInvalidAddressesAndPorts() {
val original = ServerConfig(serverKey = "keep")
assertNull(original.withNetworkInput(ServerExecutionMode.LOCAL_EMBEDDED, "0.0.0.0", "21000", "23301"))
assertNull(original.withNetworkInput(ServerExecutionMode.LOCAL_EMBEDDED, "example.com/path", "21000", "23301"))
assertNull(original.withNetworkInput(ServerExecutionMode.LOCAL_EMBEDDED, "example.com", "65536", "23301"))
assertNull(original.withNetworkInput(ServerExecutionMode.LOCAL_EMBEDDED, "example.com", "abc", "23301"))
val valid = original.withNetworkInput(ServerExecutionMode.REMOTE_SERVER, " example.com ", "1234", "5678")!!
assertEquals("example.com", valid.remoteHost)
assertEquals(1234, valid.dispatchPort)
assertEquals("keep", valid.serverKey)
}
@Test
fun localApiUsesLoopbackWhileRemoteApiUsesConfiguredHost() {
val local = ServerConfig(remoteHost = "192.168.1.42", dispatchPort = 24567)
assertEquals("http://127.0.0.1:24567", local.dispatchUrl)
assertEquals("http://192.168.1.42:24567", local.copy(executionMode = ServerExecutionMode.REMOTE_SERVER).dispatchUrl)
}
@Test
fun testSceneConfigSerialization() {
val config = SceneConfig(
@@ -0,0 +1,17 @@
package com.gacy.android
import com.gacy.android.data.parseUnstuckResponse
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class UnstuckResponseTest {
@Test fun confirmsOnlyQueuedRequestsWithAnId() {
assertTrue(parseUnstuckResponse("""{"state":"queued","request_id":"abc"}""").success)
assertFalse(parseUnstuckResponse("""{"retcode":0,"message":"OK"}""").success)
assertFalse(parseUnstuckResponse("""{"state":"queued"}""").success)
assertFalse(parseUnstuckResponse("""{"state":"queued","request_id":null}""").success)
assertFalse(parseUnstuckResponse("""{"state":"failed","request_id":"abc"}""").success)
assertFalse(parseUnstuckResponse("not json").success)
}
}