diff --git a/config/ClientsSharedConfig.ts b/config/ClientsSharedConfig.ts index ea55b0c06a35f38773c6744474159514f0687fed..77f1053b62aa98d6703a8d441e583657c24f7f5b 100644 --- a/config/ClientsSharedConfig.ts +++ b/config/ClientsSharedConfig.ts @@ -21,28 +21,28 @@ class ClientsSharedConfig { constructor() { - this.apiURL = process.env.API_URL || ''; + this.apiURL = process.env.API_URL ?? ''; this.assignment = { - filename : process.env.ASSIGNMENT_FILENAME || '', - neededFiles: JSON.parse(process.env.EXERCISE_NEEDED_FILES || '[]') + filename : process.env.ASSIGNMENT_FILENAME ?? '', + neededFiles: JSON.parse(process.env.EXERCISE_NEEDED_FILES ?? '[]') }; this.gitlab = { dojoAccount: { - id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID) || -1, - username: process.env.GITLAB_DOJO_ACCOUNT_USERNAME || '' + id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID ?? -1), + username: process.env.GITLAB_DOJO_ACCOUNT_USERNAME ?? '' } }; this.dockerCompose = { - projectName: process.env.DOCKER_COMPOSE_PROJECT_NAME || '' + projectName: process.env.DOCKER_COMPOSE_PROJECT_NAME ?? '' }; - this.exerciseResultsFolderMaxSizeInBytes = Number(process.env.EXERCISE_RESULTS_FOLDER_MAX_SIZE_IN_BYTES || 0); + this.exerciseResultsFolderMaxSizeInBytes = Number(process.env.EXERCISE_RESULTS_FOLDER_MAX_SIZE_IN_BYTES ?? 0); this.filenames = { - results: process.env.EXERCISE_RESULTS_FILENAME || '' + results: process.env.EXERCISE_RESULTS_FILENAME ?? '' }; } } diff --git a/helpers/Dojo/AssignmentValidator.ts b/helpers/Dojo/AssignmentValidator.ts index 6ecec2ccefd9f71051d4334a9f5c20c51d35cce1..a45d61d23f7f60ac7502523ae87994df47b23789 100644 --- a/helpers/Dojo/AssignmentValidator.ts +++ b/helpers/Dojo/AssignmentValidator.ts @@ -17,6 +17,9 @@ const execAsync = util.promisify(exec); class AssignmentValidator { + private readonly folderAssignment: string; + private readonly doDown: boolean; + readonly events: TypedEmitter<AssignmentValidatorEvents> = new TypedEmitter<AssignmentValidatorEvents>(); public displayableLogs: string = ''; @@ -30,7 +33,13 @@ class AssignmentValidator { private currentStep: string = 'NOT_RUNNING'; private currentSubStep: string = 'NOT_RUNNING'; - constructor(private folderAssignment: string) { + private dockerComposeFile!: DojoDockerCompose; + private assignmentFile!: AssignmentFile; + + constructor(folderAssignment: string, doDown: boolean = false) { + this.folderAssignment = folderAssignment; + this.doDown = doDown; + this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => { this.allLogs += log; this.displayableLogs += displayable ? log : ''; @@ -77,212 +86,224 @@ class AssignmentValidator { this.finished(false, code); } - run(doDown: boolean = false) { - (async () => { - let dockerComposeFile: DojoDockerCompose; - let assignmentFile: AssignmentFile; + /** + * Step 1: Check requirements + * - Check if Docker daemon is running + * - Check if required files exists + * @private + */ + private async checkRequirements() { + this.newStep('REQUIREMENTS_CHECKING', 'Please wait while we are checking requirements...'); - /* - //////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1: Check requirements - - Check if Docker daemon is running - - Check if required files exists - */ - { - this.newStep('REQUIREMENTS_CHECKING', 'Please wait while we are checking requirements...'); + // Check requirements + this.newSubStep('DOCKER_RUNNING', 'Checking if Docker daemon is running'); + try { + await execAsync(`docker ps`); + } catch ( error ) { + this.emitError(`Docker daemon isn't running`, `Some requirements are not satisfied.`, AssignmentCheckerError.DOCKER_DAEMON_NOT_RUNNING); + throw new Error(); + } + this.endSubStep('Docker daemon is running', false); - // Check requirements - this.newSubStep('DOCKER_RUNNING', 'Checking if Docker daemon is running'); - try { - await execAsync(`docker ps`); - } catch ( error ) { - this.emitError(`Docker daemon isn't running`, `Some requirements are not satisfied.`, AssignmentCheckerError.DOCKER_DAEMON_NOT_RUNNING); - return; - } - this.endSubStep('Docker daemon is running', false); + // Check if required files exists + this.newSubStep('REQUIRED_FILES_EXISTS', 'Checking if required files exists'); + const files = fs.readdirSync(this.folderAssignment); + const missingFiles = ClientsSharedConfig.assignment.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]); + if ( missingFiles.length > 0 ) { + this.emitError(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`, 'Some requirements are not satisfied', AssignmentCheckerError.REQUIRED_FILES_MISSING); + throw new Error(); + } + this.endSubStep('All required files exists', false); - // Check if required files exists - this.newSubStep('REQUIRED_FILES_EXISTS', 'Checking if required files exists'); - const files = fs.readdirSync(this.folderAssignment); - const missingFiles = ClientsSharedConfig.assignment.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]); - if ( missingFiles.length > 0 ) { - this.emitError(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`, 'Some requirements are not satisfied', AssignmentCheckerError.REQUIRED_FILES_MISSING); - return; - } - this.endSubStep('All required files exists', false); + this.endStep('All requirements are satisfied', false); + } + /** + * Step 2: dojo_assignment.json file validation + * - Structure validation + * - Immutable files validation (Check if exists and if the given type is correct) + * @private + */ + private dojoAssignmentFileValidation() { + this.newStep('ASSIGNMENT_FILE_VALIDATION', 'Please wait while we are validating dojo_assignment.json file...'); + + const assignmentFileValidationError = `${ ClientsSharedConfig.assignment.filename } file is invalid`; + + // Structure validation + this.newSubStep('ASSIGNMENT_FILE_SCHEMA_VALIDATION', 'Validating dojo_assignment.json file schema'); + const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.folderAssignment, ClientsSharedConfig.assignment.filename)); + if ( !validationResults.isValid ) { + this.emitError(`dojo_assignment.json file schema is invalid.\nHere are the errors:\n${ validationResults.error }`, assignmentFileValidationError, AssignmentCheckerError.ASSIGNMENT_FILE_SCHEMA_ERROR); + throw new Error(); + } + this.assignmentFile = validationResults.content!; + this.endSubStep('dojo_assignment.json file schema is valid', false); + + + // Immutable files validation (Check if exists and if the given type is correct) + this.newSubStep('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Validating immutable files'); + for ( const immutable of this.assignmentFile.immutable ) { + const immutablePath = path.join(this.folderAssignment, immutable.path); + if ( !fs.existsSync(immutablePath) ) { + this.emitError(`Immutable path not found: ${ immutable.path }`, assignmentFileValidationError, AssignmentCheckerError.IMMUTABLE_PATH_NOT_FOUND); + throw new Error(); + } - this.endStep('All requirements are satisfied', false); + const isDirectory = fs.lstatSync(immutablePath).isDirectory(); + if ( isDirectory && !immutable.isDirectory ) { + this.emitError(`Immutable (${ immutable.path }) is declared as a file but is a directory.`, assignmentFileValidationError, AssignmentCheckerError.IMMUTABLE_PATH_IS_NOT_DIRECTORY); + throw new Error(); + } else if ( !isDirectory && immutable.isDirectory === true ) { + this.emitError(`Immutable (${ immutable.path }) is declared as a directory but is a file.`, assignmentFileValidationError, AssignmentCheckerError.IMMUTABLE_PATH_IS_DIRECTORY); + throw new Error(); } + } + this.endSubStep('Immutable files are valid', false); - /* - //////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 2: dojo_assignment.json file validation - - Structure validation - - Immutable files validation (Check if exists and if the given type is correct) - */ - { - this.newStep('ASSIGNMENT_FILE_VALIDATION', 'Please wait while we are validating dojo_assignment.json file...'); - - - // Structure validation - this.newSubStep('ASSIGNMENT_FILE_SCHEMA_VALIDATION', 'Validating dojo_assignment.json file schema'); - const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.folderAssignment, ClientsSharedConfig.assignment.filename)); - if ( !validationResults.isValid ) { - this.emitError(`dojo_assignment.json file schema is invalid.\nHere are the errors:\n${ validationResults.error }`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.ASSIGNMENT_FILE_SCHEMA_ERROR); - return; - } - assignmentFile = validationResults.content!; - this.endSubStep('dojo_assignment.json file schema is valid', false); - - - // Immutable files validation (Check if exists and if the given type is correct) - this.newSubStep('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Validating immutable files'); - for ( const immutable of validationResults.content!.immutable ) { - const immutablePath = path.join(this.folderAssignment, immutable.path); - if ( !fs.existsSync(immutablePath) ) { - this.emitError(`Immutable path not found: ${ immutable.path }`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_NOT_FOUND); - return; - } - - const isDirectory = fs.lstatSync(immutablePath).isDirectory(); - if ( isDirectory && !immutable.isDirectory ) { - this.emitError(`Immutable (${ immutable.path }) is declared as a file but is a directory.`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_IS_NOT_DIRECTORY); - return; - } else if ( !isDirectory && immutable.isDirectory === true ) { - this.emitError(`Immutable (${ immutable.path }) is declared as a directory but is a file.`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_IS_DIRECTORY); - return; - } - } - this.endSubStep('Immutable files are valid', false); - - - this.endStep('dojo_assignment.json file is valid', false); - } + this.endStep('dojo_assignment.json file is valid', false); + } + /** + * Step 3: Docker Compose file validation + * - Global validation + * - Validation of the containers and volumes named in dojo_assignment.json + * @private + */ + private async dockerComposeFileValidation() { + this.newStep('DOCKER_COMPOSE_VALIDATION', 'Please wait while we are validating docker compose file...'); + + const composeFileValidationError = `Docker compose file is invalid`; + + // Global validation + this.newSubStep('DOCKER_COMPOSE_STRUCTURE_VALIDATION', 'Docker compose file structure validation'); + try { + this.dockerComposeFile = YAML.parse(fs.readFileSync(path.join(this.folderAssignment, 'docker-compose.yml'), 'utf8')) as DojoDockerCompose; + } catch ( error ) { + this.emitError(`Docker compose file yaml structure is invalid.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_YAML_ERROR); + throw new Error(); + } + + try { + await new Promise<void>((resolve, reject) => { + const dockerComposeValidation = spawn(`docker compose -f docker-compose.yml config --quiet`, { + cwd : this.folderAssignment, + shell: true + }); + + dockerComposeValidation.on('exit', code => { + code !== null && code === 0 ? resolve() : reject(code); + }); + }); + } catch ( error ) { + this.emitError(`Docker compose file structure is invalid.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_SCHEMA_ERROR); + throw new Error(); + } + this.endSubStep('Docker compose file structure is valid', false); + + + // Validation of the containers and volumes named in dojo_assignment.json + this.newSubStep('DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content validation'); + if ( !(this.assignmentFile.result.container in this.dockerComposeFile.services) ) { + this.emitError(`Container specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_CONTAINER_MISSING); + throw new Error(); + } + if ( this.assignmentFile.result.volume && (!this.dockerComposeFile.volumes || !(this.assignmentFile.result.volume in this.dockerComposeFile.volumes)) ) { + this.emitError(`Volume specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_VOLUME_MISSING); + throw new Error(); + } + this.endSubStep('Docker compose file content is valid', false); + + + this.endStep('Docker compose file is valid', false); + } + + /** + * Step 4: Dockerfiles validation + * - Check if file exists + * - TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint + * @private + */ + private dockerfilesValidation() { + this.newStep('DOCKERFILE_VALIDATION', 'Please wait while we are validating dockerfiles...'); - /* - //////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 3: Docker Compose file validation - - Global validation - - Validation of the containers and volumes named in dojo_assignment.json - */ - { - this.newStep('DOCKER_COMPOSE_VALIDATION', 'Please wait while we are validating docker compose file...'); - - - // Global validation - this.newSubStep('DOCKER_COMPOSE_STRUCTURE_VALIDATION', 'Docker compose file structure validation'); - try { - dockerComposeFile = YAML.parse(fs.readFileSync(path.join(this.folderAssignment, 'docker-compose.yml'), 'utf8')) as DojoDockerCompose; - } catch ( error ) { - this.emitError(`Docker compose file yaml structure is invalid.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_YAML_ERROR); - return; - } - - try { - await new Promise<void>((resolve, reject) => { - const dockerComposeValidation = spawn(`docker compose -f docker-compose.yml config --quiet`, { - cwd : this.folderAssignment, - shell: true - }); - - dockerComposeValidation.on('exit', (code) => { - code !== null && code == 0 ? resolve() : reject(); - }); - }); - } catch ( error ) { - this.emitError(`Docker compose file structure is invalid.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_SCHEMA_ERROR); - return; - } - this.endSubStep('Docker compose file structure is valid', false); - - - // Validation of the containers and volumes named in dojo_assignment.json - this.newSubStep('DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content validation'); - if ( !(assignmentFile.result.container in dockerComposeFile!.services) ) { - this.emitError(`Container specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_CONTAINER_MISSING); - return; - } - if ( assignmentFile.result.volume && (!dockerComposeFile!.volumes || !(assignmentFile.result.volume in dockerComposeFile!.volumes)) ) { - this.emitError(`Volume specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_VOLUME_MISSING); - return; - } - this.endSubStep('Docker compose file content is valid', false); - - - this.endStep('Docker compose file is valid', false); - } + this.newSubStep('DOCKERFILE_EXIST', 'Docker compose file content validation'); + const dockerfilesPaths = Object.values(this.dockerComposeFile.services).filter(service => service.build).map(service => path.join(this.folderAssignment, service.build!.context ?? '', service.build!.dockerfile!)); + const filesNotFound = dockerfilesPaths.filter(dockerfilePath => !fs.existsSync(dockerfilePath)); + if ( filesNotFound.length > 0 ) { + this.emitError(`Dockerfiles not found: ${ filesNotFound.join(', ') }`, 'Dockerfiles are invalid', AssignmentCheckerError.DOCKERFILE_NOT_FOUND); + throw new Error(); + } + this.endSubStep('Docker compose file content is valid', false); - /* - //////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 4: Dockerfiles validation - - Check if file exists - - TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint - */ - { - this.newStep('DOCKERFILE_VALIDATION', 'Please wait while we are validating dockerfiles...'); + this.endStep('Dockerfiles are valid', false); + } - this.newSubStep('DOCKERFILE_EXIST', 'Docker compose file content validation'); - const dockerfilesPaths = Object.values(dockerComposeFile!.services).filter((service) => service.build).map((service) => path.join(this.folderAssignment, service.build!.context ?? '', service.build!.dockerfile!)); - const filesNotFound = dockerfilesPaths.filter((dockerfilePath) => !fs.existsSync(dockerfilePath)); - if ( filesNotFound.length > 0 ) { - this.emitError(`Dockerfiles not found: ${ filesNotFound.join(', ') }`, 'Dockerfiles are invalid', AssignmentCheckerError.DOCKERFILE_NOT_FOUND); - return; - } - this.endSubStep('Docker compose file content is valid', false); + /** + * Step 5: Run + * - Make a run of the assignment (If the return code is 0, the assignment is not valid because it means that there no need of modification for succeed the exercise) + * @private + */ + private async runAssignment() { + this.newStep('ASSIGNMENT_RUN', 'Please wait while we are running the assignment...'); - this.endStep('Dockerfiles are valid', false); - } + const exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, this.assignmentFile, this.folderAssignment); + try { + await new Promise<void>((resolve, reject) => { + exerciseDockerCompose.events.on('logs', (log: string, error: boolean, displayable: boolean) => { + this.log(log, error, displayable); + }); - /* - //////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 5: Run - - Make a run of the assignment (If the return code is 0, the assignment is not valid because it means that there no need of modification for succeed the exercise) - */ - { - this.newStep('ASSIGNMENT_RUN', 'Please wait while we are running the assignment...'); + exerciseDockerCompose.events.on('step', (name: string, message: string) => { + this.newSubStep(name, message); + }); + exerciseDockerCompose.events.on('endStep', (_stepName: string, message: string, error: boolean) => { + this.endSubStep(message, error); + }); - const exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, assignmentFile, this.folderAssignment); + exerciseDockerCompose.events.on('finished', (_success: boolean, exitCode: number) => { + exitCode !== 0 ? resolve() : reject(exitCode); + }); - try { - await new Promise<void>((resolve, reject) => { - exerciseDockerCompose.events.on('logs', (log: string, error: boolean, displayable: boolean) => { - this.log(log, error, displayable); - }); + exerciseDockerCompose.run(this.doDown); + }); + } catch ( error ) { + this.fatalErrorMessage = 'Assignment is already solved'; + this.endStep(this.fatalErrorMessage, true); + this.finished(false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY); + throw new Error(); + } - exerciseDockerCompose.events.on('step', (name: string, message: string) => { - this.newSubStep(name, message); - }); - exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => { - this.endSubStep(message, error); - }); + this.endStep('Assignment run successfully', false); + } - exerciseDockerCompose.events.on('finished', (success: boolean, exitCode: number) => { - exitCode != 0 ? resolve() : reject(); - }); + run() { + (async () => { + try { + await this.checkRequirements(); - exerciseDockerCompose.run(doDown); - }); - } catch ( error ) { - this.fatalErrorMessage = 'Assignment is already solved'; - this.endStep(this.fatalErrorMessage, true); - this.finished(false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY); - return; - } + this.dojoAssignmentFileValidation(); + await this.dockerComposeFileValidation(); - this.endStep('Assignment run successfully', false); - } + this.dockerfilesValidation(); + await this.runAssignment(); - this.finished(true, 0); + this.finished(true, 0); + } catch ( error ) { + return; + } })(); } } diff --git a/helpers/Dojo/ClientsSharedAssignmentHelper.ts b/helpers/Dojo/ClientsSharedAssignmentHelper.ts index c47d2b4d0ff9eec6019b387429dcc8c00c031825..9c0eb460ac75a392c00af9c515b691979479eb2a 100644 --- a/helpers/Dojo/ClientsSharedAssignmentHelper.ts +++ b/helpers/Dojo/ClientsSharedAssignmentHelper.ts @@ -6,7 +6,8 @@ import AssignmentValidator from './AssignmentValidator'; class ClientsSharedAssignmentHelper { displayExecutionResults(validator: AssignmentValidator, successMessage: string, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }) { - const finalLogGlobalResult = `${ Style.INFO('Global result') } : ${ validator.success ? Style.SUCCESS(`${ Icon.SUCCESS } Success`) : Style.FAILURE(`${ Icon.FAILURE } Failure`) }`; + const globalResult = validator.success ? Style.SUCCESS(`${ Icon.SUCCESS } Success`) : Style.FAILURE(`${ Icon.FAILURE } Failure`); + const finalLogGlobalResult = `${ Style.INFO('Global result') } : ${ globalResult }`; const finalLogSuccessMessage = validator.success ? `${ successMessage }` : ''; const finalLogErrorMessage = !validator.success ? `${ Style.INFO('Error message') } :\n${ Style.FAILURE(validator.fatalErrorMessage) }` : ''; diff --git a/helpers/Dojo/ClientsSharedExerciseHelper.ts b/helpers/Dojo/ClientsSharedExerciseHelper.ts index dd8b173f49f911c55759b01042ad1b48e8409b57..740b7695bf484dbb9e0c54bfa0f21bd902926c6a 100644 --- a/helpers/Dojo/ClientsSharedExerciseHelper.ts +++ b/helpers/Dojo/ClientsSharedExerciseHelper.ts @@ -5,27 +5,31 @@ import Icon from '../../../shared/types/Icon'; class ClientsSharedExerciseHelper { - displayExecutionResults(exerciseResults: ExerciseResultsFile, containerExitCode: number, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }, additionalText: string = '') { - const finalLogGlobalResult = `${ Style.INFO('Global result: ') }${ exerciseResults.success ? Style.SUCCESS(`${ Icon.SUCCESS } Success`) : Style.FAILURE(`${ Icon.FAILURE } Failure`) }`; - const finalLogExecutionExitCode = `${ Style.INFO('Execution exit code: ') }${ (containerExitCode == 0 ? Style.SUCCESS : Style.FAILURE)(containerExitCode) }`; + private getOtherInformations(exerciseResults: ExerciseResultsFile, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }) { + return exerciseResults.otherInformations ? [ '', ...exerciseResults.otherInformations.map(information => { + const informationTitle = Style.INFO(`${ information.icon && information.icon !== '' ? Icon[information.icon] + ' ' : '' }${ information.name }: `); + const informationItems = typeof information.itemsOrInformations == 'string' ? information.itemsOrInformations : information.itemsOrInformations.map(item => `- ${ item }`).join('\n'); - const finalLogResultNumbers = exerciseResults.successfulTests || exerciseResults.failedTests ? `\n\n${ Style.INFO(Style.SUCCESS('Tests passed: ')) }${ exerciseResults.successfulTests ?? '--' }\n${ Style.INFO(Style.FAILURE('Tests failed: ')) }${ exerciseResults.failedTests ?? '--' }` : ''; + return `${ informationTitle }\n${ informationItems }`; + }) ].join('\n\n') : ''; + } - const finalLogSuccessResultDetails = (exerciseResults.successfulTestsList ?? []).map(testName => `- ${ Icon.SUCCESS } ${ testName }`).join('\n'); - const finalLogFailedResultDetails = (exerciseResults.failedTestsList ?? []).map(testName => `- ${ Icon.FAILURE } ${ testName }`).join('\n'); - const finalLogResultDetails = exerciseResults.successfulTestsList || exerciseResults.failedTestsList ? `\n\n${ Style.INFO('Tests: ') }${ finalLogSuccessResultDetails != '' ? '\n' + finalLogSuccessResultDetails : '' }${ finalLogFailedResultDetails != '' ? '\n' + finalLogFailedResultDetails : '' }` : ''; + displayExecutionResults(exerciseResults: ExerciseResultsFile, containerExitCode: number, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }, additionalText: string = '') { + const globalResult = exerciseResults.success ? Style.SUCCESS(`${ Icon.SUCCESS } Success`) : Style.FAILURE(`${ Icon.FAILURE } Failure`); + const finalLogGlobalResult = `${ Style.INFO('Global result: ') }${ globalResult }`; + + const finalLogExecutionExitCode = `${ Style.INFO('Execution exit code: ') }${ (containerExitCode === 0 ? Style.SUCCESS : Style.FAILURE)(containerExitCode) }`; - let finalLogInformations = ''; - if ( exerciseResults.otherInformations ) { - finalLogInformations = [ '', ...exerciseResults.otherInformations.map(information => { - const informationTitle = Style.INFO(`${ information.icon && information.icon != '' ? Icon[information.icon] + ' ' : '' }${ information.name }: `); - const informationItems = typeof information.itemsOrInformations == 'string' ? information.itemsOrInformations : information.itemsOrInformations.map(item => `- ${ item }`).join('\n'); + const finalLogResultNumbers = exerciseResults.successfulTests || exerciseResults.failedTests ? `\n\n${ Style.INFO(Style.SUCCESS('Tests passed: ')) }${ exerciseResults.successfulTests ?? '--' }\n${ Style.INFO(Style.FAILURE('Tests failed: ')) }${ exerciseResults.failedTests ?? '--' }` : ''; - return `${ informationTitle }\n${ informationItems }`; - }) ].join('\n\n'); - } + let finalLogSuccessResultDetails = (exerciseResults.successfulTestsList ?? []).map(testName => `- ${ Icon.SUCCESS } ${ testName }`).join('\n'); + finalLogSuccessResultDetails = finalLogSuccessResultDetails !== '' ? '\n' + finalLogSuccessResultDetails : ''; + let finalLogFailedResultDetails = (exerciseResults.failedTestsList ?? []).map(testName => `- ${ Icon.FAILURE } ${ testName }`).join('\n'); + finalLogFailedResultDetails = finalLogFailedResultDetails !== '' ? '\n' + finalLogFailedResultDetails : ''; + const finalLogResultDetails = exerciseResults.successfulTestsList || exerciseResults.failedTestsList ? `\n\n${ Style.INFO('Tests: ') }${ finalLogSuccessResultDetails }${ finalLogFailedResultDetails }` : ''; + const finalLogInformations = this.getOtherInformations(exerciseResults, Style); console.log(boxen(`${ finalLogGlobalResult }\n\n${ finalLogExecutionExitCode }${ finalLogResultNumbers }${ finalLogResultDetails }${ finalLogInformations }${ additionalText }`, { title : 'Results', diff --git a/helpers/Dojo/DojoBackendHelper.ts b/helpers/Dojo/DojoBackendHelper.ts new file mode 100644 index 0000000000000000000000000000000000000000..c682bfb71fdd895a6d3761fea6694754542074ee --- /dev/null +++ b/helpers/Dojo/DojoBackendHelper.ts @@ -0,0 +1,28 @@ +import ApiRoute from '../../types/Dojo/ApiRoute'; +import ClientsSharedConfig from '../../config/ClientsSharedConfig'; + + +class DojoBackendHelper { + public getApiUrl(route: ApiRoute, options?: Partial<{ assignmentNameOrUrl: string, exerciseIdOrUrl: string, gitlabProjectId: string }>): string { + const url = `${ ClientsSharedConfig.apiURL }${ route }`; + + if ( options ) { + if ( options.assignmentNameOrUrl ) { + return url.replace('{{assignmentNameOrUrl}}', encodeURIComponent(options.assignmentNameOrUrl)); + } + + if ( options.exerciseIdOrUrl ) { + return url.replace('{{exerciseIdOrUrl}}', encodeURIComponent(options.exerciseIdOrUrl)); + } + + if ( options.gitlabProjectId ) { + return url.replace('{{gitlabProjectId}}', encodeURIComponent(options.gitlabProjectId)); + } + } + + return url; + } +} + + +export default new DojoBackendHelper(); \ No newline at end of file diff --git a/helpers/Dojo/ExerciseDockerCompose.ts b/helpers/Dojo/ExerciseDockerCompose.ts index ad033438c57eb8918aff6bc7278b8197ecc585ac..64d60c38346bee7ed44cc242e3ca6d79ed6c093e 100644 --- a/helpers/Dojo/ExerciseDockerCompose.ts +++ b/helpers/Dojo/ExerciseDockerCompose.ts @@ -18,7 +18,17 @@ class ExerciseDockerCompose { private currentStep: string = 'NOT_RUNNING'; - constructor(private projectName: string, private assignmentFile: AssignmentFile, private executionFolder: string, private composeFileOverride: Array<string> = []) { + private readonly projectName: string; + private readonly assignmentFile: AssignmentFile; + private readonly executionFolder: string; + private readonly composeFileOverride: Array<string> = []; + + constructor(projectName: string, assignmentFile: AssignmentFile, executionFolder: string, composeFileOverride: Array<string> = []) { + this.projectName = projectName; + this.assignmentFile = assignmentFile; + this.executionFolder = executionFolder; + this.composeFileOverride = composeFileOverride; + this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => { this.allLogs += log; this.displayableLogs += displayable ? log : ''; @@ -49,15 +59,15 @@ class ExerciseDockerCompose { } private registerChildProcess(childProcess: ChildProcessWithoutNullStreams, resolve: (value: (number | PromiseLike<number>)) => void, reject: (reason?: unknown) => void, displayable: boolean, rejectIfCodeIsNotZero: boolean) { - childProcess.stdout.on('data', (data) => { + childProcess.stdout.on('data', data => { this.log(data.toString(), false, displayable); }); - childProcess.stderr.on('data', (data) => { + childProcess.stderr.on('data', data => { this.log(data.toString(), true, displayable); }); - childProcess.on('exit', (code) => { + childProcess.on('exit', code => { code === null || (rejectIfCodeIsNotZero && code !== 0) ? reject(code) : resolve(code); }); } @@ -66,7 +76,8 @@ class ExerciseDockerCompose { (async () => { let containerExitCode: number = -1; - const dockerComposeCommand = `docker compose --project-name ${ this.projectName } --progress plain --file docker-compose.yml ${ this.composeFileOverride.map((file) => `--file "${ file }"`).join(' ') }`; + const filesOverrideArguments = this.composeFileOverride.map(file => `--file "${ file }"`).join(' '); + const dockerComposeCommand = `docker compose --project-name ${ this.projectName } --progress plain --file docker-compose.yml ${ filesOverrideArguments }`; // Run the service { diff --git a/helpers/Dojo/ExerciseResultsSanitizerAndValidator.ts b/helpers/Dojo/ExerciseResultsSanitizerAndValidator.ts index 8a1ec41c241c1e3c757726723dd7509440689b12..38ffa923fd74d191787e20ebf832adc9e6325bec 100644 --- a/helpers/Dojo/ExerciseResultsSanitizerAndValidator.ts +++ b/helpers/Dojo/ExerciseResultsSanitizerAndValidator.ts @@ -17,7 +17,15 @@ class ExerciseResultsSanitizerAndValidator { private resultsFilePath: string = ''; - constructor(private folderResultsDojo: string, private folderResultsExercise: string, private containerExitCode: number) { } + private readonly folderResultsDojo: string; + private readonly folderResultsExercise: string; + private readonly containerExitCode: number; + + constructor(folderResultsDojo: string, folderResultsExercise: string, containerExitCode: number) { + this.folderResultsDojo = folderResultsDojo; + this.folderResultsExercise = folderResultsExercise; + this.containerExitCode = containerExitCode; + } private async resultsFileSanitization() { this.events.emit('step', 'RESULTS_FILE_SANITIZATION', 'Sanitizing results file'); @@ -33,11 +41,11 @@ class ExerciseResultsSanitizerAndValidator { this.events.emit('endStep', 'RESULTS_FILE_SANITIZATION', 'Results file sanitized', false); } - private async resultsFileProvided(path: string): Promise<boolean> { + private async resultsFileProvided(resultFilePath: string): Promise<boolean> { // Results file schema validation { this.events.emit('step', 'VALIDATE_RESULTS_FILE', 'Validating results file schema'); - const validationResults = Json5FileValidator.validateFile(ExerciseResultsFile, path); + const validationResults = Json5FileValidator.validateFile(ExerciseResultsFile, resultFilePath); if ( !validationResults.isValid ) { this.events.emit('endStep', 'VALIDATE_RESULTS_FILE', `Results file is not valid. Here are the errors :\n${ validationResults.error }`, true); this.events.emit('finished', false, ExerciseCheckerError.EXERCISE_RESULTS_FILE_SCHEMA_NOT_VALID); diff --git a/models/Exercise.ts b/models/Exercise.ts index 710314ee1d377a117650fe1ced9ec171de104638..b8a2b83b02d763effa7516905028fd760da37cf7 100644 --- a/models/Exercise.ts +++ b/models/Exercise.ts @@ -1,5 +1,5 @@ import GitlabRepository from '../../shared/types/Gitlab/GitlabRepository'; -import { CommitSchema } from '@gitbeaker/rest'; +import * as Gitlab from '@gitbeaker/rest'; import User from './User'; import Assignment from './Assignment'; @@ -18,7 +18,7 @@ interface Exercise { assignment: Assignment | undefined; isCorrection: boolean; - correctionCommit: CommitSchema | undefined; + correctionCommit: Gitlab.CommitSchema | undefined; } diff --git a/types/Dojo/ApiRoute.ts b/types/Dojo/ApiRoute.ts index 53e3f9f59129ce7eecef1c82d9ecd6a0ab28db27..8e4969b6de34599ca29e803ca6fbcd3a3c91d799 100644 --- a/types/Dojo/ApiRoute.ts +++ b/types/Dojo/ApiRoute.ts @@ -2,16 +2,16 @@ enum ApiRoute { LOGIN = '/login', REFRESH_TOKENS = '/refresh_tokens', TEST_SESSION = '/test_session', - GITLAB_CHECK_TEMPLATE_ACCESS = '/gitlab/project/{{id}}/checkTemplateAccess', - ASSIGNMENT_GET = '/assignments/{{nameOrUrl}}', + GITLAB_CHECK_TEMPLATE_ACCESS = '/gitlab/project/{{gitlabProjectId}}/checkTemplateAccess', + ASSIGNMENT_GET = '/assignments/{{assignmentNameOrUrl}}', ASSIGNMENT_CREATE = '/assignments', - ASSIGNMENT_PUBLISH = '/assignments/{{nameOrUrl}}/publish', - ASSIGNMENT_UNPUBLISH = '/assignments/{{nameOrUrl}}/unpublish', + ASSIGNMENT_PUBLISH = '/assignments/{{assignmentNameOrUrl}}/publish', + ASSIGNMENT_UNPUBLISH = '/assignments/{{assignmentNameOrUrl}}/unpublish', ASSIGNMENT_CORRECTION_LINK = '/assignments/{{assignmentNameOrUrl}}/corrections', ASSIGNMENT_CORRECTION_UPDATE = '/assignments/{{assignmentNameOrUrl}}/corrections/{{exerciseIdOrUrl}}', - EXERCISE_CREATE = '/assignments/{{nameOrUrl}}/exercises', - EXERCISE_ASSIGNMENT = '/exercises/{{id}}/assignment', - EXERCISE_RESULTS = '/exercises/{{id}}/results' + EXERCISE_CREATE = '/assignments/{{assignmentNameOrUrl}}/exercises', + EXERCISE_ASSIGNMENT = '/exercises/{{exerciseIdOrUrl}}/assignment', + EXERCISE_RESULTS = '/exercises/{{exerciseIdOrUrl}}/results' }