Skip to content
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

Grade caching code merged into development branch #205

Merged
merged 19 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
49cf790
Added caching of grader results.
mckeownp Jan 15, 2024
2fdb0ea
Response caching code for testing on a real server.
mckeownp Jan 29, 2024
d526eb3
Response caching code for testing on a real server.
mckeownp Jan 29, 2024
0dccea6
Merge branch 'grade_caching' of https://github.com/mckeownp/moodle-qt…
mckeownp Jan 29, 2024
1af42c7
Minor update to allow running on php < v8...
mckeownp Jan 29, 2024
6de3382
Updated some tests to make them work in php < 8
mckeownp Jan 29, 2024
4b37081
Merge remote-tracking branch 'origin/development' into grade_caching
mckeownp Jan 31, 2024
46f66c5
Little bugfix...
mckeownp Jan 31, 2024
589891c
Trial with grade caching at jobesandbox.php level.
mckeownp Feb 10, 2024
9522f63
Small tweaks while testing jobe caching.
mckeownp Feb 10, 2024
16b36e3
Tidied up code ready for trial on a real server
mckeownp Feb 25, 2024
51fc87a
Minor updates, removing useless bits and pieces.
mckeownp Feb 28, 2024
e0f7ac9
Merge branch 'grade_caching' into development
mckeownp Feb 29, 2024
feb3b5d
Updated behat tests to initialise jobe sandbox properly each time
mckeownp Mar 3, 2024
6339265
Reverted behat fixtures to roughly what they were with minor improvem…
mckeownp Mar 5, 2024
d363a18
Reverted behat fixtures to roughly what they were with minor improvem…
mckeownp Mar 5, 2024
8902d3e
Merge branch 'grade_cache_for_merging' into development
mckeownp Mar 5, 2024
c333529
Added attribute definitions to qtype_coderunner_combinator_grader_out…
mckeownp Mar 5, 2024
60f5321
Updated grade caching so that questions with different question text …
mckeownp Mar 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ NonRepoFiles/*
/amd/src/ui_blockly.json
/amd/src/.eslintrc.js
.grunt

.vscode/
2 changes: 1 addition & 1 deletion bulktestindex.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
expander.innerHTML = 'Collapse';
expander.nextSibling.style.display = 'inline';
} else {
expander.innerTHML = 'Expand';
expander.innerHTML = 'Expand';
expander.nextSibling.style.display = 'none';
}
});
Expand Down
9 changes: 9 additions & 0 deletions classes/combinator_grader_outcome.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ class qtype_coderunner_combinator_grader_outcome extends qtype_coderunner_testin
/** @var bool If true, is used when the question is to be used only to display the output and perhaps images from a run, with no mark. */
public $showoutputonly;

/** @var array Array where each item is a rows of test result table */
public $testresults;

/** @var ?string The feedback for a given question attempt */
public $feedbackhtml;

/** @var bool Whether or no show differences is selected */
public $showdifferences;

// A list of the allowed attributes in the combinator template grader return value.
public $allowedfields = ['fraction', 'prologuehtml', 'testresults', 'epiloguehtml',
'feedbackhtml', 'columnformats', 'showdifferences',
Expand Down
187 changes: 111 additions & 76 deletions classes/jobesandbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
global $CFG;
require_once($CFG->libdir . '/filelib.php'); // Needed when run as web service.

const READ_FROM_CACHE = true;
const WRITE_TO_CACHE = true;

class qtype_coderunner_jobesandbox extends qtype_coderunner_sandbox {
const DEBUGGING = 0;
const HTTP_GET = 1;
Expand Down Expand Up @@ -197,57 +200,86 @@ public function execute($sourcecode, $language, $input, $files = null, $params =
$this->apikey = $params['jobeapikey'];
}
}

$postbody = ['run_spec' => $runspec];
// QUESTION: Do we need this when using cached result?
$this->currentjobid = sprintf('%08x', mt_rand());

// Try submitting the job. If we get a 404, try again after
// putting all the files on the server. Anything else is an error.
$httpcode = $this->submit($postbody);
if ($httpcode == 404) { // If it's a file not found error ...
foreach ($files as $filename => $contents) {
if (($httpcode = $this->put_file($contents)) != 204) {
break;
}
}
if ($httpcode == 204) {
// Try again if put_files all worked.
$httpcode = $this->submit($postbody);
$cache = cache::make('qtype_coderunner', 'coderunner_grading_cache');
$runresult = null;
if (READ_FROM_CACHE) {
// NOTE: Changing jobeserver setting will effectively flush the cache
// eg, adding another jobeserver to a list of servers will mean the
// jobeserver parameter has changed and therefore the key will change.

// QUESTION: Do we want the cache to ignore the jobeserver setting?
// eg, adding a new, presumeably equal jobeserver to the mix shouldn't
// change the result (unless it isn't equal!)
// But, remember that the server is chosen at random from the pool!

$key = hash("md5", serialize($runspec));
// Debugger: echo '<pre>' . serialize($runspec) . '</pre>';.
$runresult = $cache->get($key); // Unserializes the returned value :) false if not found.
if ($runresult) {
// echo $key . '-----------> FOUND' . '<br>'; .
}
}

$runresult = [];
$runresult['sandboxinfo'] = [
'jobeserver' => $this->jobeserver,
'jobeapikey' => $this->apikey,
];
if (!$runresult) { // if cache read failed regrade to be safe
$postbody = ['run_spec' => $runspec];
// Try submitting the job. If we get a 404, try again after
// putting all the files on the server. Anything else is an error.
$httpcode = $this->submit($postbody);
if ($httpcode == 404) { // If it's a file not found error ...
foreach ($files as $filename => $contents) {
if (($httpcode = $this->put_file($contents)) != 204) {
break;
}
}
if ($httpcode == 204) {
// Try again if put_files all worked.
$httpcode = $this->submit($postbody);
}
}

$okresponse = in_array($httpcode, [200, 203]); // Allow 203, which can result from an intevening proxy server.
if (
!$okresponse // If it's not an OK response...
|| !is_object($this->response) // ... or there's any sort of broken ...
|| !isset($this->response->outcome)
) { // ... communication with server.
// Return with errorcode set and as much extra info as possible in stderr.
$errorcode = $okresponse ? self::UNKNOWN_SERVER_ERROR : $this->get_error_code($httpcode);
$this->currentjobid = null;
$runresult['error'] = $errorcode;
$runresult['stderr'] = "HTTP response from Jobe ({$this->jobeserver}) was $httpcode: " . json_encode($this->response);
} else if ($this->response->outcome == self::RESULT_SERVER_OVERLOAD) {
$runresult['error'] = self::SERVER_OVERLOAD;
} else {
$stderr = $this->filter_file_path($this->response->stderr);
// Any stderr output is treated as a runtime error.
if (trim($stderr ?? '') !== '') {
$this->response->outcome = self::RESULT_RUNTIME_ERROR;
$runresult = [];
$runresult['sandboxinfo'] = [
'jobeserver' => $this->jobeserver,
'jobeapikey' => $this->apikey,
];

$okresponse = in_array($httpcode, [200, 203]); // Allow 203, which can result from an intevening proxy server.
if (
!$okresponse // If it's not an OK response...
|| !is_object($this->response) // ... or there's any sort of broken ...
|| !isset($this->response->outcome)
) { // ... communication with server.
// Return with errorcode set and as much extra info as possible in stderr.
$errorcode = $okresponse ? self::UNKNOWN_SERVER_ERROR : $this->get_error_code($httpcode);
$this->currentjobid = null;
$runresult['error'] = $errorcode;
$runresult['stderr'] = "HTTP response from Jobe was $httpcode: " . json_encode($this->response);
} else if ($this->response->outcome == self::RESULT_SERVER_OVERLOAD) {
$runresult['error'] = self::SERVER_OVERLOAD;
} else {
$stderr = $this->filter_file_path($this->response->stderr);
// Any stderr output is treated as a runtime error.
if (trim($stderr ?? '') !== '') {
$this->response->outcome = self::RESULT_RUNTIME_ERROR;
}
$this->currentjobid = null;
$runresult['error'] = self::OK;
$runresult['stderr'] = $stderr;
$runresult['result'] = $this->response->outcome;
$runresult['signal'] = 0; // Jobe doesn't return signals.
$runresult['cmpinfo'] = $this->response->cmpinfo;
$runresult['output'] = $this->filter_file_path($this->response->stdout);

// Got a useable result from Jobe server so cache it if required.
if (WRITE_TO_CACHE) {
$key = hash("md5", serialize($runspec));
$cache->set($key, $runresult); // set serializes the result, get will unserialize.
// echo 'CACHE WRITE for ---> ' . $key . '<br>';
}
}
$this->currentjobid = null;
$runresult['error'] = self::OK;
$runresult['stderr'] = $stderr;
$runresult['result'] = $this->response->outcome;
$runresult['signal'] = 0; // Jobe doesn't return signals.
$runresult['cmpinfo'] = $this->response->cmpinfo;
$runresult['output'] = $this->filter_file_path($this->response->stdout);
}
return (object) $runresult;
}
Expand All @@ -256,58 +288,61 @@ public function execute($sourcecode, $language, $input, $files = null, $params =
// such class found. Removes comments, strings and nested code and then
// uses a regexp to find a public class.
private function get_main_class($prog) {
// filter out comments and strings
// Filter out comments and strings.
$prog = $prog . ' ';
$filteredProg = array();
$skipTo = -1;
$filteredprog = [];
$skipto = -1;

for ($i = 0; $i < strlen($prog) - 1; $i++) {
if ($skipTo == false) break; // an unclosed comment/string - bail out
if ($i < $skipTo) continue;

// skip "//" comments
if ($prog[$i].$prog[$i+1] == '//') {
$skipTo = strpos($prog, "\n", $i + 2);
if ($skipto == false) {
break; // An unclosed comment/string - bail out.
}

// skip "/**/" comments
else if ($prog[$i].$prog[$i+1] == '/*') {
$skipTo = strpos($prog, '*/', $i + 2) + 2;
$filteredProg[] = ' '; // '/**/' is a token delimiter
if ($i < $skipto) {
continue;
}

// skip strings
else if ($prog[$i] == '"') {
// matches the whole string
// Skip "//" comments.
if ($prog[$i] . $prog[$i + 1] == '//') {
$skipto = strpos($prog, "\n", $i + 2);
// Skip "/**/" comments.
} else if ($prog[$i] . $prog[$i + 1] == '/*') {
$skipto = strpos($prog, '*/', $i + 2) + 2;
$filteredprog[] = ' '; // The string '/**/' is a token delimiter.
// Skip strings.
} else if ($prog[$i] == '"') {
// Matches the whole string.
if (preg_match('/"((\\.)|[^\\"])*"/', $prog, $matches, 0, $i)) {
$skipTo = $i + strlen($matches[0]);
$skipto = $i + strlen($matches[0]);
} else {
$skipto = false;
}
else $skipTo = false;
// Copy everything else.
} else {
$filteredprog[] = $prog[$i];
}

// copy everything else
else $filteredProg[] = $prog[$i];
}

// remove nested code
// Remove nested code.
$depth = 0;
for ($i = 0; $i < count($filteredProg); $i++) {
if ($filteredProg[$i] == '{') $depth++;
if ($filteredProg[$i] == '}') $depth--;
if ($filteredProg[$i] != "\n" && $depth > 0 && !($depth == 1 && $filteredProg[$i] == '{')) {
$filteredProg[$i] = ' ';
for ($i = 0; $i < count($filteredprog); $i++) {
if ($filteredprog[$i] == '{') {
$depth++;
}
if ($filteredprog[$i] == '}') {
$depth--;
}
if ($filteredprog[$i] != "\n" && $depth > 0 && !($depth == 1 && $filteredprog[$i] == '{')) {
$filteredprog[$i] = ' ';
}
}

// search for a public class
if (preg_match('/public\s(\w*\s)*class\s*(\w+)[^\w]/', implode('', $filteredProg), $matches) !== 1) {
// Search for a public class.
if (preg_match('/public\s(\w*\s)*class\s*(\w+)[^\w]/', implode('', $filteredprog), $matches) !== 1) {
return false;
} else {
return $matches[2];
}
}



// Return the sandbox error code corresponding to the given httpcode.
private function get_error_code($httpcode) {
Expand Down
56 changes: 45 additions & 11 deletions classes/jobrunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class qtype_coderunner_jobrunner {
private $testcases = null; // The testcases (a subset of those in the question).
private $allruns = null; // Array of the source code for all runs.


/** @var ?array Array of sandbox params. */
private $sandboxparams = null;

Expand All @@ -48,15 +49,32 @@ class qtype_coderunner_jobrunner {
/** @var bool True if this grading is occurring because the student clicked the precheck button. */
private $isprecheck = false;

// Check the correctness of a student's code and possible extra attachments
// as an answer to the given
// question and and a given set of test cases (which may be empty or a
// subset of the question's set of testcases. $isprecheck is true if
// this is a run triggered by the student clicking the Precheck button.
// $answerlanguage will be the empty string except for multilanguage questions,
// when it is the language selected in the language drop-down menu.
// Returns a TestingOutcome object.
public function run_tests($question, $code, $attachments, $testcases, $isprecheck, $answerlanguage) {


/**
* Check the correctness of a student's code and possible extra attachments
* as an answer to the given question and and a given set of test cases (which may be empty or a
* subset of the question's set of testcases.
* @param qtype_coderunner_question $question object relevant to this step of the attempt
* @param string $code is the JSON repr of the code
* @param array $attachments is the array of attachments given by student, if any
* @param
* @param boolean $isprecheck is true if
* this is a run triggered by the student clicking the Precheck button.
* @param string $answerlanguage will be the empty string except for multilanguage questions,
* when it is the language selected in the language drop-down menu.
* @return qtype_coderunner_combinator_grader_outcome $testoutcome that contains the outcome
* of the grading.
*/
public function run_tests(
$question,
$code,
$attachments,
$testcases,
$isprecheck,
$answerlanguage
) {

if (empty($question->prototype)) {
// Missing prototype. We can't run this question.
$outcome = new qtype_coderunner_testing_outcome(0, 0, false);
Expand All @@ -69,7 +87,8 @@ public function run_tests($question, $code, $attachments, $testcases, $isprechec
['crtype' => $question->coderunnertype]
);
}
$outcome->set_status(qtype_coderunner_testing_outcome::STATUS_MISSING_PROTOTYPE, $message);
$status = qtype_coderunner_testing_outcome::STATUS_MISSING_PROTOTYPE;
$outcome->set_status($status, $message);
return $outcome;
}

Expand Down Expand Up @@ -131,10 +150,11 @@ public function run_tests($question, $code, $attachments, $testcases, $isprechec
if ($question->get_show_source()) {
$outcome->sourcecodelist = $this->allruns;
}


return $outcome;
}


// If the template is a combinator, try running all the tests in a single
// go.
//
Expand All @@ -143,6 +163,13 @@ public function run_tests($question, $code, $attachments, $testcases, $isprechec
// a list of all the test cases and QUESTION, the original question object.
// Return the testing outcome object if successful else null.
private function run_combinator($isprecheck) {
// Remove id and questionid keys+values from testcases so they don't
// affect caching. For example the questionid will change each time
// the question is saved thanks to question versioning - urgh!
foreach ($this->testcases as $tc) {
unset($tc->id);
unset($tc->questionid);
}
$numtests = count($this->testcases);
$this->templateparams['TESTCASES'] = $this->testcases;
$maxmark = $this->maximum_possible_mark();
Expand Down Expand Up @@ -217,6 +244,13 @@ private function run_tests_singly($isprecheck) {
if ($maxmark == 0) {
$maxmark = 1; // Something silly is happening. Probably running a prototype with no tests.
}
// Remove id and questionid keys+values from testcases so they don't
// affect caching. For example the questionid will change each time
// the question is saved thanks to question versioning - urgh!
foreach ($this->testcases as $tc) {
unset($tc->id);
unset($tc->questionid);
}
$numtests = count($this->testcases);
$outcome = new qtype_coderunner_testing_outcome($maxmark, $numtests, $isprecheck);
$question = $this->question;
Expand Down
Loading
Loading