arblant
railway-clean-ip-scanner-android / tools / TestWorkbench.kt
TestWorkbench.kt75 lines · 3.9 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
// WorkbenchLogic semantics tests (final spec 2026-10-27).
// Pure-JVM: compile Workbench.kt + ScanResult.kt + this file with
// kotlinc against kotlin-stdlib only, then run TestMainKt.
// Verified 2026-10-27: all 21 checks pass.

import com.arblant.railwayipscanner.ScanResult
import com.arblant.railwayipscanner.WorkbenchLogic as W

var failures = 0
fun expect(name: String, cond: Boolean) {
    println((if (cond) "PASS " else "FAIL ") + name)
    if (!cond) failures++
}

fun main() {
    // Results deliberately unsorted; resolve() must rank by latency.
    val results = listOf(
        ScanResult("10.0.0.3", true, 90),
        ScanResult("10.0.0.1", true, 30),
        ScanResult("10.0.0.2", true, 60),
        ScanResult("10.0.0.9", false, 0) // failures are ignored
    )
    val sortedIps = listOf("10.0.0.1", "10.0.0.2", "10.0.0.3")

    // Top N: distinct IPs ascending latency.
    var rows = W.resolve(W.Mode.TOP_N, 3, results, emptyMap())
    expect("topN distinct ascending", rows.map { it.ip } == sortedIps)
    expect("topN latencies attached", rows.map { it.latencyMs } == listOf(30L, 60L, 90L))
    expect("topN no stale/changed", rows.all { !it.stale && !it.changed })
    expect("topN no shortage at count==unique", !W.hasShortage(W.Mode.TOP_N, 3, 3))

    // Top N wrap: count > unique wraps from the top + shortage flagged.
    rows = W.resolve(W.Mode.TOP_N, 5, results, emptyMap())
    expect("topN wraps", rows.map { it.ip } == listOf("10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.1", "10.0.0.2"))
    expect("shortage flagged", W.hasShortage(W.Mode.TOP_N, 5, 3))
    expect("uniqueIps dedups wrapped rows", W.uniqueIps(rows) == sortedIps)

    // Best for all: every row the fastest IP, one unique IP to test.
    rows = W.resolve(W.Mode.BEST_ALL, 4, results, emptyMap())
    expect("bestAll all fastest", rows.all { it.ip == "10.0.0.1" })
    expect("bestAll unique = 1", W.uniqueIps(rows) == listOf("10.0.0.1"))
    expect("bestAll no shortage concept", !W.hasShortage(W.Mode.BEST_ALL, 4, 3))

    // Custom: pick wins over auto; differs-from-auto => changed.
    rows = W.resolve(W.Mode.CUSTOM, 3, results, mapOf(1 to "10.0.0.3"))
    expect("custom pick applied", rows[1].ip == "10.0.0.3" && rows[1].latencyMs == 90L)
    expect("custom changed dot", rows[1].changed && !rows[0].changed && !rows[2].changed)
    expect("custom others fall back to topN", rows[0].ip == "10.0.0.1" && rows[2].ip == "10.0.0.3")
    // A pick equal to the auto assignment is NOT "changed".
    rows = W.resolve(W.Mode.CUSTOM, 3, results, mapOf(0 to "10.0.0.1"))
    expect("custom pick == auto not changed", !rows[0].changed && !rows[0].stale)

    // Custom after a rescan where the pick vanished: kept + stale.
    val fewer = listOf(ScanResult("10.0.0.1", true, 25), ScanResult("10.0.0.2", true, 55))
    rows = W.resolve(W.Mode.CUSTOM, 3, fewer, mapOf(2 to "10.0.0.3"))
    expect("stale pick kept", rows[2].ip == "10.0.0.3")
    expect("stale flagged + no latency", rows[2].stale && rows[2].latencyMs == null)
    expect("stale pick counts as changed vs new auto", rows[2].changed)

    // Custom picks survive count changes (indexes stable) and mode hops:
    // resolve with a bigger count keeps earlier picks.
    rows = W.resolve(W.Mode.CUSTOM, 5, results, mapOf(1 to "10.0.0.3"))
    expect("pick preserved on count change", rows[1].ip == "10.0.0.3" && rows.size == 5)

    // Empty results => no rows in any mode.
    expect("empty results no rows", W.resolve(W.Mode.TOP_N, 5, emptyList(), emptyMap()).isEmpty())
    expect("empty results no rows custom", W.resolve(W.Mode.CUSTOM, 5, emptyList(), mapOf(0 to "1.2.3.4")).isEmpty())

    // Count clamps are the UI's job; logic respects the given count.
    rows = W.resolve(W.Mode.TOP_N, 1, results, emptyMap())
    expect("count 1", rows.size == 1 && rows[0].ip == "10.0.0.1")

    println(if (failures == 0) "ALL WORKBENCH TESTS PASSED" else "$failures FAILURES")
    if (failures > 0) kotlin.system.exitProcess(1)
}