Added several unit tests and improved testing infra.

This commit is contained in:
Daan Vanden Bosch 2020-11-08 14:27:02 +01:00
parent 25f015dfbb
commit 346a2cb4f9
57 changed files with 572 additions and 187 deletions

View File

@ -10,12 +10,18 @@ abstract class TrackedDisposable : Disposable {
init {
disposableCount++
if (trackPrecise) {
@Suppress("LeakingThis")
disposables.add(this)
}
}
final override fun dispose() {
if (!disposed) {
disposed = true
disposableCount--
disposables.remove(this)
internalDispose()
}
}
@ -25,16 +31,45 @@ abstract class TrackedDisposable : Disposable {
}
companion object {
const val DISPOSABLE_PRINT_COUNT = 10
var disposables: MutableSet<Disposable> = mutableSetOf()
var trackPrecise = false
var disposableCount: Int = 0
private set
fun checkNoLeaks(block: () -> Unit) {
val count = disposableCount
inline fun checkNoLeaks(trackPrecise: Boolean = false, block: () -> Unit) {
val initialCount = disposableCount
val initialTrackPrecise = this.trackPrecise
val initialDisposables = disposables
this.trackPrecise = trackPrecise
disposables = mutableSetOf()
try {
block()
checkLeaks(disposableCount - initialCount)
} finally {
check(count == disposableCount) { "TrackedDisposables were leaked." }
this.trackPrecise = initialTrackPrecise
disposables = initialDisposables
}
}
fun checkLeaks(leakCount: Int) {
buildString {
append("$leakCount TrackedDisposables were leaked")
if (trackPrecise) {
append(": ")
disposables.take(DISPOSABLE_PRINT_COUNT).joinTo(this) {
it::class.simpleName ?: "Anonymous"
}
if (disposables.size > DISPOSABLE_PRINT_COUNT) {
append(",..")
}
}
append(".")
}
}
}

View File

@ -48,6 +48,7 @@ kotlin {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation(project(":test-utils"))
}
}

View File

@ -11,6 +11,20 @@ class QuestNpc(
override var areaId: Int,
val data: Buffer,
) : QuestEntity<NpcType> {
constructor(
type: NpcType,
episode: Episode,
areaId: Int,
wave: Int,
) : this(episode, areaId, Buffer.withSize(NPC_BYTE_SIZE)) {
this.type = type
// TODO: Set default data.
// Set area_id after type, because you might want to overwrite the area_id that type has
// determined.
this.areaId = areaId
// TODO: Set wave properties.
}
var typeId: Short
get() = data.getShort(0)
set(value) {

View File

@ -1,11 +1,12 @@
package world.phantasmal.lib.assembly
import world.phantasmal.core.Success
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class AssemblyTests {
class AssemblyTests : LibTestSuite() {
@Test
fun assemble_basic_script() {
val result = assemble("""

View File

@ -1,11 +1,12 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.toInstructions
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ControlFlowGraphTests {
class ControlFlowGraphTests : LibTestSuite() {
@Test
fun single_instruction() {
val im = toInstructions("""

View File

@ -1,13 +1,14 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.toInstructions
import kotlin.test.Test
import kotlin.test.assertEquals
private const val MAX_REGISTER_VALUES_SIZE: Long = 1L shl 32
class GetRegisterValueTests {
class GetRegisterValueTests : LibTestSuite() {
@Test
fun when_no_instruction_sets_the_register_zero_is_returned() {
val im = toInstructions("""

View File

@ -1,11 +1,12 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ValueSetTests {
class ValueSetTests : LibTestSuite() {
@Test
fun empty_set_has_size_0() {
val vs = ValueSet.empty()

View File

@ -1,11 +1,12 @@
package world.phantasmal.lib.buffer
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class BufferTests {
class BufferTests : LibTestSuite() {
@Test
fun withCapacity() {
withCapacity(Endianness.Little)

View File

@ -2,11 +2,12 @@ package world.phantasmal.lib.compression.prs
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.cursor
import world.phantasmal.lib.test.LibTestSuite
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
class PrsCompressTests {
class PrsCompressTests : LibTestSuite() {
@Test
fun edge_case_0_bytes() {
val compressed = prsCompress(Buffer.withSize(0).cursor())

View File

@ -3,13 +3,13 @@ package world.phantasmal.lib.compression.prs
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.Cursor
import world.phantasmal.lib.cursor.cursor
import world.phantasmal.lib.test.asyncTest
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
class PrsDecompressTests {
class PrsDecompressTests : LibTestSuite() {
@Test
fun edge_case_0_bytes() {
testWithBuffer(Buffer.withSize(0))

View File

@ -1,6 +1,7 @@
package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
@ -8,7 +9,7 @@ import kotlin.test.assertEquals
* Test suite for all [Cursor] implementations. There is a subclass of this suite for every [Cursor]
* implementation.
*/
abstract class CursorTests {
abstract class CursorTests : LibTestSuite() {
abstract fun createCursor(
bytes: ByteArray,
endianness: Endianness,

View File

@ -2,7 +2,7 @@ package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer
import kotlin.math.abs
import world.phantasmal.testUtils.assertCloseTo
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@ -144,8 +144,8 @@ abstract class WritableCursorTests : CursorTests() {
// The read floats won't be exactly the same as the written floats in Kotlin JS, because
// they're backed by numbers (64-bit floats).
assertTrue(abs(1337.9001f - cursor.float()) < 0.001)
assertTrue(abs(103.502f - cursor.float()) < 0.001)
assertCloseTo(1337.9001f, cursor.float(), epsilon = 0.001f)
assertCloseTo(103.502f, cursor.float(), epsilon = 0.001f)
assertEquals(8, cursor.position)
}

View File

@ -0,0 +1,28 @@
package world.phantasmal.lib.fileFormats
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile
import world.phantasmal.testUtils.assertCloseTo
import kotlin.test.Test
import kotlin.test.assertEquals
class AreaCollisionGeometryTests : LibTestSuite() {
@Test
fun parse_forest_1() = asyncTest {
val obj = parseAreaCollisionGeometry(readFile("/map_forest01c.rel"))
assertEquals(69, obj.meshes.size)
assertEquals(11, obj.meshes[0].vertices.size)
assertCloseTo(-589.5195f, obj.meshes[0].vertices[0].x)
assertCloseTo(16.7166f, obj.meshes[0].vertices[0].y)
assertCloseTo(-218.6852f, obj.meshes[0].vertices[0].z)
assertEquals(12, obj.meshes[0].triangles.size)
assertEquals(0b100000001, obj.meshes[0].triangles[0].flags)
assertEquals(5, obj.meshes[0].triangles[0].index1)
assertEquals(0, obj.meshes[0].triangles[0].index2)
assertEquals(7, obj.meshes[0].triangles[0].index3)
assertCloseTo(0.0137f, obj.meshes[0].triangles[0].normal.x)
assertCloseTo(0.9994f, obj.meshes[0].triangles[0].normal.y)
assertCloseTo(-0.0307f, obj.meshes[0].triangles[0].normal.z)
}
}

View File

@ -1,13 +1,13 @@
package world.phantasmal.lib.fileFormats.ninja
import world.phantasmal.core.Success
import world.phantasmal.lib.test.asyncTest
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class NinjaTests {
class NinjaTests : LibTestSuite() {
@Test
fun can_parse_rag_rappy_model() = asyncTest {
val result = parseNj(readFile("/RagRappy.nj"))

View File

@ -1,11 +1,11 @@
package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.lib.test.asyncTest
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile
import kotlin.test.Test
import kotlin.test.assertEquals
class BinTests {
class BinTests : LibTestSuite() {
@Test
fun parse_quest_towards_the_future() = asyncTest {
val bin = parseBin(readFile("/quest118_e_decompressed.bin"))

View File

@ -5,11 +5,12 @@ import world.phantasmal.lib.assembly.InstructionSegment
import world.phantasmal.lib.assembly.OP_BB_MAP_DESIGNATE
import world.phantasmal.lib.assembly.OP_SET_EPISODE
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ByteCodeTests {
class ByteCodeTests : LibTestSuite() {
@Test
fun minimal() {
val buffer = Buffer.fromByteArray(ubyteArrayOf(

View File

@ -1,11 +1,11 @@
package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.lib.test.asyncTest
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile
import kotlin.test.Test
import kotlin.test.assertEquals
class DatTests {
class DatTests : LibTestSuite() {
@Test
fun parse_quest_towards_the_future() = asyncTest {
val dat = parseDat(readFile("/quest118_e_decompressed.dat"))

View File

@ -1,12 +1,12 @@
package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.lib.test.asyncTest
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class QstTests {
class QstTests : LibTestSuite() {
@Test
fun parse_a_GC_quest() = asyncTest {
val cursor = readFile("/lost_heat_sword_gc.qst")

View File

@ -2,13 +2,13 @@ package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.core.Success
import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.test.asyncTest
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class QuestTests {
class QuestTests : LibTestSuite() {
@Test
fun parseBinDatToQuest_with_towards_the_future() = asyncTest {
val result = parseBinDatToQuest(readFile("/quest118_e.bin"), readFile("/quest118_e.dat"))

View File

@ -0,0 +1,9 @@
package world.phantasmal.lib.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.AbstractTestSuite
import world.phantasmal.testUtils.TestContext
abstract class LibTestSuite : AbstractTestSuite<TestContext>() {
override fun createContext(disposer: Disposer) = TestContext(disposer)
}

View File

@ -6,14 +6,6 @@ import world.phantasmal.lib.assembly.assemble
import world.phantasmal.lib.cursor.Cursor
import kotlin.test.assertTrue
/**
* Ensure you return the value of this function in your test function. On Kotlin/JS this function
* actually returns a Promise. If this promise is not returned from the test function, the testing
* framework won't wait for its completion. This is a workaround for issue
* [https://youtrack.jetbrains.com/issue/KT-22228].
*/
expect fun asyncTest(block: suspend () -> Unit)
expect suspend fun readFile(path: String): Cursor
fun toInstructions(assembly: String): List<InstructionSegment> {

Binary file not shown.

View File

@ -1,15 +1,11 @@
package world.phantasmal.lib.test
import kotlinx.browser.window
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.await
import kotlinx.coroutines.promise
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.cursor.ArrayBufferCursor
import world.phantasmal.lib.cursor.Cursor
actual fun asyncTest(block: suspend () -> Unit): dynamic = GlobalScope.promise { block() }
actual suspend fun readFile(path: String): Cursor {
return window.fetch(path)
.then {

View File

@ -2,15 +2,10 @@
package world.phantasmal.lib.test
import kotlinx.coroutines.runBlocking
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.Cursor
import world.phantasmal.lib.cursor.cursor
actual fun asyncTest(block: suspend () -> Unit) {
runBlocking { block() }
}
actual suspend fun readFile(path: String): Cursor {
val stream = {}::class.java.getResourceAsStream(path)
?: error("""Couldn't load resource "$path".""")

View File

@ -1,6 +1,6 @@
package world.phantasmal.observable
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
@ -10,7 +10,7 @@ typealias ObservableAndEmit = Pair<Observable<*>, () -> Unit>
* Test suite for all [Observable] implementations. There is a subclass of this suite for every
* [Observable] implementation.
*/
abstract class ObservableTests : TestSuite() {
abstract class ObservableTests : ObservableTestSuite() {
protected abstract fun create(): ObservableAndEmit
@Test

View File

@ -0,0 +1,9 @@
package world.phantasmal.observable.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.AbstractTestSuite
import world.phantasmal.testUtils.TestContext
abstract class ObservableTestSuite : AbstractTestSuite<TestContext>() {
override fun createContext(disposer: Disposer) = TestContext(disposer)
}

View File

@ -1,10 +1,10 @@
package world.phantasmal.observable.value
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
class StaticValTests : TestSuite() {
class StaticValTests : ObservableTestSuite() {
@Test
fun observing_StaticVal_should_never_create_leaks() = test {
val static = StaticVal("test value")

View File

@ -1,9 +1,9 @@
package world.phantasmal.observable.value
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.*
class ValCreationTests : TestSuite() {
class ValCreationTests : ObservableTestSuite() {
@Test
fun test_value() = test {
assertEquals(7, value(7).value)

View File

@ -1,10 +1,10 @@
package world.phantasmal.observable.value.list
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
class StaticListValTests : TestSuite() {
class StaticListValTests : ObservableTestSuite() {
@Test
fun observing_StaticListVal_should_never_create_leaks() = test {
val static = StaticListVal(listOf(1, 2, 3))

View File

@ -9,6 +9,8 @@ kotlin {
browser {}
}
jvm()
sourceSets {
commonMain {
dependencies {
@ -24,5 +26,11 @@ kotlin {
api(kotlin("test-js"))
}
}
named("jvmMain") {
dependencies {
api(kotlin("test"))
}
}
}
}

View File

@ -0,0 +1,28 @@
package world.phantasmal.testUtils
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.TrackedDisposable
abstract class AbstractTestSuite<Ctx : TestContext> {
fun test(testBlock: Ctx.() -> Unit) {
TrackedDisposable.checkNoLeaks(trackPrecise = true) {
val disposer = Disposer()
testBlock(createContext(disposer))
disposer.dispose()
}
}
fun asyncTest(testBlock: suspend Ctx.() -> Unit) = world.phantasmal.testUtils.asyncTest {
TrackedDisposable.checkNoLeaks(trackPrecise = true) {
val disposer = Disposer()
testBlock(createContext(disposer))
disposer.dispose()
}
}
protected abstract fun createContext(disposer: Disposer): Ctx
}

View File

@ -0,0 +1,12 @@
package world.phantasmal.testUtils
import kotlin.math.abs
import kotlin.test.assertTrue
fun assertCloseTo(expected: Double, actual: Double, epsilon: Double = 0.001) {
assertTrue(abs(expected - actual) <= epsilon)
}
fun assertCloseTo(expected: Float, actual: Float, epsilon: Float = 0.001f) {
assertTrue(abs(expected - actual) <= epsilon)
}

View File

@ -0,0 +1,9 @@
package world.phantasmal.testUtils
/**
* Ensure you return the value of this function in your test function. On Kotlin/JS this function
* actually returns a Promise. If this promise is not returned from the test function, the testing
* framework won't wait for its completion. This is a workaround for issue
* [https://youtrack.jetbrains.com/issue/KT-22228].
*/
expect fun asyncTest(block: suspend () -> Unit)

View File

@ -0,0 +1,11 @@
package world.phantasmal.testUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import world.phantasmal.core.disposable.Disposer
open class TestContext(val disposer: Disposer) {
val scope: CoroutineScope = object : CoroutineScope {
override val coroutineContext = Job()
}
}

View File

@ -1,26 +0,0 @@
package world.phantasmal.testUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.TrackedDisposable
import kotlin.test.assertEquals
abstract class TestSuite {
fun test(block: TestContext.() -> Unit) {
val initialDisposableCount = TrackedDisposable.disposableCount
val disposer = Disposer()
block(TestContext(disposer))
disposer.dispose()
val leakCount = TrackedDisposable.disposableCount - initialDisposableCount
assertEquals(0, leakCount, "TrackedDisposables were leaked")
}
class TestContext(val disposer: Disposer) {
val scope: CoroutineScope = object : CoroutineScope {
override val coroutineContext = Job()
}
}
}

View File

@ -0,0 +1,6 @@
package world.phantasmal.testUtils
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
actual fun asyncTest(block: suspend () -> Unit): dynamic = GlobalScope.promise { block() }

View File

@ -0,0 +1,9 @@
@file:JvmName("AsyncTestJvm")
package world.phantasmal.testUtils
import kotlinx.coroutines.runBlocking
actual fun asyncTest(block: suspend () -> Unit) {
runBlocking { block() }
}

View File

@ -13,8 +13,8 @@ import world.phantasmal.web.questEditor.controllers.QuestInfoController
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
import world.phantasmal.web.questEditor.loading.QuestLoader
import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager
import world.phantasmal.web.questEditor.rendering.EntityManipulator
import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager
import world.phantasmal.web.questEditor.rendering.QuestRenderer
import world.phantasmal.web.questEditor.stores.AreaStore
import world.phantasmal.web.questEditor.stores.QuestEditorStore
@ -63,7 +63,7 @@ class QuestEditor(
// Main Widget
return QuestEditorWidget(
scope,
QuestEditorToolbar(scope, toolbarController),
{ s -> QuestEditorToolbar(s, toolbarController) },
{ s -> QuestInfoWidget(s, questInfoController) },
{ s -> NpcCountsWidget(s, npcCountsController) },
{ s -> QuestEditorRendererWidget(s, canvas, renderer) }

View File

@ -39,17 +39,13 @@ class EntityManipulator(
init {
state = IdleState(questEditorStore, renderer, enabled)
observe(questEditorStore.selectedEntity, ::selectedEntityChanged)
observe(questEditorStore.selectedEntity) { state.cancel() }
addDisposables(
disposableListener(renderer.canvas, "pointerdown", ::onPointerDown)
)
}
private fun selectedEntityChanged(entity: QuestEntityModel<*, *>?) {
state.cancel()
}
private fun onPointerDown(e: PointerEvent) {
processPointerEvent(e)

View File

@ -17,9 +17,12 @@ private class TestWidget(scope: CoroutineScope) : Widget(scope) {
}
}
/**
* Takes ownership of the widgets created by the given createWidget functions.
*/
class QuestEditorWidget(
scope: CoroutineScope,
private val toolbar: Widget,
private val createToolbar: (CoroutineScope) -> Widget,
private val createQuestInfoWidget: (CoroutineScope) -> Widget,
private val createNpcCountsWidget: (CoroutineScope) -> Widget,
private val createQuestRendererWidget: (CoroutineScope) -> Widget,
@ -28,7 +31,7 @@ class QuestEditorWidget(
div {
className = "pw-quest-editor-quest-editor"
addChild(toolbar)
addChild(createToolbar(scope))
addChild(DockWidget(
scope,
item = DockedRow(

View File

@ -31,6 +31,11 @@ class Viewer(
val renderer = addDisposable(MeshRenderer(viewerStore, canvas, createEngine(canvas)))
// Main Widget
return ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), canvas, renderer)
return ViewerWidget(
scope,
{ s -> ViewerToolbar(s, viewerToolbarController) },
canvas,
renderer
)
}
}

View File

@ -8,9 +8,12 @@ import world.phantasmal.web.core.widgets.RendererWidget
import world.phantasmal.webui.dom.div
import world.phantasmal.webui.widgets.Widget
/**
* Takes ownership of the widget returned by [createToolbar].
*/
class ViewerWidget(
scope: CoroutineScope,
private val toolbar: Widget,
private val createToolbar: (CoroutineScope) -> Widget,
private val canvas: HTMLCanvasElement,
private val renderer: Renderer,
) : Widget(scope) {
@ -18,7 +21,7 @@ class ViewerWidget(
div {
className = "pw-viewer-viewer"
addChild(toolbar)
addChild(createToolbar(scope))
div {
className = "pw-viewer-viewer-container"

View File

@ -1,41 +1,26 @@
package world.phantasmal.web.application
import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import kotlinx.browser.document
import kotlinx.coroutines.cancel
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.disposable
import world.phantasmal.core.disposable.use
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
class ApplicationTests : TestSuite() {
class ApplicationTests : WebTestSuite() {
@Test
fun initialization_and_shutdown_should_succeed_without_throwing() = test {
(listOf(null) + PwTool.values().toList()).forEach { tool ->
(listOf(null) + PwToolType.values().toList()).forEach { tool ->
Disposer().use { disposer ->
val httpClient = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
})
}
}
disposer.add(disposable { httpClient.cancel() })
val appUrl = TestApplicationUrl(if (tool == null) "" else "/${tool.slug}")
disposer.add(
Application(
scope,
rootElement = document.body!!,
assetLoader = AssetLoader(basePath = "", httpClient),
assetLoader = components.assetLoader,
applicationUrl = appUrl,
createEngine = { Engine(it) }
)

View File

@ -1,14 +1,15 @@
package world.phantasmal.web.core.controllers
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.PwTool
import world.phantasmal.testUtils.TestContext
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
class PathAwareTabControllerTests : TestSuite() {
class PathAwareTabControllerTests : WebTestSuite() {
@Test
fun activeTab_is_initialized_correctly() = test {
setup { ctrl, appUrl ->
@ -23,7 +24,7 @@ class PathAwareTabControllerTests : TestSuite() {
setup { ctrl, appUrl ->
ctrl.setActiveTab(ctrl.tabs[2])
assertEquals("/${PwTool.HuntOptimizer.slug}/c", appUrl.url.value)
assertEquals("/${PwToolType.HuntOptimizer.slug}/c", appUrl.url.value)
assertEquals(1, appUrl.historyEntries)
assertFalse(appUrl.canGoForward)
}
@ -32,7 +33,7 @@ class PathAwareTabControllerTests : TestSuite() {
@Test
fun activeTab_changes_when_applicationUrl_changes() = test {
setup { ctrl, applicationUrl ->
applicationUrl.pushUrl("/${PwTool.HuntOptimizer.slug}/c")
applicationUrl.pushUrl("/${PwToolType.HuntOptimizer.slug}/c")
assertEquals("/c", ctrl.activeTab.value?.path)
}
@ -44,7 +45,7 @@ class PathAwareTabControllerTests : TestSuite() {
val uiStore = disposer.add(UiStore(scope, appUrl))
disposer.add(
PathAwareTabController(scope, uiStore, PwTool.HuntOptimizer, listOf(
PathAwareTabController(uiStore, PwToolType.HuntOptimizer, listOf(
PathAwareTab("A", "/a"),
PathAwareTab("B", "/b"),
PathAwareTab("C", "/c"),
@ -55,11 +56,11 @@ class PathAwareTabControllerTests : TestSuite() {
assertFalse(appUrl.canGoForward)
assertEquals("/${uiStore.defaultTool.slug}", appUrl.url.value)
uiStore.setCurrentTool(PwTool.HuntOptimizer)
uiStore.setCurrentTool(PwToolType.HuntOptimizer)
assertEquals(1, appUrl.historyEntries)
assertFalse(appUrl.canGoForward)
assertEquals("/${PwTool.HuntOptimizer.slug}", appUrl.url.value)
assertEquals("/${PwToolType.HuntOptimizer.slug}", appUrl.url.value)
appUrl.back()
@ -69,12 +70,12 @@ class PathAwareTabControllerTests : TestSuite() {
private fun TestContext.setup(
block: (PathAwareTabController<PathAwareTab>, applicationUrl: TestApplicationUrl) -> Unit,
) {
val applicationUrl = TestApplicationUrl("/${PwTool.HuntOptimizer.slug}/b")
val applicationUrl = TestApplicationUrl("/${PwToolType.HuntOptimizer.slug}/b")
val uiStore = disposer.add(UiStore(scope, applicationUrl))
uiStore.setCurrentTool(PwTool.HuntOptimizer)
uiStore.setCurrentTool(PwToolType.HuntOptimizer)
val ctrl = disposer.add(
PathAwareTabController(scope, uiStore, PwTool.HuntOptimizer, listOf(
PathAwareTabController(uiStore, PwToolType.HuntOptimizer, listOf(
PathAwareTab("A", "/a"),
PathAwareTab("B", "/b"),
PathAwareTab("C", "/c"),

View File

@ -1,20 +1,20 @@
package world.phantasmal.web.core.store
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
class UiStoreTests : TestSuite() {
class UiStoreTests : WebTestSuite() {
@Test
fun applicationUrl_is_initialized_correctly() = test {
val applicationUrl = TestApplicationUrl("/")
val uiStore = disposer.add(UiStore(scope, applicationUrl))
assertEquals(PwTool.Viewer, uiStore.currentTool.value)
assertEquals("/${PwTool.Viewer.slug}", applicationUrl.url.value)
assertEquals(PwToolType.Viewer, uiStore.currentTool.value)
assertEquals("/${PwToolType.Viewer.slug}", applicationUrl.url.value)
}
@Test
@ -22,7 +22,7 @@ class UiStoreTests : TestSuite() {
val applicationUrl = TestApplicationUrl("/")
val uiStore = disposer.add(UiStore(scope, applicationUrl))
PwTool.values().forEach { tool ->
PwToolType.values().forEach { tool ->
uiStore.setCurrentTool(tool)
assertEquals(tool, uiStore.currentTool.value)
@ -35,13 +35,13 @@ class UiStoreTests : TestSuite() {
val applicationUrl = TestApplicationUrl("/")
val uiStore = disposer.add(UiStore(scope, applicationUrl))
assertEquals(PwTool.Viewer, uiStore.currentTool.value)
assertEquals("/${PwTool.Viewer.slug}", applicationUrl.url.value)
assertEquals(PwToolType.Viewer, uiStore.currentTool.value)
assertEquals("/${PwToolType.Viewer.slug}", applicationUrl.url.value)
listOf("/models", "/textures", "/animations").forEach { prefix ->
uiStore.setPathPrefix(prefix, replace = false)
assertEquals("/${PwTool.Viewer.slug}${prefix}", applicationUrl.url.value)
assertEquals("/${PwToolType.Viewer.slug}${prefix}", applicationUrl.url.value)
}
}
@ -50,7 +50,7 @@ class UiStoreTests : TestSuite() {
val applicationUrl = TestApplicationUrl("/")
val uiStore = disposer.add(UiStore(scope, applicationUrl))
PwTool.values().forEach { tool ->
PwToolType.values().forEach { tool ->
listOf("/a", "/b", "/c").forEach { path ->
applicationUrl.url.value = "/${tool.slug}$path"
@ -67,13 +67,13 @@ class UiStoreTests : TestSuite() {
assertEquals("/${uiStore.defaultTool.slug}", appUrl.url.value)
uiStore.setCurrentTool(PwTool.HuntOptimizer)
uiStore.setCurrentTool(PwToolType.HuntOptimizer)
assertEquals("/${PwTool.HuntOptimizer.slug}", appUrl.url.value)
assertEquals("/${PwToolType.HuntOptimizer.slug}", appUrl.url.value)
uiStore.setPathPrefix("/prefix", replace = true)
assertEquals("/${PwTool.HuntOptimizer.slug}/prefix", appUrl.url.value)
assertEquals("/${PwToolType.HuntOptimizer.slug}/prefix", appUrl.url.value)
appUrl.back()
@ -81,6 +81,6 @@ class UiStoreTests : TestSuite() {
appUrl.forward()
assertEquals("/${PwTool.HuntOptimizer.slug}/prefix", appUrl.url.value)
assertEquals("/${PwToolType.HuntOptimizer.slug}/prefix", appUrl.url.value)
}
}

View File

@ -1,37 +1,18 @@
package world.phantasmal.web.huntOptimizer
import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import kotlinx.coroutines.cancel
import world.phantasmal.core.disposable.disposable
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
class HuntOptimizerTests : TestSuite() {
class HuntOptimizerTests : WebTestSuite() {
@Test
fun initialization_and_shutdown_should_succeed_without_throwing() = test {
val httpClient = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
})
}
}
disposer.add(disposable { httpClient.cancel() })
val uiStore =
disposer.add(UiStore(scope, TestApplicationUrl("/${PwToolType.HuntOptimizer}")))
val uiStore = disposer.add(UiStore(scope, TestApplicationUrl("/${PwTool.HuntOptimizer}")))
disposer.add(
HuntOptimizer(
scope,
AssetLoader(basePath = "", httpClient),
uiStore
)
)
val huntOptimizer = disposer.add(HuntOptimizer(components.assetLoader, uiStore))
disposer.add(huntOptimizer.initialize(scope))
}
}

View File

@ -1,33 +1,15 @@
package world.phantasmal.web.questEditor
import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import kotlinx.coroutines.cancel
import world.phantasmal.core.disposable.disposable
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
class QuestEditorTests : TestSuite() {
class QuestEditorTests : WebTestSuite() {
@Test
fun initialization_and_shutdown_should_succeed_without_throwing() = test {
val httpClient = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
})
}
}
disposer.add(disposable { httpClient.cancel() })
disposer.add(
QuestEditor(
scope,
AssetLoader(basePath = "", httpClient),
createEngine = { Engine(it) }
)
val questEditor = disposer.add(
QuestEditor(components.assetLoader, createEngine = { Engine(it) })
)
disposer.add(questEditor.initialize(scope))
}
}

View File

@ -0,0 +1,42 @@
package world.phantasmal.web.questEditor.controllers
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.lib.fileFormats.quest.NpcType
import world.phantasmal.web.test.WebTestSuite
import world.phantasmal.web.test.createQuestModel
import world.phantasmal.web.test.createQuestNpcModel
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class NpcCountsControllerTests : WebTestSuite() {
@Test
fun exposes_correct_model_before_and_after_a_quest_is_loaded() = test {
val store = components.questEditorStore
val ctrl = disposer.add(NpcCountsController(store))
assertTrue(ctrl.unavailable.value)
store.setCurrentQuest(createQuestModel(
episode = Episode.I,
npcs = listOf(
createQuestNpcModel(NpcType.Scientist, Episode.I),
createQuestNpcModel(NpcType.Nurse, Episode.I),
createQuestNpcModel(NpcType.Nurse, Episode.I),
createQuestNpcModel(NpcType.Principal, Episode.I),
createQuestNpcModel(NpcType.Nurse, Episode.I),
createQuestNpcModel(NpcType.Scientist, Episode.I),
)
))
assertFalse(ctrl.unavailable.value)
assertEquals(3, ctrl.npcCounts.value.size)
assertEquals("Principal", ctrl.npcCounts.value[0].name)
assertEquals("1", ctrl.npcCounts.value[0].count)
assertEquals("Scientist", ctrl.npcCounts.value[1].name)
assertEquals("2", ctrl.npcCounts.value[1].count)
assertEquals("Nurse", ctrl.npcCounts.value[2].name)
assertEquals("3", ctrl.npcCounts.value[2].count)
}
}

View File

@ -0,0 +1,35 @@
package world.phantasmal.web.questEditor.controllers
import org.w3c.files.File
import world.phantasmal.core.Failure
import world.phantasmal.core.Severity
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class QuestEditorToolbarControllerTests : WebTestSuite() {
@Test
fun a_failure_is_exposed_when_openFiles_fails() = asyncTest {
val ctrl = disposer.add(QuestEditorToolbarController(
components.questLoader,
components.areaStore,
components.questEditorStore
))
assertNull(ctrl.result.value)
ctrl.openFiles(listOf(File(arrayOf(), "unknown.extension")))
val result = ctrl.result.value
assertTrue(result is Failure)
assertEquals(1, result.problems.size)
assertEquals(Severity.Error, result.problems.first().severity)
assertEquals(
"Please select a .qst file or one .bin and one .dat file.",
result.problems.first().uiMessage
)
}
}

View File

@ -0,0 +1,36 @@
package world.phantasmal.web.questEditor.controllers
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.web.test.WebTestSuite
import world.phantasmal.web.test.createQuestModel
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class QuestInfoControllerTests : WebTestSuite() {
@Test
fun exposes_correct_model_before_and_after_a_quest_is_loaded() = test {
val store = components.questEditorStore
val ctrl = disposer.add(QuestInfoController(store))
assertTrue(ctrl.unavailable.value)
assertTrue(ctrl.disabled.value)
store.setCurrentQuest(createQuestModel(
id = 25,
name = "A Quest",
shortDescription = "A short description.",
longDescription = "A long description.",
episode = Episode.II
))
assertFalse(ctrl.unavailable.value)
assertFalse(ctrl.disabled.value)
assertEquals("II", ctrl.episode.value)
assertEquals(25, ctrl.id.value)
assertEquals("A Quest", ctrl.name.value)
assertEquals("A short description.", ctrl.shortDescription.value)
assertEquals("A long description.", ctrl.longDescription.value)
}
}

View File

@ -0,0 +1,100 @@
package world.phantasmal.web.test
import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import kotlinx.coroutines.cancel
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
import world.phantasmal.testUtils.TestContext
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.externals.babylon.Scene
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.QuestLoader
import world.phantasmal.web.questEditor.stores.AreaStore
import world.phantasmal.web.questEditor.stores.QuestEditorStore
import kotlin.reflect.KProperty
/**
* Assigning a disposable to any of the properties in this class will add the assigned value to
* [ctx]'s disposer.
*/
class TestComponents(private val ctx: TestContext) {
var httpClient: HttpClient by default {
HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
})
}
}.also {
ctx.disposer.add(disposable { it.cancel() })
}
}
// Babylon.js
var scene: Scene by default { Scene(Engine(null)) }
// Asset Loaders
var assetLoader: AssetLoader by default { AssetLoader(basePath = "", httpClient) }
var areaAssetLoader: AreaAssetLoader by default {
AreaAssetLoader(ctx.scope, assetLoader, scene)
}
var questLoader: QuestLoader by default { QuestLoader(ctx.scope, assetLoader) }
// Stores
var areaStore: AreaStore by default { AreaStore(ctx.scope, areaAssetLoader) }
var questEditorStore: QuestEditorStore by default {
QuestEditorStore(ctx.scope, areaStore)
}
private fun <T> default(defaultValue: () -> T) = LazyDefault {
val value = defaultValue()
if (value is Disposable) {
ctx.disposer.add(value)
}
value
}
private inner class LazyDefault<T>(private val defaultValue: () -> T) {
private var initialized = false
private var value: T? = null
operator fun getValue(thisRef: Any?, prop: KProperty<*>): T {
if (!initialized) {
val value = defaultValue()
if (value is Disposable) {
ctx.disposer.add(value)
}
this.value = value
initialized = true
}
return value.unsafeCast<T>()
}
operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: T) {
require(initialized) {
"Property ${prop.name} is already initialized."
}
if (value is Disposable) {
ctx.disposer.add(value)
}
this.value = value
initialized = true
}
}
}

View File

@ -0,0 +1,32 @@
package world.phantasmal.web.test
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.lib.fileFormats.quest.NpcType
import world.phantasmal.lib.fileFormats.quest.QuestNpc
import world.phantasmal.web.questEditor.models.QuestModel
import world.phantasmal.web.questEditor.models.QuestNpcModel
import world.phantasmal.web.questEditor.models.QuestObjectModel
fun createQuestModel(
id: Int = 1,
name: String = "Test",
shortDescription: String = name,
longDescription: String = name,
episode: Episode = Episode.I,
npcs: List<QuestNpcModel> = emptyList(),
objects: List<QuestObjectModel> = emptyList(),
): QuestModel =
QuestModel(
id,
language = 1,
name,
shortDescription,
longDescription,
episode,
emptyMap(),
npcs.toMutableList(),
objects.toMutableList(),
) { _, _, _ -> null }
fun createQuestNpcModel(type: NpcType, episode: Episode): QuestNpcModel =
QuestNpcModel(QuestNpc(type, episode, areaId = 0, wave = 0), wave = null)

View File

@ -0,0 +1,9 @@
package world.phantasmal.web.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.TestContext
open class WebTestContext(disposer: Disposer) : TestContext(disposer) {
@Suppress("LeakingThis")
val components = TestComponents(this)
}

View File

@ -0,0 +1,8 @@
package world.phantasmal.web.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.AbstractTestSuite
abstract class WebTestSuite : AbstractTestSuite<WebTestContext>() {
override fun createContext(disposer: Disposer) = WebTestContext(disposer)
}

View File

@ -0,0 +1,15 @@
package world.phantasmal.web.viewer
import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
class ViewerTests : WebTestSuite() {
@Test
fun initialization_and_shutdown_should_succeed_without_throwing() = test {
val viewer = disposer.add(
Viewer(createEngine = { Engine(it) })
)
disposer.add(viewer.initialize(scope))
}
}

View File

@ -0,0 +1,9 @@
package world.phantasmal.webui.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.AbstractTestSuite
import world.phantasmal.testUtils.TestContext
abstract class WebuiTestSuite : AbstractTestSuite<TestContext>() {
override fun createContext(disposer: Disposer) = TestContext(disposer)
}

View File

@ -6,14 +6,13 @@ import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.falseVal
import world.phantasmal.observable.value.mutableVal
import world.phantasmal.observable.value.trueVal
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.webui.dom.div
import world.phantasmal.webui.test.WebuiTestSuite
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.fail
class WidgetTests : TestSuite() {
class WidgetTests : WebuiTestSuite() {
@Test
fun ancestorHidden_and_selfOrAncestorHidden_should_update_when_hidden_changes() = test {
val parentHidden = mutableVal(false)