From 22b5402926d18243d9f67e0325e3f493d1237b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B2=94=EC=88=98?= Date: Mon, 22 Jun 2026 03:50:59 +0900 Subject: [PATCH 1/3] =?UTF-8?q?ci:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20Windows?= =?UTF-8?q?=20=EB=A7=A4=ED=8A=B8=EB=A6=AD=EC=8A=A4=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verify 워크플로가 buildPlugin/verifyPlugin만 돌리고 `./gradlew test`를 실행하지 않아, 인코딩 회귀 테스트(이슈 #2/#4)가 CI에서 한 번도 검증되지 않던 문제를 해결한다. - Run Tests 스텝 추가로 회귀 테스트를 매 PR마다 실행 - ubuntu/windows 매트릭스로 비-UTF-8 OS 동작까지 자동 검증 - 리포트 아티팩트 이름을 OS별로 분리 --- .github/workflows/verify.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 57063ad..b86bace 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -8,7 +8,11 @@ on: jobs: verify: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -24,12 +28,24 @@ jobs: - name: Build Plugin run: ./gradlew buildPlugin + # 인코딩 회귀 테스트(이슈 #2/#4 포함) 실행. + # Windows 매트릭스에서 비-UTF-8 OS 동작까지 검증한다. + - name: Run Tests + run: ./gradlew test + - name: Verify Plugin (deprecated API + compatibility) run: ./gradlew verifyPlugin + - name: Upload Test Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-report-${{ matrix.os }} + path: build/reports/tests/ + - name: Upload Verification Report if: always() uses: actions/upload-artifact@v4 with: - name: plugin-verification-report + name: plugin-verification-report-${{ matrix.os }} path: build/reports/pluginVerifier/ From 39f87ba3798dcaa70d99d973503858ea9e2c625f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B2=94=EC=88=98?= Date: Mon, 22 Jun 2026 11:31:50 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20CI=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=93=9C=EB=9F=AC=EB=82=9C=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=202=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test 스텝을 CI에 추가하면서 그동안 실행되지 않던 테스트가 검증되어 발견됨. - levelToString: 범위 밖 레벨(0/음수/31+)에서 rankIdx 음수로 인한 ArrayIndexOutOfBounds 크래시 방지 (1~30만 유효, 그 외 Unrated) - sanitizeFolderName: 파일명에 안전한 . 와 + 를 보존 (예: 백준 1000 'A+B') --- .../kotlin/com/codingtestkit/service/ProblemFileManager.kt | 3 ++- src/main/kotlin/com/codingtestkit/service/SolvedAcApi.kt | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/codingtestkit/service/ProblemFileManager.kt b/src/main/kotlin/com/codingtestkit/service/ProblemFileManager.kt index 3031edf..ddc36ee 100644 --- a/src/main/kotlin/com/codingtestkit/service/ProblemFileManager.kt +++ b/src/main/kotlin/com/codingtestkit/service/ProblemFileManager.kt @@ -196,7 +196,8 @@ object ProblemFileManager { } private fun sanitizeFolderName(name: String): String { - return name.replace(Regex("[^a-zA-Z0-9가-힣_\\-() ]"), "_") + // 파일/폴더명에 안전한 . 와 + 도 보존한다 (예: 백준 1000 "A+B"). + return name.replace(Regex("[^a-zA-Z0-9가-힣_.+\\-() ]"), "_") .replace(Regex("_+"), "_") .trim() .take(60) diff --git a/src/main/kotlin/com/codingtestkit/service/SolvedAcApi.kt b/src/main/kotlin/com/codingtestkit/service/SolvedAcApi.kt index a9cc72b..7782e5e 100644 --- a/src/main/kotlin/com/codingtestkit/service/SolvedAcApi.kt +++ b/src/main/kotlin/com/codingtestkit/service/SolvedAcApi.kt @@ -261,11 +261,13 @@ object SolvedAcApi { private fun toSubscript(s: String): String = s.map { subscriptMap[it] ?: it }.joinToString("") fun levelToString(level: Int): String { - if (level == 0) return "Unrated" + // 유효 레벨은 1~30 (Bronze V ~ Ruby I). 그 밖(0, 음수, 31+)은 모두 Unrated. + // 범위 밖 입력에서 rankIdx가 음수가 되어 ArrayIndexOutOfBounds가 나던 문제 방지. + if (level < 1 || level > 30) return "Unrated" val tiers = arrayOf("Bronze", "Silver", "Gold", "Platinum", "Diamond", "Ruby") val ranks = arrayOf("V", "IV", "III", "II", "I") val tierIdx = (level - 1) / 5 val rankIdx = (level - 1) % 5 - return if (tierIdx in tiers.indices) "${tiers[tierIdx]} ${ranks[rankIdx]}" else "Unrated" + return "${tiers[tierIdx]} ${ranks[rankIdx]}" } } From 56a6115cb275578e542d7729ed15f58105f9649a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B2=94=EC=88=98?= Date: Mon, 22 Jun 2026 12:01:23 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20Windows=20=EB=8F=84=EA=B5=AC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=ED=83=90=EC=A7=80=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows에서 실행 파일 탐지가 실패해 Java/Python 실행이 전부 깨지던 문제 해결. - findExecutable: Windows는 which 대신 where 사용 (Git Bash의 which는 ProcessBuilder가 실행 못 하는 MSYS 경로 /c/... 를 반환) - 절대경로 후보에 .exe 확장자 보정, 반환 경로의 실재 여부 검증 - detectPython: Windows는 python3가 아닌 python 우선 CI Windows 매트릭스에서 드러난 CreateProcess error=2 (javac/python3 경로) 및 그 파생인 timeout 테스트 실패를 함께 해결한다. --- .../com/codingtestkit/service/CodeRunner.kt | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt b/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt index ad00448..5663e5b 100644 --- a/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt +++ b/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt @@ -494,8 +494,13 @@ else console.log(_result); } private fun detectPython(): String { - return findExecutable("python3", "/usr/bin/python3", "/usr/local/bin/python3", - "/opt/homebrew/bin/python3", "python") + // Windows는 python3가 아니라 python 명령이 일반적 + return if (isWindows) { + findExecutable("python", "python3") + } else { + findExecutable("python3", "/usr/bin/python3", "/usr/local/bin/python3", + "/opt/homebrew/bin/python3", "python") + } } private fun detectKotlinc(): String { @@ -560,19 +565,36 @@ else console.log(_result); return findExecutable("node", "/usr/local/bin/node", "/opt/homebrew/bin/node") } + private val isWindows: Boolean = System.getProperty("os.name").lowercase().contains("win") + private fun findExecutable(vararg candidates: String): String { for (candidate in candidates) { - // 절대 경로면 파일 존재 확인 - if (candidate.startsWith("/") && File(candidate).exists()) return candidate + // 절대 경로 후보 (Unix: /usr/..., Windows: C:\...) + if (candidate.startsWith("/") || candidate.matches(Regex("^[A-Za-z]:[\\\\/].*"))) { + if (File(candidate).exists()) return candidate + // Windows 실행파일은 .exe 확장자 보정 + if (isWindows && !candidate.endsWith(".exe")) { + val withExe = File("$candidate.exe") + if (withExe.exists()) return withExe.absolutePath + } + continue + } - // PATH에서 찾기 + // PATH에서 찾기: Windows는 where, 그 외는 which. + // (Git Bash의 which는 ProcessBuilder가 실행 못 하는 MSYS 경로 /c/... 를 반환하므로 where 사용) try { - val proc = ProcessBuilder("which", candidate).start() - val result = proc.inputStream.bufferedReader().readText().trim() - if (proc.waitFor() == 0 && result.isNotBlank()) return result + val locator = if (isWindows) "where" else "which" + val proc = ProcessBuilder(locator, candidate).start() + val output = proc.inputStream.bufferedReader().readText() + val ok = proc.waitFor() == 0 + // where는 여러 줄을 반환할 수 있으므로 실제로 존재하는 첫 경로를 선택 + val resolved = output.lineSequence() + .map { it.trim() } + .firstOrNull { it.isNotBlank() && File(it).exists() } + if (ok && resolved != null) return resolved } catch (_: Exception) {} } - return candidates.first() // fallback: 원래 이름 그대로 + return candidates.first() // fallback: 원래 이름 그대로 (PATH 탐색에 맡김) } fun getDetectedPaths(): Map = mapOf(