diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..cb1265b --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/java-grammar.js b/java-grammar.js new file mode 100644 index 0000000..f2e69a5 --- /dev/null +++ b/java-grammar.js @@ -0,0 +1,231 @@ +window.codioTestAssessment = window.codioTestAssessment || {} +window.codioTestAssessment.javaGrammar = window.codioTestAssessment.javaGrammar || {} + +window.codioTestAssessment.javaGrammar.SLASH = '/' +window.codioTestAssessment.javaGrammar.BACK_SLASH = '\\' +window.codioTestAssessment.javaGrammar.STAR = '*' +window.codioTestAssessment.javaGrammar.DOUBLE_QUOTE = '"' +window.codioTestAssessment.javaGrammar.NEW_LINE = '\n' +window.codioTestAssessment.javaGrammar.LEFT_BRACE = '{' +window.codioTestAssessment.javaGrammar.RIGHT_BRACE = '}' + +window.codioTestAssessment.javaGrammar.packageReg = new RegExp( + 'package\\s+([a-zA-Z_]{1}[a-zA-Z0-9_]*(\\.[a-zA-Z_]{1}[a-zA-Z0-9_]*)*)\\s*;', + 'm' +) +window.codioTestAssessment.javaGrammar.classNameReg = new RegExp( + '\\s*(public|private)?(static)?\\s*class\\s+(\\w+)' + + '\\s*((extends\\s+\\w+)|(implements\\s+\\w+( ,\\w+)*))?\\s*\\{', + 'm' +) +window.codioTestAssessment.javaGrammar.anonClassNameReg = new RegExp('\\s*new\\s+(\\w+)\\s*\\(\\w*\\)\\s*\\{', 'm') + +function removeCommentsAndStrings(content) { + const {DOUBLE_QUOTE, BACK_SLASH, SLASH, STAR, NEW_LINE} = window.codioTestAssessment.javaGrammar + let updatedContent = '' + let isCommentStarted = false + let isStringStarted = false + let isMultiline = false + for (let i = 0, len = content.length; i < len; i++) { + let current = content[i] + if (current === DOUBLE_QUOTE && !isCommentStarted) { + if (content[i - 1] !== BACK_SLASH) { + isStringStarted = !isStringStarted + if (!isStringStarted) { + current = ' ' + } + } + } + if (!isStringStarted && current === SLASH) { + if (content[i + 1] === SLASH) { + isCommentStarted = true + isMultiline = false + } + if (content[i + 1] === STAR) { + isCommentStarted = true + isMultiline = true + } + } + if (isCommentStarted) { + if (isMultiline) { + if (current === STAR && content[i + 1] === SLASH) { + isCommentStarted = false + current = ' ' + i++ + } + } else { + if (current === NEW_LINE) { + isCommentStarted = false + } + } + } + if (isStringStarted || isCommentStarted) { + if (current !== NEW_LINE) { + current = ' ' + } + } + updatedContent += current + } + return updatedContent +} + +const findClassEnd = (content) => { + const {RIGHT_BRACE, LEFT_BRACE} = window.codioTestAssessment.javaGrammar + + let level = 0 + for (let i = 0, len = content.length; i < len; i++) { + let current = content[i] + if (current === RIGHT_BRACE) { + if (level === 0) { + return i + } else { + level-- + } + } else if (current === LEFT_BRACE) { + level++ + } + } + return content.length +} + +const classesInformation = (data) => { + const {classNameReg, anonClassNameReg} = window.codioTestAssessment.javaGrammar + let allClasses = [] + + let classSearch = data + let classNameMatch = classSearch.match(classNameReg) + let offset = 0 + let start = 0 + let end = 0 + while (classNameMatch) { + start = classNameMatch.index + classNameMatch[0].length + classSearch = classSearch.substring(start, classSearch.length) + end = findClassEnd(classSearch) + allClasses.push({ + name: classNameMatch[3], + start: start + offset, + end: start + end + offset + }) + offset += start + classNameMatch = classSearch.match(classNameReg) + } + + let anonClassSearch = data + let anonClassNameMatch = anonClassSearch.match(anonClassNameReg) + offset = 0 + while (anonClassNameMatch) { + start = anonClassNameMatch.index + anonClassNameMatch[0].length + anonClassSearch = anonClassSearch.substring(start, anonClassSearch.length) + end = findClassEnd(anonClassSearch) + allClasses.push({ + start: start + offset, + end: start + end + offset + }) + offset += start + anonClassNameMatch = anonClassSearch.match(anonClassNameReg) + } + + allClasses = allClasses.sort((a, b) => { + return a.start - b.start + }) + + const classesInfo = {} + const processed = [] + + allClasses.forEach(function (item, pos) { + if (pos === 0) { + classesInfo[item.name] = item + classesInfo[item.name].anon = 1 + processed.push(classesInfo[item.name]) + } else { + let lastNested = null + processed.forEach(function (parentItem) { + if (parentItem.start < item.start && parentItem.end > item.end) { + lastNested = parentItem + } + }) + if (lastNested) { + let name + if (!item.name) { + name = lastNested.name + '$' + lastNested.anon + lastNested.anon++ + } else { + name = lastNested.name + '$' + item.name + } + + classesInfo[name] = item + classesInfo[name].anon = 1 + classesInfo[name].name = name + processed.push(classesInfo[name]) + } + } + }) + + return processed +} + +const getClassInfo = (path, content) => { + const {packageReg} = window.codioTestAssessment.javaGrammar + const pathsPart = path.split('/') + const fileName = pathsPart[pathsPart.length - 1] + + const information = { + packageName: '', + className: '', + fileName: fileName, + path: path + } + + let advancedClassInformation + const withoutComments = removeCommentsAndStrings(content) + + const packageMatch = withoutComments.match(packageReg) + if (packageMatch) { + information.packageName = packageMatch[1] + } + advancedClassInformation = classesInformation(withoutComments) + + const lastNested = advancedClassInformation[0] + + if (lastNested) { + information.className = lastNested.name + } + + return information +} + +window.codioTestAssessment.javaGrammar.getJavaInfo = (path, content) => { + const classInformation = getClassInfo(path, content) + if (!classInformation.className) { + const message = 'Java class not found in ' + path + throw new Error(message) + } + const hasTestAnnotations = + content.includes('@Test') || + content.includes('@ParameterizedTest') || + content.includes('@org.junit.jupiter.api.Test') || + content.includes('@org.junit.jupiter.params.ParameterizedTest') + if (!hasTestAnnotations && !content.includes('extends')) { + const message = 'JUnit test not found in ' + path + throw new Error(message) + } + const result = {} + const jUnit5Case = content.includes('.jupiter.') + if (jUnit5Case) { + const wrongNaming = !( + classInformation.className.endsWith('Test') || + classInformation.className.endsWith('Tests') + ) + if (wrongNaming) { + result.info = `${classInformation.className} class name do not match against class names ending in Test or Tests. This test may not function correctly.` + } + } + const className = !classInformation.packageName + ? classInformation.className + : classInformation.packageName + '.' + classInformation.className + return { + ...result, + filePath: path, + className + } +} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..e9caa94 --- /dev/null +++ b/metadata.json @@ -0,0 +1,16 @@ +{ + "name": "Advanced Code Test", + "type": "assessment", + "properties": { + "type": "test", + "icon": "./icon.svg", + "defaultHeight": 500, + "gradingControls": { + "points": true, + "allowPartialPoints": true, + "useMaximumScore": true, + "definedNumberOfAttempts": true, + "rationale": true + } + } +} diff --git a/settings.css b/settings.css new file mode 100644 index 0000000..27da8ee --- /dev/null +++ b/settings.css @@ -0,0 +1,13 @@ +.settings-content-row-container { + display: flex; + flex-direction: row; + gap: 10px; +} + +.test-case-controls-container { + align-items: end; +} + +.test-case-add-case-container { + flex: 1; +} diff --git a/settings.html b/settings.html new file mode 100644 index 0000000..067bf0d --- /dev/null +++ b/settings.html @@ -0,0 +1,168 @@ + + + + + Settings + + + + + + + + +
+
+ + +
+
+
+ +
+ +
+
+
+ + +
+
+
+
+ + +
+
+ Script will be executed isolated from user container click here +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ + +
+ +
+
+
+
+ + diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..6a396f7 --- /dev/null +++ b/settings.js @@ -0,0 +1,155 @@ +(function () { + const LANG_TYPES = { + JAVA: 'java', + CUSTOM: 'custom', + RUBY: 'ruby', + PYTHON: 'python', + JAVASCRIPT: 'javascript' + } + + const SUBTYPES_BY_LANG = { + [LANG_TYPES.JAVA]: { + STYLE: 'style', + JUNIT: 'junit' + }, + [LANG_TYPES.RUBY]: { + STYLE: 'style', + RSPEC: 'rspec' + }, + [LANG_TYPES.PYTHON]: { + STYLE: 'style', + UNITTEST: 'unittest' + }, + [LANG_TYPES.JAVASCRIPT]: { + JSHINT: 'jshint', + JSLINT: 'jslint' + } + } + + const EXTENSIONS_BY_TYPE = { + [LANG_TYPES.JAVA]: ['java'], + [LANG_TYPES.RUBY]: ['rb'], + [LANG_TYPES.PYTHON]: ['py'], + [LANG_TYPES.JAVASCRIPT]: ['js'] + } + + const collectSettings = () => { + const instructions = $('#instructions').val() + const timeout = parseInt($('#timeout').val(), 10); + + return {instructions, timeout}; + } + + const exportSettings = () => { + const data = collectSettings(); + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS_RESPONSE, data); + } + + const applySettings = (settings = {}) => { + $('#instructions').val(settings.instructions || ''); + $('#timeout').val(settings.timeout || '40'); + } + + const processMessage = (jsonData) => { + console.log('settings iframe processMessage', jsonData) + try { + const {method, data} = JSON.parse(jsonData); + switch (method) { + case window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS: + exportSettings(); + break; + case window.codioAssessmentsHelper.METHODS.GET_SETTINGS_RESPONSE: + applySettings(data.settings); + break; + } + } catch {} + } + + const onSubtypeChanged = (language, subtype) => { + $('.lang-subtype-settings-container').addClass('hide') + $(`.lang-subtype-${language}-${subtype}-settings`).removeClass('hide') + } + + const onLanguageChanged = (language) => { + $('.language-settings-container').addClass('hide') + $(`.${language}-container`).removeClass('hide') + const subtype = $(`#${language}LangSubtype`).val() + onSubtypeChanged(language, subtype) + } + + const addParsedTestCase = (path, info) => { + + } + + const addTestCase = async (path) => { + if ($(`.test-case-item[data-path="${path}"]`)) { + // todo already added warning + return + } + try { + const {content} = await window.codioAssessmentsHelper.sendAndWait( + window.codioAssessmentsHelper.METHODS.GET_FILE_CONTENT, {path} + ) + const langType = $('#languageType').val() + if (EXTENSIONS_BY_TYPE[langType]) { + return + } + const subtype = $(`${langType}LangSubtype`).val() + const ext = path.split('.').pop() + if (langType === LANG_TYPES.JAVA && subtype === 'style' && ext === 'xml') { + $('#javaStyleConfigPath').val(path) + return + } + if (!EXTENSIONS_BY_TYPE[langType].includes(ext)) { + // todo error 'Incorrect file type, should be: ' + typeToExtension[type].join(' ') + return + } + if (LANG_TYPES.JAVA && subtype === SUBTYPES_BY_LANG.JAVA.STYLE) { + try { + const info = window.codioTestAssessment.javaGrammar.getJavaInfo(path, content) + addParsedTestCase(path, info) + } catch (e) { + // todo show errors + } + } else { + addParsedTestCase(path) + } + } catch (e) { + // todo show error + } + } + + const bindEvents = () => { + $('#languageType').on('change', function () { + const languageType = $(this).val(); + onLanguageChanged(languageType); + }) + $('.lang-subtype-select').on('change', function () { + const langSubtype = $(this).val(); + onSubtypeChanged($('#languageType').val(), langSubtype); + }) + $('#customCommand').on('input', function () { + const helpBlock = $('.secure-folder-help-block') + helpBlock.addClass('hide') + if (!$(this).val().includes('.guides/secure')) { + helpBlock.removeClass('hide') + } + }) + $('#newCasePath').on('input', function () { + const addCaseBtn = $('.add-case-btn') + addCaseBtn.prop('disabled', !$(this).val()) + }) + $('.add-case-btn').on('click', function () { + addTestCase($('#newCasePath').val()) + }) + } + + const onLoad = async () => { + window.codioAssessmentsHelper.registerMessageListener(processMessage) + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_SETTINGS) + + bindEvents() + } + + window.addEventListener('load', onLoad); +})()