From 30a1febbcacfb1ce44cad0b10cfb28ab5cd011bd Mon Sep 17 00:00:00 2001 From: Danilo Becke Date: Wed, 10 Jan 2024 18:32:18 -0300 Subject: [PATCH] Allow managers to run code --- code/endpoints/results_endpoints.py | 4 +- code/services/result_service.py | 68 +++++++++++++++++++++-------- code/services/runner_service.py | 10 +++-- code/tests/result_test.py | 28 +++++++++--- 4 files changed, 80 insertions(+), 30 deletions(-) diff --git a/code/endpoints/results_endpoints.py b/code/endpoints/results_endpoints.py index b290ba0..8cc17ca 100644 --- a/code/endpoints/results_endpoints.py +++ b/code/endpoints/results_endpoints.py @@ -84,7 +84,7 @@ class ResultsResource(Resource): # type: ignore _tcase_service: TCaseService | None _result_service: ResultService | None - @_namespace.doc(description='*Students only*\nSubmit a code to be evaluated.') + @_namespace.doc(description='Submit a code to be evaluated.') @_namespace.expect(_submit_result_parser, validate=True) @_namespace.param('code', _in='formData', type='file', required=True) @_namespace.response(400, 'Error') @@ -96,7 +96,7 @@ class ResultsResource(Resource): # type: ignore @_namespace.response(500, 'Error') @_namespace.marshal_with(_result_model, code=201) @_namespace.doc(security='bearer') - @authentication_required(Role.STUDENT) + @authentication_required() def post(self, task_id: int, user: UserVO) -> tuple[ResultVO, int]: args = _submit_result_parser.parse_args() file_storage: FileStorage = args['code'] diff --git a/code/services/result_service.py b/code/services/result_service.py index 2f646d2..0df48cc 100644 --- a/code/services/result_service.py +++ b/code/services/result_service.py @@ -34,40 +34,74 @@ def __init__(self, runner_service: RunnerService, moss_service: Optional[MossSer self.__plagiarism_report_repository = PlagiarismReportRepository() def run(self, user: UserVO, task: TaskVO, tests: AllTestsVO, file: File) -> ResultVO: - now = datetime.now().astimezone() if (len(tests.closed_tests) + len(tests.open_tests)) < 1: raise Forbidden() # Prevent running without tests + match user.role: + case Role.STUDENT: + return self.__run_as_student(user, task, tests, file) + case Role.MANAGER: + return self.__run_as_manager(user, task, tests, file) + + def __run_as_student(self, user: UserVO, task: TaskVO, tests: AllTestsVO, file: File) -> ResultVO: + now = datetime.now().astimezone() if unwrap(task.starts_on) > now or (task.ends_on is not None and task.ends_on < now): raise Forbidden() attempt_number = self.__result_repository.get_number_of_results(user.id, task.id) + 1 if task.max_attempts is not None and attempt_number > task.max_attempts: raise Forbidden() - code_max_size_mb = float(Config.get('files.code-max-size-mb')) - file_path = file.save(self.__runner_service.allowed_extensions(task.languages), max_file_size_mb=code_max_size_mb) - dto = ResultDTO() - dto.correct_open = 0 - dto.correct_closed = 0 - dto.file_path = file_path - dto.student_id = user.id - dto.task_id = task.id - stored = self.__result_repository.add(dto) + stored = self.__result_repository.add(self.__create_result_dto(user, task, file)) try: - results = self.__runner_service.run(file_path, tests, stored.id) - open_results = list(filter(lambda result: any(result.test_case_id == test.id for test in tests.open_tests) , results)) - closed_results = list(filter(lambda result: any(result.test_case_id == test.id for test in tests.closed_tests) , results)) - reducer: Callable[[int, TCaseResultVO], int] = lambda current, result: current + (1 if result.success else 0) - stored.correct_open = reduce(reducer, open_results, 0) - stored.correct_closed = reduce(reducer, closed_results, 0) + results = self.__runner_service.run(stored.file_path, tests, stored.id, should_save=True) + open_results, closed_results = self.__split_open_closed_results(results, tests) + correct_open, correct_closed = self.__compute_correct_open_correct_closed(open_results, closed_results) + stored.correct_open = correct_open + stored.correct_closed = correct_closed self.__result_repository.update_session() for result in closed_results: result.diff = None return ResultVO.import_from_dto(stored, attempt_number, open_results, closed_results) except ServerError as e: # rollback - os.remove(file_path) + os.remove(stored.file_path) self.__result_repository.delete(stored.id) raise e + def __run_as_manager(self, user: UserVO, task: TaskVO, tests: AllTestsVO, file: File) -> ResultVO: + dto = self.__create_result_dto(user, task, file) + dto.id = -1 + try: + results = self.__runner_service.run(dto.file_path, tests, dto.id, should_save=False) + os.remove(dto.file_path) + open_results, closed_results = self.__split_open_closed_results(results, tests) + correct_open, correct_closed = self.__compute_correct_open_correct_closed(open_results, closed_results) + dto.correct_open = correct_open + dto.correct_closed = correct_closed + return ResultVO.import_from_dto(dto, -1, open_results, closed_results) + except ServerError as e: + # rollback + os.remove(dto.file_path) + raise e + + def __create_result_dto(self, user: UserVO, task: TaskVO, file: File) -> ResultDTO: + code_max_size_mb = float(Config.get('files.code-max-size-mb')) + file_path = file.save(self.__runner_service.allowed_extensions(task.languages), max_file_size_mb=code_max_size_mb) + dto = ResultDTO() + dto.correct_open = 0 + dto.correct_closed = 0 + dto.file_path = file_path + dto.student_id = user.id + dto.task_id = task.id + return dto + + def __split_open_closed_results(self, tcase_results: list[TCaseResultVO], tests: AllTestsVO) -> tuple[list[TCaseResultVO], list[TCaseResultVO]]: + open_results = list(filter(lambda result: any(result.test_case_id == test.id for test in tests.open_tests) , tcase_results)) + closed_results = list(filter(lambda result: any(result.test_case_id == test.id for test in tests.closed_tests) , tcase_results)) + return (open_results, closed_results) + + def __compute_correct_open_correct_closed(self, open_results: list[TCaseResultVO], closed_results: list[TCaseResultVO]) -> tuple[int, int]: + reducer: Callable[[int, TCaseResultVO], int] = lambda current, result: current + (1 if result.success else 0) + return (reduce(reducer, open_results, 0), reduce(reducer, closed_results, 0)) + def get_latest_source_code_name_path(self, task: TaskVO, user_id: int) -> tuple[str, str]: dto = self.__result_repository.get_latest_result(user_id, task.id) path = dto.file_path diff --git a/code/services/runner_service.py b/code/services/runner_service.py index d1ea84c..41773df 100644 --- a/code/services/runner_service.py +++ b/code/services/runner_service.py @@ -80,13 +80,13 @@ def __remove_directory(self, path: str, container: str) -> None: subprocess.run(self.__execution_command(f'rm -rf {path}', container), check=True) # pylint: disable=too-many-branches,too-many-statements - def run(self, path: str, tests: AllTestsVO, result_id: int) -> list[TCaseResultVO]: + def run(self, path: str, tests: AllTestsVO, result_id: int, should_save: bool) -> list[TCaseResultVO]: results: list[TCaseResultVO] = [] try: runner = next(_runner for _runner in self.__runners if _runner.is_source_code(path)) if not RunnerQueueManager.check_container_available(runner): RunnerQueueManager.continue_when_available(runner) - self.run(path, tests, result_id) + self.run(path, tests, result_id, should_save) RunnerQueueManager.set_using_container(runner) dest = str(uuid.uuid1()) # temp folder source_path = self.__add_to_sandbox(path, dest, runner.container_name) @@ -114,7 +114,8 @@ def run(self, path: str, tests: AllTestsVO, result_id: int) -> list[TCaseResultV dto.success = False dto.diff = 'Timeout.' finally: - results.append(TCaseResultVO.import_from_dto(self.__tcase_result_repository.add(dto))) + stored = self.__tcase_result_repository.add(dto) if should_save else dto + results.append(TCaseResultVO.import_from_dto(stored)) self.__remove_directory(dest, runner.container_name) RunnerQueueManager.release_container(runner) return results @@ -129,7 +130,8 @@ def run(self, path: str, tests: AllTestsVO, result_id: int) -> list[TCaseResultV dto.result_id = result_id dto.success = False dto.diff = str(e) - results.append(TCaseResultVO.import_from_dto(self.__tcase_result_repository.add(dto))) + stored = self.__tcase_result_repository.add(dto) if should_save else dto + results.append(TCaseResultVO.import_from_dto(stored)) if runner: self.__remove_directory(dest, runner.container_name) RunnerQueueManager.release_container(runner) diff --git a/code/tests/result_test.py b/code/tests/result_test.py index e87693f..0a47758 100644 --- a/code/tests/result_test.py +++ b/code/tests/result_test.py @@ -3,7 +3,7 @@ import pytest -from tests.helper import post, get_manager_id_token, create_task_json, create_test_case_json, get_student_id_token, create_join_request_group_id, CONTENT_TYPE_FORM_DATA, get_filepath_of_size, get_new_group_id_code, get_random_name, get, patch, set_up_task_id_student_token +from tests.helper import post, get_manager_id_token, create_task_json, create_test_case_json, get_student_id_token, create_join_request_group_id, CONTENT_TYPE_FORM_DATA, get_filepath_of_size, get_random_name, get, patch, set_up_task_id_student_token VALID_C_CODE = ''' #include @@ -218,17 +218,31 @@ def test_post_result_with_task_without_tests_should_return_forbidden(self) -> No assert response[0] == 403 - def test_post_result_with_manager_should_return_unauthorized(self) -> None: + def test_post_result_with_manager_should_run_and_show_diff_for_closed_tests(self) -> None: manager_token = get_manager_id_token()[1] - group_id = get_new_group_id_code(get_random_name(), manager_token)[0] - task_id = create_task_json(manager_token, group_id)['id'] - create_test_case_json(manager_token, task_id) + task_id, _ = self.__set_up_valid_2_open_2_closed_tests_task_id_student_token() + payload = { - 'code': (BytesIO(VALID_C_CODE.encode('utf-8')), 'code.c') + 'code': (BytesIO(FAIL_TWO_TESTS_C_CODE.encode('utf-8')), 'code.c') } response = post(f'/api/v1/tasks/{task_id}/results', payload, manager_token, CONTENT_TYPE_FORM_DATA) - assert response[0] == 401 + assert response[0] == 201 + assert response[1]['id'] == -1 + assert response[1]['attempt_number'] == -1 + assert response[1]['open_result_percentage'] == 50 + assert response[1]['closed_result_percentage'] == 50 + assert response[1]['result_percentage'] == 50 + assert len(response[1]['open_results']) == 2 + assert len(response[1]['closed_results']) == 2 + assert response[1]['open_results'][0]['success'] is True + assert response[1]['open_results'][0].get('diff') is None + assert response[1]['open_results'][1]['success'] is False + assert response[1]['open_results'][1].get('diff') is not None + assert response[1]['closed_results'][0]['success'] is False + assert response[1]['closed_results'][0].get('diff') is not None + assert response[1]['closed_results'][1]['success'] is True + assert response[1]['closed_results'][1].get('diff') is None def test_post_result_with_invalid_task_id_should_return_not_found(self) -> None: student_token = get_student_id_token()[1]