Skip to content

Commit d6e1d7e

Browse files
committed
Merge branch 'master' into multitenant/dev
2 parents f853e59 + 7354ddf commit d6e1d7e

File tree

15 files changed

+166
-145
lines changed

15 files changed

+166
-145
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"connected-react-router": "^6.9.1",
5050
"flexboxgrid": "^6.3.1",
5151
"flexboxgrid-helpers": "^1.1.3",
52-
"js-slang": "^0.5.10",
52+
"js-slang": "^0.5.13",
5353
"konva": "^7.2.5",
5454
"lodash": "^4.17.21",
5555
"lz-string": "^1.4.4",

src/commons/application/ApplicationTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,8 @@ export const defaultSession: SessionState = {
298298
},
299299
assessments: new Map<number, Assessment>(),
300300
assessmentOverviews: undefined,
301+
experimentApproval: false, // TODO: get this from backend or sth
302+
experimentCoinflip: Math.random() < 0.5,
301303
githubOctokitObject: { octokit: undefined },
302304
gradingOverviews: undefined,
303305
gradings: new Map<number, Grading>(),

src/commons/application/types/SessionTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ export type SessionState = {
9696

9797
readonly assessmentOverviews?: AssessmentOverview[];
9898
readonly assessments: Map<number, Assessment>;
99+
readonly experimentApproval: boolean;
100+
readonly experimentCoinflip: boolean;
99101
readonly gradingOverviews?: GradingOverview[];
100102
readonly gradings: Map<number, Grading>;
101103
readonly historyHelper: HistoryHelper;

src/commons/assessmentWorkspace/AssessmentWorkspace.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,9 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
772772
? {
773773
editorSessionId: '',
774774
editorValue: props.editorValue!,
775+
sourceChapter: question.library.chapter || 4,
776+
sourceVariant: 'default' as Variant,
777+
externalLibrary: question.library.external.name || 'NONE',
775778
handleDeclarationNavigate: props.handleDeclarationNavigate,
776779
handleEditorEval: handleEval,
777780
handleEditorValueChange: props.handleEditorValueChange,

src/commons/sagas/WorkspaceSaga.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,9 @@ export function* evalCode(
716716
const isNonDet: boolean = context.variant === 'non-det';
717717
const isLazy: boolean = context.variant === 'lazy';
718718
const isWasm: boolean = context.variant === 'wasm';
719+
const throwInfiniteLoops: boolean = yield select(
720+
(state: OverallState) => state.session.experimentCoinflip
721+
);
719722
const { result, interrupted, paused } = yield race({
720723
result:
721724
actionType === DEBUG_RESUME
@@ -726,6 +729,7 @@ export function* evalCode(
726729
scheduler: 'preemptive',
727730
originalMaxExecTime: execTime,
728731
stepLimit: stepLimit,
732+
throwInfiniteLoops: throwInfiniteLoops,
729733
useSubst: substActiveAndCorrectChapter
730734
}),
731735

@@ -776,13 +780,16 @@ export function* evalCode(
776780
context.errors = oldErrors;
777781
// for achievement event tracking
778782
const events = context.errors.length > 0 ? [EventType.ERROR] : [];
783+
779784
// report infinite loops but only for 'vanilla'/default source
780-
if (context.variant === 'default') {
781-
const infiniteLoopData = getInfiniteLoopData(context, code);
785+
if (context.variant === undefined || context.variant === 'default') {
786+
const infiniteLoopData = getInfiniteLoopData(context);
782787
if (infiniteLoopData) {
783-
events.push(EventType.INFINITE_LOOP);
784-
const [error, code] = infiniteLoopData;
785-
yield call(reportInfiniteLoopError, error, code);
788+
const approval = yield select((state: OverallState) => state.session.experimentApproval);
789+
if (approval) {
790+
events.push(EventType.INFINITE_LOOP);
791+
yield call(reportInfiniteLoopError, ...infiniteLoopData);
792+
}
786793
}
787794
}
788795

@@ -828,11 +835,14 @@ export function* evalTestCode(
828835
type: TestcaseType
829836
) {
830837
yield put(actions.resetTestcase(workspaceLocation, index));
831-
838+
const throwInfiniteLoops: boolean = yield select(
839+
(state: OverallState) => state.session.experimentCoinflip
840+
);
832841
const { result, interrupted } = yield race({
833842
result: call(runInContext, code, context, {
834843
scheduler: 'preemptive',
835-
originalMaxExecTime: execTime
844+
originalMaxExecTime: execTime,
845+
throwInfiniteLoops: throwInfiniteLoops
836846
}),
837847
/**
838848
* A BEGIN_INTERRUPT_EXECUTION signals the beginning of an interruption,

src/commons/sagas/__tests__/WorkspaceSaga.ts

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
import { Library, Testcase, TestcaseType, TestcaseTypes } from '../../assessment/AssessmentTypes';
2929
import { mockRuntimeContext } from '../../mocks/ContextMocks';
3030
import { mockTestcases } from '../../mocks/GradingMocks';
31-
import { reportInfiniteLoopError } from '../../utils/InfiniteLoopReporter';
31+
import { InfiniteLoopErrorType, reportInfiniteLoopError } from '../../utils/InfiniteLoopReporter';
3232
import { showSuccessMessage, showWarningMessage } from '../../utils/NotificationsHelper';
3333
import {
3434
beginClearContext,
@@ -64,6 +64,11 @@ function generateDefaultState(
6464
): OverallState {
6565
return {
6666
...defaultState,
67+
session: {
68+
...defaultState.session,
69+
experimentApproval: false,
70+
experimentCoinflip: true
71+
},
6772
workspaces: {
6873
...defaultState.workspaces,
6974
[workspaceLocation]: {
@@ -130,7 +135,8 @@ describe('EVAL_EDITOR', () => {
130135
scheduler: 'preemptive',
131136
originalMaxExecTime: execTime,
132137
stepLimit: 1000,
133-
useSubst: false
138+
useSubst: false,
139+
throwInfiniteLoops: true
134140
}
135141
]
136142
})
@@ -148,7 +154,8 @@ describe('EVAL_EDITOR', () => {
148154
scheduler: 'preemptive',
149155
originalMaxExecTime: execTime,
150156
stepLimit: 1000,
151-
useSubst: false
157+
useSubst: false,
158+
throwInfiniteLoops: true
152159
}
153160
]
154161
})
@@ -212,7 +219,8 @@ describe('EVAL_REPL', () => {
212219
scheduler: 'preemptive',
213220
originalMaxExecTime: 1000,
214221
stepLimit: 1000,
215-
useSubst: false
222+
useSubst: false,
223+
throwInfiniteLoops: true
216224
})
217225
.dispatch({
218226
type: EVAL_REPL,
@@ -641,7 +649,8 @@ describe('evalCode', () => {
641649
scheduler: 'preemptive',
642650
originalMaxExecTime: 1000,
643651
stepLimit: 1000,
644-
useSubst: false
652+
useSubst: false,
653+
throwInfiniteLoops: true
645654
};
646655
lastDebuggerResult = { status: 'error' };
647656
state = generateDefaultState(workspaceLocation);
@@ -656,7 +665,8 @@ describe('evalCode', () => {
656665
scheduler: 'preemptive',
657666
originalMaxExecTime: execTime,
658667
stepLimit: 1000,
659-
useSubst: false
668+
useSubst: false,
669+
throwInfiniteLoops: true
660670
})
661671
.put(evalInterpreterSuccess(value, workspaceLocation))
662672
.silentRun();
@@ -670,7 +680,8 @@ describe('evalCode', () => {
670680
scheduler: 'preemptive',
671681
originalMaxExecTime: execTime,
672682
stepLimit: 1000,
673-
useSubst: false
683+
useSubst: false,
684+
throwInfiniteLoops: true
674685
})
675686
.put(endDebuggerPause(workspaceLocation))
676687
.put(evalInterpreterSuccess('Breakpoint hit!', workspaceLocation))
@@ -684,7 +695,8 @@ describe('evalCode', () => {
684695
scheduler: 'preemptive',
685696
originalMaxExecTime: execTime,
686697
stepLimit: 1000,
687-
useSubst: false
698+
useSubst: false,
699+
throwInfiniteLoops: true
688700
})
689701
.put.like({ action: { type: EVAL_INTERPRETER_ERROR } })
690702
.silentRun();
@@ -707,17 +719,22 @@ describe('evalCode', () => {
707719
scheduler: 'preemptive',
708720
originalMaxExecTime: execTime,
709721
stepLimit: 1000,
710-
useSubst: false
722+
useSubst: false,
723+
throwInfiniteLoops: true
711724
})
712725
.put(evalInterpreterError(context.errors, workspaceLocation))
713726
.silentRun();
714727
});
715728

716729
test('calls reportInfiniteLoop on error and sends correct data to sentry', () => {
717-
context = createContext(3);
718-
const code1 = 'const test=[(x)=>x,2,3,[(x)=>x],5];function f(x){return f(x);}';
730+
state = {
731+
...state,
732+
session: { ...state.session, experimentApproval: true, experimentCoinflip: true }
733+
};
734+
const thisContext = createContext(3);
735+
context = thisContext;
736+
const code1 = 'function f(x){f(x);}';
719737
const code2 = 'f(1);';
720-
state = generateDefaultState(workspaceLocation, {});
721738

722739
return runInContext(code1, context, {
723740
scheduler: 'preemptive',
@@ -728,12 +745,64 @@ describe('evalCode', () => {
728745
.withState(state)
729746
.call(
730747
reportInfiniteLoopError,
731-
'source_protection_recursion',
732-
'function is_list(xs) {\n return is_null(xs) || is_pair(xs) && is_list(tail(xs));\n}\nfunction equal(xs, ys) {\n return is_pair(xs) ? is_pair(ys) && equal(head(xs), head(ys)) && equal(tail(xs), tail(ys)) : is_null(xs) ? is_null(ys) : is_number(xs) ? is_number(ys) && xs === ys : is_boolean(xs) ? is_boolean(ys) && (xs && ys || !xs && !ys) : is_string(xs) ? is_string(ys) && xs === ys : is_undefined(xs) ? is_undefined(ys) : is_function(ys) && xs === ys;\n}\nfunction $length(xs, acc) {\n return is_null(xs) ? acc : $length(tail(xs), acc + 1);\n}\nfunction length(xs) {\n return $length(xs, 0);\n}\nfunction $map(f, xs, acc) {\n return is_null(xs) ? reverse(acc) : $map(f, tail(xs), pair(f(head(xs)), acc));\n}\nfunction map(f, xs) {\n return $map(f, xs, null);\n}\nfunction $build_list(i, fun, already_built) {\n return i < 0 ? already_built : $build_list(i - 1, fun, pair(fun(i), already_built));\n}\nfunction build_list(fun, n) {\n return $build_list(n - 1, fun, null);\n}\nfunction for_each(fun, xs) {\n if (is_null(xs)) {\n return true;\n } else {\n fun(head(xs));\n return for_each(fun, tail(xs));\n }\n}\nfunction $list_to_string(xs, cont) {\n return is_null(xs) ? cont("null") : is_pair(xs) ? $list_to_string(head(xs), x => $list_to_string(tail(xs), y => cont("[" + x + "," + y + "]"))) : cont(stringify(xs));\n}\nfunction list_to_string(xs) {\n return $list_to_string(xs, x => x);\n}\nfunction $reverse(original, reversed) {\n return is_null(original) ? reversed : $reverse(tail(original), pair(head(original), reversed));\n}\nfunction reverse(xs) {\n return $reverse(xs, null);\n}\nfunction $append(xs, ys, cont) {\n return is_null(xs) ? cont(ys) : $append(tail(xs), ys, zs => cont(pair(head(xs), zs)));\n}\nfunction append(xs, ys) {\n return $append(xs, ys, xs => xs);\n}\nfunction member(v, xs) {\n return is_null(xs) ? null : v === head(xs) ? xs : member(v, tail(xs));\n}\nfunction $remove(v, xs, acc) {\n return is_null(xs) ? append(reverse(acc), xs) : v === head(xs) ? append(reverse(acc), tail(xs)) : $remove(v, tail(xs), pair(head(xs), acc));\n}\nfunction remove(v, xs) {\n return $remove(v, xs, null);\n}\nfunction $remove_all(v, xs, acc) {\n return is_null(xs) ? append(reverse(acc), xs) : v === head(xs) ? $remove_all(v, tail(xs), acc) : $remove_all(v, tail(xs), pair(head(xs), acc));\n}\nfunction remove_all(v, xs) {\n return $remove_all(v, xs, null);\n}\nfunction $filter(pred, xs, acc) {\n return is_null(xs) ? reverse(acc) : pred(head(xs)) ? $filter(pred, tail(xs), pair(head(xs), acc)) : $filter(pred, tail(xs), acc);\n}\nfunction filter(pred, xs) {\n return $filter(pred, xs, null);\n}\nfunction $enum_list(start, end, acc) {\n return start > end ? reverse(acc) : $enum_list(start + 1, end, pair(start, acc));\n}\nfunction enum_list(start, end) {\n return $enum_list(start, end, null);\n}\nfunction list_ref(xs, n) {\n return n === 0 ? head(xs) : list_ref(tail(xs), n - 1);\n}\nfunction $accumulate(f, initial, xs, cont) {\n return is_null(xs) ? cont(initial) : $accumulate(f, initial, tail(xs), x => cont(f(head(xs), x)));\n}\nfunction accumulate(f, initial, xs) {\n return $accumulate(f, initial, xs, x => x);\n}\nfunction is_stream(xs) {\n return is_null(xs) || is_pair(xs) && is_stream(stream_tail(xs));\n}\nfunction list_to_stream(xs) {\n return is_null(xs) ? null : pair(head(xs), () => list_to_stream(tail(xs)));\n}\nfunction stream_to_list(xs) {\n return is_null(xs) ? null : pair(head(xs), stream_to_list(stream_tail(xs)));\n}\nfunction stream_length(xs) {\n return is_null(xs) ? 0 : 1 + stream_length(stream_tail(xs));\n}\nfunction stream_map(f, s) {\n return is_null(s) ? null : pair(f(head(s)), () => stream_map(f, stream_tail(s)));\n}\nfunction build_stream(fun, n) {\n function build(i) {\n return i >= n ? null : pair(fun(i), () => build(i + 1));\n }\n return build(0);\n}\nfunction stream_for_each(fun, xs) {\n if (is_null(xs)) {\n return true;\n } else {\n fun(head(xs));\n return stream_for_each(fun, stream_tail(xs));\n }\n}\nfunction stream_reverse(xs) {\n function rev(original, reversed) {\n return is_null(original) ? reversed : rev(stream_tail(original), pair(head(original), () => reversed));\n }\n return rev(xs, null);\n}\nfunction stream_append(xs, ys) {\n return is_null(xs) ? ys : pair(head(xs), () => stream_append(stream_tail(xs), ys));\n}\nfunction stream_member(x, s) {\n return is_null(s) ? null : head(s) === x ? s : stream_member(x, stream_tail(s));\n}\nfunction stream_remove(v, xs) {\n return is_null(xs) ? null : v === head(xs) ? stream_tail(xs) : pair(head(xs), () => stream_remove(v, stream_tail(xs)));\n}\nfunction stream_remove_all(v, xs) {\n return is_null(xs) ? null : v === head(xs) ? stream_remove_all(v, stream_tail(xs)) : pair(head(xs), () => stream_remove_all(v, stream_tail(xs)));\n}\nfunction stream_filter(p, s) {\n return is_null(s) ? null : p(head(s)) ? pair(head(s), () => stream_filter(p, stream_tail(s))) : stream_filter(p, stream_tail(s));\n}\nfunction enum_stream(start, end) {\n return start > end ? null : pair(start, () => enum_stream(start + 1, end));\n}\nfunction integers_from(n) {\n return pair(n, () => integers_from(n + 1));\n}\nfunction eval_stream(s, n) {\n function es(s, n) {\n return n === 1 ? list(head(s)) : pair(head(s), es(stream_tail(s), n - 1));\n }\n return n === 0 ? null : es(s, n);\n}\nfunction stream_ref(s, n) {\n return n === 0 ? head(s) : stream_ref(stream_tail(s), n - 1);\n}\nconst test=[x => x,2,3,[x => x],5];\nfunction f(x) {\n return f(x);\n}'
748+
InfiniteLoopErrorType.NoBaseCase,
749+
false,
750+
'The function f has encountered an infinite loop. It has no base case.',
751+
[code2, code1]
733752
)
734753
.silentRun();
735754
});
736755
});
756+
757+
test('does not send correct data to sentry if approval is false', () => {
758+
state = {
759+
...state,
760+
session: { ...state.session, experimentApproval: false, experimentCoinflip: true }
761+
};
762+
context = createContext(3);
763+
const theCode = 'function f(x){f(x);} f(1);';
764+
765+
return expectSaga(evalCode, theCode, context, execTime, workspaceLocation, actionType)
766+
.withState(state)
767+
.not.call(
768+
reportInfiniteLoopError,
769+
InfiniteLoopErrorType.NoBaseCase,
770+
false,
771+
'The function f has encountered an infinite loop. It has no base case.',
772+
[theCode]
773+
)
774+
.silentRun();
775+
});
776+
777+
test('shows infinite loop error if coinflip is true', () => {
778+
state = { ...state, session: { ...state.session, experimentCoinflip: true } };
779+
const thisContext = createContext(3);
780+
context = thisContext;
781+
const theCode = 'function f(x){f(x);} f(1);';
782+
783+
return expectSaga(evalCode, theCode, context, execTime, workspaceLocation, actionType)
784+
.withState(state)
785+
.silentRun()
786+
.then(result => {
787+
const lastError = thisContext.errors[thisContext.errors.length - 1];
788+
expect(lastError.explain()).toContain('no base case');
789+
});
790+
});
791+
792+
test('does not show infinite loop error if coinflip is false', () => {
793+
state = { ...state, session: { ...state.session, experimentCoinflip: false } };
794+
const thisContext = createContext(3);
795+
context = thisContext;
796+
const theCode = 'function f(x){f(x);} f(1);';
797+
798+
return expectSaga(evalCode, theCode, context, execTime, workspaceLocation, actionType)
799+
.withState(state)
800+
.silentRun()
801+
.then(result => {
802+
const lastError = thisContext.errors[thisContext.errors.length - 1];
803+
expect(lastError.explain()).not.toContain('no base case');
804+
});
805+
});
737806
});
738807

739808
describe('on DEBUG_RESUME action without interruptions or pausing', () => {
@@ -838,7 +907,8 @@ describe('evalTestCode', () => {
838907
value = 'another test value';
839908
options = {
840909
scheduler: 'preemptive',
841-
originalMaxExecTime: 1000
910+
originalMaxExecTime: 1000,
911+
throwInfiniteLoops: true
842912
};
843913
index = 1;
844914
type = TestcaseTypes.public;

0 commit comments

Comments
 (0)