Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • ask-user-to-delete-exercises-on-duplicates
  • jw_sonar
  • jw_sonar_backup
  • main
  • move-to-esm-only
  • open_tool_for_self_hosting
  • v5.0
  • v6.0
  • v4.1
  • v4.2
10 results

Target

Select target project
  • dojo_project/projects/shared/nodeclientsharedcode
1 result
Select Git revision
  • ask-user-to-delete-exercises-on-duplicates
  • jw_sonar
  • jw_sonar_backup
  • main
  • move-to-esm-only
  • open_tool_for_self_hosting
  • v5.0
  • v6.0
  • v4.1
  • v4.2
10 results
Show changes
...@@ -4,7 +4,6 @@ import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssign ...@@ -4,7 +4,6 @@ import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssign
import path from 'node:path'; import path from 'node:path';
import AssignmentCheckerError from '../../../shared/types/Dojo/AssignmentCheckerError'; import AssignmentCheckerError from '../../../shared/types/Dojo/AssignmentCheckerError';
import fs from 'fs-extra'; import fs from 'fs-extra';
import JSON5 from 'json5';
import ClientsSharedConfig from '../../config/ClientsSharedConfig'; import ClientsSharedConfig from '../../config/ClientsSharedConfig';
import YAML from 'yaml'; import YAML from 'yaml';
import DojoDockerCompose from '../../types/Dojo/DojoDockerCompose'; import DojoDockerCompose from '../../types/Dojo/DojoDockerCompose';
...@@ -28,6 +27,9 @@ class AssignmentValidator { ...@@ -28,6 +27,9 @@ class AssignmentValidator {
public exitCode: number = -1; public exitCode: number = -1;
public fatalErrorMessage: string = ''; public fatalErrorMessage: string = '';
private currentStep: string = 'NOT_RUNNING';
private currentSubStep: string = 'NOT_RUNNING';
constructor(private folderAssignment: string) { constructor(private folderAssignment: string) {
this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => { this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
this.allLogs += log; this.allLogs += log;
...@@ -41,19 +43,45 @@ class AssignmentValidator { ...@@ -41,19 +43,45 @@ class AssignmentValidator {
}); });
} }
private newStep(name: string, message: string) {
this.currentStep = name;
this.events.emit('step', name, message);
}
private newSubStep(name: string, message: string) {
this.currentSubStep = name;
this.events.emit('subStep', name, message);
}
private endStep(message: string, error: boolean) {
this.events.emit('endStep', this.currentStep, message, error);
}
private endSubStep(message: string, error: boolean) {
this.events.emit('endSubStep', this.currentStep, message, error);
}
private log(message: string, error: boolean, displayable: boolean) {
this.events.emit('logs', message, error, displayable, this.currentStep, this.currentSubStep);
}
private finished(success: boolean, code: number) {
this.events.emit('finished', success, code);
}
private emitError(subStepMessage: string, stepMessage: string, code: AssignmentCheckerError) {
this.fatalErrorMessage = stepMessage;
this.endSubStep(subStepMessage, true);
this.endStep(stepMessage, true);
this.finished(false, code);
}
run(doDown: boolean = false) { run(doDown: boolean = false) {
(async () => { (async () => {
let dockerComposeFile: DojoDockerCompose; let dockerComposeFile: DojoDockerCompose;
let assignmentFile: AssignmentFile; let assignmentFile: AssignmentFile;
const emitError = (subStepName: string, subStepMessage: string, stepName: string, stepMessage: string, code: AssignmentCheckerError) => {
this.fatalErrorMessage = stepMessage;
this.events.emit('endSubStep', subStepName, subStepMessage, true);
this.events.emit('endStep', stepName, stepMessage, true);
this.events.emit('finished', false, code);
};
/* /*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1: Check requirements //////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1: Check requirements
...@@ -61,33 +89,33 @@ class AssignmentValidator { ...@@ -61,33 +89,33 @@ class AssignmentValidator {
- Check if required files exists - Check if required files exists
*/ */
{ {
this.events.emit('step', 'REQUIREMENTS_CHECKING', 'Please wait while we are checking requirements...'); this.newStep('REQUIREMENTS_CHECKING', 'Please wait while we are checking requirements...');
// Check requirements // Check requirements
this.events.emit('subStep', 'DOCKER_RUNNING', 'Checking if Docker daemon is running'); this.newSubStep('DOCKER_RUNNING', 'Checking if Docker daemon is running');
try { try {
await execAsync(`cd "${ this.folderAssignment }";docker ps`); await execAsync(`docker ps`);
} catch ( error ) { } catch ( error ) {
emitError('DOCKER_RUNNING', `Docker daemon isn't running`, 'REQUIREMENTS_CHECKING', `Some requirements are not satisfied.`, AssignmentCheckerError.DOCKER_DAEMON_NOT_RUNNING); this.emitError(`Docker daemon isn't running`, `Some requirements are not satisfied.`, AssignmentCheckerError.DOCKER_DAEMON_NOT_RUNNING);
return; return;
} }
this.events.emit('endSubStep', 'DOCKER_RUNNING', 'Docker daemon is running', false); this.endSubStep('Docker daemon is running', false);
// Check if required files exists // Check if required files exists
this.events.emit('subStep', 'REQUIRED_FILES_EXISTS', 'Checking if required files exists'); this.newSubStep('REQUIRED_FILES_EXISTS', 'Checking if required files exists');
const files = fs.readdirSync(this.folderAssignment); 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]); const missingFiles = ClientsSharedConfig.assignment.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]);
if ( missingFiles.length > 0 ) { if ( missingFiles.length > 0 ) {
emitError('REQUIRED_FILES_EXISTS', `The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`, 'REQUIREMENTS_CHECKING', 'Some requirements are not satisfied', AssignmentCheckerError.REQUIRED_FILES_MISSING); 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; return;
} }
this.events.emit('endSubStep', 'REQUIRED_FILES_EXISTS', 'All required files exists', false); this.endSubStep('All required files exists', false);
this.events.emit('endStep', 'REQUIREMENTS_CHECKING', 'All requirements are satisfied', false); this.endStep('All requirements are satisfied', false);
} }
...@@ -97,42 +125,42 @@ class AssignmentValidator { ...@@ -97,42 +125,42 @@ class AssignmentValidator {
- Immutable files validation (Check if exists and if the given type is correct) - Immutable files validation (Check if exists and if the given type is correct)
*/ */
{ {
this.events.emit('step', 'ASSIGNMENT_FILE_VALIDATION', 'Please wait while we are validating dojo_assignment.json file...'); this.newStep('ASSIGNMENT_FILE_VALIDATION', 'Please wait while we are validating dojo_assignment.json file...');
// Structure validation // Structure validation
this.events.emit('subStep', 'ASSIGNMENT_FILE_SCHEMA_VALIDATION', 'Validating dojo_assignment.json file schema'); this.newSubStep('ASSIGNMENT_FILE_SCHEMA_VALIDATION', 'Validating dojo_assignment.json file schema');
const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.folderAssignment, ClientsSharedConfig.assignment.filename)); const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.folderAssignment, ClientsSharedConfig.assignment.filename));
if ( !validationResults.isValid ) { if ( !validationResults.isValid ) {
emitError('ASSIGNMENT_FILE_SCHEMA_VALIDATION', `dojo_assignment.json file schema is invalid.\nHere are the errors:\n${ JSON5.stringify(validationResults.errors) }`, 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is invalid', AssignmentCheckerError.ASSIGNMENT_FILE_SCHEMA_ERROR); 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; return;
} }
assignmentFile = validationResults.results!; assignmentFile = validationResults.content!;
this.events.emit('endSubStep', 'ASSIGNMENT_FILE_SCHEMA_VALIDATION', 'dojo_assignment.json file schema is valid', false); this.endSubStep('dojo_assignment.json file schema is valid', false);
// Immutable files validation (Check if exists and if the given type is correct) // Immutable files validation (Check if exists and if the given type is correct)
this.events.emit('subStep', 'ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Validating immutable files'); this.newSubStep('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Validating immutable files');
for ( const immutable of validationResults.results!.immutable ) { for ( const immutable of validationResults.content!.immutable ) {
const immutablePath = path.join(this.folderAssignment, immutable.path); const immutablePath = path.join(this.folderAssignment, immutable.path);
if ( !fs.existsSync(immutablePath) ) { if ( !fs.existsSync(immutablePath) ) {
emitError('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', `Immutable path not found: ${ immutable.path }`, 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_NOT_FOUND); this.emitError(`Immutable path not found: ${ immutable.path }`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_NOT_FOUND);
return; return;
} }
const isDirectory = fs.lstatSync(immutablePath).isDirectory(); const isDirectory = fs.lstatSync(immutablePath).isDirectory();
if ( isDirectory && !immutable.isDirectory ) { if ( isDirectory && !immutable.isDirectory ) {
emitError('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', `Immutable (${ immutable.path }) is declared as a file but is a directory.`, 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_IS_NOT_DIRECTORY); 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; return;
} else if ( !isDirectory && immutable.isDirectory === true ) { } else if ( !isDirectory && immutable.isDirectory === true ) {
emitError('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', `Immutable (${ immutable.path }) is declared as a directory but is a file.`, 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_IS_DIRECTORY); 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; return;
} }
} }
this.events.emit('endSubStep', 'ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Immutable files are valid', false); this.endSubStep('Immutable files are valid', false);
this.events.emit('endStep', 'ASSIGNMENT_FILE_VALIDATION', 'dojo_assignment.json file is valid', false); this.endStep('dojo_assignment.json file is valid', false);
} }
...@@ -142,15 +170,15 @@ class AssignmentValidator { ...@@ -142,15 +170,15 @@ class AssignmentValidator {
- Validation of the containers and volumes named in dojo_assignment.json - Validation of the containers and volumes named in dojo_assignment.json
*/ */
{ {
this.events.emit('step', 'DOCKER_COMPOSE_VALIDATION', 'Please wait while we are validating docker compose file...'); this.newStep('DOCKER_COMPOSE_VALIDATION', 'Please wait while we are validating docker compose file...');
// Global validation // Global validation
this.events.emit('subStep', 'DOCKER_COMPOSE_STRUCTURE_VALIDATION', 'Docker compose file structure validation'); this.newSubStep('DOCKER_COMPOSE_STRUCTURE_VALIDATION', 'Docker compose file structure validation');
try { try {
dockerComposeFile = YAML.parse(fs.readFileSync(path.join(this.folderAssignment, 'docker-compose.yml'), 'utf8')) as DojoDockerCompose; dockerComposeFile = YAML.parse(fs.readFileSync(path.join(this.folderAssignment, 'docker-compose.yml'), 'utf8')) as DojoDockerCompose;
} catch ( error ) { } catch ( error ) {
emitError('DOCKER_COMPOSE_STRUCTURE_VALIDATION', `Docker compose file yaml structure is invalid.`, 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_YAML_ERROR); this.emitError(`Docker compose file yaml structure is invalid.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_YAML_ERROR);
return; return;
} }
...@@ -166,26 +194,26 @@ class AssignmentValidator { ...@@ -166,26 +194,26 @@ class AssignmentValidator {
}); });
}); });
} catch ( error ) { } catch ( error ) {
emitError('DOCKER_COMPOSE_STRUCTURE_VALIDATION', `Docker compose file structure is invalid.`, 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_SCHEMA_ERROR); this.emitError(`Docker compose file structure is invalid.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_SCHEMA_ERROR);
return; return;
} }
this.events.emit('endSubStep', 'DOCKER_COMPOSE_STRUCTURE_VALIDATION', 'Docker compose file structure is valid', false); this.endSubStep('Docker compose file structure is valid', false);
// Validation of the containers and volumes named in dojo_assignment.json // Validation of the containers and volumes named in dojo_assignment.json
this.events.emit('subStep', 'DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content validation'); this.newSubStep('DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content validation');
if ( !(assignmentFile.result.container in dockerComposeFile!.services) ) { if ( !(assignmentFile.result.container in dockerComposeFile!.services) ) {
emitError('DOCKER_COMPOSE_CONTENT_VALIDATION', `Container specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_CONTAINER_MISSING); this.emitError(`Container specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_CONTAINER_MISSING);
return; return;
} }
if ( assignmentFile.result.volume && (!dockerComposeFile!.volumes || !(assignmentFile.result.volume in dockerComposeFile!.volumes)) ) { if ( assignmentFile.result.volume && (!dockerComposeFile!.volumes || !(assignmentFile.result.volume in dockerComposeFile!.volumes)) ) {
emitError('DOCKER_COMPOSE_CONTENT_VALIDATION', `Volume specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_VOLUME_MISSING); this.emitError(`Volume specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_VOLUME_MISSING);
return; return;
} }
this.events.emit('endSubStep', 'DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content is valid', false); this.endSubStep('Docker compose file content is valid', false);
this.events.emit('endStep', 'DOCKER_COMPOSE_VALIDATION', 'Docker compose file is valid', false); this.endStep('Docker compose file is valid', false);
} }
...@@ -195,20 +223,20 @@ class AssignmentValidator { ...@@ -195,20 +223,20 @@ class AssignmentValidator {
- TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint - TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint
*/ */
{ {
this.events.emit('step', 'DOCKERFILE_VALIDATION', 'Please wait while we are validating dockerfiles...'); this.newStep('DOCKERFILE_VALIDATION', 'Please wait while we are validating dockerfiles...');
this.events.emit('subStep', 'DOCKERFILE_EXIST', 'Docker compose file content validation'); 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 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)); const filesNotFound = dockerfilesPaths.filter((dockerfilePath) => !fs.existsSync(dockerfilePath));
if ( filesNotFound.length > 0 ) { if ( filesNotFound.length > 0 ) {
emitError('DOCKERFILE_VALIDATION', `Dockerfiles not found: ${ filesNotFound.join(', ') }`, 'DOCKERFILE_VALIDATION', 'Dockerfiles are invalid', AssignmentCheckerError.DOCKERFILE_NOT_FOUND); this.emitError(`Dockerfiles not found: ${ filesNotFound.join(', ') }`, 'Dockerfiles are invalid', AssignmentCheckerError.DOCKERFILE_NOT_FOUND);
return; return;
} }
this.events.emit('endSubStep', 'DOCKER_COMPOSE_CONTENT_VALIDATION', 'Docker compose file content is valid', false); this.endSubStep('Docker compose file content is valid', false);
this.events.emit('endStep', 'DOCKERFILE_VALIDATION', 'Dockerfiles are valid', false); this.endStep('Dockerfiles are valid', false);
} }
...@@ -217,7 +245,7 @@ class AssignmentValidator { ...@@ -217,7 +245,7 @@ class AssignmentValidator {
- 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) - 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.events.emit('step', 'ASSIGNMENT_RUN', 'Please wait while we are running the assignment...'); this.newStep('ASSIGNMENT_RUN', 'Please wait while we are running the assignment...');
const exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, assignmentFile, this.folderAssignment); const exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, assignmentFile, this.folderAssignment);
...@@ -225,15 +253,15 @@ class AssignmentValidator { ...@@ -225,15 +253,15 @@ class AssignmentValidator {
try { try {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
exerciseDockerCompose.events.on('logs', (log: string, error: boolean, displayable: boolean) => { exerciseDockerCompose.events.on('logs', (log: string, error: boolean, displayable: boolean) => {
this.events.emit('logs', log, error, displayable); this.log(log, error, displayable);
}); });
exerciseDockerCompose.events.on('step', (name: string, message: string) => { exerciseDockerCompose.events.on('step', (name: string, message: string) => {
this.events.emit('subStep', name, message); this.newSubStep(name, message);
}); });
exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => { exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => {
this.events.emit('endSubStep', stepName, message, error); this.endSubStep(message, error);
}); });
exerciseDockerCompose.events.on('finished', (success: boolean, exitCode: number) => { exerciseDockerCompose.events.on('finished', (success: boolean, exitCode: number) => {
...@@ -244,17 +272,17 @@ class AssignmentValidator { ...@@ -244,17 +272,17 @@ class AssignmentValidator {
}); });
} catch ( error ) { } catch ( error ) {
this.fatalErrorMessage = 'Assignment is already solved'; this.fatalErrorMessage = 'Assignment is already solved';
this.events.emit('endStep', 'ASSIGNMENT_RUN', this.fatalErrorMessage, true); this.endStep(this.fatalErrorMessage, true);
this.events.emit('finished', false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY); this.finished(false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY);
return; return;
} }
this.events.emit('endStep', 'ASSIGNMENT_RUN', 'Assignment run successfully', false); this.endStep('Assignment run successfully', false);
} }
this.events.emit('finished', true, 0); this.finished(true, 0);
})(); })();
} }
} }
......
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import Icon from '../../types/Icon'; import Icon from '../../../shared/types/Icon';
import AssignmentValidator from './AssignmentValidator'; import AssignmentValidator from './AssignmentValidator';
......
import ExerciseResultsFile from '../../../shared/types/Dojo/ExerciseResultsFile'; import ExerciseResultsFile from '../../../shared/types/Dojo/ExerciseResultsFile';
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import Icon from '../../types/Icon'; import Icon from '../../../shared/types/Icon';
class ClientsSharedExerciseHelper { class ClientsSharedExerciseHelper {
displayExecutionResults(exerciseResults: ExerciseResultsFile, containerExitCode: number, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }, additionalText: string = '') { 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 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) }`; const finalLogExecutionExitCode = `${ Style.INFO('Execution exit code: ') }${ (containerExitCode == 0 ? Style.SUCCESS : Style.FAILURE)(containerExitCode) }`;
const finalLogResultNumbers = exerciseResults.successfulTests || exerciseResults.failedTests ? `\n\n${ Style.SUCCESS('Tests passed') } : ${ exerciseResults.successfulTests ?? '--' }\n${ Style.FAILURE('Tests failed') } : ${ exerciseResults.failedTests ?? '--' }` : ''; 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 ?? '--' }` : '';
const finalLogSuccessResultDetails = (exerciseResults.successfulTestsList ?? []).map(testName => `- ${ Icon.SUCCESS } ${ testName }`).join('\n'); const finalLogSuccessResultDetails = (exerciseResults.successfulTestsList ?? []).map(testName => `- ${ Icon.SUCCESS } ${ testName }`).join('\n');
const finalLogFailedResultDetails = (exerciseResults.failedTestsList ?? []).map(testName => `- ${ Icon.FAILURE } ${ 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 : '' }` : ''; const finalLogResultDetails = exerciseResults.successfulTestsList || exerciseResults.failedTestsList ? `\n\n${ Style.INFO('Tests: ') }${ finalLogSuccessResultDetails != '' ? '\n' + finalLogSuccessResultDetails : '' }${ finalLogFailedResultDetails != '' ? '\n' + finalLogFailedResultDetails : '' }` : '';
console.log(boxen(`${ finalLogGlobalResult }\n\n${ finalLogExecutionExitCode }${ finalLogResultNumbers }${ finalLogResultDetails }${ additionalText }`, { 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');
return `${ informationTitle }\n${ informationItems }`;
}) ].join('\n\n');
}
console.log(boxen(`${ finalLogGlobalResult }\n\n${ finalLogExecutionExitCode }${ finalLogResultNumbers }${ finalLogResultDetails }${ finalLogInformations }${ additionalText }`, {
title : 'Results', title : 'Results',
titleAlignment: 'center', titleAlignment: 'center',
borderColor : 'yellow', borderColor : 'yellow',
......
import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile'; import AssignmentFile from '../../../shared/types/Dojo/AssignmentFile';
import { TypedEmitter } from 'tiny-typed-emitter'; import { TypedEmitter } from 'tiny-typed-emitter';
import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents'; import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError'; import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError';
import { ChildProcessWithoutNullStreams } from 'node:child_process';
class ExerciseDockerCompose { class ExerciseDockerCompose {
...@@ -15,6 +16,8 @@ class ExerciseDockerCompose { ...@@ -15,6 +16,8 @@ class ExerciseDockerCompose {
public success: boolean = false; public success: boolean = false;
public exitCode: number = -1; public exitCode: number = -1;
private currentStep: string = 'NOT_RUNNING';
constructor(private projectName: string, private assignmentFile: AssignmentFile, private executionFolder: string, private composeFileOverride: Array<string> = []) { constructor(private projectName: string, private assignmentFile: AssignmentFile, private executionFolder: string, private composeFileOverride: Array<string> = []) {
this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => { this.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
this.allLogs += log; this.allLogs += log;
...@@ -28,6 +31,37 @@ class ExerciseDockerCompose { ...@@ -28,6 +31,37 @@ class ExerciseDockerCompose {
}); });
} }
private newStep(name: string, message: string) {
this.currentStep = name;
this.events.emit('step', name, message);
}
private endStep(message: string, error: boolean) {
this.events.emit('endStep', this.currentStep, message, error);
}
private log(message: string, error: boolean, displayable: boolean) {
this.events.emit('logs', message, error, displayable, this.currentStep);
}
private finished(success: boolean, code: number) {
this.events.emit('finished', success, code);
}
private registerChildProcess(childProcess: ChildProcessWithoutNullStreams, resolve: (value: (number | PromiseLike<number>)) => void, reject: (reason?: unknown) => void, displayable: boolean, rejectIfCodeIsNotZero: boolean) {
childProcess.stdout.on('data', (data) => {
this.log(data.toString(), false, displayable);
});
childProcess.stderr.on('data', (data) => {
this.log(data.toString(), true, displayable);
});
childProcess.on('exit', (code) => {
code === null || (rejectIfCodeIsNotZero && code !== 0) ? reject(code) : resolve(code);
});
}
run(doDown: boolean = false) { run(doDown: boolean = false) {
(async () => { (async () => {
let containerExitCode: number = -1; let containerExitCode: number = -1;
...@@ -37,11 +71,11 @@ class ExerciseDockerCompose { ...@@ -37,11 +71,11 @@ class ExerciseDockerCompose {
// Run the service // Run the service
{ {
try { try {
this.events.emit('step', 'COMPOSE_RUN', 'Running Docker Compose file'); this.newStep('COMPOSE_RUN', 'Running Docker Compose file');
containerExitCode = await new Promise<number>((resolve, reject) => { containerExitCode = await new Promise<number>((resolve, reject) => {
this.events.emit('logs', '####################################################### Docker Compose & Main Container Logs #######################################################\n', false, false); this.log('####################################################### Docker Compose & Main Container Logs #######################################################\n', false, false);
const dockerCompose = spawn(`${ dockerComposeCommand } run --build --rm ${ this.assignmentFile.result.container }`, { const dockerCompose = spawn(`${ dockerComposeCommand } run --build --rm ${ this.assignmentFile.result.container }`, {
cwd : this.executionFolder, cwd : this.executionFolder,
...@@ -52,97 +86,93 @@ class ExerciseDockerCompose { ...@@ -52,97 +86,93 @@ class ExerciseDockerCompose {
} }
}); });
dockerCompose.stdout.on('data', (data) => { this.registerChildProcess(dockerCompose, resolve, reject, true, false);
this.events.emit('logs', data.toString(), false, true);
});
dockerCompose.stderr.on('data', (data) => {
this.events.emit('logs', data.toString(), true, true);
});
dockerCompose.on('exit', (code) => {
code !== null ? resolve(code) : reject();
});
}); });
} catch ( error ) { } catch ( error ) {
this.events.emit('endStep', 'COMPOSE_RUN', `Error while running the docker compose file`, true); this.endStep(`Error while running the docker compose file`, true);
this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_RUN_ERROR); this.finished(false, ExerciseCheckerError.DOCKER_COMPOSE_RUN_ERROR);
return; return;
} }
this.events.emit('endStep', 'COMPOSE_RUN', `Docker Compose file run successfully`, false); this.endStep(`Docker Compose file run successfully`, false);
} }
// Get linked services logs // Get linked services logs
{ {
try { try {
this.events.emit('step', 'COMPOSE_LOGS', 'Linked services logs acquisition'); this.newStep('COMPOSE_LOGS', 'Linked services logs acquisition');
await new Promise<void>((resolve, reject) => { await new Promise<number>((resolve, reject) => {
this.events.emit('logs', '####################################################### Other Services Logs #######################################################\n', false, false); this.log('####################################################### Other Services Logs #######################################################\n', false, false);
const dockerCompose = spawn(`${ dockerComposeCommand } logs --timestamps`, { const dockerCompose = spawn(`${ dockerComposeCommand } logs --timestamps`, {
cwd : this.executionFolder, cwd : this.executionFolder,
shell: true shell: true
}); });
dockerCompose.stdout.on('data', (data) => { this.registerChildProcess(dockerCompose, resolve, reject, false, true);
this.events.emit('logs', data.toString(), false, false);
});
dockerCompose.stderr.on('data', (data) => {
this.events.emit('logs', data.toString(), true, false);
});
dockerCompose.on('exit', (code) => {
code !== null ? resolve() : reject();
});
}); });
} catch ( error ) { } catch ( error ) {
this.events.emit('endStep', 'COMPOSE_LOGS', `Error while getting the linked services logs`, true); this.endStep(`Error while getting the linked services logs`, true);
this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_LOGS_ERROR); this.finished(false, ExerciseCheckerError.DOCKER_COMPOSE_LOGS_ERROR);
return; return;
} }
this.events.emit('endStep', 'COMPOSE_LOGS', `Linked services logs acquired`, false); this.endStep(`Linked services logs acquired`, false);
} }
// Remove containers if asked // Remove containers if asked
{ {
if ( doDown ) { if ( doDown ) {
try { try {
this.events.emit('step', 'COMPOSE_DOWN', 'Stopping and removing containers'); this.newStep('COMPOSE_DOWN', 'Stopping and removing containers');
await new Promise<void>((resolve, reject) => { await new Promise<number>((resolve, reject) => {
this.events.emit('logs', '####################################################### Stop and remove containers #######################################################\n', false, false); this.log('####################################################### Stop and remove containers #######################################################\n', false, false);
const dockerCompose = spawn(`${ dockerComposeCommand } down --volumes`, { const dockerCompose = spawn(`${ dockerComposeCommand } down --volumes`, {
cwd : this.executionFolder, cwd : this.executionFolder,
shell: true shell: true
}); });
dockerCompose.stdout.on('data', (data) => { this.registerChildProcess(dockerCompose, resolve, reject, false, true);
this.events.emit('logs', data.toString(), false, false); });
}); } catch ( error ) {
this.endStep(`Error while stopping and removing containers`, true);
this.finished(false, ExerciseCheckerError.DOCKER_COMPOSE_DOWN_ERROR);
return;
}
this.endStep(`Containers stopped and removed`, false);
}
}
dockerCompose.stderr.on('data', (data) => { // Remove images if asked
this.events.emit('logs', data.toString(), true, false); {
}); if ( doDown ) {
try {
this.newStep('COMPOSE_REMOVE_DANGLING', 'Removing dangling images');
dockerCompose.on('exit', (code) => { await new Promise<number>((resolve, reject) => {
code !== null ? resolve() : reject();
this.log('####################################################### Remove dangling images #######################################################\n', false, false);
const dockerCompose = spawn(`docker image prune --force`, {
cwd : this.executionFolder,
shell: true
}); });
this.registerChildProcess(dockerCompose, resolve, reject, false, true);
}); });
} catch ( error ) { } catch ( error ) {
this.events.emit('endStep', 'COMPOSE_DOWN', `Error stop and remove containers`, true); this.endStep(`Error while removing dangling images`, true);
this.events.emit('finished', false, ExerciseCheckerError.DOCKER_COMPOSE_DOWN_ERROR); this.finished(false, ExerciseCheckerError.DOCKER_COMPOSE_REMOVE_DANGLING_ERROR);
return; return;
} }
this.events.emit('endStep', 'COMPOSE_DOWN', `Containers stopped and removed`, false); this.endStep(`Dangling images removed`, false);
} }
} }
this.events.emit('finished', true, containerExitCode); this.finished(true, containerExitCode);
})(); })();
} }
} }
......
...@@ -2,12 +2,12 @@ import { TypedEmitter } from 'tiny-typed-emitter'; ...@@ -2,12 +2,12 @@ import { TypedEmitter } from 'tiny-typed-emitter';
import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents'; import ExerciseRunningEvents from '../../types/Dojo/ExerciseRunningEvents';
import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError'; import ExerciseCheckerError from '../../../shared/types/Dojo/ExerciseCheckerError';
import path from 'node:path'; import path from 'node:path';
import SharedExerciseHelper from '../../../shared/helpers/Dojo/SharedExerciseHelper';
import ClientsSharedConfig from '../../config/ClientsSharedConfig'; import ClientsSharedConfig from '../../config/ClientsSharedConfig';
import Toolbox from '../../../shared/helpers/Toolbox'; import Toolbox from '../../../shared/helpers/Toolbox';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import ExerciseResultsFile from '../../../shared/types/Dojo/ExerciseResultsFile'; import ExerciseResultsFile from '../../../shared/types/Dojo/ExerciseResultsFile';
import JSON5 from 'json5'; import JSON5 from 'json5';
import Json5FileValidator from '../../../shared/helpers/Json5FileValidator';
class ExerciseResultsSanitizerAndValidator { class ExerciseResultsSanitizerAndValidator {
...@@ -37,13 +37,13 @@ class ExerciseResultsSanitizerAndValidator { ...@@ -37,13 +37,13 @@ class ExerciseResultsSanitizerAndValidator {
// Results file schema validation // Results file schema validation
{ {
this.events.emit('step', 'VALIDATE_RESULTS_FILE', 'Validating results file schema'); this.events.emit('step', 'VALIDATE_RESULTS_FILE', 'Validating results file schema');
const validationResults = SharedExerciseHelper.validateResultFile(path); const validationResults = Json5FileValidator.validateFile(ExerciseResultsFile, path);
if ( !validationResults.isValid ) { if ( !validationResults.isValid ) {
this.events.emit('endStep', 'VALIDATE_RESULTS_FILE', `Results file is not valid. Here are the errors :\n${ JSON.stringify(validationResults.errors) }`, true); 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); this.events.emit('finished', false, ExerciseCheckerError.EXERCISE_RESULTS_FILE_SCHEMA_NOT_VALID);
return false; return false;
} }
this.exerciseResults = validationResults.results ?? {}; this.exerciseResults = validationResults.content ?? {};
this.events.emit('endStep', 'VALIDATE_RESULTS_FILE', 'Results file is valid', false); this.events.emit('endStep', 'VALIDATE_RESULTS_FILE', 'Results file is valid', false);
} }
......
...@@ -3,7 +3,7 @@ interface AssignmentValidatorEvents { ...@@ -3,7 +3,7 @@ interface AssignmentValidatorEvents {
subStep: (name: string, message: string) => void; subStep: (name: string, message: string) => void;
endStep: (stepName: string, message: string, error: boolean) => void; endStep: (stepName: string, message: string, error: boolean) => void;
endSubStep: (subStepName: string, message: string, error: boolean) => void; endSubStep: (subStepName: string, message: string, error: boolean) => void;
logs: (log: string, error: boolean, displayable: boolean) => void; logs: (log: string, error: boolean, displayable: boolean, currentStep: string, currentSubStep: string) => void;
finished: (success: boolean, exitCode: number) => void; finished: (success: boolean, exitCode: number) => void;
} }
......
interface ExerciseRunningEvents { interface ExerciseRunningEvents {
step: (name: string, message: string) => void; step: (name: string, message: string) => void;
endStep: (stepName: string, message: string, error: boolean) => void; endStep: (stepName: string, message: string, error: boolean) => void;
logs: (log: string, error: boolean, displayable: boolean) => void; logs: (log: string, error: boolean, displayable: boolean, currentStep: string) => void;
finished: (success: boolean, exitCode: number) => void; finished: (success: boolean, exitCode: number) => void;
} }
......
enum Icon {
CAT_INFO = '▶️',
INFO = 'ℹ️',
ERROR = '⛔️',
SUCCESS = '',
FAILURE = ''
}
export default Icon;
\ No newline at end of file