@jpetit/toolkit 3.1.1 → 3.1.2

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/lib/maker.ts ADDED
@@ -0,0 +1,613 @@
1
+ import chalk from 'chalk'
2
+ import { execa } from 'execa'
3
+ import { cp, exists, glob, mkdir, rename } from 'fs/promises'
4
+ import { imageSizeFromFile } from 'image-size/fromFile'
5
+ import { basename, dirname, join, normalize, resolve } from 'path'
6
+ import prettyBytes from 'pretty-bytes'
7
+ import prettyMs from 'pretty-ms'
8
+ import tui from '../lib/tui'
9
+ import {
10
+ filesAreEqual,
11
+ fileSize,
12
+ humanid,
13
+ isDirectory,
14
+ isDirectoryInDir,
15
+ nothing,
16
+ projectDir,
17
+ readText,
18
+ toolkitPrefix,
19
+ writeText,
20
+ } from '../lib/utils'
21
+ import { getCompilerByExtension, getCompilerById } from './compilers'
22
+ import type { Compiler } from './compilers/base'
23
+ import { languageNames } from './data'
24
+ import { newInspector, Inspector } from './inspector'
25
+
26
+ export async function newMaker(directory: string): Promise<Maker> {
27
+ const inspector = await newInspector(directory)
28
+ return new Maker(inspector)
29
+ }
30
+
31
+ type ExecutionResult = {
32
+ testcase: string
33
+ time: number
34
+ inputSize: number
35
+ outputSize: number
36
+ error: boolean
37
+ }
38
+
39
+ export class Maker {
40
+ inspector: Inspector
41
+
42
+ public constructor(inspector: Inspector) {
43
+ this.inspector = inspector
44
+ }
45
+
46
+ async showDirectory() {
47
+ await tui.section('Directory', async () => {
48
+ await nothing()
49
+ tui.directory(resolve(this.inspector.directory))
50
+ const fullPath = normalize(resolve(this.inspector.directory))
51
+ if (!fullPath.endsWith('.pbm')) {
52
+ const lastPath = basename(fullPath)
53
+ const butLastPath = dirname(fullPath)
54
+
55
+ if (!Object.keys(languageNames).includes(lastPath) || !butLastPath.endsWith('.pbm')) {
56
+ throw new Error(
57
+ 'The problem directory should end with .pbm or be a language id inside a .pbm directory',
58
+ )
59
+ }
60
+ }
61
+ })
62
+ }
63
+
64
+ public async makeProblem() {
65
+ await this.makeGoldenExecutable()
66
+ await this.makeCorrects()
67
+ await this.checkSolutions()
68
+ await this.makePdfs()
69
+ await this.makeTexts()
70
+ }
71
+
72
+ public async makeExecutable(program: string) {
73
+ const compiler = this.selectCompiler()
74
+ const newProgram = `${toolkitPrefix()}-${program}`
75
+ await tui.section(
76
+ `Copying ${tui.hyperlink(this.inspector.directory, program)} to ${tui.hyperlink(this.inspector.directory, newProgram)}`,
77
+ async () => {
78
+ await cp(join(this.inspector.directory, program), join(this.inspector.directory, newProgram))
79
+ },
80
+ )
81
+ await tui.section(
82
+ `Compiling ${tui.hyperlink(this.inspector.directory, newProgram)} with ${compiler.name()} (${this.inspector.handler.source_modifier})`,
83
+ async () => {
84
+ try {
85
+ let outputPath: string
86
+ if (this.inspector.handler.source_modifier === 'none') {
87
+ outputPath = await compiler.compileNormal(
88
+ this.inspector.handler,
89
+ this.inspector.directory,
90
+ newProgram,
91
+ )
92
+ } else if (this.inspector.handler.source_modifier === 'no_main') {
93
+ outputPath = await compiler.compileWithMain(
94
+ this.inspector.handler,
95
+ this.inspector.directory,
96
+ newProgram,
97
+ )
98
+ } else {
99
+ throw new Error(`Unknown source modifier: ${this.inspector.handler.source_modifier as string}`)
100
+ }
101
+ if (!(await exists(join(this.inspector.directory, outputPath)))) {
102
+ throw new Error(`Compilation failed for ${newProgram}`)
103
+ }
104
+ tui.success(`Compiled ${newProgram} to ${outputPath}`)
105
+ } catch (error) {
106
+ throw new Error(`Compilation failed`)
107
+ }
108
+ },
109
+ )
110
+ }
111
+
112
+ public async makeExecutables() {
113
+ await tui.section('Compiling solutions', async () => {
114
+ for (const solution of this.inspector.solutions) {
115
+ await this.makeExecutable(solution)
116
+ }
117
+ })
118
+ }
119
+
120
+ public async makeGoldenExecutable() {
121
+ await tui.section(
122
+ `Compiling golden solution from ${tui.hyperlink(this.inspector.directory, this.inspector.goldenSolution!)}`,
123
+ async () => {
124
+ if (!this.inspector.goldenSolution) {
125
+ throw new Error('Golden solution not set')
126
+ }
127
+ await this.makeExecutable(this.inspector.goldenSolution)
128
+ },
129
+ )
130
+ }
131
+
132
+ public async checkSolutions() {
133
+ for (const solution of this.inspector.solutions) {
134
+ if (solution !== this.inspector.goldenSolution) {
135
+ await this.checkCandidate(solution)
136
+ }
137
+ }
138
+ }
139
+
140
+ async makeCorrect(testcase: string, compiler: Compiler, sourcePath: string): Promise<ExecutionResult> {
141
+ return await this.runTestcase(
142
+ testcase,
143
+ `${testcase}.inp`,
144
+ `${testcase}.cor`,
145
+ compiler,
146
+ toolkitPrefix() + '-' + sourcePath,
147
+ )
148
+ }
149
+
150
+ async runTestcase(
151
+ testcase: string,
152
+ input: string,
153
+ output: string,
154
+ compiler: Compiler,
155
+ sourcePath: string,
156
+ ): Promise<ExecutionResult> {
157
+ let error = false
158
+ const start = Date.now()
159
+ try {
160
+ await compiler.execute(this.inspector.handler, this.inspector.directory, sourcePath, input, output)
161
+ } catch (e) {
162
+ tui.error(`Execution failed for testcase '${testcase}'`)
163
+ error = true
164
+ }
165
+ const end = Date.now()
166
+ const time = end - start
167
+
168
+ if (this.inspector.handler.handler === 'graphic') {
169
+ await rename(join(this.inspector.directory, 'output.png'), join(this.inspector.directory, output))
170
+ }
171
+
172
+ const inputSize = await fileSize(join(this.inspector.directory, input))
173
+ const outputSize = await fileSize(join(this.inspector.directory, output))
174
+
175
+ return { testcase, error, time, inputSize, outputSize }
176
+ }
177
+
178
+ selectCompiler(): Compiler {
179
+ if (this.inspector.handler.compilers === 'RunPython') {
180
+ return getCompilerById('RunPython')
181
+ } else if (this.inspector.handler.compilers === 'RunHaskell') {
182
+ return getCompilerById('RunHaskell')
183
+ } else if (this.inspector.handler.compilers === 'RunClojure') {
184
+ return getCompilerById('RunClojure')
185
+ } else {
186
+ const extension = this.inspector.goldenSolution!.split('.').pop()!
187
+ return getCompilerByExtension(extension)
188
+ }
189
+ }
190
+
191
+ public async makeCorrects() {
192
+ const compiler = this.selectCompiler()
193
+ await tui.section(`Making correct outputs with golden solution`, async () => {
194
+ await tui.section(`Executing testcases using ${compiler.name()}`, async () => {
195
+ const results: ExecutionResult[] = []
196
+
197
+ for (const testcase of this.inspector.testcases) {
198
+ results.push(await this.makeCorrect(testcase, compiler, this.inspector.goldenSolution!))
199
+ }
200
+
201
+ console.log()
202
+ console.log(`testcase time input output`)
203
+ for (const result of results) {
204
+ const time = prettyMs(result.time)
205
+ const inputSize = prettyBytes(result.inputSize).replace(' ', '')
206
+ const outputSize = prettyBytes(result.outputSize).replace(' ', '')
207
+ console.log(
208
+ (result.error ? chalk.red : chalk.green)(
209
+ `${result.testcase.padEnd(12)} ${time.padStart(10)} ${inputSize.padStart(10)} ${outputSize.padStart(
210
+ 10,
211
+ )}`,
212
+ ),
213
+ )
214
+ }
215
+
216
+ const errors = results.filter((result) => result.error).length
217
+ if (errors > 0) {
218
+ console.log()
219
+ throw new Error(`${errors} errors occurred while making correct answers`)
220
+ }
221
+ })
222
+ })
223
+ }
224
+
225
+ public async makePdfs() {
226
+ await tui.section('Making PDF statements', async () => {
227
+ const tmpDirBase = join(this.inspector.directory, toolkitPrefix() + '-pdf', humanid())
228
+ await mkdir(tmpDirBase, { recursive: true })
229
+ await tui.section('Creating working directory', async () => {
230
+ await nothing()
231
+ tui.directory(tmpDirBase)
232
+ })
233
+ for (const language of this.inspector.languages) {
234
+ if (
235
+ this.inspector.structure === 'multi' ||
236
+ (this.inspector.structure === 'single' && language === this.inspector.language)
237
+ ) {
238
+ await tui.section(`Making PDF statement for ${languageNames[language]}`, async () => {
239
+ await this.makePdf(tmpDirBase, language)
240
+ })
241
+ }
242
+ }
243
+ })
244
+ }
245
+
246
+ async makeSamples(tmpDir: string, language: string): Promise<[string, string]> {
247
+ const graphic = this.inspector.handler.handler === 'graphic'
248
+ const samples1col: string[] = []
249
+ const samples2col: string[] = []
250
+ let index = 1
251
+ for (const testcase of this.inspector.testcases) {
252
+ if (testcase.startsWith('sample')) {
253
+ let size = ''
254
+ if (graphic) {
255
+ await cp(join(this.inspector.directory, `${testcase}.cor`), join(tmpDir, `${testcase}.cor.png`))
256
+ const dimensions = await imageSizeFromFile(join(tmpDir, `${testcase}.cor.png`))
257
+ size = `(${dimensions.width}$\\times$${dimensions.height})`
258
+ }
259
+ samples1col.push(`\n\\SampleOneColInputOutput[${size}]{${testcase}}{${index}}\n`)
260
+ samples2col.push(`\n\\SampleTwoColInputOutput[${size}]{${testcase}}{${index}}\n`)
261
+ index++
262
+ }
263
+ }
264
+ return [samples1col.join('\n'), samples2col.join('\n')]
265
+ }
266
+
267
+ async makePdf(tmpDirBase: string, language: string) {
268
+ const tmpDir = join(tmpDirBase, language)
269
+ await mkdir(tmpDir)
270
+
271
+ const date = new Date().toISOString().substring(0, 10)
272
+ const year = new Date().getFullYear()
273
+ const author = this.inspector.problemLangYmls[language].author || 'Unknown'
274
+ const authorEmail = this.inspector.problemLangYmls[language].author_email || 'unknown email'
275
+ const translator = this.inspector.problemLangYmls[language].translator || ''
276
+
277
+ const [samples1c, samples2c] = await this.makeSamples(tmpDir, language)
278
+
279
+ const root = `
280
+
281
+ \\documentclass[11pt]{article}
282
+
283
+ \\usepackage{judgeit}
284
+ \\usepackage{judgeit.${language}}
285
+
286
+ % TODO?
287
+ \\lstMakeShortInline@
288
+
289
+ \\begin{document}
290
+
291
+ \\providecommand{\\SampleOneCol}{${samples1c}}
292
+ \\providecommand{\\SampleTwoCol}{${samples2c}}
293
+ \\ProblemId{{DRAFT ${language}}}
294
+ \\DoProblem{${language}}
295
+
296
+ \\ProblemInformation
297
+ \\Author: ${author}\\\\ ${translator ? `(${language}: ${translator})` : ''}
298
+ \\Generation: ${date}
299
+
300
+ \\subsection*{Draft}
301
+ Draft generated with \\textbf{new-jutge-toolkit}.
302
+
303
+ \\end{document}
304
+
305
+ `
306
+
307
+ // copy files to tmpDir
308
+ // TODO: only copy needed files
309
+ for await (const entry of glob('*', { cwd: this.inspector.directory })) {
310
+ if (
311
+ entry.startsWith(toolkitPrefix()) ||
312
+ entry.endsWith('.exe') ||
313
+ entry.endsWith('.html') ||
314
+ entry.endsWith('.md') ||
315
+ entry.endsWith('.txt') ||
316
+ (await isDirectoryInDir(this.inspector.directory, entry))
317
+ ) {
318
+ continue
319
+ }
320
+ await cp(join(this.inspector.directory, entry), join(tmpDir, entry))
321
+ }
322
+
323
+ // write root.tex
324
+ await writeText(join(tmpDir, 'root.tex'), root)
325
+
326
+ // tweak the tex file
327
+ const tex1 = await readText(join(tmpDir, `problem.${language}.tex`))
328
+ const tex2 = tex1
329
+ .replace(/\\begin{htmlonly}[\s\S]*?\\end{htmlonly}/g, '')
330
+ .replace(/\\begin{latexonly}/g, '')
331
+ .replace(/\\end{latexonly}/g, '')
332
+ .replace(/\.eps}/g, '}')
333
+ await writeText(join(tmpDir, `problem.${language}.tex`), tex2)
334
+
335
+ // copy style files
336
+ await this.copyStyleFiles(tmpDir, language)
337
+
338
+ // latex
339
+ try {
340
+ tui.command('xelatex -interaction=nonstopmode -file-line-error root.tex')
341
+ await execa({
342
+ stderr: 'inherit',
343
+ // stdout: 'inherit',
344
+ cwd: tmpDir,
345
+ })`xelatex -interaction=nonstopmode -file-line-error root.tex`
346
+ await cp(join(tmpDir, 'root.pdf'), join(this.inspector.directory, `problem.${language}.pdf`))
347
+ tui.success(
348
+ `Generated ${tui.hyperlink(this.inspector.directory, `problem.${language}.pdf`)} see ${tui.hyperlink(tmpDir, `root.log`)}`,
349
+ )
350
+ } catch (e) {
351
+ tui.error(`Error in LaTeX: ${tui.hyperlink(tmpDir, `root.log`)}`)
352
+ }
353
+ }
354
+
355
+ async copyStyleFiles(tmpDir: string, language: string) {
356
+ const cpSty = async (path: string) => {
357
+ const source = join(projectDir(), 'assets', 'sty', path)
358
+ await cp(source, join(tmpDir, path))
359
+ }
360
+ await cpSty('picins.sty')
361
+ await cpSty('judgeit.sty')
362
+ if (language === 'ca') await cpSty('judgeit.ca.sty')
363
+ if (language === 'es') await cpSty('judgeit.es.sty')
364
+ if (language === 'en') await cpSty('judgeit.en.sty')
365
+ if (language === 'fr') await cpSty('judgeit.fr.sty')
366
+ if (language === 'de') await cpSty('judgeit.de.sty')
367
+ }
368
+
369
+ public async makeTexts() {
370
+ await tui.section('Making text statements (.{md,txt,html})', async () => {
371
+ const tmpDirBase = join(this.inspector.directory, toolkitPrefix() + '-text', humanid())
372
+ await mkdir(tmpDirBase, { recursive: true })
373
+ await tui.section('Creating working directory', async () => {
374
+ await nothing()
375
+ tui.directory(tmpDirBase)
376
+ })
377
+ try {
378
+ for (const language of this.inspector.languages) {
379
+ if (
380
+ this.inspector.structure === 'multi' ||
381
+ (this.inspector.structure === 'single' && language === this.inspector.language)
382
+ ) {
383
+ await tui.section(`Making text statements for ${languageNames[language]}`, async () => {
384
+ await this.makeText(tmpDirBase, language)
385
+ })
386
+ }
387
+ }
388
+ } finally {
389
+ // do not clean up because of hyperlinks
390
+ }
391
+ })
392
+ }
393
+
394
+ async makeText(tmpDirBase: string, language: string) {
395
+ const tmpDir = join(tmpDirBase, language)
396
+ await mkdir(tmpDir)
397
+
398
+ const date = new Date().toISOString().substring(0, 10)
399
+ const year = new Date().getFullYear()
400
+ const author = this.inspector.problemLangYmls[language].author || 'Unknown'
401
+ const authorEmail = this.inspector.problemLangYmls[language].author_email || 'unknown email'
402
+ const translator = this.inspector.problemLangYmls[language].translator || ''
403
+
404
+ const root = `
405
+
406
+ \\documentclass[11pt]{article}
407
+
408
+ \\usepackage{judgeit}
409
+ \\usepackage{judgeit.${language}}
410
+
411
+ % TODO?
412
+ \\lstMakeShortInline@
413
+
414
+ % redefine commands to simplify text output
415
+ \\renewcommand{\\Sample}{}
416
+ \\renewcommand{\\Statement}{}
417
+ \\renewcommand{\\Problem}[1]{\\section{#1}}
418
+ \\renewcommand{\\ProblemId}[1]{DRAFT ${language}}
419
+
420
+ % redefine figure commands to avoid troubles with \\parpic
421
+ \\renewcommand{\\FigureL}[2]{\\includegraphics[#1]{#2}}
422
+ \\renewcommand{\\FigureC}[2]{\\includegraphics[#1]{#2}}
423
+ \\renewcommand{\\FigureR}[2]{\\includegraphics[#1]{#2}}
424
+
425
+ \\begin{document}
426
+
427
+ \\DoProblem{${language}}
428
+
429
+ \\subsection*{\\TxtAuthor}
430
+ ${author} ${translator ? `(${language}: ${translator})` : ''}
431
+
432
+ \\subsection*{Draft}
433
+ Draft generated with \\textbf{new-jutge-toolkit}.
434
+
435
+ \\end{document}
436
+
437
+ `
438
+
439
+ // copy files to tmpDir
440
+ // TODO: only copy needed files
441
+ for await (const entry of glob('*', { cwd: this.inspector.directory })) {
442
+ if (
443
+ entry.startsWith(toolkitPrefix()) ||
444
+ entry.endsWith('.pdf') ||
445
+ entry.endsWith('.exe') ||
446
+ entry.endsWith('.html') ||
447
+ entry.endsWith('.md') ||
448
+ entry.endsWith('.txt') ||
449
+ (await isDirectoryInDir(this.inspector.directory, entry))
450
+ ) {
451
+ continue
452
+ }
453
+ await cp(join(this.inspector.directory, entry), join(tmpDir, entry))
454
+ }
455
+
456
+ // write root.tex
457
+ await writeText(join(tmpDir, 'root.tex'), root)
458
+
459
+ // tweak the tex file
460
+ const tex1 = await readText(join(tmpDir, `problem.${language}.tex`))
461
+ const tex2 = tex1
462
+ .replace(/\\begin{latexonly}[\s\S]*?\\end{latexonly}/g, '')
463
+ .replace(/\\begin{htmlonly}/g, '')
464
+ .replace(/\\end{htmlonly}/g, '')
465
+ .replace(/\\begin{minipage}/g, '')
466
+ .replace(/\\end{minipage}/g, '')
467
+ .replace(/\.eps}/g, '}')
468
+ await writeText(join(tmpDir, `problem.${language}.tex`), tex2)
469
+
470
+ // copy style files
471
+ await this.copyStyleFiles(tmpDir, language)
472
+
473
+ // convert .eps files to png
474
+ const files = (await Array.fromAsync(glob('*.eps', { cwd: tmpDir }))).sort()
475
+ for (const file of files) {
476
+ tui.command(`convert ${file} ${file.replace(/\.eps$/, '.png')}`)
477
+ await execa({ cwd: tmpDir })`convert ${file} ${file.replace(/\.eps$/, '.png')}`
478
+ }
479
+
480
+ // create lua files
481
+ const luaSource = join(projectDir(), 'assets', 'lua', 'fixCodeBlocks.lua')
482
+ await cp(luaSource, join(tmpDir, 'fixCodeBlocks.lua'))
483
+
484
+ // txt
485
+ try {
486
+ tui.command('pandoc --quiet root.tex --to plain --output root.txt')
487
+ await execa({ cwd: tmpDir })`pandoc --quiet root.tex --to plain --output root.txt`
488
+ await cp(join(tmpDir, 'root.txt'), join(this.inspector.directory, `problem.${language}.txt`))
489
+ tui.success('Generated ' + tui.hyperlink(this.inspector.directory, `problem.${language}.txt`))
490
+ } catch (e) {
491
+ console.error('pandoc error', e)
492
+ }
493
+
494
+ // md
495
+ try {
496
+ tui.command(
497
+ 'pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md',
498
+ )
499
+ await execa({
500
+ cwd: tmpDir,
501
+ })`pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md`
502
+ await cp(join(tmpDir, 'root.md'), join(this.inspector.directory, `problem.${language}.md`))
503
+ tui.success('Generated ' + tui.hyperlink(this.inspector.directory, `problem.${language}.md`))
504
+ } catch (e) {
505
+ console.error('pandoc error', e)
506
+ }
507
+
508
+ // html
509
+ try {
510
+ tui.command('pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html')
511
+ await execa({
512
+ cwd: tmpDir,
513
+ })`pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html`
514
+ await cp(join(tmpDir, 'root.html'), join(this.inspector.directory, `problem.${language}.html`))
515
+ tui.success('Generated ' + tui.hyperlink(this.inspector.directory, `problem.${language}.html`))
516
+ } catch (e) {
517
+ console.error('pandoc error', e)
518
+ }
519
+ }
520
+
521
+ public async checkCandidate(program: string) {
522
+ await tui.section(`Checking ${program}`, async () => {
523
+ const extension = program.split('.').pop()!
524
+ const compiler = getCompilerByExtension(extension)
525
+ const newProgram = `${toolkitPrefix()}-${program}`
526
+
527
+ await tui.section(
528
+ `Copying ${tui.hyperlink(this.inspector.directory, program)} to ${tui.hyperlink(this.inspector.directory, newProgram)}`,
529
+ async () => {
530
+ await cp(join(this.inspector.directory, program), join(this.inspector.directory, newProgram))
531
+ },
532
+ )
533
+
534
+ await tui.section(
535
+ `Using compiler ${compiler.name()} to compile ${tui.hyperlink(this.inspector.directory, newProgram)}`,
536
+ async () => {
537
+ try {
538
+ let outputPath: string
539
+ if (this.inspector.handler.source_modifier === 'none') {
540
+ outputPath = await compiler.compileNormal(
541
+ this.inspector.handler,
542
+ this.inspector.directory,
543
+ newProgram,
544
+ )
545
+ } else if (this.inspector.handler.source_modifier === 'no_main') {
546
+ outputPath = await compiler.compileWithMain(
547
+ this.inspector.handler,
548
+ this.inspector.directory,
549
+ newProgram,
550
+ )
551
+ } else {
552
+ throw new Error(
553
+ `Unknown source modifier: ${this.inspector.handler.source_modifier as string}`,
554
+ )
555
+ }
556
+ if (!(await exists(join(this.inspector.directory, outputPath)))) {
557
+ throw new Error(`Compilation failed for ${newProgram}`)
558
+ }
559
+ tui.success(`Compiled ${newProgram} to ${outputPath}`)
560
+ } catch (error) {
561
+ throw new Error(`Compilation failed`)
562
+ }
563
+ },
564
+ )
565
+
566
+ const results: ExecutionResult[] = []
567
+ await tui.section('Executing testcases', async () => {
568
+ for (const testcase of this.inspector.testcases) {
569
+ results.push(
570
+ await this.runTestcase(
571
+ testcase,
572
+ `${testcase}.inp`,
573
+ `${toolkitPrefix()}-${testcase}.${extension}.out`,
574
+ compiler,
575
+ newProgram,
576
+ ),
577
+ )
578
+ }
579
+
580
+ console.log()
581
+ console.log(`testcase time status`)
582
+ let errors = 0
583
+ for (const result of results) {
584
+ const status = result.error
585
+ ? 'EE'
586
+ : (await filesAreEqual(
587
+ join(this.inspector.directory, `${result.testcase}.cor`),
588
+ join(
589
+ this.inspector.directory,
590
+ `${toolkitPrefix()}-${result.testcase}.${extension}.out`,
591
+ ),
592
+ ))
593
+ ? 'OK'
594
+ : 'WA'
595
+ const time = prettyMs(result.time)
596
+ console.log(
597
+ (status !== 'OK' ? chalk.red : chalk.green)(
598
+ `${result.testcase.padEnd(12)} ${time.padStart(10)} ${status.padStart(10)}`,
599
+ ),
600
+ )
601
+ if (status !== 'OK') errors++
602
+ }
603
+ console.log()
604
+
605
+ if (errors) {
606
+ tui.error(`${errors} errors found for ${program}`)
607
+ } else {
608
+ tui.success(`All testcases passed successfully for ${program}`)
609
+ }
610
+ })
611
+ })
612
+ }
613
+ }
@@ -0,0 +1,51 @@
1
+ import envPaths from 'env-paths'
2
+ import { exists, mkdir } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { guessUserEmail, guessUserName, readYaml, writeYaml } from './utils'
5
+ import { Settings } from './types'
6
+
7
+ export const paths = envPaths('jutge', { suffix: 'toolkit' })
8
+
9
+ export function configPath() {
10
+ return join(paths.config, 'config.yml')
11
+ }
12
+
13
+ // console.log(`Configuration path: ${configPath()}`)
14
+
15
+ export async function initializePaths() {
16
+ await mkdir(paths.config, { recursive: true })
17
+ await mkdir(paths.data, { recursive: true })
18
+ await mkdir(paths.cache, { recursive: true })
19
+ await mkdir(paths.log, { recursive: true })
20
+ await mkdir(paths.temp, { recursive: true })
21
+ }
22
+
23
+ export async function saveSettings(settings: Settings) {
24
+ await initializePaths()
25
+ settings = Settings.parse(settings)
26
+ return await writeYaml(configPath(), settings)
27
+ }
28
+
29
+ export async function loadSettings(): Promise<Settings> {
30
+ const data = await readYaml(configPath())
31
+ return Settings.parse(data)
32
+ }
33
+
34
+ export async function loadSettingsAtStart(): Promise<Settings> {
35
+ if (!(await settingsExist())) {
36
+ const defaultSettings = Settings.parse({})
37
+ defaultSettings.name = (await guessUserName()) || 'John Doe'
38
+ defaultSettings.email = (await guessUserEmail()) || 'john.doe@example.com'
39
+ await saveSettings(defaultSettings)
40
+ return defaultSettings
41
+ } else {
42
+ return await loadSettings()
43
+ }
44
+ }
45
+
46
+ export async function settingsExist(): Promise<boolean> {
47
+ return await exists(configPath())
48
+ }
49
+
50
+ // settings loaded at the start of the program
51
+ export const settings = await loadSettingsAtStart()