test: Python 대조용 덤프 시험 3건의 입·출력을 환경 변수로 받고, 없으면 건너뛴다

Cm3PerTraceDump·GoldenDump·PrecisionDump 는 실측 세션 JSON 을 파이프라인에 흘려
중간값을 떨구는 개발 도구인데, 입력이 이 PC 의 절대 경로, 출력이 옛 Claude 세션의
임시 폴더로 박혀 있었다. 그 폴더가 사라진 뒤로 전체 단위시험이 늘 3건 실패였고,
다른 PC·CI 에서는 입력부터 없다.

DumpEnv: VESISCAN_DATA123_DIR(입력 폴더) · VESISCAN_DUMP_DIR(출력 폴더). 둘 다 없으면
Assume 로 건너뛴다 — 보고서에 skipped 로 남아 "통과"로 위장하지 않는다.

확인: 기본 실행 168건 중 skipped 3 · failures 0. 환경 변수를 주고 돌리면 3건 모두
실행되어 cm3/golden/precision_kotlin.json 을 쓴다.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 09:38:47 +09:00
parent 4fc40219f2
commit fef3eb28dd
4 changed files with 75 additions and 11 deletions
@@ -11,11 +11,12 @@ import kotlin.math.sqrt
/** /**
* cm=3 세션의 11 trace 각각 BV + 중간값을 dump 해서 Python (cm3_python.json) 과 대조. * cm=3 세션의 11 trace 각각 BV + 중간값을 dump 해서 Python (cm3_python.json) 과 대조.
* divergent trace 특정용. * divergent trace 특정용.
*
* 입·출력 위치는 [DumpEnv] — 환경 변수가 없으면 건너뛴다(개발 도구이지 회귀 시험이 아니다).
*/ */
class Cm3PerTraceDumpTest { class Cm3PerTraceDumpTest {
private val session = "c:/Projects/vesiscan-design-archive/data123/dwjang_HUMAN-kai_VBT26040302_SUPINE_ALIGN_3CM_2026-07-06_164842.json" private val sessionName = "dwjang_HUMAN-kai_VBT26040302_SUPINE_ALIGN_3CM_2026-07-06_164842.json"
private val outPath = "C:/Users/장동우/AppData/Local/Temp/claude/c--Projects-medilightv2android/76311e3b-2bb5-4c52-ab8a-8d89cb1052c2/scratchpad/cm3_kotlin.json"
private fun loadCycles(path: String, maxN: Int = 20): List<List<DoubleArray>> { private fun loadCycles(path: String, maxN: Int = 20): List<List<DoubleArray>> {
val root = JsonParser.parseReader(File(path).bufferedReader()).asJsonObject val root = JsonParser.parseReader(File(path).bufferedReader()).asJsonObject
@@ -54,6 +55,8 @@ class Cm3PerTraceDumpTest {
@Test @Test
fun `dump cm=3 per-trace BV for python comparison`() { fun `dump cm=3 per-trace BV for python comparison`() {
val session = DumpEnv.dataFileOrSkip(sessionName).path
val outPath = DumpEnv.outFileOrSkip("cm3_kotlin.json").path
PiezoHW.activePreset = PiezoHW.DevicePreset.V1 PiezoHW.activePreset = PiezoHW.DevicePreset.V1
val cycles = loadCycles(session, 20) val cycles = loadCycles(session, 20)
val params = MethodDParams.DEFAULT val params = MethodDParams.DEFAULT
@@ -0,0 +1,53 @@
/*
* DumpEnv — Python 대조용 덤프 시험 3개(Cm3PerTraceDump · GoldenDump · PrecisionDump)의
* 입·출력 위치.
*
* 이 셋은 "시험"이라기보다 **개발 도구**다: 실측 세션 JSON 을 파이프라인에 흘려 중간값을
* JSON 으로 떨구고 Python 결과와 diff 한다. 2026-09-29 까지는 입력이 이 PC 의 절대 경로,
* 출력이 옛 Claude 세션의 임시 폴더로 박혀 있어, 그 폴더가 사라진 뒤로 전체 단위시험이
* 늘 3건 실패였다 — 다른 PC·CI 에서는 입력부터 없다.
*
* 그래서 둘 다 환경 변수로 받고, 없으면 **건너뛴다**(JUnit Assume → skipped). 일부러 돌릴 때만:
*
* VESISCAN_DATA123_DIR=c:/Projects/vesiscan-design-archive/data123 \
* VESISCAN_DUMP_DIR=<출력 폴더> \
* ./gradlew :app:testDemoDebugUnitTest --tests '*DumpTest*'
*
* 건너뛴 것은 gradle 보고서에 skipped 로 남는다 — "통과"로 위장하지 않는다.
*/
package com.medithings.vesiscan.managers
import org.junit.Assume.assumeTrue
import java.io.File
object DumpEnv {
const val DATA_ENV = "VESISCAN_DATA123_DIR"
const val OUT_ENV = "VESISCAN_DUMP_DIR"
/** 실측 세션 JSON 폴더. 환경 변수가 없거나 폴더가 없으면 건너뛴다. */
fun dataDirOrSkip(): File {
val v = System.getenv(DATA_ENV)
assumeTrue("$DATA_ENV 가 없어 덤프 시험을 건너뛴다", !v.isNullOrBlank())
val dir = File(v!!)
assumeTrue("$DATA_ENV=$v 폴더가 없어 덤프 시험을 건너뛴다", dir.isDirectory)
return dir
}
/** [dataDirOrSkip] 안의 세션 파일. 그 파일이 없어도 건너뛴다. */
fun dataFileOrSkip(name: String): File {
val f = File(dataDirOrSkip(), name)
assumeTrue("세션 파일이 없어 덤프 시험을 건너뛴다: ${f.path}", f.isFile)
return f
}
/** 덤프 출력 파일. 환경 변수가 없으면 건너뛴다. 폴더는 만들어 둔다. */
fun outFileOrSkip(name: String): File {
val v = System.getenv(OUT_ENV)
assumeTrue("$OUT_ENV 가 없어 덤프 시험을 건너뛴다", !v.isNullOrBlank())
val dir = File(v!!)
dir.mkdirs()
assumeTrue("$OUT_ENV=$v 폴더를 만들 수 없어 덤프 시험을 건너뛴다", dir.isDirectory)
return File(dir, name)
}
}
@@ -13,17 +13,20 @@ import java.io.File
* *
* 실행: * 실행:
* 1. Python golden 생성: python scratchpad/golden_dump_python.py * 1. Python golden 생성: python scratchpad/golden_dump_python.py
* 2. Kotlin dump: ./gradlew testDevDebugUnitTest --tests "*GoldenDumpTest*" * 2. Kotlin dump: VESISCAN_DATA123_DIR=… VESISCAN_DUMP_DIR=… \
* ./gradlew :app:testDemoDebugUnitTest --tests "*GoldenDumpTest*"
* 3. diff : python scratchpad/golden_diff.py * 3. diff : python scratchpad/golden_diff.py
*
* 입·출력 위치는 [DumpEnv] — 환경 변수가 없으면 건너뛴다(개발 도구이지 회귀 시험이 아니다).
*/ */
class GoldenDumpTest { class GoldenDumpTest {
/** [DumpEnv.dataDirOrSkip] 안의 파일명. */
private val sessions = mapOf( private val sessions = mapOf(
"cm0" to "c:/Projects/vesiscan-design-archive/data123/dwjang_HUMAN-kai_VBT26050202_SUPINE_ALIGN_0CM_2026-07-06_164150.json", "cm0" to "dwjang_HUMAN-kai_VBT26050202_SUPINE_ALIGN_0CM_2026-07-06_164150.json",
"cm1" to "c:/Projects/vesiscan-design-archive/data123/dwjang_HUMAN-kai_VBT26050202_SUPINE_ALIGN_1CM_2026-07-06_164231.json", "cm1" to "dwjang_HUMAN-kai_VBT26050202_SUPINE_ALIGN_1CM_2026-07-06_164231.json",
"cm3" to "c:/Projects/vesiscan-design-archive/data123/dwjang_HUMAN-kai_VBT26040302_SUPINE_ALIGN_3CM_2026-07-06_164842.json", "cm3" to "dwjang_HUMAN-kai_VBT26040302_SUPINE_ALIGN_3CM_2026-07-06_164842.json",
) )
private val outPath = "C:/Users/장동우/AppData/Local/Temp/claude/c--Projects-medilightv2android/76311e3b-2bb5-4c52-ab8a-8d89cb1052c2/scratchpad/golden_kotlin.json"
private fun loadCycles(path: String, maxN: Int = 20): List<List<DoubleArray>> { private fun loadCycles(path: String, maxN: Int = 20): List<List<DoubleArray>> {
val root = JsonParser.parseReader(File(path).bufferedReader()).asJsonObject val root = JsonParser.parseReader(File(path).bufferedReader()).asJsonObject
@@ -115,11 +118,13 @@ class GoldenDumpTest {
@Test @Test
fun `golden dump all sessions`() { fun `golden dump all sessions`() {
val dataDir = DumpEnv.dataDirOrSkip()
val outPath = DumpEnv.outFileOrSkip("golden_kotlin.json").path
PiezoHW.activePreset = PiezoHW.DevicePreset.V1 PiezoHW.activePreset = PiezoHW.DevicePreset.V1
val cases = mutableListOf<Map<String, Any?>>() val cases = mutableListOf<Map<String, Any?>>()
val win = 10 val win = 10
for ((sid, path) in sessions) { for ((sid, name) in sessions) {
val cycles = loadCycles(path, 20) val cycles = loadCycles(File(dataDir, name).path, 20)
if (cycles.size < win) { if (cycles.size < win) {
println(" skip $sid: only ${cycles.size} cycles") println(" skip $sid: only ${cycles.size} cycles")
continue continue
@@ -15,11 +15,12 @@ import java.io.File
* *
* cm=1 세션 첫 10 cycle 평균 (trace 0) 을 pipeline 각 단계에 흘리면서 결과를 JSON 으로 저장. * cm=1 세션 첫 10 cycle 평균 (trace 0) 을 pipeline 각 단계에 흘리면서 결과를 JSON 으로 저장.
* scratchpad/precision_python.json 과 비교해 divergence 지점 특정. * scratchpad/precision_python.json 과 비교해 divergence 지점 특정.
*
* 입·출력 위치는 [DumpEnv] — 환경 변수가 없으면 건너뛴다(개발 도구이지 회귀 시험이 아니다).
*/ */
class PrecisionDumpTest { class PrecisionDumpTest {
private val session = "c:/Projects/vesiscan-design-archive/data123/dwjang_HUMAN-kai_VBT26050202_SUPINE_ALIGN_1CM_2026-07-06_164231.json" private val sessionName = "dwjang_HUMAN-kai_VBT26050202_SUPINE_ALIGN_1CM_2026-07-06_164231.json"
private val outPath = "C:/Users/장동우/AppData/Local/Temp/claude/c--Projects-medilightv2android/76311e3b-2bb5-4c52-ab8a-8d89cb1052c2/scratchpad/precision_kotlin.json"
private fun loadFirstTrace(path: String, win: Int = 10): List<DoubleArray> { private fun loadFirstTrace(path: String, win: Int = 10): List<DoubleArray> {
val root = JsonParser.parseReader(File(path).bufferedReader()).asJsonObject val root = JsonParser.parseReader(File(path).bufferedReader()).asJsonObject
@@ -55,6 +56,8 @@ class PrecisionDumpTest {
@Test @Test
fun `dump pipeline intermediates for cm=1 trace 0`() { fun `dump pipeline intermediates for cm=1 trace 0`() {
val session = DumpEnv.dataFileOrSkip(sessionName).path
val outPath = DumpEnv.outFileOrSkip("precision_kotlin.json").path
PiezoHW.activePreset = PiezoHW.DevicePreset.V1 PiezoHW.activePreset = PiezoHW.DevicePreset.V1
EllipseFitSpecific.debugForceEigenvector = null EllipseFitSpecific.debugForceEigenvector = null
val trace = loadFirstTrace(session, 10) val trace = loadFirstTrace(session, 10)