-
Notifications
You must be signed in to change notification settings - Fork 91
Support streaming output #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
5ececb3
c1f269d
27d890a
6e07cb1
0e44ec0
0fb6cba
8ef53c5
f052206
33e3289
63a046f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
import merge from 'deepmerge'; | ||
AlexanderPrendota marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import CodeMirror from 'codemirror'; | ||
import Monkberry from 'monkberry'; | ||
import directives from 'monkberry-directives'; | ||
import 'monkberry-events'; | ||
import ExecutableCodeTemplate from './executable-fragment.monk'; | ||
import Exception from './exception'; | ||
import WebDemoApi from '../webdemo-api'; | ||
import TargetPlatform from "../target-platform"; | ||
import JsExecutor from "../js-executor" | ||
|
@@ -51,9 +51,12 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { | |
code: '', | ||
foldButtonHover: false, | ||
folded: true, | ||
exception: null, | ||
output: null, | ||
errors: [] | ||
}; | ||
instance.codemirror = new CodeMirror(); | ||
instance.element = element | ||
|
||
instance.on('click', SELECTORS.FOLD_BUTTON, () => { | ||
instance.update({folded: !instance.state.folded}); | ||
|
@@ -123,16 +126,20 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { | |
} | ||
} | ||
|
||
this.state = merge.all([this.state, state, { | ||
isShouldBeFolded: this.isShouldBeFolded && state.isFoldedButton | ||
}]); | ||
|
||
if (state.output === null) { | ||
this.removeAllOutputNodes() | ||
} | ||
this.applyStateUpdate(state) | ||
super.update(this.state); | ||
this.renderNewOutputNodes(state) | ||
|
||
if (!this.initialized) { | ||
this.initializeCodeMirror(state); | ||
this.initialized = true; | ||
} else { | ||
this.showDiagnostics(state.errors); | ||
if (state.errors !== undefined) { // rerender errors if the array was explicitly changed | ||
this.showDiagnostics(this.state.errors); | ||
} | ||
if (state.folded === undefined) { | ||
return | ||
} | ||
|
@@ -181,6 +188,57 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { | |
} | ||
} | ||
|
||
removeAllOutputNodes() { | ||
const outputNode = this.element.getElementsByClassName("code-output").item(0) | ||
lyubortk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
while (outputNode && outputNode.lastChild) { | ||
outputNode.removeChild(outputNode.lastChild) | ||
lyubortk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
applyStateUpdate(stateUpdate) { | ||
if (stateUpdate.errors === null) { | ||
stateUpdate.errors = [] | ||
} else if (stateUpdate.errors !== undefined) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can do it after filter and code should be simpler after that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, I placed this peace of code after filtration but could not come up with any idea how to make it simpler. We must distinguish null here because passing null as |
||
this.state.errors.push(...stateUpdate.errors) | ||
stateUpdate.errors = this.state.errors | ||
} | ||
|
||
Object.keys(stateUpdate).forEach(key => { | ||
if (stateUpdate[key] === undefined) { | ||
delete stateUpdate[key]; | ||
lyubortk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
}) | ||
Object.assign(this.state, stateUpdate, { | ||
isShouldBeFolded: this.isShouldBeFolded && stateUpdate.isFoldedButton | ||
}) | ||
} | ||
|
||
renderNewOutputNodes(stateUpdate) { | ||
zoobestik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (stateUpdate.output) { | ||
const template = document.createElement("template"); | ||
template.innerHTML = stateUpdate.output.trim(); | ||
const newNode = template.content.firstChild; | ||
const parent = this.element.getElementsByClassName("code-output").item(0) | ||
const isMergeable = newNode.className.startsWith("standard-output") || newNode.className.startsWith("error-output") | ||
if (isMergeable && parent.lastChild !== null && parent.lastChild.className === newNode.className) { | ||
lyubortk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
parent.lastChild.textContent += newNode.textContent | ||
} else { | ||
parent.appendChild(newNode) | ||
} | ||
} | ||
|
||
if (stateUpdate.exception) { | ||
const outputNode = this.element.getElementsByClassName("code-output").item(0) | ||
const exceptionView = Monkberry.render(Exception, outputNode, { | ||
'directives': directives | ||
}) | ||
exceptionView.update({ | ||
...stateUpdate.exception, | ||
onExceptionClick: this.onExceptionClick.bind(this) | ||
}) | ||
} | ||
} | ||
|
||
markPlaceHolders() { | ||
let taskRanges = this.getTaskRanges(); | ||
this.codemirror.setValue(this.codemirror.getValue() | ||
|
@@ -226,11 +284,11 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { | |
} | ||
|
||
onConsoleCloseButtonEnter() { | ||
const {jsLibs, onCloseConsole, targetPlatform } = this.state; | ||
const {jsLibs, onCloseConsole, targetPlatform} = this.state; | ||
// creates a new iframe and removes the old one, thereby stops execution of any running script | ||
if (targetPlatform === TargetPlatform.CANVAS || targetPlatform === TargetPlatform.JS) | ||
this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe()); | ||
this.update({output: "", openConsole: false, exception: null}); | ||
this.update({output: null, openConsole: false, exception: null}); | ||
if (onCloseConsole) onCloseConsole(); | ||
} | ||
|
||
|
@@ -248,6 +306,9 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { | |
return | ||
} | ||
this.update({ | ||
errors: null, | ||
output: null, | ||
exception: null, | ||
waitingForOutput: true, | ||
openConsole: false | ||
}); | ||
|
@@ -261,18 +322,22 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { | |
theme, | ||
hiddenDependencies, | ||
onTestPassed, | ||
onTestFailed).then( | ||
onTestFailed, | ||
state => { | ||
state.waitingForOutput = false; | ||
if (state.output || state.exception) { | ||
if (state.waitingForOutput) { | ||
this.update(state) | ||
return | ||
} | ||
// all chunks were processed | ||
|
||
if (this.state.output || this.state.exception) { // previous chunk contained some text | ||
state.openConsole = true; | ||
} else { | ||
if (onCloseConsole) onCloseConsole(); | ||
} else if (onCloseConsole) { | ||
onCloseConsole() | ||
} | ||
if ((state.errors.length > 0 || state.exception) && onError) onError(); | ||
if ((this.state.errors.length > 0 || this.state.exception) && onError) onError(); | ||
this.update(state); | ||
}, | ||
() => this.update({waitingForOutput: false}) | ||
} | ||
) | ||
} else { | ||
this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe()); | ||
|
@@ -357,7 +422,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { | |
return; | ||
} | ||
diagnostics.forEach(diagnostic => { | ||
const interval = diagnostic.interval; | ||
const interval = Object.assign({}, diagnostic.interval); | ||
interval.start = this.recalculatePosition(interval.start); | ||
interval.end = this.recalculatePosition(interval.end); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import {arrayFrom, convertToHtmlTag, processingHtmlBrackets} from "../utils"; | ||
import {arrayFrom, convertToHtmlTag, escapeBrackets} from "../utils"; | ||
import isEmptyObject from "is-empty-object" | ||
lyubortk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import escapeHtml from "escape-html" | ||
|
||
|
@@ -7,62 +7,68 @@ const ACCESS_CONTROL_EXCEPTION = "java.security.AccessControlException"; | |
const SECURITY_MESSAGE = "Access control exception due to security reasons in web playground"; | ||
const UNHANDLED_JS_EXCEPTION = "Unhandled JavaScript exception"; | ||
const NO_TEST_FOUND = "No tests methods are found"; | ||
const ANGLE_BRACKETS_LEFT_HTML = "<"; | ||
const ANGLE_BRACKETS_RIGHT_HTML = ">"; | ||
|
||
const TEST_STATUS = { | ||
FAIL : { value: "FAIL", text: "Fail" }, | ||
ERROR: { value: "ERROR", text: "Error" }, | ||
PASSED : { value: "OK", text: "Passed" } | ||
}; | ||
|
||
const BUG_FLAG = `${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}BUG${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`; | ||
const BUG_REPORT_MESSAGE = `${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}Hey! It seems you just found a bug! \uD83D\uDC1E\n` + | ||
`Please click <a href=http://kotl.in/issue target=_blank>here<a> to submit it ` + | ||
const BUG_FLAG = 'BUG' | ||
const BUG_REPORT_MESSAGE = 'Hey! It seems you just found a bug! \uD83D\uDC1E\n' + | ||
`Please go here -> http://kotl.in/issue to submit it ` + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is happened with link? :( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merging DOM nodes turns into text all inner elements so the link will not be clickable. |
||
`to the issue tracker and one day we fix it, hopefully \uD83D\uDE09\n` + | ||
`✅ Don't forget to attach code to the issue${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}\n`; | ||
`✅ Don't forget to attach code to the issue\n`; | ||
|
||
export function processJVMOutput(output, theme) { | ||
let processedOutput = processingHtmlBrackets(output); // don't need to escape `&` | ||
return processedOutput | ||
.split(BUG_FLAG).join(BUG_REPORT_MESSAGE) | ||
.split(`${ANGLE_BRACKETS_LEFT_HTML}outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(`<span class="standard-output ${theme}">`) | ||
.split(`${ANGLE_BRACKETS_LEFT_HTML}/outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join("</span>") | ||
.split(`${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(`<span class="error-output ${theme}">`) | ||
.split(`${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join("</span>"); | ||
export function processJVMStdout(output, theme) { | ||
const processedOutput = escapeBrackets(output); | ||
return `<span class="standard-output ${theme}">${processedOutput}</span>` | ||
} | ||
|
||
export function processJUnitResults(data, onTestPassed, onTestFailed) { | ||
let result = ""; | ||
let totalTime = 0; | ||
let passed = true; | ||
if (isEmptyObject(data)) return NO_TEST_FOUND; | ||
for (let testClass in data) { | ||
let listOfResults = arrayFrom(data[testClass]); | ||
result += listOfResults.reduce((previousTest, currentTest) => { | ||
totalTime = totalTime + (currentTest.executionTime / 1000); | ||
if (currentTest.status === TEST_STATUS.ERROR.value || currentTest.status === TEST_STATUS.FAIL.value) passed = false; | ||
switch (currentTest.status) { | ||
case TEST_STATUS.FAIL.value: | ||
return previousTest + buildOutputTestLine(TEST_STATUS.FAIL.text, currentTest.methodName, currentTest.comparisonFailure.message); | ||
case TEST_STATUS.ERROR.value: | ||
return previousTest + buildOutputTestLine(TEST_STATUS.ERROR.text, currentTest.methodName, currentTest.exception.message); | ||
case TEST_STATUS.PASSED.value: | ||
return previousTest + buildOutputTestLine(TEST_STATUS.PASSED.text, currentTest.methodName, ""); | ||
} | ||
}, ""); | ||
export function processJVMStderr(output, theme) { | ||
if (output === BUG_FLAG) { | ||
output = BUG_REPORT_MESSAGE | ||
} | ||
const processedOutput = escapeBrackets(output); | ||
return `<span class="error-output ${theme}">${processedOutput}</span>` | ||
} | ||
|
||
export function processJUnitTotalResults(testResults, onTestPassed, onTestFailed) { | ||
if (testResults.testsRun === 0) { | ||
return NO_TEST_FOUND | ||
} | ||
if (testResults.success) { | ||
if (onTestPassed) onTestPassed() | ||
} else { | ||
if (onTestFailed) onTestFailed() | ||
} | ||
return `<div class="test-time">Total test time: ${testResults.totalTime}s</div>` | ||
} | ||
|
||
export function processJUnitTestResult(testRunInfo, testResults) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lyubortk actually i don't understand what is happend with test output. Can you explain me differences in this output. It's too much changes not clear for me :( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to rewrite that because each test result must be handled separately with streaming. |
||
let output = ""; | ||
testResults.testsRun++ | ||
testResults.totalTime += testRunInfo.executionTime / 1000 | ||
switch (testRunInfo.status) { | ||
case TEST_STATUS.FAIL.value: | ||
testResults.success = false; | ||
output = buildOutputTestLine(TEST_STATUS.FAIL.text, testRunInfo.methodName, testRunInfo.comparisonFailure.message); | ||
break; | ||
case TEST_STATUS.ERROR.value: | ||
testResults.success = false; | ||
output = buildOutputTestLine(TEST_STATUS.ERROR.text, testRunInfo.methodName, testRunInfo.exception.message); | ||
break; | ||
case TEST_STATUS.PASSED.value: | ||
output = buildOutputTestLine(TEST_STATUS.PASSED.text, testRunInfo.methodName, ""); | ||
} | ||
if (passed && onTestPassed) onTestPassed(); | ||
if (!passed && onTestFailed) onTestFailed(); | ||
let testTime = `<div class="test-time">Total test time: ${totalTime}s</div>`; | ||
return testTime + result; | ||
return output; | ||
} | ||
|
||
function buildOutputTestLine(status, method, message) { | ||
return ` | ||
<div class="console-block"> | ||
<span class="console-icon ${status.toLocaleLowerCase()}"></span> | ||
<div class="test-${status.toLocaleLowerCase()}">${status}: ${method}${message ? ': ' + convertToHtmlTag(message) : ''}</div> | ||
<div class="test-${status.toLocaleLowerCase()}">${status}: ${method}${message ? ': ' + escapeBrackets(message) : ''}</div> | ||
</div> | ||
`; | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.