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

Target

Select target project
  • dojo_project/projects/shared/nodeclientsharedcode
1 result
Select Git revision
Show changes
Commits on Source (6)
...@@ -21,28 +21,28 @@ class ClientsSharedConfig { ...@@ -21,28 +21,28 @@ class ClientsSharedConfig {
constructor() { constructor() {
this.apiURL = process.env.API_URL || ''; this.apiURL = process.env.API_URL ?? '';
this.assignment = { this.assignment = {
filename : process.env.ASSIGNMENT_FILENAME || '', filename : process.env.ASSIGNMENT_FILENAME ?? '',
neededFiles: JSON.parse(process.env.EXERCISE_NEEDED_FILES || '[]') neededFiles: JSON.parse(process.env.EXERCISE_NEEDED_FILES ?? '[]')
}; };
this.gitlab = { this.gitlab = {
dojoAccount: { dojoAccount: {
id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID) || -1, id : Number(process.env.GITLAB_DOJO_ACCOUNT_ID ?? -1),
username: process.env.GITLAB_DOJO_ACCOUNT_USERNAME || '' username: process.env.GITLAB_DOJO_ACCOUNT_USERNAME ?? ''
} }
}; };
this.dockerCompose = { 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 = { this.filenames = {
results: process.env.EXERCISE_RESULTS_FILENAME || '' results: process.env.EXERCISE_RESULTS_FILENAME ?? ''
}; };
} }
} }
......
...@@ -17,6 +17,9 @@ const execAsync = util.promisify(exec); ...@@ -17,6 +17,9 @@ const execAsync = util.promisify(exec);
class AssignmentValidator { class AssignmentValidator {
private readonly folderAssignment: string;
private readonly doDown: boolean;
readonly events: TypedEmitter<AssignmentValidatorEvents> = new TypedEmitter<AssignmentValidatorEvents>(); readonly events: TypedEmitter<AssignmentValidatorEvents> = new TypedEmitter<AssignmentValidatorEvents>();
public displayableLogs: string = ''; public displayableLogs: string = '';
...@@ -30,7 +33,13 @@ class AssignmentValidator { ...@@ -30,7 +33,13 @@ class AssignmentValidator {
private currentStep: string = 'NOT_RUNNING'; private currentStep: string = 'NOT_RUNNING';
private currentSubStep: 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.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
this.allLogs += log; this.allLogs += log;
this.displayableLogs += displayable ? log : ''; this.displayableLogs += displayable ? log : '';
...@@ -77,18 +86,13 @@ class AssignmentValidator { ...@@ -77,18 +86,13 @@ class AssignmentValidator {
this.finished(false, code); this.finished(false, code);
} }
run(doDown: boolean = false) { /**
(async () => { * Step 1: Check requirements
let dockerComposeFile: DojoDockerCompose; * - Check if Docker daemon is running
let assignmentFile: AssignmentFile; * - Check if required files exists
* @private
/*
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1: Check requirements
- Check if Docker daemon is running
- Check if required files exists
*/ */
{ private async checkRequirements() {
this.newStep('REQUIREMENTS_CHECKING', 'Please wait while we are checking requirements...'); this.newStep('REQUIREMENTS_CHECKING', 'Please wait while we are checking requirements...');
...@@ -98,7 +102,7 @@ class AssignmentValidator { ...@@ -98,7 +102,7 @@ class AssignmentValidator {
await execAsync(`docker ps`); await execAsync(`docker ps`);
} catch ( error ) { } catch ( error ) {
this.emitError(`Docker daemon isn't running`, `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; throw new Error();
} }
this.endSubStep('Docker daemon is running', false); this.endSubStep('Docker daemon is running', false);
...@@ -110,7 +114,7 @@ class AssignmentValidator { ...@@ -110,7 +114,7 @@ class AssignmentValidator {
if ( missingFiles.length > 0 ) { 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); 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; throw new Error();
} }
this.endSubStep('All required files exists', false); this.endSubStep('All required files exists', false);
...@@ -118,43 +122,44 @@ class AssignmentValidator { ...@@ -118,43 +122,44 @@ class AssignmentValidator {
this.endStep('All requirements are satisfied', false); this.endStep('All requirements are satisfied', false);
} }
/**
/* * Step 2: dojo_assignment.json file validation
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 2: dojo_assignment.json file validation * - Structure validation
- Structure validation * - 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) * @private
*/ */
{ private dojoAssignmentFileValidation() {
this.newStep('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...');
const assignmentFileValidationError = `${ ClientsSharedConfig.assignment.filename } file is invalid`;
// Structure validation // Structure validation
this.newSubStep('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 ) {
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); this.emitError(`dojo_assignment.json file schema is invalid.\nHere are the errors:\n${ validationResults.error }`, assignmentFileValidationError, AssignmentCheckerError.ASSIGNMENT_FILE_SCHEMA_ERROR);
return; throw new Error();
} }
assignmentFile = validationResults.content!; this.assignmentFile = validationResults.content!;
this.endSubStep('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.newSubStep('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Validating immutable files'); this.newSubStep('ASSIGNMENT_FILE_IMMUTABLES_VALIDATION', 'Validating immutable files');
for ( const immutable of validationResults.content!.immutable ) { for ( const immutable of this.assignmentFile.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) ) {
this.emitError(`Immutable path not found: ${ immutable.path }`, 'dojo_assignment.json file is invalid', AssignmentCheckerError.IMMUTABLE_PATH_NOT_FOUND); this.emitError(`Immutable path not found: ${ immutable.path }`, assignmentFileValidationError, AssignmentCheckerError.IMMUTABLE_PATH_NOT_FOUND);
return; throw new Error();
} }
const isDirectory = fs.lstatSync(immutablePath).isDirectory(); const isDirectory = fs.lstatSync(immutablePath).isDirectory();
if ( isDirectory && !immutable.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); this.emitError(`Immutable (${ immutable.path }) is declared as a file but is a directory.`, assignmentFileValidationError, AssignmentCheckerError.IMMUTABLE_PATH_IS_NOT_DIRECTORY);
return; throw new Error();
} else if ( !isDirectory && immutable.isDirectory === true ) { } 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); this.emitError(`Immutable (${ immutable.path }) is declared as a directory but is a file.`, assignmentFileValidationError, AssignmentCheckerError.IMMUTABLE_PATH_IS_DIRECTORY);
return; throw new Error();
} }
} }
this.endSubStep('Immutable files are valid', false); this.endSubStep('Immutable files are valid', false);
...@@ -163,23 +168,24 @@ class AssignmentValidator { ...@@ -163,23 +168,24 @@ class AssignmentValidator {
this.endStep('dojo_assignment.json file is valid', false); this.endStep('dojo_assignment.json file is valid', false);
} }
/**
/* * Step 3: Docker Compose file validation
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 3: Docker Compose file validation * - Global validation
- Global validation * - Validation of the containers and volumes named in dojo_assignment.json
- 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...'); this.newStep('DOCKER_COMPOSE_VALIDATION', 'Please wait while we are validating docker compose file...');
const composeFileValidationError = `Docker compose file is invalid`;
// Global validation // Global validation
this.newSubStep('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; this.dockerComposeFile = YAML.parse(fs.readFileSync(path.join(this.folderAssignment, 'docker-compose.yml'), 'utf8')) as DojoDockerCompose;
} catch ( error ) { } catch ( error ) {
this.emitError(`Docker compose file yaml structure is invalid.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_YAML_ERROR); this.emitError(`Docker compose file yaml structure is invalid.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_YAML_ERROR);
return; throw new Error();
} }
try { try {
...@@ -189,26 +195,26 @@ class AssignmentValidator { ...@@ -189,26 +195,26 @@ class AssignmentValidator {
shell: true shell: true
}); });
dockerComposeValidation.on('exit', (code) => { dockerComposeValidation.on('exit', code => {
code !== null && code == 0 ? resolve() : reject(); code !== null && code === 0 ? resolve() : reject(code);
}); });
}); });
} catch ( error ) { } catch ( error ) {
this.emitError(`Docker compose file structure is invalid.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_SCHEMA_ERROR); this.emitError(`Docker compose file structure is invalid.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_SCHEMA_ERROR);
return; throw new Error();
} }
this.endSubStep('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.newSubStep('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 ( !(this.assignmentFile.result.container in this.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); this.emitError(`Container specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_CONTAINER_MISSING);
return; throw new Error();
} }
if ( assignmentFile.result.volume && (!dockerComposeFile!.volumes || !(assignmentFile.result.volume in dockerComposeFile!.volumes)) ) { 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.`, 'Docker compose file is invalid', AssignmentCheckerError.COMPOSE_FILE_VOLUME_MISSING); this.emitError(`Volume specified in ${ ClientsSharedConfig.assignment.filename } is missing from compose file.`, composeFileValidationError, AssignmentCheckerError.COMPOSE_FILE_VOLUME_MISSING);
return; throw new Error();
} }
this.endSubStep('Docker compose file content is valid', false); this.endSubStep('Docker compose file content is valid', false);
...@@ -216,22 +222,22 @@ class AssignmentValidator { ...@@ -216,22 +222,22 @@ class AssignmentValidator {
this.endStep('Docker compose file is valid', false); this.endStep('Docker compose file is valid', false);
} }
/**
/* * Step 4: Dockerfiles validation
//////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 4: Dockerfiles validation * - Check if file exists
- Check if file exists * - TODO - Dockerfile structure linter - Issue #51 - https://github.com/hadolint/hadolint
- 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...'); this.newStep('DOCKERFILE_VALIDATION', 'Please wait while we are validating dockerfiles...');
this.newSubStep('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(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)); const filesNotFound = dockerfilesPaths.filter(dockerfilePath => !fs.existsSync(dockerfilePath));
if ( filesNotFound.length > 0 ) { if ( filesNotFound.length > 0 ) {
this.emitError(`Dockerfiles not found: ${ filesNotFound.join(', ') }`, 'Dockerfiles are invalid', AssignmentCheckerError.DOCKERFILE_NOT_FOUND); this.emitError(`Dockerfiles not found: ${ filesNotFound.join(', ') }`, 'Dockerfiles are invalid', AssignmentCheckerError.DOCKERFILE_NOT_FOUND);
return; throw new Error();
} }
this.endSubStep('Docker compose file content is valid', false); this.endSubStep('Docker compose file content is valid', false);
...@@ -239,16 +245,16 @@ class AssignmentValidator { ...@@ -239,16 +245,16 @@ class AssignmentValidator {
this.endStep('Dockerfiles are valid', false); this.endStep('Dockerfiles are valid', false);
} }
/**
/* * Step 5: Run
//////////////////////////////////////////////////////////////////////////////////////////////////////////// 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)
- 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.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, this.assignmentFile, this.folderAssignment);
try { try {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
...@@ -260,29 +266,44 @@ class AssignmentValidator { ...@@ -260,29 +266,44 @@ class AssignmentValidator {
this.newSubStep(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.endSubStep(message, error); this.endSubStep(message, error);
}); });
exerciseDockerCompose.events.on('finished', (success: boolean, exitCode: number) => { exerciseDockerCompose.events.on('finished', (_success: boolean, exitCode: number) => {
exitCode != 0 ? resolve() : reject(); exitCode !== 0 ? resolve() : reject(exitCode);
}); });
exerciseDockerCompose.run(doDown); exerciseDockerCompose.run(this.doDown);
}); });
} catch ( error ) { } catch ( error ) {
this.fatalErrorMessage = 'Assignment is already solved'; this.fatalErrorMessage = 'Assignment is already solved';
this.endStep(this.fatalErrorMessage, true); this.endStep(this.fatalErrorMessage, true);
this.finished(false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY); this.finished(false, AssignmentCheckerError.COMPOSE_RUN_SUCCESSFULLY);
return; throw new Error();
} }
this.endStep('Assignment run successfully', false); this.endStep('Assignment run successfully', false);
} }
run() {
(async () => {
try {
await this.checkRequirements();
this.dojoAssignmentFileValidation();
await this.dockerComposeFileValidation();
this.dockerfilesValidation();
await this.runAssignment();
this.finished(true, 0); this.finished(true, 0);
} catch ( error ) {
return;
}
})(); })();
} }
} }
......
...@@ -6,7 +6,8 @@ import AssignmentValidator from './AssignmentValidator'; ...@@ -6,7 +6,8 @@ import AssignmentValidator from './AssignmentValidator';
class ClientsSharedAssignmentHelper { class ClientsSharedAssignmentHelper {
displayExecutionResults(validator: AssignmentValidator, successMessage: string, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }) { 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 finalLogSuccessMessage = validator.success ? `${ successMessage }` : '';
const finalLogErrorMessage = !validator.success ? `${ Style.INFO('Error message') } :\n${ Style.FAILURE(validator.fatalErrorMessage) }` : ''; const finalLogErrorMessage = !validator.success ? `${ Style.INFO('Error message') } :\n${ Style.FAILURE(validator.fatalErrorMessage) }` : '';
......
...@@ -5,27 +5,31 @@ import Icon from '../../../shared/types/Icon'; ...@@ -5,27 +5,31 @@ 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 = '') {
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 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 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 : '' }` : '';
let finalLogInformations = ''; private getOtherInformations(exerciseResults: ExerciseResultsFile, Style: { INFO: chalk.Chalk, SUCCESS: chalk.Chalk, FAILURE: chalk.Chalk }) {
if ( exerciseResults.otherInformations ) { return exerciseResults.otherInformations ? [ '', ...exerciseResults.otherInformations.map(information => {
finalLogInformations = [ '', ...exerciseResults.otherInformations.map(information => { const informationTitle = Style.INFO(`${ information.icon && information.icon !== '' ? Icon[information.icon] + ' ' : '' }${ information.name }: `);
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 informationItems = typeof information.itemsOrInformations == 'string' ? information.itemsOrInformations : information.itemsOrInformations.map(item => `- ${ item }`).join('\n');
return `${ informationTitle }\n${ informationItems }`; return `${ informationTitle }\n${ informationItems }`;
}) ].join('\n\n'); }) ].join('\n\n') : '';
} }
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) }`;
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 ?? '--' }` : '';
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 }`, { console.log(boxen(`${ finalLogGlobalResult }\n\n${ finalLogExecutionExitCode }${ finalLogResultNumbers }${ finalLogResultDetails }${ finalLogInformations }${ additionalText }`, {
title : 'Results', title : 'Results',
......
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
...@@ -18,7 +18,17 @@ class ExerciseDockerCompose { ...@@ -18,7 +18,17 @@ class ExerciseDockerCompose {
private currentStep: string = 'NOT_RUNNING'; 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.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
this.allLogs += log; this.allLogs += log;
this.displayableLogs += displayable ? log : ''; this.displayableLogs += displayable ? log : '';
...@@ -49,15 +59,15 @@ class ExerciseDockerCompose { ...@@ -49,15 +59,15 @@ class ExerciseDockerCompose {
} }
private registerChildProcess(childProcess: ChildProcessWithoutNullStreams, resolve: (value: (number | PromiseLike<number>)) => void, reject: (reason?: unknown) => void, displayable: boolean, rejectIfCodeIsNotZero: boolean) { 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); this.log(data.toString(), false, displayable);
}); });
childProcess.stderr.on('data', (data) => { childProcess.stderr.on('data', data => {
this.log(data.toString(), true, displayable); this.log(data.toString(), true, displayable);
}); });
childProcess.on('exit', (code) => { childProcess.on('exit', code => {
code === null || (rejectIfCodeIsNotZero && code !== 0) ? reject(code) : resolve(code); code === null || (rejectIfCodeIsNotZero && code !== 0) ? reject(code) : resolve(code);
}); });
} }
...@@ -66,7 +76,8 @@ class ExerciseDockerCompose { ...@@ -66,7 +76,8 @@ class ExerciseDockerCompose {
(async () => { (async () => {
let containerExitCode: number = -1; 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 // Run the service
{ {
......
...@@ -17,7 +17,15 @@ class ExerciseResultsSanitizerAndValidator { ...@@ -17,7 +17,15 @@ class ExerciseResultsSanitizerAndValidator {
private resultsFilePath: string = ''; 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() { private async resultsFileSanitization() {
this.events.emit('step', 'RESULTS_FILE_SANITIZATION', 'Sanitizing results file'); this.events.emit('step', 'RESULTS_FILE_SANITIZATION', 'Sanitizing results file');
...@@ -33,11 +41,11 @@ class ExerciseResultsSanitizerAndValidator { ...@@ -33,11 +41,11 @@ class ExerciseResultsSanitizerAndValidator {
this.events.emit('endStep', 'RESULTS_FILE_SANITIZATION', 'Results file sanitized', false); 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 // 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 = Json5FileValidator.validateFile(ExerciseResultsFile, path); const validationResults = Json5FileValidator.validateFile(ExerciseResultsFile, resultFilePath);
if ( !validationResults.isValid ) { 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('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);
......
...@@ -14,6 +14,8 @@ interface Assignment { ...@@ -14,6 +14,8 @@ interface Assignment {
staff: Array<User>; staff: Array<User>;
exercises: Array<Exercise>; exercises: Array<Exercise>;
corrections: Array<Exercise>;
} }
......
import GitlabRepository from '../../shared/types/Gitlab/GitlabRepository'; import GitlabRepository from '../../shared/types/Gitlab/GitlabRepository';
import * as Gitlab from '@gitbeaker/rest';
import User from './User';
import Assignment from './Assignment';
interface Exercise { interface Exercise {
...@@ -10,6 +13,12 @@ interface Exercise { ...@@ -10,6 +13,12 @@ interface Exercise {
gitlabCreationInfo: GitlabRepository; gitlabCreationInfo: GitlabRepository;
gitlabLastInfo: GitlabRepository; gitlabLastInfo: GitlabRepository;
gitlabLastInfoDate: string; gitlabLastInfoDate: string;
members: Array<User> | undefined;
assignment: Assignment | undefined;
isCorrection: boolean;
correctionCommit: Gitlab.CommitSchema | undefined;
} }
......
...@@ -2,14 +2,16 @@ enum ApiRoute { ...@@ -2,14 +2,16 @@ enum ApiRoute {
LOGIN = '/login', LOGIN = '/login',
REFRESH_TOKENS = '/refresh_tokens', REFRESH_TOKENS = '/refresh_tokens',
TEST_SESSION = '/test_session', TEST_SESSION = '/test_session',
GITLAB_CHECK_TEMPLATE_ACCESS = '/gitlab/project/{{id}}/checkTemplateAccess', GITLAB_CHECK_TEMPLATE_ACCESS = '/gitlab/project/{{gitlabProjectId}}/checkTemplateAccess',
ASSIGNMENT_GET = '/assignments/{{nameOrUrl}}', ASSIGNMENT_GET = '/assignments/{{assignmentNameOrUrl}}',
ASSIGNMENT_CREATE = '/assignments', ASSIGNMENT_CREATE = '/assignments',
ASSIGNMENT_PUBLISH = '/assignments/{{nameOrUrl}}/publish', ASSIGNMENT_PUBLISH = '/assignments/{{assignmentNameOrUrl}}/publish',
ASSIGNMENT_UNPUBLISH = '/assignments/{{nameOrUrl}}/unpublish', ASSIGNMENT_UNPUBLISH = '/assignments/{{assignmentNameOrUrl}}/unpublish',
EXERCISE_CREATE = '/assignments/{{nameOrUrl}}/exercises', ASSIGNMENT_CORRECTION_LINK = '/assignments/{{assignmentNameOrUrl}}/corrections',
EXERCISE_ASSIGNMENT = '/exercises/{{id}}/assignment', ASSIGNMENT_CORRECTION_UPDATE = '/assignments/{{assignmentNameOrUrl}}/corrections/{{exerciseIdOrUrl}}',
EXERCISE_RESULTS = '/exercises/{{id}}/results' EXERCISE_CREATE = '/assignments/{{assignmentNameOrUrl}}/exercises',
EXERCISE_ASSIGNMENT = '/exercises/{{exerciseIdOrUrl}}/assignment',
EXERCISE_RESULTS = '/exercises/{{exerciseIdOrUrl}}/results'
} }
......