Commit 13e8ae65 authored by w-mj's avatar w-mj

Merge branch 'dev' into 'master'

Dev

See merge request !1
parents d872e23d 555af4b3
Pipeline #140 canceled with stages
......@@ -9,4 +9,8 @@ temp
__pycache__
.idea
\ No newline at end of file
.idea
pygrading/static/copy_files.txt
test/cg-kernel
test/env
\ No newline at end of file
......@@ -270,7 +270,7 @@ class Statement(JsonSerializable):
ret['context']['language'] = self.context_language
if self.context_statement_id:
ret['context']['statement'] = {
'id': self.context_statement_id, 'objectType': 'StatementRef'}
'id': str(self.context_statement_id), 'objectType': 'StatementRef'}
ret['context']['contextActivities'] = OrderedDict()
if self._context_ca:
......
......@@ -12,5 +12,5 @@ from pygrading.configuration import load_config
from pygrading.testcase import create_testcase, create_std_testcase, TestCases
__all__ = ["Job", "load_config", "create_testcase", "create_std_testcase", "TestCases",
"exec", "loge", "render_template"]
__version__ = "v1.1.7"
"exec", "loge", "render_template", "__main__"]
__version__ = "1.1.8.dev0"
......@@ -8,7 +8,7 @@
import fire
import pygrading as gg
from pygrading.init_project import init
from pygrading.project_utils import init
def version():
......
......@@ -6,6 +6,7 @@
"""
import json
import os
from typing import Dict
......@@ -31,3 +32,37 @@ def load_config(source: str) -> Dict:
"""
f = open(source, encoding='utf-8')
return json.load(f)
class Config(dict):
SUBMIT = "submit_dir"
TESTCASE = "testcase_dir"
def __init__(self, config=None):
super().__init__()
self[self.SUBMIT] = "/coursegrader/submit"
self[self.TESTCASE] = "/coursegrader/testdata"
self["debug"] = False
if config:
self.update(config)
def load(self, config_src: str = None):
if os.environ.get("CONFIG_SRC"):
self.update(load_config(os.environ.get("CONFIG_SRC")))
if not config_src:
config_src = os.path.join(self[self.TESTCASE], "config.json")
if os.path.exists(config_src):
self.update(load_config(config_src))
@property
def is_debug(self):
return self['debug']
@property
def testcase_dir(self):
return self[self.TESTCASE]
@property
def submit_dir(self):
return self[self.SUBMIT]
"""
Name: init_project.py
Author: Charles Zhang <694556046@qq.com>
Propose: Initialize a pygrading project
Coding: UTF-8
"""
import os
import pkgutil
from pygrading.utils import makedirs, writefile
def copy_file(pkg_path, target_path, name):
data = pkgutil.get_data(__package__, pkg_path).decode()
writefile(os.path.join(target_path, name), str(data))
print(f"copy {pkg_path} done.")
def init(path=os.getcwd(), name="cg-kernel"):
""" Initialize a pygrading project """
print("Create project folder", end="")
new_project_path = os.path.join(path, name)
new_project_kernel_path = os.path.join(new_project_path, "kernel")
new_project_template_path = os.path.join(new_project_kernel_path, "templates")
html_template_path = os.path.join(new_project_template_path, "html")
makedirs(new_project_template_path, exist_ok=True)
makedirs(html_template_path, exist_ok=True)
print("Create project folder success")
print("Copy files start ===")
copy_file("static/README.md", new_project_path, "README.md")
copy_file("static/Makefile", new_project_path, "Makefile")
copy_file("static/.gitignore", new_project_path, ".gitignore")
copy_file("static/kernel/__init__.py", new_project_kernel_path, "__init__.py")
copy_file("static/kernel/__main__.py", new_project_kernel_path, "__main__.py")
copy_file("static/kernel/prework.py", new_project_kernel_path, "prework.py")
copy_file("static/kernel/run.py", new_project_kernel_path, "run.py")
copy_file("static/kernel/postwork.py", new_project_kernel_path, "postwork.py")
copy_file("static/kernel/templates/__init__.py", new_project_template_path, "__init__.py")
copy_file("static/kernel/templates/html/base.jinja", html_template_path, "base.jinja")
copy_file("static/kernel/templates/html/index.html", html_template_path, "index.html")
print("Copy files success")
print("Initialization Completed!")
......@@ -20,7 +20,7 @@ class Job(JobBase):
@learning_tracker_grading
def start(self, max_workers: int = 1):
""" start a job """
self.get_config().load()
# run prework
if self.prework:
prework_inspect = inspect.getfullargspec(self.prework)
......
......@@ -7,7 +7,9 @@
import json
import types
from typing import Union
from pygrading.configuration import Config
from pygrading.testcase import TestCases
from pygrading.exception import FunctionsTypeError, FieldMissingError, DataTypeError
......@@ -29,7 +31,7 @@ class JobBase(object):
self.set_postwork(postwork)
self.__testcases = testcases
self.__config = config
self.__config = Config(config)
self.is_terminate = False
self.__verdict = "Unknown"
......@@ -114,7 +116,7 @@ class JobBase(object):
return self.__summary
def set_config(self, config):
self.__config = config
self.__config.update(config)
def get_config(self):
return self.__config
......
"""
Name: project_utils.py
Author: Charles Zhang <694556046@qq.com>
Propose: Initialize a pygrading project
Coding: UTF-8
"""
import json
import os
import pkgutil
import subprocess
import time
from datetime import datetime
import fire
import pytz
from pygrading.utils import makedirs
from jinja2 import Template
import pygrading as gg
import shutil
import zipapp
def render_file(pkg_path, target_path, name, **kwargs):
data = pkgutil.get_data(__package__, pkg_path).decode()
template = Template(data)
with open(os.path.join(target_path, name), "w", encoding='utf-8') as f:
f.write(template.render(**kwargs))
print(f"render {pkg_path} done.")
def copy_file(pkg_path, target_path, name):
data = pkgutil.get_data(__package__, pkg_path)
with open(os.path.join(target_path, name), 'wb') as f:
f.write(data)
# writefile(os.path.join(target_path, name), data)
print(f"copy {pkg_path} done.")
def init(name="cg-kernel"):
path = os.getcwd()
""" Initialize a pygrading project """
print("Create project folder", end="")
new_project_path = os.path.join(path, name)
if os.path.exists(new_project_path) and len(os.listdir(new_project_path)) > 0:
raise RuntimeError(f"{new_project_path} is already exists.")
render_file_list = ['README.md', 'manage.py', 'docker/Dockerfile']
copy_file_list = pkgutil.get_data(__package__, 'static/copy_files.txt').decode().replace('\r', '').split('\n')
render_data = {
'proj_name': name,
'version': gg.__version__
}
for file in copy_file_list:
if not file:
continue
ori_path = os.path.join('static', file)
folder, file_name = os.path.split(file)
folder = os.path.join(new_project_path, folder)
makedirs(folder, exist_ok=True)
if file in render_file_list:
render_file(ori_path, folder, file_name, **render_data)
else:
copy_file(ori_path, folder, file_name)
print("Initialization Completed!")
project_name = None
project_version = None
def package(backup=True):
if os.path.exists("kernel.pyz"):
os.remove("kernel.pyz")
zipapp.create_archive(os.path.join(os.getcwd(), "kernel"), "kernel.pyz")
if backup:
now = datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d-%H-%M")
os.makedirs("backup-kernels", exist_ok=True)
shutil.copyfile("kernel.pyz", f"backup-kernels/kernel-{now}.pyz")
def build_docker():
os.system(f"cd docker && docker build --force-rm -t {project_name}:{project_version} .")
def save_docker():
for cmd in ["pigz", "zgip"]:
if shutil.which(cmd):
tar_name = f"{project_name}_{project_version.replace('.', '-')}.tar.gz"
run = f"docker save {project_name}:{project_version} | {cmd} > {tar_name}"
print(run)
os.system(run)
return tar_name
raise RuntimeError("No pigz or zgip")
def write_file(path, content):
with open(path, "w", encoding="utf-8") as f:
f.write(content)
def test():
meta = json.load(open(os.path.join(os.getcwd(), "test.json")))
testdata_dir = meta["testdata_dir"]
submit_dir = meta["submit_dir"]
kernel_dir = meta["kernel_dir"]
kernel = f"{kernel_dir}/kernel.pyz"
local_kernel = os.path.join(os.getcwd(), "kernel.pyz")
entrypoint = meta["entrypoint"]
testcases = meta["testcases"]
test_dir = os.path.join(os.getcwd(), "test")
result_root = os.path.join(os.getcwd(), "test_result")
shutil.rmtree(result_root, ignore_errors=True)
all_cnt, pass_cnt = 0, 0
all_result = {}
for case in testcases:
name = case["name"]
if 'submits' in case:
submits = case['submits']
for item in submits:
item['path'] = os.path.join(test_dir, name, "submits", item['name'])
else:
if 'submit' in case:
submits = [case['submit']]
else:
submits = [{k: v for k, v in case.items() if k != 'name'}]
submits[0]['name'] = 'submit'
submits[0]['path'] = os.path.join(test_dir, name, 'submit')
# 开始测试
local_testdata_dir = os.path.join(test_dir, name, "testdata")
timeout = case.get("timeout")
for submit in submits:
print(f"test:{name} submit:{submit['name']} ... ", end='')
local_submit_dir = submit['path']
if not os.path.exists(local_submit_dir):
raise RuntimeError(f"no submit dir {local_submit_dir}")
begin = time.time()
cmd = ' '.join(["docker", "run", "--rm",
"--mount", f'type=bind,source="{local_testdata_dir}",target="{testdata_dir}",readonly',
"--mount", f'type=bind,source="{local_submit_dir}",target="{submit_dir}"',
"--mount", f'type=bind,source="{local_kernel}",target="{kernel}"',
"--entrypoint", entrypoint[0], f"{project_name}:{project_version}",
' '.join(entrypoint[1:])])
# print(cmd)
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf8")
out, err = process.communicate(timeout=timeout)
end = time.time()
exec_time = int((end - begin) * 1000)
result_dir = os.path.join(result_root, name, submit['name'])
shutil.rmtree(result_dir, ignore_errors=True)
os.makedirs(result_dir)
write_file(os.path.join(result_dir, "stdout.txt"), out)
write_file(os.path.join(result_dir, "stderr.txt"), err)
summary = {"exec_time": exec_time, 'cmd': cmd}
# print(summary['cmd'])
try:
result = json.loads(out)
assert_result = [
{'what': k, 'exp': v, 'act': result.get(k), 'res': v == result.get(k)}
for k, v in submit.items() if k not in ('name', 'path')]
summary['asserts'] = assert_result
write_file(os.path.join(result_dir, "comment.html"), result['comment'])
write_file(os.path.join(result_dir, "detail.html"), result.get("detail", "None"))
except Exception as e:
summary['error'] = repr(e)
all_result[name + "--" + submit['name']] = summary
with open(os.path.join(result_dir, "summary.json"), "w") as f:
json.dump(summary, f)
print(f"exec time: {exec_time}, error: {summary.get('error')}")
final_result = False
if 'asserts' in summary:
print("what|expect|actual|result")
for item in summary['asserts']:
print(f"{item['what']}|{item['exp']}|{item['act']}|{item['res']}")
final_result = all([x['res'] for x in summary['asserts']])
print(f"========== SUCCESS:{final_result}")
all_cnt += 1
if final_result:
pass_cnt += 1
with open(os.path.join(result_root, 'summary.json'), 'w', encoding='utf-8') as f:
json.dump(all_result, f)
print(f"{pass_cnt}/{all_cnt} TESTS PASSED!")
return pass_cnt == all_cnt
def main(proj_name, proj_version, require_version):
assert (require_version == gg.__version__)
global project_name, project_version
project_name = proj_name
project_version = proj_version
fire.Fire({
"info": f"{project_name}:{project_version}",
"package": package,
"docker-build": build_docker,
"docker-save": save_docker,
"test": test
})
......@@ -71,7 +71,7 @@ def resource(job: Job, resource_type: str, timeout: Optional[int]):
break
timeout -= 1
sleep(1)
if log_id:
if not log_id:
raise TimeoutError(f"waiting resource {resource_type} timeout.")
usage = requests.get(f'http://{manager_addr}/get-usage', params={"logId": log_id}).json()
ssh_usage = [x for x in usage if x['protocol'] == "ssh"]
......
......@@ -2,3 +2,6 @@
result/
*.tar.gz
*.zip
*.pyz
test_result/
backup-kernels/
\ No newline at end of file
old_dir = old-kernel
build:
rm -f kernel.zip
mkdir -p ${old_dir}
cd kernel && zip -0 -r ../kernel.zip *
cp kernel.zip ${old_dir}/kernel-`date +%Y-%m-%d-%H-%M-%S`.zip
# {{proj_name}}
version: 0.1
## 构建Docker镜像
```shell
python manage.py docker-build
```
## 导出Docker镜像
```shell
python manage.py docker-save
```
## 打包内核
```shell
python manage.py package
```
## 测试
```shell
python manage.py test
```
FROM ubuntu:20.04
LABEL maintainer="your-name <your-email>"
USER root
RUN sed -i s@/archive.ubuntu.com/@/mirrors.tuna.tsinghua.edu.cn/@g /etc/apt/sources.list && apt update
RUN apt install -y --no-install-recommends python3 python3-pip gcc libc-dev
RUN pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
RUN rm -rf /var/lib/apt/lists/*
{% if "dev" in version %}
COPY pygrading-{{version}}-py3-none-any.whl /pygrading-{{version}}-py3-none-any.whl
RUN pip3 install /pygrading-{{version}}-py3-none-any.whl
{% else %}
RUN pip3 install pygrading=={{version}}
{% endif %}
RUN groupadd -r cguser && useradd --no-log-init -m -r -g cguser cguser
USER cguser
ENV PATH=$PATH:/usr/bin
ENTRYPOINT ["python3", "/coursegrader/dockerext/kernel.pyz"]
\ No newline at end of file
......@@ -22,7 +22,8 @@ def postwork(job: Job) -> dict:
job.verdict("Accepted" if total_score == 100 else "Wrong Answer")
job.score(total_score)
job.comment("Mission Complete")
summary = job.get_summary()
# 渲染 HTML 模板页面(可选)
# 被渲染的页面应位于 kernel/templates/html 目录下
job.detail(gg.render_template("index.html", author="Charles Zhang"))
job.detail(gg.render_template("index.html", author="Charles Zhang", summary=summary))
import pygrading as gg
from pygrading import Job
from pygrading.verdict import Verdict
from pygrading.html import str2html
import re
import os
import sys
def prework(job: Job):
......@@ -15,13 +19,45 @@ def prework(job: Job):
返回值为空,预处理内容需要更新到job对象中
"""
# 创建测试用例和配置信息
config = {"debug": False}
# 检查测试样例
testcase_dir = job.get_config().testcase_dir
input_pattern = re.compile(r"input(\d+)\.txt")
inputs_num = [input_pattern.findall(x) for x in os.listdir(testcase_dir)]
inputs_num = [x[0] for x in inputs_num if len(x) > 0]
inputs_src = [os.path.join(testcase_dir, f"input{x}.txt") for x in inputs_num]
outputs_src = [os.path.join(testcase_dir, f"output{x}.txt") for x in inputs_num]
check_outputs = [x for x in outputs_src if not os.path.exists(x)]
if check_outputs:
job.verdict(Verdict.UnknownError)
job.comment(f"测试样例<br/>{'<br/>'.join(check_outputs)}<br/>不存在,请联系管理员。")
job.is_terminate = True
return
# 创建测试用例和配置信息
testcases = gg.create_testcase(100)
testcases.append(name="TestCase1", score=50, input_src="", output_src="")
testcases.append(name="TestCase2", score=50, input_src="", output_src="")
for i, v in enumerate(zip(inputs_src, outputs_src)):
input_src, output_src = v
testcases.append(name=f"TestCase{i + 1}", score=100 / len(inputs_src), input_src=input_src, output_src=output_src)
# 更新测试用例和配置信息到任务
job.set_config(config)
job.set_testcases(testcases)
# 编译提交程序
submit_dir = job.get_config().submit_dir
sources = [os.path.join(submit_dir, x) for x in os.listdir(submit_dir) if x[-2:] == '.c']
if not sources:
job.verdict(Verdict.CompileError)
job.comment("未找到有效的提交文件")
job.is_terminate = True
return
run_file = os.path.join(submit_dir, "run")
compile_result = gg.exec(f"gcc {' '.join(sources)} -o {run_file}")
if compile_result.returncode != 0 or not os.path.exists(run_file):
job.verdict(Verdict.CompileError)
job.comment(str2html(compile_result.stderr))
job.is_terminate = True
return
job.get_config()["run_file"] = run_file
import pygrading as gg
from pygrading import Job, TestCases
from pygrading.verdict import Verdict
def run(job: Job, case: TestCases.SingleTestCase) -> dict:
......@@ -17,12 +18,16 @@ def run(job: Job, case: TestCases.SingleTestCase) -> dict:
"""
# 创建一个结果对象
result = {"name": case.name, "score": 0}
result = {"name": case.name, "score": 0, "verdict": Verdict.WrongAnswer}
run_file = job.get_config()['run_file']
input_str = open(case.input_src).read()
# 执行评测任务命令
exec_result = gg.exec("echo success")
exec_result = gg.exec(run_file, input_str=input_str)
output_str = open(case.output_src).read()
if gg.utils.compare_str(exec_result.stdout, "success"):
if gg.utils.compare_str(exec_result.stdout, output_str):
result["score"] = case.score
result["verdict"] = Verdict.Accept
return result
......@@ -14,4 +14,23 @@
{{ author }}
</div>
</div>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Verdict</th>
<th scope="col">Score</th>
</tr>
</thead>
<tbody>
{% for item in summary %}
<tr>
<th scope="row">{{item.name}}</th>
<td>{{item.verdict}}</td>
<td>{{item.score}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
\ No newline at end of file
import pygrading.project_utils as project
import os
pygrading_version = "{{version}}"
project_name = "{{proj_name}}"
project_version = "0.1"
project_path = os.path.dirname(os.path.abspath(__file__))
if __name__ == '__main__':
os.chdir(project_path)
project.main(project_name, project_version, pygrading_version)
{
"submit_dir": "/coursegrader/submit",
"testdata_dir": "/coursegrader/testdata",
"kernel_dir": "/coursegrader/dockerext",
"entrypoint": ["python3", "/coursegrader/dockerext/kernel.pyz"],
"testcases":[
{
"name": "hello world",
"submit": {
"score":"100",
"verdict": "Accepted"
}
},
{
"name": "a+b",
"score": "75"
},
{
"name": "gcd",
"submits": [{
"name": "wrong",
"verdict": "Compile Error",
"score": "0"
}, {
"name": "correct",
"verdict": "Accepted",
"score": "100"
}
]
}
]
}
#include <stdio.h>
int main(void) {
int a, b;
while (~scanf("%d%d", &a, &b)) {
printf("%d\n", a + b);
}
return 0;
}
\ No newline at end of file
100 100
\ No newline at end of file
65535 65536
\ No newline at end of file
-1 -2
\ No newline at end of file
17179869184 17179869184
\ No newline at end of file
131071
\ No newline at end of file
34359738368
\ No newline at end of file
#include<stdio.h>
int main()
{
int x = 0;
int y = 0;
while (~scanf("%d%d", &x, &y)) {