Skip to content

Commit

Permalink
#11 - Feature - Allow Managers To Run Code
Browse files Browse the repository at this point in the history
  • Loading branch information
danilobecke authored Jan 10, 2024
2 parents 1caed35 + 30a1feb commit 101c5ce
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 30 deletions.
4 changes: 2 additions & 2 deletions code/endpoints/results_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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']
Expand Down
68 changes: 51 additions & 17 deletions code/services/result_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions code/services/runner_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
28 changes: 21 additions & 7 deletions code/tests/result_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<stdio.h>
Expand Down Expand Up @@ -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]
Expand Down

0 comments on commit 101c5ce

Please sign in to comment.