From 443e312f48f210131738883dd255b1f06e48fd38 Mon Sep 17 00:00:00 2001 From: 2077DevWave <2077devwave@gmail.com> Date: Wed, 17 Sep 2025 16:35:10 -0400 Subject: [PATCH 01/12] add GET, DELETE capability into CPSTFolderManager --- package.json | 2 +- src/core/CompileAndRun/TestRunner.ts | 16 +- src/core/Interfaces/classes.ts | 179 +++++- src/core/Interfaces/datastructures.ts | 30 +- src/core/Interfaces/services.ts | 13 + src/core/Managers/CPSTFolderManager.ts | 164 +++-- src/core/Managers/FileManager.ts | 12 + src/core/Services/CompilationService.ts | 2 +- src/core/Services/OrchestrationService.ts | 4 +- src/core/Services/ResultService.ts | 8 +- src/core/Services/TestRunnerService.ts | 7 +- src/test/CompileAndRun/TestRunner.test.ts | 2 + src/test/Managers/CPSTFolderManager.test.ts | 675 +++++++++++--------- src/test/services/TestRunnerService.test.ts | 45 +- 14 files changed, 752 insertions(+), 407 deletions(-) diff --git a/package.json b/package.json index daa8cde..35379d3 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "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/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..3b292fa 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. @@ -164,6 +174,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 +191,173 @@ 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; + + 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. */ - setup(solutionPath: string): ITestPaths; + createRunFolder(runId: IRunId): void; + /** - * 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. + * Adds a solution to the main JSON file. + * @param solutionName The name of the solution to add. */ - initializeTestRun(solutionName: string, runFolderName: string, mainJsonPath: string): void; + 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. + */ + readMainJson(): IMainJson; + + /** + * 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: 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; + /** * 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..6d0dc29 100644 --- a/src/core/Interfaces/services.ts +++ b/src/core/Interfaces/services.ts @@ -53,6 +53,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; } /** diff --git a/src/core/Managers/CPSTFolderManager.ts b/src/core/Managers/CPSTFolderManager.ts index 55d54cb..e9b4523 100644 --- a/src/core/Managers/CPSTFolderManager.ts +++ b/src/core/Managers/CPSTFolderManager.ts @@ -1,35 +1,91 @@ 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'; export class CPSTFolderManager implements ICPSTFolderManager { constructor( private readonly _fileManager: IFileManager, private readonly _baseDir: string - ) {} + ) { + this.createTempDir(); + this.createResultDir(); + } + + public getSolutionName(solutionPath: ISolutionPath): ISolutionName { + return path.basename(solutionPath) as ISolutionName; + } + + public getRunId(runFolderPath: IRunDirPath): IRunId { + return path.basename(runFolderPath) as IRunId; + } + + public getTempDirPath(): ITempDirPath { + return path.join(this._baseDir, 'temp') as ITempDirPath; + } + + public getResultDirPath(): IResultDirPath{ + return path.join(this._baseDir, 'results') as IResultDirPath; + } + + public getMainJsonPath(): IMainJsonPath{ + return path.join(this.getResultDirPath(), 'main.json') as IMainJsonPath; + } + + public getRunResultDirPath(runId: IRunId): IRunDirPath{ + return path.join(this.getResultDirPath(), runId) as IRunDirPath; + } - public setup(solutionPath: string): ITestPaths { - const tempDir = path.join(this._baseDir, 'temp'); - this._fileManager.createDirectory(tempDir); + public getTestCaseResultPath(runId: IRunId, testCaseNo: number): ITestCaseJsonPath{ + return path.join(this.getRunResultDirPath(runId), `test_${testCaseNo}.json`) as ITestCaseJsonPath; + } + + public getTestPaths(solutionPath: ISolutionPath, runId: IRunId): ITestPaths{ + return {tempDir: this.getTempDirPath(), resultsDir: this.getResultDirPath(), solutionPath: solutionPath, runFolderPath: this.getRunResultDirPath(runId), mainJsonPath: this.getMainJsonPath()}; + } - const resultsDir = path.join(this._baseDir, 'results'); - this._fileManager.createDirectory(resultsDir); + public generateNonce(){ + return new Date().toISOString().replace(/[:.]/g, '-'); + } - 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 createTempDir(): void{ + this._fileManager.createDirectory(this.getTempDirPath()); + } + + public createResultDir(): void{ + this._fileManager.createDirectory(this.getResultDirPath()); + } - const mainJsonPath = path.join(resultsDir, 'main.json'); + 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] = []; + } + this._fileManager.writeFile(this.getMainJsonPath(), JSON.stringify(mainJson, null, 4)); + } - return { tempDir, resultsDir, solutionDir, runFolderPath, mainJsonPath }; + 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 initializeTestRun(solutionName: string, runFolderName: string, mainJsonPath: string): void { - let mainJson: { [key: string]: string[] } = {}; + 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)); @@ -37,47 +93,38 @@ export class CPSTFolderManager implements ICPSTFolderManager { mainJson = {}; } } + return mainJson; + } - if (!mainJson[solutionName]) { - mainJson[solutionName] = []; - } - mainJson[solutionName].push(runFolderName); - this._fileManager.writeFile(mainJsonPath, JSON.stringify(mainJson, null, 4)); + public initializeTestRun(solutionName: ISolutionName, runId: IRunId): void { + this.addRun(solutionName,runId); } - public saveResult(runFolderPath: string, result: IJsonTestResult): void { - const resultFilePath = path.join(runFolderPath, `test_${result.testCase}.json`); + 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 getSolutions(): string[] { - const mainJsonPath = path.join(this._baseDir, 'results', 'main.json'); - if (!this._fileManager.exists(mainJsonPath)) { - return []; - } + 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 +145,41 @@ 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 cleanup(paths: string[]): void { 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..d7cf1fa 100644 --- a/src/core/Services/CompilationService.ts +++ b/src/core/Services/CompilationService.ts @@ -10,7 +10,7 @@ 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; } diff --git a/src/core/Services/OrchestrationService.ts b/src/core/Services/OrchestrationService.ts index 9974813..532a606 100644 --- a/src/core/Services/OrchestrationService.ts +++ b/src/core/Services/OrchestrationService.ts @@ -18,7 +18,7 @@ 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; } @@ -54,6 +54,6 @@ export class OrchestrationService implements IOrchestrationService { this._reporter.reportProgress(progress); } - this._cpstFolderManager.cleanup([this._cpstFolderManager.getTempDir()]); + this._cpstFolderManager.cleanup([this._cpstFolderManager.getTempDirPath()]); } } diff --git a/src/core/Services/ResultService.ts b/src/core/Services/ResultService.ts index efbf739..e0fe700 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,9 +10,9 @@ 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 { 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/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..4df2066 100644 --- a/src/test/Managers/CPSTFolderManager.test.ts +++ b/src/test/Managers/CPSTFolderManager.test.ts @@ -1,348 +1,399 @@ 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) - ); - }); - - 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) - ); - }); - - 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) - ); - }); - - 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) - ); - }); - }); - - 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([]); - }); + let folderManager: CPSTFolderManager; + const baseDir = '/cpst'; + const mockedPath = path as jest.Mocked; - it('should return an empty array if main.json is malformed', () => { - // Arrange - mockFileManager.exists.mockReturnValue(true); - mockFileManager.readFile.mockReturnValue('invalid json'); + beforeEach(() => { + // Setup and reset before each test + jest.clearAllMocks(); - // Act - const solutions = folderManager.getSolutions(); + // Mock path functions for deterministic behavior + mockedPath.join.mockImplementation((...args) => args.join('/')); + mockedPath.basename.mockImplementation(p => p.split('/').pop() || ''); - // Assert - expect(solutions).toEqual([]); + // We instantiate after mocking path because the constructor uses it. + folderManager = new CPSTFolderManager(mockFileManager, baseDir); }); - 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('constructor', () => { + it('should create temp and results directories on initialization', () => { + // The constructor is called in beforeEach, so we just need to assert + expect(mockFileManager.createDirectory).toHaveBeenCalledWith(`${baseDir}/temp`); + expect(mockFileManager.createDirectory).toHaveBeenCalledWith(`${baseDir}/results`); + expect(mockFileManager.createDirectory).toHaveBeenCalledTimes(2); + }); }); - }); - - 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('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'); - - // Act - const runs = folderManager.getRuns(solutionName); + 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); - // Assert - expect(runs).toEqual([]); - }); + expect(folderManager.generateNonce()).toBe(expectedNonce); - 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([]); + jest.useRealTimers(); + }); }); - 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('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('getTestResults', () => { - const solutionName = 'problemA.cpp'; - const runId = 'run1'; - const runFolderPath = `${baseDir}/results/${solutionName}/${runId}`; - - it('should return an empty array if the run folder path does not exist', () => { - // Arrange - mockFileManager.exists.mockReturnValue(false); - - // Act - const results = folderManager.getTestResults(solutionName, runId); - // Assert - expect(mockFileManager.exists).toHaveBeenCalledWith(runFolderPath); - expect(results).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 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]); + 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 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(); + 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(); + }); + }); }); - }); - describe('getTempDir', () => { - it('should return the correct temporary directory path', () => { - // Arrange - const expectedPath = `${baseDir}/temp`; - - // Act - const tempDir = folderManager.getTempDir(); - - // Assert - expect(mockedPath.join).toHaveBeenCalledWith(baseDir, 'temp'); - expect(tempDir).toBe(expectedPath); + 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('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..b436332 100644 --- a/src/test/services/TestRunnerService.test.ts +++ b/src/test/services/TestRunnerService.test.ts @@ -1,22 +1,41 @@ 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(), cleanup: jest.fn(), }; const mockTestRunner: jest.Mocked = { run: jest.fn(), + runWithInput: jest.fn() }; describe('TestRunnerService', () => { @@ -44,14 +63,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 +90,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 +110,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 +}); From 9edeafd7ae54b7c8ec9f1a3b999817ed8b4830b1 Mon Sep 17 00:00:00 2001 From: 2077DevWave <2077devwave@gmail.com> Date: Wed, 17 Sep 2025 16:46:03 -0400 Subject: [PATCH 02/12] add UPDATE functionality for result saved in .cpst folder --- src/core/Interfaces/classes.ts | 7 ++++++ src/core/Managers/CPSTFolderManager.ts | 7 ++++++ src/test/Managers/CPSTFolderManager.test.ts | 24 +++++++++++++++++++++ src/test/services/TestRunnerService.test.ts | 1 + 4 files changed, 39 insertions(+) diff --git a/src/core/Interfaces/classes.ts b/src/core/Interfaces/classes.ts index 3b292fa..7cc24dc 100644 --- a/src/core/Interfaces/classes.ts +++ b/src/core/Interfaces/classes.ts @@ -353,6 +353,13 @@ export interface ICPSTFolderManager { */ 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. diff --git a/src/core/Managers/CPSTFolderManager.ts b/src/core/Managers/CPSTFolderManager.ts index e9b4523..94abb59 100644 --- a/src/core/Managers/CPSTFolderManager.ts +++ b/src/core/Managers/CPSTFolderManager.ts @@ -182,6 +182,13 @@ export class CPSTFolderManager implements ICPSTFolderManager { } } + 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 { this._fileManager.cleanup(paths); } diff --git a/src/test/Managers/CPSTFolderManager.test.ts b/src/test/Managers/CPSTFolderManager.test.ts index 4df2066..5b5d349 100644 --- a/src/test/Managers/CPSTFolderManager.test.ts +++ b/src/test/Managers/CPSTFolderManager.test.ts @@ -389,6 +389,30 @@ describe('CPSTFolderManager', () => { }); }); + 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 update the test result file if it exists', () => { + mockFileManager.exists.mockReturnValue(true); + + folderManager.updateTestResult(runId, newJsonResult); + + expect(mockFileManager.exists).toHaveBeenCalledWith(testCasePath); + expect(mockFileManager.writeFile).toHaveBeenCalledWith(testCasePath, JSON.stringify(newJsonResult, null, 4)); + }); + + it('should not write the file if the test case does not exist', () => { + mockFileManager.exists.mockReturnValue(false); + + folderManager.updateTestResult(runId, newJsonResult); + + expect(mockFileManager.exists).toHaveBeenCalledWith(testCasePath); + expect(mockFileManager.writeFile).not.toHaveBeenCalled(); + }); + }); + describe('cleanup', () => { it('should call fileManager.cleanup with the given paths', () => { const paths = ['/path1', '/path2']; diff --git a/src/test/services/TestRunnerService.test.ts b/src/test/services/TestRunnerService.test.ts index b436332..f2c8249 100644 --- a/src/test/services/TestRunnerService.test.ts +++ b/src/test/services/TestRunnerService.test.ts @@ -30,6 +30,7 @@ const mockCpstFolderManager: jest.Mocked = { deleteRun: jest.fn(), deleteSolution: jest.fn(), deleteTestResult: jest.fn(), + updateTestResult: jest.fn(), cleanup: jest.fn(), }; From 10bb462c2631ca407e576f800fc0f300542112c3 Mon Sep 17 00:00:00 2001 From: 2077DevWave <2077devwave@gmail.com> Date: Thu, 18 Sep 2025 11:22:03 -0400 Subject: [PATCH 03/12] show previous results in webview --- src/MyPanelProvider.ts | 6 +++ src/core/Interfaces/services.ts | 11 +++++ src/core/Managers/CPSTFolderManager.ts | 13 ++++- src/core/Services/UIService.ts | 28 ++++++++++- src/extension.ts | 3 +- src/webview.html | 67 ++++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 5 deletions(-) diff --git a/src/MyPanelProvider.ts b/src/MyPanelProvider.ts index 92dc891..28d71db 100644 --- a/src/MyPanelProvider.ts +++ b/src/MyPanelProvider.ts @@ -98,6 +98,12 @@ 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; } }); diff --git a/src/core/Interfaces/services.ts b/src/core/Interfaces/services.ts index 6d0dc29..5ba00c0 100644 --- a/src/core/Interfaces/services.ts +++ b/src/core/Interfaces/services.ts @@ -121,4 +121,15 @@ 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; } diff --git a/src/core/Managers/CPSTFolderManager.ts b/src/core/Managers/CPSTFolderManager.ts index 94abb59..288c39a 100644 --- a/src/core/Managers/CPSTFolderManager.ts +++ b/src/core/Managers/CPSTFolderManager.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import { IFileManager, ICPSTFolderManager } from '../Interfaces/classes'; 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( @@ -20,11 +21,19 @@ export class CPSTFolderManager implements ICPSTFolderManager { } public getTempDirPath(): ITempDirPath { - return path.join(this._baseDir, 'temp') as ITempDirPath; + const tempPath = path.join(this._baseDir, 'temp'); + if(!this._fileManager.exists(tempPath)){ + this._fileManager.createDirectory(tempPath); + } + return tempPath as ITempDirPath; } public getResultDirPath(): IResultDirPath{ - return path.join(this._baseDir, 'results') as IResultDirPath; + const resultPath = path.join(this._baseDir, 'results'); + if(!this._fileManager.exists(resultPath)){ + this._fileManager.createDirectory(resultPath); + } + return resultPath as IResultDirPath; } public getMainJsonPath(): IMainJsonPath{ diff --git a/src/core/Services/UIService.ts b/src/core/Services/UIService.ts index 67e9338..0dd1a73 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 { 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 { @@ -82,4 +84,26 @@ export class UIService implements IUIService { await this._orchestrationService.run(solutionPath, genValPath, checkerPath, numTests); } + + 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/webview.html b/src/webview.html index 6ea1ec1..9e1abaa 100644 --- a/src/webview.html +++ b/src/webview.html @@ -25,6 +25,20 @@ color: var(--vscode-input-foreground); box-sizing: border-box; } + .run-item { + padding: 5px; + cursor: pointer; + border: 1px solid var(--vscode-sideBar-border); + margin-bottom: 3px; + } + .run-item:hover { + background-color: var(--vscode-list-hoverBackground); + } + .test-case-item { + margin-bottom: 10px; + padding: 5px; + border: 1px solid var(--vscode-peekView-border); + } @@ -41,10 +55,13 @@ +
+
+
- + diff --git a/src/webview.js b/src/webview.js new file mode 100644 index 0000000..4ec6cab --- /dev/null +++ b/src/webview.js @@ -0,0 +1,222 @@ +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 getTestCaseDetailsHtml(result) { + const status = result.status || result.lastResult; + const time = result.time || result.execTime; + const memory = result.memory || result.memoryUsed; + const input = result.input; + const output = result.output || result.userOutput; + const reason = result.reason; + const message = result.message; + + return ` +

Result: ${status}

+

Time: ${time} ms

+

Memory: ${memory} KB

+
+ Details +

Input:

${input || ''}
+

Output:

${output || ''}
+ ${reason ? `

Reason:

${reason}
` : ''} + ${message ? `

Message:

${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 = '

Test Summary

'; + for (const [status, count] of Object.entries(message.results)) { + summaryHtml += `
+ ${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' }); From 981443684f7365977d74128502db5e5a8f6b2268 Mon Sep 17 00:00:00 2001 From: 2077DevWave <2077devwave@gmail.com> Date: Thu, 18 Sep 2025 15:29:36 -0400 Subject: [PATCH 09/12] update summary result showing --- src/webview.css | 41 +++++++++++++++++++++++-------------- src/webview.js | 54 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/src/webview.css b/src/webview.css index 7f8f827..b30ddcb 100644 --- a/src/webview.css +++ b/src/webview.css @@ -106,37 +106,48 @@ pre { #summary-view { margin-bottom: 20px; - padding: 15px; + padding: 10px; border: 1px solid var(--vscode-peekView-border); border-radius: 4px; background-color: var(--vscode-list-inactiveSelectionBackground); } -#summary-view h3 { - margin-top: 0; -} - .summary-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); - gap: 10px; + display: flex; + flex-wrap: wrap; + gap: 15px; + justify-content: center; } .summary-item { display: flex; - flex-direction: column; align-items: center; - padding: 10px; + gap: 8px; + padding: 5px 10px; border-radius: 4px; background-color: var(--vscode-textBlockQuote-background); + cursor: default; } -.summary-status { - font-weight: bold; - font-size: 1.1em; +.summary-icon { + display: flex; + align-items: center; } .summary-count { - font-size: 1.5em; - color: var(--vscode-textLink-foreground); + 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.js b/src/webview.js index 4ec6cab..c867aa6 100644 --- a/src/webview.js +++ b/src/webview.js @@ -16,6 +16,49 @@ 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; @@ -156,12 +199,13 @@ window.addEventListener('message', event => { } break; case 'summary': - let summaryHtml = '

Test Summary

'; + let summaryHtml = '
'; for (const [status, count] of Object.entries(message.results)) { - summaryHtml += `
- ${status} - ${count} -
`; + const statusClass = getStatusClass(status); + summaryHtml += `
+ ${getIconForStatus(status)} + ${count} +
`; } summaryHtml += '
'; summaryView.innerHTML = summaryHtml; From 70a346f381c6a47c883bda8767fc0fabfd9b4efe Mon Sep 17 00:00:00 2001 From: 2077DevWave <2077devwave@gmail.com> Date: Fri, 19 Sep 2025 06:34:53 -0400 Subject: [PATCH 10/12] fix unit tests --- src/test/Managers/CPSTFolderManager.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/test/Managers/CPSTFolderManager.test.ts b/src/test/Managers/CPSTFolderManager.test.ts index 5b5d349..cb001f3 100644 --- a/src/test/Managers/CPSTFolderManager.test.ts +++ b/src/test/Managers/CPSTFolderManager.test.ts @@ -39,15 +39,6 @@ describe('CPSTFolderManager', () => { folderManager = new CPSTFolderManager(mockFileManager, baseDir); }); - describe('constructor', () => { - it('should create temp and results directories on initialization', () => { - // The constructor is called in beforeEach, so we just need to assert - expect(mockFileManager.createDirectory).toHaveBeenCalledWith(`${baseDir}/temp`); - expect(mockFileManager.createDirectory).toHaveBeenCalledWith(`${baseDir}/results`); - expect(mockFileManager.createDirectory).toHaveBeenCalledTimes(2); - }); - }); - describe('Path Getters', () => { it('getSolutionName should return the base name of the solution path', () => { const solutionPath = '/path/to/solution.cpp' as ISolutionPath; From cb4d40dbfce94a01779ff7742ddc5f66649a10f2 Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Fri, 19 Sep 2025 10:41:59 +0000 Subject: [PATCH 11/12] [CodeFactor] Apply fixes --- src/webview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webview.js b/src/webview.js index c867aa6..88b325b 100644 --- a/src/webview.js +++ b/src/webview.js @@ -119,7 +119,7 @@ document.getElementById('view-runs-button').addEventListener('click', () => { rerunButton.addEventListener('click', () => { const rerunTotalCount = Object.values(selectedTestsByRun).reduce((sum, tcs) => sum + tcs.length, 0); - if (rerunTotalCount === 0) return; + if (rerunTotalCount === 0) {return;} isReRunning = true; rerunButton.disabled = true; From 1423139789c128d17e7f54c92f8fe15b158416bc Mon Sep 17 00:00:00 2001 From: 2077DevWave <2077devwave@gmail.com> Date: Fri, 19 Sep 2025 06:55:25 -0400 Subject: [PATCH 12/12] fix unit tests and xss problem --- src/core/Interfaces/classes.ts | 6 ++--- src/core/Managers/CPSTFolderManager.ts | 8 +++---- src/core/Services/UIService.ts | 4 ++-- src/test/Managers/CPSTFolderManager.test.ts | 26 ++++++++++----------- src/test/services/TestRunnerService.test.ts | 6 ++--- src/webview.js | 18 +++++++------- 6 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/core/Interfaces/classes.ts b/src/core/Interfaces/classes.ts index aa31887..cbe20f4 100644 --- a/src/core/Interfaces/classes.ts +++ b/src/core/Interfaces/classes.ts @@ -347,14 +347,14 @@ export interface ICPSTFolderManager { * Gets a list of all solutions that have been tested. * @returns An array of solution names. */ - getallSolutions(): ISolutionName[]; + 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). */ - getallRuns(solutionName: ISolutionName): IRunId[]; + getAllRuns(solutionName: ISolutionName): IRunId[]; /** * Retrieves all test results for a specific test run. @@ -362,7 +362,7 @@ export interface ICPSTFolderManager { * @param runId The ID of the test run (timestamp). * @returns An array of test results. */ - getallTestResults(runId: IRunId): IJsonTestResult[]; + getAllTestResults(runId: IRunId): IJsonTestResult[]; /** * Deletes a solution and all its associated runs. diff --git a/src/core/Managers/CPSTFolderManager.ts b/src/core/Managers/CPSTFolderManager.ts index e38de4b..456a476 100644 --- a/src/core/Managers/CPSTFolderManager.ts +++ b/src/core/Managers/CPSTFolderManager.ts @@ -130,7 +130,7 @@ export class CPSTFolderManager implements ICPSTFolderManager { this._fileManager.writeFile(resultFilePath, JSON.stringify(result, null, 4)); } - public getallSolutions(): ISolutionName[] { + public getAllSolutions(): ISolutionName[] { try { const mainJson = this.readMainJson(); return Object.keys(mainJson) as ISolutionName[]; @@ -139,7 +139,7 @@ export class CPSTFolderManager implements ICPSTFolderManager { } } - public getallRuns(solutionName: ISolutionName): IRunId[] { + public getAllRuns(solutionName: ISolutionName): IRunId[] { try { const mainJson = this.readMainJson(); return mainJson[solutionName] || []; @@ -148,7 +148,7 @@ export class CPSTFolderManager implements ICPSTFolderManager { } } - public getallTestResults(runId: IRunId): IJsonTestResult[] { + public getAllTestResults(runId: IRunId): IJsonTestResult[] { const runFolderPath = this.getRunResultDirPath(runId); if (!this._fileManager.exists(runFolderPath)) { return []; @@ -191,7 +191,7 @@ export class CPSTFolderManager implements ICPSTFolderManager { } let mainJson = this.readMainJson(); - const solutionsNames = this.getallSolutions(); + const solutionsNames = this.getAllSolutions(); for(const solutionName of solutionsNames){ if (mainJson[solutionName]) { mainJson[solutionName] = mainJson[solutionName].filter(id => id !== runId); diff --git a/src/core/Services/UIService.ts b/src/core/Services/UIService.ts index e181722..4fb8149 100644 --- a/src/core/Services/UIService.ts +++ b/src/core/Services/UIService.ts @@ -116,7 +116,7 @@ export class UIService implements IUIService { return; } const solutionName = this._cpstFolderManager.getSolutionName(this._currentSolutionFile.fsPath as ISolutionPath); - const runs = this._cpstFolderManager.getallRuns(solutionName); + const runs = this._cpstFolderManager.getAllRuns(solutionName); this._reporter.reportProgress({ command: "show-runs", runs: runs, @@ -124,7 +124,7 @@ export class UIService implements IUIService { } public async getTestCasesForRun(runId: string): Promise { - const testCases = this._cpstFolderManager.getallTestResults(runId as IRunId); + const testCases = this._cpstFolderManager.getAllTestResults(runId as IRunId); this._reporter.reportProgress({ command: "show-test-cases", testCases: testCases, diff --git a/src/test/Managers/CPSTFolderManager.test.ts b/src/test/Managers/CPSTFolderManager.test.ts index cb001f3..8fe9b0c 100644 --- a/src/test/Managers/CPSTFolderManager.test.ts +++ b/src/test/Managers/CPSTFolderManager.test.ts @@ -230,46 +230,46 @@ describe('CPSTFolderManager', () => { const runId = 'run-id' as IRunId; const mainJson: IMainJson = { [solutionName]: [runId], ['other.cpp' as ISolutionName]: [] }; - describe('getallSolutions', () => { + describe('getAllSolutions', () => { it('should return all solution names from main.json', () => { jest.spyOn(folderManager, 'readMainJson').mockReturnValue(mainJson); - const solutions = folderManager.getallSolutions(); + 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(); + const solutions = folderManager.getAllSolutions(); expect(solutions).toEqual([]); }); }); - describe('getallRuns', () => { + describe('getAllRuns', () => { it('should return all runs for a given solution', () => { jest.spyOn(folderManager, 'readMainJson').mockReturnValue(mainJson); - const runs = folderManager.getallRuns(solutionName); + 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); + 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); + const runs = folderManager.getAllRuns(solutionName); expect(runs).toEqual([]); }); }); - describe('getallTestResults', () => { + 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); + const results = folderManager.getAllTestResults(runId); expect(mockFileManager.exists).toHaveBeenCalledWith(runFolderPath); expect(results).toEqual([]); }); @@ -283,7 +283,7 @@ describe('CPSTFolderManager', () => { .mockReturnValueOnce(JSON.stringify(testResult1)) .mockReturnValueOnce(JSON.stringify(testResult2)); - const results = folderManager.getallTestResults(runId); + const results = folderManager.getAllTestResults(runId); expect(results).toEqual([testResult1, testResult2]); }); @@ -297,7 +297,7 @@ describe('CPSTFolderManager', () => { .mockReturnValueOnce(JSON.stringify(testResult1)) .mockReturnValueOnce('invalid json'); - const results = folderManager.getallTestResults(runId); + const results = folderManager.getAllTestResults(runId); expect(results).toEqual([testResult1]); expect(consoleErrorSpy).toHaveBeenCalled(); @@ -338,7 +338,7 @@ describe('CPSTFolderManager', () => { 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]); + jest.spyOn(folderManager, 'getAllSolutions').mockReturnValue([solutionName]); folderManager.deleteRun(runId); @@ -351,7 +351,7 @@ describe('CPSTFolderManager', () => { 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]); + jest.spyOn(folderManager, 'getAllSolutions').mockReturnValue([solutionName]); folderManager.deleteRun(runId); diff --git a/src/test/services/TestRunnerService.test.ts b/src/test/services/TestRunnerService.test.ts index beff856..2b70307 100644 --- a/src/test/services/TestRunnerService.test.ts +++ b/src/test/services/TestRunnerService.test.ts @@ -24,9 +24,9 @@ const mockCpstFolderManager: jest.Mocked = { readMainJson: jest.fn(), initializeTestRun: jest.fn(), saveResult: jest.fn(), - getallSolutions: jest.fn(), - getallRuns: jest.fn(), - getallTestResults: jest.fn(), + getAllSolutions: jest.fn(), + getAllRuns: jest.fn(), + getAllTestResults: jest.fn(), deleteRun: jest.fn(), deleteSolution: jest.fn(), deleteTestResult: jest.fn(), diff --git a/src/webview.js b/src/webview.js index 88b325b..80da191 100644 --- a/src/webview.js +++ b/src/webview.js @@ -63,10 +63,12 @@ function getTestCaseDetailsHtml(result) { const status = result.status || result.lastResult; const time = result.time || result.execTime; const memory = result.memory || result.memoryUsed; - const input = result.input; - const output = result.output || result.userOutput; - const reason = result.reason; - const message = result.message; + + const safe = (str) => { + const div = document.createElement('div'); + div.textContent = str || ''; + return div.innerHTML; + }; return `

Result: ${status}

@@ -74,10 +76,10 @@ function getTestCaseDetailsHtml(result) {

Memory: ${memory} KB

Details -

Input:

${input || ''}
-

Output:

${output || ''}
- ${reason ? `

Reason:

${reason}
` : ''} - ${message ? `

Message:

${message}
` : ''} +

Input:

${safe(result.input)}
+

Output:

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

Reason:

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

Message:

${safe(result.message)}
` : ''}
`; }