From 2cdc275eda146e28e0b858f820ec436fcfb28896 Mon Sep 17 00:00:00 2001 From: NoCLin Date: Thu, 3 Oct 2019 10:55:10 +0800 Subject: [PATCH 01/18] update workflow --- .github/workflows/pythonpackage.yml | 6 ++--- lj/commands/__init__.py | 0 lj/utils.py | 2 +- lj/vendors/__init__.py | 0 setup.py | 6 +++++ tests/lj_cli_test.py | 42 ++++++++++++++--------------- 6 files changed, 31 insertions(+), 25 deletions(-) create mode 100644 lj/commands/__init__.py create mode 100644 lj/vendors/__init__.py diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c0517fd..887a68a 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -9,7 +9,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [2.7, 3.5, 3.6, 3.7] + python-version: [3.5, 3.6, 3.7] steps: - uses: actions/checkout@v1 @@ -21,8 +21,8 @@ jobs: run: | python -m pip install --upgrade pip python setup.py install - apt update - apt install gcc + sudo apt-get update + sudo apt-get install gcc - name: Lint with flake8 run: | pip install flake8 diff --git a/lj/commands/__init__.py b/lj/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lj/utils.py b/lj/utils.py index 6e1b4b4..5aaacbe 100644 --- a/lj/utils.py +++ b/lj/utils.py @@ -55,7 +55,7 @@ def load_options(): else: logger.debug("config file not found!") - default_config_file = str(Path(__file__).parent / ".localjudge.json") + default_config_file = str(Path(__file__).parent / "default.localjudge.json") default_options = json.loads(read_file(default_config_file, "r")) with open(str(file.resolve()), "w") as f: json.dump(default_options, f, indent=4, ensure_ascii=False) diff --git a/lj/vendors/__init__.py b/lj/vendors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index 493d7c5..ea728c4 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,12 @@ ], packages=find_packages(), zip_safe=False, + classifiers=[ + 'Programming Language :: Python :: 3', + ], + package_data={ + 'lj': ['*.json'], + }, entry_points={ 'console_scripts': [ 'lj = lj.lj:main', diff --git a/tests/lj_cli_test.py b/tests/lj_cli_test.py index 0b81b78..83c60b8 100644 --- a/tests/lj_cli_test.py +++ b/tests/lj_cli_test.py @@ -26,14 +26,14 @@ def getstatusoutput(cmd): class LocalJudgeCLITest(unittest.TestCase): def check_poj_1000(self, code, data): - self.assertEqual(0, code) + self.assertEqual(0, code, data) lines = data.splitlines() - self.assertNotEqual(-1, lines[3].find(JudgeStatus.AC)) - self.assertNotEqual(-1, lines[4].find(JudgeStatus.AC)) - self.assertNotEqual(-1, lines[5].find(JudgeStatus.AC)) - self.assertNotEqual(-1, lines[6].find(JudgeStatus.AC)) - self.assertNotEqual(-1, lines[7].find(JudgeStatus.AC)) - self.assertNotEqual(-1, lines[8].find(JudgeStatus.WA)) + self.assertNotEqual(-1, lines[3].find(JudgeStatus.AC), data) + self.assertNotEqual(-1, lines[4].find(JudgeStatus.AC), data) + self.assertNotEqual(-1, lines[5].find(JudgeStatus.AC), data) + self.assertNotEqual(-1, lines[6].find(JudgeStatus.AC), data) + self.assertNotEqual(-1, lines[7].find(JudgeStatus.AC), data) + self.assertNotEqual(-1, lines[8].find(JudgeStatus.WA), data) def test_lj_c(self): code, data = getstatusoutput(["lj", "poj-1000.c"]) @@ -51,23 +51,23 @@ def test_lj_json(self): code, data = getstatusoutput(["lj", "poj-1000.c", "--json"]) obj = json.loads(data) - self.assertEqual(0, obj["compile"]["code"]) - self.assertEqual(JudgeStatus.AC, obj["cases"][0]["status"]) - self.assertEqual(JudgeStatus.AC, obj["cases"][1]["status"]) - self.assertEqual(JudgeStatus.AC, obj["cases"][2]["status"]) - self.assertEqual(JudgeStatus.AC, obj["cases"][3]["status"]) - self.assertEqual(JudgeStatus.AC, obj["cases"][4]["status"]) - self.assertEqual(JudgeStatus.WA, obj["cases"][5]["status"]) + self.assertEqual(0, obj["compile"]["code"], obj) + + self.assertEqual(JudgeStatus.AC, obj["cases"][0]["status"], obj) + self.assertEqual(JudgeStatus.AC, obj["cases"][1]["status"], obj) + self.assertEqual(JudgeStatus.AC, obj["cases"][2]["status"], obj) + self.assertEqual(JudgeStatus.AC, obj["cases"][3]["status"], obj) + self.assertEqual(JudgeStatus.AC, obj["cases"][4]["status"], obj) + self.assertEqual(JudgeStatus.WA, obj["cases"][5]["status"], obj) def test_lj_run(self): p = Popen(["lj", "run", "poj-1000.c"], - stdin=PIPE, stdout=PIPE, cwd=PROB_DIR) + stdin=PIPE, stdout=PIPE, cwd=str(PROB_DIR)) stdout, _ = p.communicate("1 2".encode()) - print(stdout) - self.assertNotEqual(-1, stdout.find(b"Process Exit Code: 0")) - self.assertNotEqual(-1, stdout.find(b"3\n")) + self.assertNotEqual(-1, stdout.find(b"Process Exit Code: 0"), stdout) + self.assertNotEqual(-1, stdout.find(b"3\n"), stdout) - p = Popen(["ljr", "poj-1000.c"], stdin=PIPE, stdout=PIPE, cwd=PROB_DIR) + p = Popen(["ljr", "poj-1000.c"], stdin=PIPE, stdout=PIPE, cwd=str(PROB_DIR)) stdout, _ = p.communicate("1 2".encode()) - self.assertNotEqual(-1, stdout.find(b"Process Exit Code: 0")) - self.assertNotEqual(-1, stdout.find(b"3\n")) + self.assertNotEqual(-1, stdout.find(b"Process Exit Code: 0"), stdout) + self.assertNotEqual(-1, stdout.find(b"3\n"), stdout) From 5a9f7aee7fc582315d4eab672555192bf14a3a2d Mon Sep 17 00:00:00 2001 From: NoCLin Date: Sat, 5 Oct 2019 19:34:07 +0800 Subject: [PATCH 02/18] fix FileNotFoundError --- lj/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lj/utils.py b/lj/utils.py index 5aaacbe..67ba941 100644 --- a/lj/utils.py +++ b/lj/utils.py @@ -1,4 +1,5 @@ # -*-coding:utf-8-*- +import os import sys import datetime import json @@ -57,7 +58,8 @@ def load_options(): logger.debug("config file not found!") default_config_file = str(Path(__file__).parent / "default.localjudge.json") default_options = json.loads(read_file(default_config_file, "r")) - with open(str(file.resolve()), "w") as f: + # path.resolve() will raise FileNotFoundError on Py 3.5.7 if file doesn't exist. + with open(os.path.abspath(str(file)), "w") as f: json.dump(default_options, f, indent=4, ensure_ascii=False) return default_options From 898a176123b078f0bc1f0aebfe7a0a760646c430 Mon Sep 17 00:00:00 2001 From: NoCLin Date: Sat, 5 Oct 2019 19:46:41 +0800 Subject: [PATCH 03/18] fix FileNotFoundError --- lj/judger.py | 7 ++++--- tests/judge_status_test.py | 2 +- tests/lj_cli_test.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lj/judger.py b/lj/judger.py index 3d46d27..96fd444 100644 --- a/lj/judger.py +++ b/lj/judger.py @@ -1,4 +1,5 @@ import logging +import os import platform import subprocess from pathlib import Path @@ -89,8 +90,8 @@ def do_compile(src) -> (int, str): "dest": None, "exe_if_win": ".exe" if platform.system() == "Windows" else "" } - - params["dest"] = str((temp_dir / Template(dest_template).substitute(params)).resolve()) + # 此时文件还不存在 + params["dest"] = os.path.abspath(str(temp_dir / Template(dest_template).substitute(params))) if compile_cmd_template: compile_cmd = Template(compile_cmd_template).substitute(params) @@ -102,7 +103,7 @@ def do_compile(src) -> (int, str): compile_result.stdout = stdout compile_result.code = code - compile_result.temp_dir = str(temp_dir.absolute()) + compile_result.temp_dir = str(temp_dir.resolve()) compile_result.runnable = Template(run_cmd_template).substitute(params) compile_result.params = params diff --git a/tests/judge_status_test.py b/tests/judge_status_test.py index 690561e..4a32272 100644 --- a/tests/judge_status_test.py +++ b/tests/judge_status_test.py @@ -5,7 +5,7 @@ import shutil from pathlib import Path -CASE_DIR = (Path(__file__).parent / "cases").resolve().absolute() +CASE_DIR = (Path(__file__).parent / "cases").resolve() class LocalJudgeStatusTest(unittest.TestCase): # 继承unittest.TestCase diff --git a/tests/lj_cli_test.py b/tests/lj_cli_test.py index 83c60b8..26ddfd6 100644 --- a/tests/lj_cli_test.py +++ b/tests/lj_cli_test.py @@ -5,7 +5,7 @@ from lj.judger import JudgeStatus -PROB_DIR = (Path(__file__).parent / "problems").resolve().absolute() +PROB_DIR = (Path(__file__).parent / "problems").resolve() def getstatusoutput(cmd): From 06360a9ddf56143b32bcd80d1c67988f069f4c88 Mon Sep 17 00:00:00 2001 From: NoCLin Date: Sat, 5 Oct 2019 19:55:39 +0800 Subject: [PATCH 04/18] fix json dump when CE --- lj/lj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lj/lj.py b/lj/lj.py index dab1440..b9b7409 100644 --- a/lj/lj.py +++ b/lj/lj.py @@ -30,7 +30,7 @@ def explain_result(result): if result.compile.code is not None and result.compile.code != 0: print(colorful.red("Compile Error")) - print(json.dumps(result.compile, indent=2, ensure_ascii=False)) + print(obj_json_dumps(result.compile, indent=2)) exit() status_count = { From 1eb7c02064fbcbf2b0935010d4a0eff916b99732 Mon Sep 17 00:00:00 2001 From: NoCLin Date: Sat, 5 Oct 2019 20:11:56 +0800 Subject: [PATCH 05/18] auto search ignore *.class --- lj/lj.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lj/lj.py b/lj/lj.py index b9b7409..df86c93 100644 --- a/lj/lj.py +++ b/lj/lj.py @@ -138,8 +138,8 @@ def main(): # 尝试获取文件,可忽略后缀 def get_file(file, not_exists_ok=False): if file.is_dir(): - # 自动搜索后缀 - file_list = [i for i in file.parent.glob(file.stem + ".*")] + # 自动搜索后缀,忽略 .class 等非源文件 + file_list = [i for i in file.parent.glob(file.stem + ".*") if i.suffix not in [".class"]] if len(file_list): return file_list[0] else: From 36af9b55b6d79809e2c0b624c9b7757f6874328f Mon Sep 17 00:00:00 2001 From: NoCLin Date: Sat, 5 Oct 2019 20:30:54 +0800 Subject: [PATCH 06/18] dump if ljr CE, fix python compile unicode error in Windows --- lj/commands/run.py | 3 +++ lj/default.localjudge.json | 2 +- lj/lj.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lj/commands/run.py b/lj/commands/run.py index b40a39a..b27da7e 100644 --- a/lj/commands/run.py +++ b/lj/commands/run.py @@ -6,6 +6,7 @@ import sys from lj.judger import do_compile +from lj.utils import obj_json_dumps logger = logging.getLogger() @@ -17,6 +18,8 @@ def lj_compile_and_run(args): run_with_console(compile_result.runnable) shutil.rmtree(compile_result.temp_dir) print("Removing " + compile_result.temp_dir) + else: + print(obj_json_dumps(compile_result, indent=2)) def run_with_console(command): diff --git a/lj/default.localjudge.json b/lj/default.localjudge.json index 7c0aceb..ee49be2 100644 --- a/lj/default.localjudge.json +++ b/lj/default.localjudge.json @@ -25,7 +25,7 @@ ".py" ], "dest": "${stem}.pyc", - "compile": "python -c \"import py_compile; py_compile.compile('${src}','${dest}')\"", + "compile": "python -c \"import py_compile; py_compile.compile(r'${src}',r'${dest}')\"", "run": "python ${dest}" }, { diff --git a/lj/lj.py b/lj/lj.py index df86c93..30c3856 100644 --- a/lj/lj.py +++ b/lj/lj.py @@ -28,6 +28,7 @@ def explain_result(result): + # None 为不需要编译 if result.compile.code is not None and result.compile.code != 0: print(colorful.red("Compile Error")) print(obj_json_dumps(result.compile, indent=2)) From ad4a1964782cb8f97aac0bb85ee86bc939721158 Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2019年10月24日 22:48:40 +0800 Subject: [PATCH 07/18] move test codes --- .../poj-1000/AC-1s-limit-2s.cpp} | 4 +++- tests/{cases => code/poj-1000}/CE.cpp | 0 tests/code/poj-1000/MLE-1M-limit-1M.cpp | 17 +++++++++++++++++ tests/code/poj-1000/MLE-max.cpp | 12 ++++++++++++ tests/{cases => code/poj-1000}/OLE.cpp | 0 tests/{cases => code/poj-1000}/RE.cpp | 0 tests/code/poj-1000/TLE-1s-limit-1s.cpp | 12 ++++++++++++ .../poj-1000/TLE-1s-limit-500ms.cpp} | 6 ++++-- .../poj-1000/TLE-endless-limit-1s.cpp} | 2 ++ tests/{cases => code/poj-1000}/WA.cpp | 0 .../poj-1000}/poj-1000-java/Main.java | 0 .../poj-1000}/poj-1000-java/Main/1.in | 0 .../poj-1000}/poj-1000-java/Main/1.out | 0 .../poj-1000}/poj-1000-java/Main/2.in | 0 .../poj-1000}/poj-1000-java/Main/2.out | 0 .../poj-1000}/poj-1000-java/Main/3.in | 0 .../poj-1000}/poj-1000-java/Main/3.out | 0 .../poj-1000}/poj-1000-java/Main/test-error.in | 0 .../poj-1000}/poj-1000-java/Main/test-error.out | 0 tests/{problems => code/poj-1000}/poj-1000.c | 1 + tests/code/poj-1000/poj-1000.cpp | 10 ++++++++++ tests/{problems => code/poj-1000}/poj-1000.py | 0 tests/{problems => code/poj-1000}/poj-1000/1.in | 0 .../{problems => code/poj-1000}/poj-1000/1.out | 0 .../{problems => code/poj-1000}/poj-1000/11.in | 0 .../{problems => code/poj-1000}/poj-1000/11.out | 0 tests/{problems => code/poj-1000}/poj-1000/2.in | 0 .../{problems => code/poj-1000}/poj-1000/2.out | 0 tests/{problems => code/poj-1000}/poj-1000/3.in | 0 .../{problems => code/poj-1000}/poj-1000/3.out | 0 tests/{problems => code/poj-1000}/poj-1000/9.in | 0 .../{problems => code/poj-1000}/poj-1000/9.out | 0 .../poj-1000}/poj-1000/README.md | 0 .../poj-1000}/poj-1000/test-error.in | 0 .../poj-1000}/poj-1000/test-error.out | 0 tests/{problems => code}/record.txt | 0 36 files changed, 61 insertions(+), 3 deletions(-) rename tests/{cases/TLE.cpp => code/poj-1000/AC-1s-limit-2s.cpp} (70%) rename tests/{cases => code/poj-1000}/CE.cpp (100%) create mode 100755 tests/code/poj-1000/MLE-1M-limit-1M.cpp create mode 100755 tests/code/poj-1000/MLE-max.cpp rename tests/{cases => code/poj-1000}/OLE.cpp (100%) rename tests/{cases => code/poj-1000}/RE.cpp (100%) create mode 100755 tests/code/poj-1000/TLE-1s-limit-1s.cpp rename tests/{cases/MLE.cpp => code/poj-1000/TLE-1s-limit-500ms.cpp} (68%) rename tests/{cases/AC.cpp => code/poj-1000/TLE-endless-limit-1s.cpp} (80%) rename tests/{cases => code/poj-1000}/WA.cpp (100%) rename tests/{problems => code/poj-1000}/poj-1000-java/Main.java (100%) rename tests/{problems => code/poj-1000}/poj-1000-java/Main/1.in (100%) rename tests/{problems => code/poj-1000}/poj-1000-java/Main/1.out (100%) rename tests/{problems => code/poj-1000}/poj-1000-java/Main/2.in (100%) rename tests/{problems => code/poj-1000}/poj-1000-java/Main/2.out (100%) rename tests/{problems => code/poj-1000}/poj-1000-java/Main/3.in (100%) rename tests/{problems => code/poj-1000}/poj-1000-java/Main/3.out (100%) rename tests/{problems => code/poj-1000}/poj-1000-java/Main/test-error.in (100%) rename tests/{problems => code/poj-1000}/poj-1000-java/Main/test-error.out (100%) rename tests/{problems => code/poj-1000}/poj-1000.c (86%) create mode 100644 tests/code/poj-1000/poj-1000.cpp rename tests/{problems => code/poj-1000}/poj-1000.py (100%) rename tests/{problems => code/poj-1000}/poj-1000/1.in (100%) rename tests/{problems => code/poj-1000}/poj-1000/1.out (100%) rename tests/{problems => code/poj-1000}/poj-1000/11.in (100%) rename tests/{problems => code/poj-1000}/poj-1000/11.out (100%) rename tests/{problems => code/poj-1000}/poj-1000/2.in (100%) rename tests/{problems => code/poj-1000}/poj-1000/2.out (100%) rename tests/{problems => code/poj-1000}/poj-1000/3.in (100%) rename tests/{problems => code/poj-1000}/poj-1000/3.out (100%) rename tests/{problems => code/poj-1000}/poj-1000/9.in (100%) rename tests/{problems => code/poj-1000}/poj-1000/9.out (100%) rename tests/{problems => code/poj-1000}/poj-1000/README.md (100%) rename tests/{problems => code/poj-1000}/poj-1000/test-error.in (100%) rename tests/{problems => code/poj-1000}/poj-1000/test-error.out (100%) rename tests/{problems => code}/record.txt (100%) diff --git a/tests/cases/TLE.cpp b/tests/code/poj-1000/AC-1s-limit-2s.cpp similarity index 70% rename from tests/cases/TLE.cpp rename to tests/code/poj-1000/AC-1s-limit-2s.cpp index d711b25..3a781eb 100755 --- a/tests/cases/TLE.cpp +++ b/tests/code/poj-1000/AC-1s-limit-2s.cpp @@ -1,9 +1,11 @@ #include +#include +// Time Limit: 2s using namespace std; int main() { int n, m; cin>> n>> m; cout << n + m << endl; - while (1); + sleep(1); return 0; } \ No newline at end of file diff --git a/tests/cases/CE.cpp b/tests/code/poj-1000/CE.cpp similarity index 100% rename from tests/cases/CE.cpp rename to tests/code/poj-1000/CE.cpp diff --git a/tests/code/poj-1000/MLE-1M-limit-1M.cpp b/tests/code/poj-1000/MLE-1M-limit-1M.cpp new file mode 100755 index 0000000..7bad243 --- /dev/null +++ b/tests/code/poj-1000/MLE-1M-limit-1M.cpp @@ -0,0 +1,17 @@ +#include +#include +using namespace std; +/** + * Memory Limit: 1M + * POJ Memory: 1716K Time: 0MS +**/ + +int main() { + int n, m; + char *p = new char[1024*1024]; + fill(p, p + 1024 * 1024, 1); + cin>> n>> m; + cout << n + m << endl; + sleep(1); + return 0; +} diff --git a/tests/code/poj-1000/MLE-max.cpp b/tests/code/poj-1000/MLE-max.cpp new file mode 100755 index 0000000..05f5da3 --- /dev/null +++ b/tests/code/poj-1000/MLE-max.cpp @@ -0,0 +1,12 @@ +#include +using namespace std; +/** + * Memory Limit: 1M +**/ +int main() { + int n, m; + cin>> n>> m; + cout << n + m << endl; + while (1) int *p = new int[100000000]; + return 0; +} diff --git a/tests/cases/OLE.cpp b/tests/code/poj-1000/OLE.cpp similarity index 100% rename from tests/cases/OLE.cpp rename to tests/code/poj-1000/OLE.cpp diff --git a/tests/cases/RE.cpp b/tests/code/poj-1000/RE.cpp similarity index 100% rename from tests/cases/RE.cpp rename to tests/code/poj-1000/RE.cpp diff --git a/tests/code/poj-1000/TLE-1s-limit-1s.cpp b/tests/code/poj-1000/TLE-1s-limit-1s.cpp new file mode 100755 index 0000000..57b1d65 --- /dev/null +++ b/tests/code/poj-1000/TLE-1s-limit-1s.cpp @@ -0,0 +1,12 @@ +#include +#include +// Time Limit: 1s +// POJ: 660K 0MS +using namespace std; +int main() { + int n, m; + cin>> n>> m; + cout << n + m << endl; + sleep(1); + return 0; +} \ No newline at end of file diff --git a/tests/cases/MLE.cpp b/tests/code/poj-1000/TLE-1s-limit-500ms.cpp similarity index 68% rename from tests/cases/MLE.cpp rename to tests/code/poj-1000/TLE-1s-limit-500ms.cpp index a700b35..5acd419 100755 --- a/tests/cases/MLE.cpp +++ b/tests/code/poj-1000/TLE-1s-limit-500ms.cpp @@ -1,9 +1,11 @@ #include +#include +// Time Limit: 500ms using namespace std; int main() { int n, m; cin>> n>> m; cout << n + m << endl; - while (1) int *p = new int[100000000000]; + sleep(1); return 0; -} +} \ No newline at end of file diff --git a/tests/cases/AC.cpp b/tests/code/poj-1000/TLE-endless-limit-1s.cpp similarity index 80% rename from tests/cases/AC.cpp rename to tests/code/poj-1000/TLE-endless-limit-1s.cpp index 222fa7b..d7462c6 100755 --- a/tests/cases/AC.cpp +++ b/tests/code/poj-1000/TLE-endless-limit-1s.cpp @@ -1,8 +1,10 @@ #include +// Time Limit: 1s using namespace std; int main() { int n, m; cin>> n>> m; cout << n + m << endl; + while(1); return 0; } \ No newline at end of file diff --git a/tests/cases/WA.cpp b/tests/code/poj-1000/WA.cpp similarity index 100% rename from tests/cases/WA.cpp rename to tests/code/poj-1000/WA.cpp diff --git a/tests/problems/poj-1000-java/Main.java b/tests/code/poj-1000/poj-1000-java/Main.java similarity index 100% rename from tests/problems/poj-1000-java/Main.java rename to tests/code/poj-1000/poj-1000-java/Main.java diff --git a/tests/problems/poj-1000-java/Main/1.in b/tests/code/poj-1000/poj-1000-java/Main/1.in similarity index 100% rename from tests/problems/poj-1000-java/Main/1.in rename to tests/code/poj-1000/poj-1000-java/Main/1.in diff --git a/tests/problems/poj-1000-java/Main/1.out b/tests/code/poj-1000/poj-1000-java/Main/1.out similarity index 100% rename from tests/problems/poj-1000-java/Main/1.out rename to tests/code/poj-1000/poj-1000-java/Main/1.out diff --git a/tests/problems/poj-1000-java/Main/2.in b/tests/code/poj-1000/poj-1000-java/Main/2.in similarity index 100% rename from tests/problems/poj-1000-java/Main/2.in rename to tests/code/poj-1000/poj-1000-java/Main/2.in diff --git a/tests/problems/poj-1000-java/Main/2.out b/tests/code/poj-1000/poj-1000-java/Main/2.out similarity index 100% rename from tests/problems/poj-1000-java/Main/2.out rename to tests/code/poj-1000/poj-1000-java/Main/2.out diff --git a/tests/problems/poj-1000-java/Main/3.in b/tests/code/poj-1000/poj-1000-java/Main/3.in similarity index 100% rename from tests/problems/poj-1000-java/Main/3.in rename to tests/code/poj-1000/poj-1000-java/Main/3.in diff --git a/tests/problems/poj-1000-java/Main/3.out b/tests/code/poj-1000/poj-1000-java/Main/3.out similarity index 100% rename from tests/problems/poj-1000-java/Main/3.out rename to tests/code/poj-1000/poj-1000-java/Main/3.out diff --git a/tests/problems/poj-1000-java/Main/test-error.in b/tests/code/poj-1000/poj-1000-java/Main/test-error.in similarity index 100% rename from tests/problems/poj-1000-java/Main/test-error.in rename to tests/code/poj-1000/poj-1000-java/Main/test-error.in diff --git a/tests/problems/poj-1000-java/Main/test-error.out b/tests/code/poj-1000/poj-1000-java/Main/test-error.out similarity index 100% rename from tests/problems/poj-1000-java/Main/test-error.out rename to tests/code/poj-1000/poj-1000-java/Main/test-error.out diff --git a/tests/problems/poj-1000.c b/tests/code/poj-1000/poj-1000.c similarity index 86% rename from tests/problems/poj-1000.c rename to tests/code/poj-1000/poj-1000.c index 5ec745c..5d3d013 100644 --- a/tests/problems/poj-1000.c +++ b/tests/code/poj-1000/poj-1000.c @@ -1,3 +1,4 @@ +// poj 380K 16MS #include int main() { diff --git a/tests/code/poj-1000/poj-1000.cpp b/tests/code/poj-1000/poj-1000.cpp new file mode 100644 index 0000000..e015f9d --- /dev/null +++ b/tests/code/poj-1000/poj-1000.cpp @@ -0,0 +1,10 @@ +#include +using namespace std; +// poj 720K 0MS +int main() +{ + int a,b; + cin>> a>> b; + cout << a+b << endl; + return 0; +} \ No newline at end of file diff --git a/tests/problems/poj-1000.py b/tests/code/poj-1000/poj-1000.py similarity index 100% rename from tests/problems/poj-1000.py rename to tests/code/poj-1000/poj-1000.py diff --git a/tests/problems/poj-1000/1.in b/tests/code/poj-1000/poj-1000/1.in similarity index 100% rename from tests/problems/poj-1000/1.in rename to tests/code/poj-1000/poj-1000/1.in diff --git a/tests/problems/poj-1000/1.out b/tests/code/poj-1000/poj-1000/1.out similarity index 100% rename from tests/problems/poj-1000/1.out rename to tests/code/poj-1000/poj-1000/1.out diff --git a/tests/problems/poj-1000/11.in b/tests/code/poj-1000/poj-1000/11.in similarity index 100% rename from tests/problems/poj-1000/11.in rename to tests/code/poj-1000/poj-1000/11.in diff --git a/tests/problems/poj-1000/11.out b/tests/code/poj-1000/poj-1000/11.out similarity index 100% rename from tests/problems/poj-1000/11.out rename to tests/code/poj-1000/poj-1000/11.out diff --git a/tests/problems/poj-1000/2.in b/tests/code/poj-1000/poj-1000/2.in similarity index 100% rename from tests/problems/poj-1000/2.in rename to tests/code/poj-1000/poj-1000/2.in diff --git a/tests/problems/poj-1000/2.out b/tests/code/poj-1000/poj-1000/2.out similarity index 100% rename from tests/problems/poj-1000/2.out rename to tests/code/poj-1000/poj-1000/2.out diff --git a/tests/problems/poj-1000/3.in b/tests/code/poj-1000/poj-1000/3.in similarity index 100% rename from tests/problems/poj-1000/3.in rename to tests/code/poj-1000/poj-1000/3.in diff --git a/tests/problems/poj-1000/3.out b/tests/code/poj-1000/poj-1000/3.out similarity index 100% rename from tests/problems/poj-1000/3.out rename to tests/code/poj-1000/poj-1000/3.out diff --git a/tests/problems/poj-1000/9.in b/tests/code/poj-1000/poj-1000/9.in similarity index 100% rename from tests/problems/poj-1000/9.in rename to tests/code/poj-1000/poj-1000/9.in diff --git a/tests/problems/poj-1000/9.out b/tests/code/poj-1000/poj-1000/9.out similarity index 100% rename from tests/problems/poj-1000/9.out rename to tests/code/poj-1000/poj-1000/9.out diff --git a/tests/problems/poj-1000/README.md b/tests/code/poj-1000/poj-1000/README.md similarity index 100% rename from tests/problems/poj-1000/README.md rename to tests/code/poj-1000/poj-1000/README.md diff --git a/tests/problems/poj-1000/test-error.in b/tests/code/poj-1000/poj-1000/test-error.in similarity index 100% rename from tests/problems/poj-1000/test-error.in rename to tests/code/poj-1000/poj-1000/test-error.in diff --git a/tests/problems/poj-1000/test-error.out b/tests/code/poj-1000/poj-1000/test-error.out similarity index 100% rename from tests/problems/poj-1000/test-error.out rename to tests/code/poj-1000/poj-1000/test-error.out diff --git a/tests/problems/record.txt b/tests/code/record.txt similarity index 100% rename from tests/problems/record.txt rename to tests/code/record.txt From 52de673f3f2fd1be37c66e1e1de85b93d517ab14 Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2019年10月27日 16:26:57 +0800 Subject: [PATCH 08/18] test on macOS/Windows/Linux add 'shlex.split' in lj run reconstructed judge save judge log add MLE TLE check --- .github/workflows/linux.yml | 24 +++ .../{pythonpackage.yml => macos.yml} | 12 +- .github/workflows/windows.yml | 24 +++ build_windows.bat | 3 +- ci.py | 9 + lj/commands/judge.py | 203 +++++++++++++++--- lj/commands/run.py | 13 +- lj/globalvar.py | 11 + lj/judger.py | 188 ++++++++++++---- lj/lj.py | 182 ++++------------ lj/utils.py | 106 +++++++-- lj/vendors/human_bytes_converter.py | 125 +++++++++++ pytest.ini | 3 + ...it-500ms.cpp => memory-AC-1M-limit-2M.cpp} | 8 +- ...imit-1M.cpp => memory-MLE-1M-limit-1M.cpp} | 0 ...LE-max.cpp => memory-MLE-max-limit-1M.cpp} | 2 +- ...s-limit-2s.cpp => time-AC-1s-limit-2s.cpp} | 0 ...-limit-1s.cpp => time-TLE-1s-limit-1s.cpp} | 0 ...t-1s.cpp => time-TLE-endless-limit-1s.cpp} | 0 tests/judge_status_test.py | 50 +++-- tests/lj_cli_test.py | 93 +++++--- 21 files changed, 775 insertions(+), 281 deletions(-) create mode 100644 .github/workflows/linux.yml rename .github/workflows/{pythonpackage.yml => macos.yml} (78%) create mode 100644 .github/workflows/windows.yml create mode 100644 ci.py create mode 100644 lj/globalvar.py create mode 100644 lj/vendors/human_bytes_converter.py create mode 100644 pytest.ini rename tests/code/poj-1000/{TLE-1s-limit-500ms.cpp => memory-AC-1M-limit-2M.cpp} (54%) rename tests/code/poj-1000/{MLE-1M-limit-1M.cpp => memory-MLE-1M-limit-1M.cpp} (100%) rename tests/code/poj-1000/{MLE-max.cpp => memory-MLE-max-limit-1M.cpp} (77%) rename tests/code/poj-1000/{AC-1s-limit-2s.cpp => time-AC-1s-limit-2s.cpp} (100%) rename tests/code/poj-1000/{TLE-1s-limit-1s.cpp => time-TLE-1s-limit-1s.cpp} (100%) rename tests/code/poj-1000/{TLE-endless-limit-1s.cpp => time-TLE-endless-limit-1s.cpp} (100%) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..4e32c38 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,24 @@ +name: Linux + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.5, 3.6, 3.7] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + gcc hello.c -o hello.exe + .\hello.exe diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/macos.yml similarity index 78% rename from .github/workflows/pythonpackage.yml rename to .github/workflows/macos.yml index 887a68a..3795594 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/macos.yml @@ -1,11 +1,11 @@ -name: Python package +name: macOS on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: macos-latest strategy: max-parallel: 4 matrix: @@ -21,13 +21,11 @@ jobs: run: | python -m pip install --upgrade pip python setup.py install - sudo apt-get update - sudo apt-get install gcc + pip install pytest - name: Lint with flake8 run: | pip install flake8 sh lint.sh - - name: Test with pytest + - name: Test run: | - pip install pytest - pytest + python ci.py diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..ff84fbd --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,24 @@ +name: Windows + +on: [push] + +jobs: + build: + + runs-on: windows-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.5, 3.6, 3.7] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + gcc hello.c -o hello.exe + .\hello.exe diff --git a/build_windows.bat b/build_windows.bat index 09a8768..0a941b6 100644 --- a/build_windows.bat +++ b/build_windows.bat @@ -1 +1,2 @@ -pyinstaller lj\lj.py --console --onefile --distpath ./windows_dist --workpath ./windows_dist --specpath ./windows_dist -y \ No newline at end of file +pyinstaller lj\lj.py --console --onefile --distpath ./windows_dist --workpath ./windows_dist --specpath ./windows_dist -y +pause \ No newline at end of file diff --git a/ci.py b/ci.py new file mode 100644 index 0000000..da781ba --- /dev/null +++ b/ci.py @@ -0,0 +1,9 @@ +import os + +commands = [ + "pytest --doctest-modules lj{sep}utils.py lj{sep}vendors -v".format(sep=os.sep), + "pytest -v", +] +for command in commands: + print(command) + assert 0 == os.system(command) diff --git a/lj/commands/judge.py b/lj/commands/judge.py index 578f044..ca9332f 100644 --- a/lj/commands/judge.py +++ b/lj/commands/judge.py @@ -1,61 +1,216 @@ # -*-coding:utf-8-*- +import os +import sys +import collections import logging +from pathlib import Path +import colorful +from prettytable import PrettyTable -from lj.judger import do_judge_run, do_compile, JudgeResultSet +from lj import globalvar +from lj.vendors.simplediff import diff +from lj.vendors.human_bytes_converter import bytes2human +from lj.judger import do_judge_run, do_compile, JudgeStatus, JudgeResultSet from lj.utils import ( get_data_dir, get_cases, - read_file -) + read_file, + get_time_and_memory_limit, + obj_json_dumps) -logger = logging.getLogger() +logger = logging.getLogger("lj") + + +def explain_result(result, json=False): + if json: + if sys.stdout_: + sys.stdout = sys.stdout_ + print(obj_json_dumps(result, indent=2)) + return + # TODO: 动态展示judge 结果 + # None 为不需要编译 + if result.compile.code is not None and result.compile.code != 0: + print(colorful.red("Compile Error")) + print(obj_json_dumps(result.compile, indent=2)) + exit() + if len(result.cases) == 0: + print("no cases.") + return + + time_col_name = "Time" + if result.time_limit is not None: + time_col_name += " < " + "{c.bold}{time}ms{c.reset}".format(time=result.time_limit, c=colorful) + memory_col_name = "Memory" + + # colorful.italics + if result.memory_limit is not None: + memory_col_name += " < " + "{c.bold}{memory}{c.reset}".format( + memory=bytes2human(result.memory_limit), + c=colorful) + + table = PrettyTable(["Case", + "Status", + time_col_name, + memory_col_name]) + status_count = collections.OrderedDict() + status_count["All"] = 0 + status_count[JudgeStatus.AC] = 0 + + collections.OrderedDict() + for case in result.cases: + status_count.setdefault(case.status, 0) + status_count[case.status] += 1 + status_count["All"] += 1 + + table.add_row([ + case.case_index, + '{c.bold}{color_func}{status}{c.reset}'.format( + c=colorful, status=case.status, + color_func=colorful.green if case.status == JudgeStatus.AC else colorful.red), + + "{c.bold}{color_func}{time}ms{c.reset}".format( + c=colorful, + color_func=colorful.red if case.status == JudgeStatus.TLE else "", + time="%.2f" % case.time_used), + + "{c.bold}{color_func}{memory}{c.reset}".format( + c=colorful, + color_func=colorful.red if case.status == JudgeStatus.MLE else "", + memory=bytes2human(case.memory_used)), + + ]) + + print(table) + ac_ratio = (status_count[JudgeStatus.AC] / status_count["All"]) * 100 + + for status, count in status_count.items(): + + if status == JudgeStatus.AC or (status == "All" and ac_ratio == 100): + status_color_func = colorful.green + else: + status_color_func = colorful.red + print( + '{c.bold}{status_color_func}{status}{c.reset} {count} {c.bold}{ratio_color_func}{ratio_str}{c.reset}'.format( + c=colorful, + status_color_func=status_color_func, + ratio_color_func=colorful.green if ac_ratio == 100 else colorful.red, + status=status, + count=count, + ratio_str="(%.2f%%)" % ac_ratio if status == JudgeStatus.AC else "")) + print() + + for case in result.cases: + if case.status in [JudgeStatus.WA, JudgeStatus.PE]: + print('{c.bold}{c.red}-> case [{index}] <- {status} {c.reset}{c.bold}' + .format(c=colorful, + index=case.case_index, + status=case.status)) + + print("stdin:") + print(case.input) + colored_diff_str = "" + + # TODO: 用网页显示diff,开临时http server 当页面加载完毕时 关闭 + + for c in diff(case.expected_output, case.output): + char = c[1] + if c[0] == "-": + char = "{c.bold_green}" + char + "{c.reset}" + elif c[0] == "+": + char = "{c.bold_red}" + char + "{c.reset}" + colored_diff_str += char + + x = PrettyTable(["Expected", "Yours"]) + x.add_row([case.expected_output, case.output]) + print(x) + print("diff:") + print(colored_diff_str.format(c=colorful)) -# TODO: 支持带空格的文件名? -# TODO: 删除二进制文件 def lj_judge(args): + + if args.json: + # 临时屏蔽所有stdout输出 + # sys.stdout_ = sys.stdout + # sys.stdout = None + pass src = args.src - data_dir = get_data_dir(src) - case_index = args.case - # time_limit = args.time_limit - # memory_limit = args.memory_limit + case_index = args.case result = JudgeResultSet() result.time_limit = None - result.memory_limit = None # TODO: in code (regex) + result.memory_limit = None result.compile = do_compile(src) if result.compile.code is not None and result.compile.code != 0: - return result + explain_result(result, args.json) + return - # TODO: memory limit - # TODO: output limit + source_code = read_file(result.compile.params["src"]) - cases = get_cases(data_dir) if case_index is None else [case_index] - logger.debug("cases (%d): %s" % (len(cases), cases)) + logger.info("source code:\n\n%s\n\n" % source_code) - for case in cases: - stdin = read_file(str(data_dir / (case + ".in")), "r") - expected_out = read_file(str(data_dir / (case + ".out")), "r") + tl, ml = (get_time_and_memory_limit(source_code)) + + result.time_limit = args.time_limit if args.time_limit else tl + result.memory_limit = args.memory_limit if args.memory_limit else ml + + if args.in_file and args.eout_file: + judge_cases_group = [{ + "in": args.in_file, + "eout": args.eout_file, + "name": "CommandLine" + }] + else: + if not args.in_file and not args.eout_file: + data_dir = get_data_dir(src) + case_indexes = get_cases(data_dir) if case_index is None else [case_index] + judge_cases_group = [ + { + "in": str(data_dir / (i + ".in")), + "eout": str(data_dir / (i + ".out")), + "name": i + } for i in case_indexes + ] + else: + print("in_file and eout_file are required.") + exit() + + logger.debug("cases (%d): %s" % (len(judge_cases_group), judge_cases_group)) + + for case in judge_cases_group: + stdin = read_file(case["in"], "r") + expected_out = read_file(case["eout"], "r") # 支持配配置文件指定限制 case_result = do_judge_run(command=result.compile.runnable, stdin=stdin, expected_out=expected_out, - time_limit=None, - memory_limit=None, - case_index=case + time_limit=result.time_limit, + memory_limit=result.memory_limit, + case_index=case["name"] ) result.cases.append(case_result) + explain_result(result, args.json) + dest_file = result.compile.params.get("dest") if dest_file: logger.debug("delete " + dest_file) - # TODO: delete temp exe - return result + Path(dest_file).unlink() + + logger.info("result:\n%s" % obj_json_dumps(result, indent=2)) + + tmp_log_stream = globalvar.get("tmp_log_stream") + tmp_log_handler = globalvar.get("tmp_log_handler") + tmp_log_handler.flush() + result_log_file = os.path.join(result.compile.params["temp_dir"], result.compile.params["stem"] + ".log") + with open(result_log_file, "w") as f: + f.write(tmp_log_stream.getvalue()) + exit(0) if __name__ == '__main__': pass diff --git a/lj/commands/run.py b/lj/commands/run.py index b27da7e..46f69cc 100644 --- a/lj/commands/run.py +++ b/lj/commands/run.py @@ -1,14 +1,15 @@ # -*-coding:utf-8-*- import argparse import logging +import shlex import shutil import subprocess import sys from lj.judger import do_compile -from lj.utils import obj_json_dumps +from lj.utils import obj_json_dumps, IS_WINDOWS -logger = logging.getLogger() +logger = logging.getLogger("lj") def lj_compile_and_run(args): @@ -20,19 +21,21 @@ def lj_compile_and_run(args): print("Removing " + compile_result.temp_dir) else: print(obj_json_dumps(compile_result, indent=2)) + print("Compile Error:\n") + print(compile_result.stdout) def run_with_console(command): print("Running %s" % command) + + proc = subprocess.Popen(shlex.split(command, posix=not IS_WINDOWS), shell=False, stdin=sys.stdin, stdout=sys.stdout) + print("PID: %d" % proc.pid) print("-" * 20) - # 如果文件名含有空格,用户必须输入引号 - proc = subprocess.Popen(command.split(), shell=False, stdin=sys.stdin, stdout=sys.stdout) try: while proc.poll() is None: pass except KeyboardInterrupt: pass - print() print("-" * 20) print("Process Exit Code: %s" % (str(proc.returncode))) diff --git a/lj/globalvar.py b/lj/globalvar.py new file mode 100644 index 0000000..403ff0b --- /dev/null +++ b/lj/globalvar.py @@ -0,0 +1,11 @@ +class GlobalVar: + var_dict = {} + + +def get(k, default=None): + return GlobalVar.var_dict.get(k, default) + + +# noinspection PyShadowingBuiltins +def set(k, v): + GlobalVar.var_dict[k] = v diff --git a/lj/judger.py b/lj/judger.py index 96fd444..dff155b 100644 --- a/lj/judger.py +++ b/lj/judger.py @@ -1,17 +1,33 @@ +# -*-coding:utf-8-*- + import logging import os -import platform +import sys +import shlex +import json import subprocess +import tempfile +import traceback + +import threading +from io import StringIO from pathlib import Path from string import Template + +import psutil + from lj.utils import (get_now_ms, + print_and_exit, equals_ignore_presentation_error, ignore_last_newline, get_temp_dir, - load_options) + IS_WINDOWS, IS_LINUX, IS_MACOS, + read_file, get_memory_by_psutil) + +logger = logging.getLogger("lj") -logger = logging.getLogger() +DEFAULT_STDOUT_LIMIT = 1024 * 1024 * 32 class JudgeStatus: @@ -61,13 +77,30 @@ def __init__(self): self.output_len = None +def load_options(): + logging.debug("loading options") + file = Path.home() / ".localjudge.json" + if file.exists(): + try: + return json.loads(read_file(str(file.resolve()), "r")) + except json.JSONDecodeError: + print(str(file) + " parse failed.") + exit() + + else: + logger.debug("config file not found!") + default_config_file = str(Path(__file__).parent / "default.localjudge.json") + default_options = json.loads(read_file(default_config_file, "r")) + # path.resolve() will raise FileNotFoundError on Py 3.5.7 if file doesn't exist. + with open(os.path.abspath(str(file)), "w") as f: + json.dump(default_options, f, indent=4, ensure_ascii=False) + return default_options + + def get_lang_options_from_suffix(suffix): options = load_options() try: - lang_options = [lang for lang in options if suffix in lang.get("extensions", [])][0] - logging.debug("language options") - logging.debug(lang_options) - return lang_options + return [lang for lang in options if suffix in lang.get("extensions", [])][0] except IndexError: print("unsupported suffix: " + suffix) exit() @@ -77,24 +110,26 @@ def do_compile(src) -> (int, str): src_path = Path(src).resolve() temp_dir = get_temp_dir(src) lang_options = get_lang_options_from_suffix(src_path.suffix) + logging.debug("language options:") + logging.debug(lang_options) compile_result = CompileResult() - compile_cmd_template = lang_options.get("compile") - - run_cmd_template = lang_options.get("run") - dest_template = lang_options.get("dest") + tpl_compile = lang_options.get("compile") + tpl_runnable = lang_options.get("run") + tpl_dest = lang_options.get("dest") params = { "src": str(src_path), + "temp_dir": str(temp_dir), "stem": src_path.stem, "dest": None, - "exe_if_win": ".exe" if platform.system() == "Windows" else "" + "exe_if_win": ".exe" if IS_WINDOWS else "" } # 此时文件还不存在 - params["dest"] = os.path.abspath(str(temp_dir / Template(dest_template).substitute(params))) + params["dest"] = os.path.abspath(str(temp_dir / Template(tpl_dest).substitute(params))) - if compile_cmd_template: - compile_cmd = Template(compile_cmd_template).substitute(params) + if tpl_compile: + compile_cmd = Template(tpl_compile).substitute(params) logger.debug("compile command: %s" % compile_cmd) code, stdout = subprocess.getstatusoutput(compile_cmd) @@ -104,57 +139,138 @@ def do_compile(src) -> (int, str): compile_result.code = code compile_result.temp_dir = str(temp_dir.resolve()) - compile_result.runnable = Template(run_cmd_template).substitute(params) + compile_result.runnable = Template(tpl_runnable).substitute(params) compile_result.params = params return compile_result -def do_judge_run(command, stdin=None, expected_out=None, time_limit=None, memory_limit=None, output_limit=1024, +def do_judge_run(command, stdin="", expected_out="", time_limit=None, memory_limit=None, case_index=None): logger.debug("run command: %s" % command) logger.debug("stdin: %s" % stdin) logger.debug("expected_out: %s" % expected_out) + logger.debug("time limit: %s" % time_limit) + logger.debug("memory limit: %s" % memory_limit) result = JudgeResult() result.command = command + result.code = -1 result.input = stdin + result.output = "" result.expected_output = expected_out result.time_limit = time_limit result.memory_limit = memory_limit result.case_index = case_index - t1 = get_now_ms() - logger.debug("t1: %d" % t1) - ps = subprocess.Popen(command.split(), shell=False, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + # Linux preexec_fn / ulimit + # MacOS polling psutil + # Windows winc --memory or polling psutil + + # resource.setrlimit() + # https://stackoverflow.com/questions/12965023/python-subprocess-popen-communicate-equivalent-to-popen-stdout-read?r=SearchResults + + # stdout_fp = tempfile.NamedTemporaryFile("w+", encoding="utf-8") + stdout_fp = open("tmp.txt","w+",encoding="utf-8") + # print(stdout_fp.name) + p = subprocess.Popen(shlex.split(command, posix=not IS_WINDOWS), shell=False, + universal_newlines=True, + stdin=subprocess.PIPE, + stdout=stdout_fp, + # stderr=sys.stderr, + ) + + # TODO: ole check + tle_kill = ole_kill = mle_kill = False + mle_check = True + stdout_len = 0 + max_memory_used = -1 + + def memory_monitor(): + + nonlocal p, max_memory_used, mle_kill + try: + psp = psutil.Process(pid=p.pid) + while mle_check: + mem = get_memory_by_psutil(psp) + # mem = get_memory_by_ps(p.pid) * 1024 + + if mem> max_memory_used: + logger.debug("polling memory of (%s): %s bytes" % (p.pid, mem)) + + max_memory_used = mem + if memory_limit and max_memory_used> memory_limit: + logger.debug("reach limit %s %s" % (max_memory_used, memory_limit)) + mle_kill = True + p.kill() + except Exception as e: + logging.debug("get memory failed") + logging.debug(e) + + def output_monitor(): + pass + + mle_check_thread = threading.Thread(target=memory_monitor, args=()) + mle_check_thread.start() + t2 = get_now_ms() - logger.debug("t2: %d t2-t1: %d" % (t2, t2 - t1)) - # FIXME: 不要直接使用communicate,否则不好处理内存与时间占用 - stdout, _ = ps.communicate(stdin.encode()) + try: - # TODO: 处理 Runtime Error 等 + if time_limit: + def timeout_kill(): + nonlocal tle_kill # 不需要再抛异常了 + tle_kill = True + p.kill() + + # timer = Timer(time_limit / 1000.0, timeout_kill) + # timer.start() + _, _ = p.communicate(stdin + # , timeout=time_limit / 1000.0 if time_limit else None + ) + stdout_fp.seek(0) + result.output = stdout_fp.read() + except subprocess.TimeoutExpired: + tle_kill = True + except Exception as e: + logger.error(e) + traceback.print_exc() + + print_and_exit(-1, "System Error. Please submit an issue.") + finally: + stdout_fp.close() + if time_limit: + timer.cancel() + mle_check = False + # p.stdout.close() + # p.stderr.close() + + result.code = p.poll() t3 = get_now_ms() - logger.debug("t3: %d t3-t2: %d t3-t1: %d" % (t3, t3 - t2, t3 - t1)) + logger.debug("t3: %d t3-t2: %d" % (t3, t3 - t2)) + logger.debug("stdout: %s" % result.output) + logger.debug("code: %s" % result.code) - result.time_used = t3 - t1 - result.memory_used = 0 - result.output = stdout.decode() + result.time_used = t3 - t2 + result.memory_used = max_memory_used - result.code = 0 - if result.code != 0: - result.status = JudgeStatus.RE + if tle_kill: # 超时kill code 非0 必须在判RE前返回 + result.status = JudgeStatus.TLE return result - if result.memory_limit is not None \ - and result.memory_used> memory_limit: + if mle_kill: result.status = JudgeStatus.MLE return result + if ole_kill: + result.status = JudgeStatus.OLE + return result + + if result.code != 0: + result.status = JudgeStatus.RE + return result + if result.time_limit is not None \ and result.time_used> time_limit: result.status = JudgeStatus.TLE diff --git a/lj/lj.py b/lj/lj.py index 30c3856..bc28a1d 100644 --- a/lj/lj.py +++ b/lj/lj.py @@ -1,122 +1,45 @@ # -*-coding:utf-8-*- -import json -import logging -from sys import exit +import logging +import sys from pathlib import Path - import argparse -import colorful -from prettytable import PrettyTable -from lj.judger import JudgeStatus -from lj.utils import obj_json_dumps -from lj.vendors.simplediff import diff +from io import StringIO + from lj.commands.clean import lj_clean from lj.commands.create import lj_create from lj.commands.judge import lj_judge from lj.commands.show import lj_show from lj.commands.run import lj_compile_and_run +from lj.utils import print_and_exit +from lj import globalvar + +log_format = '%(asctime)s [%(filename)s:%(lineno)d] [%(levelname)s] %(message)s' + +tmp_log_stream = StringIO() +tmp_log_handler = logging.StreamHandler(tmp_log_stream) logging.basicConfig(level=logging.INFO, - format='%(asctime)s [%(filename)s:%(lineno)d] [%(levelname)s] %(message)s', + format=log_format, + handlers=[tmp_log_handler] ) -# TODO: write log file - -logger = logging.getLogger() - - -def explain_result(result): - # None 为不需要编译 - if result.compile.code is not None and result.compile.code != 0: - print(colorful.red("Compile Error")) - print(obj_json_dumps(result.compile, indent=2)) - exit() - - status_count = { - "All": 0, - JudgeStatus.AC: 0, - - } - time_col_name = "Time" + \ - (" < " + "{c.bold}{c.brown}{time}{c.reset}".format(time=result.time_limit, c=colorful) - if result.time_limit is not None else "") - memory_col_name = "Memory" + \ - (" < " + "{c.bold}{c.brown}{time}{c.reset}".format(time=result.memory_limit, c=colorful) - if result.memory_limit is not None else "") - - table = PrettyTable(["Case", - "Status", - time_col_name, - memory_col_name]) - - for case in result.cases: - color_func = colorful.green \ - if case.status == JudgeStatus.AC else colorful.red - - status_count.setdefault(case.status, 0) - status_count[case.status] += 1 - status_count["All"] += 1 - - table.add_row([ - case.case_index, - '{c.bold}{color_func}{status}{c.reset}'.format( - status=case.status, - c=colorful, - color_func=color_func), - "%dms" % case.time_used, # TODO: color - "%dMB" % case.memory_used, - - ]) - - print(table) - print("=====Summary=====") - ac_ratio = (status_count[JudgeStatus.AC] / status_count["All"]) * 100 - - for status, count in status_count.items(): - if status == JudgeStatus.AC or (status == "All" and ac_ratio == 100): - status_color_func = colorful.green - else: - status_color_func = colorful.red - print('{c.bold}{status_color_func}{status}{c.reset} {count} {ratio_color_func}{ratio_str}{c.reset}'.format( - status=status, - c=colorful, - status_color_func=status_color_func, - count=count, - ratio_color_func=colorful.green if ac_ratio == 100 else colorful.red, - ratio_str="(%f%%)" % ac_ratio if status == JudgeStatus.AC else "")) - - for case in result.cases: - if case.status in [JudgeStatus.WA, JudgeStatus.PE]: - print(colorful.red("-> case [%s] <- %s " - % (case.case_index, case.status))) - print("stdin:") - print(case.input) - colored_diff_str = "" - - # TODO: 用网页显示diff,开临时http server 当页面加载完毕时 关闭 - - for c in diff(case.expected_output, case.output): - char = c[1] - if c[0] == "-": - char = "{c.bold_green}" + char + "{c.reset}" - elif c[0] == "+": - char = "{c.bold_red}" + char + "{c.reset}" - colored_diff_str += char - - x = PrettyTable(["Expected", "Yours"]) - x.add_row([case.expected_output, case.output]) - print(x) - print("diff:") - print(colored_diff_str.format(c=colorful)) +logger = logging.getLogger("lj") + +globalvar.set("tmp_log_stream", tmp_log_stream) +globalvar.set("tmp_log_handler", tmp_log_handler) def main(): parser = argparse.ArgumentParser(description="Local Judge") - parser.add_argument("src", help="source file or sub-command") - parser.add_argument('src2', nargs='?', default="", help="source file for sub-command") + parser.add_argument("command", nargs="?", default="judge", help="command") + parser.add_argument('src', help="source file") parser.add_argument("-c", "--case", help="index of test case") + # 指定stdio文件,避免测试时新建重复文件,必须同时存在 + parser.add_argument("-i", "--in_file", help="in file", ) + parser.add_argument("-eo", "--eout_file", help="expected_out file") + parser.add_argument("-t", "--time_limit", type=int, default=None, help="time limit (ms)") parser.add_argument("-m", "--memory_limit", type=int, default=None, help="memory limit (MB)") parser.add_argument("-d", "--debug", dest="debug", action="/index.cgi/larger-text/https://github.com/NoCLin/LocalJudge/compare/store_true", help="debug mode") @@ -126,55 +49,40 @@ def main(): if args.debug: logger.setLevel(logging.DEBUG) + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter(log_format)) + logger.addHandler(handler) - logger.debug("args: %s" % args) - - sub_command_mapping = { - "clean": lj_clean, - "create": lj_create, - "show": lj_show, - "run": lj_compile_and_run - } + logger.debug("raw args: %s" % args) # 尝试获取文件,可忽略后缀 - def get_file(file, not_exists_ok=False): + def try_get_file(file, not_exists_ok=False): if file.is_dir(): # 自动搜索后缀,忽略 .class 等非源文件 - file_list = [i for i in file.parent.glob(file.stem + ".*") if i.suffix not in [".class"]] + file_list = [i for i in file.parent.glob(file.stem + ".*") if i.suffix not in [".class", ".exe", ".pyc"]] if len(file_list): return file_list[0] else: - print("自动识别后缀失败,文件不存在") - exit() + print_and_exit(-1, "自动识别后缀失败,文件不存在") if not_exists_ok is False and not file.is_file(): - print("file does not exist.") - exit(-1) + print_and_exit(-1, "file does not exist.") return file # 子命令支持 - sub_func = sub_command_mapping.get(args.src) - if sub_func: - if not args.src2: - print("invalid args") - exit() - - args.command = args.src - args.src = get_file(Path(args.src2), not_exists_ok=args.command == "create") - logger.debug("sub_command " + args.command) - logger.debug(args) - sub_func(args) - exit() - - args.src = get_file(Path(args.src), not_exists_ok=False) - - judge_result = lj_judge(args) - - # TODO: ignore any console output - if args.json: - print(obj_json_dumps(judge_result, indent=2)) - exit() - else: - explain_result(judge_result) + sub_func = { + "judge": lj_judge, + "clean": lj_clean, + "create": lj_create, + "show": lj_show, + "run": lj_compile_and_run + }.get(args.command, None) + + if not sub_func: + print_and_exit(-1, 'sub command %s is invalid.' % args.command) + + args.src = try_get_file(Path(args.src), not_exists_ok=args.command == "create") + logger.debug("args: %s" % args) + sub_func(args) if __name__ == "__main__": diff --git a/lj/utils.py b/lj/utils.py index 67ba941..33215c6 100644 --- a/lj/utils.py +++ b/lj/utils.py @@ -1,17 +1,31 @@ # -*-coding:utf-8-*- import os +import subprocess import sys +import time import datetime import json import logging import re -import time + from pathlib import Path import jsonpickle -logger = logging.getLogger() +from lj.vendors.human_bytes_converter import human2bytes + +logger = logging.getLogger("lj") + IS_WINDOWS = sys.platform == "win32" +IS_MACOS = sys.platform == "darwin" +IS_LINUX = sys.platform == "linux" + + +def print_and_exit(code, text): + if hasattr(sys, "stdout_"): + sys.stdout = sys.stdout_ + print(text) + sys.exit(code) # ref: https://stackoverflow.com/questions/4836710/does-python-have-a-built-in-function-for-string-natural-sort @@ -44,26 +58,6 @@ def get_temp_dir(src) -> Path: return temp_dir.resolve() -def load_options(): - logging.debug("loading options") - file = Path.home() / ".localjudge.json" - if file.exists(): - try: - return json.loads(read_file(str(file.resolve()), "r")) - except json.JSONDecodeError: - print(str(file) + " parse failed.") - exit() - - else: - logger.debug("config file not found!") - default_config_file = str(Path(__file__).parent / "default.localjudge.json") - default_options = json.loads(read_file(default_config_file, "r")) - # path.resolve() will raise FileNotFoundError on Py 3.5.7 if file doesn't exist. - with open(os.path.abspath(str(file)), "w") as f: - json.dump(default_options, f, indent=4, ensure_ascii=False) - return default_options - - def get_cases(data_dir): cases = map(lambda x: str(x.stem), data_dir.glob("*.in")) return natural_sort(cases) @@ -101,3 +95,71 @@ def obj_json_dumps(obj, indent=None): def diff_print_colored(source, dest): pass + + +def bytes_to_string(): + pass + + +def string_to_ms(size_str): + """ +>>> string_to_ms("1.1") + 1.1 +>>> string_to_ms("1S") + 1000.0 +>>> string_to_ms("1ms") + 1.0 +>>> string_to_ms("1.1ms") + 1.1 + """ + + if size_str[-2:].upper() == "MS": + return float(size_str[:-2]) + + if size_str[-1].upper() == "S": + return 1000 * float(size_str[:-1]) + return float(size_str) + + +def get_time_and_memory_limit(source_code): + """ + :param source_code: + :return: time_limit(ms) , memory_limit(bytes) + +>>> code = '/**' + os.linesep +>>> code += ' * Time limit: 1000MS'+ os.linesep +>>> code += ' * memoryLimit:10000K'+ os.linesep +>>> code += '**/' +>>> get_time_and_memory_limit(code) + (1000.0, 10240000) +>>> code = '#time limit: 1s'+os.linesep +>>> code += '#memorylimit: 1m'+os.linesep +>>> get_time_and_memory_limit(code) + (1000.0, 1048576) + """ + search1 = re.search(r'[Tt]ime.?[Ll]imit.?[::](.*)\n', source_code, re.M) + tl = None + ml = None + if search1: + tl = search1.group(1).strip() + tl = string_to_ms(tl) + search2 = re.search(r'[Mm]emory.?[Ll]imit.?[::](.*)\n', source_code, re.M) + if search2: + ml = search2.group(1).strip() + ml = human2bytes(ml.upper()) + ml = ml + + return tl, ml + + +def get_memory_by_ps(pid): + # windows powershell -Command "Get-Process" + out = subprocess.getoutput("ps -o rss -p %s" % pid).strip(" RS\n") + return 0 if out == "" else int(out) + + +def get_memory_by_psutil(p): + try: + return p.memory_info().rss + except: + return 0 diff --git a/lj/vendors/human_bytes_converter.py b/lj/vendors/human_bytes_converter.py new file mode 100644 index 0000000..5959c47 --- /dev/null +++ b/lj/vendors/human_bytes_converter.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python + +""" +Bytes-to-human / human-to-bytes converter. +Based on: http://goo.gl/kTQMs +Working with Python 2.x and 3.x. + +Author: Giampaolo Rodola' +License: MIT +""" + +# see: http://goo.gl/kTQMs +SYMBOLS = { + 'customary' : ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'), + 'customary_ext' : ('byte', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', + 'zetta', 'iotta'), + 'iec' : ('Bi', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), + 'iec_ext' : ('byte', 'kibi', 'mebi', 'gibi', 'tebi', 'pebi', 'exbi', + 'zebi', 'yobi'), +} + +def bytes2human(n, format='%(value).1f %(symbol)s', symbols='customary'): + """ + Convert n bytes into a human readable string based on format. + symbols can be either "customary", "customary_ext", "iec" or "iec_ext", + see: http://goo.gl/kTQMs + +>>> bytes2human(0) + '0.0 B' +>>> bytes2human(0.9) + '0.0 B' +>>> bytes2human(1) + '1.0 B' +>>> bytes2human(1.9) + '1.0 B' +>>> bytes2human(1024) + '1.0 K' +>>> bytes2human(1048576) + '1.0 M' +>>> bytes2human(1099511627776127398123789121) + '909.5 Y' + +>>> bytes2human(9856, symbols="customary") + '9.6 K' +>>> bytes2human(9856, symbols="customary_ext") + '9.6 kilo' +>>> bytes2human(9856, symbols="iec") + '9.6 Ki' +>>> bytes2human(9856, symbols="iec_ext") + '9.6 kibi' + +>>> bytes2human(10000, "%(value).1f %(symbol)s/sec") + '9.8 K/sec' + +>>> # precision can be adjusted by playing with %f operator +>>> bytes2human(10000, format="%(value).5f %(symbol)s") + '9.76562 K' + """ + n = int(n) + if n < 0: + raise ValueError("n < 0") + symbols = SYMBOLS[symbols] + prefix = {} + for i, s in enumerate(symbols[1:]): + prefix[s] = 1 << (i+1)*10 + for symbol in reversed(symbols[1:]): + if n>= prefix[symbol]: + value = float(n) / prefix[symbol] + return format % locals() + return format % dict(symbol=symbols[0], value=n) + +def human2bytes(s): + """ + Attempts to guess the string format based on default symbols + set and return the corresponding bytes as an integer. + When unable to recognize the format ValueError is raised. + +>>> human2bytes('0 B') + 0 +>>> human2bytes('1 K') + 1024 +>>> human2bytes('1 M') + 1048576 +>>> human2bytes('1 Gi') + 1073741824 +>>> human2bytes('1 tera') + 1099511627776 + +>>> human2bytes('0.5kilo') + 512 +>>> human2bytes('0.1 byte') + 0 +>>> human2bytes('1 k') # k is an alias for K + 1024 +>>> human2bytes('12 foo') + Traceback (most recent call last): + ... + ValueError: can't interpret '12 foo' + """ + init = s + num = "" + while s and s[0:1].isdigit() or s[0:1] == '.': + num += s[0] + s = s[1:] + num = float(num) + letter = s.strip() + for name, sset in SYMBOLS.items(): + if letter in sset: + break + else: + if letter == 'k': + # treat 'k' as an alias for 'K' as per: http://goo.gl/kTQMs + sset = SYMBOLS['customary'] + letter = letter.upper() + else: + raise ValueError("can't interpret %r" % init) + prefix = {sset[0]:1} + for i, s in enumerate(sset[1:]): + prefix[s] = 1 << (i+1)*10 + return int(num * prefix[letter]) + + +if __name__ == "__main__": + import doctest + doctest.testmod() \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..671827e --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +log_level=DEBUG +addopts = -v \ No newline at end of file diff --git a/tests/code/poj-1000/TLE-1s-limit-500ms.cpp b/tests/code/poj-1000/memory-AC-1M-limit-2M.cpp similarity index 54% rename from tests/code/poj-1000/TLE-1s-limit-500ms.cpp rename to tests/code/poj-1000/memory-AC-1M-limit-2M.cpp index 5acd419..aa18de6 100755 --- a/tests/code/poj-1000/TLE-1s-limit-500ms.cpp +++ b/tests/code/poj-1000/memory-AC-1M-limit-2M.cpp @@ -1,9 +1,15 @@ #include #include -// Time Limit: 500ms using namespace std; +/** + * Memory Limit: 2M + * POJ Memory: 1716K Time: 0MS +**/ + int main() { int n, m; + char *p = new char[1024*1024]; + fill(p, p + 1024 * 1024, 1); cin>> n>> m; cout << n + m << endl; sleep(1); diff --git a/tests/code/poj-1000/MLE-1M-limit-1M.cpp b/tests/code/poj-1000/memory-MLE-1M-limit-1M.cpp similarity index 100% rename from tests/code/poj-1000/MLE-1M-limit-1M.cpp rename to tests/code/poj-1000/memory-MLE-1M-limit-1M.cpp diff --git a/tests/code/poj-1000/MLE-max.cpp b/tests/code/poj-1000/memory-MLE-max-limit-1M.cpp similarity index 77% rename from tests/code/poj-1000/MLE-max.cpp rename to tests/code/poj-1000/memory-MLE-max-limit-1M.cpp index 05f5da3..700b86d 100755 --- a/tests/code/poj-1000/MLE-max.cpp +++ b/tests/code/poj-1000/memory-MLE-max-limit-1M.cpp @@ -7,6 +7,6 @@ int main() { int n, m; cin>> n>> m; cout << n + m << endl; - while (1) int *p = new int[100000000]; + while (1) char *p = new char[100000000]; return 0; } diff --git a/tests/code/poj-1000/AC-1s-limit-2s.cpp b/tests/code/poj-1000/time-AC-1s-limit-2s.cpp similarity index 100% rename from tests/code/poj-1000/AC-1s-limit-2s.cpp rename to tests/code/poj-1000/time-AC-1s-limit-2s.cpp diff --git a/tests/code/poj-1000/TLE-1s-limit-1s.cpp b/tests/code/poj-1000/time-TLE-1s-limit-1s.cpp similarity index 100% rename from tests/code/poj-1000/TLE-1s-limit-1s.cpp rename to tests/code/poj-1000/time-TLE-1s-limit-1s.cpp diff --git a/tests/code/poj-1000/TLE-endless-limit-1s.cpp b/tests/code/poj-1000/time-TLE-endless-limit-1s.cpp similarity index 100% rename from tests/code/poj-1000/TLE-endless-limit-1s.cpp rename to tests/code/poj-1000/time-TLE-endless-limit-1s.cpp diff --git a/tests/judge_status_test.py b/tests/judge_status_test.py index 4a32272..267a3da 100644 --- a/tests/judge_status_test.py +++ b/tests/judge_status_test.py @@ -5,10 +5,11 @@ import shutil from pathlib import Path -CASE_DIR = (Path(__file__).parent / "cases").resolve() +CODE_DIR = (Path(__file__).parent / "code").resolve() +POJ_1000_DIR = CODE_DIR / "poj-1000" -class LocalJudgeStatusTest(unittest.TestCase): # 继承unittest.TestCase +class JudgeStatusTest(unittest.TestCase): # 继承unittest.TestCase def tearDown(self): pass print("=" * 10) @@ -22,45 +23,58 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - directory = str(CASE_DIR / ".local_judge") - print("removing " + directory) - shutil.rmtree(directory, onerror=print) + tmp = POJ_1000_DIR / ".local_judge" + if tmp.is_dir(): + directory = str(tmp) + print("removing " + directory) + shutil.rmtree(directory, onerror=print) - def assert_status(self, src, status, time_limit=None, memory_limit=None): + def assert_status_poj_1000(self, src, status, time_limit=None, memory_limit=None): compile_result = do_compile(src) - self.assertEqual(0, compile_result.code) + self.assertEqual(0, compile_result.code, compile_result.stdout) stdin = '1 2' expected_out = '3' judge_result = do_judge_run(compile_result.runnable, stdin=stdin, expected_out=expected_out, - time_limit=None, - memory_limit=None + time_limit=time_limit, + memory_limit=memory_limit ) - print(compile_result.params["src"]) - print(obj_json_dumps(judge_result)) self.assertEqual(status, judge_result.status) + return judge_result def test_AC(self): - self.assert_status(CASE_DIR / "AC.cpp", JudgeStatus.AC) + self.assert_status_poj_1000(POJ_1000_DIR / "poj-1000.c", JudgeStatus.AC) def test_WA(self): - self.assert_status(CASE_DIR / "WA.cpp", JudgeStatus.WA) + self.assert_status_poj_1000(POJ_1000_DIR / "WA.cpp", JudgeStatus.WA) + + def test_RE(self): + self.assert_status_poj_1000(POJ_1000_DIR / "RE.cpp", JudgeStatus.RE) def test_CE(self): - compile_result = do_compile(CASE_DIR / "CE.cpp") - print(compile_result.params["src"]) - print(obj_json_dumps(compile_result)) + compile_result = do_compile(POJ_1000_DIR / "CE.cpp") self.assertNotEqual(0, compile_result.code) def test_MLE(self): - pass + path_1m = POJ_1000_DIR / "memory-MLE-1M-limit-1M.cpp" + path_max = POJ_1000_DIR / "memory-MLE-max-limit-1M.cpp" + + # NOTE: windows path_1M rss: 5341184 + self.assert_status_poj_1000(path_1m, JudgeStatus.AC, memory_limit=6341184, ) + self.assert_status_poj_1000(path_1m, JudgeStatus.MLE, memory_limit=1024 * 1024, ) + self.assert_status_poj_1000(path_max, JudgeStatus.MLE, memory_limit=2 * 1024 * 1024, ) def test_OLE(self): pass def test_TLE(self): - pass + path_1s = POJ_1000_DIR / "time-TLE-1s-limit-1s.cpp" + path_endless = POJ_1000_DIR / "time-TLE-endless-limit-1s.cpp" + + self.assert_status_poj_1000(path_1s, JudgeStatus.TLE, time_limit=999.9, ) + self.assert_status_poj_1000(path_1s, JudgeStatus.AC, time_limit=1500.1, ) + self.assert_status_poj_1000(path_endless, JudgeStatus.TLE, time_limit=1, ) if __name__ == '__main__': diff --git a/tests/lj_cli_test.py b/tests/lj_cli_test.py index 26ddfd6..296e169 100644 --- a/tests/lj_cli_test.py +++ b/tests/lj_cli_test.py @@ -5,15 +5,14 @@ from lj.judger import JudgeStatus -PROB_DIR = (Path(__file__).parent / "problems").resolve() +CODE_DIR = (Path(__file__).parent / "code").resolve() +POJ_1000_DIR = CODE_DIR / "poj-1000" +POJ_1000_DIR_STR = str(POJ_1000_DIR) -def getstatusoutput(cmd): +def getstatusoutput(cmd, cwd): try: - data = check_output(cmd, - cwd=str(PROB_DIR), - universal_newlines=True, - stderr=STDOUT) + data = check_output(cmd, cwd=cwd, universal_newlines=True, stderr=STDOUT) exitcode = 0 except CalledProcessError as ex: data = ex.output @@ -23,11 +22,12 @@ def getstatusoutput(cmd): return exitcode, data -class LocalJudgeCLITest(unittest.TestCase): +class CommandLineTest(unittest.TestCase): def check_poj_1000(self, code, data): self.assertEqual(0, code, data) lines = data.splitlines() + self.assertNotEqual(-1, lines[3].find(JudgeStatus.AC), data) self.assertNotEqual(-1, lines[4].find(JudgeStatus.AC), data) self.assertNotEqual(-1, lines[5].find(JudgeStatus.AC), data) @@ -35,39 +35,74 @@ def check_poj_1000(self, code, data): self.assertNotEqual(-1, lines[7].find(JudgeStatus.AC), data) self.assertNotEqual(-1, lines[8].find(JudgeStatus.WA), data) + def check_poj_1000_json(self, code, data): + self.assertEqual(0, code, data) + obj = json.loads(data) + + self.assertEqual(0, obj["compile"]["code"], obj) + + self.assertEqual(JudgeStatus.AC, obj["cases"][0]["status"], obj) + self.assertEqual(JudgeStatus.AC, obj["cases"][1]["status"], obj) + self.assertEqual(JudgeStatus.AC, obj["cases"][2]["status"], obj) + self.assertEqual(JudgeStatus.AC, obj["cases"][3]["status"], obj) + self.assertEqual(JudgeStatus.AC, obj["cases"][4]["status"], obj) + self.assertEqual(JudgeStatus.WA, obj["cases"][5]["status"], obj) + def test_lj_c(self): - code, data = getstatusoutput(["lj", "poj-1000.c"]) + code, data = getstatusoutput(["lj", "poj-1000.c"], cwd=POJ_1000_DIR_STR) + self.check_poj_1000(code, data) + + def test_lj_cpp(self): + code, data = getstatusoutput(["lj", "poj-1000.cpp"], cwd=POJ_1000_DIR_STR) self.check_poj_1000(code, data) def test_lj_no_ext(self): - code, data = getstatusoutput(["lj", "poj-1000"]) + code, data = getstatusoutput(["lj", "poj-1000"], cwd=POJ_1000_DIR_STR) self.check_poj_1000(code, data) def test_lj_py(self): - code, data = getstatusoutput(["lj", "poj-1000.py"]) + code, data = getstatusoutput(["lj", "poj-1000.py"], cwd=POJ_1000_DIR_STR) self.check_poj_1000(code, data) def test_lj_json(self): - code, data = getstatusoutput(["lj", "poj-1000.c", "--json"]) - obj = json.loads(data) + code, data = getstatusoutput(["lj", "poj-1000.c", "--json"], cwd=POJ_1000_DIR_STR) + self.check_poj_1000_json(code, data) + + def test_lj_run(self): + command_list = [ + ["lj", "run", "poj-1000.c"], + ["ljr", "poj-1000.c"] + ] + + for command in command_list: + p = Popen(command, universal_newlines=True, + stdin=PIPE, stdout=PIPE, cwd=str(POJ_1000_DIR)) + stdout, _ = p.communicate("1 2") + self.assertNotEqual(-1, stdout.find("Process Exit Code: 0"), stdout) + self.assertNotEqual(-1, stdout.find("3\n"), stdout) + def check_lj_json_status_poj_1000(self, src, status): + in_file = POJ_1000_DIR / "poj-1000" / "1.in" + eout_file = POJ_1000_DIR / "poj-1000" / "1.out" + commands = ["lj", str(src), "--json", + "-i", str(in_file), + "-eo", str(eout_file)] + print(" ".join(commands)) + code, data = getstatusoutput(commands, cwd=POJ_1000_DIR_STR) + print("done") + self.assertEqual(0, code, data) + obj = json.loads(data) self.assertEqual(0, obj["compile"]["code"], obj) + self.assertEqual(status, obj["cases"][0]["status"], obj) - self.assertEqual(JudgeStatus.AC, obj["cases"][0]["status"], obj) - self.assertEqual(JudgeStatus.AC, obj["cases"][1]["status"], obj) - self.assertEqual(JudgeStatus.AC, obj["cases"][2]["status"], obj) - self.assertEqual(JudgeStatus.AC, obj["cases"][3]["status"], obj) - self.assertEqual(JudgeStatus.AC, obj["cases"][4]["status"], obj) - self.assertEqual(JudgeStatus.WA, obj["cases"][5]["status"], obj) + def test_TLE_limit_in_src(self): + # 详细测试在 judge_status_test.py 中 - def test_lj_run(self): - p = Popen(["lj", "run", "poj-1000.c"], - stdin=PIPE, stdout=PIPE, cwd=str(PROB_DIR)) - stdout, _ = p.communicate("1 2".encode()) - self.assertNotEqual(-1, stdout.find(b"Process Exit Code: 0"), stdout) - self.assertNotEqual(-1, stdout.find(b"3\n"), stdout) - - p = Popen(["ljr", "poj-1000.c"], stdin=PIPE, stdout=PIPE, cwd=str(PROB_DIR)) - stdout, _ = p.communicate("1 2".encode()) - self.assertNotEqual(-1, stdout.find(b"Process Exit Code: 0"), stdout) - self.assertNotEqual(-1, stdout.find(b"3\n"), stdout) + self.check_lj_json_status_poj_1000("time-AC-1s-limit-2s.cpp", JudgeStatus.AC) + self.check_lj_json_status_poj_1000("time-TLE-1s-limit-1s.cpp", JudgeStatus.TLE) + self.check_lj_json_status_poj_1000("time-TLE-endless-limit-1s.cpp", JudgeStatus.TLE) + + def test_MLE_limit_in_src(self): + self.check_lj_json_status_poj_1000("memory-AC-1M-limit-2M.cpp", JudgeStatus.AC) + self.check_lj_json_status_poj_1000("memory-MLE-1M-limit-1M.cpp", JudgeStatus.MLE) + self.check_lj_json_status_poj_1000("memory-MLE-max-limit-1M.cpp", JudgeStatus.MLE) From f73e93ef0a0d3a0609e88e598b8019fdf94e78fd Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2019年10月28日 17:06:12 +0800 Subject: [PATCH 09/18] add github actions, update tests --- .github/workflows/linux.yml | 10 +++- .github/workflows/macos.yml | 7 ++- .github/workflows/windows.yml | 10 +++- README.md | 12 +++-- lint.sh | 2 - lj/commands/judge.py | 28 +++++----- lj/commands/run.py | 3 +- lj/default.localjudge.json | 6 +-- lj/judger.py | 51 +++++++------------ lj/lj.py | 33 +++++------- lj/utils.py | 25 ++++----- pytest.ini | 3 -- setup.cfg | 8 +++ setup.py | 4 +- ...limit-2M.cpp => memory-AC-1M-limit-5M.cpp} | 7 ++- tests/judge_status_test.py | 10 +--- tests/lj_cli_test.py | 8 +-- 17 files changed, 111 insertions(+), 116 deletions(-) delete mode 100644 lint.sh delete mode 100644 pytest.ini create mode 100644 setup.cfg rename tests/code/poj-1000/{memory-AC-1M-limit-2M.cpp => memory-AC-1M-limit-5M.cpp} (63%) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 4e32c38..08fa5fc 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -20,5 +20,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - gcc hello.c -o hello.exe - .\hello.exe + pip install . + pip install pytest flake8 + - name: Lint with flake8 + run: | + flake8 . + - name: Test + run: | + python ci.py diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 3795594..9f244d2 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -20,12 +20,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python setup.py install - pip install pytest + pip install . + pip install pytest flake8 - name: Lint with flake8 run: | - pip install flake8 - sh lint.sh + flake8 . - name: Test run: | python ci.py diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index ff84fbd..467c761 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -20,5 +20,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - gcc hello.c -o hello.exe - .\hello.exe + pip install . + pip install pytest flake8 + - name: Lint with flake8 + run: | + flake8 . + - name: Test + run: | + python ci.py diff --git a/README.md b/README.md index 83eea5a..c141ae9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Features: - 自动输入多组数据 - 自动对比输出结果 - 保留评测日志[未完成] +- OJ 出题人验题 > 刷题时,由于每个项目只能指定一个入口,每道题目都需要新建一个项目很繁琐, > LocalJudge提供`lj run hello.c` 或 `ljr hello.c`直接编译运行, @@ -28,10 +29,11 @@ Features: pip install --upgrade LocalJudge ``` -## 使用截图 +> 请确保您的Python版本>= 3.5 - +## 使用截图 +![使用截图](./screenshots/1.svg) ## 使用方法 @@ -43,9 +45,9 @@ pip install --upgrade LocalJudge 1. 创建项目 -`lj create poj-1000.c` +执行命令 `lj create poj-1000.c` 会自动创建如下所示项目文件。 -项目目录结构如下: +约定的项目目录结构如下: ``` . @@ -56,7 +58,7 @@ pip install --upgrade LocalJudge │ ├── 2.out │ ├── 3.in │ ├── 3.out -│ ├── README.md 问题描述文件 +│ ├── README.md 问题描述文件(非必需,仅供记录 └── poj-1000.c 代码文件 ``` diff --git a/lint.sh b/lint.sh deleted file mode 100644 index b1d74c1..0000000 --- a/lint.sh +++ /dev/null @@ -1,2 +0,0 @@ -set -e -flake8 . --count --exit-zero --show-source --max-complexity=10 --max-line-length=127 --statistics \ No newline at end of file diff --git a/lj/commands/judge.py b/lj/commands/judge.py index ca9332f..34f4087 100644 --- a/lj/commands/judge.py +++ b/lj/commands/judge.py @@ -1,15 +1,12 @@ # -*-coding:utf-8-*- -import os -import sys import collections import logging +import sys from pathlib import Path + import colorful from prettytable import PrettyTable -from lj import globalvar -from lj.vendors.simplediff import diff -from lj.vendors.human_bytes_converter import bytes2human from lj.judger import do_judge_run, do_compile, JudgeStatus, JudgeResultSet from lj.utils import ( get_data_dir, @@ -17,17 +14,19 @@ read_file, get_time_and_memory_limit, obj_json_dumps) +from lj.vendors.human_bytes_converter import bytes2human +from lj.vendors.simplediff import diff logger = logging.getLogger("lj") def explain_result(result, json=False): if json: - if sys.stdout_: + if hasattr(sys, "stdout_"): sys.stdout = sys.stdout_ print(obj_json_dumps(result, indent=2)) return - # TODO: 动态展示judge 结果 + # TODO: 实时展示judge 结果 # None 为不需要编译 if result.compile.code is not None and result.compile.code != 0: print(colorful.red("Compile Error")) @@ -128,7 +127,6 @@ def explain_result(result, json=False): def lj_judge(args): - if args.json: # 临时屏蔽所有stdout输出 # sys.stdout_ = sys.stdout @@ -199,18 +197,16 @@ def lj_judge(args): dest_file = result.compile.params.get("dest") if dest_file: logger.debug("delete " + dest_file) - Path(dest_file).unlink() + try: + Path(dest_file).unlink() + except Exception as e: + logger.error("delete failed.") + logger.error(e) logger.info("result:\n%s" % obj_json_dumps(result, indent=2)) - tmp_log_stream = globalvar.get("tmp_log_stream") - tmp_log_handler = globalvar.get("tmp_log_handler") - - tmp_log_handler.flush() - result_log_file = os.path.join(result.compile.params["temp_dir"], result.compile.params["stem"] + ".log") - with open(result_log_file, "w") as f: - f.write(tmp_log_stream.getvalue()) exit(0) + if __name__ == '__main__': pass diff --git a/lj/commands/run.py b/lj/commands/run.py index 46f69cc..6360807 100644 --- a/lj/commands/run.py +++ b/lj/commands/run.py @@ -28,7 +28,8 @@ def lj_compile_and_run(args): def run_with_console(command): print("Running %s" % command) - proc = subprocess.Popen(shlex.split(command, posix=not IS_WINDOWS), shell=False, stdin=sys.stdin, stdout=sys.stdout) + proc = subprocess.Popen(shlex.split(command, posix=not IS_WINDOWS), shell=False, + stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr) print("PID: %d" % proc.pid) print("-" * 20) try: diff --git a/lj/default.localjudge.json b/lj/default.localjudge.json index ee49be2..e6b27af 100644 --- a/lj/default.localjudge.json +++ b/lj/default.localjudge.json @@ -5,7 +5,7 @@ ".c" ], "dest": "${stem}${exe_if_win}", - "compile": "gcc ${src} -o ${dest}", + "compile": "gcc ${src} -o ${dest} -Wall -std=c99 -pipe", "run": "${dest}" }, { @@ -16,7 +16,7 @@ ".cc" ], "dest": "${stem}${exe_if_win}", - "compile": "g++ ${src} -o ${dest}", + "compile": "g++ ${src} -o ${dest} -Wall -std=c++14 -pipe", "run": "${dest}" }, { @@ -25,7 +25,7 @@ ".py" ], "dest": "${stem}.pyc", - "compile": "python -c \"import py_compile; py_compile.compile(r'${src}',r'${dest}')\"", + "compile": "python -c \"import py_compile; py_compile.compile('${src}','${dest}')\"", "run": "python ${dest}" }, { diff --git a/lj/judger.py b/lj/judger.py index dff155b..33dc2f0 100644 --- a/lj/judger.py +++ b/lj/judger.py @@ -1,20 +1,18 @@ # -*-coding:utf-8-*- -import logging + import os -import sys import shlex import json import subprocess import tempfile +import threading import traceback +import logging -import threading -from io import StringIO from pathlib import Path from string import Template - import psutil from lj.utils import (get_now_ms, @@ -22,8 +20,7 @@ equals_ignore_presentation_error, ignore_last_newline, get_temp_dir, - IS_WINDOWS, IS_LINUX, IS_MACOS, - read_file, get_memory_by_psutil) + IS_WINDOWS, read_file, get_memory_by_psutil) logger = logging.getLogger("lj") @@ -78,7 +75,7 @@ def __init__(self): def load_options(): - logging.debug("loading options") + logger.debug("loading options") file = Path.home() / ".localjudge.json" if file.exists(): try: @@ -110,8 +107,8 @@ def do_compile(src) -> (int, str): src_path = Path(src).resolve() temp_dir = get_temp_dir(src) lang_options = get_lang_options_from_suffix(src_path.suffix) - logging.debug("language options:") - logging.debug(lang_options) + logger.debug("language options:") + logger.debug(lang_options) compile_result = CompileResult() tpl_compile = lang_options.get("compile") @@ -170,21 +167,19 @@ def do_judge_run(command, stdin="", expected_out="", time_limit=None, memory_lim # resource.setrlimit() # https://stackoverflow.com/questions/12965023/python-subprocess-popen-communicate-equivalent-to-popen-stdout-read?r=SearchResults - # stdout_fp = tempfile.NamedTemporaryFile("w+", encoding="utf-8") - stdout_fp = open("tmp.txt","w+",encoding="utf-8") - # print(stdout_fp.name) + stdout_fp = tempfile.NamedTemporaryFile("w+", encoding="utf-8") p = subprocess.Popen(shlex.split(command, posix=not IS_WINDOWS), shell=False, universal_newlines=True, stdin=subprocess.PIPE, stdout=stdout_fp, - # stderr=sys.stderr, + stderr=subprocess.STDOUT, ) # TODO: ole check tle_kill = ole_kill = mle_kill = False mle_check = True - stdout_len = 0 - max_memory_used = -1 + + max_memory_used = 0 def memory_monitor(): @@ -204,8 +199,8 @@ def memory_monitor(): mle_kill = True p.kill() except Exception as e: - logging.debug("get memory failed") - logging.debug(e) + logger.debug("get memory failed") + logger.debug(e) def output_monitor(): pass @@ -216,21 +211,14 @@ def output_monitor(): t2 = get_now_ms() try: - - if time_limit: - def timeout_kill(): - nonlocal tle_kill # 不需要再抛异常了 - tle_kill = True - p.kill() - - # timer = Timer(time_limit / 1000.0, timeout_kill) - # timer.start() - _, _ = p.communicate(stdin - # , timeout=time_limit / 1000.0 if time_limit else None + _, _ = p.communicate(stdin, + timeout=time_limit / 1000.0 if time_limit else None ) stdout_fp.seek(0) result.output = stdout_fp.read() except subprocess.TimeoutExpired: + p.kill() + p.kill() tle_kill = True except Exception as e: logger.error(e) @@ -239,11 +227,7 @@ def timeout_kill(): print_and_exit(-1, "System Error. Please submit an issue.") finally: stdout_fp.close() - if time_limit: - timer.cancel() mle_check = False - # p.stdout.close() - # p.stderr.close() result.code = p.poll() @@ -254,6 +238,7 @@ def timeout_kill(): result.time_used = t3 - t2 result.memory_used = max_memory_used + result.output_len = len(result.output) if tle_kill: # 超时kill code 非0 必须在判RE前返回 result.status = JudgeStatus.TLE diff --git a/lj/lj.py b/lj/lj.py index bc28a1d..60802ac 100644 --- a/lj/lj.py +++ b/lj/lj.py @@ -5,30 +5,17 @@ from pathlib import Path import argparse -from io import StringIO - from lj.commands.clean import lj_clean from lj.commands.create import lj_create from lj.commands.judge import lj_judge from lj.commands.show import lj_show from lj.commands.run import lj_compile_and_run -from lj.utils import print_and_exit -from lj import globalvar +from lj.utils import print_and_exit, get_temp_dir log_format = '%(asctime)s [%(filename)s:%(lineno)d] [%(levelname)s] %(message)s' - -tmp_log_stream = StringIO() -tmp_log_handler = logging.StreamHandler(tmp_log_stream) - -logging.basicConfig(level=logging.INFO, - format=log_format, - handlers=[tmp_log_handler] - ) - +logging.basicConfig(handlers=[]) logger = logging.getLogger("lj") - -globalvar.set("tmp_log_stream", tmp_log_stream) -globalvar.set("tmp_log_handler", tmp_log_handler) +logger.setLevel(logging.INFO) def main(): @@ -49,11 +36,10 @@ def main(): if args.debug: logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler(sys.stderr) + handler = logging.StreamHandler(sys.stdout) handler.setFormatter(logging.Formatter(log_format)) logger.addHandler(handler) - - logger.debug("raw args: %s" % args) + logger.debug("raw args: %s" % args) # 尝试获取文件,可忽略后缀 def try_get_file(file, not_exists_ok=False): @@ -81,6 +67,15 @@ def try_get_file(file, not_exists_ok=False): print_and_exit(-1, 'sub command %s is invalid.' % args.command) args.src = try_get_file(Path(args.src), not_exists_ok=args.command == "create") + + tmp_dir = get_temp_dir(args.src) + logger.debug(Path(args.src).stem) + log_file = tmp_dir / ("%s.log" % Path(args.src).stem) + + log_file_handler = logging.FileHandler(str(log_file)) + log_file_handler.setFormatter(logging.Formatter(log_format)) + logger.addHandler(log_file_handler) + logger.debug("logging path: %s" % log_file) logger.debug("args: %s" % args) sub_func(args) diff --git a/lj/utils.py b/lj/utils.py index 33215c6..a51fd23 100644 --- a/lj/utils.py +++ b/lj/utils.py @@ -1,5 +1,4 @@ # -*-coding:utf-8-*- -import os import subprocess import sys import time @@ -7,6 +6,7 @@ import json import logging import re +from functools import lru_cache from pathlib import Path @@ -50,10 +50,12 @@ def get_all_temp_dir(src): return (src_path.parent / ".local_judge").glob(src_path.stem + "_*") +# NOTE: 每次运行保证返回的值一致,确保maxsize 足够大,调用次数足够少 +@lru_cache(maxsize=100) def get_temp_dir(src) -> Path: src_path = Path(src) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - temp_dir = src_path.parent / ".local_judge" / (src_path.stem + "_" + timestamp) + temp_dir = src_path.parent / ".local_judge" / str(src_path.stem) / timestamp temp_dir.mkdir(parents=True, exist_ok=True) return temp_dir.resolve() @@ -67,10 +69,9 @@ def get_now_ms(): return (time.clock() if IS_WINDOWS else time.time()) * 1000 +# 在部分系统 会多一个"\n",此处直接删除末尾所有的"\n" def ignore_last_newline(s): - if s[-1:] == '\n': # 只移除最后一个\n - return s[:-1] - return s + return s.rstrip("\n") def rstrip_each_line(s): @@ -125,15 +126,15 @@ def get_time_and_memory_limit(source_code): """ :param source_code: :return: time_limit(ms) , memory_limit(bytes) - ->>> code = '/**' + os.linesep ->>> code += ' * Time limit: 1000MS'+ os.linesep ->>> code += ' * memoryLimit:10000K'+ os.linesep +>>> from os import linesep +>>> code = '/**' + linesep +>>> code += ' * Time limit: 1000MS'+ linesep +>>> code += ' * memoryLimit:10000K'+ linesep >>> code += '**/' >>> get_time_and_memory_limit(code) (1000.0, 10240000) ->>> code = '#time limit: 1s'+os.linesep ->>> code += '#memorylimit: 1m'+os.linesep +>>> code = '#time limit: 1s' + linesep +>>> code += '#memorylimit: 1m' + linesep >>> get_time_and_memory_limit(code) (1000.0, 1048576) """ @@ -161,5 +162,5 @@ def get_memory_by_ps(pid): def get_memory_by_psutil(p): try: return p.memory_info().rss - except: + except Exception: return 0 diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 671827e..0000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -log_level=DEBUG -addopts = -v \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..c32a6aa --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[flake8] +exclude = build,tests/code,lj/vendors +max-line-length = 127 +count = true +statistics = true + +[tool:pytest] +log_level = DEBUG \ No newline at end of file diff --git a/setup.py b/setup.py index ea728c4..5768e1f 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +# -*-coding:utf-8-*- from distutils.core import setup from setuptools import find_packages @@ -17,9 +18,10 @@ url="https://github.com/NoCLin/LocalJudge", license="MIT Licence", install_requires=[ + 'psutil', 'colorful>=0.5.0', 'prettytable', - 'jsonpickle' + 'jsonpickle', ], packages=find_packages(), zip_safe=False, diff --git a/tests/code/poj-1000/memory-AC-1M-limit-2M.cpp b/tests/code/poj-1000/memory-AC-1M-limit-5M.cpp similarity index 63% rename from tests/code/poj-1000/memory-AC-1M-limit-2M.cpp rename to tests/code/poj-1000/memory-AC-1M-limit-5M.cpp index aa18de6..e292922 100755 --- a/tests/code/poj-1000/memory-AC-1M-limit-2M.cpp +++ b/tests/code/poj-1000/memory-AC-1M-limit-5M.cpp @@ -2,8 +2,11 @@ #include using namespace std; /** - * Memory Limit: 2M - * POJ Memory: 1716K Time: 0MS + * Memory Limit: 5M + * POJ: + * macOS : 1018.07ms | 1.8 M + * Windows: 1104.18ms | 3.6 M + * Linux : 1102.88ms | 3.8 M **/ int main() { diff --git a/tests/judge_status_test.py b/tests/judge_status_test.py index 267a3da..7418b34 100644 --- a/tests/judge_status_test.py +++ b/tests/judge_status_test.py @@ -1,8 +1,6 @@ from lj.judger import do_compile, do_judge_run, JudgeStatus -from lj.utils import obj_json_dumps import unittest -import shutil from pathlib import Path CODE_DIR = (Path(__file__).parent / "code").resolve() @@ -23,11 +21,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - tmp = POJ_1000_DIR / ".local_judge" - if tmp.is_dir(): - directory = str(tmp) - print("removing " + directory) - shutil.rmtree(directory, onerror=print) + pass def assert_status_poj_1000(self, src, status, time_limit=None, memory_limit=None): compile_result = do_compile(src) @@ -74,7 +68,7 @@ def test_TLE(self): self.assert_status_poj_1000(path_1s, JudgeStatus.TLE, time_limit=999.9, ) self.assert_status_poj_1000(path_1s, JudgeStatus.AC, time_limit=1500.1, ) - self.assert_status_poj_1000(path_endless, JudgeStatus.TLE, time_limit=1, ) + self.assert_status_poj_1000(path_endless, JudgeStatus.TLE, time_limit=100, ) if __name__ == '__main__': diff --git a/tests/lj_cli_test.py b/tests/lj_cli_test.py index 296e169..6647628 100644 --- a/tests/lj_cli_test.py +++ b/tests/lj_cli_test.py @@ -89,7 +89,6 @@ def check_lj_json_status_poj_1000(self, src, status): "-eo", str(eout_file)] print(" ".join(commands)) code, data = getstatusoutput(commands, cwd=POJ_1000_DIR_STR) - print("done") self.assertEqual(0, code, data) obj = json.loads(data) self.assertEqual(0, obj["compile"]["code"], obj) @@ -97,12 +96,15 @@ def check_lj_json_status_poj_1000(self, src, status): def test_TLE_limit_in_src(self): # 详细测试在 judge_status_test.py 中 - self.check_lj_json_status_poj_1000("time-AC-1s-limit-2s.cpp", JudgeStatus.AC) self.check_lj_json_status_poj_1000("time-TLE-1s-limit-1s.cpp", JudgeStatus.TLE) self.check_lj_json_status_poj_1000("time-TLE-endless-limit-1s.cpp", JudgeStatus.TLE) def test_MLE_limit_in_src(self): - self.check_lj_json_status_poj_1000("memory-AC-1M-limit-2M.cpp", JudgeStatus.AC) + self.check_lj_json_status_poj_1000("memory-AC-1M-limit-5M.cpp", JudgeStatus.AC) self.check_lj_json_status_poj_1000("memory-MLE-1M-limit-1M.cpp", JudgeStatus.MLE) self.check_lj_json_status_poj_1000("memory-MLE-max-limit-1M.cpp", JudgeStatus.MLE) + + +if __name__ == '__main__': + unittest.main() From 6cd61606f59041a84ea00b877033ac680df6f601 Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2019年10月28日 17:49:59 +0800 Subject: [PATCH 10/18] fix python compile command --- lj/default.localjudge.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lj/default.localjudge.json b/lj/default.localjudge.json index e6b27af..bda58cf 100644 --- a/lj/default.localjudge.json +++ b/lj/default.localjudge.json @@ -25,7 +25,7 @@ ".py" ], "dest": "${stem}.pyc", - "compile": "python -c \"import py_compile; py_compile.compile('${src}','${dest}')\"", + "compile": "python -c \"import py_compile; py_compile.compile(r'${src}',r'${dest}')\"", "run": "python ${dest}" }, { From feb54f53d0debad3bf8f29467c9ae6396503637a Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2019年10月28日 21:53:47 +0800 Subject: [PATCH 11/18] feat: tests now capture stdout of lj.main(argv) rather subprocess for coverage --- lj/commands/judge.py | 17 +++------- lj/lj.py | 4 ++- lj/utils.py | 2 -- tests/lj_cli_test.py | 74 +++++++++++++++++++++++++++----------------- 4 files changed, 53 insertions(+), 44 deletions(-) diff --git a/lj/commands/judge.py b/lj/commands/judge.py index 34f4087..171795c 100644 --- a/lj/commands/judge.py +++ b/lj/commands/judge.py @@ -1,11 +1,10 @@ # -*-coding:utf-8-*- import collections import logging -import sys +import warnings from pathlib import Path import colorful -from prettytable import PrettyTable from lj.judger import do_judge_run, do_compile, JudgeStatus, JudgeResultSet from lj.utils import ( @@ -19,11 +18,13 @@ logger = logging.getLogger("lj") +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + from prettytable import PrettyTable + def explain_result(result, json=False): if json: - if hasattr(sys, "stdout_"): - sys.stdout = sys.stdout_ print(obj_json_dumps(result, indent=2)) return # TODO: 实时展示judge 结果 @@ -127,13 +128,7 @@ def explain_result(result, json=False): def lj_judge(args): - if args.json: - # 临时屏蔽所有stdout输出 - # sys.stdout_ = sys.stdout - # sys.stdout = None - pass src = args.src - case_index = args.case result = JudgeResultSet() @@ -205,8 +200,6 @@ def lj_judge(args): logger.info("result:\n%s" % obj_json_dumps(result, indent=2)) - exit(0) - if __name__ == '__main__': pass diff --git a/lj/lj.py b/lj/lj.py index 60802ac..91da6ae 100644 --- a/lj/lj.py +++ b/lj/lj.py @@ -18,7 +18,9 @@ logger.setLevel(logging.INFO) -def main(): +def main(argv=None): + if argv: + sys.argv = argv parser = argparse.ArgumentParser(description="Local Judge") parser.add_argument("command", nargs="?", default="judge", help="command") parser.add_argument('src', help="source file") diff --git a/lj/utils.py b/lj/utils.py index a51fd23..f429e18 100644 --- a/lj/utils.py +++ b/lj/utils.py @@ -22,8 +22,6 @@ def print_and_exit(code, text): - if hasattr(sys, "stdout_"): - sys.stdout = sys.stdout_ print(text) sys.exit(code) diff --git a/tests/lj_cli_test.py b/tests/lj_cli_test.py index 6647628..381b41f 100644 --- a/tests/lj_cli_test.py +++ b/tests/lj_cli_test.py @@ -1,14 +1,21 @@ +import io import unittest +from contextlib import redirect_stdout from subprocess import CalledProcessError, check_output, PIPE, STDOUT, Popen from pathlib import Path import json from lj.judger import JudgeStatus +from lj import lj CODE_DIR = (Path(__file__).parent / "code").resolve() POJ_1000_DIR = CODE_DIR / "poj-1000" POJ_1000_DIR_STR = str(POJ_1000_DIR) +POJ_1000_CASE_1_PARAMS = ["--json", + "-i", str(POJ_1000_DIR / "poj-1000" / "1.in"), + "-eo", str(POJ_1000_DIR / "poj-1000" / "1.out")] + def getstatusoutput(cmd, cwd): try: @@ -22,10 +29,18 @@ def getstatusoutput(cmd, cwd): return exitcode, data +def capture_lj_command_output(commands): + print(" ".join(commands)) + + f = io.StringIO() + with redirect_stdout(f): + lj.main(commands) + return f.getvalue() + + class CommandLineTest(unittest.TestCase): - def check_poj_1000(self, code, data): - self.assertEqual(0, code, data) + def check_poj_1000(self, data): lines = data.splitlines() self.assertNotEqual(-1, lines[3].find(JudgeStatus.AC), data) @@ -35,8 +50,7 @@ def check_poj_1000(self, code, data): self.assertNotEqual(-1, lines[7].find(JudgeStatus.AC), data) self.assertNotEqual(-1, lines[8].find(JudgeStatus.WA), data) - def check_poj_1000_json(self, code, data): - self.assertEqual(0, code, data) + def check_poj_1000_json(self, data): obj = json.loads(data) self.assertEqual(0, obj["compile"]["code"], obj) @@ -49,24 +63,24 @@ def check_poj_1000_json(self, code, data): self.assertEqual(JudgeStatus.WA, obj["cases"][5]["status"], obj) def test_lj_c(self): - code, data = getstatusoutput(["lj", "poj-1000.c"], cwd=POJ_1000_DIR_STR) - self.check_poj_1000(code, data) + data = capture_lj_command_output(["lj", str(POJ_1000_DIR / "poj-1000.c")]) + self.check_poj_1000(data) def test_lj_cpp(self): - code, data = getstatusoutput(["lj", "poj-1000.cpp"], cwd=POJ_1000_DIR_STR) - self.check_poj_1000(code, data) + data = capture_lj_command_output(["lj", str(POJ_1000_DIR / "poj-1000.cpp")]) + self.check_poj_1000(data) def test_lj_no_ext(self): - code, data = getstatusoutput(["lj", "poj-1000"], cwd=POJ_1000_DIR_STR) - self.check_poj_1000(code, data) + data = capture_lj_command_output(["lj", str(POJ_1000_DIR / "poj-1000")]) + self.check_poj_1000(data) def test_lj_py(self): - code, data = getstatusoutput(["lj", "poj-1000.py"], cwd=POJ_1000_DIR_STR) - self.check_poj_1000(code, data) + data = capture_lj_command_output(["lj", str(POJ_1000_DIR / "poj-1000.py")]) + self.check_poj_1000(data) def test_lj_json(self): - code, data = getstatusoutput(["lj", "poj-1000.c", "--json"], cwd=POJ_1000_DIR_STR) - self.check_poj_1000_json(code, data) + data = capture_lj_command_output(["lj", "--json", str(POJ_1000_DIR / "poj-1000.cpp")]) + self.check_poj_1000_json(data) def test_lj_run(self): command_list = [ @@ -81,29 +95,31 @@ def test_lj_run(self): self.assertNotEqual(-1, stdout.find("Process Exit Code: 0"), stdout) self.assertNotEqual(-1, stdout.find("3\n"), stdout) - def check_lj_json_status_poj_1000(self, src, status): - in_file = POJ_1000_DIR / "poj-1000" / "1.in" - eout_file = POJ_1000_DIR / "poj-1000" / "1.out" - commands = ["lj", str(src), "--json", - "-i", str(in_file), - "-eo", str(eout_file)] - print(" ".join(commands)) - code, data = getstatusoutput(commands, cwd=POJ_1000_DIR_STR) - self.assertEqual(0, code, data) + def check_lj_json_first_status(self, data, status): obj = json.loads(data) self.assertEqual(0, obj["compile"]["code"], obj) self.assertEqual(status, obj["cases"][0]["status"], obj) def test_TLE_limit_in_src(self): # 详细测试在 judge_status_test.py 中 - self.check_lj_json_status_poj_1000("time-AC-1s-limit-2s.cpp", JudgeStatus.AC) - self.check_lj_json_status_poj_1000("time-TLE-1s-limit-1s.cpp", JudgeStatus.TLE) - self.check_lj_json_status_poj_1000("time-TLE-endless-limit-1s.cpp", JudgeStatus.TLE) + commands = ["lj", str(POJ_1000_DIR / "time-AC-1s-limit-2s.cpp")] + POJ_1000_CASE_1_PARAMS + self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.AC) + + commands = ["lj", str(POJ_1000_DIR / "time-TLE-1s-limit-1s.cpp")] + POJ_1000_CASE_1_PARAMS + self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.TLE) + + commands = ["lj", str(POJ_1000_DIR / "time-TLE-endless-limit-1s.cpp")] + POJ_1000_CASE_1_PARAMS + self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.TLE) def test_MLE_limit_in_src(self): - self.check_lj_json_status_poj_1000("memory-AC-1M-limit-5M.cpp", JudgeStatus.AC) - self.check_lj_json_status_poj_1000("memory-MLE-1M-limit-1M.cpp", JudgeStatus.MLE) - self.check_lj_json_status_poj_1000("memory-MLE-max-limit-1M.cpp", JudgeStatus.MLE) + commands = ["lj", str(POJ_1000_DIR / "memory-AC-1M-limit-5M.cpp")] + POJ_1000_CASE_1_PARAMS + self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.AC) + + commands = ["lj", str(POJ_1000_DIR / "memory-MLE-1M-limit-1M.cpp")] + POJ_1000_CASE_1_PARAMS + self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.MLE) + + commands = ["lj", str(POJ_1000_DIR / "memory-MLE-max-limit-1M.cpp")] + POJ_1000_CASE_1_PARAMS + self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.MLE) if __name__ == '__main__': From 3c3ccb360d4abeb03cc18b3a5fdb0595c3a24730 Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2019年10月28日 23:58:57 +0800 Subject: [PATCH 12/18] feat: remove java filename restrictions --- lj/default.localjudge.json | 6 +++--- lj/lj.py | 3 ++- tests/code/poj-1000/poj-1000-java/Main/1.in | 1 - tests/code/poj-1000/poj-1000-java/Main/1.out | 1 - tests/code/poj-1000/poj-1000-java/Main/2.in | 1 - tests/code/poj-1000/poj-1000-java/Main/2.out | 1 - tests/code/poj-1000/poj-1000-java/Main/3.in | 1 - tests/code/poj-1000/poj-1000-java/Main/3.out | 1 - tests/code/poj-1000/poj-1000-java/Main/test-error.in | 1 - tests/code/poj-1000/poj-1000-java/Main/test-error.out | 1 - .../poj-1000/{poj-1000-java/Main.java => poj-1000.java} | 2 +- 11 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 tests/code/poj-1000/poj-1000-java/Main/1.in delete mode 100644 tests/code/poj-1000/poj-1000-java/Main/1.out delete mode 100644 tests/code/poj-1000/poj-1000-java/Main/2.in delete mode 100644 tests/code/poj-1000/poj-1000-java/Main/2.out delete mode 100644 tests/code/poj-1000/poj-1000-java/Main/3.in delete mode 100644 tests/code/poj-1000/poj-1000-java/Main/3.out delete mode 100644 tests/code/poj-1000/poj-1000-java/Main/test-error.in delete mode 100644 tests/code/poj-1000/poj-1000-java/Main/test-error.out rename tests/code/poj-1000/{poj-1000-java/Main.java => poj-1000.java} (92%) diff --git a/lj/default.localjudge.json b/lj/default.localjudge.json index bda58cf..ac22c03 100644 --- a/lj/default.localjudge.json +++ b/lj/default.localjudge.json @@ -33,8 +33,8 @@ "extensions": [ ".java" ], - "dest": "Main.class", - "compile": "javac -J-Xms32m -J-Xmx256m Main.java", - "run": "java Main" + "dest": "${temp_dir}/Main.class", + "compile": "javac -J-Xms32m -J-Xmx256m ${src} -d ${temp_dir}", + "run": "java -classpath ${temp_dir} Main" } ] \ No newline at end of file diff --git a/lj/lj.py b/lj/lj.py index 91da6ae..e2b7c1b 100644 --- a/lj/lj.py +++ b/lj/lj.py @@ -28,7 +28,8 @@ def main(argv=None): # 指定stdio文件,避免测试时新建重复文件,必须同时存在 parser.add_argument("-i", "--in_file", help="in file", ) parser.add_argument("-eo", "--eout_file", help="expected_out file") - + # TODO: 改成指定目录 + # 废弃-i -eo 改成 -d 指定数据目录 parser.add_argument("-t", "--time_limit", type=int, default=None, help="time limit (ms)") parser.add_argument("-m", "--memory_limit", type=int, default=None, help="memory limit (MB)") parser.add_argument("-d", "--debug", dest="debug", action="store_true", help="debug mode") diff --git a/tests/code/poj-1000/poj-1000-java/Main/1.in b/tests/code/poj-1000/poj-1000-java/Main/1.in deleted file mode 100644 index 1c6ae71..0000000 --- a/tests/code/poj-1000/poj-1000-java/Main/1.in +++ /dev/null @@ -1 +0,0 @@ -1 2 \ No newline at end of file diff --git a/tests/code/poj-1000/poj-1000-java/Main/1.out b/tests/code/poj-1000/poj-1000-java/Main/1.out deleted file mode 100644 index e440e5c..0000000 --- a/tests/code/poj-1000/poj-1000-java/Main/1.out +++ /dev/null @@ -1 +0,0 @@ -3 \ No newline at end of file diff --git a/tests/code/poj-1000/poj-1000-java/Main/2.in b/tests/code/poj-1000/poj-1000-java/Main/2.in deleted file mode 100644 index e7e7d8e..0000000 --- a/tests/code/poj-1000/poj-1000-java/Main/2.in +++ /dev/null @@ -1 +0,0 @@ -3 4 \ No newline at end of file diff --git a/tests/code/poj-1000/poj-1000-java/Main/2.out b/tests/code/poj-1000/poj-1000-java/Main/2.out deleted file mode 100644 index c793025..0000000 --- a/tests/code/poj-1000/poj-1000-java/Main/2.out +++ /dev/null @@ -1 +0,0 @@ -7 \ No newline at end of file diff --git a/tests/code/poj-1000/poj-1000-java/Main/3.in b/tests/code/poj-1000/poj-1000-java/Main/3.in deleted file mode 100644 index 2a0b5e9..0000000 --- a/tests/code/poj-1000/poj-1000-java/Main/3.in +++ /dev/null @@ -1 +0,0 @@ -10 10 \ No newline at end of file diff --git a/tests/code/poj-1000/poj-1000-java/Main/3.out b/tests/code/poj-1000/poj-1000-java/Main/3.out deleted file mode 100644 index 2edeafb..0000000 --- a/tests/code/poj-1000/poj-1000-java/Main/3.out +++ /dev/null @@ -1 +0,0 @@ -20 \ No newline at end of file diff --git a/tests/code/poj-1000/poj-1000-java/Main/test-error.in b/tests/code/poj-1000/poj-1000-java/Main/test-error.in deleted file mode 100644 index 1c6ae71..0000000 --- a/tests/code/poj-1000/poj-1000-java/Main/test-error.in +++ /dev/null @@ -1 +0,0 @@ -1 2 \ No newline at end of file diff --git a/tests/code/poj-1000/poj-1000-java/Main/test-error.out b/tests/code/poj-1000/poj-1000-java/Main/test-error.out deleted file mode 100644 index 7813681..0000000 --- a/tests/code/poj-1000/poj-1000-java/Main/test-error.out +++ /dev/null @@ -1 +0,0 @@ -5 \ No newline at end of file diff --git a/tests/code/poj-1000/poj-1000-java/Main.java b/tests/code/poj-1000/poj-1000.java similarity index 92% rename from tests/code/poj-1000/poj-1000-java/Main.java rename to tests/code/poj-1000/poj-1000.java index 6a52d55..9c45013 100644 --- a/tests/code/poj-1000/poj-1000-java/Main.java +++ b/tests/code/poj-1000/poj-1000.java @@ -1,7 +1,7 @@ import java.util.Scanner; -public class Main { +class Main { public static void main(String[] args) throws Exception { Scanner scan = new Scanner(System.in); int a = scan.nextInt(); From 3902bf7b3ddc42717e56304712fc7ba440fc8361 Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2019年10月29日 16:50:44 +0800 Subject: [PATCH 13/18] feat: allow @ in filename to using one data dir for multiple source codes. add badges. cross-platform Sleep in test code. add docker support. --- .dockerignore | 8 +++ Dockerfile | 18 ++++++ README.md | 63 +++++++++++++------ docker/sources.list | 9 +++ lj/__init__.py | 2 +- lj/commands/docker.py | 21 +++++++ lj/commands/judge.py | 39 +++++------- lj/commands/run.py | 17 ++--- lj/default.localjudge.json | 4 +- lj/judger.py | 2 + lj/lj.py | 27 ++++---- lj/utils.py | 10 +-- setup.cfg | 4 +- setup.py | 8 +-- tests/code/poj-1000/test/1.in | 1 + tests/code/poj-1000/test/1.out | 1 + tests/code/poj-1000/{CE.cpp => test@CE.cpp} | 0 tests/code/poj-1000/{OLE.cpp => test@OLE.cpp} | 0 tests/code/poj-1000/{RE.cpp => test@RE.cpp} | 0 tests/code/poj-1000/{WA.cpp => test@WA.cpp} | 0 ...-5M.cpp => test@memory-AC-1M-limit-6M.cpp} | 11 +++- ...1M.cpp => test@memory-MLE-1M-limit-1M.cpp} | 7 ++- ...M.cpp => test@memory-MLE-max-limit-1M.cpp} | 0 ...it-2s.cpp => test@time-AC-1s-limit-2s.cpp} | 7 ++- ...t-1s.cpp => test@time-TLE-1s-limit-1s.cpp} | 9 ++- ...cpp => test@time-TLE-endless-limit-1s.cpp} | 8 ++- tests/judge_status_test.py | 26 ++++---- tests/lj_cli_test.py | 33 ++++++---- 28 files changed, 229 insertions(+), 106 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker/sources.list create mode 100644 lj/commands/docker.py create mode 100644 tests/code/poj-1000/test/1.in create mode 100644 tests/code/poj-1000/test/1.out rename tests/code/poj-1000/{CE.cpp => test@CE.cpp} (100%) rename tests/code/poj-1000/{OLE.cpp => test@OLE.cpp} (100%) rename tests/code/poj-1000/{RE.cpp => test@RE.cpp} (100%) rename tests/code/poj-1000/{WA.cpp => test@WA.cpp} (100%) rename tests/code/poj-1000/{memory-AC-1M-limit-5M.cpp => test@memory-AC-1M-limit-6M.cpp} (65%) rename tests/code/poj-1000/{memory-MLE-1M-limit-1M.cpp => test@memory-MLE-1M-limit-1M.cpp} (73%) rename tests/code/poj-1000/{memory-MLE-max-limit-1M.cpp => test@memory-MLE-max-limit-1M.cpp} (100%) rename tests/code/poj-1000/{time-AC-1s-limit-2s.cpp => test@time-AC-1s-limit-2s.cpp} (61%) rename tests/code/poj-1000/{time-TLE-1s-limit-1s.cpp => test@time-TLE-1s-limit-1s.cpp} (64%) rename tests/code/poj-1000/{time-TLE-endless-limit-1s.cpp => test@time-TLE-endless-limit-1s.cpp} (52%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a9754f8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +* +!docker +!lj +!tests +!ci.py +!setup.cfg +!setup.py +!README.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..838c31e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:18.04 + +ADD ./docker/sources.list /etc/apt/sources.list + +RUN apt-get update && \ + apt-get install -y software-properties-common libseccomp-dev \ + python3 python3-pip gcc g++ wget cmake make unzip && \ + add-apt-repository ppa:openjdk-r/ppa && apt-get update && apt-get install -y openjdk-8-jdk + +ADD . /lj + +#cd /lj && wget https://github.com/QingdaoU/Judger/archive/newnew.zip && \ +# unzip newnew.zip && rm newnew.zip && cd Judger-newnew && \ +# cmake . && make && make install && \ + +RUN set -ex && ln -s /usr/bin/python3 /usr/bin/python && cd /lj && pip3 install . pytest -i https://pypi.doubanio.com/simple/ && \ + pytest && \ + apt-get clean && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/* diff --git a/README.md b/README.md index c141ae9..3f38179 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ # Local Judge +![](https://github.com/NoCLin/LocalJudge/workflows/macOS/badge.svg) +![](https://github.com/NoCLin/LocalJudge/workflows/Windows/badge.svg) +![](https://github.com/NoCLin/LocalJudge/workflows/Linux/badge.svg) -## What's this? +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/LocalJudge) +![PyPI - Downloads](https://img.shields.io/pypi/dm/LocalJudge) +![PyPI](https://img.shields.io/pypi/v/LocalJudge) +![GitHub](https://img.shields.io/github/license/NoCLin/LocalJudge) Local Judge 类似于 Online Judge,但因其运行在本地,故命名为 **Local** Judge。 @@ -10,24 +16,29 @@ Local Judge 类似于 Online Judge,但因其运行在本地,故命名为 **L Features: -- 评测无需排队 -- 管理多组测试用例 -- 自动输入多组数据 -- 自动对比输出结果 -- 保留评测日志[未完成] -- OJ 出题人验题 +- 本地评测无需排队 +- **在本地文件中管理题集** +- 自动输入多组数据并对比结果 +- 提供一键编译运行命令 -> 刷题时,由于每个项目只能指定一个入口,每道题目都需要新建一个项目很繁琐, -> LocalJudge提供`lj run hello.c` 或 `ljr hello.c`直接编译运行, ->即可在IDE的一个项目里存放多个带有main函数的源文件了。 + 刷题时,由于每个项目只能指定一个入口,每道题目都需要新建一个项目很繁琐, + LocalJudge提供`lj run hello.c` 或 `ljr hello.c`直接编译运行, + 即可在IDE的一个项目里存放多个带有main函数的源文件。 -它的劣势之一是OJ实战不能使用=_=。 +- 保留每次评测历史日志 +- OJ 出题人验题 +- 语言与编译命令可扩展 +- **劣势**: **OJ实战不能使用。** ## 安装 -```bash -pip install --upgrade LocalJudge -``` +稳定版本: + +`pip install --upgrade LocalJudge` + +开发版本: + +`pip install https://github.com/NoCLin/LocalJudge/archive/dev.zip` > 请确保您的Python版本>= 3.5 @@ -69,9 +80,7 @@ pip install --upgrade LocalJudge 当然,也可以使用 `lj run poj-1000.c` 或 `ljr poj-1000.c` 手动运行。 -> 请注意:由与OJ限制,java源文件名必须为Main.java,因此请将每个项目放在独立的文件夹里。 - -> 由于IDE会报错,Java源文件无法和其他语言保持一样的文件结构(如文件夹下同时存在poj-1000.java、poj-1001.java)。 +> 请注意:java源文件名不为`Main.java`时,请不要写`public class Main`。 ## 编译器配置 @@ -82,16 +91,34 @@ ${src} 将会被预定义变量**src**替换,同理,可使用的预定义变 ``` src : 源文件路径 dest: 可执行文件路径 +temp_dir: 临时文件夹 stem: 源文件名前缀 +windows_flag: 如果为Windows系统,值为"-D_WINDOWS",否则为"" exe_if_win: 如果为Windows系统,值为".exe",否则为"" ...... ``` +## 时间与内存限制 + +内容中包含以下内容即可设置时间与内存限制: + +``` + +``` + +## 其他提示 + +`aaa.c` `aaa@fast.c` `aaa@slow.c` `aaa@xxx.c` 均使用 aaa的 +数据,无需复制多份数据,方便管理多份解答。 + ## 其他命令 ``` # 开启DEBUG模式 -lj -d poj-1000.c +lj --dubug poj-1000.c + +# 使用指定的数据文件夹 +lj -d xxx poj-1000.c # 以JSON格式输出 lj --json poj-1000.c diff --git a/docker/sources.list b/docker/sources.list new file mode 100644 index 0000000..f36debe --- /dev/null +++ b/docker/sources.list @@ -0,0 +1,9 @@ +deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse + +deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse + +deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse + +deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse + +deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse diff --git a/lj/__init__.py b/lj/__init__.py index 09d9d12..e2e193d 100644 --- a/lj/__init__.py +++ b/lj/__init__.py @@ -1 +1 @@ -__VERSION__ = (0, 0, 2) +__VERSION__ = (0, 1, 0) diff --git a/lj/commands/docker.py b/lj/commands/docker.py new file mode 100644 index 0000000..30728f1 --- /dev/null +++ b/lj/commands/docker.py @@ -0,0 +1,21 @@ +# -*-coding:utf-8-*- +import os +import subprocess +import sys + + +def lj_docker(args): + code_dir = os.getcwd() + commands = ["docker", "run", + "-it", "--rm", + "--volume", "%s:%s" % (code_dir, "/code"), + "--workdir", "/code", + "localjudge", 'bash'] + print("calling %s" % commands) + p = subprocess.Popen(commands, shell=False, + stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr) + try: + p.communicate() + except KeyboardInterrupt: + pass + print("Bye") diff --git a/lj/commands/judge.py b/lj/commands/judge.py index 171795c..4061c93 100644 --- a/lj/commands/judge.py +++ b/lj/commands/judge.py @@ -30,9 +30,11 @@ def explain_result(result, json=False): # TODO: 实时展示judge 结果 # None 为不需要编译 if result.compile.code is not None and result.compile.code != 0: - print(colorful.red("Compile Error")) + print(obj_json_dumps(result.compile, indent=2)) - exit() + print(colorful.red("Compile Error")) + print(result.compile.stdout) + return if len(result.cases) == 0: print("no cases.") return @@ -147,28 +149,17 @@ def lj_judge(args): tl, ml = (get_time_and_memory_limit(source_code)) result.time_limit = args.time_limit if args.time_limit else tl - result.memory_limit = args.memory_limit if args.memory_limit else ml - - if args.in_file and args.eout_file: - judge_cases_group = [{ - "in": args.in_file, - "eout": args.eout_file, - "name": "CommandLine" - }] - else: - if not args.in_file and not args.eout_file: - data_dir = get_data_dir(src) - case_indexes = get_cases(data_dir) if case_index is None else [case_index] - judge_cases_group = [ - { - "in": str(data_dir / (i + ".in")), - "eout": str(data_dir / (i + ".out")), - "name": i - } for i in case_indexes - ] - else: - print("in_file and eout_file are required.") - exit() + result.memory_limit = args.memory_limit * 1024 * 1024 if args.memory_limit else ml + + data_dir = get_data_dir(src, args.data_dir) + case_indexes = get_cases(data_dir) if case_index is None else [case_index] + judge_cases_group = [ + { + "in": str(data_dir / (i + ".in")), + "eout": str(data_dir / (i + ".out")), + "name": i + } for i in case_indexes + ] logger.debug("cases (%d): %s" % (len(judge_cases_group), judge_cases_group)) diff --git a/lj/commands/run.py b/lj/commands/run.py index 6360807..93f6c80 100644 --- a/lj/commands/run.py +++ b/lj/commands/run.py @@ -6,6 +6,8 @@ import subprocess import sys +import colorful + from lj.judger import do_compile from lj.utils import obj_json_dumps, IS_WINDOWS @@ -14,32 +16,31 @@ def lj_compile_and_run(args): compile_result = do_compile(args.src) - + print("compile command: %s" % compile_result.command) if compile_result.code == 0: run_with_console(compile_result.runnable) shutil.rmtree(compile_result.temp_dir) print("Removing " + compile_result.temp_dir) else: print(obj_json_dumps(compile_result, indent=2)) - print("Compile Error:\n") + print(colorful.red("Compile Error")) print(compile_result.stdout) def run_with_console(command): print("Running %s" % command) - proc = subprocess.Popen(shlex.split(command, posix=not IS_WINDOWS), shell=False, - stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr) - print("PID: %d" % proc.pid) + p = subprocess.Popen(shlex.split(command, posix=not IS_WINDOWS), shell=False, + stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr) + print("PID: %d" % p.pid) print("-" * 20) try: - while proc.poll() is None: - pass + p.communicate() except KeyboardInterrupt: pass print() print("-" * 20) - print("Process Exit Code: %s" % (str(proc.returncode))) + print("Process Exit Code: %s" % (str(p.returncode))) def main(): diff --git a/lj/default.localjudge.json b/lj/default.localjudge.json index ac22c03..ff86ac5 100644 --- a/lj/default.localjudge.json +++ b/lj/default.localjudge.json @@ -5,7 +5,7 @@ ".c" ], "dest": "${stem}${exe_if_win}", - "compile": "gcc ${src} -o ${dest} -Wall -std=c99 -pipe", + "compile": "gcc ${src} -o ${dest} -Wall -std=c99 -pipe ${flag_win} -lm", "run": "${dest}" }, { @@ -16,7 +16,7 @@ ".cc" ], "dest": "${stem}${exe_if_win}", - "compile": "g++ ${src} -o ${dest} -Wall -std=c++14 -pipe", + "compile": "g++ ${src} -o ${dest} -Wall -std=c++14 -pipe ${flag_win} -lm", "run": "${dest}" }, { diff --git a/lj/judger.py b/lj/judger.py index 33dc2f0..0dfed38 100644 --- a/lj/judger.py +++ b/lj/judger.py @@ -120,6 +120,7 @@ def do_compile(src) -> (int, str): "temp_dir": str(temp_dir), "stem": src_path.stem, "dest": None, + "flag_win": "-D_WINDOWS" if IS_WINDOWS else "", "exe_if_win": ".exe" if IS_WINDOWS else "" } # 此时文件还不存在 @@ -221,6 +222,7 @@ def output_monitor(): p.kill() tle_kill = True except Exception as e: + p.kill() logger.error(e) traceback.print_exc() diff --git a/lj/lj.py b/lj/lj.py index e2b7c1b..bc001df 100644 --- a/lj/lj.py +++ b/lj/lj.py @@ -1,6 +1,7 @@ # -*-coding:utf-8-*- import logging +import os import sys from pathlib import Path import argparse @@ -10,6 +11,7 @@ from lj.commands.judge import lj_judge from lj.commands.show import lj_show from lj.commands.run import lj_compile_and_run +from lj.commands.docker import lj_docker from lj.utils import print_and_exit, get_temp_dir log_format = '%(asctime)s [%(filename)s:%(lineno)d] [%(levelname)s] %(message)s' @@ -25,18 +27,18 @@ def main(argv=None): parser.add_argument("command", nargs="?", default="judge", help="command") parser.add_argument('src', help="source file") parser.add_argument("-c", "--case", help="index of test case") - # 指定stdio文件,避免测试时新建重复文件,必须同时存在 - parser.add_argument("-i", "--in_file", help="in file", ) - parser.add_argument("-eo", "--eout_file", help="expected_out file") - # TODO: 改成指定目录 - # 废弃-i -eo 改成 -d 指定数据目录 + parser.add_argument("-d", "--data_dir", help="data directory") parser.add_argument("-t", "--time_limit", type=int, default=None, help="time limit (ms)") parser.add_argument("-m", "--memory_limit", type=int, default=None, help="memory limit (MB)") - parser.add_argument("-d", "--debug", dest="debug", action="store_true", help="debug mode") + parser.add_argument("--debug", dest="debug", action="store_true", help="debug mode") parser.add_argument("--json", dest="json", action="store_true", help="output as json") args = parser.parse_args() + if args.src == "docker" and args.command == "judge": + lj_docker(args) + return + if args.debug: logger.setLevel(logging.DEBUG) handler = logging.StreamHandler(sys.stdout) @@ -63,7 +65,7 @@ def try_get_file(file, not_exists_ok=False): "clean": lj_clean, "create": lj_create, "show": lj_show, - "run": lj_compile_and_run + "run": lj_compile_and_run, }.get(args.command, None) if not sub_func: @@ -73,12 +75,13 @@ def try_get_file(file, not_exists_ok=False): tmp_dir = get_temp_dir(args.src) logger.debug(Path(args.src).stem) - log_file = tmp_dir / ("%s.log" % Path(args.src).stem) + if 'LJ_TEST' not in os.environ: + log_file = os.path.join(str(tmp_dir), "lj.log") + log_file_handler = logging.FileHandler(log_file) + log_file_handler.setFormatter(logging.Formatter(log_format)) + logger.addHandler(log_file_handler) + logger.debug("logging path: %s" % log_file) - log_file_handler = logging.FileHandler(str(log_file)) - log_file_handler.setFormatter(logging.Formatter(log_format)) - logger.addHandler(log_file_handler) - logger.debug("logging path: %s" % log_file) logger.debug("args: %s" % args) sub_func(args) diff --git a/lj/utils.py b/lj/utils.py index f429e18..c5964bc 100644 --- a/lj/utils.py +++ b/lj/utils.py @@ -37,9 +37,11 @@ def alphanum_key(key): return sorted(l, key=alphanum_key) -def get_data_dir(src) -> Path: +def get_data_dir(src, overwrite=None) -> Path: + if overwrite: + return Path(overwrite).resolve() src_path = Path(src) - stem = str(src_path.stem) + stem = str(src_path.stem).split("@")[0] return (src_path.parent / stem).resolve() @@ -49,7 +51,7 @@ def get_all_temp_dir(src): # NOTE: 每次运行保证返回的值一致,确保maxsize 足够大,调用次数足够少 -@lru_cache(maxsize=100) +@lru_cache(maxsize=10000) def get_temp_dir(src) -> Path: src_path = Path(src) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") @@ -64,7 +66,7 @@ def get_cases(data_dir): def get_now_ms(): - return (time.clock() if IS_WINDOWS else time.time()) * 1000 + return time.perf_counter() * 1000 # 在部分系统 会多一个"\n",此处直接删除末尾所有的"\n" diff --git a/setup.cfg b/setup.cfg index c32a6aa..772ca79 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,4 +5,6 @@ count = true statistics = true [tool:pytest] -log_level = DEBUG \ No newline at end of file +log_level = DEBUG +env = + LJ_TEST=TRUE \ No newline at end of file diff --git a/setup.py b/setup.py index 5768e1f..3f7b8b1 100644 --- a/setup.py +++ b/setup.py @@ -18,10 +18,10 @@ url="https://github.com/NoCLin/LocalJudge", license="MIT Licence", install_requires=[ - 'psutil', - 'colorful>=0.5.0', - 'prettytable', - 'jsonpickle', + 'psutil==5.6.3', + 'colorful==0.5.4', + 'prettytable==0.7.2', + 'jsonpickle==1.2', ], packages=find_packages(), zip_safe=False, diff --git a/tests/code/poj-1000/test/1.in b/tests/code/poj-1000/test/1.in new file mode 100644 index 0000000..1c6ae71 --- /dev/null +++ b/tests/code/poj-1000/test/1.in @@ -0,0 +1 @@ +1 2 \ No newline at end of file diff --git a/tests/code/poj-1000/test/1.out b/tests/code/poj-1000/test/1.out new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/tests/code/poj-1000/test/1.out @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/tests/code/poj-1000/CE.cpp b/tests/code/poj-1000/test@CE.cpp similarity index 100% rename from tests/code/poj-1000/CE.cpp rename to tests/code/poj-1000/test@CE.cpp diff --git a/tests/code/poj-1000/OLE.cpp b/tests/code/poj-1000/test@OLE.cpp similarity index 100% rename from tests/code/poj-1000/OLE.cpp rename to tests/code/poj-1000/test@OLE.cpp diff --git a/tests/code/poj-1000/RE.cpp b/tests/code/poj-1000/test@RE.cpp similarity index 100% rename from tests/code/poj-1000/RE.cpp rename to tests/code/poj-1000/test@RE.cpp diff --git a/tests/code/poj-1000/WA.cpp b/tests/code/poj-1000/test@WA.cpp similarity index 100% rename from tests/code/poj-1000/WA.cpp rename to tests/code/poj-1000/test@WA.cpp diff --git a/tests/code/poj-1000/memory-AC-1M-limit-5M.cpp b/tests/code/poj-1000/test@memory-AC-1M-limit-6M.cpp similarity index 65% rename from tests/code/poj-1000/memory-AC-1M-limit-5M.cpp rename to tests/code/poj-1000/test@memory-AC-1M-limit-6M.cpp index e292922..3854438 100755 --- a/tests/code/poj-1000/memory-AC-1M-limit-5M.cpp +++ b/tests/code/poj-1000/test@memory-AC-1M-limit-6M.cpp @@ -1,11 +1,16 @@ #include +#ifdef _WINDOWS +#include +#else #include +#define Sleep(x) usleep((x)*1000) +#endif using namespace std; /** - * Memory Limit: 5M + * Memory Limit: 6M * POJ: * macOS : 1018.07ms | 1.8 M - * Windows: 1104.18ms | 3.6 M + * Windows: 1104.18ms | 5.6 M * Linux : 1102.88ms | 3.8 M **/ @@ -15,6 +20,6 @@ int main() { fill(p, p + 1024 * 1024, 1); cin>> n>> m; cout << n + m << endl; - sleep(1); + Sleep(1000); return 0; } \ No newline at end of file diff --git a/tests/code/poj-1000/memory-MLE-1M-limit-1M.cpp b/tests/code/poj-1000/test@memory-MLE-1M-limit-1M.cpp similarity index 73% rename from tests/code/poj-1000/memory-MLE-1M-limit-1M.cpp rename to tests/code/poj-1000/test@memory-MLE-1M-limit-1M.cpp index 7bad243..018dc94 100755 --- a/tests/code/poj-1000/memory-MLE-1M-limit-1M.cpp +++ b/tests/code/poj-1000/test@memory-MLE-1M-limit-1M.cpp @@ -1,5 +1,10 @@ #include +#ifdef _WINDOWS +#include +#else #include +#define Sleep(x) usleep((x)*1000) +#endif using namespace std; /** * Memory Limit: 1M @@ -12,6 +17,6 @@ int main() { fill(p, p + 1024 * 1024, 1); cin>> n>> m; cout << n + m << endl; - sleep(1); + Sleep(1000); return 0; } diff --git a/tests/code/poj-1000/memory-MLE-max-limit-1M.cpp b/tests/code/poj-1000/test@memory-MLE-max-limit-1M.cpp similarity index 100% rename from tests/code/poj-1000/memory-MLE-max-limit-1M.cpp rename to tests/code/poj-1000/test@memory-MLE-max-limit-1M.cpp diff --git a/tests/code/poj-1000/time-AC-1s-limit-2s.cpp b/tests/code/poj-1000/test@time-AC-1s-limit-2s.cpp similarity index 61% rename from tests/code/poj-1000/time-AC-1s-limit-2s.cpp rename to tests/code/poj-1000/test@time-AC-1s-limit-2s.cpp index 3a781eb..fd36fa3 100755 --- a/tests/code/poj-1000/time-AC-1s-limit-2s.cpp +++ b/tests/code/poj-1000/test@time-AC-1s-limit-2s.cpp @@ -1,11 +1,16 @@ #include +#ifdef _WINDOWS +#include +#else #include +#define Sleep(x) usleep((x)*1000) +#endif // Time Limit: 2s using namespace std; int main() { int n, m; cin>> n>> m; cout << n + m << endl; - sleep(1); + Sleep(1000); return 0; } \ No newline at end of file diff --git a/tests/code/poj-1000/time-TLE-1s-limit-1s.cpp b/tests/code/poj-1000/test@time-TLE-1s-limit-1s.cpp similarity index 64% rename from tests/code/poj-1000/time-TLE-1s-limit-1s.cpp rename to tests/code/poj-1000/test@time-TLE-1s-limit-1s.cpp index 57b1d65..f1d9dfc 100755 --- a/tests/code/poj-1000/time-TLE-1s-limit-1s.cpp +++ b/tests/code/poj-1000/test@time-TLE-1s-limit-1s.cpp @@ -1,12 +1,17 @@ #include +#ifdef _WINDOWS +#include +#else #include +#define Sleep(x) usleep((x)*1000) +#endif +using namespace std; // Time Limit: 1s // POJ: 660K 0MS -using namespace std; int main() { int n, m; cin>> n>> m; cout << n + m << endl; - sleep(1); + Sleep(1000); return 0; } \ No newline at end of file diff --git a/tests/code/poj-1000/time-TLE-endless-limit-1s.cpp b/tests/code/poj-1000/test@time-TLE-endless-limit-1s.cpp similarity index 52% rename from tests/code/poj-1000/time-TLE-endless-limit-1s.cpp rename to tests/code/poj-1000/test@time-TLE-endless-limit-1s.cpp index d7462c6..bf2c7a0 100755 --- a/tests/code/poj-1000/time-TLE-endless-limit-1s.cpp +++ b/tests/code/poj-1000/test@time-TLE-endless-limit-1s.cpp @@ -1,10 +1,16 @@ #include +#ifdef _WINDOWS +#include +#else +#include +#define Sleep(x) usleep((x)*1000) +#endif // Time Limit: 1s using namespace std; int main() { int n, m; cin>> n>> m; cout << n + m << endl; - while(1); + while(1) Sleep(1000); return 0; } \ No newline at end of file diff --git a/tests/judge_status_test.py b/tests/judge_status_test.py index 7418b34..faa6d6a 100644 --- a/tests/judge_status_test.py +++ b/tests/judge_status_test.py @@ -1,3 +1,7 @@ +import os + +import pytest + from lj.judger import do_compile, do_judge_run, JudgeStatus import unittest @@ -5,12 +9,12 @@ CODE_DIR = (Path(__file__).parent / "code").resolve() POJ_1000_DIR = CODE_DIR / "poj-1000" +os.environ["LJ_TEST"] = "TRUE" -class JudgeStatusTest(unittest.TestCase): # 继承unittest.TestCase +class JudgeStatusTest(unittest.TestCase): def tearDown(self): pass - print("=" * 10) def setUp(self): pass @@ -41,21 +45,21 @@ def test_AC(self): self.assert_status_poj_1000(POJ_1000_DIR / "poj-1000.c", JudgeStatus.AC) def test_WA(self): - self.assert_status_poj_1000(POJ_1000_DIR / "WA.cpp", JudgeStatus.WA) + self.assert_status_poj_1000(POJ_1000_DIR / "test@WA.cpp", JudgeStatus.WA) def test_RE(self): - self.assert_status_poj_1000(POJ_1000_DIR / "RE.cpp", JudgeStatus.RE) + self.assert_status_poj_1000(POJ_1000_DIR / "test@RE.cpp", JudgeStatus.RE) def test_CE(self): - compile_result = do_compile(POJ_1000_DIR / "CE.cpp") + compile_result = do_compile(POJ_1000_DIR / "test@CE.cpp") self.assertNotEqual(0, compile_result.code) def test_MLE(self): - path_1m = POJ_1000_DIR / "memory-MLE-1M-limit-1M.cpp" - path_max = POJ_1000_DIR / "memory-MLE-max-limit-1M.cpp" + path_1m = POJ_1000_DIR / "test@memory-MLE-1M-limit-1M.cpp" + path_max = POJ_1000_DIR / "test@memory-MLE-max-limit-1M.cpp" # NOTE: windows path_1M rss: 5341184 - self.assert_status_poj_1000(path_1m, JudgeStatus.AC, memory_limit=6341184, ) + self.assert_status_poj_1000(path_1m, JudgeStatus.AC, memory_limit=6 * 1024 * 1024, ) self.assert_status_poj_1000(path_1m, JudgeStatus.MLE, memory_limit=1024 * 1024, ) self.assert_status_poj_1000(path_max, JudgeStatus.MLE, memory_limit=2 * 1024 * 1024, ) @@ -63,8 +67,8 @@ def test_OLE(self): pass def test_TLE(self): - path_1s = POJ_1000_DIR / "time-TLE-1s-limit-1s.cpp" - path_endless = POJ_1000_DIR / "time-TLE-endless-limit-1s.cpp" + path_1s = POJ_1000_DIR / "test@time-TLE-1s-limit-1s.cpp" + path_endless = POJ_1000_DIR / "test@time-TLE-endless-limit-1s.cpp" self.assert_status_poj_1000(path_1s, JudgeStatus.TLE, time_limit=999.9, ) self.assert_status_poj_1000(path_1s, JudgeStatus.AC, time_limit=1500.1, ) @@ -72,4 +76,4 @@ def test_TLE(self): if __name__ == '__main__': - unittest.main() + pytest.main() diff --git a/tests/lj_cli_test.py b/tests/lj_cli_test.py index 381b41f..dcae9e9 100644 --- a/tests/lj_cli_test.py +++ b/tests/lj_cli_test.py @@ -1,20 +1,20 @@ import io +import os import unittest from contextlib import redirect_stdout from subprocess import CalledProcessError, check_output, PIPE, STDOUT, Popen from pathlib import Path import json +import pytest + from lj.judger import JudgeStatus from lj import lj CODE_DIR = (Path(__file__).parent / "code").resolve() POJ_1000_DIR = CODE_DIR / "poj-1000" POJ_1000_DIR_STR = str(POJ_1000_DIR) - -POJ_1000_CASE_1_PARAMS = ["--json", - "-i", str(POJ_1000_DIR / "poj-1000" / "1.in"), - "-eo", str(POJ_1000_DIR / "poj-1000" / "1.out")] +os.environ["LJ_TEST"] = "TRUE" def getstatusoutput(cmd, cwd): @@ -30,8 +30,6 @@ def getstatusoutput(cmd, cwd): def capture_lj_command_output(commands): - print(" ".join(commands)) - f = io.StringIO() with redirect_stdout(f): lj.main(commands) @@ -62,6 +60,15 @@ def check_poj_1000_json(self, data): self.assertEqual(JudgeStatus.AC, obj["cases"][4]["status"], obj) self.assertEqual(JudgeStatus.WA, obj["cases"][5]["status"], obj) + @classmethod + def tearDownClass(cls): + tmp = POJ_1000_DIR / ".local_judge" + if tmp.is_dir(): + directory = str(tmp) + print("removing " + directory) + import shutil + shutil.rmtree(directory, onerror=print) + def test_lj_c(self): data = capture_lj_command_output(["lj", str(POJ_1000_DIR / "poj-1000.c")]) self.check_poj_1000(data) @@ -102,25 +109,25 @@ def check_lj_json_first_status(self, data, status): def test_TLE_limit_in_src(self): # 详细测试在 judge_status_test.py 中 - commands = ["lj", str(POJ_1000_DIR / "time-AC-1s-limit-2s.cpp")] + POJ_1000_CASE_1_PARAMS + commands = ["lj", "--json", str(POJ_1000_DIR / "test@time-AC-1s-limit-2s.cpp")] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.AC) - commands = ["lj", str(POJ_1000_DIR / "time-TLE-1s-limit-1s.cpp")] + POJ_1000_CASE_1_PARAMS + commands = ["lj", "--json", str(POJ_1000_DIR / "test@time-TLE-1s-limit-1s.cpp")] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.TLE) - commands = ["lj", str(POJ_1000_DIR / "time-TLE-endless-limit-1s.cpp")] + POJ_1000_CASE_1_PARAMS + commands = ["lj", "--json", str(POJ_1000_DIR / "test@time-TLE-endless-limit-1s.cpp")] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.TLE) def test_MLE_limit_in_src(self): - commands = ["lj", str(POJ_1000_DIR / "memory-AC-1M-limit-5M.cpp")] + POJ_1000_CASE_1_PARAMS + commands = ["lj", "--json", str(POJ_1000_DIR / "test@memory-AC-1M-limit-6M.cpp")] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.AC) - commands = ["lj", str(POJ_1000_DIR / "memory-MLE-1M-limit-1M.cpp")] + POJ_1000_CASE_1_PARAMS + commands = ["lj", "--json", str(POJ_1000_DIR / "test@memory-MLE-1M-limit-1M.cpp")] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.MLE) - commands = ["lj", str(POJ_1000_DIR / "memory-MLE-max-limit-1M.cpp")] + POJ_1000_CASE_1_PARAMS + commands = ["lj", "--json", str(POJ_1000_DIR / "test@memory-MLE-max-limit-1M.cpp")] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.MLE) if __name__ == '__main__': - unittest.main() + pytest.main() From 438630ca2494dd9ddc4f12db814b61b8927d466b Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2019年10月30日 09:16:16 +0800 Subject: [PATCH 14/18] fix: combine github actions platforms in single file (runs-on can be matrix). docs: update --- .github/workflows/macos.yml | 30 ------------------- .../{linux.yml => pythonpackage.yml} | 5 ++-- .github/workflows/windows.yml | 30 ------------------- README.md | 10 +++++-- 4 files changed, 11 insertions(+), 64 deletions(-) delete mode 100644 .github/workflows/macos.yml rename .github/workflows/{linux.yml => pythonpackage.yml} (82%) delete mode 100644 .github/workflows/windows.yml diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml deleted file mode 100644 index 9f244d2..0000000 --- a/.github/workflows/macos.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: macOS - -on: [push] - -jobs: - build: - - runs-on: macos-latest - strategy: - max-parallel: 4 - matrix: - python-version: [3.5, 3.6, 3.7] - - steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install . - pip install pytest flake8 - - name: Lint with flake8 - run: | - flake8 . - - name: Test - run: | - python ci.py diff --git a/.github/workflows/linux.yml b/.github/workflows/pythonpackage.yml similarity index 82% rename from .github/workflows/linux.yml rename to .github/workflows/pythonpackage.yml index 08fa5fc..6f94a10 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/pythonpackage.yml @@ -1,15 +1,16 @@ -name: Linux +name: Python package on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.platform }} strategy: max-parallel: 4 matrix: python-version: [3.5, 3.6, 3.7] + platform: [macos-latest, ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index 467c761..0000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Windows - -on: [push] - -jobs: - build: - - runs-on: windows-latest - strategy: - max-parallel: 4 - matrix: - python-version: [3.5, 3.6, 3.7] - - steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install . - pip install pytest flake8 - - name: Lint with flake8 - run: | - flake8 . - - name: Test - run: | - python ci.py diff --git a/README.md b/README.md index 3f38179..452766a 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Features: - 本地评测无需排队 - **在本地文件中管理题集** - 自动输入多组数据并对比结果 +- 本地时间内存限制 - 提供一键编译运行命令 刷题时,由于每个项目只能指定一个入口,每道题目都需要新建一个项目很繁琐, @@ -100,12 +101,15 @@ exe_if_win: 如果为Windows系统,值为".exe",否则为"" ## 时间与内存限制 -内容中包含以下内容即可设置时间与内存限制: +注释中包含以下内容即可设置时间与内存限制: ``` - +Memory Limit: 1M +Time Limit: 1s ``` +可以自动识别 K M G s ms等单位 + ## 其他提示 `aaa.c` `aaa@fast.c` `aaa@slow.c` `aaa@xxx.c` 均使用 aaa的 @@ -145,6 +149,8 @@ lj clean poj-1000.c # 显示某项目的内容 lj show +# 运行一个docker中的实例,自动将cwd映射为 /code,Windows / macOS 用户请注意共享文件夹权限 +lj docker ``` ## TODO From c24216013e1046b68665c0df5fb84c4a92fd9bea Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2020年1月23日 19:09:37 +0800 Subject: [PATCH 15/18] v1.0.0 feat: use python-fire rewrite CLI feat: ljr now can add additional compile flags fix: shorten overlong text --- .github/workflows/pythonpackage.yml | 52 +++++++++++------ .gitignore | 2 +- README.md | 5 +- lj/__init__.py | 56 +++++++++++++++++- lj/commands/clean.py | 20 ++++++- lj/commands/create.py | 26 ++++----- lj/commands/docker.py | 8 ++- lj/commands/judge.py | 67 +++++++++++++++------ lj/commands/run.py | 29 +++++----- lj/commands/show.py | 16 +++-- lj/{utils.py => common.py} | 51 +++++++++++++--- lj/judger.py | 34 +++++------ lj/lj.py | 90 ----------------------------- requirements-dev.txt | 3 +- setup.cfg | 9 ++- setup.py | 14 +++-- tests/judge_status_test.py | 5 +- tests/lj_cli_test.py | 32 +++++----- 18 files changed, 296 insertions(+), 223 deletions(-) rename lj/{utils.py => common.py} (74%) delete mode 100644 lj/lj.py diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6f94a10..16f238c 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -7,25 +7,41 @@ jobs: runs-on: ${{ matrix.platform }} strategy: - max-parallel: 4 matrix: - python-version: [3.5, 3.6, 3.7] + python-version: [3.5, 3.6, 3.7, 3.8] platform: [macos-latest, ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install . - pip install pytest flake8 - - name: Lint with flake8 - run: | - flake8 . - - name: Test - run: | - python ci.py + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + pip install pytest flake8 + - name: Lint with flake8 + run: | + flake8 . + - name: Test + run: | + pytest . + + # check if master + + docker_publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@master + if: {{github.ref}} == "master" + with: + name: ${{ github.repository }}/LocalJudge + username: ${{ GITHUB_ACTOR }} + password: ${{ secrets.GITHUB_TOKEN }} + registry: docker.pkg.github.com + cache: true diff --git a/.gitignore b/.gitignore index 21ac3ef..9d6d12a 100644 --- a/.gitignore +++ b/.gitignore @@ -155,7 +155,7 @@ dmypy.json .pyre/ -**/.local_judge/** +**/.lj/** windows_dist *.egg-info MANIFEST.in diff --git a/README.md b/README.md index 452766a..1318447 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # Local Judge -![](https://github.com/NoCLin/LocalJudge/workflows/macOS/badge.svg) -![](https://github.com/NoCLin/LocalJudge/workflows/Windows/badge.svg) -![](https://github.com/NoCLin/LocalJudge/workflows/Linux/badge.svg) - +![](https://github.com/NoCLin/LocalJudge/workflows/Python%20package/badge.svg) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/LocalJudge) ![PyPI - Downloads](https://img.shields.io/pypi/dm/LocalJudge) ![PyPI](https://img.shields.io/pypi/v/LocalJudge) diff --git a/lj/__init__.py b/lj/__init__.py index e2e193d..3fadbc4 100644 --- a/lj/__init__.py +++ b/lj/__init__.py @@ -1 +1,55 @@ -__VERSION__ = (0, 1, 0) +__VERSION__ = (1, 0, 0) + +import logging +import sys + +from fire import Fire + +from lj.commands.clean import lj_clean +from lj.commands.create import lj_create +from lj.commands.docker import lj_docker +from lj.commands.judge import lj_judge +from lj.commands.run import lj_compile_and_run +from lj.commands.show import lj_show +from lj.common import LOG_FORMAT + +logging.basicConfig(handlers=[]) +logger = logging.getLogger("lj") +logger.setLevel(logging.INFO) + + +class LocalJudge: + + def __init__(self, debug=False): + if debug: + logger.setLevel(logging.DEBUG) + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter(LOG_FORMAT)) + logger.addHandler(handler) + logger.debug("raw args: %s" % sys.argv) + + +LocalJudge.judge = lj_judge +LocalJudge.create = lj_create +LocalJudge.show = lj_show +LocalJudge.clean = lj_clean +LocalJudge.docker = lj_docker +LocalJudge.run = lj_compile_and_run + + +def ljc_main(): + sys.argv.insert(1, "create") + Fire(LocalJudge) + + +def ljr_main(): + # 自动省略 run 命令 + sys.argv.insert(1, "run") + Fire(LocalJudge) + + +def lj_main(): + # 未输入子命令时 默认为judge + if len(sys.argv)> 1 and not getattr(LocalJudge, sys.argv[1], None): + sys.argv.insert(1, "judge") + Fire(LocalJudge) diff --git a/lj/commands/clean.py b/lj/commands/clean.py index ff5b181..da658c6 100644 --- a/lj/commands/clean.py +++ b/lj/commands/clean.py @@ -1,11 +1,25 @@ # -*-coding:utf-8-*- import shutil +from pathlib import Path -from lj.utils import get_all_temp_dir +from lj.common import TEMP_DIR_NAME -def lj_clean(args): - temp_dirs = get_all_temp_dir(args.src) +def get_all_temp_dir(src): + src_path = Path(src) + return (src_path.parent / TEMP_DIR_NAME).glob(src_path.stem + "_*") + + +# noinspection PyUnusedLocal +def lj_clean(self, src): + """ + clean project temp files. + :param self: + :param src: + :return: + """ + temp_dirs = get_all_temp_dir(src) + for i in temp_dirs: print("clean temp dir " + str(i)) shutil.rmtree(str(i.resolve())) diff --git a/lj/commands/create.py b/lj/commands/create.py index df2f1eb..be15c50 100644 --- a/lj/commands/create.py +++ b/lj/commands/create.py @@ -1,10 +1,9 @@ # -*-coding:utf-8-*- from pathlib import Path -import argparse from sys import exit -from lj.utils import get_data_dir +from lj.common import get_data_dir def touch_not_exists_and_print(path): @@ -15,8 +14,15 @@ def touch_not_exists_and_print(path): print("create file %s" % path.resolve()) -def lj_create(args): - src_path = Path(args.src) +# noinspection PyUnusedLocal +def lj_create(self, src): + """ + create initial project files. + :param self: + :param src: + :return: + """ + src_path = Path(src) suffix = str(src_path.suffix) if not suffix: @@ -31,15 +37,3 @@ def lj_create(args): touch_list = ["1.in", "2.in", "1.out", "2.out", "README.md"] for file in touch_list: touch_not_exists_and_print(data_dir / file) - - -def main(): - parser = argparse.ArgumentParser(description="Local Judge Creator") - parser.add_argument("src", help="source file") - - args = parser.parse_args() - lj_create(args) - - -if __name__ == "__main__": - main() diff --git a/lj/commands/docker.py b/lj/commands/docker.py index 30728f1..7d37ade 100644 --- a/lj/commands/docker.py +++ b/lj/commands/docker.py @@ -4,7 +4,13 @@ import sys -def lj_docker(args): +# noinspection PyUnusedLocal +def lj_docker(self): + """ + enter docker environment. + :param self: + :return: + """ code_dir = os.getcwd() commands = ["docker", "run", "-it", "--rm", diff --git a/lj/commands/judge.py b/lj/commands/judge.py index 4061c93..438cde8 100644 --- a/lj/commands/judge.py +++ b/lj/commands/judge.py @@ -1,18 +1,20 @@ # -*-coding:utf-8-*- import collections import logging +import os +import sys import warnings from pathlib import Path import colorful -from lj.judger import do_judge_run, do_compile, JudgeStatus, JudgeResultSet -from lj.utils import ( - get_data_dir, - get_cases, - read_file, +from lj.common import ( + LOG_FORMAT, + get_data_dir, get_cases, get_temp_dir, + read_file, try_get_file, get_time_and_memory_limit, - obj_json_dumps) + obj_json_dumps, shorten_result, ) +from lj.judger import do_judge_run, do_compile, JudgeStatus, JudgeResultSet from lj.vendors.human_bytes_converter import bytes2human from lj.vendors.simplediff import diff @@ -25,12 +27,11 @@ def explain_result(result, json=False): if json: - print(obj_json_dumps(result, indent=2)) + print(obj_json_dumps(shorten_result(result), indent=2)) return # TODO: 实时展示judge 结果 # None 为不需要编译 if result.compile.code is not None and result.compile.code != 0: - print(obj_json_dumps(result.compile, indent=2)) print(colorful.red("Compile Error")) print(result.compile.stdout) @@ -107,7 +108,7 @@ def explain_result(result, json=False): .format(c=colorful, index=case.case_index, status=case.status)) - + # FIXME: shorten print("stdin:") print(case.input) colored_diff_str = "" @@ -129,9 +130,39 @@ def explain_result(result, json=False): print(colored_diff_str.format(c=colorful)) -def lj_judge(args): - src = args.src - case_index = args.case +# noinspection PyUnusedLocal +def lj_judge(self, + src, + case_index=None, + json=False, + time_limit=None, + memory_limit=None, + data_dir=None): + """ + local judge source file. + :param self: + :param src: + :param case_index: + :param json: + :param time_limit: + :param memory_limit: + :param data_dir: + :return: + """ + + tmp_dir = get_temp_dir(src) + logger.debug("Temp Dir is %s, exists: %s " % (tmp_dir, tmp_dir.exists())) + + log_file = os.path.join(str(tmp_dir), "lj.log") + logger.debug("logging path: %s" % log_file) + + log_file_handler = logging.FileHandler(log_file) + log_file_handler.setFormatter(logging.Formatter(LOG_FORMAT)) + logger.addHandler(log_file_handler) + + logger.debug("args: %s" % sys.argv) + src = try_get_file(Path(src), not_exists_ok=False) + logger.debug("src is: %s" % src) result = JudgeResultSet() result.time_limit = None @@ -139,7 +170,7 @@ def lj_judge(args): result.compile = do_compile(src) if result.compile.code is not None and result.compile.code != 0: - explain_result(result, args.json) + explain_result(result, json) return source_code = read_file(result.compile.params["src"]) @@ -148,10 +179,10 @@ def lj_judge(args): tl, ml = (get_time_and_memory_limit(source_code)) - result.time_limit = args.time_limit if args.time_limit else tl - result.memory_limit = args.memory_limit * 1024 * 1024 if args.memory_limit else ml + result.time_limit = time_limit if time_limit else tl + result.memory_limit = memory_limit * 1024 * 1024 if memory_limit else ml - data_dir = get_data_dir(src, args.data_dir) + data_dir = get_data_dir(src, data_dir) case_indexes = get_cases(data_dir) if case_index is None else [case_index] judge_cases_group = [ { @@ -178,7 +209,7 @@ def lj_judge(args): result.cases.append(case_result) - explain_result(result, args.json) + explain_result(result, json) dest_file = result.compile.params.get("dest") if dest_file: @@ -189,7 +220,7 @@ def lj_judge(args): logger.error("delete failed.") logger.error(e) - logger.info("result:\n%s" % obj_json_dumps(result, indent=2)) + logger.info("result:\n%s" % obj_json_dumps(shorten_result(result), indent=2)) if __name__ == '__main__': diff --git a/lj/commands/run.py b/lj/commands/run.py index 93f6c80..721c6f0 100644 --- a/lj/commands/run.py +++ b/lj/commands/run.py @@ -1,5 +1,4 @@ # -*-coding:utf-8-*- -import argparse import logging import shlex import shutil @@ -8,14 +7,25 @@ import colorful +from lj.common import obj_json_dumps, IS_WINDOWS, try_get_file, get_temp_dir from lj.judger import do_compile -from lj.utils import obj_json_dumps, IS_WINDOWS logger = logging.getLogger("lj") -def lj_compile_and_run(args): - compile_result = do_compile(args.src) +# noinspection PyUnusedLocal +def lj_compile_and_run(self, src, additional_compile_flags=""): + """ + directly compile and run with source file. + :param self: + :param src: + :param additional_compile_flags: + :return: + """ + tmp_dir = get_temp_dir(src) + + src = try_get_file(src) + compile_result = do_compile(src, additional_compile_flags) print("compile command: %s" % compile_result.command) if compile_result.code == 0: run_with_console(compile_result.runnable) @@ -41,14 +51,3 @@ def run_with_console(command): print() print("-" * 20) print("Process Exit Code: %s" % (str(p.returncode))) - - -def main(): - parser = argparse.ArgumentParser(description="Local Judge Runner") - parser.add_argument("src", help="source file") - args = parser.parse_args() - lj_compile_and_run(args) - - -if __name__ == "__main__": - main() diff --git a/lj/commands/show.py b/lj/commands/show.py index 0708ef3..e19a2d6 100644 --- a/lj/commands/show.py +++ b/lj/commands/show.py @@ -1,16 +1,23 @@ # -*-coding:utf-8-*- from pathlib import Path -from lj.utils import get_data_dir, get_cases, read_file +from lj.common import get_data_dir, get_cases, read_file -def lj_show(args): - src = Path(args.src) +# noinspection PyUnusedLocal +def lj_show(self, src): + """ + show project description. + :param self: + :param src: + :return: + """ + src = Path(src) data_dir = get_data_dir(src) cases = get_cases(data_dir) - print("case count:%d", len(cases)) + print("case count:%d" % len(cases)) readme_file = data_dir / "README.md" if readme_file.exists(): print(read_file(readme_file)) @@ -25,3 +32,4 @@ def lj_show(args): " " + stdin) print(" expected out:\n" + " " + expected_out) + print("-" * 10) diff --git a/lj/utils.py b/lj/common.py similarity index 74% rename from lj/utils.py rename to lj/common.py index c5964bc..866d2d3 100644 --- a/lj/utils.py +++ b/lj/common.py @@ -1,13 +1,13 @@ # -*-coding:utf-8-*- -import subprocess -import sys -import time +import copy import datetime import json import logging import re +import subprocess +import sys +import time from functools import lru_cache - from pathlib import Path import jsonpickle @@ -19,6 +19,7 @@ IS_WINDOWS = sys.platform == "win32" IS_MACOS = sys.platform == "darwin" IS_LINUX = sys.platform == "linux" +LOG_FORMAT = '%(asctime)s [%(filename)s:%(lineno)d] [%(levelname)s] %(message)s' def print_and_exit(code, text): @@ -37,6 +38,22 @@ def alphanum_key(key): return sorted(l, key=alphanum_key) +# 尝试获取文件,可忽略后缀 +def try_get_file(file, not_exists_ok=False): + file = Path(str(file)) + if file.is_dir(): + # 自动搜索后缀,忽略 .class 等非源文件 + ignore_extensions = [".class", ".exe", ".pyc"] + file_list = [i for i in file.parent.glob(file.stem + ".*") if i.suffix not in ignore_extensions] + if len(file_list): + return file_list[0] + else: + print_and_exit(-1, "自动识别后缀失败,文件不存在") + if not_exists_ok is False and not file.is_file(): + print_and_exit(-1, "file `%s` does not exist." % file) + return file + + def get_data_dir(src, overwrite=None) -> Path: if overwrite: return Path(overwrite).resolve() @@ -45,17 +62,15 @@ def get_data_dir(src, overwrite=None) -> Path: return (src_path.parent / stem).resolve() -def get_all_temp_dir(src): - src_path = Path(src) - return (src_path.parent / ".local_judge").glob(src_path.stem + "_*") +TEMP_DIR_NAME = ".lj" -# NOTE: 每次运行保证返回的值一致,确保maxsize 足够大,调用次数足够少 +# NOTE: 每次运行保证相同参数的返回值一致,确保maxsize 足够大,调用次数足够少 @lru_cache(maxsize=10000) def get_temp_dir(src) -> Path: src_path = Path(src) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - temp_dir = src_path.parent / ".local_judge" / str(src_path.stem) / timestamp + temp_dir = src_path.parent / TEMP_DIR_NAME / str(src_path.stem) / timestamp temp_dir.mkdir(parents=True, exist_ok=True) return temp_dir.resolve() @@ -87,6 +102,24 @@ def read_file(file, mode='r'): return f.read() +def shorten_str(_str, max_len=100): + return _str[:max_len] + "..." if len(_str)> max_len else _str + + +def shorten_case(c): + c.input = shorten_str(c.input) + c.output = shorten_str(c.output) + c.expected_output = shorten_str(c.expected_output) + return c + + +def shorten_result(result): + o = copy.deepcopy(result) + for c in o.cases: + c = shorten_case(c) + return o + + def obj_json_dumps(obj, indent=None): r = jsonpickle.encode(obj, unpicklable=False) if indent: diff --git a/lj/judger.py b/lj/judger.py index 0dfed38..b9a11c7 100644 --- a/lj/judger.py +++ b/lj/judger.py @@ -1,26 +1,24 @@ # -*-coding:utf-8-*- +import json +import logging import os import shlex -import json import subprocess import tempfile import threading import traceback -import logging - from pathlib import Path from string import Template import psutil -from lj.utils import (get_now_ms, - print_and_exit, - equals_ignore_presentation_error, - ignore_last_newline, - get_temp_dir, - IS_WINDOWS, read_file, get_memory_by_psutil) +from lj.common import (get_now_ms, + print_and_exit, + equals_ignore_presentation_error, + ignore_last_newline, + IS_WINDOWS, read_file, get_memory_by_psutil, shorten_str, get_temp_dir) logger = logging.getLogger("lj") @@ -75,7 +73,6 @@ def __init__(self): def load_options(): - logger.debug("loading options") file = Path.home() / ".localjudge.json" if file.exists(): try: @@ -103,15 +100,18 @@ def get_lang_options_from_suffix(suffix): exit() -def do_compile(src) -> (int, str): +def do_compile(src, additional_compile_flags=None) -> (int, str): src_path = Path(src).resolve() - temp_dir = get_temp_dir(src) + temp_dir = get_temp_dir(src_path) lang_options = get_lang_options_from_suffix(src_path.suffix) logger.debug("language options:") logger.debug(lang_options) compile_result = CompileResult() tpl_compile = lang_options.get("compile") + if additional_compile_flags: + tpl_compile += " " + additional_compile_flags + tpl_runnable = lang_options.get("run") tpl_dest = lang_options.get("dest") @@ -123,7 +123,7 @@ def do_compile(src) -> (int, str): "flag_win": "-D_WINDOWS" if IS_WINDOWS else "", "exe_if_win": ".exe" if IS_WINDOWS else "" } - # 此时文件还不存在 + # 此时文件还不存在,不要使用pathlib params["dest"] = os.path.abspath(str(temp_dir / Template(tpl_dest).substitute(params))) if tpl_compile: @@ -146,14 +146,14 @@ def do_compile(src) -> (int, str): def do_judge_run(command, stdin="", expected_out="", time_limit=None, memory_limit=None, case_index=None): logger.debug("run command: %s" % command) - logger.debug("stdin: %s" % stdin) - logger.debug("expected_out: %s" % expected_out) + logger.debug("stdin: %s" % shorten_str(stdin)) + logger.debug("expected_out: %s" % shorten_str(expected_out)) logger.debug("time limit: %s" % time_limit) logger.debug("memory limit: %s" % memory_limit) result = JudgeResult() result.command = command - result.code = -1 + result.code = None result.input = stdin result.output = "" result.expected_output = expected_out @@ -235,7 +235,7 @@ def output_monitor(): t3 = get_now_ms() logger.debug("t3: %d t3-t2: %d" % (t3, t3 - t2)) - logger.debug("stdout: %s" % result.output) + logger.debug("stdout: %s" % shorten_str(result.output)) logger.debug("code: %s" % result.code) result.time_used = t3 - t2 diff --git a/lj/lj.py b/lj/lj.py deleted file mode 100644 index bc001df..0000000 --- a/lj/lj.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*-coding:utf-8-*- - -import logging -import os -import sys -from pathlib import Path -import argparse - -from lj.commands.clean import lj_clean -from lj.commands.create import lj_create -from lj.commands.judge import lj_judge -from lj.commands.show import lj_show -from lj.commands.run import lj_compile_and_run -from lj.commands.docker import lj_docker -from lj.utils import print_and_exit, get_temp_dir - -log_format = '%(asctime)s [%(filename)s:%(lineno)d] [%(levelname)s] %(message)s' -logging.basicConfig(handlers=[]) -logger = logging.getLogger("lj") -logger.setLevel(logging.INFO) - - -def main(argv=None): - if argv: - sys.argv = argv - parser = argparse.ArgumentParser(description="Local Judge") - parser.add_argument("command", nargs="?", default="judge", help="command") - parser.add_argument('src', help="source file") - parser.add_argument("-c", "--case", help="index of test case") - parser.add_argument("-d", "--data_dir", help="data directory") - parser.add_argument("-t", "--time_limit", type=int, default=None, help="time limit (ms)") - parser.add_argument("-m", "--memory_limit", type=int, default=None, help="memory limit (MB)") - parser.add_argument("--debug", dest="debug", action="store_true", help="debug mode") - parser.add_argument("--json", dest="json", action="store_true", help="output as json") - - args = parser.parse_args() - - if args.src == "docker" and args.command == "judge": - lj_docker(args) - return - - if args.debug: - logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(logging.Formatter(log_format)) - logger.addHandler(handler) - logger.debug("raw args: %s" % args) - - # 尝试获取文件,可忽略后缀 - def try_get_file(file, not_exists_ok=False): - if file.is_dir(): - # 自动搜索后缀,忽略 .class 等非源文件 - file_list = [i for i in file.parent.glob(file.stem + ".*") if i.suffix not in [".class", ".exe", ".pyc"]] - if len(file_list): - return file_list[0] - else: - print_and_exit(-1, "自动识别后缀失败,文件不存在") - if not_exists_ok is False and not file.is_file(): - print_and_exit(-1, "file does not exist.") - return file - - # 子命令支持 - sub_func = { - "judge": lj_judge, - "clean": lj_clean, - "create": lj_create, - "show": lj_show, - "run": lj_compile_and_run, - }.get(args.command, None) - - if not sub_func: - print_and_exit(-1, 'sub command %s is invalid.' % args.command) - - args.src = try_get_file(Path(args.src), not_exists_ok=args.command == "create") - - tmp_dir = get_temp_dir(args.src) - logger.debug(Path(args.src).stem) - if 'LJ_TEST' not in os.environ: - log_file = os.path.join(str(tmp_dir), "lj.log") - log_file_handler = logging.FileHandler(log_file) - log_file_handler.setFormatter(logging.Formatter(log_format)) - logger.addHandler(log_file_handler) - logger.debug("logging path: %s" % log_file) - - logger.debug("args: %s" % args) - sub_func(args) - - -if __name__ == "__main__": - main() diff --git a/requirements-dev.txt b/requirements-dev.txt index 39395c3..5031948 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ PyInstaller flake8 termtosvg -pytest \ No newline at end of file +pytest +pytest-cov \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 772ca79..a301096 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,6 +5,13 @@ count = true statistics = true [tool:pytest] +norecursedirs = tests/code/* +addopts = --doctest-modules lj/vendors + log_level = DEBUG env = - LJ_TEST=TRUE \ No newline at end of file + LJ_TEST=TRUE + +[coverage:run] +omit = + lj/vendors/* \ No newline at end of file diff --git a/setup.py b/setup.py index 3f7b8b1..bc5ab8d 100644 --- a/setup.py +++ b/setup.py @@ -22,19 +22,25 @@ 'colorful==0.5.4', 'prettytable==0.7.2', 'jsonpickle==1.2', + 'fire==0.2.1' ], packages=find_packages(), zip_safe=False, classifiers=[ - 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent' ], package_data={ 'lj': ['*.json'], }, entry_points={ 'console_scripts': [ - 'lj = lj.lj:main', - 'ljc = lj.commands.create:main', - 'ljr = lj.commands.run:main', + 'lj = lj:lj_main', + 'ljc = lj:ljc_main', + 'ljr = lj:ljr_main', ]}, ) diff --git a/tests/judge_status_test.py b/tests/judge_status_test.py index faa6d6a..4440b1c 100644 --- a/tests/judge_status_test.py +++ b/tests/judge_status_test.py @@ -1,12 +1,11 @@ import os +import unittest +from pathlib import Path import pytest from lj.judger import do_compile, do_judge_run, JudgeStatus -import unittest -from pathlib import Path - CODE_DIR = (Path(__file__).parent / "code").resolve() POJ_1000_DIR = CODE_DIR / "poj-1000" os.environ["LJ_TEST"] = "TRUE" diff --git a/tests/lj_cli_test.py b/tests/lj_cli_test.py index dcae9e9..07e1c17 100644 --- a/tests/lj_cli_test.py +++ b/tests/lj_cli_test.py @@ -1,15 +1,13 @@ -import io +import json import os import unittest -from contextlib import redirect_stdout -from subprocess import CalledProcessError, check_output, PIPE, STDOUT, Popen from pathlib import Path -import json +from subprocess import CalledProcessError, check_output, PIPE, STDOUT, Popen import pytest +from lj.common import TEMP_DIR_NAME from lj.judger import JudgeStatus -from lj import lj CODE_DIR = (Path(__file__).parent / "code").resolve() POJ_1000_DIR = CODE_DIR / "poj-1000" @@ -30,10 +28,10 @@ def getstatusoutput(cmd, cwd): def capture_lj_command_output(commands): - f = io.StringIO() - with redirect_stdout(f): - lj.main(commands) - return f.getvalue() + p = Popen(commands, universal_newlines=True, + stdin=PIPE, stdout=PIPE, cwd=str(POJ_1000_DIR)) + stdout, _ = p.communicate() + return stdout class CommandLineTest(unittest.TestCase): @@ -62,7 +60,7 @@ def check_poj_1000_json(self, data): @classmethod def tearDownClass(cls): - tmp = POJ_1000_DIR / ".local_judge" + tmp = POJ_1000_DIR / TEMP_DIR_NAME if tmp.is_dir(): directory = str(tmp) print("removing " + directory) @@ -86,7 +84,7 @@ def test_lj_py(self): self.check_poj_1000(data) def test_lj_json(self): - data = capture_lj_command_output(["lj", "--json", str(POJ_1000_DIR / "poj-1000.cpp")]) + data = capture_lj_command_output(["lj", str(POJ_1000_DIR / "poj-1000.cpp"), "--json", ]) self.check_poj_1000_json(data) def test_lj_run(self): @@ -109,23 +107,23 @@ def check_lj_json_first_status(self, data, status): def test_TLE_limit_in_src(self): # 详细测试在 judge_status_test.py 中 - commands = ["lj", "--json", str(POJ_1000_DIR / "test@time-AC-1s-limit-2s.cpp")] + commands = ["lj", str(POJ_1000_DIR / "test@time-AC-1s-limit-2s.cpp"), "--json", ] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.AC) - commands = ["lj", "--json", str(POJ_1000_DIR / "test@time-TLE-1s-limit-1s.cpp")] + commands = ["lj", str(POJ_1000_DIR / "test@time-TLE-1s-limit-1s.cpp"), "--json", ] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.TLE) - commands = ["lj", "--json", str(POJ_1000_DIR / "test@time-TLE-endless-limit-1s.cpp")] + commands = ["lj", str(POJ_1000_DIR / "test@time-TLE-endless-limit-1s.cpp"), "--json", ] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.TLE) def test_MLE_limit_in_src(self): - commands = ["lj", "--json", str(POJ_1000_DIR / "test@memory-AC-1M-limit-6M.cpp")] + commands = ["lj", str(POJ_1000_DIR / "test@memory-AC-1M-limit-6M.cpp"), "--json", ] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.AC) - commands = ["lj", "--json", str(POJ_1000_DIR / "test@memory-MLE-1M-limit-1M.cpp")] + commands = ["lj", str(POJ_1000_DIR / "test@memory-MLE-1M-limit-1M.cpp"), "--json", ] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.MLE) - commands = ["lj", "--json", str(POJ_1000_DIR / "test@memory-MLE-max-limit-1M.cpp")] + commands = ["lj", str(POJ_1000_DIR / "test@memory-MLE-max-limit-1M.cpp"), "--json", ] self.check_lj_json_first_status(capture_lj_command_output(commands), JudgeStatus.MLE) From b70809fe1a73f94671b7640f5a98a7f4e1fc501e Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2020年1月23日 20:54:16 +0800 Subject: [PATCH 16/18] feat: add data generator `lj gin` and `lj gout` --- lj/__init__.py | 4 ++++ lj/commands/gin.py | 30 ++++++++++++++++++++++++++++++ lj/commands/gout.py | 35 +++++++++++++++++++++++++++++++++++ tests/generator/echo.py | 1 + tests/generator/randomint.py | 3 +++ tests/generator/test.sh | 2 ++ 6 files changed, 75 insertions(+) create mode 100644 lj/commands/gin.py create mode 100644 lj/commands/gout.py create mode 100644 tests/generator/echo.py create mode 100644 tests/generator/randomint.py create mode 100644 tests/generator/test.sh diff --git a/lj/__init__.py b/lj/__init__.py index 3fadbc4..5d585f5 100644 --- a/lj/__init__.py +++ b/lj/__init__.py @@ -8,6 +8,8 @@ from lj.commands.clean import lj_clean from lj.commands.create import lj_create from lj.commands.docker import lj_docker +from lj.commands.gin import lj_generator_in +from lj.commands.gout import lj_generator_out from lj.commands.judge import lj_judge from lj.commands.run import lj_compile_and_run from lj.commands.show import lj_show @@ -35,6 +37,8 @@ def __init__(self, debug=False): LocalJudge.clean = lj_clean LocalJudge.docker = lj_docker LocalJudge.run = lj_compile_and_run +LocalJudge.gin = lj_generator_in +LocalJudge.gout = lj_generator_out def ljc_main(): diff --git a/lj/commands/gin.py b/lj/commands/gin.py new file mode 100644 index 0000000..5378cc7 --- /dev/null +++ b/lj/commands/gin.py @@ -0,0 +1,30 @@ +# -*-coding:utf-8-*- + + +# noinspection PyUnusedLocal +import os +import subprocess +from pathlib import Path + + +# noinspection PyUnusedLocal +def lj_generator_in(self, in_generator_program, name, a, b): + """ + generate data by `in_generator_program`, save case to folder `name` in range(a,b+1). + :param self: + :param in_generator_program + :param name: + :param a: + :param b: + """ + for i in range(a, b + 1): + base_dir = Path(os.path.curdir) / name + base_dir.mkdir(exist_ok=True) + filename = str(base_dir / ("%d.in" % i)) + print("%s> %s" % (in_generator_program, filename)) + status, output = subprocess.getstatusoutput(in_generator_program) + if status != 0: + print("exec command `%s` got non-zero return value: %d." % (in_generator_program, status)) + return + with open(filename, "w") as f: + f.write(output) diff --git a/lj/commands/gout.py b/lj/commands/gout.py new file mode 100644 index 0000000..478a587 --- /dev/null +++ b/lj/commands/gout.py @@ -0,0 +1,35 @@ +# -*-coding:utf-8-*- +import shlex +import subprocess +from pathlib import Path + +from lj.common import IS_WINDOWS + + +# noinspection PyUnusedLocal +def lj_generator_out(self, out_generator_program, name): + """ + run `out_generator_program` to generator *.out by `name`/*.in + :param self: + :param out_generator_program: + :param name: + :return: + """ + for infile in Path(name).glob("*.in"): + with open(str(infile)) as f: + stdin = f.read() + p = subprocess.Popen(shlex.split(out_generator_program, posix=not IS_WINDOWS), shell=False, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + + stdout, _ = p.communicate(stdin) + status = p.poll() + + if status != 0: + print("exec command `%s` got non-zero return value: %d." % (out_generator_program, status)) + return + + outfile = str(infile.parent / ("%s.out" % infile.stem)) + with open(outfile, "w") as fo: + fo.write(stdout) + print("%s < %s> %s" % (out_generator_program, infile, outfile)) diff --git a/tests/generator/echo.py b/tests/generator/echo.py new file mode 100644 index 0000000..607797c --- /dev/null +++ b/tests/generator/echo.py @@ -0,0 +1 @@ +print(input()) \ No newline at end of file diff --git a/tests/generator/randomint.py b/tests/generator/randomint.py new file mode 100644 index 0000000..e1f90d0 --- /dev/null +++ b/tests/generator/randomint.py @@ -0,0 +1,3 @@ +import random + +print(random.randint(1, 100)) diff --git a/tests/generator/test.sh b/tests/generator/test.sh new file mode 100644 index 0000000..7ccba8a --- /dev/null +++ b/tests/generator/test.sh @@ -0,0 +1,2 @@ +lj gin "python randomint.py" test_project 1 5 +lj gout "python echo.py" test_project \ No newline at end of file From cacc1834590cf5f0dfc907fa889bb5f6cd8c2ff5 Mon Sep 17 00:00:00 2001 From: Junlin Liu Date: 2020年1月23日 21:18:15 +0800 Subject: [PATCH 17/18] Update pythonpackage.yml --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 16f238c..4c1424e 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -38,7 +38,7 @@ jobs: - name: Publish to Registry uses: elgohr/Publish-Docker-Github-Action@master - if: {{github.ref}} == "master" + if: "{{github.ref}} == 'master'" with: name: ${{ github.repository }}/LocalJudge username: ${{ GITHUB_ACTOR }} From 469bac5bc74bed5aed8ad03cc36491d9108dfc2f Mon Sep 17 00:00:00 2001 From: NoCLin Date: 2020年1月23日 21:26:30 +0800 Subject: [PATCH 18/18] fix action --- .github/workflows/pythonpackage.yml | 4 +- ci.py | 9 ----- lj/__init__.py | 60 ++-------------------------- lj/lj.py | 61 +++++++++++++++++++++++++++++ setup.py | 6 +-- 5 files changed, 70 insertions(+), 70 deletions(-) delete mode 100644 ci.py create mode 100644 lj/lj.py diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4c1424e..b1fd457 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -38,10 +38,10 @@ jobs: - name: Publish to Registry uses: elgohr/Publish-Docker-Github-Action@master - if: "{{github.ref}} == 'master'" + if: "github.ref == 'master'" with: name: ${{ github.repository }}/LocalJudge - username: ${{ GITHUB_ACTOR }} + username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} registry: docker.pkg.github.com cache: true diff --git a/ci.py b/ci.py deleted file mode 100644 index da781ba..0000000 --- a/ci.py +++ /dev/null @@ -1,9 +0,0 @@ -import os - -commands = [ - "pytest --doctest-modules lj{sep}utils.py lj{sep}vendors -v".format(sep=os.sep), - "pytest -v", -] -for command in commands: - print(command) - assert 0 == os.system(command) diff --git a/lj/__init__.py b/lj/__init__.py index 5d585f5..98eebc4 100644 --- a/lj/__init__.py +++ b/lj/__init__.py @@ -1,59 +1,7 @@ __VERSION__ = (1, 0, 0) -import logging -import sys +# __init__ 不要放外部依赖 避免在安装依赖之前导入了外部依赖 +if __name__ == "__main__": + from lj.lj import lj_main -from fire import Fire - -from lj.commands.clean import lj_clean -from lj.commands.create import lj_create -from lj.commands.docker import lj_docker -from lj.commands.gin import lj_generator_in -from lj.commands.gout import lj_generator_out -from lj.commands.judge import lj_judge -from lj.commands.run import lj_compile_and_run -from lj.commands.show import lj_show -from lj.common import LOG_FORMAT - -logging.basicConfig(handlers=[]) -logger = logging.getLogger("lj") -logger.setLevel(logging.INFO) - - -class LocalJudge: - - def __init__(self, debug=False): - if debug: - logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(logging.Formatter(LOG_FORMAT)) - logger.addHandler(handler) - logger.debug("raw args: %s" % sys.argv) - - -LocalJudge.judge = lj_judge -LocalJudge.create = lj_create -LocalJudge.show = lj_show -LocalJudge.clean = lj_clean -LocalJudge.docker = lj_docker -LocalJudge.run = lj_compile_and_run -LocalJudge.gin = lj_generator_in -LocalJudge.gout = lj_generator_out - - -def ljc_main(): - sys.argv.insert(1, "create") - Fire(LocalJudge) - - -def ljr_main(): - # 自动省略 run 命令 - sys.argv.insert(1, "run") - Fire(LocalJudge) - - -def lj_main(): - # 未输入子命令时 默认为judge - if len(sys.argv)> 1 and not getattr(LocalJudge, sys.argv[1], None): - sys.argv.insert(1, "judge") - Fire(LocalJudge) + lj_main() diff --git a/lj/lj.py b/lj/lj.py new file mode 100644 index 0000000..b5cdd38 --- /dev/null +++ b/lj/lj.py @@ -0,0 +1,61 @@ +import logging +import sys + +from fire import Fire + +from lj.commands.clean import lj_clean +from lj.commands.create import lj_create +from lj.commands.docker import lj_docker +from lj.commands.gin import lj_generator_in +from lj.commands.gout import lj_generator_out +from lj.commands.judge import lj_judge +from lj.commands.run import lj_compile_and_run +from lj.commands.show import lj_show +from lj.common import LOG_FORMAT + +logging.basicConfig(handlers=[]) +logger = logging.getLogger("lj") +logger.setLevel(logging.INFO) + + +class LocalJudge: + + def __init__(self, debug=False): + if debug: + logger.setLevel(logging.DEBUG) + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter(LOG_FORMAT)) + logger.addHandler(handler) + logger.debug("raw args: %s" % sys.argv) + + +LocalJudge.judge = lj_judge +LocalJudge.create = lj_create +LocalJudge.show = lj_show +LocalJudge.clean = lj_clean +LocalJudge.docker = lj_docker +LocalJudge.run = lj_compile_and_run +LocalJudge.gin = lj_generator_in +LocalJudge.gout = lj_generator_out + + +def ljc_main(): + sys.argv.insert(1, "create") + Fire(LocalJudge) + + +def ljr_main(): + # 自动省略 run 命令 + sys.argv.insert(1, "run") + Fire(LocalJudge) + + +def lj_main(): + # 未输入子命令时 默认为judge + if len(sys.argv)> 1 and not getattr(LocalJudge, sys.argv[1], None): + sys.argv.insert(1, "judge") + Fire(LocalJudge) + + +if __name__ == "__main__": + lj_main() diff --git a/setup.py b/setup.py index bc5ab8d..8e5620f 100644 --- a/setup.py +++ b/setup.py @@ -39,8 +39,8 @@ }, entry_points={ 'console_scripts': [ - 'lj = lj:lj_main', - 'ljc = lj:ljc_main', - 'ljr = lj:ljr_main', + 'lj = lj.lj:lj_main', + 'ljc = lj.lj:ljc_main', + 'ljr = lj.lj:ljr_main', ]}, )

AltStyle によって変換されたページ (->オリジナル) /