@doccident/doccident 0.0.4 → 0.0.6
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 +195 -3
- package/README.md +333 -7
- package/bin/cmd.js +12 -1
- package/dist/doctest.js +189 -7
- package/dist/languages/basic.js +51 -0
- package/dist/languages/c.js +123 -12
- package/dist/languages/cobol.js +8 -3
- package/dist/languages/csharp.js +77 -0
- package/dist/languages/fortran.js +119 -10
- package/dist/languages/go.js +124 -10
- package/dist/languages/java.js +92 -0
- package/dist/languages/javascript.js +31 -4
- package/dist/languages/pascal.js +88 -0
- package/dist/languages/perl.js +36 -0
- package/dist/languages/python.js +20 -3
- package/dist/languages/r.js +37 -0
- package/dist/languages/rust.js +58 -10
- package/dist/languages/shell.js +12 -3
- package/dist/parse-code-snippets-from-markdown.js +102 -3
- package/dist/reporter.js +39 -0
- package/package.json +6 -6
package/bin/cmd.js
CHANGED
|
@@ -22,7 +22,8 @@ const config = {
|
|
|
22
22
|
require: {},
|
|
23
23
|
globals: {},
|
|
24
24
|
ignore: [],
|
|
25
|
-
testOutput: false
|
|
25
|
+
testOutput: false,
|
|
26
|
+
timeout: 300000 // Default 5 minutes
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
// Setup commander
|
|
@@ -34,6 +35,8 @@ program
|
|
|
34
35
|
.helpOption('-h, --help', 'output usage informations')
|
|
35
36
|
.option('-c, --config <path>', 'custom config location', path.join(process.cwd(), '/.doccident-setup.js'))
|
|
36
37
|
.option('--test-output', 'output the test results to the console')
|
|
38
|
+
.option('--update-output', 'update the output blocks in markdown files')
|
|
39
|
+
.option('--timeout <ms>', 'timeout for each snippet execution in milliseconds', '300000')
|
|
37
40
|
.parse(process.argv);
|
|
38
41
|
|
|
39
42
|
const options = program.opts();
|
|
@@ -57,6 +60,14 @@ const options = program.opts();
|
|
|
57
60
|
if (options.testOutput) {
|
|
58
61
|
config.testOutput = true;
|
|
59
62
|
}
|
|
63
|
+
|
|
64
|
+
if (options.updateOutput) {
|
|
65
|
+
config.updateOutput = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (options.timeout) {
|
|
69
|
+
config.timeout = parseInt(options.timeout, 10);
|
|
70
|
+
}
|
|
60
71
|
|
|
61
72
|
// Resolve files
|
|
62
73
|
try {
|
package/dist/doctest.js
CHANGED
|
@@ -18,12 +18,27 @@ const rust_1 = require("./languages/rust");
|
|
|
18
18
|
const fortran_1 = require("./languages/fortran");
|
|
19
19
|
const cobol_1 = require("./languages/cobol");
|
|
20
20
|
const c_1 = require("./languages/c");
|
|
21
|
+
const basic_1 = require("./languages/basic");
|
|
22
|
+
const java_1 = require("./languages/java");
|
|
23
|
+
const csharp_1 = require("./languages/csharp");
|
|
24
|
+
const perl_1 = require("./languages/perl");
|
|
25
|
+
const r_1 = require("./languages/r");
|
|
26
|
+
const pascal_1 = require("./languages/pascal");
|
|
21
27
|
const javascript_1 = require("./languages/javascript");
|
|
22
28
|
function runTests(files, config) {
|
|
23
29
|
const results = files
|
|
24
30
|
.map(read)
|
|
25
|
-
.map(
|
|
26
|
-
.
|
|
31
|
+
.map(fileInfo => {
|
|
32
|
+
const parsed = (0, parse_code_snippets_from_markdown_1.default)(fileInfo);
|
|
33
|
+
// Attach original contents to parsed file if needed for updates?
|
|
34
|
+
// Actually testFile closure captures it if we pass it correctly?
|
|
35
|
+
// But map pipeline separates them.
|
|
36
|
+
// Let's modify testFile to accept FileInfo + ParsedFile or just re-read?
|
|
37
|
+
// Or better: `read` returns FileInfo. `parseCodeSnippets` returns ParsedFile (without contents).
|
|
38
|
+
// We can wrap this better.
|
|
39
|
+
return { parsed, fileInfo };
|
|
40
|
+
})
|
|
41
|
+
.map(({ parsed, fileInfo }) => testFile(config)(parsed, fileInfo));
|
|
27
42
|
return (0, utils_1.flatten)(results);
|
|
28
43
|
}
|
|
29
44
|
function read(fileName) {
|
|
@@ -52,28 +67,168 @@ function makeTestSandbox(config) {
|
|
|
52
67
|
return sandbox;
|
|
53
68
|
}
|
|
54
69
|
function testFile(config) {
|
|
55
|
-
return function testFileWithConfig(args) {
|
|
70
|
+
return function testFileWithConfig(args, fileInfo) {
|
|
56
71
|
const codeSnippets = args.codeSnippets;
|
|
57
72
|
const fileName = args.fileName;
|
|
58
73
|
const shareCodeInFile = args.shareCodeInFile;
|
|
59
74
|
let results;
|
|
75
|
+
// Map to store outputs by ID
|
|
76
|
+
const outputs = {};
|
|
77
|
+
// Store edits if updating output
|
|
78
|
+
const edits = [];
|
|
60
79
|
if (shareCodeInFile) {
|
|
61
80
|
const sandbox = makeTestSandbox(config);
|
|
62
|
-
results = codeSnippets.map(test(config, fileName, sandbox));
|
|
81
|
+
results = codeSnippets.map(test(config, fileName, sandbox, outputs, edits));
|
|
63
82
|
}
|
|
64
83
|
else {
|
|
65
|
-
results = codeSnippets.map(test(config, fileName));
|
|
84
|
+
results = codeSnippets.map(test(config, fileName, undefined, outputs, edits));
|
|
85
|
+
}
|
|
86
|
+
// Apply edits if updateOutput is true and we have file info
|
|
87
|
+
if (config.updateOutput && edits.length > 0 && fileInfo) {
|
|
88
|
+
applyEdits(fileInfo, edits);
|
|
66
89
|
}
|
|
67
90
|
return results;
|
|
68
91
|
};
|
|
69
92
|
}
|
|
70
|
-
function
|
|
93
|
+
function applyEdits(fileInfo, edits) {
|
|
94
|
+
// Sort edits by startLine descending to safely modify file
|
|
95
|
+
// Note: We need endLine for snippets. Currently Snippet only has lineNumber (start).
|
|
96
|
+
// The parser needs to provide endLine or we need to calculate it.
|
|
97
|
+
// If we assume standard fences ```...```, we can try to guess or update parser.
|
|
98
|
+
// Parser update is safer. But let's check snippet first.
|
|
99
|
+
// For now, let's assume we can't do it safely without endLine.
|
|
100
|
+
// Snippet interface needs endLine.
|
|
101
|
+
// Sort descending
|
|
102
|
+
edits.sort((a, b) => b.startLine - a.startLine);
|
|
103
|
+
const lines = fileInfo.contents.split('\n');
|
|
104
|
+
for (const edit of edits) {
|
|
105
|
+
if (edit.endLine === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
// Snippet startLine is 1-based index of the first line of content (inside fences) usually?
|
|
108
|
+
// Let's check parser.
|
|
109
|
+
// In parser: `lineNumber` is `index + 1`. `index` is passed from split('\n').map.
|
|
110
|
+
// `isStartOfSnippet` detects the opening fence.
|
|
111
|
+
// `startNewSnippet` called with that line number.
|
|
112
|
+
// So `lineNumber` is the line of the OPENING FENCE ```.
|
|
113
|
+
// Code content starts at lineNumber + 1?
|
|
114
|
+
// Wait, parser:
|
|
115
|
+
// `parseLine` calls `startNewSnippet` on `isStartOfSnippet`.
|
|
116
|
+
// So snippet.lineNumber is the line index (1-based) of the ```lang line.
|
|
117
|
+
// `endSnippet` is called on `isEndOfSnippet` (closing ```).
|
|
118
|
+
// We need to capture that line number too.
|
|
119
|
+
// We replace from `startLine` (exclusive? no, replace the CONTENT)
|
|
120
|
+
// Actually, we want to replace the content BETWEEN the fences.
|
|
121
|
+
// So from `startLine` (0-based index) + 1 to `endLine` (0-based index) - 1.
|
|
122
|
+
// Let's rely on parser providing `endLine`.
|
|
123
|
+
const startIdx = edit.startLine; // 1-based index of opening fence
|
|
124
|
+
const endIdx = edit.endLine; // 1-based index of closing fence
|
|
125
|
+
// Content lines are startIdx...endIdx-2 (0-based: startIdx is fence line)
|
|
126
|
+
// Example:
|
|
127
|
+
// 1: ```text <- startLine
|
|
128
|
+
// 2: Old <- content
|
|
129
|
+
// 3: ``` <- endLine
|
|
130
|
+
// We want to replace lines between startLine and endLine with new content.
|
|
131
|
+
// 0-based indexes:
|
|
132
|
+
// startLineIndex = startLine - 1;
|
|
133
|
+
// endLineIndex = endLine - 1;
|
|
134
|
+
// We want to replace lines from (startLineIndex + 1) to (endLineIndex - 1).
|
|
135
|
+
// splice(start, deleteCount, items...)
|
|
136
|
+
const startReplace = startIdx; // index after opening fence
|
|
137
|
+
const deleteCount = (endIdx - 1) - startReplace;
|
|
138
|
+
// If content is empty/new, we just splice.
|
|
139
|
+
lines.splice(startReplace, deleteCount, edit.content);
|
|
140
|
+
}
|
|
141
|
+
(0, fs_1.writeFileSync)(fileInfo.fileName, lines.join('\n'));
|
|
142
|
+
}
|
|
143
|
+
function test(config, _filename, sandbox, outputs, edits) {
|
|
71
144
|
return (codeSnippet) => {
|
|
145
|
+
const startTime = performance.now();
|
|
72
146
|
if (codeSnippet.skip) {
|
|
73
147
|
return { status: "skip", codeSnippet, stack: "" };
|
|
74
148
|
}
|
|
149
|
+
// Output verification / update logic
|
|
150
|
+
if (codeSnippet.outputOf) {
|
|
151
|
+
const expectedOutput = codeSnippet.code.trim(); // Trim for comparison
|
|
152
|
+
// For update, we want the raw output (maybe trimmed of trailing newline but preserving structure)
|
|
153
|
+
const actualOutput = outputs && outputs[codeSnippet.outputOf] ? outputs[codeSnippet.outputOf] : "";
|
|
154
|
+
const actualOutputTrimmed = actualOutput.trim();
|
|
155
|
+
// console.log(`[DEBUG] Verifying '${codeSnippet.outputOf}' Mode=${codeSnippet.outputMode}`);
|
|
156
|
+
// console.log(`[DEBUG] Expected: "${expectedOutput}"`);
|
|
157
|
+
// console.log(`[DEBUG] Actual: "${actualOutputTrimmed}"`);
|
|
158
|
+
if (!outputs || outputs[codeSnippet.outputOf] === undefined) {
|
|
159
|
+
return { status: "fail", codeSnippet, stack: `Snippet with id '${codeSnippet.outputOf}' not executed or not found.` };
|
|
160
|
+
}
|
|
161
|
+
if (config.updateOutput) {
|
|
162
|
+
// Schedule update
|
|
163
|
+
if (edits) {
|
|
164
|
+
// Normalize actual output: usually we want to preserve it exactly,
|
|
165
|
+
// but standard console.log adds newline.
|
|
166
|
+
// Markdown blocks usually end with newline before ```.
|
|
167
|
+
// Let's strip the very last newline if present to avoid growing gaps?
|
|
168
|
+
// Or just use trimEnd()?
|
|
169
|
+
// Generally, code blocks look like:
|
|
170
|
+
// ```text
|
|
171
|
+
// Output
|
|
172
|
+
// ```
|
|
173
|
+
// If actualOutput is "Output\n", we put "Output" inside?
|
|
174
|
+
// Or "Output\n"?
|
|
175
|
+
// If we put "Output\n", it becomes:
|
|
176
|
+
// ```text
|
|
177
|
+
// Output
|
|
178
|
+
//
|
|
179
|
+
// ```
|
|
180
|
+
// Let's trim trailing whitespace from actualOutput for the replacement content.
|
|
181
|
+
edits.push({
|
|
182
|
+
startLine: codeSnippet.lineNumber,
|
|
183
|
+
endLine: codeSnippet.endLine, // We need this!
|
|
184
|
+
content: actualOutput.trimEnd()
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
process.stdout.write(chalk_1.default.yellow("u"));
|
|
188
|
+
return { status: "pass", codeSnippet, stack: "" };
|
|
189
|
+
}
|
|
190
|
+
if (actualOutputTrimmed !== expectedOutput) {
|
|
191
|
+
if (codeSnippet.outputMode === 'ignore-whitespace') {
|
|
192
|
+
// Normalize both strings: replace all whitespace sequences with single space and trim
|
|
193
|
+
const normalize = (s) => s.replace(/\s+/g, ' ').trim();
|
|
194
|
+
if (normalize(actualOutput) === normalize(expectedOutput)) {
|
|
195
|
+
process.stdout.write(chalk_1.default.green("."));
|
|
196
|
+
return { status: "pass", codeSnippet, stack: "" };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (codeSnippet.outputMode === 'regex') {
|
|
200
|
+
// Treat expectedOutput as regex pattern
|
|
201
|
+
try {
|
|
202
|
+
const pattern = expectedOutput.trim();
|
|
203
|
+
// For regex, we match against trimmed actual output
|
|
204
|
+
const re = new RegExp(pattern);
|
|
205
|
+
if (re.test(actualOutput.trim())) {
|
|
206
|
+
process.stdout.write(chalk_1.default.green("."));
|
|
207
|
+
return { status: "pass", codeSnippet, stack: "" };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
process.stdout.write(chalk_1.default.red("x"));
|
|
212
|
+
return {
|
|
213
|
+
status: "fail",
|
|
214
|
+
codeSnippet,
|
|
215
|
+
stack: `Invalid Regex Pattern: ${e}\nPattern:\n${expectedOutput}`
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
process.stdout.write(chalk_1.default.red("x"));
|
|
220
|
+
return {
|
|
221
|
+
status: "fail",
|
|
222
|
+
codeSnippet,
|
|
223
|
+
stack: `Output verification failed (${codeSnippet.outputMode || 'exact'}).\nExpected:\n${expectedOutput}\n\nActual:\n${actualOutputTrimmed}`
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
process.stdout.write(chalk_1.default.green("."));
|
|
227
|
+
return { status: "pass", codeSnippet, stack: "" };
|
|
228
|
+
}
|
|
75
229
|
let success = false;
|
|
76
230
|
let stack = "";
|
|
231
|
+
let output = "";
|
|
77
232
|
let code = codeSnippet.code;
|
|
78
233
|
if (config.transformCode) {
|
|
79
234
|
try {
|
|
@@ -119,14 +274,41 @@ function test(config, _filename, sandbox) {
|
|
|
119
274
|
else if (codeSnippet.language === 'c') {
|
|
120
275
|
result = (0, c_1.cHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
121
276
|
}
|
|
277
|
+
else if (codeSnippet.language === 'basic') {
|
|
278
|
+
result = (0, basic_1.basicHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
279
|
+
}
|
|
280
|
+
else if (codeSnippet.language === 'java') {
|
|
281
|
+
result = (0, java_1.javaHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
282
|
+
}
|
|
283
|
+
else if (codeSnippet.language === 'perl') {
|
|
284
|
+
result = (0, perl_1.perlHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
285
|
+
}
|
|
286
|
+
else if (codeSnippet.language === 'r') {
|
|
287
|
+
result = (0, r_1.rHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
288
|
+
}
|
|
289
|
+
else if (codeSnippet.language === 'pascal') {
|
|
290
|
+
result = (0, pascal_1.pascalHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
291
|
+
}
|
|
292
|
+
else if (codeSnippet.language === 'csharp') {
|
|
293
|
+
result = (0, csharp_1.csharpHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
294
|
+
}
|
|
295
|
+
else if (codeSnippet.language === 'text') {
|
|
296
|
+
result = { success: true, stack: "" };
|
|
297
|
+
}
|
|
122
298
|
else {
|
|
123
299
|
result = (0, javascript_1.javascriptHandler)(code, codeSnippet, config, activeSandbox, isSharedSandbox);
|
|
124
300
|
}
|
|
125
301
|
success = result.success;
|
|
126
302
|
stack = result.stack;
|
|
303
|
+
output = result.output || "";
|
|
304
|
+
// Store output if ID is present
|
|
305
|
+
if (codeSnippet.id && outputs) {
|
|
306
|
+
outputs[codeSnippet.id] = output;
|
|
307
|
+
}
|
|
127
308
|
const status = success ? "pass" : "fail";
|
|
309
|
+
const executionTime = performance.now() - startTime;
|
|
128
310
|
process.stdout.write(success ? chalk_1.default.green(".") : chalk_1.default.red("x"));
|
|
129
|
-
return { status, codeSnippet, stack };
|
|
311
|
+
return { status, codeSnippet, stack, executionTime };
|
|
130
312
|
};
|
|
131
313
|
}
|
|
132
314
|
function moduleNotFoundError(moduleName) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.basicHandler = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
// Check if BASIC code is a complete program (has END statement)
|
|
6
|
+
function isCompleteBASIC(code) {
|
|
7
|
+
// Look for END as a statement (possibly with line number prefix)
|
|
8
|
+
// Matches: "END", "10 END", "END\n", etc.
|
|
9
|
+
return /^\d*\s*END\s*$/im.test(code);
|
|
10
|
+
}
|
|
11
|
+
const basicHandler = (code, _snippet, config, sandbox, isSharedSandbox) => {
|
|
12
|
+
let success = false;
|
|
13
|
+
let stack = "";
|
|
14
|
+
const context = sandbox;
|
|
15
|
+
// If sharing code, we need to accumulate previous basic snippets
|
|
16
|
+
if (isSharedSandbox) {
|
|
17
|
+
if (!context._basicContext) {
|
|
18
|
+
context._basicContext = "";
|
|
19
|
+
}
|
|
20
|
+
// Trim trailing newline from code before adding to avoid blank lines
|
|
21
|
+
// (code already has trailing newline from parser)
|
|
22
|
+
const trimmedCode = code.replace(/\n+$/, '');
|
|
23
|
+
context._basicContext += trimmedCode + "\n";
|
|
24
|
+
code = context._basicContext;
|
|
25
|
+
// In shared mode, only run when program is complete (has END)
|
|
26
|
+
// Incomplete snippets are setup code - mark as pass without running
|
|
27
|
+
if (!isCompleteBASIC(code)) {
|
|
28
|
+
return { success: true, stack: "", output: "" };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
// cbmbasic accepts input from stdin
|
|
33
|
+
const timeout = config.timeout || 30000;
|
|
34
|
+
const result = (0, child_process_1.spawnSync)('cbmbasic', [], { input: code, encoding: 'utf-8', timeout });
|
|
35
|
+
if (result.error && result.error.code === 'ETIMEDOUT') {
|
|
36
|
+
return { success: false, stack: `Execution timed out after ${timeout}ms` };
|
|
37
|
+
}
|
|
38
|
+
if (result.status === 0) {
|
|
39
|
+
success = true;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
stack = result.stderr || "BASIC execution failed with non-zero exit code";
|
|
43
|
+
}
|
|
44
|
+
return { success, stack, output: result.stdout };
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
stack = e.message || "Failed to spawn cbmbasic";
|
|
48
|
+
return { success, stack };
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
exports.basicHandler = basicHandler;
|
package/dist/languages/c.js
CHANGED
|
@@ -5,38 +5,149 @@ const child_process_1 = require("child_process");
|
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const os_1 = require("os");
|
|
8
|
-
|
|
8
|
+
// Helper to parse C code
|
|
9
|
+
function parseCCode(fullCode) {
|
|
10
|
+
const lines = fullCode.split('\n');
|
|
11
|
+
const includes = [];
|
|
12
|
+
let topLevel = "";
|
|
13
|
+
let mainBody = "";
|
|
14
|
+
let state = 'NORMAL';
|
|
15
|
+
let braceCount = 0;
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
// Empty lines preserve structure slightly better if added to last active section
|
|
20
|
+
// or just ignore. Let's add to mainBody if normal, or topLevel if in block.
|
|
21
|
+
if (state === 'IN_TOP_LEVEL_BLOCK')
|
|
22
|
+
topLevel += "\n";
|
|
23
|
+
else
|
|
24
|
+
mainBody += "\n";
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (state === 'NORMAL') {
|
|
28
|
+
if (trimmed.startsWith('#')) {
|
|
29
|
+
includes.push(line);
|
|
30
|
+
}
|
|
31
|
+
else if (trimmed.startsWith('struct ') ||
|
|
32
|
+
trimmed.startsWith('union ') ||
|
|
33
|
+
trimmed.startsWith('enum ') ||
|
|
34
|
+
trimmed.startsWith('typedef ') ||
|
|
35
|
+
// Function definition heuristic: ReturnType Name(Args) {
|
|
36
|
+
// Must end with {
|
|
37
|
+
(trimmed.endsWith('{') && !trimmed.startsWith('if') && !trimmed.startsWith('for') && !trimmed.startsWith('while') && !trimmed.startsWith('switch') && !trimmed.startsWith('do'))) {
|
|
38
|
+
topLevel += line + "\n";
|
|
39
|
+
// Count braces
|
|
40
|
+
const open = (line.match(/\{/g) || []).length;
|
|
41
|
+
const close = (line.match(/\}/g) || []).length;
|
|
42
|
+
braceCount = open - close;
|
|
43
|
+
if (braceCount > 0) {
|
|
44
|
+
state = 'IN_TOP_LEVEL_BLOCK';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Statements, variable declarations (int x;), etc. go to main
|
|
49
|
+
mainBody += line + "\n";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (state === 'IN_TOP_LEVEL_BLOCK') {
|
|
53
|
+
topLevel += line + "\n";
|
|
54
|
+
const open = (line.match(/\{/g) || []).length;
|
|
55
|
+
const close = (line.match(/\}/g) || []).length;
|
|
56
|
+
braceCount += open - close;
|
|
57
|
+
if (braceCount <= 0) {
|
|
58
|
+
state = 'NORMAL';
|
|
59
|
+
braceCount = 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { includes, topLevel, mainBody };
|
|
64
|
+
}
|
|
65
|
+
const cHandler = (code, snippet, config, sandbox, isSharedSandbox) => {
|
|
9
66
|
let success = false;
|
|
10
67
|
let stack = "";
|
|
11
|
-
|
|
12
|
-
// Auto-wrap in main function if not present
|
|
68
|
+
const context = sandbox;
|
|
13
69
|
let cCode = code;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
70
|
+
// Handle shared state
|
|
71
|
+
if (isSharedSandbox) {
|
|
72
|
+
if (!context._cCode) {
|
|
73
|
+
context._cCode = "";
|
|
74
|
+
}
|
|
75
|
+
context._cCode += code + "\n";
|
|
76
|
+
cCode = context._cCode;
|
|
77
|
+
}
|
|
78
|
+
let finalSource = "";
|
|
79
|
+
// If main is explicitly provided, use it (disable parsing/wrapping)
|
|
80
|
+
// But in shared mode, we usually want to compose.
|
|
81
|
+
// If user provides "int main() { ... }" in a snippet, we assume they take full control?
|
|
82
|
+
// Or do we try to merge?
|
|
83
|
+
// Merging multiple mains is impossible.
|
|
84
|
+
// So if any snippet contains "int main", we might just assume it's the full program so far?
|
|
85
|
+
// But earlier snippets might be structs/includes.
|
|
86
|
+
// Let's assume if "int main" or "void main" is present, we respect it,
|
|
87
|
+
// but we still might want to prepend includes from previous snippets if they were separate?
|
|
88
|
+
// For simplicity: If "main(" found, treat as full program.
|
|
89
|
+
// But parsing is safer for "includes" + "functions" + "statements".
|
|
90
|
+
if (cCode.includes('main(')) {
|
|
91
|
+
// If shared sandbox, we might have accumulated a main from previous snippets?
|
|
92
|
+
// No, if we are parsing, we construct main.
|
|
93
|
+
// If the USER typed main, we trust them.
|
|
94
|
+
finalSource = cCode;
|
|
95
|
+
// Auto-add stdio.h if missing and needed?
|
|
96
|
+
if (!finalSource.includes('<stdio.h>') && finalSource.includes('printf')) {
|
|
97
|
+
finalSource = `#include <stdio.h>\n${finalSource}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const { includes, topLevel, mainBody } = parseCCode(cCode);
|
|
102
|
+
// Auto-add stdio.h if needed
|
|
103
|
+
let autoIncludes = "";
|
|
104
|
+
const includesStr = includes.join('\n');
|
|
105
|
+
if (!includesStr.includes('<stdio.h>') && (topLevel.includes('printf') || mainBody.includes('printf'))) {
|
|
106
|
+
autoIncludes = `#include <stdio.h>\n`;
|
|
107
|
+
}
|
|
108
|
+
finalSource = `${autoIncludes}${includesStr}
|
|
109
|
+
|
|
110
|
+
${topLevel}
|
|
111
|
+
|
|
112
|
+
int main() {
|
|
113
|
+
${mainBody}
|
|
114
|
+
return 0;
|
|
115
|
+
}`;
|
|
20
116
|
}
|
|
21
117
|
const uniqueId = `${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
22
118
|
const tempSourceFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_c_${uniqueId}.c`);
|
|
23
119
|
const tempExeFile = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_c_${uniqueId}`);
|
|
24
120
|
try {
|
|
25
|
-
|
|
121
|
+
const args = snippet.args || [];
|
|
122
|
+
const env = snippet.env ? { ...process.env, ...snippet.env } : undefined;
|
|
123
|
+
const timeout = config.timeout || 30000;
|
|
124
|
+
(0, fs_1.writeFileSync)(tempSourceFile, finalSource);
|
|
26
125
|
// Compile with gcc
|
|
27
|
-
|
|
126
|
+
// args passed to GCC (compiler flags)
|
|
127
|
+
const compileResult = (0, child_process_1.spawnSync)('gcc', [tempSourceFile, '-o', tempExeFile, ...args], { encoding: 'utf-8', timeout });
|
|
28
128
|
if (compileResult.status !== 0) {
|
|
29
129
|
stack = compileResult.stderr || "C compilation failed";
|
|
130
|
+
if (isSharedSandbox) {
|
|
131
|
+
stack += `\n\nGenerated Source:\n${finalSource}`;
|
|
132
|
+
}
|
|
30
133
|
}
|
|
31
134
|
else {
|
|
32
135
|
// Run
|
|
33
|
-
|
|
136
|
+
// What if we want runtime args?
|
|
137
|
+
// Currently args are compiler flags.
|
|
138
|
+
// Maybe we need separate config for runtime args?
|
|
139
|
+
// For now, adhering to "arguments to compilers/interpreters".
|
|
140
|
+
const runResult = (0, child_process_1.spawnSync)(tempExeFile, [], { encoding: 'utf-8', env, timeout });
|
|
141
|
+
if (runResult.error && runResult.error.code === 'ETIMEDOUT') {
|
|
142
|
+
return { success: false, stack: `Execution timed out after ${timeout}ms` };
|
|
143
|
+
}
|
|
34
144
|
if (runResult.status === 0) {
|
|
35
145
|
success = true;
|
|
36
146
|
}
|
|
37
147
|
else {
|
|
38
148
|
stack = runResult.stderr || "C execution failed with non-zero exit code";
|
|
39
149
|
}
|
|
150
|
+
return { success, stack, output: runResult.stdout };
|
|
40
151
|
}
|
|
41
152
|
}
|
|
42
153
|
catch (e) {
|
package/dist/languages/cobol.js
CHANGED
|
@@ -5,7 +5,7 @@ const child_process_1 = require("child_process");
|
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const os_1 = require("os");
|
|
8
|
-
const cobolHandler = (code, _snippet,
|
|
8
|
+
const cobolHandler = (code, _snippet, config, _sandbox, _isSharedSandbox) => {
|
|
9
9
|
let success = false;
|
|
10
10
|
let stack = "";
|
|
11
11
|
// COBOL execution logic
|
|
@@ -13,22 +13,27 @@ const cobolHandler = (code, _snippet, _config, _sandbox, _isSharedSandbox) => {
|
|
|
13
13
|
const uniqueId = Math.random().toString(36).substring(2, 10);
|
|
14
14
|
const tempSourceFile = (0, path_1.join)((0, os_1.tmpdir)(), `cob_${uniqueId}.cob`);
|
|
15
15
|
const tempExeFile = (0, path_1.join)((0, os_1.tmpdir)(), `cob_${uniqueId}`);
|
|
16
|
+
const timeout = config.timeout || 30000;
|
|
16
17
|
try {
|
|
17
18
|
(0, fs_1.writeFileSync)(tempSourceFile, code);
|
|
18
19
|
// Compile with cobc -x (executable) -free (free format)
|
|
19
|
-
const compileResult = (0, child_process_1.spawnSync)('cobc', ['-x', '-free', '-o', tempExeFile, tempSourceFile], { encoding: 'utf-8' });
|
|
20
|
+
const compileResult = (0, child_process_1.spawnSync)('cobc', ['-x', '-free', '-o', tempExeFile, tempSourceFile], { encoding: 'utf-8', timeout });
|
|
20
21
|
if (compileResult.status !== 0) {
|
|
21
22
|
stack = compileResult.stderr || "COBOL compilation failed";
|
|
22
23
|
}
|
|
23
24
|
else {
|
|
24
25
|
// Run
|
|
25
|
-
const runResult = (0, child_process_1.spawnSync)(tempExeFile, [], { encoding: 'utf-8' });
|
|
26
|
+
const runResult = (0, child_process_1.spawnSync)(tempExeFile, [], { encoding: 'utf-8', timeout });
|
|
27
|
+
if (runResult.error && runResult.error.code === 'ETIMEDOUT') {
|
|
28
|
+
return { success: false, stack: `Execution timed out after ${timeout}ms` };
|
|
29
|
+
}
|
|
26
30
|
if (runResult.status === 0) {
|
|
27
31
|
success = true;
|
|
28
32
|
}
|
|
29
33
|
else {
|
|
30
34
|
stack = runResult.stderr || "COBOL execution failed with non-zero exit code";
|
|
31
35
|
}
|
|
36
|
+
return { success, stack, output: runResult.stdout };
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
catch (e) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.csharpHandler = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const os_1 = require("os");
|
|
8
|
+
const csharpHandler = (code, _snippet, config, _sandbox, _isSharedSandbox) => {
|
|
9
|
+
let success = false;
|
|
10
|
+
let stack = "";
|
|
11
|
+
// C# execution logic
|
|
12
|
+
// Auto-wrap in class Program and Main method if not present
|
|
13
|
+
let csCode = code;
|
|
14
|
+
const className = "Program";
|
|
15
|
+
// Simple heuristic to detect if class is provided
|
|
16
|
+
if (!csCode.includes("class ")) {
|
|
17
|
+
csCode = `using System;
|
|
18
|
+
public class ${className} {
|
|
19
|
+
public static void Main(string[] args) {
|
|
20
|
+
${csCode}
|
|
21
|
+
}
|
|
22
|
+
}`;
|
|
23
|
+
}
|
|
24
|
+
const uniqueId = `${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
25
|
+
const tempDir = (0, path_1.join)((0, os_1.tmpdir)(), `doccident_csharp_${uniqueId}`);
|
|
26
|
+
const timeout = config.timeout || 30000;
|
|
27
|
+
try {
|
|
28
|
+
if (!(0, fs_1.existsSync)(tempDir)) {
|
|
29
|
+
(0, fs_1.mkdirSync)(tempDir);
|
|
30
|
+
}
|
|
31
|
+
const tempSourceFile = (0, path_1.join)(tempDir, `${className}.cs`);
|
|
32
|
+
const tempExeFile = (0, path_1.join)(tempDir, `${className}.exe`);
|
|
33
|
+
(0, fs_1.writeFileSync)(tempSourceFile, csCode);
|
|
34
|
+
// Compile with mcs (Mono Compiler)
|
|
35
|
+
const compileResult = (0, child_process_1.spawnSync)('mcs', ['-out:' + tempExeFile, tempSourceFile], { encoding: 'utf-8', cwd: tempDir, timeout });
|
|
36
|
+
if (compileResult.status !== 0) {
|
|
37
|
+
stack = compileResult.stderr || "C# compilation failed (mcs)";
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Run with mono
|
|
41
|
+
const runResult = (0, child_process_1.spawnSync)('mono', [tempExeFile], { encoding: 'utf-8', cwd: tempDir, timeout });
|
|
42
|
+
if (runResult.error && runResult.error.code === 'ETIMEDOUT') {
|
|
43
|
+
return { success: false, stack: `Execution timed out after ${timeout}ms` };
|
|
44
|
+
}
|
|
45
|
+
if (runResult.status === 0) {
|
|
46
|
+
success = true;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
stack = runResult.stderr || "C# execution failed with non-zero exit code";
|
|
50
|
+
}
|
|
51
|
+
return { success, stack, output: runResult.stdout };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
stack = e.message || "Failed to execute mcs or mono";
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
try {
|
|
59
|
+
if (fs_1.rmSync) {
|
|
60
|
+
(0, fs_1.rmSync)(tempDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(tempDir, `${className}.cs`)))
|
|
64
|
+
(0, fs_1.unlinkSync)((0, path_1.join)(tempDir, `${className}.cs`));
|
|
65
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(tempDir, `${className}.exe`)))
|
|
66
|
+
(0, fs_1.unlinkSync)((0, path_1.join)(tempDir, `${className}.exe`));
|
|
67
|
+
if ((0, fs_1.existsSync)(tempDir))
|
|
68
|
+
(0, fs_1.rmdirSync)(tempDir);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Ignore cleanup error
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { success, stack };
|
|
76
|
+
};
|
|
77
|
+
exports.csharpHandler = csharpHandler;
|