Skip to content

Commit a697767

Browse files
committed
Implementing success exit codes config and proper visualization.
1 parent 2136f39 commit a697767

File tree

13 files changed

+198
-28
lines changed

13 files changed

+198
-28
lines changed

src/components/Pipelines/ParametersList/ParametersList.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ const pipelineParams = {
3737
defaultMessage="Extra source files can be added to tested solution"
3838
/>
3939
),
40+
hasSuccessExitCodes: (
41+
<FormattedMessage
42+
id="app.pipelineParams.hasSuccessExitCodes"
43+
defaultMessage="Configurable exit-codes that are accepted as a success when tested soution is executed"
44+
/>
45+
),
4046
};
4147

4248
const pipelineParameterMapping = parameter => {

src/components/Solutions/TestResultsTable/TestResultsTableRow.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { OverlayTrigger, Tooltip } from 'react-bootstrap';
66
import prettyMs from 'pretty-ms';
77

88
import Button, { TheButtonGroup } from '../../widgets/TheButton';
9+
import Explanation from '../../widgets/Explanation';
910
import { prettyPrintBytes } from '../../helpers/stringFormatters.js';
1011
import exitCodeMapping from '../../helpers/exitCodeMapping.js';
1112
import Icon, { SuccessOrFailureIcon } from '../../icons';
@@ -103,6 +104,8 @@ const TestResultsTableRow = ({
103104
wallTime,
104105
cpuTime,
105106
exitCode,
107+
exitCodeOk,
108+
exitCodeNative,
106109
exitSignal,
107110
judgeLogStdout = '',
108111
judgeLogStderr = '',
@@ -171,7 +174,23 @@ const TestResultsTableRow = ({
171174
</strong>
172175
</OverlayTrigger>
173176
)}
174-
{(exitCode !== -1 || !exitSignal) && exitCodeMapping(runtimeEnvironmentId, exitCode)}
177+
178+
{exitCodeNative ? (
179+
<>
180+
<SuccessOrFailureIcon success={exitCodeOk} gapRight />
181+
{exitCodeOk ? exitCode : exitCodeMapping(runtimeEnvironmentId, exitCode)}
182+
{(exitCode === 0) !== exitCodeOk && (
183+
<Explanation id={`exit-code-expl-${testName}`} placement="bottom">
184+
<FormattedMessage
185+
id="app.submissions.testResultsTable.exitCodeExplanation"
186+
defaultMessage="Usually, exit code 0 is treated as success and non-zero codes as errors. This exercise have specified alternative exit codes that are treated as a success of the executed solution."
187+
/>
188+
</Explanation>
189+
)}
190+
</>
191+
) : (
192+
!exitSignal && status !== 'SKIPPED' && <span className="text-muted">{exitCode}</span>
193+
)}
175194
</td>
176195

177196
{(showJudgeLogStdout || showJudgeLogStderr) && (
@@ -240,6 +259,8 @@ TestResultsTableRow.propTypes = {
240259
wallTime: PropTypes.number,
241260
cpuTime: PropTypes.number,
242261
exitCode: PropTypes.number,
262+
exitCodeOk: PropTypes.bool,
263+
exitCodeNative: PropTypes.bool,
243264
exitSignal: PropTypes.number,
244265
judgeLogStdout: PropTypes.string,
245266
judgeLogStderr: PropTypes.string,

src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigForm.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ const someValuesHaveNozeroLength = (obj, ...keys) => {
6363
return false;
6464
};
6565

66+
const nonDefaultSuccessExitCodes = obj => {
67+
if (!obj || typeof obj !== 'object' || !obj['success-exit-codes']) {
68+
return false;
69+
}
70+
71+
for (const val of Object.values(obj['success-exit-codes'])) {
72+
if (val.trim() !== '0') {
73+
return true;
74+
}
75+
}
76+
return false;
77+
};
78+
6679
/**
6780
* Make sure file(s) in form data (specified by given path) exist.
6881
* If not, proper form error message(s) is/are filled.
@@ -156,6 +169,7 @@ class EditExerciseSimpleConfigForm extends Component {
156169

157170
render() {
158171
const {
172+
initialValues,
159173
reset,
160174
change,
161175
handleSubmit,
@@ -251,13 +265,14 @@ class EditExerciseSimpleConfigForm extends Component {
251265
{exerciseTests
252266
.sort((a, b) => a.name.localeCompare(b.name, locale))
253267
.map((test, idx) => {
254-
const testData = formValues && formValues.config && formValues.config[encodeNumId(test.id)];
255-
const hasCompilationSetting = someValuesHaveNozeroLength(
256-
testData,
257-
'extra-files',
258-
'jar-files',
259-
'compile-args'
260-
);
268+
const testData = formValues?.config?.[encodeNumId(test.id)];
269+
const hasCompilationSetting =
270+
someValuesHaveNozeroLength(
271+
initialValues?.config?.[encodeNumId(test.id)],
272+
'extra-files',
273+
'jar-files',
274+
'compile-args'
275+
) || nonDefaultSuccessExitCodes(initialValues?.config?.[encodeNumId(test.id)]);
261276
return (
262277
<EditExerciseSimpleConfigTest
263278
change={change}

src/components/forms/EditExerciseSimpleConfigForm/EditExerciseSimpleConfigTestCompilation.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { EMPTY_ARRAY } from '../../../helpers/common.js';
99
import Button from '../../widgets/TheButton';
1010
import InsetPanel from '../../widgets/InsetPanel';
1111
import Icon, { ExpandCollapseIcon, WarningIcon } from '../../icons';
12-
import { SelectField, ExpandingInputFilesField, ExpandingSelectField, ExpandingTextField } from '../Fields';
12+
import { TextField, SelectField, ExpandingInputFilesField, ExpandingSelectField, ExpandingTextField } from '../Fields';
1313
import Confirm from '../../forms/Confirm';
1414
import Explanation from '../../widgets/Explanation';
1515
import {
@@ -20,6 +20,7 @@ import {
2020
ENV_MAVEN_ID,
2121
ENV_SYCL_ID,
2222
} from '../../../helpers/exercise/environments.js';
23+
import { validateExitCodes } from '../../../helpers/exercise/config.js';
2324

2425
const COMPILER_ARGS_ENVS = [ENV_C_GCC_ID, ENV_CPP_GCC_ID, ENV_ARDUINO_ID, ENV_SYCL_ID];
2526

@@ -277,6 +278,28 @@ class EditExerciseSimpleConfigTestCompilation extends Component {
277278
* End of special case for Maven
278279
*/
279280
)}
281+
282+
<Field
283+
name={`${test}.success-exit-codes.${env.id}`}
284+
component={TextField}
285+
disabled={readOnly}
286+
maxLength={1024}
287+
validate={validateExitCodes}
288+
label={
289+
<>
290+
<FormattedMessage
291+
id="app.editExerciseSimpleConfigTests.successExitCodes"
292+
defaultMessage="Accepted exit codes:"
293+
/>
294+
<Explanation id={`${test}.exec-targets-explanation`}>
295+
<FormattedMessage
296+
id="app.editExerciseSimpleConfigTests.successExitCodesExplanation"
297+
defaultMessage="List of exit codes that are accepted as successful completion of tested solution. Exit codes are separated by commas, ranges of codes may be specifed using dash '-' (e.g., '1, 3, 5-7'). Zero is the default accepted exit code."
298+
/>
299+
</Explanation>
300+
</>
301+
}
302+
/>
280303
</Col>
281304
</Row>
282305
</Container>

src/components/forms/EditPipelineForm/EditPipelineForm.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,19 @@ class EditPipelineForm extends Component {
161161
}
162162
/>
163163
</Col>
164+
<Col xl={6} lg={12}>
165+
<Field
166+
name="parameters.hasSuccessExitCodes"
167+
component={CheckboxField}
168+
onOff
169+
label={
170+
<FormattedMessage
171+
id="app.editPipelineForm.hasSuccessExitCodes"
172+
defaultMessage="Has success exit codes"
173+
/>
174+
}
175+
/>
176+
</Col>
164177
</Row>
165178
</>
166179
)}

src/components/helpers/exitCodeMapping.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
* https://www.freepascal.org/docs-html/user/userap4.html
1313
*/
1414
const pascalCodes = {
15-
0: <FormattedMessage id="app.exitCodes.pascal.0" defaultMessage="OK" />,
1615
1: <FormattedMessage id="app.exitCodes.pascal.1" defaultMessage="Invalid function number" />,
1716
2: <FormattedMessage id="app.exitCodes.pascal.2" defaultMessage="File not found" />,
1817
3: <FormattedMessage id="app.exitCodes.pascal.3" defaultMessage="Path not found" />,
@@ -70,7 +69,6 @@ const pascalCodes = {
7069
};
7170

7271
const python3Codes = {
73-
0: <FormattedMessage id="app.exitCodes.python3.0" defaultMessage="OK" />,
7472
1: <FormattedMessage id="app.exitCodes.python3.1" defaultMessage="Base exception" />,
7573
101: <FormattedMessage id="app.exitCodes.python3.101" defaultMessage="Assertion error" />,
7674
102: <FormattedMessage id="app.exitCodes.python3.102" defaultMessage="Type error" />,
@@ -119,8 +117,6 @@ const exitCodeMapping = (runtimeEnvironmentId, exitCode) => {
119117
// TODO - eventually replace those switches with objects.
120118
const javaMapping = exitCode => {
121119
switch (exitCode) {
122-
case 0:
123-
return <FormattedMessage id="app.exitCodes.java.0" defaultMessage="OK" />;
124120
case 1:
125121
return <FormattedMessage id="app.exitCodes.java.1" defaultMessage="Unknown error" />;
126122
case 2:
@@ -160,8 +156,6 @@ const javaMapping = exitCode => {
160156

161157
const csDotnetMapping = exitCode => {
162158
switch (exitCode) {
163-
case 0:
164-
return <FormattedMessage id="app.exitCodes.csharp.0" defaultMessage="OK" />;
165159
case 1:
166160
return <FormattedMessage id="app.exitCodes.csharp.1" defaultMessage="User error" />;
167161
case 101:

src/helpers/exercise/config.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,88 @@ export const SUBMIT_BUTTON_MESSAGES = {
1212
success: <FormattedMessage id="app.editExerciseConfig.success" defaultMessage="Configuration Saved." />,
1313
validating: <FormattedMessage id="generic.validating" defaultMessage="Validating..." />,
1414
};
15+
16+
/*
17+
* Exit codes config handling
18+
*/
19+
const _validateExitCodes = value =>
20+
value &&
21+
value.match(/^\s*[0-9]{0,3}(-[0-9]{0,3})?(\s*,\s*[0-9]{0,3}(-[0-9]{0,3})?)*\s*$/) !== null &&
22+
value.split(/[-,]/).every(val => {
23+
const num = parseInt(val);
24+
return !isNaN(num) && num >= 0 && num <= 255;
25+
});
26+
27+
export const validateExitCodes = value =>
28+
!_validateExitCodes(value) ? (
29+
<FormattedMessage
30+
id="app.editExerciseConfigForm.validation.successExitCodes"
31+
defaultMessage="Exit codes must be numerical values (in 0-255 range) or value intervals (written as 'from-to') separated by commas (e.g., '1, 3, 5-7')."
32+
/>
33+
) : undefined;
34+
35+
const _addToBitmap = (bitmap, from, to = from) => {
36+
if (to < from) {
37+
const tmp = from;
38+
from = to;
39+
to = tmp;
40+
}
41+
42+
while (bitmap.length < from) {
43+
bitmap.push(false);
44+
}
45+
46+
while (from <= to) {
47+
if (bitmap.length > from) {
48+
bitmap[from] = true;
49+
} else {
50+
bitmap.push(true);
51+
}
52+
++from;
53+
}
54+
};
55+
56+
/**
57+
* Convert exit codes in a string into a bitmap (array, where valid codes are true, invalid false).
58+
* @param {string} str comma separated list of values and intervals
59+
* @returns {Array}
60+
*/
61+
export const exitCodesStrToBitmap = str => {
62+
const res = [];
63+
if (_validateExitCodes(str)) {
64+
str
65+
.split(',')
66+
.map(val => val.trim())
67+
.forEach(val => {
68+
_addToBitmap(res, ...val.split('-')); // split will produce either one or two values
69+
});
70+
}
71+
return res;
72+
};
73+
74+
/**
75+
* Convert exit codes bitmap into normalized array of tokens (each token is either a value or an interval)
76+
* @param {Array} bitmap
77+
* @returns {Array}
78+
*/
79+
export const exitCodesBitmapToTokens = bitmap => {
80+
const tokens = [];
81+
let i = 0;
82+
while (i < bitmap.length) {
83+
while (i < bitmap.length && !bitmap[i]) ++i;
84+
if (i < bitmap.length) {
85+
const from = i;
86+
while (i < bitmap.length && bitmap[i]) ++i;
87+
const to = i - 1;
88+
tokens.push(from === to ? from : `${from}-${to}`);
89+
}
90+
}
91+
return tokens;
92+
};
93+
94+
/**
95+
* Convert exit codes bitmap into normalized string.
96+
* @param {Array} bitmap
97+
* @returns {string} comma separated list of values and intervals
98+
*/
99+
export const exitCodesBitmapToStr = bitmap => exitCodesBitmapToTokens(bitmap).join(', ');

src/helpers/exercise/configSimple.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ENV_SCALA_ID,
1414
ENV_SYCL_ID,
1515
} from './environments.js';
16+
import { exitCodesStrToBitmap, exitCodesBitmapToTokens } from './config.js';
1617

1718
/**
1819
* Base class for all pipeline variables being edited in the config form.
@@ -316,6 +317,14 @@ const _PIPELINE_DEFAULT_VARS_DESCRIPTORS = [
316317
}))
317318
.setTransformPostprocess(value => value || '$entry-point')
318319
.forCompilationAndExecution(),
320+
new Variable('success-exit-codes', 'string[]', '0')
321+
.individualEnvs()
322+
.setPipelineFilter('hasSuccessExitCodes')
323+
.setInitialPostprocess(({ 'success-exit-codes': codes }) => ({
324+
'success-exit-codes': objectMap(codes, c => (c || ['0']).join(', ')),
325+
}))
326+
.setTransformPostprocess(codes => exitCodesBitmapToTokens(exitCodesStrToBitmap(codes)))
327+
.forCompilationAndExecution(),
319328
];
320329

321330
const _ENV_SPECIFIC_VARS_DESCRIPTORS = {

0 commit comments

Comments
 (0)