From 8d57da416904059fc192fc159af6945d3d16db07 Mon Sep 17 00:00:00 2001 From: Marcelo Fornet Date: Sun, 31 May 2020 03:15:53 -0400 Subject: [PATCH] Companion config (#96) * Fix #65 Save all config from companion. Unblock #89 * Add better layout distribution for debugging. This is temporary before #40 --- src/companion.ts | 38 ++++++++++++++++++++++++++++++++++---- src/core.ts | 41 ++++++++++++++++++++++++++--------------- src/extension.ts | 16 ++++++++++------ src/primitives.ts | 23 ++++++++++++++++++++++- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/companion.ts b/src/companion.ts index 30706f1..ac23df5 100644 --- a/src/companion.ts +++ b/src/companion.ts @@ -5,6 +5,32 @@ import * as express from "express"; import bodyParser = require("body-parser"); import { debug } from "./utils"; +export class CompanionConfig { + name: string; + group: string; + url: string; + memoryLimit: number; + timeLimit: number; + + constructor(data: any) { + this.name = data.name; + this.group = data.group; + this.url = data.url; + this.memoryLimit = data.memoryLimit; + this.timeLimit = data.timeLimit; + } +} + +export class TestCase { + input: string; + output: string; + + constructor(data: any) { + this.input = data.input; + this.output = data.output; + } +} + export function startCompetitiveCompanionService() { let port = 0; let app = express(); @@ -22,12 +48,17 @@ export function startCompetitiveCompanionService() { app.post("/", async (req: any, res: any) => { const data = req.body; + let companionConfig = new CompanionConfig(data); + + let tests: TestCase[] = data.tests.map((value: any) => { + return new TestCase(value); + }); res.sendStatus(200); - let problmeInContest = newProblemFromCompanion(data); + let problemInContest = newProblemFromCompanion(companionConfig, tests); - let contestPath = problmeInContest.contestPath; - let mainSolution = problmeInContest.problemConfig.mainSolution.unwrapOr( + let contestPath = problemInContest.contestPath; + let mainSolution = problemInContest.problemConfig.mainSolution.unwrapOr( "" ); @@ -47,7 +78,6 @@ export function startCompetitiveCompanionService() { process.exit(1); } - // TODO(#21): Move to logs of the extension for debugging purposes. debug("companion", `Started companion. Listening on port ${port}`); }); } diff --git a/src/core.ts b/src/core.ts index 1555f2b..de57b93 100644 --- a/src/core.ts +++ b/src/core.ts @@ -38,6 +38,7 @@ import { } from "./utils"; import { preRun, run, runWithArgs } from "./runner"; import { copySync } from "fs-extra"; +import { CompanionConfig, TestCase } from "./companion"; /** * Path to static folder. @@ -182,11 +183,17 @@ export function initAcmX(testPath?: string) { // Copy default languages config let languagesFolder = globalLanguagePath(); let languageStaticFolder = join(pathToStatic(), LANGUAGES); - // TODO: Check for each languages, and copy if don't exist. - // Rationale: If we add a new language by default, users that already have this - // file created, won't have the new languages by default. if (!existsSync(languagesFolder)) { copySync(languageStaticFolder, languagesFolder); + } else { + readdirSync(languageStaticFolder).forEach((languageName) => { + if (!existsSync(join(languagesFolder, languageName))) { + copySync( + join(languageStaticFolder, languageName), + join(languagesFolder, languageName) + ); + } + }); } // Create checker folder @@ -472,10 +479,11 @@ export function getSolutionPath() { * Create new problem with configuration from competitive companion. * * @param config Json file with all data received from competitive companion. - * - * TODO: Change type of config(any) to a class with explicit arguments. */ -export function newProblemFromCompanion(config: any) { +export function newProblemFromCompanion( + config: CompanionConfig, + tests: TestCase[] +) { let path = getSolutionPath(); let contestPath = join(path!, config.group); @@ -485,7 +493,7 @@ export function newProblemFromCompanion(config: any) { let inputs: string[] = []; let outputs: string[] = []; - config.tests.forEach(function (testCase: any) { + tests.forEach(function (testCase: TestCase) { inputs.push(testCase.input); outputs.push(testCase.output); }); @@ -498,6 +506,9 @@ export function newProblemFromCompanion(config: any) { false ); + problemConfig.setCompanionConfig(config); + problemConfig.dump(problemPath); + return new ProblemInContest(problemConfig, contestPath); } @@ -621,16 +632,14 @@ export function testSolution(path: string): Option { let results: TestCaseResult[] = []; let fail = Option.none(); + // Try to find time limit from local config first, otherwise use global time limit. + // TODO: Add to wiki about this feature, and how to change custom time limit. + let timeout = config.timeLimit().unwrapOr(getTimeout()); + testcasesId.forEach((tcId) => { // Run on each test case and break on first failing case. if (fail.isNone()) { - let tcResult = timedRun( - path, - tcId, - getTimeout(), - mainSolution, - checker - ); + let tcResult = timedRun(path, tcId, timeout, mainSolution, checker); if (!tcResult.isOk()) { fail = Option.some(new SolutionResult(tcResult.status, tcId)); @@ -866,6 +875,8 @@ export function stressSolution( let results = []; + let timeout = config.timeLimit().unwrapOr(getTimeout()); + for (let index = 0; index < times; index++) { // Generate input test case generateTestCase(path, generator); @@ -900,7 +911,7 @@ export function stressSolution( let result = timedRun( path, GENERATED_TEST_CASE, - getTimeout(), + timeout, mainSolution, checker ); diff --git a/src/extension.ts b/src/extension.ts index 411fb5a..46736ee 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -128,13 +128,17 @@ async function debugTestCase(path: string, tcId: string) { orientation: 0, groups: [ { groups: [{}], size: 0.5 }, - { groups: [{}, [{}, {}]], size: 0.5 }, + { + groups: [{ groups: [{}, {}] }, {}], + size: 0.5, + }, ], }); + let sol = mainSolution(path); let inp = join(path, TESTCASES, `${tcId}.in`); - let out = join(path, TESTCASES, `${tcId}.ans`); - let cur = join(path, TESTCASES, `${tcId}.out`); + let ans = join(path, TESTCASES, `${tcId}.ans`); + let out = join(path, TESTCASES, `${tcId}.out`); await vscode.commands.executeCommand( "vscode.open", @@ -147,10 +151,10 @@ async function debugTestCase(path: string, tcId: string) { vscode.ViewColumn.Two ); // This file might not exist! - if (existsSync(cur)) { + if (existsSync(out)) { await vscode.commands.executeCommand( "vscode.open", - vscode.Uri.file(cur), + vscode.Uri.file(ans), vscode.ViewColumn.Three ); await vscode.commands.executeCommand( @@ -161,7 +165,7 @@ async function debugTestCase(path: string, tcId: string) { } else { await vscode.commands.executeCommand( "vscode.open", - vscode.Uri.file(out), + vscode.Uri.file(ans), vscode.ViewColumn.Four ); } diff --git a/src/primitives.ts b/src/primitives.ts index 68d16ad..0f5a671 100644 --- a/src/primitives.ts +++ b/src/primitives.ts @@ -2,11 +2,12 @@ import { SpawnSyncReturns } from "child_process"; import { join } from "path"; import { writeToFileSync } from "./utils"; import { readFileSync, existsSync } from "fs"; +import { CompanionConfig } from "./companion"; export const ATTIC = "attic"; export const TESTCASES = "testcases"; export const LANGUAGES = "languages"; -// TODO: Allow checker to compile without time limit (this should be added after cancel is available). +// TODO(#68): Allow checker to compile without time limit (this should be added after cancel is available). export const FRIEND_TIMEOUT = 50_000; export const MAIN_SOLUTION_BINARY = "sol"; export const CHECKER_BINARY = "checker"; @@ -230,6 +231,14 @@ export class Option { } } + map(predicate: (arg: T) => R): Option { + if (this.isSome()) { + return Option.some(predicate(this.unwrap())); + } else { + return Option.none(); + } + } + mapOr(value: R, predicate: (arg: T) => R): R { if (this.isSome()) { return predicate(this.unwrap()); @@ -263,6 +272,7 @@ export class ConfigFile { bruteSolution: Option; generator: Option; checker: Option; + companionConfig: Option; constructor( mainSolution?: string, @@ -274,6 +284,11 @@ export class ConfigFile { this.bruteSolution = new Option(bruteSolution); this.generator = new Option(generator); this.checker = new Option(checker); + this.companionConfig = Option.none(); + } + + setCompanionConfig(companionConfig: CompanionConfig) { + this.companionConfig = Option.some(companionConfig); } /** @@ -349,6 +364,8 @@ export class ConfigFile { parsed.checker?.value ); + config.setCompanionConfig(parsed.companionConfig?.value); + if (config.verify()) { config.dump(path); } @@ -362,6 +379,10 @@ export class ConfigFile { static empty(): ConfigFile { return new ConfigFile(); } + + timeLimit(): Option { + return this.companionConfig.map((config) => config.timeLimit); + } } export class ProblemInContest {