diff --git a/demo/demo.yaml b/demo/demo.yaml index 2391207..0bfe4ef 100644 --- a/demo/demo.yaml +++ b/demo/demo.yaml @@ -70,6 +70,8 @@ questions: - [tut-alert1, tut-alert2] - tut-generator - tut-yamllint + - tut-code + # test: # - ref1 diff --git a/demo/questions/questions-tutorial.yaml b/demo/questions/questions-tutorial.yaml index 876c1e5..fbfe559 100644 --- a/demo/questions/questions-tutorial.yaml +++ b/demo/questions/questions-tutorial.yaml @@ -609,3 +609,49 @@ generate-question | yamllint - correct-answer | yamllint - ``` + +# ---------------------------------------------------------------------------- +- type: code + ref: tut-code + title: Submissão de código (JOBE) + text: | + É possível enviar código para ser compilado e executado por um servidor + JOBE instalado separadamente, ver [JOBE](https://github.com/trampgeek/jobe). + + ```yaml + - type: code + ref: tut-code + title: Submissão de código (JOBE) + text: | + Escreva um programa em C que recebe uma string no standard input e + mostra a mensagem `hello ` seguida da string. + Por exemplo, se o input for `Maria`, o output deverá ser `hello Maria`. + server: 127.0.0.1 # replace by appropriate address + language: c + correct: + - stdin: 'Maria' + stdout: 'hello Maria' + - stdin: 'xyz' + stdout: 'hello xyz' + ``` + + Existem várias linguagens suportadas pelo servidor JOBE (C, C++, Java, + Python2, Python3, Octave, Pascal, PHP). + O campo `correct` deverá ser uma lista de casos a testar. + Se um caso incluir `stdin`, este será enviado para o programa e o `stdout` + obtido será comparado com o declarado. A pergunta é considerada correcta se + todos os outputs coincidirem. + answer: | + #include + int main() { + char name[20]; + scanf("%s", name); + printf("hello %s", name); + } + server: 192.168.1.141 + language: c + correct: + - stdin: 'Maria' + stdout: 'hello Maria' + - stdin: 'xyz' + stdout: 'hello xyz' diff --git a/perguntations/__init__.py b/perguntations/__init__.py index 11f2850..97c5244 100644 --- a/perguntations/__init__.py +++ b/perguntations/__init__.py @@ -32,7 +32,7 @@ proof of submission and for review. ''' APP_NAME = 'perguntations' -APP_VERSION = '2020.11.dev2' +APP_VERSION = '2020.11.dev3' APP_DESCRIPTION = __doc__ __author__ = 'Miguel Barão' diff --git a/perguntations/app.py b/perguntations/app.py index f020652..a66d33b 100644 --- a/perguntations/app.py +++ b/perguntations/app.py @@ -190,7 +190,7 @@ class App(): raise AppException('Failed to create test factory!') from exc # ------------------------------------------------------------------------ - def _pregenerate_tests(self, num): + def _pregenerate_tests(self, num): # TODO needs improvement event_loop = asyncio.get_event_loop() self.pregenerated_tests += [ event_loop.run_until_complete(self.testfactory.generate()) diff --git a/perguntations/questions.py b/perguntations/questions.py index e193cc0..6338f35 100644 --- a/perguntations/questions.py +++ b/perguntations/questions.py @@ -13,6 +13,12 @@ import re from typing import Any, Dict, NewType import uuid + +from urllib.error import HTTPError +import json +import http.client + + # this project from perguntations.tools import run_script, run_script_async @@ -584,6 +590,130 @@ class QuestionTextArea(Question): # ============================================================================ +class QuestionCode(Question): + '''An instance of QuestionCode will always have the keys: + type (str) + text (str) + correct (str with script to run) + answer (None or an actual answer) + ''' + + _outcomes = { + 0: 'JOBE outcome: Successful run', + 11: 'JOBE outcome: Compile error', + 12: 'JOBE outcome: Runtime error', + 13: 'JOBE outcome: Time limit exceeded', + 15: 'JOBE outcome: Successful run', + 17: 'JOBE outcome: Memory limit exceeded', + 19: 'JOBE outcome: Illegal system call', + 20: 'JOBE outcome: Internal error, please report', + 21: 'JOBE outcome: Server overload', + } + + # ------------------------------------------------------------------------ + def __init__(self, q: QDict) -> None: + super().__init__(q) + + self.set_defaults(QDict({ + 'text': '', + 'timeout': 5, # seconds + 'server': '127.0.0.1', # JOBE server + 'language': 'c', + 'correct': [{'stdin': '', 'stdout': '', 'stderr': '', 'args': ''}], + })) + + # ------------------------------------------------------------------------ + def correct(self) -> None: + super().correct() + + if self['answer'] is None: + return + + # submit answer to JOBE server + resource = '/jobe/index.php/restapi/runs/' + headers = {"Content-type": "application/json; charset=utf-8", + "Accept": "application/json"} + + for expected in self['correct']: + data_json = json.dumps({ + 'run_spec' : { + 'language_id': self['language'], + 'sourcecode': self['answer'], + 'input': expected.get('stdin', ''), + }, + }) + + try: + connect = http.client.HTTPConnection(self['server']) + connect.request( + method='POST', + url=resource, + body=data_json, + headers=headers + ) + response = connect.getresponse() + logger.debug('JOBE response status %d', response.status) + if response.status != 204: + content = response.read().decode('utf8') + if content: + result = json.loads(content) + connect.close() + + except (HTTPError, ValueError): + logger.error('HTTPError while connecting to JOBE server') + + try: + outcome = result['outcome'] + except (NameError, TypeError, KeyError): + logger.error('Bad result returned from JOBE server: %s', result) + return + logger.debug(self._outcomes[outcome]) + + if result['cmpinfo']: # compiler errors and warnings + self['comments'] = f'Erros de compilação:\n{result["cmpinfo"]}' + self['grade'] = 0.0 + return + + if result['stdout'] != expected['stdout']: + self['comments'] = 'O output gerado é diferente do esperado.' + self['grade'] = 0.0 + return + + self['comments'] = 'Ok!' + self['grade'] = 1.0 + + + # ------------------------------------------------------------------------ + async def correct_async(self) -> None: + self.correct() + + + # out = run_script( + # script=self['correct'], + # args=self['args'], + # stdin=self['answer'], + # timeout=self['timeout'] + # ) + + # if out is None: + # logger.warning('No grade after running "%s".', self["correct"]) + # self['comments'] = 'O programa de correcção abortou...' + # self['grade'] = 0.0 + # elif isinstance(out, dict): + # self['comments'] = out.get('comments', '') + # try: + # self['grade'] = float(out['grade']) + # except ValueError: + # logger.error('Output error in "%s".', self["correct"]) + # except KeyError: + # logger.error('No grade in "%s".', self["correct"]) + # else: + # try: + # self['grade'] = float(out) + # except (TypeError, ValueError): + # logger.error('Invalid grade in "%s".', self["correct"]) + +# ============================================================================ class QuestionInformation(Question): ''' Not really a question, just an information panel. @@ -645,6 +775,7 @@ class QFactory(): 'text-regex': QuestionTextRegex, 'numeric-interval': QuestionNumericInterval, 'textarea': QuestionTextArea, + 'code': QuestionCode, # -- informative panels -- 'information': QuestionInformation, 'success': QuestionInformation, diff --git a/perguntations/serve.py b/perguntations/serve.py index 1f6e521..3870b9c 100644 --- a/perguntations/serve.py +++ b/perguntations/serve.py @@ -227,6 +227,7 @@ class RootHandler(BaseHandler): 'text-regex': 'question-text.html', 'numeric-interval': 'question-text.html', 'textarea': 'question-textarea.html', + 'code': 'question-textarea.html', # -- information panels -- 'information': 'question-information.html', 'success': 'question-information.html', @@ -289,7 +290,7 @@ class RootHandler(BaseHandler): else: ans[i] = ans[i][0] elif question['type'] in ('text', 'text-regex', 'textarea', - 'numeric-interval'): + 'numeric-interval', 'code'): ans[i] = ans[i][0] # correct answered questions and logout @@ -477,6 +478,7 @@ class ReviewHandler(BaseHandler): 'text-regex': 'review-question-text.html', 'numeric-interval': 'review-question-text.html', 'textarea': 'review-question-text.html', + 'code': 'review-question-text.html', # -- information panels -- 'information': 'review-question-information.html', 'success': 'review-question-information.html', diff --git a/perguntations/templates/review-question.html b/perguntations/templates/review-question.html index 802b3a3..305c068 100644 --- a/perguntations/templates/review-question.html +++ b/perguntations/templates/review-question.html @@ -47,7 +47,7 @@ pontos

{{ q['comments'] }}

- {% if q.get('solution', '') %} + {% if q['solution'] %}
{{ md('**Solução:** \n\n' + q['solution']) }} {% end %} @@ -57,8 +57,8 @@ {{ round(q['grade'] * q['points'], 2) }} pontos

-

{{ q['comments'] }}

- {% if q.get('solution', '') %} +

{{ q['comments'] }}

+ {% if q['solution'] %}
{{ md('**Solução:** \n\n' + q['solution']) }} {% end %} @@ -102,7 +102,7 @@ {{ round(q['grade'] * q['points'], 2) }} pontos
{{ q['comments'] }} - {% if q.get('solution', '') %} + {% if q['solution'] %}
{{ md('**Solução:** \n\n' + q['solution']) }} {% end %} -- libgit2 0.21.2