@rhseung/ps-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runSolution
4
+ } from "../chunk-EIFFWFLS.js";
5
+ import {
6
+ detectProblemIdFromPath,
7
+ getProblemId
8
+ } from "../chunk-OOTPZD7O.js";
9
+ import {
10
+ detectLanguageFromFile,
11
+ getSupportedLanguages,
12
+ getSupportedLanguagesString
13
+ } from "../chunk-TQXMB7XV.js";
14
+ import {
15
+ LoadingSpinner
16
+ } from "../chunk-IJLJBKLK.js";
17
+ import "../chunk-FYS2JH42.js";
18
+
19
+ // src/commands/test.tsx
20
+ import { useEffect, useState } from "react";
21
+ import { render, Box as Box2, Text as Text2 } from "ink";
22
+ import chokidar from "chokidar";
23
+ import { readdir as readdir2 } from "fs/promises";
24
+ import { join as join2 } from "path";
25
+
26
+ // src/services/test-runner.ts
27
+ import { readdir, readFile } from "fs/promises";
28
+ import { join } from "path";
29
+
30
+ // src/services/diff-checker.ts
31
+ function normalizeOutput(output) {
32
+ return output.replace(/\r\n/g, "\n").split("\n").map((line) => line.trimEnd()).join("\n").trimEnd();
33
+ }
34
+ function compareOutput(expected, actual) {
35
+ const expectedNorm = normalizeOutput(expected);
36
+ const actualNorm = normalizeOutput(actual);
37
+ return {
38
+ pass: expectedNorm === actualNorm,
39
+ expected: expectedNorm,
40
+ actual: actualNorm
41
+ };
42
+ }
43
+
44
+ // src/services/test-runner.ts
45
+ function buildSummary(results) {
46
+ const summary = {
47
+ total: results.length,
48
+ passed: 0,
49
+ failed: 0,
50
+ errored: 0
51
+ };
52
+ for (const r of results) {
53
+ if (r.status === "pass") summary.passed += 1;
54
+ else if (r.status === "fail") summary.failed += 1;
55
+ else summary.errored += 1;
56
+ }
57
+ return summary;
58
+ }
59
+ async function runAllTests({
60
+ problemDir,
61
+ language,
62
+ timeoutMs
63
+ }) {
64
+ const entries = await readdir(problemDir);
65
+ const inputFiles = entries.filter((f) => /^input\d+\.txt$/.test(f));
66
+ const results = [];
67
+ let effectiveTimeout = timeoutMs;
68
+ if (effectiveTimeout == null) {
69
+ try {
70
+ const metaRaw = await readFile(join(problemDir, "meta.json"), "utf-8");
71
+ const meta = JSON.parse(metaRaw);
72
+ if (typeof meta.timeLimitMs === "number") {
73
+ effectiveTimeout = meta.timeLimitMs;
74
+ } else if (typeof meta.timeLimit === "string") {
75
+ const match = meta.timeLimit.match(/([\d.]+)/);
76
+ if (match) {
77
+ const seconds = parseFloat(match[1]);
78
+ if (!Number.isNaN(seconds)) {
79
+ effectiveTimeout = Math.round(seconds * 1e3);
80
+ }
81
+ }
82
+ }
83
+ } catch {
84
+ }
85
+ }
86
+ if (effectiveTimeout == null) {
87
+ effectiveTimeout = 5e3;
88
+ }
89
+ for (const inputFile of inputFiles) {
90
+ const match = inputFile.match(/input(\d+)\.txt$/);
91
+ const caseId = match ? Number(match[1]) : results.length + 1;
92
+ const inputPath = join(problemDir, inputFile);
93
+ const outputPath = join(problemDir, `output${caseId}.txt`);
94
+ let expected;
95
+ try {
96
+ expected = await readFile(outputPath, "utf-8");
97
+ } catch {
98
+ results.push({
99
+ caseId,
100
+ inputPath,
101
+ status: "error",
102
+ error: "\uAE30\uB300 \uCD9C\uB825(output*.txt)\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."
103
+ });
104
+ continue;
105
+ }
106
+ const runResult = await runSolution({
107
+ problemDir,
108
+ language,
109
+ inputPath,
110
+ timeoutMs: effectiveTimeout
111
+ });
112
+ if (runResult.exitCode !== 0 || runResult.timedOut) {
113
+ results.push({
114
+ caseId,
115
+ inputPath,
116
+ expected,
117
+ actual: runResult.stdout,
118
+ error: runResult.timedOut ? `\uC2DC\uAC04 \uCD08\uACFC (timeout ${effectiveTimeout}ms)` : runResult.stderr || "\uC2E4\uD589 \uC5D0\uB7EC",
119
+ status: "error",
120
+ durationMs: runResult.durationMs
121
+ });
122
+ continue;
123
+ }
124
+ const diff = compareOutput(expected ?? "", runResult.stdout);
125
+ results.push({
126
+ caseId,
127
+ inputPath,
128
+ expected: diff.expected,
129
+ actual: diff.actual,
130
+ status: diff.pass ? "pass" : "fail",
131
+ durationMs: runResult.durationMs
132
+ });
133
+ }
134
+ return { results, summary: buildSummary(results) };
135
+ }
136
+
137
+ // src/components/test-result.tsx
138
+ import { Box, Text } from "ink";
139
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
140
+ function truncate(text = "", max = 200) {
141
+ return text.length > max ? `${text.slice(0, max)}...` : text;
142
+ }
143
+ function formatDuration(ms) {
144
+ if (ms < 1e3) return `${ms}ms`;
145
+ return `${(ms / 1e3).toFixed(2)}s`;
146
+ }
147
+ function ResultRow({ result }) {
148
+ const statusIcon = result.status === "pass" ? "\u2713" : result.status === "fail" ? "\u2717" : "!";
149
+ const statusColor = result.status === "pass" ? "green" : result.status === "fail" ? "red" : "yellow";
150
+ const statusText = result.status === "pass" ? "PASS" : result.status === "fail" ? "FAIL" : "ERROR";
151
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
152
+ /* @__PURE__ */ jsxs(Box, { children: [
153
+ /* @__PURE__ */ jsx(Text, { color: statusColor, bold: true, children: statusIcon }),
154
+ /* @__PURE__ */ jsx(Text, { children: " " }),
155
+ /* @__PURE__ */ jsx(Text, { color: statusColor, bold: true, children: statusText }),
156
+ /* @__PURE__ */ jsx(Text, { children: " " }),
157
+ /* @__PURE__ */ jsxs(Text, { children: [
158
+ "\uCF00\uC774\uC2A4 ",
159
+ result.caseId
160
+ ] }),
161
+ result.durationMs !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
162
+ /* @__PURE__ */ jsx(Text, { children: " " }),
163
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
164
+ "(",
165
+ formatDuration(result.durationMs),
166
+ ")"
167
+ ] })
168
+ ] })
169
+ ] }),
170
+ result.status === "fail" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 3, marginTop: 1, children: [
171
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
172
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "\uAE30\uB300\uAC12:" }),
173
+ /* @__PURE__ */ jsx(Text, { children: truncate(result.expected ?? "") })
174
+ ] }),
175
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
176
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "\uC2E4\uC81C\uAC12:" }),
177
+ /* @__PURE__ */ jsx(Text, { children: truncate(result.actual ?? "") })
178
+ ] })
179
+ ] }),
180
+ result.status === "error" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 3, marginTop: 1, children: [
181
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: result.error ?? "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958" }),
182
+ result.stderr && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: truncate(result.stderr) })
183
+ ] })
184
+ ] });
185
+ }
186
+ function TestResultView({ summary, results }) {
187
+ const allPassed = summary.failed === 0 && summary.errored === 0;
188
+ const summaryColor = allPassed ? "green" : "red";
189
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
190
+ /* @__PURE__ */ jsxs(
191
+ Box,
192
+ {
193
+ borderStyle: "round",
194
+ borderColor: summaryColor,
195
+ paddingX: 1,
196
+ alignSelf: "flex-start",
197
+ flexDirection: "column",
198
+ children: [
199
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { bold: true, children: "\uD14C\uC2A4\uD2B8 \uACB0\uACFC" }) }),
200
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
201
+ /* @__PURE__ */ jsxs(Text, { children: [
202
+ "\uCD1D ",
203
+ /* @__PURE__ */ jsx(Text, { bold: true, children: summary.total }),
204
+ "\uAC1C"
205
+ ] }),
206
+ /* @__PURE__ */ jsx(Text, { children: " | " }),
207
+ /* @__PURE__ */ jsxs(Text, { color: "green", children: [
208
+ "Pass ",
209
+ /* @__PURE__ */ jsx(Text, { bold: true, children: summary.passed })
210
+ ] }),
211
+ summary.failed > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
212
+ /* @__PURE__ */ jsx(Text, { children: " | " }),
213
+ /* @__PURE__ */ jsxs(Text, { color: "red", children: [
214
+ "Fail ",
215
+ /* @__PURE__ */ jsx(Text, { bold: true, children: summary.failed })
216
+ ] })
217
+ ] }),
218
+ summary.errored > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
219
+ /* @__PURE__ */ jsx(Text, { children: " | " }),
220
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
221
+ "Error ",
222
+ /* @__PURE__ */ jsx(Text, { bold: true, children: summary.errored })
223
+ ] })
224
+ ] })
225
+ ] })
226
+ ]
227
+ }
228
+ ),
229
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: results.map((r) => /* @__PURE__ */ jsx(ResultRow, { result: r }, r.caseId)) })
230
+ ] });
231
+ }
232
+
233
+ // src/commands/test.tsx
234
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
235
+ function TestCommand({
236
+ problemDir,
237
+ language,
238
+ watch,
239
+ timeoutMs,
240
+ onComplete
241
+ }) {
242
+ const [status, setStatus] = useState("loading");
243
+ const [results, setResults] = useState([]);
244
+ const [summary, setSummary] = useState({
245
+ total: 0,
246
+ passed: 0,
247
+ failed: 0,
248
+ errored: 0
249
+ });
250
+ const [error, setError] = useState(null);
251
+ const runTests = (isWatchTrigger = false) => {
252
+ if (isWatchTrigger && watch) {
253
+ console.clear();
254
+ }
255
+ setStatus("loading");
256
+ void runAllTests({
257
+ problemDir,
258
+ language,
259
+ timeoutMs
260
+ }).then(({ results: results2, summary: summary2 }) => {
261
+ setResults(results2);
262
+ setSummary(summary2);
263
+ setStatus("ready");
264
+ }).catch((err) => {
265
+ setError(err instanceof Error ? err.message : String(err));
266
+ setStatus("error");
267
+ });
268
+ };
269
+ useEffect(() => {
270
+ void runTests();
271
+ if (watch) {
272
+ const watcher = chokidar.watch(
273
+ [
274
+ join2(problemDir, "solution.*"),
275
+ join2(problemDir, "input*.txt"),
276
+ join2(problemDir, "output*.txt")
277
+ ],
278
+ {
279
+ ignoreInitial: true
280
+ }
281
+ );
282
+ watcher.on("change", () => {
283
+ runTests(true);
284
+ });
285
+ return () => {
286
+ void watcher.close();
287
+ };
288
+ }
289
+ return void 0;
290
+ }, [problemDir, language, watch]);
291
+ useEffect(() => {
292
+ if (!watch && status === "ready") {
293
+ const timer = setTimeout(() => onComplete(), 200);
294
+ return () => clearTimeout(timer);
295
+ }
296
+ return void 0;
297
+ }, [status, watch, onComplete]);
298
+ if (status === "loading") {
299
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
300
+ /* @__PURE__ */ jsx2(Text2, { children: "\uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uC911..." }),
301
+ /* @__PURE__ */ jsx2(LoadingSpinner, {})
302
+ ] });
303
+ }
304
+ if (status === "error") {
305
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
306
+ /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u2717 \uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uC2E4\uD328" }),
307
+ error && /* @__PURE__ */ jsx2(Text2, { color: "gray", children: error })
308
+ ] });
309
+ }
310
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
311
+ /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
312
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "\uD14C\uC2A4\uD2B8 \uC2E4\uD589 \uC911" }),
313
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
314
+ /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
315
+ problemDir,
316
+ " \u2022 ",
317
+ language,
318
+ watch && " \u2022 watch"
319
+ ] })
320
+ ] }),
321
+ /* @__PURE__ */ jsx2(TestResultView, { results, summary })
322
+ ] });
323
+ }
324
+ async function detectLanguage(problemDir) {
325
+ const files = await readdir2(problemDir);
326
+ const solutionFile = files.find((f) => f.startsWith("solution."));
327
+ if (!solutionFile) {
328
+ throw new Error("solution.* \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
329
+ }
330
+ const lang = detectLanguageFromFile(solutionFile);
331
+ if (!lang) {
332
+ throw new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC5B8\uC5B4\uC785\uB2C8\uB2E4: ${solutionFile}`);
333
+ }
334
+ return lang;
335
+ }
336
+ async function testCommand(options = {}) {
337
+ const currentPathProblemId = detectProblemIdFromPath(process.cwd());
338
+ const problemDir = options.id && currentPathProblemId !== options.id ? join2(process.cwd(), "problems", String(options.id)) : process.cwd();
339
+ const language = options.language ?? await detectLanguage(problemDir);
340
+ return new Promise((resolve) => {
341
+ const { unmount } = render(
342
+ /* @__PURE__ */ jsx2(
343
+ TestCommand,
344
+ {
345
+ problemDir,
346
+ language,
347
+ watch: Boolean(options.watch),
348
+ timeoutMs: options.timeoutMs,
349
+ onComplete: () => {
350
+ unmount();
351
+ resolve();
352
+ }
353
+ }
354
+ )
355
+ );
356
+ });
357
+ }
358
+ var testHelp = `
359
+ \uC0AC\uC6A9\uBC95:
360
+ $ ps test [\uBB38\uC81C\uBC88\uD638] [\uC635\uC158]
361
+
362
+ \uC124\uBA85:
363
+ \uC608\uC81C \uC785\uCD9C\uB825 \uAE30\uBC18\uC73C\uB85C \uB85C\uCEEC \uD14C\uC2A4\uD2B8\uB97C \uC2E4\uD589\uD569\uB2C8\uB2E4.
364
+ - \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC \uB610\uB294 \uC9C0\uC815\uD55C \uBB38\uC81C \uBC88\uD638\uC758 \uD14C\uC2A4\uD2B8 \uC2E4\uD589
365
+ - solution.* \uD30C\uC77C\uC744 \uC790\uB3D9\uC73C\uB85C \uCC3E\uC544 \uC5B8\uC5B4 \uAC10\uC9C0
366
+ - input*.txt\uC640 output*.txt \uD30C\uC77C\uC744 \uAE30\uBC18\uC73C\uB85C \uD14C\uC2A4\uD2B8
367
+ - \uBB38\uC81C\uC758 \uC2DC\uAC04 \uC81C\uD55C\uC744 \uC790\uB3D9\uC73C\uB85C \uC801\uC6A9
368
+ - --watch \uC635\uC158\uC73C\uB85C \uD30C\uC77C \uBCC0\uACBD \uC2DC \uC790\uB3D9 \uC7AC\uD14C\uC2A4\uD2B8
369
+
370
+ \uC635\uC158:
371
+ --language, -l \uC5B8\uC5B4 \uC120\uD0DD (\uC9C0\uC815 \uC2DC \uC790\uB3D9 \uAC10\uC9C0 \uBB34\uC2DC)
372
+ \uC9C0\uC6D0 \uC5B8\uC5B4: ${getSupportedLanguagesString()}
373
+ --watch, -w watch \uBAA8\uB4DC (\uD30C\uC77C \uBCC0\uACBD \uC2DC \uC790\uB3D9 \uC7AC\uD14C\uC2A4\uD2B8)
374
+ solution.*, input*.txt, output*.txt \uD30C\uC77C \uBCC0\uACBD \uAC10\uC9C0
375
+
376
+ \uC608\uC81C:
377
+ $ ps test # \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uD14C\uC2A4\uD2B8
378
+ $ ps test 1000 # 1000\uBC88 \uBB38\uC81C \uD14C\uC2A4\uD2B8
379
+ $ ps test --watch # watch \uBAA8\uB4DC\uB85C \uD14C\uC2A4\uD2B8
380
+ $ ps test 1000 --watch # 1000\uBC88 \uBB38\uC81C\uB97C watch \uBAA8\uB4DC\uB85C \uD14C\uC2A4\uD2B8
381
+ `;
382
+ async function testExecute(args, flags) {
383
+ if (flags.help) {
384
+ console.log(testHelp.trim());
385
+ process.exit(0);
386
+ return;
387
+ }
388
+ const problemId = getProblemId(args);
389
+ const validLanguages = getSupportedLanguages();
390
+ const language = flags.language;
391
+ if (flags.language && language && !validLanguages.includes(language)) {
392
+ console.error(
393
+ `\uC624\uB958: \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC5B8\uC5B4\uC785\uB2C8\uB2E4. (${getSupportedLanguagesString()})`
394
+ );
395
+ process.exit(1);
396
+ }
397
+ await testCommand({
398
+ id: problemId ?? void 0,
399
+ language: language ?? void 0,
400
+ watch: Boolean(flags.watch)
401
+ });
402
+ }
403
+ var testCommandDef = {
404
+ name: "test",
405
+ help: testHelp,
406
+ execute: testExecute
407
+ };
408
+ var test_default = testCommandDef;
409
+ export {
410
+ test_default as default,
411
+ testExecute,
412
+ testHelp
413
+ };