From 604d46ed0b63004376edcaec65219288229eed2a Mon Sep 17 00:00:00 2001 From: ste1hi <49638961+ste1hi@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:31:57 +0800 Subject: [PATCH 1/3] Add remote judge (#16) * Add feature. * Add test. * Add init test. * Add test. * Finish the tests, and change the output checking method. * Add documentation * Fix test failing in python 3.8. --- .gitignore | 4 +- Makefile | 8 +- OiRunner/BetterRunner.py | 42 ++++++-- OiRunner/__init__.py | 2 +- OiRunner/submit.py | 210 +++++++++++++++++++++++++++++++++++++++ OiRunner/util.py | 24 +++++ docs/judge.md | 2 +- docs/remotejudge.md | 44 ++++++++ img/find_cookies.png | Bin 0 -> 47083 bytes pyproject.toml | 1 + requirements.txt | 8 +- tests/test_basic.py | 172 ++++++++++++++------------------ tests/test_submit.py | 143 ++++++++++++++++++++++++++ tests/util.py | 93 ++++++++++++++++- 14 files changed, 640 insertions(+), 113 deletions(-) create mode 100644 OiRunner/submit.py create mode 100644 OiRunner/util.py create mode 100644 docs/remotejudge.md create mode 100644 img/find_cookies.png create mode 100644 tests/test_submit.py diff --git a/.gitignore b/.gitignore index f5839e4..11f27de 100755 --- a/.gitignore +++ b/.gitignore @@ -164,4 +164,6 @@ cython_debug/ token *.html -.vscode \ No newline at end of file +.vscode +local_* +local_*/ \ No newline at end of file diff --git a/Makefile b/Makefile index 0f2cbb0..2714d56 100755 --- a/Makefile +++ b/Makefile @@ -1,20 +1,20 @@ .PHONY: build install release clean clean-file lint test build:lint test - python3 -m build + python -m build install:build - pip3 install . + pip install . release:build - python3 -m twine upload dist/* + python -m twine upload dist/* clean:clean-file pip uninstall -y OiRunner lint: flake8 OiRunner/ tests/ --count --statistics --max-line-length=127 - mypy OiRunner/ + mypy OiRunner/ tests/ test: coverage run --source OiRunner -m unittest diff --git a/OiRunner/BetterRunner.py b/OiRunner/BetterRunner.py index 1b93a4d..3644a0c 100755 --- a/OiRunner/BetterRunner.py +++ b/OiRunner/BetterRunner.py @@ -8,6 +8,8 @@ import time from typing import Optional +from .submit import Submit + class Functions: @@ -28,6 +30,8 @@ def _modify_file(self, file_name: str, file_type: str) -> int: Raise: SystemExit -- The file is empty. + + Exitcode `11` means some file is empty. ''' i = 1 a = "" @@ -44,7 +48,7 @@ def _modify_file(self, file_name: str, file_type: str) -> int: if not a: print(f"error:{file_name} is empty.") shutil.rmtree("~tmp") - sys.exit() + sys.exit(11) with open(file_path, "w") as f: f.write(a) i += 1 @@ -52,7 +56,7 @@ def _modify_file(self, file_name: str, file_type: str) -> int: if not flag: print(f"error:{file_name} is empty.") shutil.rmtree("~tmp") - sys.exit() + sys.exit(11) if a: file_path = os.path.join("~tmp", f"{i}.{file_type}") @@ -75,7 +79,7 @@ def _output(self, num: int, opt_file: str) -> None: out_file = os.path.join("~tmp", f"{file_num}.out") with open(out_file, "r") as f: for line in f: - a += f"{line}" + a += f"{line}\n" with open(opt_file, "w") as out: out.write(a) @@ -116,13 +120,17 @@ def cmd_parse(self) -> None: pa.add_argument("filename", help="CPP file to be compiled (omitting '. cpp').") pa.add_argument("-n", "--name", default="a", help="Generate executable file name (omit '. exe').") pa.add_argument("-j", "--judge", action="store_true", help="Whether to evaluate.") - pa.add_argument("-p", "--print", action="store_true", help="Whether to print.") + pa.add_argument("-p", "--print", action="store_true", help="Whether to print details.") pa.add_argument("-if", "--inputfile", default="in.txt", help="Input file name.") pa.add_argument("-of", "--outputfile", default="out.txt", help="Output file name.") pa.add_argument("-af", "--answerfile", default="ans.txt", help="Answer file name.") pa.add_argument("-g", "--gdb", action="store_true", help="Whether to debug via gdb when the answer is incorrect.") pa.add_argument("-f", "--freopen", action="store_true", help="Add or delete freopen command.") pa.add_argument("-d", "--directgdb", action="store_true", help="Directly using gdb for debugging.") + pa.add_argument("-r", "--remote", type=str, default=None, help="The question id in luogu.") + pa.add_argument("-dO2", "--disabledO2", action="store_true", help="Disabled `O2` flag during remote judging.") + pa.add_argument("-l", "--language", type=int, default=11, + help="The ID of the programming language used during remote judging.") pa.add_argument("--onlyinput", action="store_true", help="Using file input (invalid for - j).") pa.add_argument("--onlyoutput", action="store_true", help="Using file output (invalid when - j).") self.args = pa.parse_args() @@ -138,8 +146,10 @@ def compile(self) -> None: ''' Compile files and generate executable files. - Raise: + Raise: SystemExit -- Compilation failed. + + Exitcode `1` means compilation failed. ''' try: compile = sp.Popen(["g++", self.args.filename + ".cpp", "-g", "-o", self.args.name]) @@ -148,7 +158,7 @@ def compile(self) -> None: print("Compilation successful.") else: print("Compilation failed.") - sys.exit() + sys.exit(1) # Can't sent Ctrl+c and get the messages. except KeyboardInterrupt: # pragma: no cover @@ -160,7 +170,7 @@ def _check(self, opt_file: str, ipt_file: str, ans_file: str, ''' Local evaluation and get results. - Args: + Args: opt_file -- Output file name. ipt_file -- Input file name. @@ -173,7 +183,7 @@ def _check(self, opt_file: str, ipt_file: str, ans_file: str, if_print -- Whether to output (None means use the value of the command line parameter). - Return: + Return: if_pass -- Whether to pass the test. ''' @@ -260,6 +270,14 @@ def run(self) -> None: if flag == 0 and self.args.freopen: self.func.delete_freopen(self.args.filename + ".cpp") + if flag == 0 and self.args.remote is not None: + print("\nSubmitting to remote judge.") + self.submit = Submit() + enableO2 = not self.args.disabledO2 + rid = self.submit.upload_answer(self.args.remote, self.args.filename + ".cpp", + enableO2, self.args.language) + self.submit.get_record(rid, if_show_details=self.args.print) + if self.args.gdb and flag > 0: gdb = sp.Popen(["gdb", self.args.name]) gdb.wait() @@ -281,6 +299,14 @@ def run(self) -> None: if run.returncode != 0: print(f"The return value is {run.returncode}. There may be issues with the program running.") + if self.args.remote is not None: + print("\nSubmitting to remote judge.") + self.submit = Submit() + enableO2 = not self.args.disabledO2 + rid = self.submit.upload_answer(self.args.remote, self.args.filename + ".cpp", + enableO2, self.args.language) + self.submit.get_record(rid, if_show_details=self.args.print) + # Can't sent Ctrl+c and get the messages. except KeyboardInterrupt: # pragma: no cover if os.path.exists("~tmp"): diff --git a/OiRunner/__init__.py b/OiRunner/__init__.py index 95ea0b1..bdafc4c 100755 --- a/OiRunner/__init__.py +++ b/OiRunner/__init__.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/OiRunner/submit.py b/OiRunner/submit.py new file mode 100644 index 0000000..9910366 --- /dev/null +++ b/OiRunner/submit.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +import os +import re +import sys +import time +from typing import Dict, MutableMapping, Union + +import requests + +from .util import ACCEPT, BAD_URL, CONTENT_PAT, CONTENT_TYPE, CONTENT_VALUE_PAT, MATE_TAG_PAT +from .util import PARAMS, QUESTION_URL, RECORD_URL, STATUS_CODE, SUBMIT_URL, USER_AGENT + + +class Submit: + def __init__(self) -> None: + ''' + Initialization Submit class. + + Raise: + SystemExit -- Missing the required environment variable. + + Exitcode `12` means missing the required environment variable. + ''' + client_id = os.getenv("__client_id") + uid = os.getenv("_uid") + + if client_id is None or uid is None: + print("Missing the required environment variable.('__client_id' or '_uid')") + sys.exit(12) + + self._cookies = { + '__client_id': client_id, + '_uid': uid + } + self.session = requests.Session() + self.headers: MutableMapping[str, Union[str, bytes]] = {"User-Agent": USER_AGENT} + self._csrf_token = self._get_csrf_token() + self.headers["Accept"] = ACCEPT + self.headers["content-type"] = CONTENT_TYPE + + def _get_csrf_token(self) -> str: + ''' + Get csrf-token from `luogu`. + + Return: + token -- Csrf-token. + + SystemExit -- Something was wrong during web connection. + + - Exitcode `21` means the server returned data doesn't contain meta tag. + - Exitcode `22` means there is no content in meta tag. + - Exitcode `23` means there is no value in content attribute. + + ''' + self.session.headers = self.headers + self.session.cookies.update(self._cookies) + html_content = self.session.get(BAD_URL).text + + # Get csrf-token meta tag in html file. + meta_tag_pat = re.compile(MATE_TAG_PAT) + meta_tag = re.search(meta_tag_pat, html_content) + + if meta_tag is None: + print("The server returned anomalous data (which does not contain meta tag).") + sys.exit(21) + + # Get content attribute in meta tag. + meta = meta_tag.group() + content_pat = re.compile(CONTENT_PAT) + content_result = re.search(content_pat, meta) + if content_result is None: + print("The server returned anomalous data (which does not contain 'content' in meta tag).") + sys.exit(22) + content = content_result.group() + + # Get the value of content attribute. + content_value_pat = re.compile(CONTENT_VALUE_PAT) + content_value_result = re.search(content_value_pat, content) + # It must include a quote mark. + if content_value_result is None: # pragma: no cover + print("The server returned anomalous data (which does not contain anything in the content attribute of meta tag).") + sys.exit(23) + content_value = content_value_result.group() + + token = content_value.strip('"') # Remove quotes before and after strings + + return token + + def upload_answer(self, question: str, file_path: str, + enableO2: bool = True, lang: int = 11) -> str: + ''' + Upload answer to `luogu`. + + Args: + question -- The question id in `luogu`. + + file_path -- The program file which is to be uploaded. + + enableO2 -- Whether to use `O2` flag during compiling. + + lang -- The programming language which is included in the program file. + + Return: + rid -- The `rid` of its record. + + Raise: + SystemExit -- An error occurred while uploading the answer. + + Exitcode `24` means an error occurred while uploading the answer. + ''' + question_url = QUESTION_URL + question + self.headers["x-csrf-token"] = self._csrf_token + self.headers["referer"] = question_url + url = SUBMIT_URL + question + + with open(file_path, "r") as code_file: + code = code_file.read() + + data = { + "enableO2": enableO2, + "lang": lang, + "code": code + } + + response = self.session.post(url=url, json=data) + + try: + return str(response.json()["rid"]) + except KeyError: + print(f"An error occurred while uploading the answer, with the server returning: \n {response.text}") + sys.exit(24) + + def get_record(self, rid: str, retry_interval: float = 1, + retry_count: int = -1, if_show_details: bool = False) -> None: + ''' + Get the judge result from the `rid`. + + Args: + rid -- The record id. + + retry_interval -- The delay time between two request. The unit is second. + + retry_count -- Maximum number of retries. + + if_show_details -- Whether to show the details of record. + ''' + record_url = RECORD_URL + rid + self.headers.pop("referer", None) + self.headers.pop("content-type", None) + + counter = 0 + while True: + counter += 1 + time.sleep(retry_interval) + if retry_count != -1 and counter > retry_count: + return None + + record = self.session.get(url=record_url, params=PARAMS) + data = record.json()["currentData"] + judge_result: Dict[str, dict] = data["record"]["detail"]["judgeResult"] + test_case_groups: dict = data["testCaseGroup"] + case_counter = 0 + + for test_case_group in test_case_groups: + case_counter += len(test_case_group) + + if case_counter == judge_result["finishedCaseCount"]: + break + + total_score = 0 + if type(judge_result["subtasks"]) is list: + subtasks: Union[list, dict] = judge_result["subtasks"] + else: + subtasks = list(judge_result["subtasks"].values()) + for subtask in subtasks: + total_score += subtask["score"] + + if if_show_details: + print("\ndetails:") + summary = "" + for subtask in subtasks: + testcases = subtask["testCases"] + subtask_id = subtask["id"] + if type(test_case_groups) is list: + subtask_cases_id = test_case_groups[subtask_id] + else: + subtask_cases_id = test_case_groups[str(subtask_id)] + + if subtask_id == 1: # If the id of subtask is 1, the testcases in this subtask are in a list. + for i in range(len(subtask_cases_id)): + score = testcases[i]["score"] + description = testcases[i]["description"] + status = testcases[i]["status"] + if if_show_details: + print(f"Case:{i+1}\nscore:{score}\nstatus:{STATUS_CODE[status]}\ndescription:{description}\n") + else: + summary += f"{STATUS_CODE[status]}|" + else: + for i in subtask_cases_id: + score = testcases[str(i)]["score"] + description = testcases[str(i)]["description"] + status = testcases[str(i)]["status"] + if if_show_details: + print(f"Case:{i+1}\nscore:{score}\nstatus:{STATUS_CODE[status]}\ndescription:{description}\n") + else: + summary += f"{STATUS_CODE[status]}|" + summary = summary[:-1] + print("summary:") + print(total_score) + print(summary) diff --git a/OiRunner/util.py b/OiRunner/util.py new file mode 100644 index 0000000..7121ee2 --- /dev/null +++ b/OiRunner/util.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +BAD_URL = r"https://www.luogu.com.cn/404" +SUBMIT_URL = r"https://www.luogu.com.cn/fe/api/problem/submit/" +QUESTION_URL = r"https://www.luogu.com.cn/problem/" +RECORD_URL = r"https://www.luogu.com.cn/record/" + + +USER_AGENT = ("Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit" + "/537.36 (KHTML, like Gecko) Chrome/94.0.4606.54 Safari/537.36") + +MATE_TAG_PAT = r'' +CONTENT_PAT = r'content="(([\s\S])*?)"' +CONTENT_VALUE_PAT = r'"(([\s\S])*?)"' + +ACCEPT = "application/json, text/plain, */*" +CONTENT_TYPE = "application/json" +PARAMS = {'_contentOnly': '1'} + +STATUS_CODE = { + 12: "AC", + 5: "TLE", + 7: "RE", + 6: "WA" + } diff --git a/docs/judge.md b/docs/judge.md index 7e0844d..1aeef42 100644 --- a/docs/judge.md +++ b/docs/judge.md @@ -1,6 +1,6 @@ # Judge program result -You can add `-j` or `--judge` flag to enable the judging feature. +You can add `-j` or `--judge` flag to enable the judging feature. You can also add `-r` or `--remote` flag to judge your program on remote platform. [(see details)](./remotejudge.md) ## File directory structure You need four files in your project directory. diff --git a/docs/remotejudge.md b/docs/remotejudge.md new file mode 100644 index 0000000..50ca86c --- /dev/null +++ b/docs/remotejudge.md @@ -0,0 +1,44 @@ +# Remote Judge +You can add `-r` or `--remote` flag to submit your program to online judge platform. + +> [!NOTE] +> This feature is currently supported only on the `luogu` platform. + +## Usage + +### Get uid and client_id +They can be found in your browser cookies after logging in to `luogu`. + +This is an example of how to use chrome to find them. + +1. Logging in to `luogu`. +2. Then, press `F12` to open the `DevTools`. +3. You will find two cookies in the `application` tab. + +![find_cookies](../img/find_cookies.png) + +### Set environment variable + +You need to set two environment variables that named `_uid` and `__client_id`. The value of them are your cookies value. + +#### Linux +In Linux, you can execute the following commands. +```bash +export _uid= +export __client_id= +``` + +#### Windows +In Windows, you can add variables via the System Properties. + +### Add OiRunner flag +Add `-r ` or `--remote ` to submit to `luogu`. + +For example: +```bash +oirun 1 -r P1001 +``` +This command will submit `1.cpp` to `P1001` question on `luogu`. + +## Copyright +We respect the copyright of other Online Judges' questions. We don't provide judge services. Users use OiRunner to submit questions just like they do the same in browser. \ No newline at end of file diff --git a/img/find_cookies.png b/img/find_cookies.png new file mode 100644 index 0000000000000000000000000000000000000000..afeaeedf52427ca086d181f06e2c3224e029001d GIT binary patch literal 47083 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 eRR+yow_Sebt8g!_rmxdL<+`V Date: Sat, 15 Jun 2024 17:07:12 +0800 Subject: [PATCH 2/3] Change the output checking method in test_func.py. (#18) * Change the output checking method in test_func.py. Use redirect_stdout method in test_func.py. Change the `venv/` into `venv*/` in .gitignore. * Delete a package that is not used in test_func.py --- .gitignore | 2 +- tests/test_func.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 11f27de..5485bba 100755 --- a/.gitignore +++ b/.gitignore @@ -125,7 +125,7 @@ celerybeat.pid .env .venv env/ -venv/ +venv*/ ENV/ env.bak/ venv.bak/ diff --git a/tests/test_func.py b/tests/test_func.py index dea8d45..77cc10f 100644 --- a/tests/test_func.py +++ b/tests/test_func.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- +from contextlib import redirect_stdout +import io import unittest import os -import sys import shutil from OiRunner import BetterRunner from .util import GARBAGE, FILEOUT, FREOPEN, clean @@ -19,15 +20,12 @@ def tearDown(self): def test_modify_file(self): for i in range(1, 3): - out = sys.stdout with self.assertRaises(SystemExit): - with open("~temp", "w") as f: - sys.stdout = f + with io.StringIO() as buf, redirect_stdout(buf): self.func._modify_file(f"empty{i}.in", "in") - self.assertFalse(os.path.exists("~tmp")) - with open("~temp", "r") as f: - self.assertEqual(f.read(), f"error:empty{i}.in is empty.\n") - sys.stdout = out + self.assertFalse(os.path.exists("~tmp")) + output = buf.getvalue() + self.assertEqual(output, f"error:empty{i}.in is empty.\n") ret_code = self.func._modify_file("a.in", "in") self.assertEqual(ret_code, 3) From 4b1e776ce5365827a632c31231f63d07165c3633 Mon Sep 17 00:00:00 2001 From: ste1hi <49638961+ste1hi@users.noreply.github.com> Date: Sun, 16 Jun 2024 16:08:43 +0800 Subject: [PATCH 3/3] Add log. (#20) * Add log. * Support python 3.8. * Fix failed test in python38. Change directory sturcture. Add test for log.py. * Delete Tools folder. --- Makefile | 2 +- OiRunner/BetterRunner.py | 35 ++++++++++++++++++++++++++++++++--- OiRunner/submit.py | 29 +++++++++++++++++++++++++++-- OiRunner/tools/__init__.py | 1 + OiRunner/tools/log.py | 29 +++++++++++++++++++++++++++++ tests/test_func.py | 1 + tests/test_submit.py | 1 + tests/test_tools.py | 37 +++++++++++++++++++++++++++++++++++++ 8 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 OiRunner/tools/__init__.py create mode 100644 OiRunner/tools/log.py create mode 100644 tests/test_tools.py diff --git a/Makefile b/Makefile index 2714d56..e725b45 100755 --- a/Makefile +++ b/Makefile @@ -21,4 +21,4 @@ test: coverage report clean-file: - rm -rf dist build *.egg-info htmlcov .coverage .mypy_cache OiRunner/__pycache__ tests/__pycache__ \ No newline at end of file + rm -rf dist build *.egg-info htmlcov .coverage .mypy_cache OiRunner/__pycache__ OiRunner/tools/__pycache__ tests/__pycache__ \ No newline at end of file diff --git a/OiRunner/BetterRunner.py b/OiRunner/BetterRunner.py index 3644a0c..0cbc91f 100755 --- a/OiRunner/BetterRunner.py +++ b/OiRunner/BetterRunner.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- -import subprocess as sp import argparse -import sys import os import re import shutil +import subprocess as sp +import sys import time from typing import Optional from .submit import Submit +from .tools import log class Functions: @@ -38,6 +39,7 @@ def _modify_file(self, file_name: str, file_type: str) -> int: flag = 0 if not os.path.exists("~tmp"): os.mkdir("~tmp") + log.logger.info("Make ~tmp directory.") with open(file_name, "r") as f: for line in f: file_path = os.path.join("~tmp", f"{i}.{file_type}") @@ -47,7 +49,9 @@ def _modify_file(self, file_name: str, file_type: str) -> int: else: if not a: print(f"error:{file_name} is empty.") + log.logger.critical(f"error:{file_name} is empty.") shutil.rmtree("~tmp") + log.logger.info("Delete ~tmp directory.") sys.exit(11) with open(file_path, "w") as f: f.write(a) @@ -55,7 +59,9 @@ def _modify_file(self, file_name: str, file_type: str) -> int: a = "" if not flag: print(f"error:{file_name} is empty.") + log.logger.critical(f"error:{file_name} is empty.") shutil.rmtree("~tmp") + log.logger.info("Delete ~tmp directory.") sys.exit(11) if a: @@ -83,6 +89,7 @@ def _output(self, num: int, opt_file: str) -> None: with open(opt_file, "w") as out: out.write(a) + log.logger.info("Write output file.") def delete_freopen(self, path: str) -> None: ''' @@ -92,6 +99,7 @@ def delete_freopen(self, path: str) -> None: path -- The cpp file path. ''' if not os.path.exists(path): + log.logger.critical(f"{path} file not exists.") raise ValueError("File not exists.") with open(path, "r") as file: @@ -99,12 +107,14 @@ def delete_freopen(self, path: str) -> None: back_file_path = os.path.join(os.path.dirname(path), os.path.basename(path) + ".bak") shutil.copy2(path, back_file_path) + log.logger.info(f"Backup file is made in {back_file_path}.") re_match = r'freopen(\s)*\((\s)*"(\w)*\.{0,1}(\w)*"(\s)*,(\s)*"\w"(\s)*,(\s)*std\w{2,3}(\s)*\)(\s)*[,|;]' changed_content = re.sub(re_match, "", content) with open(path, "w") as file: file.write(changed_content) + log.logger.info("Delete freopen.") class BetterRunner: @@ -141,6 +151,7 @@ def cmd_parse(self) -> None: self.args.name = "./" + self.args.name + ".out" elif sys.platform == "win32": self.args.name = self.args.name + ".exe" + log.logger.debug(f"Output file name is set to {self.args.name}") def compile(self) -> None: ''' @@ -156,13 +167,16 @@ def compile(self) -> None: compile.wait() if compile.returncode == 0: print("Compilation successful.") + log.logger.info("Compilation successful.") else: print("Compilation failed.") + log.logger.critical("Compilation failed.") sys.exit(1) # Can't sent Ctrl+c and get the messages. except KeyboardInterrupt: # pragma: no cover print("\nManually exit, wish AC~(^ v ^)") + log.logger.info("Manually exit.") sys.exit() def _check(self, opt_file: str, ipt_file: str, ans_file: str, @@ -203,6 +217,8 @@ def _check(self, opt_file: str, ipt_file: str, ans_file: str, if run.returncode != 0: print(f"The return value is {run.returncode}. There may be issues with the program running.") + log.logger.error(f"The return value is {run.returncode}. There may be issues with the program running.") + log.logger.debug("Program completed.") with open(ans_file, "r") as ans, open(opt_file, "r") as my_ans: ans_list = [line.rstrip() for line in ans if line.rstrip()] @@ -212,6 +228,7 @@ def _check(self, opt_file: str, ipt_file: str, ans_file: str, print(f"Correct answer, takes {now:.5} seconds.") else: print("Correct answer.") + log.logger.debug("Judgement finished.") return True else: if if_print: @@ -219,6 +236,7 @@ def _check(self, opt_file: str, ipt_file: str, ans_file: str, print(f"Standard answer:{ans_list}\nYour answer:{my_ans_list}") else: print("The number of answer lines is too large.") + log.logger.info("The number of answer lines is too large.") print("Wrong answer.") print("Error data:") with open(ipt_file, "r") as _in: @@ -231,9 +249,10 @@ def _check(self, opt_file: str, ipt_file: str, ans_file: str, print(data) else: print("The number of data words is too large.") + log.logger.info("The number of data words is too large.") else: print("Wrong answer.") - + log.logger.debug("Judgement finished.") return False def run(self) -> None: @@ -241,6 +260,7 @@ def run(self) -> None: try: if self.args.directgdb: gdb = sp.Popen(["gdb", self.args.name]) + log.logger.info("GDB is started.") gdb.wait() sys.exit() @@ -252,8 +272,10 @@ def run(self) -> None: if os.path.exists("~tmp"): shutil.rmtree("~tmp") + log.logger.info("Delete ~tmp directory.") self.func._modify_file(self.input_file, "in") i = self.func._modify_file(self.answer_file, "ans") + log.logger.info(f"The number of test data is {i}.") for file_num in range(1, i + 1): out_file = os.path.join("~tmp", f"{file_num}.out") @@ -266,12 +288,14 @@ def run(self) -> None: print(f"#final:Accuracy{((i - flag) / i): .2%}") self.func._output(i, self.output_file) shutil.rmtree("~tmp") + log.logger.info("Delete ~tmp directory.") if flag == 0 and self.args.freopen: self.func.delete_freopen(self.args.filename + ".cpp") if flag == 0 and self.args.remote is not None: print("\nSubmitting to remote judge.") + log.logger.info("Submitting to remote judge.") self.submit = Submit() enableO2 = not self.args.disabledO2 rid = self.submit.upload_answer(self.args.remote, self.args.filename + ".cpp", @@ -280,12 +304,14 @@ def run(self) -> None: if self.args.gdb and flag > 0: gdb = sp.Popen(["gdb", self.args.name]) + log.logger.info("GDB is started.") gdb.wait() else: if self.args.onlyinput: with open(self.input_file, "r") as _in: print("The file has been executed.") + log.logger.info("The file has been executed.") run = sp.Popen([self.args.name], stdin=_in) elif self.args.onlyoutput: @@ -298,6 +324,7 @@ def run(self) -> None: run.wait() if run.returncode != 0: print(f"The return value is {run.returncode}. There may be issues with the program running.") + log.logger.error(f"The return value is {run.returncode}. There may be issues with the program running.") if self.args.remote is not None: print("\nSubmitting to remote judge.") @@ -305,6 +332,7 @@ def run(self) -> None: enableO2 = not self.args.disabledO2 rid = self.submit.upload_answer(self.args.remote, self.args.filename + ".cpp", enableO2, self.args.language) + log.logger.info(f"Submit rid is {rid}.") self.submit.get_record(rid, if_show_details=self.args.print) # Can't sent Ctrl+c and get the messages. @@ -312,6 +340,7 @@ def run(self) -> None: if os.path.exists("~tmp"): shutil.rmtree("~tmp") print("\nManually exit, wish AC~(^ v ^)") + log.logger.info("Manually exit.") def main(): diff --git a/OiRunner/submit.py b/OiRunner/submit.py index 9910366..ff5245b 100644 --- a/OiRunner/submit.py +++ b/OiRunner/submit.py @@ -7,6 +7,7 @@ import requests +from .tools import log from .util import ACCEPT, BAD_URL, CONTENT_PAT, CONTENT_TYPE, CONTENT_VALUE_PAT, MATE_TAG_PAT from .util import PARAMS, QUESTION_URL, RECORD_URL, STATUS_CODE, SUBMIT_URL, USER_AGENT @@ -23,20 +24,25 @@ def __init__(self) -> None: ''' client_id = os.getenv("__client_id") uid = os.getenv("_uid") + log.logger.debug("Get environment variable.") if client_id is None or uid is None: print("Missing the required environment variable.('__client_id' or '_uid')") + log.logger.critical("Missing the required environment variable.('__client_id' or '_uid')") sys.exit(12) self._cookies = { '__client_id': client_id, '_uid': uid } + log.logger.debug(f"Set cookies {self._cookies}.") self.session = requests.Session() self.headers: MutableMapping[str, Union[str, bytes]] = {"User-Agent": USER_AGENT} + log.logger.debug("Get csrf token.") self._csrf_token = self._get_csrf_token() self.headers["Accept"] = ACCEPT self.headers["content-type"] = CONTENT_TYPE + log.logger.debug(f"Set headers {self.headers}.") def _get_csrf_token(self) -> str: ''' @@ -55,6 +61,8 @@ def _get_csrf_token(self) -> str: self.session.headers = self.headers self.session.cookies.update(self._cookies) html_content = self.session.get(BAD_URL).text + log.logger.debug(f"Request {BAD_URL} to get csrf-token.") + log.logger.debug(f"It returned {html_content}.") # Get csrf-token meta tag in html file. meta_tag_pat = re.compile(MATE_TAG_PAT) @@ -62,16 +70,20 @@ def _get_csrf_token(self) -> str: if meta_tag is None: print("The server returned anomalous data (which does not contain meta tag).") + log.logger.critical("The server returned anomalous data (which does not contain meta tag).") sys.exit(21) # Get content attribute in meta tag. meta = meta_tag.group() + log.logger.debug(f"Meta tag is {meta}") content_pat = re.compile(CONTENT_PAT) content_result = re.search(content_pat, meta) if content_result is None: print("The server returned anomalous data (which does not contain 'content' in meta tag).") + log.logger.critical("The server returned anomalous data (which does not contain 'content' in meta tag).") sys.exit(22) content = content_result.group() + log.logger.debug(f"The content of meta tag is {content}") # Get the value of content attribute. content_value_pat = re.compile(CONTENT_VALUE_PAT) @@ -79,8 +91,11 @@ def _get_csrf_token(self) -> str: # It must include a quote mark. if content_value_result is None: # pragma: no cover print("The server returned anomalous data (which does not contain anything in the content attribute of meta tag).") + log.logger.critical("The server returned anomalous data " + "(which does not contain anything in the content attribute of meta tag).") sys.exit(23) content_value = content_value_result.group() + log.logger.debug(f"Content value is {content_value}") token = content_value.strip('"') # Remove quotes before and after strings @@ -112,6 +127,8 @@ def upload_answer(self, question: str, file_path: str, self.headers["x-csrf-token"] = self._csrf_token self.headers["referer"] = question_url url = SUBMIT_URL + question + log.logger.debug(f"Request url is {url}.") + log.logger.debug(f"Request headers are {self.headers}.") with open(file_path, "r") as code_file: code = code_file.read() @@ -121,13 +138,14 @@ def upload_answer(self, question: str, file_path: str, "lang": lang, "code": code } - + log.logger.info("Request api.") response = self.session.post(url=url, json=data) try: return str(response.json()["rid"]) except KeyError: print(f"An error occurred while uploading the answer, with the server returning: \n {response.text}") + log.logger.critical(f"An error occurred while uploading the answer, with the server returning: \n {response.text}") sys.exit(24) def get_record(self, rid: str, retry_interval: float = 1, @@ -145,27 +163,33 @@ def get_record(self, rid: str, retry_interval: float = 1, if_show_details -- Whether to show the details of record. ''' record_url = RECORD_URL + rid + log.logger.debug(f"Record url is {record_url}.") self.headers.pop("referer", None) self.headers.pop("content-type", None) counter = 0 while True: counter += 1 + log.logger.debug(f"Retry count {counter}.") time.sleep(retry_interval) if retry_count != -1 and counter > retry_count: return None record = self.session.get(url=record_url, params=PARAMS) + log.logger.debug(f"Record is {record}.") data = record.json()["currentData"] judge_result: Dict[str, dict] = data["record"]["detail"]["judgeResult"] test_case_groups: dict = data["testCaseGroup"] case_counter = 0 + finished_test = judge_result["finishedCaseCount"] for test_case_group in test_case_groups: case_counter += len(test_case_group) - if case_counter == judge_result["finishedCaseCount"]: + if case_counter == finished_test: + log.logger.debug("All tests are finished.") break + log.logger.debug(f"The number of tests is {case_counter}, already have finished {finished_test}.") total_score = 0 if type(judge_result["subtasks"]) is list: @@ -174,6 +198,7 @@ def get_record(self, rid: str, retry_interval: float = 1, subtasks = list(judge_result["subtasks"].values()) for subtask in subtasks: total_score += subtask["score"] + log.logger.debug(f"Subtasks are {subtasks}.") if if_show_details: print("\ndetails:") diff --git a/OiRunner/tools/__init__.py b/OiRunner/tools/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/OiRunner/tools/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/OiRunner/tools/log.py b/OiRunner/tools/log.py new file mode 100644 index 0000000..33dbfd8 --- /dev/null +++ b/OiRunner/tools/log.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import logging +import sys + + +def init_logging(): + _logger = logging.getLogger("OiRunner") + loglevel = logging.DEBUG + + # Python 3.8 didn't support encoding argument. + # See at https://docs.python.org/3/howto/logging.html#logging-to-a-file. + if sys.version_info < (3, 9): + logging.basicConfig(filename="OiRunner.log", + level=loglevel, + format="[%(asctime)s][%(levelname)s][%(name)s-%(filename)s-%(funcName)s-%(lineno)d]:%(message)s", + datefmt="%Y/%m/%d %H:%M:%S" + ) + else: + logging.basicConfig(filename="OiRunner.log", + encoding="utf-8", + level=loglevel, + format="[%(asctime)s][%(levelname)s][%(name)s-%(filename)s-%(funcName)s-%(lineno)d]:%(message)s", + datefmt="%Y/%m/%d %H:%M:%S" + ) + + return _logger + + +logger = init_logging() diff --git a/tests/test_func.py b/tests/test_func.py index 77cc10f..72e5490 100644 --- a/tests/test_func.py +++ b/tests/test_func.py @@ -4,6 +4,7 @@ import unittest import os import shutil + from OiRunner import BetterRunner from .util import GARBAGE, FILEOUT, FREOPEN, clean diff --git a/tests/test_submit.py b/tests/test_submit.py index 0b69fe0..33b0aa6 100644 --- a/tests/test_submit.py +++ b/tests/test_submit.py @@ -4,6 +4,7 @@ import unittest import os import unittest.mock + from OiRunner.submit import Submit from .util import GARBAGE, HTML_CORRECT, HTML_WITHOUT_META_TAG, HTML_WITHOUT_CONTENT, \ WAITING_DATA, RECORD_DATA_WITHOUT_SUBTASKS, \ diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 0000000..4b0c608 --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +import unittest +import os +import unittest.mock + +from OiRunner import tools +from .util import GARBAGE, clean + + +class TestTools(unittest.TestCase): + + def setUp(self): + if os.getcwd().split(os.sep)[-1] != "data": + os.chdir(os.path.join("tests", "data")) + + def tearDown(self): + clean(GARBAGE) + + @unittest.mock.patch("logging.basicConfig") + def test_encoding_argument_under_python38(self, mock_config): + with unittest.mock.patch("sys.version_info", (3, 8)): + tools.log.init_logging() + format_string = "[%(asctime)s][%(levelname)s][%(name)s-%(filename)s-%(funcName)s-%(lineno)d]:%(message)s" + mock_config.assert_called_with(filename="OiRunner.log", + level=10, + format=format_string, + datefmt="%Y/%m/%d %H:%M:%S" + ) + with unittest.mock.patch("sys.version_info", (3, 9)): + tools.log.init_logging() + format_string = "[%(asctime)s][%(levelname)s][%(name)s-%(filename)s-%(funcName)s-%(lineno)d]:%(message)s" + mock_config.assert_called_with(filename="OiRunner.log", + level=10, + encoding="utf-8", + format=format_string, + datefmt="%Y/%m/%d %H:%M:%S" + )