Commit ae352101 authored by Artem Baranovskyi's avatar Artem Baranovskyi
Browse files

Added Integration Test with all necessary faked Entities.

parent 5a0a6e1c
...@@ -3,7 +3,6 @@ MARIADB_DATABASE=moodle ...@@ -3,7 +3,6 @@ MARIADB_DATABASE=moodle
MARIADB_USER=moodleuser MARIADB_USER=moodleuser
MARIADB_PASSWORD=moodlepassword MARIADB_PASSWORD=moodlepassword
MOODLE_DATABASE_ROOT_PASSWORD=rootpassword MOODLE_DATABASE_ROOT_PASSWORD=rootpassword
#MOODLE_DATABASE_HOST=localhost
MOODLE_DATABASE_HOST=mariadb MOODLE_DATABASE_HOST=mariadb
MOODLE_DATABASE_NAME=moodle MOODLE_DATABASE_NAME=moodle
MOODLE_DATABASE_USER=moodleuser MOODLE_DATABASE_USER=moodleuser
......
...@@ -17,7 +17,7 @@ ENV MOODLE_BASE_DIR_DATA ${MOODLE_BASE_DIR_DATA} ...@@ -17,7 +17,7 @@ ENV MOODLE_BASE_DIR_DATA ${MOODLE_BASE_DIR_DATA}
# Installing necessary packages # Installing necessary packages
RUN apt-get update && apt-get upgrade -y && \ RUN apt-get update && apt-get upgrade -y && \
apt-get install -y apache2 php libapache2-mod-php php-mysqli php-mysql php-xml php-pdo php-pdo-mysql mariadb-client mariadb-server wget unzip p7zip-full python3 python3-pip iputils-ping php-mbstring graphviz aspell ghostscript clamav php8.2-pspell php8.2-curl php8.2-gd php8.2-intl php8.2-mysql php8.2-xml php8.2-xmlrpc php8.2-ldap php8.2-zip php8.2-soap php8.2-mbstring openssl git nano supervisor && \ apt-get install -y apache2 php libapache2-mod-php php-mysqli php-mysql php-xml php-pdo php-pdo-mysql mariadb-client mariadb-server wget unzip p7zip-full python3 python3-pip iputils-ping php-mbstring graphviz aspell ghostscript clamav php8.2-pspell php8.2-curl php8.2-gd php8.2-intl php8.2-mysql php8.2-xml php8.2-xmlrpc php8.2-ldap php8.2-zip php8.2-soap php8.2-mbstring openssl git nano supervisor php-xdebug ca-certificates && \
apt-get clean && rm -rf /var/lib/apt/lists/* apt-get clean && rm -rf /var/lib/apt/lists/*
# Setting necessary php params # Setting necessary php params
...@@ -26,6 +26,13 @@ RUN echo "mysql.default_socket=/run/mysqld/mysqld.sock" >> /etc/php/8.2/apache2/ ...@@ -26,6 +26,13 @@ RUN echo "mysql.default_socket=/run/mysqld/mysqld.sock" >> /etc/php/8.2/apache2/
RUN echo "max_input_vars = 5000" >> /etc/php/8.2/apache2/php.ini RUN echo "max_input_vars = 5000" >> /etc/php/8.2/apache2/php.ini
RUN echo "max_input_vars = 5000" >> /etc/php/8.2/cli/php.ini RUN echo "max_input_vars = 5000" >> /etc/php/8.2/cli/php.ini
# Setting Xdebug
#RUN echo "zend_extension=xdebug.so" >> /etc/php/8.2/apache2/php.ini
#RUN echo "xdebug.mode=debug" >> /etc/php/8.2/apache2/php.ini
#RUN echo "xdebug.start_with_request=yes" >> /etc/php/8.2/apache2/php.ini
#RUN echo "xdebug.client_host=host.docker.internal" >> /etc/php/8.2/apache2/php.ini
#RUN echo "xdebug.client_port=9003" >> /etc/php/8.2/apache2/php.ini
# Starting Apache server # Starting Apache server
RUN chmod 1777 /tmp RUN chmod 1777 /tmp
...@@ -119,6 +126,18 @@ RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ ...@@ -119,6 +126,18 @@ RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-out /etc/ssl/certs/apache-selfsigned.crt \ -out /etc/ssl/certs/apache-selfsigned.crt \
-subj "/C=EU/ST=Berlin/L=Berlin/O=HFT/CN=www.moodle.loc" -subj "/C=EU/ST=Berlin/L=Berlin/O=HFT/CN=www.moodle.loc"
# Установка корневых сертификатов
RUN update-ca-certificates
RUN curl -O https://curl.se/ca/cacert.pem \
&& mv cacert.pem /etc/ssl/certs/cacert.pem \
# Настройка php.ini для OpenSSL
RUN echo "openssl.cafile=/etc/ssl/certs/ca-certificates.crt" >> /etc/php/8.2/cli/php.ini \
&& echo "openssl.capath=/etc/ssl/certs" >> /etc/php/8.2/cli/php.ini \
&& echo "openssl.cafile=/etc/ssl/certs/ca-certificates.crt" >> /etc/php/8.2/apache2/php.ini \
&& echo "openssl.capath=/etc/ssl/certs" >> /etc/php/8.2/apache2/php.ini \
# Configure SSL virtual host # Configure SSL virtual host
RUN echo \ RUN echo \
"<VirtualHost *:443>\n" \ "<VirtualHost *:443>\n" \
......
...@@ -37,7 +37,7 @@ function local_asystgrade_before_footer() ...@@ -37,7 +37,7 @@ function local_asystgrade_before_footer()
{ {
global $PAGE; global $PAGE;
// Получение параметров из URL // Obtaining parameters from URL
$qid = optional_param('qid', null, PARAM_INT); $qid = optional_param('qid', null, PARAM_INT);
$slot = optional_param('slot', false, PARAM_INT); $slot = optional_param('slot', false, PARAM_INT);
......
<?php
use local_asystgrade\api\client;
use local_asystgrade\api\http_client;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/phpunit/classes/advanced_testcase.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/generator/lib.php');
class quiz_api_test extends advanced_testcase
{
/**
* @throws Exception
*/
public function test_quiz_api()
{
global $DB;
$generator = $this->getDataGenerator();
$quizgen = $generator->get_plugin_generator('mod_quiz');
$questiongen = $generator->get_plugin_generator('core_question');
// Create a course
$coursegen = $generator->create_course([
'fullname' => 'Test Course',
'shortname' => 'testcourse',
'category' => 1,
]);
$teacher = $this->getDataGenerator()->create_user();
$teacherRoleId = $DB->get_record('role', ['shortname' => 'teacher'])->id;
$this->getDataGenerator()->enrol_user($teacher->id, $coursegen->id, $teacherRoleId);
$this->setUser($teacher);
// Create a quiz in the course
$quiz = $quizgen->create_instance([
'course' => $coursegen->id,
'name' => 'Test Quiz',
'intro' => 'This is a test quiz.',
'attempts' => 1,
'timeopen' => time(),
'timeclose' => time() + 3600,
]);
// Create questions and answers
$questions = [
[
'questiontext' => 'Warum kann Ihr Programm durch die Verwendung von Threads schneller werden, auch wenn Sie nur einen einzigen Prozessor zur Verfügung haben?',
'qtype' => 'essay',
'answers' => [
'Die Ausführung wird schneller, weil der Prozessor zwischen verschiedenen Teilaufgaben hin- und herspringen kann und so z.B. Wartezeiten auf Daten in einem Thread zur Bearbeitung anderer Threads genutzt werden können.' => 1
]
],
[
'questiontext' => 'Warum wird Ihr Programm durch die Verwendung von Threads schneller, auch wenn Sie nur einen einzigen Prozessor zur Verfügung haben?',
'qtype' => 'essay',
'answers' => [
'Die Ausführung wird schneller, weil der Prozessor zwischen verschiedenen Teilaufgaben hin- und herspringen kann und so z.B. Wartezeiten auf Daten in einem Thread zur Bearbeitung anderer Threads genutzt werden können.' => 1
]
],
[
'questiontext' => 'Beschreiben Sie die Struktur einer Stream-Pipeline. Woher kommen die Daten, was geschieht im Stream mit ihnen, wie endet der Stream? Geben Sie für jeden Streamabschnitt mindestens eine Beispielkomponente an.',
'qtype' => 'essay',
'answers' => [
'Datenquelle: Collections, Arrays, Generatoren (z.B. Datenbankabfragen, eigene Methoden). Verarbeitung: Filtern, Umformung, Begrenzung. Datensenke: Minimum / Maximum / Durchschnitt / Anzahl, Ausgabe in Collection oder Array / Reduktion / Auswertung.' => 1
]
],
[
'questiontext' => 'Welche Auswirkungen hat die Model-View-Aufteilung bei Swing-Komponenten? Nennen Sie Beispiele anhand der Klasse JTable.',
'qtype' => 'essay',
'answers' => [
'Datenhaltung und Darstellung werden getrennt, so dass dieselben Daten flexibel dargestellt werden können. Datenhaltung findet in der Klasse TableModel statt: Welche Information steht in welcher Zelle? Information z.B. über die Editierbarkeit der Zellen, den zu verwendenden Editor und die Spaltenreihenfolge werden in JTable bzw. TableColumnModel gehalten.' => 3
]
],
[
'questiontext' => 'Warum braucht man bei der Arbeit mit Threads Synchronisation?',
'qtype' => 'essay',
'answers' => [
'Man muss vermeiden, dass verschiedene Threads gleichzeitig auf Daten oder Objekte zugreifen, weil es dadurch zur Zerstörung von Werten und zu inkonsistenten Zuständen kommen kann.' => 1
]
],
[
'questiontext' => 'Beantworten Sie kurz die 3 Fragen 1 - Was wird unter einem Thread verstanden / wann werden Threads benutzt ? (3 Pkt) 2 - Welche Ressourcen nutzt ein Thread exklusiv ? (2 Pkt) 3 - Welche Ressourcen teilen sich die Threads eines Programms ? (2 Pkt)',
'qtype' => 'essay',
'answers' => [
'Thread: 1 PT Ablauffaden, Ablaufeinheit, Ausführung etc. 1Pt Parallel, Quasi Parallel, Core, 1Pt Geschwindigkeit, Resourcenauslastung, parallele Ausführung' => 7
]
],
];
// Create a question category
$context = context_course::instance($coursegen->id);
$category = $this->create_question_category($context->id);
foreach ($questions as $questiondata) {
// Create a question
$question = $questiongen->create_question($questiondata['qtype'], null, [
'category' => $category->id, // категория вопроса
'questiontext' => [
'text' => $questiondata['questiontext'],
'format' => FORMAT_HTML,
],
'name' => 'Test Question',
'contextid' => $context->id, // Убедитесь, что контекст передается правильно
'modifiedby' => $teacher->id,
]);
// Check the question ID
if (!$question) {
throw new Exception("Failed to create question.");
}
// Add answers to the question
foreach ($questiondata['answers'] as $answertext => $fraction) {
// Check the validity of answer data
if (!is_string($answertext) || !is_numeric($fraction)) {
throw new InvalidArgumentException("Invalid answer format.");
}
error_log("Created answer: Answer Text: $answertext, Fraction: $fraction");
$answer = [
'question' => $question->id,
'answer' => $answertext,
'fraction' => $fraction,
'feedback' => '', // можно добавить текст обратной связи
'feedbackformat' => FORMAT_MOODLE,
];
$answer_id = $DB->insert_record('question_answers', $answer);
// Check the answer ID
if (!$answer_id) {
throw new Exception("Failed to create answer.");
}
}
error_log("Created question: " . print_r($questiondata, true));
// Get the current maximum slot for the quiz
$maxslot = $DB->get_field_sql("
SELECT MAX(slot)
FROM {quiz_slots}
WHERE quizid = :quizid", ['quizid' => $quiz->id]);
// Set the new slot
$newslot = $maxslot ? $maxslot + 1 : 1;
// Define the number of questions per page
$questions_per_page = 1;
$current_page = floor(($newslot - 1) / $questions_per_page) + 1;
// Check if a record already exists
$existing_slot = $DB->get_record('quiz_slots', [
'quizid' => $quiz->id,
'slot' => $newslot,
'page' => $current_page
]);
if ($existing_slot) {
echo "Slot and page combination already exists: Quiz ID: {$quiz->id}, Slot: {$newslot}, Page: {$current_page}\n";
continue; // Skip adding this question
}
// Manually add the question to the quiz via the quiz_slots table
$slotdata = [
'quizid' => $quiz->id,
'questionid' => $question->id,
'slot' => $newslot, // Set a new slot
'page' => $current_page, // Set the current page
'requireprevious' => 0,
'maxmark' => 1.0, // Maximum mark for the question
];
echo "Inserting into quiz_slots with Quiz ID: {$quiz->id}, Slot: {$newslot}, Page: {$current_page}\n";
// Insert the data into the table
try {
$transaction = $DB->start_delegated_transaction();
$DB->insert_record('quiz_slots', (object)$slotdata);
$transaction->allow_commit();
} catch (dml_exception $e) {
$transaction->rollback($e);
throw new Exception("Failed to add question to quiz: " . $e->getMessage());
}
}
// Creating students
$students = [];
for ($i = 0; $i < 7; $i++) {
$students[] = $this->getDataGenerator()->create_user();
}
// Students course enrollment
foreach ($students as $student) {
$this->getDataGenerator()->enrol_user($student->id, $coursegen->id, 'student');
}
// Create attempts for students manually
$all_answers = array_map(function ($question) {
return array_keys($question['answers']);
}, $questions);
$flat_answers = array_merge(...$all_answers);
$questions_db = $DB->get_records('question', []);
var_dump($questions_db);
$question_keys = array_merge(
...array_map(fn($question) => [
$question->id => $question->questiontext
], $questions_db)
);
$random_key = array_rand($question_keys);
var_dump($random_key, array_column($questions_db, 'id')[$random_key]);
$random_question_id = array_column($questions_db, 'id')[$random_key];
for ($i = 0; $i < count($students); $i++) {
$this->create_quiz_attempt($quiz->id, $students[$i]->id, $random_question_id, $flat_answers[$random_key], $i);
}
global $DB;
$studentAnswers = [];
// Logging the question ID
error_log("Question ID: $question->id");
// Logging the question text
error_log("Question Text: $question_keys[$random_key]");
$referenceAnswer = $flat_answers[$random_key];
$answers = $DB->get_records('question_attempt_step_data', ['name' => 'answer']);
// Logging the number of responses
error_log("Number of Answers: " . count($answers));
foreach ($answers as $answer) {
// Log each response
error_log("Answer: " . $answer->value);
$studentAnswers[] = $answer->value;
var_dump($answer->value);
error_log("Answer: " . $answer->value);
}
$apiendpoint = get_config('local_asystgrade', 'apiendpoint');
if (!$apiendpoint) {
$apiendpoint = 'http://127.0.0.1:5000/api/autograde'; // Default setting
}
error_log('APIendpoint: ' . $apiendpoint);
try {
$httpClient = new http_client();
$apiClient = client::getInstance($apiendpoint, $httpClient);
error_log('ApiClient initiated.');
error_log('Sending data to API and getting grade');
$data = [
'referenceAnswer' => $referenceAnswer,
'studentAnswers' => $studentAnswers
];
var_dump($data);
error_log("Data to send to API: " . print_r($data, true));
$response = $apiClient->send_data($data);
$grades = json_decode($response, true);
error_log('Grade obtained: ' . print_r($grades, true));
} catch (Exception $e) {
error_log('Error sending data to API: ' . $e->getMessage());
return;
}
var_dump($grades);
// Check the result
$this->assertNotEmpty($grades);
$this->assertEquals($grades[0]['predicted_grade'], 'correct');
$this->assertEquals($grades[6]['predicted_grade'], 'incorrect');
}
private function create_question_category($contextid)
{
global $DB;
$category = [
'name' => 'Test Category',
'contextid' => $contextid,
'parent' => 0,
'info' => '',
'infoformat' => FORMAT_MOODLE,
];
// Use Moodle API to create a category
$categoryid = $DB->insert_record('question_categories', (object)$category);
if (!$categoryid) {
throw new coding_exception("Failed to create category.");
}
return $DB->get_record('question_categories', array('id' => $categoryid));
}
private function create_quiz_attempt($quizid, $userid, $questionid, $exapmle_answers, $student_id)
{
global $DB;
$sql = "SELECT MAX(attempt) as max_attempt FROM {quiz_attempts} WHERE quiz = :quizid AND userid = :userid";
$params = array('quizid' => $quizid, 'userid' => $userid);
$record = $DB->get_record_sql($sql, $params);
// Set unique attempt number
$attempt_number = ($record && $record->max_attempt) ? $record->max_attempt + 1 : 1;
// Generate unique id for uniqueid
// This query takes the maximum uniqueid value from the table and adds 1
$uniqueid = $DB->get_field_sql('SELECT COALESCE(MAX(uniqueid), 0) + 1 FROM {quiz_attempts}');
$attempt = [
'quiz' => $quizid,
'userid' => $userid,
'attempt' => $attempt_number,
'timestart' => time() - 3600,
'timefinish' => time(),
'sumgrades' => 10,
'layout' => '1,0,2,0,3,0,4,0,5,0,6,0', // array of slot numbers on the page in order
'uniqueid' => $uniqueid,
];
$question_attempt = [
'questionusageid' => $uniqueid, // Linked to the test attempt
'slot' => 1, // Question slot number in the test
'behaviour' => 'manualgraded', // Essay requires manual grading
'questionid' => $questionid, // Question ID
'variant' => 1, // Question variant
'maxmark' => 10.0, // Maximum score for the question
'minfraction' => 0.0, // Minimum fraction of the score
'maxfraction' => 1.0, // Maximum fraction of the score
'flagged' => 0, // Flag indicating whether the question was flagged
'questionsummary' => 'Essay question', // Brief description of the question
'rightanswer' => '', // For essays, there may be no correct answer
'responsesummary' => '', // Summary response (if any)
'timemodified' => time(), // Time of modification
];
// Insert a record into the quiz_attempt* tables
try {
$DB->insert_record('quiz_attempts', $attempt);
$question_attempt_id = $DB->insert_record('question_attempts', $question_attempt);
$question_attempt_step = [
'questionattemptid' => $question_attempt_id, // ID of the question from the `mdl_question_attempts` table
'sequencenumber' => 1, // Step sequence number
'state' => 'complete', // Step state
'fraction' => 0.0, // For essays, this may be 0 until graded
'timecreated' => time(), // Time of step creation
'userid' => $userid, // ID of the user who created the step
];
$question_attempt_step_id = $DB->insert_record('question_attempt_steps', $question_attempt_step);
// Example of how to shorten the answer length
$answer_length = strlen($exapmle_answers) - ($student_id * strlen($exapmle_answers) / 6); // Reduce the answer by 10 characters for each subsequent student
$student_answer = substr($exapmle_answers, 0, $answer_length);
// Saving the answer text
$question_attempt_step_data_answer = [
'attemptstepid' => $question_attempt_step_id, // ID of the step from the `mdl_question_attempt_steps` table
'name' => 'answer', // Data name (e.g., 'answer')
'value' => $student_answer, // User's answer
];
// Saving the answer format
$question_attempt_step_data_format = [
'attemptstepid' => $question_attempt_step_id, // ID of the step from the `mdl_question_attempt_steps` table
'name' => 'answerformat', // Data name (e.g., 'answerformat')
'value' => '1', // Answer format (1 = HTML, 0 = plain text, etc.)
];
// Inserting data into the question_attempt_step_data table
$DB->insert_record('question_attempt_step_data', $question_attempt_step_data_answer);
$DB->insert_record('question_attempt_step_data', $question_attempt_step_data_format);
echo `Quiz attempt inserted successfully. $quizid\n`;
} catch (dml_write_exception $e) {
// Handling database write errors
echo "Error inserting quiz attempt: " . $e->getMessage() . "\n";
}
}
protected function setUp(): void
{
global $DB;
$this->resetAfterTest();
parent::setUp();
$DB->execute('TRUNCATE TABLE {quiz_attempts}');
$DB->execute('TRUNCATE TABLE {quiz_slots}');
}
}
...@@ -22,6 +22,7 @@ docker-compose exec moodle chmod -R 775 ${MOODLE_BASE_DIR_DATA} ...@@ -22,6 +22,7 @@ docker-compose exec moodle chmod -R 775 ${MOODLE_BASE_DIR_DATA}
sleep 5 sleep 5
docker-compose exec moodle php ${MOODLE_BASE_DIR}/admin/cli/install.php \ docker-compose exec moodle php ${MOODLE_BASE_DIR}/admin/cli/install.php \
--wwwroot="${MOODLE_WWWROOT}" \ --wwwroot="${MOODLE_WWWROOT}" \
--phpunit_dataroot="${MOODLE_WWWROOT}" \
--dataroot="${MOODLE_BASE_DIR_DATA}" \ --dataroot="${MOODLE_BASE_DIR_DATA}" \
--dbtype="mariadb" \ --dbtype="mariadb" \
--dbname="${MOODLE_DATABASE_NAME}" \ --dbname="${MOODLE_DATABASE_NAME}" \
...@@ -46,6 +47,10 @@ docker-compose exec moodle php ${MOODLE_BASE_DIR}/admin/cli/install.php \ ...@@ -46,6 +47,10 @@ docker-compose exec moodle php ${MOODLE_BASE_DIR}/admin/cli/install.php \
echo "No database backup found. Skipping restore." echo "No database backup found. Skipping restore."
fi fi
# Composer installation to run phpunit tests
docker-compose exec moodle composer install
# Next, configure PHPUnit for Moodle:
docker-compose exec moodle php admin/tool/phpunit/cli/init.php
# Ensure correct ownership and permissions after installation # Ensure correct ownership and permissions after installation
docker-compose exec moodle chown -R www-data:www-data ${MOODLE_BASE_DIR} docker-compose exec moodle chown -R www-data:www-data ${MOODLE_BASE_DIR}
......
...@@ -24,3 +24,10 @@ route http://127.0.0.1:5000/api/autograde ...@@ -24,3 +24,10 @@ route http://127.0.0.1:5000/api/autograde
Now the preinstalled MOODLE LMS is available at https://www.moodle.loc Now the preinstalled MOODLE LMS is available at https://www.moodle.loc
**Note**: Bind https://www.moodle.loc to your localhost at **hosts** file depending on your OS. **Note**: Bind https://www.moodle.loc to your localhost at **hosts** file depending on your OS.
## Running Unit Tests
To run only Plugin's Test please run at project's CLI:
~~~bash
vendor/bin/phpunit --testsuite local_asystgrade_testsuite
~~~
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment