atcoder-workspace 1.1.0-beta.1
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.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/THIRD_PARTY_LICENSES +21 -0
- package/dist/atcoder/client.d.ts +2 -0
- package/dist/atcoder/client.js +23 -0
- package/dist/atcoder/new.d.ts +15 -0
- package/dist/atcoder/new.js +123 -0
- package/dist/atcoder/parser/contest-tasks.d.ts +6 -0
- package/dist/atcoder/parser/contest-tasks.js +86 -0
- package/dist/atcoder/parser/limits.d.ts +10 -0
- package/dist/atcoder/parser/limits.js +71 -0
- package/dist/atcoder/parser/problem-page.d.ts +12 -0
- package/dist/atcoder/parser/problem-page.js +136 -0
- package/dist/atcoder/parser/submission-status.d.ts +8 -0
- package/dist/atcoder/parser/submission-status.js +78 -0
- package/dist/atcoder/submit.d.ts +9 -0
- package/dist/atcoder/submit.js +182 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +508 -0
- package/dist/config/config-store.d.ts +17 -0
- package/dist/config/config-store.js +92 -0
- package/dist/session/auth.d.ts +8 -0
- package/dist/session/auth.js +117 -0
- package/dist/session/store.d.ts +15 -0
- package/dist/session/store.js +75 -0
- package/dist/test-runner/diff.d.ts +7 -0
- package/dist/test-runner/diff.js +32 -0
- package/dist/test-runner/runner.d.ts +46 -0
- package/dist/test-runner/runner.js +274 -0
- package/dist/utils/errors.d.ts +15 -0
- package/dist/utils/errors.js +35 -0
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.js +89 -0
- package/dist/utils/i18n.d.ts +345 -0
- package/dist/utils/i18n.js +413 -0
- package/dist/utils/open.d.ts +8 -0
- package/dist/utils/open.js +50 -0
- package/dist/workspace/finder.d.ts +9 -0
- package/dist/workspace/finder.js +62 -0
- package/dist/workspace/initializer.d.ts +4 -0
- package/dist/workspace/initializer.js +109 -0
- package/package.json +38 -0
- package/src/atcoder/client.ts +21 -0
- package/src/atcoder/new.ts +107 -0
- package/src/atcoder/parser/contest-tasks.test.ts +37 -0
- package/src/atcoder/parser/contest-tasks.ts +61 -0
- package/src/atcoder/parser/limits.test.ts +52 -0
- package/src/atcoder/parser/limits.ts +75 -0
- package/src/atcoder/parser/problem-page.test.ts +68 -0
- package/src/atcoder/parser/problem-page.ts +126 -0
- package/src/atcoder/parser/submission-status.test.ts +36 -0
- package/src/atcoder/parser/submission-status.ts +54 -0
- package/src/atcoder/submit.ts +170 -0
- package/src/cli.ts +554 -0
- package/src/config/config-store.ts +72 -0
- package/src/session/auth.ts +87 -0
- package/src/session/store.ts +50 -0
- package/src/test-runner/diff.test.ts +26 -0
- package/src/test-runner/diff.ts +42 -0
- package/src/test-runner/runner.test.ts +70 -0
- package/src/test-runner/runner.ts +315 -0
- package/src/utils/errors.ts +31 -0
- package/src/utils/format.test.ts +69 -0
- package/src/utils/format.ts +95 -0
- package/src/utils/i18n.test.ts +74 -0
- package/src/utils/i18n.ts +418 -0
- package/src/utils/open.ts +47 -0
- package/src/workspace/finder.ts +29 -0
- package/src/workspace/initializer.ts +85 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loginWithCookie = loginWithCookie;
|
|
37
|
+
exports.whoami = whoami;
|
|
38
|
+
const cheerio = __importStar(require("cheerio"));
|
|
39
|
+
const store_1 = require("./store");
|
|
40
|
+
const client_1 = require("../atcoder/client");
|
|
41
|
+
const errors_1 = require("../utils/errors");
|
|
42
|
+
/**
|
|
43
|
+
* Saves a manually entered REVEL_SESSION cookie and verifies the session.
|
|
44
|
+
*/
|
|
45
|
+
async function loginWithCookie(workspaceRoot, revelSession) {
|
|
46
|
+
let cleanSession = revelSession.trim();
|
|
47
|
+
if (!cleanSession) {
|
|
48
|
+
throw new errors_1.AuthError('REVEL_SESSION cookie value cannot be empty.');
|
|
49
|
+
}
|
|
50
|
+
if (cleanSession.startsWith('REVEL_SESSION=')) {
|
|
51
|
+
cleanSession = cleanSession.substring('REVEL_SESSION='.length);
|
|
52
|
+
}
|
|
53
|
+
cleanSession = cleanSession.split(';')[0].trim();
|
|
54
|
+
if (!cleanSession) {
|
|
55
|
+
throw new errors_1.AuthError('Could not extract a valid REVEL_SESSION value.');
|
|
56
|
+
}
|
|
57
|
+
const savedCookies = [
|
|
58
|
+
{
|
|
59
|
+
name: 'REVEL_SESSION',
|
|
60
|
+
value: cleanSession,
|
|
61
|
+
domain: '.atcoder.jp',
|
|
62
|
+
path: '/',
|
|
63
|
+
expires: Math.floor(Date.now() / 1000) + 86400 * 30,
|
|
64
|
+
httpOnly: true,
|
|
65
|
+
secure: true,
|
|
66
|
+
sameSite: 'Lax'
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
(0, store_1.saveSession)(workspaceRoot, savedCookies);
|
|
70
|
+
try {
|
|
71
|
+
const username = await whoami(workspaceRoot);
|
|
72
|
+
return username;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
try {
|
|
76
|
+
const { clearSession } = require('./store');
|
|
77
|
+
clearSession(workspaceRoot);
|
|
78
|
+
}
|
|
79
|
+
catch (e) { }
|
|
80
|
+
throw new errors_1.AuthError(`The provided REVEL_SESSION cookie is invalid or expired: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Checks the login status by requesting the AtCoder settings page with saved session cookies.
|
|
85
|
+
*/
|
|
86
|
+
async function whoami(workspaceRoot) {
|
|
87
|
+
const session = (0, store_1.loadSession)(workspaceRoot);
|
|
88
|
+
if (!session || !session.some(c => c.name === 'REVEL_SESSION')) {
|
|
89
|
+
throw new errors_1.AuthError('No active session. Please log in using "atc login".');
|
|
90
|
+
}
|
|
91
|
+
const client = (0, client_1.createAtCoderClient)(workspaceRoot);
|
|
92
|
+
try {
|
|
93
|
+
const res = await client.get('/settings');
|
|
94
|
+
const $ = cheerio.load(res.data);
|
|
95
|
+
const userLink = $('header a[href^="/users/"], .navbar-right a[href^="/users/"]').first();
|
|
96
|
+
const href = userLink.attr('href');
|
|
97
|
+
if (href) {
|
|
98
|
+
const match = href.match(/\/users\/([a-zA-Z0-9_]+)/);
|
|
99
|
+
if (match && match[1]) {
|
|
100
|
+
return match[1];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const usernameSpan = $('header a.username, header .dropdown-toggle').first();
|
|
104
|
+
if (usernameSpan.length) {
|
|
105
|
+
const name = usernameSpan.text().replace(/\s+/g, ' ').trim();
|
|
106
|
+
if (name && name !== 'Sign In' && name !== 'ログイン' && name !== 'Sign Up' && name !== '新規登録') {
|
|
107
|
+
return name;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw new errors_1.AuthError('Session is invalid or expired.');
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err instanceof errors_1.AuthError)
|
|
114
|
+
throw err;
|
|
115
|
+
throw new errors_1.AuthError(`Failed to verify session: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SavedCookie {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
domain: string;
|
|
5
|
+
path: string;
|
|
6
|
+
expires: number;
|
|
7
|
+
httpOnly: boolean;
|
|
8
|
+
secure: boolean;
|
|
9
|
+
sameSite: 'Strict' | 'Lax' | 'None';
|
|
10
|
+
}
|
|
11
|
+
export declare function getSessionPath(workspaceRoot: string): string;
|
|
12
|
+
export declare function saveSession(workspaceRoot: string, cookies: SavedCookie[]): void;
|
|
13
|
+
export declare function loadSession(workspaceRoot: string): SavedCookie[] | null;
|
|
14
|
+
export declare function clearSession(workspaceRoot: string): void;
|
|
15
|
+
export declare function getCookieHeaderString(cookies: SavedCookie[]): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getSessionPath = getSessionPath;
|
|
37
|
+
exports.saveSession = saveSession;
|
|
38
|
+
exports.loadSession = loadSession;
|
|
39
|
+
exports.clearSession = clearSession;
|
|
40
|
+
exports.getCookieHeaderString = getCookieHeaderString;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
function getSessionPath(workspaceRoot) {
|
|
44
|
+
return path.join(workspaceRoot, '.atcoder-cli', 'session.json');
|
|
45
|
+
}
|
|
46
|
+
function saveSession(workspaceRoot, cookies) {
|
|
47
|
+
const sessionPath = getSessionPath(workspaceRoot);
|
|
48
|
+
const dir = path.dirname(sessionPath);
|
|
49
|
+
if (!fs.existsSync(dir)) {
|
|
50
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
fs.writeFileSync(sessionPath, JSON.stringify(cookies, null, 2), 'utf8');
|
|
53
|
+
}
|
|
54
|
+
function loadSession(workspaceRoot) {
|
|
55
|
+
const sessionPath = getSessionPath(workspaceRoot);
|
|
56
|
+
if (!fs.existsSync(sessionPath)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const raw = fs.readFileSync(sessionPath, 'utf8');
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function clearSession(workspaceRoot) {
|
|
68
|
+
const sessionPath = getSessionPath(workspaceRoot);
|
|
69
|
+
if (fs.existsSync(sessionPath)) {
|
|
70
|
+
fs.unlinkSync(sessionPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function getCookieHeaderString(cookies) {
|
|
74
|
+
return cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
75
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compareOutput = compareOutput;
|
|
4
|
+
function compareOutput(actual, expected) {
|
|
5
|
+
const actualLines = normalizeAndSplit(actual);
|
|
6
|
+
const expectedLines = normalizeAndSplit(expected);
|
|
7
|
+
let isMatch = true;
|
|
8
|
+
let firstDiffLine;
|
|
9
|
+
const maxLines = Math.max(actualLines.length, expectedLines.length);
|
|
10
|
+
for (let i = 0; i < maxLines; i++) {
|
|
11
|
+
if (actualLines[i] !== expectedLines[i]) {
|
|
12
|
+
isMatch = false;
|
|
13
|
+
firstDiffLine = i + 1;
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
isMatch,
|
|
19
|
+
actualNormalized: actualLines.join('\n'),
|
|
20
|
+
expectedNormalized: expectedLines.join('\n'),
|
|
21
|
+
firstDiffLine
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function normalizeAndSplit(str) {
|
|
25
|
+
const lines = str.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
26
|
+
const trimmed = lines.map(line => line.trimEnd());
|
|
27
|
+
// Remove trailing empty lines
|
|
28
|
+
while (trimmed.length > 0 && trimmed[trimmed.length - 1] === '') {
|
|
29
|
+
trimmed.pop();
|
|
30
|
+
}
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Config, LanguageConfig } from '../config/config-store';
|
|
2
|
+
export interface TestCaseResult {
|
|
3
|
+
index: number;
|
|
4
|
+
status: 'AC' | 'WA' | 'TLE' | 'RE';
|
|
5
|
+
durationMs: number;
|
|
6
|
+
actualOutput: string;
|
|
7
|
+
expectedOutput: string;
|
|
8
|
+
errorOutput?: string;
|
|
9
|
+
firstDiffLine?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface RunAllTestsResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
compileError?: string;
|
|
14
|
+
results: TestCaseResult[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the absolute path to a task directory.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveTaskDirectory(workspaceRoot: string, taskArg?: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Detects the code file to execute in the task directory.
|
|
22
|
+
*/
|
|
23
|
+
export declare function detectCodeFile(workspaceRoot: string, taskDir: string, config: Config, fileArg?: string): {
|
|
24
|
+
codeFile: string;
|
|
25
|
+
langKey: string;
|
|
26
|
+
langConfig: LanguageConfig;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Resolves the build and run commands by substituting template filenames with the actual filename.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveCommands(workspaceRoot: string, langConfig: LanguageConfig, actualFile: string, extension: string): {
|
|
32
|
+
build: string;
|
|
33
|
+
run: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Runs the build command if present.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runBuild(buildCommand: string, taskDir: string): Promise<{
|
|
39
|
+
code: number;
|
|
40
|
+
stderr: string;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Runs a single test case.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runTestCase(runCommand: string, taskDir: string, inputPath: string, outputPath: string, timeLimitMs: number, index: number): Promise<TestCaseResult>;
|
|
46
|
+
export declare function runAllTests(workspaceRoot: string, taskDir: string, fileArg?: string, timeLimitMs?: number): Promise<RunAllTestsResult>;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveTaskDirectory = resolveTaskDirectory;
|
|
37
|
+
exports.detectCodeFile = detectCodeFile;
|
|
38
|
+
exports.resolveCommands = resolveCommands;
|
|
39
|
+
exports.runBuild = runBuild;
|
|
40
|
+
exports.runTestCase = runTestCase;
|
|
41
|
+
exports.runAllTests = runAllTests;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const config_store_1 = require("../config/config-store");
|
|
46
|
+
const diff_1 = require("./diff");
|
|
47
|
+
const errors_1 = require("../utils/errors");
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the absolute path to a task directory.
|
|
50
|
+
*/
|
|
51
|
+
function resolveTaskDirectory(workspaceRoot, taskArg) {
|
|
52
|
+
const cwd = process.cwd();
|
|
53
|
+
if (taskArg) {
|
|
54
|
+
const pathFromCwd = path.resolve(cwd, taskArg);
|
|
55
|
+
if (fs.existsSync(pathFromCwd) && fs.statSync(pathFromCwd).isDirectory()) {
|
|
56
|
+
return pathFromCwd;
|
|
57
|
+
}
|
|
58
|
+
const pathFromRoot = path.resolve(workspaceRoot, taskArg);
|
|
59
|
+
if (fs.existsSync(pathFromRoot) && fs.statSync(pathFromRoot).isDirectory()) {
|
|
60
|
+
return pathFromRoot;
|
|
61
|
+
}
|
|
62
|
+
const config = (0, config_store_1.loadConfig)(workspaceRoot);
|
|
63
|
+
if (config.contestDir) {
|
|
64
|
+
const pathFromConfigDir = path.resolve(workspaceRoot, config.contestDir, taskArg);
|
|
65
|
+
if (fs.existsSync(pathFromConfigDir) && fs.statSync(pathFromConfigDir).isDirectory()) {
|
|
66
|
+
return pathFromConfigDir;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const labelPath = path.join(cwd, taskArg);
|
|
70
|
+
if (fs.existsSync(labelPath) && fs.statSync(labelPath).isDirectory()) {
|
|
71
|
+
return labelPath;
|
|
72
|
+
}
|
|
73
|
+
throw new errors_1.AtcError(`Task directory "${taskArg}" not found.`);
|
|
74
|
+
}
|
|
75
|
+
if (path.resolve(cwd) === path.resolve(workspaceRoot)) {
|
|
76
|
+
throw new errors_1.AtcError('You are in the workspace root. Please specify a task directory (e.g., "atc test abc300/a").');
|
|
77
|
+
}
|
|
78
|
+
return cwd;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Detects the code file to execute in the task directory.
|
|
82
|
+
*/
|
|
83
|
+
function detectCodeFile(workspaceRoot, taskDir, config, fileArg) {
|
|
84
|
+
if (fileArg) {
|
|
85
|
+
const fullPath = path.resolve(taskDir, fileArg);
|
|
86
|
+
if (!fs.existsSync(fullPath)) {
|
|
87
|
+
throw new errors_1.AtcError(`Specified source file "${fileArg}" not found in "${taskDir}"`);
|
|
88
|
+
}
|
|
89
|
+
const ext = path.extname(fileArg).slice(1);
|
|
90
|
+
for (const [key, langConfig] of Object.entries(config.languages)) {
|
|
91
|
+
if (langConfig.extension === ext) {
|
|
92
|
+
return { codeFile: fileArg, langKey: key, langConfig };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw new errors_1.AtcError(`No language configuration found for file extension ".${ext}"`);
|
|
96
|
+
}
|
|
97
|
+
const files = fs.readdirSync(taskDir);
|
|
98
|
+
const defLang = config.languages[config.defaultLanguage];
|
|
99
|
+
if (defLang) {
|
|
100
|
+
const matchedFile = files.find(f => f.endsWith(`.${defLang.extension}`) && fs.statSync(path.join(taskDir, f)).isFile());
|
|
101
|
+
if (matchedFile) {
|
|
102
|
+
return { codeFile: matchedFile, langKey: config.defaultLanguage, langConfig: defLang };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const [key, langConfig] of Object.entries(config.languages)) {
|
|
106
|
+
if (key === config.defaultLanguage)
|
|
107
|
+
continue;
|
|
108
|
+
const matchedFile = files.find(f => f.endsWith(`.${langConfig.extension}`) && fs.statSync(path.join(taskDir, f)).isFile());
|
|
109
|
+
if (matchedFile) {
|
|
110
|
+
return { codeFile: matchedFile, langKey: key, langConfig };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw new errors_1.AtcError(`No source files found in "${taskDir}" matching configured languages.`);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolves the build and run commands by substituting template filenames with the actual filename.
|
|
117
|
+
*/
|
|
118
|
+
function resolveCommands(workspaceRoot, langConfig, actualFile, extension) {
|
|
119
|
+
const templateDir = langConfig.templateDir;
|
|
120
|
+
let templateFileName = `main.${extension}`;
|
|
121
|
+
const fullTemplatePath = path.join(workspaceRoot, '.atcoder-cli', templateDir);
|
|
122
|
+
if (fs.existsSync(fullTemplatePath) && fs.statSync(fullTemplatePath).isDirectory()) {
|
|
123
|
+
const files = fs.readdirSync(fullTemplatePath);
|
|
124
|
+
const matched = files.find(f => f.endsWith(`.${extension}`));
|
|
125
|
+
if (matched) {
|
|
126
|
+
templateFileName = matched;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const escapedTemplateName = templateFileName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
130
|
+
const regex = new RegExp(escapedTemplateName, 'g');
|
|
131
|
+
const resolvedBuild = langConfig.build.replace(regex, actualFile);
|
|
132
|
+
const resolvedRun = langConfig.run.replace(regex, actualFile);
|
|
133
|
+
return {
|
|
134
|
+
build: resolvedBuild,
|
|
135
|
+
run: resolvedRun
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Runs the build command if present.
|
|
140
|
+
*/
|
|
141
|
+
function runBuild(buildCommand, taskDir) {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
(0, child_process_1.exec)(buildCommand, { cwd: taskDir }, (err, stdout, stderr) => {
|
|
144
|
+
resolve({
|
|
145
|
+
code: err ? (err.code ?? 1) : 0,
|
|
146
|
+
stderr: stderr || stdout
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Runs a single test case.
|
|
153
|
+
*/
|
|
154
|
+
function runTestCase(runCommand, taskDir, inputPath, outputPath, timeLimitMs, index) {
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const input = fs.readFileSync(inputPath, 'utf8');
|
|
157
|
+
const expected = fs.readFileSync(outputPath, 'utf8');
|
|
158
|
+
const startTime = process.hrtime.bigint();
|
|
159
|
+
const child = (0, child_process_1.spawn)(runCommand, {
|
|
160
|
+
cwd: taskDir,
|
|
161
|
+
shell: true
|
|
162
|
+
});
|
|
163
|
+
let stdout = '';
|
|
164
|
+
let stderr = '';
|
|
165
|
+
let killedByTimeout = false;
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
killedByTimeout = true;
|
|
168
|
+
child.kill('SIGKILL');
|
|
169
|
+
}, timeLimitMs);
|
|
170
|
+
child.stdout.on('data', (data) => {
|
|
171
|
+
stdout += data.toString();
|
|
172
|
+
});
|
|
173
|
+
child.stderr.on('data', (data) => {
|
|
174
|
+
stderr += data.toString();
|
|
175
|
+
});
|
|
176
|
+
child.on('error', (err) => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
const endTime = process.hrtime.bigint();
|
|
179
|
+
const durationMs = Number(endTime - startTime) / 1e6;
|
|
180
|
+
resolve({
|
|
181
|
+
index,
|
|
182
|
+
status: 'RE',
|
|
183
|
+
durationMs,
|
|
184
|
+
actualOutput: stdout,
|
|
185
|
+
expectedOutput: expected,
|
|
186
|
+
errorOutput: err.message
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
child.on('exit', (code, signal) => {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
const endTime = process.hrtime.bigint();
|
|
192
|
+
const durationMs = Number(endTime - startTime) / 1e6;
|
|
193
|
+
if (killedByTimeout || signal === 'SIGKILL') {
|
|
194
|
+
resolve({
|
|
195
|
+
index,
|
|
196
|
+
status: 'TLE',
|
|
197
|
+
durationMs,
|
|
198
|
+
actualOutput: stdout,
|
|
199
|
+
expectedOutput: expected,
|
|
200
|
+
errorOutput: 'Time Limit Exceeded'
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (code !== 0) {
|
|
205
|
+
resolve({
|
|
206
|
+
index,
|
|
207
|
+
status: 'RE',
|
|
208
|
+
durationMs,
|
|
209
|
+
actualOutput: stdout,
|
|
210
|
+
expectedOutput: expected,
|
|
211
|
+
errorOutput: stderr || `Exit code ${code}`
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const comp = (0, diff_1.compareOutput)(stdout, expected);
|
|
216
|
+
resolve({
|
|
217
|
+
index,
|
|
218
|
+
status: comp.isMatch ? 'AC' : 'WA',
|
|
219
|
+
durationMs,
|
|
220
|
+
actualOutput: stdout,
|
|
221
|
+
expectedOutput: expected,
|
|
222
|
+
firstDiffLine: comp.firstDiffLine
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
child.stdin.write(input);
|
|
226
|
+
child.stdin.end();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
async function runAllTests(workspaceRoot, taskDir, fileArg, timeLimitMs = 2000) {
|
|
230
|
+
const config = (0, config_store_1.loadConfig)(workspaceRoot);
|
|
231
|
+
const { codeFile, langConfig } = detectCodeFile(workspaceRoot, taskDir, config, fileArg);
|
|
232
|
+
const { build, run } = resolveCommands(workspaceRoot, langConfig, codeFile, langConfig.extension);
|
|
233
|
+
if (build.trim() !== '') {
|
|
234
|
+
const buildRes = await runBuild(build, taskDir);
|
|
235
|
+
if (buildRes.code !== 0) {
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
compileError: buildRes.stderr,
|
|
239
|
+
results: []
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const testDirName = config.testDirName || 'tests';
|
|
244
|
+
const testDir = path.join(taskDir, testDirName);
|
|
245
|
+
if (!fs.existsSync(testDir) || !fs.statSync(testDir).isDirectory()) {
|
|
246
|
+
throw new errors_1.AtcError(`Test directory "${testDir}" not found.`);
|
|
247
|
+
}
|
|
248
|
+
const files = fs.readdirSync(testDir);
|
|
249
|
+
const inFiles = files.filter(f => f.startsWith('sample-') && f.endsWith('.in'));
|
|
250
|
+
const results = [];
|
|
251
|
+
inFiles.sort((a, b) => {
|
|
252
|
+
const aIdx = parseInt(a.match(/sample-(\d+)\.in/)[1], 10);
|
|
253
|
+
const bIdx = parseInt(b.match(/sample-(\d+)\.in/)[1], 10);
|
|
254
|
+
return aIdx - bIdx;
|
|
255
|
+
});
|
|
256
|
+
for (const inFile of inFiles) {
|
|
257
|
+
const match = inFile.match(/sample-(\d+)\.in/);
|
|
258
|
+
if (!match)
|
|
259
|
+
continue;
|
|
260
|
+
const index = parseInt(match[1], 10);
|
|
261
|
+
const inputPath = path.join(testDir, inFile);
|
|
262
|
+
const outputPath = path.join(testDir, `sample-${index}.out`);
|
|
263
|
+
if (!fs.existsSync(outputPath)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const testRes = await runTestCase(run, taskDir, inputPath, outputPath, timeLimitMs, index);
|
|
267
|
+
results.push(testRes);
|
|
268
|
+
}
|
|
269
|
+
const success = results.length > 0 && results.every(r => r.status === 'AC');
|
|
270
|
+
return {
|
|
271
|
+
success,
|
|
272
|
+
results
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class AtcError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class WorkspaceNotFoundError extends AtcError {
|
|
5
|
+
constructor();
|
|
6
|
+
}
|
|
7
|
+
export declare class AuthError extends AtcError {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class ParseError extends AtcError {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class TestError extends AtcError {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TestError = exports.ParseError = exports.AuthError = exports.WorkspaceNotFoundError = exports.AtcError = void 0;
|
|
4
|
+
class AtcError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = this.constructor.name;
|
|
8
|
+
Error.captureStackTrace(this, this.constructor);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.AtcError = AtcError;
|
|
12
|
+
class WorkspaceNotFoundError extends AtcError {
|
|
13
|
+
constructor() {
|
|
14
|
+
super('.atcoder-cli/ directory was not found. Please run "atc init" in your workspace root first.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.WorkspaceNotFoundError = WorkspaceNotFoundError;
|
|
18
|
+
class AuthError extends AtcError {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(`Authentication failed: ${message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.AuthError = AuthError;
|
|
24
|
+
class ParseError extends AtcError {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(`Parsing failed: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.ParseError = ParseError;
|
|
30
|
+
class TestError extends AtcError {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(`Test execution failed: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.TestError = TestError;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats multi-line output with line numbers, a left border,
|
|
3
|
+
* a mismatch pointer, and smart truncation for large outputs.
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatOutputLines(output: string, firstDiffLine?: number): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Formats error output with red text, a left border, and clean indents.
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatErrorOutputLines(errorOutput: string): string[];
|