From 60cf838cceefc0114f2ed84fe333537103d09591 Mon Sep 17 00:00:00 2001 From: ANIRUDH PANWAR Date: Mon, 29 Jul 2024 15:12:26 +0530 Subject: [PATCH 1/4] Update validator with new properties required for multifile --- enums/supportedMultifileSetupTypes.js | 1 + enums/supportedOutputFormatsMultifile.js | 3 +++ validators/code.validator.js | 19 ++++++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 enums/supportedOutputFormatsMultifile.js diff --git a/enums/supportedMultifileSetupTypes.js b/enums/supportedMultifileSetupTypes.js index 5e428533..91883f6e 100644 --- a/enums/supportedMultifileSetupTypes.js +++ b/enums/supportedMultifileSetupTypes.js @@ -1,4 +1,5 @@ module.exports = { FRONTEND_STATIC_JASMINE: 'frontend_static_jasmine', FRONTEND_REACT_JASMINE: 'frontend_react_jasmine', + NODEJS_JUNIT: 'nodejs_junit', } \ No newline at end of file diff --git a/enums/supportedOutputFormatsMultifile.js b/enums/supportedOutputFormatsMultifile.js new file mode 100644 index 00000000..cd88860d --- /dev/null +++ b/enums/supportedOutputFormatsMultifile.js @@ -0,0 +1,3 @@ +module.exports = { + JUNIT: 'junit' +} \ No newline at end of file diff --git a/validators/code.validator.js b/validators/code.validator.js index 4dfd1563..cbd53021 100644 --- a/validators/code.validator.js +++ b/validators/code.validator.js @@ -8,7 +8,9 @@ const { const { FRONTEND_REACT_JASMINE, FRONTEND_STATIC_JASMINE, + NODEJS_JUNIT, } = require('../enums/supportedMultifileSetupTypes') +const { JUNIT } = require('../enums/supportedOutputFormatsMultifile') const _getBaseSchema = () => { return Joi.object({ @@ -33,11 +35,26 @@ const _getMultiFileSchema = () => { url: Joi.string().trim().required(), type: Joi.string() .trim() - .valid(FRONTEND_REACT_JASMINE, FRONTEND_STATIC_JASMINE) + .valid(FRONTEND_REACT_JASMINE, FRONTEND_STATIC_JASMINE, NODEJS_JUNIT) .required(), non_editable_files: Joi.object() .pattern(Joi.string(), Joi.string().pattern(/^[a-fA-F0-9]{64}$/)) .optional(), + commands: Joi.alternatives().conditional('type', { + is: NODEJS_JUNIT, + then: Joi.array().items(Joi.string().required()), + otherwise: Joi.optional(), + }), + output_file: Joi.alternatives().conditional('type', { + is: NODEJS_JUNIT, + then: Joi.string().required(), + otherwise: Joi.optional(), + }), + output_format: Joi.alternatives().conditional('type', { + is: NODEJS_JUNIT, + then: Joi.string().valid(JUNIT).required(), + otherwise: Joi.optional(), + }), }) } From 4b247085d0983fb3e0f55913ca852b36aae1fdb3 Mon Sep 17 00:00:00 2001 From: ANIRUDH PANWAR Date: Mon, 12 Aug 2024 09:08:47 +0530 Subject: [PATCH 2/4] Enable nodejs_junit evaluation for PMF --- helpers/childProcess.helper.js | 54 +++++++++++++++++++++++++++++++ helpers/fileParser.helper.js | 44 ++++++++++++++++++++++++++ package.json | 4 ++- services/code.service.js | 58 +++++++++++++++++++++++----------- 4 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 helpers/childProcess.helper.js create mode 100644 helpers/fileParser.helper.js diff --git a/helpers/childProcess.helper.js b/helpers/childProcess.helper.js new file mode 100644 index 00000000..2cb63f4f --- /dev/null +++ b/helpers/childProcess.helper.js @@ -0,0 +1,54 @@ +const logger = require('../loader').helpers.l + +const initExeca = async () => { + const module = await import('execa') + return module.execa +} + +const runCommand = async (command, workingDir) => { + const execa = await initExeca() + logger.info(`Executing: ${command}`); + try { + const result = await execa(command, { + cwd: workingDir, + shell: true, // Usefull for npm commands having '&&' + stdio: 'inherit', + preferLocal: true, // Use local binaries if available + localDir: workingDir, + reject: false, // Don't throw on non-zero exit code + }) + + if (result.failed) { + throw new Error(`Command failed with exit code ${result.exitCode}`) + } + return result + } catch (error) { + if (error.exitCode !== undefined) { + logger.error(`Command exited with code ${error.exitCode}`) + if (error.stderr) logger.error(`Error output: ${error.stderr}`) + } else if (error.signal) { + logger.error(`Command was killed with signal ${error.signal}`) + } else { + logger.error(`Command failed to execute: ${error.message}`) + } + throw error + } +} + +const runCommandsSequentially = async ( + commands, + workingDir = process.cwd() +) => { + for (const command of commands) { + try { + await runCommand(command, workingDir) + } catch (error) { + logger.error(`Error executing "${command}"`) + if (error.stdout) logger.info(`Command output: ${error.stdout}`) + throw new Error(`Command sequence failed at: ${command}`) + } + } + logger.info("All commands completed successfully") +} + +module.exports = { runCommandsSequentially } \ No newline at end of file diff --git a/helpers/fileParser.helper.js b/helpers/fileParser.helper.js new file mode 100644 index 00000000..46a4097e --- /dev/null +++ b/helpers/fileParser.helper.js @@ -0,0 +1,44 @@ +const logger = require('../loader').helpers.l +const fs = require('fs') +const xml2js = require('xml2js') + +// recursively process all the test suites +const processTestSuite = (testsuite, success, failed) => { + if (testsuite.testsuite) { + testsuite.testsuite.forEach(suite => processTestSuite(suite, success, failed)) + } + if (testsuite.testcase) { + testsuite.testcase.forEach(testcase => { + const testName = testcase.$.name + + if (testcase.failure) { + failed.push(testName) + } else { + success.push(testName) + } + }) + } +} + +const extractTestCasesJunit = async (xmlFilePath) => { + try { + const xmlData = await fs.promises.readFile(xmlFilePath, 'utf8') + const parser = new xml2js.Parser() + const result = await parser.parseStringPromise(xmlData) + + const success = [] + const failed = [] + + // Start processing from the root + if (result.testsuites) { + processTestSuite(result.testsuites, success, failed) + } + + return { success, failed } + } catch (error) { + logger.error(`Error processing XML file: ${error.message}`) + throw error + } +} + +module.exports = { extractTestCasesJunit } \ No newline at end of file diff --git a/package.json b/package.json index c252de61..541153a6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^16.0.3", + "execa": "^9.3.0", "express": "^4.18.2", "helmet": "^6.0.1", "husky": "^8.0.1", @@ -26,7 +27,8 @@ "sqlite-parser": "^1.0.1", "sqlite3": "^5.1.7", "uuid": "^9.0.0", - "winston": "^3.11.0" + "winston": "^3.11.0", + "xml2js": "^0.6.2" }, "devDependencies": { "eslint": "8.22.0", diff --git a/services/code.service.js b/services/code.service.js index f8426c34..a4120248 100644 --- a/services/code.service.js +++ b/services/code.service.js @@ -18,12 +18,15 @@ const express = require('express') const http = require('http') const { spawn } = require('child_process'); const appConfig = require('../configs/app.config.js') -const { FRONTEND_STATIC_JASMINE } = require('../enums/supportedMultifileSetupTypes.js') +const { FRONTEND_STATIC_JASMINE, NODEJS_JUNIT, FRONTEND_REACT_JASMINE } = require('../enums/supportedMultifileSetupTypes.js') const axios = require('axios') const supportedLanguages = require('../enums/supportedLanguages') const { generate } = require('@builder.io/sqlgenerate') const parser = require('sqlite-parser') const crypto = require('crypto') +const { JUNIT } = require('../enums/supportedOutputFormatsMultifile.js') +const { runCommandsSequentially } = require('../helpers/childProcess.helper.js') +const { extractTestCasesJunit } = require('../helpers/fileParser.helper.js') const _runScript = async (cmd, res, runMemoryCheck = false) => { let initialMemory = 0 @@ -802,8 +805,9 @@ const _killProcessOnPort = async (port) => { }) } -const _preCleanUp = async () => { +const _preCleanUp = async (multifileType) => { try { + if(multifileType === NODEJS_JUNIT) return; await _killProcessOnPort(appConfig.multifile.jasminePort) // TODO: add pre cleanup for puppeteer and jasmine server to prevent memory leak } catch (err) { @@ -832,10 +836,17 @@ const _checkIntegrity = async (non_editable_files) => { return true } +const parseResults = async (filePath, testCaseFormat) => { + switch (testCaseFormat) { + case JUNIT: + return extractTestCasesJunit(filePath) + } +} + const _executeMultiFile = async (req, res, response) => { logger.info(`serving ${req.type}`) try { - await _preCleanUp() + await _preCleanUp(req.type) const fileContent = await _getSubmissionDataFromGCS(req.url, appConfig.multifile.submissionFileDownloadPath) await _writeFilesToDisk(fileContent, appConfig.multifile.workingDir) } catch (err) { @@ -849,22 +860,31 @@ const _executeMultiFile = async (req, res, response) => { const isValidSubmission = await _checkIntegrity(req.non_editable_files) if(!isValidSubmission) throw new Error(`A non editable file has been modified, exiting...`) } - if (req.type === FRONTEND_STATIC_JASMINE) { - const staticServerInstance = await _startStaticServer(appConfig.multifile.staticServerPath) - jasmineResults = await _runTests() - if (staticServerInstance) { - staticServerInstance.close(() => { - logger.error('Static server closed') - }); - } - } else { - if (!fs.existsSync(appConfig.multifile.workingDir + 'package.json')) { - throw new Error(`No package.json found`) - } - await _installDependencies(appConfig.multifile.workingDir) - const jasmineServer = await _startJasmineServer() - jasmineResults = await _runTests() - process.kill(-jasmineServer.pid) // kill entire process group including child process and transitive child processes + switch (req.type) { + case FRONTEND_STATIC_JASMINE: + const staticServerInstance = await _startStaticServer(appConfig.multifile.staticServerPath) + jasmineResults = await _runTests() + if (staticServerInstance) { + staticServerInstance.close(() => { + logger.error('Static server closed') + }) + } + break + case FRONTEND_REACT_JASMINE: + if (!fs.existsSync(appConfig.multifile.workingDir + 'package.json')) { + throw new Error(`No package.json found`) + } + await _installDependencies(appConfig.multifile.workingDir) + const jasmineServer = await _startJasmineServer() + jasmineResults = await _runTests() + process.kill(-jasmineServer.pid) // kill entire process group including child process and transitive child processes + break + case NODEJS_JUNIT: + if (!fs.existsSync(appConfig.multifile.workingDir + 'package.json')) { + throw new Error(`No package.json found`) + } + await runCommandsSequentially(req.commands, appConfig.multifile.workingDir) + jasmineResults = await parseResults(appConfig.multifile.workingDir + req.output_file, req.output_format) } await _cleanUpDir(appConfig.multifile.workingDir, appConfig.multifile.submissionFileDownloadPath) From ba86ede39e62cba90ba04769bf6504ecb17f27f2 Mon Sep 17 00:00:00 2001 From: ANIRUDH PANWAR Date: Mon, 12 Aug 2024 09:37:24 +0530 Subject: [PATCH 3/4] Rename modules --- ...OutputFormatsMultifile.js => supportedPMFOutputFormats.js} | 0 .../{supportedMultifileSetupTypes.js => supportedPMFTypes.js} | 0 services/code.service.js | 2 +- validators/code.validator.js | 4 ++-- 4 files changed, 3 insertions(+), 3 deletions(-) rename enums/{supportedOutputFormatsMultifile.js => supportedPMFOutputFormats.js} (100%) rename enums/{supportedMultifileSetupTypes.js => supportedPMFTypes.js} (100%) diff --git a/enums/supportedOutputFormatsMultifile.js b/enums/supportedPMFOutputFormats.js similarity index 100% rename from enums/supportedOutputFormatsMultifile.js rename to enums/supportedPMFOutputFormats.js diff --git a/enums/supportedMultifileSetupTypes.js b/enums/supportedPMFTypes.js similarity index 100% rename from enums/supportedMultifileSetupTypes.js rename to enums/supportedPMFTypes.js diff --git a/services/code.service.js b/services/code.service.js index a4120248..b04e33b6 100644 --- a/services/code.service.js +++ b/services/code.service.js @@ -18,7 +18,7 @@ const express = require('express') const http = require('http') const { spawn } = require('child_process'); const appConfig = require('../configs/app.config.js') -const { FRONTEND_STATIC_JASMINE, NODEJS_JUNIT, FRONTEND_REACT_JASMINE } = require('../enums/supportedMultifileSetupTypes.js') +const { FRONTEND_STATIC_JASMINE, NODEJS_JUNIT, FRONTEND_REACT_JASMINE } = require('../enums/supportedPMFTypes.js') const axios = require('axios') const supportedLanguages = require('../enums/supportedLanguages') const { generate } = require('@builder.io/sqlgenerate') diff --git a/validators/code.validator.js b/validators/code.validator.js index cbd53021..a145863e 100644 --- a/validators/code.validator.js +++ b/validators/code.validator.js @@ -9,8 +9,8 @@ const { FRONTEND_REACT_JASMINE, FRONTEND_STATIC_JASMINE, NODEJS_JUNIT, -} = require('../enums/supportedMultifileSetupTypes') -const { JUNIT } = require('../enums/supportedOutputFormatsMultifile') +} = require('../enums/supportedPMFTypes') +const { JUNIT } = require('../enums/supportedPMFOutputFormats') const _getBaseSchema = () => { return Joi.object({ From a527d8d0c803904b054fc173ccce95a0b7c7e014 Mon Sep 17 00:00:00 2001 From: ANIRUDH PANWAR Date: Mon, 12 Aug 2024 09:38:58 +0530 Subject: [PATCH 4/4] Fix imports --- services/code.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/code.service.js b/services/code.service.js index b04e33b6..3fe9df10 100644 --- a/services/code.service.js +++ b/services/code.service.js @@ -24,7 +24,7 @@ const supportedLanguages = require('../enums/supportedLanguages') const { generate } = require('@builder.io/sqlgenerate') const parser = require('sqlite-parser') const crypto = require('crypto') -const { JUNIT } = require('../enums/supportedOutputFormatsMultifile.js') +const { JUNIT } = require('../enums/supportedPMFOutputFormats.js') const { runCommandsSequentially } = require('../helpers/childProcess.helper.js') const { extractTestCasesJunit } = require('../helpers/fileParser.helper.js')