summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPrabhu Ramachandran2017-09-15 16:31:50 +0530
committerGitHub2017-09-15 16:31:50 +0530
commit7419e3b3f4e14f86f21f9464843f9263638fe7a2 (patch)
treea046617f76879ea5c056ae2c8f39b0a97c8a18a7
parente3a43662d2aae8688039671d3de532e48fbdfda9 (diff)
parentf65102cf4b6a117a3ff86971ad9c1ddd3362c9fd (diff)
downloadonline_test-7419e3b3f4e14f86f21f9464843f9263638fe7a2.tar.gz
online_test-7419e3b3f4e14f86f21f9464843f9263638fe7a2.tar.bz2
online_test-7419e3b3f4e14f86f21f9464843f9263638fe7a2.zip
Merge pull request #326 from FOSSEE/improve-code-server
Improve code server
-rw-r--r--.travis.yml3
-rwxr-xr-x[-rw-r--r--]yaksh/code_server.py274
-rw-r--r--yaksh/live_server_tests/load_test.py4
-rw-r--r--yaksh/live_server_tests/selenium_test.py16
-rw-r--r--yaksh/models.py11
-rw-r--r--yaksh/settings.py6
-rw-r--r--yaksh/static/yaksh/css/ontop.css20
-rw-r--r--yaksh/static/yaksh/js/question.js12
-rw-r--r--yaksh/static/yaksh/js/requesthandler.js126
-rw-r--r--yaksh/templates/base.html6
-rw-r--r--yaksh/templates/yaksh/question.html56
-rw-r--r--yaksh/tests/test_code_server.py163
-rw-r--r--yaksh/urls.py1
-rw-r--r--yaksh/views.py95
14 files changed, 519 insertions, 274 deletions
diff --git a/.travis.yml b/.travis.yml
index b2d2a58..153f89b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,5 +26,8 @@ after_success:
- coverage combine
- coverage report
+dist:
+ precise
+
addons:
firefox: "46.0"
diff --git a/yaksh/code_server.py b/yaksh/code_server.py
index 834c5af..75dd9b2 100644..100755
--- a/yaksh/code_server.py
+++ b/yaksh/code_server.py
@@ -1,59 +1,34 @@
#!/usr/bin/env python
-"""This server runs an HTTP server (using tornado) and several code servers
-using XMLRPC that can be submitted code
-and tests and returns the output. It *should* be run as root and will run as
-the user 'nobody' so as to minimize any damange by errant code. This can be
-configured by editing settings.py to run as many servers as desired. One can
-also specify the ports on the command line. Here are examples::
+"""This server runs an HTTP server (using tornado) to which code can be
+submitted for checking. This is asynchronous so once submitted the user can
+check for the result.
- $ sudo ./code_server.py
- # Runs servers based on settings.py:SERVER_PORTS one server per port given.
-
-or::
-
- $ sudo ./code_server.py 8001 8002 8003 8004 8005
- # Runs 5 servers on ports specified.
-
-All these servers should be running as nobody. This will also start a server
-pool that defaults to port 50000 and is configurable in
-settings.py:SERVER_POOL_PORT. This port exposes a `get_server_port` function
-that returns an available server.
+It *should* be run as root and will run as the user 'nobody' so as to minimize
+any damange by errant code. This can be configured by editing settings.py to
+run as many servers as desired.
"""
# Standard library imports
from __future__ import unicode_literals
+from argparse import ArgumentParser
import json
-from multiprocessing import Process, Queue
+from multiprocessing import Process, Queue, Manager
import os
-from os.path import isdir, dirname, abspath, join, isfile
+from os.path import dirname, abspath
import pwd
-import re
-import signal
-import stat
-import subprocess
import sys
-
-try:
- from SimpleXMLRPCServer import SimpleXMLRPCServer
-except ImportError:
- # The above import will not work on Python-3.x.
- from xmlrpc.server import SimpleXMLRPCServer
-
-try:
- from urllib import unquote
-except ImportError:
- # The above import will not work on Python-3.x.
- from urllib.parse import unquote
+import time
# Library imports
+import requests
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler
+from six.moves import urllib
# Local imports
-from .settings import SERVER_PORTS, SERVER_POOL_PORT
-from .language_registry import create_evaluator_instance
+from .settings import N_CODE_SERVERS, SERVER_POOL_PORT
from .grader import Grader
@@ -69,70 +44,45 @@ def run_as_nobody():
os.seteuid(nobody.pw_uid)
-###############################################################################
-# `CodeServer` class.
-###############################################################################
-class CodeServer(object):
- """A code server that executes user submitted test code, tests it and
- reports if the code was correct or not.
+def check_code(pid, job_queue, results):
+ """Check the code, this runs forever.
"""
- def __init__(self, port, queue):
- self.port = port
- self.queue = queue
-
- # Public Protocol ##########
- def check_code(self, language, json_data, in_dir=None):
- """Calls relevant EvaluateCode class based on language to check the
- answer code
- """
+ while True:
+ uid, json_data, user_dir = job_queue.get(True)
+ results[uid] = dict(status='running', pid=pid, result=None)
data = json.loads(json_data)
- grader = Grader(in_dir)
- result = grader.evaluate(data)
-
- # Put us back into the server pool queue since we are free now.
- self.queue.put(self.port)
-
- return json.dumps(result)
-
- def run(self):
- """Run XMLRPC server, serving our methods."""
- server = SimpleXMLRPCServer(("0.0.0.0", self.port))
- self.server = server
- server.register_instance(self)
- self.queue.put(self.port)
- server.serve_forever()
+ grader = Grader(user_dir)
+ result = grader.evaluate(data)
+ results[uid] = dict(status='done', result=json.dumps(result))
###############################################################################
# `ServerPool` class.
###############################################################################
class ServerPool(object):
- """Manages a pool of CodeServer objects."""
- def __init__(self, ports, pool_port=50000):
- """Create a pool of servers. Uses a shared Queue to get available
- servers.
+ """Manages a pool of processes checking code."""
+ def __init__(self, n, pool_port=50000):
+ """Create a pool of servers.
Parameters
----------
- ports : list(int)
- List of ports at which the CodeServer's should run.
+ n : int
+ Number of code servers to run
pool_port : int
Port at which the server pool should serve.
"""
+ self.n = n
+ self.manager = Manager()
+ self.results = self.manager.dict()
self.my_port = pool_port
- self.ports = ports
- queue = Queue(maxsize=len(self.ports))
- self.queue = queue
- servers = []
+
+ self.job_queue = Queue()
processes = []
- for port in self.ports:
- server = CodeServer(port, queue)
- servers.append(server)
- p = Process(target=server.run)
+ for i in range(n):
+ p = self._make_process(i)
processes.append(p)
- self.servers = servers
self.processes = processes
self.app = self._make_app()
@@ -143,28 +93,56 @@ class ServerPool(object):
app.listen(self.my_port)
return app
+ def _make_process(self, pid):
+ return Process(
+ target=check_code, args=(pid, self.job_queue, self.results)
+ )
+
def _start_code_servers(self):
for proc in self.processes:
if proc.pid is None:
proc.start()
- # Public Protocol ##########
+ def _handle_dead_process(self, result):
+ if result.get('status') == 'running':
+ pid = result.get('pid')
+ proc = self.processes[pid]
+ if not proc.is_alive():
+ # If the processes is dead, something bad happened so
+ # restart that process.
+ new_proc = self._make_process(pid)
+ self.processes[pid] = new_proc
+ new_proc.start()
+ result['status'] = 'done'
+ result['result'] = json.dumps(dict(
+ success=False, weight=0.0,
+ error=['Process ended with exit code %s.'
+ % proc.exitcode]
+ ))
- def get_server_port(self):
- """Get available server port from ones in the pool. This will block
- till it gets an available server.
- """
- return self.queue.get()
+ # Public Protocol ##########
def get_status(self):
- """Returns current queue size and total number of ports used."""
- try:
- qs = self.queue.qsize()
- except NotImplementedError:
- # May not work on OS X so we return a dummy.
- qs = len(self.ports)
-
- return qs, len(self.ports)
+ """Returns current job queue size, total number of processes alive.
+ """
+ qs = sum(r['status'] == 'not started'
+ for r in self.results.values())
+ alive = sum(p.is_alive() for p in self.processes)
+ n_running = sum(r['status'] == 'running'
+ for r in self.results.values())
+
+ return qs, alive, n_running
+
+ def submit(self, uid, json_data, user_dir):
+ self.results[uid] = dict(status='not started')
+ self.job_queue.put((uid, json_data, user_dir))
+
+ def get_result(self, uid):
+ result = self.results.get(uid, dict(status='unknown'))
+ self._handle_dead_process(result)
+ if result.get('status') == 'done':
+ self.results.pop(uid)
+ return json.dumps(result)
def run(self):
"""Run server which returns an available server port where code
@@ -189,30 +167,102 @@ class MainHandler(RequestHandler):
def get(self):
path = self.request.path[1:]
if len(path) == 0:
- port = self.server.get_server_port()
- self.write(str(port))
- elif path == "status":
- q_size, total = self.server.get_status()
- result = "%d servers out of %d are free.\n"%(q_size, total)
- load = float(total - q_size)/total*100
- result += "Load: %s%%\n"%load
+ q_size, alive, running = self.server.get_status()
+ result = "%d processes, %d running, %d queued" % (
+ alive, running, q_size
+ )
self.write(result)
+ else:
+ uid = path
+ json_result = self.server.get_result(uid)
+ self.write(json_result)
+
+ def post(self):
+ uid = self.get_argument('uid')
+ json_data = self.get_argument('json_data')
+ user_dir = self.get_argument('user_dir')
+ self.server.submit(uid, json_data, user_dir)
+ self.write('OK')
+
+
+def submit(url, uid, json_data, user_dir):
+ '''Submit a job to the code server.
+
+ Parameters
+ ----------
+
+ url : str
+ URL of the server pool.
+
+ uid : str
+ Unique ID of the submission.
+
+ json_data : jsonized str
+ Data to send to the code checker.
+
+ user_dir : str
+ User directory.
+ '''
+ requests.post(
+ url, data=dict(uid=uid, json_data=json_data, user_dir=user_dir)
+ )
+
+
+def get_result(url, uid, block=False):
+ '''Get the status of a job submitted to the code server.
+
+ Returns the result currently known in the form of a dict. The dictionary
+ contains two keys, 'status' and 'result'. The status can be one of
+ ['running', 'not started', 'done', 'unknown']. The result is the result of
+ the code execution as a jsonized string.
+
+ Parameters
+ ----------
+
+ url : str
+ URL of the server pool.
+
+ uid : str
+ Unique ID of the submission.
+
+ block : bool
+ Set to True if you wish to block till result is done.
+
+ '''
+ def _get_data():
+ r = requests.get(urllib.parse.urljoin(url, str(uid)))
+ return json.loads(r.content.decode('utf-8'))
+ data = _get_data()
+ if block:
+ while data.get('status') != 'done':
+ time.sleep(0.1)
+ data = _get_data()
+
+ return data
###############################################################################
def main(args=None):
- if args:
- ports = [int(x) for x in args]
- else:
- ports = SERVER_PORTS
-
- server_pool = ServerPool(ports=ports, pool_port=SERVER_POOL_PORT)
- # This is done *after* the server pool is created because when the tornado
- # app calls listen(), it cannot be nobody.
+ parser = ArgumentParser(description=__doc__)
+ parser.add_argument(
+ 'n', nargs='?', type=int, default=N_CODE_SERVERS,
+ help="Number of servers to run."
+ )
+ parser.add_argument(
+ '-p', '--port', dest='port', default=SERVER_POOL_PORT,
+ help="Port at which the http server should run."
+ )
+
+ options = parser.parse_args(args)
+
+ # Called before serverpool is created so that the multiprocessing
+ # can work properly.
run_as_nobody()
+ server_pool = ServerPool(n=options.n, pool_port=options.port)
server_pool.run()
+
if __name__ == '__main__':
args = sys.argv[1:]
main(args)
diff --git a/yaksh/live_server_tests/load_test.py b/yaksh/live_server_tests/load_test.py
index 47d267c..5ab1cc2 100644
--- a/yaksh/live_server_tests/load_test.py
+++ b/yaksh/live_server_tests/load_test.py
@@ -26,7 +26,9 @@ class YakshSeleniumTests(StaticLiveServerTestCase):
"yaksh.cpp_code_evaluator.CppCodeEvaluator"
settings.code_evaluators['bash']['standardtestcase'] = \
"yaksh.bash_code_evaluator.BashCodeEvaluator"
- code_server_pool = ServerPool(ports=settings.SERVER_PORTS, pool_port=settings.SERVER_POOL_PORT)
+ code_server_pool = ServerPool(
+ n=settings.N_CODE_SERVERS, pool_port=settings.SERVER_POOL_PORT
+ )
cls.code_server_pool = code_server_pool
cls.code_server_thread = t = Thread(target=code_server_pool.run)
t.start()
diff --git a/yaksh/live_server_tests/selenium_test.py b/yaksh/live_server_tests/selenium_test.py
index bc400fd..31efcac 100644
--- a/yaksh/live_server_tests/selenium_test.py
+++ b/yaksh/live_server_tests/selenium_test.py
@@ -8,6 +8,20 @@ from selenium.common.exceptions import WebDriverException
import multiprocessing
import argparse
+
+class ElementDisplay(object):
+ '''Custom expected condition '''
+ def __init__(self, locator):
+ self.locator = locator
+
+ def __call__(self, driver):
+ try:
+ element = EC._find_element(driver, self.locator)
+ return element.value_of_css_property("display") == "none"
+ except Exception as e:
+ return False
+
+
class SeleniumTestError(Exception):
pass
@@ -53,6 +67,8 @@ class SeleniumTest():
submit_answer_elem = self.driver.find_element_by_id("check")
self.driver.execute_script('global_editor.editor.setValue({});'.format(answer))
submit_answer_elem.click()
+ WebDriverWait(self.driver, 90).until(ElementDisplay(
+ (By.XPATH, "//*[@id='ontop']")))
def test_c_question(self, question_label):
# Incorrect Answer
diff --git a/yaksh/models.py b/yaksh/models.py
index 063e1e3..d9e07fd 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -28,7 +28,7 @@ import tempfile
from textwrap import dedent
from ast import literal_eval
from .file_utils import extract_files, delete_files
-from yaksh.xmlrpc_clients import code_server
+from yaksh.code_server import submit, SERVER_POOL_PORT
from django.conf import settings
from django.forms.models import model_to_dict
@@ -1306,7 +1306,7 @@ class AnswerPaper(models.Model):
if question.type == 'code':
return self.answers.filter(question=question).order_by('-id')
- def validate_answer(self, user_answer, question, json_data=None):
+ def validate_answer(self, user_answer, question, json_data=None, uid=None):
"""
Checks whether the answer submitted by the user is right or wrong.
If right then returns correct = True, success and
@@ -1367,10 +1367,9 @@ class AnswerPaper(models.Model):
elif question.type == 'code' or question.type == "upload":
user_dir = self.user.profile.get_user_dir()
- json_result = code_server.run_code(
- question.language, json_data, user_dir
- )
- result = json.loads(json_result)
+ url = 'http://localhost:%s' % SERVER_POOL_PORT
+ submit(url, uid, json_data, user_dir)
+ result = {'uid': uid, 'status': 'running'}
return result
def regrade(self, question_id):
diff --git a/yaksh/settings.py b/yaksh/settings.py
index 72f9fda..d500d93 100644
--- a/yaksh/settings.py
+++ b/yaksh/settings.py
@@ -1,9 +1,9 @@
"""
settings for yaksh app.
"""
-# The ports the code server should run on. This will run one separate
-# server for each port listed in the following list.
-SERVER_PORTS = [8001] # range(8001, 8026)
+
+# The number of code server processes to run..
+N_CODE_SERVERS = 5
# The server pool port. This is the server which returns available server
# ports so as to minimize load. This is some random number where no other
diff --git a/yaksh/static/yaksh/css/ontop.css b/yaksh/static/yaksh/css/ontop.css
new file mode 100644
index 0000000..fb22066
--- /dev/null
+++ b/yaksh/static/yaksh/css/ontop.css
@@ -0,0 +1,20 @@
+#ontop {
+ position: fixed;
+ display: none;
+ width: 100%;
+ height: 100%;
+ top:0;
+ bottom: 0;
+ left:0;
+ right: 0;
+ background-color: rgba(0,0,0,0.5);
+ z-index: 1001; /* 1001 coz sidebar is 1000. So will be on top of sidebar*/
+}
+
+#state {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ font-size: 30px;
+ color: white;
+}
diff --git a/yaksh/static/yaksh/js/question.js b/yaksh/static/yaksh/js/question.js
deleted file mode 100644
index 96ff3de..0000000
--- a/yaksh/static/yaksh/js/question.js
+++ /dev/null
@@ -1,12 +0,0 @@
-function submitCode()
-{
- document.forms["code"].submit();
- var x = document.getElementById("status");
- x.innerHTML = "<strong>Checking answer ...</strong>";
- x = document.getElementById("check");
- x.disabled = true;
- x.value = "Checking Answer ...";
- if (document.getElementById("skip")!=null) {
- document.getElementById("skip").disabled = true;
- }
-}
diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js
new file mode 100644
index 0000000..9fcf5b6
--- /dev/null
+++ b/yaksh/static/yaksh/js/requesthandler.js
@@ -0,0 +1,126 @@
+request_status = "initial";
+count = 0;
+MAX_COUNT = 14
+
+function reset_values() {
+ request_status = "initial";
+ count = 0;
+}
+function check_state(state, uid) {
+ if ((state == "running" || state == "not started") && count < MAX_COUNT) {
+ count++;
+ setTimeout(function() {get_result(uid);}, 2000);
+ } else if (state == "unknown") {
+ reset_values();
+ notify("Request timeout. Try again later.");
+ unlock_screen();
+ } else {
+ reset_values()
+ notify("Please try again.");
+ unlock_screen();
+ }
+}
+
+function notify(text) {
+ var notice = document.getElementById("notification");
+ notice.classList.add("alert");
+ notice.classList.add("alert-success");
+ notice.innerHTML = text;
+}
+
+function lock_screen() {
+ document.getElementById("ontop").style.display = "block";
+}
+
+function unlock_screen() {
+ document.getElementById("ontop").style.display = "none";
+}
+
+function get_result(uid){
+ var url = "/exam/get_result/" + uid + "/";
+ ajax_check_code(url, "GET", "html", null, uid)
+}
+
+function response_handler(method_type, content_type, data, uid){
+ if(content_type.indexOf("text/html") !== -1) {
+ if( method_type === "POST") {
+ reset_values();
+ }
+ unlock_screen();
+ document.open();
+ document.write(data);
+ document.close();
+ } else if(content_type.indexOf("application/json") !== -1) {
+ res = JSON.parse(data);
+ request_status = res.status;
+ if(method_type === "POST") {
+ uid = res.uid;
+ }
+ check_state(request_status, uid);
+ } else {
+ reset_values();
+ unlock_screen();
+ }
+}
+
+function ajax_check_code(url, method_type, data_type, data, uid) {
+ $.ajax({
+ method: method_type,
+ url: url,
+ data: data,
+ dataType: data_type,
+ success: function(data, status, xhr) {
+ content_type = xhr.getResponseHeader("content-type");
+ response_handler(method_type, content_type, data, uid)
+ },
+ error: function(xhr, text_status, error_thrown ) {
+ reset_values();
+ unlock_screen();
+ notify("There is some problem. Try later.")
+ }
+ });
+
+}
+
+var global_editor = {};
+$(document).ready(function(){
+ // Codemirror object, language modes and initial content
+ // Get the textarea node
+ var textarea_node = document.querySelector('#answer');
+
+ var mode_dict = {
+ 'python': 'python',
+ 'c': 'text/x-csrc',
+ 'cpp': 'text/x-c++src',
+ 'java': 'text/x-java',
+ 'bash': 'text/x-sh',
+ 'scilab': 'text/x-csrc'
+ }
+
+ // Code mirror Options
+ var options = {
+ mode: mode_dict[lang],
+ gutter: true,
+ lineNumbers: true,
+ onChange: function (instance, changes) {
+ render();
+ }
+ };
+
+ // Initialize the codemirror editor
+ global_editor.editor = CodeMirror.fromTextArea(textarea_node, options);
+
+ // Setting code editors initial content
+ global_editor.editor.setValue(init_val);
+
+ function reset_editor() {
+ global_editor.editor.setValue(init_val);
+ global_editor.editor.clearHistory();
+ }
+ $('#code').submit(function(e) {
+ lock_screen();
+ var data = $(this).serializeArray();
+ ajax_check_code($(this).attr("action"), "POST", "html", data, null)
+ e.preventDefault(); // To stop the default form submission.
+ });
+});
diff --git a/yaksh/templates/base.html b/yaksh/templates/base.html
index 35c6976..cbe396f 100644
--- a/yaksh/templates/base.html
+++ b/yaksh/templates/base.html
@@ -19,6 +19,7 @@
<link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/theme.css" type="text/css" />
<link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/sticky-footer.css" type="text/css" />
<link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/dashboard.css" type="text/css" />
+ <link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/ontop.css" type="text/css" />
{% block meta %}
@@ -36,6 +37,11 @@
</head>
<body {% block onload %} {% endblock %}>
+ <div id="ontop">
+ <div id="state">
+ Checking...
+ </div>
+ </div>
{% block nav %}
{% endblock %}
<div class="container">
diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html
index 74dd8c3..3a3066c 100644
--- a/yaksh/templates/yaksh/question.html
+++ b/yaksh/templates/yaksh/question.html
@@ -15,7 +15,7 @@
{% endblock %}
{% block script %}
-<script src="{{ URL_ROOT }}/static/yaksh/js/question.js"></script>
+<script src="{{ URL_ROOT }}/static/yaksh/js/requesthandler.js"></script>
<script src="{{ URL_ROOT }}/static/yaksh/js/codemirror/lib/codemirror.js"></script>
<script src="{{ URL_ROOT }}/static/yaksh/js/codemirror/mode/python/python.js"></script>
<script src="{{ URL_ROOT }}/static/yaksh/js/codemirror/mode/clike/clike.js"></script>
@@ -79,7 +79,7 @@ function validate(){
}
else
{
- return true;
+ send_request();
}
}
@@ -89,47 +89,8 @@ function call_skip(url)
form.action = url
form.submit();
}
-</script>
-<script>
-
- var init_val = '{{ last_attempt|escape_quotes|safe }}';
- var global_editor = {};
- $(document).ready(function(){
- // Codemirror object, language modes and initial content
- // Get the textarea node
- var textarea_node = document.querySelector('#answer');
-
- var lang = "{{ question.language }}"
- var mode_dict = {
- 'python': 'python',
- 'c': 'text/x-csrc',
- 'cpp': 'text/x-c++src',
- 'java': 'text/x-java',
- 'bash': 'text/x-sh',
- 'scilab': 'text/x-csrc'
- }
-
- // Code mirror Options
- var options = {
- mode: mode_dict[lang],
- gutter: true,
- lineNumbers: true,
- onChange: function (instance, changes) {
- render();
- }
- };
-
- // Initialize the codemirror editor
- global_editor.editor = CodeMirror.fromTextArea(textarea_node, options);
-
- // Setting code editors initial content
- global_editor.editor.setValue(init_val);
-
- function reset_editor() {
- global_editor.editor.setValue(init_val);
- global_editor.editor.clearHistory();
- }
- });
+init_val = '{{ last_attempt|escape_quotes|safe }}';
+lang = "{{ question.language }}"
</script>
{% endblock script %}
@@ -140,14 +101,17 @@ function call_skip(url)
<p id="status"></p>
{% if notification %}
{% if question.type == "code" %}
- <div class="alert alert-success" role="alert">
+ <div id="notification" class="alert alert-success" role="alert">
<strong>Note:</strong> {{ notification }}
</div>
{% else %}
- <div class="alert alert-warning" role="alert">
+ <div id="notification" class="alert alert-warning" role="alert">
<strong>Note:</strong> {{ notification }}
</div>
{% endif %}
+ {% else %}
+ <div id="notification" role="alert">
+ </div>
{% endif %}
<form id="code" action="{{URL_ROOT}}/exam/{{ question.id }}/check/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/" method="post" enctype="multipart/form-data">
{% csrf_token %}
@@ -245,7 +209,7 @@ function call_skip(url)
{% else %}
{% if question in paper.get_questions_unanswered %}
- <button class="btn btn-primary" type="submit" name="check" id="check" onClick="submitCode();">Check Answer <span class="glyphicon glyphicon-cog"></span></button>&nbsp;&nbsp;
+ <button class="btn btn-primary" type="submit" name="check" id="check" >Check Answer <span class="glyphicon glyphicon-cog"></span></button>&nbsp;&nbsp;
{% endif %}
{% endif %}
diff --git a/yaksh/tests/test_code_server.py b/yaksh/tests/test_code_server.py
index 47c1da7..5f80f2d 100644
--- a/yaksh/tests/test_code_server.py
+++ b/yaksh/tests/test_code_server.py
@@ -8,9 +8,8 @@ from threading import Thread
import unittest
from six.moves import urllib
-from yaksh.code_server import ServerPool, SERVER_POOL_PORT
+from yaksh.code_server import ServerPool, SERVER_POOL_PORT, submit, get_result
from yaksh import settings
-from yaksh.xmlrpc_clients import CodeServerProxy
class TestCodeServer(unittest.TestCase):
@@ -19,8 +18,7 @@ class TestCodeServer(unittest.TestCase):
def setUpClass(cls):
settings.code_evaluators['python']['standardtestcase'] = \
"yaksh.python_assertion_evaluator.PythonAssertionEvaluator"
- ports = range(8001, 8006)
- server_pool = ServerPool(ports=ports, pool_port=SERVER_POOL_PORT)
+ server_pool = ServerPool(n=5, pool_port=SERVER_POOL_PORT)
cls.server_pool = server_pool
cls.server_thread = t = Thread(target=server_pool.run)
t.start()
@@ -33,70 +31,78 @@ class TestCodeServer(unittest.TestCase):
"python_assertion_evaluator.PythonAssertionEvaluator"
def setUp(self):
- self.code_server = CodeServerProxy()
+ self.url = 'http://localhost:%s' % SERVER_POOL_PORT
def test_infinite_loop(self):
# Given
- testdata = {'metadata': {'user_answer': 'while True: pass',
- 'language': 'python',
- 'partial_grading': False
- },
- 'test_case_data': [{'test_case':'assert 1==2',
- 'test_case_type': 'standardtestcase',
- 'weight': 0.0
- }]
- }
+ testdata = {
+ 'metadata': {
+ 'user_answer': 'while True: pass',
+ 'language': 'python',
+ 'partial_grading': False
+ },
+ 'test_case_data': [
+ {'test_case': 'assert 1==2',
+ 'test_case_type': 'standardtestcase',
+ 'weight': 0.0}
+ ]
+ }
# When
- result = self.code_server.run_code(
- 'python', json.dumps(testdata), ''
- )
+ submit(self.url, '0', json.dumps(testdata), '')
+ result = get_result(self.url, '0')
# Then
- data = json.loads(result)
+ self.assertTrue(result.get('status') in ['running', 'not started'])
+
+ # When
+ result = get_result(self.url, '0', block=True)
+
+ # Then
+ data = json.loads(result.get('result'))
self.assertFalse(data['success'])
self.assertTrue('infinite loop' in data['error'][0])
def test_correct_answer(self):
# Given
- testdata = {'metadata': { 'user_answer': 'def f(): return 1',
- 'language': 'python',
- 'partial_grading': False
- },
- 'test_case_data': [{'test_case':'assert f() == 1',
- 'test_case_type': 'standardtestcase',
- 'weight': 0.0
- }]
- }
+ testdata = {
+ 'metadata': {
+ 'user_answer': 'def f(): return 1',
+ 'language': 'python',
+ 'partial_grading': False
+ },
+ 'test_case_data': [{'test_case': 'assert f() == 1',
+ 'test_case_type': 'standardtestcase',
+ 'weight': 0.0}]
+ }
# When
- result = self.code_server.run_code(
- 'python', json.dumps(testdata), ''
- )
+ submit(self.url, '0', json.dumps(testdata), '')
+ result = get_result(self.url, '0', block=True)
# Then
- data = json.loads(result)
+ data = json.loads(result.get('result'))
self.assertTrue(data['success'])
def test_wrong_answer(self):
# Given
- testdata = {'metadata': { 'user_answer': 'def f(): return 1',
- 'language': 'python',
- 'partial_grading': False
- },
- 'test_case_data': [{'test_case':'assert f() == 2',
- 'test_case_type': 'standardtestcase',
- 'weight': 0.0
- }]
- }
+ testdata = {
+ 'metadata': {
+ 'user_answer': 'def f(): return 1',
+ 'language': 'python',
+ 'partial_grading': False
+ },
+ 'test_case_data': [{'test_case': 'assert f() == 2',
+ 'test_case_type': 'standardtestcase',
+ 'weight': 0.0}]
+ }
# When
- result = self.code_server.run_code(
- 'python', json.dumps(testdata), ''
- )
+ submit(self.url, '0', json.dumps(testdata), '')
+ result = get_result(self.url, '0', block=True)
# Then
- data = json.loads(result)
+ data = json.loads(result.get('result'))
self.assertFalse(data['success'])
self.assertTrue('AssertionError' in data['error'][0])
@@ -104,28 +110,27 @@ class TestCodeServer(unittest.TestCase):
# Given
results = Queue()
- def run_code():
+ def run_code(uid):
"""Run an infinite loop."""
- testdata = {'metadata': { 'user_answer': 'while True: pass',
- 'language': 'python',
- 'partial_grading': False
- },
- 'test_case_data': [{'test_case':'assert 1==2',
- 'test_case_type': 'standardtestcase',
- 'weight': 0.0
- }]
- }
- result = self.code_server.run_code(
- 'python', json.dumps(testdata), ''
- )
- results.put(json.loads(result))
+ testdata = {
+ 'metadata': {
+ 'user_answer': 'while True: pass',
+ 'language': 'python',
+ 'partial_grading': False
+ },
+ 'test_case_data': [{'test_case': 'assert 1==2',
+ 'test_case_type': 'standardtestcase',
+ 'weight': 0.0}]
+ }
+ submit(self.url, uid, json.dumps(testdata), '')
+ result = get_result(self.url, uid, block=True)
+ results.put(json.loads(result.get('result')))
N = 10
# When
- import time
threads = []
for i in range(N):
- t = Thread(target=run_code)
+ t = Thread(target=run_code, args=(str(i),))
threads.append(t)
t.start()
@@ -142,16 +147,48 @@ class TestCodeServer(unittest.TestCase):
def test_server_pool_status(self):
# Given
- url = "http://localhost:%s/status"%SERVER_POOL_PORT
+ url = "http://localhost:%s/" % SERVER_POOL_PORT
# When
response = urllib.request.urlopen(url)
data = response.read().decode('utf-8')
# Then
- expect = 'out of 5 are free'
+ expect = '5 processes, 0 running, 0 queued'
self.assertTrue(expect in data)
- expect = 'Load:'
+
+ def test_killing_process_revives_it(self):
+ # Given
+ testdata = {
+ 'metadata': {
+ 'user_answer': 'import sys; sys.exit()',
+ 'language': 'python',
+ 'partial_grading': False
+ },
+ 'test_case_data': [{'test_case': '',
+ 'test_case_type': 'standardtestcase',
+ 'weight': 0.0}]
+ }
+
+ # When
+ submit(self.url, '0', json.dumps(testdata), '')
+ result = get_result(self.url, '0', block=True)
+
+ # Then
+ data = json.loads(result.get('result'))
+ self.assertFalse(data['success'])
+ self.assertTrue('Process ended with exit code' in data['error'][0])
+
+ # Now check the server status to see if the right number
+ # processes are running.
+ url = "http://localhost:%s/" % SERVER_POOL_PORT
+
+ # When
+ response = urllib.request.urlopen(url)
+ data = response.read().decode('utf-8')
+
+ # Then
+ expect = '5 processes, 0 running, 0 queued'
self.assertTrue(expect in data)
diff --git a/yaksh/urls.py b/yaksh/urls.py
index 4aa3276..c236640 100644
--- a/yaksh/urls.py
+++ b/yaksh/urls.py
@@ -20,6 +20,7 @@ urlpatterns = [
views.complete),
url(r'^register/$', views.user_register, name="register"),
url(r'^(?P<q_id>\d+)/check/$', views.check),
+ url(r'^get_result/(?P<uid>\d+)/$', views.get_result),
url(r'^(?P<q_id>\d+)/check/(?P<attempt_num>\d+)/(?P<questionpaper_id>\d+)/$',\
views.check),
url(r'^(?P<q_id>\d+)/skip/(?P<attempt_num>\d+)/(?P<questionpaper_id>\d+)/$',
diff --git a/yaksh/views.py b/yaksh/views.py
index 1c6feca..3f9f622 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -4,7 +4,7 @@ import os
from datetime import datetime, timedelta
import collections
import csv
-from django.http import HttpResponse
+from django.http import HttpResponse, JsonResponse
from django.core.urlresolvers import reverse
from django.contrib.auth import login, logout, authenticate
from django.shortcuts import render_to_response, get_object_or_404, redirect
@@ -31,6 +31,7 @@ except ImportError:
from io import BytesIO as string_io
import re
# Local imports.
+from yaksh.code_server import get_result as get_result_from_code_server, SERVER_POOL_PORT
from yaksh.models import (
Answer, AnswerPaper, AssignmentUpload, Course, FileUpload, FloatTestCase,
HookTestCase, IntegerTestCase, McqTestCase, Profile,
@@ -44,7 +45,6 @@ from yaksh.forms import (
UploadFileForm, get_object_form, FileForm, QuestionPaperForm
)
from .settings import URL_ROOT
-from yaksh.models import AssignmentUpload
from .file_utils import extract_files
from .send_emails import send_user_mail, generate_activation_key, send_bulk_mail
from .decorators import email_verified, has_profile
@@ -532,7 +532,6 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
current_question = get_object_or_404(Question, pk=q_id)
if request.method == 'POST':
- snippet_code = request.POST.get('snippet')
# Add the answer submitted, regardless of it being correct or not.
if current_question.type == 'mcq':
user_answer = request.POST.get('answer')
@@ -592,8 +591,7 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
next_q = paper.add_completed_question(current_question.id)
return show_question(request, next_q, paper)
else:
- user_code = request.POST.get('answer')
- user_answer = snippet_code + "\n" + user_code if snippet_code else user_code
+ user_answer = request.POST.get('answer')
if not user_answer:
msg = ["Please submit a valid option or code"]
return show_question(
@@ -604,6 +602,7 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
correct=False, error=json.dumps([])
)
new_answer.save()
+ uid = new_answer.id
paper.answers.add(new_answer)
# If we were not skipped, we were asked to check. For any non-mcq
# questions, we obtain the results via XML-RPC with the code executed
@@ -612,37 +611,71 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
if current_question.type == 'code' or \
current_question.type == 'upload' else None
result = paper.validate_answer(
- user_answer, current_question, json_data
+ user_answer, current_question, json_data, uid
)
- if result.get('success'):
- new_answer.marks = (current_question.points * result['weight'] /
- current_question.get_maximum_test_case_weight()) \
- if current_question.partial_grading and \
- current_question.type == 'code' or current_question.type == 'upload' \
- else current_question.points
- new_answer.correct = result.get('success')
- error_message = None
- new_answer.error = json.dumps(result.get('error'))
- next_question = paper.add_completed_question(current_question.id)
+ if current_question.type in ['code', 'upload']:
+ if paper.time_left() <= 0:
+ url = 'http://localhost:%s' % SERVER_POOL_PORT
+ result_details = get_result_from_code_server(url, uid, block=True)
+ result = json.loads(result_details.get('result'))
+ next_question, error_message, paper = _update_paper(request, uid,
+ result)
+ return show_question(request, next_question, paper, error_message)
+ else:
+ return JsonResponse(result)
else:
- new_answer.marks = (current_question.points * result['weight'] /
- current_question.get_maximum_test_case_weight()) \
- if current_question.partial_grading and \
- current_question.type == 'code' or current_question.type == 'upload' \
- else 0
- error_message = result.get('error') if current_question.type == 'code' \
- or current_question.type == 'upload' else None
- new_answer.error = json.dumps(result.get('error'))
- next_question = current_question if current_question.type == 'code' \
- or current_question.type == 'upload' \
- else paper.add_completed_question(current_question.id)
- new_answer.save()
- paper.update_marks('inprogress')
- paper.set_end_time(timezone.now())
- return show_question(request, next_question, paper, error_message)
+ next_question, error_message, paper = _update_paper(request, uid, result)
+ return show_question(request, next_question, paper, error_message)
else:
return show_question(request, current_question, paper)
+
+@csrf_exempt
+def get_result(request, uid):
+ result = {}
+ url = 'http://localhost:%s' % SERVER_POOL_PORT
+ result_state = get_result_from_code_server(url, uid)
+ result['status'] = result_state.get('status')
+ if result['status'] == 'done':
+ result = json.loads(result_state.get('result'))
+ next_question, error_message, paper = _update_paper(request, uid, result)
+ return show_question(request, next_question, paper, error_message)
+ return JsonResponse(result)
+
+
+def _update_paper(request, uid, result):
+ new_answer = Answer.objects.get(id=uid)
+ current_question = new_answer.question
+ paper = new_answer.answerpaper_set.first()
+
+ if result.get('success'):
+ new_answer.marks = (current_question.points * result['weight'] /
+ current_question.get_maximum_test_case_weight()) \
+ if current_question.partial_grading and \
+ current_question.type == 'code' or current_question.type == 'upload' \
+ else current_question.points
+ new_answer.correct = result.get('success')
+ error_message = None
+ new_answer.error = json.dumps(result.get('error'))
+ next_question = paper.add_completed_question(current_question.id)
+ else:
+ new_answer.marks = (current_question.points * result['weight'] /
+ current_question.get_maximum_test_case_weight()) \
+ if current_question.partial_grading and \
+ current_question.type == 'code' or current_question.type == 'upload' \
+ else 0
+ error_message = result.get('error') if current_question.type == 'code' \
+ or current_question.type == 'upload' else None
+ new_answer.error = json.dumps(result.get('error'))
+ next_question = current_question if current_question.type == 'code' \
+ or current_question.type == 'upload' \
+ else paper.add_completed_question(current_question.id)
+ new_answer.save()
+ paper.update_marks('inprogress')
+ paper.set_end_time(timezone.now())
+ return next_question, error_message, paper
+
+
@login_required
@email_verified
def quit(request, reason=None, attempt_num=None, questionpaper_id=None):