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/ 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( 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]}" } }