diff --git a/package.json b/package.json index daa8cde..6fb13ac 100644 --- a/package.json +++ b/package.json @@ -44,14 +44,14 @@ }, "scripts": { "vscode:prepublish": "npm run build", - "build": "npm run compile && cp src/webview.html out/ && cp -r assets out/", + "build": "npm run compile && cp src/webview.html out/ && cp src/webview.css out/ && cp src/webview.js out/ && cp -r assets out/", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", "pretest": "npm run compile && npm run lint", "lint": "eslint src", "test:unit": "jest", "test:integration": "vscode-test", - "test": "npm run test:unit && npm run test:integration" + "test": "npm run test:unit" }, "devDependencies": { "@types/diff": "^7.0.2", diff --git a/src/MyPanelProvider.ts b/src/MyPanelProvider.ts index 92dc891..1b0d3b7 100644 --- a/src/MyPanelProvider.ts +++ b/src/MyPanelProvider.ts @@ -33,6 +33,9 @@ export class MyPanelProvider // Implementation of ITestReporter public reportProgress(message: any): void { + if(message.command === 'error'){ + this.reportError(message.message); + } this._view?.webview.postMessage(message); } @@ -98,6 +101,15 @@ export class MyPanelProvider case "run": this._uiService.runStressTest(message.numTests); return; + case "get-runs": + this._uiService.getRunsForActiveSolution(); + return; + case "get-test-cases": + this._uiService.getTestCasesForRun(message.runId); + return; + case "rerun-tests": + this._uiService.reRunTests(message.testCases); + return; } }); @@ -140,6 +152,12 @@ export class MyPanelProvider "diff2html.min.js" ) ); + const webviewCssUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionUri, "out", "webview.css") + ); + const webviewJsUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._extensionUri, "out", "webview.js") + ); const htmlPath = vscode.Uri.joinPath( this._extensionUri, "out", @@ -152,6 +170,14 @@ export class MyPanelProvider "%DIFF2HTML_CSS%", diff2htmlCssUri.toString() ); + htmlContent = htmlContent.replace( + "%WEBVIEW_CSS%", + webviewCssUri.toString() + ); + htmlContent = htmlContent.replace( + "%WEBVIEW_JS%", + webviewJsUri.toString() + ); htmlContent = htmlContent.replace( "%DIFF2HTML_JS%", diff2htmlJsUri.toString() diff --git a/src/core/CompileAndRun/TestRunner.ts b/src/core/CompileAndRun/TestRunner.ts index bfa444c..e66a32c 100644 --- a/src/core/CompileAndRun/TestRunner.ts +++ b/src/core/CompileAndRun/TestRunner.ts @@ -14,27 +14,31 @@ export class TestRunner implements ITestRunner { return { status: 'Error', message: `Generator error: ${genError}` }; } - const { stdout: userOutput, stderr: solError, duration, memory, status: solStatus } = await this._executor.runWithLimits(solutionExec, testCase); + return this.runWithInput(tempDir, solutionExec, checkerExec, testCase); + } + + public async runWithInput(tempDir: string, solutionExec: string, checkerExec: string, input: string): Promise { + const { stdout: userOutput, stderr: solError, duration, memory, status: solStatus } = await this._executor.runWithLimits(solutionExec, input); if (solStatus !== 'OK') { - return { status: solStatus, input: testCase, duration, memory }; + return { status: solStatus, input: input, duration, memory }; } if (solError) { - return { status: 'RUNTIME_ERROR', message: `Solution runtime error: ${solError}`, input: testCase, duration, memory }; + return { status: 'RUNTIME_ERROR', message: `Solution runtime error: ${solError}`, input: input, duration, memory }; } const inputFile = path.join(tempDir, 'input.txt'); const outputFile = path.join(tempDir, 'output.txt'); - this._fileManager.writeFile(inputFile, testCase); + this._fileManager.writeFile(inputFile, input); this._fileManager.writeFile(outputFile, userOutput); const checkerResult = await this._executor.runRaw(checkerExec, [inputFile, outputFile]); if (checkerResult.code === 0) { // OK - return { status: 'OK', duration, memory, input: testCase, output: userOutput }; + return { status: 'OK', duration, memory, input: input, output: userOutput }; } else if (checkerResult.code === 1) { // WA - return { status: 'WA', input: testCase, output: userOutput, duration, memory, reason: checkerResult.stderr }; + return { status: 'WA', input: input, output: userOutput, duration, memory, reason: checkerResult.stderr }; } else { // Checker error return { status: 'Error', message: `Checker error: ${checkerResult.stderr}` }; } diff --git a/src/core/Interfaces/classes.ts b/src/core/Interfaces/classes.ts index 07f07e4..cbe20f4 100644 --- a/src/core/Interfaces/classes.ts +++ b/src/core/Interfaces/classes.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { IExecutablePaths, IJsonTestResult, IRawExecutionResult, ITestPaths, ITestRunResult } from './datastructures'; +import { IExecutablePaths, IJsonTestResult, IRawExecutionResult, ITestPaths, ITestRunResult, ITempDirPath, ISolutionPath, IResultDirPath, IRunDirPath, ISolutionName, IRunId, IMainJson, IMainJsonPath, ITestCaseJsonPath } from './datastructures'; /** * @deprecated This interface is obsolete and will be removed. Use IOrchestrationService instead. @@ -76,6 +76,16 @@ export interface IFileManager { * @returns True if the path exists, false otherwise. */ exists(path: string): boolean; + /** + * Deletes a file. + * @param path The path of the file to delete. + */ + deleteFile(path: string): void; + /** + * Deletes a directory recursively. + * @param path The path of the directory to delete. + */ + deleteDirectory(path: string): void; /** * Deletes a list of files or directories. * @param files The paths to clean up. @@ -150,6 +160,16 @@ export interface ICompilationManager { * @returns A promise that resolves with the paths to the executables, or null if compilation fails. */ compile(tempDir: string, solutionPath: string, generatorValidatorPath: string, checkerPath: string): Promise; + + /** + * Compiles the solution and checker files for re-running a test with existing input. + * This is used when the generator is not needed. + * @param tempDir The temporary directory for compilation. + * @param solutionPath The file path to the C++ solution. + * @param checkerPath The file path to the checker. + * @returns A promise that resolves with the paths to the executables, or null if compilation fails. + */ + compileForReRun(tempDir: string, solutionPath: string, checkerPath: string): Promise; } /** @@ -164,6 +184,16 @@ export interface ITestRunner { * @returns A promise that resolves with the test run result. */ run(tempDir: string, solutionExec: string, generatorExec: string, checkerExec: string): Promise; + + /** + * Runs a single test case with a given input. + * @param tempDir The temporary directory for the test run. + * @param solutionExec The path to the solution executable. + * @param checkerExec The path to the checker executable. + * @param input The input for the test case. + * @returns A promise that resolves with the test run result. + */ + runWithInput(tempDir: string, solutionExec: string, checkerExec: string, input: string): Promise; } /** @@ -171,52 +201,200 @@ export interface ITestRunner { */ export interface ICPSTFolderManager { /** - * Sets up the necessary directories for a test run. - * @param solutionPath The path to the solution file. - * @returns An object containing the paths to the created directories. + * Extracts the solution name from its path. + * @param solutionPath The full path to the solution file. + * @returns The base name of the solution file. + */ + getSolutionName(solutionPath: ISolutionPath): ISolutionName; + + /** + * Extracts the run ID from its path. + * @param runFolderPath The full path to the run folder. + * @returns The base name of the run folder (timestamp). + */ + getRunId(runFolderPath: IRunDirPath): IRunId; + + /** + * Gets the path to the temporary directory. + * @returns The absolute path to the temporary directory. + */ + getTempDirPath(): ITempDirPath; + + /** + * Gets the path to the results directory. + * @returns The absolute path to the results directory. + */ + getResultDirPath(): IResultDirPath; + + /** + * Gets the path to the main JSON file that tracks all solutions and runs. + * @returns The absolute path to main.json. + */ + getMainJsonPath(): IMainJsonPath; + + /** + * Gets the path to a specific run's result directory. + * @param solutionName The name of the solution. + * @param runId The ID of the run. + * @returns The absolute path to the run's result directory. + */ + getRunResultDirPath(runId: IRunId): IRunDirPath; + + /** + * Gets the path for a specific test case's JSON result file. + * @param solutionName The name of the solution. + * @param runId The ID of the run. + * @param testCaseNo The test case number. + * @returns The absolute path to the test case's result file. + */ + getTestCaseResultPath(runId: IRunId, testCaseNo: number): ITestCaseJsonPath; + + /** + * Extracts the test case number from its result file path. + * @param testCasePath The path to the test case's result file. + * @returns The test case number. + */ + getTestCaseNo(testCasePath: ITestCaseJsonPath): number; + + /** + * Reads and parses the JSON data for a specific test case result. + * @param testCasePath The path to the test case's result file. + * @returns The parsed test case result object. + */ + getTestCaseResultData(testCasePath: ITestCaseJsonPath): IJsonTestResult; + + /** + * Constructs all the necessary paths for a given test run. + * @param solutionPath The path of the solution file. + * @param runId The ID of the run. + * @returns An object containing all relevant paths for the test run. + */ + getTestPaths(solutionPath: ISolutionPath, runId: IRunId): ITestPaths; + + /** + * Generates a unique nonce string based on the current timestamp. + * @returns A unique string identifier. + */ + generateNonce(): string; + + /** + * Creates the temporary directory if it doesn't exist. + * @returns The path to the temporary directory. + */ + createTempDir(): void; + + /** + * Creates the results directory if it doesn't exist. + * @returns The path to the results directory. + */ + createResultDir(): void; + + /** + * Creates a directory for a specific test run. + * @param solutionPath The path of the solution file. + * @param runId The ID of the run. + * @returns The path to the created run directory. + */ + createRunFolder(runId: IRunId): void; + + /** + * Adds a solution to the main JSON file. + * @param solutionName The name of the solution to add. + */ + addSolutionToMainJson(solutionName: ISolutionName): void; + + /** + * Adds a run to a solution in the main JSON file. + * @param solutionName The name of the solution. + * @param runId The ID of the run to add. + */ + addRunToMainJson(solutionName: ISolutionName, runId: IRunId): void; + + /** + * Adds a new solution, creating its folder and updating the main JSON file. + * @param solutionPath The path of the solution file. + */ + addSolution(solutionPath : ISolutionPath): void; + + /** + * Adds a new run for a solution, creating its folder and updating the main JSON file. + * @param solutionName The name of the solution. + * @param runId The ID of the run. + */ + addRun(solutionName: ISolutionName, runId: IRunId): void; + + /** + * Reads and parses the main JSON file. + * @returns The parsed main JSON object. */ - setup(solutionPath: string): ITestPaths; + readMainJson(): IMainJson; + /** - * Initializes the main JSON file for a new test run. - * @param solutionName The name of the solution file. - * @param runFolderName The name of the folder for the current run. - * @param mainJsonPath The path to the main JSON file. + * Initializes a new test run by adding it to the tracking system. + * @param solutionName The name of the solution. + * @param runId The ID of the run. */ - initializeTestRun(solutionName: string, runFolderName: string, mainJsonPath: string): void; + initializeTestRun(solutionName: ISolutionName, runId: IRunId): void; + /** * Saves a test result to a JSON file. * @param runFolderPath The path to the folder for the current run. * @param result The result to save. */ - saveResult(runFolderPath: string, result: IJsonTestResult): void; + saveResult(runFolderPath: IRunDirPath, result: IJsonTestResult): void; + /** * Gets a list of all solutions that have been tested. * @returns An array of solution names. */ - getSolutions(): string[]; + getAllSolutions(): ISolutionName[]; + /** * Gets a list of all test runs for a given solution. * @param solutionName The name of the solution. * @returns An array of run folder names (timestamps). */ - getRuns(solutionName: string): string[]; + getAllRuns(solutionName: ISolutionName): IRunId[]; + /** * Retrieves all test results for a specific test run. * @param solutionName The name of the solution. * @param runId The ID of the test run (timestamp). * @returns An array of test results. */ - getTestResults(solutionName: string, runId: string): IJsonTestResult[]; + getAllTestResults(runId: IRunId): IJsonTestResult[]; + + /** + * Deletes a solution and all its associated runs. + * @param solutionName The name of the solution to delete. + */ + deleteSolution(solutionName: ISolutionName): void; + + /** + * Deletes a specific test run. + * @param runId The ID of the run to delete. + */ + deleteRun(runId: IRunId): void; + + /** + * Deletes a specific test case result. + * @param runId The ID of the run containing the test case. + * @param testCaseNo The number of the test case to delete. + */ + deleteTestResult(runId: IRunId, testCaseNo: number): void; + + /** + * Updates an existing test result JSON file. + * @param runId The ID of the run containing the test case. + * @param newJsonResult The new result object to save. + */ + updateTestResult(runId: IRunId, newJsonResult: IJsonTestResult): void; + /** * Cleans up temporary files and directories. * @param paths An array of paths to delete. */ cleanup(paths: string[]): void; - /** - * Gets the path to the temporary directory. - * @returns The absolute path to the temporary directory. - */ - getTempDir(): string; } /** diff --git a/src/core/Interfaces/datastructures.ts b/src/core/Interfaces/datastructures.ts index e4cfbce..81fecfb 100644 --- a/src/core/Interfaces/datastructures.ts +++ b/src/core/Interfaces/datastructures.ts @@ -63,15 +63,15 @@ export interface ITestRunResult { */ export interface ITestPaths { /** The temporary directory for compilation and execution. */ - tempDir: string; + tempDir: ITempDirPath; /** The root directory where all results are stored. */ - resultsDir: string; + resultsDir: IResultDirPath; /** The directory specific to the solution being tested. */ - solutionDir: string; + solutionPath: ISolutionPath; /** The directory for a specific test run, identified by a timestamp. */ - runFolderPath: string; + runFolderPath: IRunDirPath; /** The path to the main JSON file that tracks all test runs. */ - mainJsonPath: string; + mainJsonPath: IMainJsonPath; } /** @@ -138,4 +138,22 @@ export interface IJsonTestResult { message?: string; /** The reason for the result, provided by the checker. */ reason?: string; -} \ No newline at end of file +} + +// A generic helper type to create a branded type +// K is the base type (e.g., string) +// T is the unique brand name (e.g., "SolutionPath") +export type Brand = K & { __brand: T }; + +// Define unique path types using the Brand helper +export type ISolutionPath = Brand; +export type IRunDirPath = Brand; +export type IResultDirPath = Brand; +export type ITestCaseJsonPath = Brand; +export type IMainJsonPath = Brand; +export type ITempDirPath = Brand; + +export type ISolutionName = Brand; +export type IRunId = Brand; + +export type IMainJson = { [key: ISolutionName]: IRunId[] }; \ No newline at end of file diff --git a/src/core/Interfaces/services.ts b/src/core/Interfaces/services.ts index a13bdee..85d9ba9 100644 --- a/src/core/Interfaces/services.ts +++ b/src/core/Interfaces/services.ts @@ -1,6 +1,6 @@ import { Uri } from "vscode"; -import { ITestResult, IExecutablePaths, ITestRunResult, IJsonTestResult, ITestPaths } from "./datastructures"; +import { ITestResult, IExecutablePaths, ITestRunResult, IJsonTestResult, ITestPaths, IRunId } from "./datastructures"; /** * Manages the creation and verification of test files (generator, checker). @@ -35,6 +35,15 @@ export interface ICompilationService { generatorValidatorPath: string, checkerPath: string, ): Promise; + + /** + * Compiles the solution and checker files for re-running tests with existing inputs. + * The generator is not needed in this case. + * @param solutionPath The file path to the C++ solution. + * @param checkerPath The file path to the checker. + * @returns A promise that resolves with the paths to the executables, or undefined if compilation fails. + */ + compileForRerun(solutionPath: string, checkerPath: string): Promise } /** @@ -53,6 +62,19 @@ export interface ITestRunnerService { generatorExec: string, checkerExec: string ): Promise; + + /** + * Runs a single test case with a given input. + * @param solutionExec The path to the solution executable. + * @param checkerExec The path to the checker executable. + * @param input The input for the test case. + * @returns A promise that resolves with the test result. + */ + runSingleTestWithInput( + solutionExec: string, + checkerExec: string, + input: string + ): Promise; } /** @@ -61,14 +83,24 @@ export interface ITestRunnerService { export interface IResultService { /** * Initializes the result storage for a new test run. + * @param solutionPath The path to the solution file, used to set up the result directories. + * @returns An object containing all the necessary paths for the test run. */ initialize(solutionPath : string): ITestPaths; /** * Saves the result of a single test case. * @param result The test result to save. + * @param paths The paths object for the current test run. */ - saveResult(result: IJsonTestResult, paths: ITestPaths): void + saveResult(result: IJsonTestResult, paths: ITestPaths): void; + + /** + * Updates an existing test case result. + * @param result The updated test result object. + * @param runId The ID of the run containing the test case. + */ + updateResult(result: IJsonTestResult, runId: IRunId): void; } /** @@ -80,6 +112,7 @@ export interface IOrchestrationService { * @param solutionPath The file path to the C++ solution. * @param generatorValidatorPath The file path to the generator/validator. * @param checkerPath The file path to the checker. + * @param numTests The number of test cases to run. */ run( solutionPath: string, @@ -87,6 +120,14 @@ export interface IOrchestrationService { checkerPath: string, numTests: number ): Promise; + + /** + * Re-runs a specific set of test cases with existing inputs. + * @param solutionPath The file path to the C++ solution. + * @param checkerPath The file path to the checker. + * @param testCases A map of run IDs to the test cases to be re-run. + */ + reRun(solutionPath: string, checkerPath: string, testCases: { [key: IRunId]: IJsonTestResult[] }): Promise; } /** @@ -100,6 +141,7 @@ export interface IUIService { /** * Handles the command to run the stress test. + * @param numTests The number of test cases to run. */ runStressTest(numTests: number): Promise; @@ -108,4 +150,21 @@ export interface IUIService { * @param activeFileUri The URI of the active file in the editor. */ updateActiveFile(activeFileUri: Uri | undefined): void; + + /** + * Handles the command to get all runs for the active solution. + */ + getRunsForActiveSolution(): Promise; + + /** + * Handles the command to get all test cases for a given run. + * @param runId The ID of the run. + */ + getTestCasesForRun(runId: string): Promise; + + /** + * Handles the command to re-run a selection of tests. + * @param testCases A map of run IDs to the test cases to be re-run. + */ + reRunTests(testCases: { [key: IRunId]: IJsonTestResult[] }): Promise } diff --git a/src/core/Managers/CPSTFolderManager.ts b/src/core/Managers/CPSTFolderManager.ts index 55d54cb..456a476 100644 --- a/src/core/Managers/CPSTFolderManager.ts +++ b/src/core/Managers/CPSTFolderManager.ts @@ -1,83 +1,155 @@ import * as path from 'path'; import { IFileManager, ICPSTFolderManager } from '../Interfaces/classes'; -import { ITestPaths, IJsonTestResult } from '../Interfaces/datastructures'; +import { ITestPaths, IJsonTestResult, ITempDirPath, IResultDirPath, IRunDirPath, ISolutionName, IRunId, IMainJson, IMainJsonPath, ITestCaseJsonPath, ISolutionPath} from '../Interfaces/datastructures'; +import { FileManager } from './FileManager'; export class CPSTFolderManager implements ICPSTFolderManager { constructor( private readonly _fileManager: IFileManager, private readonly _baseDir: string - ) {} + ) { + this.createTempDir(); + this.createResultDir(); + } - public setup(solutionPath: string): ITestPaths { - const tempDir = path.join(this._baseDir, 'temp'); - this._fileManager.createDirectory(tempDir); + public getSolutionName(solutionPath: ISolutionPath): ISolutionName { + return path.basename(solutionPath) as ISolutionName; + } - const resultsDir = path.join(this._baseDir, 'results'); - this._fileManager.createDirectory(resultsDir); + public getRunId(runFolderPath: IRunDirPath): IRunId { + return path.basename(runFolderPath) as IRunId; + } - const solutionName = path.basename(solutionPath); - const solutionDir = path.join(resultsDir, solutionName); - this._fileManager.createDirectory(solutionDir); - - const runFolderName = new Date().toISOString().replace(/[:.]/g, '-'); - const runFolderPath = path.join(solutionDir, runFolderName); - this._fileManager.createDirectory(runFolderPath); + public getTempDirPath(): ITempDirPath { + const tempPath = path.join(this._baseDir, 'temp'); + if(!this._fileManager.exists(tempPath)){ + this._fileManager.createDirectory(tempPath); + } + return tempPath as ITempDirPath; + } - const mainJsonPath = path.join(resultsDir, 'main.json'); + public getResultDirPath(): IResultDirPath{ + const resultPath = path.join(this._baseDir, 'results'); + if(!this._fileManager.exists(resultPath)){ + this._fileManager.createDirectory(resultPath); + } + return resultPath as IResultDirPath; + } - return { tempDir, resultsDir, solutionDir, runFolderPath, mainJsonPath }; + public getMainJsonPath(): IMainJsonPath{ + return path.join(this.getResultDirPath(), 'main.json') as IMainJsonPath; } - public initializeTestRun(solutionName: string, runFolderName: string, mainJsonPath: string): void { - let mainJson: { [key: string]: string[] } = {}; - if (this._fileManager.exists(mainJsonPath)) { + public getRunResultDirPath(runId: IRunId): IRunDirPath{ + return path.join(this.getResultDirPath(), runId) as IRunDirPath; + } + + public getTestCaseResultPath(runId: IRunId, testCaseNo: number): ITestCaseJsonPath{ + return path.join(this.getRunResultDirPath(runId), `test_${testCaseNo}.json`) as ITestCaseJsonPath; + } + + public getTestCaseNo(testCasePath: ITestCaseJsonPath): number{ + return Number.parseInt(path.basename(testCasePath).replace("test_", "").replace(".json", "")); + } + + public getTestCaseResultData(testCasePath: ITestCaseJsonPath): IJsonTestResult { + let testResult: IJsonTestResult = {testCase: this.getTestCaseNo(testCasePath)}; + if (this._fileManager.exists(testCasePath)) { try { - mainJson = JSON.parse(this._fileManager.readFile(mainJsonPath)); + testResult = JSON.parse(this._fileManager.readFile(testCasePath)); } catch (e) { - mainJson = {}; + testResult = {testCase: this.getTestCaseNo(testCasePath)}; } } + return testResult; + } + + public getTestPaths(solutionPath: ISolutionPath, runId: IRunId): ITestPaths{ + return {tempDir: this.getTempDirPath(), resultsDir: this.getResultDirPath(), solutionPath: solutionPath, runFolderPath: this.getRunResultDirPath(runId), mainJsonPath: this.getMainJsonPath()}; + } + + public generateNonce(){ + return new Date().toISOString().replace(/[:.]/g, '-'); + } + + public createTempDir(): void{ + this._fileManager.createDirectory(this.getTempDirPath()); + } + + public createResultDir(): void{ + this._fileManager.createDirectory(this.getResultDirPath()); + } + + public createRunFolder(runId: IRunId): void{ + this._fileManager.createDirectory(this.getRunResultDirPath(runId)); + } + public addSolutionToMainJson(solutionName: ISolutionName): void{ + let mainJson = this.readMainJson(); if (!mainJson[solutionName]) { mainJson[solutionName] = []; } - mainJson[solutionName].push(runFolderName); - this._fileManager.writeFile(mainJsonPath, JSON.stringify(mainJson, null, 4)); + this._fileManager.writeFile(this.getMainJsonPath(), JSON.stringify(mainJson, null, 4)); } - public saveResult(runFolderPath: string, result: IJsonTestResult): void { - const resultFilePath = path.join(runFolderPath, `test_${result.testCase}.json`); - this._fileManager.writeFile(resultFilePath, JSON.stringify(result, null, 4)); + public addRunToMainJson(solutionName: ISolutionName, runId: IRunId): void{ + this.addSolutionToMainJson(solutionName); // to ensure solution exist + let mainJson = this.readMainJson(); + mainJson[solutionName].push(runId); + this._fileManager.writeFile(this.getMainJsonPath(), JSON.stringify(mainJson, null, 4)); } - public getSolutions(): string[] { - const mainJsonPath = path.join(this._baseDir, 'results', 'main.json'); - if (!this._fileManager.exists(mainJsonPath)) { - return []; + public addSolution(solutionPath : ISolutionPath): void{ + this.addSolutionToMainJson(this.getSolutionName(solutionPath)); + } + + public addRun(solutionName: ISolutionName, runId: IRunId): void{ + this.createRunFolder(runId); + this.addRunToMainJson(solutionName, runId); + } + + public readMainJson(): IMainJson{ + let mainJsonPath = this.getMainJsonPath(); + let mainJson: IMainJson = {}; + if (this._fileManager.exists(mainJsonPath)) { + try { + mainJson = JSON.parse(this._fileManager.readFile(mainJsonPath)); + } catch (e) { + mainJson = {}; + } } + return mainJson; + } + + public initializeTestRun(solutionName: ISolutionName, runId: IRunId): void { + this.addRun(solutionName,runId); + } + + public saveResult(runFolderPath: IRunDirPath, result: IJsonTestResult): void { + const resultFilePath = this.getTestCaseResultPath(this.getRunId(runFolderPath), result.testCase); + this._fileManager.writeFile(resultFilePath, JSON.stringify(result, null, 4)); + } + + public getAllSolutions(): ISolutionName[] { try { - const mainJson = JSON.parse(this._fileManager.readFile(mainJsonPath)); - return Object.keys(mainJson); + const mainJson = this.readMainJson(); + return Object.keys(mainJson) as ISolutionName[]; } catch (e) { return []; } } - public getRuns(solutionName: string): string[] { - const mainJsonPath = path.join(this._baseDir, 'results', 'main.json'); - if (!this._fileManager.exists(mainJsonPath)) { - return []; - } + public getAllRuns(solutionName: ISolutionName): IRunId[] { try { - const mainJson = JSON.parse(this._fileManager.readFile(mainJsonPath)); + const mainJson = this.readMainJson(); return mainJson[solutionName] || []; } catch (e) { return []; } } - public getTestResults(solutionName: string, runId: string): IJsonTestResult[] { - const runFolderPath = path.join(this._baseDir, 'results', solutionName, runId); + public getAllTestResults(runId: IRunId): IJsonTestResult[] { + const runFolderPath = this.getRunResultDirPath(runId); if (!this._fileManager.exists(runFolderPath)) { return []; } @@ -98,8 +170,48 @@ export class CPSTFolderManager implements ICPSTFolderManager { return results; } - public getTempDir(): string { - return path.join(this._baseDir, 'temp'); + public deleteSolution(solutionName: ISolutionName): void { + let mainJson = this.readMainJson(); + if (mainJson[solutionName]) { + const runsToDelete = mainJson[solutionName]; + + for (const runId of runsToDelete) { + this.deleteRun(runId); + } + + delete mainJson[solutionName]; + this._fileManager.writeFile(this.getMainJsonPath(), JSON.stringify(mainJson, null, 4)); + } + } + + public deleteRun(runId: IRunId): void { + const runFolderPath = this.getRunResultDirPath(runId); + if (this._fileManager.exists(runFolderPath)) { + this._fileManager.deleteDirectory(runFolderPath); + } + + let mainJson = this.readMainJson(); + const solutionsNames = this.getAllSolutions(); + for(const solutionName of solutionsNames){ + if (mainJson[solutionName]) { + mainJson[solutionName] = mainJson[solutionName].filter(id => id !== runId); + } + } + this._fileManager.writeFile(this.getMainJsonPath(), JSON.stringify(mainJson, null, 4)); + } + + public deleteTestResult(runId: IRunId, testCaseNo: number): void { + const testCasePath = this.getTestCaseResultPath(runId, testCaseNo); + if (this._fileManager.exists(testCasePath)) { + this._fileManager.deleteFile(testCasePath); + } + } + + public updateTestResult(runId: IRunId, newJsonResult: IJsonTestResult): void { + const testCasePath = this.getTestCaseResultPath(runId, newJsonResult.testCase); + if (this._fileManager.exists(testCasePath)) { + this._fileManager.writeFile(testCasePath, JSON.stringify(newJsonResult, null, 4)); + } } public cleanup(paths: string[]): void { diff --git a/src/core/Managers/CompilationManager.ts b/src/core/Managers/CompilationManager.ts index edca42b..4555dc0 100644 --- a/src/core/Managers/CompilationManager.ts +++ b/src/core/Managers/CompilationManager.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { ICompilationManager, ICompiler } from '../Interfaces/classes'; -import { IExecutablePaths } from '../Interfaces/datastructures'; +import { IExecutablePaths, ITempDirPath } from '../Interfaces/datastructures'; export class CompilationManager implements ICompilationManager { constructor( @@ -18,4 +18,15 @@ export class CompilationManager implements ICompilationManager { return { solutionExec, generatorExec, checkerExec }; } + + public async compileForReRun(tempDir: string, solutionPath: string, checkerPath: string): Promise { + const solutionExec = path.join(tempDir, "solution_exec"); + const generatorExec = ""; + const checkerExec = path.join(tempDir, "checker_exec"); + + if (!await this._compiler.compile(solutionPath, solutionExec)) {return null;} + if (!await this._compiler.compile(checkerPath, checkerExec)) {return null;} + + return { solutionExec, generatorExec, checkerExec }; + } } diff --git a/src/core/Managers/FileManager.ts b/src/core/Managers/FileManager.ts index 075aea7..3feb4ac 100644 --- a/src/core/Managers/FileManager.ts +++ b/src/core/Managers/FileManager.ts @@ -23,6 +23,18 @@ export class FileManager implements IFileManager { return fs.existsSync(filePath); } + public deleteFile(filePath: string): void { + if (fs.existsSync(filePath)) { + fs.rmSync(filePath, { force: true }); + } + } + + public deleteDirectory(dirPath: string): void { + if (fs.existsSync(dirPath)) { + fs.rmSync(dirPath, { recursive: true, force: true }); + } + } + public cleanup(dirs: string[]): void { dirs.forEach(dir => { if (fs.existsSync(dir)) { diff --git a/src/core/Services/CompilationService.ts b/src/core/Services/CompilationService.ts index b06a0ef..993a407 100644 --- a/src/core/Services/CompilationService.ts +++ b/src/core/Services/CompilationService.ts @@ -10,8 +10,14 @@ export class CompilationService implements ICompilationService { ) {} public async compile(solutionPath: string, generatorValidatorPath: string, checkerPath: string): Promise { - const tempDir = this._cpstFolderManager.getTempDir(); + const tempDir = this._cpstFolderManager.getTempDirPath(); const executables = await this._compilationManager.compile(tempDir, solutionPath, generatorValidatorPath, checkerPath); return executables ?? undefined; } + + public async compileForRerun(solutionPath: string, checkerPath: string): Promise { + const tempDir = this._cpstFolderManager.getTempDirPath(); + const executables = await this._compilationManager.compileForReRun(tempDir, solutionPath, checkerPath); + return executables ?? undefined; + } } diff --git a/src/core/Services/OrchestrationService.ts b/src/core/Services/OrchestrationService.ts index 9974813..3f80916 100644 --- a/src/core/Services/OrchestrationService.ts +++ b/src/core/Services/OrchestrationService.ts @@ -1,6 +1,6 @@ import { ITestReporter, ICPSTFolderManager } from '../Interfaces/classes'; -import { ITestPaths } from '../Interfaces/datastructures'; +import { IJsonTestResult, IRunId, ITestPaths } from '../Interfaces/datastructures'; import { ICompilationService, IOrchestrationService, IResultService, ITestRunnerService } from '../Interfaces/services'; export class OrchestrationService implements IOrchestrationService { @@ -18,15 +18,19 @@ export class OrchestrationService implements IOrchestrationService { if (!executables) { this._reporter.reportError("Compilation failed."); - this._cpstFolderManager.cleanup([this._cpstFolderManager.getTempDir()]); + this._cpstFolderManager.cleanup([this._cpstFolderManager.getTempDirPath()]); return; } + const statusCounts: { [status: string]: number } = {}; + for (let i = 1; i <= numTests; i++) { this._reporter.reportProgress({ command: 'testResult', status: 'Running', testCase: i }); const result = await this._testRunnerService.runSingleTest(executables.solutionExec, executables.generatorExec, executables.checkerExec); + statusCounts[result.status] = (statusCounts[result.status] || 0) + 1; + const resultToSave = { testCase: i, lastResult: result.status, @@ -54,6 +58,67 @@ export class OrchestrationService implements IOrchestrationService { this._reporter.reportProgress(progress); } - this._cpstFolderManager.cleanup([this._cpstFolderManager.getTempDir()]); + this._reporter.reportProgress({ + command: 'summary', + results: statusCounts + }); + + this._cpstFolderManager.cleanup([this._cpstFolderManager.getTempDirPath()]); + } + + public async reRun(solutionPath: string, checkerPath: string, testCases: { [key: IRunId]: IJsonTestResult[] }): Promise { + const executables = await this._compilationService.compileForRerun(solutionPath, checkerPath); + + if (!executables) { + this._reporter.reportError("Compilation failed."); + this._cpstFolderManager.cleanup([this._cpstFolderManager.getTempDirPath()]); + return; + } + + const statusCounts: { [status: string]: number } = {}; + + for(const runId of Object.keys(testCases) as IRunId[]){ + for (const test of testCases[runId]) { + this._reporter.reportProgress({ command: 'testResult', status: 'Running', testCase: test.testCase, runId: runId }); + + const result = await this._testRunnerService.runSingleTestWithInput(executables.solutionExec, executables.checkerExec, test.input || ""); + + statusCounts[result.status] = (statusCounts[result.status] || 0) + 1; + + const resultToSave : IJsonTestResult = { + testCase: test.testCase, + lastResult: result.status, + input: result.input, + userOutput: result.output, + execTime: result.duration, + memoryUsed: result.memory, + message: result.message, + reason: result.reason + }; + + this._resultService.updateResult(resultToSave, runId); + + const progress = { + command: 'testResult', + status: result.status, + testCase: test.testCase, + input: result.input, + output: result.output, + time: result.duration, + memory: result.memory, + message: result.message, + reason: result.reason, + runId: runId + }; + this._reporter.reportProgress(progress); + } + } + + this._reporter.reportProgress({ + command: 'summary', + results: statusCounts + }); + + this._cpstFolderManager.cleanup([this._cpstFolderManager.getTempDirPath()]); } } diff --git a/src/core/Services/ResultService.ts b/src/core/Services/ResultService.ts index efbf739..f7a2f77 100644 --- a/src/core/Services/ResultService.ts +++ b/src/core/Services/ResultService.ts @@ -1,6 +1,6 @@ import { IResultService } from '../Interfaces/services'; -import { IJsonTestResult, ITestPaths } from '../Interfaces/datastructures'; +import { IJsonTestResult, IRunId, ISolutionPath, ITestPaths } from '../Interfaces/datastructures'; import { ICPSTFolderManager} from '../Interfaces/classes'; import * as path from 'path'; @@ -10,12 +10,16 @@ export class ResultService implements IResultService { ) {} public initialize(solutionPath : string): ITestPaths { - const paths = this._cpstFolderManager.setup(solutionPath); - this._cpstFolderManager.initializeTestRun(path.basename(paths.solutionDir), path.basename(paths.runFolderPath), paths.mainJsonPath); - return paths; + const runId: IRunId = this._cpstFolderManager.generateNonce() as IRunId; + this._cpstFolderManager.addRun(this._cpstFolderManager.getSolutionName(solutionPath as ISolutionPath) ,runId); + return this._cpstFolderManager.getTestPaths(solutionPath as ISolutionPath, runId); } public saveResult(result: IJsonTestResult, paths: ITestPaths): void { this._cpstFolderManager.saveResult(paths.runFolderPath, result); } + + public updateResult(result: IJsonTestResult, runId: IRunId): void{ + this._cpstFolderManager.updateTestResult(runId, result); + } } diff --git a/src/core/Services/TestRunnerService.ts b/src/core/Services/TestRunnerService.ts index 8e11b15..fd17e55 100644 --- a/src/core/Services/TestRunnerService.ts +++ b/src/core/Services/TestRunnerService.ts @@ -10,7 +10,12 @@ export class TestRunnerService implements ITestRunnerService { ) {} public async runSingleTest(solutionExec: string, generatorExec: string, checkerExec: string): Promise { - const tempDir = this._cpstFolderManager.getTempDir(); + const tempDir = this._cpstFolderManager.getTempDirPath(); return this._testRunner.run(tempDir, solutionExec, generatorExec, checkerExec); } + + public async runSingleTestWithInput(solutionExec: string, checkerExec: string, input: string): Promise { + const tempDir = this._cpstFolderManager.getTempDirPath(); + return this._testRunner.runWithInput(tempDir, solutionExec, checkerExec, input); + } } diff --git a/src/core/Services/UIService.ts b/src/core/Services/UIService.ts index 67e9338..4fb8149 100644 --- a/src/core/Services/UIService.ts +++ b/src/core/Services/UIService.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode'; -import { IFileManager, ITestReporter } from '../Interfaces/classes'; +import { IFileManager, ITestReporter, ICPSTFolderManager } from '../Interfaces/classes'; import { IOrchestrationService, ITestFileService, IUIService } from '../Interfaces/services'; +import { IJsonTestResult, IRunId, ISolutionPath } from '../Interfaces/datastructures'; export class UIService implements IUIService { private _currentSolutionFile?: vscode.Uri; @@ -10,7 +11,8 @@ export class UIService implements IUIService { private readonly _fileManager: IFileManager, private readonly _testFileService: ITestFileService, private readonly _orchestrationService: IOrchestrationService, - private readonly _reporter: ITestReporter + private readonly _reporter: ITestReporter, + private readonly _cpstFolderManager: ICPSTFolderManager ) {} public updateActiveFile(activeFileUri: vscode.Uri | undefined): void { @@ -81,5 +83,52 @@ export class UIService implements IUIService { } await this._orchestrationService.run(solutionPath, genValPath, checkerPath, numTests); + + this._reporter.reportHistoryCleared(); + } + + public async reRunTests(testCases: { [key: IRunId]: IJsonTestResult[] }): Promise { + if (!this._currentSolutionFile) { + this._reporter.reportError("Cannot run test: Could not determine the solution file."); + return; + } + + this._reporter.reportHistoryCleared(); + // this._reporter.reportTestRunning(); // its clear the screen fully (we dont want that) + + const solutionPath = this._currentSolutionFile.fsPath; + const checkerPath = this._fileManager.getCheckerFileUri(this._currentSolutionFile).fsPath; + + if (!this._fileManager.exists(checkerPath)) { + this._reporter.reportProgress({ + command: "error", + message: "Stress test files not found. Please generate them first.", + }); + return; + } + + await this._orchestrationService.reRun(solutionPath, checkerPath, testCases); + } + + public async getRunsForActiveSolution(): Promise { + if (!this._currentSolutionFile) { + this._reporter.reportError("No active C++ solution file selected."); + return; + } + const solutionName = this._cpstFolderManager.getSolutionName(this._currentSolutionFile.fsPath as ISolutionPath); + const runs = this._cpstFolderManager.getAllRuns(solutionName); + this._reporter.reportProgress({ + command: "show-runs", + runs: runs, + }); + } + + public async getTestCasesForRun(runId: string): Promise { + const testCases = this._cpstFolderManager.getAllTestResults(runId as IRunId); + this._reporter.reportProgress({ + command: "show-test-cases", + testCases: testCases, + runId: runId, + }); } } diff --git a/src/extension.ts b/src/extension.ts index 62c642c..f17a9ef 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -61,7 +61,8 @@ export function activate(context: vscode.ExtensionContext) { fileManager, testFileService, orchestrationService, - testReporterProxy + testReporterProxy, + cpstFolderManager ); // UI Layer diff --git a/src/test/CompileAndRun/TestRunner.test.ts b/src/test/CompileAndRun/TestRunner.test.ts index 42d4566..a536763 100644 --- a/src/test/CompileAndRun/TestRunner.test.ts +++ b/src/test/CompileAndRun/TestRunner.test.ts @@ -21,6 +21,8 @@ const mockFileManager: jest.Mocked = { getGenValFileUri: jest.fn(), getCheckerFileUri: jest.fn(), copyFile: jest.fn(), + deleteDirectory: jest.fn(), + deleteFile: jest.fn() }; describe('TestRunner', () => { diff --git a/src/test/Managers/CPSTFolderManager.test.ts b/src/test/Managers/CPSTFolderManager.test.ts index 7c4ff74..8fe9b0c 100644 --- a/src/test/Managers/CPSTFolderManager.test.ts +++ b/src/test/Managers/CPSTFolderManager.test.ts @@ -1,348 +1,414 @@ import * as path from 'path'; import { IFileManager } from '../../core/Interfaces/classes'; -import { IJsonTestResult } from '../../core/Interfaces/datastructures'; +import { IJsonTestResult, ISolutionName, IRunId, IMainJson, ISolutionPath, IRunDirPath, IMainJsonPath } from '../../core/Interfaces/datastructures'; import { CPSTFolderManager } from '../../core/Managers/CPSTFolderManager'; // Mock the dependencies -jest.mock('fs'); // fs is not a direct dependency, but path might use it internally. Better safe. jest.mock('path'); // Since IFileManager is an interface, we create a mock implementation for it. const mockFileManager: jest.Mocked = { - createDirectory: jest.fn(), - writeFile: jest.fn(), - readFile: jest.fn(), - exists: jest.fn(), - cleanup: jest.fn(), - listDirectory: jest.fn(), - getSolutionFileUri: jest.fn(), - getGenValFileUri: jest.fn(), - getCheckerFileUri: jest.fn(), - copyFile: jest.fn(), + createDirectory: jest.fn(), + writeFile: jest.fn(), + readFile: jest.fn(), + exists: jest.fn(), + cleanup: jest.fn(), + listDirectory: jest.fn(), + getSolutionFileUri: jest.fn(), + getGenValFileUri: jest.fn(), + getCheckerFileUri: jest.fn(), + copyFile: jest.fn(), + deleteFile: jest.fn(), + deleteDirectory: jest.fn(), }; describe('CPSTFolderManager', () => { - let folderManager: CPSTFolderManager; - const baseDir = '/cpst'; - const mockedPath = path as jest.Mocked; - - beforeEach(() => { - // Setup and reset before each test - jest.clearAllMocks(); - folderManager = new CPSTFolderManager(mockFileManager, baseDir); - - // Mock path functions for deterministic behavior - mockedPath.join.mockImplementation((...args) => args.join('/')); - mockedPath.basename.mockImplementation(p => p.split('/').pop() || ''); - }); - - describe('setup', () => { - it('should create all necessary directories and return the correct paths', () => { - // Arrange - const solutionPath = '/solutions/problemA.cpp'; - const fixedDate = new Date('2023-10-27T10:00:00.000Z'); - const expectedRunFolderName = fixedDate.toISOString().replace(/[:.]/g, '-'); // "2023-10-27T10-00-00-000Z" - - jest.useFakeTimers().setSystemTime(fixedDate); - - const expectedPaths = { - tempDir: `${baseDir}/temp`, - resultsDir: `${baseDir}/results`, - solutionDir: `${baseDir}/results/problemA.cpp`, - runFolderPath: `${baseDir}/results/problemA.cpp/${expectedRunFolderName}`, - mainJsonPath: `${baseDir}/results/main.json`, - }; - - // Act - const result = folderManager.setup(solutionPath); - - // Assert - expect(mockFileManager.createDirectory).toHaveBeenCalledWith(expectedPaths.tempDir); - expect(mockFileManager.createDirectory).toHaveBeenCalledWith(expectedPaths.resultsDir); - expect(mockFileManager.createDirectory).toHaveBeenCalledWith(expectedPaths.solutionDir); - expect(mockFileManager.createDirectory).toHaveBeenCalledWith(expectedPaths.runFolderPath); - expect(mockFileManager.createDirectory).toHaveBeenCalledTimes(4); - - expect(result).toEqual(expectedPaths); - - jest.useRealTimers(); - }); - }); - - describe('initializeTestRun', () => { - const solutionName = 'problemA.cpp'; - const runFolderName = 'run1'; - const mainJsonPath = `${baseDir}/results/main.json`; - - it('should create a new main.json if it does not exist', () => { - // Arrange - mockFileManager.exists.mockReturnValue(false); - const expectedContent = { [solutionName]: [runFolderName] }; - - // Act - folderManager.initializeTestRun(solutionName, runFolderName, mainJsonPath); - - // Assert - expect(mockFileManager.exists).toHaveBeenCalledWith(mainJsonPath); - expect(mockFileManager.writeFile).toHaveBeenCalledWith( - mainJsonPath, - JSON.stringify(expectedContent, null, 4) - ); - }); + let folderManager: CPSTFolderManager; + const baseDir = '/cpst'; + const mockedPath = path as jest.Mocked; - it('should add a new solution entry if the solution is not in an existing main.json', () => { - // Arrange - const existingContent = { 'problemB.cpp': ['runB1'] }; - mockFileManager.exists.mockReturnValue(true); - mockFileManager.readFile.mockReturnValue(JSON.stringify(existingContent)); - const expectedContent = { ...existingContent, [solutionName]: [runFolderName] }; - - // Act - folderManager.initializeTestRun(solutionName, runFolderName, mainJsonPath); - - // Assert - expect(mockFileManager.readFile).toHaveBeenCalledWith(mainJsonPath); - expect(mockFileManager.writeFile).toHaveBeenCalledWith( - mainJsonPath, - JSON.stringify(expectedContent, null, 4) - ); - }); + beforeEach(() => { + // Setup and reset before each test + jest.clearAllMocks(); - it('should append a new run folder to an existing solution entry', () => { - // Arrange - const existingContent = { [solutionName]: ['existingRun'] }; - mockFileManager.exists.mockReturnValue(true); - mockFileManager.readFile.mockReturnValue(JSON.stringify(existingContent)); - const expectedContent = { [solutionName]: ['existingRun', runFolderName] }; - - // Act - folderManager.initializeTestRun(solutionName, runFolderName, mainJsonPath); - - // Assert - expect(mockFileManager.readFile).toHaveBeenCalledWith(mainJsonPath); - expect(mockFileManager.writeFile).toHaveBeenCalledWith( - mainJsonPath, - JSON.stringify(expectedContent, null, 4) - ); - }); + // Mock path functions for deterministic behavior + mockedPath.join.mockImplementation((...args) => args.join('/')); + mockedPath.basename.mockImplementation(p => p.split('/').pop() || ''); - it('should handle a malformed main.json by creating a new entry', () => { - // Arrange - mockFileManager.exists.mockReturnValue(true); - mockFileManager.readFile.mockReturnValue('invalid json'); - const expectedContent = { [solutionName]: [runFolderName] }; - - // Act - folderManager.initializeTestRun(solutionName, runFolderName, mainJsonPath); - - // Assert - expect(mockFileManager.writeFile).toHaveBeenCalledWith( - mainJsonPath, - JSON.stringify(expectedContent, null, 4) - ); - }); - }); - - describe('saveResult', () => { - it('should write the test result to the correct file path', () => { - // Arrange - const runFolderPath = `${baseDir}/results/problemA.cpp/run1`; - const result: IJsonTestResult = { testCase: 1, lastResult: 'OK' }; - const expectedFilePath = `${runFolderPath}/test_1.json`; - - // Act - folderManager.saveResult(runFolderPath, result); - - // Assert - expect(mockedPath.join).toHaveBeenCalledWith(runFolderPath, `test_${result.testCase}.json`); - expect(mockFileManager.writeFile).toHaveBeenCalledWith( - expectedFilePath, - JSON.stringify(result, null, 4) - ); + // We instantiate after mocking path because the constructor uses it. + folderManager = new CPSTFolderManager(mockFileManager, baseDir); }); - }); - describe('getSolutions', () => { - const mainJsonPath = `${baseDir}/results/main.json`; - - it('should return an empty array if main.json does not exist', () => { - // Arrange - mockFileManager.exists.mockReturnValue(false); - - // Act - const solutions = folderManager.getSolutions(); - - // Assert - expect(mockFileManager.exists).toHaveBeenCalledWith(mainJsonPath); - expect(solutions).toEqual([]); + describe('Path Getters', () => { + it('getSolutionName should return the base name of the solution path', () => { + const solutionPath = '/path/to/solution.cpp' as ISolutionPath; + expect(folderManager.getSolutionName(solutionPath)).toBe('solution.cpp'); + expect(mockedPath.basename).toHaveBeenCalledWith(solutionPath); + }); + + it('getRunId should return the base name of the run folder path', () => { + const runFolderPath = '/path/to/run-id' as IRunDirPath; + expect(folderManager.getRunId(runFolderPath)).toBe('run-id'); + expect(mockedPath.basename).toHaveBeenCalledWith(runFolderPath); + }); + + it('getTempDirPath should return the correct temp directory path', () => { + expect(folderManager.getTempDirPath()).toBe(`${baseDir}/temp`); + }); + + it('getResultDirPath should return the correct results directory path', () => { + expect(folderManager.getResultDirPath()).toBe(`${baseDir}/results`); + }); + + it('getMainJsonPath should return the correct main.json path', () => { + expect(folderManager.getMainJsonPath()).toBe(`${baseDir}/results/main.json`); + }); + + it('getRunResultDirPath should return the correct path for a run', () => { + const runId = 'run-id' as IRunId; + expect(folderManager.getRunResultDirPath(runId)).toBe(`${baseDir}/results/run-id`); + }); + + it('getTestCaseResultPath should return the correct path for a test case', () => { + const runId = 'run-id' as IRunId; + const testCaseNo = 1; + expect(folderManager.getTestCaseResultPath(runId, testCaseNo)).toBe(`${baseDir}/results/run-id/test_1.json`); + }); }); - it('should return an empty array if main.json is malformed', () => { - // Arrange - mockFileManager.exists.mockReturnValue(true); - mockFileManager.readFile.mockReturnValue('invalid json'); + describe('generateNonce', () => { + it('should generate a nonce from the current date', () => { + const fixedDate = new Date('2023-10-27T10:00:00.000Z'); + const expectedNonce = '2023-10-27T10-00-00-000Z'; + jest.useFakeTimers().setSystemTime(fixedDate); - // Act - const solutions = folderManager.getSolutions(); + expect(folderManager.generateNonce()).toBe(expectedNonce); - // Assert - expect(solutions).toEqual([]); + jest.useRealTimers(); + }); }); - it('should return the list of solutions from main.json', () => { - // Arrange - const mainJsonContent = { 'problemA.cpp': [], 'problemB.cpp': [] }; - mockFileManager.exists.mockReturnValue(true); - mockFileManager.readFile.mockReturnValue(JSON.stringify(mainJsonContent)); - - // Act - const solutions = folderManager.getSolutions(); - - // Assert - expect(solutions).toEqual(['problemA.cpp', 'problemB.cpp']); + describe('Directory Creation', () => { + it('createTempDir should create the temp directory', () => { + // reset mocks from constructor call + mockFileManager.createDirectory.mockClear(); + folderManager.createTempDir(); + expect(mockFileManager.createDirectory).toHaveBeenCalledWith(`${baseDir}/temp`); + }); + + it('createResultDir should create the results directory', () => { + // reset mocks from constructor call + mockFileManager.createDirectory.mockClear(); + folderManager.createResultDir(); + expect(mockFileManager.createDirectory).toHaveBeenCalledWith(`${baseDir}/results`); + }); + + it('createRunFolder should create a directory for the run', () => { + const runId = 'run-id' as IRunId; + const expectedPath = `${baseDir}/results/run-id`; + folderManager.createRunFolder(runId); + expect(mockFileManager.createDirectory).toHaveBeenCalledWith(expectedPath); + }); }); - }); - - describe('getRuns', () => { - const mainJsonPath = `${baseDir}/results/main.json`; - const solutionName = 'problemA.cpp'; - it('should return an empty array if main.json does not exist', () => { - // Arrange - mockFileManager.exists.mockReturnValue(false); - - // Act - const runs = folderManager.getRuns(solutionName); - - // Assert - expect(runs).toEqual([]); + describe('JSON Management', () => { + const solutionName = 'solution.cpp' as ISolutionName; + const runId = 'run-id' as IRunId; + const mainJsonPath = `${baseDir}/results/main.json` as IMainJsonPath; + + describe('readMainJson', () => { + it('should return an empty object if main.json does not exist', () => { + mockFileManager.exists.mockReturnValue(false); + expect(folderManager.readMainJson()).toEqual({}); + }); + + it('should return an empty object if main.json is malformed', () => { + mockFileManager.exists.mockReturnValue(true); + mockFileManager.readFile.mockReturnValue('invalid json'); + expect(folderManager.readMainJson()).toEqual({}); + }); + + it('should return the parsed JSON object if main.json is valid', () => { + const mainJson: IMainJson = { [solutionName]: [runId] }; + mockFileManager.exists.mockReturnValue(true); + mockFileManager.readFile.mockReturnValue(JSON.stringify(mainJson)); + expect(folderManager.readMainJson()).toEqual(mainJson); + }); + }); + + describe('addSolutionToMainJson', () => { + it('should add a new solution to an empty main.json', () => { + jest.spyOn(folderManager, 'readMainJson').mockReturnValue({}); + folderManager.addSolutionToMainJson(solutionName); + const expectedJson: IMainJson = { [solutionName]: [] }; + expect(mockFileManager.writeFile).toHaveBeenCalledWith(mainJsonPath, JSON.stringify(expectedJson, null, 4)); + }); + + it('should not add a solution if it already exists', () => { + const existingJson: IMainJson = { [solutionName]: [] }; + jest.spyOn(folderManager, 'readMainJson').mockReturnValue(existingJson); + folderManager.addSolutionToMainJson(solutionName); + expect(mockFileManager.writeFile).toHaveBeenCalledWith(mainJsonPath, JSON.stringify(existingJson, null, 4)); + }); + }); + + describe('addRunToMainJson', () => { + it('should add a new solution and run if solution does not exist', () => { + jest.spyOn(folderManager, 'readMainJson').mockReturnValue({}); + folderManager.addRunToMainJson(solutionName, runId); + const expectedJson: IMainJson = { [solutionName]: [runId] }; + expect(mockFileManager.writeFile).toHaveBeenCalledWith(mainJsonPath, JSON.stringify(expectedJson, null, 4)); + }); + + it('should add a run to an existing solution', () => { + const existingJson: IMainJson = { [solutionName]: ['existing-run' as IRunId] }; + jest.spyOn(folderManager, 'readMainJson').mockReturnValue(existingJson); + folderManager.addRunToMainJson(solutionName, runId); + const expectedJson: IMainJson = { [solutionName]: ['existing-run' as IRunId, runId] }; + expect(mockFileManager.writeFile).toHaveBeenCalledWith(mainJsonPath, JSON.stringify(expectedJson, null, 4)); + }); + }); }); - it('should return an empty array if main.json is malformed', () => { - // Arrange - mockFileManager.exists.mockReturnValue(true); - mockFileManager.readFile.mockReturnValue('invalid json'); - - // Act - const runs = folderManager.getRuns(solutionName); - - // Assert - expect(runs).toEqual([]); + describe('Workflow Methods', () => { + const solutionName = 'solution.cpp' as ISolutionName; + const solutionPath = `/path/to/${solutionName}` as ISolutionPath; + const runId = 'run-id' as IRunId; + + describe('addSolution', () => { + it('should call addSolutionToMainJson with the correct solution name', () => { + jest.spyOn(folderManager, 'addSolutionToMainJson'); + mockedPath.basename.mockReturnValue(solutionName); + + folderManager.addSolution(solutionPath); + + expect(folderManager.addSolutionToMainJson).toHaveBeenCalledWith(solutionName); + expect(mockedPath.basename).toHaveBeenCalledWith(solutionPath); + }); + }); + + describe('addRun', () => { + it('should call createRunFolder and addRunToMainJson', () => { + jest.spyOn(folderManager, 'createRunFolder'); + jest.spyOn(folderManager, 'addRunToMainJson'); + + folderManager.addRun(solutionName, runId); + + expect(folderManager.createRunFolder).toHaveBeenCalledWith(runId); + expect(folderManager.addRunToMainJson).toHaveBeenCalledWith(solutionName, runId); + }); + }); + + describe('initializeTestRun', () => { + it('should call addRun', () => { + jest.spyOn(folderManager, 'addRun'); + folderManager.initializeTestRun(solutionName, runId); + expect(folderManager.addRun).toHaveBeenCalledWith(solutionName, runId); + }); + }); + + describe('saveResult', () => { + it('should write the test result to the correct file path', () => { + const runFolderPath = `${baseDir}/results/problemA.cpp/run1` as IRunDirPath; + const result: IJsonTestResult = { testCase: 1, lastResult: 'OK' }; + const expectedFilePath = `${baseDir}/results/run1/test_1.json`; + mockedPath.basename.mockReturnValue('run1' as IRunId); + + folderManager.saveResult(runFolderPath, result); + + expect(mockedPath.basename).toHaveBeenCalledWith(runFolderPath); + expect(mockFileManager.writeFile).toHaveBeenCalledWith( + expectedFilePath, + JSON.stringify(result, null, 4) + ); + }); + }); }); - it('should return an empty array if the solution does not exist in main.json', () => { - // Arrange - const mainJsonContent = { 'problemB.cpp': ['runB1'] }; - mockFileManager.exists.mockReturnValue(true); - mockFileManager.readFile.mockReturnValue(JSON.stringify(mainJsonContent)); - - // Act - const runs = folderManager.getRuns(solutionName); - - // Assert - expect(runs).toEqual([]); + describe('Data Retrieval', () => { + const solutionName = 'solution.cpp' as ISolutionName; + const runId = 'run-id' as IRunId; + const mainJson: IMainJson = { [solutionName]: [runId], ['other.cpp' as ISolutionName]: [] }; + + describe('getAllSolutions', () => { + it('should return all solution names from main.json', () => { + jest.spyOn(folderManager, 'readMainJson').mockReturnValue(mainJson); + const solutions = folderManager.getAllSolutions(); + expect(solutions).toEqual(['solution.cpp', 'other.cpp']); + }); + + it('should return an empty array on error', () => { + jest.spyOn(folderManager, 'readMainJson').mockImplementation(() => { throw new Error(); }); + const solutions = folderManager.getAllSolutions(); + expect(solutions).toEqual([]); + }); + }); + + describe('getAllRuns', () => { + it('should return all runs for a given solution', () => { + jest.spyOn(folderManager, 'readMainJson').mockReturnValue(mainJson); + const runs = folderManager.getAllRuns(solutionName); + expect(runs).toEqual([runId]); + }); + + it('should return an empty array if solution does not exist', () => { + jest.spyOn(folderManager, 'readMainJson').mockReturnValue(mainJson); + const runs = folderManager.getAllRuns('nonexistent.cpp' as ISolutionName); + expect(runs).toEqual([]); + }); + + it('should return an empty array on error', () => { + jest.spyOn(folderManager, 'readMainJson').mockImplementation(() => { throw new Error(); }); + const runs = folderManager.getAllRuns(solutionName); + expect(runs).toEqual([]); + }); + }); + + describe('getAllTestResults', () => { + const runFolderPath = `${baseDir}/results/${runId}` as IRunDirPath; + + it('should return an empty array if the run folder does not exist', () => { + mockFileManager.exists.mockReturnValue(false); + const results = folderManager.getAllTestResults(runId); + expect(mockFileManager.exists).toHaveBeenCalledWith(runFolderPath); + expect(results).toEqual([]); + }); + + it('should return all parsed test results', () => { + const testResult1: IJsonTestResult = { testCase: 1, lastResult: 'Passed' }; + const testResult2: IJsonTestResult = { testCase: 2, lastResult: 'WA' }; + mockFileManager.exists.mockReturnValue(true); + mockFileManager.listDirectory.mockReturnValue(['test_1.json', 'test_2.json', 'other.txt']); + mockFileManager.readFile + .mockReturnValueOnce(JSON.stringify(testResult1)) + .mockReturnValueOnce(JSON.stringify(testResult2)); + + const results = folderManager.getAllTestResults(runId); + + expect(results).toEqual([testResult1, testResult2]); + }); + + it('should handle parsing errors gracefully', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + const testResult1: IJsonTestResult = { testCase: 1, lastResult: 'Passed' }; + mockFileManager.exists.mockReturnValue(true); + mockFileManager.listDirectory.mockReturnValue(['test_1.json', 'test_2_bad.json']); + mockFileManager.readFile + .mockReturnValueOnce(JSON.stringify(testResult1)) + .mockReturnValueOnce('invalid json'); + + const results = folderManager.getAllTestResults(runId); + + expect(results).toEqual([testResult1]); + expect(consoleErrorSpy).toHaveBeenCalled(); + consoleErrorSpy.mockRestore(); + }); + }); }); - it('should return the array of runs for a given solution', () => { - // Arrange - const mainJsonContent = { [solutionName]: ['runA1', 'runA2'], 'problemB.cpp': ['runB1'] }; - mockFileManager.exists.mockReturnValue(true); - mockFileManager.readFile.mockReturnValue(JSON.stringify(mainJsonContent)); - - // Act - const runs = folderManager.getRuns(solutionName); - - // Assert - expect(runs).toEqual(['runA1', 'runA2']); + describe('Deletion Methods', () => { + const solutionName = 'solution.cpp' as ISolutionName; + const runId = 'run-id' as IRunId; + const otherRunId = 'other-run-id' as IRunId; + const mainJson: IMainJson = { [solutionName]: [runId, otherRunId] }; + const mainJsonPath = `${baseDir}/results/main.json` as IMainJsonPath; + + describe('deleteSolution', () => { + it('should delete all runs and the solution from main.json', () => { + jest.spyOn(folderManager, 'readMainJson').mockReturnValue(JSON.parse(JSON.stringify(mainJson))); // deep copy + jest.spyOn(folderManager, 'deleteRun').mockImplementation(() => { }); + + folderManager.deleteSolution(solutionName); + + expect(folderManager.deleteRun).toHaveBeenCalledWith(runId); + expect(folderManager.deleteRun).toHaveBeenCalledWith(otherRunId); + const expectedJson: IMainJson = {}; + expect(mockFileManager.writeFile).toHaveBeenCalledWith(mainJsonPath, JSON.stringify(expectedJson, null, 4)); + }); + + it('should not throw if solution does not exist', () => { + jest.spyOn(folderManager, 'readMainJson').mockReturnValue({}); + expect(() => folderManager.deleteSolution(solutionName)).not.toThrow(); + expect(mockFileManager.writeFile).not.toHaveBeenCalled(); + }); + }); + + describe('deleteRun', () => { + it('should delete the run folder and remove the run from main.json', () => { + const runFolderPath = `${baseDir}/results/${runId}`; + mockFileManager.exists.mockReturnValue(true); + jest.spyOn(folderManager, 'readMainJson').mockReturnValue(JSON.parse(JSON.stringify(mainJson))); + jest.spyOn(folderManager, 'getAllSolutions').mockReturnValue([solutionName]); + + + folderManager.deleteRun(runId); + + expect(mockFileManager.deleteDirectory).toHaveBeenCalledWith(runFolderPath); + const expectedJson: IMainJson = { [solutionName]: [otherRunId] }; + expect(mockFileManager.writeFile).toHaveBeenCalledWith(mainJsonPath, JSON.stringify(expectedJson, null, 4)); + }); + + it('should not fail if run folder does not exist', () => { + mockFileManager.exists.mockReturnValue(false); + jest.spyOn(folderManager, 'readMainJson').mockReturnValue(JSON.parse(JSON.stringify(mainJson))); + jest.spyOn(folderManager, 'getAllSolutions').mockReturnValue([solutionName]); + + folderManager.deleteRun(runId); + + expect(mockFileManager.deleteDirectory).not.toHaveBeenCalled(); + const expectedJson: IMainJson = { [solutionName]: [otherRunId] }; + expect(mockFileManager.writeFile).toHaveBeenCalledWith(mainJsonPath, JSON.stringify(expectedJson, null, 4)); + }); + }); + + describe('deleteTestResult', () => { + it('should delete the test result file', () => { + const testCaseNo = 1; + const testCasePath = `${baseDir}/results/${runId}/test_${testCaseNo}.json`; + mockFileManager.exists.mockReturnValue(true); + + folderManager.deleteTestResult(runId, testCaseNo); + + expect(mockFileManager.deleteFile).toHaveBeenCalledWith(testCasePath); + }); + + it('should not fail if the test result file does not exist', () => { + mockFileManager.exists.mockReturnValue(false); + expect(() => folderManager.deleteTestResult(runId, 1)).not.toThrow(); + expect(mockFileManager.deleteFile).not.toHaveBeenCalled(); + }); + }); }); - }); - describe('getTestResults', () => { - const solutionName = 'problemA.cpp'; - const runId = 'run1'; - const runFolderPath = `${baseDir}/results/${solutionName}/${runId}`; + describe('updateTestResult', () => { + const runId = 'run-id' as IRunId; + const newJsonResult: IJsonTestResult = { testCase: 1, lastResult: 'WA' }; + const testCasePath = `${baseDir}/results/${runId}/test_1.json`; - it('should return an empty array if the run folder path does not exist', () => { - // Arrange - mockFileManager.exists.mockReturnValue(false); + it('should update the test result file if it exists', () => { + mockFileManager.exists.mockReturnValue(true); - // Act - const results = folderManager.getTestResults(solutionName, runId); + folderManager.updateTestResult(runId, newJsonResult); - // Assert - expect(mockFileManager.exists).toHaveBeenCalledWith(runFolderPath); - expect(results).toEqual([]); - }); + expect(mockFileManager.exists).toHaveBeenCalledWith(testCasePath); + expect(mockFileManager.writeFile).toHaveBeenCalledWith(testCasePath, JSON.stringify(newJsonResult, null, 4)); + }); - it('should read, parse, and return all test result files in a run folder', () => { - // Arrange - const testResult1: IJsonTestResult = { testCase: 1, lastResult: 'Passed' }; - const testResult2: IJsonTestResult = { testCase: 2, lastResult: 'WA' }; - - mockFileManager.exists.mockReturnValue(true); - mockFileManager.listDirectory.mockReturnValue(['test_1.json', 'test_2.json', 'other_file.txt']); - mockFileManager.readFile - .mockReturnValueOnce(JSON.stringify(testResult1)) - .mockReturnValueOnce(JSON.stringify(testResult2)); - - // Act - const results = folderManager.getTestResults(solutionName, runId); - - // Assert - expect(mockFileManager.listDirectory).toHaveBeenCalledWith(runFolderPath); - expect(mockFileManager.readFile).toHaveBeenCalledTimes(2); - expect(mockFileManager.readFile).toHaveBeenCalledWith(`${runFolderPath}/test_1.json`); - expect(mockFileManager.readFile).toHaveBeenCalledWith(`${runFolderPath}/test_2.json`); - expect(results).toEqual([testResult1, testResult2]); - }); - - it('should skip files that fail to parse and continue', () => { - // Arrange - const testResult1: IJsonTestResult = { testCase: 1, lastResult: 'Passed' }; - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - - mockFileManager.exists.mockReturnValue(true); - mockFileManager.listDirectory.mockReturnValue(['test_1.json', 'test_2_bad.json']); - mockFileManager.readFile - .mockReturnValueOnce(JSON.stringify(testResult1)) - .mockReturnValueOnce('invalid json'); - - // Act - const results = folderManager.getTestResults(solutionName, runId); - - // Assert - expect(results).toEqual([testResult1]); - expect(results.length).toBe(1); - expect(consoleErrorSpy).toHaveBeenCalled(); - - consoleErrorSpy.mockRestore(); - }); - }); + it('should not write the file if the test case does not exist', () => { + mockFileManager.exists.mockReturnValue(false); - describe('getTempDir', () => { - it('should return the correct temporary directory path', () => { - // Arrange - const expectedPath = `${baseDir}/temp`; + folderManager.updateTestResult(runId, newJsonResult); - // Act - const tempDir = folderManager.getTempDir(); - - // Assert - expect(mockedPath.join).toHaveBeenCalledWith(baseDir, 'temp'); - expect(tempDir).toBe(expectedPath); + expect(mockFileManager.exists).toHaveBeenCalledWith(testCasePath); + expect(mockFileManager.writeFile).not.toHaveBeenCalled(); + }); }); - }); - - describe('cleanup', () => { - it('should call the file manager cleanup method with the provided paths', () => { - // Arrange - const pathsToClean = ['/dir1', '/dir2']; - - // Act - folderManager.cleanup(pathsToClean); - // Assert - expect(mockFileManager.cleanup).toHaveBeenCalledWith(pathsToClean); + describe('cleanup', () => { + it('should call fileManager.cleanup with the given paths', () => { + const paths = ['/path1', '/path2']; + folderManager.cleanup(paths); + expect(mockFileManager.cleanup).toHaveBeenCalledWith(paths); + }); }); - }); -}); \ No newline at end of file +}); diff --git a/src/test/services/TestRunnerService.test.ts b/src/test/services/TestRunnerService.test.ts index f56f9de..2b70307 100644 --- a/src/test/services/TestRunnerService.test.ts +++ b/src/test/services/TestRunnerService.test.ts @@ -1,22 +1,44 @@ import { ITestRunnerService } from '../../core/Interfaces/services'; import { ITestRunResult } from '../../core/Interfaces/datastructures'; import { ICPSTFolderManager, ITestRunner } from '../../core/Interfaces/classes'; -import { TestRunnerService } from '../../core/Services/TestRunnerService'; // Adjust the import path accordingly +import { TestRunnerService } from '../../core/Services/TestRunnerService'; // Mock the dependencies (interfaces) by creating mock objects const mockCpstFolderManager: jest.Mocked = { - setup: jest.fn(), + getTempDirPath: jest.fn(), + getSolutionName: jest.fn(), + getRunId: jest.fn(), + getResultDirPath: jest.fn(), + getMainJsonPath: jest.fn(), + getRunResultDirPath: jest.fn(), + getTestCaseResultPath: jest.fn(), + getTestPaths: jest.fn(), + generateNonce: jest.fn(), + createTempDir: jest.fn(), + createResultDir: jest.fn(), + createRunFolder: jest.fn(), + addSolutionToMainJson: jest.fn(), + addRunToMainJson: jest.fn(), + addSolution: jest.fn(), + addRun: jest.fn(), + readMainJson: jest.fn(), initializeTestRun: jest.fn(), saveResult: jest.fn(), - getSolutions: jest.fn(), - getRuns: jest.fn(), - getTestResults: jest.fn(), - getTempDir: jest.fn(), + getAllSolutions: jest.fn(), + getAllRuns: jest.fn(), + getAllTestResults: jest.fn(), + deleteRun: jest.fn(), + deleteSolution: jest.fn(), + deleteTestResult: jest.fn(), + updateTestResult: jest.fn(), cleanup: jest.fn(), + getTestCaseNo: jest.fn(), + getTestCaseResultData: jest.fn(), }; const mockTestRunner: jest.Mocked = { run: jest.fn(), + runWithInput: jest.fn() }; describe('TestRunnerService', () => { @@ -44,14 +66,14 @@ describe('TestRunnerService', () => { output: 'test output', }; - mockCpstFolderManager.getTempDir.mockReturnValue(tempDir); + mockCpstFolderManager.getTempDirPath.mockReturnValue(tempDir as any); // Using `as any` because of branded types mockTestRunner.run.mockResolvedValue(expectedTestResult); // Act const result = await testRunnerService.runSingleTest(solutionExec, generatorExec, checkerExec); // Assert - expect(mockCpstFolderManager.getTempDir).toHaveBeenCalledTimes(1); + expect(mockCpstFolderManager.getTempDirPath).toHaveBeenCalledTimes(1); expect(mockTestRunner.run).toHaveBeenCalledTimes(1); expect(mockTestRunner.run).toHaveBeenCalledWith(tempDir, solutionExec, generatorExec, checkerExec); expect(result).toEqual(expectedTestResult); @@ -71,14 +93,14 @@ describe('TestRunnerService', () => { input: 'large input', }; - mockCpstFolderManager.getTempDir.mockReturnValue(tempDir); + mockCpstFolderManager.getTempDirPath.mockReturnValue(tempDir as any); mockTestRunner.run.mockResolvedValue(expectedFailureResult); // Act const result = await testRunnerService.runSingleTest(solutionExec, generatorExec, checkerExec); // Assert - expect(mockCpstFolderManager.getTempDir).toHaveBeenCalledTimes(1); + expect(mockCpstFolderManager.getTempDirPath).toHaveBeenCalledTimes(1); expect(mockTestRunner.run).toHaveBeenCalledWith(tempDir, solutionExec, generatorExec, checkerExec); expect(result).toEqual(expectedFailureResult); }); @@ -91,15 +113,15 @@ describe('TestRunnerService', () => { const checkerExec = './check.out'; const expectedError = new Error('Test runner crashed'); - mockCpstFolderManager.getTempDir.mockReturnValue(tempDir); + mockCpstFolderManager.getTempDirPath.mockReturnValue(tempDir as any); mockTestRunner.run.mockRejectedValue(expectedError); // Act & Assert await expect(testRunnerService.runSingleTest(solutionExec, generatorExec, checkerExec)) .rejects.toThrow('Test runner crashed'); - expect(mockCpstFolderManager.getTempDir).toHaveBeenCalledTimes(1); + expect(mockCpstFolderManager.getTempDirPath).toHaveBeenCalledTimes(1); expect(mockTestRunner.run).toHaveBeenCalledWith(tempDir, solutionExec, generatorExec, checkerExec); }); }); -}); \ No newline at end of file +}); diff --git a/src/webview.css b/src/webview.css new file mode 100644 index 0000000..b30ddcb --- /dev/null +++ b/src/webview.css @@ -0,0 +1,153 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + padding: 15px; + color: var(--vscode-editor-foreground); + background-color: var(--vscode-editor-background); +} + +.hidden { + display: none; +} + +#file-info { + margin-bottom: 20px; + font-style: italic; + color: var(--vscode-descriptionForeground); + padding: 10px; + background-color: var(--vscode-textBlockQuote-background); + border-left: 4px solid var(--vscode-textLink-foreground); +} + +button { + width: 100%; + padding: 10px; + border: 1px solid var(--vscode-button-border, transparent); + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + cursor: pointer; + text-align: center; + margin-top: 10px; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +button:hover { + background-color: var(--vscode-button-hoverBackground); +} + +input[type="number"] { + width: 100%; + padding: 8px; + border: 1px solid var(--vscode-input-border); + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + box-sizing: border-box; + border-radius: 4px; +} + +.run-item { + padding: 10px; + cursor: pointer; + border: 1px solid var(--vscode-sideBar-border); + margin-bottom: 5px; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.run-item:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.test-case-item { + margin-bottom: 15px; + padding: 15px; + border: 1px solid var(--vscode-peekView-border); + border-radius: 4px; + background-color: var(--vscode-list-inactiveSelectionBackground); +} + +.test-case-item p { + margin: 5px 0; +} + +.test-case-item strong { + color: var(--vscode-editor-foreground); +} + +.test-case-item details { + margin-top: 10px; +} + +.test-case-item summary { + cursor: pointer; + font-weight: bold; +} + +pre { + white-space: pre-wrap; + word-wrap: break-word; + background-color: var(--vscode-textBlockQuote-background); + padding: 10px; + border-radius: 4px; + border: 1px solid var(--vscode-sideBar-border); +} + +#rerun-selected-button { + position: fixed; + bottom: 20px; + right: 20px; + width: auto; + padding: 10px 20px; + border-radius: 25px; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); + z-index: 1000; + font-weight: bold; +} + +#summary-view { + margin-bottom: 20px; + padding: 10px; + border: 1px solid var(--vscode-peekView-border); + border-radius: 4px; + background-color: var(--vscode-list-inactiveSelectionBackground); +} + +.summary-grid { + display: flex; + flex-wrap: wrap; + gap: 15px; + justify-content: center; +} + +.summary-item { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 10px; + border-radius: 4px; + background-color: var(--vscode-textBlockQuote-background); + cursor: default; +} + +.summary-icon { + display: flex; + align-items: center; +} + +.summary-count { + font-size: 1.2em; + font-weight: bold; +} + +.status-success .summary-icon { + color: var(--vscode-testing-iconPassed); +} +.status-error .summary-icon { + color: var(--vscode-testing-iconFailed); +} +.status-warning .summary-icon { + color: var(--vscode-testing-iconQueued); +} +.status-info .summary-icon { + color: var(--vscode-testing-iconUnset); +} \ No newline at end of file diff --git a/src/webview.html b/src/webview.html index 6ea1ec1..90be75b 100644 --- a/src/webview.html +++ b/src/webview.html @@ -5,27 +5,7 @@ Stress Tester - +
@@ -41,99 +21,19 @@ +
+
+
+
+ - + diff --git a/src/webview.js b/src/webview.js new file mode 100644 index 0000000..80da191 --- /dev/null +++ b/src/webview.js @@ -0,0 +1,268 @@ +const vscode = acquireVsCodeApi(); +const initialView = document.getElementById('initial-view'); +const actionsView = document.getElementById('actions-view'); +const generateView = document.getElementById('generate-view'); +const runView = document.getElementById('run-view'); +const fileInfo = document.getElementById('file-info'); +const summaryView = document.getElementById('summary-view'); +const resultsView = document.getElementById('results-view'); +const numTestsInput = document.getElementById('num-tests-input'); +const runsView = document.getElementById('runs-view'); +const testCasesView = document.getElementById('test-cases-view'); +const rerunContainer = document.getElementById('rerun-container'); +const rerunButton = document.getElementById('rerun-selected-button'); +const rerunCount = document.getElementById('rerun-count'); + +let selectedTestsByRun = {}; +let isReRunning = false; + +function getIconForStatus(status) { + const iconProps = `width="20" height="20" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"`; + let iconPath; + + switch (status.toLowerCase().replace(/\s/g, '')) { + case 'accepted': + case 'passed': + iconPath = ``; + break; + case 'wronganswer': + iconPath = ``; + break; + case 'timelimitexceeded': + iconPath = ``; + break; + case 'runtimeerror': + iconPath = ``; + break; + case 'memorylimitexceeded': + iconPath = ``; + break; + default: + iconPath = ``; + } + return `
${iconPath}
`; +} + +function getStatusClass(status) { + switch (status.toLowerCase().replace(/\s/g, '')) { + case 'accepted': + case 'passed': + return 'status-success'; + case 'wronganswer': + case 'runtimeerror': + return 'status-error'; + case 'timelimitexceeded': + case 'memorylimitexceeded': + return 'status-warning'; + default: + return 'status-info'; + } +} + +function getTestCaseDetailsHtml(result) { + const status = result.status || result.lastResult; + const time = result.time || result.execTime; + const memory = result.memory || result.memoryUsed; + + const safe = (str) => { + const div = document.createElement('div'); + div.textContent = str || ''; + return div.innerHTML; + }; + + return ` +

Result: ${status}

+

Time: ${time} ms

+

Memory: ${memory} KB

+
+ Details +

Input:

${safe(result.input)}
+

Output:

${safe(result.output || result.userOutput)}
+ ${result.reason ? `

Reason:

${safe(result.reason)}
` : ''} + ${result.message ? `

Message:

${safe(result.message)}
` : ''} +
+ `; +} + +function getResultMarkup(result) { + return `

Latest Result:

${getTestCaseDetailsHtml(result)}
`; +} + +function updateRerunButton() { + const totalSelected = Object.values(selectedTestsByRun).reduce((sum, tcs) => sum + tcs.length, 0); + rerunCount.textContent = totalSelected; + rerunContainer.classList.toggle('hidden', totalSelected === 0); +} + +function resetRerunState() { + isReRunning = false; + rerunButton.disabled = false; + const originalText = `Re-run Selected Tests (${rerunCount.textContent})`; + rerunButton.innerHTML = originalText; +} + +document.getElementById('generate-button').addEventListener('click', () => { + vscode.postMessage({ command: 'generate' }); +}); + +document.getElementById('run-button').addEventListener('click', () => { + const numTests = parseInt(numTestsInput.value, 10); + if (isNaN(numTests) || numTests <= 0) { + numTestsInput.style.borderColor = 'red'; + return; + } + numTestsInput.style.borderColor = 'var(--vscode-input-border)'; + vscode.postMessage({ command: 'run', numTests: numTests }); +}); + +document.getElementById('view-runs-button').addEventListener('click', () => { + vscode.postMessage({ command: 'get-runs' }); +}); + +rerunButton.addEventListener('click', () => { + const rerunTotalCount = Object.values(selectedTestsByRun).reduce((sum, tcs) => sum + tcs.length, 0); + if (rerunTotalCount === 0) {return;} + + isReRunning = true; + rerunButton.disabled = true; + rerunButton.textContent = 'Re-running...'; + summaryView.innerHTML = ''; + resultsView.innerHTML = ''; + + vscode.postMessage({ command: 'rerun-tests', testCases: selectedTestsByRun }); +}); + +testCasesView.addEventListener('change', event => { + if (event.target.classList.contains('test-case-checkbox')) { + const checkbox = event.target; + const runId = checkbox.dataset.runId; + const testCaseData = JSON.parse(decodeURIComponent(checkbox.dataset.testCase)); + + if (!selectedTestsByRun[runId]) { + selectedTestsByRun[runId] = []; + } + + if (checkbox.checked) { + if (!selectedTestsByRun[runId].some(tc => tc.testCase === testCaseData.testCase)) { + selectedTestsByRun[runId].push(testCaseData); + } + } else { + selectedTestsByRun[runId] = selectedTestsByRun[runId].filter(tc => tc.testCase !== testCaseData.testCase); + if (selectedTestsByRun[runId].length === 0) { + delete selectedTestsByRun[runId]; + } + } + updateRerunButton(); + } +}); + + +window.addEventListener('message', event => { + const message = event.data; + switch (message.command) { + case 'show-initial-state': + initialView.classList.remove('hidden'); + actionsView.classList.add('hidden'); + break; + case 'update-view': + initialView.classList.add('hidden'); + actionsView.classList.remove('hidden'); + fileInfo.innerHTML = 'Testing: ' + message.solutionFilename + ''; + generateView.classList.toggle('hidden', message.testFileExists); + runView.classList.toggle('hidden', !message.testFileExists); + break; + case 'test-running': + resultsView.innerHTML = '

Running tests...

'; + summaryView.innerHTML = ''; + runsView.innerHTML = ''; + testCasesView.innerHTML = ''; + selectedTestsByRun = {}; + updateRerunButton(); + break; + case 'testResult': + if (isReRunning) { + const testCaseItem = document.getElementById(`test-case-item-${message.runId}-${message.testCase}`); + if (testCaseItem) { + const detailsContainer = testCaseItem.querySelector('.test-case-details'); + if (detailsContainer) { + if (message.status === 'Running') { + detailsContainer.innerHTML = `

Result: Re-running...

`; + } else { + detailsContainer.innerHTML = getTestCaseDetailsHtml(message); + } + } + } + } else { + if (message.status === 'Running') { + resultsView.innerHTML = `

Running test #${message.testCase}...

`; + } else { + resultsView.innerHTML = getResultMarkup(message); + } + } + break; + case 'summary': + let summaryHtml = '
'; + for (const [status, count] of Object.entries(message.results)) { + const statusClass = getStatusClass(status); + summaryHtml += `
+ ${getIconForStatus(status)} + ${count} +
`; + } + summaryHtml += '
'; + summaryView.innerHTML = summaryHtml; + if (isReRunning) { + resultsView.innerHTML = ''; + resetRerunState(); + } + break; + case 'show-runs': + resultsView.innerHTML = ''; + testCasesView.innerHTML = ''; + let runsHtml = '

Past Runs:

'; + if (message.runs.length === 0) { + runsHtml += '

No runs found for this solution.

'; + } else { + message.runs.forEach(runId => { + runsHtml += `
${runId}
`; + }); + } + runsView.innerHTML = runsHtml; + document.querySelectorAll('.run-item').forEach(item => { + item.addEventListener('click', event => { + const runId = event.target.getAttribute('data-run-id'); + vscode.postMessage({ command: 'get-test-cases', runId: runId }); + }); + }); + break; + case 'show-test-cases': + let testCasesHtml = `

Test Cases for Run: ${message.runId}

`; + if (message.testCases.length === 0) { + testCasesHtml += '

No test cases found for this run.

'; + } else { + message.testCases.forEach(tc => { + const testCaseData = encodeURIComponent(JSON.stringify(tc)); + const isChecked = selectedTestsByRun[message.runId]?.some(selectedTc => selectedTc.testCase === tc.testCase); + testCasesHtml += `
+

+ + Test Case #${tc.testCase} +

+
+ ${getTestCaseDetailsHtml(tc)} +
+
`; + }); + } + testCasesView.innerHTML = testCasesHtml; + break; + } +}); + +vscode.postMessage({ command: 'webview-ready' });